Skip to content

Fix surface-code hook errors with an orientation-aware CNOT schedule#672

Merged
bmhowe23 merged 8 commits into
NVIDIA:mainfrom
bmhowe23:surface-code-cnot-schedule
Jul 15, 2026
Merged

Fix surface-code hook errors with an orientation-aware CNOT schedule#672
bmhowe23 merged 8 commits into
NVIDIA:mainfrom
bmhowe23:surface-code-cnot-schedule

Conversation

@bmhowe23

@bmhowe23 bmhowe23 commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Description

The surface-code stabilizer kernel executed every plaquette's CNOTs in ascending data-qubit index order — the flattened parity matrices it receives carry no ordering information, so an optimized schedule could not even be expressed. With the same row-major order on both X and Z plaquettes, a mid-round ancilla fault ("hook error", Dennis et al., arXiv:quant-ph/0110143) propagates onto a data-qubit pair aligned with one of the logical operators, halving the effective circuit-level distance of one memory basis.

Measured impact (before this PR, ZH orientation, prep0): effective distance 2 instead of 3 at d=3, and 3 instead of 5 at d=5. Which basis is damaged depends on the code orientation; prepp was coincidentally unaffected for ZH.

This PR gives the surface code the standard zigzag CNOT schedule (Tomita & Svore, arXiv:1404.3747), selected per orientation so hook pairs land perpendicular to the same-type logical, and executes the X and Z checks interleaved over four shared timesteps (one depth-4 extraction round).

Design, with no breaking interface changes:

  • stabilizer_grid::get_cnot_schedule_x/z(): schedule matrices with the same shape/support as the parity matrices; entry 0 = no support, k ∈ 1..4 = CNOT timestep. Rows match the sorted parity-matrix rows (enforced: a first-support tie throws instead of silently misaligning ancilla wiring). get_cnot_schedule_pairs_x/z() provides the flat (stabilizer, data) pair-list form used by the realtime examples.
  • code::get_stabilizer_schedule_x/z(): new virtuals defaulting to the parity matrices, so the stabilizer_round kernel signature is untouched and repetition/Steane receive byte-identical inputs. The memory-circuit drivers pass these to the kernel.
  • The surface kernel executes CNOTs timestep-major with X and Z checks interleaved; the zigzag pair is conflict-free and commutation-valid under interleaving (proven by the DEM tests — stim rejects non-deterministic detectors).
  • The four realtime C++ app examples and the Python example consume the grid API instead of five copy-pasted ascending-order schedule builders; Python bindings return the schedule as numpy arrays consistent with get_parity_x/z().
  • Docs: the QEC introduction page documents the schedule-matrix kernel contract and the surface-code CNOT schedule, cross-referencing the Doxygen API docs as the single source of truth for the encoding.

Validation:

  • New effective-distance test (test_qec_stim): builds the DEM from the actual kernel under two-qubit depolarizing noise and asserts the shortest undetectable logical error has weight d, for d=3,5 × both bases × all four orientations. Fails with weight ⌈d/2⌉ against the previous ordering.
  • New schedule-property test (test_qec): d=3,5,7 × all orientations — parity-row alignment, valid distinct timesteps, hook-pair perpendicularity, no data qubit touched twice per timestep.
  • The schedule matches ising-decoding's up to a left–right mirror (theirs starts each plaquette at NE, ours at NW) — same hook orientation and validity class; flipping the corner order is a one-line change if exact equivalence with predecoder training circuits is ever needed.

Runtime / performance impact

Monte Carlo (pymatching, CX depolarizing noise, 100k shots, rounds = d): at p=0.002 the prep0 logical error rate improves 14× at d=3 (0.030 → 0.0018) and 5.8× at d=5 (0.0057 → 0.00098); prepp is statistically unchanged and the two bases are balanced after the fix. The pre-fix DEM also contained parallel edges with identical detector signatures but different observable effects — fundamentally undecodable mechanisms that pymatching's default merge strategy rejects outright; these disappear with the fix.

Build/test-time cost: the new tests add ~50 ms to the two unit-test binaries. Kernel classical overhead adds a constant-factor scan per stabilizer round (negligible on stim, which traces the kernel once).

Self-review checklist

Before requesting review

  • I reviewed my own full diff in GitHub or my editor.
  • PR is in Draft if it is not yet ready for review.
  • Temporary / debugging changes have been removed.
  • Local test logs reviewed; no unexplained warnings or errors.
  • CI logs reviewed; no unexplained warnings or errors.
  • Full CI has been run.

Scope and size

  • PR is under ~1000 lines, or an exception is justified in the description.
  • Refactoring-only changes are isolated in their own PR(s). (the self-review hardening/dedup is a separate commit within this PR)
  • No existing tests were disabled or modified just to make this PR pass.

Tests

  • New functionality has new tests.
  • Tests fail if the new functionality is broken (including crashes), not just when it is missing. (effective-distance test verified to fail against the previous ordering)
  • Negative tests added where exceptions are expected. The new no-tie guard in build_cnot_schedule is unreachable for any valid rotated surface code by construction (geometric argument in the code comment), so it cannot be exercised without an artificial grid; existing invalid-orientation/invalid-distance exception tests still cover the public entry points.
  • Truth data added where simple EXPECT_* / assert checks are insufficient for algorithmic correctness. (effective distance == d via graphlike-DEM shortest-error search; Monte Carlo numbers above)
  • CI runtime impact considered; team notified if significant. (~50 ms added)

Documentation

  • Public-facing APIs have Doxygen docs.
  • User-visible behavior changes have public docs. (QEC introduction page updated; full docs build verified locally — zero new warnings, new cross-references resolve)

Code style

  • Naming follows the existing convention (snake_case) for the area being modified.

Dependencies

  • No new third-party dependencies.

🤖 Generated with Claude Code

bmhowe23 and others added 3 commits July 10, 2026 21:08
The stabilizer-round kernel executed every plaquette's CNOTs in ascending
data-qubit index order (the parity matrices carry no ordering), so both X
and Z plaquettes used the same row-major order. Mid-round ancilla faults
("hook errors") therefore propagated onto data pairs aligned *with* one of
the logical operators, halving the effective circuit-level distance of one
memory basis (measured: 2 instead of 3 at d=3, 3 instead of 5 at d=5 for
prep0/ZH).

- stabilizer_grid::get_cnot_schedule_x/z(): timestep-encoded schedule
  matrices (0 = no support, k in 1..4 = CNOT timestep) using the standard
  zigzag schedule pair (Tomita & Svore, arXiv:1404.3747; hook errors per
  Dennis et al., arXiv:quant-ph/0110143), selected per orientation so hook
  pairs land perpendicular to the same-type logical. Rows match the sorted
  parity matrix rows.
- code::get_stabilizer_schedule_x/z(): new virtuals, defaulting to the
  parity matrices, so the stabilizer_round kernel signature and all other
  codes' behavior are unchanged; surface_code overrides them.
- memory-circuit drivers pass schedule matrices to the kernel; the surface
  code kernel executes the X and Z checks interleaved over the four shared
  timesteps (one depth-4 extraction round; the zigzag pair is a valid
  schedule under interleaving).
- realtime app examples and python bindings consume the new grid API
  instead of rebuilding ascending-order pair lists.
- tests: schedule properties (parity alignment, hook orientation, per-step
  conflict freedom) for d=3,5,7 across all orientations, plus a graphlike
  DEM effective-distance check (== d for both bases, all orientations, at
  d=3,5) that fails against the previous ordering.

Monte Carlo validation (pymatching, CX depolarization, 100k shots,
rounds=d): at p=0.002 the prep0 logical error rate improves 14x at d=3
(0.030 -> 0.0018) and 5.8x at d=5 (0.0057 -> 0.00098); prepp is unchanged,
and the two bases are balanced after the fix, as expected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Ben Howe <bhowe@nvidia.com>
Findings from self-review of the CNOT-schedule change:

- build_cnot_schedule aligned schedule rows with parity-matrix rows via an
  unenforced no-tie invariant (stated only in a comment). Enforce it: throw
  if two same-type stabilizers ever share their first supported data qubit,
  turning a silent CNOT-to-wrong-ancilla misalignment into a loud error.
- Add stabilizer_grid::get_cnot_schedule_pairs_x/z() and replace the five
  copy-pasted schedule-to-(stabilizer, data)-pair conversions (four C++ app
  examples plus the python example) with calls to it.
- get_stab_data_supports in surface_code-4-yaml now derives supports from
  the schedule matrix rows, so support[s] aligns with ancilla[s] by
  construction instead of through a parallel sortStabilizerOps ordering.
- Python bindings return the schedule matrices as numpy arrays via the
  existing copyCUDAQXTensorToPyArray helper, consistent with get_parity_x/z,
  instead of hand-rolled list-of-lists copies.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Ben Howe <bhowe@nvidia.com>
…e CNOT schedule

- The stabilizer_round kernel-contract section notes the two vector
  arguments are schedule matrices and links to
  code::get_stabilizer_schedule_x, whose Doxygen comment is the single
  source of truth for the encoding and the parity-matrix default.
- The surface-code section documents what is unique to the narrative page:
  the depth-4 interleaved extraction round, the orientation-aware zigzag
  schedule and its hook-error rationale (with references), and links to the
  get_cnot_schedule[_pairs]_x/z accessors.

The C++/Python API pages pick up the new methods automatically via
doxygenclass/autoclass :members:.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Ben Howe <bhowe@nvidia.com>
@bmhowe23 bmhowe23 changed the title Update surface code cnot schedule Fix surface-code hook errors with an orientation-aware CNOT schedule Jul 10, 2026
@bmhowe23
bmhowe23 marked this pull request as ready for review July 14, 2026 15:34
@bmhowe23
bmhowe23 requested review from eliotheinrich, kvmto and melody-ren and removed request for melody-ren July 14, 2026 15:34

@eliotheinrich eliotheinrich left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few suggestions, but looks good to me. Most substantial is probably the Python support, but that's maybe best suited for a follow-up.

Comment thread libs/qec/lib/codes/surface_code_device.cpp Outdated
Comment thread libs/qec/lib/codes/surface_code_device.cpp Outdated
Comment thread libs/qec/python/bindings/py_surface_code.cpp
Comment thread libs/qec/unittests/test_qec.cpp
bmhowe23 and others added 3 commits July 14, 2026 23:06
- Hardcode num_steps = 4 in the surface-code stabilizer kernel instead
  of computing the max schedule entry at runtime
- Rename the stabilizer kernel and memory_circuit vector parameters to
  x/z_stabilizer_schedule to reflect that they carry CNOT schedules
- Document the stabilizer_round schedule semantics in the user-defined
  code section of the QEC introduction
- Add Python bindings for code.get_stabilizer_schedule_x/z with a test
- Extend checkCnotSchedule to verify the plugin's Z schedule too

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Ben Howe <bhowe@nvidia.com>
PyCodeHandle now dispatches get_stabilizer_schedule_x/z to optional
same-named methods on the Python code class, validating that the
returned matrix matches the parity matrix's shape and support pattern.
A plain hasattr check does not work because the @qec.code decorator
makes every Python code inherit from the bound Code class, which
exposes same-named read-only getters, so the handle walks the class
__mro__ to detect a genuine user override.

Includes an end-to-end test with a schedule-aware stabilizer_round
kernel, negative shape/support tests, and a doc note in the Python
code-authoring section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Ben Howe <bhowe@nvidia.com>

@kvmto kvmto left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good thanks, Ben!

@bmhowe23
bmhowe23 merged commit 22772f7 into NVIDIA:main Jul 15, 2026
13 of 15 checks passed
@bmhowe23
bmhowe23 deleted the surface-code-cnot-schedule branch July 15, 2026 16:01
anjbur added a commit to anjbur/cudaqx that referenced this pull request Jul 16, 2026
…chedule (NVIDIA#672)"

This reverts commit 22772f7.

Signed-off-by: Angela Burton <angelab@nvidia.com>
anjbur added a commit to anjbur/cudaqx that referenced this pull request Jul 16, 2026
…integration

Signed-off-by: Angela Burton <angelab@nvidia.com>
@bmhowe23 bmhowe23 mentioned this pull request Jul 16, 2026
18 tasks
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.

3 participants