Feature/calibration driver - #36
Open
ch-ahindura wants to merge 24 commits into
Open
Conversation
RFC 0004 §10 required this be decided explicitly rather than inherited
from the events log's public read. The collections were already
authenticated-read, but the rule in migrate.go still carried the open
question ("Public read (maybe auth only?)") next to code that says
otherwise, and the RFC still posed it as pending.
A public events log leaks that a calibration happened; a public report
leaks the chip.
Two independent density matrices cannot be entangled — not "are not", but cannot be, so a Bell state could not come out wrong because it could not come out at all. `simulation/coupled.py` holds both transmons in one 9-dimensional register and couples them, and the CZ falls out of walking the control through the |11>-|02> avoided crossing: conditional phase 179.3 degrees, Bell concurrence >0.9, leakage <0.5%, none of it written down. Two of these numbers are chosen rather than derived — the exchange coupling and the flux-to-detuning curve — because they describe a coupler this project has no device to measure. That makes two-qubit tier 3 weaker than the one-qubit tier, and both the module and the RFC say so. Driving the real routines through it found three faults: - fit_chevron took the global argmax of the control population. Off resonance the control also reads ~1, so the maximum of that surface is as likely to sit on a row where nothing happened as on the gate. It now finds resonance by oscillation contrast, then the round trip along that row, and refuses a sweep whose largest swing is below 0.15 — the default amplitude grid steps over a 4.5 MHz crossing in ~75 MHz strides and comes back flat, which is the same failure spectroscopy already guards against. - fit_conditional_phase read the zero crossing of the two fringes' difference, which sits at phi0 + phi_cz/2 — half the wanted angle, displaced by the control's dynamical phase over the flux pulse. That phase is tens of turns and is precisely what having two fringes is for. It now fits each fringe and subtracts: 179.2 against a true 179.3. - The correction was `np.pi - crossing` with crossing in degrees. A gate needing 30 degrees back was told to apply -26.86. The tier-1 test asserted `phase_correction == np.pi - conditional_phase` — it encoded the bug as the expectation, which is why none of this surfaced there.
The coordinator held one density matrix per qubit, so a flux pulse could only raise: two independent states cannot be entangled. Qubits now start independent and merge into a joint register the moment a flux pulse couples them, so a one-qubit circuit stays a 3x3 matrix and a two-qubit one is correct. `h; cz; h` comes out a Bell state, and half a CZ does not — which is the control that makes the first mean anything. Getting there needed two things the gate-level model does not show: - A long flux pulse is not a pulse. The qblox backend emits a held DC offset — set to amp at t=0, cleared at 106 ns — plus a 4 ns square tail. Reading the pulses alone applied 4 ns of a gate that ran for 110, which is a quarter turn of an exchange instead of a full one. - Correlation is not in either qubit. Both halves of a Bell pair have a marginal of exactly 0.5, so drawing each qubit's shots from its own marginal reproduces both marginals perfectly and destroys the only thing that made the state interesting. Shots are drawn from the pair's joint distribution, cached per register so the two halves agree on the shot they belong to, and invalidated by gates rather than by any evolution — a readout idles the register between the two halves being read. The resulting Bell state is |01>+|10> rather than |00>+|11>: a CZ leaves a large single-qubit phase behind, and correcting it is exactly what conditional_phase writes cz.phase_correction for. The tests assert |correlation| for that reason, via a Pearson coefficient — "how often do the bits agree" calls a state with lopsided marginals strongly correlated. RFC 0004 §7 and §11 updated: the executor side now interprets the compiled schedule, the routines still do not, and two-qubit tier 3 rests on a coupler whose constants were chosen.
ShiftClockPhase never reached the simulated coordinator's pulse walk, so every rz, z, s and t ran as an identity — silently, since a frame shift plays no pulse and its absence looks like nothing happening. `h; h` and `h; rz(pi); h` returned the same answer. The frame is now tracked per clock and every drive is referenced to it. This is not only about two-qubit gates: it is wrong for any circuit with a virtual Z in it, which is most of them once a compiler has decomposed anything. It is also the mechanism a CZ's per-qubit phase corrections use (FluxTunableCoupler emits one ShiftClockPhase per qubit), so those could not have worked either. Also adds TransmonSimulator.at_f01, which solves EJ for a target frequency by Newton on the real diagonalisation. A chip is not one qubit repeated — the lab's five sit between 4.25 and 5.27 GHz — and a coupler gate cannot be modelled honestly while every qubit is the same transmon. The tests thread delays between phase updates because the compiler refuses two closer than 4 ns: an NCO constraint of the real hardware, surfaced only because these compile for it.
The simulated coordinator answered every protocol with an integrated IQ point, so two of the three measurement levels were wrong in ways a job could not see: - meas_level=0 asks for a Trace, which is a time series. It got a single point, so a raw-waveform job produced a one-sample waveform and looked like it had worked. It is now the readout resonator's response over the integration window, sampled per nanosecond as the digitiser does, and it rings up at the resonator linewidth rather than appearing settled. - meas_level=2 asks for ThresholdedAcquisition, which the *instrument* discriminates and returns as int 0/1. Handing back IQ still produced correct counts, because the executor falls through to discriminating in software — so the path every real job takes went untested while a different one passed. The simulator now applies the hardware's own rule: rotate by acq_rotation, compare the real part against acq_threshold. A device configured with the wrong rotation now produces wrong bits here, exactly as on the bench. meas_level=1 was already right. Tests run one circuit at all three and assert they agree — level 2 says excited, so level 1's IQ must sit on the excited blob and level 0's settled trace must point the same way. A level that disagrees is reporting a different circuit than the one that ran.
The cypress e2e ran against the mock executor, and the quantify path it could have used was pinned to `is_dummy=true` — a dummy cluster that returns nan for every acquisition. Neither can show one measurement level behaving differently from another, which is where the dashboard's job results have actually gone wrong. QPI_E2E_SIMULATED=1 now switches install_driver to pull the `sim` dependency group and start_driver to pass `is_simulated=true`. The dashboard spec sets it and runs a quantify driver. The device config is generated rather than reused: the checked-in fixture is deliberately 214 MHz off resonance so the calibration tests have a qubit to find, and a job console driven by an uncalibrated chip returns wrong counts for every circuit — which reads as a broken dashboard rather than an uncalibrated one. simulated_device.py writes the same fixture with the parameters a successful calibration would have produced. Also adds `is_simulated` to the driver catalog, which only offered `is_dummy` — so the registration form could not offer the simulator even though both builders have read the option since 620023e. The catalog's cross-check test listed the calibrate driver's options by hand and had gone stale in the same way; it now agrees with calibrate.py again.
Three things, all found by driving the FluxTunableCoupler path rather than the CompositeSquareEdge one the tests had been using. The coupler's CZ never reached the simulator. Its pulse goes on the coupler's own port — q1_q2:fl — and `compile_cz` lowers the gate into a fresh subschedule, so the gate-level device_elements is gone by the time the pulse arrives and the pair could not be identified. The port names the edge, which is the reliable statement of which two qubits a coupler sits between, so that is what it reads now. A qubit's own flux port still takes its partner from the gate. The coupler now carries its DC parking current, validated to the +/-3.1 mA the couplers are characterised over. It is deliberately not scheduled: a seconds-scale bias from an SPI rack's S4g is not a pulse, and quantify-scheduler has no notion of one. But it is calibrated state — a coupler parked at the wrong current has the wrong effective coupling and every CZ across it is wrong — so it belongs in the device file beside the pulse parameters. A `source` parameter records whether SPI or a QCM delivers it. The pulse is a SoftSquarePulse now, matching the tergite instruction this element was ported from. A hard square's edges are broadband and carry power at the 1-2 transition a CZ spends its whole duration avoiding. And the dashboard's IQ plot mapped both axes onto a hardcoded -0.5..1.5 window. IQ comes back in whatever units the readout chain produces, so that window is wrong for most devices — against the simulator both clusters land outside it and the tab renders empty. It scales to the data now, over one square window so the distance between the blobs, which is the readout fidelity, is not stretched.
A `FluxTunableCoupler` does not push a qubit onto the |11>-|02> crossing with DC flux. It parks the coupler with a bias current and then modulates it at microwave frequency, and a sideband of that modulation bridges the gap. Two things follow that the DC model could not express: - it works with qubits nowhere near each other, which is why the lab's five sit 0.5-1 GHz apart and still have CZs between them; - frequency is the *resonance condition*, not a carrier detail. A drive 50 MHz off the transition compiles, plays, and performs no gate. There is now a test for exactly that, and it could not have failed under a model that ignored the clock. The exchange rate is not invented. The lab's four calibrated edges imply 4.29, 1.91, 4.91 and 6.41 MHz per unit amplitude from their own (amplitude, duration) pairs, reading each duration as one round trip; the model uses their mean, so a device config taken off the bench lands near a real CZ here instead of nowhere. And the Bell state is |00> + |11> now, not merely *a* maximally entangled pair. The coupler Stark-shifts both qubits while it drives them and the phase that leaves is single-qubit, so no amount of tuning amplitude or duration removes it — only the edge's two <qubit>_phase_correction parameters do, which is what they are for and why this element carries them although the tergite instruction it was ported from does not (qpi goes through quantify's gate library, where there is nowhere else to put them). Zeroing them still gives a perfectly entangled pair; it just gives the wrong one, and that is the control test. Getting there needed the virtual-Z fix to have landed first: the corrections are ShiftClockPhase operations, and until d9543dc those did nothing at all.
The job-console specs checked that a job submits and that the panel names
the right QPU. They passed whether or not the numbers in it meant
anything, which is the wrong kind of green for the one place these bugs
were actually seen.
Four specs now assert the numbers. Level 2 reads the histogram bins and
requires the weight to sit on |00> and |11> — the form's default circuit
is a Bell state, so that is an end-to-end check of the CZ through the
compiler, the simulator and the readout. Level 1 requires many IQ points
rather than one, and requires them inside the viewBox, which is what the
hardcoded axis range broke. Level 0 requires a curve over many samples.
On the weight rather than on which bars exist: a real Bell state still
puts a few shots in |01> and |10>, and a bar is drawn for each.
Two things had to be fixed to make them meaningful:
- The device the e2e generates calibrated the qubits and left the
couplers at fixture defaults, so the CNOT was not a CZ and the
histogram spread over all four outcomes. Both edge types are calibrated
now, each by its own mechanism — the coupler edge parametrically, the
qubit-port edge with DC flux — and both get their two phase
corrections, without which the gate is conditional-phase correct and
still builds the wrong Bell state.
- The Meas Level slider is a React controlled input, so `.invoke("val")`
moved the thumb while leaving the component on the old value. Every
test would have run at the default and measured the same thing. It goes
through the native setter now, and asserts the value landed.
The level-0 spec uses a one-qubit circuit because a Qblox module can put
only one sequencer in scope mode: raw trace on two qubits does not
compile. Worth knowing — the form's default circuit measures both, so
selecting level 0 in the dashboard fails before the job runs.
A tunable coupler needs two things on two timescales, and only one is a pulse: the CZ, a few hundred nanoseconds compiled into the schedule, and the parking bias, a DC current that holds the coupler at its operating point for as long as the fridge is cold. quantify has no way to express the second. Its hardware config describes clusters and nothing else — there is no SpiRack type in backends.types.qblox at all — so the bias cannot be part of a schedule even in principle, and if the driver does not set it directly then nothing does and every CZ runs against a coupler parked wherever it was left. So `coupler_bias.py` sets it out of band at startup, through whichever mechanism the edge names: - `spi`, an S4g current source over qcodes, which is what the lab uses. Ramped rather than stepped: an S4g can slew fast enough to put a transient in the coupler's loop, and a parking current is meant to be the thing that is not moving while a gate runs. - `qcm`, the same current held as an offset on a baseband cluster output, which takes an instrument out of the rack. Switching is a config change, not a code change, which was the ask. The refusals are the interesting part. A chip whose couplers disagree about the mechanism is rejected — one rack cannot be two things, and picking one silently would park half the chip through hardware that is not wired to it. The QCM path refuses to guess a line resistance, since guessing turns a current into the wrong voltage and every CZ across that coupler is wrong in the way that looks like drift. And a chip with no tunable couplers needs no rack and no address, because most here have none. Failing to park logs and continues rather than refusing to start: a coupler that cannot be biased is a broken two-qubit gate, not a broken node, and taking the whole QPU out for it would be the wrong trade.
A Qblox module can put exactly one sequencer into scope mode, so asking two qubits for a raw trace in one schedule does not compile — "Only one sequencer per device can trigger raw trace capture". That made meas_level=0 unavailable for any circuit measuring more than one qubit, including the dashboard's own default, which fails before the job runs. The constraint is per module and per *run*, not per job, so the fix is to take the traces in separate runs: the circuit is played once per measured qubit with a single scope-mode acquisition each time, and the traces are merged. Each pass carries one qubit's variable on channel-suffixed dimensions, so there is nothing to collide when they come back together. This is what the instrument allows and what a lab does by hand. The cost is real and unavoidable — N runs for N qubits, because the shots are repeated per qubit rather than shared — so it is logged rather than hidden. The dashboard e2e now exercises level 0 on the form's default two-qubit circuit instead of being given a narrower one to keep it compiling.
The qblox tier-2 test asserted only that a routine *built* a schedule, while the quantify one compiled it — so a routine whose schedule would not compile under qblox passed. `HardwareAgent.compile` exists, so both backends are held to the same standard now, and the first thing it caught was real: `drag` swept its parameter over -1.0 to 1.0. The DRAG parameter is in seconds — it scales a time derivative of the pulse envelope, so its size comes from the pulse, not from an amplitude. A 20 ns gate wants order 1e-11, which the lab's own device file agrees with: -5.4e-11 and -1.5e-11. At the old scale the derivative term dwarfs the carrier, the waveform exceeds full scale, and the schedule does not compile at all — so the routine could never have run on hardware. Empirically it compiles below about 2e-8. The default now sweeps ±2e-10, which brackets the lab's values with room. The same wrong scale was in the test's own sweep override, which is why neither backend's tier-2 noticed the units even when quantify did compile. Also ports the multi-qubit raw trace split to the qblox executor: the scope-mode limit is a property of the module, not of the scheduler driving it, so qblox hit it too. `measured_qubits` moves to the shared qiskit utils rather than qblox importing from quantify. Adds the sim environment to CI, which ran neither test-py-sim nor test-py-loop — so the tier-3 physics and the whole calibrate/execute loop had never run there.
`make test` was failing at test-docs-static with 17 problems claiming --qpi-addr, --token and --ca-fingerprint are not flags the CLI accepts. They are. The target passed alone and failed inside `make test`, which is the shape of an environment left behind by an earlier target. check_docs forces PYTHONPATH at the source tree so `qpi_driver.cli` imports whether or not it is installed. Without typer that import gets a stub, which prints a near-empty help and exits 0 — and `cli_flags` only skips on a non-zero exit, so it believed the stub and reported the whole documented CLI as removed. Two changes, because the target should not have been able to drift and the checker should not have believed the result: - `uv run` now names `--extra cli` as well as the sync before it, so the environment is a property of this target rather than of whatever ran first. Starting from a quantify-only environment it now checks all 138 claims instead of failing. - A help with fewer than four flags across every subcommand is not a help worth checking against. It is treated as "the CLI could not be run here" — the warning-and-skip path that already existed for an absent toolchain — rather than as evidence that every flag was deleted.
qblox used to refuse the flag. The reasoning was that its HardwareAgent compiles and runs in one call, so quantify's coordinator has nowhere to plug in — true of `run`, and beside the point: the agent's `compile` is offline and needs no instrument at all. So `SimulatedAgent` keeps a real agent for everything structural and takes over only execution, handing the compiled schedule to the same simulator quantify uses. The compilation stays real, which means a schedule that would not assemble for a cluster does not assemble here either. quantify-scheduler is being deprecated, so this is the path that has to keep working rather than the one that can stay a stub. Making one simulator serve both meant naming four places where the two schedulers disagree, all previously invisible because only one was ever exercised: - `pulse_info` and `acquisition_info` are a list of dicts under quantify and a single dict under qblox; - a subschedule is a `Schedule` with `.operations` under quantify and a `TimeableSchedule` carrying `operation_dict` in `.data` under qblox, nested several deep where quantify has one level; - a drive amplitude is `G_amp` under quantify and `amplitude` under qblox; - `QuantumDevice.edges` is a *method* under quantify and a *dict* under qblox, and a bias submodule's parameters are callables under one and plain values under the other. Both of those were found by the qblox coupler parking failing with "'dict' object is not callable" — caught, logged, and given up on, which is exactly the silent failure the catch-and-continue was meant to allow for a broken rack rather than for a bug. Also fixes a latent fragility in both device loaders: elements were added in file order, so an edge listed before either of its qubits failed to load. Anything that rewrites the YAML alphabetically — `yaml.safe_dump`'s default — produced a config that could no longer be read. Elements are added before edges now, whatever the order. And `make test` was red on macOS for an unrelated reason: uv sync reinstalls qblox_instruments, macOS strips the signature from its bundled q1asm assembler, and only three of the eight targets that sync put it back. The step is one definition now and every syncing target uses it.
test_calibration_loop is parametrised over quantify and qblox rather than duplicated, so every claim it makes — the calibration finds the chip, the executor plays what was written, virtual Z rotates the frame, a CZ entangles, all three measurement levels agree — is proved for both. 23 tests became 46. Parametrising rather than copying is the point: a claim proved for one scheduler and quietly untested for the other is exactly how qblox came to be a stub. Turning it on found four things, all of which meant the qblox calibrator had never completed a calibration: - The write-back walked `device.elements()` and `device.edges()`, which are methods under quantify and dicts under qblox. So it raised, the report came back partial_failure, and no qblox calibration had ever reached the device file. - What it wrote, qblox could not read back. Edges were written with positional constructor arguments, and qblox's edges are pydantic models — pydantic takes no positional arguments at all. They are keyword now, which both schedulers accept. - It also wrote `edge_type`, `parent_element_name` and `child_element_name` as if they were calibration. They are structural and already in `element_type`, and re-emitting them makes the loader *assign* to a discriminated field, which pydantic refuses. Same for `element_type` and `name`, which qblox carries as ordinary fields and which overwrote persistence's own class-path mapping with a bare string. - Both loaders added elements in file order, so an edge listed before either of its qubits failed. Anything that rewrites the YAML alphabetically produced an unloadable config. And `conditional_phase` applied nothing at all. It wrote `cz.phase_correction`, a name *neither* scheduler has — quantify names them after the qubits, qblox after the roles — behind a `hasattr` guard that was therefore never true. The names are resolved from the edge now. Fixing that exposed what the routine was missing rather than merely mis-spelling: the two corrections cancel each qubit's *single-qubit* phase, which is not the conditional phase and is not derivable from it. The routine measures four fringes instead of two — a Ramsey on each qubit with the other down and up — so both phases are measured rather than one measured and the other assumed. `make test-py-loop` and the CI sim job now install both schedulers; installing one silently skipped half the file.
A failed job rendered nothing at all. The status badge went red and the
three result tabs each reported "No counts data available" — true, and
useless — while the reason the driver had already sent sat unread in the
record. Hunting through three empty tabs for an explanation that was never
going to be in any of them is worse than a wall of text.
The reason is shown in place of the tabs now, which are hidden because
none of them has anything to display. Compiler errors are the common case
and they are long: both schedulers attach a paragraph of guidance to a
one-line problem, and that paragraph is usually the fix. So the first line
leads and the rest is kept verbatim rather than truncated.
A job that failed with no reason at all says so, and points at the driver
logs, rather than looking like a job with no results.
The spec drives a real refusal rather than a contrived one — two virtual Z
gates with no time between them, which the NCO cannot do ("the minimum
time between phase updates must be 4 ns"). That is a failure a user hits
by generating circuits programmatically, and it now explains itself.
`SIDEBAND_GAP_GHZ` exists because this project has no coupler to measure, which makes it the weakest number in the two-qubit tier. An edge can now say what its own drive is meant to bridge — `clock_freqs.sideband_gap`, alongside the `cz` frequency it is played *at* — and the simulator uses that in place of the constant wherever it is set. Keeping the two separate is the point. The frequency a drive is played at and the transition it should hit are different facts, and a config that disagrees with itself is then a detectable mistake rather than a definition: declaring a gap 50 MHz from the drive detunes the gate and the Bell state stops being one, which is what the new test asserts. Only an uncharacterised coupler falls back to the constant. Both schedulers carry the parameter. Also records the turn in the CHANGELOG and RFC 0004 §7, including that this tier now runs under both schedulers rather than one — the arrangement that would have caught the qblox write-back years earlier than it did.
The Unreleased section had grown to 235 lines — longer than all of 0.2.0 — because each entry explained the mechanism of a fix rather than its effect. One block ran to 881 words. That is a postmortem, and it was already written in the commit messages and RFC 0004 §7, so the changelog was the third copy and the least useful place to read it. Condensed to the convention the rest of the file already follows: prose where a migration needs guiding, one-line bullets under Added, Fixed and Changed. 140 lines out, 75 in. Nothing user-facing dropped — what went was the diagnostic narrative (`device_elements`, `partial_failure`, how check_docs is wired). The chevron and conditional-phase fits nearly went with it and are back as bullets; they are real fixes, not commentary.
RFC 0004 §11 is the record of what is built and what is not, and it predated the qblox work entirely. Four more faults belong in it, all found by running the same tests under both schedulers rather than one: - the qblox tuner had never completed a calibration — four separate faults in the write-back and the loaders, each alone fatal; - `conditional_phase` applied nothing on either scheduler, writing a name neither has behind a guard that was therefore never true; - `drag` swept a seconds-valued parameter over ±1.0, so its schedule could not compile at all; - the qblox tier-2 test never compiled anything, which is why it did not catch the one above. Their common cause is worth naming rather than leaving as four accidents: every one sat in *shared* code that quantify happened to satisfy. Two were also encoded as expectations — the tier-1 conditional-phase test asserted the bug, and the tier-2 drag sweep carried the same wrong scale as the routine. A test written from the code cannot contradict it. Also corrects claims that had gone stale in the same move: the catalog and the tuners README both said `is_simulated` was quantify-only. And §11's "not implemented" list grows two honest entries — no coupler here has been characterised, and the SPI and QCM bias paths have never touched hardware.
`CZChevron.apply` wrote `cz.amp` and `cz.duration`. Both schedulers spell them `square_amp` and `square_duration`, so the write raised on every real device — the routine measured the operating point correctly and then failed at its last step, and `conditional_phase` never ran at all because it depends on this one. Two-qubit calibration could not complete on hardware. Neither existing tier could catch it. Tier 2 builds and compiles a schedule without ever calling `apply`. Tier 3's fake device was given `cz.amp` and `cz.duration` — the names the routine used — so it agreed with the routine rather than with any device. That is the third time in this branch a fake shaped to the code has hidden a fault in the code, after the tier-1 conditional-phase test asserting `np.pi - crossing` and the tier-2 drag sweep carrying the routine's own wrong units. So the fake now carries the names a real `CompositeSquareEdge` has, and there is a test that calls `apply` against a real `QuantumDevice` under both schedulers and reads the values back — which is the only arrangement that could have caught any of the three.
Three faults in this branch were hidden by the same thing: a double that agreed with the code. `cz_chevron` wrote `cz.amp`, a name no device has; the tier-1 conditional-phase test asserted `np.pi - crossing`, the bug itself; the tier-2 drag sweep carried the routine's own wrong units. Tier 2 compiles a schedule and never calls `apply`. Tier 3's fake device was given the names the routine used. So the loop test now calibrates *every* routine the simulated chip can answer — through the shipped tuner, over a real `QuantumDevice` loaded from YAML, with nothing replaced but the instrument — and requires the report to come back `success` with no errors. Both schedulers, 23 routine-target results each, up from three routines. Getting there found three more faults, each of the same shape — a routine that measured correctly and then produced something unusable: - `fit_chevron` refines the CZ duration between its setpoints, so it reported sub-nanosecond precision. The instrument plays on a 1 ns grid, so writing it made every *later* schedule containing that CZ fail to compile, well away from the routine that caused it. Rounded on apply now. - A flux pulse on a single qubit's port was treated as a two-qubit gate, so `flux_spectroscopy` — which holds a DC bias on one qubit to move its frequency — failed complaining about a partner it never mentioned. A flux pulse whose gate names no second qubit is a frequency shift. - An edge routine needs *both* its qubits calibrated. With only q0 targeted, X on q1 did nothing, so the chevron prepared |10> rather than |11> and saw no exchange to find. The config now targets both; whether the DAG should refuse an edge whose qubits are not targeted is left open. Three routines stay disabled, named in `UNSIMULATED` with reasons rather than left out silently: the two resonator routines, because the coordinator's readout is two blobs with no resonator behind them — and calibrating a readout frequency the hardware config's fixed LO+IF cannot reach makes every later schedule fail to compile; and `drag`, because the coordinator ignores a pulse's DRAG parameter, so beta has no effect and the fit has nothing to find. Each is a gap in the simulator, not in the routine.
The whole calibration DAG now runs against the simulator. Three routines were excluded before, each because the simulator had no physics to answer them with, and closing those two gaps found five faults in shipped code — all of the shape this branch keeps producing: a routine that measures correctly and then writes something unusable, or nothing at all. **A readout resonator** (`simulation/resonator.py`). The readout was two fixed points in the IQ plane: enough to tell |0> from |1>, and a flat line for the two routines whose subject *is* the readout chain. It now has a Lorentzian response with a linewidth to find and a power at which the resonance walks from dressed to bare. `resonator_spectroscopy` recovers the modelled kappa (1.96 MHz fitted against 2.00) and reading out away from resonance costs contrast, which is what makes the frequency load-bearing downstream rather than decorative. **Pulse envelopes, and the derivative that rides on them.** A shaped pulse is integrated in steps instead of averaged to a constant amplitude. That averaging was precisely why the Motzoi parameter did nothing: the DRAG correction *is* the derivative of the envelope, which averages to zero. The leakage it cancels comes from the same three-level ladder that produces it, so the optimum is found rather than assumed — 0.113 as a ratio, against the -1/(2*alpha) theory gives for this transmon. Converged to five figures against eight times the step resolution; the envelope is normalised to unit mean so `amp180` still means what it meant, and a propagator cache makes the loop suite faster than before rather than slower. What that surfaced, all fixed: - **The DRAG parameter is not the same quantity in the two schedulers.** quantify's `motzoi` is the dimensionless ratio of derivative to Gaussian; qblox's `beta` is that ratio times the pulse sigma, so in seconds — 2.5 ns apart for a 20 ns gate. One constant sweep served both, so it was nine orders out for whichever it was not written for, and out in the large direction the derivative exceeds full scale and nothing compiles. `SchedulerBackend.drag_span` gives each backend its own default; both now recover the same physical optimum to four figures. The previous fix to this routine, and the note in RFC 0004 saying the parameter "is in seconds", were right about qblox and wrong about quantify. - **`drag` wrote to a parameter qblox does not have.** `rxy.motzoi` unconditionally, where qblox's transmon has `rxy.beta` — an optimum measured correctly with nowhere to put it. `drag_parameter_name` asks the element, as `phase_correction_names` already did for the edge. - **`fit_chevron` calibrated a population swap and called it a CZ.** It walked to the first trough and on to the first sample that stopped rising. Right description of the experiment, wrong way to measure it: the trough is not smooth, because the exchange beats against the transitions the flux pulse sits near, so the walk stopped at the top of a wiggle a few per cent deep and still deep in |02> — 55 ns for a round trip of 110. A complete transfer is a perfectly good gate and the wrong one. The return is a level crossing now. Found only once the DAG ran far enough for `conditional_phase` to consume the answer, where it surfaced as a flat fringe two routines downstream. - **The readout could be calibrated once and never again.** The hardware fixture pinned each readout port's IF and let the LO float, so three qubits sharing one QRM_RF agreed on an LO only because their configured frequencies happened to. Move one by a kilohertz and the module is asked for two LOs, and every *later* schedule fails to compile. The lab's own config pins the LO and lets each IF differ, which is what makes a readout frequency recalibratable at all. - **`resonator_punchout` left the readout pointing where the resonator used to be.** A frequency and a power are not independent numbers: the resonance moves *because* the power changed, which is the entire content of punchout. So choosing a new power invalidated the frequency spectroscopy measured at the old one, and nothing revisited it — half a linewidth off, a fifth of the readout contrast, for every routine downstream. Punchout writes both now, and needed no extra measurement: it already fits a spectrum at every power in its range, so the row at the power it selects *is* the corrected frequency. It was being thrown away. Two test faults worth naming, since both were passing: - `qubit_spectroscopy` at the routine's 1% drive is a signal-to-noise of about five. The same sweep returned centres 14 MHz low, 12 MHz high and 62 MHz low on nothing but noise, and the full-DAG test had been passing on that margin. Its sweep is also centred above the fixture's claim now: the qubit is 214 MHz *above* it, so a symmetric span left the line 6 MHz inside the boundary, where a Lorentzian's centre is essentially unconstrained. - The full-DAG test asserted the CZ duration was a whole number of nanoseconds and never that it was the right number, which is how a 55 ns swap passed. It checks the round trip against the coupled model now. The driver e2e's QPU-seconds check read its baseline nine API calls before the approval it was measuring, so a job settling in between turned "approving 300 seconds credits 300 seconds" into an assertion that nothing else happened meanwhile. It reads the baseline where it means to. `test_calibration_loop` runs all sixteen routines over two qubits and the edge between them, under both schedulers, and requires the report to come back `success` having skipped none — the equality matters more than the success, since a routine quietly excluded is indistinguishable from one that works. CI already runs it in the `sim` matrix entry.
Two gaps left by the previous commit, both in RFC 0004. **§7 never mentioned the readout resonator or the pulse envelopes.** They were described in §11, which is a narrative of what was built; §7 is where the simulator's claims and the limits on them belong, and a caveat is no use to a reader in the section that does not list caveats. Both now sit in tier 3 alongside the two-qubit paragraph they rhyme with, including the one that matters: the readout is not a dispersive model. The two qubit states do not pull the resonance to two frequencies, so the best point to read out at here is the resonance itself, where a real chip has an optimum between the two pulled peaks — a calibration this simulator cannot pose. §11 keeps two lines and points at §7. **§9's automated-test list omitted `make test-py-loop`**, which is the target covering most of what this RFC claims and the only evidence that a fitted number survives the fit, the write-back, the YAML, the loader and the compiler with its meaning intact. `test-py-cli` and the two e2e targets were missing too, and the note that CI runs all of them — including why the matrix carries a `sim` entry naming no executor, which is what stopped the loop tests from silently never running there. The tier-3 rows of the test-file table were also stale: the physics tests now cover the resonator's own lineshape and punchout curve, and the loop test runs the whole DAG rather than three routines. No behaviour change. The status line still reads correctly — what §7 and §9 reserve is now the coupler's five chosen constants, the readout simplification above, and the whole of the manual hardware verification, none of which has run on a lab node.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
To complete the CZ simulation started in the base repository's branch