use std::collections::{HashMap, hash_map::RandomState};
use std::f64::consts::{FRAC_1_SQRT_2, PI};
use std::hash::{BuildHasher, Hasher};
use crate::frames::{self, CliffordFrame, coordinates_in_frame, preimage};
use crate::pauli::{PauliString, measurement_phase_sign, pauli_anticommutes};
use crate::random::rand_float;
use num_complex::Complex64;
mod error;
mod label;
pub use error::SimError;
use label::{Key, Label, LabelKey, Width};
#[derive(Clone, Copy)]
enum Axis {
X,
Y,
Z,
}
const TOL: f64 = 1e-9;
const DEFAULT_PRUNE_EPSILON: f64 = 1e-12;
const DEFAULT_RANK_CAP: usize = 1 << 20;
const PAULI_EXP_SIGN: f64 = -1.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MeasureResult {
pub outcome: bool,
pub probability: f64,
pub deterministic: bool,
}
#[derive(Clone, Debug)]
pub struct TableauSimulator {
core: Core,
amps: Amps,
}
#[derive(Clone, Debug)]
struct Core {
n: usize,
words: usize,
r: CliffordFrame,
rng: u64,
prune_epsilon: f64,
rank_cap: usize,
}
#[derive(Clone, Debug)]
struct Terms<K: LabelKey> {
map: HashMap<K, Complex64>,
rotation: RotationScratch<K>,
}
impl<K: LabelKey> PartialEq for Terms<K> {
fn eq(&self, other: &Self) -> bool {
self.map == other.map
}
}
#[derive(Clone, Debug, PartialEq)]
enum Amps {
W1(Terms<Key<1>>),
W2(Terms<Key<2>>),
W4(Terms<Key<4>>),
W8(Terms<Key<8>>),
Wide(Terms<Label>),
}
macro_rules! with_terms {
($sim:expr, |$core:ident, $terms:ident| $body:expr) => {{
let TableauSimulator {
core: ref mut $core,
amps: ref mut storage,
} = *$sim;
match storage {
Amps::W1($terms) => $body,
Amps::W2($terms) => $body,
Amps::W4($terms) => $body,
Amps::W8($terms) => $body,
Amps::Wide($terms) => $body,
}
}};
}
macro_rules! with_terms_ref {
($sim:expr, |$core:ident, $terms:ident| $body:expr) => {{
let TableauSimulator {
core: ref $core,
amps: ref storage,
} = *$sim;
match storage {
Amps::W1($terms) => $body,
Amps::W2($terms) => $body,
Amps::W4($terms) => $body,
Amps::W8($terms) => $body,
Amps::Wide($terms) => $body,
}
}};
}
#[derive(Clone, Debug)]
struct RotationScratch<K> {
values: Vec<Complex64>,
inserts: Vec<(K, Complex64)>,
partners: Vec<Option<Complex64>>,
}
impl<K> Default for RotationScratch<K> {
fn default() -> Self {
RotationScratch {
values: Vec::new(),
inserts: Vec::new(),
partners: Vec::new(),
}
}
}
impl<K> RotationScratch<K> {
fn clear(&mut self) {
self.values.clear();
self.inserts.clear();
self.partners.clear();
}
}
#[inline]
fn i_pow(k: u8) -> Complex64 {
match k & 3 {
0 => Complex64::new(1.0, 0.0),
1 => Complex64::new(0.0, 1.0),
2 => Complex64::new(-1.0, 0.0),
_ => Complex64::new(0.0, -1.0),
}
}
struct Decomp<K> {
a: K,
b: K,
phase: u8,
zeta: Complex64,
}
struct Projection<'a, K> {
d: &'a Decomp<K>,
gb: &'a K,
pivot: usize,
s: bool,
ssign: f64,
compress: Complex64,
shift_flip: f64,
}
impl<K: LabelKey> Projection<'_, K> {
#[inline(always)]
fn rewrite_pair(&self, c: &K, x: Complex64, y: Complex64) -> (Complex64, Complex64) {
let zc = if self.s && c.get(self.pivot) {
-1.0
} else {
1.0
};
let zp = if self.s { -zc } else { 1.0 };
let sb = if c.dot_parity(&self.d.b) { -1.0 } else { 1.0 };
let sg = if c.dot_parity(self.gb) { -1.0 } else { 1.0 };
let shift_b = -self.shift_flip;
let u = 0.5 * zc * x + (0.5 * self.ssign * (sb * shift_b) * zc) * self.d.zeta * y;
let v = (0.5 * self.ssign * sb * zp) * self.d.zeta * x + 0.5 * zp * y;
(
FRAC_1_SQRT_2 * u + self.compress * (sg * self.shift_flip) * v,
self.compress * sg * u + FRAC_1_SQRT_2 * v,
)
}
}
impl TableauSimulator {
#[must_use]
pub fn new(num_qubits: usize) -> Self {
Self::with_seed(num_qubits, RandomState::new().build_hasher().finish())
}
#[must_use]
pub fn with_seed(num_qubits: usize, seed: u64) -> Self {
let r = CliffordFrame::new(num_qubits);
let words = storage_words(num_qubits.div_ceil(64));
TableauSimulator {
core: Core {
n: num_qubits,
words,
r,
rng: seed,
prune_epsilon: DEFAULT_PRUNE_EPSILON,
rank_cap: DEFAULT_RANK_CAP,
},
amps: Amps::unit(words),
}
}
#[doc(hidden)]
pub fn set_rank_cap(&mut self, cap: usize) {
self.core.rank_cap = cap;
}
#[doc(hidden)]
pub fn set_prune_epsilon(&mut self, epsilon: f64) {
self.core.prune_epsilon = epsilon;
}
pub fn reseed_rng(&mut self, seed: u64) {
self.core.rng = seed;
}
pub fn restore_rng_from(&mut self, snapshot: &Self) {
self.core.rng = snapshot.core.rng;
}
#[must_use]
pub fn num_qubits(&self) -> usize {
self.core.n
}
#[must_use]
pub fn rank(&self) -> usize {
self.amps.len()
}
fn ensure_qubits(&mut self, need: usize) {
if need <= self.core.n {
return;
}
self.core.r.grow_to(need);
let new_words = storage_words(need.div_ceil(64));
if new_words > self.core.words {
self.amps.widen(new_words);
self.core.words = new_words;
}
self.core.n = need;
debug_assert_eq!(self.core.n, self.core.r.nqubits, "frame tracks register");
debug_assert_eq!(
self.core.words,
storage_words(self.core.n.div_ceil(64)),
"label width tracks register",
);
}
fn ensure_for(&mut self, pauli: &PauliString) {
if let Some(max) = max_support(pauli) {
self.ensure_qubits(max + 1);
}
}
pub fn h(&mut self, q: usize) {
self.ensure_qubits(q + 1);
frames::left_h(&mut self.core.r, q);
}
pub fn s(&mut self, q: usize) {
self.ensure_qubits(q + 1);
frames::left_s(&mut self.core.r, q);
}
pub fn s_dag(&mut self, q: usize) {
self.ensure_qubits(q + 1);
frames::left_sdg(&mut self.core.r, q);
}
pub fn x(&mut self, q: usize) {
self.ensure_qubits(q + 1);
frames::left_x(&mut self.core.r, q);
}
pub fn y(&mut self, q: usize) {
self.ensure_qubits(q + 1);
frames::left_y(&mut self.core.r, q);
}
pub fn z(&mut self, q: usize) {
self.ensure_qubits(q + 1);
frames::left_z(&mut self.core.r, q);
}
pub fn sqrt_x(&mut self, q: usize) {
self.ensure_qubits(q + 1);
frames::left_sqrt_x(&mut self.core.r, q);
}
pub fn sqrt_x_dag(&mut self, q: usize) {
self.ensure_qubits(q + 1);
frames::left_sqrt_x_dag(&mut self.core.r, q);
}
pub fn sqrt_y(&mut self, q: usize) {
self.ensure_qubits(q + 1);
frames::left_sqrt_y(&mut self.core.r, q);
}
pub fn sqrt_y_dag(&mut self, q: usize) {
self.ensure_qubits(q + 1);
frames::left_sqrt_y_dag(&mut self.core.r, q);
}
pub fn cx(&mut self, control: usize, target: usize) -> Result<(), SimError> {
if control == target {
return Err(SimError::RepeatedQubit(control));
}
self.ensure_qubits(control.max(target) + 1);
frames::left_cx(&mut self.core.r, control, target);
Ok(())
}
pub fn cz(&mut self, a: usize, b: usize) -> Result<(), SimError> {
if a == b {
return Err(SimError::RepeatedQubit(a));
}
self.ensure_qubits(a.max(b) + 1);
frames::left_cz(&mut self.core.r, a, b);
Ok(())
}
pub fn swap(&mut self, a: usize, b: usize) {
self.ensure_qubits(a.max(b) + 1);
frames::left_swap(&mut self.core.r, a, b);
}
pub fn iswap(&mut self, a: usize, b: usize) -> Result<(), SimError> {
self.cz(a, b)?;
self.s(a);
self.s(b);
self.swap(a, b);
Ok(())
}
pub fn iswap_dag(&mut self, a: usize, b: usize) -> Result<(), SimError> {
self.cz(a, b)?;
self.s_dag(a);
self.s_dag(b);
self.swap(a, b);
Ok(())
}
pub fn c_xyz(&mut self, q: usize) {
self.ensure_qubits(q + 1);
frames::left_c_xyz(&mut self.core.r, q);
}
pub fn c_zyx(&mut self, q: usize) {
self.ensure_qubits(q + 1);
frames::left_c_zyx(&mut self.core.r, q);
}
pub fn h_xy(&mut self, q: usize) {
self.ensure_qubits(q + 1);
frames::left_h_xy(&mut self.core.r, q);
}
pub fn h_yz(&mut self, q: usize) {
self.ensure_qubits(q + 1);
frames::left_h_yz(&mut self.core.r, q);
}
pub fn cnot(&mut self, control: usize, target: usize) -> Result<(), SimError> {
self.cx(control, target)
}
pub fn cy(&mut self, control: usize, target: usize) -> Result<(), SimError> {
self.zcy(control, target)
}
pub fn xcx(&mut self, control: usize, target: usize) -> Result<(), SimError> {
self.apply_two_qubit(control, target, frames::left_xcx)
}
pub fn xcy(&mut self, control: usize, target: usize) -> Result<(), SimError> {
self.apply_two_qubit(control, target, frames::left_xcy)
}
pub fn xcz(&mut self, control: usize, target: usize) -> Result<(), SimError> {
self.apply_two_qubit(control, target, frames::left_xcz)
}
pub fn ycx(&mut self, control: usize, target: usize) -> Result<(), SimError> {
self.apply_two_qubit(control, target, frames::left_ycx)
}
pub fn ycy(&mut self, control: usize, target: usize) -> Result<(), SimError> {
self.apply_two_qubit(control, target, frames::left_ycy)
}
pub fn ycz(&mut self, control: usize, target: usize) -> Result<(), SimError> {
self.apply_two_qubit(control, target, frames::left_ycz)
}
pub fn zcx(&mut self, control: usize, target: usize) -> Result<(), SimError> {
self.cx(control, target)
}
pub fn zcy(&mut self, control: usize, target: usize) -> Result<(), SimError> {
self.apply_two_qubit(control, target, frames::left_cy)
}
pub fn zcz(&mut self, a: usize, b: usize) -> Result<(), SimError> {
self.cz(a, b)
}
pub fn pauli(&mut self, p: &PauliString) {
self.ensure_for(p);
let p = pauli_on_register(p, self.core.n).expect("register covers Pauli support");
frames::left_pauli(&mut self.core.r, &p);
}
pub fn controlled_pauli(
&mut self,
control: &PauliString,
target: &PauliString,
) -> Result<(), SimError> {
let nqubits = [max_support(control), max_support(target)]
.into_iter()
.flatten()
.max()
.map_or(self.core.n, |q| self.core.n.max(q + 1));
let control = pauli_on_register(control, nqubits)?;
let target = pauli_on_register(target, nqubits)?;
if pauli_anticommutes(&control, &target) {
return Err(SimError::NonCommutingControlledPaulis);
}
if measurement_phase_sign(&control).ok() != Some(false)
|| measurement_phase_sign(&target).ok() != Some(false)
{
return Err(SimError::InvalidControlledPauli);
}
self.ensure_qubits(nqubits);
frames::left_controlled_pauli(&mut self.core.r, &control, &target);
Ok(())
}
fn apply_two_qubit(
&mut self,
a: usize,
b: usize,
gate: fn(&mut CliffordFrame, usize, usize),
) -> Result<(), SimError> {
if a == b {
return Err(SimError::RepeatedQubit(a));
}
self.ensure_qubits(a.max(b) + 1);
gate(&mut self.core.r, a, b);
Ok(())
}
pub fn t_pauli(&mut self, axis: &PauliString, adjoint: bool) -> Result<(), SimError> {
measurement_phase_sign(axis).map_err(|_| SimError::NonHermitianPauli)?;
self.ensure_for(axis);
with_terms!(self, |core, terms| {
let d = core.decompose(axis)?;
terms.t_decomposed(core, &d, adjoint)
})
}
pub fn t(&mut self, q: usize) -> Result<(), SimError> {
self.t_about(Axis::Z, q, false)
}
pub fn t_dag(&mut self, q: usize) -> Result<(), SimError> {
self.t_about(Axis::Z, q, true)
}
fn t_about(&mut self, axis: Axis, q: usize, adjoint: bool) -> Result<(), SimError> {
self.ensure_qubits(q + 1);
with_terms!(self, |core, terms| {
let d = core.decompose_basis(axis, q);
terms.t_decomposed(core, &d, adjoint)
})
}
pub fn ccz(&mut self, a: usize, b: usize, c: usize) -> Result<(), SimError> {
if a == b || a == c {
return Err(SimError::RepeatedQubit(a));
}
if b == c {
return Err(SimError::RepeatedQubit(b));
}
let safe = self.rank() <= self.core.rank_cap >> 7
&& self.core.prune_epsilon <= DEFAULT_PRUNE_EPSILON;
if safe {
return self.ccz_rotations(a, b, c);
}
let mut next = self.clone();
next.ccz_rotations(a, b, c)?;
*self = next;
Ok(())
}
fn ccz_rotations(&mut self, a: usize, b: usize, c: usize) -> Result<(), SimError> {
self.ensure_qubits(a.max(b).max(c) + 1);
let mut axis = PauliString::new(self.core.n);
for (operands, adjoint) in [
(&[a][..], false),
(&[b][..], false),
(&[c][..], false),
(&[a, b][..], true),
(&[a, c][..], true),
(&[b, c][..], true),
(&[a, b, c][..], false),
] {
axis.z.fill(0);
for &site in operands {
axis.set_zbit(site, true);
}
self.t_pauli(&axis, adjoint)?;
}
Ok(())
}
pub fn measure(&mut self, q: usize) -> Result<MeasureResult, SimError> {
self.measure_axis(Axis::Z, q, None)
}
pub fn measure_observable(
&mut self,
observable: &PauliString,
) -> Result<MeasureResult, SimError> {
measurement_phase_sign(observable).map_err(|_| SimError::NonHermitianPauli)?;
self.ensure_for(observable);
with_terms!(self, |core, terms| {
let d = core.decompose(observable)?;
terms.measure_decomposed(core, &d, None)
})
}
pub fn postselect_observable(
&mut self,
observable: &PauliString,
desired_value: bool,
) -> Result<MeasureResult, SimError> {
measurement_phase_sign(observable).map_err(|_| SimError::NonHermitianPauli)?;
self.ensure_for(observable);
with_terms!(self, |core, terms| {
let d = core.decompose(observable)?;
terms.measure_decomposed(core, &d, Some(desired_value))
})
}
pub fn postselect_z(
&mut self,
q: usize,
desired_value: bool,
) -> Result<MeasureResult, SimError> {
self.measure_axis(Axis::Z, q, Some(desired_value))
}
pub fn postselect_x(
&mut self,
q: usize,
desired_value: bool,
) -> Result<MeasureResult, SimError> {
self.measure_axis(Axis::X, q, Some(desired_value))
}
pub fn postselect_y(
&mut self,
q: usize,
desired_value: bool,
) -> Result<MeasureResult, SimError> {
self.measure_axis(Axis::Y, q, Some(desired_value))
}
fn measure_axis(
&mut self,
axis: Axis,
q: usize,
forced: Option<bool>,
) -> Result<MeasureResult, SimError> {
self.ensure_qubits(q + 1);
with_terms!(self, |core, terms| {
let d = core.decompose_basis(axis, q);
terms.measure_decomposed(core, &d, forced)
})
}
pub fn peek_observable_expectation(&self, observable: &PauliString) -> Result<f64, SimError> {
with_terms_ref!(self, |core, terms| {
let d = core.decompose(observable)?;
Ok(terms.expectation_of(&d).clamp(-1.0, 1.0))
})
}
pub fn peek_z(&self, q: usize) -> Result<f64, SimError> {
self.peek_axis(Axis::Z, q)
}
pub fn peek_x(&self, q: usize) -> Result<f64, SimError> {
self.peek_axis(Axis::X, q)
}
pub fn peek_y(&self, q: usize) -> Result<f64, SimError> {
self.peek_axis(Axis::Y, q)
}
fn peek_axis(&self, axis: Axis, q: usize) -> Result<f64, SimError> {
if q >= self.core.n {
return Err(SimError::QubitIndexOutOfRange {
index: q,
num_qubits: self.core.n,
});
}
with_terms_ref!(self, |core, terms| {
let d = core.decompose_basis(axis, q);
Ok(terms.expectation_of(&d).clamp(-1.0, 1.0))
})
}
pub fn reset(&mut self, q: usize) -> Result<(), SimError> {
self.reset_z(q)
}
pub fn reset_z(&mut self, q: usize) -> Result<(), SimError> {
self.reset_about(Axis::Z, q, Axis::X)
}
pub fn reset_x(&mut self, q: usize) -> Result<(), SimError> {
self.reset_about(Axis::X, q, Axis::Z)
}
pub fn reset_y(&mut self, q: usize) -> Result<(), SimError> {
self.reset_about(Axis::Y, q, Axis::Z)
}
fn reset_about(&mut self, axis: Axis, q: usize, correction: Axis) -> Result<(), SimError> {
if self.measure_axis(axis, q, None)?.outcome {
match correction {
Axis::X => frames::left_x(&mut self.core.r, q),
Axis::Y => frames::left_y(&mut self.core.r, q),
Axis::Z => frames::left_z(&mut self.core.r, q),
}
}
Ok(())
}
#[must_use]
pub fn state_vector(&self) -> Vec<Complex64> {
let n = self.core.n;
assert!(
n < usize::BITS as usize,
"state-vector reconstruction needs a 2^{n} length that fits a usize"
);
let dim = 1usize << n;
let stabs: Vec<PauliString> = (0..n)
.map(|i| {
coordinates_in_frame(&self.core.r, &crate::pauli::pauli_z(n, i))
.expect("valid Clifford frame")
})
.collect();
let mut psi0 = vec![Complex64::new(0.0, 0.0); dim];
for fiducial in 0..dim {
let mut v = vec![Complex64::new(0.0, 0.0); dim];
v[fiducial] = Complex64::new(1.0, 0.0);
for s in &stabs {
let projected = apply_pauli_dense(&v, s);
for (dst, add) in v.iter_mut().zip(projected) {
*dst = (*dst + add) * 0.5;
}
}
let norm: f64 = v.iter().map(num_complex::Complex::norm_sqr).sum();
if norm > TOL {
let scale = norm.sqrt().recip();
for (dst, src) in psi0.iter_mut().zip(v) {
*dst = src * scale;
}
break;
}
}
let destabs: Vec<PauliString> = (0..n)
.map(|i| {
coordinates_in_frame(&self.core.r, &crate::pauli::pauli_x(n, i))
.expect("valid Clifford frame")
})
.collect();
let mut out = vec![Complex64::new(0.0, 0.0); dim];
match &self.amps {
Amps::W1(t) => replay_terms(&t.map, &psi0, &destabs, &mut out),
Amps::W2(t) => replay_terms(&t.map, &psi0, &destabs, &mut out),
Amps::W4(t) => replay_terms(&t.map, &psi0, &destabs, &mut out),
Amps::W8(t) => replay_terms(&t.map, &psi0, &destabs, &mut out),
Amps::Wide(t) => replay_terms(&t.map, &psi0, &destabs, &mut out),
}
out
}
}
impl Core {
fn decompose<K: LabelKey>(&self, p: &PauliString) -> Result<Decomp<K>, SimError> {
measurement_phase_sign(p).map_err(|_| SimError::NonHermitianPauli)?;
let p = pauli_on_register(p, self.n)?;
let transformed = preimage(&self.r, &p);
let mut a = K::zeros(self.words);
let mut b = K::zeros(self.words);
a.as_mut_slice()[..transformed.x.len()].copy_from_slice(&transformed.x);
b.as_mut_slice()[..transformed.z.len()].copy_from_slice(&transformed.z);
let phase = transformed.phase_exponent() as u8;
Ok(Decomp {
a,
b,
phase,
zeta: i_pow(phase),
})
}
fn decompose_basis<K: LabelKey>(&self, axis: Axis, qubit: usize) -> Decomp<K> {
let pauli = match axis {
Axis::X => crate::pauli::pauli_x(self.n, qubit),
Axis::Y => crate::pauli::pauli_y(self.n, qubit),
Axis::Z => crate::pauli::pauli_z(self.n, qubit),
};
self.decompose(&pauli)
.expect("single-qubit Pauli is Hermitian and in range")
}
fn choose(&mut self, forced: Option<bool>, p0: f64) -> bool {
match forced {
Some(o) => o,
None => {
let r = rand_float(&mut self.rng);
r >= p0
}
}
}
fn check_cap(&self, rank: usize) -> Result<(), SimError> {
if rank > self.rank_cap {
return Err(SimError::RankOverflow {
rank,
cap: self.rank_cap,
});
}
Ok(())
}
}
impl Amps {
fn unit(words: usize) -> Self {
match Width::for_words(words) {
Width::W1 => Amps::W1(Terms::unit(words)),
Width::W2 => Amps::W2(Terms::unit(words)),
Width::W4 => Amps::W4(Terms::unit(words)),
Width::W8 => Amps::W8(Terms::unit(words)),
Width::Wide => Amps::Wide(Terms::unit(words)),
}
}
fn width(&self) -> Width {
match self {
Amps::W1(_) => Width::W1,
Amps::W2(_) => Width::W2,
Amps::W4(_) => Width::W4,
Amps::W8(_) => Width::W8,
Amps::Wide(_) => Width::Wide,
}
}
fn len(&self) -> usize {
match self {
Amps::W1(t) => t.map.len(),
Amps::W2(t) => t.map.len(),
Amps::W4(t) => t.map.len(),
Amps::W8(t) => t.map.len(),
Amps::Wide(t) => t.map.len(),
}
}
fn widen(&mut self, words: usize) {
let target = Width::for_words(words);
if target == self.width() && target != Width::Wide {
return;
}
let live = self.drain_terms();
*self = match target {
Width::W1 => Amps::W1(Terms::rekeyed(live, words)),
Width::W2 => Amps::W2(Terms::rekeyed(live, words)),
Width::W4 => Amps::W4(Terms::rekeyed(live, words)),
Width::W8 => Amps::W8(Terms::rekeyed(live, words)),
Width::Wide => Amps::Wide(Terms::rekeyed(live, words)),
};
}
fn drain_terms(&mut self) -> Vec<(Vec<u64>, Complex64)> {
fn collect<K: LabelKey>(map: &mut HashMap<K, Complex64>) -> Vec<(Vec<u64>, Complex64)> {
map.drain()
.map(|(key, value)| (key.as_slice().to_vec(), value))
.collect()
}
match self {
Amps::W1(t) => collect(&mut t.map),
Amps::W2(t) => collect(&mut t.map),
Amps::W4(t) => collect(&mut t.map),
Amps::W8(t) => collect(&mut t.map),
Amps::Wide(t) => collect(&mut t.map),
}
}
}
impl<K: LabelKey> Terms<K> {
fn unit(words: usize) -> Self {
let mut map = HashMap::new();
map.insert(K::zeros(words), Complex64::new(1.0, 0.0));
Terms {
map,
rotation: RotationScratch::default(),
}
}
fn rekeyed(live: Vec<(Vec<u64>, Complex64)>, words: usize) -> Self {
Terms {
map: live
.into_iter()
.map(|(bits, value)| (K::from_words(&bits, words), value))
.collect(),
rotation: RotationScratch::default(),
}
}
fn t_decomposed(&mut self, core: &Core, d: &Decomp<K>, adjoint: bool) -> Result<(), SimError> {
self.t_decomposed_inner(core, d, adjoint)
}
#[inline(always)]
fn t_decomposed_inner(
&mut self,
core: &Core,
d: &Decomp<K>,
adjoint: bool,
) -> Result<(), SimError> {
let cos = (PI / 8.0).cos();
let sin = (PI / 8.0).sin();
let branch = Complex64::new(0.0, if adjoint { sin } else { -sin });
if d.a.is_zero() {
return self.t_diagonal(core, d, cos, branch);
}
let mut scratch = std::mem::take(&mut self.rotation);
let result = self.t_paired(core, d, cos, branch, &mut scratch);
scratch.clear();
self.rotation = scratch;
result
}
fn t_diagonal(
&mut self,
core: &Core,
d: &Decomp<K>,
cos: f64,
branch: Complex64,
) -> Result<(), SimError> {
debug_assert!(
d.zeta.im.abs() < TOL,
"diagonal T axis has a non-real phase"
);
core.check_cap(self.map.len())?;
let plus = Complex64::new(cos, branch.im * d.zeta.re);
let minus = plus.conj();
for (c, value) in &mut self.map {
*value *= if c.dot_parity(&d.b) { minus } else { plus };
}
Ok(())
}
fn t_paired(
&mut self,
core: &Core,
d: &Decomp<K>,
cos: f64,
branch: Complex64,
scratch: &mut RotationScratch<K>,
) -> Result<(), SimError> {
let (removals, norm) = self.stage_t_rotation(core, d, cos, branch, scratch);
self.commit_pair_rewrite(core, scratch, removals, norm)
}
fn commit_pair_rewrite(
&mut self,
core: &Core,
scratch: &mut RotationScratch<K>,
removals: usize,
norm: Option<f64>,
) -> Result<(), SimError> {
let rank = self.map.len() + scratch.inserts.len() - removals;
if rank == 0 {
return Err(SimError::EmptyStateAfterPruning {
epsilon: core.prune_epsilon,
});
}
core.check_cap(rank)?;
let scale = match norm {
Some(total) if total > 0.0 => total.sqrt().recip(),
_ => 1.0,
};
debug_assert_eq!(scratch.values.len(), self.map.len());
if removals == 0 {
for (slot, &value) in self.map.values_mut().zip(&scratch.values) {
*slot = value * scale;
}
} else {
let eps_sq = core.prune_epsilon.powi(2);
let mut staged = scratch.values.iter();
self.map.retain(|_, slot| {
let &value = staged
.next()
.expect("one staged amplitude per live label, in map order");
*slot = value * scale;
value.norm_sqr() > eps_sq
});
}
self.map.reserve(scratch.inserts.len());
for (label, value) in scratch.inserts.drain(..) {
self.map.insert(label, value * scale);
}
if removals > 0 {
shrink_if_sparse(&mut self.map, rank);
}
Ok(())
}
fn stage_t_rotation(
&self,
core: &Core,
d: &Decomp<K>,
cos: f64,
branch: Complex64,
scratch: &mut RotationScratch<K>,
) -> (usize, Option<f64>) {
let g = branch * d.zeta;
let flip = if d.a.dot_parity(&d.b) { -1.0 } else { 1.0 };
let eps_sq = core.prune_epsilon.powi(2);
let mut removals = 0;
let mut pruned = false;
let mut norm = 0.0;
scratch.values.clear();
scratch.inserts.clear();
scratch.values.reserve(self.map.len());
for (c, &x) in &self.map {
let sign_c = if c.dot_parity(&d.b) { -1.0 } else { 1.0 };
let partner = c.xor(&d.a);
let value = match self.map.get(&partner) {
Some(&y) => cos * x + g * (sign_c * flip) * y,
None => {
let added = g * sign_c * x;
let weight = added.norm_sqr();
if weight > eps_sq {
scratch.inserts.push((partner, added));
norm += weight;
} else {
pruned = true;
}
cos * x
}
};
let weight = value.norm_sqr();
if weight <= eps_sq {
removals += 1;
pruned = true;
} else {
norm += weight;
}
scratch.values.push(value);
}
(removals, pruned.then_some(norm))
}
fn measure_decomposed(
&mut self,
core: &mut Core,
d: &Decomp<K>,
forced: Option<bool>,
) -> Result<MeasureResult, SimError> {
self.measure_decomposed_inner(core, d, forced)
}
#[inline(always)]
fn measure_decomposed_inner(
&mut self,
core: &mut Core,
d: &Decomp<K>,
forced: Option<bool>,
) -> Result<MeasureResult, SimError> {
if d.a.is_zero() {
self.measure_frame_deterministic(core, d, forced)
} else {
self.measure_random(core, d, forced)
}
}
fn measure_frame_deterministic(
&mut self,
core: &mut Core,
d: &Decomp<K>,
forced: Option<bool>,
) -> Result<MeasureResult, SimError> {
debug_assert!(
d.zeta.im.abs() < TOL,
"diagonal observable has non-real phase"
);
let zsign = d.zeta.re;
let p_plus = ((1.0 + self.expectation_of(d)) / 2.0).clamp(0.0, 1.0);
let deterministic = !(TOL..=1.0 - TOL).contains(&p_plus);
let outcome = core.choose(forced, p_plus);
let probability = if outcome { 1.0 - p_plus } else { p_plus };
if forced.is_some() && probability < TOL {
return Err(SimError::PostselectImpossible {
outcome,
probability,
});
}
let keep_plus = !outcome;
let eps_sq = core.prune_epsilon.powi(2);
let keep = |label: &K, amplitude: &Complex64| {
eig_plus(label, &d.b, zsign) == keep_plus && amplitude.norm_sqr() > eps_sq
};
let mut live = 0;
let mut total = 0.0;
for (label, amplitude) in &self.map {
if keep(label, amplitude) {
live += 1;
total += amplitude.norm_sqr();
}
}
if live == 0 {
return Err(SimError::EmptyStateAfterPruning {
epsilon: core.prune_epsilon,
});
}
core.check_cap(live)?;
if live < self.map.len() {
self.map.retain(|label, amplitude| keep(label, amplitude));
shrink_if_sparse(&mut self.map, live);
}
self.rescale(total);
Ok(MeasureResult {
outcome,
probability,
deterministic,
})
}
fn measure_random(
&mut self,
core: &mut Core,
d: &Decomp<K>,
forced: Option<bool>,
) -> Result<MeasureResult, SimError> {
let mut scratch = std::mem::take(&mut self.rotation);
let result = self.measure_random_staged(core, d, forced, &mut scratch);
scratch.clear();
self.rotation = scratch;
result
}
fn measure_random_staged(
&mut self,
core: &mut Core,
d: &Decomp<K>,
forced: Option<bool>,
scratch: &mut RotationScratch<K>,
) -> Result<MeasureResult, SimError> {
let pivot = d.a.first_set_bit().expect("random branch has nonzero a");
let p0 = ((1.0 + self.expectation_paired(d, &mut scratch.partners)) / 2.0).clamp(0.0, 1.0);
let outcome = core.choose(forced, p0);
let probability = if outcome { 1.0 - p0 } else { p0 };
if forced.is_some() && probability < TOL {
return Err(SimError::PostselectImpossible {
outcome,
probability,
});
}
let s = outcome;
let g_phase = (d.phase + 3) & 3;
let gzeta = i_pow(g_phase);
let mut gb = d.b.clone();
gb.flip(pivot);
let projection = Projection {
d,
gb: &gb,
pivot,
s,
ssign: if s { -1.0 } else { 1.0 },
compress: Complex64::new(0.0, PAULI_EXP_SIGN * FRAC_1_SQRT_2) * gzeta,
shift_flip: if gb.dot_parity(&d.a) { -1.0 } else { 1.0 },
};
debug_assert_ne!(
gb.dot_parity(&d.a),
d.b.dot_parity(&d.a),
"the pivot bit of `a` is set, so `gb` and `b` differ in their `a` parity"
);
let (removals, norm) = self.stage_projection(core, &projection, scratch);
self.commit_pair_rewrite(core, scratch, removals, Some(norm))?;
if s {
let z = crate::pauli::pauli_z(core.n, pivot);
frames::right_pauli(&mut core.r, &z);
}
let mut generator = PauliString::new(core.n);
let words = generator.x.len();
generator.x.copy_from_slice(&d.a.as_slice()[..words]);
generator.z.copy_from_slice(&gb.as_slice()[..words]);
generator.set_phase(i32::from(g_phase));
frames::right_pauli_exp(&mut core.r, &generator);
let deterministic = !(TOL..=1.0 - TOL).contains(&p0);
Ok(MeasureResult {
outcome,
probability,
deterministic,
})
}
fn stage_projection(
&self,
core: &Core,
projection: &Projection<'_, K>,
scratch: &mut RotationScratch<K>,
) -> (usize, f64) {
let eps_sq = core.prune_epsilon.powi(2);
let mut removals = 0;
let mut norm = 0.0;
let RotationScratch {
values,
inserts,
partners,
} = scratch;
values.clear();
inserts.clear();
values.reserve(self.map.len());
debug_assert_eq!(partners.len(), self.map.len());
for ((c, &x), partner) in self.map.iter().zip(partners.iter()) {
let value = match *partner {
Some(y) => projection.rewrite_pair(c, x, y).0,
None => {
let (kept, sent) = projection.rewrite_pair(c, x, Complex64::new(0.0, 0.0));
let weight = sent.norm_sqr();
if weight > eps_sq {
inserts.push((c.xor(&projection.d.a), sent));
norm += weight;
}
kept
}
};
let weight = value.norm_sqr();
if weight <= eps_sq {
removals += 1;
} else {
norm += weight;
}
values.push(value);
}
(removals, norm)
}
fn expectation_of(&self, d: &Decomp<K>) -> f64 {
self.expectation_of_inner(d)
}
#[inline(always)]
fn expectation_of_inner(&self, d: &Decomp<K>) -> f64 {
if d.a.is_zero() {
let zsign = d.zeta.re;
self.map
.iter()
.map(|(c, &x)| {
let signed = if c.dot_parity(&d.b) { -zsign } else { zsign };
signed * x.norm_sqr()
})
.sum()
} else {
let mut ev = 0.0;
for (c, &x) in &self.map {
if let Some(&y) = self.map.get(&c.xor(&d.a)) {
let sign = if c.dot_parity(&d.b) { -1.0 } else { 1.0 };
ev += (d.zeta * sign * x * y.conj()).re;
}
}
ev
}
}
fn expectation_paired(&self, d: &Decomp<K>, partners: &mut Vec<Option<Complex64>>) -> f64 {
partners.clear();
partners.reserve(self.map.len());
let mut ev = 0.0;
for (c, &x) in &self.map {
let partner = self.map.get(&c.xor(&d.a)).copied();
if let Some(y) = partner {
let sign = if c.dot_parity(&d.b) { -1.0 } else { 1.0 };
ev += (d.zeta * sign * x * y.conj()).re;
}
partners.push(partner);
}
ev
}
fn rescale(&mut self, total: f64) {
if total > 0.0 {
let scale = total.sqrt().recip();
for v in self.map.values_mut() {
*v *= scale;
}
}
}
}
fn storage_words(words: usize) -> usize {
match words {
0 | 1 => 1,
2 => 2,
3 | 4 => 4,
5..=8 => 8,
_ => words,
}
}
fn max_support(pauli: &PauliString) -> Option<usize> {
pauli
.x
.iter()
.zip(&pauli.z)
.enumerate()
.rev()
.find_map(|(word, (&x, &z))| {
let bits = x | z;
(bits != 0).then(|| word * 64 + 63 - bits.leading_zeros() as usize)
})
}
fn pauli_on_register(pauli: &PauliString, nqubits: usize) -> Result<PauliString, SimError> {
if let Some(index) = max_support(pauli).filter(|&index| index >= nqubits) {
return Err(SimError::QubitIndexOutOfRange {
index,
num_qubits: nqubits,
});
}
let mut out = PauliString::new(nqubits);
let words = out.x.len().min(pauli.x.len());
out.x[..words].copy_from_slice(&pauli.x[..words]);
out.z[..words].copy_from_slice(&pauli.z[..words]);
out.set_phase(pauli.phase_exponent());
Ok(out)
}
fn shrink_if_sparse<K: LabelKey>(map: &mut HashMap<K, Complex64>, live: usize) {
let target = live.max(16);
if map.capacity() > 4 * target {
map.shrink_to(2 * target);
}
}
#[inline]
fn eig_plus<K: LabelKey>(c: &K, b: &K, zsign: f64) -> bool {
let signed = if c.dot_parity(b) { -zsign } else { zsign };
signed > 0.0
}
fn replay_terms<K: LabelKey>(
map: &HashMap<K, Complex64>,
psi0: &[Complex64],
destabs: &[PauliString],
out: &mut [Complex64],
) {
for (c, &) in map {
let mut term = psi0.to_vec();
for (i, d_i) in destabs.iter().enumerate() {
if c.get(i) {
term = apply_pauli_dense(&term, d_i);
}
}
for (dst, t) in out.iter_mut().zip(term) {
*dst += amp * t;
}
}
}
fn apply_pauli_dense(v: &[Complex64], p: &PauliString) -> Vec<Complex64> {
fn index_mask(mask: &[u64]) -> usize {
debug_assert!(
mask[1..].iter().all(|&word| word == 0),
"state-vector reconstruction is unreachable past 64 qubits"
);
mask[0] as usize
}
let zeta = i_pow(p.phase_exponent() as u8);
let amask = index_mask(&p.x);
let bmask = index_mask(&p.z);
let mut out = vec![Complex64::new(0.0, 0.0); v.len()];
for (y, &val) in v.iter().enumerate() {
let sign = if (y & bmask).count_ones() & 1 == 1 {
-1.0
} else {
1.0
};
out[y ^ amask] += zeta * sign * val;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pauli::{neg, pauli_string, pauli_x, pauli_y, pauli_z};
fn assert_same_state(actual: &[Complex64], expected: &[Complex64]) {
let (&a, &b) = actual
.iter()
.zip(expected)
.find(|(_, b)| b.norm_sqr() > TOL)
.expect("state has a nonzero amplitude");
let phase = a / b;
for (&a, &b) in actual.iter().zip(expected) {
assert!((a - phase * b).norm() < TOL, "{actual:?} != {expected:?}");
}
}
#[test]
fn measurement_can_drive_external_control_flow() {
let mut sim = TableauSimulator::with_seed(2, 7);
sim.h(0);
sim.cx(0, 1).expect("distinct qubits");
if sim.measure(0).expect("measurement succeeds").outcome {
sim.x(1);
}
let second = sim.measure(1).expect("measurement succeeds");
assert!(!second.outcome);
assert!(second.deterministic);
}
#[test]
fn pauli_rotation_and_expectation_use_existing_pauli_strings() {
let mut sim = TableauSimulator::with_seed(1, 0);
sim.h(0);
sim.t_pauli(&pauli_z(1, 0), false)
.expect("rotation succeeds");
let x = sim
.peek_observable_expectation(&pauli_string("X").expect("valid Pauli"))
.expect("observable is in range");
assert!((x - FRAC_1_SQRT_2).abs() < TOL);
}
#[test]
fn gates_grow_the_shared_tableau_engine() {
let mut sim = TableauSimulator::with_seed(1, 0);
sim.h(65);
assert_eq!(sim.num_qubits(), 66);
assert_eq!(sim.core.r.nqubits, 66);
assert!((sim.peek_x(65).expect("qubit exists") - 1.0).abs() < TOL);
}
#[test]
fn state_vector_matches_hadamard_and_t() {
let mut sim = TableauSimulator::with_seed(1, 0);
sim.h(0);
sim.t(0).expect("rotation succeeds");
let phase = Complex64::new(FRAC_1_SQRT_2, FRAC_1_SQRT_2);
assert_same_state(
&sim.state_vector(),
&[Complex64::new(FRAC_1_SQRT_2, 0.0), FRAC_1_SQRT_2 * phase],
);
}
#[test]
fn generic_controlled_paulis_match_all_named_axis_pairs() {
type AxisPauli = fn(usize, usize) -> PauliString;
type NamedGate = fn(&mut TableauSimulator, usize, usize) -> Result<(), SimError>;
let cases: [(AxisPauli, AxisPauli, NamedGate); 9] = [
(pauli_x, pauli_x, TableauSimulator::xcx),
(pauli_x, pauli_y, TableauSimulator::xcy),
(pauli_x, pauli_z, TableauSimulator::xcz),
(pauli_y, pauli_x, TableauSimulator::ycx),
(pauli_y, pauli_y, TableauSimulator::ycy),
(pauli_y, pauli_z, TableauSimulator::ycz),
(pauli_z, pauli_x, TableauSimulator::zcx),
(pauli_z, pauli_y, TableauSimulator::zcy),
(pauli_z, pauli_z, TableauSimulator::zcz),
];
for (control, target, named_gate) in cases {
let mut generic = TableauSimulator::with_seed(2, 0);
generic.h(0);
generic.s(0);
generic.h(1);
generic.sqrt_x(1);
let mut named = generic.clone();
generic
.controlled_pauli(&control(2, 0), &target(2, 1))
.expect("axes on distinct qubits commute");
named_gate(&mut named, 0, 1).expect("qubits are distinct");
assert_same_state(&generic.state_vector(), &named.state_vector());
}
}
#[test]
fn ccz_flips_only_the_all_one_amplitude() {
let mut sim = TableauSimulator::with_seed(3, 0);
for q in 0..3 {
sim.h(q);
}
sim.ccz(0, 1, 2).expect("distinct qubits");
let mut expected = vec![Complex64::new(FRAC_1_SQRT_2.powi(3), 0.0); 8];
expected[7] = -expected[7];
assert_same_state(&sim.state_vector(), &expected);
}
#[test]
fn signed_observables_work_and_non_hermitian_inputs_do_not_grow() {
let mut sim = TableauSimulator::with_seed(1, 0);
let measured = sim
.measure_observable(&neg(pauli_z(1, 0)))
.expect("negative Z is Hermitian");
assert!(measured.outcome && measured.deterministic);
let mut invalid = pauli_x(5, 4);
invalid.phase_shift(1);
assert_eq!(
sim.measure_observable(&invalid),
Err(SimError::NonHermitianPauli)
);
assert_eq!(sim.num_qubits(), 1);
}
#[test]
fn postselection_recompresses_to_the_selected_state() {
for outcome in [false, true] {
let mut sim = TableauSimulator::with_seed(1, 0);
sim.h(0);
sim.postselect_z(0, outcome)
.expect("both Hadamard branches are reachable");
let expected = 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(), &expected);
}
}
}