Skip to content

Idle-gate passes and fail-loud idle-noise guard for the Guppy DEM pipeline - #420

Draft
ciaranra wants to merge 22 commits into
devfrom
guppy-dem-idle-docs
Draft

Idle-gate passes and fail-loud idle-noise guard for the Guppy DEM pipeline#420
ciaranra wants to merge 22 commits into
devfrom
guppy-dem-idle-docs

Conversation

@ciaranra

@ciaranra ciaranra commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

Gives the Guppy-to-DEM pipeline first-class idle-gate handling and documents the full
workflow from a Guppy QEC program to decoded logical error counts.

API (pecos.qec.dem)

Both DetectorErrorModel.from_guppy and build_dem_from_guppy gain two keyword-only
pass parameters, applied to the traced circuit after normalization and before metadata
attachment:

  • strip_traced_idles=True removes identity-like gates (I, Idle, zero-angle
    rotations) from the trace.
  • idle_after_2q_duration=<positive float> inserts an Idle of that duration on both
    qubits after every two-qubit gate. Stripping runs before insertion when both are set.
    Non-finite or non-positive durations are rejected.

New fail-loud guard: supplying any idle-noise parameter (p_idle, t1/t2, or the
p_idle_*_rate family) while the final traced circuit contains no Idle gates now
raises ValueError instead of silently building a DEM without the requested idle noise.
The default Selene runtime emits no idle gates, so this closes a silent no-op that
previously made those parameters inert. No in-repo caller passes idle-noise parameters
to these entry points, so no existing usage breaks.

Both passes reuse the existing Rust TickCircuit passes (remove_identity,
insert_idle_after_two_qubit_gates); no Rust changes.

Docs

docs/user-guide/dem-from-guppy.md gains three sections, every code block an
executable generated doc test:

  • Idle Noise — the strip/insert passes, the guard, and time-unit semantics
    (runtime idles are nanosecond TimeUnits; linear/sine rates scale per time unit,
    quadratic rates per time unit squared).
  • Exporting the DEM as Stim Textto_string() is directly Stim-parsable;
    to_string_decomposed() (source-attached, hyperedge-preserving) vs
    to_string_terminal_graphlike_decomposed() (lossy edge projection for graph
    matchers), each parse-checked against stim as the format oracle.
  • Decoding: PyMatching, Tesseract, and BP-OSD — one sampled batch through
    SampleBatch.decode_count(...) for all three decoders, plus direct per-shot
    construction via TesseractDecoder.from_dem and
    DemAwareDecoder.from_dem(..., decoder_type="bp_osd"). The direct Tesseract example
    uses preset="fast" to match the decode_count configuration.

docs/user-guide/decoders.md: the Python decoder table now reflects the actual
pecos.decoders re-exports instead of the stale MWPM2D/DummyDecoder-only list.

Tests

  • Oracle test: from_guppy(..., idle_after_2q_duration=...) is byte-identical to the
    manual trace -> normalize -> insert-idle -> from_circuit pipeline.
  • The guard is mutation-tested across all 15 idle-noise parameters on both entry
    points, plus non-positive/NaN/inf duration rejection.
  • Strip mechanism is pinned by a runtime-emitted-idle trace whose idles disappear
    under strip_traced_idles=True (guard then fires), paired with the accept case.
  • result_tags coexistence with idle insertion is pinned byte-identical to the
    records form.

Verification

  • pytest on test_from_guppy_dem.py, test_from_guppy_result_tags.py,
    test_guppy_dem_build.py, and the regenerated user-guide doc tests: 310 passed (including the Pauli-twirl handoff suite),
    1 xfailed (6 expected DeprecationWarnings from the exercised legacy aliases).
  • just lint over the whole repo: clean.

Structured idle-noise interface (three rate+model families)

Engines-consistent structured knobs on both entry points. The unifying
concept: model values are relative rates — per-axis multipliers on the
family rate (r_axis = rate * m_axis); the familiar relative-probability
reading is the special case that holds exactly when the law is linear.

  • p_idle_linear + p_idle_linear_model — stochastic, p = rate*m*t.
    Default model uniform {X: 1/3, Y: 1/3, Z: 1/3}, sum-to-1 required (it is
    the engines distribution); scalar alone = total depolarizing rate. The
    engines leakage key "L" is reserved and rejected (DEM cannot represent
    leakage).
  • p_idle_sin_squared + p_idle_sin_squared_model — stochastic sine-law
    dephasing, p = sin(rate*m*t)^2. Default model {Z: 1.0} (scalar alone =
    Z dephasing at full rate, the engines stochastic branch); multipliers
    unconstrained in sum.
  • p_idle_coherent + p_idle_coherent_model — coherent rotations,
    angle = rate*m*t. Default model {RZ: 1.0}; keys RX/RY/RZ with "U"
    reserved. DEM v1 represents RZ only (stored as its exact Pauli twirl,
    sin(rate/2)^2 per unit idle time via an equivalent T2 — coherent
    cross-location accumulation is the EEG pipeline's domain); nonzero RX/RY
    fail loud.
  • The p_idle shorthand is removed from these two entry points (explicit
    p_idle_linear only); the low-level Rust from_circuit keeps its
    primitive p_idle.
  • Combinations the Rust NoiseConfig would silently clobber fail loud:
    p_idle_coherent with p_idle or t1/t2 (set_idle_rz replaces
    them), and p_idle with t1/t2 (the T1/T2 channel shadows the
    depolarizing base channel).
  • The bare Z-only aliases p_idle_linear_rate, p_idle_quadratic_rate, and
    p_idle_quadratic_sine_rate are deprecated with explicit replacements;
    ambiguous structured + low-level combinations fail loud.
  • These parameters match the engines runtime application semantics; the
    GeneralNoiseModelBuilder additionally rescales its public inputs, so
    builder inputs are not directly interchangeable.

Consistency follow-ups filed

The twirl/native-surface route and lower layers are aligned in follow-up issues rather
than this PR: #422 (native-surface NoiseModel port), #423 (layer-2 idle-channel
vocabulary + EEG alignment), #424 (engines GeneralNoiseModel deprecation ramp), #425
(non-idle GeneralNoiseModel surfaces), #426 (bug: from_circuit silently ignores or
overwrites combined idle parameters — the Python-level guards added here are the model
for the binding-level fix). In-repo twirl coherence fixes included here: the Pauli-twirl
handoff test now uses the explicit per-axis names, and a misleading idle-insertion
comment in examples/surface/validate_dem_generators.py is corrected.

Workflow guide (seeds the #414 structure)

docs/workflows/guppy-dem-decoding.md — the first task-oriented workflow page
(new "Workflows" nav section, direction of #414 without migrating existing
pages). It walks one story in five stages, each its own code block: define the
code in Guppy (a hand-written three-qubit repetition-code memory, chosen over
the surface-code generator so the Guppy program stays in view), define typed
detectors and observables with result_ref, generate the DEM with a custom
Z-biased linear idle distribution plus Z-only sine-law dephasing, sample it two
ways, and decode.

Stage 4 shows both sampling paths: dem.to_sampler() for error-model sampling,
and executing the program under sim(...) with build.evaluate_result_columns()
mapping the run's tagged result columns into a SampleBatch. The page is
explicit that the two paths carry different noise (the default Selene runtime
emits no idle gates), so they are not a like-for-like comparison.

Supporting fix: <!--continuation--> was documented in
scripts/docs/generate_doc_tests.py but parsed into a field that was never
read, so multi-block narratives silently generated broken tests. It now
accumulates the preceding visible blocks in the chain. The five decorative
markers in docs/user-guide/fault-catalog.md (whose blocks are self-contained
via <!--setup-->) were removed, keeping that page's behavior unchanged.
mkdocs build --strict is clean; a stale pre-existing proposals/README.md nav
entry was removed.

Typed-spec ergonomics

Detector and Observable now accept a bare tag string as shorthand for
result_ref(tag), so Detector("s0_r0", "s0_r1") replaces
Detector(result_ref("s0_r0"), result_ref("s0_r1")). Strings previously raised
TypeError in that position, so the change is purely additive; rec[-k] and
the explicit result_ref(..., occurrence=...) form are unchanged and can be
mixed freely in one call. Mistyped tags already fail loudly
(ValueError: result_ref 's0_typo' is absent from the compiled Guppy program),
which is the matching check that makes the plain-string spelling safe.

Defect fixes folded in (issues closed by this PR)

Reviewing this work surfaced real bugs; they are fixed here rather than deferred.

Semantics characterization

tests/qec/test_record_vs_meas_id_semantics.py pins that records[-k] and
meas_ids name the same measurement on every fixture available, using the
builder's redundancy rule as the oracle. A two-arm design review had reported
these as divergent coordinate systems; that claim was derived rather than
executed and does not reproduce. The test includes a negative case so it cannot
pass vacuously, and it fails loudly if a future reordering runtime separates them.

Verification (final)

  • tests/qec plus the full generated doc-test suite: 1585 passed, 11
    skipped, 1 xfailed, 2 failed — both failures pre-existing and unrelated
    (qec-guppy transversal-CNOT blocks hit a HUGR execution stall; reproduced
    with and without this branch's changes and filed as Doc tests for qec-guppy transversal-CNOT examples fail with a HUGR execution stall #427).
  • mkdocs build --strict: clean.
  • Repo-wide lint: clean on a cold run with .ruff_cache cleared. Note that
    just lint ends with a Go formatting step, so its exit status does not reflect
    the Python hooks; these runs were verified by hook output text.

Consistency follow-ups filed

The twirl/native-surface route and lower layers are aligned in follow-up issues
rather than this PR: #422 (native-surface NoiseModel port), #423 (layer-2
idle-channel vocabulary + EEG alignment), #424 (engines GeneralNoiseModel
deprecation ramp), #425 (non-idle GeneralNoiseModel surfaces), #426 (bug:
from_circuit silently ignores or overwrites combined idle parameters — the
Python-level guards added here are the model for the binding-level fix), #427
(pre-existing qec-guppy doc-test stall). In-repo twirl coherence fixes included
here: the Pauli-twirl handoff test now uses the explicit per-axis names, and a
misleading idle-insertion comment in examples/surface/validate_dem_generators.py
is corrected.

@ciaranra ciaranra added the enhancement New feature or request label Aug 3, 2026
Comment thread docs/workflows/guppy-dem-decoding.md Outdated
Comment thread docs/workflows/guppy-dem-decoding.md Outdated
Comment thread docs/workflows/guppy-dem-decoding.md Outdated

pymatching = PyMatchingDecoder.from_dem(terminal_graphlike_text)
tesseract = TesseractDecoder.from_dem(source_graphlike_text, preset="fast")
bp_osd = DemAwareDecoder.from_dem(raw_text, decoder_type="bp_osd")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have a BpOsdDecoder or a LdpcDecoder...


pymatching_errors += pymatching.decode(syndrome).correction[0] != actual
tesseract_errors += (tesseract.decode_syndrome(syndrome).observables_mask & 1) != actual
bp_osd_errors += (bp_osd.decode_syndrome(syndrome).observables_mask & 1) != actual

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should think about having a nice convenience function for each of these decoders that looks nice/same name...

Comment thread docs/workflows/guppy-dem-decoding.md
Comment thread docs/workflows/guppy-dem-decoding.md Outdated
<!--continuation-->
```python
sampler = dem.to_sampler()
batch = sampler.generate_samples(2000, 1)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the 1?

Detector("s0_r1", "m0", "m1"),
Detector("s1_r1", "m1", "m2"),
]
observables = [Observable("m0")]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These Detectors and Observables should have names to reference?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or... Maybe the name is implied by the indexing... Di... Li... etc...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should maybe at least put a comment on this... I also wonder if we should just have Detectors(...) and Observables(...) Or DetectorDefs(...) and ObservableDefs(...) that take an array of Detector()/Observable()

Comment thread docs/workflows/guppy-dem-decoding.md
.with_p1_probability(0.002)
.with_p2_probability(0.02)
.with_meas_probability(0.02)
.with_prep_probability(0.02)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should include idle parameters

.with_prep_probability(0.02)
)

results = sim(rep_code_memory).classical(selene_engine()).quantum(stabilizer()).qubits(7).noise(noise).seed(42).run(500)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to show both how to run guppy... but also maybe get the TickCircuit from the QIS trace and do the same sort of Idle stripping and adding after each two-qubit gate like the DEM generation is doing...


`build_dem_from_guppy` returns the DEM together with the audit trail and the
result-column evaluator used in stage 4b. `DetectorErrorModel.from_guppy` builds
the same DEM from JSON metadata instead of typed specifications.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having both build_dem_from_guppy and DetectorErrorModel.from_guppy feels odd... can we unify?

…eResult export, registry reconciliation, stub refresh
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant