Tutorial: Programming with QoolQit
This notebook explains how QoolQit can be used to build quantum programs by employing the dimensionless Hamiltonian framework described in Programming a neutral atom QPU.
Programming with QoolQit is based on five core steps:
- Defining a Register — where qubits live (their geometry).
- Defining a Drive — how you control the system over time (waveforms).
- Building a QuantumProgram — the pairing of Register + Drive.
- Compiling the QuantumProgram — mapping the abstract program onto a specific device model.
- Executing the compiled program— running the compiled sequence and collecting results.
1. Register
Section titled “1. Register”A register defines the positions of qubits in your program. In neutral-atom / Rydberg analog models, geometry directly determines how strongly qubits interact.
Tip — use unit spacing: the closest pair of qubits should be at distance 1. This convention normalises interaction strengths and keeps drive parameters interpretable across different layouts.
from qoolqit import Register
# Create a register from coordinates (dimensionless units)register = Register.from_coordinates([ (0, 0), (1, 0), (0.5, 0.866)])
# Or use built-in graph patternsfrom qoolqit import DataGraph
graph = DataGraph.square(m=2, n=2)register = Register.from_graph(graph) # 2x2 square latticeregister.draw()For problem embedding, QoolQit also provides layout embedders that map graph structure onto qubit positions:
from qoolqit import DataGraph, Registerfrom qoolqit.embedding import SpringLayoutEmbedder
graph = DataGraph.random_er(n=5, p=0.3, seed=3)embedded_graph = SpringLayoutEmbedder().embed(graph)register = Register.from_graph(embedded_graph)register.draw()📖 See Registers for all available register creation methods and options.
📖 See Problem Embedding for embedding data and problems into the Rydberg analog model.
2. Waveforms and Drives
Section titled “2. Waveforms and Drives”A drive is the time-dependent control applied to the system, built from one or more waveforms — for example, amplitude and detuning as functions of time .
Waveform durations are dimensionless: measures duration relative to an interaction timescale. In an interacting many-body system, this gives a natural physical interpretation in terms of the buildup and propagation of correlations.
| Regime | Condition | Physical meaning |
|---|---|---|
| Short time | $\tilde{t} \ll 1$ | Too short for interactions to strongly reshape the state |
| Long time | $\tilde{t} \sim n$ | Correlations may have propagated across a distance of order $n$ lattice spacings |
from qoolqit import Drivefrom qoolqit.waveforms import Constant, Interpolated, Ramp
# Interpolated waveform: smooth curve through specified valuesomega_wf = Interpolated(duration=10, values=[0, 1, 0])
# Constant waveformdelta_wf = Constant(duration=10, value=-2)
# Ramp waveformdelta_wf = Ramp(duration=10, initial_value=-2, final_value=2)
# Combine into a Drivedrive = Drive( amplitude=omega_wf, detuning=delta_wf)
drive.draw()📖 See Waveforms for all waveform types and options.
📖 See Drive Hamiltonian for details on combining waveforms into drives.
3. QuantumProgram
Section titled “3. QuantumProgram”A QuantumProgram pairs a register (layout and interactions) with a drive (time-dependent controls). The initial state is always , so every program describes how the drive and interactions evolve this fixed initial state.
from qoolqit import QuantumProgram
program = QuantumProgram( register=register, drive=drive)📖 See Quantum Programs for more details.
4. Compilation
Section titled “4. Compilation”Compilation translates your abstract program into something a given device (or emulator) can actually run, applying device-specific constraints such as maximum duration, amplitude and detuning bounds, discretization rules, and other hardware limits.
from qoolqit import AnalogDevice
device = AnalogDevice()program.compile_to(device)5. Execution
Section titled “5. Execution”Once compiled, run the sequence and retrieve results. The three stages map cleanly onto three concepts:
- Program — what you want to run (abstract physics).
- Sequence — what you will run (device-compatible instructions).
- Result — what you observed (samples, probabilities, observables, etc.).
from qoolqit.execution import LocalEmulator
emulator = LocalEmulator()results = emulator.run(program)Example: Rydberg Blockade Demonstration
Section titled “Example: Rydberg Blockade Demonstration”The blockade regime occurs when interactions prevent nearby atoms from being simultaneously excited. In the dimensionless framework this is straightforward to set up:
- Place two qubits at unit distance $\Rightarrow \tilde{J} = 1$.
- Blockade condition: $\tilde{\Omega} \ll 1$ (drive is weak compared to interactions).
We will compare two programs:
| Case | $\tilde{\Omega}$ | Regime | Expected behaviour |
|---|---|---|---|
| Blockade | $0.3$ | $\tilde{\Omega} \ll \tilde{J}$ | Double excitation $\|11\rangle $ suppressed |
| Non-blockade | $2.0$ | $\tilde{\Omega} \gg \tilde{J}$ | Qubits behave more independently; $\|11\rangle $ accessible |
import numpy as np
from qoolqit import Drive, QuantumProgram, Registerfrom qoolqit.devices import AnalogDevicefrom qoolqit.execution import BitStrings, EmulationConfig, LocalEmulatorfrom qoolqit.waveforms import Constant
# Two qubits at unit distance => maximum interaction J = 1register = Register.from_coordinates([(0, 0), (1, 0)])
duration = 10
# Blockade regime: Omega << J => double excitation is suppresseddrive_blockade = Drive( amplitude=Constant(duration, 0.3), # Omega = 0.3 << 1 detuning=Constant(duration, 0.0))
# Non-blockade regime: Omega >> J => drive dominates, both atoms can be exciteddrive_no_blockade = Drive( amplitude=Constant(duration, 2.0), # Omega = 2.0 >> 1 detuning=Constant(duration, 0.0))# Build and compile programsprogram_blockade = QuantumProgram(register, drive_blockade)program_no_blockade = QuantumProgram(register, drive_no_blockade)
device = AnalogDevice()program_blockade.compile_to(device)program_no_blockade.compile_to(device)# Configure emulation: sample bitstrings at 81 evaluation timeseval_times = np.linspace(0.0, 1.0, 81)bitstrings = BitStrings(evaluation_times=list(eval_times), num_shots=1000)configuration = EmulationConfig(observables=[bitstrings])
emulator = LocalEmulator(emulation_config=configuration)
results_blockade = emulator.run(program_blockade)results_no_blockade = emulator.run(program_no_blockade)import matplotlib.pyplot as plt
times=results_blockade[0].get_result_times(bitstrings)occupation=[results_blockade[0].get_result(bitstrings.tag,time=t)["11"]/1000 for k,t in enumerate(times)]plt.plot(times,occupation, label="Blockade", color="navy")times=results_no_blockade[0].get_result_times(bitstrings)occupation=[results_no_blockade[0].get_result(bitstrings.tag,time=t)["11"]/1000 for k,t in enumerate(times)]plt.plot(times,occupation, label="No Blockade", color="crimson")plt.xlabel(r"$t$",fontsize=22)plt.ylabel(r"$P_{rr}$",fontsize=22)plt.xticks(fontsize=18)plt.yticks(fontsize=18)plt.legend(fontsize=16)plt.show()What to expect
Section titled “What to expect”- Blockade case ($\tilde{\Omega} = 0.3$): the strong interaction heavily penalises the $|11\rangle$ state. You should observe a suppressed probability of both qubits being excited simultaneously.
- Non-blockade case ($\tilde{\Omega} = 2.0$): the drive dominates and interactions are comparatively weak, so the two qubits behave more independently and $|11\rangle$ becomes much more accessible.
📖 See Solving a Basic QUBO Problem for another application example.
