Skip to content

feat(circuits): qLDPC lattice surgery module - #512

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

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

Conversation

@tiangangzhou

Copy link
Copy Markdown
Collaborator

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
Collaborator 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.

@perlinm

perlinm commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

I'm slowly coming to terms with my lack of time to give this PR the review it deserves.

One consideration on my mind for reviewing this PR is that the field of surgery is rapidly evolving, so I want to be careful with the choice of names and abstractions that we commit to in the API. I also want to ensure that the API and implementations are flexible enough to accommodate future developments in the field. It would be helpful to find an independent surgery expert to review this PR.

In the meantime, what if we merged this material into an experimental module of qldpc? We can then add a warning that triggers on the first call of an experimental feature, letting the user know that the experimental surgery API is subject to change (and that we recommend any code relying on experimental features to pin its version of qldpc).

Surgery seems like a big enough module that it makes sense to pull it out of circuits, into src/qldpc/surgery (or, for now, src/qldpc/experimental/surgery).

@perlinm perlinm added enhancement New feature or request help wanted Extra attention is needed high desirability Some features are highly desirable, but may require a formidable effort to implement. labels Jun 26, 2026
@tiangangzhou

Copy link
Copy Markdown
Collaborator Author

This PR might be too large, since it bundles several pieces of implementation together. I think we could start from the easiest case and begin the design there.

For a single logical Pauli-$X$ measurement via lattice surgery, the end goal is to construct the merged check matrices

$$\tilde{H}_X = \begin{pmatrix} H_X & 0 \\\ f_1^T & \partial_1 \end{pmatrix}, \qquad \tilde{H}_Z=\begin{pmatrix} H_Z & f_0 \\\ 0 & \partial_0 \end{pmatrix}.$$

For a concrete construction, we can choose

$$\tilde{H}_X = \begin{pmatrix} H_X & 0 \\\ \pi_{V_0} & \pi_{V_0} H_Z^{T} \pi_{C_0}^{T} \end{pmatrix}, \qquad \tilde{H}_Z = \begin{pmatrix} H_Z & \pi_{C_0}^{T} \\\ 0 & \ker\left(\pi_{V_0} H_Z^{T} \pi_{C_0}^{T}\right) \end{pmatrix}$$

where $V_0$ is the set of physical qubits on which the logical operator $\bar{X}$ has support,

$$V_0 = \mathrm{supp}(x) = (v_0,\dots ,v_{|V_0|-1})$$

and $C_0$ is the set of dual stabilizer checks ($S_Z$) that overlap with $V_0$,

$$C_0 = \{j : (H_Z)_{j,V_0} \neq 0 \} = (c_0,\dots,c_{|C_0|-1}).$$

For any ordered subset $S=(s_0,\dots,s_{|S|-1})$, we define the projection $\pi_S \in \mathbb{F}_2^{|S|\times N}$ by

$$(\pi_S)_{i,j} = \delta_{j,s_i},\ (\pi_S^T)_{i,j} = \delta_{i, s_j}.$$

We can verify that the product of the newly introduced $X$ stabilizers (the second row block of $\tilde{H}_X$) reproduces the original logical $X$:

$$\prod_{i=0}^{|V_0|-1}(S_X')_i = \prod_{l=0}^{|V_0|-1} X_{v_l} = \bar{X}$$

A further improvement would be to ask how we can obtain better $f_1$, $\partial_1$, $f_0$, $\partial_0$ to achieve a larger merged code distance (via some graph theory boost). I think we can expose this as an input API while keeping the standard construction above as the default. We can then design a protocol to translate these stabilizers into Stim circuits. This is essentially why I designed build_single_ppm_circuit.

What do you think about opening a separate PR that deals only with the single Pauli-$X$/$Z$ PPM, so we can keep it clean and review it?

@perlinm

perlinm commented Jul 5, 2026

Copy link
Copy Markdown
Collaborator

@tiangangzhou I appreciate the thoughtfulness of breaking this into more manageable chunks! That may make sense to do independently of my comments below.

It is a very busy time for me at work, so I will regardless not be able to review parts of this PR in the near term. I therefore want to re-surface my proposal to merge this into an experimental module, which I would be comfortable merging without detailed review. What do you think?

(By the way, I invited you @tiangangzhou to be a collaborator with write access to qldpc. Let me know if the invitation got lost, and I can re-send it.)

@tiangangzhou

Copy link
Copy Markdown
Collaborator Author

Thanks @perlinm. I think an experimental module is a good idea. As of today, graph-based surgery is still under rapid development, and the choice of the $f_1, \partial_1, f_0, \partial_0$ gadgets is generally non-standard. It can be optimized for different hardware or codes. However, we can always build circuit-level simulations using $f_1, \partial_1, f_0, \partial_0$ as input. Users can write their own class methods to propose new sets of gadgets, though of course some default constructions would be provided.

Btw, the link has expired. Could you please resend the invitation?

@perlinm

perlinm commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

In the absence of a human expert, I did my best to review this with Claude. AI-assisted review below. I tried to be a careful steward of the AI-assisted review, but please do not hesitate to correct any of the below if you believe that it is inapplicable, invalid, or incorrect.

TLDR: There are a few minor changes we should make before merging this into an experimental module. I'll open a separate PR into tiangangzhou:pr/surgery-construction with the suggested changes for you to review @tiangangzhou.


Thanks again for opening this PR. It's a substantial, carefully-built contribution and a useful new capability for qldpc: four well-separated modules (gadget, bridge, distance boost, circuit) that faithfully track a five-stage construction lineage, plus fixtures and a working example notebook. The physics and the code both look sound; most of what follows is about tightening guarantees, fixing a couple of real bugs, and setting the API up so the fast-moving surgery literature can be tracked without a rewrite. The review aims to be specific and actionable, and to separate "should fix before merging" from "nice to have later."

The file:line references below track the PR's current src/qldpc/circuits/surgery/ layout. The PR discussion already leans toward a dedicated experimental namespace, likely outside circuits; the review doesn't depend on the final path.

Summary (BLUF)

What this PR does. It adds src/qldpc/circuits/surgery/: fault-tolerant logical Pauli-product measurement (PPM) for general CSS/qLDPC codes. The modules are single-PPM "gadgets" (gadget.py), joint-PPM "bridges" via universal adapters (bridge.py), a distance boost (cheeger.py), and a stim circuit layer (circuit.py), plus fixtures and an example notebook. It faithfully tracks a coherent and very recent slice of the literature (Cohen → Cross/Williamson–Yoder → Webster → Swaroop → Cain).

What the review covers. (1) Correctness — is the construction faithful to the papers it cites, and does the code do what its docstrings claim? (2) Genericity — does it really work for arbitrary CSS codes? (3) Code quality — modularity, naming, contracts, documentation. (4) Future-proofing — is the API organized so the 2025–26 surgery frontier (parallel / fast / extractor-based surgery) can be added later without a rewrite?

What needs attention before merging.

  • Two real correctness issues, both in the distance machinery. The spectral fallback in cheeger.py is not a valid distance lower bound, so it can falsely certify "distance preserved" on large logical supports; and the combinatorial boost can silently return an under-target gadget when its qubit budget runs out. Neither corrupts anything shipped today (the affected paths aren't exercised by the shipping code — see §1 and §2), but both are public functions whose docstrings promise more than the code delivers.
  • Citation and docstring accuracy. A handful of attributions are wrong and will mislead readers checking the math against the papers: a result cited as "Webster Lemma 9" in one place and "Cross Thm 6" in another (§3), section pointers into a paper whose sections aren't numbered, and links to spec files that aren't in the repo. Relatedly, the observable-timing docstrings cite the wrong readout round (§4) — a docs-only fix, since the circuit itself is fault-tolerant (see below).

Two further docstring-honesty items are worth addressing before the module is relied upon (Phase 1 below), not before merge: the joint-PPM (bridge) distance is currently spot-checked by simulation rather than guaranteed, so that language should be scoped (§5); and the docstrings should distinguish a proven bound from a heuristic and an exact distance from an upper bound (§6).

Two things were checked and are fine:

  • The single-PPM path is generic over the CSSCode interface. The modules call only generic CSSCode methods; the PR's tests exercise the path on Steane (a non-bicycle code); and the gadget and circuit build cleanly on surface, hypergraph-product, and toric codes too.
  • The observable-timing choice looks fault-tolerant. It reads a different round than the cited paper, but the two rounds should be decoding-equivalent, so the discrepancy is most likely a docstring issue rather than a bug. A small test would confirm the equivalence (§4).

Recommendation. The natural home is the planned experimental surgery module, with an "API subject to change" warning on first use, and none of the findings need to hold up landing it there. The two correctness fixes, the citation fixes, and the observable-timing docstring correction are still worth doing before merging: all are low-churn, and each is a public-facing correctness or accuracy issue rather than polish. The one thing most worth getting right early is the choice of names and abstractions. In a fast-moving field an experimental warning covers behavior changes, but a public name is harder to walk back, so names are the most important thing to settle now (the roadmap's Phase 2 object-model cleanup below is the main way to do that). The roadmap at the end sequences the rest: what's needed before relying on the module or promoting it out of experimental.

What works well

  • Clean modular split. gadget.py (single-PPM), bridge.py (joint-PPM adapter), cheeger.py (distance boost), circuit.py (stim assembly). The public API is small and discoverable, and the layout dataclasses are frozen. Easy to navigate.
  • Faithful to a real, current construction lineage, and the citations are to real papers (the arXiv IDs resolve to the works claimed). The single-PPM gadget follows Webster–Smith–Cohen (arXiv:2511.15989); the distance argument, Cross–He–Rall–Yoder (arXiv:2407.18393) and Williamson–Yoder (arXiv:2410.02213); the joint measurement, Swaroop–Jochym-O'Connor–Yoder (arXiv:2410.03628); the circuit, Cain et al. (arXiv:2603.28627); with Cohen–Kim–Bartlett–Brown (arXiv:2110.10794) underneath.
  • Correct where it counts. gadget.py computes the gauge checks as an actual GF(2) null space (via galois), so the count is right even for a disconnected restriction graph, with no hardcoded formula, and it touches only the generic CSSCode interface. The Webster block-matrix algebra checks out.
  • Tests exercise CSS commutation and stim observables for intra- and inter-code joint measurement, in both bases, and cover the SkipTree adapter invariants (TGP = H_R, (3,2)-sparsity, and the adapter check-weight ≤ 8 bound) in bridge_test.py.
  • No new attack surface: the only I/O is a hardcoded fixture JSON.

Correctness & hygiene fixes

Ordered by severity — the first two matter most, but all four are low-churn and belong before merge (together they are Phase 0 in the roadmap).

1. The spectral Cheeger fallback is not a valid distance bound (recommended before merge)

cheeger.py's _spectral_cheeger_lower_bound uses an ordinary spectral gap of F Fᵀ (the checks' Gram matrix), but the quantity it stands in for is the mod-2 boundary Cheeger constant (the smallest boundary-to-support ratio over cuts of at most half the support). The two are not the same object, and the spectral value is not a lower bound on the Cheeger constant. The mismatch reproduces directly:

import galois
from qldpc.circuits.surgery.cheeger import _spectral_cheeger_lower_bound
# The interface F below has true mod-2 boundary Cheeger constant 0 (the cut {0,1}
# has empty boundary), yet the spectral estimate is well above 1:
_spectral_cheeger_lower_bound(galois.GF(2)([[1, 1, 1, 0], [0, 0, 1, 1]]))   # ≈ 1.809

cheeger_constant uses this fallback whenever the logical support exceeds ~26 qubits (cheeger.py:136–139), and the function's docstring presents its output as certifying "distance preserved" (cheeger.py:129). So for a large logical support it can report h ≥ 1 ⇒ distance preserved when the true h is much smaller: an unsound distance certificate from a public function.

The scope today is narrow. cheeger_constant is reached only via its public export, the tests, and the example notebook. No shipping code calls it; the notebook and the non-spectral tests all use logical operators of modest support (≤26), so they take the exact enumeration path, and the one test that exercises the spectral branch feeds a synthetic 27-wide incidence and only asserts h ≥ 0. So nothing shipped is silently corrupted. But a public function returning an unsound certificate is still a real problem, and the fix is cheap. Suggested fix: don't let a heuristic gate a distance claim. Rename _spectral_cheeger_lower_bound to something like _spectral_cheeger_screen, and have cheeger_constant either raise / return None for large supports or fall back to the exact distance, rather than returning an unsound number. (A valid relaxation, or wiring in the exact brute-force distance from get_distance_exact, would be even better.)

2. The combinatorial boost can silently under-deliver (recommended before merge)

boost_gadget_cheeger_combinatorial breaks out of its loop when max_extra_qubits is exhausted (cheeger.py:293–294) and returns (cheeger.py:327–328) with no check that the target h was reached and no warning. Yet the docstring promises a guarantee it doesn't deliver ("deterministic distance guarantee", cheeger.py:195; "GUARANTEES distance preservation", :206). On the qubit-cap path, then, the docstring's guarantee is stronger than what the code delivers, and a caller gets a distance-degraded gadget with no signal. Suggested fix: raise or warn when the target isn't met, and bring the docstring into line with what's actually guaranteed.

3. Citation & attribution fixes (recommended before merge — trivial, high value)

The attributions below are easy to get wrong and easy to fix, and they matter because someone will trace the math back to the papers. The clearest-cut of the four can be verified within the PR itself:

  • "Webster Lemma 9" (cheeger.py:130) vs. "Cross §III Thm 6" (cheeger.py:205): the same implication (h ≥ 1 ⇒ distance preserved) is attributed two different ways, so at least one is wrong. The correct attribution is Cross et al. (arXiv:2407.18393), Theorem 6; Webster–Smith–Cohen (arXiv:2511.15989) states the construction via definitions rather than a numbered "Lemma 9."
  • The "§III.A" pointer (circuit.py:350): Cain has no Roman-numeral sections, so "§III.A" doesn't resolve (the appendix-style pointers elsewhere, e.g. "§B.1", at least resolve to real sections — whether their content is cited correctly is a separate matter; see §4). The underlying single-PPM gadget is prior work (Cross / Williamson–Yoder / Webster / He), not originated by Cain; repoint it accordingly.
  • "Cain mapping" (recurring in docstrings): the mapping symbols (V₀, C₀, F, κ, χ, G) come from Webster/Cohen/Cross rather than Cain. Renaming it (e.g. "gadget notation") and defining the symbol mapping once, in a module-level Notation docstring, would help.
  • Dead spec links: __init__.py:2, bridge.py:25, and bridge.py:364 point to docs/superpowers/specs/… files that aren't in the repo. Replace with a short inline summary or an arXiv citation.

4. Observable-timing docstrings are mis-cited (the code looks correct) (recommended before merge — docs; the fault-distance test is Phase 1)

_surgery_observable reads the logical observable at the last merged round (the readout offset defaults to -1), and the docstrings (circuit.py:356–359, 680, 1096–1101) justify the choice by citing Cain "§B.1: the final round is the readout point." But §B.1 doesn't designate the final round as the readout point — if anything it reads the outcome from the first merged cycle. Reading the first vs. the last round should be decoding-equivalent here: the interface detectors telescope, so the observable's fault distance is unchanged. The mismatch is therefore most likely a docstring issue rather than a bug. But the PR doesn't currently demonstrate the equivalence. There's no fault-distance test for the surgery observable (the nearest test, circuit_test.py:1922–1923, only checks num_detectors > 0), and the first-round variant isn't implemented. Suggested fix: reword the docstring to state the first/last equivalence rather than mis-cite, and add a stim fault-distance test so the fault-tolerance claim is checkable rather than asserted.

5. Joint-PPM (bridge) distance is spot-checked, not guaranteed (should scope; implementing Eq. 9 is optional/later)

bridge.py keeps the hyperedge checks as gauge instead of deforming them, and does not implement Swaroop's Eq. 9 perfect-matching decomposition (honestly noted at bridge.py:217–219). So the Theorem-12 distance guarantee is replaced by empirical logical-error-rate (LER) checks. That tradeoff is a reasonable v1 choice; the cheap, honest fix is to scope the "distance-preserving" language to match, since for joint measurements distance is currently spot-checked, not guaranteed. (Restoring the guarantee by implementing the Eq. 9 deformation is larger, best left to Phase 2.) Two smaller gaps: unequal-width joins truncate both ports to [:width] (bridge.py:385–389, port_l_all / port_r_all) rather than matching widths on T as Swaroop prescribes, and are untested. Set-valued and overlapping ports are missing entirely.

6. Distinguish guaranteed vs. heuristic vs. upper-bound distances (should fix — docs)

The h(F) ≥ 1 ⇒ distance preserved condition is sufficient, not tight, and the code mixes several notions of distance that the docstrings should keep separate:

  • guaranteed vs. heuristic: Williamson–Yoder give d* ≥ min(h,1)·d. The code provides two boosts — a deterministic combinatorial one (greedy toward the worst Cheeger cut) and a random-edge heuristic — but neither guarantees the distance up front for a general code; such a guarantee would need the expander construction (Williamson–Yoder Thm 3).
  • screened vs. certified: distance is screened by BP+OSD (belief propagation + ordered-statistics decoding), an upper bound, and only certified by the exact distance (get_distance_exact, a brute-force enumeration).

The docstrings should label each distance for what it is, and never conflate a proven bound with a heuristic or a BP+OSD upper bound with the true distance.

7. Fault-tolerance guardrail: rounds is currently unconstrained (should fix)

build_single_ppm_circuit and build_joint_ppm_circuit take rounds as a free kwarg (circuit.py:346, :666) with no relation to the code distance, so rounds=1 (indeed even rounds=0) silently produces a non-fault-tolerant circuit. A soft warning when rounds is small relative to the code distance would catch the mistake. A warning is better than a hard requirement, since fast / transversal surgery legitimately uses far fewer rounds (see Future plans), so a hard Θ(d) check would need to be undone later.

8. Robustness items

  • build_bridge doesn't validate port_subset_l/r (should fix): out-of-range or duplicate indices raise a cryptic IndexError/NetworkXError from deep in the internals instead of a clean ValueError. And an index past the point where the ports are truncated to the smaller width (bridge.py:388–389) is silently dropped. Validate the ports and raise a clear error.
  • _skip_tree (bridge.py:54–76) is unguarded recursion, so it hits RecursionError for logical weight ≳ 1000. Make it iterative or add a depth guard. (nice to fix)
  • The combinatorial boost builds very large pure-Python lists at the ~26-qubit threshold, and the "~5–30 s in numpy" note (cheeger.py:41) describes a routine that's actually a Python loop. Vectorize, or cap and document. (nice to fix)
  • A cheap assert HX_merged · HZ_mergedᵀ = 0 guard in gadget.py would add defense-in-depth for free. (nice to fix)

Recommended changes & roadmap to experimental

One way to sequence the work. Phase 0 is what to do before merging even to an experimental namespace; Phase 1, before relying on the module or promoting it; Phase 2 is the future-proofing refactor that lets the frontier below slot in without a rewrite. Items that address a finding above cite it (§N).

Phase 0 — before merging to experimental (all low code-churn; these items are public-facing correctness/citation issues, which is why they belong before merge rather than before relying on it):

  • Fix or scope the spectral Cheeger certification (§1) so a heuristic never gates a distance claim.
  • Make the combinatorial boost raise/warn on under-target, and align its docstring (§2).
  • Fix the wrong citations and dead spec links (§3); correct the observable-timing docstrings (§4).

Phase 1 — before relying on the module / promoting out of experimental.

Correctness & guarantees:

  • Scope the joint-PPM distance claim to "spot-checked" in the docstring (§5).
  • Add the soft rounds warning (§7).
  • Validate build_bridge's port_subset_l/r (§8): today bad indices raise cryptic errors, and an over-long subset is silently truncated.
  • Calibrate the guarantee labelling throughout the docstrings (§6).
  • Exercise the distance-boost loop end-to-end and certify with the exact distance. The loop itself is currently # pragma: no cover, and get_distance_exact (a brute-force enumeration referenced at cheeger.py:369) is never actually called.
  • Add the stim fault-distance test for the observable (§4).

Code quality & contracts:

  • Firm up the contracts:
    • Replace the -1 sentinel in build_gadget_augmented (gadget.py:257) with an explicit n_extra/mask.
    • Give GadgetLayout explicit provenance fields (interface qubits, measured checks, boost boundary), so consumers stop slicing .incidence[k:] and re-calling _step1_restriction.
    • Tighten boost_gadget's typing: Literal["combinatorial","distance"] instead of method: str, no silent float→int on the target, no catch-all **kwargs.
    • Decide whether build_gadget_augmented is public (it's public by name but not in __all__).
    • Move the in-function imports to the top of the file.
  • Reduce the dual-observable footgun: an include_crosscheck: bool = False flag on the builders would be safer than the post-hoc keep_only_observable(…, keep_idx=0), and would make the fault-tolerant single-observable circuit the default.
  • Move the historical asides in a few docstrings (e.g. the abandoned _cellulate_strict path, past-regression notes) to PR / commit notes; keep docstrings about current behavior.
  • The fixture helpers (load_webster_seed_set, build_generalised_bicycle_code) are private but the example uses them. Either make them public with good names (generalized-bicycle construction arguably belongs in qldpc.codes) or inline them in the example.
  • Robustness cleanups from §8 (_skip_tree recursion guard; the boost's Python-list blowup and mislabeled "numpy" note; the CSS-commutation assert).

Phase 2 — future-proofing (do incrementally, no behavior change):

  • The PR builds a separate CSSCode from the merged matrices alongside the raw-array GadgetLayout / Bridge on nearly every operation (circuit.py:368, 509–510, 588–589; cheeger.py:387); the joint builder even returns a CSSCode (built at circuit.py:694, returned at :783) next to the raw-array Bridge it took as input. A single merged-code type that also records where each part came from could replace this duplication, with the modules routed through it: concretely, a CSSCode (or a thin wrapper) that records what's original vs. interface, the measured checks, the boost boundary, and the split-back map. The type would be new — it removes today's duplication but is more than a rename, since a bare CSSCode records no origin and GadgetLayout holds pre-merge fields a merged code doesn't cover — so budget it accordingly. That same object is also the natural place to make the gadget/boost blocks (f₁, ∂₁, f₀, ∂₀) a user-supplied input with the standard construction as the default. That design keeps the public surface small and stable (fewer names to commit to now) while letting callers plug in code- or hardware-specific gadgets.
  • Add a guaranteed expander boost (Williamson–Yoder Thm 3) as a peer to the heuristic one, closing the "no a-priori guarantee for general codes" gap.
  • Optionally, implement Swaroop Eq. 9 to restore the joint-PPM distance guarantee (§5).
  • Planning note: the circuit layer (~1200 lines) does not reuse get_memory_experiment / MemoryExperimentParts. It reimplements the init/QEC/readout/detector scaffolding, because surgery needs merge/split boundaries and the product-of-measured-checks observable. Whether to reuse or fork the memory machinery is the main integration decision, and it's real work, not a rename.

A follow-up PR against this branch can carry the Phase 0 / Phase 1 changes.

Future plans — where surgery in qldpc can go after this

This PR implements the single-PPM era of the theory, which is exactly the right first step. A natural near-term extension is rounding out the measurable Pauli set: single X̄/Z̄ measurements and same-basis joint products (X̄X̄, Z̄Z̄) are here, and Y-type and mixed-basis products are the obvious next gadgets (the parallel work below already handles mixed/Y-type operators). Here is the broader landscape the API should be aimed at:

  • Parallel measurements. Measuring many logical Paulis at once, in time roughly independent of how many: Cowtan–He–Williamson–Yoder, Parallel Logical Measurements via Quantum Code Surgery (arXiv:2503.05003), and Zhang–Li, Time-Efficient Logical Operations on Quantum Low-Density Parity Check Codes (PRL 134, 070602; arXiv:2408.01339). These schemes fit the merge side but need correlated/modular decoding across the merged region, which the current Decoder interface (decode(syndrome) → error) has no notion of; worth keeping in mind when shaping the circuit/decoder boundary.
  • Fewer rounds: "fast" / transversal surgery. Cutting the Θ(d) round cost toward O(1): Baspin–Berent–Cohen, Fast surgery for quantum LDPC codes (arXiv:2510.04521), and Cowtan et al., Fast and fault-tolerant logical measurements: Auxiliary hypergraphs and transversal surgery (arXiv:2510.14895). These results are exactly why the rounds check in §7 should stay a soft warning: the round count is a design parameter, not a fixed property of "surgery," so it should stay a knob.
  • Extractors. A standing, fixed-connectivity ancilla that measures any logical Pauli without rewiring: He–Cowtan–Williamson–Yoder, Extractors: QLDPC Architectures for Efficient Pauli-Based Computation (arXiv:2503.10390). An extractor is a long-lived system with its own lifecycle, not "build a fresh merge per request," so it will need a distinct object shape rather than another build_* call — the clearest case that not all future work is just adding to what's here.
  • High-rate surgery. Pushing the space–time product toward constant on concrete codes: Zheng–Jiang–Xu, High-Rate Surgery (arXiv:2510.08523).
  • A unifying view. CSS-code surgery is a universal construction (a pushout of chain complexes): Cowtan–Burton, CSS code surgery as a universal construction (Quantum 8, 1344; arXiv:2301.13738), with an automated tool, SSIP (arXiv:2407.09423). The pushout view is a good conceptual organizer and a homology/commutation sanity check, but not an equality oracle to test merged codes against, precisely because the fault-tolerant constructions here deliberately deviate from the bare pushout (the boost adds edges; the bridge drops hyperedges) to recover distance and sparsity.

The through-line — bare pushout → distance boost → adapter/bridge → standing extractor — suggests organizing the surgery subsystem so that the merge construction, the distance boost, the circuit compiler, and (eventually) the decoder region structure are separable stages. The Phase 2 object-model unification above is the first concrete step in that direction.


Thanks again for putting this PR together. It's a strong, well-organized first cut on a genuinely hard and timely problem, and the core (single-PPM gadget + fault-tolerant observable, generic over CSS codes) is sound. Most of the above is about making the guarantees precise and keeping the door open to the frontier — all of it open to discussion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request help wanted Extra attention is needed high desirability Some features are highly desirable, but may require a formidable effort to implement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants