Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
2a7f074
initial commit of breaking up roger's PR on rebuilding the trait syst…
JonhasA Sep 9, 2026
fab45e2
added ppvm-traits-2 to workspace and forgot to factor back in fermion…
JonhasA Sep 10, 2026
ebb25e0
ran cargo fmt
JonhasA Sep 10, 2026
5e2e779
adding missing header to fermion_factor
JonhasA Sep 10, 2026
17d676e
condensed comments to maximum 3 lines or so
JonhasA Sep 16, 2026
0c358a7
removed clippy expections, and uses toggle_x, toggle_z masks instead …
JonhasA Sep 16, 2026
24dbeed
removed orphaned traits and structs that don't use it. Such traits an…
JonhasA Sep 16, 2026
140ae62
modified exponent(self)-> u8 in algebra.rs to just return self as u8
JonhasA Sep 16, 2026
26c2d12
merged CliffordExtension into Clifford
JonhasA Sep 16, 2026
a14a33a
Merge branch 'main' into trait-2/ppvm-traits-2
JonhasA Sep 16, 2026
c26c14b
Added AddAssign and MulAssign bounds to coefficient to avoid cloning
JonhasA Sep 16, 2026
5e066c7
moved RotationOne and RotationTwo to match Clifford/CliffordBatch org…
JonhasA Sep 16, 2026
db46e67
removed BlanketClifford as we force end users to define the clifford …
JonhasA Sep 16, 2026
cf2cbc3
seperated KeyColumn into two seperate traits. KeyColumn for reading, …
JonhasA Sep 22, 2026
bb21999
made pattern matching explicit for k & 3 in algebra.rs
JonhasA Sep 22, 2026
6341d2c
removed add_assign_ref as *self += rhs is more natural
JonhasA Sep 22, 2026
2bb8f63
another round of responses
JonhasA Sep 28, 2026
fbb95dd
other round of cleanup given feedback
JonhasA Sep 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions crates/ppvm-traits-2/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
166 changes: 166 additions & 0 deletions crates/ppvm-traits-2/src/algebra.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// SPDX-FileCopyrightText: 2026 The PPVM Authors
// SPDX-License-Identifier: Apache-2.0

//! Algebra capabilities: key products, coefficient conjugation, and imaginary units.
//! [`Phase`] carries the residual fourth root of unity emitted by a key product.

use crate::arithmetic::Coefficient;

/// A fourth root of unity `iᵏ`, with `k` modulo four.
/// [`KeyProduct::key_mul`] returns this residual phase for the coefficient to absorb.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Phase {
/// `i⁰ = +1`.
Pos1,
/// `i¹ = +i`.
PosI,
/// `i² = −1`.
Neg1,
/// `i³ = −i`.
NegI,
}

impl Phase {
/// The exponent `k ∈ {0, 1, 2, 3}` such that this phase equals `iᵏ`.
#[inline]
pub fn exponent(self) -> u8 {
self as u8
}

/// The phase `iᵏ` for exponent `k` (taken mod 4).
#[inline]
pub fn from_exponent(k: u8) -> Self {
match k & 3 {
0 => Phase::Pos1,
1 => Phase::PosI,
2 => Phase::Neg1,
3 => Phase::NegI,
_ => unreachable!("masking with 3 produces an exponent in 0..=3"),
}
}

/// The identity phase: `p.compose(Self::one()) == p`.
#[inline]
pub fn one() -> Self {
Phase::Pos1
}

/// Compose phases by adding exponents modulo four: `iᵃ · iᵇ = i^{a+b}`.
/// Composition is commutative and can accumulate key-product phases before application.
#[inline]
pub fn compose(self, other: Self) -> Self {
Phase::from_exponent(self.exponent() + other.exponent())
}

/// The group inverse `i^{-k} = i^{4-k}`, i.e. the phase `q` with
/// `self.compose(q) == Phase::one()`.
#[inline]
pub fn inverse(self) -> Self {
// 4 - k is exact for k ∈ {0,1,2,3}; the mod-4 reduction in
// `from_exponent` sends k = 0 back to 0.
Phase::from_exponent(4 - self.exponent())
}

/// Apply `iᵏ` to a coefficient through [`ImaginaryUnit::mul_i_pow`].
/// Overrides can preserve symbolic representations; complex multiplication by `i`
/// uses component swaps to preserve signed zeros and avoid non-finite contamination.
#[inline]
pub fn apply<C: ImaginaryUnit>(self, c: &C) -> C {
c.mul_i_pow(self.exponent())
}
}

/// `iᵃ · iᵇ = i^{a+b}` — [`compose`](Phase::compose) as the `*` operator, so a
/// residual-phase accumulator reads `acc *= phase` / `acc = a * b`.
impl core::ops::Mul for Phase {
type Output = Phase;

#[inline]
fn mul(self, rhs: Phase) -> Phase {
self.compose(rhs)
}
}

impl core::ops::MulAssign for Phase {
#[inline]
fn mul_assign(&mut self, rhs: Phase) {
*self = self.compose(rhs);
}
}

/// A key product returning a key and residual phase `i^{β(u,v)}`.
/// Key multiplication must be associative, and phases must satisfy the cocycle law:
/// `β(u,v) + β(u·v,w) == β(v,w) + β(u,v·w)` modulo four.
pub trait KeyProduct: Eq + Clone {
/// Product of two keys, with the phase it produces (folded onto the coeff).
fn key_mul(&self, other: &Self) -> (Self, Phase);
}

/// A commutative coefficient ring with an imaginary unit.
/// Implementations must satisfy `Self::imaginary_unit() * Self::imaginary_unit()
/// == -Self::one()`, hence `i⁴ = 1`.
pub trait ImaginaryUnit: Coefficient + num::One {
/// The imaginary unit `i`; impls must satisfy
/// `Self::imaginary_unit() * Self::imaginary_unit() == -Self::one()`.
fn imaginary_unit() -> Self;

/// Multiply by `i`; the default uses ring multiplication.
/// Complex values override this with `(re, im) ↦ (-im, re)` to preserve signed
/// zeros and avoid contaminating both components when one is non-finite.
#[inline]
fn mul_i(&self) -> Self {
self.clone() * Self::imaginary_unit()
}

/// Multiply by `iᵏ`, with `k` modulo four; used by [`Phase::apply`].
/// Overrides may fold phases into symbolic data, including when `k == 0`.
/// The result must denote `iᵏ · self`, with `mul_i_pow(1) == mul_i()`.
#[inline]
fn mul_i_pow(&self, k: u8) -> Self {
match k & 3 {
0 => self.clone(),
1 => self.mul_i(),
2 => -(self.clone()),
3 => -(self.mul_i()),
_ => unreachable!("masking with 3 produces an exponent in 0..=3"),
}
}
}

/// A commutative ring involution used by [`crate::containers::Pair::hermitian_overlap`].
/// Requires `conj(conj(a)) == a`, preservation of addition and multiplication,
/// and `conj(i) == -i` when the ring also implements [`ImaginaryUnit`].
pub trait Conjugate: Coefficient {
/// The ring involution applied to this value.
fn conj(&self) -> Self;
}

impl ImaginaryUnit for num::Complex<f64> {
#[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<f64> {
#[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
}
}
36 changes: 36 additions & 0 deletions crates/ppvm-traits-2/src/arithmetic/angle.rs
Original file line number Diff line number Diff line change
@@ -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<C: Coefficient> {
/// Return `(sin θ, cos θ)` in the coefficient domain `C`.
fn sin_cos(&self) -> (C, C);
}

impl Angle<f64> 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<f64>`.
impl Angle<num::Complex<f64>> for num::Complex<f64> {
#[inline]
fn sin_cos(&self) -> (num::Complex<f64>, num::Complex<f64>) {
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<num::Complex<f64>> for f64 {
#[inline]
fn sin_cos(&self) -> (num::Complex<f64>, num::Complex<f64>) {
let (s, c) = num::traits::Float::sin_cos(*self);
(num::Complex::new(s, 0.0), num::Complex::new(c, 0.0))
}
}
78 changes: 78 additions & 0 deletions crates/ppvm-traits-2/src/arithmetic/coefficient.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: 2026 The PPVM Authors
// SPDX-License-Identifier: Apache-2.0

use std::ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub};

pub trait Coefficient:
PartialEq
+ Clone
+ num::Zero
+ Neg<Output = Self>
+ Add<Self, Output = Self>
+ Sub<Self, Output = Self>
+ Mul<Self, Output = Self>
+ AddAssign<Self>
+ for<'a> AddAssign<&'a Self>
+ MulAssign<Self>
+ for<'a> MulAssign<&'a Self>
+ std::iter::Sum
+ Send
+ Sync
{
/// Multiply by `sign ∈ {-1, +1}` (encoded as `i8`).
fn mul_sign(&self, sign: i8) -> Self;

/// Multiply this coefficient in place by `sign ∈ {-1, +1}`.
#[inline]
fn mul_sign_assign(&mut self, sign: i8) {
*self = self.mul_sign(sign)
}

/// Add this coefficient to itself. Numeric implementations may use their
/// native multiply-by-two operation; exact rings retain the additive default.
#[inline(always)]
fn doubled(&self) -> Self {
let mut result = self.clone();
result += self;
result
}

/// Nonnegative magnitude. Exposes a property of the value for a `Policy` to
/// threshold; it does not itself decide any cutoff. Replaces the old
/// `Coefficient::cutoff`.
fn magnitude(&self) -> f64;
}

impl Coefficient for f64 {
#[inline]
fn mul_sign(&self, sign: i8) -> Self {
(sign as f64) * (*self)
}

#[inline(always)]
fn doubled(&self) -> Self {
*self * 2.0
}

#[inline]
fn magnitude(&self) -> f64 {
self.abs()
}
}

impl Coefficient for num::Complex<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.norm()
}
}
28 changes: 28 additions & 0 deletions crates/ppvm-traits-2/src/arithmetic/halvable.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2026 The PPVM Authors
// SPDX-License-Identifier: Apache-2.0

use crate::arithmetic::Coefficient;

/// A coefficient ring in which halving (`0.5·x`) is total and exact: the
/// capability the projective computational-basis measurement kernel needs to
/// apply the `(I ± Z)/2` projectors.
pub trait Halvable: Coefficient {
/// Divide by two. Impls must be exact: `x.half() + x.half() == x` for every
/// finite `x` (`f64::NAN` and the infinities are exempt — they are not
/// ring elements).
fn half(&self) -> Self;
}

impl Halvable for f64 {
#[inline]
fn half(&self) -> Self {
*self / 2.0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised that this holds up in the exact x.half() + x.half() == x condition.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, there is an edge case here: this doesn't hold for f64::NAN, which is a perfectly valid f64, but x.half() == x if x is NAN.

I'm not saying this bothers me overly much, but as I said I'd also be happy not to go the full formal ring way anyway.

}
}

impl Halvable for num::Complex<f64> {
#[inline]
fn half(&self) -> Self {
*self / 2.0
}
}
12 changes: 12 additions & 0 deletions crates/ppvm-traits-2/src/arithmetic/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Loading
Loading