from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from pulser import Pulse, Sequence, Register
from pulser.devices import DigitalAnalogDevice
from pulser.waveforms import BlackmanWaveform
from pulser_patch.simulation import QutipEmulator
import matplotlib.pyplot as plt
import numpy as np
from util import plot_histogram, simulate
Pulse-Level Quantum Computing with Neutral Atoms#
In this notebook, we introduce neutral atom quantum computing with pulse sequences. Pulse sequences enable us to control a quantum computer in both digital and analog modes.
Programming with Pulse Sequences#
As a reminder, a program for a neutral atom computer consists of
a register defining the locations of the qubits and
waveforms for the Rabi frequency \(\Omega_j\), Rabi phase \(\phi_j\), and detuning \(\Delta\).
These are inputs to to the Rydberg Hamiltonian, reproduced below
Example: Bell State#
Following [1], we’ll use pulse sequences to construct a Bell state. First, we’ll review a gate-based program that constructs the Bell-state before mimicing it with a pulse sequence.
Gate-Based Program#
The standard gate-based program uses a Hadamard gate followed by a CX gate.
qc = QuantumCircuit(QuantumRegister(1, "c"), QuantumRegister(1, "t"), ClassicalRegister(2, "cl"))
qc.h(0)
qc.cx(0, 1)
qc.measure(range(2), range(2))
qc.draw(output="mpl")
After running this circuit, we obtain the Bell state
counts = simulate(qc)
plot_histogram(counts)
Equivalent Gate-Based Program#
On a neutral atom computer, it will be easier to apply \(RY(\theta)\) (i.e., rotation around \(y\)-axis on the Bloch sphere) and the \(CZ\) gate (i.e., controlled \(RZ\) gate). We can rewrite the program above equivalently using only \(RY(\theta)\) and \(CZ\) gates.
qc_equiv = QuantumCircuit(QuantumRegister(1, "c"), QuantumRegister(1, "t"), ClassicalRegister(2, "cl"))
qc_equiv.ry(np.pi/2, 0)
qc_equiv.ry(-np.pi/2, 1)
qc_equiv.cz(0, 1)
qc_equiv.ry(np.pi/2, 1)
qc_equiv.measure(range(2), range(2))
qc_equiv.draw(output="mpl", style="iqp")
counts = simulate(qc_equiv)
plot_histogram(counts)
Pulse Sequence Program#
We begin by defining the qubits in a register. We’ll use two qubits labeled c
and t
respectively.
qubits = {
'c': (0, 0),
't': (4, 0)
}
reg = Register(qubits)
reg.draw()
Pulse#
A pulse is a signal \(p: [T_1, T_2] \rightarrow \mathbb{R}\). One popular pulse is known as the Blackman waveform.
half_pi_wf = BlackmanWaveform(200, np.pi/2)
plt.plot(half_pi_wf.samples)
plt.title("Blackman Waveform")
plt.xlabel("Time (ns)")
plt.ylabel("Amplitude (rad/µs)")
plt.grid(True)
Pulse Sequence#
A pulse sequence is a list of pulses. We’ll define two channels to accumulate pulse sequences using pulser
.
The
single
channel will be used to accumulate pulses applied to single qubits.The
rydberg
channel will be used to accumulate pulses applied to multiple qubits.
seq = Sequence(reg, DigitalAnalogDevice)
seq.declare_channel(
'single', # Name of channel
'raman_local' # Single qubit channel only requires raman component
)
seq.declare_channel(
'rydberg', # Name of channel
'rydberg_local' # Multi-qubit channel requires Rydberg Blockade
)
seq.draw(draw_phase_curve=True)
\(RY(\pi/2)\)#
We can use our first pulse sequence to simulate a \(RY(\pi/2)\) gate.
ry_pulse = Pulse.ConstantDetuning(
amplitude=half_pi_wf,
detuning=0,
phase=-np.pi/2,
)
seq.target('c', 'single')
seq.add(ry_pulse, 'single')
seq.draw(draw_phase_curve=True)
\(RY(-\pi/2)\)#
We can also apply \(RY(-\pi/2)\) by changing the phase. Note that the qubit we are applying the qubit to is notated in the top-left.
ry_dag_pulse = Pulse.ConstantDetuning(
amplitude=half_pi_wf,
detuning=0,
phase=np.pi/2,
)
seq.target('t', 'single')
seq.add(ry_dag_pulse, 'single')
seq.draw(draw_phase_curve=True)
CZ Gate#
We will now apply the sequence of pulses corresponding to a CZ gate. To accomplish this, we will need to use the Rydberg Blockade so that nearby atoms can interact and devise more pulses.
max_val = DigitalAnalogDevice.rabi_from_blockade(8)
max_val
20.67626392364502
pi_wf = BlackmanWaveform(200, np.pi)
two_pi_wf = BlackmanWaveform.from_max_val(
max_val=max_val,
area=2*np.pi,
)
plt.plot(half_pi_wf.samples, label=r"$\pi/2$")
plt.plot(pi_wf.samples, label=r"$\pi$")
plt.plot(two_pi_wf.samples, label=r"$2\pi$")
plt.title("Blackman Waveform")
plt.xlabel("Time (ns)")
plt.ylabel("Amplitude (rad/µs)")
plt.grid(True)
plt.legend();
seq.target('c', 'rydberg')
seq.align('single', 'rydberg')
pi_pulse = Pulse.ConstantDetuning(pi_wf, 0, 0)
seq.add(pi_pulse, 'rydberg')
two_pi_pulse = Pulse.ConstantDetuning(
amplitude=two_pi_wf,
detuning=0,
phase=0,
)
seq.target('t', 'rydberg')
seq.add(two_pi_pulse, 'rydberg')
seq.target('c', 'rydberg')
seq.add(pi_pulse, 'rydberg')
seq.draw(draw_phase_curve=True)
/opt/hostedtoolcache/Python/3.12.6/x64/lib/python3.12/site-packages/pulser/sequence/sequence.py:1302: UserWarning: A duration of 725 ns is not a multiple of the channel's clock period (4 ns). It was rounded up to 728 ns.
self._add(pulse, channel, protocol)
\(RY(\pi/2)\)#
Finally, we apply the last \(RY(\pi/2)\) gate.
seq.align('single', 'rydberg')
seq.add(ry_pulse, 'single')
seq.draw(draw_phase_curve=True)
Measurement#
seq.measure(basis='digital')
seq.draw(draw_phase_curve=True)
sim = QutipEmulator.from_sequence(seq)
res = sim.run()
counts = res.sample_final_state(N_samples=2048)
plot_histogram(counts)
/opt/hostedtoolcache/Python/3.12.6/x64/lib/python3.12/site-packages/qutip/solver/options.py:16: FutureWarning: Dedicated options class are no longer needed, options should be passed as dict to solvers.
warnings.warn(
/opt/hostedtoolcache/Python/3.12.6/x64/lib/python3.12/site-packages/qutip/solver/solver_base.py:459: FutureWarning: "progress_bar" is now included in options:
Use `options={"progress_bar": False / True / "tqdm" / "enhanced"}`
warnings.warn(
Summary#
We saw how to program a neutral atom quantum computer with pulse sequences.
A pulse sequence gives fine-grained and analog control over the implementation of a digital gates.
We saw how to use the Rydberg blockade to implement a pulse sequence to implement the \(CZ\) gate.