Skip to content

Fix the discarded two-qubit noise merge and suppress the gate on a leaked qubit at the caller - #898

Open
ciaranra wants to merge 2 commits into
devfrom
generic-error-model-noise-slots
Open

ciaranra wants to merge 2 commits into
devfrom
generic-error-model-noise-slots

Conversation

@ciaranra

Copy link
Copy Markdown
Member

Closes #893. Closes #888.

#893 — two-qubit noise silently discarded, or a crash

GenericErrorModel.process merged memory noise into gate noise with

if qops_after:
    qops_after = qops_after.extend(qops_mem)

list.extend() mutates in place and returns None, so this had three failure modes with p2_mem configured:

  • gate noise produced operations and memory noise produced some too: qops_after became None, the later if qops_after: was false, and all two-qubit noise for that gate was silently dropped
  • gate noise produced operations and memory noise produced none: qops_after.extend(None) raised TypeError: 'NoneType' object is not iterable
  • gate noise produced none: memory noise was never merged at all, because the merge sat inside if qops_after:

The same line existed in DepolarizingErrorModel (depolarizing_error_model.py:191). A repo-wide sweep for = <list>.extend(...)-style assignments found exactly those two; both are fixed.

#888 — the gate was never suppressed on a leaked qubit

noise_tq_depolarizing_leakage filtered leaked pairs into a local rebinding and returned only noise, so the caller emitted the original unfiltered gate. Its own comment says "Don't apply a gate if an input qubit has already leaked", which did not happen. The single-qubit helper does honour it, by returning the filtered gate inside its result.

The caller now owns suppression, and the helper's internal filter is deleted. Copying the single-qubit pattern would not work: with p2_mem the two-qubit path calls the helper twice, so a gate-returning helper would emit the gate twice.

A pure surviving_two_qubit_op(op, machine) — no sampling, no mutation — returns the op restricted to pairs where neither qubit has already leaked, or None. process calls it twice, because the gate's own noise can leak a qubit through an "L" fault and so change the machine state mid-operation:

  • before gate noise, to decide what gate to emit and what pairs the gate noise may target. The pre-gate survivor is the right thing to emit: a qubit that leaks because of this gate's noise still had the gate applied.
  • again after gate noise, against the updated leaked set, so memory noise cannot touch a pair the gate just leaked — which is what the helper's internal filter did.

RNG behaviour is unchanged. Each helper call receives exactly the pairs its old internal filter would have retained at that moment, so draw counts match per call; a zero-pair call is skipped and consumes nothing. Verified by 800 seeded comparisons against the original helper on noise operations, leakage state and subsequent RNG output, plus a test using a ("L", "I") fault model, the case that makes the two filters differ.

Also removed: qops_before, initialised every iteration and never assigned anywhere, along with its emission branch.

Tests

tests/pecos/unit/test_two_qubit_noise.py now covers the caller, which had no direct tests despite producing four defects (#816, #888, #893, #897):

  • both models' merge cases, including memory noise surviving when gate noise produced nothing
  • one qubit leaked: the emitted gate acts only on surviving pairs; no pair surviving means no gate at all; noise targets only survivors
  • the four slot behaviours asserted explicitly, so a future change cannot silently swap which noise replaces the gate and which follows it
  • seeded gate-leakage-then-memory-noise equivalence

Mutation-checked, by me as well as the implementer: restoring the extend assignment fails 8 tests; emitting the unfiltered op instead of the survivor fails 21; moving the single-qubit call into the append-after slot fails the slot test.

Reported, not fixed

#897: the gate is emitted only on the erroneous_ops is None branch, so a helper returning [] rather than None makes the gate vanish. Unreachable with the shipped GenericMachine, whose leak() always returns an Init op, but reachable for a custom machine implementing the protocol.

Verification

just python-ci-build-test, just pytest-ci-core-shard rest (6786 passed, 50 skipped, the pre-existing #889 xfail), just python-ci-lint. Branch is level with dev.

How this was produced

Implemented by OpenAI gpt-6-astra (Codex CLI 0.154.0) from a task packet written by Claude Fable 5.1 in Claude Code. The arm stopped once rather than implement my design: I had specified filtering once up front, and it showed that gate noise can leak a qubit between the two helper calls, so one filter cannot preserve either the old eligibility or the RNG stream. That stop produced the filter-twice design here. It also found the second extend instance. Claude reviewed every hunk, tightened two readability points directly (a comment explaining why a clean gate travels in the erroneous_ops slot, and computing the memory filter only when p2_mem is set), re-ran the named lanes, and repeated two mutation checks independently. Posted at the maintainer's request.

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

Labels

None yet

Projects

None yet

1 participant