Tutorial: Programming with Pulser
This tutorial demonstrates how to use Pulser to program the evolution of a quantum system. Two programs are presented:
In a first part, we excite one atom from its ground state to its excited state using a constant pulse.
In a second part, we show how to prepare a quantum system of 9 atoms in an anti-ferromagnetic state using time-dependent pulses.
This tutorial follows the step-py-step guide on how to create a quantum program using Pulser that is provided in the programming page. For more information regarding the steps followed and the mathematical objects at stake, please refer to this page.
import numpy as npimport pulserfrom matplotlib import pyplot as plt
Preparing an atom in the Rydberg state
Section titled “Preparing an atom in the Rydberg state”As presented in “Programming a neutral-atom QPU”, Pulser enables you to program an Hamiltonian composed of an interaction Hamiltonian and a drive Hamiltonian.
Let’s program this Hamiltonian \(H\) such that an atom initially in the ground state \(\left|g\right>\) is measured in the Rydberg state \(\left|r\right>\) after a time \(\Delta t\).
Since we are working with a single atom, there is no interaction Hamiltonian. In this specific example, \(H=H^D\). For a simple pulse having a duration \(\Delta t\), a constant amplitude along time \(\Omega\), detuning \(\delta=0\) and phase \(\phi=0\) the Hamiltonian between 0 and Δt is:
To find the atom in the Rydberg state at the end of the program, we want \(\Omega \Delta t = \pi\) so we choose \(\Delta t=1000\ ns\) and \(\Omega=\pi\ rad/\mu s\).
We can use the Bloch sphere representation
The pulse being of duration \(\Delta t\), of detuning \(\delta=0\), of phase \(\phi=0\) and constant amplitude \(\Omega\), the pulse will make the vector representing the state rotate by an angle \(\Omega \Delta t\) around the axis \((1, 0, 0)\). To go from the ground state \(\left|g\right>\) to the excited state \(\left|r\right>\) by rotating around the \((1, 0, 0)\) axis, we need to make a rotation of angle \(\pi\).
Therefore we get that the final state will be the Rydberg state if \(\Omega \Delta t = \pi\). From this condition, we choose \(\Delta t = 1000\ ns\) and \(\Omega=\pi\ rad/\mu s\).
The Bloch vector rotates around the x axis by an angle of π, going from the ground state to the Rydberg state.
We can compute the final state knowing the initial state
The initial state being the ground state and the Hamiltonian \(H\) being constant along time, the final state is:
\[\begin{split}\begin{align} \left|\Psi_f\right> &= e^{-\frac{i}{\hbar} H \Delta t} \left|g\right> \\ &= \left(\cos\left(\frac{\Omega}{2} \Delta t\right)(|g\rangle\langle g| + |r\rangle\langle r|) - i \sin\left(\frac{\Omega}{2} \Delta t\right)(|g\rangle\langle r| + |r\rangle\langle g|)\right)\left|g\right>\\ &= \cos\left(\frac{\Omega}{2} \Delta t\right)\left|g\right> - i \sin\left(\frac{\Omega}{2} \Delta t\right)\left|r\right> \end{align}\end{split}\]The final state will be the Rydberg state \(\left|r\right>\) if \(\frac{\Omega}{2} \Delta t = \frac{\pi}{2}\). From this condition, we choose \(\Delta t = 1000\ ns\) and \(\Omega=\pi\ rad/\mu s\).
1. Picking a Device
Section titled “1. Picking a Device”We need a Device
that will enable us to target the transition between the ground and the Rydberg state. pulser.AnalogDevice
contains the Rydberg.Global
channel, which targets the transition between these two states. Let’s select this Device
!
We can check in the device specifications (accessed via Device.specs
) that the AnalogDevice
supports the ground-rydberg transition.
device = pulser.AnalogDeviceprint(device.specs)
A realistic device for analog sequence execution.
Register parameters:
- Dimensions: 2D
- Maximum number of atoms: 80
- Maximum distance from origin: 38 µm
- Minimum distance between neighbouring atoms: 5 μm
Layout parameters:
- Requires layout: Yes
- Accepts new layout: Yes
- Minimal number of traps: 1
- Maximum layout filling fraction: 0.5
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: 2000
Channels:
- 'rydberg_global': Rydberg(addressing='Global', max_abs_detuning=125.66370614359172, max_amp=12.566370614359172, min_retarget_interval=None, fixed_retarget_t=None, max_targets=None, clock_period=4, min_duration=16, max_duration=100000000, min_avg_amp=0, mod_bandwidth=8, custom_phase_jump_time=None, eom_config=RydbergEOM(limiting_beam=<RydbergBeam.RED: 2>, max_limiting_amp=188.49555921538757, intermediate_detuning=2827.4333882308138, controlled_beams=(<RydbergBeam.BLUE: 1>,), mod_bandwidth=40, custom_buffer_time=240, multiple_beam_control=True, blue_shift_coeff=1.0, red_shift_coeff=1.0), propagation_dir=None)
2. Creating the Register
Section titled “2. Creating the Register”We want to excite one atom. There will therefore be only one atom in the Register
, whose position does not matter because it will not interact with another atom.
Let’s then create a Register
containing one atom at the coordinate (0, 0).
register = pulser.Register.from_coordinates([(0, 0)], prefix="q")register.draw()

At this stage, we can initialize the Sequence
, our quantum program. This will check that the created Register
matches the parameters set by the Device
we picked.
sequence = pulser.Sequence(register, device)
3. Picking the Channels
Section titled “3. Picking the Channels”The only channel we need to pick is a Rydberg
channel to target the transition between \(\left|g\right>\) and \(\left|r\right>\). Since we only have one atom, the addressing does not matter, the Rydberg.Global
channel will address the atom in the register.
sequence.declare_channel("rydberg_global", "rydberg_global")print( "The states used in the computation are", sequence.get_addressed_states())
The states used in the computation are ['r', 'g']
At this stage, the atom is initialized in the ground state \(\left|g\right>\) and only two energy levels are used in the computation - the state of the system is described by a qubit.
4. Adding the pulses
Section titled “4. Adding the pulses”Let’s now add the pulse of duration \(\Delta t = 1000\ ns\), amplitude \(\Omega=\pi\ rad/\mu s\), detuning \(\delta=0\) and phase \(\phi=0\) to the Rydberg.Global
channel to modify the state of the atom and make it reach the state \(\left|r\right>\).
pi_pulse = pulser.Pulse.ConstantPulse(1000, np.pi, 0, 0)sequence.add(pi_pulse, "rydberg_global")sequence.draw(mode="input")

Executing the Pulse Sequence
Section titled “Executing the Pulse Sequence”We are now done with our first Pulser program! We can now submit it to a backend for execution. Pulser provides multiple backends, notably the QPUs, but also a backend to simulate small quantum systems on your laptop based on QuTip. Let’s use this QutipBackend
to simulate the final state of the system:
backend = pulser.backends.QutipBackend(sequence)result = backend.run()
When running an experiment on a neutral-atom QPU, the output of the quantum program is the sampling of the final state. It is a dictionnary associating to each measured state the number of times it was measured.
result.sample_final_state(1000)
Counter({'1': 1000})
When measuring in the ground-rydberg basis, the ground state is labelled “0” and the rydberg state “1”. For each of the 1000 measurements we did, the atom was measured in the Rydberg state, which means we designed our quantum program correctly!
Adiabatic preparation of an Anti-Ferromagnetic State
Section titled “Adiabatic preparation of an Anti-Ferromagnetic State”Let’s now program the Ising Hamiltonian such that a set of 9 atoms initially in the ground state \(\left|ggggggggg\right>\) are prepared in an antiferromagnetic state \(\left|rgrgrgrgr\right>\).
To reach the desired antiferromagentic state, we can take advantage of the adiabatic theorem (external). The idea is to use a time-dependent Hamiltonian that changes slowly so that the system stays in its ground state. Therefore, we must choose a final Hamiltonian that has the antiferromagnetic state as its ground state.
This final Hamiltonian should simultaneously favor having the largest number of atoms in the \(\left|r\right>\) state (by having \(\delta > 0\)) and discourage nearest neighbors from being both in \(\left|r\right>\) (via the interaction Hamiltonian). When these contributions are appropriately balanced, we get an Hamiltonian with \(\left|rgrgrgrgr\right>\) as its ground state.
Let’s follow the protocol from this paper (external), where we define the parameters with respect to the interaction strength between nearest neighbours, \(U\) (see Table 1 of the paper):
and define \(\Omega(t)\) and \(\delta(t)\) over time as (see Figure 1 (b)):
The Hamiltonian we are implementing is (the phase is constant and equal to \(0\) over time):
where \(U_{ij} = \frac{C_6}{\hbar R_{ij}^6}\).
# Parameters in rad/µsU = 2 * np.piOmega_max = 2.0 * Udelta_0 = -6 * Udelta_f = 2 * U
# Parameters in nst_rise = 252t_fall = 500t_sweep = (delta_f - delta_0) / (2 * np.pi * 10) * 1000
1. Picking a Device
Section titled “1. Picking a Device”We need a Device
that will enable us to target the transition between the ground and the rydberg state. As above, we select pulser.AnalogDevice
since it contains the Rydberg.Global
channel, that targets the transition between these two states.
device = pulser.AnalogDeviceprint(device.specs)
A realistic device for analog sequence execution.
Register parameters:
- Dimensions: 2D
- Maximum number of atoms: 80
- Maximum distance from origin: 38 µm
- Minimum distance between neighbouring atoms: 5 μm
Layout parameters:
- Requires layout: Yes
- Accepts new layout: Yes
- Minimal number of traps: 1
- Maximum layout filling fraction: 0.5
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: 2000
Channels:
- 'rydberg_global': Rydberg(addressing='Global', max_abs_detuning=125.66370614359172, max_amp=12.566370614359172, min_retarget_interval=None, fixed_retarget_t=None, max_targets=None, clock_period=4, min_duration=16, max_duration=100000000, min_avg_amp=0, mod_bandwidth=8, custom_phase_jump_time=None, eom_config=RydbergEOM(limiting_beam=<RydbergBeam.RED: 2>, max_limiting_amp=188.49555921538757, intermediate_detuning=2827.4333882308138, controlled_beams=(<RydbergBeam.BLUE: 1>,), mod_bandwidth=40, custom_buffer_time=240, multiple_beam_control=True, blue_shift_coeff=1.0, red_shift_coeff=1.0), propagation_dir=None)
2. Creating the Register
Section titled “2. Creating the Register”Let’s keep following the protocol (external) and create the Register
. We place the atoms in a square lattice, such that the distance between two neighbouring atoms (that is, the spacing of the square layout) is the same, and we choose that distance such that \(\frac{C_6}{R^6}=\hbar U\).
R_interatomic = (device.interaction_coeff / U) ** (1 / 6)N_side = 3register = pulser.Register.square(N_side, R_interatomic, prefix="q")print(f"Interatomic Radius is: {R_interatomic}µm.")register.draw()
Interatomic Radius is: 7.186760677748386µm.

Note: In Pulser, we can access the interaction coefficient \(\frac{C_6}{\hbar}\) with device.interaction_coeff
Let’s now initialize our quantum program, the Sequence
, and check that the created Register
matches the parameters set by the Device
we picked:
sequence = pulser.Sequence(register, device)
3. Picking the Channels
Section titled “3. Picking the Channels”The only channel we need to pick is a Rydberg
channel to target the transition between \(\left|g\right>\) and \(\left|r\right>\). Since we want to apply the same amplitude, detuning and phase on each atom, we can use the Rydberg.Global
channel:
sequence.declare_channel("rydberg_global", "rydberg_global")print( "The states used in the computation are", sequence.get_addressed_states())
The states used in the computation are ['r', 'g']
At this stage, all the atoms are initialized in the state \(\left|g\right>\) and only two energy levels are used in the computation, i.e. each atom is a qubit and the initial state of the quantum system is \(\left|ggggggggg\right>\).
The interaction Hamiltonian is now completely determined, and will not change over time.
4. Adding the pulses
Section titled “4. Adding the pulses”Let’s now define the driving Hamiltonian at each nanosecond between \(0\) and \(t_{tot}=t_{rise}+t_{sweep}+t_{fall}\). We follow the program that we described above. The Sequence
will be composed of three pulses:
A first “rise” pulse with:
Duration: \(t_{rise}\)
Amplitude: \(0 \rightarrow \Omega_{max}\)
Detuning: \(\delta_0\)
Phase: \(0\)
A second “sweep” pulse with:
Duration: \(t_{sweep}\)
Amplitude: \(\Omega_{max}\)
Detuning: \(\delta_0 \rightarrow\delta_{final}\)
Phase: \(0\)
A third “fall” pulse with:
Duration: \(t_{fall}\)
Amplitude: \(\Omega_{max}\rightarrow 0\)
Detuning: \(\delta_{final}\)
Phase: \(0\)
rise = pulser.Pulse.ConstantDetuning( pulser.RampWaveform(t_rise, 0.0, Omega_max), delta_0, 0.0)sweep = pulser.Pulse.ConstantAmplitude( Omega_max, pulser.RampWaveform(t_sweep, delta_0, delta_f), 0.0)fall = pulser.Pulse.ConstantDetuning( pulser.RampWaveform(t_fall, Omega_max, 0.0), delta_f, 0.0)sequence.add(rise, "rydberg_global")sequence.add(sweep, "rydberg_global")sequence.add(fall, "rydberg_global")sequence.draw(mode="input")

Executing the Pulse Sequence
Section titled “Executing the Pulse Sequence”We are now done with this program! Let’s use the QutipBackend
to simulate the final state of the system:
backend = pulser.backends.QutipBackend(sequence)result = backend.run()counts = result.sample_final_state(1000)# Let's plot the histogram associated to the measurements# Let's select only the states that are measured more than 10 timesmost_freq = {k: v for k, v in counts.items() if v > 10}plt.bar(list(most_freq.keys()), list(most_freq.values()))plt.xticks(rotation="vertical")plt.show()

The state that is measured the most frequently is the \(\left|101010101\right>\rightarrow\left|rgrgrgrgr\right>\): our quantum program correctly excites the ground sate \(\left|ggggggggg\right>\) into the state \(\left|rgrgrgrgr\right>\).