QPU Execution
What you will learn:
how the execution of a
Sequenceon a QPU differs from executing it on an emulator;how to execute a
Sequenceon a QPU;how to submit multiple jobs using a parametrized
Sequences.
Introduction
Section titled “Introduction”When the time comes to execute a Pulser Sequence, there are many options: one can choose to execute it on a QPU or on an emulator, which might happen locally or remotely. All these options are accessible through a unified interface we call a Backend.
This tutorial is a step-by-step guide on how to use a QPU backend for Sequence execution.
Since we recommend always testing on an emulator beforehand, please make sure you’ve followed the tutorial Execution on an Emulator first.
1. Preparation for execution on QPUBackend
Section titled “1. Preparation for execution on QPUBackend”Sequence execution on a QPU is done through the QPUBackend, which is a remote backend. Therefore, it requires a remote backend connection which should be created at the start due to two additional QPU constraints:
The
Devicemust be compatible with the connection. Available devices can be found throughconnection.fetch_available_devices().If in the chosen device
Device.requires_layoutisTrue, theRegistermust be defined from a register layout:If
Device.accepts_new_layoutsisFalse, use one of the register layouts calibrated for the chosenDevice(found underDevice.calibrated_register_layouts). Check out this tutorial for more information on how to define aRegisterfrom aRegisterLayout.Otherwise, we may choose to define our own custom layout or rely on
Register.with_automatic_layout()to give us a register from an automatically generated register layout that fits our desired register while obeying the device constraints.
Let us first create the PasqalCloud connection and use it to fetch the FRESNEL_CAN1 Device.
from pulser_pasqal import PasqalCloud
connection = PasqalCloud( username=USERNAME, # Your username or email address for the Pasqal Cloud Platform project_id=PROJECT_ID, # The ID of the project associated to your account password=PASSWORD, # The password for your Pasqal Cloud Platform account)print(connection.fetch_available_devices())device = connection.fetch_available_devices()["FRESNEL_CAN1"]
{'FRESNEL_CAN1': FRESNEL_CAN1, 'FRESNEL': FRESNEL}
Now we can check what options we have for working with layouts. As documented in the tutorial on hardware specifications, we can print the specs for the device. Doing this for FRESNEL_CAN1 shows that it requires a layout, but it also allows us to define new layouts, which will allow us to just create the desired register from an automatic layout.
print(device.specs)
Register parameters:
- Dimensions: 2D
- Maximum number of atoms: 100
- Maximum distance from origin: 46 µm
- Minimum distance between neighbouring atoms: 5 μm
Layout parameters:
- Requires layout: Yes
- Accepts new layout: Yes
- Minimal number of traps: 60
- Maximal number of traps: 217
- Minimum layout filling fraction: 0.35
- Maximum layout filling fraction: 0.55
Device parameters:
- Rydberg level: 60
- Ising interaction coefficient: 865723.02
- Channels can be reused: No
- Supported bases: ground-rydberg
- Supported states: r, g
- SLM Mask: No
- Maximum sequence duration: 6000 ns
- Maximum number of runs: 1000
- Default noise model: NoiseModel(noise_types=('SPAM', 'dephasing', 'relaxation'), state_prep_error=0.0, p_false_pos=0.025, p_false_neg=0.1, relaxation_rate=0.01, dephasing_rate=0.2222222222222222, hyperfine_dephasing_rate=0.0)
Channels:
- 'rydberg_global': Rydberg(addressing='Global',
max_abs_detuning=62.83185307179586,
max_amp=12.566370614359172,
min_retarget_interval=None,
fixed_retarget_t=None,
max_targets=None,
clock_period=4,
min_duration=16,
max_duration=6000,
min_avg_amp=0.3141592653589793,
mod_bandwidth=5,
custom_phase_jump_time=0,
eom_config=RydbergEOM(limiting_beam=<RydbergBeam.RED: 2>,
max_limiting_amp=175.92918860102841,
intermediate_detuning=2827.4333882308138,
controlled_beams=(<RydbergBeam.BLUE: 1>,),
mod_bandwidth=26,
custom_buffer_time=240,
multiple_beam_control=False,
blue_shift_coeff=1.0,
red_shift_coeff=2.0),
propagation_dir=(0, 1, 0))
2. Creating the Pulse Sequence
Section titled “2. Creating the Pulse Sequence”Let’s create the same sequence as in Execution on an Emulator. The one difference with before is the call to Register.with_automatic_layout. To show the effect of this, let’s also draw the register, including the trap sites generated by the automatic layout. FRESNEL_CAN1 also contains a precalibrated triangular lattice layout with a spacing of 5µm which we equally could have used for our chosen register. Notice that the automatically generated layout is not the
same as the precalibrated one: it is not quite triangular.
import numpy as npimport pulser
# 1. Picking a `Device`# device = connection.fetch_available_devices()["FRESNEL_CAN1"]# 2. Creating the RegisterR_interatomic = 5 # umbare_register = pulser.Register.hexagon(1, R_interatomic, prefix="q")# Associating a layout to the Registerregister = bare_register.with_automatic_layout(device)
seq = pulser.Sequence(register, device)
# 3. Picking the channelsseq.declare_channel("rydberg_global", "rydberg_global")
# 4. Adding the pulses
# Parameters in rad/µsU = device.interaction_coeff / R_interatomic**6
# Time parameterstotal_duration = 4000 # in nsinterp_pts = np.linspace(0, 1, 4) # between 0 and 1
seq.add( pulser.Pulse( pulser.InterpolatedWaveform( total_duration, U * np.array([1e-9, 0.22, 0.2181, 1e-9]), times=interp_pts, ), pulser.InterpolatedWaveform( total_duration, U * np.array([-1, 0.0556, 0.332, 1]), times=interp_pts, ), 0, ), "rydberg_global",)
seq.draw(draw_register=True)
3. Running on a QPU
Section titled “3. Running on a QPU”3.1. Executing on QPUBackend
Section titled “3.1. Executing on QPUBackend”Running the Sequence on a QPU is very similar to having an emulator run it. The QPUBackend automatically determines which QPU to run on from the Device specified in the Sequence. Consequently, we create a QPUBackend which, being a remote backend, will require the Sequence and the Connection.
QPUBackend can take a configuration object to define the default number of shots to take for each job. Note that this value has an upper bound as specified by the Device. From the printed device specs above, we can see that the largest possible value is 1000 for FRESNEL_CAN1.
A single bitstring will be measured at the end of each run. More measurements per run are not possible since measuring bitstrings destroys the quantum state, and the sequence will have to be run again.
config = pulser.backend.BackendConfig(default_num_shots=1000)backend = pulser.backends.QPUBackend(seq, connection, config=config)results = backend.run()print(f"Submitted batch {results.batch_id}")BATCH_ID = results.batch_id
Submitted batch: b86a95be-e42c-4d36-b53f-0f09cd892abf
3.2. Restoring RemoteResults
Section titled “3.2. Restoring RemoteResults”It is quite frequent that one has to wait for the results to be available and interrupt the notebook in the mean time. This is not an issue! You can recreate the RemoteResults object associated to a batch at any time with the batch ID (see results.batch_id above), by doing
results = pulser.backend.RemoteResults( BATCH_ID, connection # ID of the submitted batch)3.3 Retrieving the Results
Section titled “3.3 Retrieving the Results”We can now print the results as they come in, just as for emulator backends.
print(results.get_batch_status())
results.get_available_results()
BatchStatus.DONE
{'798b18bd-5fab-4d7a-9d1c-4ae27201e36e': SampledResult(atom_order=('q0', 'q1', 'q2', 'q3', 'q4', 'q5', 'q6'), meas_basis='ground-rydberg', bitstring_counts={'0010101': 321, '0101010': 310, '0010100': 22, '0100010': 21, '0101000': 20, '0000101': 19, '0111010': 16, '0010001': 14, '0100100': 13, '0011101': 13, '0010111': 13, '0001010': 12, '0101011': 12, '0110101': 10, '0010011': 10, '0110010': 9, '0101001': 9, '0011010': 8, '0010010': 8, '1010101': 7, '0011001': 6, '0101110': 6, '0100110': 6, '0010110': 6, '0001001': 6, '0110100': 6, '0001101': 5, '0100101': 5, '0110110': 4, '0001000': 4, '0101100': 4, '1101010': 4, '0111000': 3, '1100100': 3, '1000001': 3, '0000001': 2, '1100010': 2, '1100000': 2, '1010100': 2, '1001111': 2, '1000100': 2, '0111110': 2, '0111101': 2, '0110111': 2, '0110011': 2, '0101111': 2, '0101101': 2, '0100001': 2, '0011000': 2, '0010000': 2, '0001011': 2, '0000100': 2, '0011111': 1, '0011110': 1, '0111100': 1, '0011100': 1, '0011011': 1, '0111111': 1, '1000000': 1, '1111011': 1, '1000111': 1, '1001000': 1, '1001001': 1, '1001011': 1, '1010001': 1, '1010010': 1, '1010111': 1, '0000011': 1, '0001110': 1, '1011101': 1, '0000010': 1, '1101000': 1, '1101011': 1, '1101100': 1, '0000110': 1, '0110001': 1, '0100011': 1, '0111011': 1, '1111010': 1, '0100000': 1}, evaluation_time=1.0)}
The QPU only returns bitstrings at the final time, and as for emulator backends, there is a dedicated method for accessing them.
print(results[0].final_bitstrings)
Counter({'0010101': 321, '0101010': 310, '0010100': 22, '0100010': 21, '0101000': 20, '0000101': 19, '0111010': 16, '0010001': 14, '0100100': 13, '0011101': 13, '0010111': 13, '0001010': 12, '0101011': 12, '0110101': 10, '0010011': 10, '0110010': 9, '0101001': 9, '0011010': 8, '0010010': 8, '1010101': 7, '0011001': 6, '0101110': 6, '0100110': 6, '0010110': 6, '0001001': 6, '0110100': 6, '0001101': 5, '0100101': 5, '0110110': 4, '0001000': 4, '0101100': 4, '1101010': 4, '0111000': 3, '1100100': 3, '1000001': 3, '0000001': 2, '1100010': 2, '1100000': 2, '1010100': 2, '1001111': 2, '1000100': 2, '0111110': 2, '0111101': 2, '0110111': 2, '0110011': 2, '0101111': 2, '0101101': 2, '0100001': 2, '0011000': 2, '0010000': 2, '0001011': 2, '0000100': 2, '0011111': 1, '0011110': 1, '0111100': 1, '0011100': 1, '0011011': 1, '0111111': 1, '1000000': 1, '1111011': 1, '1000111': 1, '1001000': 1, '1001001': 1, '1001011': 1, '1010001': 1, '1010010': 1, '1010111': 1, '0000011': 1, '0001110': 1, '1011101': 1, '0000010': 1, '1101000': 1, '1101011': 1, '1101100': 1, '0000110': 1, '0110001': 1, '0100011': 1, '0111011': 1, '1111010': 1, '0100000': 1})
4. Batch execution with parametrized Sequences
Section titled “4. Batch execution with parametrized Sequences”The Sequence we just executed on the QPU is composed of a single Pulse whose amplitude and detuning are each defined with an InterpolatedWaveform. This makes it very easy to play with the duration of the Sequence. Let’s see how to do this in practice.
QPUBackend is instantiated with a Sequence. If you want to run an experiment in which you vary a parameter in your Sequence (like in this tutorial), you don’t need to create a QPUBackend for each of the created Sequence: you can use a parametrized Sequence to submit a batch of Sequences to the QPU. This will have the advantage to conveniently group the outcomes together. Specifically, this is possible for sequences composed of the same series of Pulses,
with the same RegisterLayout (a Sequence can be defined with a MappableRegister to define multiple Sequences from the same layout).
Let us demonstrate how this works by taking the Sequence defined in section 2, and making the duration t a parameter. We will then submit two jobs in a single batch, using different values for t. The Sequence under study being an “adiabatic” state preparation, the quality of the preparation of the AFM state \(\frac{1}{\sqrt{2}}\left(\left|grgrgrg\right> + \left|ggrgrgr\right>\right)\) should increase with the duration of the Sequence (you can see this by playing with the
total_duration in the tutorial on the execution on emulators).
param_seq = pulser.Sequence(register, device)
# 3. Picking the channelsparam_seq.declare_channel("rydberg_global", "rydberg_global")
# 4. Adding the pulses
t = param_seq.declare_variable("t", dtype=int)
param_seq.add( pulser.Pulse( pulser.InterpolatedWaveform( t, U * np.array([1e-9, 0.22, 0.2181, 1e-9]), times=interp_pts ), pulser.InterpolatedWaveform( t, U * np.array([-1, 0.0556, 0.332, 1]), times=interp_pts ), 0, ), "rydberg_global",)print(param_seq) # A parametrized Sequence cannot be drawn
Prelude
-------
Channel: rydberg_global
t: 0 | Initial targets: q0, q1, q2, q3, q4, q5, q6 | Phase Reference: 0.0
Stored calls
------------
1. add(Pulse(InterpolatedWaveform(t[0], [5.54062733e-08 1.21893801e+01 1.20841082e+01 5.54062733e-08], times=[0. 0.33333333 0.66666667 1. ]), InterpolatedWaveform(t[0], [-55.40627328 3.08058879 18.39488273 55.40627328], times=[0. 0.33333333 0.66666667 1. ]), 0), rydberg_global)
We can now plot the sequence only after we specify a value for t. Let us put t=4000 and show that the sequence is identical to that in section 2.
seq_4000 = param_seq.build(t=4000)seq_4000.draw()
We can now submit jobs with different values of t by leveraging the job_params argument of QPUBackend.run. However, to do that we must first create a new QPUBackend with the parameterized sequence.
A job will be created for each dictionnary provided in job_params. For each of these jobs:
the value of each parameter in the
Sequencemust be provided under thevariableskey.the number of
runsis by defaultconfig.default_num_shots, but it can be specified under therunskey.
config = pulser.backend.BackendConfig(default_num_shots=1000)param_backend = pulser.backends.QPUBackend( param_seq, connection, config=config)param_job_params = [ {"variables": {"t": 300}}, # uses config.default_num_shots {"runs": 1000, "variables": {"t": 1000}}, # uses 1000 shots {"variables": {"t": 4000}}, # uses config.default_num_shots]param_results = param_backend.run(job_params=param_job_params)print(f"Submitted batch {param_results.batch_id}")print(f"With job IDs {param_results.job_ids}")
Submitted batch f2a752b3-87ef-4ee4-ac75-eee04f32eb5b
With job IDs ['ce8a5be0-2565-463a-885b-54ade900be26', 'f3c78b1d-e3f3-4bfb-b2f7-7db30e3441e4', '4e754800-1c3c-4f68-b943-47a65ab913be']
Once again, we can see the results as they come in. Note that since we now specify 3 jobs in the job_params we expect 3 results in total.
param_results.get_available_results()
{'ce8a5be0-2565-463a-885b-54ade900be26': SampledResult(atom_order=('q0', 'q1', 'q2', 'q3', 'q4', 'q5', 'q6'), meas_basis='ground-rydberg', bitstring_counts=Counter({'0000000': 827, '0000100': 35, '0000001': 28, '0010000': 22, '0100000': 21, '0001000': 19, '0000010': 16, '1000000': 9, '1000100': 3, '0000101': 2, '0011000': 2, '0001101': 1, '0001010': 1, '0001001': 1, '0000110': 1, '0000011': 1, '0010010': 1, '1001110': 1, '1001000': 1, '0110000': 1, '0101000': 1, '0100100': 1, '0100010': 1, '0100001': 1, '0010100': 1, '1010000': 1, '0010001': 1}), evaluation_time=1.0), 'f3c78b1d-e3f3-4bfb-b2f7-7db30e3441e4': SampledResult(atom_order=('q0', 'q1', 'q2', 'q3', 'q4', 'q5', 'q6'), meas_basis='ground-rydberg', bitstring_counts=Counter({'0101010': 161, '0010101': 158, '0000101': 67, '0010100': 61, '0100010': 60, '0101000': 50, '0010001': 50, '0001010': 44, '0000010': 31, '0001000': 29, '0000100': 26, '0010010': 22, '0000001': 22, '0001001': 22, '0100100': 22, '0100000': 20, '0010000': 19, '1000000': 13, '0111010': 10, '0101011': 8, '0011101': 6, '0000000': 6, '0001101': 6, '0010011': 5, '0011010': 4, '0010111': 4, '0001100': 4, '0110000': 3, '0110010': 3, '0100110': 3, '0100101': 3, '0110100': 3, '0111000': 3, '0010110': 3, '1000100': 3, '0000011': 3, '0101100': 3, '1010101': 2, '0110001': 2, '0011100': 2, '1010111': 2, '0011001': 2, '0000110': 2, '1010100': 2, '1101010': 2, '0101110': 2, '0110101': 2, '1010000': 1, '1010001': 1, '0001111': 1, '0001110': 1, '0001011': 1, '0101001': 1, '1011101': 1, '0110011': 1, '0100011': 1, '1100000': 1, '0100001': 1, '0011111': 1, '1100010': 1, '1101110': 1, '0011000': 1, '1101000': 1, '0111100': 1, '0111101': 1, '1100100': 1, '1001101': 1}), evaluation_time=1.0), '4e754800-1c3c-4f68-b943-47a65ab913be': SampledResult(atom_order=('q0', 'q1', 'q2', 'q3', 'q4', 'q5', 'q6'), meas_basis='ground-rydberg', bitstring_counts=Counter({'0010101': 359, '0101010': 261, '0010001': 33, '0001010': 27, '0101000': 23, '0100010': 22, '0010100': 20, '0111010': 20, '0000101': 19, '0101110': 17, '0001001': 15, '0110010': 14, '1010101': 13, '0010010': 12, '0100100': 12, '0011101': 11, '0110101': 11, '0010111': 9, '1101010': 8, '0110100': 8, '0011010': 7, '0110110': 6, '0101100': 6, '0100110': 4, '0101011': 4, '0001011': 4, '0101101': 3, '0100101': 3, '0100000': 3, '0000010': 3, '0001101': 3, '0011001': 3, '0010011': 2, '0101001': 2, '0111011': 2, '0100011': 2, '0111100': 2, '1000001': 2, '0001110': 2, '1101000': 2, '0000111': 2, '0000100': 2, '0110000': 1, '1100101': 1, '1100110': 1, '0010000': 1, '0001111': 1, '0110001': 1, '0011011': 1, '0000001': 1, '0000011': 1, '1101100': 1, '0001000': 1, '1111000': 1, '1110111': 1, '1010100': 1, '1011010': 1, '1011100': 1, '1100000': 1}), evaluation_time=1.0)}
We can now print the final bitstrings for each job:
print("t=300:", param_results[0].final_bitstrings)print("t=1000:", param_results[1].final_bitstrings)print("t=4000:", param_results[2].final_bitstrings)
t=300ns: Counter({'0000000': 827, '0000100': 35, '0000001': 28, '0010000': 22, '0100000': 21, '0001000': 19, '0000010': 16, '1000000': 9, '1000100': 3, '0000101': 2, '0011000': 2, '0001101': 1, '0001010': 1, '0001001': 1, '0000110': 1, '0000011': 1, '0010010': 1, '1001110': 1, '1001000': 1, '0110000': 1, '0101000': 1, '0100100': 1, '0100010': 1, '0100001': 1, '0010100': 1, '1010000': 1, '0010001': 1})
t=1000ns: Counter({'0101010': 161, '0010101': 158, '0000101': 67, '0010100': 61, '0100010': 60, '0101000': 50, '0010001': 50, '0001010': 44, '0000010': 31, '0001000': 29, '0000100': 26, '0010010': 22, '0000001': 22, '0001001': 22, '0100100': 22, '0100000': 20, '0010000': 19, '1000000': 13, '0111010': 10, '0101011': 8, '0011101': 6, '0000000': 6, '0001101': 6, '0010011': 5, '0011010': 4, '0010111': 4, '0001100': 4, '0110000': 3, '0110010': 3, '0100110': 3, '0100101': 3, '0110100': 3, '0111000': 3, '0010110': 3, '1000100': 3, '0000011': 3, '0101100': 3, '1010101': 2, '0110001': 2, '0011100': 2, '1010111': 2, '0011001': 2, '0000110': 2, '1010100': 2, '1101010': 2, '0101110': 2, '0110101': 2, '1010000': 1, '1010001': 1, '0001111': 1, '0001110': 1, '0001011': 1, '0101001': 1, '1011101': 1, '0110011': 1, '0100011': 1, '1100000': 1, '0100001': 1, '0011111': 1, '1100010': 1, '1101110': 1, '0011000': 1, '1101000': 1, '0111100': 1, '0111101': 1, '1100100': 1, '1001101': 1})
t=4000ns: Counter({'0010101': 359, '0101010': 261, '0010001': 33, '0001010': 27, '0101000': 23, '0100010': 22, '0010100': 20, '0111010': 20, '0000101': 19, '0101110': 17, '0001001': 15, '0110010': 14, '1010101': 13, '0010010': 12, '0100100': 12, '0011101': 11, '0110101': 11, '0010111': 9, '1101010': 8, '0110100': 8, '0011010': 7, '0110110': 6, '0101100': 6, '0100110': 4, '0101011': 4, '0001011': 4, '0101101': 3, '0100101': 3, '0100000': 3, '0000010': 3, '0001101': 3, '0011001': 3, '0010011': 2, '0101001': 2, '0111011': 2, '0100011': 2, '0111100': 2, '1000001': 2, '0001110': 2, '1101000': 2, '0000111': 2, '0000100': 2, '0110000': 1, '1100101': 1, '1100110': 1, '0010000': 1, '0001111': 1, '0110001': 1, '0011011': 1, '0000001': 1, '0000011': 1, '1101100': 1, '0001000': 1, '1111000': 1, '1110111': 1, '1010100': 1, '1011010': 1, '1011100': 1, '1100000': 1})
We see that the longer the Sequence the closer the final state is to the antiferromagnetic state \(\frac{1}{\sqrt{2}}\left(\left|ggrgrgr\right>+\left|grgrgrg\right>\right)\) (\(\left|ggrgrgr\right>\) is measured as \(\left|0010101\right>\), \(\left|grgrgrg\right>\) is measured as \(\left|0101010\right>\), see the Convention page), as expected of an adiabatic state preparation.