Skip to main content

Simulator

Struct Simulator 

pub struct Simulator { /* private fields */ }
Expand description

A stim-TableauSimulator-style procedural quantum-state simulator.

Apply operations one at a time, read measure outcomes, branch on them (feedforward), and continue. See the module docs for the state representation.

§Naming

The method names are stim’s wherever stim has one, so a circuit written against stim.TableauSimulator transliterates: the single-qubit Cliffords (h, s, sqrt_x, c_xyz, h_xy, …), the two-qubit <A>C<B> family (cx/cnot/zcx, cy/zcy, cz/zcz, xcx … ycz) plus swap/iswap, measure/measure_observable, the postselect_* family, reset/reset_x/reset_y/reset_z, and peek_observable_expectation/peek_x/peek_y/peek_z. Two departures: the measurements return a MeasureResult rather than a bare bool (the branch weight is computed to sample from it, so reporting it is free), and observables are PauliStrings.

The Pauli-rotation extension keeps this workspace’s names — t, t_dag, t_pauli, ccz and rank — as does the batched apply_batch instruction path.

Implementations§

§

impl TableauSimulator

pub fn gate1(&mut self, gate: Gate1Q, qubit: usize)

Apply a named single-qubit Clifford.

Realized from the engine’s primitive frame updates rather than from Gate1Q::images: one or two O(⌈n/64⌉) row operations, no tableau and no allocation.

§Examples
use ticit::TableauSimulator;
use ticit::Gate1Q;

// C_XYZ cubes to the identity, so |+⟩ comes back to |+⟩.
let mut sim = TableauSimulator::with_seed(1, 0);
sim.h(0);
for _ in 0..3 {
    sim.gate1(Gate1Q::Cxyz, 0);
}
assert!((sim.peek_x(0)? - 1.0).abs() < 1e-9);

pub fn gate2( &mut self, control: PauliBasis, target: PauliBasis, control_qubit: usize, target_qubit: usize, ) -> Result<(), SimError>

Apply the two-qubit gate <A>C<B>: target acts on target_qubit exactly when control_qubit sits in the −1 eigenstate of control.

Same operator as controlled_pauli on the corresponding single-qubit axes, reached without building either Pauli. Writing CZ = ½[(I + Z_c) ⊗ I + (I − Z_c) ⊗ Z_t], conjugating a qubit by any Clifford G replaces that qubit’s Z with G Z G† and touches nothing else, so <A>C<B> = (G_A ⊗ G_B) · CZ · (G_A ⊗ G_B)† for any G_A, G_B carrying Z onto A and B — here H for X and √X† for Y. CX, CZ and XCZ skip the conjugation and hit the engine’s primitives directly.

§Errors

SimError::RepeatedQubit if both operands name the same qubit. (The controlled_pauli path reports that case as SimError::NonCommutingControlledPaulis instead, since two distinct axes on one qubit anticommute.)

pub fn t_basis( &mut self, basis: PauliBasis, qubit: usize, adjoint: bool, ) -> Result<(), SimError>

Apply T/T† about a single-qubit basis axis.

Equivalent to t_pauli on the corresponding single-qubit Pauli, but reached by conjugating the engine’s Z-axis rotation (T_B = G · T_Z · G† for G Z G† = B), so no observable is built and the rotation decomposes a stored frame row rather than a Pauli product.

§Errors

Propagates t_pauli errors.

pub fn apply_circuit( &mut self, circuit: &Circuit, ) -> Result<BatchOutcome, SimError>

Apply a circuit, returning its measurement records.

The circuit is translated to Instructions and replayed through apply_batch. Detector and observable annotations do not add entries to the returned outcome.

Every executable parser instruction is supported, including arbitrary Pauli rotations, noise channels, heralded records, MPAD, and EXP_VAL.

§Errors

Returns the relevant execution error. As with apply_batch, a failure leaves the operations before the failing instruction applied.

§Examples
use ticit::{Circuit, TableauSimulator};

let circuit = Circuit::from_text("H 0\nCX 0 1\nM 0 1")?;
let mut sim = TableauSimulator::with_seed(0, 7);
let outcome = sim.apply_circuit(&circuit)?;
assert_eq!(outcome.records[0].outcome, outcome.records[1].outcome);

pub fn apply_batch( &mut self, instructions: &[Instruction], ) -> Result<BatchOutcome, SimError>

Run instructions in order, collecting every measurement.

This is exactly the loop a caller would write by hand over the same operations — including the failure behaviour. An error aborts the batch where it happened, so the simulator reflects the instructions before the failure and the outcomes of those measurements are lost with the discarded BatchOutcome.

§Errors

Whatever the underlying operation returns, plus SimError::MissingBatchRecord if an Instruction::ConditionalPauli names a measurement the batch has not reached yet.

§Examples
use ticit::{Gate1Q, Instruction};
use ticit::{Pauli, PauliBasis, PauliString, TableauSimulator};

// Measure a |+⟩ and undo the random outcome with a feedforward Z.
let program = [
    Instruction::Gate1 { gate: Gate1Q::H, qubit: 0 },
    Instruction::Measure(PauliString::single(1, 0, Pauli::X)),
    Instruction::ConditionalPauli { basis: PauliBasis::Z, qubit: 0, control: 0 },
];

let mut sim = TableauSimulator::with_seed(1, 3);
sim.apply_batch(&program)?;
assert!((sim.peek_x(0)? - 1.0).abs() < 1e-9);
§

impl TableauSimulator

pub fn new(num_qubits: usize) -> TableauSimulator

A fresh |0…0⟩ simulator on num_qubits qubits with an OS-seeded RNG.

pub fn with_seed(num_qubits: usize, seed: u64) -> TableauSimulator

A fresh |0…0⟩ simulator with a fixed RNG seed (reproducible sampling).

pub fn reseed_rng(&mut self, seed: u64)

Reseed the outcome RNG in place, leaving the quantum state alone.

A RepeatUntilSuccess retry replays the same ops from the same state; without a fresh seed every attempt would sample the identical outcomes and the loop could never make progress.

pub fn restore_rng_from(&mut self, snapshot: &TableauSimulator)

Adopt snapshot’s RNG position. Rolling a failed attempt’s state back must not also roll the RNG back, or the retry repeats the same outcomes.

pub fn num_qubits(&self) -> usize

Current qubit count.

pub fn rank(&self) -> usize

Number of live amplitude terms (the stabilizer rank).

pub fn h(&mut self, q: usize)

Hadamard.

pub fn s(&mut self, q: usize)

Phase gate S = √Z.

pub fn s_dag(&mut self, q: usize)

S† = √Z†.

pub fn x(&mut self, q: usize)

Pauli X.

pub fn y(&mut self, q: usize)

Pauli Y.

pub fn z(&mut self, q: usize)

Pauli Z.

pub fn sqrt_x(&mut self, q: usize)

√X.

pub fn sqrt_x_dag(&mut self, q: usize)

√X†.

pub fn sqrt_y(&mut self, q: usize)

√Y.

pub fn sqrt_y_dag(&mut self, q: usize)

√Y†.

pub fn cx(&mut self, control: usize, target: usize) -> Result<(), SimError>

CNOT with control, target.

§Errors

SimError::RepeatedQubit if both operands name the same qubit.

pub fn cz(&mut self, a: usize, b: usize) -> Result<(), SimError>

Controlled-Z (symmetric).

§Errors

SimError::RepeatedQubit if both operands name the same qubit.

pub fn swap(&mut self, a: usize, b: usize)

Swap.

pub fn iswap(&mut self, a: usize, b: usize) -> Result<(), SimError>

ISWAP: swap a and b, with an i on the two odd-parity terms.

ISWAP = SWAP·CZ·(S⊗S). All three factors are symmetric and either diagonal or a permutation, so they commute and the order is free; the CZ goes first only because it is the fallible step, which keeps a rejected call from leaving half a gate behind.

§Errors

SimError::RepeatedQubit if both operands name the same qubit.

pub fn iswap_dag(&mut self, a: usize, b: usize) -> Result<(), SimError>

ISWAP†.

§Errors

SimError::RepeatedQubit if both operands name the same qubit.

pub fn c_xyz(&mut self, q: usize)

C_XYZ: the order-three Pauli cycle X → Y → Z → X.

pub fn c_zyx(&mut self, q: usize)

C_ZYX: the inverse cycle X → Z → Y → X.

pub fn h_xy(&mut self, q: usize)

H_XY: the Hadamard-like exchange of X and Y.

pub fn h_yz(&mut self, q: usize)

H_YZ: the Hadamard-like exchange of Y and Z.

pub fn cnot(&mut self, control: usize, target: usize) -> Result<(), SimError>

CNOT — stim’s spelling of cx.

§Errors

SimError::RepeatedQubit if both operands name the same qubit.

pub fn cy(&mut self, control: usize, target: usize) -> Result<(), SimError>

Controlled-Y, i.e. zcy.

§Errors

SimError::RepeatedQubit if both operands name the same qubit.

pub fn xcx(&mut self, control: usize, target: usize) -> Result<(), SimError>

XCX: apply X to target when control is in the −1 eigenstate of X.

§Errors

SimError::RepeatedQubit if both operands name the same qubit.

pub fn xcy(&mut self, control: usize, target: usize) -> Result<(), SimError>

XCY.

§Errors

SimError::RepeatedQubit if both operands name the same qubit.

pub fn xcz(&mut self, control: usize, target: usize) -> Result<(), SimError>

XCZ.

§Errors

SimError::RepeatedQubit if both operands name the same qubit.

pub fn ycx(&mut self, control: usize, target: usize) -> Result<(), SimError>

YCX.

§Errors

SimError::RepeatedQubit if both operands name the same qubit.

pub fn ycy(&mut self, control: usize, target: usize) -> Result<(), SimError>

YCY.

§Errors

SimError::RepeatedQubit if both operands name the same qubit.

pub fn ycz(&mut self, control: usize, target: usize) -> Result<(), SimError>

YCZ.

§Errors

SimError::RepeatedQubit if both operands name the same qubit.

pub fn zcx(&mut self, control: usize, target: usize) -> Result<(), SimError>

ZCX, i.e. cx.

§Errors

SimError::RepeatedQubit if both operands name the same qubit.

pub fn zcy(&mut self, control: usize, target: usize) -> Result<(), SimError>

ZCY, i.e. cy.

§Errors

SimError::RepeatedQubit if both operands name the same qubit.

pub fn zcz(&mut self, a: usize, b: usize) -> Result<(), SimError>

ZCZ, i.e. cz.

§Errors

SimError::RepeatedQubit if both operands name the same qubit.

pub fn pauli(&mut self, p: &PauliString)

Apply a Pauli P to the state (R → P·R).

pub fn controlled_pauli( &mut self, control: &PauliString, target: &PauliString, ) -> Result<(), SimError>

Apply control-conditioned target (both Paulis).

§Errors

SimError::NonCommutingControlledPaulis if the axes anticommute, or SimError::InvalidControlledPauli if either axis carries a sign or an imaginary coefficient — conditioning on −P is a different operation, and the frame update below only represents the positive one.

pub fn t_pauli( &mut self, axis: &PauliString, adjoint: bool, ) -> Result<(), SimError>

Apply T_P(±) = cos(π/8)·I ∓ i·sin(π/8)·P about the Pauli axis axis (adjoint = true selects the + sign, i.e. T†).

A PauliString is Hermitian by construction, so any axis is a legal rotation generator; the register grows to cover its support.

§Errors

SimError::RankOverflow if the term count exceeds the cap, or SimError::EmptyStateAfterPruning if pruning erases every term.

§Examples
use ticit::{PauliString, TableauSimulator};

// T about Z on |+⟩ leaves ⟨X⟩ = ⟨Y⟩ = 1/√2.
let mut sim = TableauSimulator::with_seed(1, 0);
sim.h(0);
sim.t_pauli(&PauliString::try_from("Z")?, false)?;
let x = sim.peek_observable_expectation(&PauliString::try_from("X")?)?;
assert!((x - std::f64::consts::FRAC_1_SQRT_2).abs() < 1e-9);

pub fn pauli_rotation( &mut self, axis: &PauliString, kernel_angle: f64, ) -> Result<(), SimError>

Apply exp(-i * kernel_angle * axis) about a Pauli axis.

§Errors

Returns SimError::InvalidRotationAngle for a non-finite angle, SimError::NonHermitianPauli for a non-observable axis, or the same rank and pruning errors as t_pauli.

pub fn t(&mut self, q: usize) -> Result<(), SimError>

T = T_Z on qubit q.

§Errors

Propagates t_pauli errors.

pub fn t_dag(&mut self, q: usize) -> Result<(), SimError>

T† = T_Z† on qubit q.

§Errors

Propagates t_pauli errors.

pub fn ccz(&mut self, a: usize, b: usize, c: usize) -> Result<(), SimError>

Apply a CCZ on qubits a, b, c (symmetric) via seven π/8 rotations.

CCZ has the phase-polynomial decomposition exp(iπ/8·[−Z_a − Z_b − Z_c + Z_aZ_b + Z_aZ_c + Z_bZ_c − Z_aZ_bZ_c]) (global phase e^{iπ/8} unobservable), following clifft’s append_ccz_decomposition (github.com/unitaryfoundation/clifft, src/clifft/circuit/parser.cc): 4abc = a + b + c − (a⊕b) − (a⊕c) − (b⊕c) + (a⊕b⊕c), i.e. 7 Ts + 6 CXs. In the stabilizer frame the CX conjugations only retarget the T axes, so we apply the seven rotations directly on multi-qubit Z axes — no CXs needed:

  • T on Z_a, Z_b, Z_c (singles, − sign → non-adjoint),
  • T† on Z_aZ_b, Z_aZ_c, Z_bZ_c (pairs, + sign → adjoint),
  • T on Z_aZ_bZ_c (triple, − sign → non-adjoint).
§Errors

SimError::RepeatedQubit if a, b, c are not distinct; propagates t_pauli errors (e.g. rank overflow — the seven axes lift the rank by at most 2^7 transiently before pruning). Errors leave the simulator unchanged.

pub fn measure(&mut self, q: usize) -> Result<MeasureResult, SimError>

Measure qubit q in the Z basis, sampling the outcome and collapsing the state onto it. The register grows to cover q.

The single-qubit case never builds an observable: Z_q’s preimage is a stored frame row, so this decomposes it directly. Use measure_observable for a Pauli product.

§Errors

SimError::RankOverflow if the projection exceeds the rank cap.

§Examples
use ticit::TableauSimulator;

let mut sim = TableauSimulator::with_seed(2, 7);
sim.h(0);
sim.cx(0, 1)?;
let first = sim.measure(0)?;
let second = sim.measure(1)?;
assert_eq!(first.outcome, second.outcome, "a Bell pair agrees");
assert!(second.deterministic, "the partner is pinned by the first read");

pub fn measure_observable( &mut self, observable: &PauliString, ) -> Result<MeasureResult, SimError>

Measure the Pauli observable observable, sampling the outcome. The register grows to cover its support.

A PauliString is Hermitian by construction, so any string is a legal observable.

§Errors

SimError::RankOverflow if the projection exceeds the rank cap.

§Examples
use ticit::{PauliString, TableauSimulator};

let mut sim = TableauSimulator::with_seed(2, 1);
sim.h(0);
sim.cx(0, 1)?;
// `ZZ` stabilizes a Bell pair, so reading it disturbs nothing.
let result = sim.measure_observable(&PauliString::try_from("ZZ")?)?;
assert!(result.deterministic && !result.outcome);

pub fn postselect_observable( &mut self, observable: &PauliString, desired_value: bool, ) -> Result<MeasureResult, SimError>

Force observable’s measurement to desired_value (false = +1) instead of sampling it, projecting the state onto that eigenspace.

The returned MeasureResult::probability is the weight the branch had before the projection — what a caller weights the post-selected shot by.

§Errors

SimError::PostselectImpossible if desired_value has probability ≈ 0, plus the measure_observable errors.

pub fn postselect_z( &mut self, q: usize, desired_value: bool, ) -> Result<MeasureResult, SimError>

Force qubit q’s Z measurement to desired_value: false selects |0⟩, true selects |1⟩.

§Errors

As postselect_observable.

pub fn postselect_x( &mut self, q: usize, desired_value: bool, ) -> Result<MeasureResult, SimError>

Force qubit q’s X measurement: false selects |+⟩, true |−⟩.

§Errors

As postselect_observable.

pub fn postselect_y( &mut self, q: usize, desired_value: bool, ) -> Result<MeasureResult, SimError>

Force qubit q’s Y measurement: false selects |i⟩, true |−i⟩.

§Errors

As postselect_observable.

pub fn peek_observable_expectation( &self, observable: &PauliString, ) -> Result<f64, SimError>

Non-collapsing expectation value ⟨P⟩ ∈ [−1, 1] of observable, leaving both the state and the RNG untouched.

This is the same ⟨Q⟩ the measurement path derives its outcome probability from (p₊ = (1 + ⟨Q⟩)/2), read out without projecting. On an eigenstate it is exactly ±1; off an eigenstate it is the true expectation (e.g. ⟨X⟩ = ⟨Y⟩ = 1/√2 on T|+⟩).

observable’s support must lie within the allocated qubits — being &self, this cannot grow the register.

§Errors

SimError::QubitIndexOutOfRange if the support exceeds the live register.

§Examples
use ticit::{PauliString, TableauSimulator};

let mut sim = TableauSimulator::with_seed(2, 0);
sim.h(0);
sim.cx(0, 1)?;
let xx = PauliString::try_from("XX")?;
assert!((sim.peek_observable_expectation(&xx)? - 1.0).abs() < 1e-9);

pub fn peek_z(&self, q: usize) -> Result<f64, SimError>

⟨Z_q⟩, without measuring.

§Errors

SimError::QubitIndexOutOfRange if q is outside the live register.

pub fn peek_x(&self, q: usize) -> Result<f64, SimError>

⟨X_q⟩, without measuring.

§Errors

SimError::QubitIndexOutOfRange if q is outside the live register.

pub fn peek_y(&self, q: usize) -> Result<f64, SimError>

⟨Y_q⟩, without measuring.

§Errors

SimError::QubitIndexOutOfRange if q is outside the live register.

pub fn reset(&mut self, q: usize) -> Result<(), SimError>

Reset qubit q to |0⟩ — stim’s spelling of reset_z.

§Errors

Propagates measurement errors.

pub fn reset_z(&mut self, q: usize) -> Result<(), SimError>

Reset qubit q to |0⟩: measure Z_q, then apply X_q if the outcome was −1.

§Errors

Propagates measurement errors.

pub fn reset_x(&mut self, q: usize) -> Result<(), SimError>

Reset qubit q to |+⟩: measure X_q, correct with Z_q if −1.

§Errors

Propagates measurement errors.

pub fn reset_y(&mut self, q: usize) -> Result<(), SimError>

Reset qubit q to |+i⟩: measure Y_q, correct with Z_q if −1.

§Errors

Propagates measurement errors.

pub fn state_vector(&self) -> Vec<Complex<f64>>

Reconstruct the dense state vector |ψ⟩ (length 2^n).

Replays the frame R (via its image_x/image_z images) against the amplitude map. This is O(rank · n · 2^n) and intended for testing and small n only.

§Panics

Panics unless n < 64, where the length stops fitting a usize. The check is an assertion rather than an error because the bound it enforces is nowhere near the one that matters: 2^n amplitudes are 16 GiB by n = 30, so every caller that gets an answer at all is far below it. Without the check the shift wraps, and a release build hands back a vector of the wrong length instead of failing.

Trait Implementations§

§

impl Clone for TableauSimulator

§

fn clone(&self) -> TableauSimulator

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
§

impl Debug for TableauSimulator

§

fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
§

impl<T, BorrowedSelf> BitwiseMutViaBorrow<BorrowedSelf> for T
where T: BorrowMut<BorrowedSelf>, BorrowedSelf: BitwiseMut + ?Sized,

§

fn assign_index(&mut self, index: usize, to: bool)

§

fn negate_index(&mut self, index: usize)

§

fn clear_bits(&mut self)

§

fn assign_random( &mut self, bit_count: usize, random_number_generator: &mut impl RngExt, )
where BorrowedSelf: BitLength,

§

impl<Bits, Other, BorrowedSelf, BorrowedOther> BitwisePairMutViaBorrow<Other, BorrowedSelf, BorrowedOther> for Bits
where Bits: BorrowMut<BorrowedSelf> + ?Sized, Other: Borrow<BorrowedOther> + ?Sized, BorrowedSelf: Bitwise + BitwisePairMut<BorrowedOther> + ?Sized, BorrowedOther: Bitwise + ?Sized,

§

fn assign(&mut self, other: &Other)

§

fn bitxor_assign(&mut self, other: &Other)

§

fn bitand_assign(&mut self, other: &Other)

§

fn bitor_assign(&mut self, other: &Other)

§

impl<T, Other, BorrowedSelf, BorrowedOther> BitwisePairViaBorrow<Other, BorrowedSelf, BorrowedOther> for T
where T: Borrow<BorrowedSelf> + ?Sized, Other: Borrow<BorrowedOther> + ?Sized, BorrowedSelf: Bitwise + BitwisePair<BorrowedOther> + ?Sized, BorrowedOther: Bitwise + ?Sized,

§

fn dot(&self, other: &Other) -> bool

§

fn and_weight(&self, other: &Other) -> usize

§

fn or_weight(&self, other: &Other) -> usize

§

fn xor_weight(&self, other: &Other) -> usize

§

impl<T, BorrowedSelf> BitwiseViaBorrow<BorrowedSelf> for T
where T: Borrow<BorrowedSelf>, BorrowedSelf: Bitwise + ?Sized,

§

fn index(&self, index: usize) -> bool

§

fn weight(&self) -> usize

§

fn parity(&self) -> bool

§

fn is_zero(&self) -> bool

§

fn is_unit(&self, index: usize) -> bool

§

fn support<'a>(&'a self) -> impl SortedIterator
where BorrowedSelf: 'a,

§

fn max_support(&self) -> Option<usize>

§

fn min_support(&self) -> Option<usize>

Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

§

impl<T, S> SimdFrom<T, S> for T
where S: Simd,

§

fn simd_from(_simd: S, value: T) -> T

§

impl<F, T, S> SimdInto<T, S> for F
where T: SimdFrom<F, S>, S: Simd,

§

fn simd_into(self, simd: S) -> T

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.