Skip to content

Bitpack symplectic Pauli operators - #204

Merged
MIWdlB merged 20 commits into
mainfrom
claude/symplectic-bitpacking-explore-ajlkxb
Jul 20, 2026
Merged

Bitpack symplectic Pauli operators#204
MIWdlB merged 20 commits into
mainfrom
claude/symplectic-bitpacking-explore-ajlkxb

Conversation

@MIWdlB

@MIWdlB MIWdlB commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Symplectic operators were previously built around a Array<bool>. This update moves them over to a bitpacked DenseIndex type to improve performance.

@codspeed-hq

codspeed-hq Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will improve performance by ×2.2

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 56 improved benchmarks
⏩ 100 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation test_benchmark_decode_zbasis_ensemble[128-JKMN] 2,069.5 ms 205.9 ms ×10
Simulation test_benchmark_decode_zbasis_ensemble[128-Parity] 2,069.6 ms 206.5 ms ×10
Simulation test_benchmark_decode_zbasis_ensemble[128-BravyiKitaev] 2,069.3 ms 206.6 ms ×10
Simulation test_benchmark_decode_zbasis_ensemble[128-JordanWigner] 2,070 ms 206.8 ms ×10
Simulation test_benchmark_decode_zbasis_ensemble[64-JordanWigner] 555 ms 96 ms ×5.8
Simulation test_benchmark_decode_zbasis_ensemble[64-BravyiKitaev] 555.4 ms 96.7 ms ×5.7
Simulation test_benchmark_decode_zbasis_ensemble[64-Parity] 554.6 ms 96.8 ms ×5.7
Simulation test_benchmark_decode_zbasis_ensemble[64-JKMN] 554.8 ms 96.9 ms ×5.7
Simulation test_benchmark_decode_zbasis_ensemble[32-JKMN] 159 ms 51.1 ms ×3.1
Simulation test_benchmark_decode_zbasis_ensemble[32-BravyiKitaev] 159.2 ms 51.5 ms ×3.1
Simulation test_benchmark_decode_zbasis_ensemble[32-JordanWigner] 158.9 ms 51.4 ms ×3.1
Simulation test_benchmark_decode_zbasis_ensemble[32-Parity] 159.4 ms 51.6 ms ×3.1
Simulation test_benchmark_hartree_fock_state[128-JordanWigner] 261.3 µs 92.6 µs ×2.8
Simulation test_benchmark_hartree_fock_state[128-Parity] 261.9 µs 92.9 µs ×2.8
Simulation test_benchmark_hartree_fock_state[128-BravyiKitaev] 261.9 µs 93.1 µs ×2.8
Simulation test_benchmark_hartree_fock_state[128-JKMN] 262 µs 93.5 µs ×2.8
Simulation test_benchmark_hartree_fock_state[64-BravyiKitaev] 139.5 µs 67.9 µs ×2.1
Simulation test_benchmark_hartree_fock_state[64-JordanWigner] 139.1 µs 67.7 µs ×2.1
Simulation test_benchmark_hartree_fock_state[64-JKMN] 139.4 µs 68.1 µs ×2
Simulation test_benchmark_hartree_fock_state[64-Parity] 140.2 µs 68.6 µs ×2
... ... ... ... ... ...

ℹ️ Only the first 20 benchmarks are displayed. Go to the app to view all benchmarks.

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/symplectic-bitpacking-explore-ajlkxb (3285cc1) with main (e6f4717)

Open in CodSpeed

Footnotes

  1. 100 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

MIWdlB pushed a commit that referenced this pull request Jul 4, 2026
No-op commit to re-trigger the Benchmarks workflow on PR #204. The prior
run compared BASE and HEAD across different runner environments (CodSpeed
flagged this), producing implausible ±3x swings on the jordan_wigner
decode cases while the identical bravyi_kitaev/jkmn cases moved the other
way. Re-running to get a consistent-environment comparison.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qe4s3qCaXxKJ6BeafNaBzx
claude added 8 commits July 8, 2026 17:24
Store the X and Z blocks of SymplecticOperator, SymplecticOperatorView
and SymplecticMatrix as bitpacked integers (one bit per qubit) via the
bitvec crate instead of dense bool arrays. This reduces memory ~8x and
turns the symplectic product, Pauli weight, and Clifford conjugation
into word-level bit operations.

- Add a Block newtype over BitVec<u64, Lsb0> with the bit ops the
  symplectic layer needs (and/or popcounts, in-place XOR, iter_ones,
  lexicographic Ord) plus bool-array interop helpers.
- Make SymplecticMatrix's fields private behind an accessor/mutator API
  (row_x, row_z, ipowers, x_bools, z_bools, view_row, select_rows,
  set_x, set_z, set_ipower). Update all core consumers (TernaryTree,
  MajoranaEncoding, SymplecticHamiltonian, anneal, topphatt) to use it.
- Keep the Python/JSON boundary unchanged: symplectic data still crosses
  as the concatenated [x_block | z_block] boolean array with a uint8
  ipowers vector, converting to/from Block at the edge.

All Rust tests (144 lib + 52 doctests) and Python tests (336) pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qe4s3qCaXxKJ6BeafNaBzx
Benchmarks MajoranaEncoding::encode(&MajoranaSparse) -> QubitHamiltonian,
the symplectic-product hot path (view_row + mul_assign_view), for JW
encodings across mode counts and one-/two-body Hamiltonians. Enables
apples-to-apples comparison of the symplectic representation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qe4s3qCaXxKJ6BeafNaBzx
and_count_ones, or_count_ones and xor_assign now operate directly on the
underlying u64 storage words instead of iter_ones()/BitSlice iteration.
This makes the symplectic product inner loop (mul_assign_view) run as
native word ops, closing and reversing the encode regression measured at
n = 8-16: encode is now ~1.05x-1.43x faster than the bool-array baseline
(was 5-27% slower), and decode is ~1.35x faster, while memory stays ~8x
smaller.

Sound because every Block keeps its unused high bits zero (documented as
an invariant), so AND/OR/XOR over the final partial word never counts
stray bits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qe4s3qCaXxKJ6BeafNaBzx
No-op commit to re-trigger the Benchmarks workflow on PR #204. The prior
run compared BASE and HEAD across different runner environments (CodSpeed
flagged this), producing implausible ±3x swings on the jordan_wigner
decode cases while the identical bravyi_kitaev/jkmn cases moved the other
way. Re-running to get a consistent-environment comparison.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qe4s3qCaXxKJ6BeafNaBzx
decode_zbasis_ensemble computed the annihilation parity as
z.iter_ones().filter(|q| state[q]).count() against a dense bool state
row. For dense operators (Jordan-Wigner / parity have Z-strings spanning
every qubit) that iterates every set bit with a random bool-array index,
which regressed ~3x vs the old fused sequential scan at 128 qubits
(CodSpeed flagged decode[128-jordan_wigner] at -67%).

Pack each ensemble state into a bitpacked Block once, reused across the
mode loop, and compute parity via a word-level popcount of (z & state)
plus a word-level XOR state update. Measured on JW at 128 qubits, 1000
states: batch decode 49.9ms -> 3.96ms (now 3.9x faster than the
bool-array baseline's 15.5ms). Correctness covered by the existing decode
roundtrip tests and ensemble proptests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qe4s3qCaXxKJ6BeafNaBzx
try_encode applied each Majorana operator to the state via the
Mul<ZBasisState> path, whose parity used and_count_bools /
iter_ones against a dense bool state - the same small-n regression the
decode path had (CodSpeed: hartree_fock_state[32-jordan_wigner] -14.6%,
[32-parity] -12.8%).

Carry the working state as a bitpacked Block and compute each operator's
parity as a word-level popcount of (z & state), with a word-level XOR for
the shared X flip. The transform is exact: the i^k phase factors are
unit-norm (so the running unit-norm coefficient needs no rescale), and
the one combining branch renormalises to unit norm exactly as
ZBasisState::new does (its combined value is always non-zero there).
Covered by the existing Hartree-Fock roundtrip tests and proptests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qe4s3qCaXxKJ6BeafNaBzx
Benchmarks SymplecticHamiltonian::clone() and clone+apply_clifford_chain,
the hot path of clifford_heuristic_optimisation (called once per
simulated-annealing cost-function evaluation, and transitively by
randomised_subsystem_descent).

Diagnostic only, no source changes: measured against the pre-bitpacking
baseline (2d44397), clone() is ~16-76x slower depending on term count
(15.9k rows: 18.5us -> 1.40ms; 251k rows: 1.51ms -> 23.6ms), because
SymplecticMatrix now stores one heap-allocated Block per row instead of
two contiguous Array2<bool> buffers, turning clone into O(rows) small
allocations instead of a couple of memcpys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qe4s3qCaXxKJ6BeafNaBzx
Back each of SymplecticMatrix's X and Z blocks with a single contiguous
Vec<u64>, rows padded to whole u64 words (words_per_row = n_qubits.div_ceil(64)),
instead of a Vec of per-row heap-allocated blocks. Cloning the matrix is now a
pair of contiguous copies rather than one allocation per operator row, which
removes a large regression in SymplecticHamiltonian::clone that showed up in the
Clifford-heuristic and randomised-subsystem-descent annealers (clone runs once
per cost evaluation inside their simulated-annealing loops).

symplectic_bits.rs gains a borrowed BlockRef<'a> { words, n_bits } view (Copy)
alongside the owned Block; SymplecticOperatorView and the matrix row accessors
hand out BlockRefs over sub-slices of the contiguous buffer. Padding bits above
n_qubits are kept zero by every constructor and mutator, so word-level
AND/OR/XOR/popcount/iter_ones need no masking. Public method signatures are
preserved, so accessor-only consumers are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qe4s3qCaXxKJ6BeafNaBzx
@MIWdlB
MIWdlB force-pushed the claude/symplectic-bitpacking-explore-ajlkxb branch from 35cbced to 6588e30 Compare July 8, 2026 17:27
claude and others added 2 commits July 13, 2026 12:54
Pack ZBasisState and ZBasisEnsemble (and the encoding vacuum state) into the
same u64-word Block used by the symplectic layer, instead of dense Array1<bool>
/ Array2<bool>. Applying a Pauli operator to a state
(SymplecticOperator * ZBasisState) and decoding now run as whole-word
AND/XOR/popcount rather than iterating individual set-bit indices via
Block::and_count_bools / xor_into_bools, which are removed. The occupation is
still reachable as a boolean array through ZBasisState::state_bools(), and the
Python boundary (vacuum state, Hartree-Fock state, decoded occupations) is
unchanged.

With the state layer packed, the last two bitvec uses in symplectic_bits.rs --
set-bit iteration and the lexicographic block ordering -- are reimplemented
directly on the u64 storage words (relying on the existing padding-zero
invariant), so the bitvec dependency is dropped from ferrmion-core. The whole
symplectic/state layer is now plain u64 internally with bool only at the
Python edge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qe4s3qCaXxKJ6BeafNaBzx
@MIWdlB
MIWdlB force-pushed the claude/symplectic-bitpacking-explore-ajlkxb branch from 9ddbebc to f160ea3 Compare July 16, 2026 12:13
claude and others added 3 commits July 16, 2026 13:59
Follows up the rename to DenseBlock/DenseIndex by finishing the cleanup:

- DenseIndex is now the packed storage word (one usize lane by default),
  carrying the word-level primitives (get/set/toggle, and/or/xor, count_ones,
  iter_ones, cmp_bits, words_for) that were previously inlined raw-u64 ops. The
  Clifford kernels, SymplecticMatrix packing/addressing, and block bit access
  all route through it, so the earlier raw >> 6 / & 63 / 1u64 << .. arithmetic
  is gone. This also fixes the build: words_for was left callable only as an
  associated fn.

- DenseBlock is now generic over its backing: DenseBlock<Vec<DenseIndex>>
  (owned) and DenseBlock<&[DenseIndex]> (borrowed, Copy). The borrowed form
  replaces DenseBlockRef, which is removed. SymplecticMatrix keeps its single
  contiguous Vec<DenseIndex> buffer, with rows exposed as borrowed DenseBlocks
  over sub-slices, so clone stays a contiguous copy. Binary ops take a borrow
  of DenseBlock (&DenseBlock<T>) rather than a separate view type.

Public method signatures and the Python/bool boundary are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qe4s3qCaXxKJ6BeafNaBzx
@MIWdlB
MIWdlB force-pushed the claude/symplectic-bitpacking-explore-ajlkxb branch from 41cb712 to 88ad111 Compare July 16, 2026 15:51
@MIWdlB MIWdlB changed the title Bitpack symplectic Pauli operators with bitvec Bitpack symplectic Pauli operators Jul 16, 2026
@review-notebook-app

Copy link
Copy Markdown

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@MIWdlB
MIWdlB force-pushed the claude/symplectic-bitpacking-explore-ajlkxb branch from 6505fc1 to 28731ff Compare July 17, 2026 16:43
MIWdlB and others added 5 commits July 17, 2026 17:55
Finish migrating SymplecticMatrix to bitpacked DenseBlock storage and make
Clifford conjugation operate on whole machine words rather than one bit per
operator row.

- Rework SymplecticMatrixTranspose to hold a qubit-major transpose of the X/Z
  blocks (via a new word-level DenseBlock::transpose using a blocked 64x64 bit
  transpose) so H/S/CNOT and hamming_weights become whole-word AND/OR/XOR and
  column-run copies; results are written back on drop.
- Restore SymplecticMatrix::x_bools/z_bools, fix the identity() row
  over-allocation, and update stale doc examples to from_arrays.
- Add a multi-word (>64 qubit/operator) Clifford-chain test that checks the
  word-level kernels against a per-bit reference, plus a DenseBlock::transpose
  round-trip/oracle test.

Fix latent DenseBlock bugs that only surfaced once the crate compiled:
- multi-word term/index addressing in get_index/set_index/set_term and the
  From<Array2<bool>> conversion, and the word offset in DenseIndex::iter_ones
- to_bool_matrix now maps each term to a row
- SymplecticOperator::to_pauli_string keeps the stored i-power
- ZBasisState::adjoint again reverses qubit order; reindex sizes its block right

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sr1igxaqbCZgjLDGTpUi3
Proptest records discovered failing seeds under the crate manifest dir; check
them in (as the project already does for the root-level files) so the saved
cases re-run for everyone. All currently pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sr1igxaqbCZgjLDGTpUi3
The index reversal was mistakenly carried over from FermionProduct, which does
need to reverse indices; a Z-basis state's adjoint keeps the same occupation.
Remove the reversal and update the test expectation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sr1igxaqbCZgjLDGTpUi3
@MIWdlB
MIWdlB force-pushed the claude/symplectic-bitpacking-explore-ajlkxb branch from 9ca2731 to bf9c8fe Compare July 20, 2026 13:52
@MIWdlB
MIWdlB force-pushed the claude/symplectic-bitpacking-explore-ajlkxb branch from bf9c8fe to 3285cc1 Compare July 20, 2026 14:25
This was referenced Jul 20, 2026
@MIWdlB
MIWdlB merged commit 2f633b2 into main Jul 20, 2026
3 of 4 checks passed
@MIWdlB
MIWdlB deleted the claude/symplectic-bitpacking-explore-ajlkxb branch July 20, 2026 14:40
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