Idle-gate passes and fail-loud idle-noise guard for the Guppy DEM pipeline - #420
Draft
ciaranra wants to merge 42 commits into
Draft
Idle-gate passes and fail-loud idle-noise guard for the Guppy DEM pipeline#420ciaranra wants to merge 42 commits into
ciaranra wants to merge 42 commits into
Conversation
…pipeline, with user-guide coverage
…/_model, p_idle_quadratic, p_idle_coherent)
…clobbers; correct coherent-branch docs
…n of coherent idle noise in the standard DEM builder
This was referenced Aug 3, 2026
…twirl handoff test off deprecated aliases
… rates with idle noise
…tead of the surface-code generator
…r and split the workflow guide into stages
ciaranra
commented
Aug 4, 2026
ciaranra
commented
Aug 4, 2026
ciaranra
commented
Aug 4, 2026
ciaranra
commented
Aug 4, 2026
|
|
||
| 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") |
Member
Author
There was a problem hiding this comment.
We should have a BpOsdDecoder or a LdpcDecoder...
ciaranra
commented
Aug 4, 2026
|
|
||
| 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 |
Member
Author
There was a problem hiding this comment.
We should think about having a nice convenience function for each of these decoders that looks nice/same name...
ciaranra
commented
Aug 4, 2026
ciaranra
commented
Aug 4, 2026
| <!--continuation--> | ||
| ```python | ||
| sampler = dem.to_sampler() | ||
| batch = sampler.generate_samples(2000, 1) |
ciaranra
commented
Aug 4, 2026
…eResult export, registry reconciliation, stub refresh
This was referenced Aug 4, 2026
# Conflicts: # examples/surface_code_experiments.ipynb # python/quantum-pecos/tests/qec/test_from_guppy_dem.py
…setters to field names
…herent dephasing twirl
…ne-law unit conversion
…r the legacy preset
…e mode-switch bool
ciaranra
commented
Aug 5, 2026
| .with_p_idle_linear_model({"X": 0.25, "Y": 0.25, "Z": 0.5}) | ||
| .with_p_idle_coherent(False) | ||
| .with_p_idle_quadratic_rate(0.03 / math.pi) | ||
| .with_idle_after_2q(1.0) |
Member
Author
There was a problem hiding this comment.
do the general_noise().with_idle_after_2q(...) ignore other idles by default like the DEM builder does... should we have and option to not ignore other idles?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_guppyandbuild_dem_from_guppygain two keyword-onlypass parameters, applied to the traced circuit after normalization and before metadata
attachment:
strip_traced_idles=Trueremoves identity-like gates (I,Idle, zero-anglerotations) from the trace.
idle_after_2q_duration=<positive float>inserts anIdleof that duration on bothqubits 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 thep_idle_*_ratefamily) while the final traced circuit contains noIdlegates nowraises
ValueErrorinstead 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
TickCircuitpasses (remove_identity,insert_idle_after_two_qubit_gates); no Rust changes.Docs
docs/user-guide/dem-from-guppy.mdgains three sections, every code block anexecutable generated doc test:
(runtime idles are nanosecond
TimeUnits; linear/sine rates scale per time unit,quadratic rates per time unit squared).
to_string()is directly Stim-parsable;to_string_decomposed()(source-attached, hyperedge-preserving) vsto_string_terminal_graphlike_decomposed()(lossy edge projection for graphmatchers), each parse-checked against stim as the format oracle.
SampleBatch.decode_count(...)for all three decoders, plus direct per-shotconstruction via
TesseractDecoder.from_demandDemAwareDecoder.from_dem(..., decoder_type="bp_osd"). The direct Tesseract exampleuses
preset="fast"to match thedecode_countconfiguration.docs/user-guide/decoders.md: the Python decoder table now reflects the actualpecos.decodersre-exports instead of the staleMWPM2D/DummyDecoder-only list.Tests
from_guppy(..., idle_after_2q_duration=...)is byte-identical to themanual trace -> normalize -> insert-idle ->
from_circuitpipeline.points, plus non-positive/NaN/inf duration rejection.
under
strip_traced_idles=True(guard then fires), paired with the accept case.result_tagscoexistence with idle insertion is pinned byte-identical to therecordsform.Verification
pytestontest_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 lintover 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-probabilityreading 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 isthe engines distribution); scalar alone = total depolarizing rate. The
engines leakage key
"L"is reserved and rejected (DEM cannot representleakage).
p_idle_sin_squared+p_idle_sin_squared_model— stochastic sine-lawdephasing,
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)^2per unit idle time via an equivalent T2 — coherentcross-location accumulation is the EEG pipeline's domain); nonzero RX/RY
fail loud.
p_idleshorthand is removed from these two entry points (explicitp_idle_linearonly); the low-level Rustfrom_circuitkeeps itsprimitive
p_idle.NoiseConfigwould silently clobber fail loud:p_idle_coherentwithp_idleort1/t2(set_idle_rzreplacesthem), and
p_idlewitht1/t2(the T1/T2 channel shadows thedepolarizing base channel).
p_idle_linear_rate,p_idle_quadratic_rate, andp_idle_quadratic_sine_rateare deprecated with explicit replacements;ambiguous structured + low-level combinations fail loud.
GeneralNoiseModelBuilderadditionally rescales its public inputs, sobuilder 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.pyis 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 customZ-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(...)withbuild.evaluate_result_columns()mapping the run's tagged result columns into a
SampleBatch. The page isexplicit 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 inscripts/docs/generate_doc_tests.pybut parsed into a field that was neverread, 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-containedvia
<!--setup-->) were removed, keeping that page's behavior unchanged.mkdocs build --strictis clean; a stale pre-existingproposals/README.mdnaventry was removed.
Typed-spec ergonomics
DetectorandObservablenow accept a bare tag string as shorthand forresult_ref(tag), soDetector("s0_r0", "s0_r1")replacesDetector(result_ref("s0_r0"), result_ref("s0_r1")). Strings previously raisedTypeErrorin that position, so the change is purely additive;rec[-k]andthe explicit
result_ref(..., occurrence=...)form are unchanged and can bemixed 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.
from_circuitsilently resolved conflicting idleparameters.
set_t1_t2makes T1/T2 the base channel that shadowsp_idle,and
set_idle_rzzeroesp_idleand overwrites T1/T2. The guard now lives inapply_noise_options, the shared helper behindDetectorErrorModel.from_circuit,DemBuilder.with_noise,DemSampler.from_circuit,DemSampler.with_detectors,and
DemSamplerBuilder.with_noise, so every ingest path is covered.Verified by mutation: with the guard removed and the extension rebuilt,
p_idle=0.4alongsidet1/t2produced a DEM byte-identical tot1/t2alone — the requested rate silently discarded — and the mutation failed exactly
the two tests that target that guard.
DemAwareDecoderwrapped observable bits past 64. It hadno width guard and packed with
1 << iinto au64, so observable 70 set bit 6in release builds. It now uses the wide
ObsMaskpath and returns anarbitrary-precision Python integer; masks that fit in 64 bits are unchanged.
FusionBlossomDecoder.from_demexposed (the Rust method existed but was unreachable),
DemAwareResultre-exported, the stale
.pyistub refreshed, and the two decoder registriesreconciled — including making
perturbeddelegate to its inner decoder, sinceperturbed:inner=TYPEwraps an arbitrary decoder and a fixed classification iswrong for half its uses.
shared
pecos/qec/_idle_noise.py, and the native-surfaceNoiseModelnowaccepts the same three families as the Guppy entry points, so the twirl and
threshold routes inherit the same conflicts, deprecations, and validation.
Semantics characterization
tests/qec/test_record_vs_meas_id_semantics.pypins thatrecords[-k]andmeas_idsname the same measurement on every fixture available, using thebuilder'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/qecplus the full generated doc-test suite: 1585 passed, 11skipped, 1 xfailed, 2 failed — both failures pre-existing and unrelated
(
qec-guppytransversal-CNOT blocks hit a HUGR execution stall; reproducedwith 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..ruff_cachecleared. Note thatjust lintends with a Go formatting step, so its exit status does not reflectthe 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.pyis corrected.