Rabi oscillations with finite interactions¶
Scan the duration of a constant drive and compare two independent atoms with two interacting pairs. By the end, you should have a probability curve, the raw results used to draw it, and a short explanation of how geometry changes the dynamics.
Complete global drive, physical units and experimental design. Use the environment from installation. Run from the repository root. The classroom configuration makes 24 two-atom runs: three cases, eight durations, 32 integration steps and 256 shots per point, for 6,144 shots total. A development run on macOS with Python 3.11 took about 1.5 seconds including the reports; this is an observation, not a runtime guarantee.
Set the control experiment¶
All cases start in |00⟩. The global drive has Ω = 2π rad/us, zero detuning and zero phase. Durations range from 0.125 to 1 us. In the independent case, each atom has excitation probability p(t) = sin²(πt) and the double-excitation probability is p(t)². At t = 0.5 us, both atoms should be excited.
The other cases enable a finite interaction V = C6/r^6, with the illustrative value C6 = 312,500 rad*um^6/us. The cutoff is 12 um, so both distances remain inside it. At 5 um, V = 20 rad/us; at 10 um, V = 0.3125 rad/us. Doubling the distance reduces the interaction by 64. This coefficient was chosen for the exercise and is not an atomic-species calibration.
Every case explicitly uses the full state space. The interacting models retain |11⟩ with a finite energy shift; they do not remove it as a hard-blockade approximation would. Predict how far the 5 um curve will depart from the independent case, then compare your prediction with the actual result.
Run and open the report¶
python3 examples/learning/projects/rabi_interactions.py
Open artifacts/rabi-interactions/rabi-en.html in a browser. It contains single-atom and double-excitation probability plots; click a legend entry to hide a curve. The lines connect computed points and are visual guides, not extra simulation points. scan.json retains all 24 result objects, counts, probabilities, seeds, units and simulation options. Both language reports use these same runs.
These files were generated during this documentation build:
{
"artifacts": {
"data": "artifacts/rabi-interactions/scan.json",
"report_en": "artifacts/rabi-interactions/rabi-en.html",
"report_zh": "artifacts/rabi-interactions/rabi-zh.html"
},
"interaction_rad_per_us": {
"far": 0.3125,
"independent": 0.0,
"near": 20.0
},
"middle_points": [
{
"case": "independent",
"counts": {
"11": 256
},
"duration_us": 0.5,
"frequency_11": 1.0,
"interaction_rad_per_us": 0.0,
"probability_11": 1.0,
"probability_q0": 1.0,
"seed": 4704,
"spacing_um": 5.0
},
{
"case": "near",
"counts": {
"00": 90,
"01": 81,
"10": 73,
"11": 12
},
"duration_us": 0.5,
"frequency_11": 0.046875,
"interaction_rad_per_us": 20.0,
"probability_11": 0.04991117028410551,
"probability_q0": 0.3570432870543567,
"seed": 4712,
"spacing_um": 5.0
},
{
"case": "far",
"counts": {
"01": 1,
"11": 255
},
"duration_us": 0.5,
"frequency_11": 0.99609375,
"interaction_rad_per_us": 0.3125,
"probability_11": 0.9983828099961084,
"probability_q0": 0.9990007685341564,
"seed": 4720,
"spacing_um": 10.0
}
],
"points_per_case": 8,
"sdk_version": "1.0.8a",
"shots_per_point": 256,
"time_steps": 32,
"total_shots": 6144
}
At 0.5 us, the independent pair has P(11) = 1. With finite interaction, the 5 um result is about 0.04991; the 10 um result is about 0.99838. At that time, interaction suppresses double excitation strongly in the nearer pair. Do not turn this one comparison into a claim that every observable decreases monotonically with interaction strength or time.
The plotted values are simulated probabilities. frequency_11 comes from 256 samples and can differ; for a true probability near 0.05, its binomial standard error is about sqrt(0.05 × 0.95 / 256) = 0.014. A fixed seed helps repeat a run in one environment but does not make these sampled integers universal reference values.
Inspect the implementation¶
The script builds each program with the same drive, changes only the case and duration, and saves each result before making charts. enabled=False turns off the finite interaction in the independent case. It keeps blockade_mode="full", so disabling an interaction cannot silently replace it with a hard-blockade projection.
"""Scan pulse duration for independent atoms and two finite interaction strengths."""
from __future__ import annotations
import argparse
import json
from math import pi
from pathlib import Path
from typing import Any
from bokeh.embed import file_html
from bokeh.layouts import column
from bokeh.plotting import figure
from bokeh.resources import INLINE
import cascaqit
from cascaqit import (
AHSProgram,
AtomRegister,
LocalBackend,
SimulationOptions,
VanDerWaalsInteraction,
Waveform,
)
def experiment(
output_dir: Path, *, points: int = 8, time_steps: int = 32, shots: int = 256
) -> dict[str, Any]:
"""Retain each result and draw both language reports from the same rows."""
if not 2 <= points <= 256 or time_steps < 1 or shots < 1:
raise ValueError("Use 2..256 points, positive time steps and positive shots.")
output_dir.mkdir(parents=True, exist_ok=True)
omega, c6, cutoff, seed = 2 * pi, 312_500.0, 12.0, 4701
configurations = (
("independent", 5.0, False),
("near", 5.0, True),
("far", 10.0, True),
)
options = SimulationOptions(
method="state_vector",
blockade_mode="full",
dtype="complex128",
integrator="fixed_step_krylov",
max_steps=time_steps,
)
rows: list[dict[str, Any]] = []
raw: list[dict[str, Any]] = []
for label, spacing, enabled in configurations:
for index in range(points):
duration = (index + 1) / points
program = (
AHSProgram(
AtomRegister.line(count=2, spacing=spacing),
interaction=VanDerWaalsInteraction(
c6=c6,
cutoff_radius=cutoff,
enabled=enabled,
),
)
.drive(
rabi=Waveform.constant(omega, duration=duration),
detuning=Waveform.constant(0.0, duration=duration),
phase=0.0,
)
.measure()
)
result = (
LocalBackend(analog_time_steps=time_steps)
.run(
program,
shots=shots,
seed=seed + len(rows),
options=options,
)
.result()
)
if result.probabilities is None or sum(result.counts.values()) != shots:
raise RuntimeError("The scan did not return a complete distribution.")
probabilities = result.probabilities
rows.append(
{
"case": label,
"spacing_um": spacing,
"duration_us": duration,
"interaction_rad_per_us": c6 / spacing**6 if enabled else 0.0,
"probability_q0": probabilities.get("10", 0)
+ probabilities.get("11", 0),
"probability_11": probabilities.get("11", 0),
"frequency_11": result.counts.get("11", 0) / shots,
"seed": seed + len(rows),
"counts": result.counts,
}
)
raw.append(result.to_dict())
artifacts = {"data": str(output_dir / "scan.json")}
labels = {
"en": (
"Pulse duration (us)",
"Excitation probability of atom 0",
"Double excitation",
("Independent", "5 um, finite interaction", "10 um, finite interaction"),
),
"zh": (
"脉冲时长 (us)",
"第 0 个原子的激发概率",
"双激发概率",
("独立原子", "5 um,有限相互作用", "10 um,有限相互作用"),
),
}
for language, (xlabel, first_title, second_title, legends) in labels.items():
plots = []
for field, title in (
("probability_q0", first_title),
("probability_11", second_title),
):
plot = figure(
title=title,
x_axis_label=xlabel,
y_axis_label="P",
width=880,
height=330,
y_range=(-0.03, 1.03),
)
for (label, _, _), color, legend in zip(
configurations, ("#2563eb", "#dc2626", "#15803d"), legends
):
subset = [row for row in rows if row["case"] == label]
xs, ys = [r["duration_us"] for r in subset], [r[field] for r in subset]
plot.line(xs, ys, color=color, legend_label=legend, line_width=2)
plot.scatter(xs, ys, color=color, size=6)
plot.legend.click_policy = "hide"
plots.append(plot)
path = output_dir / f"rabi-{language}.html"
path.write_text(file_html(column(*plots), INLINE, "Rabi"), encoding="utf-8")
artifacts[f"report_{language}"] = str(path)
payload = {
"sdk_version": cascaqit.__version__,
"omega_rad_per_us": omega,
"c6_rad_um6_per_us": c6,
"cutoff_um": cutoff,
"points_per_case": points,
"time_steps": time_steps,
"shots_per_point": shots,
"total_shots": len(rows) * shots,
"simulation_options": options.to_dict(),
"rows": rows,
"raw_results": raw,
"artifacts": artifacts,
}
Path(artifacts["data"]).write_text(
json.dumps(payload, indent=2) + "\n", encoding="utf-8"
)
return {
"sdk_version": cascaqit.__version__,
"points_per_case": points,
"time_steps": time_steps,
"shots_per_point": shots,
"total_shots": len(rows) * shots,
"interaction_rad_per_us": {
label: c6 / spacing**6 if enabled else 0.0
for label, spacing, enabled in configurations
},
"middle_points": [r for r in rows if r["duration_us"] == 0.5],
"artifacts": artifacts,
}
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--output-dir", type=Path, default=Path("artifacts/rabi-interactions")
)
parser.add_argument("--points", type=int, default=8)
parser.add_argument("--time-steps", type=int, default=32)
parser.add_argument("--shots", type=int, default=256)
args = parser.parse_args()
print(
json.dumps(
experiment(
args.output_dir,
points=args.points,
time_steps=args.time_steps,
shots=args.shots,
),
sort_keys=True,
)
)
if __name__ == "__main__":
main()
Submit a small experiment record¶
Keep the script revision, dependency versions, JSON and report together. Write one paragraph explaining the near/far comparison, then complete these checks:
- Derive the independent single-atom and double-excitation curves. Compare them with every independent row in
scan.json, not just the largest peak. - Repeat with
--time-steps 64 --output-dir artifacts/rabi-refined. Compare probabilities at identical durations. Explain what this comparison says about numerical error and what it leaves untested. - Repeat with
--shots 1024 --output-dir artifacts/rabi-more-shots. Which plotted values should stay fixed, and which sampled frequencies may change? - In a local copy, reduce the cutoff to
7 um. Predict which interacting pair will lose its finite interaction term.
Check your reasoning
The one-atom propagator is exp(−i Ωt X/2), giving sin²(Ωt/2); independent probabilities multiply. This Hamiltonian is constant in time, so the step comparison may already agree near floating-point precision. It is a consistency check, not proof that 32 steps suffice for arbitrary changing controls. More shots change frequencies but do not change ideal probabilities. A 7 um cutoff excludes the 10 um pair while keeping the 5 um pair; the full-space setting stays explicit in both.
For a research extension, increase resolution with --points 32 --time-steps 128, introduce a detuning scan in a copy, and repeat with several independently recorded seeds. Test convergence after adding time-dependent controls. Report error bars for sampled quantities and retain the unselected runs. This project does not include decoherence or device imperfections; add a specified noise model before making claims about laboratory performance.
The MIS route comparison uses a different experimental question: one optimization problem represented by three execution routes.