Backend Execution
What you will learn:
what a
Backend
is for;what types of
Backend
exist;how to choose the best
Backend
for your needs;how to execute a
Sequence
on aBackend
and retrieve the results.
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 the different backends for Pulser sequence execution.
1. Choosing the type of backend
Section titled “1. Choosing the type of backend”Although the backend interface nearly doesn’t change between backends, some will unavoidably enforce more restrictions on the sequence being executed or require extra steps. In particular, there are two questions to answer:
Is it local or remote? Execution on remote backends requires a working remote connection. For now, this is only available through
pulser_pasqal.PasqalCloud
.Is it a QPU or an Emulator? For QPU execution, there are extra constraints on the sequence to take into account. Nonetheless, we can still enforce the same constraints when using an Emulator by setting
mimic_qpu=True
.
1.1. Starting a remote connection
Section titled “1.1. Starting a remote connection”For remote backend execution, start by ensuring that you have access and start a remote connection. For PasqalCloud
, we could start one by running:
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 **kwargs)
1.2. Preparation for execution on QPUBackend
Section titled “1.2. 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 open from the start due to two additional QPU constraints:
The
Device
must be chosen among the options available at the moment, which can be found throughconnection.fetch_available_devices()
.If in the chosen device
Device.requires_layout
isTrue
, theRegister
must be defined from a register layout:If
Device.accepts_new_layouts
isFalse
, 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 aRegister
from 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.
On the contrary, execution on emulator backends imposes no further restriction on the device and the register. We will stick to emulator backends in this tutorial, so we will forego the requirements of QPU backends in the following steps.
2. Creating the Pulse Sequence
Section titled “2. Creating the Pulse Sequence”The next step is to create the sequence that we want to execute. Here, we make a sequence with a variable duration 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 a VirtualDevice
that does not impose hardware restrictions (like the MockDevice
).
import numpy as npimport pulser
reg = pulser.Register({"q0": (-5, 0), "q1": (5, 0)})
seq = pulser.Sequence(reg, pulser.MockDevice)seq.declare_channel("rydberg_global", "rydberg_global")t = seq.declare_variable("t", dtype=int)
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")
# We build with t=1000 so that we can draw itseq.build(t=1000).draw()

3. Starting the backend
Section titled “3. Starting the backend”It is now time to select and initialize the backend. Currently, these are the available backends (but bear in mind that the list may grow in the future):
Local:
QutipBackend
(frompulser_simulation
): UsesQutipEmulator
to emulate the sequence execution locally.
Remote:
QPUBackend
(frompulser
): Executes on a QPU through a remote connection.EmuFreeBackend
(frompulser_pasqal
): Emulates the sequence execution using free Hamiltonian time evolution (similar toQutipBackend
, but runs remotely).EmuTNBackend
(frompulser_pasqal
): Emulates the sequence execution using a tensor network simulator.
If the appropriate packages are installed, all backends should be available via the pulser.backends
module so we don’t need to explicitly import them.
Upon creation, all backends require the sequence they will execute. Emulator backends also accept, optionally, a configuration given as an instance of the EmulatorConfig
class. This class allows for setting all the parameters available in QutipEmulator
and is forward looking, meaning that it envisions that these options will at some point be available on other emulator backends. This also means that trying to change parameters in the configuration of a backend that does not support them
yet will raise an error.
Even so, EmulatorConfig
also has a dedicated backend_options
for options specific to each backend, which are detailed in the backends’ docstrings.
With QutipBackend
, we have free reign over the configuration. In this example, we will:
Change the
sampling_rate
Include measurement errors using a custom
NoiseModel
On the other hand, QutipBackend
does not support parametrized sequences. Since it is running locally, they can always be built externally before being given to the backend. Therefore, we will build the sequence (with t=2000
) before we give it to the backend.
config = pulser.EmulatorConfig( sampling_rate=0.1, noise_model=pulser.NoiseModel( p_false_pos=0.01, p_false_neg=0.004, ),)
qutip_bknd = pulser.backends.QutipBackend(seq.build(t=2000), config=config)
Currently, the remote emulator backends are still quite limited in the number of parameters they allow to be changed. Furthermore, the default configuration of a given backend does not necessarily match that of EmulatorConfig()
, so it’s important to start from the correct default configuration. Here’s how to do that for the EmuTNBackend
:
import dataclasses
emu_tn_default = pulser.backends.EmuTNBackend.default_config# This will create a new config with a different sampling rate# All other parameters remain the sameemu_tn_config = dataclasses.replace(emu_tn_default, sampling_rate=0.5)
We will stick to the default configuration for EmuFreeBackend
, but the process to create a custom configuration would be identical. To know which parameters can be changed, consult the backend’s docstring.
free_bknd = pulser.backends.EmuFreeBackend(seq, connection=connection)tn_bknd = pulser.backends.EmuTNBackend( seq, connection=connection, config=emu_tn_config)
Note also that the remote backends require an open connection upon initialization. This would also be the case for QPUBackend
.
4. Executing the Sequence
Section titled “4. Executing the Sequence”Once the backend is created, executing the sequence is always done through the backend’s run()
method.
For the QutipBackend
, all arguments are optional and are the same as the ones in QutipEmulator
. On the other hand, remote backends all require job_params
to be specified. job_params
are given as a list of dictionaries, each containing the number of runs and the values for the variables of the parametrized sequence (if any). The sequence is then executed with the parameters specified within each entry of job_params
.
# Local execution, returns the same results as QutipEmulatorqutip_results = qutip_bknd.run()
# Remote execution, requires job_paramsjob_params = [ {"runs": 100, "variables": {"t": 1000}}, {"runs": 50, "variables": {"t": 2000}},]free_results = free_bknd.run(job_params=job_params)tn_results = tn_bknd.run(job_params=job_params)
5. Retrieving the Results
Section titled “5. Retrieving the Results”For the QutipBackend
the results are identical to those of QutipEmulator
: a sequence of individual QutipResult
objects, one for each evaluation time. As usual we can, for example, get the final state:
qutip_results[-1].state
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:
free_results.get_batch_status()
<BatchStatus.DONE: 3>
When the batch states shows as DONE
, the results can be accessed. In this case, they are a sequence of SampledResult
objects, one for each entry in job_params
in the same order. For example, we can retrieve the bitstring counts or even plot an histogram with the results:
print(free_results[0].bitstring_counts)free_results[0].plot_histogram()
{'00': 4, '01': 19, '10': 22, '11': 55}

The same could be done with the results from EmuTNBackend
or even from QPUBackend
, as they all share the same format.
6. Alternative user interfaces for using remote backends
Section titled “6. 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.