feat(stim): X/Y-basis measure/reset, rec[-k] feed-forward, MPP, and native T/T_DAG - #147
Conversation
Port of #131 onto current main. Adds execution support for the X/Y-basis measurement and reset gates (MX, MY, MRX, MRY, RX, RY) and for classically-controlled gates whose control is a measurement record (CX/CNOT/CY/CZ rec[-k]). X/Y-basis gates are implemented via Stim's basis-change decomposition around the existing Z-basis machinery (MX = H M H, MY = S_DAG H M H S, RX = R H, RY = R H S), so readout-noise and RNG-draw shape stay consistent with M/MR. RX/RY are new GateName variants (ResetX/ResetY). Feed-forward required a typed gate operand: the new Target enum (Qubit | Rec) replaces the qubit-only Vec<usize> on the gate path, threaded through parser -> extended AST -> executor. The control bit is looked up in the running measurement record and the target Pauli applied iff it is 1, mirroring TableauSimulator::single_cx. The batched _many fast paths are preserved for gates with no record control. validate rejects rec[-k] on non-controlled gates and in the Pauli-target slot. Adapted to main's structure: prepare -> validate, execute_prepared -> execute_validated, and overwrite_last_measurement_record instead of direct measurement_record field access. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extends the Stim subset so the magic-state cultivation circuit
(crates/ppvm-stim/tests/data/cultivation_d5.stim) runs end-to-end.
MPP: multi-qubit Pauli-product measurement. A new PauliFactor/PauliAxis
AST and a dedicated RawInstruction::Mpp / ExtendedInstruction::Mpp carry
the `X0*Y1*Z2`-style products (parsed on their own path, since the
targets are Pauli factors rather than qubit indices). The executor
measures each product non-destructively via Stim's basis-change +
CX-ladder gadget: each factor's axis is rotated to Z (X = H·Z·H,
Y = (S H)·Z·(H S_DAG)), a CX ladder onto the first qubit folds
`Z_0 ... Z_{m-1}` into a single `Z_0`, that qubit is measured, then the
ladder and basis changes are undone so only the product operator is
projected. One result per product; readout noise via the optional arg.
Native T / T_DAG: the cultivation circuit uses bare `T`/`T_DAG`
mnemonics (rather than the `S[T]`/`S_DAG[T]` sugar). Added as GateName
variants that interpret lowers to the existing ExtendedInstruction::T /
TDag, so they share the tableau's T-gate path.
Tests: parser round-trip + rejection (stim-parser/tests/mpp.rs),
end-to-end MPP semantics incl. parity, Bell-state eigenvalues,
mixed-basis products and non-destructiveness (ppvm-stim/tests/mpp.rs),
the full cultivation_d5 run asserting 112 measurements
(ppvm-stim/tests/cultivation.rs, also wired into the corpus), and Python
binding tests for MPP.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The `Target` enum changed gate targets from `Vec<usize>` to `Vec<Target>`, so the executor must convert to `&[usize]` before calling the batched `*_many` Clifford paths. The conversion helpers returned `Vec`, adding one heap allocation per single-qubit gate instruction (two-qubit gates already allocated a pairs `Vec`, so they were neutral) — paid on every shot of the `sample`/`sample_parallel` loop. Return `SmallVec<[_; 16]>` instead, mirroring `build_masks`'s `MaskBuf`: typical narrow/moderate-width gates stay on the stack, wide broadcasts spill to the heap exactly as before, so it is never worse than the previous `Vec`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous `mpp_three_qubit_product_parity` measured `Z0*Z1*Z2` on a GHZ state, but GHZ is not an eigenstate of that odd-weight product (the |000> and |111> branches carry opposite eigenvalues), so the outcome is genuinely 50/50 under the entropy-seeded RNG `GeneralizedTableau::new` uses — it only passed by luck. Replace it with eigenstate checks: computational-basis Z parity (deterministic by # of 1s) and `X0*X1*X2` on GHZ (a stabilizer, +1), which still exercises the 3-factor ladder. Verified stable over 20 runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
FYI, @rafaelha @Roger-luo: turns out we do need this if we want to reproduce the benchmarks from the clifft paper shown in table 1. For that, we also need MPP. And I also added a T and T_DAG instruction here, which acts the same way as the tagged S[T]. I guess it doesn't hurt to have those synonyms. |
rafaelha
left a comment
There was a problem hiding this comment.
Review comments on uncovered edge cases.
…ted-qubit MPP Address review feedback on #147: - [P1] Range-check rec[-k] feed-forward controls in `validate`. An out-of-range lookback (e.g. `CX rec[-1] 0` with no prior measurement, or `M 0; CX rec[-2] 0`) previously resolved to `false` via `unwrap_or(false)` and silently applied no correction, hiding invalid circuits as valid no-ops. Stim raises IndexError for these; we now reject them. `validate_slice` threads a running measurement count; REPEAT bodies are checked against the first iteration's (shortest) record. - [P2] `qubit_targets` returned a panicking `.expect()` for record targets, so `parse_extended("T rec[-1]")` / `S[T] rec[-1]` / tagged `I[...] rec[-1]` panicked before validation. It now returns a new `ExtendedParseError::RecordTargetNotAllowed`. - MPP: reject products that name the same qubit twice (`MPP X0*X0`, anti-Hermitian `MPP Z0*X0`). The non-destructive CX-ladder gadget assumes one factor per qubit, so a repeat would misbehave. New `ExecError::InvalidPauliProduct` points users to open an issue if they have a use case for repeated Paulis in a product. Adds 12 tests (parser + validate). fmt/clippy -D warnings/workspace check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR extends PPVM’s supported Stim subset (parser + extended AST + ppvm-stim executor) to run a full magic-state cultivation circuit end-to-end by adding X/Y-basis measure/reset, measurement-record (rec[-k]) feed-forward controls on controlled Paulis, MPP Pauli-product measurements, and native T/T_DAG gate mnemonics.
Changes:
- Add typed gate targets (
Target::{Qubit, Rec}) to supportCX/CY/CZ rec[-k]feed-forward and update parsing/printing/interpretation accordingly. - Add
MPPparsing + AST representation and execute it via a basis-change + CX-ladder gadget, with extensive new Rust and Python tests (including a full cultivation corpus fixture). - Add native
T/T_DAGgate names in the parser and lower them through the existing extendedT/TDagexecution path.
Reviewed changes
Copilot reviewed 24 out of 27 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| ppvm-python/test/generalized_tableau/test_stim.py | Adds Python-level semantic tests for RX/RY + MX/MY, rec[-k] feed-forward, and basic MPP behavior. |
| crates/stim-parser/tests/record_targets.rs | New parser tests for rec[-k] controls and RX/RY gate names. |
| crates/stim-parser/tests/proptest_ast.rs | Updates proptest generators to use Target::Qubit and includes new MPP variants in line-zeroing helpers. |
| crates/stim-parser/tests/mpp.rs | New parser tests for MPP target syntax + display round-trips and rejection cases. |
| crates/stim-parser/tests/extended.rs | Ensures rec[-k] on sugar-lowered gates (e.g. T) returns a parse error (not a panic). |
| crates/stim-parser/src/table.rs | Adds table entries for RX/RY and native T/T_DAG mnemonics (MPP already tabled but now specially parsed). |
| crates/stim-parser/src/parser.rs | Introduces Target parsing for gates, dedicated MPP target parsing, and shared arg-count validation helper. |
| crates/stim-parser/src/lib.rs | Exposes Target in the public prelude. |
| crates/stim-parser/src/extended/parser.rs | Adds RecordTargetNotAllowed error for record targets on non-record-capable sugar gates. |
| crates/stim-parser/src/extended/interpret.rs | Lowers native T/T_DAG into extended instructions, threads Target through passthrough gates, and rejects record targets for qubit-only sugar gates. |
| crates/stim-parser/src/extended/ast.rs | Updates passthrough gate targets to Vec<Target> and adds an ExtendedInstruction::Mpp variant with measurement counting support. |
| crates/stim-parser/src/display.rs | Prints Target (including rec[-k]) for gates and adds printing for raw/extended MPP. |
| crates/stim-parser/src/ast.rs | Adds Target, PauliAxis, PauliFactor, gate-name variants (RX/RY, T/TDag), and raw Mpp instruction variant. |
| crates/ppvm-stim/tests/xy_and_feedforward.rs | New end-to-end Rust semantics tests for X/Y basis operations and rec[-k] feed-forward. |
| crates/ppvm-stim/tests/validate.rs | Updates validation tests for newly supported MX-family + record controls and new MPP validation rules. |
| crates/ppvm-stim/tests/stim_corpus.rs | Updates corpus expectations and adds new fixtures (feedback_cx, cultivation_d5, etc.). |
| crates/ppvm-stim/tests/mpp.rs | New end-to-end Rust semantics tests for MPP parity, mixed basis, and non-destructiveness. |
| crates/ppvm-stim/tests/data/mzz_unsupported.stim | New corpus fixture documenting that MZZ remains unsupported. |
| crates/ppvm-stim/tests/data/mx_basis.stim | New corpus fixture exercising MX. |
| crates/ppvm-stim/tests/data/inverted_target_unsupported.stim | New corpus fixture documenting M !0 is still rejected at parse time. |
| crates/ppvm-stim/tests/data/feedback_cx.stim | New corpus fixture exercising CX rec[-k] feed-forward. |
| crates/ppvm-stim/tests/data/cultivation_d5.stim | Adds the full cultivation circuit used as an end-to-end acceptance test. |
| crates/ppvm-stim/tests/cultivation.rs | New smoke test ensuring cultivation parses/executes and produces the expected number of measurements. |
| crates/ppvm-stim/src/validate.rs | Adds validation for record controls and MPP distinct-qubit requirements; threads measurement-record length through REPEAT. |
| crates/ppvm-stim/src/executor.rs | Implements RX/RY, MX/MY/MRX/MRY via basis changes, rec[-k] feed-forward execution, and MPP gadget execution; uses smallvec for target buffers. |
| crates/ppvm-stim/Cargo.toml | Adds smallvec dependency for executor fast-path target buffering. |
| Cargo.lock | Locks smallvec as a new dependency via ppvm-stim. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return Err(ExecError::InvalidRecordControl { | ||
| name: name.canonical_name().to_string(), | ||
| line, | ||
| message: "measurement-record controls (rec[-k]) are only valid on CX/CNOT, CY and CZ" | ||
| .to_string(), | ||
| }); |
| fn check_mpp_distinct_qubits(product: &[PauliFactor], line: usize) -> Result<(), ExecError> { | ||
| for (i, factor) in product.iter().enumerate() { | ||
| if product[..i].iter().any(|prev| prev.qubit == factor.qubit) { | ||
| return Err(ExecError::InvalidPauliProduct { | ||
| line, | ||
| message: format!( | ||
| "qubit {} appears more than once; MPP products must act on distinct qubits. \ | ||
| If you have a use case for repeated Paulis in a product, please open an \ | ||
| issue at https://github.com/QuEraComputing/ppvm/issues", | ||
| factor.qubit | ||
| ), | ||
| }); | ||
| } | ||
| } | ||
| Ok(()) | ||
| } |
| #[error( | ||
| "'{instruction}' at line {line} does not accept a measurement-record target rec[-k]; \ | ||
| only the control slot of CX/CNOT, CY and CZ may be a record" | ||
| )] | ||
| RecordTargetNotAllowed { instruction: String, line: usize }, |
|
I think I want to get this PR in, but I'd like to refactor the Steam code executor a bit first. Previously, we were just using a simple loop to run a Steam text file, but it is getting more complicated now. Since it doesn't have any abstractions, the code is becoming a little bit cumbersome to me at the moment. |

Summary
Extends the Stim subset accepted by
GeneralizedTableauso the magic-state cultivation circuitcrates/ppvm-stim/tests/data/cultivation_d5.stimruns end-to-end. Two commits:MX,MY,MRX,MRY,RX,RY) and classically-controlled feed-forward (CX/CNOT/CY/CZ rec[-k]), carried over onto currentmain.T/T_DAG(new) — multi-qubit Pauli-product measurement and the bareT/T_DAGmnemonics the cultivation circuit uses.#131 targeted the unmerged
stim-mirror-apibranch, so this is a port (not a cherry-pick):prepare→validate,execute_prepared→execute_validated, accessor methods instead of directmeasurement_recordaccess, and the batched_manyfast paths preserved for gates with no record control.X/Y-basis & feed-forward (commit 1, @rafaelha)
X/Y-basis gates use Stim's basis-change decomposition around the existing Z-basis machinery (
MX = H·M·H,MY = S_DAG·H·M·H·S,RX = R·H,RY = R·H·S), so readout-noise / RNG-draw shape stays identical toM/MR. Feed-forward adds a typed gate operand: a newTargetenum (Qubit|Rec) replaces the qubit-onlyVec<usize>on the gate path, threaded parser → extended AST → executor. The control bit is read from the running measurement record and the Pauli applied iff it is 1, mirroringTableauSimulator::single_cx.validaterejectsrec[-k]on non-controlled gates and in the Pauli-target slot.MPP & native T/T_DAG (commit 2)
PauliFactor/PauliAxisAST and a dedicatedMppinstruction variant carry theX0*Y1*Z2-style products (their own parse path, since targets are Pauli factors not qubit indices). The executor measures each product non-destructively via Stim's basis-change + CX-ladder gadget: rotate each factor's axis to Z (X = H·Z·H,Y = (S H)·Z·(H S_DAG)), a CX ladder onto the first qubit foldsZ_0 … Z_{m-1}into a singleZ_0, measure that qubit, then undo the ladder and basis changes so only the product operator is projected. One result per product; optional readout-noise arg.T/T_DAG: cultivation uses the bare mnemonics rather than theS[T]/S_DAG[T]sugar. Added asGateNamevariants thatinterpretlowers to the existingExtendedInstruction::T/TDag, sharing the tableau's T-gate path.cultivation_d5.stim
Parses, validates, and executes end-to-end: 112 measurements (36
M+ 57MX+ 19MPPproducts). Runtime ~11ms release / ~240ms debug — only 11T/T_DAGgates. Covered bytests/cultivation.rsand wired into the corpus.Tests
stim-parser/tests/{record_targets,mpp}.rs—rec[-k]/RX/RYparsing, MPP product parsing + round-trip + rejection.ppvm-stim/tests/{xy_and_feedforward,mpp,cultivation}.rs— quantum semantics (eigenstate prep, feed-forward on/off, lookback depth, REPEAT; MPP parity / Bell eigenvalues / mixed-basis / non-destructiveness) and the full cultivation run.validate.rs,stim_corpus.rs, corpus fixtures,proptest_ast.rs.test_stim.py.Checks
cargo fmt --check,cargo clippy --all-targets -D warnings,cargo check --workspace --all-targets— clean.stim-parser+ppvm-stimsuites pass; full Python suite (191 tests) passes after a native rebuild.Still out of scope (and documented by corpus fixtures):
M !0inverted targets,MXX/MYY/MZZ.🤖 Generated with Claude Code