Programming Neutral Atom Quantum Computers with Bloqade
Learn how neutral atom quantum computers use optical tweezers and Rydberg blockade to implement quantum logic, how to define pulse sequences with Bloqade's Python SDK, and how to run simulations and submit to QuEra's Aquila QPU via Amazon Braket.
Neutral Atoms as Qubits
When most people think of quantum computers, they picture superconducting chips cooled to millikelvin temperatures. Neutral atom quantum computers look radically different: they are room-temperature vacuum chambers containing clouds of individual rubidium or cesium atoms, each trapped and positioned by a focused laser beam called an optical tweezer.
Each atom is a natural qubit. Two hyperfine ground states of rubidium-87, called |0> and |1>, are separated by a 6.8 GHz microwave transition with a coherence time of seconds. Unlike superconducting qubits, which must be lithographically patterned and are fixed in place, neutral atom qubits can be repositioned in real time. A spatial light modulator steers the tweezer beams to arbitrary 2D or 3D configurations, effectively programming the qubit connectivity graph on the fly.
QuEra Computing, spun out of Harvard and MIT in 2020, has built the leading neutral atom platform. Their Aquila processor has 256 atoms arranged in 2D arrays with arbitrary geometry. It is available to external users via Amazon Braket.
Rydberg Blockade: The Mechanism for Two-Qubit Interaction
Neutral atoms in their ground states barely interact with each other. To implement two-qubit gates, you need a controllable interaction. The Rydberg blockade provides exactly this.
When you shine a laser near 420 nm on a rubidium atom, you excite it to a Rydberg state, a highly excited electronic state with principal quantum number n around 60 to 100. Rydberg atoms have enormous electric dipole moments. Two Rydberg atoms separated by a few micrometers experience a van der Waals interaction energy of:
V(r) = C6 / r^6
where C6 is the interaction coefficient (on the order of 10^12 rad/s * um^6 for n=70) and r is the separation in micrometers. At a separation of 5 um, this interaction is so strong (megahertz to gigahertz scale) that it completely dominates the laser coupling. If one atom is already excited to the Rydberg state, the energy shift prevents a neighboring atom within the blockade radius from being excited. This is the Rydberg blockade.
The blockade radius is the distance at which the interaction energy equals the laser Rabi frequency:
r_blockade = (C6 / Omega)^(1/6)
For typical Rabi frequencies of 1 MHz and n=70 Rydberg states, the blockade radius is 8 to 12 um.
The blockade enables entangling gates: if atom A is in |1>, atom B cannot be excited to Rydberg, so the two-qubit evolution depends on the joint state. This produces a controlled-Z or CX gate (depending on the pulse sequence) with typical fidelities of 97 to 99.5% in state-of-the-art experiments.
Analog vs Digital Mode
Neutral atom platforms can operate in two distinct modes:
Analog mode treats the entire array as a continuous quantum system. You apply global laser pulses described by a Rabi frequency Omega(t) and a detuning Delta(t) that evolve over time. All atoms experience the same pulse (up to geometry-dependent interaction patterns). This is not a gate-based model: you are solving a quantum simulation of the Hamiltonian:
H(t) = (Omega(t)/2) * sum_i sigma_i^x - Delta(t) * sum_i n_i + sum_{i<j} V_ij n_i n_j
where sigma^x is the Pauli X operator, n_i = |r><r| is the Rydberg occupation number, and V_ij is the interaction between atoms i and j. This is ideal for quantum simulation of Ising-like spin models and combinatorial optimization (the interaction graph encodes the problem structure).
Digital mode uses individual site addressing: different laser beams target different atoms to implement discrete two-qubit gates with arbitrary connectivity. This is closer to the gate-based model used in superconducting and trapped-ion platforms. QuEra’s Aquila currently supports analog mode; digital mode is in active development.
Bloqade: QuEra’s Programming SDK
Bloqade is QuEra’s open-source toolkit for programming neutral atom quantum computers. It has two implementations:
- Bloqade.jl: a Julia package with the full feature set, including exact and approximate emulators
- bloqade-python: a Python package that provides the pulse specification API and can submit to QuEra’s Aquila QPU via Amazon Braket
Install the Python package:
pip install bloqade
For local emulation:
pip install bloqade[emulate]
Defining an Atom Register
The first step in any Bloqade program is defining the atom register: the 2D spatial positions of the atoms in micrometers.
from bloqade import start
from bloqade.atom_arrangement import Square, Chain, Honeycomb
import numpy as np
# A simple 1D chain of 7 atoms, spaced 6.5 micrometers apart
chain_register = (
start
.add_position([(i * 6.5, 0) for i in range(7)])
)
# A 3x3 square lattice
square_register = Square(3, lattice_spacing=6.5)
# An arbitrary geometry for a specific problem
custom_positions = [
(0.0, 0.0),
(6.5, 0.0),
(3.25, 5.63), # equilateral triangle apex
(9.75, 5.63),
(6.5, 11.26),
]
custom_register = start.add_position(custom_positions)
print("5-atom custom register positions (um):")
for i, pos in enumerate(custom_positions):
print(f" Atom {i}: ({pos[0]:.2f}, {pos[1]:.2f})")
The register geometry defines which atoms are within the Rydberg blockade radius of each other and therefore which pairs interact. Atoms closer than the blockade radius are blockaded; atoms farther apart evolve independently.
Building a Pulse Sequence: Rabi Oscillation
A Rabi oscillation is the simplest quantum dynamics: drive a two-level system with a resonant laser and watch it oscillate between |0> and |1>. For a single atom with Omega constant and Delta = 0:
P(|1>) = sin^2(Omega * t / 2)
In Bloqade, you build the pulse sequence by specifying the time dependence of Omega(t) and Delta(t):
from bloqade import start
import numpy as np
import matplotlib.pyplot as plt
# Single atom at the origin
program = (
start
.add_position((0, 0))
.rydberg.rabi.amplitude
.uniform_waveform(duration=4.0, value=np.pi / 2) # constant Omega = pi/2 MHz for 4 us
.detuning
.uniform_waveform(duration=4.0, value=0.0) # zero detuning (resonant)
)
# Run on the built-in emulator (exact Schrodinger equation integration)
emulation_result = (
program
.bloqade.python()
.run(shots=1000, solver_name="dop853", interaction_picture=True)
)
# Extract Rydberg population at end of pulse
report = emulation_result.report()
rydberg_densities = report.rydberg_densities()
print(f"Rydberg population after pi pulse: {rydberg_densities[0]:.4f}")
print(f"Expected (pi/2 * 4 / pi = 2 full oscillations -> should be near 0): ~0.0")
For a more informative sweep, vary the pulse duration to trace out the full Rabi oscillation curve:
from bloqade import start
import numpy as np
import matplotlib.pyplot as plt
durations = np.linspace(0.1, 3.0, 30) # microseconds
rydberg_pops = []
for duration in durations:
result = (
start
.add_position((0, 0))
.rydberg.rabi.amplitude
.uniform_waveform(duration=duration, value=np.pi) # Omega = pi MHz
.detuning
.uniform_waveform(duration=duration, value=0.0)
.bloqade.python()
.run(shots=2000, solver_name="dop853")
)
density = result.report().rydberg_densities()[0]
rydberg_pops.append(density)
# Theoretical Rabi oscillation
t = np.linspace(0.1, 3.0, 300)
omega = np.pi # MHz
theoretical = np.sin(omega * t / 2) ** 2
plt.figure(figsize=(9, 5))
plt.plot(t, theoretical, 'b-', linewidth=2, label='Theory: sin^2(Omega*t/2)')
plt.scatter(durations, rydberg_pops, color='red', s=60, zorder=5, label='Bloqade emulation')
plt.xlabel('Pulse Duration (us)')
plt.ylabel('Rydberg Population')
plt.title('Rabi Oscillation -- Single Neutral Atom')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('rabi_oscillation.png', dpi=150)
plt.show()
Maximum Independent Set: A Native Problem
One of the most natural problems for analog neutral atom computers is the Maximum Independent Set (MIS) problem. Given a graph G = (V, E), find the largest subset of vertices such that no two vertices in the subset share an edge.
This maps directly onto Rydberg atom physics. Place atoms at positions corresponding to graph vertices. Set the atom spacing so that connected vertices (edges) fall within the blockade radius, and disconnected vertices fall outside it. Then the blockade constraint (at most one atom per blockaded cluster can be excited) directly encodes the MIS constraint.
The ground state of the Rydberg Hamiltonian at positive detuning is the maximum independent set of the blockade graph.
from bloqade import start
import numpy as np
# Define a 5-vertex graph and its positions
# This is a "unit disk graph": edges exist between atoms within blockade radius
# Layout: a pentagon shape where non-adjacent vertices are beyond blockade radius
blockade_radius = 8.0 # micrometers
graph_radius = 5.0 # micrometers (pentagon circumradius, within blockade range)
positions = [
(graph_radius * np.cos(2 * np.pi * k / 5),
graph_radius * np.sin(2 * np.pi * k / 5))
for k in range(5)
]
# Verify which pairs are within blockade radius
print("Atom pair distances and blockade status:")
for i in range(5):
for j in range(i+1, 5):
xi, yi = positions[i]
xj, yj = positions[j]
dist = np.sqrt((xi-xj)**2 + (yi-yj)**2)
blocked = "BLOCKADED" if dist < blockade_radius else "free"
print(f" Atoms {i}-{j}: {dist:.2f} um -- {blocked}")
# MIS pulse: adiabatic sweep from negative to positive detuning
# Start with Delta << 0 (ground state = |00000>, no atoms excited)
# End with Delta >> 0 (ground state = MIS configuration)
total_time = 4.0 # microseconds
mis_program = (
start
.add_position(positions)
.rydberg.rabi.amplitude
# Gaussian envelope: ramp up and down smoothly
.piecewise_linear(
durations=[0.5, 3.0, 0.5],
values=[0.0, 4.0, 4.0, 0.0] # MHz
)
.detuning
# Linear sweep from -10 MHz to +10 MHz
.uniform_linear(start=-10.0, stop=10.0, duration=total_time)
)
# Run the emulation
mis_result = (
mis_program
.bloqade.python()
.run(shots=1000)
)
report = mis_result.report()
# Analyze the most common measurement outcomes
counts = report.counts()[0] # counts for first (only) geometry
sorted_outcomes = sorted(counts.items(), key=lambda x: x[1], reverse=True)
print("\nTop 5 measurement outcomes (1 = Rydberg = 'in set'):")
for bitstring, count in sorted_outcomes[:5]:
n_excited = sum(int(b) for b in bitstring)
print(f" |{bitstring}> : {count} shots, {n_excited} atoms in set")
# For a 5-cycle graph (C5), the MIS has size 2
# The two MIS solutions are alternating vertices: {0,2} and {1,3} etc.
# (C5 has maximum independent set size 2 by the independence number formula)
Running on QuEra Aquila via Amazon Braket
Submitting to the real QPU requires an Amazon Braket account and QuEra Aquila access:
from bloqade import start
from bloqade.analog_emulator import RydbergHamiltonianSolver
# Same program definition as above (mis_program)
# Switch the backend from bloqade.python() to braket.aquila()
aquila_result = (
mis_program
.braket.aquila()
.run_async(shots=100) # async because QPU jobs take minutes
)
# Save the task ID to retrieve results later
task_id = aquila_result.task_id()
print(f"QuEra Aquila task submitted: {task_id}")
print("Retrieve results with: aquila_result.fetch()")
# To retrieve results after the task completes:
# final_result = aquila_result.fetch()
# report = final_result.report()
Aquila jobs typically complete within 5 to 15 minutes including queue time. The QPU currently supports arrays of up to 256 atoms with positions specified to 0.1 um precision. Atoms must be separated by at least 4 um and at most 30 um.
Performance and Current Limitations
Neutral atom quantum computers have several distinctive characteristics that make them attractive for certain problem classes:
Variable connectivity. The atom positions define the interaction graph. You can encode arbitrary unit-disk graphs natively without SWAP overhead. This is ideal for combinatorial optimization, quantum simulation of lattice models, and graph problems.
Long coherence times. Hyperfine qubit coherence of 1 to 10 seconds is two to four orders of magnitude longer than superconducting qubits. This in principle allows much deeper circuits before decoherence dominates.
Scalability. Tweezer arrays have been demonstrated with 1000+ atoms in research settings. The bottleneck is not adding more atoms but achieving high single-site detection fidelity and gate fidelity at scale.
Current limitations. Atom loading is probabilistic: optical tweezers load single atoms from a MOT (magneto-optical trap) with roughly 50% probability per site. Software rearrangement fills gaps, but loading overhead adds latency. Analog mode restricts you to global pulses and Ising-like Hamiltonians; digital mode with local addressing is still maturing.
Bloqade makes neutral atom programming accessible without deep knowledge of AMO physics. The pulse-based abstraction maps naturally onto the hardware, and the emulator allows full development and testing before submitting to the QPU. For problems with graph structure (optimization, constraint satisfaction, quantum simulation of spin models), the Rydberg platform and Bloqade SDK are worth serious consideration.
Was this tutorial helpful?