Skip to content

feat(stim): X/Y-basis measure/reset, rec[-k] feed-forward, MPP, and native T/T_DAG - #147

Merged
Roger-luo merged 5 commits into
mainfrom
david/stim-xy-mpp
Jun 23, 2026
Merged

feat(stim): X/Y-basis measure/reset, rec[-k] feed-forward, MPP, and native T/T_DAG#147
Roger-luo merged 5 commits into
mainfrom
david/stim-xy-mpp

Conversation

@david-pl

Copy link
Copy Markdown
Collaborator

Summary

Extends the Stim subset accepted by GeneralizedTableau so the magic-state cultivation circuit crates/ppvm-stim/tests/data/cultivation_d5.stim runs end-to-end. Two commits:

  1. Port of feat(stim): X/Y-basis measure/reset and rec[-k] feed-forward gates #131 (authored by @rafaelha) — X/Y-basis measure/reset (MX, MY, MRX, MRY, RX, RY) and classically-controlled feed-forward (CX/CNOT/CY/CZ rec[-k]), carried over onto current main.
  2. MPP + native T/T_DAG (new) — multi-qubit Pauli-product measurement and the bare T/T_DAG mnemonics the cultivation circuit uses.

#131 targeted the unmerged stim-mirror-api branch, so this is a port (not a cherry-pick): preparevalidate, execute_preparedexecute_validated, accessor methods instead of direct measurement_record access, and the batched _many fast 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 to M/MR. Feed-forward adds a typed gate operand: a new Target enum (Qubit | Rec) replaces the qubit-only Vec<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, mirroring TableauSimulator::single_cx. validate rejects rec[-k] on non-controlled gates and in the Pauli-target slot.

MPP & native T/T_DAG (commit 2)

  • MPP: new PauliFactor/PauliAxis AST and a dedicated Mpp instruction variant carry the X0*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 folds Z_0 … Z_{m-1} into a single Z_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.
  • Native T/T_DAG: cultivation uses the bare mnemonics rather than the S[T]/S_DAG[T] sugar. Added as GateName variants that interpret lowers to the existing ExtendedInstruction::T/TDag, sharing the tableau's T-gate path.

cultivation_d5.stim

Parses, validates, and executes end-to-end: 112 measurements (36 M + 57 MX + 19 MPP products). Runtime ~11ms release / ~240ms debug — only 11 T/T_DAG gates. Covered by tests/cultivation.rs and wired into the corpus.

Tests

  • stim-parser/tests/{record_targets,mpp}.rsrec[-k]/RX/RY parsing, 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.
  • Updated validate.rs, stim_corpus.rs, corpus fixtures, proptest_ast.rs.
  • Python binding tests for X/Y, feed-forward, and MPP in test_stim.py.

Checks

  • cargo fmt --check, cargo clippy --all-targets -D warnings, cargo check --workspace --all-targets — clean.
  • Full Rust stim-parser + ppvm-stim suites pass; full Python suite (191 tests) passes after a native rebuild.

Still out of scope (and documented by corpus fixtures): M !0 inverted targets, MXX/MYY/MZZ.

🤖 Generated with Claude Code

rafaelha and others added 2 commits June 23, 2026 15:23
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>
@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-06-23 17:08 UTC

david-pl and others added 2 commits June 23, 2026 16:12
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>
@david-pl

Copy link
Copy Markdown
Collaborator Author

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

rafaelha commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Amazing, this looks great! And I will be using this for the playground as well.

One small subtlety for MPP. When paulis are repetaed, i.e. MPP X0*X0, stim simplifies this as MPP I (and always returns 0). Or MPP X0*X0*Z0 becomes MPP Z0.

For something like MPP Z0*X0, stim will raise ValueError: Acted on an anti-Hermitian operator... because Z0*X0=iY0 is non-hermitian.

ppvm does not currently treat this edge case correctly. The simplest way would be to disallow repeated Paulis on the same index. Or you could do what stim does.

image

@rafaelha rafaelha left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review comments on uncovered edge cases.

Comment thread crates/ppvm-stim/src/executor.rs
Comment thread crates/stim-parser/src/extended/interpret.rs
…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>
@david-pl
david-pl marked this pull request as ready for review June 23, 2026 16:48
Copilot AI review requested due to automatic review settings June 23, 2026 16:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 support CX/CY/CZ rec[-k] feed-forward and update parsing/printing/interpretation accordingly.
  • Add MPP parsing + 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_DAG gate names in the parser and lower them through the existing extended T/TDag execution 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.

Comment on lines +147 to +152
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(),
});
Comment on lines +187 to +202
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(())
}
Comment on lines +27 to +31
#[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 },
@Roger-luo

Copy link
Copy Markdown
Collaborator

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.

@Roger-luo
Roger-luo merged commit 6ecad96 into main Jun 23, 2026
14 checks passed
@Roger-luo
Roger-luo deleted the david/stim-xy-mpp branch June 23, 2026 17:08
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.

4 participants