Skip to content

Results are limited to the current section: Pulser

Product news

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 np
import pulser
from matplotlib import pyplot as plt

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 HH such that an atom initially in the ground state g>\left|g\right> is measured in the Rydberg state r>\left|r\right> after a time Δt\Delta t.

Since we are working with a single atom, there is no interaction Hamiltonian. In this specific example, H=HDH=H^D. For a simple pulse having a duration Δt\Delta t, a constant amplitude along time Ω\Omega, detuning δ=0\delta=0 and phase ϕ=0\phi=0 the Hamiltonian between 0 and Δt is:

H(t)=Ω2(gr+rg)H(t) = \hbar\frac{\Omega}{2} (|g\rangle\langle r| + |r\rangle\langle g|)

To find the atom in the Rydberg state at the end of the program, we want ΩΔt=π\Omega \Delta t = \pi so we choose Δt=1000 ns\Delta t=1000\ ns and Ω=π rad/μs\Omega=\pi\ rad/\mu s.

  • We can use the Bloch sphere representation

    The pulse being of duration Δt\Delta t, of detuning δ=0\delta=0, of phase ϕ=0\phi=0 and constant amplitude Ω\Omega, the pulse will make the vector representing the state rotate by an angle ΩΔt\Omega \Delta t around the axis (1,0,0)(1, 0, 0). To go from the ground state g>\left|g\right> to the excited state r>\left|r\right> by rotating around the (1,0,0)(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 ΩΔt=π\Omega \Delta t = \pi. From this condition, we choose Δt=1000 ns\Delta t = 1000\ ns and Ω=π rad/μs\Omega=\pi\ rad/\mu s.

    Bloch rotation of angle pi

    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 HH being constant along time, the final state is:

    Ψf>=eiHΔtg>=(cos(Ω2Δt)(gg+rr)isin(Ω2Δt)(gr+rg))g>=cos(Ω2Δt)g>isin(Ω2Δt)r>\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 r>\left|r\right> if Ω2Δt=π2\frac{\Omega}{2} \Delta t = \frac{\pi}{2}. From this condition, we choose Δt=1000 ns\Delta t = 1000\ ns and Ω=π rad/μs\Omega=\pi\ rad/\mu s.

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.AnalogDevice
print(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
 - Minimum layout filling fraction: 0.0
 - 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)

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()
../_images/tutorials_creating_11_0.png

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)

The only channel we need to pick is a Rydberg channel to target the transition between g>\left|g\right> and r>\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 g>\left|g\right> and only two energy levels are used in the computation - the state of the system is described by a qubit.

Let’s now add the pulse of duration Δt=1000 ns\Delta t = 1000\ ns, amplitude Ω=π rad/μs\Omega=\pi\ rad/\mu s, detuning δ=0\delta=0 and phase ϕ=0\phi=0 to the Rydberg.Global channel to modify the state of the atom and make it reach the state r>\left|r\right>.

pi_pulse = pulser.Pulse.ConstantPulse(1000, np.pi, 0, 0)
sequence.add(pi_pulse, "rydberg_global")
sequence.draw(mode="input")
../_images/tutorials_creating_20_0.png

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 QutipBackendV2 to simulate the final state of the system:

backend = pulser.backends.QutipBackendV2(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 dictionary associating to each measured state the number of times it was measured.

result.final_bitstrings
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 ggggggggg>\left|ggggggggg\right> are prepared in an antiferromagnetic state rgrgrgrgr>\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 r>\left|r\right> state (by having δ>0\delta > 0) and discourage nearest neighbors from being both in r>\left|r\right> (via the interaction Hamiltonian). When these contributions are appropriately balanced, we get an Hamiltonian with rgrgrgrgr>\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, UU (see Table 1 of the paper):

U=2π rad/μsΩmax=2Uδ0=6Uδf=2Utrise=252 ns, tfall=500 nstsweep=δfδ0[radμs1]2π10 [radμs2]\begin{split}U = 2\pi\ rad/\mu s\\ \Omega_{max} = 2 U\\ \delta_0 = -6 U\\ \delta_f = 2U\\ t_{rise} = 252\ ns,\ t_{fall} = 500\ ns\\ t_{sweep} = \frac{\delta_f - \delta_0 [rad\cdot\mu s^{-1}]}{2 \pi \cdot 10\ [rad\cdot\mu s^{-2}]}\end{split}

and define Ω(t)\Omega(t) and δ(t)\delta(t) over time as (see Figure 1 (b)):

AF Pulse Sequence

The Hamiltonian we are implementing is (the phase is constant and equal to 00 over time):

H=i(Ω(t)2(gr+rg)δ(t)rr+j<iUijn^in^j)H = \hbar \sum_i \left (\frac{\Omega(t)}{2} \left(|g\rangle\langle r| + |r\rangle\langle g|\right) - \delta(t) |r\rangle\langle r| + \sum_{j<i}U_{ij}\hat{n}_i \hat{n}_j \right)

where Uij=C6Rij6U_{ij} = \frac{C_6}{\hbar R_{ij}^6}.

# Parameters in rad/µs
U = 2 * np.pi
Omega_max = 2.0 * U
delta_0 = -6 * U
delta_f = 2 * U
# Parameters in ns
t_rise = 252
t_fall = 500
t_sweep = (delta_f - delta_0) / (2 * np.pi * 10) * 1000

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.AnalogDevice
print(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
 - Minimum layout filling fraction: 0.0
 - 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)

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 C6R6=U\frac{C_6}{R^6}=\hbar U.

R_interatomic = (device.interaction_coeff / U) ** (1 / 6)
N_side = 3
register = 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.
../_images/tutorials_creating_35_1.png

Note: In Pulser, we can access the interaction coefficient C6\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)

The only channel we need to pick is a Rydberg channel to target the transition between g>\left|g\right> and r>\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 g>\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 ggggggggg>\left|ggggggggg\right>.

The interaction Hamiltonian is now completely determined, and will not change over time.

Let’s now define the driving Hamiltonian at each nanosecond between 00 and ttot=trise+tsweep+tfallt_{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: triset_{rise}

    • Amplitude: 0Ωmax0 \rightarrow \Omega_{max}

    • Detuning: δ0\delta_0

    • Phase: 00

  • A second “sweep” pulse with:

    • Duration: tsweept_{sweep}

    • Amplitude: Ωmax\Omega_{max}

    • Detuning: δ0δfinal\delta_0 \rightarrow\delta_{final}

    • Phase: 00

  • A third “fall” pulse with:

    • Duration: tfallt_{fall}

    • Amplitude: Ωmax0\Omega_{max}\rightarrow 0

    • Detuning: δfinal\delta_{final}

    • Phase: 00

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")
../_images/tutorials_creating_44_0.png

We are now done with this program! Let’s use the QutipBackendV2 to simulate the final state of the system:

backend = pulser.backends.QutipBackendV2(sequence)
result = backend.run()
counts = result.final_bitstrings
# Let's plot the histogram associated to the measurements
# Let's select only the states that are measured more than 10 times
most_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()
../_images/tutorials_creating_47_0.png

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