A guided tour · Quantum error correction · Bayesian inference
How to tell a quantum computer its noise has changed
GitHub(private)
A long read, roughly 40 minutes with the interactive figures. Assumes comfort with algebra, logarithms, and coin-flip probability, and nothing else. Every claim on this page traces to a number in the paper or the repo.
Where we are going
The animation above shows a swarm of guesses chasing a moving dot. The dot is the machine's true noise, two levels really, one for operations and one for readout, and it wanders as the machine runs. The swarm is a statistical filter reading the computer's own error-correction data and inferring, in real time, where the noise went. By the end of this page you will understand every piece of that picture: what a surface code is, why its decoder needs an up-to-date map of the noise, why the obvious ways to update that map fail, and how a particle filter with a carefully calibrated likelihood does the job fast enough to keep up with the hardware. At the end we point it at 12.4 million measurements from Google's Willow chip and catch a real drift event in the act.
## why qubits fail
A classical bit is a 0 or a 1. A qubit is allowed to be a blend of both at once: its state is a pair of numbers (α, β), one weight on 0 and one on 1, and until you measure it, both weights are physically real. Measuring forces a choice. You read out 0 with probability α² and 1 with probability β², and the blend is gone. That blend is the entire advantage of a quantum computer, and it is absurdly delicate.
A qubit has two distinct ways of failing where a classical bit has one. The first is the familiar kind: the 0 and 1 weights swap. That is a bit flip, written X, and a classical bit can suffer the same fate. The second has no classical counterpart: the relative sign between the two weights flips, (α, β) becomes (α, −β). That is a phase flip, written Z. If you measured the qubit immediately you would never notice, since β² and (−β)² are the same number. But quantum algorithms work by letting the weights of different computational paths interfere, adding when signs agree and cancelling when they disagree, and a stray sign flip silently corrupts that arithmetic. A real error is generally some mix of both, so if you can correct X and Z separately, you can correct anything. Keep that in mind: every error-correction structure below comes in two mirrored copies, one hunting bit flips and one hunting phase flips.
Where do the errors come from? A superconducting qubit is a tiny resonant circuit held at about 15 millikelvin, and its environment never fully leaves it alone. Stray defects in the chip's oxide layers behave like parasitic two-level systems that couple to a qubit, sap its energy, and then let go, sometimes for hours at a time. The readout resonators used to measure qubits drift with temperature. Control electronics age over a day. The result is that the rates of bit and phase flips are not constants of the machine. They are slowly moving targets, wandering over seconds to hours.
Hold on to the timescales, because the whole project lives in the gap between them. One round of error-checking takes about a microsecond; a full experiment, a few dozen rounds. The noise wanders over seconds to hours. Between those two clocks sits everything this page builds.
## correcting errors you are not allowed to look at
The classical recipe for fighting errors is repetition. Store your bit three times, 000 or 111, and if one copy flips, majority vote fixes it. Try that on a qubit and you hit two walls at once. You cannot copy an unknown quantum state (this is a theorem, not an engineering limitation), and you cannot take the majority vote, because reading the qubits to compare them destroys the very blend you are trying to protect.
The escape is one of the cleverest ideas in the field. You are not allowed to ask a qubit "what are you?", but you are allowed to ask a pair of qubits "are you two the same or different?" That question, a parity check, has a physical implementation that reveals only the answer, agree or disagree, and nothing about the underlying values. The blend survives the question.
Now redo repetition with parity checks. Spread one logical bit across three physical qubits so that the legal states are "all agree." Ask qubits 1 and 2 whether they agree, and qubits 2 and 3. If a bit flip hits qubit 1, the first check reports disagree while the second reports agree, and you know exactly where the error sits without ever learning the data. Flip qubit 2 and both checks report disagree. Each error location produces its own fingerprint of upset checks. That fingerprint is called the syndrome, the same word medicine uses: a pattern of symptoms that points at a cause you cannot observe directly.
This little code corrects bit flips only. To also catch phase flips you need checks of a second type, and you need the two types woven together so that neither kind of question disturbs the other's answers. Doing that weaving on a two-dimensional grid gives you the surface code.
## the surface code, and what a detector is
The surface code arranges data qubits in a d × d grid. In the picture below, d = 3: nine data qubits, drawn as hollow circles. Between them sit the parity checks, one per shaded tile (the half-moons along the edges are checks too, just smaller ones). The tiles come in two types in a checkerboard pattern: one type asks its surrounding data qubits "would you all agree if I measured you in the bit basis?" and catches bit flips; the other asks the mirrored question, agreement in the sign basis, and catches phase flips. Each tile has a small measurement qubit at its center whose only job is to collect the answer, over and over, once per round, about once a microsecond.
Bigger grids tolerate more. The code's distance d is the length of the shortest chain of errors that can sneak from one side of the patch to the other and change the stored logical bit without upsetting a single check. At d = 3 a chain of three unlucky errors can do it; at d = 7 you need seven. As long as errors are rare enough, doubling the distance suppresses logical failures multiplicatively, which is why the field is betting on this code.
There is one more wrinkle, and it matters for everything that follows. The raw answer a check returns in any given round is not by itself meaningful; what carries information is whether the answer changed since the previous round. A check that reports "disagree, disagree, disagree" across three rounds is describing a stable, already-known situation. A check that flips from agree to disagree is telling you an error happened just now, right here. So the objects the decoder actually works with are detectors: the exclusive-or of the same check in consecutive rounds. A detector fires when something changed. In a perfect machine, no detector ever fires.
Click around the patch below and watch which checks light up. (The toy shows a single round starting from a clean slate, so an upset check here is exactly a fired detector.)
Two behaviors in that toy are the key to the whole subject. First, a single error never lights one detector; it lights the pair of checks that flank it (or a single check, if the error sits at the boundary of the patch, where its partner check would be off the edge). Second, a chain of errors lights only its endpoints. The middle of the chain upsets every interior check twice, once from each side, and two upsets cancel: the check sees both its neighbors flip and reports "still agreeing." Errors are invisible except at their ends.
Now run the machine for r rounds and collect everything. A single experiment, one shot, produces a binary record: for each of D detectors (all checks, all rounds), did it fire? A batch of B shots is a B × D table of zeros and ones streaming out of the fridge. Everything the filter does later in this page, it does by staring at that table. No extra probes, no dedicated calibration circuits. It is nothing more than the exhaust the machine already produces.
## decoding is a matching puzzle
So a shot comes back and some detectors have fired. The decoder's job: figure out which physical errors most plausibly caused that pattern, so the bookkeeping can cancel them out. Get it right and the logical bit survives. Get it wrong and the "correction" completes an error chain instead of erasing it.
Here is the structure that makes this tractable. List every elementary way the circuit can err: a bit flip on this qubit between these rounds, a misread measurement on that check, and so on. Each such error mechanism e happens with some small probability pe, and each one lights a specific pair of detectors (or one detector at a boundary). So draw a graph: detectors are nodes, and each mechanism is an edge connecting the pair it lights. This graph, mechanisms plus their probabilities, is called the detector error model, and it is nothing more or less than the decoder's map of the machine's noise. Real errors on real qubits become fired nodes, and explaining the syndrome means choosing a set of edges whose endpoints are exactly the fired nodes: pairing up the lit detectors along plausible paths.
Which pairing? The most probable one. The probability of a specific set E of mechanisms all occurring while the rest do not is
Take the logarithm and shuffle terms, and maximizing P(E) becomes minimizing a sum of per-edge costs:
Each edge's weight is the log-odds against that mechanism. Rare mechanisms are expensive roads; common ones are cheap. The decoding problem is now: find the cheapest set of paths that pairs up all the fired detectors. That is a classic combinatorial problem, minimum-weight perfect matching, and fast exact algorithms for it have existed since the 1960s. This is the workhorse decoder for surface codes, and the one this project uses (via the PyMatching library).
Notice what the weights are. They are beliefs. The decoder is only as good as its pe values, because ambiguous syndromes get settled entirely by which explanation the map says is cheaper. Play with the space-time strip below: the same four fired detectors, and a decoder whose belief about the readout noise you control.
The flip you can produce with that slider is the entire stake of this project, in miniature. Nothing about the data changed. Only the decoder's belief about the relative noisiness of two error types changed, and with it the correction it applies. If its beliefs are right, those tie-breaks go the right way most of the time. If its beliefs are stale, a systematic bias in tie-breaking quietly eats the code's performance. The horizontal edges in that strip are gate errors and the vertical ones are measurement errors, and that distinction, which type of edge got noisier, is precisely what the rest of this page learns to track.
## the map goes stale
Where do the decoder's pe values come from? From calibration: before the experiment, the machine is characterized, every mechanism gets a probability, and the map is handed to the decoder. Then the experiment runs. And the machine, as we said at the start, does not hold still. A two-level defect wanders into resonance with a qubit and gate errors on it double. A readout resonator drifts and measurement errors creep up over minutes. The map was accurate the moment it was drawn, and every second afterward it describes the machine a little less.
Labs deal with this today by pausing every few hours and recalibrating. That works, but it costs uptime, and the field's ambitions point the wrong way for it: useful logical computations will run for hours without a natural pause. What you would like is a way to notice the drift while the machine runs, and to refresh the decoder's map on the fly.
Here is the observation that makes that plausible. The syndrome stream itself is a record of the noise. Detectors fire more when errors are more common; which detectors fire more, and in what combinations, encodes which error types got worse. The decoder is already consuming this stream. Nothing extra needs to be measured. The question is whether you can read the noise level out of it accurately enough, and fast enough, to be useful, and this turns out to be a statistics problem with two genuine surprises in it. Both surprises shaped the design, so we take them in order.
One piece of vocabulary first. The natural unit of streaming is the batch: a contiguous block of B shots, milliseconds to seconds of wall time. Within one batch the noise is effectively frozen (drift is much slower than that); across batches it moves. So the picture is: batch arrives, decode it, learn from it, update the map, repeat. Drift becomes a discrete-time process, one step per batch, and tracking it is what statisticians call a filtering problem.
## first surprise: the obvious drift doesn't matter
Suppose the whole machine gets uniformly noisier: every mechanism's probability scales by the same factor s, so pe becomes s pe everywhere. This is the drift everyone instinctively monitors, "the error rate went up." What happens to the decoder's edge weights? Since the pe are small, a little algebra on equation (2) gives
(Read O(pe) as "plus a leftover about the size of pe itself," thousandths here, too small to matter.) The scale enters every edge as the same additive shift, −ln s. And a matching decoder compares total costs of candidate pairings. If two candidate pairings use the same number of edges, shifting every edge by a constant moves both totals by the same amount, and the comparison is untouched. Competing explanations of a syndrome almost always do use the same number of edges, so the decoder's decisions survive uniform drift essentially unchanged. Watch it happen:
This is a strange fact, and a useful one. A decoder that is handed the true, exact value of a uniformly drifting noise level decodes no better than one that never updates at all. (The paper's simulations confirm this to within statistical noise, at every code distance tested: it is the negative control, and it comes out null exactly as the algebra says it should.) Two consequences follow, and they set up everything downstream.
First, a corollary worth pinning to the wall: any drift monitor that summarizes the machine in a single number, an error rate, a fidelity, is useless to a matching decoder, no matter how accurately it tracks. It measures exactly the direction of change the decoder cannot feel.
Second, an enormous simplification. If the overall level doesn't matter, only the mix does: which error types got worse relative to the others. The noise state we track can collapse from one number per mechanism (thousands) down to a couple of relative scales. This project uses two: sg, multiplying the gate-type mechanisms (the errors that happen while qubits are being operated on or idling), and sm, multiplying the measurement-type mechanisms (misread checks, faulty resets). The drifts that actually happen on hardware, a readout resonator degrading while gates hold steady, are precisely of this relative kind. So the thing to track is a two-dimensional point (sg, sm), and that point is the moving dot in the animation at the top of this page.
## second surprise: the obvious signal can't see the drift that does
Fine: track (sg, sm) from the syndrome stream. The obvious statistic to extract from a batch is the firing rate of each detector: out of B shots, how often did detector i fire? There is even an exact formula for what that rate should be. A detector fires when an odd number of the mechanisms touching it occurred, and the probability of an odd count has a lovely closed form.
A small gem, worth two minutes
Let each mechanism e occur independently with probability pe, and let N count how many occurred. The trick is to compute the average of (−1)N. Because the mechanisms are independent, the average of the product is the product of the averages, and each factor averages to (1−pe)·(+1) + pe·(−1) = 1−2pe. But (−1)N is +1 for even N and −1 for odd, so its average also equals P(even) − P(odd) = 1 − 2P(odd). Set the two expressions equal and solve:
Ei is the set of mechanisms touching detector i. This formula is exact, no approximations, and the filter evaluates it exactly as written.
So we know precisely how each firing rate responds to (sg, sm). Here is the problem: nearly every detector is touched by mechanisms of both types, in roughly similar proportions. Turn up the gate noise and all the rates rise; turn up the measurement noise and all the rates rise in almost the same pattern. The two knobs push the data in nearly the same direction. It is like being told x + y = 7 and being asked for x and y: an entire line of answers fits, and the data cannot pick between them.
In two dimensions this failure has a shape. Plot how well each candidate (sg, sm) explains a batch, and instead of a single bright peak at the truth you get a long bright ridge: a canyon of near-equally-good explanations running diagonally through the plane. A filter built on firing rates alone slides along that ridge. In early tests it pinned the gate scale to within about 3% while the measurement scale wandered by about 25%, an order of magnitude worse, the signature of an estimator that looks like it works if you only plot the coordinate it happens to pin down.
The cure has to be a statistic that responds to the two channels differently, and there is a natural candidate hiding in the time structure. Think about what a measurement error does: the check is misread once. That corrupts the comparison with the round before and the round after, so the same check's detectors fire in two consecutive rounds, a vertical domino in spacetime. Gate errors don't have that signature. So the fingerprint of measurement noise is time-like coincidences: pairs of detectors, same check, adjacent rounds, firing together in the same shot.
Coincidence rates also have an exact formula. For a pair of detectors (i, j), sort the mechanisms into three bins: those touching only i, only j, or both. Let a, b, c be the probabilities that an odd number occurred in each bin (each computed by formula (4)). Both detectors fire together when the shared bin is quiet and both private bins are odd, or the shared bin is odd and both private bins are even:
The c term is what matters: it is driven by the shared mechanisms, and for time-like pairs the shared mechanisms are dominated by measurement errors. Adding coincidence counts to the statistic snaps the ridge shut (toggle it in the figure above). In the pilot runs, the typical measurement-scale error dropped from about 25% to 13%, and more importantly the filter stopped wandering. Not every pair is worth counting: the implementation ranks candidate pairs by how strongly they separate the two channels and keeps the best 4000.
## bayes for a moving target
Now the estimation problem is well posed: a two-dimensional state, and per batch, a set of counts (each detector's firings, each chosen pair's coincidences) whose expected rates are exact known functions of the state. What remains is inference, and because the state moves, it has to be inference that updates.
Strip the problem to its cartoon: estimating the bias of a coin. You hold beliefs about the bias, a whole distribution over possible values, not a single guess. A batch of flips arrives, say 14 heads in 20. Bayes' rule says the updated belief in any candidate bias is proportional to how strongly you believed it before, times how well it explains what you saw:
Candidates near 0.7 explain 14-of-20 well and get boosted; candidates near 0.3 get crushed. Repeat per batch and beliefs sharpen forever. But our coin's bias drifts. So between batches you must also un-sharpen: each candidate value could have wandered a little, so the belief distribution smears out by however much one step of drift allows. The rhythm of a filter is exactly this two-step: predict (smear beliefs by the drift model) then update (multiply by the new batch's likelihood and renormalize). Sharpen, smear, sharpen, smear. The steady state is a belief distribution that is exactly as sure as the data allows it to be, forever chasing.
For our problem the state is the 2D point (ln sg, ln sm) (logs, so drift is multiplicative: hardware degrades by factors, not by increments), the drift model is a small Gaussian random step per batch, and the likelihood comes from formulas (4) and (5). But the posterior distribution has no tidy formula, so you cannot carry it around exactly. The trick is to carry an approximation made of points: a swarm of candidate states called particles, a thousand of them in this system, dense where belief is strong. The filter's loop becomes concrete and almost embarrassingly simple:
- Predict. Give every particle its own small random kick. The swarm spreads: that is uncertainty growing while you wait.
- Update. When the batch arrives, weight every particle by how well its (sg, sm) explains the counts. Good explainers get heavy, bad ones fade.
- Resample. When too few particles carry most of the weight (the standard gauge is the effective sample size, 1/Σw²), redraw the swarm in proportion to weight: clone the heavy, drop the light, reset to equal weights. Survival of the fittest guesses.
The posterior mean of the swarm is the tracked estimate; its spread is genuine uncertainty, and from it you can draw the 90% credible ellipse you saw in the opening animation. This is the machine in the hero image, and here it is again with every knob exposed (a lighter swarm of 400 particles, so your browser keeps up; the real system runs 1,000 or more). This one rewards fiddling.
Try this
- Pick readout step and watch the moment the step hits: the swarm is wrong for about one batch, stretches toward the new truth, then snaps tight around it. That lag is the price of causality, roughly one batch, and no causal method does better.
- Shrink the batch size. Smaller batches mean noisier counts, so the swarm fattens and the strip chart's band widens. Information per batch is the currency everything else is priced in.
- Pick uniform (both) and recall the first surprise: the truth slides along the diagonal, the filter tracks it faithfully, and none of it matters to the decoder. Tracking well and mattering are different things.
- Now the sneaky one. Push trust in each batch to its maximum and shrink the batch size, then watch the coverage readout. The ellipse hugs the truth too tightly and the coverage number sags well below 90%. The filter has become confidently wrong. Hold that thought; it is the next section.
One nicety before we move on: how the filter's output actually reaches the decoder. Batch k is always decoded with the map predicted from batches 1 through k−1, and only afterward does the filter get to see batch k. Strictly causal, one step ahead, the streaming discipline an online system would face. The decoder's edge probabilities get multiplied by the posterior mean scales, the matcher is rebuilt (with caching, since slow drift means the rebuild is usually a cache hit), and the next batch is decoded with fresher beliefs.
## honest error bars from a dishonest likelihood
There is a lie buried in the update step, and it would be easy to never notice. The likelihood multiplies together one binomial term per detector and per pair, as if all those counts were independent readings. They are not. One physical error lights two detectors; detectors share mechanisms; the counts are correlated. Statisticians call what we built a composite likelihood: a principled fake, known to point at the right answer (the peak lands in the right place as data accumulates) but to be overconfident about it, because double-counted evidence shrinks the error bars as if you had more independent information than you do.
Does it matter? If all you report is the estimate, maybe not. But an instrument that says "readout noise is up 40%, and I am 90% sure it is between 35 and 45" had better be right about the 90% part, or nobody should act on it. The property to demand is coverage: across many uses, the intervals you stamp "90%" should contain the truth about 90% of the time. Coverage is measurable. So measure it: run the full filter against simulations where the true drifting scales are known, and count how often the 90% band actually catches them.
The composite likelihood fails this audit spectacularly, and the fix is old and blunt: tempering. Raise the likelihood to a power λ before using it, which is exactly "count each batch as a λ-sized fraction of the evidence it claims to be." Small λ widens the posterior without (to first order) moving its center. Then tune λ on simulation until measured coverage hits nominal, and only then trust the bands. The measured curve below says how badly this was needed: even at λ = 0.3, each batch already discounted to less than a third of the evidence it claims to be, the nominal-90% bands catch the truth 5% of the time, and the untempered likelihood is worse still. Honest coverage arrives only near λ = 0.005 (this is the d = 5 tuning; the crossing shifts with code distance, so each distance gets its own exponent). The naive likelihood overstated its information by a factor of a couple hundred. (The toy in the previous section needed only mild tempering because it multiplies two statistics; the real likelihood multiplies thousands of overlapping counts, so its overconfidence is thousands of times worse.)
The result that makes this workable is in the amber series above: decoding performance is flat in λ. Across two and a half decades of tempering, the adaptive logical error rate moves by 0.2%, within noise. Why: the decoder consumes only the posterior mean, and tempering rescales the posterior's spread while barely relocating its center. Tempering costs decoding nothing, so calibration is free. You tune λ for honest bands and give up nothing in exchange. Very few knobs in engineering are this polite.
## making it fast enough to run live
An online instrument has a deadline. Here it is concrete: a batch of 500 shots of a d = 7 memory experiment takes the quantum computer about 19 milliseconds to produce. If the filter cannot finish its update in that window, it falls behind the machine it is supposed to watch. (For contrast, the decoder's inline correction path has a budget more than ten thousand times tighter, under a microsecond, and belongs to dedicated hardware. The filter never touches that path; it lives on the calibration timescale, where milliseconds are the unit. Statistical computing can live there.)
Count the naive cost. Each of P particles must evaluate its likelihood: for every detector and every tracked pair, interpolate that statistic's expected rate at the particle's (sg, sm), take logs, multiply by the observed counts. With D + NP around 5,700 statistics at d = 7, that is P × 5,700 table lookups per batch, and at a million particles that means billions of reads per 19-millisecond window, hopelessly over budget.
Two ingredients dissolve it. First, the exact rate formulas are expensive to evaluate from scratch, so the system precomputes them on a 15 × 15 grid of (sg, sm) values spanning ¼× to 4×, once, before streaming starts; at run time a particle's rates are bilinear interpolations off the grid, standard numerical practice. The second ingredient is the one worth teaching. Write down what a particle actually computes: its log-likelihood is a giant weighted sum of interpolated log-rate tables, with weights given by the batch's counts. Sums commute. Instead of interpolating 5,700 tables and then dotting with the counts, dot the counts with the tables at each grid node first, getting a single number per node:
A(g) answers "how well would grid node g explain this batch," and because bilinear interpolation is itself a weighted sum (of four corner nodes), each particle's exact log-likelihood is just the interpolation of A at its position. Four reads and a blend. The per-batch cost collapses to 225 × 5,700 for the field, independent of particle count, plus a trivial per-particle tail. And this is an identity, not an approximation: the code unit-tests the collapsed path against the naive one down to floating-point round-off.
The measurements say it worked. The naive path costs 6.2 seconds per batch at 10⁵ particles on eight CPU cores, 300× over deadline. The field path holds 10⁵ particles at 22 ms on the same cores, and a GPU port is essentially flat in particle count: about 2 ms per batch on an H100 whether the swarm holds a thousand particles or a million. Flat, because past the field construction the update is bookkeeping, and a million-particle posterior costs the same few kernel launches as a thousand. An order of magnitude inside the deadline, with the surplus available for richer noise models: the same algebra prices a k-dimensional noise state at Gᵗ × 5,700, so four to six channels stay affordable on the GPU.
## does it work? four million shots per answer
Time to grade it, and grading requires ground truth, so this section is simulation (the real machine comes next). The protocol: simulated surface-code memory experiments at d = 3, 5, 7, with the true noise scales following scripted drift trajectories the filter never sees. Every batch is decoded three ways, by decoders that differ only in the map handed to the matcher:
- Static: calibrated at the start, never updated. The status quo.
- Adaptive: updated between batches by the filter, strictly causally. Ours.
- Oracle: handed the true scales at every batch. The ceiling: no tracker can beat a decoder that already knows the answer.
The fair score is how much of the static-to-oracle gap the adaptive decoder recovered, not merely whether it beat static. Recovering most of the gap means the tracker is extracting nearly everything the syndrome stream has to give. Each condition gets four million simulated shots, so the comparisons that follow are measured to a fraction of a percent.
The headline scenario is the readout step, hardware's classic misbehavior: mid-stream, the measurement channel jumps to 3× while gates hold steady, exactly the kind of relative drift the first surprise said should hurt. It does. After the step, the static decoder's logical error rate runs 11.1% to 15.7% above the adaptive one's, depending on distance. Read as error reduction: adapting cuts post-step logical errors by 10.0% at d = 3, 10.0% at d = 5, and 13.6% at d = 7, recovering 93%, 99%, and 100% of the oracle gap. (Each of those comparisons carries a z-score above 18. A z-score counts how many standard deviations of pure luck would be needed to fake the result; beyond about 5, journals stop asking, and 18 is not a fluke in any universe.) The trend with distance deserves a pause: bigger codes suffer more from stale maps, because matching paths get longer and a longer path compounds more miscalibrated weights. The better your code, the more this kind of tracking is worth.
The rest of the table behaves the way the theory predicts, which is most of why you should believe the table. Gentle continuous drift (slow sinusoids, random walks) costs a static decoder little overall, since the trajectory hovers near the calibration point most of the time; the filter's decoder stays within 0.7% of the oracle in every such condition, pinned to the ceiling even when the ceiling is low. And uniform drift, the negative control, delivers its promised nothing: static and oracle statistically tie, so there was nothing for adaptation to win, and the filter correctly wins nothing. A method that only showed gains would be suspicious; this one shows gains exactly where the math says gains exist, and nulls exactly where the math says nulls.
One more comparison, against the strongest prior approach: per-edge estimation (Spitz et al., 2018), which re-fits every edge probability of the matching graph directly from syndrome correlations, no physics model at all. At big batches it is excellent, statistically tying our filter (and at d = 7 it even edges out the "oracle" by a hair, which sounds impossible until you notice the oracle knows the true parameters but still pushes them through an imperfect graph decomposition; empirically fitted weights can quietly absorb that modeling error). But per-edge fitting needs every edge to collect enough events in every window, and as batches shrink it starves: at 1,000-shot batches it is measurably behind, and at 250-shot batches it collapses to a quarter worse than the oracle, below even the static decoder it was meant to improve. The filter, pooling every detector and every pair into two parameters, is flat across the entire range, within 0.7% of oracle even at 250-shot batches. That factor of ten in data efficiency is also a factor of ten in trackable drift speed, and it is the difference between a method that needs the machine to drift slowly and one that keeps up.
## the real thing: replaying google's willow
Simulation can only prove the method works on the world you simulated. Google recently ran their 105-qubit Willow chip through a landmark surface-code campaign and then did the field a favor by releasing the raw data: 248 experiment configurations across three code distances, 50,000 shots each, 12.4 million shots total, in execution order. Execution order is the gift. It means the files preserve time, and anything that drifted while they recorded is still in there, waiting to be found by the first analysis that thinks to look.
The replay discipline: strictly causal, as if live. Each file streams through the filter in 100 batches of 500 shots; every batch is decoded (both statically and adaptively) before the filter is allowed to learn from it; the first 20 batches serve as a calibration window that anchors the baseline, so the tracked scales measure drift within the file, relative to its own opening minutes, and nothing is ever peeked at ahead of time. And before making any drift claims, the pipeline first had to reproduce the dataset's published anchors, detector firing densities and error-burst statistics, from the raw files. It does; and its plain matching decoder lands above Google's published error rates by the margin expected, since the published numbers came from heavier decoders that exploit correlations plain matching ignores.
Two findings, then the flagship.
Finding one arrived uninvited. The released dataset ships with per-configuration noise priors, tuned by Google via reinforcement learning to maximize decoding performance. Anchoring the filter's likelihood to them seemed natural. It backfired in an instructive way: in aggregate the priors match the data almost perfectly (median observed-to-predicted firing ratio 0.998), but per-detector the ratios spread from 0.60 to 1.66. Optimized to decode well, not to be statistically true, and nobody had audited them this way before. Fed to a likelihood as truth, those static discrepancies masquerade as drift, and the filter chased them to a spurious optimum (dragging sm to 0.65 and making adaptive decoding 1.7% worse than static). The fix: trust the data for levels, trust the physics for responses. Each statistic's baseline rate is anchored to its empirical value from the calibration window, and the detector error model supplies only how that rate bends as (sg, sm) move. The runaway disappeared. Concurrent work by another group has since confirmed the miscalibration independently from the decoding side.
Finding two is a calibrated null, which is more interesting than it sounds. For most of the 248 configurations, the filter's 90% band contains 1.0 (no drift) in every batch, and the bands tighten as the code grows, since more detectors mean more information per batch. That is not "we found nothing." It is a stability certificate: within this file, noise scales moved less than a few percent (at d = 7, essentially for free), with stated confidence and audited coverage behind the statement. Raw firing rates could never say that; only an estimator with honest error bars can certify an absence. Willow gets recalibrated every few hours, and here is an independent instrument reporting, from the syndrome stream alone, that between recalibrations the device mostly holds still. Mostly.
The flagship is the exception. A fifth of configurations show at least one batch where the band excludes 1.0 on the measurement channel, and the strong events concentrate in two chip placements, which is itself hardware forensics: something about those neighborhoods misbehaves. The clearest single file is below, real data, drawn from the replay outputs. The measurement scale ramps smoothly from 1.0 to 1.81 over the file, the band peeling away from 1.0 and excluding it for the file's whole back half (peak z = 6.8, far past fluke territory), while the gate scale sits pinned within a few percent of 1.0 throughout. Readout noise nearly doubling inside a couple of minutes, resolved to the correct channel, on a file whose ordinary decoding statistics look unremarkable. Flip to the second tab and the same instrument shows a different personality of drift: the syndrome density (the fraction of detectors firing per batch) hopping between two levels in plateaus of about 5,000 shots, the classic signature of a single two-level defect toggling on and off, with the filter's burst gate, a guard that sets aside any batch whose counts are wildly out of line with the running record instead of feeding it to the update, flagging the plateaus (small triangles) rather than letting them poison the estimate.
And the decoding scoreboard? Across all 248 configurations, adaptive and static decoding tie exactly: no configuration differs by more than ordinary luck produces. If you have followed the whole page, you already know why, and you should be suspicious if it had come out otherwise: the simulations place the matching payoff at channel imbalances around 3×, and Willow's few-hourly recalibration keeps its within-file excursions well below that, 1.81× at the very worst. On a device this well kept, the filter's value is the measurement itself: certificates, channel attribution, transient catalogs, from data the machine was already producing. The regime where tracking also pays in logical errors, long uninterrupted computations with rare recalibration pauses, is exactly the regime hour-long logical algorithms will create.
One caveat for the road: the released files carry no wall-clock timestamps, so the shot index is the only time axis, and whatever happened between files (including those recalibrations) is invisible. Within-file drift is therefore a lower bound on what the device does between calibrations. The tracked scales are also relative to each file's own calibration window by construction; the second tab of the figure shows the one artifact that convention produces (a plateau that begins inside the window makes later batches read below 1.0), flagged rather than hidden.
## what this adds up to
Step back and look at the shape of the thing. The product is not a decoder; it is an instrument. A streaming, self-calibrating posterior over physically meaningful noise scales, computed from data the decoder already consumes, cheap enough to run beside any experiment without being noticed. It certifies stability when there is nothing to find, attributes drift to a channel when there is, catalogs transients as it goes, and, on hardware drifting harder than today's, hands the decoder back 10 to 14% of its logical errors, with the gain growing exactly where the field is heading: bigger codes, longer runs.
The load-bearing ideas are worth restating without the machinery, because none of them is really about quantum computing:
- Before estimating something, check whether your consumer can feel it. One direction of drift turned out to be invisible to the decoder, and that single fact collapsed the state space from thousands of dimensions to two.
- Before trusting a statistic, check what it can distinguish. Firing rates confounded the two channels; one extra second-order statistic separated them. Rank analysis first, then engineering.
- If your likelihood is approximate, your error bars are claims requiring proof. Measure coverage against ground truth, temper until the claims come true, and verify the tempering costs nothing downstream. Then, and only then, publish the bands.
- Exact algebra is the best performance engineering. The grid-field identity bought four orders of magnitude without approximating anything.
That template, pool scarce data through low-dimensional physics, verify your own uncertainty, stay inside the latency budget, applies well beyond this project, and well beyond quantum computing. The machine at the top of this page is just its clearest portrait: a swarm of guesses, honestly uncertain, forever half a step behind a truth it can never observe directly, and close enough behind to matter.
Built with Python, Stim, PyMatching, NumPy, CuPy (float64) · SLURM array sweeps on
Sapelo2 · A100/H100 · 17 unit tests incl. grid-field-vs-dense to round-off and
GPU/CPU parity. Every figure on this page is live: the toys run real algorithms (the filter
widget is a genuine bootstrap particle filter) and the Willow and benchmark charts plot the
actual replay outputs from the repo.
Liam Kozma · liam@liamkozma.com