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
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?

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