Backend Execution
What you will learn:
what a
Backendis for;what types of emulator
Backendexist;how to choose the best
Backendfor your needs;how to execute a
Sequenceon an emulatorBackendand retrieve the results.what changes when you want to submit on the QPU
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 an unified interface we call a Backend.
This tutorial is a step-by-step guide on how to use emulator backends for Pulser sequence execution.
Although the final goal of a quantum algorithm is to run on a QPU, we always recommend starting on an emulator. Emulators are more readily available, and at lower cost, in addition to providing much more information than a QPU, which can only measure bitstrings. When running on an emulator we recommend first doing some exploratory runs locally to ensure you’re doing the right thing, before submitting your heavier jobs to be run on the cluster through the cloud.
1. Creating the Pulse Sequence
Section titled “1. Creating the Pulse Sequence”The first step is to create the sequence that we want to execute. Here, we make a sequence by combining a Blackman waveform in amplitude and a ramp in detuning. Since it will be executed on an emulator, we can create the register we want and choose an appropriate Device.
import numpy as npimport pulserimport pulser_simulationreg = pulser.Register({"q0": (-5, 0), "q1": (5, 0)})
seq = pulser.Sequence(reg, pulser.AnalogDevice)seq.declare_channel("rydberg_global", "rydberg_global")t = 2000 # ns
amp_wf = pulser.BlackmanWaveform(t, np.pi)det_wf = pulser.RampWaveform(t, -5, 5)seq.add(pulser.Pulse(amp_wf, det_wf, 0), "rydberg_global")
seq.draw()
2. Running on a local backend
Section titled “2. Running on a local backend”It is now time to select and initialize the backend. We will start running things locally using the QutipBackendV2 (from pulser_simulation). It uses qutip to emulate the sequence execution locally.
Keep in mind that other emulators are available, and the full list is available here.
Upon creation, all backends require the sequence they will execute.
qutip_bknd = pulser.backends.QutipBackendV2(seq)qutip_results = qutip_bknd.run()3. Retrieving the Results
Section titled “3. Retrieving the Results”For any Results returned from an EmulatorBackend, we can query the contents of a result. As we can see below, QutipBackendV2 returns the bitstrings and quantum state at the final time.
print(qutip_results.get_result_tags())print(qutip_results.get_result_times("bitstrings"))print(qutip_results.get_result_times("state"))
['bitstrings', 'state']
[1.0]
[1.0]
We can query the bitstrings at the final time, if present, using a dedicated property.
print(qutip_results.final_bitstrings)
Counter({'11': 949, '10': 26, '01': 22, '00': 3})
The same is true for the final state.
print(qutip_results.final_state)
QutipState
----------
Eigenstates: ('r', 'g')
Quantum object: dims=[[2, 2], [1]], shape=(4, 1), type='ket', dtype=Dense
Qobj data =
[[ 0.62161132+0.75098248j]
[-0.03357887+0.14524584j]
[-0.03357887+0.14524584j]
[-0.00110516-0.07194168j]]
All results of a specific type can also be accessed through the name returned in get_result_tags(). Notice that this will return a list of each result of that type, at each time it was measured. The corresponding measurement times are returned by get_result_times() as we demonstrated above.
qutip_results.bitstrings[-1]
Counter({'11': 949,
'10': 26,
'01': 22,
'00': 3})
4. Configuring the Backend
Section titled “4. Configuring the Backend”Emulator backends also accept, optionally, a configuration given as an instance of the EmulationConfig class. As can be seen from the docs, this class defines a few generally applicable configuration options, and then accepts backend specific options as keyword arguments. Each backend defines a subclass of EmulationConfig, such as QutipConfig for QutipBackendV2, that makes explicit the specific options for that
backend. Since we are not interested in changing configuration options specific to QutipBackendV2, we use the EmulationConfig class directly.
To showcase the flexibility of emulation backends specifically, we will configure QutipBackendV2 to return the overlap with the Bell state halfway through the sequence and at the end, which can be done using the Fidelity observable documented here.
The additional observables available by default are documented here.
fidelity_state = pulser_simulation.QutipState.from_state_amplitudes( eigenstates=("r", "g"), amplitudes={"rr": 1 / np.sqrt(2), "gg": 1 / np.sqrt(2)},)fidelity = pulser.backend.Fidelity( state=fidelity_state, tag_suffix="bell", evaluation_times=[0.5, 1.0])
config = pulser.backend.EmulationConfig( observables=[fidelity],)Now that we have defined a config, we can create a new backend, run the emulation, and analyze the results.
qutip_bknd_custom = pulser.backends.QutipBackendV2(seq, config=config)custom_results = qutip_bknd_custom.run()Since we requested the fidelity at two different times, asking for the result times will reflect this.
print(custom_results.get_result_tags())print(custom_results.get_result_times("fidelity_bell"))
['fidelity_bell']
[0.5, 1.0]
The two fidelities are in the results in chronological order.
print(custom_results.fidelity_bell[0])print(custom_results.fidelity_bell[1])
0.07682093627900612
0.42306198747321244
5. Running Remotely
Section titled “5. Running Remotely”Now that we understand the behaviour of our sequence, and how to use emulator backends locally, let’s run the sequence on a pair of remote backends
EmuFreeBackendV2(frompulser_pasqal): Executes the sequence onQutipBackendV2, but runs remotely in the cloud.EmuMPSBackend(frompulser_pasqal): Executes the sequence onMPSBackendbut runs remotely in the cloud.
A full list of available backends is available in the backends’ docstrings.
Notice that running things remotely in the cloud requires us to specify login credentials through a RemoteConnection object. The appropriate connection object for running through the Pasqal cloud is as follows:
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)We now have a connection to the cloud. However, before creating the backends, let us create a custom EmulationConfig with a Fidelity observable again. A user running remotely cannot be expected to have each Backend available in the cloud installed locally. To compensate for this fact, specifying a Fidelity observable for use remotely can be done using a generic State type appropriate for all remote backends.
remote_fidelity_state = pulser.backend.StateRepr.from_state_amplitudes( eigenstates=("r", "g"), amplitudes={"rr": 1 / np.sqrt(2), "gg": 1 / np.sqrt(2)},)remote_fidelity = pulser.backend.Fidelity( state=remote_fidelity_state, tag_suffix="bell", evaluation_times=[0.5, 1.0])
remote_config = pulser.backend.EmulationConfig( observables=[remote_fidelity],)Now that we have a proper Sequence, EmulationConfig and Connection, we can create the EmuFreeBackendV2 and EmuMPSBackend with which we want to run the emulation.
free_bknd = pulser.backends.EmuFreeBackendV2( seq, config=remote_config, connection=connection)mps_bknd = pulser.backends.EmuMPSBackend( seq, config=remote_config, connection=connection)For a local Backend, the run method takes no arguments, but for cloud, it is required to pass a job_params. Inside job_params the number of runs must be specified for legacy reasons. Since the value will be ignored in our current case, we will put runs=1.
# Remote execution, requires job_paramsjob_params = [{"runs": 1}]free_results = free_bknd.run(job_params=job_params)mps_results = mps_bknd.run(job_params=job_params)For remote backends, the object returned is a RemoteResults instance, which uses the connection to fetch the results once they are ready. To check the status of the batch, we can run:
print(free_results.get_batch_status())print(mps_results.get_batch_status())
BatchStatus.DONE
BatchStatus.DONE
When the batch states shows as DONE, the results can be accessed. They are packaged in a RemoteResults objects, which will contain one Results for each entry in job_params in the same order. We can use get_available_results to see the results as they become available.
free_results.get_available_results()
{'d6e6e73e-88d5-4b58-81d2-e291cbe16493': Results(atom_order=('q0', 'q1'), total_duration=2000, _results={UUID('1158fa1a-a268-4abe-95c2-246a656e1a68'): [0.07682093627900612, 0.42306198747321244]}, _times={UUID('1158fa1a-a268-4abe-95c2-246a656e1a68'): [0.5, 1.0]}, _aggregation_methods={UUID('1158fa1a-a268-4abe-95c2-246a656e1a68'): <AggregationMethod.MEAN: 2>}, _tagmap={'fidelity_bell': UUID('1158fa1a-a268-4abe-95c2-246a656e1a68')})}
We can see that there is only one Results object in the list after the job is done since only a single item was present in job_params above. Note that since we ran the same sequence as locally, the fidelity is the same as in that case.
print(free_results[-1].fidelity_bell[0])print(free_results[-1].fidelity_bell[1])
0.07682093627900612
0.42306198747321244
In addition to the above, MPSBackend, SVBackend and their derivatives, such as the remote EmuMPSBackend store runtime statistics in the Results. For more information about this, please see the corresponding documentation (external).
print(mps_results[-1].get_result_tags())print(mps_results[-1].fidelity_bell[0])print(mps_results[-1].fidelity_bell[1])
['statistics', 'fidelity_bell']
0.07685184646911727
0.4228824148514162
6. Differences when running on a QPU
Section titled “6. Differences when running on a QPU”Sequence execution on a QPU is done through the QPUBackend, which is a remote backend. Therefore, it requires a RemoteConnection, which should be open from the start due to two additional QPU constraints:
The
Devicemust be chosen among the options available at the moment, which 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.
7. Alternative user interfaces for using remote backends
Section titled “7. Alternative user interfaces for using remote backends”Once you have created a Pulser sequence, you can also use specialized Python SDKs to send it for execution:
the pasqal-cloud (external) Python SDK, developed by PASQAL and used under-the-hood by Pulser’s remote backends.
Azure’s Quantum Development Kit (QDK) which you can use by creating an Azure Quantum workspace (external) directly integrated with PASQAL emulators and QPU.