Skip to content

Fix multi-fault propagation: inject each fault at its own tick and before/after position - #441

Closed
ciaranra wants to merge 1 commit into
devfrom
fix-multifault-prop
Closed

Fix multi-fault propagation: inject each fault at its own tick and before/after position#441
ciaranra wants to merge 1 commit into
devfrom
fix-multifault-prop

Conversation

@ciaranra

@ciaranra ciaranra commented Aug 5, 2026

Copy link
Copy Markdown
Member

Correctness fix in pecos-qec fault-tolerance analysis. Independent of #415.

The bug

propagate_faults XORed every fault into the initial PauliProp and then propagated from min_tick onward. Two consequences:

  1. Multi-fault results were wrong. A fault at tick 5 was injected as though it existed at min_tick, so gates between those ticks acted on a Pauli that should not have existed yet. Gate order matters, so any weight >= 2 result was untrustworthy.
  2. It disagreed with propagate_fault even at weight 1. propagate_fault honours before (for before=false it skips ticks <= fault_tick); propagate_faults ignored before entirely and applied gates at min_tick inclusive. A single before=false fault whose own tick contains a gate on its qubit therefore produced different answers from the two functions.

The doc comment ("Faults are combined (XORed) and then propagated") described the implementation rather than justifying it, and PauliFaultIterator feeds this function location combinations spanning arbitrary ticks.

The fix

Walk every tick in order, injecting each fault at its own tick and before position: before-faults, then the tick's gates, then after-faults. This reduces exactly to propagate_fault for a single fault in both before cases, which is the property the new tests pin.

Walking ticks earlier than the first fault is deliberately not skipped. apply_gate_flip_ledger maintains no dense measurement counter — MZ acts only when prop.contains_z, PZ/QAlloc/MPZ clear, and Clifford gates map identity to identity — so an empty frame passes through untouched. A min_tick skip is what caused the divergence in the first place.

The Pauli-injection mapping was duplicated between the two functions, which is how they drifted apart. It is now a single inject_fault helper that init_pauli_prop_with_fault delegates to.

Tests

Four new tests, three of which fail against the old implementation (the before=true equivalence case already agreed, because of the old inclusive min_tick):

  • single-fault equivalence between propagate_fault and propagate_faults, for both before values
  • a hand-derived two-fault case at different ticks, where the later fault must not experience the earlier tick's H
  • a seeded property test asserting single-fault agreement across many circuits and fault locations, which guards the class rather than the two specific instances

Verified by mutation: injecting after-faults before the tick's gates fails both the after-tick equivalence test and the property test.

A second, separate bug this exposed

Three pre-existing tests changed outcome, because two bugs had been cancelling each other out. PauliFaultIterator builds its Pauli set from X/Y/Z with no identity (fault_tolerance.rs:353), so at a multi-qubit gate location every qubit always receives a non-identity Pauli — the single-leg faults IX/XI/IY/YI/IZ/ZI at a CX location are structurally unreachable. Those tests' comments described a single data-qubit X, the iterator actually produced XX, and the buggy propagation pushed XX through the CX a second time, cancelling one leg and accidentally reproducing the intended single-qubit effect.

Rather than weaken those tests, they now construct the intended single-leg fault directly as a PauliFault (representable even though the iterator will not generate one), and their original assertions are unchanged — including that naive three-qubit syndrome extraction is not 1-fault tolerant. Each carries a comment explaining why the fault is built directly.

The enumerator gap itself is not fixed here; it is a distinct bug and deserves its own change. It matters because it errs toward false confidence — unenumerated faults make a circuit look more fault-tolerant than it is — and because a hook error is exactly a single-leg fault on one arm of a two-qubit gate. Note the DAG path is unaffected: possible_faults (propagator/dag.rs:1170) offers the identity option per qubit and so does enumerate single-leg faults.

Several diagnostic-only tests report different aggregate counts now that before=false is honoured; all still pass. Some of them compute an FT verdict without asserting it, which is worth tightening separately.

Verification

  • cargo test -p pecos-qec: 802 passed, 0 failed (636 unit, 108 integration, 58 doctests)
  • uv run --frozen pytest python/quantum-pecos/tests/qec -q: 1138 passed, 1 skipped, 1 xfailed
  • just build-debug, just lint: clean

@ciaranra

ciaranra commented Aug 6, 2026

Copy link
Copy Markdown
Member Author

Folded into #415 — the commit is now on code-distance-rust and verified there. Closing to keep review in one place.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant