Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
8101e6d
docs(rfcs): stop scoping design facts to one lab's chip
ch-ahindura Aug 18, 2026
d707866
docs: add the three RFCs missing from the nav
ch-ahindura Aug 18, 2026
2107b0e
docs(rfcs): add RFC 0009 — parallel calibration
ch-ahindura Aug 18, 2026
8487789
fix(qpi-driver,qpi-ui): draw the node a calibration is on
ch-ahindura Aug 18, 2026
6267b58
feat(qpi-driver): group the targets that can be calibrated at once
ch-ahindura Aug 18, 2026
7dcabd2
feat(qpi-driver): measure a group of targets in one schedule
ch-ahindura Aug 18, 2026
9ad2309
feat(qpi-driver): fuse the two readout benchmarks
ch-ahindura Aug 18, 2026
9edace6
fix(qpi-driver): the fusion tests needed the sim extra to count channels
ch-ahindura Aug 18, 2026
b2d8ec2
refactor(qpi-driver): give a target its own sweep object
ch-ahindura Aug 18, 2026
c30679d
fix(qpi-driver): fit each target against its own sweep
ch-ahindura Aug 18, 2026
85de160
feat(qpi-driver): escalate a group, widening only what refused
ch-ahindura Aug 18, 2026
96fc3cd
test(qpi-driver): hold the sweep object to the coverage floor
ch-ahindura Aug 18, 2026
dcea8be
docs(rfcs): record how far RFC 0009 is implemented
ch-ahindura Aug 18, 2026
7e781e7
fix(qpi-driver): do not group when nothing says what couples
ch-ahindura Aug 18, 2026
3f0fce7
feat(qpi-driver): split a group whose sweep differs per target
ch-ahindura Aug 18, 2026
4763a01
feat(qpi-driver): fuse rabi and drag
ch-ahindura Aug 18, 2026
5cc1dfd
fix(qpi-driver): move the last per-target values onto the sweep
ch-ahindura Aug 18, 2026
4df68a5
feat(qpi-driver): fuse the two fine-amplitude ladders
ch-ahindura Aug 18, 2026
6f18927
feat(qpi-driver): benchmark a group with simultaneous RB
ch-ahindura Aug 19, 2026
b25f8b0
fix(qpi-driver): keep a routine's own loop when its schedule can be f…
ch-ahindura Aug 19, 2026
d4df5fc
test(qpi-driver): return the fusion helpers to full coverage
ch-ahindura Aug 19, 2026
8c74897
docs(rfcs): correct RFC 0009's own count of what measures itself
ch-ahindura Aug 19, 2026
43ee5e8
feat(qpi-driver): calibrate a group of edges at once
ch-ahindura Aug 19, 2026
5aa1c6b
feat(qpi-driver): measure what running in company costs
ch-ahindura Aug 19, 2026
8765d2b
feat(qpi-driver): fuse the remaining CZ sweeps
ch-ahindura Aug 19, 2026
0a58e33
docs(rfcs): close phases 5 and 6 in RFC 0009
ch-ahindura Aug 19, 2026
d4b7096
feat(qpi-driver): sweep a group of resonators at once
ch-ahindura Aug 19, 2026
c02fef7
feat(qpi-driver): fuse the readout operating points and discrimination
ch-ahindura Aug 19, 2026
97ee306
feat(qpi-driver): fuse the EF amplitude sweeps, and align their raw p…
ch-ahindura Aug 19, 2026
77f6515
feat(qpi-driver): fuse the rest of the EF chain
ch-ahindura Aug 19, 2026
2780ef1
feat(qpi-driver): fuse the traces and 2-D sweeps, and keep their chun…
ch-ahindura Aug 19, 2026
592d2ff
feat(qpi-driver): fuse the last window sweep, and close RFC 0009
ch-ahindura Aug 19, 2026
39373f3
docs(rfcs): qubit_spectroscopy is unconverted, not unconvertible
ch-ahindura Aug 19, 2026
c28365a
feat(qpi-driver): search a group of qubits at once
ch-ahindura Aug 19, 2026
b9c82c1
feat(qpi-driver): refine a group of qubits, and measure what grouping…
ch-ahindura Aug 19, 2026
109f118
docs(rfcs): record the measured saving, and both wrong claims
ch-ahindura Aug 19, 2026
abcacf3
feat(qpi-driver): sweep a group of coupler biases, and drop the last …
ch-ahindura Aug 19, 2026
30cc457
docs: cut the changelog back to what a release changed
ch-ahindura Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ on:
branches: [ main ]
tags: [ 'v*' ]
pull_request:
# `bench-parallel` runs only from here. It walks the whole graph twice and takes
# minutes, which is not something every push should pay for.
workflow_dispatch:
inputs:
benchmark:
description: 'Also measure what grouping saves (make bench-parallel)'
type: boolean
default: false

concurrency:
# cancel any previous run on this branch or tag that is still in progress
Expand Down Expand Up @@ -150,6 +158,16 @@ jobs:
# -------------------------------------------------------------------------
# qpi-client / Python SDK
# -------------------------------------------------------------------------
benchmark-parallel-calibration:
# Manual only, and only when asked for: see `workflow_dispatch.inputs.benchmark`.
if: github.event_name == 'workflow_dispatch' && inputs.benchmark
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- name: Measure what grouping saves
run: make bench-parallel

lint-and-test-py-client:
runs-on: ubuntu-latest
strategy:
Expand Down
46 changes: 45 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,50 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project follows versions of format `{year}.{month}.{patch_number}`.

## [Unreleased]

### Added

- `qpi-driver/py`: `parallel` in `calibration.yml` groups a routine's targets into sets that
can be measured at once, coloured from the coupling graph — `qubit_spacing`,
`edge_spacing`, `max_group`, `exclude`, or explicit `groups`. Off unless the file says so,
and a walk with no readable coupling graph runs one target at a time rather than guessing.
- `qpi-driver/py`: every routine in the graph can measure a group in one schedule, so a
group costs one arm-and-wait cycle instead of one per target. Each target reads its own
acquisition channel, and a group whose instruments cannot play it at once — readout clocks
outside one LO band, amplitudes that would clip, more clocks than sequencers — is split,
with the measured figure and the ceiling in the message.
- `qpi-driver/py`: `parallel.measure_penalty` benchmarks each target alone as well as in
company and reports `parallel_penalty` per target — the fidelity the group cost it, which
is what a tighter `qubit_spacing` has to be earned with. Off by default: it doubles what
the benchmarks cost.
- `qpi-driver/py`: `make bench-parallel` reports what grouping saves on a 3-qubit,
2-coupler chain — 52 acquisitions sequentially against 37 grouped. Skipped unless
`QPI_BENCH=1`, so a normal run does not pay for it.
- `docs`: RFC 0009 — Parallel Calibration. Why a group is a colouring of the coupling graph
rather than a hand-written list, why concurrent submission to one cluster cannot work, and
what licenses a tighter spacing.

### Changed

- `qpi-driver/py`: a two-qubit routine resets and excites both of an edge's qubits at the
same time rather than one after the other, which halves the reset every CZ sweep waits
through. This applies to a single edge too, not only to a group.

### Fixed

- `qpi-driver/py`, `qpi-ui`: the calibration graph draws the node a walk is on. A progress
event only fired after a target finished, so a single-target node went straight from
`pending` to `done` and the `running` style was unreachable; a node whose every target was
blocked stayed `pending` for the whole run.
- `qpi-driver/py`: a routine's swept setpoints belong to the target rather than to the
routine. They were kept on the routine and read back in `analyse`, so a routine measuring
several targets in one schedule would have fitted every one against whichever target built
last — a plausible curve against the wrong axis, not an error.
- `qpi-driver/py`: check schedules are compiled once per scheduler rather than once against
both. The only such compile in the suite needed both installed, so under the per-extra CI
matrix it ran nowhere.

## [0.4.2] - 2026-08-16

### Added
Expand Down Expand Up @@ -359,7 +403,7 @@ and this project follows versions of format `{year}.{month}.{patch_number}`.
with `AttributeError: 'SimulatedBackend' object has no attribute 'last_allowance_s'`.
- `qpi-driver/py`: a quantify tuner or executor resets the cluster when it opens one.
Sequencer offsets, NCO frequencies and `sync_en` survive a reconnect, so the driver
inherited whatever the last process left emitting — which held this chip's qubit in a
inherited whatever the last process left emitting — which held a qubit in a
mixture that made X the identity, and deadlocked `wait_sync` before that.
- `qpi-driver/py`: a quantify tuner or executor stops the cluster after every run,
including a failed one. Only `stop` clears `sync_en` on the modules a schedule did
Expand Down
21 changes: 17 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ DOCS_SITE_VENV := bin/.docs-site-venv
DOCS_SITE_OUT := bin/.docs-site

# The framework modules the coverage floor applies to: the SDK, the CLI, the device
# registry and its options, and the executors that need no hardware. Everything else
# is reported but not gated — see cov-py.
PY_COV_INCLUDE := qpi_driver/cli.py,qpi_driver/sdk.py,qpi_driver/events.py,qpi_driver/paths.py,qpi_driver/options.py,qpi_driver/builtins/*.py,qpi_driver/executors/__init__.py,qpi_driver/executors/base/*.py,qpi_driver/executors/mock/*.py
# registry and its options, the executors that need no hardware, and the calibration
# grouping and fusion. The last two qualify where the routines do not: they are pure
# functions over a config and a dataset, with no instrument behind them. Everything else is reported but not gated —
# see cov-py.
PY_COV_INCLUDE := qpi_driver/cli.py,qpi_driver/sdk.py,qpi_driver/events.py,qpi_driver/paths.py,qpi_driver/options.py,qpi_driver/builtins/*.py,qpi_driver/executors/__init__.py,qpi_driver/executors/base/*.py,qpi_driver/executors/mock/*.py,qpi_driver/tuners/base/grouping.py,qpi_driver/tuners/base/fusion.py,qpi_driver/tuners/base/sweep.py
PY_COV_MIN := 96

# `uv sync` reinstalls qblox_instruments, and macOS strips the code signature
Expand Down Expand Up @@ -71,7 +73,7 @@ serve-docs:
# ---------------------------------------------------------------------------
# Test targets
# ---------------------------------------------------------------------------
.PHONY: test test-docs test-docs-static test-docs-snippets test-docs-example test-docs-site \
.PHONY: test bench-parallel test-docs test-docs-static test-docs-snippets test-docs-example test-docs-site \
test-go test-py test-py-driver \
test-py-cli test-py-sim test-py-loop \
test-dashboard test-js-client test-go-client test-py-client \
Expand Down Expand Up @@ -187,6 +189,17 @@ test-py-sim:
qpi-driver/py/tests/test_physics_simulation.py \
qpi-driver/py/tests/test_calibration_e2e.py

# Not part of `test-py`: it walks the whole graph twice and takes minutes, so it is a
# manual trigger rather than a pre-commit check. The number it reports is *acquisitions* —
# one arm-and-wait cycle each — since a fused schedule's pulses are one target's and the
# sequencers play concurrently. See tests/test_parallel_savings.py.
bench-parallel:
@echo "Measuring what grouping saves on a 3-qubit, 2-coupler chain..."
$(UV) sync --project qpi-driver/py --extra sim --dev
$(RESIGN_Q1ASM)
QPI_BENCH=1 $(UV) run --no-sync --project qpi-driver/py pytest -s -v \
qpi-driver/py/tests/test_parallel_savings.py

test-py-loop:
@echo "Running the calibrate/process loop against the $(EXECUTOR) simulated chip..."
$(UV) sync --project qpi-driver/py --extra $(EXECUTOR) --extra sim --dev
Expand Down
2 changes: 1 addition & 1 deletion docs/rfcs/0005-calibration-graph-completion.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ came after the machinery. §14 records where the result diverged from §6 and §
mechanism every calibration routine already uses to sweep a readout, and the same
one the EF subspace's `ro2`/`ro_3st_opt` will need.

**What it is worth on this chip, honestly.** The frequency optimum sits about
**What it is worth on the simulated chip, honestly.** The frequency optimum sits about
200 kHz off the resonance — a tenth of a linewidth — so the frequency axis
contributes almost nothing here, and the node earns its place through the
amplitude, where signal grows linearly with drive while punch-through only bends
Expand Down
20 changes: 10 additions & 10 deletions docs/rfcs/0007-calibration-without-priors.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ a search but is never required to make one possible.
| Decision | Resolution |
|---|---|
| New operation or event type? | **No.** `calibrate` carries this. Python driver only. |
| What a `RoutineConfig` means | **Changed, and this is the core of the RFC.** Today a sweep parameter is often load-bearing: omit it and the node cannot work on this chip. After this, every sweep has a derived default that works on any chip the hardware can address; config may only *narrow* a search to save time. A node that cannot run without an operator-supplied range is a bug. |
| What a `RoutineConfig` means | **Changed, and this is the core of the RFC.** Today a sweep parameter is often load-bearing: omit it and the node cannot work on a given chip. After this, every sweep has a derived default that works on any chip the hardware can address; config may only *narrow* a search to save time. A node that cannot run without an operator-supplied range is a bug. |
| Where a bound comes from | Hardware config for instrument limits, upstream measurements for physical ones, escalation for the rest. New `tuners/base/limits.py`; the hardware config is already reachable from a routine via `device.hardware_config()`, as `has_flux_port` shows. |
| Guards as signals | **Changed.** The six "your window is wrong" guards added in August 2026 raise prose. They gain a structured form the caller can act on, so the same detection drives a retry instead of a failure. §6. |
| Routine interface | **Unchanged.** `measure` already absorbs a routine whose setpoints depend on an earlier acquisition — `qubit_spectroscopy` is the second implementor. No third interface. |
Expand Down Expand Up @@ -118,7 +118,7 @@ codebase and was applied once.
`f12_spectroscopy` centres on `f01 + anharmonicity_prior` and **ignores the config's
`f12` entirely** — a physical relationship beats an unmeasured field, and its docstring
says so. That is exactly the pattern this RFC generalises. It is also why that node was
the only one that ever found the August 2026 chip's qubit: it was the only node
the only one that ever found the qubit in the August 2026 bring-up: it was the only node
searching from physics rather than from a prior.

`conditional_phase` sweeps 0–360°. A phase has no range to guess at, so it never had
Expand Down Expand Up @@ -268,7 +268,7 @@ Two changes, both cheap:
and `fit_spectroscopy_power` already fits every row; today it picks the best row and
discards the rest. Requiring the chosen centre to agree with a second row to within a
linewidth costs nothing, since the data is already acquired, and noise does not
reproduce across powers. On the August 2026 chip this would have refused run one rather
reproduce across powers. In the August 2026 bring-up this would have refused run one rather
than run six: its three rows fitted 782.7 kHz, 8.5 kHz and 28 kHz, which no real line
does.

Expand Down Expand Up @@ -412,8 +412,8 @@ after the two classes that need no loop at all.
register budget, not for a better coefficient.

`drag` is deliberately untouched: §5 proposed centring it on the measured
anharmonicity, and nothing measured says the symmetric sweep is wrong. Its failure on
the August 2026 chip was contrast, not placement.
anharmonicity, and nothing measured says the symmetric sweep is wrong. Its failure in
the August 2026 bring-up was contrast, not placement.
5. **Escalation — done.** `OutOfRange` carries the axis and the direction; `escalating`
follows it, bounded at three attempts, and leaves an operator who named the axis alone.
Two directions turned out to be needed rather than one: a flat *decay* wants a longer
Expand All @@ -431,7 +431,7 @@ pulled into scope — the accept side of the guards is §6.2.

**A prior is still indistinguishable from a measurement.** After this RFC the driver finds
the qubit wherever it is, but nothing says whether `clock_freqs.f01` was measured by this
driver or typed in from a design document. The August 2026 chip carried
driver or typed in from a design document. The August 2026 bring-up carried
`f01: 4735509751.238763` — nine significant figures, and the line was never there.

Three things here want that distinction: §2's definition of a prior, §11's "no
Expand Down Expand Up @@ -461,7 +461,7 @@ not an index over one.
## 11. Skipping what cannot succeed

A node whose prerequisite was never produced cannot measure anything, and running it
anyway is how one failure became six. The August 2026 chip is the worked example:
anyway is how one failure became six. The August 2026 bring-up is the worked example:
`qubit_spectroscopy` failed, and `rabi`, `resonator_spectroscopy_excited`,
`readout_discrimination`, `allxy`, `drag` and `readout_fidelity` all then measured a
qubit still in `|0⟩` and reported confident numbers from its noise. Six failures with
Expand All @@ -480,7 +480,7 @@ plainly wrong. Twelve of the thirty-three nodes write nothing at all, so nothing
depend on their output, and some are still depended on in the walk order.

**Disabled is not failed.** `qubit_spectroscopy` depends on `resonator_punchout`, which
is switched off on the August 2026 chip because its amplitude grid never reaches
was switched off in the August 2026 bring-up because its amplitude grid never reaches
punch-through, which phase 3 fixes (§12). `time_of_flight` is off too, and under naive
propagation disabling either would skip the entire graph beneath it — which is to say,
everything. That both are off *because* of range bugs this RFC fixes does not help: the
Expand Down Expand Up @@ -542,8 +542,8 @@ and `drag` can legitimately run — as can `allxy`, `fine_amplitude`, `rb` and
produced by design" from "producer switched off". Two read paths —
`measure.integration_time` and `r12.ef_duration` — have no producer anywhere in the
graph and are supplied by hand on every chip, so a sole-producer rule fires on them
every run; and the August 2026 chip disables `time_of_flight` while its
`measure.acq_delay` is a perfectly good hand-set 200 ns. Nothing is lost by waiting:
every run; and the August 2026 bring-up disabled `time_of_flight` while its
`measure.acq_delay` was a perfectly good hand-set 200 ns. Nothing is lost by waiting:
the parameter view below already declines to block on either case.
**Reinstated by RFC 0008 as a report, not an error.** Provenance splits the three cases
the rule could not: a prior a routine in this run will measure, a prior whose producer is
Expand Down
4 changes: 2 additions & 2 deletions docs/rfcs/0008-parameter-provenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ driver measured, and a value somebody typed in.

That is all "provenance" means here; §2 says it at more length.

The August 2026 chip carried `clock_freqs.f01: 4735509751.238763`. Nine significant
The August 2026 bring-up carried `clock_freqs.f01: 4735509751.238763`. Nine significant
figures, so it reads as a measurement, and the qubit was 302 MHz away; the line had
never been there. Six calibration runs were spent on the consequences. Precision is not
provenance, and a file that cannot say which it is holding forces every reader, human or
Expand Down Expand Up @@ -142,7 +142,7 @@ Not *how old* the measurement is — see §8.
**Corrected while implementing phase 4: this must not relax the blocking rule.** The
tempting reading is that a node blocked because its input failed *this* run should run
anyway when an earlier run measured that input — the device does hold a real number. It is
wrong, and the August 2026 chip is the counterexample: a failure to *measure* f01 is
wrong, and the August 2026 bring-up is the counterexample: a failure to *measure* f01 is
evidence against whatever f01 the file holds, because the usual reason spectroscopy finds
no line is that the qubit is not where the file says. Running the six nodes behind it
against last week's value fits the same noise, whatever the value's pedigree. So a failed
Expand Down
Loading
Loading