qbm/pauli_prop.py stores the Pauli sum as a Python dict keyed by (xmask, zmask) and applies each imaginary-time gate in a Python loop over terms. That is clear and exactly validatable, but it is the bottleneck: measured 474 ms per training step for a 6-qubit model at coeff_cutoff=1e-6, where the reference Julia implementation handles tens of qubits.
What to do
Replace the dict with contiguous integer arrays (xmask[], zmask[], coeff[]) and vectorise _apply_ite_gate:
- commutation is
popcount(x1 & z2) ^ popcount(z1 & x2) — computable for all terms at once
- the branch product and its phase are pure bit arithmetic
- merging duplicates becomes a sort/
np.unique on the packed key rather than dict insertion
- truncation becomes a mask, not a rebuild
Appendix G of arXiv:2602.04878 describes the array-of-bitmasks layout and the POPCNT-based filter.
Acceptance
tests/test_pauli_propagation.py passes unchanged (33 tests) — the numerics must not move
- a benchmark showing the speedup, added to
benchmarks/
- no change to the public API
Getting oriented
Week 20 of the codebase tour covers this module.
qbm/pauli_prop.pystores the Pauli sum as a Pythondictkeyed by(xmask, zmask)and applies each imaginary-time gate in a Python loop over terms. That is clear and exactly validatable, but it is the bottleneck: measured 474 ms per training step for a 6-qubit model atcoeff_cutoff=1e-6, where the reference Julia implementation handles tens of qubits.What to do
Replace the dict with contiguous integer arrays (
xmask[],zmask[],coeff[]) and vectorise_apply_ite_gate:popcount(x1 & z2) ^ popcount(z1 & x2)— computable for all terms at oncenp.uniqueon the packed key rather than dict insertionAppendix G of arXiv:2602.04878 describes the array-of-bitmasks layout and the POPCNT-based filter.
Acceptance
tests/test_pauli_propagation.pypasses unchanged (33 tests) — the numerics must not movebenchmarks/Getting oriented
Week 20 of the codebase tour covers this module.