Skip to content

Hotfix/clean up the single-qubit DAG for calibration driver - #47

Merged
ch-ahindura merged 130 commits into
sopherapps:mainfrom
ch-ahindura:hotfix/code-cleanup
Aug 16, 2026
Merged

Hotfix/clean up the single-qubit DAG for calibration driver#47
ch-ahindura merged 130 commits into
sopherapps:mainfrom
ch-ahindura:hotfix/code-cleanup

Conversation

@ch-ahindura

Copy link
Copy Markdown
Collaborator

Why

We needed to test the single-qubit calibration part of the builtin driver on a real device

Added

  • qpi-driver/py: readout_integration_time calibrates measure.integration_time by
    sweeping 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_12 carries its trace on success as well as on refusal, and rb
    reports decay_observed — how much of the decay its deepest sequence actually saw, since
    r is extrapolated from the rest. A chip reporting 0.15% error per gate had seen 17.6% of
    a decay, below what its own T1 allows and 34x better than allxy_check on the same run.
  • qpi-driver/py: readout_operating_point reports the magnitude contrast across its
    sweep, 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_of magnitude
    nearly 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 the
    report carries the trace and not only the sentence. It is marked as a refusal and is not
    attributed any parameter.
  • qpi-driver/py: allxy_check reports its normalised response alongside the rms, so the
    21 pairs can be read after the single-qubit chain finishes. allxy runs before
    fine_amplitude and fine_amplitude_90, so it cannot show whether either helped.
  • qpi-driver/py: fine_amplitude_90 measures the pi/2 amplitude and writes it to a new
    fine.amp90 on CalibratedTransmon. Both schedulers derived a pi/2 from amp180 by linear
    interpolation, 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, so rabi_12
    can 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 own timeout_s in calibration.yml, overriding the
    global routine_timeout_s. One ceiling had to be set for the slowest node, so it could not
    also catch a fast one hanging.
  • qpi-driver/py: a quantify routine logs how long its schedule should take before
    running 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 did
    not 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 one
    the 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.yml beside the device config
    recording 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, per
    target 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 when
    they 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_spectroscopy keeps the f12 already measured on a qubit when no
    drive 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 flags
    unresolved, instead of refusing. rb and t2_echo write no device parameter, so a
    wide error bar is a fact about the chip and withholding it published nothing; drag and
    the 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_time sweeps the whole reachable range of windows
    rather 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 a
    resolved sweep on periods and population swing alone — both properties of the sweep
    rather than of any chip. ef_ladder measures the same relation directly, so the modelled
    prediction is deliberately the weaker witness.
  • qpi-driver/py: rabi_12 now judges an off-ladder 1-2 pi by how much population it
    swings — rabi records its own contrast for the comparison — instead of refusing every
    amplitude 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_12 refines the EF pi on the ordinary 0-1 readout, by
    mapping |1> back the way rabi_12 already does, and three_state_operating_point now
    depends 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 passes
    the poor one and marks it degraded, rather than refusing and taking every node downstream
    with it. require_resolved_curve takes its floor from what noise fakes over that many
    points 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_point from the
    separation at which its closest pair reaches MIN_ASSIGNMENT_FIDELITY. One rabi_12 at
    2.5x had been costing four nodes that each carry their own guard.
  • qpi-driver/py: a schedule whose pulses outlast routine_timeout_s raises its own
    wait 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 on
    their summed allowance rather than the last one's. qubit_spectroscopy's search is
    three 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 than
    after it (RFC 0007 §11). Six nodes read a parameter in analyse, which describes a
    sweep that had already happened and is too late to check as a prerequisite.
  • qpi-driver/py: a routine declares the device parameters it reads, the counterpart
    of the updates it already declared (RFC 0007 §11). A test derives the true set from
    an instrumented read_path and 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_spectroscopy cost six runs of debugging six downstream nodes that
    had each fitted the noise of a qubit still in its ground state.
  • repo: Cleaned up and refactored Makefile.
  • repo: Cleaned up .github/workflows/ci.yml.
  • qpi-driver/py: Optimized test-py-loop execution speed with
    @functools.lru_cache to _cached_scqubits_eigenvals in transmon.py.

Fixed

  • qpi-driver/py: rb pins its decay asymptote at 1/2^n instead of fitting it, which
    is 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_echo can no
    longer 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 spectroscopy
    line 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_time stops the window at the readout pulse plus a
    couple 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_12 can act on the shortening its own refusal asks for,
    as fine_amplitude already could. Without it the refusal named a remedy nothing applied
    and the node could never run on a chip whose ef sweep overran.
  • qpi-driver/py: t2_echo snaps its delays so that half of one lands on the hardware
    grid, which a window scaled from a measured T1 otherwise misses — the schedule compiled
    until qblox refused a time value.
  • qpi-driver/py: fit_rb_decay refuses an amplitude a hundred times its own span rather
    than 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_amplitude judges whether the amplified rotation overran from the
    span 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_spectroscopy accepts an anharmonicity_range override, so a
    transmon deliberately built outside the usual -400 to -150 MHz is a config fact rather
    than a refusal.
  • qpi-driver/py: rb scores survival against measured |0> and X|0> references
    instead 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_echo sizes its delays from the measured T1 rather than a fixed
    100 us window. A Hahn echo can reach 2*T1, so a fixed window truncates the decay on any
    chip whose T1 outruns it and fit_t2 then refuses a decay more shots cannot bound.
  • qpi-driver/py: fit_drag refuses a sweep whose rise across its whole beta range is
    under 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_AREA
    read quantify's nr_sigma as spanning the whole DRAG pulse rather than each side of
    centre. 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_amplitude and fine_amplitude_90 refuse a sweep no straight line
    passes 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: ramsey measures which of the fringe's two roots is the chip's instead
    of 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 ramsey moved f01
    6.18 MHz off where the other root sits 60 kHz from the chip's working value.
  • qpi-driver/py: three_state_operating_point sizes its frequency sweep from the measured
    resonator 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_echo refuses a T2 above the 2*T1 ceiling a Hahn echo cannot
    exceed, and t1 now keeps its result on the element for it to read. A chip reported
    201 us of T2 against a 32.8 us T1 — 3.07x the ceiling — and every other guard passed it.
  • qpi-driver/py: rabi no longer accepts a pi amplitude up to 10% above the top of its
    own sweep. A chip fitted 0.5060 against a sweep stopping at 0.5 and wrote it, where
    ef_ladder measured 0.1647 on the same grid — every node downstream then calibrated
    against 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 instead
    of doubling it. conditional_phase wrote the measured fringe phase where it needed minus
    it, so every CZ left 151.7 degrees on the control and interleaved_rb came back as
    scatter. 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 may
    have 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 failed produced a
    2.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 running for ever.
  • qpi-driver/py: drag, fine_amplitude_12, resonator_punchout and flux_spectroscopy
    can 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 — drag
    failed run after run with an optimum of -0.614 against a swept +/-0.2 and never widened.
  • qpi-driver/py: fine_amplitude_90 shortens onto odd repetition counts, and no
    shortening 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_spectroscopy and qubit_spectroscopy run a
    2-D grid as one schedule per group of rows. MAX_SWEEP_POINTS bounds the points in a
    sweep, 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. "needs
    256.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_ladder measures the sqrt(2) ladder directly, playing rabi_12's own
    pulse 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: t1 and t2_echo widen their delays when the fitted coherence time
  • qpi-driver/py: a benchmark that runs its own measurement loop reaches report.benchmarks.
    It previously appeared in routine_results and nowhere else, so it looked like it had run
    while the drift check compared against nothing.
  • qpi-driver/py: a fit refused for scatter carries the sweep it refused, as the other
    refusals already did.
  • qpi-driver/py: t2_echo and t1 widen their delays when the fitted coherence time
    lands 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: rb and interleaved_rb average more circuits per depth when the decay
    cannot 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: drag widens its beta sweep when the optimum lies outside it, as
    drag_12 already did. A chip whose optimum was -0.4803 against a swept +/-0.2 refused a
    fit that had found its answer, leaving every node after it on an uncorrected pulse.
  • qpi-driver/py: fine_amplitude and fine_amplitude_90 shorten their repetition counts
    when 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 is
    bounded 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 the
    sqrt(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 through
    the 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 a
    20 ns constant, and rabi_12's ladder guard scales by the two durations instead of
    assuming they match. Against an rxy.duration of 56 ns the old default put the 1-2 pi at
    2.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 number
    in the payload. A run's readout_fidelity of 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_12 maps the qubit back to the ground state before measuring, so the
    1-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: ramsey re-measures after correcting the qubit frequency, until the residual
    detuning 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 gate
    fidelity, and allxy_check's diagnostic score no longer outvotes it. The default 0.999
    threshold 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_point refuses a point whose closest two clouds its
    own consumer would reject, instead of writing one and letting
    three_state_discrimination fail.
  • qpi-driver/py: drag_12 widens its beta sweep when the optimum lies outside it, and a
    widened sweep that is symmetric about zero stays symmetric — it previously dropped the whole
    negative half, which is where drag had measured its own optimum.
  • qpi-driver/py: rabi_12 refuses a 1-2 pi amplitude that the measured 0-1 one says cannot
    be 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 the
    AWG for more than it has. rabi starting at half scale and escalating past it compiled to
    a 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 the
    run would try anyway, derived from drive_amps, rather than a constant that fell out of
    step with it. Raising drive_amps previously left the search probing weaker than the pass
    it exists to feed.
  • qpi-driver/py: a sweep axis written as 4e-9 reaches the schedule as a number rather
    than the string PyYAML actually parsed it to, and a device config frequency written as
    5.318e9 loads as one too. Both forms need a decimal point and a signed exponent to be
    numbers, which is invisible on the page.
  • qpi-driver/py: calibration.example.yml reached full readout scale in
    resonator_punchout, sets DRAG in seconds rather than ten orders of magnitude out, and
    spells its exponents so YAML reads them as numbers.
  • qpi-driver/py: 22 routines now declare the qubit frequency and pi-pulse amplitude their
    gates need, so a failed qubit_spectroscopy skips everything behind it. One dead
    frequency previously produced eight separate failures, each looking like its own fault.
  • qpi-driver/py: resonator_punchout sweeps readout power to full scale rather than
    stopping 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 to
    calibrate, rather than measuring a gate through an endpoint that was never brought up.
  • qpi-driver/py: resonator_spectroscopy widens and re-runs its own sweep when the
    resonator 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 frequency
    sweep 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 narrow
    pass 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 is
    refused rather than written. readout_operating_point previously wrote an operating point
    it had measured at 53% assignment fidelity.
  • qpi-driver/py: allxy and allxy_check refuse a response whose own reference plateaus
    are indistinguishable, instead of normalising noise to full scale — which had allxy_check
    reporting a fidelity of 0.53 to the drift check. allxy_check also now normalises the way
    allxy does, rather than by min and max, which inverts on half of all readout chains.
  • qpi-driver/py: the simulator's rabi, t1, t2_echo and ramsey carry the drive
    detuning, so a wrong clock_freqs.f01 costs a calibration its contrast. They built
    their 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 travels
    against 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_spectroscopy only lets a drive power that shows a line set the
    broadening 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, with
    no 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 both
    its constraints at once, and rabi reaches past half scale only when the fit says the
    pi pulse is above it.
  • qpi-driver/py: t1, t2_echo and ramsey lengthen their delays and try again when
    the 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 the
    measured linewidth rather than a 20 MHz constant, which was 6 linewidths on one chip
    and 54 on another.
  • qpi-driver/py: f12_spectroscopy refuses an anharmonicity that is not a transmon's,
    in its prior and in what it fits. A device file carrying f12 = 4.8e9 against an f01
    near 4.7 GHz implied a positive anharmonicity on four of five qubits, and nothing
    objected.
  • qpi-driver/py: readout_operating_point sizes its sweep from the measured resonator
    linewidth 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: a CalibratedTransmon keeps the resonator linewidth
    resonator_spectroscopy measured, and the two resonator checks judge against it
    instead 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: rabi sweeps drive amplitude to full scale rather than to half of it,
    so a pi pulse above 0.5 can be found. require_in_range cannot catch one that is
    missing 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_spectroscopy sizes the sweep that confirms a searched-out line
    from 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 can
    actually 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 routine
    by, 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_en survive a reconnect, so the driver
    inherited whatever the last process left emitting — which held this chip's qubit in a
    mixture that made X the identity, and deadlocked wait_sync before that.
  • qpi-driver/py: a quantify tuner or executor stops the cluster after every run,
    including a failed one. Only stop clears sync_en on the modules a schedule did
    not use, so one left in the sync network by an earlier routine hung every later one
    on wait_sync, at any routine_timeout_s.
  • qpi-driver/py: the quantify tuner finds its cluster again.
    InstrumentCoordinator.components holds component names, so reading .instrument
    off them found nothing and coupler_anticrossing could not open a bias source inside
    the cluster.
  • qpi-driver/py: ramsey rounds its delays to the 1 ns grid, as ramsey_12 already
    did. 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 the
    residual scatter, and resonator_spectroscopy is guarded at all. A 1.53 MHz fit at
    snr 1.32 wrote an f01 5 MHz out, which put ramsey_12 1.5 MHz off and cost the run.
  • qpi-driver/py: the two excited-state resonator sweeps are guarded too, and report
    the 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: rabi refuses an oscillation no taller than the scatter it was
    fitted through, rather than writing the amp180 it implies. A first sweep through a
    starved 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_echo and ramsey refuse a curve no taller than the
    scatter it was fitted through. require_in_range allowed a time constant ten times
    the 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_amplitude and fine_amplitude_12 refuse a demodulated sweep
    far past the bound their own model sets, rather than writing the amp180 it 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: rb and interleaved_rb refuse a decay no deeper than the scatter
    it 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_excited refuses a dispersive shift under 5%
    of the resonator linewidth, which no readout can resolve. A chip whose f01 sat an
    anharmonicity 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_spectroscopy widens to a 600 MHz search when no line turns up
    near the configured f01, rather than refusing — the configured value is a prior, not
    an 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_spectroscopy and cz_chevron decline a chip whose flux
    reaches the couplers rather than the qubits, instead of failing with
    KeyError: 'q0:fl was not found in the connectivity.'. cz_chevron was missing the
    architecture test both its parametric counterparts already make.

…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.
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.
@ch-ahindura
ch-ahindura merged commit 3b66dbf into sopherapps:main Aug 16, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant