Control-Z Gate Sequence
Introduction
In this tutorial we show how to prepare the pulse sequence that generates a Controlled - Z gate. We will prepare our state with atoms in any of the “digital” states that we shall call \(|g\rangle\) and \(|h \rangle\) ( for “ground” and “hyperfine”, respectively). Then we will use the Rydberg blockade effect to create the logic gate. The levels that each atom can take are the following:
We will be using NumPy and Matplotlib for calculations and plots. Many additional details about the CZ gate construction can be found in 1111.6083v2
[1]:
import numpy as np
import matplotlib.pyplot as plt
import qutip
from itertools import product
We import the following Classes from Pulser:
[2]:
from pulser import Pulse, Sequence, Register
from pulser.devices import DigitalAnalogDevice
from pulser_simulation import QutipEmulator
from pulser.waveforms import BlackmanWaveform, ConstantWaveform
1. Loading the Register on a Device
Defining an atom register can simply be done by choosing one of the predetermined shapes included in the Register
class. We can also construct a dictionary with specific labels for each atom. The atoms must lie inside the Rydberg blockade radius \(R_b\), which we will characterize by
where the coefficient \(C_6\) determines the strength of the interaction (\(C_6/\hbar \approx 5008\) GHz.\(\mu m^6\)). We can obtain the corresponding Rydberg blockade radius from a given \(\Omega_{\text{Rabi}}^{\text{max}}\) using the rydberg_blockade_radius()
method from DigitalAnalogDevice
. For the pulses in this tutorial, \(\Omega^{\text{Max}}_{\text{Rabi}}\) is below \(2\pi \times 10\) Mhz so:
[3]:
Rabi = np.linspace(1, 10, 10)
R_blockade = [
DigitalAnalogDevice.rydberg_blockade_radius(2.0 * np.pi * rabi)
for rabi in Rabi
]
plt.figure()
plt.plot(Rabi, R_blockade, "--o")
plt.xlabel(r"$\Omega/(2\pi)$ [MHz]", fontsize=14)
plt.ylabel(r"$R_b$ [$\mu\.m$]", fontsize=14)
plt.show()

Thus, we place our atoms at relative distances below \(5\) µm, therefore ensuring we are inside the Rydberg blockade volume.
[4]:
# Atom Register and Device
q_dict = {
"control": np.array([-2, 0.0]),
"target": np.array([2, 0.0]),
}
reg = Register(q_dict)
reg.draw()

2. State Preparation
The first part of our sequence will correspond to preparing the different states on which the CZ gate will act. For this, we define the following Pulse
instances that correspond to \(\pi\) and \(2\pi\) pulses (notice that the area can be easily fixed using the predefined BlackmanWaveform
):
Let us construct a function that takes the label string (or “id”) of a state and turns it into a ket state. This ket can be in any of the “digital” (ground-hyperfine levels), “ground-rydberg” or “all” levels. We also include a three-atom system case, which will be useful in the CCZ gate in the last section.
[5]:
def build_state_from_id(s_id, basis_name):
if len(s_id) not in {2, 3}:
raise ValueError("Not a valid state ID string")
ids = {"digital": "gh", "ground-rydberg": "rg", "all": "rgh"}
if basis_name not in ids:
raise ValueError("Not a valid basis")
pool = {"".join(x) for x in product(ids[basis_name], repeat=len(s_id))}
if s_id not in pool:
raise ValueError("Not a valid state id for the given basis.")
ket = {
op: qutip.basis(len(ids[basis_name]), i)
for i, op in enumerate(ids[basis_name])
}
if len(s_id) == 3:
# Recall that s_id = 'C1'+'C2'+'T' while in the register reg_id = 'C1'+'T'+'C2'.
reg_id = s_id[0] + s_id[2] + s_id[1]
return qutip.tensor([ket[x] for x in reg_id])
else:
return qutip.tensor([ket[x] for x in s_id])
We try this out:
[6]:
build_state_from_id("hg", "digital")
[6]:
Let’s now write the state preparation sequence. We will also create the prepared state to be able to calculate its overlap during the simulation. First, let us define a π-pulse along the Y axis that will excite the atoms to the hyperfine state if requested:
[7]:
duration = 300
pi_Y = Pulse.ConstantDetuning(
BlackmanWaveform(duration, np.pi), 0.0, -np.pi / 2
)
pi_Y.draw()

The sequence preparation itself acts with the Raman channel if the desired initial state has atoms in the hyperfine level. We have also expanded it for the case of a CCZ in order to use it below:
[8]:
def preparation_sequence(state_id, reg):
global seq
if not set(state_id) <= {"g", "h"} or len(state_id) != len(reg.qubits):
raise ValueError("Not a valid state ID")
if len(reg.qubits) == 2:
seq_dict = {"1": "target", "0": "control"}
elif len(reg.qubits) == 3:
seq_dict = {"2": "target", "1": "control2", "0": "control1"}
seq = Sequence(reg, DigitalAnalogDevice)
if set(state_id) == {"g"}:
basis = "ground-rydberg"
print(
f"Warning: {state_id} state does not require a preparation sequence."
)
else:
basis = "all"
for k in range(len(reg.qubits)):
if state_id[k] == "h":
if "raman" not in seq.declared_channels:
seq.declare_channel(
"raman", "raman_local", seq_dict[str(k)]
)
else:
seq.target(seq_dict[str(k)], "raman")
seq.add(pi_Y, "raman")
prep_state = build_state_from_id(
state_id, basis
) # Raises error if not a valid `state_id` for the register
return prep_state
Let’s test this sequence. Notice that the state “gg” (both atoms in the ground state) is automatically fed to the Register so a pulse sequence is not needed to prepare it.
[9]:
# Define sequence and Set channels
prep_state = preparation_sequence("hh", reg)
seq.draw(draw_phase_area=True)

3. Constructing the Gate Sequence
We apply the common \(\pi-2\pi-\pi\) sequence for the CZ gate
[10]:
pi_pulse = Pulse.ConstantDetuning(BlackmanWaveform(duration, np.pi), 0.0, 0)
twopi_pulse = Pulse.ConstantDetuning(
BlackmanWaveform(duration, 2 * np.pi), 0.0, 0
)
[11]:
def CZ_sequence(initial_id):
# Prepare State
prep_state = preparation_sequence(initial_id, reg)
prep_time = max(
(seq._last(ch).tf for ch in seq.declared_channels), default=0
)
# Declare Rydberg channel
seq.declare_channel("ryd", "rydberg_local", "control")
# Write CZ sequence:
seq.add(
pi_pulse, "ryd", "wait-for-all"
) # Wait for state preparation to finish.
seq.target("target", "ryd") # Changes to target qubit
seq.add(twopi_pulse, "ryd")
seq.target("control", "ryd") # Changes back to control qubit
seq.add(pi_pulse, "ryd")
return prep_state, prep_time
[12]:
prep_state, prep_time = CZ_sequence(
"gh"
) # constructs seq, prep_state and prep_time
seq.draw(draw_phase_area=True)
print(f"Prepared state: {prep_state}")
print(f"Preparation time: {prep_time}ns")

Prepared state: Quantum object: dims = [[3, 3], [1, 1]], shape = (9, 1), type = ket
Qobj data =
[[0.]
[0.]
[0.]
[0.]
[0.]
[1.]
[0.]
[0.]
[0.]]
Preparation time: 300ns
4. Simulating the CZ sequence
[13]:
CZ = {}
for state_id in {"gg", "hg", "gh", "hh"}:
# Get CZ sequence
prep_state, prep_time = CZ_sequence(
state_id
) # constructs seq, prep_state and prep_time
# Construct Simulation instance
simul = QutipEmulator.from_sequence(seq)
res = simul.run()
data = [st.overlap(prep_state) for st in res.states]
final_st = res.states[-1]
CZ[state_id] = final_st.overlap(prep_state)
plt.figure()
plt.plot(np.real(data))
plt.xlabel(r"Time [ns]")
plt.ylabel(rf"$ \langle\,{state_id} |\, \psi(t)\rangle$")
plt.axvspan(0, prep_time, alpha=0.06, color="royalblue")
plt.title(rf"Action of gate on state $|${state_id}$\rangle$")
Warning: gg state does not require a preparation sequence.




[14]:
CZ
[14]:
{'gg': (-0.9990727843670977+0.04305312374451492j),
'hh': (0.9999999998981344-3.67394039706781e-16j),
'gh': (-0.999999999746591+1.836970198255537e-16j),
'hg': (-0.999999999846732+1.8369701984394828e-16j)}
5. CCZ Gate
The same principle can be applied for composite gates. As an application, let us construct the CCZ gate, which determines the phase depending on the level of two control atoms. We begin by reconstructing the Register:
[15]:
# Atom Register and Device
q_dict = {
"control1": np.array([-2.0, 0.0]),
"target": np.array([0.0, 2 * np.sqrt(3.001)]),
"control2": np.array([2.0, 0.0]),
}
reg = Register(q_dict)
reg.draw()

[16]:
preparation_sequence("hhh", reg)
seq.draw(draw_phase_area=True)

[17]:
def CCZ_sequence(initial_id):
# Prepare State
prep_state = preparation_sequence(initial_id, reg)
prep_time = max(
(seq._last(ch).tf for ch in seq.declared_channels), default=0
)
# Declare Rydberg channel
seq.declare_channel("ryd", "rydberg_local", "control1")
# Write CCZ sequence:
seq.add(
pi_pulse, "ryd", protocol="wait-for-all"
) # Wait for state preparation to finish.
seq.target("control2", "ryd")
seq.add(pi_pulse, "ryd")
seq.target("target", "ryd")
seq.add(twopi_pulse, "ryd")
seq.target("control2", "ryd")
seq.add(pi_pulse, "ryd")
seq.target("control1", "ryd")
seq.add(pi_pulse, "ryd")
return prep_state, prep_time
[18]:
CCZ_sequence("hhh")
seq.draw(draw_phase_area=True)

[19]:
CCZ = {}
for state_id in {"".join(x) for x in product("gh", repeat=3)}:
# Get CCZ sequence
prep_state, prep_time = CCZ_sequence(state_id)
# Construct Simulation instance
simul = QutipEmulator.from_sequence(seq)
res = simul.run()
data = [st.overlap(prep_state) for st in res.states]
final_st = res.states[-1]
CCZ[state_id] = final_st.overlap(prep_state)
plt.figure()
plt.plot(np.real(data))
plt.xlabel(r"Time [ns]")
plt.ylabel(rf"$ \langle\,{state_id} | \psi(t)\rangle$")
plt.axvspan(0, prep_time, alpha=0.06, color="royalblue")
plt.title(rf"Action of gate on state $|${state_id}$\rangle$")
Warning: ggg state does not require a preparation sequence.








[20]:
CCZ
[20]:
{'ghg': (-0.9990713765789713+0.04308578030098482j),
'ggg': (-0.9979118035151472+0.06459125797557387j),
'hgg': (-0.9990713580955076+0.04308613130117829j),
'hhh': (0.9999999999559352-5.510910595920242e-16j),
'hhg': (-0.9999999999097571+3.6739403971104873e-16j),
'hgh': (-0.9999999965725218+3.6739403848496926e-16j),
'ggh': (-0.9997680602980688+0.021536609866114793j),
'ghh': (-0.99999999995561+3.673940397278954e-16j)}
Our results are as expected: only the \(|hhh\rangle\) state (which corresponds to a \(111\) digital state) gets its phase flipped in sign