Skip to content

Add strategy="statevector" for sampling on a dense state vector - #196

Open
rafaelha wants to merge 18 commits into
mainfrom
statevector-sampler
Open

rafaelha wants to merge 18 commits into
mainfrom
statevector-sampler

Conversation

@rafaelha

@rafaelha rafaelha commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds strategy="statevector" to compile_sampler and compile_detector_sampler. It builds on the zxparam migration (#194 and #195), which is now in main.

Every component of the reduced sampling graph that is not a single error-basis bit is simulated as a circuit on a dense state vector, in the style of Clifft:

  1. zxparam.extract.extract_circuit turns the reduced component back into a circuit.
  2. statevector/heisenberg.py rewrites the circuit as Pauli rotations and measurements in the Heisenberg frame. Noise only flips signs, so each op carries an affine GF(2) sign row over the error-basis bits, and commuting ops can be reordered freely.
  3. statevector/schedule.py reorders commuting ops to keep the active width small. The width is n − dim S, where S is the Pauli group that stabilizes every trajectory up to sign. The scheduler runs measurements and non-widening rotations eagerly. Otherwise it runs the pending measurement with the fewest unrun rotations ahead of it.
  4. statevector/plan.py plans the schedule on a fixed width W, tracking a Clifford frame.
  5. statevector/program.py lowers the plan to an action tape. statevector/execute.py runs the tape in JAX as a fori_loop over a lax.switch of six kernels, on lanes × 2^W amplitudes.

Details:

  • Shared pipeline: detector and measurement sampling use the same path.
  • Precision: amplitudes are complex64 by default and complex128 with jax_enable_x64.
  • Randomness and trajectory sharing: all randomness goes through one choose_outcome(p1, count, key), which draws outcomes with 2^-64 resolution. Lanes carry a count, and run(start, stop) can resume a tape, so trajectory sharing can be added later.
  • Unsupported cases: the strategy raises for mode="joint", and for components wider than 30 qubits or that zxparam cannot extract. The error suggests cat5.
  • Commit history: the commits before the last one still import zx_param; the last commit ports them to zxparam.

Active width

circuit Clifft, no passes Clifft, default passes Clifft + ActiveWidthSchedulePass Tsim, detectors Tsim, measurements
distillation 5 5 3 3 3
cultivation d3 4 4 4 5 5
coherent d3r3 8 5 4 5 4
cultivation d5 10 10 10 6 6
coherent d5r5 24 13 13 12 15
coherent d3r1 8 4 0 0 0
coherent d5r1 24 12 0 0 0
surface d7r7 0 0 0 0 0
cultivation d5 + d=15 escape (463 qubits) 10 10 10 6 7
  • Clifft columns: peak_active_width from Clifft 0.11.0, compiled three ways: with hir_passes=None, with the default passes, and with the default passes plus the opt-in ActiveWidthSchedulePass. The Clifft paper's table matches the no-pass column.
  • Tsim columns: the largest state-vector width over the statevector components of the detector and the measurement sampler, measured on this branch.
  • Circuits: the first eight are the clifft-bench circuits. The escape-stage circuit was generated with convert_s_to_t.make_d5_end2end(0.001) from unitaryfoundation/clifft-paper.
  • Width matters only in the exponent: cost per action scales with 2^width, so Tsim's 6 against Clifft's 10 on cultivation d5 is a 16× smaller state.

Performance (same machine, Apple M4 Pro)

circuit Tsim, 1 thread Clifft, 1 thread Tsim, all threads Clifft, all threads
distillation 2.46M 2.76M 2.87M 20.2M
cultivation d3 563k 4.74M 1.11M 30.4M
coherent d3r3 249k 1.92M 528k 13.7M
cultivation d5 312k 137k 572k 1.21M
coherent d5r5 272 3.7k 886 25.5k

Shots/s, postselecting every detector as clifft-bench does. NPROC=1 pins JAX to one thread.

  • Tsim vs Clifft: Clifft 0.11.0 is faster on four of the five circuits. Tsim is faster only on cultivation d5 on one thread (2.3×), where its width is 6 against Clifft's 10.
  • Multi-core scaling: on all threads, Tsim keeps 1.3–3.7 cores busy and Clifft 8–11.
  • Against the stabilizer-rank path: that path does not compile coherent d3r3, cultivation d5 or coherent d5r5 within 120 s. With strategy="statevector" every clifft-bench circuit compiles in under 1 s.
  • Escape-stage circuit:
    • Building the sampler takes about 2 minutes: 79 s of noise-channel preprocessing in ChannelSampler.__init__ and 32 s of ZX reduction.
    • Sampling runs at 12.9k shots/s, postselecting the cultivation-stage detectors, and 93% of that time is in ChannelSampler.sample. The statevector part is no more expensive than without the escape stage.
    • Clifft compiles this circuit in 0.6 s and samples 53k shots/s on one thread.

The performance numbers were measured before the rebase onto #195. The statevector code is unchanged since then, and the widths and action counts are identical on this branch. Full tables are in scripts/statevector/results.md, produced by scripts/statevector/benchmark.py and benchmark_clifft.py.

How this was verified

  • Exactness: planned components are checked exactly against the stabilizer-rank path. Unit tests compare each stage's output distribution with the previous stage's and with the stabilizer-rank path. The whole-branch review also fuzzed 150 random circuits (785 components) with no mismatch.
  • Integration tests: they compare statevector samples with the stabilizer-rank samplers on hand-built circuits:
    • T interference;
    • GHZ with T gates;
    • arbitrary rotations;
    • injection checks;
    • mid-circuit feed-forward;
    • circuits with several components.
  • Acceptance: on the five clifft-bench circuits, acceptance agrees with Clifft 0.10.0 within 5 standard errors.
  • Logical-error rates: they agree with Clifft 0.10.0, Clifft 0.11.0 and SymFT within the spread between those references. For example, on cultivation d3 Tsim has 82 errors in 97M accepted shots (8.4e-7), against 8.5e-7 to 1.09e-6 for the references.
  • Precision: at seed 0, complex64 and complex128 give bit-identical samples on 2.9M shots across the five circuits. complex128 is 24–40% slower at width ≥ 5.

Test plan

  • pytest: 1478 passed
  • pyright src test: 0 errors
  • pre-commit hooks pass on the branch head

Known limitations, left for follow-ups:

  • Rare outcomes under complex64: when outcome 0 has probability below about 3e-8, float32 rounds p1 to 1 and that outcome is never drawn. The error is up to 3% at 1e-6 and 30% at 1e-7. The planned fix samples the rarer weight and flips. Until then, use complex128 for logical-error rates near 1e-7.
  • Fixed width: every action touches all 2^W amplitudes. Exact per-step widths would do 1.5–3.8× less array work on the d3 and d5 circuits.
  • Multi-core scaling: XLA spreads the kernels over its thread pool but keeps only a few cores busy.
  • Noise sampling on large circuits: ChannelSampler dominates both build time and sampling time on the escape-stage circuit.
  • Trajectory sharing: the hooks are in place, and the sharing itself is follow-up work.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-12T04:05:09.397111Z 3721934 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://queracomputing.github.io/tsim/pr-preview/pr-196/

Built to branch gh-pages at 2026-10-13 15:52 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

☂️ Code Coverage

current status: ✅

Overall Coverage

Statements Covered Coverage Threshold Status
3382 3249 96% 0% 🟢

New Files

File Coverage Status
src/tsim/statevector/_init_.py 100% 🟢
src/tsim/statevector/execute.py 100% 🟢
src/tsim/statevector/heisenberg.py 98% 🟢
src/tsim/statevector/plan.py 100% 🟢
src/tsim/statevector/program.py 100% 🟢
src/tsim/statevector/schedule.py 99% 🟢
TOTAL 100% 🟢

Modified Files

File Coverage Status
src/tsim/_init_.py 100% 🟢
src/tsim/circuit.py 92% 🟢
src/tsim/compile/pipeline.py 100% 🟢
src/tsim/core/types.py 100% 🟢
src/tsim/sampler.py 93% 🟢
TOTAL 97% 🟢

updated for commit: ec8caf6 by action🐍

Base automatically changed from zx-param-migration-review-fixes to main October 12, 2026 13:12
@rafaelha
rafaelha force-pushed the statevector-sampler branch from 3721934 to 37fb756 Compare October 12, 2026 13:13
rafaelha and others added 18 commits October 13, 2026 11:40
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A float32 uniform draw is a multiple of 2^-23, so any p1 below 2^-23 gave
outcome 1 with probability 2^-23: deterministic outcomes, whose p1 rounds to
~1e-15 instead of 0, fired at ~1.2e-7 per measurement under complex64.
Compare two uint32 draws with p1·2^32 instead, independent of the dtype.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
p1 = (nu - <P>)/(2 nu) cancels catastrophically (p1 ~ 1e-7 instead of ~1e-15
on eigenstates under complex64), and renormalizing by 1 - p1 loses the
relative precision of a small p0. Sum each outcome's weight directly
(|psi +- P psi|^2 / 4, or the even and odd parity halves) and renormalize by
the selected weight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 1e-5 threshold was built for stabilizer-rank arithmetic. Under complex64
each rotation adds ~1e-7 relative drift, which is harmless because every
probability is normalized by the measured weights, so warn above 1e-3 for
complex64 and 1e-9 for complex128. Stabilizer-rank components keep 1e-5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The per-shot estimate counted only the amplitudes; peak memory was up to 2.7x
higher, and the memory-derived batches (millions of lanes) were also slower.
Count the f-params and records on host and device, cap the automatic batch at
2^16 lanes, and require one lane to fit in the same 50% budget that sizes
batches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hand-built ops reach a dormant measurement of Y (S_DAG on the pivot), a
forgotten dormant measurement that no later op depends on, and a diagonal
measurement on two active axes. Running a tape as [0, k) then [k, A)
reproduces running [0, A) field by field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
All five acceptances still agree with Clifft 0.10.0 and no norm warning is
printed. Write zero logical-error counts as upper bounds, and note that the
previous logical-error section could not resolve the ~1e-7 sampling floor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add benchmark_clifft.py, which runs Clifft on the five clifft-bench
workloads the way clifft-bench does, and workloads.py, which both
benchmark scripts share. benchmark.py now reports the cores kept busy in
the timed window and takes --seed; NPROC=1 pins JAX to one thread.
results.md compares Tsim with Clifft 0.11.0 on one thread and on all
threads of the same machine, lists both libraries' active widths, and
checks the logical-error rates against Clifft and SymFT.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zx_param is now zxparam: rename the imports in the statevector modules,
their tests and the test helper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Read every output off the final symbols instead of carrying records
through the tape: an output is the measurement's sign, XOR its branch
bit when the measurement is random. This removes the Record action, the
RECORD kernel, slots, the scratch record column and the sign of
measurement actions.

- Dispatch on the component type in sample_component and give each
  component type its own norm-warning threshold, so sample_program is
  the loop it is on main.
- Pick the per-component compiler once in compile_program and reuse its
  output indices and f-bit selection in ComponentLayout.of.
- Share one anticommute helper across heisenberg, schedule and plan, and
  one elimination step between StabilizerGroup.rotate and measure.
- Derive num_lines, num_f, num_words, num_outputs and num_branches
  instead of storing them; drop the unused dtype option.
- Gather once in the diagonal measurement kernel and jit
  sample_statevector_component as a whole.
- Plan on ops in schedule order and emit each Rotate in one place.
- Share test fixtures, merge duplicated tests and drop subsumed ones.
- Default the benchmark circuits to a clifft-bench checkout next to the
  repository instead of a personal path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

1 participant