Sequence Creation and Validation
What you will learn:
what a Pulser
Sequence
is;what you can do with a
Sequence
;how to make sure your
Sequence
is valid.
The Pulser Sequence
Section titled “The Pulser Sequence”The Sequence
is what encapsulates a Pulser program and it brings together all the concepts we have introduced so far in the Fundamentals section. Concretely, a Sequence
combines:
a Register that defines the relative positions of the atoms involved in the computation;
a Device that dictates the physical constraints the program must respect;
Channels, selected from the
Device
, that define which states are used in the computation;a schedule of operations, wherein Pulses are included, that determine what happens over time.
As described in Programming a Neutral-Atom QPU and exemplified in the Tutorial:, constructing a Sequence
is akin to programming a time-dependent Hamiltonian. This program can then be given to a Backend that will evolve an initial state according to the programmed Hamiltonian, producing a result.
The pages linked above covered the step-by-step process of constructing a Sequence
. In this page, we will instead highlight the most relevant capabilities of the Sequence
object itself.
Fundamental Sequence
Capabilities
Section titled “Fundamental Sequence Capabilities”In this section, we focus only on the fundamental features for programming a sequence in Pulser.
Building
Section titled “Building”
|
Declares a new channel in the Sequence. |
|
Adds a pulse to a channel. |
|
Idles a given channel for a specific duration. |
|
Measures in a valid basis. |
These four method are all you need to build a basic Pulser Sequence
. As shown in this tutorial, they should be used in this order:
Pick a channel from the
Device
and declare it withSequence.declare_channel()
.Add pulses and delays to the channel with
Sequence.add()
andSequence.delay()
.Terminate the sequence with
Sequence.measure()
.
Inspecting
Section titled “Inspecting”During and after the sequence building process, you migh want to inspect or access its contents. These properties and methods allow you to do so:
Properties
Channels declared in this Sequence. |
|
Device that the sequence is using. |
Methods
|
Draws the sequence in its current state. |
Returns the bases addressed by the declared channels. |
|
Returns the states addressed by the declared channels. |
|
|
Returns the current duration of a channel or the whole sequence. |
Gets the sequence’s measurement basis. |
|
|
The atom register on which to apply the pulses. |
States whether the sequence has been measured. |
Drawing and Printing
Section titled “Drawing and Printing”In particular, it is often useful to visualize a Sequence
’s contents, which we can do either by drawing or printing. Let’s exemplify these two options with the very simple sequence in this tutorial.
import pulser
sequence = pulser.Sequence( pulser.Register({"q0": (0, 0)}), pulser.AnalogDevice)sequence.declare_channel("rydberg_global", "rydberg_global")pulse = pulser.Pulse.ConstantPulse(1000, 3.14, 0, 0)sequence.add(pulse, "rydberg_global")
print(sequence)sequence.draw()
Channel: rydberg_global
t: 0 | Initial targets: q0 | Phase Reference: 0.0
t: 0->1000 | Pulse(Amp=3.14 rad/µs, Detuning=0 rad/µs, Phase=0) | Targets: q0

With
print(sequence)
, we have access to its contents in text form. This is particularly useful to access the exact timings of each instruction.
With
sequence.draw()
, we see a plot of the channel’s contents over time. This method is highly configurable, though most of its options are related to features we have not yet covered.
Runtime Validation
Section titled “Runtime Validation”Alongside containing all the information necessary for execution on a backend, the Sequence
’s main function is to ensure that all its contents respect the Device specifications.
Here is an example:
pulser.AnalogDevice
has a definedmin_atom_distance
, a minimum distance that must be respected between any two atoms in a register;if we create a
Register
where two atoms are closer than this distance, it will not respect the device’s constraints;therefore, once they are both given to the
Sequence
it will complain right away, giving us a chance to modify our register or choose a new device before we proceed with theSequence
creation.
import pulser
spacing = 2 # The spacing we will use between atomsreg = pulser.Register({"q0": (0, 0), "q1": (spacing, 0)})
# spacing is below the AnalogDevice's specs, so we expect an errorassert spacing < pulser.AnalogDevice.min_atom_distance
try: pulser.Sequence(reg, pulser.AnalogDevice)except ValueError as e: print("Failed with error: ", e)
Failed with error: The minimal distance between atoms in this device (5 µm) is not respected (up to a precision of 1e-6 µm) for the pairs: [('q0', 'q1')]
Switching the device
Section titled “Switching the device”Sometimes, one wants to change the device of an already constructed Sequence
. Before manually reconstructing the Sequence
with the new device, one can try the switch_device()
method:
- pulser.Sequence.switch_device(self, new_device, strict=False)
Replicate the sequence with a different device.
This method is designed to replicate the sequence with as few changes to the original contents as possible. If the strict option is chosen, the device switch will fail whenever it cannot guarantee that the new sequence’s contents will not be modified in the process.
- Parameters:
new_device (
TypeVar
(DeviceType
, bound=BaseDevice
)) – The target device instance.strict (
bool
, default:False
) – Enforce a strict match between devices and channels to guarantee the pulse sequence is left unchanged.
- Return type:
- Returns:
The sequence on the new device.
This method attemps to automatically reconstruct the sequence with the new device, which will only succeed if the new sequence is valid on the new device.
Backend-specific validation
Section titled “Backend-specific validation”There are a handful of backend-specific constraints that are not enforced during sequence construction. This is because execution on a QPU enforces additional restrictions that don’t directly affect the programmed Hamiltonian, so they are not enforced by default. Nonetheless, when using an emulator to fully mimic the QPU execution process, all the QPU constraints can be enabled via the mimic_qpu
argument.