Skip to content
Pasqal Documentation

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 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.

In this section, we focus only on the fundamental features for programming a sequence in Pulser.

Sequence.declare_channel(name, channel_id[, …])

Declares a new channel in the Sequence.

Sequence.add(pulse, channel[, protocol])

Adds a pulse to a channel.

Sequence.delay(duration, channel[, at_rest])

Idles a given channel for a specific duration.

Sequence.measure([basis])

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:

  1. Pick a channel from the Device and declare it with Sequence.declare_channel().

  2. Add pulses and delays to the channel with Sequence.add() and Sequence.delay().

  3. Terminate the sequence with Sequence.measure().

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

Sequence.declared_channels

Channels declared in this Sequence.

Sequence.device

Device that the sequence is using.

Methods

Sequence.draw([mode, as_phase_modulated, …])

Draws the sequence in its current state.

Sequence.get_addressed_bases()

Returns the bases addressed by the declared channels.

Sequence.get_addressed_states()

Returns the states addressed by the declared channels.

Sequence.get_duration([channel, …])

Returns the current duration of a channel or the whole sequence.

Sequence.get_measurement_basis()

Gets the sequence’s measurement basis.

Sequence.get_register([include_mappable])

The atom register on which to apply the pulses.

Sequence.is_measured()

States whether the sequence has been measured.

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


_images/sequence_8_1.png
  • 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.

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 defined min_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 the Sequence creation.

import pulser
spacing = 2 # The spacing we will use between atoms
reg = pulser.Register({"q0": (0, 0), "q1": (spacing, 0)})
# spacing is below the AnalogDevice's specs, so we expect an error
assert 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')]

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:

Sequence

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.

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.