Idle-gate passes and fail-loud idle-noise guard for the Guppy DEM pipeline - #420
Idle-gate passes and fail-loud idle-noise guard for the Guppy DEM pipeline#420ciaranra wants to merge 22 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
…twirl handoff test off deprecated aliases
… rates with idle noise
…tead of the surface-code generator
…r and split the workflow guide into stages
|
|
||
| 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") |
There was a problem hiding this comment.
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 |
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...
| <!--continuation--> | ||
| ```python | ||
| sampler = dem.to_sampler() | ||
| batch = sampler.generate_samples(2000, 1) |
| Detector("s0_r1", "m0", "m1"), | ||
| Detector("s1_r1", "m1", "m2"), | ||
| ] | ||
| observables = [Observable("m0")] |
There was a problem hiding this comment.
These Detectors and Observables should have names to reference?
There was a problem hiding this comment.
Or... Maybe the name is implied by the indexing... Di... Li... etc...
There was a problem hiding this comment.
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()
…flow review comments
| .with_p1_probability(0.002) | ||
| .with_p2_probability(0.02) | ||
| .with_meas_probability(0.02) | ||
| .with_prep_probability(0.02) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Having both build_dem_from_guppy and DetectorErrorModel.from_guppy feels odd... can we unify?
…eResult export, registry reconciliation, stub refresh
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.