Skip to content

Reimplement PauliString with a symplectic bit representation#65

Open
JaipreethTiruvaipati wants to merge 1 commit into
tqec:mainfrom
JaipreethTiruvaipati:bounty/issue-47-paulistring-symplectic
Open

Reimplement PauliString with a symplectic bit representation#65
JaipreethTiruvaipati wants to merge 1 commit into
tqec:mainfrom
JaipreethTiruvaipati:bounty/issue-47-paulistring-symplectic

Conversation

@JaipreethTiruvaipati

Copy link
Copy Markdown

What I changed

I went after #47 by profiling first, and the issue is right: on the surface-code circuits almost all of the time really is spent inside PauliString. So I rebuilt it on the symplectic representation (two integer bitmasks) and then followed the profile into boundary, cover, flow and match until the per-string work stopped showing up at the top.

Concretely:

  • pauli.py - a Pauli string is now two integers, xs and zs. Multiplication is an XOR, (anti)commutation is a popcount, collapse is a mask clear, so the hot operations are plain integer arithmetic and the constant stim.PauliStringdict round-trips go away. I also added __slots__ and a cached hash, bit-packed NumPy for the stim conversions, cover vectors built straight from the masks, and a small CollapsingOperators value type that holds a boundary's single-qubit collapse as combined masks. boundary.py - after_collapse is cached, and the collapsing operations are kept as their CollapsingOperators instead of a frozenset of one-qubit PauliStrings. That turns the anti-commutation check and the collapse into single bit operations rather than a loop over operators, which was taking a surprisingly large share of the runtime.
  • cover.py - the GF(2) cover is solved over whole-string symplectic vectors.
  • flow.py - each fragment's collapse masks are computed once, and "which measurements/resets does this tabilizer touch" becomes a bitwise AND plus a lookup instead of a scan over every operator.
  • match.py - the loop sanity-check was doing a deepcopy; since the boundary stabilizers are immutable I swapped it for a shallow clone of the flow lists.

The public API of PauliString is unchanged, and the detectors that come out are identical to main everywhere I checked.

Numbers

Benchmark environment:

  • tqecd base 6782ed5, this branch 5685fda
  • tqec 0.2.0, Python 3.12.7, NumPy 2.0.2, Stim 1.15.0
  • Intel Core i5-10300H @ 2.50 GHz, Fedora Linux 42 (kernel 6.19)

The k = 1..9 tqec memory(Basis.Z) benchmark from the issue, median of
several runs:

k        |   1   ,   2   ,   3   ,   4   ,   5   ,   6   ,   7   ,   8   ,   9
main (s) | 0.015 , 0.052 , 0.130 , 0.300 , 0.571 , 1.056 , 1.833 , 3.031 , 4.669
this (s) | 0.020 , 0.032 , 0.046 , 0.079 , 0.151 , 0.200 , 0.321 , 0.448 , 0.606

The k = 1 case is actually a touch slower - at 25 qubits the bitmask setup costs bit more than it saves - but from k = 2 the gap opens up fast, reaching about 7.7× at k = 9, and the ratio keeps growing with the code distance. One note on
reading those multipliers: they are measured against main on the hardware above, and the multiplier gets larger the slower that baseline machine is, so the absolute times are the more meaningful figure. Detector counts and the full
annotated output match main at every k.

With this in, the profile is dominated by stim's tableau construction and _try_merge_anticommuting_flows_inplace rather than PauliString, which is what the issue predicted would happen. I'm happy to take that on next in a separate PR if it would be useful.

Checks

  • The full pytest suite passes, including the end-to-end round-trip over the test_files corpus. I added tests for the symplectic operations, the cover vectors, CollapsingOperators, the after_collapse cache and the shallow flow clone. CI covers the 3.10–3.14 matrix. ruff check, ruff format --check, mypy src/tqecd and the full pre-commit set are green.

On AI use

I used an AI assistant while working on this - for profiling, parts of the implementation, the benchmark harness, and a first draft of this description. I read through the whole diff, ran and checked the tests, types, lint and benchmarks myself, and I understand the design: in particular why a boundary's single-qubit collapse can be reduced to combined (X, Z) masks, and why ((xs & cz) ^ (zs & cx)) != 0 is the same thing as "at least one collapsing operator anti-commutes with the stabilizer". I looked at how the problem has been approached before and wrote my own version. Glad to walk through any part of it.

Fixes #47

Replace the dict-based PauliString with two integer bitmasks (the
symplectic X/Z representation), turning multiplication, (anti)commutation,
collapsing and cover-vector encoding into pure integer bit operations and
removing the repeated stim.PauliString <-> dict conversions.

- pauli.py: symplectic masks + __slots__ + precomputed hash; O(1) exact and
  commuting cover vectors; bit-packed stim conversions; a CollapsingOperators
  value type that stores a boundary's single-qubit collapse as combined masks.
- boundary.py: cache after_collapse; represent collapsing operations by their
  CollapsingOperators instead of a frozenset of PauliString objects.
- cover.py: encode the GF(2) cover system over whole strings.
- flow.py: share per-fragment collapse masks; find the measurements/resets a
  stabilizer overlaps with a bitwise AND instead of scanning every operator.
- match.py: replace the loop-sanity deepcopy with a shallow flow clone.

The annotated detectors are unchanged; on tqec surface-code memory circuits
this is ~7x faster than the dict baseline and the gap grows with code size.

Fixes tqec#47
@nelimee nelimee added unitaryHACK26 Issues that are part of the Unitary Hack 2026 used-llm For PRs stating that LLM were used. labels Jun 15, 2026
@nelimee

nelimee commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

This is quite a chunky PR and #63 exists and has already been through several rounds of review. Because the hackathon is close to an end, we might not have the time to iterate on this PR. I'll try to provide you feedback on it anyway, but know that there is a high chance that #63 is merged instead, because I am reasonably certain it is correct after 2 rounds of review. In that case, my review will likely focus on the differences between this PR and #63 to find where the additional performance is gained in this PR.

@nelimee

nelimee commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Hey, #63 will be merged, but I keep this PR open for the moment because we might re-use some of the optimisations here. Just wanted to let you know about #63.

@JaipreethTiruvaipati

Copy link
Copy Markdown
Author

Sure, @nelimee... Will keep it open... Lets let me know what specific optimizations you might need..

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

Labels

unitaryHACK26 Issues that are part of the Unitary Hack 2026 used-llm For PRs stating that LLM were used.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make PauliString more efficient

2 participants