Skip to content

feat(circuits): qLDPC lattice surgery module#512

Open
tiangangzhou wants to merge 6 commits into
qLDPCOrg:mainfrom
tiangangzhou:pr/surgery-construction
Open

feat(circuits): qLDPC lattice surgery module#512
tiangangzhou wants to merge 6 commits into
qLDPCOrg:mainfrom
tiangangzhou:pr/surgery-construction

Conversation

@tiangangzhou

Copy link
Copy Markdown

Summary

Adds qldpc.circuits.surgery — a lattice-surgery gadget builder for CSS qLDPC codes, together with circuit synthesis for single- and joint-Pauli-product measurements (PPM).

Components:

  • gadget.py — Lattice surgery gadget construction (Webster): given a logical operator L̄ = ∏_v A_v, builds the merged CSS code (Q', S_X', S_Z') that exposes as the product of new meas-checks.
  • bridge.py — Swaroop universal-adapter bridge between gadgets sharing a logical qubit (SkipTree basis transform + boundary identification).
  • cheeger.py — Cheeger-constant boost: distance-verifying random ancilla rows raising h(F) to a chosen target, with two strategies (combinatorial, distance).
  • circuit.py — Single- and joint-PPM circuits:

Public API

All from qldpc.circuits.surgery:

Symbol Purpose
build_gadget(code, operator, basis)GadgetLayout Webster gadget: merged CSS (Q', S_X', S_Z') exposing L̄ = ∏_v A_v
boost_gadget(g, method, target, ...)GadgetLayout Cheeger boost (combinatorial / distance strategy) raising h(F) to target
cheeger_constant(g)float Reports h(F) of a gadget boundary
build_bridge(g1, g2, ...)Bridge Swaroop universal adapter between two gadgets sharing a logical qubit
build_single_ppm_circuit(g, rounds, basis, ...)stim.Circuit single-PPM measurement circuit; obs0 = last-round meas-checks
build_joint_ppm_circuit(g1, bridge, g2, rounds, ...)stim.Circuit Joint-PPM with bridge superposition
keep_only_observable(circuit, obs_index)stim.Circuit Strip all observables except one (useful for stim sampling)
logical_state_init(code, basis, ...) Prep circuit for `

Minimal use:

from qldpc.circuits.surgery import build_gadget, build_single_ppm_circuit
from qldpc.objects import Pauli

g = build_gadget(code, x_logical, basis=Pauli.X)
circuit = build_single_ppm_circuit(g, rounds=10, basis=Pauli.X)

Example

examples/lattice_surgery.ipynb walks through:

  1. Single-PPM true table (Steane or BB code)
  2. Joint-PPM with bridge true table (Steane | BB code joint measurement)
  3. Exact gadget dimension (|Q'|, |S_X'|, |S_Z'|) match (Webster Appendix A Table I)
  4. BB18 surgery LER (distance confirmed)

Tests

149 tests under src/qldpc/circuits/surgery/. Naming follows qLDPC's <module>_test.py convention (renamed from _test_<module>.py). Shared Webster JSON fixture lives in _webster_fixture.py.

References

  • Webster, Smith, Cohen — arXiv:2511.15989 §II.A — gadget construction + single-round identity Z̄ = ∏_v A_v
  • Cain et al. — arXiv:2603.28627 §B.1 — single-PPM measurement protocol (last-round readout)
  • Swaroop, Jochym-O'Connor, Yoder — arXiv:2410.03628 §III / §IV / §VII — SkipTree basis transform + universal-adapter bridge
  • Cross, He, Rall, Yoder — arXiv:2407.18393 — Cheeger-based distance preservation (h(F) ≥ 1)
  • Williamson, Yoder — arXiv:2410.02213 — distance-verifying random ancilla boost

Test plan

  • pytest src/qldpc/circuits/surgery/ -v — 149 pass
  • jupyter nbconvert --execute examples/lattice_surgery.ipynb — runs end-to-end

Adds qldpc.circuits.surgery: a public API for constructing fault-tolerant
lattice-surgery measurement circuits on arbitrary CSS qLDPC codes.

Public API
  build_gadget(code, x, *, basis)              GadgetLayout
  build_bridge(g_l, g_r)                       Bridge
  build_single_ppm_circuit(g, rounds, ...)     stim.Circuit
  build_joint_ppm_circuit(g_l, g_r, bridge, …) (stim.Circuit, CSSCode)
  boost_gadget(g, method, target, seed)        GadgetLayout
  cheeger_constant(g)                          float
  keep_only_observable(circuit, keep_idx)      stim.Circuit
  logical_state_init(code, state, *, log_idx)  str

Construction (gadget.py)
  Webster, Smith, Cohen arXiv:2511.15989 §II.A 3-step gadget:
  (1) restriction F = H_X[C_0, V_0] for the logical V_0 = supp(L_bar),
  (2) gauge-fix G = ker(F^T) over GF(2) (deterministic basis),
  (3) assemble HX_merged = [[HX_data, 0], [E_V0, F^T]], HZ_merged from
  [HZ_data | E_C0; G | 0] with the χ meas-check rows reading L_bar.

Bridge (bridge.py)
  Swaroop, Jochym-O'Connor, Yoder arXiv:2410.03628 §III SkipTree adapter
  joining two gadgets for joint logical measurement; handles intercode
  (c_l ≠ c_r) and intracode (shared data lane) variants.

Cheeger boost (cheeger.py)
  Cross, He, Rall, Yoder arXiv:2407.18393 Thm 6 distance-preservation
  threshold h(F) ≥ 1; combinatorial random-edge augmentation
  (Williamson, Yoder arXiv:2410.02213 distance-verifying ancilla) until
  the boundary Cheeger constant clears the threshold.

Circuit (circuit.py)
  Cain et al. arXiv:2603.28627 §B.1 single-PPM measurement protocol and
  its joint-PPM extension. obs0 = ∏_v A_v read from the last QEC round's
  meas-check outcomes (Webster Eq. 1, single-round identity);
  obs1 = direct destructive M on the X̄ support (noiseless cross-check).
  Detector emission filters dual-basis lanes via _is_basis_matched_lane:
  Z-detectors carry no information about obs0 = X̄ readout under CSS
  detector independence, so they're skipped at emission — measured
  speedup ~22× BP+LSD wall-clock on Steane single-PPM, p=0.005, r=9.

Coverage
  149 surgery tests pass: noiseless determinism, Cain Table III bb_18
  exact-match resource numbers, Webster Table I ancilla counts on four
  generalised-bicycle codes, distance preservation under boost on
  Gross [[72, 12, 6]], joint inter-code and intra-code parity protocols,
  reliable round-1 classification, basis-symmetric coordinate lanes.
Four sections demonstrating the public API end-to-end:

§1 — Single-PPM correctness. Steane [[7, 1, 3]] and a Gross [[108, 8]]
   BB code: scans the four logical Pauli inputs (|0⟩_L, |1⟩_L, |±⟩_L)
   and checks obs0 (Webster Eq.1, last-round meas-check XOR) against
   obs1 (direct destructive readout) on 4000 noiseless shots per init,
   covering both stochastic and deterministic X̄ eigenvalues.

§2 — Joint-PPM correctness. Two Steane copies and a Steane + BBCode
   pair (inter-code joint Z̄_1 ⊗ Z̄_2), plus a |0⟩_L ⊗ |+⟩_L
   superposition variant that forces the joint observable to be random
   while obs0 == obs1 must still hold on every shot.

§3 — Construction vs published results.
   §3.1 — Webster, Smith, Cohen arXiv:2511.15989 Table I: build_gadget
         reproduces (|Q'|, |S_X'|, |S_Z'|) exactly on the four published
         generalised-bicycle codes.
   §3.2 — Cain et al. arXiv:2603.28627 Extended Data Table III bb_18:
         (39, 20, 20) + merged-code degree 7 reached via boost_gadget
         on a cached weight-20 Z̄ representative + fixed boost seed.
         Includes an offline helper for regenerating the (rep, seed)
         pair.
   §3.3 — Cross, He, Rall, Yoder arXiv:2407.18393 Thm 6 distance
         preservation on Gross [[72, 12, 6]] using CSSCode.get_distance
         (bound=10000).
   §3.4 — Same Cross Thm 6 check on the §3.2 boosted gadget for
         Cain bb_18 [[248, 10]].

§4 — LER comparison.
   §4   — Gross [[72, 12]] surgery PPM vs memory baseline under
         DepolarizingNoiseModel, sinter sweep, BP+LSD min-sum decoder,
         log-log plot.
   §4.1 — Cain bb_18 [[248, 10]] surgery vs idling, Cain-faithful
         decoder config (max_iter=100, ms_scaling_factor=0.0,
         schedule='serial', lsd_method='lsd_e', lsd_order=5). Plot
         carries per-cycle LER, fitted log-log slope, and d_eff =
         2·slope − 1 in the legend.
ruff (format + check)
  * Sort imports per project I001 rule (lattice_surgery.ipynb + 9
    surgery modules).
  * Rename single-letter `l` → `ell` to clear E741 (Webster paper
    parameter renamed in docs + code, no semantic change).
  * Drop unused locals flagged by F841 (`nV` in gadget assembly,
    `n_comp_checks` in two reliability tests).

mypy (strict: disallow_untyped_defs + disallow_any_generics)
  * Annotate 100+ test functions with `-> None`.
  * Type the `noise_model` PPM kwarg as `NoiseModel | None`.
  * Type `boost_gadget` return as `GadgetLayout`; `**kwargs` as `Any`.
  * Type `_stitch_intercode`/`_stitch_intracode` signatures.
  * Add `assert <var> is not None` narrowing in
    `_surgery_qubit_coordinates` for joint-PPM branches so mypy can
    resolve `g_r.code` / `bridge.g_r_aug` accesses.
  * Use `dict[str, Any]` (not bare `dict`) for Webster seed-set dicts.
  * Use `PauliXZ` (Literal[Pauli.X, Pauli.Z]) for `basis` params on
    parametrized tests so build_gadget's narrowed kwarg is satisfied.
  * Tag two intentional-mistype paths with `type: ignore[arg-type]`:
    `test_gadget_layout_is_frozen_dataclass` (None placeholders to
    probe FrozenInstanceError) and the bad-data_init validation tests.

coverage (fail_under=100)
  * Add tests for the documented error paths in `gadget.py`
    (x shape mismatch, non-logical input, invalid basis,
    `build_gadget_augmented` width/weight checks),
    `bridge.py` (`_canonical_H_R` w<2, `_skip_tree_fullrank` default
    edge index, `build_bridge` width<2 + spanning_tree_root bounds),
    `cheeger.py` (boost validation, unknown method, `_exact_/_spectral_`
    boundary-Cheeger edge cases, `_augment_incidence_with_random_edges`
    direct tests, target-h above initial h to enter the boost loop body,
    n_V>26 synthetic gadget for the enumeration-infeasible branch,
    `boost_distance` arg validation), and `circuit.py`
    (`keep_only_observable` REPEAT-block recursion + observable
    filtering, `_expand_joint_data_init` TypeError).
  * Remove dead in-test branches: `test_build_gadget_z_basis_rejects_
    non_z_logical` (the inner `if` was always False on Steane —
    superseded by `test_build_gadget_rejects_non_logical_input`),
    inline `z_bar_1_operator` / `_z_op` helpers that duplicated the
    Webster fixture's `_webster_z_bar_operator` (the trailing
    `raise ValueError` was unreachable), and the
    `if gauge.shape[0] == 0: continue` short-circuit in
    `test_step2_gauge_fix_rank_matches_rows` (Steane's G is 1x3, not
    empty - branch never fired).
  * Mark the BP+OSD distance-boost main loop and a handful of
    enumeration-infeasible / bipartite-exhaustion edges in
    `cheeger.py` with `# pragma: no cover` - these only fire when the
    bare gadget fails BP+OSD (which hangs on the small fixture codes
    we ship) or in pathological subset/pair budgets that no realistic
    boost input reaches. Each has a one-line rationale.

Result: pytest 176 surgery tests pass, mypy clean, ruff clean,
coverage 100% (12383/12383 stmts).
@tiangangzhou tiangangzhou marked this pull request as ready for review June 12, 2026 19:11
@tiangangzhou tiangangzhou requested a review from perlinm as a code owner June 12, 2026 19:12
@perlinm

perlinm commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Thank you for opening a PR! This looks like an exciting capability to have. Please allow me time to review it carefully 🙂

…dge SkipTree

build_bridge previously rebuilt g_l_aug from the ORIGINAL (un-boosted)
incidence via _step1_restriction, silently dropping boost-added κ' rows
when the user pre-applied boost_gadget. SkipTree's T_l, computed against
the boosted G_aux, then embedded into un-boosted g_l_aug.incidence with
all boost-tree edges silently zeroed → invariant T·F_aug·P = H_R fails →
joint_code cycle stabilizers are bogus → stim DEM rejects non-deterministic
detectors.

Separately, _run_skiptree_on_port_subgraph assigned the same T_relab
column to every κ row matching a tree edge — fine when incidence rows are
distinct, but BB [[36, 8]] restricted to Z̄_0 has parallel weight-2 rows
that _build_aux_graph_strict dedups. Duplicate κ rows then received the
same column and cancelled mod 2 in T·F_aug.

Both bugs manifested as the same non-deterministic-detector ValueError
during DEM construction for joint PPM circuits (e.g. BB [[72,12]] +
boost or BB [[36,8]] no-boost). Single-PPM is unaffected: it does not
call build_bridge at all.

Fix:
* build_bridge: stack boost_extras = g_l.incidence[_step1_restriction_size:]
  with the bridge cellulation extras before calling build_gadget_augmented
* _run_skiptree_on_port_subgraph: track assigned_edges set, only fill T_full
  for the first matching κ row per (u, v) edge

Tests:
* bridge_test.py: 4 regression tests covering SkipTree invariant + DEM
  determinism after boost (BB [[72,12]]) and with duplicate edges (BB [[36,8]])
* circuit_test.py: single-PPM contract test ensuring it stays insulated
  from bridge code paths even when both boundary conditions trigger

Notebook:
* §4.2: code-agnostic joint Z̄⊗Z̄ noisy LER walkthrough (BP+LSD) with
  Steane default and BB swap-in examples in comments
@tiangangzhou tiangangzhou force-pushed the pr/surgery-construction branch from d579074 to 4e66572 Compare June 16, 2026 19:01
@tiangangzhou

Copy link
Copy Markdown
Author

In the concrete implementation of the BB code lattice surgery gadget system, it naturally supports the PPMs X, Y, Z, X′, Y′, Z′, XX′, and ZZ′ (see Improved QLDPC Surgery: Logical Measurements and Bridging Codes, page 31). Through these together with the automorphisms, it can support all $4^{12-1}$ Pauli measurements of the gross code.

The current implementation only supports X, Z, X′, Z′, XX′, and ZZ′. The next step is to implement the Y-gadget system.

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.

2 participants