diff --git a/Cargo.lock b/Cargo.lock index 84ea97efc..5fd7902f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1461,6 +1461,14 @@ dependencies = [ "rayon", ] +[[package]] +name = "ppvm-traits-2" +version = "0.1.0" +dependencies = [ + "num", + "rand 0.10.1", +] + [[package]] name = "ppvm-tui" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 35cb8102a..92cc72838 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ members = [ "crates/vihaco-circuit-isa", "crates/ppvm-vihaco", "crates/ppvm-cli", + "crates/ppvm-traits-2", # Runnable copies of the Rust code blocks in skills/ppvm-usage/SKILL.md. # Built by `cargo build --workspace --all-targets` in CI so the skill # can't silently drift away from the public API. diff --git a/crates/ppvm-traits-2/Cargo.toml b/crates/ppvm-traits-2/Cargo.toml new file mode 100644 index 000000000..e00b311bd --- /dev/null +++ b/crates/ppvm-traits-2/Cargo.toml @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2026 The PPVM Authors +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "ppvm-traits-2" +version = "0.1.0" +edition = "2024" + +[dependencies] +num = "0.4" +rand = "0.10" diff --git a/crates/ppvm-traits-2/src/algebra.rs b/crates/ppvm-traits-2/src/algebra.rs new file mode 100644 index 000000000..a61bf21cd --- /dev/null +++ b/crates/ppvm-traits-2/src/algebra.rs @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Algebra capabilities: key products, coefficient conjugation, and imaginary units. +//! [`Phase`] carries the residual fourth root of unity emitted by a key product. + +use crate::arithmetic::Coefficient; + +/// A fourth root of unity `iᵏ`, with `k` modulo four. +/// [`KeyProduct::key_mul`] returns this residual phase for the coefficient to absorb. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Phase { + /// `i⁰ = +1`. + Pos1, + /// `i¹ = +i`. + PosI, + /// `i² = −1`. + Neg1, + /// `i³ = −i`. + NegI, +} + +impl Phase { + /// The exponent `k ∈ {0, 1, 2, 3}` such that this phase equals `iᵏ`. + #[inline] + pub fn exponent(self) -> u8 { + self as u8 + } + + /// The phase `iᵏ` for exponent `k` (taken mod 4). + #[inline] + pub fn from_exponent(k: u8) -> Self { + match k & 3 { + 0 => Phase::Pos1, + 1 => Phase::PosI, + 2 => Phase::Neg1, + 3 => Phase::NegI, + _ => unreachable!("masking with 3 produces an exponent in 0..=3"), + } + } + + /// The identity phase: `p.compose(Self::one()) == p`. + #[inline] + pub fn one() -> Self { + Phase::Pos1 + } + + /// Compose phases by adding exponents modulo four: `iᵃ · iᵇ = i^{a+b}`. + /// Composition is commutative and can accumulate key-product phases before application. + #[inline] + pub fn compose(self, other: Self) -> Self { + Phase::from_exponent(self.exponent() + other.exponent()) + } + + /// The group inverse `i^{-k} = i^{4-k}`, i.e. the phase `q` with + /// `self.compose(q) == Phase::one()`. + #[inline] + pub fn inverse(self) -> Self { + // 4 - k is exact for k ∈ {0,1,2,3}; the mod-4 reduction in + // `from_exponent` sends k = 0 back to 0. + Phase::from_exponent(4 - self.exponent()) + } + + /// Apply `iᵏ` to a coefficient through [`ImaginaryUnit::mul_i_pow`]. + /// Overrides can preserve symbolic representations; complex multiplication by `i` + /// uses component swaps to preserve signed zeros and avoid non-finite contamination. + #[inline] + pub fn apply(self, c: &C) -> C { + c.mul_i_pow(self.exponent()) + } +} + +/// `iᵃ · iᵇ = i^{a+b}` — [`compose`](Phase::compose) as the `*` operator, so a +/// residual-phase accumulator reads `acc *= phase` / `acc = a * b`. +impl core::ops::Mul for Phase { + type Output = Phase; + + #[inline] + fn mul(self, rhs: Phase) -> Phase { + self.compose(rhs) + } +} + +impl core::ops::MulAssign for Phase { + #[inline] + fn mul_assign(&mut self, rhs: Phase) { + *self = self.compose(rhs); + } +} + +/// A key product returning a key and residual phase `i^{β(u,v)}`. +/// Key multiplication must be associative, and phases must satisfy the cocycle law: +/// `β(u,v) + β(u·v,w) == β(v,w) + β(u,v·w)` modulo four. +pub trait KeyProduct: Eq + Clone { + /// Product of two keys, with the phase it produces (folded onto the coeff). + fn key_mul(&self, other: &Self) -> (Self, Phase); +} + +/// A commutative coefficient ring with an imaginary unit. +/// Implementations must satisfy `Self::imaginary_unit() * Self::imaginary_unit() +/// == -Self::one()`, hence `i⁴ = 1`. +pub trait ImaginaryUnit: Coefficient + num::One { + /// The imaginary unit `i`; impls must satisfy + /// `Self::imaginary_unit() * Self::imaginary_unit() == -Self::one()`. + fn imaginary_unit() -> Self; + + /// Multiply by `i`; the default uses ring multiplication. + /// Complex values override this with `(re, im) ↦ (-im, re)` to preserve signed + /// zeros and avoid contaminating both components when one is non-finite. + #[inline] + fn mul_i(&self) -> Self { + self.clone() * Self::imaginary_unit() + } + + /// Multiply by `iᵏ`, with `k` modulo four; used by [`Phase::apply`]. + /// Overrides may fold phases into symbolic data, including when `k == 0`. + /// The result must denote `iᵏ · self`, with `mul_i_pow(1) == mul_i()`. + #[inline] + fn mul_i_pow(&self, k: u8) -> Self { + match k & 3 { + 0 => self.clone(), + 1 => self.mul_i(), + 2 => -(self.clone()), + 3 => -(self.mul_i()), + _ => unreachable!("masking with 3 produces an exponent in 0..=3"), + } + } +} + +/// A commutative ring involution used by [`crate::containers::Pair::hermitian_overlap`]. +/// Requires `conj(conj(a)) == a`, preservation of addition and multiplication, +/// and `conj(i) == -i` when the ring also implements [`ImaginaryUnit`]. +pub trait Conjugate: Coefficient { + /// The ring involution applied to this value. + fn conj(&self) -> Self; +} + +impl ImaginaryUnit for num::Complex { + #[inline] + fn imaginary_unit() -> Self { + num::Complex::new(0.0, 1.0) + } + + /// The old `ComplexCoefficient::mul_phase(1)` component swap, verbatim + /// (`crates/ppvm-traits/src/traits/coefficient.rs`): total on non-finite + /// components and sign-of-zero exact, unlike the generic `self * i`. + #[inline] + fn mul_i(&self) -> Self { + num::Complex::new(-self.im, self.re) + } +} + +impl Conjugate for num::Complex { + #[inline] + fn conj(&self) -> Self { + num::Complex::conj(self) + } +} + +impl Conjugate for f64 { + /// Conjugation is the identity on a real ring. + #[inline] + fn conj(&self) -> Self { + *self + } +} diff --git a/crates/ppvm-traits-2/src/arithmetic/angle.rs b/crates/ppvm-traits-2/src/arithmetic/angle.rs new file mode 100644 index 000000000..8d8982872 --- /dev/null +++ b/crates/ppvm-traits-2/src/arithmetic/angle.rs @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +use crate::arithmetic::Coefficient; + +// A rotation angle that yields `(sin, cos)` already in coefficient domain `C`. +pub trait Angle { + /// Return `(sin θ, cos θ)` in the coefficient domain `C`. + fn sin_cos(&self) -> (C, C); +} + +impl Angle for f64 { + #[inline] + fn sin_cos(&self) -> (f64, f64) { + num::traits::Float::sin_cos(*self) + } +} + +/// The complex-coefficient angle domain, i.e. the defaulted `A = C` case of +/// [`crate::gates::RotationOne`] at `C = Complex`. +impl Angle> for num::Complex { + #[inline] + fn sin_cos(&self) -> (num::Complex, num::Complex) { + let (s, c) = num::traits::Float::sin_cos(self.re); + (num::Complex::new(s, 0.0), num::Complex::new(c, 0.0)) + } +} + +// A **real** angle driving a complex-coefficient sum. +impl Angle> for f64 { + #[inline] + fn sin_cos(&self) -> (num::Complex, num::Complex) { + let (s, c) = num::traits::Float::sin_cos(*self); + (num::Complex::new(s, 0.0), num::Complex::new(c, 0.0)) + } +} diff --git a/crates/ppvm-traits-2/src/arithmetic/coefficient.rs b/crates/ppvm-traits-2/src/arithmetic/coefficient.rs new file mode 100644 index 000000000..8d012b7b4 --- /dev/null +++ b/crates/ppvm-traits-2/src/arithmetic/coefficient.rs @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub}; + +pub trait Coefficient: + PartialEq + + Clone + + num::Zero + + Neg + + Add + + Sub + + Mul + + AddAssign + + for<'a> AddAssign<&'a Self> + + MulAssign + + for<'a> MulAssign<&'a Self> + + std::iter::Sum + + Send + + Sync +{ + /// Multiply by `sign ∈ {-1, +1}` (encoded as `i8`). + fn mul_sign(&self, sign: i8) -> Self; + + /// Multiply this coefficient in place by `sign ∈ {-1, +1}`. + #[inline] + fn mul_sign_assign(&mut self, sign: i8) { + *self = self.mul_sign(sign) + } + + /// Add this coefficient to itself. Numeric implementations may use their + /// native multiply-by-two operation; exact rings retain the additive default. + #[inline(always)] + fn doubled(&self) -> Self { + let mut result = self.clone(); + result += self; + result + } + + /// Nonnegative magnitude. Exposes a property of the value for a `Policy` to + /// threshold; it does not itself decide any cutoff. Replaces the old + /// `Coefficient::cutoff`. + fn magnitude(&self) -> f64; +} + +impl Coefficient for f64 { + #[inline] + fn mul_sign(&self, sign: i8) -> Self { + (sign as f64) * (*self) + } + + #[inline(always)] + fn doubled(&self) -> Self { + *self * 2.0 + } + + #[inline] + fn magnitude(&self) -> f64 { + self.abs() + } +} + +impl Coefficient for num::Complex { + #[inline] + fn mul_sign(&self, sign: i8) -> Self { + (sign as f64) * (*self) + } + + #[inline(always)] + fn doubled(&self) -> Self { + *self * 2.0 + } + + #[inline] + fn magnitude(&self) -> f64 { + self.norm() + } +} diff --git a/crates/ppvm-traits-2/src/arithmetic/halvable.rs b/crates/ppvm-traits-2/src/arithmetic/halvable.rs new file mode 100644 index 000000000..743268206 --- /dev/null +++ b/crates/ppvm-traits-2/src/arithmetic/halvable.rs @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +use crate::arithmetic::Coefficient; + +/// A coefficient ring in which halving (`0.5·x`) is total and exact: the +/// capability the projective computational-basis measurement kernel needs to +/// apply the `(I ± Z)/2` projectors. +pub trait Halvable: Coefficient { + /// Divide by two. Impls must be exact: `x.half() + x.half() == x` for every + /// finite `x` (`f64::NAN` and the infinities are exempt — they are not + /// ring elements). + fn half(&self) -> Self; +} + +impl Halvable for f64 { + #[inline] + fn half(&self) -> Self { + *self / 2.0 + } +} + +impl Halvable for num::Complex { + #[inline] + fn half(&self) -> Self { + *self / 2.0 + } +} diff --git a/crates/ppvm-traits-2/src/arithmetic/mod.rs b/crates/ppvm-traits-2/src/arithmetic/mod.rs new file mode 100644 index 000000000..26f59b082 --- /dev/null +++ b/crates/ppvm-traits-2/src/arithmetic/mod.rs @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Scalar arithmetic and rotation-angle capabilities. + +mod angle; +mod coefficient; +mod halvable; + +pub use angle::Angle; +pub use coefficient::Coefficient; +pub use halvable::Halvable; diff --git a/crates/ppvm-traits-2/src/containers/batch.rs b/crates/ppvm-traits-2/src/containers/batch.rs new file mode 100644 index 000000000..663e463b1 --- /dev/null +++ b/crates/ppvm-traits-2/src/containers/batch.rs @@ -0,0 +1,377 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +use crate::containers::Indexable; +use crate::loss::LossState; +use crate::word::PauliBits; + +/// An [`Indexable`] key with a structure-of-arrays column representation. +/// Hashing and column layout remain separate capabilities. +pub trait Columnar: Indexable { + /// The concrete structure-of-arrays column for this key type. + type Column: KeyColumn; +} + +/// Read and value-producing operations on a structure-of-arrays key column, +/// generic over the key type. Pauli/loss row accessors live in [`PauliColumn`] +/// and [`LossColumn`]; construction and mutation in [`KeyColumnMut`]. +pub trait KeyColumn: Default + Clone { + /// The key type this column stores. + type Key: Columnar; + + /// Number of keys currently in the column. + fn len(&self) -> usize; + + /// Number of keys the existing plane allocations can hold without growing. + fn capacity(&self) -> usize; + + /// Whether the column is empty. + fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Bulk structural hash of the whole column into a parallel hash column. + /// `out[i]` must equal the `i`-th key's [`Indexable::key_hash`]. + fn hash_into(&self, out: &mut [u64]); + + /// Join confirm: compare element `i` against a build-side key after a hash + /// or tag match, without materializing the whole element. + fn key_eq(&self, i: usize, other: &Self::Key) -> bool; + + /// Select or permute elements into a new column (radix partitioning, + /// compaction, device staging). Operates plane by plane, never scalar. + fn gather(&self, indices: &[u32]) -> Self; + + /// Scalar materialization of one element — a naive backend's fallback, + /// never the hot path. + fn get(&self, i: usize) -> Self::Key; +} + +/// Row-level Pauli bit access for columns whose keys implement [`PauliBits`]. +/// Defaults materialize the row via [`KeyColumn::get`]; packed columns override +/// them to address bit planes directly, which is why there is no blanket impl. +pub trait PauliColumn: KeyColumn +where + Self::Key: PauliBits, +{ + /// Read one row's X bit. + #[inline] + fn x_bit(&self, row: usize, qubit: usize) -> bool { + self.get(row).x_bit(qubit) + } + + /// Read one row's Z bit. + #[inline] + fn z_bit(&self, row: usize, qubit: usize) -> bool { + self.get(row).z_bit(qubit) + } + + /// Materialize one row while toggling selected bits at one site. + #[inline] + fn toggled_bits(&self, row: usize, qubit: usize, toggle_x: bool, toggle_z: bool) -> Self::Key { + self.get(row).toggled_bits(qubit, toggle_x, toggle_z) + } + + /// Materialize one row while toggling two sites with `[toggle_x, toggle_z]` masks. + #[inline] + fn toggled_bits2( + &self, + row: usize, + i: usize, + toggle_i: [bool; 2], + j: usize, + toggle_j: [bool; 2], + ) -> Self::Key { + self.get(row).toggled_bits2(i, toggle_i, j, toggle_j) + } +} + +/// Row-level loss access for columns whose keys implement [`LossState`]. +/// Defaults materialize the row; packed columns override. No blanket impl, for +/// the same reason as [`PauliColumn`]. +pub trait LossColumn: KeyColumn +where + Self::Key: LossState, +{ + /// Read one row's loss bit. + #[inline] + fn is_lost(&self, row: usize, qubit: usize) -> bool { + self.get(row).is_lost(qubit) + } +} + +/// Construction and mutation of a [`KeyColumn`]. +pub trait KeyColumnMut: KeyColumn { + /// Create an empty column with capacity for `n` keys. + fn with_capacity(n: usize) -> Self; + + /// Append one key while keeping each plane contiguous. + fn push(&mut self, key: Self::Key); + + /// Reserve room for `additional` keys while retaining existing entries. + /// The default is a no-op for backends without reservation support. + #[inline] + fn reserve(&mut self, _additional: usize) {} + + /// Clear the column while retaining its backing allocations. + fn clear(&mut self); + + /// Overwrite element `i`, preserving other elements and backing allocations. + /// Panics (or debug-panics) if `i >= len()`. + fn set(&mut self, i: usize, key: Self::Key); + + /// Shorten to `len` elements, keeping the backing allocation. A no-op if + /// `len >= self.len()`. The tail step of a stable retain compaction + /// (`set` the survivors down, then cut). + fn truncate(&mut self, len: usize); + + /// Remove row `i` by moving the final row into its slot. + fn swap_remove(&mut self, i: usize) -> Self::Key { + let len = self.len(); + let removed = self.get(i); + if i + 1 != len { + self.set(i, self.get(len - 1)); + } + self.truncate(len - 1); + removed + } +} + +/// Keys and cached structural hashes in parallel columns, without coefficients. +/// Uses `Vec` so keys need not implement [`Columnar`]. +/// Mutations invalidate hashes; [`Self::hashes`] exposes cache validity. +#[derive(Debug, Clone)] +pub struct KeyBatch { + keys: Vec, + hashes: Vec, + hashes_valid: bool, +} + +impl Default for KeyBatch { + fn default() -> Self { + Self { + keys: Vec::new(), + hashes: Vec::new(), + hashes_valid: false, + } + } +} + +impl KeyBatch { + /// An empty key batch. + pub fn new() -> Self { + Self::default() + } + + /// A key batch pre-sized for `n` keys. + pub fn with_capacity(n: usize) -> Self { + Self { + keys: Vec::with_capacity(n), + hashes: Vec::with_capacity(n), + hashes_valid: false, + } + } + + /// Number of keys in the batch. + pub fn len(&self) -> usize { + self.keys.len() + } + + /// Number of keys the existing columns can hold without growing. + pub fn capacity(&self) -> usize { + self.keys.capacity().min(self.hashes.capacity()) + } + + /// Whether the batch is empty. + pub fn is_empty(&self) -> bool { + self.keys.is_empty() + } + + /// The key column. + pub fn keys(&self) -> &[W] { + &self.keys + } + + /// The complete parallel hash column, or `None` if it has not been filled + /// since the last mutation. A filled empty batch returns `Some(&[])`. + pub fn hashes(&self) -> Option<&[u64]> { + self.hashes_valid.then_some(self.hashes.as_slice()) + } + + /// Iterate keys in insertion order. + pub fn iter(&self) -> impl Iterator { + self.keys.iter() + } + + /// Clear both columns without releasing capacity (for buffer reuse). + pub fn clear(&mut self) { + self.hashes_valid = false; + self.keys.clear(); + self.hashes.clear(); + } + + /// Append a key and invalidate cached hashes without releasing capacity. + /// Call [`KeyBatch::fill_hashes`] to make the full hash column available again. + pub fn push(&mut self, key: W) { + self.hashes_valid = false; + self.hashes.clear(); + self.keys.push(key); + } +} + +impl KeyBatch { + /// Fill the parallel hash column from each key's [`Indexable::key_hash`], so + /// `hashes().unwrap()[i] == keys()[i].key_hash()`. + /// The cache becomes available only after all keys have been hashed. + pub fn fill_hashes(&mut self) { + self.hashes_valid = false; + self.hashes.clear(); + self.hashes + .extend(self.keys.iter().map(Indexable::key_hash)); + self.hashes_valid = true; + } +} + +/// A [`KeyBatch`] plus a separate coefficient column, ready for accumulation. +#[derive(Debug, Clone)] +pub struct TermBatch { + keys: KeyBatch, + coeffs: Vec, +} + +impl Default for TermBatch { + fn default() -> Self { + Self { + keys: KeyBatch::new(), + coeffs: Vec::new(), + } + } +} + +impl TermBatch { + /// An empty term batch. + pub fn new() -> Self { + Self::default() + } + + /// A term batch pre-sized for `n` terms. + pub fn with_capacity(n: usize) -> Self { + Self { + keys: KeyBatch::with_capacity(n), + coeffs: Vec::with_capacity(n), + } + } + + /// Number of terms in the batch. + pub fn len(&self) -> usize { + self.coeffs.len() + } + + /// Number of terms all three columns can hold without growing. + pub fn capacity(&self) -> usize { + self.keys.capacity().min(self.coeffs.capacity()) + } + + /// Whether the batch is empty. + pub fn is_empty(&self) -> bool { + self.coeffs.is_empty() + } + + /// The probe-side key batch. + pub fn keys(&self) -> &KeyBatch { + &self.keys + } + + /// The coefficient column. + pub fn coeffs(&self) -> &[C] { + &self.coeffs + } + + /// Iterate `(key, coeff)` pairs — the read side an `accumulate_batch` merge + /// loop consumes. Synthesizes the pairs from the two columns; the layout + /// stays structure-of-arrays. + pub fn iter(&self) -> impl Iterator { + self.keys.keys().iter().zip(self.coeffs.iter()) + } + + /// Clear all columns without releasing capacity (for buffer reuse). + pub fn clear(&mut self) { + self.keys.clear(); + self.coeffs.clear(); + } +} + +/// Append produced key/coefficient pairs to a sink. +/// Backends may collect scalar pairs or append to separate columns. +pub trait TermSink { + /// Append one produced term. + fn push(&mut self, key: K, coeff: C); +} + +impl TermSink for TermBatch { + #[inline] + fn push(&mut self, key: W, coeff: C) { + self.keys.push(key); + self.coeffs.push(coeff); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Clone, PartialEq, Eq)] + struct Key(u64); + + impl std::hash::Hash for Key { + fn hash(&self, state: &mut H) { + state.write_u64(self.key_hash()); + } + } + + impl Indexable for Key { + fn key_hash(&self) -> u64 { + self.0.wrapping_mul(0x9E37_79B9_7F4A_7C15) + } + } + + #[test] + fn push_invalidates_hashes_and_refill_covers_every_key() { + let mut batch = KeyBatch::with_capacity(4); + batch.push(Key(1)); + assert_eq!(batch.hashes(), None); + batch.fill_hashes(); + assert_eq!(batch.hashes(), Some([Key(1).key_hash()].as_slice())); + let capacity = batch.capacity(); + let snapshot = batch.clone(); + + batch.push(Key(2)); + assert_eq!(batch.hashes(), None); + assert_eq!(batch.capacity(), capacity); + assert_eq!(snapshot.hashes(), Some([Key(1).key_hash()].as_slice())); + + batch.fill_hashes(); + assert_eq!( + batch.hashes(), + Some([Key(1).key_hash(), Key(2).key_hash()].as_slice()), + ); + } + + #[test] + fn empty_and_cleared_batches_have_explicit_cache_state() { + let mut batch = KeyBatch::::new(); + assert_eq!(batch.hashes(), None); + batch.fill_hashes(); + assert_eq!(batch.hashes(), Some([].as_slice())); + + batch.push(Key(3)); + batch.fill_hashes(); + let capacity = batch.capacity(); + batch.clear(); + assert!(batch.is_empty()); + assert_eq!(batch.hashes(), None); + assert_eq!(batch.capacity(), capacity); + batch.fill_hashes(); + assert_eq!(batch.hashes(), Some([].as_slice())); + } +} diff --git a/crates/ppvm-traits-2/src/containers/coordinate_list.rs b/crates/ppvm-traits-2/src/containers/coordinate_list.rs new file mode 100644 index 000000000..675353dbd --- /dev/null +++ b/crates/ppvm-traits-2/src/containers/coordinate_list.rs @@ -0,0 +1,199 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! An unsorted `Vec<(K, C)>` backend using linear scans and `K: Eq + Clone`. +//! Suitable for small supports; no hashing is required. + +use crate::algebra::{ImaginaryUnit, KeyProduct}; +use crate::arithmetic::Coefficient; +use crate::containers::{Accumulate, Multiply, Pair, Retain, Scale, Support, TermBatch}; + +impl Support for Vec<(K, C)> +where + K: Eq + Clone, + C: Coefficient, +{ + type Key = K; + type Coeff = C; + + #[inline] + fn len(&self) -> usize { + self.as_slice().len() + } + + #[inline] + fn get(&self, key: &K) -> Option { + self.as_slice() + .iter() + .find(|(k, _)| k == key) + .map(|(_, c)| c.clone()) + } + + #[inline] + fn iter(&self) -> impl Iterator { + self.as_slice().iter().map(|(k, c)| (k.clone(), c.clone())) + } + + /// The borrowing scan: no clone at all, so a reader that rejects most terms + /// pays nothing for the ones it rejects. + #[inline] + fn for_each_ref(&self, mut f: impl FnMut(&K, &C)) { + for (k, c) in self.as_slice() { + f(k, c); + } + } +} + +impl Accumulate for Vec<(K, C)> +where + K: Eq + Clone, + C: Coefficient, +{ + /// Linear-scan hash-join: for each produced term, find the matching key and + /// add onto it, else append. `O(n·m)` in the support size, which is the + /// right cost model for the small support this backend targets. + #[inline] + fn accumulate_batch(&mut self, terms: &TermBatch) { + for (k, c) in terms.iter() { + if let Some(slot) = self.iter_mut().find(|(ek, _)| ek == k) { + slot.1 += c; + } else { + self.push((k.clone(), c.clone())); + } + } + } + + /// Drop every zero-coefficient term (`reduce_structural`): canonicalize to + /// reduced finite support. Runs only at finalize, never inline. + #[inline] + fn reduce(&mut self) { + self.retain(|(_, v)| !v.is_zero()); + } +} + +impl Scale for Vec<(K, C)> +where + K: Eq + Clone, + C: Coefficient, +{ + #[inline] + fn scale(&mut self, s: &C) { + for (_, v) in self.iter_mut() { + *v *= s; + } + } +} + +/// Takes every [`Pair`] default. Each probe is a linear [`Support::get`] scan, +/// so pairing costs `O(|self|·|other|)` — fine for the small supports this +/// backend targets. +impl Pair for Vec<(K, C)> +where + K: Eq + Clone, + C: Coefficient, +{ +} + +impl Retain for Vec<(K, C)> +where + K: Eq + Clone, + C: Coefficient, +{ + #[inline] + fn retain(&mut self, keep: impl Fn(&K, &C) -> bool) { + // Inherent `Vec::retain` shadows the trait method (inherent-first + // resolution), so this does not recurse. + self.retain(|(k, v)| keep(k, v)); + } +} + +impl Multiply for Vec<(K, C)> +where + K: KeyProduct, + C: ImaginaryUnit, +{ + /// Accumulate `(A·B)[k] = Σ_{p·q=k} A[p] B[q] i^{β(p,q)}` into `acc`. + /// No reduction or truncation runs; exact-zero entries remain until explicit reduction. + fn multiply_into(&self, other: &Self, acc: &mut Self) { + for (p, a) in self.as_slice() { + for (q, b) in other.as_slice() { + let (k, phase) = p.key_mul(q); + let mut product = a.clone(); + product *= b; + let c = phase.apply(&product); + if let Some(slot) = acc.iter_mut().find(|(ek, _)| *ek == k) { + slot.1 += c; + } else { + acc.push((k, c)); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::containers::TermSink; + + fn batch(terms: &[(&str, f64)]) -> TermBatch { + let mut b = TermBatch::with_capacity(terms.len()); + for (k, c) in terms { + b.push((*k).to_string(), *c); + } + b + } + + #[test] + fn vec_accumulate_combines_keys() { + let mut v: Vec<(String, f64)> = Vec::new(); + v.accumulate_batch(&batch(&[("a", 1.0), ("b", 2.0), ("a", 3.0)])); + assert_eq!(Support::get(&v, &"a".to_string()), Some(4.0)); + assert_eq!(Support::get(&v, &"b".to_string()), Some(2.0)); + assert_eq!(Support::len(&v), 2); + } + + #[test] + fn vec_reduce_drops_zero() { + let mut v: Vec<(String, f64)> = Vec::new(); + v.accumulate_batch(&batch(&[("a", 1.0), ("a", -1.0), ("b", 2.0)])); + v.reduce(); + assert_eq!(Support::len(&v), 1); + assert_eq!(Support::get(&v, &"b".to_string()), Some(2.0)); + } + + #[test] + fn vec_scale_and_overlap() { + let mut a: Vec<(String, f64)> = Vec::new(); + a.accumulate_batch(&batch(&[("x", 2.0), ("y", 3.0)])); + let mut b: Vec<(String, f64)> = Vec::new(); + b.accumulate_batch(&batch(&[("x", 5.0), ("z", 7.0)])); + a.scale(&2.0); + // overlap = (2*2)*5 = 20; y and z do not match. + assert_eq!(Pair::overlap(&a, &b), 20.0); + } + + #[test] + fn for_each_ref_agrees_with_iter() { + let terms = batch(&[("a", 1.0), ("b", 2.0), ("c", -3.0), ("a", 0.5)]); + + let mut v: Vec<(String, f64)> = Vec::new(); + v.accumulate_batch(&terms); + let mut seen: Vec<(String, f64)> = Vec::new(); + v.for_each_ref(|k, c| seen.push((k.clone(), *c))); + seen.sort_by(|a, b| a.0.cmp(&b.0)); + let mut want: Vec<(String, f64)> = Support::iter(&v).collect(); + want.sort_by(|a, b| a.0.cmp(&b.0)); + assert_eq!(seen, want); + assert_eq!(seen.len(), 3); + } + + #[test] + fn vec_retain_filters() { + let mut v: Vec<(String, f64)> = Vec::new(); + v.accumulate_batch(&batch(&[("keep", 2.0), ("drop", 0.5)])); + Retain::retain(&mut v, |_, c| *c >= 1.0); + assert_eq!(Support::len(&v), 1); + assert_eq!(Support::get(&v, &"keep".to_string()), Some(2.0)); + } +} diff --git a/crates/ppvm-traits-2/src/containers/graded.rs b/crates/ppvm-traits-2/src/containers/graded.rs new file mode 100644 index 000000000..6b939eae5 --- /dev/null +++ b/crates/ppvm-traits-2/src/containers/graded.rs @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Container algebra over `C[K]`: support, accumulation, scaling, pairing, and products. +//! Keys require `Eq + Clone`; hashing and Pauli capabilities are backend-specific. +//! [`Retain`] is separate because truncation can break algebraic exactness. + +use num::Zero; + +use crate::algebra::{Conjugate, ImaginaryUnit, KeyProduct}; +use crate::arithmetic::Coefficient; +use crate::containers::{KeyBatch, TermBatch, TermSink}; + +/// A finitely supported map from keys to coefficients. +/// Read-only export avoids requiring mutable pair slots in columnar backends. +pub trait Support { + /// The key type — minimal `Eq + Clone`; hash backends add `Indexable`. + type Key: Eq + Clone; + /// The coefficient ring. + type Coeff: Coefficient; + + /// Number of terms in the (reduced) support. + fn len(&self) -> usize; + + /// Whether the support is empty. + fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// The coefficient at `key`, if present. + fn get(&self, key: &Self::Key) -> Option; + + /// Read-only export of the support as `(key, coeff)` pairs. A SoA backend + /// synthesizes the pairs from its columns. + fn iter(&self) -> impl Iterator; + + /// Visit each supported key and coefficient by reference, in backend order. + /// The default materializes owned pairs through [`Self::iter`]; stored backends + /// should override it to avoid cloning coefficients before filtering. + fn for_each_ref(&self, mut f: impl FnMut(&Self::Key, &Self::Coeff)) { + for (k, c) in self.iter() { + f(&k, &c); + } + } +} + +/// Accumulate linear combinations and explicitly reduce exact-zero coefficients. +pub trait Accumulate: Support { + /// Merge a batch, adding to existing keys or inserting new ones. + /// The algebraic multiset contract permits reordering and partitioning; + /// floating-point accumulation can still differ through rounding. + fn accumulate_batch(&mut self, terms: &TermBatch); + + /// Canonicalize to reduced finite-support form: drop every key whose + /// coefficient `is_zero()`. First-class and run **only** at finalize — never + /// inline during accumulation. + fn reduce(&mut self); + + /// Accumulate one term through a singleton batch. + fn accumulate(&mut self, key: Self::Key, coeff: Self::Coeff) { + let mut batch = TermBatch::with_capacity(1); + batch.push(key, coeff); + self.accumulate_batch(&batch); + } +} + +/// Scale coefficients by a scalar without changing their keys. +pub trait Scale: Support { + /// Multiply every coefficient by `s`: `∀ k. c_k *= s`. + fn scale(&mut self, s: &Self::Coeff); +} + +/// Pair supports: `overlap` computes `Σ a_k b_k` without conjugation. +/// `hermitian_overlap` computes `Σ conj(a_k) b_k` and requires [`Conjugate`]. +/// For Pauli coefficients, the bilinear pairing is the normalized trace `Tr(A B)/2ⁿ`. +pub trait Pair: Support { + /// Read-only probe of a key column: `out[i]` is the coefficient at + /// `keys[i]`, or `None` on a miss. + fn probe_batch(&self, keys: &KeyBatch, out: &mut [Option]) { + debug_assert!(out.len() >= keys.keys().len()); + for (slot, k) in out.iter_mut().zip(keys.keys().iter()) { + *slot = self.get(k); + } + } + + /// The symmetric bilinear trace pairing `∑_k a_k b_k`. + fn overlap(&self, other: &Self) -> Self::Coeff { + fold_common(self, other, |a, b| { + let mut product = a.clone(); + product *= b; + product + }) + } + + /// The sesquilinear inner product `∑_k conj(a_k)·b_k`, conjugating `self`. + fn hermitian_overlap(&self, other: &Self) -> Self::Coeff + where + Self::Coeff: Conjugate, + { + fold_common(self, other, |a, b| { + let mut product = a.conj(); + product *= b; + product + }) + } +} + +/// Sum `combine` over keys present in both supports, scanning the smaller side +/// (`O(min(|lhs|, |rhs|))` probes, so summation order varies with the sizes). +/// `combine` always receives `(lhs coeff, rhs coeff)`, whichever side scanned. +fn fold_common( + lhs: &S, + rhs: &S, + mut combine: impl FnMut(&S::Coeff, &S::Coeff) -> S::Coeff, +) -> S::Coeff +where + S: Support + ?Sized, +{ + let mut acc = S::Coeff::zero(); + if lhs.len() <= rhs.len() { + lhs.for_each_ref(|k, a| { + if let Some(b) = rhs.get(k) { + acc += combine(a, &b); + } + }); + } else { + rhs.for_each_ref(|k, b| { + if let Some(a) = lhs.get(k) { + acc += combine(&a, b); + } + }); + } + acc +} + +/// Accumulate a ring product using [`KeyProduct`] and [`ImaginaryUnit`]. +/// Key-product phases are absorbed into coefficients; types without a product +/// need not implement this layer. +pub trait Multiply: Accumulate +where + Self::Key: KeyProduct, + Self::Coeff: ImaginaryUnit, +{ + /// Accumulate the ring product `self · other` into `acc`. + fn multiply_into(&self, other: &Self, acc: &mut Self); +} + +/// Retain terms selected by a predicate, independently of the algebraic layers. +/// Used by truncation policies; error guarantees depend on the coefficient magnitude. +pub trait Retain { + /// Retain exactly the terms for which `keep(&word, &coeff)` is `true`. + fn retain(&mut self, keep: impl Fn(&W, &C) -> bool); +} diff --git a/crates/ppvm-traits-2/src/containers/hash.rs b/crates/ppvm-traits-2/src/containers/hash.rs new file mode 100644 index 000000000..938ee09f3 --- /dev/null +++ b/crates/ppvm-traits-2/src/containers/hash.rs @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Structural key digests and a pass-through hasher. +//! [`Indexable`] is required by hash backends, not by all container keys. + +use std::hash::{BuildHasher, Hash, Hasher}; + +/// A key with an avalanche-quality structural digest; equal keys have equal digests. +/// `Hash` must write exactly one `u64`: `state.write_u64(self.key_hash())`. +/// Column hashing must reproduce that digest bit for bit. +pub trait Indexable: Clone + Eq + Hash { + /// The finalized structural digest of this key. + fn key_hash(&self) -> u64; +} + +/// A pass-through hasher returning the single `u64` digest written by a key. +#[derive(Debug, Default, Clone)] +pub struct IdentityHasher(u64); + +impl Hasher for IdentityHasher { + #[inline] + fn write_u64(&mut self, n: u64) { + self.0 = n; // store the digest + } + + fn write(&mut self, _: &[u8]) { + unreachable!("Indexable keys write exactly one u64 (their key_hash())") + } + + #[inline] + fn finish(&self) -> u64 { + self.0 // hand it back verbatim + } +} + +/// Builds [`IdentityHasher`] instances so map hashes equal the supplied key digests. +#[derive(Debug, Default, Clone)] +pub struct IdentityBuildHasher; + +impl BuildHasher for IdentityBuildHasher { + type Hasher = IdentityHasher; + + #[inline] + fn build_hasher(&self) -> IdentityHasher { + IdentityHasher::default() + } +} diff --git a/crates/ppvm-traits-2/src/containers/hash_join.rs b/crates/ppvm-traits-2/src/containers/hash_join.rs new file mode 100644 index 000000000..dd6a10e25 --- /dev/null +++ b/crates/ppvm-traits-2/src/containers/hash_join.rs @@ -0,0 +1,206 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! A `HashMap` backend for large supports. +//! [`Indexable`] keys supply structural digests consumed by the pass-through hasher. + +use std::collections::HashMap; +use std::collections::hash_map::Entry; + +use crate::algebra::{ImaginaryUnit, KeyProduct}; +use crate::arithmetic::Coefficient; +use crate::containers::{ + Accumulate, IdentityBuildHasher, Indexable, Multiply, Pair, Retain, Scale, Support, TermBatch, +}; + +impl Support for HashMap +where + K: Indexable, + C: Coefficient, +{ + type Key = K; + type Coeff = C; + + #[inline] + fn len(&self) -> usize { + HashMap::len(self) + } + + #[inline] + fn get(&self, key: &K) -> Option { + HashMap::get(self, key).cloned() + } + + #[inline] + fn iter(&self) -> impl Iterator { + HashMap::iter(self).map(|(k, v)| (k.clone(), v.clone())) + } + + /// The borrowing scan: hands out `(&K, &C)` straight from the buckets, so a + /// filtering reader never clones a coefficient it is about to reject. Same + /// order as [`Support::iter`] (the map's own bucket order). + #[inline] + fn for_each_ref(&self, mut f: impl FnMut(&K, &C)) { + for (k, v) in HashMap::iter(self) { + f(k, v); + } + } +} + +impl Accumulate for HashMap +where + K: Indexable, + C: Coefficient, +{ + /// Build side of the hash join: probe each produced term, accumulate its + /// coefficient onto a matching key, insert on a miss. + #[inline] + fn accumulate_batch(&mut self, terms: &TermBatch) { + for (k, c) in terms.iter() { + if let Some(value) = self.get_mut(k) { + *value += c; + } else { + self.insert(k.clone(), c.clone()); + } + } + } + + /// Drop every zero-coefficient key (`reduce_structural`). + #[inline] + fn reduce(&mut self) { + self.retain(|_, v| !v.is_zero()); + } +} + +impl Scale for HashMap +where + K: Indexable, + C: Coefficient, +{ + #[inline] + fn scale(&mut self, s: &C) { + for v in self.values_mut() { + *v *= s; + } + } +} + +/// Takes every [`Pair`] default: the shared scan already drives from the +/// smaller support and probes through [`Support::get`], which is a hash lookup +/// here. +impl Pair for HashMap +where + K: Indexable, + C: Coefficient, +{ +} + +impl Retain for HashMap +where + K: Indexable, + C: Coefficient, +{ + #[inline] + fn retain(&mut self, keep: impl Fn(&K, &C) -> bool) { + // Inherent `HashMap::retain` shadows the trait method; no recursion. + self.retain(|k, v| keep(k, v)); + } +} + +impl Multiply for HashMap +where + K: Indexable + KeyProduct, + C: ImaginaryUnit, +{ + /// Accumulate every key-pair product and its phase into a distinct `acc`. + /// The outer product costs `O(|A|·|B|)`; reduction and truncation are explicit. + /// Exact-zero entries remain until [`Accumulate::reduce`] runs. + fn multiply_into(&self, other: &Self, acc: &mut Self) { + for (p, a) in HashMap::iter(self) { + for (q, b) in HashMap::iter(other) { + let (k, phase) = p.key_mul(q); + let mut product = a.clone(); + product *= b; + let c = phase.apply(&product); + match acc.entry(k) { + Entry::Occupied(mut slot) => *slot.get_mut() += c, + Entry::Vacant(slot) => { + slot.insert(c); + } + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// A minimal [`Indexable`] key, so the hash backend is testable here (the + /// only real one, `PauliWord`, lives downstream). `Hash` is exactly + /// `write_u64(key_hash())`, as the contract requires. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + struct Key(u64); + + impl std::hash::Hash for Key { + fn hash(&self, state: &mut H) { + state.write_u64(self.key_hash()); + } + } + + impl Indexable for Key { + fn key_hash(&self) -> u64 { + self.0.wrapping_mul(0x9E37_79B9_7F4A_7C15) + } + } + + #[test] + fn borrowed_arithmetic_matches_vector_backend() { + use crate::TermSink; + use num::Complex; + + let mut terms = TermBatch::new(); + terms.push(Key(1), Complex::new(1.0, 2.0)); + terms.push(Key(1), Complex::new(2.0, -1.0)); + terms.push(Key(2), Complex::new(-1.0, 3.0)); + let mut map = HashMap::, IdentityBuildHasher>::default(); + let mut vector = Vec::<(Key, Complex)>::new(); + map.accumulate_batch(&terms); + vector.accumulate_batch(&terms); + let scalar = Complex::new(2.0, -1.0); + map.scale(&scalar); + vector.scale(&scalar); + assert_eq!(Support::get(&map, &Key(1)), Some(Complex::new(7.0, -1.0))); + + let other_vector = vec![(Key(1), Complex::new(1.0, 1.0))]; + let other_map = other_vector.as_slice().iter().copied().collect(); + assert_eq!(map.overlap(&other_map), Complex::new(8.0, 6.0)); + assert_eq!(map.hermitian_overlap(&other_map), Complex::new(6.0, 8.0)); + assert_eq!(map.overlap(&other_map), vector.overlap(&other_vector)); + assert_eq!(other_map.overlap(&map), other_vector.overlap(&vector)); + assert_eq!( + map.hermitian_overlap(&other_map), + vector.hermitian_overlap(&other_vector) + ); + assert_eq!( + other_map.hermitian_overlap(&map), + other_vector.hermitian_overlap(&vector) + ); + } + + #[test] + fn for_each_ref_agrees_with_iter() { + let mut m: HashMap = HashMap::default(); + for (k, c) in [(1u64, 1.0), (2, 2.0), (3, -3.0)] { + m.insert(Key(k), c); + } + let mut seen: Vec<(Key, f64)> = Vec::new(); + m.for_each_ref(|k, c| seen.push((*k, *c))); + seen.sort_by_key(|(k, _)| k.0); + let mut want: Vec<(Key, f64)> = Support::iter(&m).collect(); + want.sort_by_key(|(k, _)| k.0); + assert_eq!(seen, want); + assert_eq!(seen.len(), 3); + } +} diff --git a/crates/ppvm-traits-2/src/containers/mod.rs b/crates/ppvm-traits-2/src/containers/mod.rs new file mode 100644 index 000000000..9cd39784d --- /dev/null +++ b/crates/ppvm-traits-2/src/containers/mod.rs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Container algebra, batches, and hashing. +//! +//! The private backend modules implement these traits for `Vec` and `HashMap`. + +mod batch; +mod coordinate_list; +mod graded; +mod hash; +mod hash_join; + +pub use batch::{ + Columnar, KeyBatch, KeyColumn, KeyColumnMut, LossColumn, PauliColumn, TermBatch, TermSink, +}; +pub use graded::{Accumulate, Multiply, Pair, Retain, Scale, Support}; +pub use hash::{IdentityBuildHasher, IdentityHasher, Indexable}; diff --git a/crates/ppvm-traits-2/src/gates/channel.rs b/crates/ppvm-traits-2/src/gates/channel.rs new file mode 100644 index 000000000..e266ff065 --- /dev/null +++ b/crates/ppvm-traits-2/src/gates/channel.rs @@ -0,0 +1,225 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Noise-channel capabilities. `rng` is threaded through each call rather than +//! owned by the state: trajectory backends draw from it, deterministic ones +//! ignore it, and the caller owns seeding and stream sharing. + +use crate::arithmetic::Coefficient; + +/// Optional coefficient capability for single-qubit Pauli-channel factors. +/// Implementers can use the generic default or specialize the noise calculation. +pub trait PauliErrorFactors: Coefficient + num::One { + /// Transfer eigenvalues `(λ_X, λ_Z, λ_Y)` for Pauli probabilities + /// `(p_X, p_Y, p_Z)`. + #[inline(always)] + fn pauli_error_factors(probabilities: [Self; 3]) -> [Self; 3] { + let [px, py, pz] = probabilities; + [ + Self::one() - py.doubled() - pz.doubled(), + Self::one() - px.doubled() - py.doubled(), + Self::one() - px.doubled() - pz.doubled(), + ] + } +} + +// Both numeric types already specialize `Coefficient::doubled` to `*self * 2.0`, +// so the generic default inlines to exactly the hand-written expression. +impl PauliErrorFactors for f64 {} + +impl PauliErrorFactors for num::Complex {} + +/// A unital single-qubit Pauli error channel `P ↦ λ_P·P`. +pub trait PauliError { + /// Apply a single-qubit Pauli channel with `X`, `Y`, `Z` probabilities. + fn pauli_error( + &mut self, + qubit: usize, + probabilities: [C; 3], + rng: &mut R, + ); + + /// stim `X_ERROR(p)` — apply `X` with probability `p` to one qubit. + fn x_error(&mut self, qubit: usize, p: C, rng: &mut R) { + self.pauli_error(qubit, [p, C::zero(), C::zero()], rng) + } + + /// stim `Y_ERROR(p)` — apply `Y` with probability `p` to one qubit. + fn y_error(&mut self, qubit: usize, p: C, rng: &mut R) { + self.pauli_error(qubit, [C::zero(), p, C::zero()], rng) + } + + /// stim `Z_ERROR(p)` — apply `Z` with probability `p` to one qubit. + fn z_error(&mut self, qubit: usize, p: C, rng: &mut R) { + self.pauli_error(qubit, [C::zero(), C::zero(), p], rng) + } + + /// Explicit batched Pauli-error channel. + fn pauli_error_many( + &mut self, + targets: &[usize], + p: [C; 3], + rng: &mut R, + ) { + for &q in targets { + self.pauli_error(q, p.clone(), rng); + } + } + + /// Explicit batched `X_ERROR(p)`. + fn x_error_many(&mut self, targets: &[usize], p: C, rng: &mut R) { + for &q in targets { + self.x_error(q, p.clone(), rng); + } + } + + /// Explicit batched `Y_ERROR(p)`. + fn y_error_many(&mut self, targets: &[usize], p: C, rng: &mut R) { + for &q in targets { + self.y_error(q, p.clone(), rng); + } + } + + /// Explicit batched `Z_ERROR(p)`. + fn z_error_many(&mut self, targets: &[usize], p: C, rng: &mut R) { + for &q in targets { + self.z_error(q, p.clone(), rng); + } + } +} + +/// Two-qubit Pauli error channel. +pub trait TwoQubitPauliError { + /// Apply a two-qubit Pauli-error channel to one pair. Probabilities are given + /// in the order + /// `{IX, IY, IZ, XI, XX, XY, XZ, YI, YX, YY, YZ, ZI, ZX, ZY, ZZ}`. + fn two_qubit_pauli_error( + &mut self, + qubit0: usize, + qubit1: usize, + p: [C; 15], + rng: &mut R, + ); + + /// Explicit batched two-qubit Pauli-error channel. + fn two_qubit_pauli_error_many( + &mut self, + pairs: &[(usize, usize)], + p: [C; 15], + rng: &mut R, + ) { + for &(a, b) in pairs { + self.two_qubit_pauli_error(a, b, p.clone(), rng); + } + } +} + +/// Single-qubit depolarizing channel. +pub trait Depolarizing { + /// Depolarize one qubit with probability `p`. + fn depolarize1(&mut self, qubit: usize, p: C, rng: &mut R); + + /// Explicit batched single-qubit depolarizing channel. + fn depolarize1_many(&mut self, targets: &[usize], p: C, rng: &mut R) { + for &q in targets { + self.depolarize1(q, p.clone(), rng); + } + } +} + +/// Two-qubit depolarizing channel. +pub trait Depolarizing2 { + /// Depolarize one qubit pair with probability `p`. + fn depolarize2( + &mut self, + qubit0: usize, + qubit1: usize, + p: C, + rng: &mut R, + ); + + /// Explicit batched two-qubit depolarizing channel. + fn depolarize2_many( + &mut self, + pairs: &[(usize, usize)], + p: C, + rng: &mut R, + ) { + for &(a, b) in pairs { + self.depolarize2(a, b, p.clone(), rng); + } + } +} + +/// Amplitude-damping channel (single qubit). +pub trait AmplitudeDamping { + /// Apply amplitude damping with damping parameter `gamma`. + fn amplitude_damping(&mut self, qubit: usize, gamma: C); +} + +/// Single-qubit loss channel — with probability `p`, mark the qubit as lost +/// (see [`LossState`](crate::loss::LossState)). +pub trait LossChannel { + /// Apply a loss channel to `qubit` with loss probability `p`. + fn loss_channel(&mut self, qubit: usize, p: C, rng: &mut R); +} + +/// Correlated two-qubit loss channel. Completely positive exactly on +/// `p[0], p[1] >= 0`, `p[0] + 2·p[1] <= 1`, `p[2] ∈ [0, 1]`. +pub trait CorrelatedLossChannel { + /// Apply correlated loss `p = [p_LL, p_LQ, p_LN]`: `p[0]` loses both, `p[2]` + /// the survivor of an earlier loss. `p[1]` loses a **named** one, so exactly + /// one goes with probability `2·p[1]` and both remain with `1−2·p[1]−p[0]`. + fn correlated_loss_channel( + &mut self, + qubit0: usize, + qubit1: usize, + p: [C; 3], + rng: &mut R, + ); +} + +/// Reset the loss bit on a qubit — models a re-cooling / re-loading event that +/// brings a previously-lost atom back. +pub trait ResetLossChannel { + /// Clear the loss bit at `qubit`. + fn reset_loss_channel(&mut self, qubit: usize); +} + +/// State-dependent loss: probability `p0` from `|0⟩`, `p1` from `|1⟩`. +/// The total loss probability depends on the populations, so the channel reads `⟨Z⟩`. +pub trait AsymmetricLossChannel { + /// Apply asymmetric loss to `qubit`, with `p0` / `p1` the loss probabilities + /// from `|0⟩` / `|1⟩`. See the backend impl for the trajectory approximation + /// used (the survival back-action is omitted). + fn asymmetric_loss_channel( + &mut self, + qubit: usize, + p0: C, + p1: C, + rng: &mut R, + ); +} + +#[cfg(test)] +mod tests { + use super::PauliErrorFactors; + use num::Complex; + + #[test] + fn deterministic_pauli_errors_have_expected_conjugation_signs() { + // Inputs are X/Y/Z probabilities; outputs are X/Z/Y eigenvalues. + for (probabilities, expected) in [ + ([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]), + ([1.0, 0.0, 0.0], [1.0, -1.0, -1.0]), + ([0.0, 1.0, 0.0], [-1.0, -1.0, 1.0]), + ([0.0, 0.0, 1.0], [-1.0, 1.0, -1.0]), + ] { + assert_eq!(f64::pauli_error_factors(probabilities), expected); + assert_eq!( + Complex::::pauli_error_factors(probabilities.map(|p| Complex::new(p, 0.0)),), + expected.map(|factor| Complex::new(factor, 0.0)), + ); + } + } +} diff --git a/crates/ppvm-traits-2/src/gates/clifford.rs b/crates/ppvm-traits-2/src/gates/clifford.rs new file mode 100644 index 000000000..431f21f4d --- /dev/null +++ b/crates/ppvm-traits-2/src/gates/clifford.rs @@ -0,0 +1,134 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +/// The Clifford gate set, applied in the Heisenberg picture. +/// Consumers implement the gate methods directly; aliases delegate to those methods. +pub trait Clifford { + /// Apply Pauli `X` to one qubit. + fn x(&mut self, qubit: usize); + /// Apply Pauli `Y` to one qubit. + fn y(&mut self, qubit: usize); + /// Apply Pauli `Z` to one qubit. + fn z(&mut self, qubit: usize); + /// Apply Hadamard `H` to one qubit. + fn h(&mut self, qubit: usize); + /// Apply the phase gate `S` to one qubit. + fn s(&mut self, qubit: usize); + /// Apply `CNOT` to one `(control, target)` pair. + fn cnot(&mut self, control: usize, target: usize); + /// Apply `CZ` to one qubit pair. + fn cz(&mut self, qubit0: usize, qubit1: usize); + + /// stim alias for [`cnot`](Clifford::cnot). + fn cx(&mut self, control: usize, target: usize) { + self.cnot(control, target) + } + /// stim alias for [`cnot`](Clifford::cnot). + fn zcx(&mut self, control: usize, target: usize) { + self.cnot(control, target) + } + /// stim alias for [`cz`](Clifford::cz). + fn zcz(&mut self, qubit0: usize, qubit1: usize) { + self.cz(qubit0, qubit1) + } + /// Apply `S†` to one qubit. + fn s_dag(&mut self, qubit: usize); + /// Apply `√X` to one qubit. + fn sqrt_x(&mut self, qubit: usize); + /// Apply `(√X)†` to one qubit. + fn sqrt_x_dag(&mut self, qubit: usize); + /// Apply `√Y` to one qubit. + fn sqrt_y(&mut self, qubit: usize); + /// Apply `(√Y)†` to one qubit. + fn sqrt_y_dag(&mut self, qubit: usize); + /// Apply `CY` to one `(control, target)` pair. + fn cy(&mut self, control: usize, target: usize); + /// stim alias for [`cy`](Clifford::cy). + fn zcy(&mut self, control: usize, target: usize) { + self.cy(control, target) + } +} + +/// Batched Clifford gates: apply the same gate to many qubits in one call. +/// The defaults loop over [`Clifford`] and are load-bearing — fused backends +/// (bit-plane tableaus) override them, term-wise ones take the empty impl. +pub trait CliffordBatch: Clifford { + /// Apply Pauli `X` to every qubit in `indices`. + fn x_many(&mut self, indices: &[usize]) { + for &q in indices { + self.x(q); + } + } + /// Apply Pauli `Y` to every qubit in `indices`. + fn y_many(&mut self, indices: &[usize]) { + for &q in indices { + self.y(q); + } + } + /// Apply Pauli `Z` to every qubit in `indices`. + fn z_many(&mut self, indices: &[usize]) { + for &q in indices { + self.z(q); + } + } + /// Apply Hadamard `H` to every qubit in `indices`. + fn h_many(&mut self, indices: &[usize]) { + for &q in indices { + self.h(q); + } + } + /// Apply the phase gate `S` to every qubit in `indices`. + fn s_many(&mut self, indices: &[usize]) { + for &q in indices { + self.s(q); + } + } + /// Apply `CNOT` to every `(control, target)` pair. + fn cnot_many(&mut self, pairs: &[(usize, usize)]) { + for &(c, t) in pairs { + self.cnot(c, t); + } + } + /// Apply `CZ` to every `(control, target)` pair. + fn cz_many(&mut self, pairs: &[(usize, usize)]) { + for &(c, t) in pairs { + self.cz(c, t); + } + } + /// apply `s†` to every qubit in `indices`. + fn s_dag_many(&mut self, indices: &[usize]) { + for &q in indices { + self.s_dag(q); + } + } + /// apply `√x` to every qubit in `indices`. + fn sqrt_x_many(&mut self, indices: &[usize]) { + for &q in indices { + self.sqrt_x(q); + } + } + /// apply `(√x)†` to every qubit in `indices`. + fn sqrt_x_dag_many(&mut self, indices: &[usize]) { + for &q in indices { + self.sqrt_x_dag(q); + } + } + /// apply `√y` to every qubit in `indices`. + fn sqrt_y_many(&mut self, indices: &[usize]) { + for &q in indices { + self.sqrt_y(q); + } + } + /// apply `(√y)†` to every qubit in `indices`. + fn sqrt_y_dag_many(&mut self, indices: &[usize]) { + for &q in indices { + self.sqrt_y_dag(q); + } + } + /// apply `cy` to every `(control, target)` pair. + fn cy_many(&mut self, pairs: &[(usize, usize)]) { + for &(c, t) in pairs { + self.cy(c, t); + } + } +} diff --git a/crates/ppvm-traits-2/src/gates/measure.rs b/crates/ppvm-traits-2/src/gates/measure.rs new file mode 100644 index 000000000..5a70c8178 --- /dev/null +++ b/crates/ppvm-traits-2/src/gates/measure.rs @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Measurement, projection, and reset operations. + +use super::Clifford; + +/// Loss-aware projective computational-basis measurement. +/// +pub trait Measure { + /// Measure `qubit`; `None` if the qubit has been lost. + fn measure(&mut self, qubit: usize, rng: &mut R) -> Option; + + /// Measure each target in order, one result per target. + fn measure_many( + &mut self, + targets: &[usize], + rng: &mut R, + ) -> Vec> { + targets.iter().map(|&q| self.measure(q, rng)).collect() + } +} + +// Reset one qubit to a computational/Pauli basis state. +pub trait Reset: Clifford { + /// Reset one qubit to `|0⟩` (stim `R`/`RZ`). + fn reset(&mut self, qubit: usize, rng: &mut R); + + /// stim `RZ` alias — reset to `|0⟩`. + fn reset_z(&mut self, qubit: usize, rng: &mut R) { + self.reset(qubit, rng) + } + + /// stim `RX` — reset to `|+⟩`. + fn reset_x(&mut self, qubit: usize, rng: &mut R) { + self.reset(qubit, rng); + self.h(qubit); + } + + /// stim `RY` — reset to `|i⟩`. + fn reset_y(&mut self, qubit: usize, rng: &mut R) { + self.reset(qubit, rng); + self.h(qubit); + self.s(qubit); + } + + /// Explicit batched reset to `|0⟩`. + fn reset_many(&mut self, targets: &[usize], rng: &mut R) { + for &q in targets { + self.reset(q, rng); + } + } + + /// Explicit batched `RZ` alias. + fn reset_z_many(&mut self, targets: &[usize], rng: &mut R) { + self.reset_many(targets, rng) + } + + /// Explicit batched `RX`. + fn reset_x_many(&mut self, targets: &[usize], rng: &mut R) { + for &q in targets { + self.reset_x(q, rng); + } + } + + /// Explicit batched `RY`. + fn reset_y_many(&mut self, targets: &[usize], rng: &mut R) { + for &q in targets { + self.reset_y(q, rng); + } + } +} + +/// Projective Z-basis projectors `|0⟩⟨0|` and `|1⟩⟨1|` +pub trait Projection { + /// Project `qubit` onto `|0⟩`. + fn p0(&mut self, qubit: usize); + /// Project `qubit` onto `|1⟩`. + fn p1(&mut self, qubit: usize); +} diff --git a/crates/ppvm-traits-2/src/gates/mod.rs b/crates/ppvm-traits-2/src/gates/mod.rs new file mode 100644 index 000000000..f2480be9b --- /dev/null +++ b/crates/ppvm-traits-2/src/gates/mod.rs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Gate, measurement, reset, and noise-channel interfaces. + +mod channel; +mod clifford; +mod measure; +mod rot; + +pub use channel::{ + AmplitudeDamping, AsymmetricLossChannel, CorrelatedLossChannel, Depolarizing, Depolarizing2, + LossChannel, PauliError, PauliErrorFactors, ResetLossChannel, TwoQubitPauliError, +}; +pub use clifford::{Clifford, CliffordBatch}; +pub use measure::{Measure, Projection, Reset}; +pub use rot::{ + CRx, RotXY, RotationOne, RotationOneBatch, RotationTwo, RotationTwoBatch, TGate, U3Gate, +}; diff --git a/crates/ppvm-traits-2/src/gates/rot.rs b/crates/ppvm-traits-2/src/gates/rot.rs new file mode 100644 index 000000000..fdcf8c544 --- /dev/null +++ b/crates/ppvm-traits-2/src/gates/rot.rs @@ -0,0 +1,247 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + arithmetic::{Angle, Coefficient}, + pauli::Pauli, +}; + +/// Single-qubit rotations with angle domain `A` producing coefficients in `C`. +/// Defaults to `A = C`; distinct angle types can represent symbolic parameters. +pub trait RotationOne = C> { + /// Rotate about `axis` (one of `X`, `Y`, `Z`) on `qubit` by `theta`. + /// + /// `Pauli::I` commutes with every term, so an `I` axis is a no-op. + fn rotate_1(&mut self, axis: Pauli, qubit: usize, theta: A); + + /// Rotate about `X` on `qubit` by `theta`. + fn rx(&mut self, qubit: usize, theta: A) { + self.rotate_1(Pauli::X, qubit, theta) + } + /// Rotate about `Y` on `qubit` by `theta`. + fn ry(&mut self, qubit: usize, theta: A) { + self.rotate_1(Pauli::Y, qubit, theta) + } + /// Rotate about `Z` on `qubit` by `theta`. + fn rz(&mut self, qubit: usize, theta: A) { + self.rotate_1(Pauli::Z, qubit, theta) + } +} + +/// Batched single-qubit rotations, defaulting to scalar calls in target order. +pub trait RotationOneBatch = C>: RotationOne { + /// Explicit batched `RX(θ)`. + fn rx_many(&mut self, targets: &[usize], theta: A) + where + A: Clone, + { + for &q in targets { + self.rx(q, theta.clone()) + } + } + /// Explicit batched `RY(θ)`. + fn ry_many(&mut self, targets: &[usize], theta: A) + where + A: Clone, + { + for &q in targets { + self.ry(q, theta.clone()) + } + } + /// Explicit batched `RZ(θ)`. + fn rz_many(&mut self, targets: &[usize], theta: A) + where + A: Clone, + { + for &q in targets { + self.rz(q, theta.clone()) + } + } +} + +/// Two-qubit rotations `exp(-i θ/2 · P_a ⊗ P_b)`. +pub trait RotationTwo = C> { + /// Rotate about the supplied Pauli axes. + /// + /// The generator is `axis_a ⊗ axis_b` on sites `a` and `b`. + fn rotate_2(&mut self, axis_a: Pauli, axis_b: Pauli, a: usize, b: usize, theta: A); + + /// Rotate about X ⊗ X. + fn rxx(&mut self, a: usize, b: usize, theta: A) { + self.rotate_2(Pauli::X, Pauli::X, a, b, theta); + } + + /// Rotate about X ⊗ Y. + fn rxy(&mut self, a: usize, b: usize, theta: A) { + self.rotate_2(Pauli::X, Pauli::Y, a, b, theta); + } + + /// Rotate about X ⊗ Z. + fn rxz(&mut self, a: usize, b: usize, theta: A) { + self.rotate_2(Pauli::X, Pauli::Z, a, b, theta); + } + + /// Rotate about Y ⊗ X. + fn ryx(&mut self, a: usize, b: usize, theta: A) { + self.rotate_2(Pauli::Y, Pauli::X, a, b, theta); + } + + /// Rotate about Y ⊗ Y. + fn ryy(&mut self, a: usize, b: usize, theta: A) { + self.rotate_2(Pauli::Y, Pauli::Y, a, b, theta); + } + + /// Rotate about Y ⊗ Z. + fn ryz(&mut self, a: usize, b: usize, theta: A) { + self.rotate_2(Pauli::Y, Pauli::Z, a, b, theta); + } + + /// Rotate about Z ⊗ X. + fn rzx(&mut self, a: usize, b: usize, theta: A) { + self.rotate_2(Pauli::Z, Pauli::X, a, b, theta); + } + + /// Rotate about Z ⊗ Y. + fn rzy(&mut self, a: usize, b: usize, theta: A) { + self.rotate_2(Pauli::Z, Pauli::Y, a, b, theta); + } + + /// Rotate about Z ⊗ Z. + fn rzz(&mut self, a: usize, b: usize, theta: A) { + self.rotate_2(Pauli::Z, Pauli::Z, a, b, theta); + } +} + +/// Batched two-qubit rotations, defaulting to scalar calls in pair order. +pub trait RotationTwoBatch = C>: RotationTwo { + /// Apply RXX to each pair in order. + fn rxx_many(&mut self, pairs: &[(usize, usize)], theta: A) + where + A: Clone, + { + for &(a, b) in pairs { + self.rxx(a, b, theta.clone()); + } + } + + /// Apply RXY to each pair in order. + fn rxy_many(&mut self, pairs: &[(usize, usize)], theta: A) + where + A: Clone, + { + for &(a, b) in pairs { + self.rxy(a, b, theta.clone()); + } + } + + /// Apply RXZ to each pair in order. + fn rxz_many(&mut self, pairs: &[(usize, usize)], theta: A) + where + A: Clone, + { + for &(a, b) in pairs { + self.rxz(a, b, theta.clone()); + } + } + + /// Apply RYX to each pair in order. + fn ryx_many(&mut self, pairs: &[(usize, usize)], theta: A) + where + A: Clone, + { + for &(a, b) in pairs { + self.ryx(a, b, theta.clone()); + } + } + + /// Apply RYY to each pair in order. + fn ryy_many(&mut self, pairs: &[(usize, usize)], theta: A) + where + A: Clone, + { + for &(a, b) in pairs { + self.ryy(a, b, theta.clone()); + } + } + + /// Apply RYZ to each pair in order. + fn ryz_many(&mut self, pairs: &[(usize, usize)], theta: A) + where + A: Clone, + { + for &(a, b) in pairs { + self.ryz(a, b, theta.clone()); + } + } + + /// Apply RZX to each pair in order. + fn rzx_many(&mut self, pairs: &[(usize, usize)], theta: A) + where + A: Clone, + { + for &(a, b) in pairs { + self.rzx(a, b, theta.clone()); + } + } + + /// Apply RZY to each pair in order. + fn rzy_many(&mut self, pairs: &[(usize, usize)], theta: A) + where + A: Clone, + { + for &(a, b) in pairs { + self.rzy(a, b, theta.clone()); + } + } + + /// Apply RZZ to each pair in order. + fn rzz_many(&mut self, pairs: &[(usize, usize)], theta: A) + where + A: Clone, + { + for &(a, b) in pairs { + self.rzz(a, b, theta.clone()); + } + } +} + +/// Rotation about an axis in the X/Y plane: +/// `R(axis_angle, θ) = exp(-i θ/2 · (cos(axis_angle) X + sin(axis_angle) Y))`. +pub trait RotXY = C> { + /// `R(axis_angle, θ)` on `qubit`. + fn r(&mut self, qubit: usize, axis_angle: A, theta: A); +} + +/// Controlled `RX` rotation +pub trait CRx = C> { + /// Apply `CRX(θ)` with the given control and target. + fn crx(&mut self, control: usize, target: usize, theta: A); +} + +/// The general single-qubit `U3(θ, φ, λ)` gate +pub trait U3Gate = C> { + /// Apply `U3(θ, φ, λ)` to `qubit`. + fn u3(&mut self, qubit: usize, theta: A, phi: A, lambda: A); +} + +/// The non-Clifford `T` gate and its adjoint, `T = diag(1, e^{iπ/4})`. +pub trait TGate { + /// Apply `T` (`diag(1, e^{iπ/4})`) to one qubit. + fn t(&mut self, qubit: usize); + /// Apply `T†` to one qubit. + fn t_dag(&mut self, qubit: usize); + + /// Explicit batched `T`. + fn t_many(&mut self, targets: &[usize]) { + for &q in targets { + self.t(q); + } + } + + /// Explicit batched `T†`. + fn t_dag_many(&mut self, targets: &[usize]) { + for &q in targets { + self.t_dag(q); + } + } +} diff --git a/crates/ppvm-traits-2/src/lib.rs b/crates/ppvm-traits-2/src/lib.rs new file mode 100644 index 000000000..d9f8edc50 --- /dev/null +++ b/crates/ppvm-traits-2/src/lib.rs @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Shared arithmetic, word, gate, and container interfaces. +//! Includes small algebraic types, default gates, and `Vec`/`HashMap` implementations. + +pub mod algebra; +pub mod arithmetic; +pub mod containers; +pub mod gates; +pub mod loss; +pub mod pauli; +pub mod word; + +pub use algebra::{Conjugate, ImaginaryUnit, KeyProduct, Phase}; +pub use arithmetic::{Angle, Coefficient, Halvable}; +pub use containers::{ + Accumulate, Columnar, IdentityBuildHasher, IdentityHasher, Indexable, KeyBatch, KeyColumn, + KeyColumnMut, LossColumn, Multiply, Pair, PauliColumn, Retain, Scale, Support, TermBatch, + TermSink, +}; +pub use gates::{ + AmplitudeDamping, AsymmetricLossChannel, CRx, Clifford, CliffordBatch, CorrelatedLossChannel, + Depolarizing, Depolarizing2, LossChannel, Measure, PauliError, PauliErrorFactors, Projection, + Reset, ResetLossChannel, RotXY, RotationOne, RotationOneBatch, RotationTwo, RotationTwoBatch, + TGate, TwoQubitPauliError, U3Gate, +}; +pub use loss::LossState; +pub use pauli::{Pauli, PhaseTrack, SymplecticColumns}; +pub use word::{PauliBits, Word}; + +/// Common traits and types for implementing and using the interfaces. +/// Globbed per module so it cannot drift from the re-exports above. +pub mod prelude { + pub use crate::{ + algebra::*, arithmetic::*, containers::*, gates::*, loss::*, pauli::*, word::*, + }; +} diff --git a/crates/ppvm-traits-2/src/loss.rs b/crates/ppvm-traits-2/src/loss.rs new file mode 100644 index 000000000..7c51a3b19 --- /dev/null +++ b/crates/ppvm-traits-2/src/loss.rs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +/// Loss state associated with word sites. +pub trait LossState { + /// Whether site `i` is lost. Lossy words override this. + fn is_lost(&self, _i: usize) -> bool { + false + } + + /// Number of lost sites. + fn loss_weight(&self) -> usize { + 0 + } + + /// Mark index `i` lost. + fn set_lost(&mut self, i: usize); + + /// Clear the loss flag at index `i`, returning the site to identity. + fn clear_lost(&mut self, i: usize); + + /// A copy of this word with the loss flag at `i` cleared. + fn loss_cleared(&self, i: usize) -> Self + where + Self: Sized + Clone, + { + let mut out = self.clone(); + out.clear_lost(i); + out + } +} diff --git a/crates/ppvm-traits-2/src/pauli.rs b/crates/ppvm-traits-2/src/pauli.rs new file mode 100644 index 000000000..74aa71e45 --- /dev/null +++ b/crates/ppvm-traits-2/src/pauli.rs @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +/// A single-qubit Pauli symbol — the site alphabet of an ordinary packed Pauli +/// word (`Word`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Pauli { + /// Identity `I`. + I, + /// Pauli `X`. + X, + /// Pauli `Y`. + Y, + /// Pauli `Z`. + Z, +} +/// Phase-free symplectic operations on X/Z bit columns. +/// Shared by single-word bits and multi-row tableau columns. +pub trait SymplecticColumns { + /// Number of qubits (columns) this operator spans. + fn n_qubits(&self) -> usize; + + /// `H` on `q`: swap the X and Z columns. + fn swap_xz(&mut self, q: usize); + + /// S bit update on `q`: `z_q ⊕= x_q` (maps X to Y). + fn xor_z_from_x(&mut self, q: usize); + + /// `CNOT` bit rule, part one: `x_tgt ⊕= x_ctrl`. + fn xor_x_col(&mut self, ctrl: usize, tgt: usize); + + /// `CNOT` bit rule, part two: `z_ctrl ⊕= z_tgt`. + fn xor_z_col(&mut self, tgt: usize, ctrl: usize); + + /// CZ bit update: `z_a ⊕= x_b` and `z_b ⊕= x_a`. + fn cz_bits(&mut self, a: usize, b: usize); +} + +/// Extension-part: the phase algebra. `ℤ₄` for a phased word, `ℤ₂` + the +/// Aaronson–Gottesman `g`-rule for a tableau. One phase delta per gate; the +/// role-dependent half of conjugation, written **per type**. +pub trait PhaseTrack { + /// `H` phase delta: flip the sign of a component with both `x` and `z` set + /// (`Y → −Y`). + fn flip_phase_where_xz(&mut self, q: usize); + + /// `S` phase delta on `q`. + fn s_phase(&mut self, q: usize); + + /// `CNOT` phase delta on `(ctrl, tgt)`. + fn cnot_phase(&mut self, ctrl: usize, tgt: usize); + + /// `CZ` phase delta on `(a, b)`. + fn cz_phase(&mut self, a: usize, b: usize); + + /// `X` phase delta on `q` (pure sign; no bit change). + fn x_phase(&mut self, q: usize); + + /// `Y` phase delta on `q` (pure sign; no bit change). + fn y_phase(&mut self, q: usize); + + /// `Z` phase delta on `q` (pure sign; no bit change). + fn z_phase(&mut self, q: usize); +} diff --git a/crates/ppvm-traits-2/src/word.rs b/crates/ppvm-traits-2/src/word.rs new file mode 100644 index 000000000..45661c46b --- /dev/null +++ b/crates/ppvm-traits-2/src/word.rs @@ -0,0 +1,129 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +/// The common **read-only** concept for an indexed algebraic monomial. +pub trait Word { + /// The operator alphabet at one index. + type Site; + + /// Number of sites (for a dense Pauli word, the qubit width). + fn n_sites(&self) -> usize; + + /// Read the site at `index`. + fn get(&self, index: usize) -> Self::Site; + + /// Number of non-identity factors in the site alphabet. + /// Representations without explicit identities may have `weight() == n_sites()`. + fn weight(&self) -> usize; + + /// Iterate the sites in index order. + fn iter(&self) -> impl Iterator; +} + +/// Mutable single-vector X/Z access — a point of `GF(2)^{2n}`. Composite +/// defaults compose the four scalar accessors; a packed backend may override +/// any of them to refresh metadata once per call instead of once per bit. +pub trait PauliBits: Word { + /// Read the X bit at index `i`. + fn x_bit(&self, i: usize) -> bool; + + /// Read the Z bit at index `i`. + fn z_bit(&self, i: usize) -> bool; + + /// Set the X bit at index `i` (refreshes any structural-hash cache). + fn set_x_bit(&mut self, i: usize, v: bool); + + /// Set the Z bit at index `i` (refreshes any structural-hash cache). + fn set_z_bit(&mut self, i: usize, v: bool); + + /// Set both bit planes at one site. + #[inline(always)] + fn set_xz_bits(&mut self, i: usize, x: bool, z: bool) { + self.set_x_bit(i, x); + self.set_z_bit(i, z); + } + /// Set both bit planes at two sites. + #[inline(always)] + fn set_xz_bits2(&mut self, i: usize, xi: bool, zi: bool, j: usize, xj: bool, zj: bool) { + self.set_xz_bits(i, xi, zi); + self.set_xz_bits(j, xj, zj); + } + /// Set one X bit and one Z bit together, as required by CNOT. + #[inline(always)] + fn set_x_bit_and_z_bit(&mut self, x_i: usize, x: bool, z_i: usize, z: bool) { + self.set_x_bit(x_i, x); + self.set_z_bit(z_i, z); + } + /// Set two Z bits together, as required by CZ, leaving X bits unchanged. + #[inline(always)] + fn set_z_bit_pair(&mut self, i: usize, zi: bool, j: usize, zj: bool) { + self.set_z_bit(i, zi); + self.set_z_bit(j, zj); + } + + /// Packed local Pauli code: `0=I, 1=X, 2=Z, 3=Y`. + #[inline(always)] + fn pauli_code(&self, i: usize) -> u8 { + (self.x_bit(i) as u8) | ((self.z_bit(i) as u8) << 1) + } + + /// Copy this word and toggle selected X/Z bits at one site. + #[inline] + fn toggled_bits(&self, i: usize, toggle_x: bool, toggle_z: bool) -> Self + where + Self: Sized + Clone, + { + self.clone().into_toggled_bits(i, [toggle_x, toggle_z]) + } + + /// Copy this word once and toggle selected X/Z bits at two sites. + /// Each mask is `[toggle_x, toggle_z]`; only one copy is made. + #[inline] + fn toggled_bits2(&self, i: usize, toggle_i: [bool; 2], j: usize, toggle_j: [bool; 2]) -> Self + where + Self: Sized + Clone, + { + self.clone().into_toggled_bits2(i, toggle_i, j, toggle_j) + } + + /// Consume this word and toggle the bits selected by `[toggle_x, toggle_z]` + /// at one site. The in-place primitive every variant above is built from. + #[inline] + fn into_toggled_bits(mut self, i: usize, toggle: [bool; 2]) -> Self + where + Self: Sized, + { + if toggle[0] { + let x = self.x_bit(i); + self.set_x_bit(i, !x); + } + if toggle[1] { + let z = self.z_bit(i); + self.set_z_bit(i, !z); + } + self + } + + /// Consume this word and toggle two sites using `[toggle_x, toggle_z]` masks. + #[inline] + fn into_toggled_bits2( + self, + i: usize, + toggle_i: [bool; 2], + j: usize, + toggle_j: [bool; 2], + ) -> Self + where + Self: Sized, + { + self.into_toggled_bits(i, toggle_i) + .into_toggled_bits(j, toggle_j) + } + + /// Whether this word anticommutes with `pauli = (x_bit, z_bit)` at site `i`. + /// Computes the local symplectic form `x_P·z_Q ⊕ z_P·x_Q`. + #[inline] + fn anticommutes_at(&self, i: usize, pauli: (bool, bool)) -> bool { + (self.x_bit(i) & pauli.1) ^ (self.z_bit(i) & pauli.0) + } +}