use std::f64::consts::{FRAC_1_SQRT_2, PI};
use std::hash::{BuildHasher, Hasher, RandomState};
use num_complex::Complex64;
use rustc_hash::FxHashMap;
use crate::pauli::measurement_phase_sign;
use crate::pauli::{Pauli, PauliBasis, PauliString};
use crate::random::rand_float;
#[cfg(test)]
use paulimer::CliffordUnitary;
mod batch;
mod error;
mod frame;
mod label;
pub use batch::{BatchOutcome, Gate1Q, Instruction};
pub use error::SimError;
#[cfg(target_arch = "x86_64")]
use frame::has_popcnt;
use frame::{Axis, Frame, PauliWords, RowPauli};
use label::{Key, Label, LabelKey, Width};
#[inline]
fn words(pauli: &PauliString) -> PauliWords<'_> {
PauliWords {
x: pauli.x_words(),
z: pauli.z_words(),
}
}
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: Frame,
rng: u64,
prune_epsilon: f64,
rank_cap: usize,
}
#[derive(Clone, Debug)]
struct Terms<K: LabelKey> {
map: FxHashMap<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 = Frame::identity(num_qubits);
let words = r.words();
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.resize(need);
let new_words = self.core.r.words();
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.num_qubits(),
"frame tracks register"
);
debug_assert_eq!(self.core.words, self.core.r.words(), "widths agree");
}
fn ensure_for(&mut self, pauli: &PauliString) {
if let Some(max) = pauli.max_support() {
self.ensure_qubits(max + 1);
}
}
fn ensure_for_observable(&mut self, pauli: &PauliString) -> Result<(), SimError> {
measurement_phase_sign(pauli).map_err(|_| SimError::NonHermitianPauli)?;
self.ensure_for(pauli);
Ok(())
}
#[cfg(test)]
fn apply_clifford(&mut self, cl: &CliffordUnitary, support: &[usize]) {
if let Some(&max) = support.iter().max() {
self.ensure_qubits(max + 1);
}
self.core.r.left_clifford(cl, support);
}
pub fn h(&mut self, q: usize) {
self.ensure_qubits(q + 1);
self.core.r.left_h(q);
}
pub fn s(&mut self, q: usize) {
self.ensure_qubits(q + 1);
self.core.r.left_s(q);
}
pub fn s_dag(&mut self, q: usize) {
self.ensure_qubits(q + 1);
self.core.r.left_s_dag(q);
}
pub fn x(&mut self, q: usize) {
self.ensure_qubits(q + 1);
self.core.r.left_x(q);
}
pub fn y(&mut self, q: usize) {
self.ensure_qubits(q + 1);
self.core.r.left_y(q);
}
pub fn z(&mut self, q: usize) {
self.ensure_qubits(q + 1);
self.core.r.left_z(q);
}
pub fn sqrt_x(&mut self, q: usize) {
self.ensure_qubits(q + 1);
self.core.r.left_sqrt_x(q);
}
pub fn sqrt_x_dag(&mut self, q: usize) {
self.ensure_qubits(q + 1);
self.core.r.left_sqrt_x_dag(q);
}
pub fn sqrt_y(&mut self, q: usize) {
self.ensure_qubits(q + 1);
self.core.r.left_sqrt_y(q);
}
pub fn sqrt_y_dag(&mut self, q: usize) {
self.ensure_qubits(q + 1);
self.core.r.left_sqrt_y_dag(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);
self.core.r.left_cx(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);
self.core.r.left_cz(a, b);
Ok(())
}
pub fn swap(&mut self, a: usize, b: usize) {
self.ensure_qubits(a.max(b) + 1);
self.core.r.left_swap(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.gate1(Gate1Q::Cxyz, q);
}
pub fn c_zyx(&mut self, q: usize) {
self.gate1(Gate1Q::Czyx, q);
}
pub fn h_xy(&mut self, q: usize) {
self.gate1(Gate1Q::Hxy, q);
}
pub fn h_yz(&mut self, q: usize) {
self.gate1(Gate1Q::Hyz, 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.gate2(PauliBasis::X, PauliBasis::X, control, target)
}
pub fn xcy(&mut self, control: usize, target: usize) -> Result<(), SimError> {
self.gate2(PauliBasis::X, PauliBasis::Y, control, target)
}
pub fn xcz(&mut self, control: usize, target: usize) -> Result<(), SimError> {
self.gate2(PauliBasis::X, PauliBasis::Z, control, target)
}
pub fn ycx(&mut self, control: usize, target: usize) -> Result<(), SimError> {
self.gate2(PauliBasis::Y, PauliBasis::X, control, target)
}
pub fn ycy(&mut self, control: usize, target: usize) -> Result<(), SimError> {
self.gate2(PauliBasis::Y, PauliBasis::Y, control, target)
}
pub fn ycz(&mut self, control: usize, target: usize) -> Result<(), SimError> {
self.gate2(PauliBasis::Y, PauliBasis::Z, control, target)
}
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.gate2(PauliBasis::Z, PauliBasis::Y, control, target)
}
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);
self.core.r.left_pauli(words(p));
}
pub fn controlled_pauli(
&mut self,
control: &PauliString,
target: &PauliString,
) -> Result<(), SimError> {
if !control.commutes_with(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_for(control);
self.ensure_for(target);
self.core
.r
.left_controlled_pauli(words(control), words(target));
Ok(())
}
pub fn t_pauli(&mut self, axis: &PauliString, adjoint: bool) -> Result<(), SimError> {
self.ensure_for_observable(axis)?;
with_terms!(self, |core, terms| {
let d = core.decompose(axis)?;
terms.t_decomposed(core, &d, adjoint)
})
}
pub fn pauli_rotation(
&mut self,
axis: &PauliString,
kernel_angle: f64,
) -> Result<(), SimError> {
if !kernel_angle.is_finite() {
return Err(SimError::InvalidRotationAngle(kernel_angle));
}
self.ensure_for_observable(axis)?;
with_terms!(self, |core, terms| {
let d = core.decompose(axis)?;
terms.rotate_decomposed(core, &d, kernel_angle)
})
}
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),
] {
for site in [a, b, c] {
axis.set(site, Pauli::I);
}
for &site in operands {
axis.set(site, Pauli::Z);
}
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> {
self.ensure_for_observable(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> {
self.ensure_for_observable(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 => self.core.r.left_x(q),
Axis::Y => self.core.r.left_y(q),
Axis::Z => self.core.r.left_z(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<RowPauli> = (0..n).map(|i| self.core.r.image_z(i)).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<RowPauli> = (0..n).map(|i| self.core.r.image_x(i)).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> {
if let Some(index) = p.max_support().filter(|&index| index >= self.n) {
return Err(SimError::QubitIndexOutOfRange {
index,
num_qubits: self.n,
});
}
let negated = measurement_phase_sign(p).map_err(|_| SimError::NonHermitianPauli)?;
let mut a = K::zeros(self.words);
let mut b = K::zeros(self.words);
let phase = (self
.r
.preimage_into(words(p), a.as_mut_slice(), b.as_mut_slice())
+ 2 * u8::from(negated))
& 3;
Ok(Decomp {
a,
b,
phase,
zeta: i_pow(phase),
})
}
fn decompose_basis<K: LabelKey>(&self, axis: Axis, qubit: usize) -> Decomp<K> {
let mut a = K::zeros(self.words);
let mut b = K::zeros(self.words);
let phase = self
.r
.preimage_basis_into(axis, qubit, a.as_mut_slice(), b.as_mut_slice());
Decomp {
a,
b,
phase,
zeta: i_pow(phase),
}
}
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 FxHashMap<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),
}
}
#[cfg(test)]
fn narrow(&mut self) -> &mut Terms<Key<1>> {
match self {
Amps::W1(terms) => terms,
other => panic!("expected a one-word map, got {:?}", other.width()),
}
}
}
impl<K: LabelKey> Terms<K> {
fn unit(words: usize) -> Self {
let mut map = FxHashMap::default();
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> {
let cos = (PI / 8.0).cos();
let sin = (PI / 8.0).sin();
self.rotation_decomposed(
core,
d,
cos,
Complex64::new(0.0, if adjoint { sin } else { -sin }),
)
}
fn rotate_decomposed(
&mut self,
core: &Core,
d: &Decomp<K>,
kernel_angle: f64,
) -> Result<(), SimError> {
self.rotation_decomposed(
core,
d,
kernel_angle.cos(),
Complex64::new(0.0, -kernel_angle.sin()),
)
}
fn rotation_decomposed(
&mut self,
core: &Core,
d: &Decomp<K>,
cos: f64,
branch: Complex64,
) -> Result<(), SimError> {
#[cfg(target_arch = "x86_64")]
if has_popcnt() {
#[allow(unsafe_code)]
return unsafe { self.rotation_decomposed_popcnt(core, d, cos, branch) };
}
self.rotation_decomposed_inner(core, d, cos, branch)
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "popcnt")]
fn rotation_decomposed_popcnt(
&mut self,
core: &Core,
d: &Decomp<K>,
cos: f64,
branch: Complex64,
) -> Result<(), SimError> {
self.rotation_decomposed_inner(core, d, cos, branch)
}
#[inline(always)]
fn rotation_decomposed_inner(
&mut self,
core: &Core,
d: &Decomp<K>,
cos: f64,
branch: Complex64,
) -> Result<(), SimError> {
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 {
core.r.right_pauli_z(pivot);
}
core.r
.right_pauli_exp(d.a.as_slice(), gb.as_slice(), g_phase);
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 {
#[cfg(target_arch = "x86_64")]
if has_popcnt() {
#[allow(unsafe_code)] return unsafe { self.expectation_of_popcnt(d) };
}
self.expectation_of_inner(d)
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "popcnt")]
fn expectation_of_popcnt(&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 shrink_if_sparse<K: LabelKey>(map: &mut FxHashMap<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: &FxHashMap<K, Complex64>,
psi0: &[Complex64],
destabs: &[RowPauli],
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: &RowPauli) -> 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);
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_z};
#[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 controlled_paulis_reject_signed_axes() {
let mut sim = TableauSimulator::with_seed(2, 0);
assert_eq!(
sim.controlled_pauli(&neg(pauli_z(2, 0)), &pauli_x(2, 1)),
Err(SimError::InvalidControlledPauli)
);
assert_eq!(
sim.controlled_pauli(&pauli_z(2, 0), &neg(pauli_x(2, 1))),
Err(SimError::InvalidControlledPauli)
);
}
#[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("X is in range");
assert!((x - FRAC_1_SQRT_2).abs() < TOL);
}
#[test]
fn deterministic_measurement_pruning_error_is_transactional() {
let mut sim = TableauSimulator::with_seed(1, 0);
sim.set_prune_epsilon(0.7);
let words = sim.core.words;
{
let terms = sim.amps.narrow();
terms.map.clear();
terms
.map
.insert(Key::zeros(words), Complex64::new(0.6, 0.0));
terms.map.insert(
Key::mask_from_support(words, [0].into_iter()),
Complex64::new(0.8, 0.0),
);
}
let amps_before = sim.amps.clone();
let diagonal_z = Decomp {
a: Key::<1>::zeros(words),
b: Key::<1>::mask_from_support(words, [0].into_iter()),
phase: 0,
zeta: Complex64::new(1.0, 0.0),
};
let TableauSimulator {
ref mut core,
ref mut amps,
} = sim;
let err = amps
.narrow()
.measure_frame_deterministic(core, &diagonal_z, Some(false))
.expect_err("the retained 0.6 amplitude is below the pruning threshold");
assert_eq!(err, SimError::EmptyStateAfterPruning { epsilon: 0.7 });
assert_eq!(sim.amps, amps_before, "failed projection must not commit");
}
#[test]
fn measure_random_rank_overflow_leaves_state_unchanged() {
let observable = PauliString::single(2, 0, Pauli::Z);
let build = || {
let mut sim = TableauSimulator::with_seed(2, 0);
sim.h(0);
sim.t(0).expect("off-diagonal T stays within the cap");
sim.h(1);
sim.t(1).expect("off-diagonal T stays within the cap");
sim
};
let dry_run = build();
let d = dry_run
.core
.decompose::<Key<1>>(&observable)
.expect("Z is Hermitian");
assert!(
!d.a.is_zero(),
"test must exercise the random-measurement branch"
);
let mut dry_run = dry_run;
dry_run
.postselect_observable(&observable, false)
.expect("+1 outcome is achievable");
let post_rank = dry_run.rank();
assert!(
post_rank >= 2,
"need a post-rank a cap can sit below, got {post_rank}"
);
let mut sim = build();
sim.set_rank_cap(post_rank - 1);
let frame_before = sim.core.r.clone();
let amps_before = sim.amps.clone();
let err = sim
.postselect_observable(&observable, false)
.expect_err("the lowered cap must overflow");
assert_eq!(
err,
SimError::RankOverflow {
rank: post_rank,
cap: post_rank - 1,
}
);
assert_eq!(
sim.core.r, frame_before,
"R must be untouched when finalize errors"
);
assert_eq!(
sim.amps, amps_before,
"amps must be untouched when finalize errors"
);
}
#[test]
fn ccz_rank_overflow_leaves_state_unchanged() {
let mut sim = TableauSimulator::with_seed(2, 0);
sim.set_rank_cap(2);
for q in 0..2 {
sim.h(q);
}
let qubits_before = sim.num_qubits();
let frame_before = sim.core.r.clone();
let amps_before = sim.amps.clone();
assert_eq!(
sim.ccz(0, 1, 2),
Err(SimError::RankOverflow { rank: 4, cap: 2 })
);
assert_eq!(sim.num_qubits(), qubits_before);
assert_eq!(sim.core.r, frame_before);
assert_eq!(sim.amps, amps_before);
}
#[test]
fn peeks_reject_support_outside_live_register() {
let sim = TableauSimulator::with_seed(1, 0);
let out_of_range = Err(SimError::QubitIndexOutOfRange {
index: 1,
num_qubits: 1,
});
assert_eq!(
sim.peek_observable_expectation(&PauliString::single(2, 1, Pauli::Z)),
out_of_range
);
assert_eq!(sim.peek_z(1), out_of_range);
assert_eq!(sim.peek_x(1), out_of_range);
assert_eq!(sim.peek_y(1), out_of_range);
}
#[test]
fn invalid_operands_do_not_mutate_or_grow() {
let mut sim = TableauSimulator::with_seed(1, 0);
sim.x(0);
let frame_before = sim.core.r.clone();
let amps_before = sim.amps.clone();
assert_eq!(sim.cx(5, 5), Err(SimError::RepeatedQubit(5)));
assert_eq!(sim.cz(6, 6), Err(SimError::RepeatedQubit(6)));
let control = PauliString::single(9, 8, Pauli::X);
let target = PauliString::single(9, 8, Pauli::Z);
assert_eq!(
sim.controlled_pauli(&control, &target),
Err(SimError::NonCommutingControlledPaulis)
);
assert_eq!(sim.num_qubits(), 1);
assert_eq!(sim.core.r, frame_before);
assert_eq!(sim.amps, amps_before);
}
#[test]
fn width_class_tracks_the_register() {
for (n, want) in [
(1usize, Width::W1),
(64, Width::W1),
(65, Width::W2),
(128, Width::W2),
(129, Width::W4),
(256, Width::W4),
(257, Width::W8),
(512, Width::W8),
(513, Width::Wide),
] {
let sim = TableauSimulator::with_seed(n, 0);
assert_eq!(sim.amps.width(), want, "n = {n}");
assert_eq!(sim.core.words, sim.core.r.words(), "n = {n}");
}
}
#[test]
#[should_panic(expected = "fits a usize")]
fn state_vector_refuses_a_register_it_cannot_index() {
let _ = TableauSimulator::with_seed(64, 0).state_vector();
}
}