Hotfix/clean up the single-qubit DAG for calibration driver - #47
Merged
Conversation
…ify run A calibration was failing every node with `TimeoutError: Sequencer 0 did not stop in timeout period of N minutes`, at any `routine_timeout_s` — 15 minutes failed the same way 5 did. The schedule was not slow: `resonator_spectroscopy` on q0 compiles to 10.6s of pulses, and the longest routine in the graph is a 58.5s punchout. `ClusterComponent.stop()` is the only thing that clears `sync_en` across all modules; `prepare()` reaches only the modules named in the current program. Nothing called `stop()`. So a node that used module6 for `q0:mw` left that sequencer in the cluster's SYNQ network, and the next node — touching only module20 — armed a sequencer that blocked on `wait_sync` waiting for a sequencer that would never arrive. quantify says as much on `disable_sync`: "Prevent hanging on next run if instrument is not used." Both reference pipelines (tergite-tuner, tergite-autocalibration) call `lab_ic.stop()` after retrieving. `sync_en` is instrument state, so once poisoned it survived driver restarts, which is why even the first node failed. Stopping goes in a `finally`: the run that most needs it is the one that failed. Four changes rather than one because they rewrite the same `run()` body and depend on each other's helpers: - `stop()` after every run, in the tuner and in the executor's circuit path, which has the same gap. `SimulatedCoordinator` gains a no-op `stop` so the callers need not special-case it. - A timeout now names the module, sequencer, state and flags. qblox-instruments raises with a bare sequencer index, so an operator could not tell which of twelve modules had hung; probed against a dummy cluster with one sequencer deliberately left in the sync network, it reports exactly that sequencer. - A schedule whose pulses outlast `routine_timeout_s` raises its own wait instead of failing. That ceiling exists to bound being *stuck*; killing a 59s punchout under a 30s ceiling failed it for being large. The allowance is rounded up to a whole minute plus one, because quantify floors the wait to minutes — passing 130s exactly would stop 10s short — and it is recorded so the DAG's own elapsed-time check judges the routine by what was allowed rather than discarding data it waited for. It also warns when it overrides the configured value. - `_cluster()` looked up components by iterating `components()`, which holds component *names*, so `getattr(name, "instrument")` found nothing and it always returned None — `coupler_anticrossing` could not open a bias source inside the cluster.
`ramsey` never compiled. Its default sweep is 41 points from 4 ns to 10 us, which steps 249.9 ns, and the schedule died with "Attempting to use a time value of 404117.89999999997 ns. Please ensure that the durations of operations and wait times between operations are multiples of 1 ns." `grid_duration` exists for exactly this and `ramsey_12` already applies it to its own delays; `ramsey` was the one that did not. Gridded where the setpoints are built rather than on the way into the schedule, because `analyse` fits against the same list and the fit should describe the delays that were played. Found by compiling all 33 routines against the chip's real device and hardware configs; `ramsey` was the only one failing on grid time.
…chip
`flux_spectroscopy` and `cz_chevron` both failed with
`KeyError: 'q0:fl was not found in the connectivity.'` on a chip whose flux
reaches the couplers instead of the qubits. That is not a wiring gap: it is the
architecture, and the graph already models both kinds. `CZParametrization`'s own
docstring draws the line — a DC-flux CZ is brought onto the |11>-|02> crossing
by amplitude, so its calibration is a chevron over amplitude and duration; a
parametric CZ is brought onto it by frequency, which `cz_spectroscopy` finds.
`CZSpectroscopy` and `CZParametrization` both guard with
`parametric_edge(...) is not None`. `CZChevron` had no `applies_to` at all, so
where its two counterparts correctly declined it went ahead and built a
schedule against a port the chip does not have. `FluxSpectroscopy`, which feeds
it and sweeps a qubit's own flux, had the same omission.
Both now ask `has_flux_port`, which reads the answer off the connectivity
graph — the same question the compiler was answering with a `KeyError` that
named neither the routine nor the reason. It returns True when the wiring
cannot be read, so an unrecognised config keeps failing as it did rather than
being silently skipped.
`conditional_phase` still runs on a parametric chip even though it declares
`depends_on = ("cz_chevron",)`: `depends_on` only orders the walk, and a
routine with no applicable targets is skipped rather than blocking its
dependents. Pinned with a test, since the alternative would have been to lose
the node.
The fixture chip carries both architectures — q0/q1 have their own `:fl` and
join through the DC-flux q0_q1, while q2 has none and joins q1 through the
parametric q1_q2 — so both branches are covered against a real QuantumDevice
rather than a fake shaped to the code.
All 33 routines now either compile or decline against the chip's real configs;
none fails at build time.
`rb` reported 0.9999970, then 0.9410470, then 0.5858176 on three consecutive runs of a chip whose readout sat a megahertz off its resonator. The survival data behind them was non-monotonic noise: [0, 1, 0.54, 0.68, 0.80, 0.49, 0.30] -> 0.9999970 [0, 0.53, 0.008, 0.34, 0.89, 1, 0.43] -> 0.9410470 [0, 0.50, 0.87, 0.25, 1, 0.28, 0.77] -> 0.5858176 Only `r` is bounded by the fit, deliberately — bounding the amplitude pins the reported fidelity near 0.98 for every chip better than 3% error per Clifford, which the rescaled-signal test exists to prevent. The cost is that on data with no decay in it the least-squares solution runs away instead: the first of those reached `A = 629` against a signal spanning one, an exponential degenerated into a straight line, with `r` no longer the depolarising parameter the fidelity formula reads it as. So the amplitude stays unbounded and the *result* is refused: the fitted decay must span at least three times the residual scatter it was drawn through. The three above sit at 0.8, 2.5 and 2.4. A real measurement clears it by an order of magnitude — the simulated chip at sixty circuits a depth sits near 25, and 0.2% noise near 130 — and a decay that has not reached its asymptote by the deepest sequence still passes, which is the case the fit is shaped around. Compared as a span rather than by the sign of the amplitude: the `rb` routine rescales its acquisition to [0, 1] without orienting it, so a chip whose readout brightens with excitation returns a rising survival, and that is a readout convention rather than a bad fit. `interleaved_rb` shares the fit and is covered too. A benchmark that raises is absent from the report rather than wrong in it, which is what the drift check needs — it was comparing these against a threshold.
`_require_resolved_line` caught a fit that was too *narrow* for its sweep and nothing else, so the opposite shape sailed through: a broad Lorentzian drawn through flat data. Measured on hardware, `qubit_spectroscopy` returned linewidth 1,534,405 Hz snr 1.32 f01 4,731,348,587 from a sweep whose points span 0.7% peak to peak. It cleared the width test by a factor of eleven, sat 5 MHz from the runs either side of it, and was written straight to f01 — which put `ramsey_12`'s detuning 1.5 MHz out and cost the run. That is the second time a noise fit has poisoned the device config: `rb` did the same with a fidelity. So the guard now also requires the fitted line to stand at least 3x above its own residual scatter. Three from the spread of what has been measured rather than from theory: the simulated chip returns 127 and lands within 2.5 kHz of the true f01; on hardware the one `qubit_spectroscopy` whose answer reproduced across runs came back at 3.55, and the two that did not came back at 1.56 — through a starved readout — and 1.32. The asymmetry sets the threshold more than the gap does: a refused fit leaves the last good frequency in place and says why, an accepted one overwrites it and breaks every node downstream. `resonator_spectroscopy` had no guard at all, which is the worse omission of the two: it is the root, and the frequency it writes is where every other node reads. A 72% dip confined to a single 400 kHz bin was fitted as a 2379 Hz linewidth at Q = 2.9 million, and the centre it wrote sat 47 kHz off the deepest sample it had actually taken. It is guarded now, and the width half of the check would have caught that one on its own. `fit_resonator_spectroscopy` and `fit_qubit_spectroscopy` forward the `snr` that `_fit_lorentzian` has always computed and they were dropping; `fit_spectroscopy_power` already selected on it. A fit that reports no snr is still judged on width alone, so nothing starts failing on absence. Verified against the scqubits suite as well as the fast one — the simulated chip's resonator and qubit spectroscopy both clear the floor, and its 8 pre-existing failures in test_calibration_e2e.py are unchanged.
…weeps it comes from `resonator_spectroscopy_excited` reports `0.5 * (excited - clock_freqs.readout)`. The subtrahend is not measured by the routine: it is whatever the device happens to hold, which `resonator_spectroscopy` writes and anything pinning the config can override. Pin the readout to a stale seed and the routine reports the distance to the seed rather than a dispersive shift — observed at 186 kHz on a chip whose ground and excited resonances, measured properly, agree to 173 Hz. Nothing in the report said which reference had been used, so the 186 kHz read as a physics result and contradicted the 385 Hz from the run before it. Both numbers were correct arithmetic on different references. So both excited sweeps now report `readout_frequency_ground` alongside the shift, and forward the fitted spectrum. The trace matters more than the centre here: when chi is a fraction of a linewidth, two overlaid curves show it and two fitted centres do not — which is the measurement still outstanding on this chip. Neither sweep was guarded. They returned linewidths of 510 kHz and 3067 Hz for the same resonator on consecutive runs, which is an unresolved line rather than two answers. Both call `require_resolved_line` now. That guard moves from `spectroscopy.py` to `base/routines.py`, beside `grid_duration` and `setpoints_of`, because a second module needs it — `resonator_spectroscopy_second_excited` lives in `ef.py`. It is public for the same reason. The simulated chip clears the floor on every newly guarded sweep: the scqubits suite is unchanged at 133 passed and its 8 pre-existing e2e failures.
`fine_amplitude` writes `amp180` — the amplitude every X pulse afterwards uses — and on a chip whose readout was not resolving the qubit it wrote one fitted from noise. Twice. The demodulated sweep is sin(n*delta), so the model bounds it at one. It is reached by dividing the raw signal by the measured |0>-|1> contrast, and when the two reference points come back nearly equal that divisor collapses: the quotient explodes and the slope through it is noise. The existing check refused only an exactly-zero contrast, which this is not — it is merely far too small. Measured, so the threshold is not a guess. Across the loop suite the simulated chip reaches 0.108 to 0.659, inside the bound. The two hardware runs reached 8.9 and 144.3, the second writing amp180 = 0.0339 and breaking every node after it. A ceiling of three sits 4.6x above the worst the simulator shows and refuses both. Deliberately not the span-over-scatter test used for RB and for spectroscopy: this is a refinement, so a small slope through a lot of scatter is what success looks like here, and that test would refuse a well-calibrated pulse. What is wrong in these runs is the normalisation, not the slope, so that is what is checked. `fine_amplitude_12` shares the fit and is covered.
A Qblox cluster keeps sequencer offsets, NCO frequencies, `sync_en` flags and uploaded programs across connections. This driver never reset one, so every run inherited whatever the previous process left in the modules. tergite-tuner resets on every non-recalibration start, on this same cluster, and reads this chip correctly. Two symptoms on one chip trace back to it. The first is the `wait_sync` deadlock fixed in ecaa5a8: a `sync_en` left set on a module the next schedule did not use, so every routine timed out at any `routine_timeout_s`. Calling `stop()` after each run clears it going forward; resetting on connection is the upstream half, and it also covers state left by something that is not this driver. The second is state preparation. Overlaying the resonator sweeps measured with and without an X pulse, the spectra differ by 1.1 sigma — an implied 4.4 kHz against an 11.5 kHz detection limit — while the same sweep after an additional ef pulse shifts by 114.7 kHz at 29.9 sigma. So the chip has ~60 kHz of dispersive pull per level and the readout resolves it easily, the ef pulse finds |1> population already there, and X does not change the populations at all. The only state X leaves unchanged is one with equal |0> and |1> populations, and a sequencer left emitting near f01 — the mw LO sits at 4.55 GHz, f01 at 4.736 GHz — saturates the qubit into exactly that. `reset.duration` cannot empty a level that is being refilled. It explains the whole pattern: rabi flat at 1.6e-4 contrast, qubit_spectroscopy finding no line, discrimination stuck at 0.52, chi(|1>) measured near zero four runs running — and, at the same time, ramsey_12 returning a fringe within 0.17% of its artificial detuning, because everything on the 1-2 ladder acts on population that is really present. Real clusters only: a dummy has no leftover state and need not support it. Done before any coupler bias is applied, so it cannot drop a current this process is holding — it does drop one held by a previous process, which is the intent.
`amp180` is the amplitude of every X pulse the chip plays, and `rabi` wrote it from a flat sweep. The chip's calibrated value is 0.5683; the six runs on record wrote 0.0134, 0.0233, 0.0272, 0.0319, 0.0133 and 0.0158 — between 17x and 43x too small. The X pulse in the most recent run rotates five degrees instead of 180, leaving 0.19% excited population. It is self-perpetuating, which is why it survived six runs. The first sweep was taken through a readout starved by `resonator_punchout` and fitted noise. From then on X was dead, so every later Rabi sweep was flat by construction, so it wrote another dead amplitude. Nothing failed; `require_in_range` passed because 0.0158 is inside the swept 0 to 0.5. Everything on the 0-1 side follows from it. `qubit_spectroscopy` finds no line because the drive it sweeps has nothing to say; `readout_discrimination` sits at 0.52 because |0> and X|0> are the same state to a fifth of a percent; `resonator_spectroscopy_excited` measures a 591 Hz dispersive shift where the second-excited sweep measures 126 kHz, because X moves no population while the ef pulse does. That last pair is what this fit had been hiding: it looked like a chip with no dispersive coupling, and it is a chip with no X gate. Guarded the same way as the RB decay, and calibrated the same way: the simulated chip's Rabi reaches a span-to-scatter of 211, and the two hardware sweeps that wrote a dead amplitude reached 1.41 and 1.78. Three refuses both with seventy times the margin on the simulator. The live device config on the instrument still holds 0.0158 and has to be restored to 0.5683 by hand — this only stops it happening again.
…noise `t1` returned 169 us from a monotonically *rising* curve inside a 100 us window, and `ramsey` returned a 173 kHz detuning from a sweep with no fringe in it — which it then wrote to f01. `require_in_range` passed both: it allows a time constant up to ten times the window, and 169 us is only 1.7x it. Both now require the fitted curve to stand at least 3x above its own residual scatter. Measured, as for the other three: on the simulated chip a Ramsey fringe reaches 194 and a T1 decay 117, and a T1 through 5% noise still reaches 20. This chip's t1 reached 0.47. This is the fourth fit needing the same test, so it moves into `core.py` as `require_resolved_curve` and `fit_rabi` and `fit_rb_decay` migrate onto it. Their messages were the only part that differed — what to do about a flat curve is not the same for a Rabi sweep as for an RB decay — so that is a parameter and the rest is shared. The evidence for the threshold now lives in one place, on `MIN_CURVE_TO_SCATTER`, rather than in two constants saying the same thing. Compared as a span, never by a parameter's sign or size: which way a feature points is a readout convention, and a small fitted parameter is sometimes exactly what success looks like — `fine_amplitude`'s slope is, which is why that fit is guarded on its normalisation instead and stays where it is. Both suites verified: the simulated chip clears all four guards, and its 8 pre-existing e2e failures are unchanged.
`resonator_spectroscopy_excited` writes no parameter, so it was the one node that could report a meaningless number indefinitely without breaking anything itself. It reported dispersive shifts of 385, 173, 735, 591 and 1954 Hz across six runs on a chip with a 375 kHz linewidth — 0.05% to 0.5% of it — and the calibration walked on each time. Everything downstream of an X gate then measured a qubit still in |0>: readout_discrimination returned 0.52 assignment fidelity from clouds 2.8e-4 apart, allxy an rms deviation of 3.86, drag a slope of -3.7e-5 through pure noise. Six nodes failing for what read as six unrelated reasons, none of them naming the cause. The floor is on the shift as a fraction of the linewidth rather than on either number, because that ratio is exactly what decides whether the ground and excited Lorentzians can be told apart: below a twentieth of a linewidth they overlap and no rotation or threshold recovers the states, so assignment fidelity is pinned near chance whatever the discriminator does. 5% leaves an order of magnitude either side of everything measured. The simulated chip returns 0.62 at the configured readout power, and the six hardware runs returned 0.0005 to 0.005.
…re the config says This is the node whose job is to measure f01, and it could only refine it. The sweep was +/-20 MHz around whatever `clock_freqs.f01` already held, so a config carrying a design value — or one measured at a different flux bias — put the qubit outside every window the node would ever look in, and it refused what it fitted there. Six runs of "no drive power in the sweep resolved a line" on a chip sitting 302 MHz below its design frequency, with nothing in the message suggesting the window was the problem, and the operator left to supply by hand the one number the node exists to produce. Now the configured value is treated as what it is, a prior: when no line turns up near it, the routine sweeps 600 MHz and re-runs the narrow sweep where the line actually is. Costs nothing on a chip that is where it says it is, since the wide pass runs only after the narrow one has failed. The split is what keeps it safe. The wide pass only chooses where to look — what gets written still comes from the narrow sweep and still has to clear `require_resolved_line`. So a coarse grid, on which every real line is narrower than one step, can never be the thing that sets f01. The wide pass takes the tallest bin rather than a fitted centre, and that was not the first attempt. Fitting a Lorentzian there is wrong twice: on a 2 MHz grid there is no lineshape to fit, and an optimiser handed 301 points of noise returned a confident centre at snr 3, which cleared MIN_LINE_SNR in the simulator and would have aimed the narrow sweep at an arbitrary frequency. A peak-to-scatter ratio against a median absolute deviation separates cleanly instead: 115-126 on the line, 2.5-2.9 off it, against a floor of 6. Half a step of precision is all a locate pass owes. 600 MHz rather than wider because the ceiling is hardware, not ambition: an RF module reaches +/-500 MHz either side of its LO, so a 1 GHz search is addressable only when the LO sits at the search centre. 600 leaves 200 MHz of slack for an LO placed off-centre; past 300 MHz the operator still sets `search_span`, and the refusal says so. `measure` therefore has a second implementor, for a second reason: setpoints that depend on an earlier acquisition, not DC state between acquisitions. The compile tests now name `coupler_anticrossing` instead of deriving the exclusion from `measures_itself`, which no longer implies there is no schedule to build.
Judging a routine against what its schedule was owed rather than against
`routine_timeout_s` meant the DAG started reading `last_allowance_s` off the
backend. `SchedulerBackend` declares it; the test double duck-types the backend
instead of subclassing it, and so did not.
Every node of a simulated calibration therefore died with
AttributeError: 'SimulatedBackend' object has no attribute 'last_allowance_s'
which took the whole of test_calibration_e2e.py down with it: eight tests,
including the one that walks the entire DAG over a simulated chip and writes the
device back. That is the test which says a chip can be calibrated end to end, and
it has been red since the allowance change went in — long enough that I had been
reading those eight as a pre-existing baseline and clearing changes against it.
They were not pre-existing.
The double's docstring argues that duck typing is the honest relationship because
routines only ever use the attributes. That holds for routines and not for the
DAG, which is not one — so the attribute is declared, with a note saying why it
is not optional.
test_calibration_e2e.py: 8 failed, 4 passed -> 12 passed. The scqubits suite is
green outright, 143 passed.
…hedules `routine_timeout_s` bounds a whole routine, and `allow` raises that bound when a single schedule's pulses need longer than it. A routine overriding `measure` runs more than one schedule under the same bound, so judging it by the last schedule's allowance is judging three waits by the third — which fails a routine that was inside its allowance at every step. That is precisely the failure `allow` exists to prevent, one level out. It bites now because `qubit_spectroscopy` widening to a search is three acquisitions: the narrow sweep that found nothing, the wide search, and the narrow sweep at what the search found. `coupler_anticrossing` has always been a loop of thirteen, and had the same exposure. The backend accumulates, the DAG resets the total per routine and target and reads it afterwards. Both paths in the DAG now compare against the total; on the single-schedule path it equals the last allowance, and is written that way so the two read alike.
The graph is complete and every node writes what it should, and it still cannot calibrate a chip nobody has calibrated before: almost every sweep is a window around a value the config already holds, which on a new chip is a guess. That inverts the purpose — not knowing the parameters is the reason the nodes exist. Three failures on the August 2026 chip, all the same shape. `qubit_spectroscopy` swept +/-20 MHz about a design value the qubit was 302 MHz from, six runs in a row. `rabi` sweeps amplitude to 0.5 where the element validates [0, 1], and that chip's own working calibration used 0.5683 — above the top of the sweep, in a place `require_in_range` tests the wrong direction to catch. And `readout_operating_point` sweeps +/-1 MHz over three points on a resonator whose 370 kHz linewidth was measured two nodes earlier and sitting in the report. The RFC sorts every sweep into hardware-bounded (the LO is readable from a routine today, so the addressable band is derivable), physics-bounded (the bound is an upstream measurement the node currently ignores), or escalation-bounded (time constants, which have no derivable ceiling). The first two need no loop. The third reuses the six guards added this month: each already detects that the window cannot support the number, so each becomes a retry signal rather than a verdict. Two things it deliberately does not fix, both recorded in §10: a prior is still indistinguishable from a measurement in the device file, and a wrong window can still be *accepted* rather than refused — measured while writing this, a 61-point window of pure noise cleared MIN_LINE_SNR. Escalation only triggers on a refusal, so hardening the accept side is the sequel, and is what would have caught this chip on run one rather than run six.
Running a node whose input does not exist is how one failure became six on the
August 2026 chip: `qubit_spectroscopy` failed, and six nodes behind it measured a
qubit still in |0> and reported confident numbers from its noise. Six causes on
screen, none of them the one that mattered. Before this month's guards those
nodes did not even fail — they wrote the noise to the device file.
So the idea is right, but the obvious mechanism is not. "If a node fails, skip
its dependents" breaks this graph three ways, and the graph itself says so:
- `depends_on` orders the walk, it is not a data dependency. `cz_chevron`
depends on `rb` and `flux_spectroscopy` and neither writes a parameter;
twelve of the thirty-three nodes write nothing at all.
- Disabled is not failed. `qubit_spectroscopy` depends on
`resonator_punchout`, switched off on that chip — naive propagation would
skip the entire graph beneath it.
- A refiner is not a producer. Seven parameters have two writers. `ramsey`
only refines the `f01` that `qubit_spectroscopy` produced, so a failed
`ramsey` would needlessly skip `drag`, `allxy`, `fine_amplitude`, `rb` and
`allxy_check`.
§11 therefore blocks on an unsatisfied *parameter* rather than a failed node,
via a `reads` declaration to complement the `updates` routines already carry. A
disabled sole producer becomes a config error raised before the walk instead of a
cascade during it, and blocked nodes are skipped with the blocker named rather
than auto-failed — fabricating six failures would be worse than the six
misleading ones, and would feed the drift check a history that never happened.
It lands as phase 0: independent of the rest, and it is what makes the failures
of the later phases legible. Two new open questions — whether `reads` is
declared or derived from `read_path`, and whether a skipped node keeps its stale
parameter — both of which turn on §10's missing provenance field.
Six comments, four adopted as written, one adopted in part, one that changed the shape of the RFC. All now recorded in §13 with the reasoning, since two of them were decisions rather than details. **The accept side moves into scope** (§6.2). This was the structural one. It had been deferred to a sequel, and that was wrong: escalation only fires when a guard refuses, so a guard that *accepts* noise does not report one bad number — it bypasses the entire RFC, because the widening never runs. It is the trigger condition, not a parallel concern. Two changes, both cheap, and the cheaper one is stronger: scale `require_resolved_line`'s floor with the point count, and require `qubit_spectroscopy`'s centre to reproduce across a second drive power, which needs no new acquisition — `fit_spectroscopy_power` already fits every row and discards all but one. On the August 2026 chip its three rows fitted 782.7 kHz, 8.5 kHz and 28 kHz, so this is what would have refused run one rather than run six. It lands as phase 1, before the derived ranges, because measuring their effect through a broken detector measures nothing. **Supplied ranges become suggestions** (§7), and this collapsed a distinction the draft was carrying for nothing: an operator's `span` is just escalation's first attempt, with the derived bound as the fallback. Not a second mechanism — the narrow-then-widen shape `qubit_spectroscopy` already has *is* this design. A good hint saves a sweep; a wrong one costs one wasted sweep, bounded and reported. **Chunking, derived `reads`, and keeping a skipped node's value** adopted as suggested (§5, §11). Chunking gets the one constraint that is easy to miss: the chunks must overlap by a linewidth, or a line on a boundary is resolved in neither and reads as a dead qubit. Two declined, both in part rather than outright: Rewriting `calibration.yml` when a hint proves wrong. That file is hand-authored reasoning, and remembering a search hint is a bug this codebase already has — `spec.amplitude` latched at 0.16 in the device file and `qubit_spectroscopy` then only ever tried multiples of it, which is why the August 2026 config names amplitudes outright. Report the range that worked instead; same information, operator's choice. Staging writes in a separate store until the run succeeds — and here the diagnosis matters more than the answer. The August 2026 corruption was not an early commit: `rabi` reported *success* while writing 0.0158, so a staging store would have committed it too. Deferring cannot help when the producing node believes it succeeded, and the value cannot be withheld from the walk anyway since `rabi` reads the `f01` `qubit_spectroscopy` just wrote. An all-or-nothing commit would also discard the good measurements a partly failed run did make, which on this chip is the difference between converging and not. The part worth keeping is finer than a store: commit per parameter, gated on provenance.
Three comments. Two adopted, and one that made the provenance problem much smaller than §10 had claimed. **Provenance needs no new format, and neither config file** (§10). The question was whether it could go in `calibration.yml` rather than the device file. Neither: the device file's schema is not ours — it deserialises into a `QuantumDevice` and quantify's models reject unknown keys, as `output_att` against the wrong config class demonstrated during this RFC's research — and writing to `calibration.yml` contradicts §7 one section later. But the data already exists. `RoutineResult` carries `routine_name`, `target`, `parameters`, `timestamp` and the `fit` a value came from, and the report is already emitted as an event payload. So every parameter this driver has written is recorded with when, by which node, and from what data. What is missing is a lookup, not a field — and that makes §2's "is this a prior?" exactly decidable with what ships today: is there a successful RoutineResult writing this parameter for this target? If nothing ever wrote `clock_freqs.f01` for q0, the device file's nine significant figures are a prior. The earlier framing as a device-file format change was wrong. **`reads` becomes declared, reversing the previous round's resolution** (§11). The observation behind the FIXME — that a node knows in advance what it reads — is right, and it undercuts the case for deriving at runtime: if the set is static, the runtime machinery buys nothing a test cannot. It also fails where the interface is least uniform. `coupler_anticrossing` and `qubit_spectroscopy` override `measure` and own their whole acquisition loop, so there is no `build_schedule` to inspect before deciding whether to run them, and a derived-at-runtime set cannot cover those two at all. So: declared like `updates`, with a test that instruments `read_path` and asserts the declaration covers what the code really reads. Six `analyse`-time reads hoisted, now enumerated rather than estimated — ef.py:439 and :660, single_qubit.py:233 and :513, spectroscopy.py:594 and :1018. A read after the acquisition cannot be a prerequisite, and each is a one-line move. **A per-node acquisition cap, with a default** (§5), for how wide a chunked sweep may get. Worth being clear why this is not the kind of knob the RFC removes: a resource budget needs no knowledge of the chip, where a range needs to know roughly where the answer already is. `routine_timeout_s` is already this kind, and nobody needs a qubit's frequency to set it.
Four comments. Three settle open questions, one reverses a decision, and checking the second turned up an error in the RFC's own arithmetic. **Provenance goes in a sidecar the driver owns** (§10), which reverses the blanket "no second store" from the round before. That objection is sound against a second store of *values* — two sources of truth for what the chip is, needing synchronisation with the file the executor reads — and not against metadata that never holds a number anything needs to run a circuit. Delete the sidecar and you learn nothing about provenance, which is exactly today's position, so its worst failure is the status quo rather than a chip driven from a stale duplicate. Two properties it has to have. Merge per key, not per file: "overwritten each run" read literally erases the provenance of every parameter a run did not touch, and a partial run touches few. And safe to be absent, since a missing sidecar should mean "everything is a prior" rather than a failure — otherwise a fresh checkout cannot calibrate. **The IF limit is 500 MHz in both schedulers**, checked rather than assumed: `NCO_FREQ_LIMIT_STEPS / NCO_FREQ_STEPS_PER_HZ` is 2e9/4 in quantify-scheduler 0.28 and qblox-scheduler 1.0.0b4 alike. It still belongs on `SchedulerBackend` where `drag_span` is, because the fact belongs to the backend either way, but the agreement is recorded so nobody models a divergence that does not exist. That check also corrected §5. The instruction ceiling I had quoted — an empirical 12,376-works / 13,074-fails from the August 2026 cluster — was in units of acquisitions times an *estimated* instructions-per-acquisition, not counted instructions, and it straddles the QRM's real limit of 12,288. So the bracket was measuring my estimate. The real per-module ceilings are 16,384 for a QCM and 12,288 for a QRM, from the schedulers' own constants, and the QRM's is the binding one for anything that acquires. The conclusion survives — 501 acquisitions at ~6,500 instructions still fits — but phase 3's chunking arithmetic needs the right number, and `_log_program` already reports the compiled count at debug level. **Escalation lives in `measure`**, with the attempt count reported so the DAG and the report still see it. **`resonator_punchout` comes back** in phase 3: its amplitude grid stopping at 0.5 is exactly the hardware-bounded bug §5 fixes, so the phase that fixes the cause re-enables the node, with the August 2026 chip as the test case. §12 is down to one open question.
…er it Six routines read a device parameter inside `analyse`: the ground-state resonance the two excited sweeps difference against, the clock `ramsey` and `ramsey_12` correct, and the amplitude `fine_amplitude` refines. All are now read in `build_schedule` and kept on the instance, as the setpoints already are. RFC 0007 §11 wants this because a read that happens after the acquisition cannot be checked as a prerequisite — the point of declaring what a routine reads is to decide whether to run it, and by `analyse` it has already run. It is also more honest on its own terms. `fine_amplitude` plays every X in its sweep at `rxy.amp180` and then read that value afterwards to interpret the result; nothing writes in between today, so the number was the same, but it described a sweep that had already happened rather than the one that was performed. Two of the six were reading a value `build_schedule` had already read to centre the sweep on, so those are now one read instead of two. `analyse` on `resonator_spectroscopy_excited` and `f12_spectroscopy` no longer touches the device at all, which is why the guard test for the dispersive shift now sets `_ground` alongside `_frequencies` and passes no device. No behaviour change intended: 660 passed with the same 35 environmental failures (28 dummy-cluster assembly, 7 a quantify `QuantumDevice.elements` mismatch), and the scqubits suite green at 143.
`updates` has always said what a routine writes; nothing said what it needs. RFC 0007 §11 wants the DAG to decline a node whose input was never produced rather than let it measure an uncalibrated chip and fit the noise, and that decision is made before the node runs — so it needs the read set as data. Twenty-three routines declare one; the other ten read no device parameter at all. The sets were derived rather than guessed, by instrumenting `read_path` and building every routine against the real fixture device, which is also what ships as the test. Declared rather than derived at run time, per the RFC. The set is static — which paths a node needs is fixed at authoring time, only the values are dynamic — so runtime machinery buys nothing a test does not, and it fails where the interface is least uniform: `coupler_anticrossing` and `qubit_spectroscopy` override `measure` and own their acquisition loop, so there is no schedule to inspect before deciding whether to run them. `test_a_routine_declares_every_parameter_it_reads` closes the gap that argument opens. It patches `read_path` in the module that defines it *and* in every module that imported it — patching only the former would miss every routine — then asserts each declaration covers what the build actually asked for. Coverage rather than equality, since a declaration may be legitimately wider than one build's reads. Mutation-checked: blanking `fine_amplitude`'s declaration fails with `fine_amplitude reads ['rxy.amp180'] without declaring it`. The test builds its own tuner rather than sharing the module-scoped one, for the reason two of its neighbours already give: an earlier test in the file calls `Instrument.close_all()`, and a test that walks every routine needs a device that is readable whatever ran before it. One read in the graph the notation cannot reach, recorded where it is: `coupler_anticrossing` reads its *parent qubit's* `clock_freqs.f01` to centre each probe sweep, and `reads` names paths on a routine's own target. `depends_on` is what orders that one. 661 passed, the same 35 environmental failures; scqubits green at 143.
The failure this closes cost six runs. `qubit_spectroscopy` failed on an August
2026 chip, and `rabi`, `resonator_spectroscopy_excited`, `readout_discrimination`,
`allxy`, `drag` and `readout_fidelity` each then measured a qubit still in |0> and
reported a confident number fitted from its noise — six failures with six
different-looking causes, none of them naming the one that mattered. Before this
month's fit guards, those six did not even fail: they wrote the noise to the
device file and the next run inherited it.
Blocking is on an unsatisfied *parameter*, not on a failed neighbour, because
`depends_on` orders the walk and is not a data dependency. Three things fall out
of that, each of which node-level propagation would get wrong, and each with a
test:
- `cz_chevron` depends on `rb` and `flux_spectroscopy`, neither of which writes
a parameter — twelve of the thirty-three nodes write nothing at all — so a low
benchmark cannot stop two-qubit calibration.
- A failed *refiner* blocks nothing. `ramsey` failing leaves the
`clock_freqs.f01` that `qubit_spectroscopy` produced.
- A disabled node never ran, so it never failed. `time_of_flight` is switched
off on that chip with `measure.acq_delay` legitimately set by hand.
Skipped, not failed. Auto-failing would replace six misleading failures with six
fabricated ones and feed the drift check a history of failures that never
happened, so a blocked node goes to `report.notes` — which is already the field
for what an operator needs and the event payload does not carry — and the summary
log line gains a skipped count. A chain of skips names the failure that *started*
it rather than the neighbour in front of it, which is the choice `diagnose`
already makes in blaming the deepest failing ancestor.
RFC 0007 §11 also called for a pre-walk config error when a disabled node is the
only producer of a parameter something reads. Not implemented, because building it
showed the check is wrong: `measure.integration_time` and `r12.ef_duration` have
no producer anywhere in the graph and are supplied by hand on every chip, and that
same chip disables `time_of_flight` while its `measure.acq_delay` is valid. The
check cannot tell "never produced by design" from "producer switched off" without
the provenance §10 defers, so it waits for that.
667 passed, the same 35 environmental failures; scqubits green at 143.
Phase 0 is done, and building it corrected the RFC twice. The pre-walk config error for a disabled sole producer is withdrawn. It cannot be written without §10's provenance, because it cannot distinguish "never produced by design" from "producer switched off": `measure.integration_time` and `r12.ef_duration` have no producer anywhere in the graph and are hand-supplied on every chip, so the rule fires on them every run — and the August 2026 chip disables `time_of_flight` while its `measure.acq_delay` is a good hand-set 200 ns. Nothing is lost, because the parameter view declines to block on either case anyway. And the read the notation cannot express is now known: `coupler_anticrossing` reads its *parent qubit's* `clock_freqs.f01`, where `reads` names paths on a routine's own target. One case in thirty-three, ordered by `depends_on`.
"What this does not fix" collected two entries when it was written. One of them — the accept side of the guards — was pulled into scope as §6.2, and the other grew sixty lines of settled design across three rounds of review. So the heading now describes neither what is under it nor how much of it is decided. The limitation itself stays: this RFC does not fix the fact that a prior and a measurement are indistinguishable, and saying so is the point of having the section. What did not belong under that heading is the answer — where provenance lives, why not the two config files, why staging value commits does not address what actually went wrong. That is design for the follow-up RFC, recorded here because it was argued out here. Retitled to "What this defers, and the shape it should take", with the status said outright — problem unfixed, answer decided, none of it built — and the design split into §10.1 and §10.2 so the deferral and its shape are not read as one thing. Also notes why the deferral is affordable: each of the three things wanting provenance has a weaker version that works without it, and §11's ledger is the worked example, asking "did this walk produce it?" rather than "was this ever measured?".
Provenance *could* be built inside 0007 — it is roughly phase-0 sized — but it should not be, and checking whether it could turned up a claim 0007 had wrong. 0007 §10 argued that provenance needed no new content because `RoutineResult` already records which node wrote what, when, and from which fit, so only a lookup was missing. It does not. The report is assembled, converted by `to_event_payload`, put on a result queue and sent to the server, and *nothing writes it to disk*. The driver cannot ask what it measured last week: the facts exist, but not anywhere the process that needs them can read. So the sidecar is the driver's only copy rather than an index over one, and the work is larger than 0007 estimated — and it is a persistence question, not a calibration one. That settles the sidecar-versus-index question 0007 left open, and it is recorded in 0008 §4 as a correction rather than quietly fixed, because it is the reason the scope estimate moved. 0008 carries what 0007's review settled: the sidecar beside the device file keyed by target and dotted path, merged per key and safe to be absent; why neither config file is its home; and why staging value commits does not address what actually went wrong on the August 2026 chip — `rabi` reported success while writing 0.0158, so a staging store would have committed it too. Plus a §6 table of the four consumers waiting on it, a plan whose first three phases are additive and observable before anything depends on them, and the regression test that is the August 2026 failure written down: a nine-significant-figure `f01` nothing measured, asserted to be reported as a prior. 0007 §10 shrinks back to the limitation and what deferring it costs — that §11's ledger asks "did this walk produce it?" rather than "was this ever measured?", which is right for a bring-up and blind on a recalibration.
…n vocabulary Two problems, both mine, both from writing for a reader who already agreed with me. **The expiry question was badly posed and did not need asking.** I left "does provenance expire?" open on the grounds that an age is only actionable against a threshold and a sensible threshold is per parameter. RFC 0005 already decided this, and against: it added a `check` form per node specifically so that "staleness is measured rather than remembered". An age threshold is remembering — it guesses at the answer a three-point check goes and measures for one acquisition. So §8 now states it as a conclusion rather than a question: two separate questions with two separate mechanisms. *Was this ever measured* is this RFC, answered from whether a record exists. *Is it still right* is RFC 0005's checks, measured per run. The timestamp is stored for the operator and the report, and nothing compares it to a threshold. That also removed a contradiction §6 had acquired: its ledger row claimed provenance turns the question into "an age, and an age is what a drift check is entitled to act on". It does not, and the row now says what the ledger actually wants — whether a parameter was measured in *some* run rather than only in this one, which is what makes a partial recalibration checkable at all. **And the document used "provenance" without defining it.** The word means "where a thing came from" — a gallery's paperwork proving a painting is what it claims — and an RFC whose title is a term of art owes the reader that in the first paragraph rather than assuming it. §1 says it plainly and §2 explains both it and "sidecar", which had the same problem.
§14 records what building phase 1 turned up, because it is not visible from the code without being pointed at, and because it blocks §8's acceptance test rather than merely inconveniencing it. `test_calibration_loop.py`'s fixture claims f01 = 5.0 GHz against a simulated qubit at 5.2142 GHz, and that suite was green. It was green because `qubit_spectroscopy` returned a broad noise fit clearing both the old signal-to-noise floor and the linewidth test — and because nothing downstream cares. `_acquire_rabi` passes only amplitudes to the simulator, and rabi, ramsey, t1 and t2 all build their Hamiltonian with no detuning. The physics is already present: `_anharmonic_hamiltonian(detuning_ghz=0.0)` is the drive-frame Hamiltonian and carries the delta term. Only spectroscopy passes a detuning; the four gate call sites take the default. So a drive 302 MHz off resonance rotates the simulated qubit exactly as well as one on resonance — the one thing this month's hardware failure turned on. Also records the wrinkle worth knowing before starting: `SimulatedBackend` answers from the schedule, and a schedule carries no clock frequency for a gate, so `SimulatedTuner` is the natural place to set the detuning from configured minus true f01. And the reason this comes before phase 3: the loop fixture is now red with three possible fixes — clamp the sweep, change the fixture, widen the span — and two of them restore the blind spot. The suite cannot say which is right until a wrong f01 fails on its own. §9's phase 1 entry updated to say what was actually built, why it differs from what was planned, and that it is parked rather than merged.
RFC 0007 §14. `_anharmonic_hamiltonian(detuning_ghz=0.0)` is the drive-frame Hamiltonian and already carried the delta term, but only spectroscopy passed a detuning. `rabi`, `t1`, `t2_echo` and `ramsey` took the default, so a drive 302 MHz off resonance rotated the simulated qubit exactly as well as one on resonance — the one thing this month's hardware failure turned on, and the reason no suite could fail for it. Each now takes `detuning_ghz`, defaulting to zero. Measured on the real integrator, rabi's peak-to-peak against detuning: 0.996 on resonance, 0.967 at 3 MHz, 0.446 at 50 MHz, 0.038 at 302 MHz. Against a Rabi rate of pi/20ns ~ 25 MHz that is Omega^2/(Omega^2 + delta^2) as it should be, and there is no oscillation left in the sweep for a fit to find. The other three are the physics claims that come with it, and they are now assertions rather than accidents. Ramsey's sign is the load-bearing one: a device configured 3 MHz *above* the qubit gives a 4 MHz fringe against a 1 MHz artificial detuning, so the residual is +3e6 and `clock_freq_01 = current - detuning` lands on the true f01. The other sign would correct away from it. T1 is unmoved, the term being diagonal and so is the state. The echo refocuses the detuning away — 0.4798 against 0.4782 at 302 MHz, which is shot noise. **Where the number comes from.** A schedule carries no clock frequency for a gate; only a `SetClockFrequency` sweep does. So `SimulatedBackend` takes the device it is calibrating and reads `configured f01 - true f01` off it, per run rather than once — a walk that corrects f01 at spectroscopy has to get gates that then work, which is the DAG's whole premise. `SimulatedTuner` hands its device over at construction. No device means zero, so every direct caller is unchanged. **`_free` now exponentiates the Liouvillian instead of stepping to it.** Not cosmetic: with a detuning in the frame, `t2_echo` and `ramsey` died outright with `IntegratorException: Excess work done` — a few hundred MHz over a microsecond idle is thousands of radians of accumulated phase, which exhausts any step budget. This is the same failure `coordinator._propagate` documents and the same fix, and over a Hamiltonian constant across the interval `exp(L·t)` is exact as well as faster. **What this does not change: `test_calibration_loop.py`.** It runs the tuners over `SimulatedCoordinator`, which reads clock frequencies off the compiled schedule's clock resources and has always been detuning-aware — and it already carries the negative test, `test_an_uncalibrated_chip_gets_the_answer_wrong`, asserting an X gate 214 MHz off leaves the qubit in |0>. So §14's "the suites cannot detect a wrong clock_freqs.f01" holds only for the schedule-reading shortcut in tests/utils, which is what this commit fixes. The loop fixture's `f01: 5e9` is load-bearing and correct: spectroscopy's 600 MHz span genuinely finds 5.2142 GHz. Its three-way fix on `wip/rfc0007-accept-side-and-band` is not a fixture question — the fixture LO puts the true line at 224 MHz of IF, well inside the 500 MHz limit, and the rejected 5.040000e+08 setpoint is the widening search overshooting a clamp that is not being applied. Changing the fixture or widening the span would restore the blind spot. Tier 3 asserts all of it: no usable contrast at 302 MHz, `fit_rabi` refusing that sweep, and the wiring — the same routine through `SimulatedBackend` fitting at the true f01 and refusing at true + 302 MHz. 143 passed -> 146 passed on `-m scqubits`, nothing regressed. The fast suite is unchanged at 667 passed with the usual 35 environmental macOS failures.
The simulator fix in d35147a corrected this section twice, and the RFC still said the wrong thing. Rewritten to record what was actually true, since the RFC is the durable artefact and a commit message is not where a reader will look. **§14 claimed "the suites cannot detect a wrong clock_freqs.f01".** One of the two could. `test_calibration_loop.py` runs the tuners over `SimulatedCoordinator`, which reads clock frequencies off the compiled schedule's clock resources and has always tracked per-qubit detunings — and it already carried `test_an_uncalibrated_chip_gets_the_answer_wrong`, asserting that an X gate 214 MHz off leaves the qubit in |0>. Verified both before rewriting. The blind spot was only in `tests/utils/simulation.py`'s schedule-reading shortcut. **And on that basis, the three-way fix I warned about did not exist.** I told the next session that the loop fixture's red had three possible fixes — clamp the sweep, change the fixture, widen the span — and that two would re-hide the bug. Wrong: the fixture's `f01: 5e9` is load-bearing and correct, its LO puts the true line at 224 MHz of IF, and the rejected 5.040000e+08 setpoint is simply the widening search overshooting a clamp that is not being applied. So the warning pointed at the wrong door, and changing the fixture would have restored the blind spot for no reason at all. The practical effect is that phase 2's remaining work is a plain bug rather than a question of what the fixture is for, and phase 3's acceptance test can now be written honestly. Suites verified on this branch after d35147a: 667 passed on the fast suite with the same 35 environmental macOS failures, and 146 passed on `-m scqubits`, exit 0.
…h it measured `three_state_operating_point` swept a hardcoded 6 MHz. That number came from the simulated chip, where it is 1.8 linewidths of a 3.31 MHz resonator — the node's own comment had already worked that coefficient out and then kept the constant, noting that deriving it wanted a bigger register budget first. It did not: the width can follow the chip while the five-point cap stays exactly where the sequencer puts it. On the August 2026 B chip the constant is eighteen linewidths of a 327 kHz resonator, so four of the five frequencies sat where nothing comes back. That is the same failure a constant span produced in `readout_operating_point`, whose 2 MHz was 5.4 linewidths on that chip and put the outer setpoints off resonance altogether — and it is why this node has never once passed there. Narrowing it by hand goes wrong the other way, and that chip's config did: 200 kHz is 0.61 linewidths, which is the two-state coefficient this node's docstring records as breaking it outright. Neither end reached the flank where |1> and |2> separate. Those two sit 11 kHz apart in resonator shift — -102 against -91 kHz — against a 327 kHz linewidth, so the point that resolves them is a flank away, not a tenth of a linewidth away, and the closest-pair separation never exceeded 1.05 of the 1.5 scatters it needs across six runs. `resonator.linewidth` joins its `reads`, which the instrumented declaration test required as soon as the span started reading it. Verified: 897 fast tests pass against the 35 unchanged environmental failures, 172 simulated pass. The simulated chip's span moves 6.000 to 5.958 MHz, 0.7%, which is why its own three-state calibration is unaffected; the B chip's moves 0.200 to 0.588 MHz.
…refusing it A guard here had two jobs conflated: deciding whether a number is a measurement at all, and deciding whether it is a good one. Only the first is a reason to refuse. The second took four nodes down with it that had never been given the chance to judge their own data — `three_state_discrimination`, `ramsey_12`, `drag_12` and `fine_amplitude_12` have not run once on the August 2026 B chip, behind a `three_state_operating_point` reading 0.93 against a bar of 1.5 and a `rabi_12` reading 2.5 against a bar of 3. Both floors are now derived rather than picked, and neither comes from a chip. `require_resolved_curve` refuses below what pure noise fakes over that many points. Six hundred decaying-cosine fits through unit Gaussian noise gave a 99th percentile of 3.53 at 21 points, 2.36 at 41 and 1.84 at 81 — `16/sqrt(n)` to within 6%, which is the same methodology `MIN_LINE_REACH` was calibrated by. The scaling is the finding: a fixed 3.0 *admits* noise at 21 points and discards three-sigma results at 81. What a fit can counterfeit depends on how many points it had to counterfeit through, and on nothing about the chip. `three_state_operating_point` refuses below the separation at which its closest pair reaches `MIN_ASSIGNMENT_FIDELITY` — `2*Phi^-1(0.6) = 0.51` scatters, from the Gaussian overlap of two clouds. Between there and 1.5 it writes the point and marks it degraded. `fit_three_state_discrimination` gets the same treatment: its confusion matrix is the honest description of a poor readout and is more use to whatever reads it than a refusal is. Guards that answer the first question keep refusing, and the audit says which: `MIN_LINE_REACH` (calibrated on noise), `MAX_ACCUMULATED_ROTATION` (past a radian the model is wrong, not noisy), `MAX_DEMODULATED` (a signal bounded at one reaching three means the normaliser is wrong), `MAX_T2_OVER_T1` (impossible), `MAX_AMPLITUDE_REACH` (r off the fit's boundary), `require_in_range` and rabi's past-the-sweep-top check (never swept through). One simulated test moved with the floor rather than against it: `test_mapping_back_is_what_survives_a_noisier_readout` asserts a window where the plain 1-2 sequence is refused and the mapped-back one is not. The window is the claim and it still holds; it now sits at 0.5 shot noise where it sat at 0.3, because 41 points tolerate more than the constant assumed. Verified: 897 fast tests pass against the 35 unchanged environmental failures, 172 simulated pass.
`_require_ef_ladder` accepted any ratio once the sweep resolved a full oscillation, on the reasoning that a drive too weak to turn a pi shows less than one period and never more — so a resolved oscillation had to be a measurement the ladder simply did not describe. That is right in general and wrong at a factor of two, and the August 2026 B chip shows why. Three runs there resolved two clean oscillations at 1.81x the ladder. Every one of them also measured |2>'s dispersive shift at -35 kHz against |1>'s -100 — |2> was not being populated at all — and `three_state_operating_point` collapsed to 0.11 where the ladder-consistent runs gave 0.93. The oscillation is real; it is not the 1-2 transition. It also cannot be recovered by refitting, which is worth recording because it was the obvious thing to try. Seeding the fit at the ladder frequency converges back to the same answer, and forcing the period there describes that data 1.2x worse — outside the 11% standard error on the comparison. The two branches are two datasets, not two minima of one. So a resolved oscillation near double or half the ladder is now refused rather than warned about. Accepting it wrote an `ef_amp180` that took `ef_ladder`, `resonator_spectroscopy_second_excited`, `three_state_operating_point` and the four nodes behind it down, and surfaced two nodes later as an unexplained three-state collapse. Refused here it is one message naming the drive. `fit_rabi` gains an optional `expected_amp180` that seeds a second fit and keeps the physics-seeded one only while the data cannot separate the two, by the `1/sqrt(2n)` standard error on an rms residual. It changes nothing on this chip — both seeds converge — but a sweep whose cosine really does have two near-degenerate minima is a general failure mode and the seed costs one extra fit. `ladder_amplitude` is split out of `_require_ef_ladder` so both callers form the prediction the same way. Verified: 897 fast tests pass against the 35 unchanged environmental failures, 172 simulated pass.
…t at 0.10 `f12_spectroscopy` drove one amplitude, a constant 0.10, where every other spectroscopy node in the graph sweeps power and lets `fit_spectroscopy_power` choose. That function already drops rows a saturating drive broadened and ranks what survives; it was simply never given more than one row here. The constant was tuned against the simulator and does not transfer. tergite-autocalibration, which calibrates this chip family, sweeps 6e-3 to 3e-2 for its 1-2 line — 0.10 is 3.3x its ceiling. On the August 2026 B chip that broadened the line to 37-42 MHz where the intrinsic width at its 70 us T2* is 4.5 kHz, and the fitted centre then wandered 3.06 MHz across five runs while f01, measured on a node that does sweep power, held to 6.5 kHz. `rabi_12` drives at that centre, which is the most likely reason it intermittently found an oscillation that was real and was not the 1-2 transition. The ladder is anchored to `spec.amplitude` — the drive `qubit_spectroscopy` chose for this chip — times 4.7, and spans a factor of five in three points. Both numbers come from tergite's own two ladders: geometric centres 2.8e-3 for 0-1 against 1.34e-2 for 1-2. A *ratio* rather than an amplitude because the absolute value is a property of the drive chain's attenuation and of nothing else, so anchoring to what the 0-1 line actually needed makes this follow the chip instead of following whichever chip a constant was tuned on. Unmeasured, it falls back to the old 0.10 as the ladder's centre, so a config without `spec.amplitude` behaves as before. `spec.amplitude` joins its `reads`, which the instrumented declaration test required as soon as the anchor started reading it. Verified: 897 fast tests pass against the 35 unchanged environmental failures, 172 simulated pass.
…cannot be seen `DEFAULT_AMPLITUDES` reads as a starting bracket and behaved as a hard ceiling. Its own note says "a chip that needs more than this says so by refusing, and the refusal names the axis" — but every refusal in `fit_spectroscopy_power` was a plain `FitError` naming nothing, `drive_amps` was in neither escalatable set, and `_sweep` called `acquire` and `analyse` directly with no retry. So a chip whose drive chain is more attenuated than the one the ladder was tuned on simply died at `qubit_spectroscopy`, and its operator had to discover a working amplitude by hand. The August 2026 B chip did exactly that: 3.33x over its own scatter at 0.08 against the 5x `require_resolved_line` clears, and a hand-written `drive_amps` reaching 0.3. That number is right for one chip and wrong for the next, because the power a line needs is a property of the drive chain's attenuation and of nothing else. The only chip-independent way to find it is to start low, where the line is narrow and its centre honest, and climb when the chip says it cannot be seen. So the two "nothing resolved" refusals now carry `axis="drive_amps"`, and `_sweep` goes through `escalating`. One escalation from the default reaches 0.305, which brackets what that chip needed, and `escalating` leaves an axis the operator named alone — so a config that sets `drive_amps` still wins. Both drive ladders also record `_drive_amps_ceiling`. Without it `_widened` scaled them past full scale and the compiler refused the waveform with `awg_gain_0 is set to 1.2038`, which took out 28 simulated tests on the first attempt — `Rabi` carries the same line for the same reason and its comment predicted this exactly. Verified: 897 fast tests pass against the 35 unchanged environmental failures, 172 simulated pass.
Anchoring the f12 ladder to `spec.amplitude` was wrong and the chip said so on the first run. The reasoning was tergite's ratio of 4.7 between its 0-1 and 1-2 spectroscopy optima — a ratio travels between chips where an amplitude does not — but that ratio holds between two *unsaturated* optima. On the August 2026 B chip the 0-1 line is only visible at 0.3, which is saturated itself; 4.7x that clamped to full scale, and f12 then fitted a 55 MHz line at an anharmonicity of -329 MHz where all five runs before it agreed on -250. `rabi_12` drove 80 MHz off the transition and saw 1.7x over its own scatter. Multiplying a saturated anchor compounds the saturation. The sweep itself was the right idea and stays: this node fixed one amplitude where every other spectroscopy node in the graph sweeps and lets `fit_spectroscopy_power` drop what broadened. What changes is direction. The ladder's top rung is now exactly the amplitude this node used to fix, and the other two are weaker — so a chip it already worked on keeps a row that worked, and a chip it saturated gains two chances not to be. A ladder that can only reduce power cannot do worse than the single amplitude it replaces; one that could raise it did. Verified: 897 fast tests pass against the 35 unchanged environmental failures, 172 simulated pass.
`fine_amplitude_12` read at the three-state point and depended on `three_state_operating_point`. That is a deadlock. Populating |2> at all is the three-state node's whole premise — its refusal says so, "most often the sweep never prepared |2>" — and the node that makes the ef pi accurate enough to populate it was sitting behind it. On the August 2026 B chip `three_state_operating_point` has never once passed, so `fine_amplitude_12`, `three_state_discrimination`, `ramsey_12` and `drag_12` have never run at all, in six attempts. The justification for reading there was sound and had an answer one node away: the reference states are |1> and |2>, which sit almost on top of each other at a 0-1 readout. `rabi_12` already solves exactly that — a second 0-1 pi after the ef pulses returns |1> to |0> and leaves |2> alone, so the ef rotation lands in the |0> population, which is what a 0-1 readout is good at. This now plays the same trick, on the sweep and on both reference states, so the contrast the fit divides by is the one the sweep traverses. That map-back is Chen et al., PRL 116, 020501 (2016): measure twice, the second time with a final pi on 0-1, which swaps |0> and |1> and leaves |2> untouched. Amplified amplitude refinement seeded from a coarse Rabi is likewise the standard ef workflow. Neither is new; the ef ladder was simply written without the literature the rest of the graph in RFC 0004 has, and this commit puts it back. tergite-autocalibration solves the same deadlock differently. Its `n_rabi_12_oscillations` at `qubit_state = 1` prepares |1> with an X *before* the ef pulses and reads with `Measure_RO1` — the same readout pulse on the `ro1` clock, the resonator frequency with the qubit excited — so it discriminates |1> from |2> rather than |0> from |2>. Mapping back instead should give the larger separation of the two at a 0-1-optimised readout, and needs no second readout frequency; `ro1` remains the fallback if it does not, since `resonator_spectroscopy_excited` already measures it. `three_state_operating_point` now depends on `fine_amplitude_12` rather than `rabi_12`, so it gets the refined pi instead of a coarse fit that lands within a few per cent at best and on the wrong oscillation at worst. Amplification is also what disambiguates the oscillation `rabi_12` finds. A per-pulse error grows linearly with repetitions while noise does not, so the factor of two that chip alternates between is unmistakable by the seventh pulse where a single-pulse sweep confuses the two. Verified: 897 fast tests pass against the 35 unchanged environmental failures, 172 simulated pass — including the full-DAG simulated run, which exercises this node's new schedule and readout end to end.
…by the ladder The 2026-08-15 B chip run fitted a 1-2 pi at 0.0736 against the 0.1558 the sqrt(2) ladder predicts — 0.47x, six per cent outside the 0.5x bound — and `_require_ef_ ladder` refused it, taking `fine_amplitude_12`, `three_state_operating_point` and four nodes behind them with it. The measurement was good: - 3.4 clean periods over the sweep, flat envelope, evenly spaced extrema; - contrast 0.006061 against `rabi`'s own 0.004257 — 1.42x, and |0>-|2> should exceed |0>-|1> because it is two dispersive shifts rather than one; - the extremes sit at 0.0053 and 0.0112 where the |0> and |2> readout magnitudes are, given the -100.5 kHz per excitation `resonator_spectroscopy_excited` measured independently; - the second minimum falls at exactly twice the fitted amplitude, so that is a full 2 pi and the first maximum is a pi, not a pi/2. The factor-of-two refusal was added when the only evidence available came from another node: `resonator_spectroscopy_second_excited` put |2>'s shift at -35 kHz against |1>'s -100, so |2> was not being populated and the oscillation, though real, was not the 1-2 transition. Now that `rabi_12` maps |2> back through a 0-1 pi that inference can be made in-node, from this sweep, against `rabi`'s contrast — and it is the sharper test. A drive too weak to turn a pi moves a fraction of the population by definition, whatever the ladder ratio reads; one that moves all of it is turning a pi between some pair of levels. So `rabi` publishes its contrast on `resonator.contrast` — the same shape as `resonator.linewidth` and `coherence.t1`, a number measured and thrown away that a later guard needs — and the guard accepts a resolved sweep swinging at least MIN_LADDER_SWING of it. Threshold 0.7, a ratio of two contrasts on the same qubit through the same readout minutes apart, not a constant from any chip. Without a recorded reference the factor-of-two refusal stands unchanged. That leaves the ladder constant unexplained at 2.1x on this chip, which is a real question and not one a resolved measurement should be refused over. Verified: 897 fast against the 35 unchanged environmental failures, 172 simulated.
…per side
`EF_ENVELOPE_AREA` was `sqrt(2*pi)/4`, derived by hand from `sigma = T/nr_sigma`.
quantify's `nr_sigma` is "after how many sigma the Gaussian is cut off" — per side —
so a pulse of length T has `sigma = T/(2*nr_sigma)` and the area ratio is
`sqrt(2*pi)/8 = 0.3133`. Numerically integrating quantify's own `drag` at
`nr_sigma = 4` gives 0.3133; the constant said 0.6267. Exactly two.
The 2026-08-15 B chip settles it from the other end. `ef_ladder` drives 0-1 with the
*same* pulse as the ef one and measured the ratio directly:
matched_amp180 / ef_amp180 = 0.10927 / 0.07317 = 1.4933 vs sqrt(2) = 1.4142
5.6% — the anharmonic correction of order E_C/hf01, which is 4.7% on this chip. The
sqrt(2) ladder holds. What did not hold was the prediction: `rxy.amp180` of 0.3456
against a measured envelope correction of 0.3162, where the constant claimed 0.6267.
With it corrected the same run lands at 0.956 of the ladder rather than 0.47, inside
the bound by a wide margin instead of six per cent outside it.
So the factor of two chased across four days was in this file the whole time, and the
chip was right at every step. The previous commit's swing test is what let the run
through to produce `ef_ladder`'s measurement, which is what identified it — but the
ratio guard should never have needed rescuing.
A factor of two is the one error this module is least able to see: it is also the
spacing of the cosine roots `fit_rabi` picks between, so a wrong constant and a
halved period are indistinguishable in `ef_amp180` alone. Hence
`test_ef_envelope_area_matches_the_real_waveform`, which integrates the emitted
envelope rather than restating the derivation — the old test asserted 0.6267, which
is the same arithmetic that was wrong.
Three ladder tests changed with it. `test_the_b_chip_s_ef_pulse_is_refused` becomes
`..._is_on_the_ladder` on the real 2026-08-15 numbers; the envelope test now asserts
a refusal, since omitting the correction is a 3.2x error rather than the 1.6x it was
described as; and the partial-rotation test moves to an amplitude that is actually a
partial rotation.
Verified: 898 fast against the 35 unchanged environmental failures, 172 simulated.
…s own extremes `analyse` min-max normalised the per-depth means. That forces the lowest depth to exactly 0 and the highest to exactly 1 whatever they measured, so every dataset comes out looking like a decay from 1 — including one that rises, which RB cannot do. It also destroys the amplitude, which is the parameter the fit reports its confidence through, hence "the fitted amplitude reached -200". The 2026-08-15 B chip returned exactly 0 at depth 2 and exactly 1 at depth 64 with the trend running upward. Both endpoints were arithmetic rather than measurement, and raising `circuits_per_depth` from 10 to 30 could not have moved either — which is what the operator tried, and why it changed nothing. `|0>` and `X|0>` are now played first and the depths scored against them, so survival is a probability again and the sign is constrained by something measured. Two acquisitions against several hundred. This is the same defect `normalised_allxy` already documents at length and fixes the same way; RB was the last place still scaling to its own range. The references are per-chunk, so the split path averages them — free shots on the scale the whole fit divides by, and drift between chunks shows up in their spread.
… 100 us A Hahn echo refocuses static dephasing and nothing else, so `T2 <= 2*T1` bounds it. A window has to clear that bound to constrain the fit rather than truncate it — and a duration cannot, because it is only right for the T1 it was chosen against. The 2026-08-15 B chip measured T1 = 59.6 us, putting the echo's own ceiling at 119 us against a sweep that stopped at 100. The trace rose monotonically with no knee, the fit ran to 487 us, and `fit_t2` refused it correctly — but the remedy its message offers, more shots, cannot bound a decay the window never reached. The same chip measured T1 = 31.8 us the run before, where 100 us was ample. That is the tell: the constant was not wrong, it was only ever right by coincidence. Three T1 puts the ceiling at two thirds of the sweep, so the decay is visibly flattened before the last point wherever T2 falls in its allowed range. Falls back to the constant when `t1` has not run, which also keeps it safe on an element with nowhere to store one — the same opt-in shape `measured_linewidth` uses.
`fit_drag` took the root of its linear fit whenever the slope was not exactly zero. A line through noise has a slope too, and its root lands inside the sweep just like a real optimum, so nothing downstream could tell the two apart — `slope` is in demodulated units and has no scale of its own to be judged against. Judged against the scatter about the line it came from, which does. On the 2026-08-15 B chip `drag_12` rose 0.00118 across its whole beta range against a scatter of 0.00078 — 1.5x — and wrote `ef_motzoi = 0.482` to every ef pulse afterwards without a word. The 0-1 `drag` on the same run clears the bound and its answer is unchanged. Relative because neither quantity has an absolute scale here: both are demodulated units that move with readout gain, and the useful beta range differs by an order of magnitude between the two transitions. A ratio of the two is the only form that transfers between chips. Three is where the rest of the package puts "a trend rather than noise" — the same reasoning as MIN_ALLXY_CONTRAST — and a working DRAG sweep clears it by a wide margin, since making the signal first order in the error is the whole point of the sequence.
…_sigma to the pulse Two pieces of this guard were fitted to one chip rather than to the physics. The factor-of-two window — `1.6 <= ratio <= 2.5 or 0.4 <= ratio <= 0.625` — was added to refuse the August 2026 B chip's `rabi_12`, on the reasoning that a clean oscillation at twice the ladder had to be some other transition. It was not. The prediction was wrong by two because `EF_ENVELOPE_AREA` read `nr_sigma` off the wrong side of the Gaussian, and the window was a hand-cut hole around my own arithmetic error. On any chip whose ef pulse genuinely sits near half or double the modelled amplitude for an unrelated reason, it refuses a good measurement and names the drive. What is left is two readings, both properties of the sweep rather than of a chip: whether it holds a whole oscillation, and whether it moves as much population as `rabi` does. A drive too weak to turn a pi fails both by construction — fewer than one period, and a fraction of the swing — so clearing them together is what a pi looks like wherever the ladder expected the amplitude to be. `expected` is a model of the pulse chain and `ef_ladder` measures the same relation directly by playing one pulse on both transitions, so this guard is deliberately the weaker witness. `RXY_NR_SIGMA` stays a literal, because the tuner layer has no scheduler to ask and importing one would cross the executor boundary. It is now asserted against the `nr_sigma` a real `DRAGPulse` emits, so a change to the scheduler's default fails the test rather than silently recentring every ef bound — a library coupling that is checked, rather than a chip constant that is assumed. Audited the rest of this session's constants on the same question. `MIN_LADDER_SWING` is a ratio of two contrasts on one qubit through one readout; `MIN_DRAG_RISE` and `MIN_ALLXY_CONTRAST` are signal against scatter; `SPAN_IN_LINEWIDTHS` and `T2_WINDOW_IN_T1` are sweeps in units of what the chip measured; `NOISE_FAKEABLE_SPAN` scales as 1/sqrt(n); `MIN_USABLE_SEPARATION` comes from a fidelity target through a normal quantile. None carries a number off a particular chip. Verified: 899 fast against the 35 unchanged environmental failures, 172 simulated.
…arched in `ANHARMONICITY_RANGE_HZ` gates the `anharmonicity_prior` a config supplies, and it exists to catch a placeholder — a device file carrying f12 as a round number put the implied anharmonicity positive on four qubits and the EF chain measured nothing for several runs. But -400 to -150 MHz is a prior over the transmon family, not a fact about any one device, and a design deliberately outside it is exactly the kind of chip fact that belongs in a config. Refusing it there left no way to say so. `anharmonicity_range` now overrides the window, and the message names it.
`measure.integration_time` was whatever the config was written with, and every discriminating node inherited it. It is the last free parameter in readout SNR and the one with a genuine interior optimum: signal accumulates with the window and noise only with its square root, so separation climbs as sqrt(t) until the qubit starts relaxing inside the window, after which a longer one only adds shots of the wrong state. Where those meet is set by T1 and chi, so it is a property of the chip. `resonator_relaxation` has asked for this node in its own docstring since it was written — it measures the ring-up, which is a floor on the window and says nothing about noise or relaxation, and it deferred writing the parameter until there was a discrimination fidelity to choose against. There is now. Runs ahead of the rest of the readout chain, because every node after it measures a separation this scales. Three things the implementation had to accommodate: - One schedule per window, not one sweep. Every square acquisition in a Qblox program shares an integration length — a second raises "attempting to set an integration_length of 500 ns, while this was previously determined to be 250" — so the one axis this node exists to sweep is the one that cannot be swept in a schedule. `acquire` walks it and concatenates, as `rb` does for its own reason. - Hold on a tie. An SNR from n shots carries about 1/sqrt(2n) of relative error, so a flat landscape still has a winner and taking it moves the readout on noise. Every window is a multiple of the incumbent, so the incumbent is always in the sweep and is what a tie falls back to. This is not hypothetical: the simulator models no acquisition window at all, so its landscape is exactly flat, and without the guard the node shortened the simulated chip's readout fourfold and `resonator_relaxation`'s own test caught it. - The window ladder is multiples of what the config arrived with, which is the only scale available before anything has been measured on this axis. A config already right keeps its value, since 1.0 is in the ladder. Note for the simulated suite: it exercises that this node runs, chooses and holds, but cannot exercise the physics, because the simulator's readout does not depend on the window. That has to come from hardware. Verified: 901 fast against the 35 unchanged environmental failures, 172 simulated including the full-DAG run.
…om its own fit Two defects, and the first is the same circularity as `rb`'s normalisation. `MAX_ACCUMULATED_ROTATION` was checked against `slope * n_max`, where the slope comes from the linear fit. A sine fitted with a line always yields a shallow slope, so the product under-reports by exactly the amount that makes the guard necessary and it can never fire in the case it exists for. The 2026-08-15 B chip's sweep spanned 0.920 of its own contrast — at least 1.17 rad of turn, and about 3 rad by a sine fit — while the slope claimed 0.73 and the ceiling let it through. The amplitude came from a line through a saturating sine and went to every X pulse afterwards. The span is model-free: the demodulated signal is `sin(n*d)` bounded at one, so `arcsin(span)` is a floor on the turn whatever the line says. A floor, because arcsin saturates at pi/2 and cannot see a sine that has already turned back — which is why the shortening is at least a halving rather than the literal ratio. Taking the ratio would have gone 25 -> 21 -> 17 and refused with both passes spent; halving reaches 6 and lands inside the linear regime. Ordered before the scatter test but gated on a span of two, the most a bounded sine can produce. Above that the contrast is the suspect rather than the rotation, and the scatter and reach guards give the right remedy — "average more shots" rather than "shorten the sweep". Second, `fine_amplitude_90` wrote an amp90 off four points of noise: a slope of 0.0037 against a standard error of 0.0153 on it, a quarter of a sigma. My first attempt at this was a rise-against-scatter test like `fit_drag`'s, and it was wrong in principle — the slope *is* the calibrated quantity here, so a perfectly tuned pulse has no rise and the test refused the success case. It took the simulated DAG failing to show that. What is refusable is the *correction*, not the fit. Below three standard errors the sweep has not resolved one and zero is written instead, with a warning. Honest, and harmless where a refusal would have blocked every node behind it. Verified: 901 fast against the 35 unchanged environmental failures, 172 simulated.
… RB fit slipped through Two fixes from the 2026-08-15 23:36 run. `t2_echo` crashed outright, and it was mine: sizing the window from a measured T1 makes steps of no particular length — 73.82 us of T1 gave 5536.857838 ns — and a Hahn echo hands `idle` half of each one. It compiled up to the point qblox refused "a time value of 806792.4289192001 ns", from a routine that looked fine. The delays are now snapped so that *half* a delay lands on the grid, which is the quantity actually played. `rb` produced a real decay for the first time and reported a fidelity of 0.99997 from it, against the 9.5% per gate AllXY measured on the same chip in the same run. The decay is real — 0.916 to 0.848 — but shallow enough that `a` and `r` are not separable: only their product sets the slope, so the fit ran `a` to 17.85 and the asymptote to -16.94 to draw a straight line, and the rate it reports is assumed. `MAX_AMPLITUDE_REACH` exists for exactly this and its docstring already names the test — "a real one fits A near the span it spans". It was set at 200 and this fit landed at 199, one part in two hundred inside the wall. So the wall moves rather than a fourth guard joining the three already here. Measured against clean decays over the same depths: p = 0.9 fits A at 1.1 times its span, p = 0.99 at 2.2, and p = 0.9998 — slow enough that the curve has barely bent — at 50. A hundred is twice the slowest of those and half the degenerate one; the gap between the two populations is two orders wide, which is what makes a threshold in it safe. Three sharper-looking guards were tried first and each refused a case it should have passed: a floor on how far the deepest sequence decayed (refuses a good qubit, which legitimately decays slowly), a bound on the fitted asymptote (breaks the rescaling invariance `fit_rb_decay` is tested for), and a significance test on the rate against its own covariance (refuses clean synthetic decays outright). The existing bound was the right shape the whole time. Verified: 901 fast against the 35 unchanged environmental failures, 172 simulated.
…t a few factors out The 2026-08-15 23:36 run chose 3.6 us from a ladder whose top rung was 3.6 us. That is not a measured optimum, it is where the sweep stopped — and the separation was still climbing when it did, which is exactly the case the node exists to find the end of. Nothing was forcing the ladder to be short. Each rung is already its own schedule, because a Qblox program takes one integration length and a second raises from inside the backend, so the two-acquisition register budget that bounds every other sweep in this file does not apply across rungs. The cost of another rung is one short schedule. So the ladder now doubles from a quarter of the configured window up to the ceiling — eight rungs from 0.225 to 16.384 us on this chip, against the five it had — and the rung it picks is bracketed on both sides unless it is the ceiling itself. Escalation would have been the other route and is the wrong one here. `OutOfRange` on an exhausted axis re-raises, so a node that merely picked its top setpoint would fail and take the whole readout chain with it. Choosing the longest reachable window is a correct answer, not a failure; it is only an incomplete one, and a warning is the right weight for that. When the winner is the ceiling the message says what that means — more readout SNR on this chip needs different hardware, not a different window. This is the axis the three-state chain is short of. The last run's move from 0.9 to 3.6 us took `three_state_discrimination`'s |1> error from 0.567 to 0.405 and its leakage from 0.25 to 0.189, with the ladder's top rung as the only thing stopping it. Verified: 901 fast against the 35 unchanged environmental failures, 172 simulated.
Its refusal has been telling the operator to "shorten the repetition counts" since it was written, and nothing could. `fine_amplitude` routes through `amplified`, which catches the refusal and rebuilds the ladder; `fine_amplitude_12` went straight to `escalating`, and `_widened` declines the "shorter" direction on purpose because rebuilding a repetition ladder is not a generic stretch. So the node refused outright every run. On the 2026-08-15 23:36 run its sweep spanned 1.707 of its own contrast, asked to be shortened, and stopped there — and `r12`'s pi has never been refined once across this whole effort. It needs the reach more than the 0-1 node does, not less: how far a ladder may run depends on the per-pulse error, which is the thing being measured, and the ef pi starts from a coarser `rabi_12` than the 0-1 pi starts from `rabi`. The ladder is `range(1, 26)`, the same shape and the same step, so `amplified(step=1)` applies unchanged. `_amplified` becomes `amplified`, since a helper used from another module is not module-private. `ef` already imports from `spectroscopy`, so the direction is established and adds no cycle. Verified: 901 fast against the 35 unchanged environmental failures, 172 simulated.
…trument The ceiling on this axis is the pulse, not the sequencer. Once the drive stops there is no more signal to integrate — only ring-down, then noise — so a longer window grows the denominator and not the numerator. The 2026-08-16 run is the demonstration and the fault is mine. q5's readout pulse is 3.8 us behind a 200 ns delay, so 3.6 us is every sample that carries anything. The ladder I widened to the instrument's 16.384 us last commit chose 7.2, half of it noise. Discrimination still improved — it came from 0.9 us and gained more signal than it lost, taking readout fidelity 0.824 to 0.865 and `three_state_discrimination` 0.714 to 0.770 — but every magnitude node paid for it: contrast fell 27%, `qubit_spectroscopy` fitted a 573 MHz linewidth on a transmon whose anharmonicity is 253, and `f12_spectroscopy` lost its line entirely. Both read a magnitude, and half their window was empty. Read from the element rather than assumed, because pulse length is a chip fact: 3.8 us here against the fixture's 300 ns wants windows an octave apart and neither is wrong. Two ring-down constants of headroom, since a 322 kHz resonator rings for about a microsecond and cutting exactly at the pulse would discard it. The incumbent now always joins the ladder, unclamped, and that is not a detail. A config integrating past its own pulse is precisely what this node should shorten, and it may only shorten on evidence — the value being replaced has to be measured beside the alternatives. The fixture integrates 1 us behind a 300 ns pulse: clamped out of its own sweep, the tie-hold lost the rung it compares against and the node quartered the window on noise. The simulated DAG caught that. Verified: 901 fast against the 35 unchanged environmental failures, 172 simulated.
A calibration is a picture of the chip. "0.9% per Clifford, poorly constrained" is that picture; refusing to publish it is withholding the measurement, and it takes every node behind it down as well. I had the line in the wrong place. The line that matters is whether a node *writes* a device parameter. `rb`, `t2_echo`, `allxy`, `readout_fidelity`, `three_state_discrimination` and `ef_ladder` all have `updates = ()`. Nothing downstream can be corrupted by a number they report, so their quality bars become flags: - `fit_rb_decay` returns the fidelity with `unresolved` set when the amplitude pins against its bound or the survival does not clear the span pure noise fakes over that many points. The 2026-08-16 B chip pins at every bound tried — 100x span, 20x, 5x, 3x — with the answer moving from 3.7e-05 to 1.4e-03 per Clifford while the residual goes 0.0123 to 0.0131. There is no minimum there, so the honest output is the number and a flag, not silence. - `fit_t2` reports a T2 past 2*T1 as the lower bound it is. That the window did not contain the decay is a measurement of the window, and the run should say so. A node that *does* write keeps its prior instead: - `fit_drag` returns the motzoi already on the element when the sweep does not rise above its own scatter. The root of a line through noise must not be written; that never made declining to report the right answer. `drag_12` refused on every run for want of this. - `amplified` returns the routine's `uncorrected` result when the ladder runs out. A rotation that outruns the linear model at every reachable length is a real finding, and it licenses no correction — but the amplitude it would have refined is still the best available. `fine_amplitude_12` refused on all six runs of this chip and `r12`'s pi went unrefined the whole time for want of a number it already had. Six tests changed from asserting a refusal to asserting the flag. Their evidence still stands — the dead-readout survivals still come back marked, and a real decay at fidelity 0.986 through 0.9998 still comes back clean. Verified: 901 fast against the 35 unchanged environmental failures, 172 simulated.
…e line The last node still refusing for a reason the rest of the graph now reports. On the 2026-08-16 run no drive power showed the line above its own scatter — the strongest reached 3.36x against the 5x required — and the node failed, while every ef routine behind it went on reading the same `clock_freqs.f12` from the previous run regardless. The refusal changed nothing except the report. So the prior stands and the node says it is a prior. Only where there is one: on a chip that has never resolved this line there is nothing to fall back on, and the refusal is then the whole answer rather than a formality. That completes the pass. Every node in the graph now either measures its parameter, or keeps the best value already measured and marks the result `unresolved` — the split being whether it writes a device parameter at all. Nothing refuses merely for measuring something imprecise. Verified: 901 fast against the 35 unchanged environmental failures, 172 simulated.
…hout support Every node passed. Two of them published a number nothing measured, and neither said so — which is the failure mode the whole `unresolved` split exists to prevent. `MAX_T2_OVER_T1` is a tolerance applied to `2*T1`, so 1.5 put the bar at three times T1 — half again past a bound a Hahn echo cannot exceed at all. The run returned 148.17 us against a 49.41 us T1, exactly 3.00x, and cleared the bar by six hundred picoseconds with `unresolved` at zero. A decay fitted over two time constants does not carry fifty per cent of error. At 1.2 that reading is flagged, while a genuinely T1-limited echo sitting *at* 2*T1 still comes back clean. `require_resolved_line` tested a fitted line for being too shallow and for being narrower than the sweep's own grid, and never for being wider than the sweep itself. The same run fitted 107 MHz across a 20 MHz window and reported a quality factor of 49.7 and an SNR of 163 from it. The centre of a power-broadened line can still be sound — `ramsey` refines f01 regardless, and did — but its *width* was never in the data, so the width and everything derived from it is extrapolation. Flagged, not refused, and for the reason established last commit: both nodes write a parameter that is still the best available, and withholding the report would leave the device in the same state minus the record. `require_resolved_line` returns the flag now rather than only raising; its two existing refusals are unchanged. Verified: 901 fast against the 35 unchanged environmental failures, 172 simulated.
…h measure again Flagging said "do not trust this". These two now produce the right number instead. **RB.** The asymptote was fitted, and that freedom is exactly what made the rate unmeasurable: below one bend only the product `a*(1-r)` sets the slope, so the fit slides along the degeneracy until a bound stops it and the bound decides the answer. Every bound tried on the 2026-08-16 chip pinned — 100x the span, 10x, 3x, 1.5x — reporting 3.6e-05 to 3.4e-03 per Clifford as it went, with `b` at -20.3 for the loosest. A survival does not decay to minus twenty. It is not a free parameter. A depolarised n-qubit state survives with probability `1/2^n`, and since `rb` began normalising against measured |0> and X|0> references that is where the curve ends by construction. Pinned, the same data gives 2.19e-03 per Clifford with `a = 0.443` — survival at zero depth 0.94, which is where a readout of 0.87 fidelity puts it — and the physics finally agrees with itself: `allxy_check` measured 6.6e-02 per gate on that run against RB's 1.1e-03, and that gap is what RB is *for*, since random sequences average a coherent miscalibration into the depolarising rate while AllXY is built to see it. There is no softer version. Bounding `b` near a half instead of fixing it pins at the bound for any tolerance from 0.05 upward, and the reported rate walks with it. The trade is real and the e2e tolerance widened from 15% to 20% to hold it: an imperfect asymptote biases a shallow simulated decay by a few per cent. It buys three orders of magnitude on a real one. **T2 echo.** `T2Echo` set no `_delays_ceiling`, so escalation widened the window until the fit resolved *something* — 148 us to 355 us on that run, arriving at 148 us of T2 against a 49.4 us T1. Three times a bound nothing can exceed. Six T1 is three time constants of the longest T2 physics allows, and past it a rising signal is a readout wandering over a sweep of minutes, which looks exactly like a slow decay. Four tests changed with the model. Two encoded the min-max normalisation `rb` no longer does; one asserted a scale invariance deliberately traded away, and now asserts the trade in both directions; one gave a two-qubit fit a one-qubit asymptote. Verified: 901 fast against the 35 unchanged environmental failures, 172 simulated.
ch-ahindura
force-pushed
the
hotfix/code-cleanup
branch
from
August 16, 2026 11:52
04dbebb to
94bf57a
Compare
Four separate breakages, only two of them mine. **`test_tuner_routines` needed the sim extra to count acquisitions.** `TestATwoDimensionalGridIsSplitByRows` built its device with `device_for(TransmonSimulator())`, and constructing that imports `scqubits` — which lives in the `sim` extra. `test-py-driver` installs one executor extra and never `sim`, so these seven failed on the import under every leg of the matrix, including before this branch. Nothing in them runs physics: they count acquisitions per schedule and check the seams fall between whole rows. A `FakeDevice` with literal frequencies does that, and now does it under all four extras rather than none. **`test_ef_envelope_area_matches_the_real_waveform` imported quantify directly.** Mine, from this branch. It integrates quantify's own DRAG envelope, so it belongs to the `quantify` extra and now says so through `importorskip`, as the scqubits tests already do. **An unused import.** Mine: `OutOfRange` survived an escalating RB guard that did not. **`ruff format`.** Seven files, all touched on this branch. `lint-py` checks formatting as well as lint and I had only been running the tests. **The dashboard exported a helper beside a component.** `FidelityGrid.tsx` exported `worstComparable`, which trips `react-refresh/only-export-components` and failed `lint-dashboard` before Cypress ever started. `worstComparable` and `isEdge` move to `fidelity.ts` beside `format.ts`, `layout.ts` and `nodeDetail.ts`, which is where this project already keeps the pure helpers that have their own tests. Verified by running the jobs rather than inferring them: all four `test-py-driver` legs, `test-py-loop` under quantify and qblox, `lint-py`, `lint-go`, `test-py-cli`, `test-py-sim`, `test-e2e-driver`, `lint-dashboard`, `test-dashboard`, and `test-e2e-dashboard` — 143 Cypress specs, all passing.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
We needed to test the single-qubit calibration part of the builtin driver on a real device
Added
qpi-driver/py:readout_integration_timecalibratesmeasure.integration_timebysweeping the acquisition window and taking the one that separates
|0>and|1>best.It was a config constant every discriminating node inherited, and it is the last free
parameter in readout SNR. Holds its current value when no window beats it by more than
shot noise.
qpi-driver/py:rabi_12carries its trace on success as well as on refusal, andrbreports
decay_observed— how much of the decay its deepest sequence actually saw, sinceris extrapolated from the rest. A chip reporting 0.15% error per gate had seen 17.6% ofa decay, below what its own T1 allows and 34x better than
allxy_checkon the same run.qpi-driver/py:readout_operating_pointreports the magnitude contrast across itssweep, and how much of it survives at the point it picks. It optimises complex
separation, which is right for a discriminator and invisible to the
signal_ofmagnitudenearly every other node reads — and nothing measured the difference.
qpi-driver/py: a routine refused by a guard keeps the sweep behind the refusal, so thereport carries the trace and not only the sentence. It is marked as a refusal and is not
attributed any parameter.
qpi-driver/py:allxy_checkreports its normalised response alongside the rms, so the21 pairs can be read after the single-qubit chain finishes.
allxyruns beforefine_amplitudeandfine_amplitude_90, so it cannot show whether either helped.qpi-driver/py:fine_amplitude_90measures the pi/2 amplitude and writes it to a newfine.amp90onCalibratedTransmon. Both schedulers derived a pi/2 fromamp180by linearinterpolation, so a drive that compresses near full scale left an AllXY error nothing could
correct.
qpi-driver/py: the simulator has three-level physics for the 1-2 transition, sorabi_12can be tested without a chip. The sqrt(2) ladder between the two transitions comes out of
the model rather than being written into it.
qpi-driver/py: a routine may set its owntimeout_sincalibration.yml, overriding theglobal
routine_timeout_s. One ceiling had to be set for the slowest node, so it could notalso catch a fast one hanging.
qpi-driver/py: a quantify routine logs how long its schedule should take beforerunning it, and its Q1ASM at debug level. A timeout previously gave no way to tell a
schedule that needed longer from one that was stuck.
qpi-driver/py: a timed-out quantify routine names the module and sequencer that didnot stop, its state and its flags. qblox-instruments raises with a bare sequencer
index, so the operator could not tell which of twelve modules had hung.
qpi-driver/py: an end-to-end test asserts the benchmarked gate error against the onethe simulator was given, so a calibration that leaves a gate wrong now fails the suite
instead of clearing a fixed fidelity threshold.
qpi-driver/py: a calibration writes a*.provenance.ymlbeside the device configrecording which routine last measured each parameter, and when. A device config could
not say whether a value was measured or typed in, so every reader had to assume the
better case.
qpi-driver/py: a calibration report names the inputs nothing has ever measured, pertarget and per routine. A run built on a hand-supplied frequency previously read exactly
like one built on a measured one.
qpi-driver/py: a skipped routine reports which parameters it left unconfirmed and whenthey were last measured, and a run whose producer for a never-measured parameter is
switched off says so before the walk starts.
Changed
qpi-driver/py:f12_spectroscopykeeps the f12 already measured on a qubit when nodrive power in the sweep resolves the line, rather than refusing. Only where a prior
exists — a chip that has never resolved it still fails, because there is nothing to keep.
qpi-driver/py: a node that measured something imprecise now reports it and flagsunresolved, instead of refusing.rbandt2_echowrite no device parameter, so awide error bar is a fact about the chip and withholding it published nothing;
dragandthe fine-amplitude nodes keep the value they would have refined rather than writing a
correction their own model could not describe.
qpi-driver/py:readout_integration_timesweeps the whole reachable range of windowsrather than a few factors either side of the configured one, so the window it writes is
an optimum it bracketed instead of the edge it stopped at — and it says so when the best
window is the hardware ceiling.
qpi-driver/py: the 1-2 ladder guard drops its factor-of-two special case and judges aresolved sweep on periods and population swing alone — both properties of the sweep
rather than of any chip.
ef_laddermeasures the same relation directly, so the modelledprediction is deliberately the weaker witness.
qpi-driver/py:rabi_12now judges an off-ladder 1-2 pi by how much population itswings —
rabirecords its own contrast for the comparison — instead of refusing everyamplitude a factor of two off the sqrt(2) ladder. A resolved sweep that moves the full
population is turning a pi somewhere, so the ladder constant is the likelier thing to be
wrong; refusing it cost four downstream nodes on a chip that measured cleanly.
qpi-driver/py:fine_amplitude_12refines the EF pi on the ordinary 0-1 readout, bymapping |1> back the way
rabi_12already does, andthree_state_operating_pointnowdepends on it. It used to read at the three-state point and sit behind that node — a
deadlock, since populating |2> needs the refined pi that this node produces. On one chip
it and the three nodes behind it never ran once in six attempts.
qpi-driver/py: a guard that can tell a poor measurement from no measurement now passesthe poor one and marks it degraded, rather than refusing and taking every node downstream
with it.
require_resolved_curvetakes its floor from what noise fakes over that manypoints instead of a fixed 3x — noise reaches 3.5 at 21 points and 1.8 at 81, so the
constant was wrong in both directions — and
three_state_operating_pointfrom theseparation at which its closest pair reaches
MIN_ASSIGNMENT_FIDELITY. Onerabi_12at2.5x had been costing four nodes that each carry their own guard.
qpi-driver/py: a schedule whose pulses outlastroutine_timeout_sraises its ownwait rather than failing, and says so. The ceiling bounds a sequencer that never
stops; a 59 s punchout under a 30 s ceiling was failing for being large.
qpi-driver/py: a routine running several schedules under one ceiling is judged ontheir summed allowance rather than the last one's.
qubit_spectroscopy's search isthree acquisitions, and the last alone would fail a routine that never exceeded its
allowance once.
qpi-driver/py: every routine reads the device before its acquisition rather thanafter it (RFC 0007 §11). Six nodes read a parameter in
analyse, which describes asweep that had already happened and is too late to check as a prerequisite.
qpi-driver/py: a routine declares the device parameters itreads, the counterpartof the
updatesit already declared (RFC 0007 §11). A test derives the true set froman instrumented
read_pathand fails a declaration that is short of it.qpi-driver/py: the walk skips a routine whose input this run failed to produce,naming the routine to blame, instead of measuring an uncalibrated chip (RFC 0007 §11).
One failed
qubit_spectroscopycost six runs of debugging six downstream nodes thathad each fitted the noise of a qubit still in its ground state.
repo: Cleaned up and refactoredMakefile.repo: Cleaned up.github/workflows/ci.yml.qpi-driver/py: Optimizedtest-py-loopexecution speed with@functools.lru_cacheto_cached_scqubits_eigenvalsintransmon.py.Fixed
qpi-driver/py:rbpins its decay asymptote at1/2^ninstead of fitting it, whichis what makes the rate measurable — with it free the amplitude and the rate are
inseparable and the answer comes off whatever bound stops the fit.
t2_echocan nolonger widen its delays past six times T1, where a rising signal is drift rather than an
echo.
qpi-driver/py: a Hahn echo is flagged past 2.4x T1 rather than 3x, and a spectroscopyline fitted wider than the window it was swept in is flagged too. Both were reporting
numbers no measurement supports — a T2 of exactly 3x T1, and a 107 MHz linewidth across a
20 MHz sweep — without saying so.
qpi-driver/py:readout_integration_timestops the window at the readout pulse plus acouple of resonator ring-down times, rather than at the instrument's limit. Integrating
past the pulse adds noise with no signal; on one chip it chose twice the pulse length and
every magnitude-based node lost contrast for it.
qpi-driver/py:fine_amplitude_12can act on the shortening its own refusal asks for,as
fine_amplitudealready could. Without it the refusal named a remedy nothing appliedand the node could never run on a chip whose ef sweep overran.
qpi-driver/py:t2_echosnaps its delays so that half of one lands on the hardwaregrid, which a window scaled from a measured T1 otherwise misses — the schedule compiled
until qblox refused a time value.
qpi-driver/py:fit_rb_decayrefuses an amplitude a hundred times its own span ratherthan two hundred. A degenerate fit was squeezing under the old wall and reporting a
per-gate error three orders below what AllXY measured on the same chip.
qpi-driver/py:fine_amplitudejudges whether the amplified rotation overran from thespan of the data rather than from its own fitted slope, which under-reported by exactly
the amount that made the guard necessary. A per-pulse error that does not clear its own
standard error is now written as no correction instead of as noise.
qpi-driver/py:f12_spectroscopyaccepts ananharmonicity_rangeoverride, so atransmon deliberately built outside the usual -400 to -150 MHz is a config fact rather
than a refusal.
qpi-driver/py:rbscores survival against measured|0>andX|0>referencesinstead of scaling to the sweep's own extremes, which forced one depth to exactly 0 and
another to exactly 1 and could not tell a decay from a rise. No circuit count could fix
it — the endpoints were arithmetic.
qpi-driver/py:t2_echosizes its delays from the measured T1 rather than a fixed100 us window. A Hahn echo can reach
2*T1, so a fixed window truncates the decay on anychip whose T1 outruns it and
fit_t2then refuses a decay more shots cannot bound.qpi-driver/py:fit_dragrefuses a sweep whose rise across its whole beta range isunder three times the scatter about the fitted line. The root of a line through noise
landed inside the sweep and was written to every pulse afterwards.
qpi-driver/py: the 1-2 ladder prediction was out by exactly two —EF_ENVELOPE_AREAread quantify's
nr_sigmaas spanning the whole DRAG pulse rather than each side ofcentre. It refused pulses sitting on the ladder; a test now pins the constant to the
integrated waveform instead of to the arithmetic.
qpi-driver/py:fine_amplitudeandfine_amplitude_90refuse a sweep no straight linepasses through. The demodulated signal is bounded at one, so its scatter has an absolute
scale — a chip whose points sat 0.35 off their own fitted line still reported a quarter
turn correct to 0.03%, and wrote the amplitude every gate afterwards uses.
qpi-driver/py:ramseymeasures which of the fringe's two roots is the chip's insteadof assuming the smaller one. A fringe is a magnitude, so the artificial detuning only
signs the correction while it is the larger of the two — past that
ramseymoved f016.18 MHz off where the other root sits 60 kHz from the chip's working value.
qpi-driver/py:three_state_operating_pointsizes its frequency sweep from the measuredresonator linewidth instead of a 6 MHz constant, at the 1.8 linewidths that constant
encoded. On a 327 kHz resonator 6 MHz is eighteen linewidths, so four of its five points
sat where nothing comes back — the same way a constant span once broke
readout_operating_point.qpi-driver/py:t2_echorefuses a T2 above the2*T1ceiling a Hahn echo cannotexceed, and
t1now keeps its result on the element for it to read. A chip reported201 us of T2 against a 32.8 us T1 — 3.07x the ceiling — and every other guard passed it.
qpi-driver/py:rabino longer accepts a pi amplitude up to 10% above the top of itsown sweep. A chip fitted 0.5060 against a sweep stopping at 0.5 and wrote it, where
ef_laddermeasured 0.1647 on the same grid — every node downstream then calibratedagainst a pulse turning three times too far. It escalates to a wider sweep instead.
qpi-driver/py: the CZ's virtual-Z corrections cancel the phase the gate leaves insteadof doubling it.
conditional_phasewrote the measured fringe phase where it needed minusit, so every CZ left 151.7 degrees on the control and
interleaved_rbcame back asscatter. The conditional phase itself was unaffected, being a difference of two fringes.
qpi-driver/py: an error message reaches QPI-UI without the Q1ASM program a library mayhave embedded in it. qcodes puts the value being set into a failed set's message, and the
value quantify sets on a sequencer is its program — so one
Assembly failedproduced a2.4 MB error, a 1.37 MB payload, and a record the server refused for exceeding the 1 MB
its JSON field takes. The calibration had run; its request stayed
runningfor ever.qpi-driver/py:drag,fine_amplitude_12,resonator_punchoutandflux_spectroscopycan actually be widened. Each kept its setpoints under a name escalation does not look
for, so widening found nothing and the refusal named the range already swept —
dragfailed run after run with an optimum of -0.614 against a swept +/-0.2 and never widened.
qpi-driver/py:fine_amplitude_90shortens onto odd repetition counts, and noshortening goes below the four points a fit takes. It cut its ladder to two and then
refused for having two, spending its retry to complain about its own sweep.
qpi-driver/py:resonator_punchout,flux_spectroscopyandqubit_spectroscopyrun a2-D grid as one schedule per group of rows.
MAX_SWEEP_POINTSbounds the points in asweep, and a 2-D schedule is rows times points — eleven rows of a 700-point sweep is nine
times the instructions a sequencer takes.
qpi-driver/py: the timeout warning names the duration it actually compares. "needs256.0s of pulses, more than the 300s ceiling" was self-contradictory: 256s rounds to the
instrument's 60s grid and gains a minute for arming, so what did not fit was 360s. It now
says so, and names the 240s the pulses have to come under rather than the 300s ceiling.
qpi-driver/py: RB runs a sweep too large for one schedule as several and combines them,instead of capping how hard it may average. A sequencer takes 12288 instructions and RB's
cost is per gate, so 2413 Cliffords compiled to 1.13 MB of Q1ASM and would not assemble.
The split is exact: the mean of a partition is the mean of the whole.
qpi-driver/py:ef_laddermeasures the sqrt(2) ladder directly, playingrabi_12's ownpulse on the 0-1 clock so the envelope and duration cancel. It writes nothing; it says
whether an ef amplitude that misses the prediction misses the ladder.
qpi-driver/py:t1andt2_echowiden their delays when the fitted coherence timeqpi-driver/py: a benchmark that runs its own measurement loop reachesreport.benchmarks.It previously appeared in
routine_resultsand nowhere else, so it looked like it had runwhile the drift check compared against nothing.
qpi-driver/py: a fit refused for scatter carries the sweep it refused, as the otherrefusals already did.
qpi-driver/py:t2_echoandt1widen their delays when the fitted coherence timelands past the window, instead of refusing. A chip fitted 2.12 ms of T2 over a 100 us
sweep and failed, because that guard named no axis for escalation to act on.
qpi-driver/py:rbandinterleaved_rbaverage more circuits per depth when the decaycannot be told from the scatter around it. "Average more circuits per depth" was already
the advice the refusal gave, and nothing acted on it.
qpi-driver/py:dragwidens its beta sweep when the optimum lies outside it, asdrag_12already did. A chip whose optimum was -0.4803 against a swept +/-0.2 refused afit that had found its answer, leaving every node after it on an uncorrected pulse.
qpi-driver/py:fine_amplitudeandfine_amplitude_90shorten their repetition countswhen the amplified rotation outruns the linearisation, instead of failing. How many
repetitions the fit can take depends on the error it is measuring, so no default is right
in advance.
qpi-driver/py: an RB fidelity fitted off a straight line is refused. The amplitude isbounded to 200x the survival's own span and a fit that reaches that stop is rejected: a
chip reported 0.9999887 and 0.9999978 — thirty to three hundred times better than its T1
allows — from an amplitude of -807 and -4109 on a survival normalised to [0, 1].
qpi-driver/py:rabi_12's ladder guard accepts a resolved oscillation however far off thesqrt(2) ladder it sits, and refuses only a sweep holding less than one period. It exists to
catch a cosine fitted to a partial rotation, which shows fewer oscillations than the sweep
and never more — it had been refusing a clean three-and-a-half-period measurement.
qpi-driver/py: the fine-amplitude fit takes an intercept instead of being pinned throughthe origin, and refuses a sweep whose rotation accumulates past a radian. Two runs of an
unchanged pi/2 pulse reported errors twelve times apart because a real baseline offset was
being absorbed into the slope.
qpi-driver/py: an EF pulse defaults to the length of the 0-1 pulse rather than to a20 ns constant, and
rabi_12's ladder guard scales by the two durations instead ofassuming they match. Against an
rxy.durationof 56 ns the old default put the 1-2 pi at2.8x the 0-1 amplitude, past the top of the sweep, and the guard blamed the drive.
qpi-ui: the fidelity card shows the measured gate fidelity rather than the lowest numberin the payload. A run's
readout_fidelityof 92.5% was displayed as being below the 99.9%one-qubit gate threshold while randomised benchmarking sat unread beside it.
qpi-driver/py:rabi_12maps the qubit back to the ground state before measuring, so the1-2 oscillation appears in the population the readout is tuned to resolve. It previously
asked a 0-1 discriminator to tell the two upper levels apart, and fitted a pi pulse six
times too small from a trace that barely moved.
qpi-driver/py:ramseyre-measures after correcting the qubit frequency, until the residualdetuning is below what its own sweep can resolve. A single pass measured the detuning with
the uncorrected frequency in the drive, so it landed near the answer rather than on it.
qpi-driver/py: a reported fidelity is the worst of the protocols that measure a gatefidelity, and
allxy_check's diagnostic score no longer outvotes it. The default 0.999threshold demanded an AllXY rms of 0.001, so a drift check with AllXY enabled fired on every
run of every chip.
qpi-driver/py:three_state_operating_pointrefuses a point whose closest two clouds itsown consumer would reject, instead of writing one and letting
three_state_discriminationfail.qpi-driver/py:drag_12widens its beta sweep when the optimum lies outside it, and awidened sweep that is symmetric about zero stays symmetric — it previously dropped the whole
negative half, which is where
draghad measured its own optimum.qpi-driver/py:rabi_12refuses a 1-2 pi amplitude that the measured 0-1 one says cannotbe one. A cosine fitted to a partial rotation reports a smaller amplitude with no sign
anything is wrong, and the whole EF chain then measured a qubit still in the first excited
state.
qpi-driver/py: a widened drive-amplitude sweep stops at full scale instead of asking theAWG for more than it has.
rabistarting at half scale and escalating past it compiled toa gain of 1.05, which the compiler refused while naming a pulse rather than the routine.
qpi-driver/py:qubit_spectroscopy's widening pass drives at the strongest power therun would try anyway, derived from
drive_amps, rather than a constant that fell out ofstep with it. Raising
drive_ampspreviously left the search probing weaker than the passit exists to feed.
qpi-driver/py: a sweep axis written as4e-9reaches the schedule as a number ratherthan the string PyYAML actually parsed it to, and a device config frequency written as
5.318e9loads as one too. Both forms need a decimal point and a signed exponent to benumbers, which is invisible on the page.
qpi-driver/py:calibration.example.ymlreached full readout scale inresonator_punchout, sets DRAG in seconds rather than ten orders of magnitude out, andspells its exponents so YAML reads them as numbers.
qpi-driver/py: 22 routines now declare the qubit frequency and pi-pulse amplitude theirgates need, so a failed
qubit_spectroscopyskips everything behind it. One deadfrequency previously produced eight separate failures, each looking like its own fault.
qpi-driver/py:resonator_punchoutsweeps readout power to full scale rather thanstopping at half, so punch-through is reachable on an attenuated readout line. On a chip
with 20 dB of output attenuation the old ceiling was ~26 dB short of finding it.
qpi-driver/py: a two-qubit routine is skipped when either of its qubits failed tocalibrate, rather than measuring a gate through an endpoint that was never brought up.
qpi-driver/py:resonator_spectroscopywidens and re-runs its own sweep when theresonator is outside the window, or samples it harder when the line is thinner than the
grid. The root of the calibration graph previously stopped the whole chip instead.
qpi-driver/py: an escalating sweep is capped at 700 points rather than 900. A frequencysweep costs three operations per point, measured at 15 Q1ASM instructions, so 900 built a
program over the sequencer's 12288-instruction ceiling — which quantify only warns about.
qpi-driver/py:qubit_spectroscopy's confirming sweep holds the resolution the narrowpass asks for instead of a fixed 41 points, so a line the search power-broadened is no
longer refused for being thinner than the step of the sweep sent to measure it.
qpi-driver/py: a readout whose single shots are assigned little better than by chance isrefused rather than written.
readout_operating_pointpreviously wrote an operating pointit had measured at 53% assignment fidelity.
qpi-driver/py:allxyandallxy_checkrefuse a response whose own reference plateausare indistinguishable, instead of normalising noise to full scale — which had
allxy_checkreporting a fidelity of 0.53 to the drift check.
allxy_checkalso now normalises the wayallxydoes, rather than by min and max, which inverts on half of all readout chains.qpi-driver/py: the simulator'srabi,t1,t2_echoandramseycarry the drivedetuning, so a wrong
clock_freqs.f01costs a calibration its contrast. They builttheir Hamiltonian on resonance whatever the device was configured for, which is why no
suite could fail for the reason a chip 302 MHz out of config did.
qpi-driver/py: the spectroscopy roots judge a fitted line by how far its curve travelsagainst the scatter around it, not by a signal-to-noise floor. 16% to 55% of pure-noise
fits cleared the old floor of 3, because that ratio divides a fitted parameter by the
residual and an optimiser can inflate it without limit.
qpi-driver/py:qubit_spectroscopyonly lets a drive power that shows a line set thebroadening reference the other powers are judged against. A row that converged, cleared
the sweep step and still showed nothing became the narrowest, and the 2x bound then
rejected every power that did show the line.
qpi-driver/py: a calibration finds a chip known only from its design document, withno sweep supplied. The fixture claims f01 = 5.0 GHz against a transmon at 5.21 GHz, and
the driver recovers it to under a megahertz.
ramsey's default sweep is sized for bothits constraints at once, and
rabireaches past half scale only when the fit says thepi pulse is above it.
qpi-driver/py:t1,t2_echoandramseylengthen their delays and try again whenthe fit says the decay was never seen in the window, rather than failing. Bounded at
three attempts, and an operator who named the delays themselves is not overruled.
qpi-driver/py: the two excited-state resonator sweeps size their spans from themeasured linewidth rather than a 20 MHz constant, which was 6 linewidths on one chip
and 54 on another.
qpi-driver/py:f12_spectroscopyrefuses an anharmonicity that is not a transmon's,in its prior and in what it fits. A device file carrying
f12 = 4.8e9against an f01near 4.7 GHz implied a positive anharmonicity on four of five qubits, and nothing
objected.
qpi-driver/py:readout_operating_pointsizes its sweep from the measured resonatorlinewidth rather than a 2 MHz constant. That constant was 5.4 linewidths on a 370 kHz
resonator, which put its outer setpoints off resonance altogether and it chose one.
qpi-driver/py: aCalibratedTransmonkeeps the resonator linewidthresonator_spectroscopymeasured, and the two resonator checks judge against itinstead of a 2 MHz constant (RFC 0005 §13). On a chip whose resonator is 370 kHz wide
that constant was five times too wide, and nothing recorded the measured value.
qpi-driver/py:rabisweeps drive amplitude to full scale rather than to half of it,so a pi pulse above 0.5 can be found.
require_in_rangecannot catch one that ismissing for being too large, since it checks the fitted value lies inside the swept
range, and a chip whose own calibration used 0.5683 returned a flat Rabi every run.
qpi-driver/py:qubit_spectroscopysizes the sweep that confirms a searched-out linefrom the width the search measured, rather than reusing the operator's span. That span
said where to look, and once the search has answered it is spent — re-centring a
600 MHz window steps 10 MHz across a line about as wide.
qpi-driver/py: every spectroscopy sweep is trimmed to the frequencies its port canactually be driven at — its LO plus or minus the module's intermediate-frequency
limit. Asking outside it failed compilation with
Attempting to set NCO frequency,naming neither the routine nor the setpoint.
qpi-driver/py: the simulated backend carries the allowance the DAG judges a routineby, so the whole simulated calibration walks again. Without it every node of it died
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_ensurvive a reconnect, so the driverinherited whatever the last process left emitting — which held this chip's qubit in a
mixture that made X the identity, and deadlocked
wait_syncbefore that.qpi-driver/py: a quantify tuner or executor stops the cluster after every run,including a failed one. Only
stopclearssync_enon the modules a schedule didnot use, so one left in the sync network by an earlier routine hung every later one
on
wait_sync, at anyroutine_timeout_s.qpi-driver/py: the quantify tuner finds its cluster again.InstrumentCoordinator.componentsholds component names, so reading.instrumentoff them found nothing and
coupler_anticrossingcould not open a bias source insidethe cluster.
qpi-driver/py:ramseyrounds its delays to the 1 ns grid, asramsey_12alreadydid. Its default sweep steps 249.9 ns and the node never compiled.
qpi-driver/py: the spectroscopy roots refuse a line no more than 3x above theresidual scatter, and
resonator_spectroscopyis guarded at all. A 1.53 MHz fit atsnr 1.32 wrote an f01 5 MHz out, which put
ramsey_121.5 MHz off and cost the run.qpi-driver/py: the two excited-state resonator sweeps are guarded too, and reportthe ground frequency they differenced against plus their own spectrum. A stale
reference had them reporting a 186 kHz dispersive shift on a chip whose real shift
was under 1 kHz.
qpi-driver/py:rabirefuses an oscillation no taller than the scatter it wasfitted through, rather than writing the
amp180it implies. A first sweep through astarved readout wrote 0.0134 where the chip's calibrated value was 0.5683, and the six
runs after it played an X pulse that rotated five degrees — leaving every 0-1
measurement in the graph blind, and self-perpetuating, since a dead X gate guarantees
the next Rabi sweep is flat.
qpi-driver/py:t1,t2_echoandramseyrefuse a curve no taller than thescatter it was fitted through.
require_in_rangeallowed a time constant ten timesthe window, so a flat sweep returned T1 = 169 us from a rising curve, and a ramsey
with no fringe moved f01 by 173 kHz.
qpi-driver/py:fine_amplitudeandfine_amplitude_12refuse a demodulated sweepfar past the bound their own model sets, rather than writing the
amp180it implies.A readout that was not resolving the qubit gave a sweep reaching 144 where one is the
maximum, and the slope through it set the amplitude every X pulse used.
qpi-driver/py:rbandinterleaved_rbrefuse a decay no deeper than the scatterit was fitted through. Three consecutive runs reported 0.99999, 0.941 and 0.586 from
non-monotonic noise, and the drift check compared them against a threshold.
qpi-driver/py:resonator_spectroscopy_excitedrefuses a dispersive shift under 5%of the resonator linewidth, which no readout can resolve. A chip whose
f01sat ananharmonicity away from its real transition reported 0.05% to 0.5% for six runs, while
every node after it fitted the noise of an idle qubit.
qpi-driver/py:qubit_spectroscopywidens to a 600 MHz search when no line turns upnear the configured
f01, rather than refusing — the configured value is a prior, notan answer. A chip 302 MHz from its design frequency gave six runs of "no drive power
resolved a line", and the operator had to supply by hand the number the node measures.
qpi-driver/py:flux_spectroscopyandcz_chevrondecline a chip whose fluxreaches the couplers rather than the qubits, instead of failing with
KeyError: 'q0:fl was not found in the connectivity.'.cz_chevronwas missing thearchitecture test both its parametric counterparts already make.