Point it at a quantum computer and it tells you whether a two-qubit pair can be used as a clock — and gives you the numbers behind the answer.
Clock, here, means using the decay of an entangled pair as a timing reference: the pair is left idle for a chosen delay, and what has happened to it in that time is read out and corrected for. GPS is the number that says whether the correction helped — the fidelity of the corrected circuit minus the fidelity of an uncorrected one at the same delay. It is the papers' term for the quantity; positive means fidelity was recovered, and the verdict is GO when it stands more than three standard errors from zero.
$ eedt-bios --platform ibm
Select backend:
[1] ibm_kingston [ibm] queue=86
[2] ibm_fez [ibm] queue=88
choice> 1
Select qubit pair:
[1] Q149-Q150 T2=333us CZ=0.0050 score=0.6
choice> 1
[calibration] Q149-Q150 on ibm_kingston
sweep tau: [17, 53, 90, 127, 163, 200] us + 8 Ramsey delays
tau= 16.7us F_ff=0.9355 F_ref=0.5430 GPS=+0.3926
tau= 53.3us F_ff=0.8516 F_ref=0.5625 GPS=+0.2891
...
T1 from the reference curve: 410us
nu_ZZ = -3.135kHz
tau* = 88.6us
GPS = +0.4316 z = +13.81
boot mode: GOChoosing a pair and a delay. A 156-qubit processor has upwards of a hundred neighbouring pairs, and the useful idle time on each is a property of that pair on that day. Handed a backend, this ranks the candidates, calibrates one, and returns the delay to use — the input any feedforward experiment needs before it can start.
Finding out whether feedforward helps here at all. It is not a given. On 2026-07-27 the same code found a clear gain on IBM Heron (z up to 13.8 across five pairs) and nothing measurable on IQM Garnet (twenty-nine measurements pooling to zero). Two jobs tell you which situation you are in.
Measuring the ZZ coupling of the pair you are about to use. Neighbouring qubits shift each other's frequency; how much, and in which direction, is what sets the delay. Measured signed, in the same job as everything else, and withheld rather than guessed at when coherence is too short to resolve it.
Seeing the operating window. Where the correction helps on that pair, and where it stops. Hardware drifts — the same pair moved measurably inside half an hour during validation — so a window measured last month is not the one you have now.
Reproducing or extending the published work. Every run records the backend job id, the sweep, and the raw coupling measurement, so a result can be traced or re-examined without repeating it.
Two jobs on the backend, and three files you can keep.
| file | what is in it |
|---|---|
eedt_bios_<stamp>.csv |
one row: the verdict, the operating point, the ZZ coupling, T1, and the backend job id |
..._sweep.csv |
the delay sweep — where the correction helps on that pair, that day |
..._ramsey.csv |
the raw coupling measurement, kept so a rejected fit can be re-examined |
The headline number is GPS, and the verdict is GO when it is more than three standard errors from zero.
Not published on PyPI. Install from source:
git clone https://github.com/okudat9/eedt-bios
cd eedt-bios
pip install -e ".[all]" # both vendors
pip install -e ".[ibm]" # IBM Runtime only
pip install -e ".[iqm]" # IQM onlyAdd dev for the test dependencies: pip install -e ".[all,dev]".
pyproject.toml carries lower bounds so the tool stays installable alongside
current Qiskit. The exact combination the results below were taken with is in
requirements-lock.txt.
Read from the environment; nothing is stored in the source.
# Windows PowerShell
$env:QSCRIPT_IBM_TOKEN = "..."
$env:IQM_TOKEN = "..."# Linux / macOS
export QSCRIPT_IBM_TOKEN=...
export IQM_TOKEN=...--ibm-token / --iqm-token override them.
Interactive — the tool lists what it found, with queue depth where the backend reports it, and you choose:
$ eedt-bios --platform ibm
Select backend:
[1] ibm_kingston [ibm] queue=86
[2] ibm_fez [ibm] queue=88
[3] ibm_marrakesh [ibm] queue=2340
choice> 1
Select qubit pair:
[1] Q149-Q150 T2=333us CZ=0.0050 score=0.6
[2] Q125-Q126 T2=322us CZ=0.0050 score=0.6
choice> 1Scripted:
eedt-bios --platform ibm --backend ibm_kingston --shots 512 --yes
eedt-bios --platform iqm --iqm-device garnet --clock-impl static --yes
eedt-bios --platform ibm --nu-zz 2.655 --yes # known coupling, one job
eedt-bios --platform iqm --tau-star 10 --yes # known delay, one job| flag | meaning |
|---|---|
--platform {auto,ibm,iqm} |
vendor; auto follows whichever token is set |
--backend NAME |
a specific IBM backend; otherwise the account is enumerated |
--iqm-device NAME |
a specific IQM machine (garnet, emerald, sirius) |
--clock-impl {auto,dynamic,static} |
force the correction |
--shots N |
shots for the clock run (default 256) |
--nu-zz KHZ |
known coupling; skips the Ramsey scan |
--tau-star US |
known delay; skips calibration entirely |
--measure-nu / --no-measure-nu |
force the coupling measurement on or off |
--zero-baseline |
also run tau = 0 and report the decay cost |
--output-dir DIR |
where the CSVs go (default results/) |
--no-csv |
skip writing files |
--yes, -y |
non-interactive: first backend, best-ranked pair |
--version |
print the version and exit |
| Bell state | the two qubits prepared as (|00> + |11>)/sqrt(2) — correlated, and decaying |
| T1 / T2 | how long a qubit keeps its energy / its phase. Microseconds to a millisecond on current hardware |
| ZZ coupling, nu_ZZ | neighbouring qubits shift each other's frequency. That shift, in kHz. Usually negative on fixed-frequency transmons |
| tau* | the delay the clock is run at. Chosen from nu_ZZ, or from the sweep |
| phi* | 0.873 rad. The phase the pair should have accumulated at tau*. An operating set-point that held across the hardware it was measured on — not a claimed constant of nature |
| MCM | mid-circuit measurement: reading one qubit part-way through and acting on the result |
| GPS | F_ff − F_ref: the fidelity gained by correcting, at that delay |
| z | GPS divided by its standard error. How many error bars from zero |
The problem. Neighbouring qubits are always slightly coupled — a
"ZZ" interaction, so called because it acts on both qubits' Z axes. Leave an
entangled pair idle and that coupling winds phase onto it, while decoherence
takes the phase apart. Standard practice is to fight the coupling. The Bell
Decay Clock reads it instead.
Why decay, not phase. A conventional ZZ clock recovers the accumulated phase, which only works while the off-diagonal elements of the density matrix survive. On a machine with dephasing-type coupling, or one whose mid-circuit measurement is slower than its coherence budget, they do not. Populations — the diagonal elements — outlive them, so a clock built on the differential decay of Bell-state populations keeps working after the phase is gone.
What gets measured.
GPS = F_ff(tau) - F_ref(tau)
F_ff is the fidelity of the corrected circuit, F_ref of the uncorrected
one, both at the same delay on the same pair, both measured on every run.
GPS > 0 means the correction recovered fidelity that decay had taken away.
z = GPS / SE SE = sqrt(0.5 / shots)
The reference is not optional. Comparing against a tau = 0 circuit would
mostly measure how much the state decayed rather than what the correction did;
that circuit is available separately as --zero-baseline.
Two corrections, and the hardware picks. Not every backend compiles
control flow, and one that does may still do better without it — a slow
mid-circuit measurement eats the coherence it was meant to exploit. The tool
reads backend.target.operation_names and builds what the machine can run.
--clock-impl overrides it.
Each pairs a corrected circuit with a reference that differs only by the correction. Both run at the same delay, in the same job.
dynamic |
static |
|
|---|---|---|
| correction | measure q_bell mid-circuit, act on q_sys |
unconditional Rz(-phi*) on q_bell |
| readout | q_sys only |
both qubits |
| fidelity | P(q_sys = 0) |
P(00) + P(11) |
| requires | if_else in the target |
r, cz, delay, measure |
| cost | the mid-circuit measurement takes time out of the coherence q_sys has left |
nothing spent waiting |
The static rotation is fixed at phi* rather than fitted per delay, so it
matches the accumulated phase exactly only at the delay phi* implies.
The two measure different observables. Compare GPS only within the same
clock_impl; every row records which was used.
What a run consists of. Two jobs. The first sweeps the delay and, where coherence allows, measures the coupling alongside — one job, because queue time dominates everything else on a shared machine. The second runs the clock at the chosen delay.
The Bell-state relative phase advances at half the bare ZZ rate, so
phi = pi * nu_ZZ * tau, and
tau* = phi* / (pi * nu_ZZ) phi* = 0.873 rad
Every constant in the source says where it came from. The ones established in the papers carry a pointer to the DOI; the rest — the gates, the sampling, the thresholds — carry their reasoning in full, because there is nowhere else to look it up.
phi* is an operating set-point, reported as phi_ff_rad so it is never
mistaken for a measurement. Where it came from, and how far it travels between
machines, is a question for the papers rather than for this tool; here it is
simply the number the delay is derived from, and --tau-star overrides it.
nu_ZZ is measured alongside the sweep but never used to choose tau* unless
it passes its checks — amplitude, residual against the shot-noise floor, and
significance against its own Cramér-Rao uncertainty. A coupling that cannot be
resolved costs a reported number and nothing else; the delay then comes from
the sweep. A delay past half the coherence budget is refused outright, because
relaxation alone lifts the reference there and leaves the correction nothing to
recover.
Only coupling-map neighbours are offered, and the chosen pair is pinned as an explicit initial layout — without that the transpiler picks its own qubits and every pair returns the same numbers.
Queue time dominates on a shared machine, so everything that can share a job does.
| circuits | jobs | |
|---|---|---|
| calibration, with the coupling measured | 44 | 1 |
| calibration, coupling skipped | 12 | 1 |
| clock run | 2 (3 with --zero-baseline) |
1 |
--nu-zz or --tau-star |
2 | 1 total |
The coupling measurement is skipped by default below a T2 of 100 µs: frequency
precision comes from the length of the record, the record cannot outlast
coherence, and on a 30 µs machine the uncertainty lands near 7 kHz — larger
than the couplings being looked for. --measure-nu forces it anyway.
Written to results/YYYYMMDD/, each with a commented legend so a file read
months later explains itself.
eedt_bios_<stamp>.csv — one row: the result and everything that produced it.
What happened
| column | meaning |
|---|---|
boot_mode |
GO (z ≥ 3), DEGRADED (z ≥ 2), NO-GO |
bios_version, timestamp |
which version wrote this, and when |
backend, platform, q_sys, q_bell |
the machine and the pair. q_sys carries the amplitude and is corrected; q_bell is measured or rotated |
job_ids |
backend job id(s) — the row can be traced on the provider |
The result
| column | meaning |
|---|---|
F_ff, F_ref |
fidelity with and without the correction, same delay, same pair |
GPS |
F_ff − F_ref. Positive means the correction recovered fidelity |
z, SE, shots |
significance, its standard error, and the shot count behind it |
fidelity_def |
which observable F_ff and F_ref are — P(q0=0) or P(00)+P(11) |
How it was measured
| column | meaning |
|---|---|
clock_impl |
dynamic (conditioned on a mid-circuit measurement) or static (unconditional). Different observables — do not compare GPS across them |
correction |
mcm_cond_x, qfeed_plus, or static_rz |
tau_star_us |
the delay the clock ran at |
tau_star_source |
swept (peak of the sweep), from_measured_nu, swept_fallback, supplied, or from_nu |
phi_ff_rad |
the operating set-point, 0.873 rad. Not a measurement |
N_run |
mid-circuit rounds the coherence budget allows |
The coupling
| column | meaning |
|---|---|
nu_ZZ_khz |
the measured coupling, signed. Blank when it could not be resolved |
nu_ZZ_source |
measured, supplied, or why it was refused: not_significant, poor_fit, implausible_contrast, out_of_range, fit_failed |
nu_ZZ_code_khz |
nu_ZZ / 2 — the rate the Bell relative phase advances at |
tau_star_from_nu_us |
phi*/(pi·nu_ZZ). Compare against tau_star_us: agreement checks the relation, disagreement is worth a look |
ramsey_f0_khz, ramsey_f1_khz |
the two fitted fringe frequencies, partner in |0> and |1>. Their difference is nu_ZZ |
ramsey_amp_min, ramsey_amp_max, ramsey_resid_max |
fit diagnostics, written whether or not the coupling was accepted, so a refusal can be understood |
The pair
| column | meaning |
|---|---|
T2_sys_us, T2_bell_us |
as the backend reported them at discovery. A placeholder on IQM, which does not publish them |
cz_error |
entangling-gate error for the edge, where the backend reports it |
ref_decay_us |
measured from the reference across the sweep. T1 for dynamic, Bell decay time for static |
F_ref_zero, decoh_drop |
the tau = 0 baseline and F_ref_zero − F_ref. Blank unless --zero-baseline. The sign follows the readout: negative for dynamic, positive for static |
..._sweep.csv — one row per swept delay: tau_us, F_ff, F_ref, GPS,
and chosen marking the delay the clock then ran at. This is the operating
window of that pair, that day.
..._ramsey.csv — the raw coupling measurement: tau_us and
P(q_sys = 0) with the partner in |0> and |1>, each in two quadratures
(X_partner_0, Y_partner_0, X_partner_1, Y_partner_1). Kept raw so a
rejected fit can be re-examined without going back to the hardware.
Measured on 2026-07-27. Job ids are on IBM Quantum Platform and IQM Resonance.
IBM Heron r2 (ibm_kingston) — the clock works.
| pair | tau* | GPS | z | verdict | job |
|---|---|---|---|---|---|
| Q149–Q150 | 16.7 µs | +0.4316 | +13.81 | GO | d9jj3i3jf64c739gt8s0 |
| Q57–Q67 | 41.0 µs | +0.3105 | +9.94 | GO | d9jia63hdfks73cib3hg |
| Q125–Q126 | 42.6 µs | +0.2930 | +9.38 | GO | d9ji8kbhdfks73cib1h0 |
| Q126–Q127 | 16.1 µs | +0.2461 | +7.88 | GO | d9jiosqbr2fc73e61il0 |
| Q50–Q51 | 57.0 µs | +0.1797 | +5.75 | GO | d9jii7abr2fc73e614m0 |
The coupling measurement resolved on Q126–Q127 at −3.13 kHz — negative, as expected for fixed-frequency transmons — from fringes at −9.16 and −12.29 kHz. That raw data is a regression test in the suite.
IQM Garnet — no gain measurable that day. Twenty-nine static
measurements across four pairs pool to GPS = −0.0065 ± 0.0048, a 95%
interval of [−0.0159, +0.0028], with scatter consistent with shot noise alone.
The tool reported NO-GO throughout rather than manufacturing a result. What
a different session, a different pair, or a differently built circuit would
give is not something this run can say.
Garnet does compile control flow, contrary to what its public fake backend
suggests, so clock_impl came out dynamic until forced otherwise. Compiling
and working are different things: the conditioned correction did no better
there than the unconditional one.
Stated plainly so you know which parts have never met hardware.
qfeed_plus— the cumulative-phase correction. Its condition (N_run == 0) never arose on the machines tested, so the circuit has not run. Reading it afterwards found it paired with the wrong reference: it leaves the pair unentangled, and the Bell reference it was compared against would have shown half a unit of gain regardless. Corrected and covered by a test, but still unmeasured.--zero-baseline— thetau = 0diagnostic.- IQM Emerald and Sirius — Emerald connected and listed a different coupling map, but no run completed; Sirius was never reached.
--yes,--no-csv,--output-dir— exercised by tests, not on hardware.- A backend that reports no T2 —
qubit_propertiesreturningNoneis handled, but has not been seen.
Test coverage is 70%. The uncovered lines are almost entirely the vendor transport layer and the interactive CLI, which need a real backend to reach.
Every run writes three files and records the backend job id in each. Sending
results/<date>/eedt_bios_<stamp>*.csv is usually enough to diagnose a failure
without repeating the measurement — that is what the raw sweep and Ramsey files
are for, and the fit diagnostics are written whether or not the fit was
accepted.
Every one of these came up while validating the tool on 2026-07-27.
Authentication parameters given both as initialisation args and as environment variables — the IQM client refuses a token supplied twice, and
having IQM_TOKEN set while also passing --iqm-token counts as twice. The
tool now routes the token through the environment either way, so this should
not reach you; if it does, unset one source.
The IQM Server URL must not contain the quantum computer name — older
deployments took the machine in the URL path (.../garnet). The client now
wants a base URL and the machine named separately. Clear any IQM_SERVER_URL
left over from an older setup:
set IQM_SERVER_URL=520 is not a valid HTTPStatus — a fault at the server, not in the
circuits or the token; the client raises on a status it does not recognise.
Usually worth retrying. The tool does not retry by itself, because a job that
failed after submission may still have been charged.
Not enough credits — the job was declined and nothing ran. Reduce the
work rather than the shots: --no-measure-nu drops the calibration from 44
circuits to 12, and --tau-star US skips calibration altogether, leaving one
job of two circuits.
Every pair returns the same numbers — on IBM this means the transpiler is choosing its own qubits. Fixed here by pinning an explicit initial layout; if you see it in your own code, that is where to look.
nu_ZZ not resolved — the reason is recorded rather than a doubtful
number. not_significant means the difference was smaller than its own
uncertainty, which on a short-T2 machine is the normal outcome and is why the
measurement is skipped there by default. poor_fit and implausible_contrast
point at the fringe itself; the raw data is in ..._ramsey.csv and the fitted
frequencies, amplitudes and residuals are in the result row, so the refusal
can be examined without measuring again.
A delay far longer than expected — refused if it exceeds half the coherence budget. Relaxation alone lifts the reference at such delays, so a correction measured there has nothing left to recover. The tool falls back to the swept peak and says so.
Queue waits — shown in the backend list where the vendor reports them. On
2026-07-27 ibm_kingston had 86 jobs waiting and ibm_marrakesh had 2340.
pytest picks up the wrong copy — if a stale eedt_bios.py sits in a parent
directory it can shadow the one under test. The header line of a full pytest
run prints the path actually imported.
pytest -qRuns without hardware. Circuit construction, both fidelity estimators, pair discovery, calibration, the runner, the gates and CSV output are covered with a fake adapter; a further set compiles circuits against vendor fake backends, and several are regressions built from recorded hardware data. Backend-specific tests skip cleanly when a vendor SDK is absent.
nu_zz_map_iqm maps signed ZZ
coupling across a device with mandatory negative controls — it answers where
the coupling is. This answers how to run a clock on it.
Okuda, T. Bell Decay Clock: Extracting Useful Information from Decoherence via Mid-Circuit Measurement Feedforward. Zenodo, 2026. doi:10.5281/zenodo.19367565
Supporting theory:
Okuda, T. Phase-Invariant Condition for Optimal Feedforward in ZZ-Coupled Quantum Systems. Zenodo, 2026. doi:10.5281/zenodo.19593677
Machine-readable metadata: CITATION.cff.
Apache-2.0. See LICENSE.
