From 2a7f074d0ab35394001dacecb8368bf9790db7d5 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Wed, 9 Sep 2026 10:52:14 -0400 Subject: [PATCH 01/17] initial commit of breaking up roger's PR on rebuilding the trait system for ppvm. Focused on moving files to more isolated modules to have better appreciation on module responsibility --- crates/ppvm-traits-2/Cargo.toml | 11 + crates/ppvm-traits-2/src/algebra.rs | 277 +++++++++++ crates/ppvm-traits-2/src/arithmetic/angle.rs | 36 ++ .../src/arithmetic/coefficient.rs | 80 +++ .../ppvm-traits-2/src/arithmetic/halvable.rs | 26 + crates/ppvm-traits-2/src/arithmetic/mod.rs | 12 + crates/ppvm-traits-2/src/containers/batch.rs | 460 ++++++++++++++++++ .../src/containers/coordinate_list.rs | 229 +++++++++ crates/ppvm-traits-2/src/containers/graded.rs | 220 +++++++++ crates/ppvm-traits-2/src/containers/hash.rs | 70 +++ .../ppvm-traits-2/src/containers/hash_join.rs | 235 +++++++++ crates/ppvm-traits-2/src/containers/mod.rs | 18 + .../ppvm-traits-2/src/containers/storage.rs | 10 + crates/ppvm-traits-2/src/gates/channel.rs | 257 ++++++++++ crates/ppvm-traits-2/src/gates/clifford.rs | 298 ++++++++++++ crates/ppvm-traits-2/src/gates/measure.rs | 80 +++ crates/ppvm-traits-2/src/gates/mod.rs | 18 + crates/ppvm-traits-2/src/gates/rot.rs | 248 ++++++++++ crates/ppvm-traits-2/src/lib.rs | 47 ++ crates/ppvm-traits-2/src/loss.rs | 31 ++ crates/ppvm-traits-2/src/pauli.rs | 95 ++++ crates/ppvm-traits-2/src/word.rs | 214 ++++++++ 22 files changed, 2972 insertions(+) create mode 100644 crates/ppvm-traits-2/Cargo.toml create mode 100644 crates/ppvm-traits-2/src/algebra.rs create mode 100644 crates/ppvm-traits-2/src/arithmetic/angle.rs create mode 100644 crates/ppvm-traits-2/src/arithmetic/coefficient.rs create mode 100644 crates/ppvm-traits-2/src/arithmetic/halvable.rs create mode 100644 crates/ppvm-traits-2/src/arithmetic/mod.rs create mode 100644 crates/ppvm-traits-2/src/containers/batch.rs create mode 100644 crates/ppvm-traits-2/src/containers/coordinate_list.rs create mode 100644 crates/ppvm-traits-2/src/containers/graded.rs create mode 100644 crates/ppvm-traits-2/src/containers/hash.rs create mode 100644 crates/ppvm-traits-2/src/containers/hash_join.rs create mode 100644 crates/ppvm-traits-2/src/containers/mod.rs create mode 100644 crates/ppvm-traits-2/src/containers/storage.rs create mode 100644 crates/ppvm-traits-2/src/gates/channel.rs create mode 100644 crates/ppvm-traits-2/src/gates/clifford.rs create mode 100644 crates/ppvm-traits-2/src/gates/measure.rs create mode 100644 crates/ppvm-traits-2/src/gates/mod.rs create mode 100644 crates/ppvm-traits-2/src/gates/rot.rs create mode 100644 crates/ppvm-traits-2/src/lib.rs create mode 100644 crates/ppvm-traits-2/src/loss.rs create mode 100644 crates/ppvm-traits-2/src/pauli.rs create mode 100644 crates/ppvm-traits-2/src/word.rs 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..181724b6d --- /dev/null +++ b/crates/ppvm-traits-2/src/algebra.rs @@ -0,0 +1,277 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Algebra capabilities that lift `C[K]` from a module to a (twisted) algebra: +//! the key product [`KeyProduct`] and the two coefficient-ring capabilities it +//! and the sesquilinear pairing need, [`ImaginaryUnit`] and [`Conjugate`], plus +//! the [`Phase`] the Pauli key product emits. +//! +//! Design: `traits-2-configuration-and-hashing.md` §"The map is a graded +//! algebra over `C[K]`" (L4 and its coefficient capabilities). + +use crate::arithmetic::Coefficient; + +/// A fourth root of unity `iᵏ`, `k ∈ ℤ/4`, i.e. an element of `{1, i, −1, −i}`. +/// +/// The Pauli key product is not closed on keys: `v·w = iᵏ (v⊕w)`, so +/// [`KeyProduct::key_mul`] returns the residual `Phase` for the coefficient to +/// absorb. `iᵏ` already spans `{1, i, −1, −i}`, so no separate `±` is carried. +/// +/// Design: §"The map is a graded algebra over `C[K]`" (`KeyProduct`). The +/// exponent `k` is the packed `phaseExp` of `lean/PPVM/Pauli/Phase.lean` +/// (`phaseExp_eq_ref`). +#[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 { + match self { + Phase::Pos1 => 0, + Phase::PosI => 1, + Phase::Neg1 => 2, + Phase::NegI => 3, + } + } + + /// 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, + _ => Phase::NegI, + } + } + + /// The group identity `i⁰ = +1` of the `ℤ/4` phase group. + /// + /// The neutral element for [`compose`](Self::compose): `p.compose(one()) == + /// p` for every phase `p`. + #[inline] + pub fn one() -> Self { + Phase::Pos1 + } + + /// The `ℤ/4` group product `iᵃ · iᵇ = i^{a+b}` (exponents added mod 4). + /// + /// This is the first-class group operation on phases: a `KeyProduct` chain + /// can accumulate the residual phases each [`KeyProduct::key_mul`] emits with + /// `compose` (or the equivalent [`Mul`](std::ops::Mul) impl) without a coefficient in hand, + /// deferring the [`apply`](Self::apply) fold onto the coefficient to the end. + /// The group is abelian, so `a.compose(b) == b.compose(a)`. + #[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()) + } + + /// Fold this phase onto a coefficient: return `iᵏ · c`. + /// + /// This is the `iPow` fold of `lean/PPVM/Algebra/Twisted.lean` — the phase a + /// [`KeyProduct::key_mul`] emits is absorbed by the coefficient here. Needs a + /// primitive fourth root of unity, hence the [`ImaginaryUnit`] bound. + /// + /// # Behaviour parity + /// + /// The `±i` arms go through [`ImaginaryUnit::mul_i`], **not** through a bare + /// `c * imaginary_unit()`. On `Complex` the two are *not* the same + /// function: the ring multiply computes `(re·0 − im·1, re·1 + im·0)`, and + /// `inf·0`/`NaN·0` are `NaN`, so `(inf + 0i)·i` is `NaN + inf·i` while the + /// old `ppvm_traits::ComplexCoefficient::mul_phase` — which swapped the + /// components by hand — gave `−0 + inf·i`. `mul_i` restores the old + /// component swap (and with it the sign of zero, visible through `Display` + /// and serialization). See `phase_apply_matches_old_mul_phase_encoding`. + /// + /// The whole fold is delegated to [`ImaginaryUnit::mul_i_pow`], which is an + /// **override point**: a ring whose values carry `iᵏ` symbolically (the + /// symbolic `Term` of `ppvm-sym-2`) folds `iᵏ` into its own representation + /// rather than through the `±1`/`mul_i` arms, which is what old's + /// `ComplexCoefficient::mul_phase` did. The default body *is* those arms, so + /// nothing changes for `f64`/`Complex`/`GaussianInt`. + #[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 whose set carries a product — the (projective) group structure that +/// lifts `C[K]` from a module to an algebra. +/// +/// The keys form a group only *up to phase*: the product is **not closed on +/// keys**, it emits an `iᵏ`, which is why `key_mul` returns `(Self, Phase)` and +/// why `C[PauliWord]` is a **2-cocycle-twisted** group algebra. +/// +/// # Laws +/// +/// Write `key_mul(u, v) = (u · v, i^{β(u,v)})`. Every impl must satisfy: +/// +/// * the key product is associative: `(u · v) · w == u · (v · w)`; +/// * the phase exponent is a **2-cocycle**: +/// `β(u,v) + β(u·v, w) == β(v,w) + β(u, v·w)` in `ℤ/4`. +/// +/// Under exactly those two hypotheses the twisted product on `C × K` is +/// associative for any commutative coefficient ring `C` with `i⁴ = 1` — proved +/// key-agnostically in `lean/PPVM/Algebra/Twisted.lean` (`gtmul_assoc`, over an +/// abstract `kmul` and `IsCocycle`), so the obligation is stated once and every +/// key discharges it. `PauliWord` does so via `Bool.xor_assoc` and +/// `lean/PPVM/Pauli/Phase.lean` (`phaseExp_cocycle`), recovered as the instance +/// in `phaseExp_isCocycle` / `tmul_assoc_of_gtmul` (with `tmul_assoc` the +/// concrete Pauli statement). A future ordered fermionic-word key must discharge +/// the same two hypotheses; it does **not** inherit associativity from the Pauli +/// proof. +/// +/// Design: §"The map is a graded algebra over `C[K]`" (`KeyProduct`). +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); +} + +/// The phase capability L4 needs, over a **commutative** coefficient ring: a +/// distinguished primitive fourth root of unity `i`. +/// +/// Impls must satisfy `Self::imaginary_unit() * Self::imaginary_unit() == +/// -Self::one()` (hence `i⁴ = 1`). This is strictly weaker than requiring +/// `Complex`: `GaussianInt` (`ℤ[i]`), `Complex`, and cyclotomic +/// integers all satisfy it, so L4 does not foreclose exact Pauli multiplication. +/// +/// Design: §"The map is a graded algebra over `C[K]`" (`ImaginaryUnit`). Law +/// machine-checked in `lean/PPVM/Pauli/Matrix.lean` (`iU_sq`: `iU * iU = -1`), +/// and the twisted product is associative over any commutative ring with +/// `i⁴ = 1` in `lean/PPVM/Algebra/Twisted.lean` (`tmul_assoc`). +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`. Semantically `self * imaginary_unit()`, which is the + /// default body — but it is an **override point**, because on a + /// floating-point ring the generic product is not extensionally equal to the + /// rotation it denotes. + /// + /// On `Complex`, `c * i` expands to + /// `(re·0 − im·1, re·1 + im·0)`; `inf·0` and `NaN·0` are `NaN`, so a + /// non-finite component contaminates *both* output components, and `re·0` + /// also loses the sign of zero. Multiplication by `i` is really the + /// component swap `(re, im) ↦ (−im, re)`, which is total and exact — this is + /// what the old `ppvm_traits::ComplexCoefficient::mul_phase` did, and the + /// `Complex` impl below restores it verbatim. It is also cheaper (two + /// negations instead of four multiplies and two adds). + #[inline] + fn mul_i(&self) -> Self { + self.clone() * Self::imaginary_unit() + } + + /// Multiply by `iᵏ` (`k` taken mod 4) — the fold [`Phase::apply`] delegates + /// to, and the second **override point**. + /// + /// The default body is the four-arm `{clone, mul_i, neg, neg∘mul_i}` fold, + /// which is the only sensible spelling on a ring whose values are numbers. + /// It is overridable because a ring whose values carry the `iᵏ` *as data* — + /// the symbolic `Term` of `ppvm-sym-2`, whose monomials hold a `ℤ/4` phase + /// byte — must fold the phase into that representation instead: old's + /// `ComplexCoefficient::mul_phase` promoted `Const(c)` to `One(i⁰, c)` + /// **unconditionally**, including at `k = 0`, and `Term`'s `PartialEq` and + /// `Display` are representational, so taking the `clone()` arm at `k = 0` + /// would be a user-visible divergence from old. + /// + /// Impls must satisfy `x.mul_i_pow(k) == iᵏ · x` denotationally, and + /// `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()), + _ => -(self.mul_i()), + } + } +} + +/// A coefficient ring carrying a ring involution (a commutative `*`-ring): +/// complex conjugation on `Complex` / `GaussianInt` / cyclotomic integers, +/// and the identity on real rings. +/// +/// Supplies exactly the conjugation the sesquilinear +/// [`crate::containers::Pair::hermitian_overlap`] needs; nothing in propagation +/// requires it, so — like [`ImaginaryUnit`] — it is a separate capability, not a +/// `Coefficient` bound. +/// +/// Laws (commutative `*`-ring): `conj(conj(a)) == a`, `conj(a + b) == +/// conj(a) + conj(b)`, `conj(a · b) == conj(a) · conj(b)`; and when the ring is +/// also [`ImaginaryUnit`], `conj(i) == −i`. +/// +/// Design: §"The map is a graded algebra over `C[K]`" (`Conjugate`). The +/// `conj(i) == −i` law is machine-checked in `lean/PPVM/Pauli/Matrix.lean` +/// (`star_iU`: `star iU = -iU`). +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..5b108b50c --- /dev/null +++ b/crates/ppvm-traits-2/src/arithmetic/coefficient.rs @@ -0,0 +1,80 @@ +// 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 + + MulAssign + + 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) + } + + /// Accumulates a borrowed coefficient. + #[inline] + fn add_assign_ref(&mut self, rhs: &Self) { + *self += rhs.clone(); + } + + /// 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 { + self.clone() + self.clone() + } + + /// 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..8c5cd32da --- /dev/null +++ b/crates/ppvm-traits-2/src/arithmetic/halvable.rs @@ -0,0 +1,26 @@ +// 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`. + 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..922eebfda --- /dev/null +++ b/crates/ppvm-traits-2/src/containers/batch.rs @@ -0,0 +1,460 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +use crate::containers::Indexable; +use crate::loss::LossState; +use crate::word::PauliBits; + +/// A key that can be laid out as a structure-of-arrays column. Separate from +/// [`Indexable`] so the minimal hashing contract is unchanged: a batched key is +/// both `Indexable` (a valid map key) and `Columnar` (has a column layout). +/// +/// Design: §"The batch contract". +pub trait Columnar: Indexable { + /// The concrete structure-of-arrays column for this key type. + type Column: KeyColumn; +} + +/// A structure-of-arrays column of keys, owned by the concrete key type (only it +/// knows its planes). Operates plane by plane, never scalar on the hot path. +/// +/// Design: §"The batch contract". +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. + /// + /// Live stores use this when cloning persistent workspaces: cloning only the + /// populated rows would silently discard a caller's capacity hint. + fn capacity(&self) -> usize; + + /// Whether the column is empty. + fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// A column pre-sized for `n` keys. + fn with_capacity(n: usize) -> Self; + + /// Append one produced key; the column keeps each plane contiguous. + fn push(&mut self, key: Self::Key); + + /// Reserve room for `additional` more keys, keeping the ones already stored. + /// + /// The column spelling of `Vec::reserve`, and the counterpart of + /// [`with_capacity`](Self::with_capacity) for a column that is a *live + /// support* rather than a throwaway batch: `ColumnStore`'s branch-merge pass + /// knows its worst-case append count up front (the scratch length), and + /// pre-sizing from it collapses a doubling chain of plane reallocations — + /// plus the parallel bucket-table `reindex`es — into one. + /// + /// **Default: a no-op.** Pre-sizing is a pure optimization, so a column that + /// cannot express it (a device-backed or fixed-extent one) stays legal and + /// simply reallocates on `push`. + #[inline] + fn reserve(&mut self, additional: usize) { + let _ = additional; + } + + /// 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; + + /// Read one row's X bit without requiring scalar materialization when the + /// concrete column can address its packed plane directly. + #[inline] + fn x_bit(&self, row: usize, qubit: usize) -> bool + where + Self::Key: PauliBits, + { + self.get(row).x_bit(qubit) + } + + /// Read one row's Z bit directly when supported. + #[inline] + fn z_bit(&self, row: usize, qubit: usize) -> bool + where + Self::Key: PauliBits, + { + self.get(row).z_bit(qubit) + } + + /// Read one row's loss bit directly when supported. + #[inline] + fn is_lost(&self, row: usize, qubit: usize) -> bool + where + Self::Key: LossState, + { + self.get(row).is_lost(qubit) + } + + /// Materialize one row while toggling selected bits. Packed columns can + /// build the branch key directly from their planes. + #[inline] + fn toggled_bits(&self, row: usize, qubit: usize, toggle_x: bool, toggle_z: bool) -> Self::Key + where + Self::Key: PauliBits, + { + self.get(row).toggled_bits(qubit, toggle_x, toggle_z) + } + + /// Materialize one row while toggling bits at two sites. + #[inline] + #[allow(clippy::too_many_arguments)] + fn toggled_bits2( + &self, + row: usize, + i: usize, + toggle_x_i: bool, + toggle_z_i: bool, + j: usize, + toggle_x_j: bool, + toggle_z_j: bool, + ) -> Self::Key + where + Self::Key: PauliBits, + { + self.get(row) + .toggled_bits2(i, toggle_x_i, toggle_z_i, j, toggle_x_j, toggle_z_j) + } + + /// Reset to an empty column, **keeping the backing plane allocations**. + /// + /// # Friction: a column that is a *store* needs in-place mutation, and the + /// batch-only surface has none + /// + /// [`gather`](Self::gather) allocates a fresh column, and the design's + /// batch contract needs nothing more: a `TermBatch`'s column is built by + /// `push` and thrown away. The `ColumnStore` backend + /// (implementation-plan Phase 6) makes the *same* column type the live + /// support, and every one of its buffer-reusing fast paths — the old + /// crate's `map_add` clear→write→swap (architecture feature 1), the retain + /// compaction, the in-place Clifford re-key — is defined by mutating a + /// column it already owns. Expressed through `gather` alone each of those + /// allocates a whole new key column **per gate**, which is exactly the + /// per-gate allocation churn the double-buffer exists to remove. + /// + /// So this trio ([`clear`](Self::clear), [`set`](Self::set), + /// [`truncate`](Self::truncate)) is the minimal in-place surface: they are + /// the column spellings of `Vec::clear`/`IndexMut`/`Vec::truncate`, they + /// stay plane-oriented (a SIMD/GPU column implements them as plane writes), + /// and they expose no `&mut Key` — so design rule 4 of §"Backends are + /// containers" ("no signature exposes `&mut (W, C)` or `&mut [C]`") is + /// untouched and the AoS layout still cannot leak. + fn clear(&mut self); + + /// Overwrite element `i` in place, keeping every other element and the + /// backing allocation. Panics (or debug-panics) if `i >= len()`. + /// + /// The write side of the in-place re-key: a Clifford conjugation is a + /// bijection, so a columnar backend rewrites the key planes at each slot and + /// leaves the parallel coefficient column completely untouched. + 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 plus their precomputed structural hashes, in parallel columns. The +/// probe side of the join; it carries no coefficients. +/// +/// See the module-level friction note: the key column is a scalar `Vec` +/// fallback rather than the design's `W::Column`, so the batch is expressible +/// for any `W: Eq + Clone` (not only `Columnar` keys). +/// +/// Design: §"The batch contract". +#[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`] with the coefficient column attached: the produced terms +/// awaiting merge. Coefficients are a separate column, touched only when a +/// probe resolves to an aggregate. +/// +/// Design: §"The batch contract". +#[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(); + } +} + +/// The append side of a term batch: a producer pushes `(key, coeff)` terms into +/// a sink, filling the key and coefficient columns. A naive sink collects into a +/// scalar `Vec`; a columnar sink appends into planes. +/// +/// Design: §"Every gate is a producer feeding `accumulate`". +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); + } +} + +/// A monomorphized, inlinable term producer — never `dyn`, since this is the +/// hot loop and the abstraction must compile to nothing. +/// +/// # `Send + Sync` is part of the contract (architecture feature 12) +/// +/// A producer is *read-only* over its own state (`produce` takes `&self`), so a +/// storage backend is free to split the produce walk across threads — which is +/// the whole point of keeping the backend a **configuration choice**: the old +/// crate bounded every driver closure `F: Fn(..) + Sync + Send` +/// (`ppvm-traits/src/map/hashmap.rs`, `map_add_assign`/`map_insert*`/`scale`) so +/// that a concurrent map (it shipped a `DashMap`-backed config benchmarked beside +/// the `HashMap`/`IndexMap` ones) was a **backend swap, not an engine rewrite**. +/// Requiring it here — and on the `ppvm-pauli-sum-2` in-place walk closures +/// (`ScaleByKey`/`SignFlipByKey`/`RekeyBijective`/`RotateInPlace`/ +/// `BranchInPlace`) — keeps that door open: widening the bound later would mean +/// touching every trait signature *and* every impl, i.e. exactly the coupling the +/// feature exists to prevent. Every real producer is a closure over gate indices +/// and ring elements, and `Coefficient` is already `Send + Sync`, so the bound +/// costs nothing today. +pub trait TermProducer: Send + Sync { + /// Push the produced terms for one existing `(key, coeff)` into the sink. + fn produce>(&self, key: &K, coeff: &C, sink: &mut S); +} + +#[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..8ba5232fc --- /dev/null +++ b/crates/ppvm-traits-2/src/containers/coordinate_list.rs @@ -0,0 +1,229 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! `Vec<(K, C)>` — the coordinate-list backend: an unsorted association list +//! scanned linearly, requiring only `K: Eq + Clone` (it never hashes). Best for +//! small support, e.g. the `GeneralizedTableau` amplitude vector. +//! +//! See [`super`] for the shared design references and the orphan-rule note. + +use crate::algebra::{Conjugate, ImaginaryUnit, KeyProduct}; +use crate::arithmetic::Coefficient; +use crate::containers::{Accumulate, Multiply, Pair, Retain, Scale, Support}; +use crate::containers::{KeyBatch, 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.clone(); + } 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.clone(); + } + } +} + +impl Pair for Vec<(K, C)> +where + K: Eq + Clone, + C: Coefficient, +{ + #[inline] + 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 = Support::get(self, k); + } + } + + #[inline] + fn overlap(&self, other: &Self) -> C { + self.as_slice() + .iter() + .filter_map(|(k, a)| Support::get(other, k).map(|b| a.clone() * b)) + .sum() + } + + #[inline] + fn hermitian_overlap(&self, other: &Self) -> C + where + C: Conjugate, + { + self.as_slice() + .iter() + .filter_map(|(k, a)| Support::get(other, k).map(|b| a.conj() * b)) + .sum() + } +} + +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, +{ + /// The twisted convolution `(A·B)[k] = Σ_{p·q = k} A[p]·B[q]·i^{β(p,q)}`, + /// accumulated into `acc` — the coordinate-list spelling of `twistedConv` + /// (`lean/PPVM/Algebra/Twisted.lean`), whose monomial case is `tmul`. + /// + /// Neither `reduce` nor any truncation runs: `acc` keeps an exact-zero + /// cancellation, exactly as `twistedConv` (a finitely-supported map is + /// canonicalized only by an explicit [`Accumulate::reduce`]). + 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 c = phase.apply(&(a.clone() * b.clone())); + 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..c691454d2 --- /dev/null +++ b/crates/ppvm-traits-2/src/containers/graded.rs @@ -0,0 +1,220 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! The graded map algebra over `C[K]`: the free `C`-module on a key set, +//! layered by algebraic strength. Each layer is a distinct trait justified by a +//! distinct algebraic property *and* a distinct consumer. +//! +//! Design: `traits-2-configuration-and-hashing.md` §"The map is a graded algebra +//! over `C[K]`". The layers are `Support` (L0, the container), `Accumulate` (L1, +//! the module core), `Scale` (L2, the `C`-module action), `Pair` (L3, the trace +//! pairings), and `Multiply` (L4, the ring product). [`Retain`] sits *outside* +//! the algebra: dropping supported terms breaks module exactness, so it is a +//! non-algebraic capability that `Policy` (in `ppvm-pauli-sum-2`) — not the +//! algebra — consumes. +//! +//! The key type is bounded on `Eq + Clone`, **not** [`crate::containers::Indexable`]: +//! `C[K]` is the free module over any index set, so the algebra needs only a +//! valid map key. Hash backends re-add `Indexable` on *their* impls; Pauli +//! propagation re-adds `Word`/`PauliBits` on *its* methods. + +use crate::algebra::{Conjugate, ImaginaryUnit, KeyProduct}; +use crate::arithmetic::Coefficient; +use crate::containers::{KeyBatch, TermBatch, TermSink}; + +/// L0 — the container: a finitely-supported function `K ⇀ C`. +/// +/// No `&mut (K, C)` and no `&mut [C]` slot access is exposed, so a columnar +/// (structure-of-arrays) backend is expressible; `iter` is read-only export. +/// +/// Design: §"The map is a graded algebra over `C[K]`" (L0 `Support`). +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; + + /// Read-only **borrowing** visit of the support: `f(&key, &coeff)` once per + /// supported key, in the backend's own order. + /// + /// This is the scan side of L0 for readers that *reject most terms* — a + /// pattern trace, a predicate count, a max-weight scan. [`iter`](Self::iter) + /// hands out owned pairs (so that a columnar backend can synthesize them), + /// which forces a `Coeff::clone()` on **every** key before the reader gets to + /// look at it. That clone is free for `f64` and ruinous for a coefficient + /// that owns a heap table: on the `sym.random.circuit` shape (support 65534, + /// 255 matching keys, symbolic coefficients) cloning the whole support before + /// filtering cost 7.06 ms against 1.2 ms for the old crate's borrowing fold, + /// and the gap *grew* with coefficient size. Old could stay generic here + /// because its map iterator borrowed; this method restores that read side + /// without giving up the columnar option — a SoA backend passes + /// `(&keys[i], &coeffs[i])`, which needs no materialized pair. + /// + /// The default body goes through `iter`, so this is a capability an existing + /// backend may ignore; every backend whose keys and coefficients are actually + /// *stored* should override it, and the shipped ones do. + /// + /// It is a scan, not an iterator, deliberately: returning + /// `impl Iterator` would force every backend to have a + /// lending iterator, and a callback composes with the `Retain`/`Policy` + /// closures already used on this side of the API. + fn for_each_ref(&self, mut f: impl FnMut(&Self::Key, &Self::Coeff)) { + for (k, c) in self.iter() { + f(&k, &c); + } + } +} + +/// L1 — the module core: form linear combinations, then canonicalize. +/// +/// Design: §"The map is a graded algebra over `C[K]`" (L1 `Accumulate`). The +/// module laws are machine-checked in `lean/PPVM/Algebra/GradedMap.lean` +/// (`accumulate_comm`, `accumulate_assoc`); `reduce` drops exactly the zero +/// coefficients (`reduce_structural`). +pub trait Accumulate: Support { + /// Build side of the hash join: merge a produced batch, accumulating onto an + /// existing key or inserting a new one. Columnar in. + /// + /// The batch is algebraically a **multiset** of terms — folding it in is a + /// homomorphism from the free commutative monoid — so an impl is free to + /// reorder it and to split it across partitions/threads: order-invariance + /// (`accumulateTerms_perm`) and partition-invariance + /// (`accumulateTerms_add`, `accumulateTerms B m` for `B = B1 + B2`) are + /// machine-checked in `lean/PPVM/Algebra/GradedMap.lean`. + 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); + + /// Scalar sugar over a batch of one — accumulate a single `(key, coeff)`. + /// + /// Design: §"The map is a graded algebra over `C[K]`" ("the scalar + /// `accumulate(k, c)` is provided sugar over a batch of one"); that the + /// singleton batch is the same operation is `accumulateTerms_singleton` in + /// `lean/PPVM/Algebra/GradedMap.lean`. + 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); + } +} + +/// L2 — the `C`-module action: a pure elementwise map over the coefficients. +/// +/// Design: §"The map is a graded algebra over `C[K]`" (L2 `Scale`). Machine- +/// checked in `lean/PPVM/Algebra/GradedMap.lean` (`scale_scale`, +/// `scale_accumulate`). +pub trait Scale: Support { + /// Multiply every coefficient by `s`: `∀ k. c_k *= s`. + fn scale(&mut self, s: &Self::Coeff); +} + +/// L3 — the read side of the hash join. Two pairings live here, differing only +/// in whether the first operand is conjugated. +/// +/// `overlap` is the **symmetric bilinear** Hilbert–Schmidt trace pairing +/// `⟨A, B⟩ = ∑_k a_k b_k` — bilinear, *not* conjugated. Full `C`-bilinearity +/// (biadditivity, homogeneity in each slot, and symmetry over a commutative +/// ring) is machine-checked in `lean/PPVM/Algebra/GradedMap.lean` +/// (`overlap_add_left`/`overlap_add_right`, `overlap_smul_left`/`overlap_smul_right`, +/// `overlap_comm`); Pauli-basis orthonormality is `overlap_single_single` in +/// `lean/PPVM/Algebra/Noise.lean`. The `= Tr(A B)/2ⁿ` reading is proved, not +/// asserted: `overlap_eq_trace_div` in `lean/PPVM/Pauli/Matrix.lean` gives +/// `Tr(Â B̂) = 2ⁿ · ⟨A, B⟩` for the genuine `2ⁿ×2ⁿ` operators over `ℤ[i]` +/// (via `trace_tensorPauli_mul`), and `twistedConv_apply_id` in +/// `lean/PPVM/Algebra/Twisted.lean` states the same fact inside `C[K]`: +/// `⟨A, B⟩` is the identity coefficient of the L4 product. +/// +/// `hermitian_overlap` is the **sesquilinear** inner product +/// `⟨φ | ψ⟩ = ∑_k conj(a_k)·b_k`, conjugate-linear in the first argument, so it +/// requires the coefficient ring to carry a [`Conjugate`]. Conjugate symmetry, +/// sesquilinearity, and `⟨f, f⟩ ≥ 0` are machine-checked in +/// `lean/PPVM/Algebra/GradedMap.lean` (`hermitianOverlap_conj_symm`, +/// `hermitianOverlap_smul_left`/`smul_right`, `hermitianOverlap_self_nonneg`). +/// +/// Design: §"The map is a graded algebra over `C[K]`" (L3 `Pair`). +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]); + + /// The symmetric bilinear trace pairing `∑_k a_k b_k`. + fn overlap(&self, other: &Self) -> Self::Coeff; + + /// The sesquilinear inner product `∑_k conj(a_k)·b_k`. + fn hermitian_overlap(&self, other: &Self) -> Self::Coeff + where + Self::Coeff: Conjugate; +} + +/// `tr(self · value)` against a *different* right-hand type. +/// +/// The old `ppvm_traits::Trace` (`ppvm-traits/src/traits/trace.rs`), ported +/// unchanged. It is **not** subsumed by [`Pair::overlap`]: `overlap` pairs a map +/// with another map of the *same* type, while `Trace` is the heterogeneous form +/// the old crate used to trace a word or a whole sum against a `PauliPattern` +/// (`ppvm-pauli-word/src/pattern/trace.rs`, `ppvm-pauli-sum/src/sum/trace.rs`), +/// which is why the right-hand type and the numeric `Output` are both free. +/// +/// Design: §"Compatibility with current names" (`Trace`). Its `-2` implementers +/// land with the pattern port; the same-type Pauli-sum case is `Pair::overlap`, +/// whose bilinearity is machine-checked in `lean/PPVM/Algebra/GradedMap.lean`. +pub trait Trace<'a, RHS: 'a> { + /// Numeric output of the trace. + type Output; + /// Compute `tr(self · value)`. + fn trace(&'a self, value: &'a RHS) -> Self::Output; +} + +/// L4 — the ring product. The only layer that needs the *key* to carry a +/// product; it stays optional and is not implemented for a key type that has +/// none. The Pauli product injects powers of `i`, so the coefficient must absorb +/// phase — bounded on [`ImaginaryUnit`], the minimal requirement. +/// +/// Design: §"The map is a graded algebra over `C[K]`" (L4 `Multiply`). +/// Associativity of the twisted product holds over any commutative ring with +/// `i⁴ = 1`, machine-checked in `lean/PPVM/Algebra/Twisted.lean` (`tmul_assoc`); +/// the basis-monomial product is `multiply_single` in +/// `lean/PPVM/Algebra/GradedMap.lean`. The whole-map product is `twistedConv` +/// (`lean/PPVM/Algebra/Twisted.lean`), whose identity-key coefficient is exactly +/// [`Pair::overlap`] (`twistedConv_apply_id`) — the spec tying L4 to L3. +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); +} + +/// The one non-algebraic map operation: keep only the terms a predicate +/// selects. Dropping supported terms breaks module exactness, so this lives +/// outside the graded algebra and is consumed by `Policy`, not the algebra. +/// +/// Design: §"The map is a graded algebra over `C[K]`" and §"Truncation" +/// (`Policy::truncate` bounds on `Retain`). The truncation error incurred is +/// bounded in `lean/PPVM/Algebra/Truncation.lean` (`l1_bound` over `ℝ`, +/// `l1_bound_abv` over any coefficient ring whose +/// [`Coefficient::magnitude`] is an +/// absolute value — the law that bound consumes). +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..15ae6d215 --- /dev/null +++ b/crates/ppvm-traits-2/src/containers/hash.rs @@ -0,0 +1,70 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Indexable keys and the identity pass-through hasher. +//! +//! Design: `traits-2-configuration-and-hashing.md` §"Indexable values" and +//! §"The pass-through storage contract". `Indexable` is *not* the universal key +//! bound (that is `Eq + Clone`); it is required only on the hash backends. + +use std::hash::{BuildHasher, Hash, Hasher}; + +/// A key whose finalized structural digest is first class. +/// +/// The digest is avalanche-quality in both the low bits (the hashbrown bucket) +/// and the top 7 (the control tag), so it can be consumed *directly* as the map +/// hash. Contracts: +/// +/// * `Hash for Self` is exactly `state.write_u64(self.key_hash())`; +/// * structurally equal keys return equal digests; and +/// * `KeyColumn::hash_into` reproduces this value bit for bit. +/// +/// This exposes the digest *value*, not the cache mechanics — there is no cache +/// type or invalidation hook in the contract. +/// +/// Design: §"Indexable values". +pub trait Indexable: Clone + Eq + Hash { + /// The finalized structural digest of this key. + fn key_hash(&self) -> u64; +} + +/// A pass-through `Hasher`: a key writes its already-finalized `key_hash()` as a +/// single `u64` and this hands it back verbatim, so the digest reaches +/// hashbrown untouched. +/// +/// Design: §"The pass-through storage contract". +#[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 + } +} + +/// `BuildHasher` for [`IdentityHasher`]; the storage aliases in +/// `ppvm-pauli-sum-2` bake this into their `HashMap` so `finish() == +/// key.key_hash()`. +/// +/// Design: §"The pass-through storage contract". +#[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..5657c2a77 --- /dev/null +++ b/crates/ppvm-traits-2/src/containers/hash_join.rs @@ -0,0 +1,235 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! `HashMap` — the hash-join backend: `accumulate` +//! is a probe-and-merge against the bucket table, requiring `K: Indexable` (the +//! direct structural digest, consumed pass-through through +//! [`IdentityBuildHasher`]). Best for large support, e.g. `PauliSum`. +//! +//! See [`super`] for the shared design references and the orphan-rule note. + +use std::collections::HashMap; + +use crate::algebra::{Conjugate, ImaginaryUnit, KeyProduct}; +use crate::arithmetic::Coefficient; +use crate::containers::{Accumulate, Multiply, Pair, Retain, Scale, Support}; +use crate::containers::{IdentityBuildHasher, Indexable}; +use crate::containers::{KeyBatch, 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() { + self.entry(k.clone()) + .and_modify(|e| *e += c.clone()) + .or_insert_with(|| 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.clone(); + } + } +} + +impl Pair for HashMap +where + K: Indexable, + C: Coefficient, +{ + #[inline] + 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 = HashMap::get(self, k).cloned(); + } + } + + /// `Σ_k self[k]·other[k]`, driven from the **smaller** support. + /// + /// The shared support is contained in both, so scanning either side and + /// probing the other is `O(1)` per candidate and yields the same pair set; + /// walking the smaller one makes the cost `O(min(|a|, |b|))` instead of + /// `O(|self|)`. Against old's `data().trace(k)`-per-term + /// (`ppvm-pauli-sum/src/sum/trace.rs`, a full linear scan of `self` per term + /// of `other` — anti-feature 13) this is the intended asymptotic improvement; + /// picking the smaller side is the second half of it, and it matters when the + /// operands are deliberately unequal (`|a| = 10⁵`, `|b| = 10`). + /// + /// The *value* is unchanged — the pairing is symmetric and the left factor + /// stays `self`'s coefficient either way — only the float **summation order** + /// depends on the direction, which is why the differential bar on `overlap` is + /// relative (`1e-12`) rather than bit-exact. + #[inline] + fn overlap(&self, other: &Self) -> C { + if self.len() <= other.len() { + HashMap::iter(self) + .filter_map(|(k, a)| HashMap::get(other, k).map(|b| a.clone() * b.clone())) + .sum() + } else { + HashMap::iter(other) + .filter_map(|(k, b)| HashMap::get(self, k).map(|a| a.clone() * b.clone())) + .sum() + } + } + + /// `Σ_k conj(self[k])·other[k]`, driven from the smaller support — see + /// [`Pair::overlap`] for why the direction is free to differ. + #[inline] + fn hermitian_overlap(&self, other: &Self) -> C + where + C: Conjugate, + { + if self.len() <= other.len() { + HashMap::iter(self) + .filter_map(|(k, a)| HashMap::get(other, k).map(|b| a.conj() * b.clone())) + .sum() + } else { + HashMap::iter(other) + .filter_map(|(k, b)| HashMap::get(self, k).map(|a| a.conj() * b.clone())) + .sum() + } + } +} + +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, +{ + /// The twisted convolution `(A·B)[k] = Σ_{p·q = k} A[p]·B[q]·i^{β(p,q)}`, + /// accumulated into `acc` through the hash join — `twistedConv` of + /// `lean/PPVM/Algebra/Twisted.lean` (monomial case `tmul`; associative by + /// `tmul_assoc`/`gtmul_assoc`, and `(A·B)[I] = ⟨A, B⟩` by + /// `twistedConv_apply_id`, tying L4 back to [`Pair::overlap`]). + /// + /// Every `(p, q)` pair contributes: the outer product is `O(|A|·|B|)` and is + /// accumulated **into a distinct `acc`**, never folded back into an operand. + /// (That is the bilinearity old's `MulAssign` loses — see + /// `crate::containers::Multiply` and `ppvm-pauli-sum-2::multiply`.) + /// + /// Neither `reduce` nor any truncation runs here: an exact-zero cancellation + /// stays in `acc`'s support. Canonicalization is the caller's explicit + /// [`Accumulate::reduce`], per §"`reduce()` is first-class, and runs only at + /// finalize". + 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 c = phase.apply(&(a.clone() * b.clone())); + // Warm the fresh product key's structural digest *before* the + // probe, for the reason `RotateInPlace` does: `key_mul` returns a + // key with an empty hash cache, and letting the finalize fold fire + // lazily inside `entry()` puts its mul-chain latency on the + // bucket-index critical path with nothing to hide it. Semantic + // no-op (identical digest). + let _ = k.key_hash(); + acc.entry(k).and_modify(|e| *e += c.clone()).or_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 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..90f65c9ad --- /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, hashing, and traversal preferences. +//! +//! The private backend modules implement these traits for `Vec` and `HashMap`. + +mod batch; +mod coordinate_list; +mod graded; +mod hash; +mod hash_join; +mod storage; + +pub use batch::{Columnar, KeyBatch, KeyColumn, TermBatch, TermProducer, TermSink}; +pub use graded::{Accumulate, Multiply, Pair, Retain, Scale, Support, Trace}; +pub use hash::{IdentityBuildHasher, IdentityHasher, Indexable}; +pub use storage::RekeyStrategy; diff --git a/crates/ppvm-traits-2/src/containers/storage.rs b/crates/ppvm-traits-2/src/containers/storage.rs new file mode 100644 index 000000000..f81d48d08 --- /dev/null +++ b/crates/ppvm-traits-2/src/containers/storage.rs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +/// Implemented by storage backends, independently of coefficient arithmetic. +/// Both traversals must preserve the same keys and coefficient values. +pub trait RekeyStrategy { + /// Prefer draining entries and moving coefficients when true. + /// When false, prefer borrowing entries and cloning coefficients. + const PREFER_MOVED_REKEY: bool = false; +} 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..0a19a8b22 --- /dev/null +++ b/crates/ppvm-traits-2/src/gates/channel.rs @@ -0,0 +1,257 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +use crate::arithmetic::Coefficient; + +/// Coefficients that can calculate single-qubit Pauli-channel factors. +/// +/// This optional channel capability keeps noise formulas out of scalar +/// arithmetic. Implementers may use the generic default or specialize it. +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; + let one = Self::one(); + [ + one.clone() - py.doubled() - pz.doubled(), + one.clone() - px.doubled() - py.doubled(), + one - px.doubled() - pz.doubled(), + ] + } +} + +impl PauliErrorFactors for f64 { + #[inline(always)] + fn pauli_error_factors([px, py, pz]: [Self; 3]) -> [Self; 3] { + [ + 1.0 - py * 2.0 - pz * 2.0, + 1.0 - px * 2.0 - py * 2.0, + 1.0 - px * 2.0 - pz * 2.0, + ] + } +} + +impl PauliErrorFactors for num::Complex { + #[inline(always)] + fn pauli_error_factors([px, py, pz]: [Self; 3]) -> [Self; 3] { + let one = Self::new(1.0, 0.0); + [ + one - py * 2.0 - pz * 2.0, + one - px * 2.0 - py * 2.0, + one - px * 2.0 - pz * 2.0, + ] + } +} + +/// 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) { + let zero = C::zero(); + self.pauli_error(qubit, [p, zero.clone(), 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) { + let zero = C::zero(); + self.pauli_error(qubit, [zero.clone(), p, 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) { + let zero = C::zero(); + self.pauli_error(qubit, [zero.clone(), 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); + } + } +} + +/// Apply the same single-qubit Pauli error channel uniformly to every qubit in +/// the system. +pub trait PauliErrorAll { + /// Apply the Pauli channel `p = [p_x, p_y, p_z]` to every qubit. + fn pauli_error_all(&mut self, p: [C; 3], rng: &mut R); +} + +/// 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. +pub trait CorrelatedLossChannel { + /// Apply a correlated loss channel to `qubit0` and `qubit1`. + /// + /// The three probabilities are: + /// * `p[0]`: losing both qubits simultaneously when both are in the qubit + /// subspace. + /// * `p[1]`: losing either one qubit when both are in the qubit subspace. + /// * `p[2]`: losing one qubit when the other has already been lost prior to + /// the channel. + 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 ("asymmetric") single-qubit loss channel: a qubit is lost from +/// `|0⟩` with probability `p0` and from `|1⟩` with probability `p1`. Unlike +/// [`LossChannel`], the total loss probability depends on the qubit's +/// populations, so the channel reads the current `⟨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..b45f404cd --- /dev/null +++ b/crates/ppvm-traits-2/src/gates/clifford.rs @@ -0,0 +1,298 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +use crate::pauli::{BlanketClifford, PhaseTrack, SymplecticColumns}; + +/// The Clifford gate set, applied in the Heisenberg picture. +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) + } +} + +/// Additional Clifford gates beyond the minimal set: `S†`, `√X`, `√X†`, `√Y`, +/// `√Y†`, and `CY`. +pub trait CliffordExtensions: Clifford { + /// 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`](CliffordExtensions::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. +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); + } + } +} + +pub trait CliffordExtensionsBatch: CliffordExtensions + CliffordBatch { + /// 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); + } + } +} + +impl Clifford for T { + #[inline] + fn x(&mut self, q: usize) { + self.x_phase(q); + } + + #[inline] + fn y(&mut self, q: usize) { + self.y_phase(q); + } + + #[inline] + fn z(&mut self, q: usize) { + self.z_phase(q); + } + + #[inline] + fn h(&mut self, q: usize) { + self.flip_phase_where_xz(q); + self.swap_xz(q); + } + + #[inline] + fn s(&mut self, q: usize) { + self.s_phase(q); + self.xor_z_from_x(q); + } + + #[inline] + fn cnot(&mut self, c: usize, t: usize) { + self.cnot_phase(c, t); + self.xor_x_col(c, t); + self.xor_z_col(t, c); + } + + #[inline] + fn cz(&mut self, a: usize, b: usize) { + self.cz_phase(a, b); + self.cz_bits(a, b); + } +} + +/// The derived [`CliffordExtensions`] behavior, blanket-implemented for the same +/// [`BlanketClifford`] opt-ins — the counterpart of the old crate's +/// `impl CliffordExtensions for T`. +/// +/// # Why generator products rather than new primitives +/// +/// The old blanket could write each extension gate as a raw bit rule because a +/// bare `PauliWordTrait` carries no phase, so the *only* content was the +/// `Sp(2n,2)` action. Here the blanket must also be correct for phase-carrying +/// opt-ins (`Tableau`), so each gate is expressed as a product of the audited +/// [`Clifford`] generators instead: the bit rule and the sign then follow from +/// generators whose signs are already machine-checked, rather than from six new +/// hand-written [`PhaseTrack`] deltas that would have to be re-proved (and +/// re-implemented by every word crate). +/// +/// Calls compose in the *backward* Heisenberg convention this crate uses +/// (`P ↦ U†PU`; `lean/PPVM/Pauli/Conjugation.lean`, `conjSdag`): applying `A` +/// then `B` conjugates by the operator product `A·B`. With that, and using +/// `S³ = S†`, `SZ = S³`: +/// +/// | gate | operator identity | call sequence | +/// |:---:|:---|:---| +/// | `s_dag` | `S† = S·Z` | `s`, `z` | +/// | `sqrt_x` | `√X ≃ H·S·H` | `h`, `s`, `h` | +/// | `sqrt_x_dag` | `√X† ≃ H·S†·H` | `h`, `s_dag`, `h` | +/// | `sqrt_y` | `√Y ≃ H·Z` | `h`, `z` | +/// | `sqrt_y_dag` | `√Y† ≃ Z·H` | `z`, `h` | +/// | `cy` | `CY = (I⊗S)·CNOT·(I⊗S†)` | `s(t)`, `cnot(c,t)`, `s_dag(t)` | +/// +/// Each row reproduces the old crate's bit rule **and** the old phased word's +/// sign formula exactly — pinned by `tests/phase1_gate_surface.rs` +/// (`blanket_clifford_extensions_match_old_conjugation_table`, +/// `blanket_cy_matches_old_two_qubit_table`), which replays the gates on a +/// ℤ₄-phased stub and checks the full `s`/`s_dag`/`√X`/`√X†`/`√Y`/`√Y†` +/// conjugation table plus the 16-entry `CY` table of `ppvm-traits`; by the +/// `ppvm-conformance-2` differential suites against the old crate; and, for the +/// gate identities themselves, by the `ℤ[i]` matrix oracle +/// (`phased_pauli_word_lean.rs`). +/// +/// The composition itself — the step the stub tests cannot check for a +/// phase-*carrying* opt-in — is machine-checked in +/// `lean/PPVM/Pauli/Conjugation.lean`, where each row above is *defined* as the +/// product of the audited generator homs (the crate's backward `s` is `conjSdag` +/// there) and is therefore a `MonoidHom` for free: `extSdag`/`extSqrtX`/ +/// `extSqrtXdag`/`extSqrtY`/`extSqrtYdag` (+ `extSdagHom`… `MonoidHom.comp`s), +/// with the tables as `extSdag_eq_conjS`, `extSqrtX_X`/`_Y`/`_Z`, …, the +/// dagger-inverse pairs as `extSqrtXdag_extSqrtX`/`extSqrtYdag_extSqrtY`/ +/// `extSdag_conjSdag`, and `extSqrtX_sq`/`extSqrtY_sq` for `√X² = X`, `√Y² = Y`. +/// `cy` is the same on `𝒫₂`: `conjCY` (= `conjST ∘ conjCNOT ∘ conjSdagT`, with +/// `conjCY_calls` collapsing the literal four-primitive sequence and +/// `conjCYHom` the hom), whose `conjCY_bits` + `conjCY_sign` *are* the old +/// 16-entry table. Corollary — every composite delta is still real +/// (`extSqrtX_isRealPhase`, …, `conjCY_isRealPhase`), so the `±1` drain in +/// `ppvm-pauli-sum-2` stays total on the extension gates too. +/// +/// # Loss guard +/// +/// A lossy word implements the guard inside its column primitives ("a gate +/// touching a lost qubit is a no-op"), so the single-qubit rows above inherit it +/// unchanged. `cy` is the one case worth stating: with a **lost control and a +/// present target**, the old whole-gate skip did nothing, while the decomposition +/// still runs `s(t)` and `s_dag(t)`. Those two share the `z ⊕= x` bit map and are +/// inverse conjugations, so they cancel exactly and the word is left untouched — +/// verified against the old reference over the full 25-word lossy alphabet in +/// `ppvm-conformance-2::lossy_pauli_word_diff`, and proven on every loss +/// configuration by `sActL_cnotActL_sActL_eq_cyActL` +/// (`lean/PPVM/Pauli/Symplectic.lean`): the guarded composite equals the old +/// crate's atomic whole-gate skip `cyActL`. The phase half of the same +/// cancellation, for a phase-carrying opt-in, is `conjS_conjSdag`/ +/// `conjSdag_conjS` (`lean/PPVM/Pauli/Conjugation.lean`). +/// +/// A concrete type that wants the old fused single-pass cost (the `Tableau`'s +/// per-gate bit-plane sweep) opts out of [`BlanketClifford`] and writes its own +/// `impl CliffordExtensions`, exactly as `Phased` does for [`Clifford`]. +impl CliffordExtensions for T { + #[inline] + fn s_dag(&mut self, q: usize) { + self.s(q); + self.z(q); + } + + #[inline] + fn sqrt_x(&mut self, q: usize) { + self.h(q); + self.s(q); + self.h(q); + } + + #[inline] + fn sqrt_x_dag(&mut self, q: usize) { + self.h(q); + self.s_dag(q); + self.h(q); + } + + #[inline] + fn sqrt_y(&mut self, q: usize) { + self.h(q); + self.z(q); + } + + #[inline] + fn sqrt_y_dag(&mut self, q: usize) { + self.z(q); + self.h(q); + } + + #[inline] + fn cy(&mut self, control: usize, target: usize) { + self.s(target); + self.cnot(control, target); + self.s_dag(target); + } +} 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..0745284ba --- /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, CliffordExtensions}; + +/// 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 + CliffordExtensions { + /// 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..4a8355428 --- /dev/null +++ b/crates/ppvm-traits-2/src/gates/mod.rs @@ -0,0 +1,18 @@ +// 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, PauliErrorAll, PauliErrorFactors, ResetLossChannel, + TwoQubitPauliError, +}; +pub use clifford::{Clifford, CliffordBatch, CliffordExtensions, CliffordExtensionsBatch}; +pub use measure::{Measure, Projection, Reset}; +pub use rot::{CRx, RotXY, RotationOne, RotationTwo, 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..1d391cb04 --- /dev/null +++ b/crates/ppvm-traits-2/src/gates/rot.rs @@ -0,0 +1,248 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + arithmetic::{Angle, Coefficient}, + pauli::Pauli, +}; + +/// Single-qubit rotations parameterized by an angle domain `A` that yields +/// amplitudes in coefficient domain `C`. +/// +/// The angle defaults to the coefficient (`A = C`), recovering today's +/// `rx(theta: C)` while permitting a symbolic/parametric angle over an +/// `f64`-coefficient sum. +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) + } + + /// 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); + } + + /// 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))`. +/// +/// The in-plane axis is `X` rotated about `Z` by `axis_angle`, so +/// `R(axis_angle, θ) = RZ(axis_angle)·RX(θ)·RZ(−axis_angle)` +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..4c9911fb1 --- /dev/null +++ b/crates/ppvm-traits-2/src/lib.rs @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Shared arithmetic, word, gate, and container interfaces. +//! +//! Includes small algebraic types, default gate implementations, and +//! container implementations for `Vec` and `HashMap`. + +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, + Multiply, Pair, RekeyStrategy, Retain, Scale, Support, TermBatch, TermProducer, TermSink, + Trace, +}; +pub use gates::{ + AmplitudeDamping, AsymmetricLossChannel, CRx, Clifford, CliffordBatch, CliffordExtensions, + CliffordExtensionsBatch, CorrelatedLossChannel, Depolarizing, Depolarizing2, LossChannel, + Measure, PauliError, PauliErrorAll, PauliErrorFactors, Projection, Reset, ResetLossChannel, + RotXY, RotationOne, RotationTwo, TGate, TwoQubitPauliError, U3Gate, +}; +pub use loss::LossState; +pub use pauli::{BlanketClifford, Pauli, PhaseTrack, StabilizerFrame, SymplecticColumns}; +pub use word::{PauliBits, Word}; + +/// Common traits and types for implementing and using the interfaces. +pub mod prelude { + pub use crate::{ + Accumulate, AmplitudeDamping, Angle, AsymmetricLossChannel, BlanketClifford, CRx, Clifford, + CliffordBatch, CliffordExtensions, CliffordExtensionsBatch, Coefficient, Columnar, + Conjugate, CorrelatedLossChannel, Depolarizing, Depolarizing2, Halvable, + IdentityBuildHasher, IdentityHasher, ImaginaryUnit, Indexable, KeyBatch, KeyColumn, + KeyProduct, LossChannel, LossState, Measure, Multiply, Pair, Pauli, PauliBits, PauliError, + PauliErrorAll, PauliErrorFactors, Phase, PhaseTrack, Projection, RekeyStrategy, Reset, + ResetLossChannel, Retain, RotXY, RotationOne, RotationTwo, Scale, StabilizerFrame, Support, + SymplecticColumns, TGate, TermBatch, TermProducer, TermSink, Trace, TwoQubitPauliError, + U3Gate, 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..15f95a20d --- /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. LossyPauliWord overrides 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..e8b32e9c6 --- /dev/null +++ b/crates/ppvm-traits-2/src/pauli.rs @@ -0,0 +1,95 @@ +// 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, +} +/// `Sp`-part: bit-plane column algebra, written **once** and shared by +/// `PhasedPauliWord` (1-bit columns) and `Tableau` (SIMD blocks over its `2n` +/// rows). Same meaning, different width. No phase — this is the role-independent +/// symplectic action. +/// +/// Design: §"Pauli algebra traits". The bit rules realize the per-generator +/// `Sp(2n, 2)` isometries of `lean/PPVM/Pauli/Symplectic.lean` +/// (`hAct_isometry`/`sAct_isometry`/`cnotAct_isometry`/`czAct_isometry`). +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` on `q`: `z_q ⊕= x_q` (maps `X → Y`). + /// + /// (Completes the design's abbreviated `// ...`; see the module friction + /// note.) + 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 rule on `(a, b)`: `z_a ⊕= x_b` and `z_b ⊕= x_a`. + /// + /// (Completes the design's abbreviated `// ...`; see the module friction + /// note.) + 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); +} + +/// Role-*exclusive* operations that interpret the rows as a symplectic basis +/// rather than as independent operators. A tableau-only trait a word never +/// implements. Holds the frame **primitives**, not `measure` itself — the two +/// measurement algorithms are built *on* these. +pub trait StabilizerFrame { + /// Find a generator that anticommutes with the measured Pauli (the pivot). + fn anticommuting_pivot(&self, qubit: usize) -> Option; + + /// Multiply generator `src` into `dst` (uses the Aaronson–Gottesman + /// `g`-rule). + fn row_multiply(&mut self, src: usize, dst: usize); + + /// Restore canonical form after elimination. + fn canonicalize(&mut self); +} + +pub trait BlanketClifford {} diff --git a/crates/ppvm-traits-2/src/word.rs b/crates/ppvm-traits-2/src/word.rs new file mode 100644 index 000000000..e9dfd0d25 --- /dev/null +++ b/crates/ppvm-traits-2/src/word.rs @@ -0,0 +1,214 @@ +// 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 according to the concrete site alphabet. + /// + /// A Pauli-motivated read (the `MaxPauliWeight` policy needs it); an ordered + /// representation that stores no 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}`. +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. + /// + /// The default composes the scalar setters. Packed words whose setters + /// refresh structural metadata eagerly may override this to refresh once. + #[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 packed bit planes at two sites. + /// + /// The default composes the one-site setter. Packed words with eager + /// structural metadata can override this to refresh exactly once after the + /// four writes. + #[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-plane bit and one Z-plane bit as one structural mutation. + /// + /// `CNOT` updates exactly this pair (`x_target`, `z_control`). Packed words + /// with eager structural metadata can override this to refresh once without + /// also reading or rewriting the two unchanged companion bits. + #[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-plane bits as one structural mutation. + /// + /// `CZ` updates exactly this pair (`z_a`, `z_b`) and leaves both X bits + /// alone — the counterpart of [`set_x_bit_and_z_bit`](PauliBits::set_x_bit_and_z_bit) + /// for `CNOT`. Routing it through the four-bit + /// [`set_xz_bits2`](PauliBits::set_xz_bits2) instead makes a packed word read + /// and rewrite the two unchanged X bits on every gate. The default composes + /// the scalar setters. + #[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) + } + + /// A copy of this word with the X and/or Z bit at `i` toggled — the + /// **rotation-branch key builder** (`iGP` from a diagonal `P`). + /// + /// Provided as clone-then-flip so every `PauliBits` implementer gets a branch + /// builder for free and the rotation/branching kernels can be generic over the + /// word type (the ordinary and the lossy key run the *same* kernel — see + /// `ppvm-pauli-sum-2`'s rotation and loss modules). `PauliWord` overrides it + /// with a direct plane copy that computes the digest exactly once, skipping + /// the redundant refresh a `clone` + `set_*_bit` pair performs. + fn toggled_bits(&self, i: usize, toggle_x: bool, toggle_z: bool) -> Self + where + Self: Sized + Clone, + { + let mut out = self.clone(); + if toggle_x { + let b = out.x_bit(i); + out.set_x_bit(i, !b); + } + if toggle_z { + let b = out.z_bit(i); + out.set_z_bit(i, !b); + } + out + } + + /// A copy of this word with the X and/or Z bits at **two** sites toggled — + /// the **two-qubit** rotation-branch key builder (`iG_aG_b·P`). + /// + /// Chaining [`toggled_bits`](PauliBits::toggled_bits) twice would build two + /// whole words per produced branch term, and for a packed word each of those is + /// a full copy of *both* bit planes plus a word rebuild — so the intermediate + /// is pure waste on the hot path of `rzz`/`rxx`/`ryy`/`rotate_2` (old built + /// **one** `k.clone()` and then wrote four bits into it, + /// `ppvm-pauli-sum/src/sum/rot2.rs`). The two-site entry point makes the single + /// copy the *only* copy, and it scales with the storage tier: at `[u8; 32]` the + /// chained form moved 64 redundant bytes per branch term. + /// + /// The default is clone-then-flip (one clone, up to four bit writes), which is + /// already old's shape; `PauliWord` overrides it with a direct plane copy that + /// computes the digest exactly once, as it does for the single-site form. + #[inline] + #[allow(clippy::too_many_arguments)] + fn toggled_bits2( + &self, + i: usize, + toggle_x_i: bool, + toggle_z_i: bool, + j: usize, + toggle_x_j: bool, + toggle_z_j: bool, + ) -> Self + where + Self: Sized + Clone, + { + let mut out = self.clone(); + if toggle_x_i { + let b = out.x_bit(i); + out.set_x_bit(i, !b); + } + if toggle_z_i { + let b = out.z_bit(i); + out.set_z_bit(i, !b); + } + if toggle_x_j { + let b = out.x_bit(j); + out.set_x_bit(j, !b); + } + if toggle_z_j { + let b = out.z_bit(j); + out.set_z_bit(j, !b); + } + out + } + + /// Consume a word and toggle X/Z bits at two sites in place. + /// + /// Re-keying kernels already own their key, so this avoids the structural + /// copy required by [`toggled_bits2`](PauliBits::toggled_bits2). Packed words + /// may override it to defer derived-hash refresh until all writes complete. + #[inline] + #[allow(clippy::too_many_arguments)] + fn into_toggled_bits2( + mut self, + i: usize, + toggle_x_i: bool, + toggle_z_i: bool, + j: usize, + toggle_x_j: bool, + toggle_z_j: bool, + ) -> Self + where + Self: Sized, + { + if toggle_x_i { + let b = self.x_bit(i); + self.set_x_bit(i, !b); + } + if toggle_z_i { + let b = self.z_bit(i); + self.set_z_bit(i, !b); + } + if toggle_x_j { + let b = self.x_bit(j); + self.set_x_bit(j, !b); + } + if toggle_z_j { + let b = self.z_bit(j); + self.set_z_bit(j, !b); + } + self + } + + /// Whether this word anticommutes with the single-qubit Pauli + /// `pauli = (x_bit, z_bit)` at index `i`, i.e. whether the symplectic form + /// `ω(P, Q) = x_P·z_Q ⊕ z_P·x_Q` is `1` there. + /// + /// The old `PauliWordTrait::anticommutes_at` + /// (`ppvm-traits/src/traits/word_trait.rs`), reproduced verbatim as a + /// provided method — it is the pivot test the tableau measurement search + /// runs (`ppvm-tableau/src/data.rs`), and it is derivable from the two bit + /// reads, so it needs no new required method. + #[inline] + fn anticommutes_at(&self, i: usize, pauli: (bool, bool)) -> bool { + (self.x_bit(i) & pauli.1) ^ (self.z_bit(i) & pauli.0) + } +} From fab45e235a7c131f1b3f58de5d1048cb3dc0a46d Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Thu, 10 Sep 2026 10:31:42 -0400 Subject: [PATCH 02/17] added ppvm-traits-2 to workspace and forgot to factor back in fermionAction and FermionSite --- Cargo.lock | 8 ++++++++ Cargo.toml | 1 + crates/ppvm-traits-2/src/fermion_factor.rs | 19 +++++++++++++++++++ crates/ppvm-traits-2/src/lib.rs | 4 +++- 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 crates/ppvm-traits-2/src/fermion_factor.rs 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/src/fermion_factor.rs b/crates/ppvm-traits-2/src/fermion_factor.rs new file mode 100644 index 000000000..e3db71bf3 --- /dev/null +++ b/crates/ppvm-traits-2/src/fermion_factor.rs @@ -0,0 +1,19 @@ +/// The action a fermionic factor performs on its mode. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum FermionAction { + /// A creation operator `a†`. + Create, + /// An annihilation operator `a`. + Annihilate, +} + +/// One factor of an ordered fermionic product — the alphabet of a future +/// ordered fermionic word (`Word`), whose index denotes +/// factor order while the site carries the physical mode. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct FermionSite { + /// The physical mode this factor acts on. + pub mode: usize, + /// Whether this factor creates or annihilates. + pub action: FermionAction, +} diff --git a/crates/ppvm-traits-2/src/lib.rs b/crates/ppvm-traits-2/src/lib.rs index 4c9911fb1..3f7b0d76f 100644 --- a/crates/ppvm-traits-2/src/lib.rs +++ b/crates/ppvm-traits-2/src/lib.rs @@ -13,6 +13,7 @@ pub mod gates; pub mod loss; pub mod pauli; pub mod word; +pub mod fermion_factor; pub use algebra::{Conjugate, ImaginaryUnit, KeyProduct, Phase}; pub use arithmetic::{Angle, Coefficient, Halvable}; @@ -30,6 +31,7 @@ pub use gates::{ pub use loss::LossState; pub use pauli::{BlanketClifford, Pauli, PhaseTrack, StabilizerFrame, SymplecticColumns}; pub use word::{PauliBits, Word}; +pub use fermion_factor::{FermionSite, FermionAction}; /// Common traits and types for implementing and using the interfaces. pub mod prelude { @@ -42,6 +44,6 @@ pub mod prelude { PauliErrorAll, PauliErrorFactors, Phase, PhaseTrack, Projection, RekeyStrategy, Reset, ResetLossChannel, Retain, RotXY, RotationOne, RotationTwo, Scale, StabilizerFrame, Support, SymplecticColumns, TGate, TermBatch, TermProducer, TermSink, Trace, TwoQubitPauliError, - U3Gate, Word, + U3Gate, Word, FermionAction, FermionSite }; } From ebb25e0607a1a502a0e22244981b4c0d2ebfaf92 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Thu, 10 Sep 2026 10:37:21 -0400 Subject: [PATCH 03/17] ran cargo fmt --- crates/ppvm-traits-2/src/lib.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/ppvm-traits-2/src/lib.rs b/crates/ppvm-traits-2/src/lib.rs index 3f7b0d76f..6008a250f 100644 --- a/crates/ppvm-traits-2/src/lib.rs +++ b/crates/ppvm-traits-2/src/lib.rs @@ -9,11 +9,11 @@ pub mod algebra; pub mod arithmetic; pub mod containers; +pub mod fermion_factor; pub mod gates; pub mod loss; pub mod pauli; pub mod word; -pub mod fermion_factor; pub use algebra::{Conjugate, ImaginaryUnit, KeyProduct, Phase}; pub use arithmetic::{Angle, Coefficient, Halvable}; @@ -22,6 +22,7 @@ pub use containers::{ Multiply, Pair, RekeyStrategy, Retain, Scale, Support, TermBatch, TermProducer, TermSink, Trace, }; +pub use fermion_factor::{FermionAction, FermionSite}; pub use gates::{ AmplitudeDamping, AsymmetricLossChannel, CRx, Clifford, CliffordBatch, CliffordExtensions, CliffordExtensionsBatch, CorrelatedLossChannel, Depolarizing, Depolarizing2, LossChannel, @@ -31,19 +32,18 @@ pub use gates::{ pub use loss::LossState; pub use pauli::{BlanketClifford, Pauli, PhaseTrack, StabilizerFrame, SymplecticColumns}; pub use word::{PauliBits, Word}; -pub use fermion_factor::{FermionSite, FermionAction}; /// Common traits and types for implementing and using the interfaces. pub mod prelude { pub use crate::{ Accumulate, AmplitudeDamping, Angle, AsymmetricLossChannel, BlanketClifford, CRx, Clifford, CliffordBatch, CliffordExtensions, CliffordExtensionsBatch, Coefficient, Columnar, - Conjugate, CorrelatedLossChannel, Depolarizing, Depolarizing2, Halvable, - IdentityBuildHasher, IdentityHasher, ImaginaryUnit, Indexable, KeyBatch, KeyColumn, - KeyProduct, LossChannel, LossState, Measure, Multiply, Pair, Pauli, PauliBits, PauliError, - PauliErrorAll, PauliErrorFactors, Phase, PhaseTrack, Projection, RekeyStrategy, Reset, - ResetLossChannel, Retain, RotXY, RotationOne, RotationTwo, Scale, StabilizerFrame, Support, - SymplecticColumns, TGate, TermBatch, TermProducer, TermSink, Trace, TwoQubitPauliError, - U3Gate, Word, FermionAction, FermionSite + Conjugate, CorrelatedLossChannel, Depolarizing, Depolarizing2, FermionAction, FermionSite, + Halvable, IdentityBuildHasher, IdentityHasher, ImaginaryUnit, Indexable, KeyBatch, + KeyColumn, KeyProduct, LossChannel, LossState, Measure, Multiply, Pair, Pauli, PauliBits, + PauliError, PauliErrorAll, PauliErrorFactors, Phase, PhaseTrack, Projection, RekeyStrategy, + Reset, ResetLossChannel, Retain, RotXY, RotationOne, RotationTwo, Scale, StabilizerFrame, + Support, SymplecticColumns, TGate, TermBatch, TermProducer, TermSink, Trace, + TwoQubitPauliError, U3Gate, Word, }; } From 5e2e77967c163901e9cd6c5d0c34d0a333666581 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Thu, 10 Sep 2026 10:40:01 -0400 Subject: [PATCH 04/17] adding missing header to fermion_factor --- crates/ppvm-traits-2/src/fermion_factor.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/ppvm-traits-2/src/fermion_factor.rs b/crates/ppvm-traits-2/src/fermion_factor.rs index e3db71bf3..fc8abacd3 100644 --- a/crates/ppvm-traits-2/src/fermion_factor.rs +++ b/crates/ppvm-traits-2/src/fermion_factor.rs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + /// The action a fermionic factor performs on its mode. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum FermionAction { From 17d676e221e0a0e860df5f6808efd05cf90cc7e3 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Wed, 16 Sep 2026 12:02:05 -0400 Subject: [PATCH 05/17] condensed comments to maximum 3 lines or so --- crates/ppvm-traits-2/src/algebra.rs | 158 +++--------------- crates/ppvm-traits-2/src/containers/batch.rs | 109 +++--------- .../src/containers/coordinate_list.rs | 16 +- crates/ppvm-traits-2/src/containers/graded.rs | 154 +++-------------- crates/ppvm-traits-2/src/containers/hash.rs | 36 +--- .../ppvm-traits-2/src/containers/hash_join.rs | 52 ++---- crates/ppvm-traits-2/src/gates/channel.rs | 23 +-- crates/ppvm-traits-2/src/gates/clifford.rs | 77 +-------- crates/ppvm-traits-2/src/gates/rot.rs | 15 +- crates/ppvm-traits-2/src/lib.rs | 4 +- crates/ppvm-traits-2/src/pauli.rs | 26 +-- crates/ppvm-traits-2/src/word.rs | 83 +++------ 12 files changed, 129 insertions(+), 624 deletions(-) diff --git a/crates/ppvm-traits-2/src/algebra.rs b/crates/ppvm-traits-2/src/algebra.rs index 181724b6d..434c45bf8 100644 --- a/crates/ppvm-traits-2/src/algebra.rs +++ b/crates/ppvm-traits-2/src/algebra.rs @@ -1,25 +1,13 @@ // SPDX-FileCopyrightText: 2026 The PPVM Authors // SPDX-License-Identifier: Apache-2.0 -//! Algebra capabilities that lift `C[K]` from a module to a (twisted) algebra: -//! the key product [`KeyProduct`] and the two coefficient-ring capabilities it -//! and the sesquilinear pairing need, [`ImaginaryUnit`] and [`Conjugate`], plus -//! the [`Phase`] the Pauli key product emits. -//! -//! Design: `traits-2-configuration-and-hashing.md` §"The map is a graded -//! algebra over `C[K]`" (L4 and its coefficient capabilities). +//! 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ᵏ`, `k ∈ ℤ/4`, i.e. an element of `{1, i, −1, −i}`. -/// -/// The Pauli key product is not closed on keys: `v·w = iᵏ (v⊕w)`, so -/// [`KeyProduct::key_mul`] returns the residual `Phase` for the coefficient to -/// absorb. `iᵏ` already spans `{1, i, −1, −i}`, so no separate `±` is carried. -/// -/// Design: §"The map is a graded algebra over `C[K]`" (`KeyProduct`). The -/// exponent `k` is the packed `phaseExp` of `lean/PPVM/Pauli/Phase.lean` -/// (`phaseExp_eq_ref`). +/// 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`. @@ -55,22 +43,14 @@ impl Phase { } } - /// The group identity `i⁰ = +1` of the `ℤ/4` phase group. - /// - /// The neutral element for [`compose`](Self::compose): `p.compose(one()) == - /// p` for every phase `p`. + /// The identity phase: `p.compose(Self::one()) == p`. #[inline] pub fn one() -> Self { Phase::Pos1 } - /// The `ℤ/4` group product `iᵃ · iᵇ = i^{a+b}` (exponents added mod 4). - /// - /// This is the first-class group operation on phases: a `KeyProduct` chain - /// can accumulate the residual phases each [`KeyProduct::key_mul`] emits with - /// `compose` (or the equivalent [`Mul`](std::ops::Mul) impl) without a coefficient in hand, - /// deferring the [`apply`](Self::apply) fold onto the coefficient to the end. - /// The group is abelian, so `a.compose(b) == b.compose(a)`. + /// 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()) @@ -85,29 +65,9 @@ impl Phase { Phase::from_exponent(4 - self.exponent()) } - /// Fold this phase onto a coefficient: return `iᵏ · c`. - /// - /// This is the `iPow` fold of `lean/PPVM/Algebra/Twisted.lean` — the phase a - /// [`KeyProduct::key_mul`] emits is absorbed by the coefficient here. Needs a - /// primitive fourth root of unity, hence the [`ImaginaryUnit`] bound. - /// - /// # Behaviour parity - /// - /// The `±i` arms go through [`ImaginaryUnit::mul_i`], **not** through a bare - /// `c * imaginary_unit()`. On `Complex` the two are *not* the same - /// function: the ring multiply computes `(re·0 − im·1, re·1 + im·0)`, and - /// `inf·0`/`NaN·0` are `NaN`, so `(inf + 0i)·i` is `NaN + inf·i` while the - /// old `ppvm_traits::ComplexCoefficient::mul_phase` — which swapped the - /// components by hand — gave `−0 + inf·i`. `mul_i` restores the old - /// component swap (and with it the sign of zero, visible through `Display` - /// and serialization). See `phase_apply_matches_old_mul_phase_encoding`. - /// - /// The whole fold is delegated to [`ImaginaryUnit::mul_i_pow`], which is an - /// **override point**: a ring whose values carry `iᵏ` symbolically (the - /// symbolic `Term` of `ppvm-sym-2`) folds `iᵏ` into its own representation - /// rather than through the `±1`/`mul_i` arms, which is what old's - /// `ComplexCoefficient::mul_phase` did. The default body *is* those arms, so - /// nothing changes for `f64`/`Complex`/`GaussianInt`. + /// 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()) @@ -132,88 +92,33 @@ impl core::ops::MulAssign for Phase { } } -/// A key whose set carries a product — the (projective) group structure that -/// lifts `C[K]` from a module to an algebra. -/// -/// The keys form a group only *up to phase*: the product is **not closed on -/// keys**, it emits an `iᵏ`, which is why `key_mul` returns `(Self, Phase)` and -/// why `C[PauliWord]` is a **2-cocycle-twisted** group algebra. -/// -/// # Laws -/// -/// Write `key_mul(u, v) = (u · v, i^{β(u,v)})`. Every impl must satisfy: -/// -/// * the key product is associative: `(u · v) · w == u · (v · w)`; -/// * the phase exponent is a **2-cocycle**: -/// `β(u,v) + β(u·v, w) == β(v,w) + β(u, v·w)` in `ℤ/4`. -/// -/// Under exactly those two hypotheses the twisted product on `C × K` is -/// associative for any commutative coefficient ring `C` with `i⁴ = 1` — proved -/// key-agnostically in `lean/PPVM/Algebra/Twisted.lean` (`gtmul_assoc`, over an -/// abstract `kmul` and `IsCocycle`), so the obligation is stated once and every -/// key discharges it. `PauliWord` does so via `Bool.xor_assoc` and -/// `lean/PPVM/Pauli/Phase.lean` (`phaseExp_cocycle`), recovered as the instance -/// in `phaseExp_isCocycle` / `tmul_assoc_of_gtmul` (with `tmul_assoc` the -/// concrete Pauli statement). A future ordered fermionic-word key must discharge -/// the same two hypotheses; it does **not** inherit associativity from the Pauli -/// proof. -/// -/// Design: §"The map is a graded algebra over `C[K]`" (`KeyProduct`). +/// 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); } -/// The phase capability L4 needs, over a **commutative** coefficient ring: a -/// distinguished primitive fourth root of unity `i`. -/// -/// Impls must satisfy `Self::imaginary_unit() * Self::imaginary_unit() == -/// -Self::one()` (hence `i⁴ = 1`). This is strictly weaker than requiring -/// `Complex`: `GaussianInt` (`ℤ[i]`), `Complex`, and cyclotomic -/// integers all satisfy it, so L4 does not foreclose exact Pauli multiplication. -/// -/// Design: §"The map is a graded algebra over `C[K]`" (`ImaginaryUnit`). Law -/// machine-checked in `lean/PPVM/Pauli/Matrix.lean` (`iU_sq`: `iU * iU = -1`), -/// and the twisted product is associative over any commutative ring with -/// `i⁴ = 1` in `lean/PPVM/Algebra/Twisted.lean` (`tmul_assoc`). +/// 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`. Semantically `self * imaginary_unit()`, which is the - /// default body — but it is an **override point**, because on a - /// floating-point ring the generic product is not extensionally equal to the - /// rotation it denotes. - /// - /// On `Complex`, `c * i` expands to - /// `(re·0 − im·1, re·1 + im·0)`; `inf·0` and `NaN·0` are `NaN`, so a - /// non-finite component contaminates *both* output components, and `re·0` - /// also loses the sign of zero. Multiplication by `i` is really the - /// component swap `(re, im) ↦ (−im, re)`, which is total and exact — this is - /// what the old `ppvm_traits::ComplexCoefficient::mul_phase` did, and the - /// `Complex` impl below restores it verbatim. It is also cheaper (two - /// negations instead of four multiplies and two adds). + /// 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ᵏ` (`k` taken mod 4) — the fold [`Phase::apply`] delegates - /// to, and the second **override point**. - /// - /// The default body is the four-arm `{clone, mul_i, neg, neg∘mul_i}` fold, - /// which is the only sensible spelling on a ring whose values are numbers. - /// It is overridable because a ring whose values carry the `iᵏ` *as data* — - /// the symbolic `Term` of `ppvm-sym-2`, whose monomials hold a `ℤ/4` phase - /// byte — must fold the phase into that representation instead: old's - /// `ComplexCoefficient::mul_phase` promoted `Const(c)` to `One(i⁰, c)` - /// **unconditionally**, including at `k = 0`, and `Term`'s `PartialEq` and - /// `Display` are representational, so taking the `clone()` arm at `k = 0` - /// would be a user-visible divergence from old. - /// - /// Impls must satisfy `x.mul_i_pow(k) == iᵏ · x` denotationally, and - /// `mul_i_pow(1) == mul_i`. + /// 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 { @@ -225,22 +130,9 @@ pub trait ImaginaryUnit: Coefficient + num::One { } } -/// A coefficient ring carrying a ring involution (a commutative `*`-ring): -/// complex conjugation on `Complex` / `GaussianInt` / cyclotomic integers, -/// and the identity on real rings. -/// -/// Supplies exactly the conjugation the sesquilinear -/// [`crate::containers::Pair::hermitian_overlap`] needs; nothing in propagation -/// requires it, so — like [`ImaginaryUnit`] — it is a separate capability, not a -/// `Coefficient` bound. -/// -/// Laws (commutative `*`-ring): `conj(conj(a)) == a`, `conj(a + b) == -/// conj(a) + conj(b)`, `conj(a · b) == conj(a) · conj(b)`; and when the ring is -/// also [`ImaginaryUnit`], `conj(i) == −i`. -/// -/// Design: §"The map is a graded algebra over `C[K]`" (`Conjugate`). The -/// `conj(i) == −i` law is machine-checked in `lean/PPVM/Pauli/Matrix.lean` -/// (`star_iU`: `star iU = -iU`). +/// 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; diff --git a/crates/ppvm-traits-2/src/containers/batch.rs b/crates/ppvm-traits-2/src/containers/batch.rs index 922eebfda..e1c1144df 100644 --- a/crates/ppvm-traits-2/src/containers/batch.rs +++ b/crates/ppvm-traits-2/src/containers/batch.rs @@ -5,20 +5,15 @@ use crate::containers::Indexable; use crate::loss::LossState; use crate::word::PauliBits; -/// A key that can be laid out as a structure-of-arrays column. Separate from -/// [`Indexable`] so the minimal hashing contract is unchanged: a batched key is -/// both `Indexable` (a valid map key) and `Columnar` (has a column layout). -/// -/// Design: §"The batch contract". +/// 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; } -/// A structure-of-arrays column of keys, owned by the concrete key type (only it -/// knows its planes). Operates plane by plane, never scalar on the hot path. -/// -/// Design: §"The batch contract". +/// A structure-of-arrays key column with plane-oriented operations. +/// The concrete key type determines its column representation. pub trait KeyColumn: Default + Clone { /// The key type this column stores. type Key: Columnar; @@ -27,9 +22,6 @@ pub trait KeyColumn: Default + Clone { fn len(&self) -> usize; /// Number of keys the existing plane allocations can hold without growing. - /// - /// Live stores use this when cloning persistent workspaces: cloning only the - /// populated rows would silently discard a caller's capacity hint. fn capacity(&self) -> usize; /// Whether the column is empty. @@ -43,18 +35,8 @@ pub trait KeyColumn: Default + Clone { /// Append one produced key; the column keeps each plane contiguous. fn push(&mut self, key: Self::Key); - /// Reserve room for `additional` more keys, keeping the ones already stored. - /// - /// The column spelling of `Vec::reserve`, and the counterpart of - /// [`with_capacity`](Self::with_capacity) for a column that is a *live - /// support* rather than a throwaway batch: `ColumnStore`'s branch-merge pass - /// knows its worst-case append count up front (the scratch length), and - /// pre-sizing from it collapses a doubling chain of plane reallocations — - /// plus the parallel bucket-table `reindex`es — into one. - /// - /// **Default: a no-op.** Pre-sizing is a pure optimization, so a column that - /// cannot express it (a device-backed or fixed-extent one) stays legal and - /// simply reallocates on `push`. + /// 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) { let _ = additional; @@ -134,37 +116,13 @@ pub trait KeyColumn: Default + Clone { .toggled_bits2(i, toggle_x_i, toggle_z_i, j, toggle_x_j, toggle_z_j) } - /// Reset to an empty column, **keeping the backing plane allocations**. - /// - /// # Friction: a column that is a *store* needs in-place mutation, and the - /// batch-only surface has none - /// - /// [`gather`](Self::gather) allocates a fresh column, and the design's - /// batch contract needs nothing more: a `TermBatch`'s column is built by - /// `push` and thrown away. The `ColumnStore` backend - /// (implementation-plan Phase 6) makes the *same* column type the live - /// support, and every one of its buffer-reusing fast paths — the old - /// crate's `map_add` clear→write→swap (architecture feature 1), the retain - /// compaction, the in-place Clifford re-key — is defined by mutating a - /// column it already owns. Expressed through `gather` alone each of those - /// allocates a whole new key column **per gate**, which is exactly the - /// per-gate allocation churn the double-buffer exists to remove. - /// - /// So this trio ([`clear`](Self::clear), [`set`](Self::set), - /// [`truncate`](Self::truncate)) is the minimal in-place surface: they are - /// the column spellings of `Vec::clear`/`IndexMut`/`Vec::truncate`, they - /// stay plane-oriented (a SIMD/GPU column implements them as plane writes), - /// and they expose no `&mut Key` — so design rule 4 of §"Backends are - /// containers" ("no signature exposes `&mut (W, C)` or `&mut [C]`") is - /// untouched and the AoS layout still cannot leak. + /// Clear the column while retaining its backing allocations. + /// Together with [`Self::set`] and [`Self::truncate`], permits in-place storage + /// updates without exposing mutable key references or allocating gathered columns. fn clear(&mut self); - /// Overwrite element `i` in place, keeping every other element and the - /// backing allocation. Panics (or debug-panics) if `i >= len()`. - /// - /// The write side of the in-place re-key: a Clifford conjugation is a - /// bijection, so a columnar backend rewrites the key planes at each slot and - /// leaves the parallel coefficient column completely untouched. + /// 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 @@ -184,14 +142,9 @@ pub trait KeyColumn: Default + Clone { } } -/// Keys plus their precomputed structural hashes, in parallel columns. The -/// probe side of the join; it carries no coefficients. -/// -/// See the module-level friction note: the key column is a scalar `Vec` -/// fallback rather than the design's `W::Column`, so the batch is expressible -/// for any `W: Eq + Clone` (not only `Columnar` keys). -/// -/// Design: §"The batch contract". +/// 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, @@ -284,11 +237,7 @@ impl KeyBatch { } } -/// A [`KeyBatch`] with the coefficient column attached: the produced terms -/// awaiting merge. Coefficients are a separate column, touched only when a -/// probe resolves to an aggregate. -/// -/// Design: §"The batch contract". +/// A [`KeyBatch`] plus a separate coefficient column, ready for accumulation. #[derive(Debug, Clone)] pub struct TermBatch { keys: KeyBatch, @@ -357,11 +306,8 @@ impl TermBatch { } } -/// The append side of a term batch: a producer pushes `(key, coeff)` terms into -/// a sink, filling the key and coefficient columns. A naive sink collects into a -/// scalar `Vec`; a columnar sink appends into planes. -/// -/// Design: §"Every gate is a producer feeding `accumulate`". +/// 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); @@ -375,25 +321,8 @@ impl TermSink for TermBatch { } } -/// A monomorphized, inlinable term producer — never `dyn`, since this is the -/// hot loop and the abstraction must compile to nothing. -/// -/// # `Send + Sync` is part of the contract (architecture feature 12) -/// -/// A producer is *read-only* over its own state (`produce` takes `&self`), so a -/// storage backend is free to split the produce walk across threads — which is -/// the whole point of keeping the backend a **configuration choice**: the old -/// crate bounded every driver closure `F: Fn(..) + Sync + Send` -/// (`ppvm-traits/src/map/hashmap.rs`, `map_add_assign`/`map_insert*`/`scale`) so -/// that a concurrent map (it shipped a `DashMap`-backed config benchmarked beside -/// the `HashMap`/`IndexMap` ones) was a **backend swap, not an engine rewrite**. -/// Requiring it here — and on the `ppvm-pauli-sum-2` in-place walk closures -/// (`ScaleByKey`/`SignFlipByKey`/`RekeyBijective`/`RotateInPlace`/ -/// `BranchInPlace`) — keeps that door open: widening the bound later would mean -/// touching every trait signature *and* every impl, i.e. exactly the coupling the -/// feature exists to prevent. Every real producer is a closure over gate indices -/// and ring elements, and `Coefficient` is already `Send + Sync`, so the bound -/// costs nothing today. +/// A statically dispatched producer that reads terms and writes to a sink. +/// `Send + Sync` permits storage backends to partition production across threads. pub trait TermProducer: Send + Sync { /// Push the produced terms for one existing `(key, coeff)` into the sink. fn produce>(&self, key: &K, coeff: &C, sink: &mut S); diff --git a/crates/ppvm-traits-2/src/containers/coordinate_list.rs b/crates/ppvm-traits-2/src/containers/coordinate_list.rs index 8ba5232fc..87c690a29 100644 --- a/crates/ppvm-traits-2/src/containers/coordinate_list.rs +++ b/crates/ppvm-traits-2/src/containers/coordinate_list.rs @@ -1,11 +1,8 @@ // SPDX-FileCopyrightText: 2026 The PPVM Authors // SPDX-License-Identifier: Apache-2.0 -//! `Vec<(K, C)>` — the coordinate-list backend: an unsorted association list -//! scanned linearly, requiring only `K: Eq + Clone` (it never hashes). Best for -//! small support, e.g. the `GeneralizedTableau` amplitude vector. -//! -//! See [`super`] for the shared design references and the orphan-rule note. +//! An unsorted `Vec<(K, C)>` backend using linear scans and `K: Eq + Clone`. +//! Suitable for small supports; no hashing is required. use crate::algebra::{Conjugate, ImaginaryUnit, KeyProduct}; use crate::arithmetic::Coefficient; @@ -139,13 +136,8 @@ where K: KeyProduct, C: ImaginaryUnit, { - /// The twisted convolution `(A·B)[k] = Σ_{p·q = k} A[p]·B[q]·i^{β(p,q)}`, - /// accumulated into `acc` — the coordinate-list spelling of `twistedConv` - /// (`lean/PPVM/Algebra/Twisted.lean`), whose monomial case is `tmul`. - /// - /// Neither `reduce` nor any truncation runs: `acc` keeps an exact-zero - /// cancellation, exactly as `twistedConv` (a finitely-supported map is - /// canonicalized only by an explicit [`Accumulate::reduce`]). + /// 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() { diff --git a/crates/ppvm-traits-2/src/containers/graded.rs b/crates/ppvm-traits-2/src/containers/graded.rs index c691454d2..136e5aaa5 100644 --- a/crates/ppvm-traits-2/src/containers/graded.rs +++ b/crates/ppvm-traits-2/src/containers/graded.rs @@ -1,33 +1,16 @@ // SPDX-FileCopyrightText: 2026 The PPVM Authors // SPDX-License-Identifier: Apache-2.0 -//! The graded map algebra over `C[K]`: the free `C`-module on a key set, -//! layered by algebraic strength. Each layer is a distinct trait justified by a -//! distinct algebraic property *and* a distinct consumer. -//! -//! Design: `traits-2-configuration-and-hashing.md` §"The map is a graded algebra -//! over `C[K]`". The layers are `Support` (L0, the container), `Accumulate` (L1, -//! the module core), `Scale` (L2, the `C`-module action), `Pair` (L3, the trace -//! pairings), and `Multiply` (L4, the ring product). [`Retain`] sits *outside* -//! the algebra: dropping supported terms breaks module exactness, so it is a -//! non-algebraic capability that `Policy` (in `ppvm-pauli-sum-2`) — not the -//! algebra — consumes. -//! -//! The key type is bounded on `Eq + Clone`, **not** [`crate::containers::Indexable`]: -//! `C[K]` is the free module over any index set, so the algebra needs only a -//! valid map key. Hash backends re-add `Indexable` on *their* impls; Pauli -//! propagation re-adds `Word`/`PauliBits` on *its* methods. +//! 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 crate::algebra::{Conjugate, ImaginaryUnit, KeyProduct}; use crate::arithmetic::Coefficient; use crate::containers::{KeyBatch, TermBatch, TermSink}; -/// L0 — the container: a finitely-supported function `K ⇀ C`. -/// -/// No `&mut (K, C)` and no `&mut [C]` slot access is exposed, so a columnar -/// (structure-of-arrays) backend is expressible; `iter` is read-only export. -/// -/// Design: §"The map is a graded algebra over `C[K]`" (L0 `Support`). +/// 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; @@ -49,30 +32,9 @@ pub trait Support { /// synthesizes the pairs from its columns. fn iter(&self) -> impl Iterator; - /// Read-only **borrowing** visit of the support: `f(&key, &coeff)` once per - /// supported key, in the backend's own order. - /// - /// This is the scan side of L0 for readers that *reject most terms* — a - /// pattern trace, a predicate count, a max-weight scan. [`iter`](Self::iter) - /// hands out owned pairs (so that a columnar backend can synthesize them), - /// which forces a `Coeff::clone()` on **every** key before the reader gets to - /// look at it. That clone is free for `f64` and ruinous for a coefficient - /// that owns a heap table: on the `sym.random.circuit` shape (support 65534, - /// 255 matching keys, symbolic coefficients) cloning the whole support before - /// filtering cost 7.06 ms against 1.2 ms for the old crate's borrowing fold, - /// and the gap *grew* with coefficient size. Old could stay generic here - /// because its map iterator borrowed; this method restores that read side - /// without giving up the columnar option — a SoA backend passes - /// `(&keys[i], &coeffs[i])`, which needs no materialized pair. - /// - /// The default body goes through `iter`, so this is a capability an existing - /// backend may ignore; every backend whose keys and coefficients are actually - /// *stored* should override it, and the shipped ones do. - /// - /// It is a scan, not an iterator, deliberately: returning - /// `impl Iterator` would force every backend to have a - /// lending iterator, and a callback composes with the `Retain`/`Policy` - /// closures already used on this side of the API. + /// 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); @@ -80,22 +42,11 @@ pub trait Support { } } -/// L1 — the module core: form linear combinations, then canonicalize. -/// -/// Design: §"The map is a graded algebra over `C[K]`" (L1 `Accumulate`). The -/// module laws are machine-checked in `lean/PPVM/Algebra/GradedMap.lean` -/// (`accumulate_comm`, `accumulate_assoc`); `reduce` drops exactly the zero -/// coefficients (`reduce_structural`). +/// Accumulate linear combinations and explicitly reduce exact-zero coefficients. pub trait Accumulate: Support { - /// Build side of the hash join: merge a produced batch, accumulating onto an - /// existing key or inserting a new one. Columnar in. - /// - /// The batch is algebraically a **multiset** of terms — folding it in is a - /// homomorphism from the free commutative monoid — so an impl is free to - /// reorder it and to split it across partitions/threads: order-invariance - /// (`accumulateTerms_perm`) and partition-invariance - /// (`accumulateTerms_add`, `accumulateTerms B m` for `B = B1 + B2`) are - /// machine-checked in `lean/PPVM/Algebra/GradedMap.lean`. + /// 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 @@ -103,12 +54,7 @@ pub trait Accumulate: Support { /// inline during accumulation. fn reduce(&mut self); - /// Scalar sugar over a batch of one — accumulate a single `(key, coeff)`. - /// - /// Design: §"The map is a graded algebra over `C[K]`" ("the scalar - /// `accumulate(k, c)` is provided sugar over a batch of one"); that the - /// singleton batch is the same operation is `accumulateTerms_singleton` in - /// `lean/PPVM/Algebra/GradedMap.lean`. + /// 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); @@ -116,40 +62,15 @@ pub trait Accumulate: Support { } } -/// L2 — the `C`-module action: a pure elementwise map over the coefficients. -/// -/// Design: §"The map is a graded algebra over `C[K]`" (L2 `Scale`). Machine- -/// checked in `lean/PPVM/Algebra/GradedMap.lean` (`scale_scale`, -/// `scale_accumulate`). +/// 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); } -/// L3 — the read side of the hash join. Two pairings live here, differing only -/// in whether the first operand is conjugated. -/// -/// `overlap` is the **symmetric bilinear** Hilbert–Schmidt trace pairing -/// `⟨A, B⟩ = ∑_k a_k b_k` — bilinear, *not* conjugated. Full `C`-bilinearity -/// (biadditivity, homogeneity in each slot, and symmetry over a commutative -/// ring) is machine-checked in `lean/PPVM/Algebra/GradedMap.lean` -/// (`overlap_add_left`/`overlap_add_right`, `overlap_smul_left`/`overlap_smul_right`, -/// `overlap_comm`); Pauli-basis orthonormality is `overlap_single_single` in -/// `lean/PPVM/Algebra/Noise.lean`. The `= Tr(A B)/2ⁿ` reading is proved, not -/// asserted: `overlap_eq_trace_div` in `lean/PPVM/Pauli/Matrix.lean` gives -/// `Tr(Â B̂) = 2ⁿ · ⟨A, B⟩` for the genuine `2ⁿ×2ⁿ` operators over `ℤ[i]` -/// (via `trace_tensorPauli_mul`), and `twistedConv_apply_id` in -/// `lean/PPVM/Algebra/Twisted.lean` states the same fact inside `C[K]`: -/// `⟨A, B⟩` is the identity coefficient of the L4 product. -/// -/// `hermitian_overlap` is the **sesquilinear** inner product -/// `⟨φ | ψ⟩ = ∑_k conj(a_k)·b_k`, conjugate-linear in the first argument, so it -/// requires the coefficient ring to carry a [`Conjugate`]. Conjugate symmetry, -/// sesquilinearity, and `⟨f, f⟩ ≥ 0` are machine-checked in -/// `lean/PPVM/Algebra/GradedMap.lean` (`hermitianOverlap_conj_symm`, -/// `hermitianOverlap_smul_left`/`smul_right`, `hermitianOverlap_self_nonneg`). -/// -/// Design: §"The map is a graded algebra over `C[K]`" (L3 `Pair`). +/// 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. @@ -164,18 +85,8 @@ pub trait Pair: Support { Self::Coeff: Conjugate; } -/// `tr(self · value)` against a *different* right-hand type. -/// -/// The old `ppvm_traits::Trace` (`ppvm-traits/src/traits/trace.rs`), ported -/// unchanged. It is **not** subsumed by [`Pair::overlap`]: `overlap` pairs a map -/// with another map of the *same* type, while `Trace` is the heterogeneous form -/// the old crate used to trace a word or a whole sum against a `PauliPattern` -/// (`ppvm-pauli-word/src/pattern/trace.rs`, `ppvm-pauli-sum/src/sum/trace.rs`), -/// which is why the right-hand type and the numeric `Output` are both free. -/// -/// Design: §"Compatibility with current names" (`Trace`). Its `-2` implementers -/// land with the pattern port; the same-type Pauli-sum case is `Pair::overlap`, -/// whose bilinearity is machine-checked in `lean/PPVM/Algebra/GradedMap.lean`. +/// Compute `tr(self · value)` against a potentially different right-hand type. +/// Unlike [`Pair::overlap`], both the right-hand type and output are unconstrained. pub trait Trace<'a, RHS: 'a> { /// Numeric output of the trace. type Output; @@ -183,18 +94,9 @@ pub trait Trace<'a, RHS: 'a> { fn trace(&'a self, value: &'a RHS) -> Self::Output; } -/// L4 — the ring product. The only layer that needs the *key* to carry a -/// product; it stays optional and is not implemented for a key type that has -/// none. The Pauli product injects powers of `i`, so the coefficient must absorb -/// phase — bounded on [`ImaginaryUnit`], the minimal requirement. -/// -/// Design: §"The map is a graded algebra over `C[K]`" (L4 `Multiply`). -/// Associativity of the twisted product holds over any commutative ring with -/// `i⁴ = 1`, machine-checked in `lean/PPVM/Algebra/Twisted.lean` (`tmul_assoc`); -/// the basis-monomial product is `multiply_single` in -/// `lean/PPVM/Algebra/GradedMap.lean`. The whole-map product is `twistedConv` -/// (`lean/PPVM/Algebra/Twisted.lean`), whose identity-key coefficient is exactly -/// [`Pair::overlap`] (`twistedConv_apply_id`) — the spec tying L4 to L3. +/// 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, @@ -204,16 +106,8 @@ where fn multiply_into(&self, other: &Self, acc: &mut Self); } -/// The one non-algebraic map operation: keep only the terms a predicate -/// selects. Dropping supported terms breaks module exactness, so this lives -/// outside the graded algebra and is consumed by `Policy`, not the algebra. -/// -/// Design: §"The map is a graded algebra over `C[K]`" and §"Truncation" -/// (`Policy::truncate` bounds on `Retain`). The truncation error incurred is -/// bounded in `lean/PPVM/Algebra/Truncation.lean` (`l1_bound` over `ℝ`, -/// `l1_bound_abv` over any coefficient ring whose -/// [`Coefficient::magnitude`] is an -/// absolute value — the law that bound consumes). +/// 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 index 15ae6d215..938ee09f3 100644 --- a/crates/ppvm-traits-2/src/containers/hash.rs +++ b/crates/ppvm-traits-2/src/containers/hash.rs @@ -1,38 +1,20 @@ // SPDX-FileCopyrightText: 2026 The PPVM Authors // SPDX-License-Identifier: Apache-2.0 -//! Indexable keys and the identity pass-through hasher. -//! -//! Design: `traits-2-configuration-and-hashing.md` §"Indexable values" and -//! §"The pass-through storage contract". `Indexable` is *not* the universal key -//! bound (that is `Eq + Clone`); it is required only on the hash backends. +//! 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 whose finalized structural digest is first class. -/// -/// The digest is avalanche-quality in both the low bits (the hashbrown bucket) -/// and the top 7 (the control tag), so it can be consumed *directly* as the map -/// hash. Contracts: -/// -/// * `Hash for Self` is exactly `state.write_u64(self.key_hash())`; -/// * structurally equal keys return equal digests; and -/// * `KeyColumn::hash_into` reproduces this value bit for bit. -/// -/// This exposes the digest *value*, not the cache mechanics — there is no cache -/// type or invalidation hook in the contract. -/// -/// Design: §"Indexable values". +/// 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`: a key writes its already-finalized `key_hash()` as a -/// single `u64` and this hands it back verbatim, so the digest reaches -/// hashbrown untouched. -/// -/// Design: §"The pass-through storage contract". +/// A pass-through hasher returning the single `u64` digest written by a key. #[derive(Debug, Default, Clone)] pub struct IdentityHasher(u64); @@ -52,11 +34,7 @@ impl Hasher for IdentityHasher { } } -/// `BuildHasher` for [`IdentityHasher`]; the storage aliases in -/// `ppvm-pauli-sum-2` bake this into their `HashMap` so `finish() == -/// key.key_hash()`. -/// -/// Design: §"The pass-through storage contract". +/// Builds [`IdentityHasher`] instances so map hashes equal the supplied key digests. #[derive(Debug, Default, Clone)] pub struct IdentityBuildHasher; diff --git a/crates/ppvm-traits-2/src/containers/hash_join.rs b/crates/ppvm-traits-2/src/containers/hash_join.rs index 5657c2a77..8aa92b640 100644 --- a/crates/ppvm-traits-2/src/containers/hash_join.rs +++ b/crates/ppvm-traits-2/src/containers/hash_join.rs @@ -1,12 +1,8 @@ // SPDX-FileCopyrightText: 2026 The PPVM Authors // SPDX-License-Identifier: Apache-2.0 -//! `HashMap` — the hash-join backend: `accumulate` -//! is a probe-and-merge against the bucket table, requiring `K: Indexable` (the -//! direct structural digest, consumed pass-through through -//! [`IdentityBuildHasher`]). Best for large support, e.g. `PauliSum`. -//! -//! See [`super`] for the shared design references and the orphan-rule note. +//! A `HashMap` backend for large supports. +//! [`Indexable`] keys supply structural digests consumed by the pass-through hasher. use std::collections::HashMap; @@ -99,21 +95,9 @@ where } } - /// `Σ_k self[k]·other[k]`, driven from the **smaller** support. - /// - /// The shared support is contained in both, so scanning either side and - /// probing the other is `O(1)` per candidate and yields the same pair set; - /// walking the smaller one makes the cost `O(min(|a|, |b|))` instead of - /// `O(|self|)`. Against old's `data().trace(k)`-per-term - /// (`ppvm-pauli-sum/src/sum/trace.rs`, a full linear scan of `self` per term - /// of `other` — anti-feature 13) this is the intended asymptotic improvement; - /// picking the smaller side is the second half of it, and it matters when the - /// operands are deliberately unequal (`|a| = 10⁵`, `|b| = 10`). - /// - /// The *value* is unchanged — the pairing is symmetric and the left factor - /// stays `self`'s coefficient either way — only the float **summation order** - /// depends on the direction, which is why the differential bar on `overlap` is - /// relative (`1e-12`) rather than bit-exact. + /// Compute `Σ_k self[k]·other[k]` by scanning the smaller support and probing the other. + /// The traversal costs `O(min(|self|, |other|))` expected hash lookups. + /// Changing traversal direction may change floating-point summation order. #[inline] fn overlap(&self, other: &Self) -> C { if self.len() <= other.len() { @@ -163,32 +147,16 @@ where K: Indexable + KeyProduct, C: ImaginaryUnit, { - /// The twisted convolution `(A·B)[k] = Σ_{p·q = k} A[p]·B[q]·i^{β(p,q)}`, - /// accumulated into `acc` through the hash join — `twistedConv` of - /// `lean/PPVM/Algebra/Twisted.lean` (monomial case `tmul`; associative by - /// `tmul_assoc`/`gtmul_assoc`, and `(A·B)[I] = ⟨A, B⟩` by - /// `twistedConv_apply_id`, tying L4 back to [`Pair::overlap`]). - /// - /// Every `(p, q)` pair contributes: the outer product is `O(|A|·|B|)` and is - /// accumulated **into a distinct `acc`**, never folded back into an operand. - /// (That is the bilinearity old's `MulAssign` loses — see - /// `crate::containers::Multiply` and `ppvm-pauli-sum-2::multiply`.) - /// - /// Neither `reduce` nor any truncation runs here: an exact-zero cancellation - /// stays in `acc`'s support. Canonicalization is the caller's explicit - /// [`Accumulate::reduce`], per §"`reduce()` is first-class, and runs only at - /// finalize". + /// 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 c = phase.apply(&(a.clone() * b.clone())); - // Warm the fresh product key's structural digest *before* the - // probe, for the reason `RotateInPlace` does: `key_mul` returns a - // key with an empty hash cache, and letting the finalize fold fire - // lazily inside `entry()` puts its mul-chain latency on the - // bucket-index critical path with nothing to hide it. Semantic - // no-op (identical digest). + // Precompute the product key digest before probing to move hash computation + // off the bucket-index critical path; the digest value is unchanged. let _ = k.key_hash(); acc.entry(k).and_modify(|e| *e += c.clone()).or_insert(c); } diff --git a/crates/ppvm-traits-2/src/gates/channel.rs b/crates/ppvm-traits-2/src/gates/channel.rs index 0a19a8b22..0501ad950 100644 --- a/crates/ppvm-traits-2/src/gates/channel.rs +++ b/crates/ppvm-traits-2/src/gates/channel.rs @@ -3,10 +3,8 @@ use crate::arithmetic::Coefficient; -/// Coefficients that can calculate single-qubit Pauli-channel factors. -/// -/// This optional channel capability keeps noise formulas out of scalar -/// arithmetic. Implementers may use the generic default or specialize it. +/// 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)`. @@ -192,14 +190,9 @@ pub trait LossChannel { /// Correlated two-qubit loss channel. pub trait CorrelatedLossChannel { - /// Apply a correlated loss channel to `qubit0` and `qubit1`. - /// - /// The three probabilities are: - /// * `p[0]`: losing both qubits simultaneously when both are in the qubit - /// subspace. - /// * `p[1]`: losing either one qubit when both are in the qubit subspace. - /// * `p[2]`: losing one qubit when the other has already been lost prior to - /// the channel. + /// Apply correlated loss: `p[0]` loses both qubits when both are present; + /// `p[1]` loses either one when both are present; `p[2]` loses the remaining + /// qubit when the other was already lost. fn correlated_loss_channel( &mut self, qubit0: usize, @@ -216,10 +209,8 @@ pub trait ResetLossChannel { fn reset_loss_channel(&mut self, qubit: usize); } -/// State-dependent ("asymmetric") single-qubit loss channel: a qubit is lost from -/// `|0⟩` with probability `p0` and from `|1⟩` with probability `p1`. Unlike -/// [`LossChannel`], the total loss probability depends on the qubit's -/// populations, so the channel reads the current `⟨Z⟩`. +/// 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 diff --git a/crates/ppvm-traits-2/src/gates/clifford.rs b/crates/ppvm-traits-2/src/gates/clifford.rs index b45f404cd..690a1e2a1 100644 --- a/crates/ppvm-traits-2/src/gates/clifford.rs +++ b/crates/ppvm-traits-2/src/gates/clifford.rs @@ -182,80 +182,9 @@ impl Clifford for T { } } -/// The derived [`CliffordExtensions`] behavior, blanket-implemented for the same -/// [`BlanketClifford`] opt-ins — the counterpart of the old crate's -/// `impl CliffordExtensions for T`. -/// -/// # Why generator products rather than new primitives -/// -/// The old blanket could write each extension gate as a raw bit rule because a -/// bare `PauliWordTrait` carries no phase, so the *only* content was the -/// `Sp(2n,2)` action. Here the blanket must also be correct for phase-carrying -/// opt-ins (`Tableau`), so each gate is expressed as a product of the audited -/// [`Clifford`] generators instead: the bit rule and the sign then follow from -/// generators whose signs are already machine-checked, rather than from six new -/// hand-written [`PhaseTrack`] deltas that would have to be re-proved (and -/// re-implemented by every word crate). -/// -/// Calls compose in the *backward* Heisenberg convention this crate uses -/// (`P ↦ U†PU`; `lean/PPVM/Pauli/Conjugation.lean`, `conjSdag`): applying `A` -/// then `B` conjugates by the operator product `A·B`. With that, and using -/// `S³ = S†`, `SZ = S³`: -/// -/// | gate | operator identity | call sequence | -/// |:---:|:---|:---| -/// | `s_dag` | `S† = S·Z` | `s`, `z` | -/// | `sqrt_x` | `√X ≃ H·S·H` | `h`, `s`, `h` | -/// | `sqrt_x_dag` | `√X† ≃ H·S†·H` | `h`, `s_dag`, `h` | -/// | `sqrt_y` | `√Y ≃ H·Z` | `h`, `z` | -/// | `sqrt_y_dag` | `√Y† ≃ Z·H` | `z`, `h` | -/// | `cy` | `CY = (I⊗S)·CNOT·(I⊗S†)` | `s(t)`, `cnot(c,t)`, `s_dag(t)` | -/// -/// Each row reproduces the old crate's bit rule **and** the old phased word's -/// sign formula exactly — pinned by `tests/phase1_gate_surface.rs` -/// (`blanket_clifford_extensions_match_old_conjugation_table`, -/// `blanket_cy_matches_old_two_qubit_table`), which replays the gates on a -/// ℤ₄-phased stub and checks the full `s`/`s_dag`/`√X`/`√X†`/`√Y`/`√Y†` -/// conjugation table plus the 16-entry `CY` table of `ppvm-traits`; by the -/// `ppvm-conformance-2` differential suites against the old crate; and, for the -/// gate identities themselves, by the `ℤ[i]` matrix oracle -/// (`phased_pauli_word_lean.rs`). -/// -/// The composition itself — the step the stub tests cannot check for a -/// phase-*carrying* opt-in — is machine-checked in -/// `lean/PPVM/Pauli/Conjugation.lean`, where each row above is *defined* as the -/// product of the audited generator homs (the crate's backward `s` is `conjSdag` -/// there) and is therefore a `MonoidHom` for free: `extSdag`/`extSqrtX`/ -/// `extSqrtXdag`/`extSqrtY`/`extSqrtYdag` (+ `extSdagHom`… `MonoidHom.comp`s), -/// with the tables as `extSdag_eq_conjS`, `extSqrtX_X`/`_Y`/`_Z`, …, the -/// dagger-inverse pairs as `extSqrtXdag_extSqrtX`/`extSqrtYdag_extSqrtY`/ -/// `extSdag_conjSdag`, and `extSqrtX_sq`/`extSqrtY_sq` for `√X² = X`, `√Y² = Y`. -/// `cy` is the same on `𝒫₂`: `conjCY` (= `conjST ∘ conjCNOT ∘ conjSdagT`, with -/// `conjCY_calls` collapsing the literal four-primitive sequence and -/// `conjCYHom` the hom), whose `conjCY_bits` + `conjCY_sign` *are* the old -/// 16-entry table. Corollary — every composite delta is still real -/// (`extSqrtX_isRealPhase`, …, `conjCY_isRealPhase`), so the `±1` drain in -/// `ppvm-pauli-sum-2` stays total on the extension gates too. -/// -/// # Loss guard -/// -/// A lossy word implements the guard inside its column primitives ("a gate -/// touching a lost qubit is a no-op"), so the single-qubit rows above inherit it -/// unchanged. `cy` is the one case worth stating: with a **lost control and a -/// present target**, the old whole-gate skip did nothing, while the decomposition -/// still runs `s(t)` and `s_dag(t)`. Those two share the `z ⊕= x` bit map and are -/// inverse conjugations, so they cancel exactly and the word is left untouched — -/// verified against the old reference over the full 25-word lossy alphabet in -/// `ppvm-conformance-2::lossy_pauli_word_diff`, and proven on every loss -/// configuration by `sActL_cnotActL_sActL_eq_cyActL` -/// (`lean/PPVM/Pauli/Symplectic.lean`): the guarded composite equals the old -/// crate's atomic whole-gate skip `cyActL`. The phase half of the same -/// cancellation, for a phase-carrying opt-in, is `conjS_conjSdag`/ -/// `conjSdag_conjS` (`lean/PPVM/Pauli/Conjugation.lean`). -/// -/// A concrete type that wants the old fused single-pass cost (the `Tableau`'s -/// per-gate bit-plane sweep) opts out of [`BlanketClifford`] and writes its own -/// `impl CliffordExtensions`, exactly as `Phased` does for [`Clifford`]. +/// Shared extension gates for [`BlanketClifford`] opt-ins, composed from generators +/// in backward Heisenberg order (`P ↦ U†PU`). Loss guards belong in the primitives. +/// Types needing fused implementations can opt out and implement the traits directly. impl CliffordExtensions for T { #[inline] fn s_dag(&mut self, q: usize) { diff --git a/crates/ppvm-traits-2/src/gates/rot.rs b/crates/ppvm-traits-2/src/gates/rot.rs index 1d391cb04..8aaf7cd51 100644 --- a/crates/ppvm-traits-2/src/gates/rot.rs +++ b/crates/ppvm-traits-2/src/gates/rot.rs @@ -6,12 +6,8 @@ use crate::{ pauli::Pauli, }; -/// Single-qubit rotations parameterized by an angle domain `A` that yields -/// amplitudes in coefficient domain `C`. -/// -/// The angle defaults to the coefficient (`A = C`), recovering today's -/// `rx(theta: C)` while permitting a symbolic/parametric angle over an -/// `f64`-coefficient sum. +/// 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`. /// @@ -203,11 +199,8 @@ pub trait RotationTwo = C> { } } -/// Rotation about an axis in the x/y plane: -/// `R(axis_angle, θ) = exp(−i·θ/2·(cos(axis_angle)·X + sin(axis_angle)·Y))`. -/// -/// The in-plane axis is `X` rotated about `Z` by `axis_angle`, so -/// `R(axis_angle, θ) = RZ(axis_angle)·RX(θ)·RZ(−axis_angle)` +/// 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); diff --git a/crates/ppvm-traits-2/src/lib.rs b/crates/ppvm-traits-2/src/lib.rs index 6008a250f..53402db6d 100644 --- a/crates/ppvm-traits-2/src/lib.rs +++ b/crates/ppvm-traits-2/src/lib.rs @@ -2,9 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 //! Shared arithmetic, word, gate, and container interfaces. -//! -//! Includes small algebraic types, default gate implementations, and -//! container implementations for `Vec` and `HashMap`. +//! Includes small algebraic types, default gates, and `Vec`/`HashMap` implementations. pub mod algebra; pub mod arithmetic; diff --git a/crates/ppvm-traits-2/src/pauli.rs b/crates/ppvm-traits-2/src/pauli.rs index e8b32e9c6..bc8ddd069 100644 --- a/crates/ppvm-traits-2/src/pauli.rs +++ b/crates/ppvm-traits-2/src/pauli.rs @@ -15,14 +15,8 @@ pub enum Pauli { /// Pauli `Z`. Z, } -/// `Sp`-part: bit-plane column algebra, written **once** and shared by -/// `PhasedPauliWord` (1-bit columns) and `Tableau` (SIMD blocks over its `2n` -/// rows). Same meaning, different width. No phase — this is the role-independent -/// symplectic action. -/// -/// Design: §"Pauli algebra traits". The bit rules realize the per-generator -/// `Sp(2n, 2)` isometries of `lean/PPVM/Pauli/Symplectic.lean` -/// (`hAct_isometry`/`sAct_isometry`/`cnotAct_isometry`/`czAct_isometry`). +/// 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; @@ -30,10 +24,7 @@ pub trait SymplecticColumns { /// `H` on `q`: swap the X and Z columns. fn swap_xz(&mut self, q: usize); - /// `S` on `q`: `z_q ⊕= x_q` (maps `X → Y`). - /// - /// (Completes the design's abbreviated `// ...`; see the module friction - /// note.) + /// 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`. @@ -42,10 +33,7 @@ pub trait SymplecticColumns { /// `CNOT` bit rule, part two: `z_ctrl ⊕= z_tgt`. fn xor_z_col(&mut self, tgt: usize, ctrl: usize); - /// `CZ` bit rule on `(a, b)`: `z_a ⊕= x_b` and `z_b ⊕= x_a`. - /// - /// (Completes the design's abbreviated `// ...`; see the module friction - /// note.) + /// CZ bit update: `z_a ⊕= x_b` and `z_b ⊕= x_a`. fn cz_bits(&mut self, a: usize, b: usize); } @@ -76,10 +64,8 @@ pub trait PhaseTrack { fn z_phase(&mut self, q: usize); } -/// Role-*exclusive* operations that interpret the rows as a symplectic basis -/// rather than as independent operators. A tableau-only trait a word never -/// implements. Holds the frame **primitives**, not `measure` itself — the two -/// measurement algorithms are built *on* these. +/// Tableau frame primitives interpreting rows as a symplectic basis. +/// Measurement algorithms build on these operations; individual words do not implement them. pub trait StabilizerFrame { /// Find a generator that anticommutes with the measured Pauli (the pivot). fn anticommuting_pivot(&self, qubit: usize) -> Option; diff --git a/crates/ppvm-traits-2/src/word.rs b/crates/ppvm-traits-2/src/word.rs index e9dfd0d25..614505bc4 100644 --- a/crates/ppvm-traits-2/src/word.rs +++ b/crates/ppvm-traits-2/src/word.rs @@ -12,11 +12,8 @@ pub trait Word { /// Read the site at `index`. fn get(&self, index: usize) -> Self::Site; - /// Number of non-identity factors according to the concrete site alphabet. - /// - /// A Pauli-motivated read (the `MaxPauliWeight` policy needs it); an ordered - /// representation that stores no explicit identities may have - /// `weight() == n_sites()`. + /// 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. @@ -38,42 +35,28 @@ pub trait PauliBits: Word { fn set_z_bit(&mut self, i: usize, v: bool); /// Set both bit planes at one site. - /// - /// The default composes the scalar setters. Packed words whose setters - /// refresh structural metadata eagerly may override this to refresh once. + /// Packed implementations may override the scalar default to refresh metadata once. #[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 packed bit planes at two sites. - /// - /// The default composes the one-site setter. Packed words with eager - /// structural metadata can override this to refresh exactly once after the - /// four writes. + /// Set both bit planes at two sites. + /// Packed implementations may override the default to refresh metadata once. #[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-plane bit and one Z-plane bit as one structural mutation. - /// - /// `CNOT` updates exactly this pair (`x_target`, `z_control`). Packed words - /// with eager structural metadata can override this to refresh once without - /// also reading or rewriting the two unchanged companion bits. + /// Set one X bit and one Z bit together, as required by CNOT. + /// Overrides can refresh metadata once without touching unchanged companion bits. #[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-plane bits as one structural mutation. - /// - /// `CZ` updates exactly this pair (`z_a`, `z_b`) and leaves both X bits - /// alone — the counterpart of [`set_x_bit_and_z_bit`](PauliBits::set_x_bit_and_z_bit) - /// for `CNOT`. Routing it through the four-bit - /// [`set_xz_bits2`](PauliBits::set_xz_bits2) instead makes a packed word read - /// and rewrite the two unchanged X bits on every gate. The default composes - /// the scalar setters. + /// Set two Z bits together, as required by CZ, leaving X bits unchanged. + /// Overrides can refresh metadata once; the default composes scalar setters. #[inline(always)] fn set_z_bit_pair(&mut self, i: usize, zi: bool, j: usize, zj: bool) { self.set_z_bit(i, zi); @@ -86,15 +69,9 @@ pub trait PauliBits: Word { (self.x_bit(i) as u8) | ((self.z_bit(i) as u8) << 1) } - /// A copy of this word with the X and/or Z bit at `i` toggled — the - /// **rotation-branch key builder** (`iGP` from a diagonal `P`). - /// - /// Provided as clone-then-flip so every `PauliBits` implementer gets a branch - /// builder for free and the rotation/branching kernels can be generic over the - /// word type (the ordinary and the lossy key run the *same* kernel — see - /// `ppvm-pauli-sum-2`'s rotation and loss modules). `PauliWord` overrides it - /// with a direct plane copy that computes the digest exactly once, skipping - /// the redundant refresh a `clone` + `set_*_bit` pair performs. + /// Copy this word and toggle selected X/Z bits at one site. + /// The default clones then flips; packed implementations may build the branch key + /// directly and compute its structural digest once. fn toggled_bits(&self, i: usize, toggle_x: bool, toggle_z: bool) -> Self where Self: Sized + Clone, @@ -111,21 +88,9 @@ pub trait PauliBits: Word { out } - /// A copy of this word with the X and/or Z bits at **two** sites toggled — - /// the **two-qubit** rotation-branch key builder (`iG_aG_b·P`). - /// - /// Chaining [`toggled_bits`](PauliBits::toggled_bits) twice would build two - /// whole words per produced branch term, and for a packed word each of those is - /// a full copy of *both* bit planes plus a word rebuild — so the intermediate - /// is pure waste on the hot path of `rzz`/`rxx`/`ryy`/`rotate_2` (old built - /// **one** `k.clone()` and then wrote four bits into it, - /// `ppvm-pauli-sum/src/sum/rot2.rs`). The two-site entry point makes the single - /// copy the *only* copy, and it scales with the storage tier: at `[u8; 32]` the - /// chained form moved 64 redundant bytes per branch term. - /// - /// The default is clone-then-flip (one clone, up to four bit writes), which is - /// already old's shape; `PauliWord` overrides it with a direct plane copy that - /// computes the digest exactly once, as it does for the single-site form. + /// Copy this word once and toggle selected X/Z bits at two sites. + /// Avoids the intermediate copy from chaining two single-site builders. + /// Packed implementations may compute the structural digest once. #[inline] #[allow(clippy::too_many_arguments)] fn toggled_bits2( @@ -160,11 +125,8 @@ pub trait PauliBits: Word { out } - /// Consume a word and toggle X/Z bits at two sites in place. - /// - /// Re-keying kernels already own their key, so this avoids the structural - /// copy required by [`toggled_bits2`](PauliBits::toggled_bits2). Packed words - /// may override it to defer derived-hash refresh until all writes complete. + /// Consume this word and toggle selected X/Z bits at two sites without cloning. + /// Packed implementations may defer metadata refresh until all writes complete. #[inline] #[allow(clippy::too_many_arguments)] fn into_toggled_bits2( @@ -198,15 +160,8 @@ pub trait PauliBits: Word { self } - /// Whether this word anticommutes with the single-qubit Pauli - /// `pauli = (x_bit, z_bit)` at index `i`, i.e. whether the symplectic form - /// `ω(P, Q) = x_P·z_Q ⊕ z_P·x_Q` is `1` there. - /// - /// The old `PauliWordTrait::anticommutes_at` - /// (`ppvm-traits/src/traits/word_trait.rs`), reproduced verbatim as a - /// provided method — it is the pivot test the tableau measurement search - /// runs (`ppvm-tableau/src/data.rs`), and it is derivable from the two bit - /// reads, so it needs no new required method. + /// 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) From 0c358a782fbb158a153acc4706f1479890346417 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Wed, 16 Sep 2026 12:07:21 -0400 Subject: [PATCH 06/17] removed clippy expections, and uses toggle_x, toggle_z masks instead of individual bits. --- crates/ppvm-traits-2/src/containers/batch.rs | 12 +++---- crates/ppvm-traits-2/src/word.rs | 38 +++++++------------- 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/crates/ppvm-traits-2/src/containers/batch.rs b/crates/ppvm-traits-2/src/containers/batch.rs index e1c1144df..ed8a61638 100644 --- a/crates/ppvm-traits-2/src/containers/batch.rs +++ b/crates/ppvm-traits-2/src/containers/batch.rs @@ -96,24 +96,20 @@ pub trait KeyColumn: Default + Clone { self.get(row).toggled_bits(qubit, toggle_x, toggle_z) } - /// Materialize one row while toggling bits at two sites. + /// Materialize one row while toggling two sites with `[toggle_x, toggle_z]` masks. #[inline] - #[allow(clippy::too_many_arguments)] fn toggled_bits2( &self, row: usize, i: usize, - toggle_x_i: bool, - toggle_z_i: bool, + toggle_i: [bool; 2], j: usize, - toggle_x_j: bool, - toggle_z_j: bool, + toggle_j: [bool; 2], ) -> Self::Key where Self::Key: PauliBits, { - self.get(row) - .toggled_bits2(i, toggle_x_i, toggle_z_i, j, toggle_x_j, toggle_z_j) + self.get(row).toggled_bits2(i, toggle_i, j, toggle_j) } /// Clear the column while retaining its backing allocations. diff --git a/crates/ppvm-traits-2/src/word.rs b/crates/ppvm-traits-2/src/word.rs index 614505bc4..2a3055738 100644 --- a/crates/ppvm-traits-2/src/word.rs +++ b/crates/ppvm-traits-2/src/word.rs @@ -89,71 +89,59 @@ pub trait PauliBits: Word { } /// Copy this word once and toggle selected X/Z bits at two sites. - /// Avoids the intermediate copy from chaining two single-site builders. + /// Each mask is `[toggle_x, toggle_z]`; only one copy is made. /// Packed implementations may compute the structural digest once. #[inline] - #[allow(clippy::too_many_arguments)] - fn toggled_bits2( - &self, - i: usize, - toggle_x_i: bool, - toggle_z_i: bool, - j: usize, - toggle_x_j: bool, - toggle_z_j: bool, - ) -> Self + fn toggled_bits2(&self, i: usize, toggle_i: [bool; 2], j: usize, toggle_j: [bool; 2]) -> Self where Self: Sized + Clone, { let mut out = self.clone(); - if toggle_x_i { + if toggle_i[0] { let b = out.x_bit(i); out.set_x_bit(i, !b); } - if toggle_z_i { + if toggle_i[1] { let b = out.z_bit(i); out.set_z_bit(i, !b); } - if toggle_x_j { + if toggle_j[0] { let b = out.x_bit(j); out.set_x_bit(j, !b); } - if toggle_z_j { + if toggle_j[1] { let b = out.z_bit(j); out.set_z_bit(j, !b); } out } - /// Consume this word and toggle selected X/Z bits at two sites without cloning. + /// Consume this word and toggle two sites using `[toggle_x, toggle_z]` masks. /// Packed implementations may defer metadata refresh until all writes complete. #[inline] - #[allow(clippy::too_many_arguments)] fn into_toggled_bits2( mut self, i: usize, - toggle_x_i: bool, - toggle_z_i: bool, + toggle_i: [bool; 2], j: usize, - toggle_x_j: bool, - toggle_z_j: bool, + toggle_j: [bool; 2], ) -> Self where Self: Sized, { - if toggle_x_i { + if toggle_i[0] { let b = self.x_bit(i); self.set_x_bit(i, !b); } - if toggle_z_i { + if toggle_i[1] { let b = self.z_bit(i); self.set_z_bit(i, !b); } - if toggle_x_j { + if toggle_j[0] { let b = self.x_bit(j); self.set_x_bit(j, !b); } - if toggle_z_j { + if toggle_j[1] { let b = self.z_bit(j); self.set_z_bit(j, !b); } From 24dbeed8bb6e89b4963c5895aa2ac3f9cd93e4bf Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Wed, 16 Sep 2026 12:14:39 -0400 Subject: [PATCH 07/17] removed orphaned traits and structs that don't use it. Such traits and structs can be added later if deemed necessary --- crates/ppvm-traits-2/src/containers/batch.rs | 7 ------ crates/ppvm-traits-2/src/containers/graded.rs | 9 -------- crates/ppvm-traits-2/src/containers/mod.rs | 8 +++---- .../ppvm-traits-2/src/containers/storage.rs | 10 --------- crates/ppvm-traits-2/src/fermion_factor.rs | 22 ------------------- crates/ppvm-traits-2/src/lib.rs | 20 +++++++---------- crates/ppvm-traits-2/src/pauli.rs | 14 ------------ 7 files changed, 11 insertions(+), 79 deletions(-) delete mode 100644 crates/ppvm-traits-2/src/containers/storage.rs delete mode 100644 crates/ppvm-traits-2/src/fermion_factor.rs diff --git a/crates/ppvm-traits-2/src/containers/batch.rs b/crates/ppvm-traits-2/src/containers/batch.rs index ed8a61638..2968f437a 100644 --- a/crates/ppvm-traits-2/src/containers/batch.rs +++ b/crates/ppvm-traits-2/src/containers/batch.rs @@ -317,13 +317,6 @@ impl TermSink for TermBatch { } } -/// A statically dispatched producer that reads terms and writes to a sink. -/// `Send + Sync` permits storage backends to partition production across threads. -pub trait TermProducer: Send + Sync { - /// Push the produced terms for one existing `(key, coeff)` into the sink. - fn produce>(&self, key: &K, coeff: &C, sink: &mut S); -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/ppvm-traits-2/src/containers/graded.rs b/crates/ppvm-traits-2/src/containers/graded.rs index 136e5aaa5..4e5c0b9be 100644 --- a/crates/ppvm-traits-2/src/containers/graded.rs +++ b/crates/ppvm-traits-2/src/containers/graded.rs @@ -85,15 +85,6 @@ pub trait Pair: Support { Self::Coeff: Conjugate; } -/// Compute `tr(self · value)` against a potentially different right-hand type. -/// Unlike [`Pair::overlap`], both the right-hand type and output are unconstrained. -pub trait Trace<'a, RHS: 'a> { - /// Numeric output of the trace. - type Output; - /// Compute `tr(self · value)`. - fn trace(&'a self, value: &'a RHS) -> Self::Output; -} - /// Accumulate a ring product using [`KeyProduct`] and [`ImaginaryUnit`]. /// Key-product phases are absorbed into coefficients; types without a product /// need not implement this layer. diff --git a/crates/ppvm-traits-2/src/containers/mod.rs b/crates/ppvm-traits-2/src/containers/mod.rs index 90f65c9ad..05a4877bf 100644 --- a/crates/ppvm-traits-2/src/containers/mod.rs +++ b/crates/ppvm-traits-2/src/containers/mod.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 The PPVM Authors // SPDX-License-Identifier: Apache-2.0 -//! Container algebra, batches, hashing, and traversal preferences. +//! Container algebra, batches, and hashing. //! //! The private backend modules implement these traits for `Vec` and `HashMap`. @@ -10,9 +10,7 @@ mod coordinate_list; mod graded; mod hash; mod hash_join; -mod storage; -pub use batch::{Columnar, KeyBatch, KeyColumn, TermBatch, TermProducer, TermSink}; -pub use graded::{Accumulate, Multiply, Pair, Retain, Scale, Support, Trace}; +pub use batch::{Columnar, KeyBatch, KeyColumn, TermBatch, TermSink}; +pub use graded::{Accumulate, Multiply, Pair, Retain, Scale, Support}; pub use hash::{IdentityBuildHasher, IdentityHasher, Indexable}; -pub use storage::RekeyStrategy; diff --git a/crates/ppvm-traits-2/src/containers/storage.rs b/crates/ppvm-traits-2/src/containers/storage.rs deleted file mode 100644 index f81d48d08..000000000 --- a/crates/ppvm-traits-2/src/containers/storage.rs +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-FileCopyrightText: 2026 The PPVM Authors -// SPDX-License-Identifier: Apache-2.0 - -/// Implemented by storage backends, independently of coefficient arithmetic. -/// Both traversals must preserve the same keys and coefficient values. -pub trait RekeyStrategy { - /// Prefer draining entries and moving coefficients when true. - /// When false, prefer borrowing entries and cloning coefficients. - const PREFER_MOVED_REKEY: bool = false; -} diff --git a/crates/ppvm-traits-2/src/fermion_factor.rs b/crates/ppvm-traits-2/src/fermion_factor.rs deleted file mode 100644 index fc8abacd3..000000000 --- a/crates/ppvm-traits-2/src/fermion_factor.rs +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-FileCopyrightText: 2026 The PPVM Authors -// SPDX-License-Identifier: Apache-2.0 - -/// The action a fermionic factor performs on its mode. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum FermionAction { - /// A creation operator `a†`. - Create, - /// An annihilation operator `a`. - Annihilate, -} - -/// One factor of an ordered fermionic product — the alphabet of a future -/// ordered fermionic word (`Word`), whose index denotes -/// factor order while the site carries the physical mode. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct FermionSite { - /// The physical mode this factor acts on. - pub mode: usize, - /// Whether this factor creates or annihilates. - pub action: FermionAction, -} diff --git a/crates/ppvm-traits-2/src/lib.rs b/crates/ppvm-traits-2/src/lib.rs index 53402db6d..ec4f9d359 100644 --- a/crates/ppvm-traits-2/src/lib.rs +++ b/crates/ppvm-traits-2/src/lib.rs @@ -7,7 +7,6 @@ pub mod algebra; pub mod arithmetic; pub mod containers; -pub mod fermion_factor; pub mod gates; pub mod loss; pub mod pauli; @@ -17,10 +16,8 @@ pub use algebra::{Conjugate, ImaginaryUnit, KeyProduct, Phase}; pub use arithmetic::{Angle, Coefficient, Halvable}; pub use containers::{ Accumulate, Columnar, IdentityBuildHasher, IdentityHasher, Indexable, KeyBatch, KeyColumn, - Multiply, Pair, RekeyStrategy, Retain, Scale, Support, TermBatch, TermProducer, TermSink, - Trace, + Multiply, Pair, Retain, Scale, Support, TermBatch, TermSink, }; -pub use fermion_factor::{FermionAction, FermionSite}; pub use gates::{ AmplitudeDamping, AsymmetricLossChannel, CRx, Clifford, CliffordBatch, CliffordExtensions, CliffordExtensionsBatch, CorrelatedLossChannel, Depolarizing, Depolarizing2, LossChannel, @@ -28,7 +25,7 @@ pub use gates::{ RotXY, RotationOne, RotationTwo, TGate, TwoQubitPauliError, U3Gate, }; pub use loss::LossState; -pub use pauli::{BlanketClifford, Pauli, PhaseTrack, StabilizerFrame, SymplecticColumns}; +pub use pauli::{BlanketClifford, Pauli, PhaseTrack, SymplecticColumns}; pub use word::{PauliBits, Word}; /// Common traits and types for implementing and using the interfaces. @@ -36,12 +33,11 @@ pub mod prelude { pub use crate::{ Accumulate, AmplitudeDamping, Angle, AsymmetricLossChannel, BlanketClifford, CRx, Clifford, CliffordBatch, CliffordExtensions, CliffordExtensionsBatch, Coefficient, Columnar, - Conjugate, CorrelatedLossChannel, Depolarizing, Depolarizing2, FermionAction, FermionSite, - Halvable, IdentityBuildHasher, IdentityHasher, ImaginaryUnit, Indexable, KeyBatch, - KeyColumn, KeyProduct, LossChannel, LossState, Measure, Multiply, Pair, Pauli, PauliBits, - PauliError, PauliErrorAll, PauliErrorFactors, Phase, PhaseTrack, Projection, RekeyStrategy, - Reset, ResetLossChannel, Retain, RotXY, RotationOne, RotationTwo, Scale, StabilizerFrame, - Support, SymplecticColumns, TGate, TermBatch, TermProducer, TermSink, Trace, - TwoQubitPauliError, U3Gate, Word, + Conjugate, CorrelatedLossChannel, Depolarizing, Depolarizing2, Halvable, + IdentityBuildHasher, IdentityHasher, ImaginaryUnit, Indexable, KeyBatch, KeyColumn, + KeyProduct, LossChannel, LossState, Measure, Multiply, Pair, Pauli, PauliBits, PauliError, + PauliErrorAll, PauliErrorFactors, Phase, PhaseTrack, Projection, Reset, ResetLossChannel, + Retain, RotXY, RotationOne, RotationTwo, Scale, Support, SymplecticColumns, TGate, + TermBatch, TermSink, TwoQubitPauliError, U3Gate, Word, }; } diff --git a/crates/ppvm-traits-2/src/pauli.rs b/crates/ppvm-traits-2/src/pauli.rs index bc8ddd069..8f3325c7b 100644 --- a/crates/ppvm-traits-2/src/pauli.rs +++ b/crates/ppvm-traits-2/src/pauli.rs @@ -64,18 +64,4 @@ pub trait PhaseTrack { fn z_phase(&mut self, q: usize); } -/// Tableau frame primitives interpreting rows as a symplectic basis. -/// Measurement algorithms build on these operations; individual words do not implement them. -pub trait StabilizerFrame { - /// Find a generator that anticommutes with the measured Pauli (the pivot). - fn anticommuting_pivot(&self, qubit: usize) -> Option; - - /// Multiply generator `src` into `dst` (uses the Aaronson–Gottesman - /// `g`-rule). - fn row_multiply(&mut self, src: usize, dst: usize); - - /// Restore canonical form after elimination. - fn canonicalize(&mut self); -} - pub trait BlanketClifford {} From 140ae6206d28e9e2887ab0442d9ea0784cadb75d Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Wed, 16 Sep 2026 12:19:06 -0400 Subject: [PATCH 08/17] modified exponent(self)-> u8 in algebra.rs to just return self as u8 --- crates/ppvm-traits-2/src/algebra.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/crates/ppvm-traits-2/src/algebra.rs b/crates/ppvm-traits-2/src/algebra.rs index 434c45bf8..d4bdde692 100644 --- a/crates/ppvm-traits-2/src/algebra.rs +++ b/crates/ppvm-traits-2/src/algebra.rs @@ -24,12 +24,7 @@ impl Phase { /// The exponent `k ∈ {0, 1, 2, 3}` such that this phase equals `iᵏ`. #[inline] pub fn exponent(self) -> u8 { - match self { - Phase::Pos1 => 0, - Phase::PosI => 1, - Phase::Neg1 => 2, - Phase::NegI => 3, - } + self as u8 } /// The phase `iᵏ` for exponent `k` (taken mod 4). From 26c2d122000f52f5aa9502a04afb66a47c9cd279 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Wed, 16 Sep 2026 12:28:02 -0400 Subject: [PATCH 09/17] merged CliffordExtension into Clifford --- crates/ppvm-traits-2/src/gates/clifford.rs | 26 +++++----------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/crates/ppvm-traits-2/src/gates/clifford.rs b/crates/ppvm-traits-2/src/gates/clifford.rs index 690a1e2a1..8d2dbd68a 100644 --- a/crates/ppvm-traits-2/src/gates/clifford.rs +++ b/crates/ppvm-traits-2/src/gates/clifford.rs @@ -32,11 +32,6 @@ pub trait Clifford { fn zcz(&mut self, qubit0: usize, qubit1: usize) { self.cz(qubit0, qubit1) } -} - -/// Additional Clifford gates beyond the minimal set: `S†`, `√X`, `√X†`, `√Y`, -/// `√Y†`, and `CY`. -pub trait CliffordExtensions: Clifford { /// Apply `S†` to one qubit. fn s_dag(&mut self, qubit: usize); /// Apply `√X` to one qubit. @@ -99,40 +94,37 @@ pub trait CliffordBatch: Clifford { self.cz(c, t); } } -} - -pub trait CliffordExtensionsBatch: CliffordExtensions + CliffordBatch { - /// Apply `S†` to every qubit in `indices`. + /// 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`. + /// 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`. + /// 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`. + /// 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`. + /// 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. + /// apply `cy` to every `(control, target)` pair. fn cy_many(&mut self, pairs: &[(usize, usize)]) { for &(c, t) in pairs { self.cy(c, t); @@ -180,12 +172,6 @@ impl Clifford for T { self.cz_phase(a, b); self.cz_bits(a, b); } -} - -/// Shared extension gates for [`BlanketClifford`] opt-ins, composed from generators -/// in backward Heisenberg order (`P ↦ U†PU`). Loss guards belong in the primitives. -/// Types needing fused implementations can opt out and implement the traits directly. -impl CliffordExtensions for T { #[inline] fn s_dag(&mut self, q: usize) { self.s(q); From c26c14bddf5257448b302a5921ba086c4635dc8b Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Wed, 16 Sep 2026 12:49:22 -0400 Subject: [PATCH 10/17] Added AddAssign and MulAssign bounds to coefficient to avoid cloning --- crates/ppvm-traits-2/src/arithmetic/coefficient.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/ppvm-traits-2/src/arithmetic/coefficient.rs b/crates/ppvm-traits-2/src/arithmetic/coefficient.rs index 5b108b50c..ea3b42717 100644 --- a/crates/ppvm-traits-2/src/arithmetic/coefficient.rs +++ b/crates/ppvm-traits-2/src/arithmetic/coefficient.rs @@ -12,7 +12,9 @@ pub trait Coefficient: + Sub + Mul + AddAssign + + for<'a> AddAssign<&'a Self> + MulAssign + + for<'a> MulAssign<&'a Self> + std::iter::Sum + Send + Sync @@ -29,7 +31,7 @@ pub trait Coefficient: /// Accumulates a borrowed coefficient. #[inline] fn add_assign_ref(&mut self, rhs: &Self) { - *self += rhs.clone(); + *self += rhs; } /// Add this coefficient to itself. Numeric implementations may use their From 5e066c75917a10580ead1dcabccee30eea528e32 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Wed, 16 Sep 2026 12:55:03 -0400 Subject: [PATCH 11/17] moved RotationOne and RotationTwo to match Clifford/CliffordBatch organization. So, we now have RotationOne, RotationOneBatch, RotationTwo, and RotationTwoBatch --- crates/ppvm-traits-2/src/gates/mod.rs | 4 +++- crates/ppvm-traits-2/src/gates/rot.rs | 6 ++++++ crates/ppvm-traits-2/src/lib.rs | 7 ++++--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/ppvm-traits-2/src/gates/mod.rs b/crates/ppvm-traits-2/src/gates/mod.rs index 4a8355428..7c992f9a4 100644 --- a/crates/ppvm-traits-2/src/gates/mod.rs +++ b/crates/ppvm-traits-2/src/gates/mod.rs @@ -15,4 +15,6 @@ pub use channel::{ }; pub use clifford::{Clifford, CliffordBatch, CliffordExtensions, CliffordExtensionsBatch}; pub use measure::{Measure, Projection, Reset}; -pub use rot::{CRx, RotXY, RotationOne, RotationTwo, TGate, U3Gate}; +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 index 8aaf7cd51..fdcf8c544 100644 --- a/crates/ppvm-traits-2/src/gates/rot.rs +++ b/crates/ppvm-traits-2/src/gates/rot.rs @@ -26,7 +26,10 @@ pub trait RotationOne = C> { 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 @@ -107,7 +110,10 @@ pub trait RotationTwo = C> { 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 diff --git a/crates/ppvm-traits-2/src/lib.rs b/crates/ppvm-traits-2/src/lib.rs index ec4f9d359..eca01759e 100644 --- a/crates/ppvm-traits-2/src/lib.rs +++ b/crates/ppvm-traits-2/src/lib.rs @@ -22,7 +22,8 @@ pub use gates::{ AmplitudeDamping, AsymmetricLossChannel, CRx, Clifford, CliffordBatch, CliffordExtensions, CliffordExtensionsBatch, CorrelatedLossChannel, Depolarizing, Depolarizing2, LossChannel, Measure, PauliError, PauliErrorAll, PauliErrorFactors, Projection, Reset, ResetLossChannel, - RotXY, RotationOne, RotationTwo, TGate, TwoQubitPauliError, U3Gate, + RotXY, RotationOne, RotationOneBatch, RotationTwo, RotationTwoBatch, TGate, TwoQubitPauliError, + U3Gate, }; pub use loss::LossState; pub use pauli::{BlanketClifford, Pauli, PhaseTrack, SymplecticColumns}; @@ -37,7 +38,7 @@ pub mod prelude { IdentityBuildHasher, IdentityHasher, ImaginaryUnit, Indexable, KeyBatch, KeyColumn, KeyProduct, LossChannel, LossState, Measure, Multiply, Pair, Pauli, PauliBits, PauliError, PauliErrorAll, PauliErrorFactors, Phase, PhaseTrack, Projection, Reset, ResetLossChannel, - Retain, RotXY, RotationOne, RotationTwo, Scale, Support, SymplecticColumns, TGate, - TermBatch, TermSink, TwoQubitPauliError, U3Gate, Word, + Retain, RotXY, RotationOne, RotationOneBatch, RotationTwo, RotationTwoBatch, Scale, + Support, SymplecticColumns, TGate, TermBatch, TermSink, TwoQubitPauliError, U3Gate, Word, }; } From db46e67207c4c6a205268f13e4cf7791af518ff0 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Wed, 16 Sep 2026 13:16:26 -0400 Subject: [PATCH 12/17] removed BlanketClifford as we force end users to define the clifford operations with respect to the consumer. This allows sepcialized or optimal implementation dependent on the consumer, rather than just inheriting it. --- crates/ppvm-traits-2/src/gates/clifford.rs | 85 +--------------------- crates/ppvm-traits-2/src/gates/measure.rs | 4 +- crates/ppvm-traits-2/src/gates/mod.rs | 2 +- crates/ppvm-traits-2/src/lib.rs | 27 ++++--- crates/ppvm-traits-2/src/pauli.rs | 2 - 5 files changed, 18 insertions(+), 102 deletions(-) diff --git a/crates/ppvm-traits-2/src/gates/clifford.rs b/crates/ppvm-traits-2/src/gates/clifford.rs index 8d2dbd68a..69a296a9c 100644 --- a/crates/ppvm-traits-2/src/gates/clifford.rs +++ b/crates/ppvm-traits-2/src/gates/clifford.rs @@ -1,9 +1,8 @@ // SPDX-FileCopyrightText: 2026 The PPVM Authors // SPDX-License-Identifier: Apache-2.0 -use crate::pauli::{BlanketClifford, PhaseTrack, SymplecticColumns}; - /// 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); @@ -44,7 +43,7 @@ pub trait Clifford { 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`](CliffordExtensions::cy). + /// stim alias for [`cy`](Clifford::cy). fn zcy(&mut self, control: usize, target: usize) { self.cy(control, target) } @@ -131,83 +130,3 @@ pub trait CliffordBatch: Clifford { } } } - -impl Clifford for T { - #[inline] - fn x(&mut self, q: usize) { - self.x_phase(q); - } - - #[inline] - fn y(&mut self, q: usize) { - self.y_phase(q); - } - - #[inline] - fn z(&mut self, q: usize) { - self.z_phase(q); - } - - #[inline] - fn h(&mut self, q: usize) { - self.flip_phase_where_xz(q); - self.swap_xz(q); - } - - #[inline] - fn s(&mut self, q: usize) { - self.s_phase(q); - self.xor_z_from_x(q); - } - - #[inline] - fn cnot(&mut self, c: usize, t: usize) { - self.cnot_phase(c, t); - self.xor_x_col(c, t); - self.xor_z_col(t, c); - } - - #[inline] - fn cz(&mut self, a: usize, b: usize) { - self.cz_phase(a, b); - self.cz_bits(a, b); - } - #[inline] - fn s_dag(&mut self, q: usize) { - self.s(q); - self.z(q); - } - - #[inline] - fn sqrt_x(&mut self, q: usize) { - self.h(q); - self.s(q); - self.h(q); - } - - #[inline] - fn sqrt_x_dag(&mut self, q: usize) { - self.h(q); - self.s_dag(q); - self.h(q); - } - - #[inline] - fn sqrt_y(&mut self, q: usize) { - self.h(q); - self.z(q); - } - - #[inline] - fn sqrt_y_dag(&mut self, q: usize) { - self.z(q); - self.h(q); - } - - #[inline] - fn cy(&mut self, control: usize, target: usize) { - self.s(target); - self.cnot(control, target); - self.s_dag(target); - } -} diff --git a/crates/ppvm-traits-2/src/gates/measure.rs b/crates/ppvm-traits-2/src/gates/measure.rs index 0745284ba..5a70c8178 100644 --- a/crates/ppvm-traits-2/src/gates/measure.rs +++ b/crates/ppvm-traits-2/src/gates/measure.rs @@ -3,7 +3,7 @@ //! Measurement, projection, and reset operations. -use super::{Clifford, CliffordExtensions}; +use super::Clifford; /// Loss-aware projective computational-basis measurement. /// @@ -22,7 +22,7 @@ pub trait Measure { } // Reset one qubit to a computational/Pauli basis state. -pub trait Reset: Clifford + CliffordExtensions { +pub trait Reset: Clifford { /// Reset one qubit to `|0⟩` (stim `R`/`RZ`). fn reset(&mut self, qubit: usize, rng: &mut R); diff --git a/crates/ppvm-traits-2/src/gates/mod.rs b/crates/ppvm-traits-2/src/gates/mod.rs index 7c992f9a4..b25abeba0 100644 --- a/crates/ppvm-traits-2/src/gates/mod.rs +++ b/crates/ppvm-traits-2/src/gates/mod.rs @@ -13,7 +13,7 @@ pub use channel::{ LossChannel, PauliError, PauliErrorAll, PauliErrorFactors, ResetLossChannel, TwoQubitPauliError, }; -pub use clifford::{Clifford, CliffordBatch, CliffordExtensions, CliffordExtensionsBatch}; +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/lib.rs b/crates/ppvm-traits-2/src/lib.rs index eca01759e..6758cfc64 100644 --- a/crates/ppvm-traits-2/src/lib.rs +++ b/crates/ppvm-traits-2/src/lib.rs @@ -19,26 +19,25 @@ pub use containers::{ Multiply, Pair, Retain, Scale, Support, TermBatch, TermSink, }; pub use gates::{ - AmplitudeDamping, AsymmetricLossChannel, CRx, Clifford, CliffordBatch, CliffordExtensions, - CliffordExtensionsBatch, CorrelatedLossChannel, Depolarizing, Depolarizing2, LossChannel, - Measure, PauliError, PauliErrorAll, PauliErrorFactors, Projection, Reset, ResetLossChannel, - RotXY, RotationOne, RotationOneBatch, RotationTwo, RotationTwoBatch, TGate, TwoQubitPauliError, - U3Gate, + AmplitudeDamping, AsymmetricLossChannel, CRx, Clifford, CliffordBatch, CorrelatedLossChannel, + Depolarizing, Depolarizing2, LossChannel, Measure, PauliError, PauliErrorAll, + PauliErrorFactors, Projection, Reset, ResetLossChannel, RotXY, RotationOne, RotationOneBatch, + RotationTwo, RotationTwoBatch, TGate, TwoQubitPauliError, U3Gate, }; pub use loss::LossState; -pub use pauli::{BlanketClifford, Pauli, PhaseTrack, SymplecticColumns}; +pub use pauli::{Pauli, PhaseTrack, SymplecticColumns}; pub use word::{PauliBits, Word}; /// Common traits and types for implementing and using the interfaces. pub mod prelude { pub use crate::{ - Accumulate, AmplitudeDamping, Angle, AsymmetricLossChannel, BlanketClifford, CRx, Clifford, - CliffordBatch, CliffordExtensions, CliffordExtensionsBatch, Coefficient, Columnar, - Conjugate, CorrelatedLossChannel, Depolarizing, Depolarizing2, Halvable, - IdentityBuildHasher, IdentityHasher, ImaginaryUnit, Indexable, KeyBatch, KeyColumn, - KeyProduct, LossChannel, LossState, Measure, Multiply, Pair, Pauli, PauliBits, PauliError, - PauliErrorAll, PauliErrorFactors, Phase, PhaseTrack, Projection, Reset, ResetLossChannel, - Retain, RotXY, RotationOne, RotationOneBatch, RotationTwo, RotationTwoBatch, Scale, - Support, SymplecticColumns, TGate, TermBatch, TermSink, TwoQubitPauliError, U3Gate, Word, + Accumulate, AmplitudeDamping, Angle, AsymmetricLossChannel, CRx, Clifford, CliffordBatch, + Coefficient, Columnar, Conjugate, CorrelatedLossChannel, Depolarizing, Depolarizing2, + Halvable, IdentityBuildHasher, IdentityHasher, ImaginaryUnit, Indexable, KeyBatch, + KeyColumn, KeyProduct, LossChannel, LossState, Measure, Multiply, Pair, Pauli, PauliBits, + PauliError, PauliErrorAll, PauliErrorFactors, Phase, PhaseTrack, Projection, Reset, + ResetLossChannel, Retain, RotXY, RotationOne, RotationOneBatch, RotationTwo, + RotationTwoBatch, Scale, Support, SymplecticColumns, TGate, TermBatch, TermSink, + TwoQubitPauliError, U3Gate, Word, }; } diff --git a/crates/ppvm-traits-2/src/pauli.rs b/crates/ppvm-traits-2/src/pauli.rs index 8f3325c7b..187336d6e 100644 --- a/crates/ppvm-traits-2/src/pauli.rs +++ b/crates/ppvm-traits-2/src/pauli.rs @@ -63,5 +63,3 @@ pub trait PhaseTrack { /// `Z` phase delta on `q` (pure sign; no bit change). fn z_phase(&mut self, q: usize); } - -pub trait BlanketClifford {} From cf2cbc3975b6497c92fedcd3a2e6d06a1ea83475 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Mon, 21 Sep 2026 23:57:03 -0400 Subject: [PATCH 13/17] seperated KeyColumn into two seperate traits. KeyColumn for reading, hashing, gathering keys and KeyColumnMut for key construction, resevrqation, and removal --- crates/ppvm-traits-2/src/containers/batch.rs | 33 ++++++++++---------- crates/ppvm-traits-2/src/containers/mod.rs | 2 +- crates/ppvm-traits-2/src/lib.rs | 12 +++---- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/crates/ppvm-traits-2/src/containers/batch.rs b/crates/ppvm-traits-2/src/containers/batch.rs index 2968f437a..6aef2c518 100644 --- a/crates/ppvm-traits-2/src/containers/batch.rs +++ b/crates/ppvm-traits-2/src/containers/batch.rs @@ -12,8 +12,8 @@ pub trait Columnar: Indexable { type Column: KeyColumn; } -/// A structure-of-arrays key column with plane-oriented operations. -/// The concrete key type determines its column representation. +/// Read access and value-producing operations on a structure-of-arrays key column. +/// See [`KeyColumnMut`] for construction and mutation. pub trait KeyColumn: Default + Clone { /// The key type this column stores. type Key: Columnar; @@ -29,19 +29,6 @@ pub trait KeyColumn: Default + Clone { self.len() == 0 } - /// A column pre-sized for `n` keys. - fn with_capacity(n: usize) -> Self; - - /// Append one produced key; the column keeps 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) { - let _ = additional; - } - /// 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]); @@ -111,10 +98,22 @@ pub trait KeyColumn: Default + Clone { { self.get(row).toggled_bits2(i, toggle_i, j, toggle_j) } +} + +/// 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. - /// Together with [`Self::set`] and [`Self::truncate`], permits in-place storage - /// updates without exposing mutable key references or allocating gathered columns. fn clear(&mut self); /// Overwrite element `i`, preserving other elements and backing allocations. diff --git a/crates/ppvm-traits-2/src/containers/mod.rs b/crates/ppvm-traits-2/src/containers/mod.rs index 05a4877bf..582f2b9b9 100644 --- a/crates/ppvm-traits-2/src/containers/mod.rs +++ b/crates/ppvm-traits-2/src/containers/mod.rs @@ -11,6 +11,6 @@ mod graded; mod hash; mod hash_join; -pub use batch::{Columnar, KeyBatch, KeyColumn, TermBatch, TermSink}; +pub use batch::{Columnar, KeyBatch, KeyColumn, KeyColumnMut, 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/lib.rs b/crates/ppvm-traits-2/src/lib.rs index 6758cfc64..9c0a00012 100644 --- a/crates/ppvm-traits-2/src/lib.rs +++ b/crates/ppvm-traits-2/src/lib.rs @@ -16,7 +16,7 @@ pub use algebra::{Conjugate, ImaginaryUnit, KeyProduct, Phase}; pub use arithmetic::{Angle, Coefficient, Halvable}; pub use containers::{ Accumulate, Columnar, IdentityBuildHasher, IdentityHasher, Indexable, KeyBatch, KeyColumn, - Multiply, Pair, Retain, Scale, Support, TermBatch, TermSink, + KeyColumnMut, Multiply, Pair, Retain, Scale, Support, TermBatch, TermSink, }; pub use gates::{ AmplitudeDamping, AsymmetricLossChannel, CRx, Clifford, CliffordBatch, CorrelatedLossChannel, @@ -34,10 +34,10 @@ pub mod prelude { Accumulate, AmplitudeDamping, Angle, AsymmetricLossChannel, CRx, Clifford, CliffordBatch, Coefficient, Columnar, Conjugate, CorrelatedLossChannel, Depolarizing, Depolarizing2, Halvable, IdentityBuildHasher, IdentityHasher, ImaginaryUnit, Indexable, KeyBatch, - KeyColumn, KeyProduct, LossChannel, LossState, Measure, Multiply, Pair, Pauli, PauliBits, - PauliError, PauliErrorAll, PauliErrorFactors, Phase, PhaseTrack, Projection, Reset, - ResetLossChannel, Retain, RotXY, RotationOne, RotationOneBatch, RotationTwo, - RotationTwoBatch, Scale, Support, SymplecticColumns, TGate, TermBatch, TermSink, - TwoQubitPauliError, U3Gate, Word, + KeyColumn, KeyColumnMut, KeyProduct, LossChannel, LossState, Measure, Multiply, Pair, + Pauli, PauliBits, PauliError, PauliErrorAll, PauliErrorFactors, Phase, PhaseTrack, + Projection, Reset, ResetLossChannel, Retain, RotXY, RotationOne, RotationOneBatch, + RotationTwo, RotationTwoBatch, Scale, Support, SymplecticColumns, TGate, TermBatch, + TermSink, TwoQubitPauliError, U3Gate, Word, }; } From bb219997cc76f83abf3e26d18581cdee4012a3a8 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Tue, 22 Sep 2026 00:07:44 -0400 Subject: [PATCH 14/17] made pattern matching explicit for k & 3 in algebra.rs --- crates/ppvm-traits-2/src/algebra.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/ppvm-traits-2/src/algebra.rs b/crates/ppvm-traits-2/src/algebra.rs index d4bdde692..a61bf21cd 100644 --- a/crates/ppvm-traits-2/src/algebra.rs +++ b/crates/ppvm-traits-2/src/algebra.rs @@ -34,7 +34,8 @@ impl Phase { 0 => Phase::Pos1, 1 => Phase::PosI, 2 => Phase::Neg1, - _ => Phase::NegI, + 3 => Phase::NegI, + _ => unreachable!("masking with 3 produces an exponent in 0..=3"), } } @@ -120,7 +121,8 @@ pub trait ImaginaryUnit: Coefficient + num::One { 0 => self.clone(), 1 => self.mul_i(), 2 => -(self.clone()), - _ => -(self.mul_i()), + 3 => -(self.mul_i()), + _ => unreachable!("masking with 3 produces an exponent in 0..=3"), } } } From 6341d2c610d4f6a745638ff7c4a137bef63d0ed6 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Tue, 22 Sep 2026 00:12:16 -0400 Subject: [PATCH 15/17] removed add_assign_ref as *self += rhs is more natural --- crates/ppvm-traits-2/src/arithmetic/coefficient.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/ppvm-traits-2/src/arithmetic/coefficient.rs b/crates/ppvm-traits-2/src/arithmetic/coefficient.rs index ea3b42717..24ebd1250 100644 --- a/crates/ppvm-traits-2/src/arithmetic/coefficient.rs +++ b/crates/ppvm-traits-2/src/arithmetic/coefficient.rs @@ -28,12 +28,6 @@ pub trait Coefficient: *self = self.mul_sign(sign) } - /// Accumulates a borrowed coefficient. - #[inline] - fn add_assign_ref(&mut self, rhs: &Self) { - *self += rhs; - } - /// Add this coefficient to itself. Numeric implementations may use their /// native multiply-by-two operation; exact rings retain the additive default. #[inline(always)] From 2bb8f63981ee5ff4fa937962911db4e1cfaab88c Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Mon, 28 Sep 2026 10:03:45 -0400 Subject: [PATCH 16/17] another round of responses --- .../src/arithmetic/coefficient.rs | 4 +- .../src/containers/coordinate_list.rs | 32 +++++-- .../ppvm-traits-2/src/containers/hash_join.rs | 87 ++++++++++++++++--- crates/ppvm-traits-2/src/gates/channel.rs | 16 ++-- 4 files changed, 113 insertions(+), 26 deletions(-) diff --git a/crates/ppvm-traits-2/src/arithmetic/coefficient.rs b/crates/ppvm-traits-2/src/arithmetic/coefficient.rs index 24ebd1250..8d012b7b4 100644 --- a/crates/ppvm-traits-2/src/arithmetic/coefficient.rs +++ b/crates/ppvm-traits-2/src/arithmetic/coefficient.rs @@ -32,7 +32,9 @@ pub trait Coefficient: /// native multiply-by-two operation; exact rings retain the additive default. #[inline(always)] fn doubled(&self) -> Self { - self.clone() + self.clone() + let mut result = self.clone(); + result += self; + result } /// Nonnegative magnitude. Exposes a property of the value for a `Policy` to diff --git a/crates/ppvm-traits-2/src/containers/coordinate_list.rs b/crates/ppvm-traits-2/src/containers/coordinate_list.rs index 87c690a29..93bf1f893 100644 --- a/crates/ppvm-traits-2/src/containers/coordinate_list.rs +++ b/crates/ppvm-traits-2/src/containers/coordinate_list.rs @@ -57,7 +57,7 @@ where 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.clone(); + slot.1 += c; } else { self.push((k.clone(), c.clone())); } @@ -80,7 +80,7 @@ where #[inline] fn scale(&mut self, s: &C) { for (_, v) in self.iter_mut() { - *v *= s.clone(); + *v *= s; } } } @@ -102,7 +102,17 @@ where fn overlap(&self, other: &Self) -> C { self.as_slice() .iter() - .filter_map(|(k, a)| Support::get(other, k).map(|b| a.clone() * b)) + .filter_map(|(k, a)| { + other + .as_slice() + .iter() + .find(|(key, _)| key == k) + .map(|(_, b)| { + let mut product = a.clone(); + product *= b; + product + }) + }) .sum() } @@ -113,7 +123,17 @@ where { self.as_slice() .iter() - .filter_map(|(k, a)| Support::get(other, k).map(|b| a.conj() * b)) + .filter_map(|(k, a)| { + other + .as_slice() + .iter() + .find(|(key, _)| key == k) + .map(|(_, b)| { + let mut product = a.conj(); + product *= b; + product + }) + }) .sum() } } @@ -142,7 +162,9 @@ where for (p, a) in self.as_slice() { for (q, b) in other.as_slice() { let (k, phase) = p.key_mul(q); - let c = phase.apply(&(a.clone() * b.clone())); + 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 { diff --git a/crates/ppvm-traits-2/src/containers/hash_join.rs b/crates/ppvm-traits-2/src/containers/hash_join.rs index 8aa92b640..3e0377915 100644 --- a/crates/ppvm-traits-2/src/containers/hash_join.rs +++ b/crates/ppvm-traits-2/src/containers/hash_join.rs @@ -56,9 +56,11 @@ where #[inline] fn accumulate_batch(&mut self, terms: &TermBatch) { for (k, c) in terms.iter() { - self.entry(k.clone()) - .and_modify(|e| *e += c.clone()) - .or_insert_with(|| c.clone()); + if let Some(value) = self.get_mut(k) { + *value += c; + } else { + self.insert(k.clone(), c.clone()); + } } } @@ -77,7 +79,7 @@ where #[inline] fn scale(&mut self, s: &C) { for v in self.values_mut() { - *v *= s.clone(); + *v *= s; } } } @@ -102,11 +104,23 @@ where fn overlap(&self, other: &Self) -> C { if self.len() <= other.len() { HashMap::iter(self) - .filter_map(|(k, a)| HashMap::get(other, k).map(|b| a.clone() * b.clone())) + .filter_map(|(k, a)| { + HashMap::get(other, k).map(|b| { + let mut product = a.clone(); + product *= b; + product + }) + }) .sum() } else { HashMap::iter(other) - .filter_map(|(k, b)| HashMap::get(self, k).map(|a| a.clone() * b.clone())) + .filter_map(|(k, b)| { + HashMap::get(self, k).map(|a| { + let mut product = a.clone(); + product *= b; + product + }) + }) .sum() } } @@ -120,11 +134,23 @@ where { if self.len() <= other.len() { HashMap::iter(self) - .filter_map(|(k, a)| HashMap::get(other, k).map(|b| a.conj() * b.clone())) + .filter_map(|(k, a)| { + HashMap::get(other, k).map(|b| { + let mut product = a.conj(); + product *= b; + product + }) + }) .sum() } else { HashMap::iter(other) - .filter_map(|(k, b)| HashMap::get(self, k).map(|a| a.conj() * b.clone())) + .filter_map(|(k, b)| { + HashMap::get(self, k).map(|a| { + let mut product = a.conj(); + product *= b; + product + }) + }) .sum() } } @@ -154,11 +180,18 @@ where for (p, a) in HashMap::iter(self) { for (q, b) in HashMap::iter(other) { let (k, phase) = p.key_mul(q); - let c = phase.apply(&(a.clone() * b.clone())); + let mut product = a.clone(); + product *= b; + let c = phase.apply(&product); // Precompute the product key digest before probing to move hash computation // off the bucket-index critical path; the digest value is unchanged. let _ = k.key_hash(); - acc.entry(k).and_modify(|e| *e += c.clone()).or_insert(c); + match acc.entry(k) { + std::collections::hash_map::Entry::Occupied(mut entry) => *entry.get_mut() += c, + std::collections::hash_map::Entry::Vacant(entry) => { + entry.insert(c); + } + } } } } @@ -186,6 +219,40 @@ mod tests { } } + #[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(); diff --git a/crates/ppvm-traits-2/src/gates/channel.rs b/crates/ppvm-traits-2/src/gates/channel.rs index 0501ad950..9c0dcdd39 100644 --- a/crates/ppvm-traits-2/src/gates/channel.rs +++ b/crates/ppvm-traits-2/src/gates/channel.rs @@ -11,11 +11,10 @@ pub trait PauliErrorFactors: Coefficient + num::One { #[inline(always)] fn pauli_error_factors(probabilities: [Self; 3]) -> [Self; 3] { let [px, py, pz] = probabilities; - let one = Self::one(); [ - one.clone() - py.doubled() - pz.doubled(), - one.clone() - px.doubled() - py.doubled(), - one - px.doubled() - pz.doubled(), + Self::one() - py.doubled() - pz.doubled(), + Self::one() - px.doubled() - py.doubled(), + Self::one() - px.doubled() - pz.doubled(), ] } } @@ -55,20 +54,17 @@ pub trait PauliError { /// stim `X_ERROR(p)` — apply `X` with probability `p` to one qubit. fn x_error(&mut self, qubit: usize, p: C, rng: &mut R) { - let zero = C::zero(); - self.pauli_error(qubit, [p, zero.clone(), zero], rng) + 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) { - let zero = C::zero(); - self.pauli_error(qubit, [zero.clone(), p, zero], rng) + 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) { - let zero = C::zero(); - self.pauli_error(qubit, [zero.clone(), zero, p], rng) + self.pauli_error(qubit, [C::zero(), C::zero(), p], rng) } /// Explicit batched Pauli-error channel. From fbb95dd922d785231a7cfa1f5c9e5d174b2499b3 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Mon, 28 Sep 2026 11:38:16 -0400 Subject: [PATCH 17/17] other round of cleanup given feedback --- .../ppvm-traits-2/src/arithmetic/halvable.rs | 4 +- crates/ppvm-traits-2/src/containers/batch.rs | 64 +++++++------- .../src/containers/coordinate_list.rs | 54 ++---------- crates/ppvm-traits-2/src/containers/graded.rs | 56 +++++++++++- .../ppvm-traits-2/src/containers/hash_join.rs | 86 +++---------------- crates/ppvm-traits-2/src/containers/mod.rs | 4 +- crates/ppvm-traits-2/src/gates/channel.rs | 45 +++------- crates/ppvm-traits-2/src/gates/clifford.rs | 2 + crates/ppvm-traits-2/src/gates/mod.rs | 3 +- crates/ppvm-traits-2/src/lib.rs | 19 ++-- crates/ppvm-traits-2/src/loss.rs | 10 +-- crates/ppvm-traits-2/src/pauli.rs | 1 - crates/ppvm-traits-2/src/word.rs | 78 ++++++----------- 13 files changed, 159 insertions(+), 267 deletions(-) diff --git a/crates/ppvm-traits-2/src/arithmetic/halvable.rs b/crates/ppvm-traits-2/src/arithmetic/halvable.rs index 8c5cd32da..743268206 100644 --- a/crates/ppvm-traits-2/src/arithmetic/halvable.rs +++ b/crates/ppvm-traits-2/src/arithmetic/halvable.rs @@ -7,7 +7,9 @@ use crate::arithmetic::Coefficient; /// 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`. + /// 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; } diff --git a/crates/ppvm-traits-2/src/containers/batch.rs b/crates/ppvm-traits-2/src/containers/batch.rs index 6aef2c518..663e463b1 100644 --- a/crates/ppvm-traits-2/src/containers/batch.rs +++ b/crates/ppvm-traits-2/src/containers/batch.rs @@ -12,8 +12,9 @@ pub trait Columnar: Indexable { type Column: KeyColumn; } -/// Read access and value-producing operations on a structure-of-arrays key column. -/// See [`KeyColumnMut`] for construction and mutation. +/// 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; @@ -44,42 +45,30 @@ pub trait KeyColumn: Default + Clone { /// Scalar materialization of one element — a naive backend's fallback, /// never the hot path. fn get(&self, i: usize) -> Self::Key; +} - /// Read one row's X bit without requiring scalar materialization when the - /// concrete column can address its packed plane directly. +/// 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 - where - Self::Key: PauliBits, - { + fn x_bit(&self, row: usize, qubit: usize) -> bool { self.get(row).x_bit(qubit) } - /// Read one row's Z bit directly when supported. + /// Read one row's Z bit. #[inline] - fn z_bit(&self, row: usize, qubit: usize) -> bool - where - Self::Key: PauliBits, - { + fn z_bit(&self, row: usize, qubit: usize) -> bool { self.get(row).z_bit(qubit) } - /// Read one row's loss bit directly when supported. - #[inline] - fn is_lost(&self, row: usize, qubit: usize) -> bool - where - Self::Key: LossState, - { - self.get(row).is_lost(qubit) - } - - /// Materialize one row while toggling selected bits. Packed columns can - /// build the branch key directly from their planes. + /// 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 - where - Self::Key: PauliBits, - { + 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) } @@ -92,14 +81,25 @@ pub trait KeyColumn: Default + Clone { toggle_i: [bool; 2], j: usize, toggle_j: [bool; 2], - ) -> Self::Key - where - Self::Key: PauliBits, - { + ) -> 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. diff --git a/crates/ppvm-traits-2/src/containers/coordinate_list.rs b/crates/ppvm-traits-2/src/containers/coordinate_list.rs index 93bf1f893..675353dbd 100644 --- a/crates/ppvm-traits-2/src/containers/coordinate_list.rs +++ b/crates/ppvm-traits-2/src/containers/coordinate_list.rs @@ -4,10 +4,9 @@ //! An unsorted `Vec<(K, C)>` backend using linear scans and `K: Eq + Clone`. //! Suitable for small supports; no hashing is required. -use crate::algebra::{Conjugate, ImaginaryUnit, KeyProduct}; +use crate::algebra::{ImaginaryUnit, KeyProduct}; use crate::arithmetic::Coefficient; -use crate::containers::{Accumulate, Multiply, Pair, Retain, Scale, Support}; -use crate::containers::{KeyBatch, TermBatch}; +use crate::containers::{Accumulate, Multiply, Pair, Retain, Scale, Support, TermBatch}; impl Support for Vec<(K, C)> where @@ -85,57 +84,14 @@ where } } +/// 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, { - #[inline] - 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 = Support::get(self, k); - } - } - - #[inline] - fn overlap(&self, other: &Self) -> C { - self.as_slice() - .iter() - .filter_map(|(k, a)| { - other - .as_slice() - .iter() - .find(|(key, _)| key == k) - .map(|(_, b)| { - let mut product = a.clone(); - product *= b; - product - }) - }) - .sum() - } - - #[inline] - fn hermitian_overlap(&self, other: &Self) -> C - where - C: Conjugate, - { - self.as_slice() - .iter() - .filter_map(|(k, a)| { - other - .as_slice() - .iter() - .find(|(key, _)| key == k) - .map(|(_, b)| { - let mut product = a.conj(); - product *= b; - product - }) - }) - .sum() - } } impl Retain for Vec<(K, C)> diff --git a/crates/ppvm-traits-2/src/containers/graded.rs b/crates/ppvm-traits-2/src/containers/graded.rs index 4e5c0b9be..6b939eae5 100644 --- a/crates/ppvm-traits-2/src/containers/graded.rs +++ b/crates/ppvm-traits-2/src/containers/graded.rs @@ -5,6 +5,8 @@ //! 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}; @@ -74,15 +76,61 @@ pub trait Scale: Support { 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]); + 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; + 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`. + /// The sesquilinear inner product `∑_k conj(a_k)·b_k`, conjugating `self`. fn hermitian_overlap(&self, other: &Self) -> Self::Coeff where - Self::Coeff: Conjugate; + 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`]. diff --git a/crates/ppvm-traits-2/src/containers/hash_join.rs b/crates/ppvm-traits-2/src/containers/hash_join.rs index 3e0377915..dd6a10e25 100644 --- a/crates/ppvm-traits-2/src/containers/hash_join.rs +++ b/crates/ppvm-traits-2/src/containers/hash_join.rs @@ -5,12 +5,13 @@ //! [`Indexable`] keys supply structural digests consumed by the pass-through hasher. use std::collections::HashMap; +use std::collections::hash_map::Entry; -use crate::algebra::{Conjugate, ImaginaryUnit, KeyProduct}; +use crate::algebra::{ImaginaryUnit, KeyProduct}; use crate::arithmetic::Coefficient; -use crate::containers::{Accumulate, Multiply, Pair, Retain, Scale, Support}; -use crate::containers::{IdentityBuildHasher, Indexable}; -use crate::containers::{KeyBatch, TermBatch}; +use crate::containers::{ + Accumulate, IdentityBuildHasher, Indexable, Multiply, Pair, Retain, Scale, Support, TermBatch, +}; impl Support for HashMap where @@ -84,76 +85,14 @@ where } } +/// 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, { - #[inline] - 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 = HashMap::get(self, k).cloned(); - } - } - - /// Compute `Σ_k self[k]·other[k]` by scanning the smaller support and probing the other. - /// The traversal costs `O(min(|self|, |other|))` expected hash lookups. - /// Changing traversal direction may change floating-point summation order. - #[inline] - fn overlap(&self, other: &Self) -> C { - if self.len() <= other.len() { - HashMap::iter(self) - .filter_map(|(k, a)| { - HashMap::get(other, k).map(|b| { - let mut product = a.clone(); - product *= b; - product - }) - }) - .sum() - } else { - HashMap::iter(other) - .filter_map(|(k, b)| { - HashMap::get(self, k).map(|a| { - let mut product = a.clone(); - product *= b; - product - }) - }) - .sum() - } - } - - /// `Σ_k conj(self[k])·other[k]`, driven from the smaller support — see - /// [`Pair::overlap`] for why the direction is free to differ. - #[inline] - fn hermitian_overlap(&self, other: &Self) -> C - where - C: Conjugate, - { - if self.len() <= other.len() { - HashMap::iter(self) - .filter_map(|(k, a)| { - HashMap::get(other, k).map(|b| { - let mut product = a.conj(); - product *= b; - product - }) - }) - .sum() - } else { - HashMap::iter(other) - .filter_map(|(k, b)| { - HashMap::get(self, k).map(|a| { - let mut product = a.conj(); - product *= b; - product - }) - }) - .sum() - } - } } impl Retain for HashMap @@ -183,13 +122,10 @@ where let mut product = a.clone(); product *= b; let c = phase.apply(&product); - // Precompute the product key digest before probing to move hash computation - // off the bucket-index critical path; the digest value is unchanged. - let _ = k.key_hash(); match acc.entry(k) { - std::collections::hash_map::Entry::Occupied(mut entry) => *entry.get_mut() += c, - std::collections::hash_map::Entry::Vacant(entry) => { - entry.insert(c); + Entry::Occupied(mut slot) => *slot.get_mut() += c, + Entry::Vacant(slot) => { + slot.insert(c); } } } diff --git a/crates/ppvm-traits-2/src/containers/mod.rs b/crates/ppvm-traits-2/src/containers/mod.rs index 582f2b9b9..9cd39784d 100644 --- a/crates/ppvm-traits-2/src/containers/mod.rs +++ b/crates/ppvm-traits-2/src/containers/mod.rs @@ -11,6 +11,8 @@ mod graded; mod hash; mod hash_join; -pub use batch::{Columnar, KeyBatch, KeyColumn, KeyColumnMut, TermBatch, TermSink}; +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 index 9c0dcdd39..e266ff065 100644 --- a/crates/ppvm-traits-2/src/gates/channel.rs +++ b/crates/ppvm-traits-2/src/gates/channel.rs @@ -1,6 +1,10 @@ // 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. @@ -19,28 +23,11 @@ pub trait PauliErrorFactors: Coefficient + num::One { } } -impl PauliErrorFactors for f64 { - #[inline(always)] - fn pauli_error_factors([px, py, pz]: [Self; 3]) -> [Self; 3] { - [ - 1.0 - py * 2.0 - pz * 2.0, - 1.0 - px * 2.0 - py * 2.0, - 1.0 - px * 2.0 - pz * 2.0, - ] - } -} +// 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 { - #[inline(always)] - fn pauli_error_factors([px, py, pz]: [Self; 3]) -> [Self; 3] { - let one = Self::new(1.0, 0.0); - [ - one - py * 2.0 - pz * 2.0, - one - px * 2.0 - py * 2.0, - one - px * 2.0 - pz * 2.0, - ] - } -} +impl PauliErrorFactors for num::Complex {} /// A unital single-qubit Pauli error channel `P ↦ λ_P·P`. pub trait PauliError { @@ -101,13 +88,6 @@ pub trait PauliError { } } -/// Apply the same single-qubit Pauli error channel uniformly to every qubit in -/// the system. -pub trait PauliErrorAll { - /// Apply the Pauli channel `p = [p_x, p_y, p_z]` to every qubit. - fn pauli_error_all(&mut self, p: [C; 3], rng: &mut R); -} - /// Two-qubit Pauli error channel. pub trait TwoQubitPauliError { /// Apply a two-qubit Pauli-error channel to one pair. Probabilities are given @@ -184,11 +164,12 @@ pub trait LossChannel { fn loss_channel(&mut self, qubit: usize, p: C, rng: &mut R); } -/// Correlated two-qubit loss channel. +/// 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[0]` loses both qubits when both are present; - /// `p[1]` loses either one when both are present; `p[2]` loses the remaining - /// qubit when the other was already lost. + /// 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, diff --git a/crates/ppvm-traits-2/src/gates/clifford.rs b/crates/ppvm-traits-2/src/gates/clifford.rs index 69a296a9c..431f21f4d 100644 --- a/crates/ppvm-traits-2/src/gates/clifford.rs +++ b/crates/ppvm-traits-2/src/gates/clifford.rs @@ -50,6 +50,8 @@ pub trait Clifford { } /// 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]) { diff --git a/crates/ppvm-traits-2/src/gates/mod.rs b/crates/ppvm-traits-2/src/gates/mod.rs index b25abeba0..f2480be9b 100644 --- a/crates/ppvm-traits-2/src/gates/mod.rs +++ b/crates/ppvm-traits-2/src/gates/mod.rs @@ -10,8 +10,7 @@ mod rot; pub use channel::{ AmplitudeDamping, AsymmetricLossChannel, CorrelatedLossChannel, Depolarizing, Depolarizing2, - LossChannel, PauliError, PauliErrorAll, PauliErrorFactors, ResetLossChannel, - TwoQubitPauliError, + LossChannel, PauliError, PauliErrorFactors, ResetLossChannel, TwoQubitPauliError, }; pub use clifford::{Clifford, CliffordBatch}; pub use measure::{Measure, Projection, Reset}; diff --git a/crates/ppvm-traits-2/src/lib.rs b/crates/ppvm-traits-2/src/lib.rs index 9c0a00012..d9f8edc50 100644 --- a/crates/ppvm-traits-2/src/lib.rs +++ b/crates/ppvm-traits-2/src/lib.rs @@ -16,28 +16,23 @@ pub use algebra::{Conjugate, ImaginaryUnit, KeyProduct, Phase}; pub use arithmetic::{Angle, Coefficient, Halvable}; pub use containers::{ Accumulate, Columnar, IdentityBuildHasher, IdentityHasher, Indexable, KeyBatch, KeyColumn, - KeyColumnMut, Multiply, Pair, Retain, Scale, Support, TermBatch, TermSink, + KeyColumnMut, LossColumn, Multiply, Pair, PauliColumn, Retain, Scale, Support, TermBatch, + TermSink, }; pub use gates::{ AmplitudeDamping, AsymmetricLossChannel, CRx, Clifford, CliffordBatch, CorrelatedLossChannel, - Depolarizing, Depolarizing2, LossChannel, Measure, PauliError, PauliErrorAll, - PauliErrorFactors, Projection, Reset, ResetLossChannel, RotXY, RotationOne, RotationOneBatch, - RotationTwo, RotationTwoBatch, TGate, TwoQubitPauliError, U3Gate, + 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::{ - Accumulate, AmplitudeDamping, Angle, AsymmetricLossChannel, CRx, Clifford, CliffordBatch, - Coefficient, Columnar, Conjugate, CorrelatedLossChannel, Depolarizing, Depolarizing2, - Halvable, IdentityBuildHasher, IdentityHasher, ImaginaryUnit, Indexable, KeyBatch, - KeyColumn, KeyColumnMut, KeyProduct, LossChannel, LossState, Measure, Multiply, Pair, - Pauli, PauliBits, PauliError, PauliErrorAll, PauliErrorFactors, Phase, PhaseTrack, - Projection, Reset, ResetLossChannel, Retain, RotXY, RotationOne, RotationOneBatch, - RotationTwo, RotationTwoBatch, Scale, Support, SymplecticColumns, TGate, TermBatch, - TermSink, TwoQubitPauliError, U3Gate, Word, + 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 index 15f95a20d..7c51a3b19 100644 --- a/crates/ppvm-traits-2/src/loss.rs +++ b/crates/ppvm-traits-2/src/loss.rs @@ -1,22 +1,22 @@ // SPDX-FileCopyrightText: 2026 The PPVM Authors // SPDX-License-Identifier: Apache-2.0 -// Loss state associated with word sites +/// Loss state associated with word sites. pub trait LossState { - // Whether site i is lost. LossyPauliWord overrides this + /// Whether site `i` is lost. Lossy words override this. fn is_lost(&self, _i: usize) -> bool { false } - // Number of lost sites + /// Number of lost sites. fn loss_weight(&self) -> usize { 0 } - // Mark index `i` lost + /// Mark index `i` lost. fn set_lost(&mut self, i: usize); - // Clear the loss flag at index `i`, returning the site to identity. + /// 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. diff --git a/crates/ppvm-traits-2/src/pauli.rs b/crates/ppvm-traits-2/src/pauli.rs index 187336d6e..74aa71e45 100644 --- a/crates/ppvm-traits-2/src/pauli.rs +++ b/crates/ppvm-traits-2/src/pauli.rs @@ -3,7 +3,6 @@ /// 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`. diff --git a/crates/ppvm-traits-2/src/word.rs b/crates/ppvm-traits-2/src/word.rs index 2a3055738..45661c46b 100644 --- a/crates/ppvm-traits-2/src/word.rs +++ b/crates/ppvm-traits-2/src/word.rs @@ -20,7 +20,9 @@ pub trait Word { fn iter(&self) -> impl Iterator; } -// Mutable single-vector X/Z access — a point of `GF(2)^{2n}`. +/// 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; @@ -35,28 +37,24 @@ pub trait PauliBits: Word { fn set_z_bit(&mut self, i: usize, v: bool); /// Set both bit planes at one site. - /// Packed implementations may override the scalar default to refresh metadata once. #[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. - /// Packed implementations may override the default to refresh metadata once. #[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. - /// Overrides can refresh metadata once without touching unchanged companion bits. #[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. - /// Overrides can refresh metadata once; the default composes scalar setters. #[inline(always)] fn set_z_bit_pair(&mut self, i: usize, zi: bool, j: usize, zj: bool) { self.set_z_bit(i, zi); @@ -70,57 +68,46 @@ pub trait PauliBits: Word { } /// Copy this word and toggle selected X/Z bits at one site. - /// The default clones then flips; packed implementations may build the branch key - /// directly and compute its structural digest once. + #[inline] fn toggled_bits(&self, i: usize, toggle_x: bool, toggle_z: bool) -> Self where Self: Sized + Clone, { - let mut out = self.clone(); - if toggle_x { - let b = out.x_bit(i); - out.set_x_bit(i, !b); - } - if toggle_z { - let b = out.z_bit(i); - out.set_z_bit(i, !b); - } - out + 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. - /// Packed implementations may compute the structural digest once. #[inline] fn toggled_bits2(&self, i: usize, toggle_i: [bool; 2], j: usize, toggle_j: [bool; 2]) -> Self where Self: Sized + Clone, { - let mut out = self.clone(); - if toggle_i[0] { - let b = out.x_bit(i); - out.set_x_bit(i, !b); - } - if toggle_i[1] { - let b = out.z_bit(i); - out.set_z_bit(i, !b); - } - if toggle_j[0] { - let b = out.x_bit(j); - out.set_x_bit(j, !b); + 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_j[1] { - let b = out.z_bit(j); - out.set_z_bit(j, !b); + if toggle[1] { + let z = self.z_bit(i); + self.set_z_bit(i, !z); } - out + self } /// Consume this word and toggle two sites using `[toggle_x, toggle_z]` masks. - /// Packed implementations may defer metadata refresh until all writes complete. #[inline] fn into_toggled_bits2( - mut self, + self, i: usize, toggle_i: [bool; 2], j: usize, @@ -129,23 +116,8 @@ pub trait PauliBits: Word { where Self: Sized, { - if toggle_i[0] { - let b = self.x_bit(i); - self.set_x_bit(i, !b); - } - if toggle_i[1] { - let b = self.z_bit(i); - self.set_z_bit(i, !b); - } - if toggle_j[0] { - let b = self.x_bit(j); - self.set_x_bit(j, !b); - } - if toggle_j[1] { - let b = self.z_bit(j); - self.set_z_bit(j, !b); - } - self + 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`.