Skip to content

Add fold-transversal logical S to the logical builder with derived detectors and a Guppy protocol factory - #777

Merged
ciaranra merged 1 commit into
devfrom
surface-fold-s-builder
Sep 16, 2026
Merged

ciaranra merged 1 commit into
devfrom
surface-fold-s-builder

Conversation

@ciaranra

@ciaranra ciaranra commented Sep 15, 2026

Copy link
Copy Markdown
Member

Slice 4b of the surface-code gadget library, stacked on #776: the fold-transversal logical S and S-dagger become first-class LogicalCircuitBuilder operations with correct detectors and observables, the Guppy protocol module gains the matching factory, and the fault-distance results are pinned numerically.

What changed

  • add_logical_s(label, *, dagger=False) and add_logical_sdg(label): a fold round is a one-round segment (LogicalGateType.FOLD_S) that the generator expands into fold_s_round_gadget in the patch's current orientation. It requires a square rotated patch of distance at least 2 and follows the existing placement rules (after preparation, before final readout). The generator's step emitter accepts OpType.CZ.
  • Detectors: the index-free term walk is unchanged. The emitter adds the fold round's Z partner records where the physics requires them: an X record in a fold round is certified together with the Z record of the left-boundary check at (0, x) for the bottom-row bulk X checks (the "before" map), and the next round's X record together with the fold round's Z record at (y + 2, x) (the "after" map). Both maps are computed once per fold segment from the geometry in the current frame, not tabulated. Zero-round final readouts after a fold resolve the same way.
  • Observables: the readout walk carries X, Y and Z. A fold exchanges X and Y; a term that crossed a fold as X or Y brings every Z-check record of that fold round into the observable's parity (the flow is X_L -> Y_L times the product of the Z checks). So a Z memory keeps its observable, one fold on an X-prepared patch yields none (Y readout is unsupported), and S then S-dagger restores a deterministic X observable. CX images of Y terms are included and same-patch products combine (X times Z is Y).
  • Descriptor: both fold S and fold S-dagger emit the boundary gate SGate (the descriptor tracks sign-free Pauli frames).
  • Guppy: make_logical_s_experiment(rounds_before, rounds_after, *, dagger=False) in the protocol module, rendered with scoped sidebands and checked against the builder program by measurement-partition agreement.
  • pecos.testing gains a reusable deterministic-parity-space helper (noiseless Stim shots, GF(2) rank) so tests can assert that the emitted detectors and observables span exactly the deterministic parities of a circuit.

Evidence

Rank oracle, 2048 noiseless shots per shape, emitted rank equal to the deterministic-parity dimension in every case: Z memory with the fold in the first, middle and last round (25 each at d=3); X preparation with one fold (24), adjacent S and S-dagger (33), separated by two rounds (49); H then fold (41); fold before and after a transversal CX (74 each); at d=5 the X pair adjacent (97) and separated (145). Every emitted detector is deterministic over seeds 0 to 7.

Fault distances by DetectorErrorModel.from_circuit with p = 0.001 everywhere: Z memory with one fold 3 at d=3 and 5 at d=5; X preparation with S then S-dagger 2 at d=3 and 4 at d=5. The second pair is the scheme's known reduction (a Y fault before the fold becomes a Z pair on a mirror pair), stated in #776 and now pinned.

Mutations pinned: dropping the after partner, dropping the before partner, or swapping the maps fails four rank tests each; letting an X term pass through a fold unchanged fails the observable-count test and the emitted observable samples both 0 and 1.

Goldens: five new builder shapes (fold first, mid, last; X pair; H then fold) and a protocol-module golden, captured after the implementation and documented as drift guards; all 145 pre-existing goldens byte-identical.

Verification

Targeted suites, the QEC surface, Guppy, DEM and reliable-observable suites, the generated guide tests, pre-commit on the changed files, and just pytest-ci-core.

Review round (two blind arms, 2026-09-14)

A physics arm (its own GF(2) and Stim flow tooling) and a structure arm (a 3324-program fold sweep, a 1320-program non-fold sweep against the base branch, 26 mutations) found no soundness bug: every emitted detector and observable is deterministic in every fold program, the emitted rank equals Stim's exact determined-measurement count in every fold shape, the partner maps match independently derived flows at d=2 to 7 in both frames, and the fault distances agree with Stim's exact search and are intrinsic to the circuit (no fuller detector set can raise them).

Changes in the fix round:

  • Behaviour change for programs without folds, now announced and pinned. A final memory with zero rounds now gets final boundary detectors; before this PR it got none, which was a completeness gap (a d=3 memory ending in a zero-round readout: 12 detectors before, 16 after; all deterministic; observables, circuit bodies and observable counts unchanged). It is needed for a fold followed by a zero-round readout and is correct on its own. Two non-fold goldens and a rank-oracle test pin it.
  • The two load-bearing rejections in the readout walk (a physical S layer on an X or Y term; a Y term at preparation) now have tests; mutating either had made 52 or 74 sweep programs emit random observables with the suite still green.
  • Fold state is one typed field with an invariant tying it to the operation type and to exactly one round (a multi-round fold silently fired detectors); five inert pieces of logic were removed, keeping one cancellation mechanism, with a test for a same-patch X times Z product through a CX.
  • The fold S-dagger now emits the SGate boundary gate instead of refusing a descriptor: the descriptor tracks sign-free Pauli frames, in which S and S-dagger act identically, and the DEMs are byte-identical.
  • The d=5 X-sector distance test runs in ci-core (it was marked slow at 0.45 s). The parity helper is deterministic_parity_basis and refuses too few shots.

Two things this PR documents and pins rather than changes, because each is a decision beyond it:

  • Observable sign. Observables are defined relative to the noiseless reference, as in Stim. An S then S program (S squared is Z_L) flips the X observable's noiseless raw parity to 1, which raw-parity consumers such as pecos.testing.simulate_tick_circuit will see; the observable metadata has no sign field. Tests pin raw parity 1 for S,S and 0 for S,S-dagger.
  • Matching and hyperedges. The fold's CZ layer creates hyperedges that the matching route (build_decoder, LogicalSubgraphDecoder) skips; at d=5 its logical error rate is 2 to 2.8 times a hypergraph decoder's. The docstring and guide point fold circuits at a hypergraph decoder; a test pins the skipped-hyperedge count (12 with a fold, 0 without at d=3).

Re-review of the fix round (2026-09-14)

A fresh reviewer found no correctness defect: the head is byte-identical to the previous commit's output across 1160 random programs (904 with folds, d=2 to 5), and the zero-round final detectors are complete after a CX, on two patches, and after SZ teleportation or T injection, at d=3 and 5. It did find that the observable-sign behaviour is broader than first documented: 36 of those 904 fold programs have noiseless raw parity 1 (for example S,S before a Z readout via H, or S-dagger pairs through a CX), none without a fold, and the public pecos.qec.surface.extract_detection_events_and_observables reports them flipped on every shot. The docstring and guide now state the general rule (raw parity is the sign with which the program's net logical Clifford maps the readout Pauli back onto the prepared eigenstate) and name both raw-parity consumers, and tests pin three examples. The structural fix, an expected-parity field in the observable metadata that raw consumers subtract, is a schema change left for a follow-up decision.

Also in that round: the matching-route hyperedge counts are pinned exactly (12 with a fold, 0 without, at d=3); the zero-final detector count is asserted beside the guide's prose; an invalid fold variant is rejected at runtime; and six builder goldens from #763 that had no capture recipe now have one, with capture.py reproducing all 166 golden artifacts byte for byte.


Rebuilt onto dev after #776 was squash-merged: the branch is now a single commit holding this slice only, with the same tree as before on every file it owns. Its history was rewritten, so re-fetch before working from it locally. Verified after the rebuild: the surface and Guppy suites including the slow cases, the generated guide tests, pre-commit on the changed files, and just pytest-ci-core.

Base automatically changed from surface-fold-s to dev September 16, 2026 01:14
…tectors, fold-aware observables, and a Guppy protocol factory
@ciaranra
ciaranra force-pushed the surface-fold-s-builder branch from 7256830 to 51b45c7 Compare September 16, 2026 01:38
@ciaranra
ciaranra merged commit c71f467 into dev Sep 16, 2026
38 checks passed
@ciaranra
ciaranra deleted the surface-fold-s-builder branch September 16, 2026 05:48
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