use std::f64::consts::FRAC_1_SQRT_2;
use crate::{Pauli, PauliString, SimError, TableauSimulator};
use num_complex::Complex64;
use rand::{RngExt, SeedableRng, rngs::SmallRng};
fn apply_pauli(v: &[Complex64], p: &PauliString) -> Vec<Complex64> {
const I: Complex64 = Complex64::new(0.0, 1.0);
let mut out = v.to_vec();
for site in 0..p.nqubits {
let pauli = p.get(site);
if pauli == Pauli::I {
continue;
}
let bit = 1usize << site;
let mut next = vec![Complex64::new(0.0, 0.0); out.len()];
for (y, &value) in out.iter().enumerate() {
let set = y & bit != 0;
let (target, factor) = match pauli {
Pauli::X => (y ^ bit, Complex64::new(1.0, 0.0)),
Pauli::Z => (y, Complex64::new(if set { -1.0 } else { 1.0 }, 0.0)),
Pauli::Y => (y ^ bit, if set { -I } else { I }),
Pauli::I => unreachable!("skipped above"),
};
next[target] += factor * value;
}
out = next;
}
out
}
fn project(v: &[Complex64], p: &PauliString, outcome: bool) -> Option<Vec<Complex64>> {
let sign = if outcome { -1.0 } else { 1.0 };
let applied = apply_pauli(v, p);
let mut out: Vec<Complex64> = v
.iter()
.zip(applied)
.map(|(&x, y)| 0.5 * (x + sign * y))
.collect();
let norm_sqr: f64 = out.iter().map(num_complex::Complex::norm_sqr).sum();
if norm_sqr < 1e-12 {
return None;
}
let scale = norm_sqr.sqrt().recip();
for value in &mut out {
*value *= scale;
}
Some(out)
}
fn overlap(left: &[Complex64], right: &[Complex64]) -> f64 {
left.iter()
.zip(right)
.map(|(&a, &b)| a.conj() * b)
.sum::<Complex64>()
.norm()
}
fn assert_same_state(got: &[Complex64], want: &[Complex64], context: &str) {
let fidelity = overlap(got, want);
assert!(
(fidelity - 1.0).abs() < 1e-9,
"{context}: states differ (|<want|got>| = {fidelity})"
);
}
fn random_state(n: usize, seed: u64, cliffords: usize, magic: usize) -> TableauSimulator {
let mut rng = SmallRng::seed_from_u64(seed);
let mut sim = TableauSimulator::with_seed(n, seed);
for _ in 0..magic {
let q = rng.random_range(0..n);
sim.h(q);
sim.t(q).expect("a handful of T gates stays under the cap");
}
let choices = if n >= 2 { 5 } else { 3 };
for _ in 0..cliffords {
let q = rng.random_range(0..n);
match rng.random_range(0..choices) {
0 => sim.h(q),
1 => sim.s(q),
2 => sim.sqrt_x(q),
3 => {
let other = (q + 1 + rng.random_range(0..n - 1)) % n;
sim.cx(q, other).expect("operands are distinct");
}
_ => {
let other = (q + 1 + rng.random_range(0..n - 1)) % n;
sim.cz(q, other).expect("operands are distinct");
}
}
}
sim
}
fn random_pauli(rng: &mut SmallRng, n: usize, max_weight: usize) -> PauliString {
let weight = rng.random_range(1..=max_weight.min(n));
let mut paulis = PauliString::new(n);
let mut placed = 0;
while placed < weight {
let site = rng.random_range(0..n);
if paulis.get(site) != Pauli::I {
continue;
}
paulis.set(
site,
match rng.random_range(0..3) {
0 => Pauli::X,
1 => Pauli::Y,
_ => Pauli::Z,
},
);
placed += 1;
}
paulis
}
#[test]
fn measurement_matches_the_dense_projection() {
for n in [1usize, 2, 3, 4] {
for seed in 0..12u64 {
let base = random_state(n, 0x51D0 + seed, 12, n.min(2));
let mut rng = SmallRng::seed_from_u64(0xBEEF + seed);
let before = base.state_vector();
for trial in 0..4 {
let observable = random_pauli(&mut rng, n, n);
for outcome in [false, true] {
let Some(want) = project(&before, &observable, outcome) else {
continue; };
let mut sim = base.clone();
sim.postselect_observable(&observable, outcome)
.expect("an outcome with weight is post-selectable");
assert_same_state(
&sim.state_vector(),
&want,
&format!("n={n} seed={seed} trial={trial} {observable:#} -> {outcome}"),
);
}
}
}
}
}
#[test]
fn chained_measurements_match_the_dense_projections() {
for n in [2usize, 3, 4] {
let mut rng = SmallRng::seed_from_u64(0xC4A1 + n as u64);
let mut sim = random_state(n, 0xC4A1 + n as u64, 20, n.min(3));
for step in 0..12 {
let observable = random_pauli(&mut rng, n, n);
let before = sim.state_vector();
let outcome = rng.random_bool(0.5);
let Some(want) = project(&before, &observable, outcome) else {
continue;
};
sim.postselect_observable(&observable, outcome)
.expect("an outcome with weight is post-selectable");
assert_same_state(
&sim.state_vector(),
&want,
&format!("n={n} step={step} {observable:#} -> {outcome}"),
);
}
}
}
#[test]
fn a_measured_observable_becomes_deterministic() {
let n = 130;
for outcome in [false, true] {
let mut sim = TableauSimulator::with_seed(n, 5);
sim.h(70);
sim.t(70).expect("one T stays under the cap");
sim.cx(70, 3).expect("operands are distinct");
sim.h(3);
let observable = PauliString::single(n, 70, Pauli::Z);
sim.postselect_observable(&observable, outcome)
.expect("Z on a magic qubit has weight on both outcomes");
let sign = if outcome { -1.0 } else { 1.0 };
let expectation = sim.peek_z(70).expect("qubit 70 is live");
assert!(
(expectation - sign).abs() < 1e-12,
"outcome={outcome}: <Z> = {expectation}, want {sign}"
);
let repeat = sim.measure_observable(&observable).expect("within the cap");
assert_eq!(repeat.outcome, outcome, "the repeat must agree");
assert!(repeat.deterministic, "the repeat must be forced");
assert!((repeat.probability - 1.0).abs() < 1e-12);
}
}
#[test]
fn basis_axis_paths_match_the_pauli_string_paths() {
let n = 5;
let seed = 0xAA55;
let build = || random_state(n, seed, 15, 2);
for q in 0..n {
let mut fast = build();
let mut slow = build();
fast.t(q).expect("T stays under the cap");
slow.t_pauli(&PauliString::single(n, q, Pauli::Z), false)
.expect("T stays under the cap");
assert_same_state(
&fast.state_vector(),
&slow.state_vector(),
&format!("t({q})"),
);
let mut fast = build();
let mut slow = build();
fast.t_dag(q).expect("T† stays under the cap");
slow.t_pauli(&PauliString::single(n, q, Pauli::Z), true)
.expect("T† stays under the cap");
assert_same_state(
&fast.state_vector(),
&slow.state_vector(),
&format!("t_dag({q})"),
);
type Reset = fn(&mut TableauSimulator, usize) -> Result<(), SimError>;
for (axis, reset) in [
("z", TableauSimulator::reset_z as Reset),
("x", TableauSimulator::reset_x),
("y", TableauSimulator::reset_y),
] {
let mut fast = build();
let mut slow = build();
reset(&mut fast, q).expect("reset is a measurement plus a correction");
let observable = PauliString::single(
n,
q,
match axis {
"z" => Pauli::Z,
"x" => Pauli::X,
_ => Pauli::Y,
},
);
if slow
.measure_observable(&observable)
.expect("within the rank cap")
.outcome
{
match axis {
"z" => slow.x(q),
_ => slow.z(q),
}
}
assert_same_state(
&fast.state_vector(),
&slow.state_vector(),
&format!("reset_{axis}({q})"),
);
}
}
}
#[test]
fn the_compression_preserves_the_state_norm() {
let n = 3;
let mut rng = SmallRng::seed_from_u64(0x2E57);
for seed in 0..8u64 {
let mut sim = random_state(n, 0x2E57 + seed, 10, 2);
for _ in 0..6 {
let observable = random_pauli(&mut rng, n, n);
sim.measure_observable(&observable)
.expect("within the rank cap");
let norm_sqr: f64 = sim
.state_vector()
.iter()
.map(num_complex::Complex::norm_sqr)
.sum();
assert!(
(norm_sqr - 1.0).abs() < 1e-9,
"state norm drifted to {norm_sqr}"
);
}
}
}
#[test]
fn a_single_qubit_random_measurement_lands_on_the_basis_state() {
for outcome in [false, true] {
let mut sim = TableauSimulator::with_seed(1, 0);
sim.h(0);
let result = sim
.postselect_z(0, outcome)
.expect("Z on |+> is an even coin");
assert!((result.probability - 0.5).abs() < 1e-12);
let want = if outcome {
[Complex64::new(0.0, 0.0), Complex64::new(1.0, 0.0)]
} else {
[Complex64::new(1.0, 0.0), Complex64::new(0.0, 0.0)]
};
assert_same_state(&sim.state_vector(), &want, &format!("Z|+> -> {outcome}"));
let x_expectation = sim.peek_x(0).expect("qubit 0 is live");
assert!(x_expectation.abs() < 1e-12, "<X> = {x_expectation}");
}
}
#[test]
fn sampled_outcomes_follow_the_born_rule() {
let n = 2;
let base = {
let mut sim = TableauSimulator::with_seed(n, 11);
sim.h(0);
sim.t(0).expect("one T stays under the cap");
sim.h(0);
sim
};
let expectation = base.peek_z(0).expect("qubit 0 is live");
assert!((expectation - FRAC_1_SQRT_2).abs() < 1e-12, "{expectation}");
let shots: u32 = 20_000;
let mut minus = 0u32;
for seed in 0..shots {
let mut sim = base.clone();
sim.reseed_rng(u64::from(seed));
if sim.measure(0).expect("within the rank cap").outcome {
minus += 1;
}
}
let observed = f64::from(minus) / f64::from(shots);
let expected = (1.0 - FRAC_1_SQRT_2) / 2.0;
assert!(
(observed - expected).abs() < 0.01,
"sampled {observed}, expected {expected}"
);
}