From 55fac2983ae04a1ae8e84d7abaef9226bfac2619 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Wed, 4 Nov 2026 11:27:49 -0500 Subject: [PATCH 01/10] removing phase from ppvm-pauli-word-2 and having ppvm-phased-pauli-word-2 own it instead --- Cargo.lock | 9 + Cargo.toml | 2 +- crates/ppvm-pauli-word-2/src/data.rs | 20 ++- crates/ppvm-pauli-word-2/src/lib.rs | 2 - crates/ppvm-pauli-word-2/src/pauli_parser.rs | 7 - crates/ppvm-pauli-word-2/src/phase.rs | 174 ------------------- 6 files changed, 29 insertions(+), 185 deletions(-) delete mode 100644 crates/ppvm-pauli-word-2/src/phase.rs diff --git a/Cargo.lock b/Cargo.lock index 88057ff6e..1a277123f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2262,6 +2262,15 @@ dependencies = [ "ppvm-traits-2", ] +[[package]] +name = "ppvm-phased-pauli-word-2" +version = "0.1.0" +dependencies = [ + "ppvm-lossy-pauli-word-2", + "ppvm-pauli-word-2", + "ppvm-traits-2", +] + [[package]] name = "ppvm-python-native" version = "0.1.2" diff --git a/Cargo.toml b/Cargo.toml index cb9d807c7..255cdab87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ members = [ # Built by `cargo build --workspace --all-targets` in CI so the skill # can't silently drift away from the public API. "crates/ppvm-tui", - "skills/ppvm-usage/examples/rust", "crates/ppvm-lossy-pauli-word-2", + "skills/ppvm-usage/examples/rust", "crates/ppvm-lossy-pauli-word-2", "crates/ppvm-phased-pauli-word-2", ] # Single version for every crate and the `bloqade-ppvm` Python package diff --git a/crates/ppvm-pauli-word-2/src/data.rs b/crates/ppvm-pauli-word-2/src/data.rs index 60d5d3eee..93f441269 100644 --- a/crates/ppvm-pauli-word-2/src/data.rs +++ b/crates/ppvm-pauli-word-2/src/data.rs @@ -10,7 +10,7 @@ use std::marker::PhantomData; use bitvec::array::BitArray; use bitvec::view::BitView; use num::{One, Zero}; -use ppvm_traits_2::{Pauli, PauliBits, Word}; +use ppvm_traits_2::{LossState, Pauli, PauliBits, Word}; use crate::hash::structural_hash; use crate::{DefaultStorage, HashFinalize, PauliStorage}; @@ -226,6 +226,24 @@ impl Word for PauliWord { } } +/// Ordinary words report no loss. Setting loss is unsupported; clearing resets a site. +impl LossState for PauliWord +where + A: PauliStorage, + H: BuildHasher + Default + HashFinalize, +{ + /// Panics because ordinary Pauli words cannot represent lost sites. + fn set_lost(&mut self, _i: usize) { + panic!("PauliWord cannot represent loss; use LossyPauliWord instead"); + } + + /// Resets the site to identity and refreshes the structural hash if needed. + #[inline] + fn clear_lost(&mut self, i: usize) { + self.set_xz_bits(i, false, false); + } +} + impl PauliBits for PauliWord where A: PauliStorage, diff --git a/crates/ppvm-pauli-word-2/src/lib.rs b/crates/ppvm-pauli-word-2/src/lib.rs index 0f320bea7..b74274dfd 100644 --- a/crates/ppvm-pauli-word-2/src/lib.rs +++ b/crates/ppvm-pauli-word-2/src/lib.rs @@ -8,12 +8,10 @@ mod column; mod data; mod hash; mod pauli_parser; -mod phase; mod product; mod storage; pub use column::PauliKeyColumn; pub use data::PauliWord; pub use hash::HashFinalize; -pub use phase::Phased; pub use storage::{DefaultStorage, PauliStorage}; diff --git a/crates/ppvm-pauli-word-2/src/pauli_parser.rs b/crates/ppvm-pauli-word-2/src/pauli_parser.rs index 9137eae0f..9bc35c5ba 100644 --- a/crates/ppvm-pauli-word-2/src/pauli_parser.rs +++ b/crates/ppvm-pauli-word-2/src/pauli_parser.rs @@ -85,13 +85,6 @@ mod tests { for input in ["IIIIIIIII", "XXXXXXXXI", "IIIIIIIIX", "I_I_I_I_I_I_I_I_I"] { assert!(std::panic::catch_unwind(|| PauliWord::::from(input)).is_err()); assert!(std::panic::catch_unwind(|| PauliWord::::from(input.to_owned())).is_err()); - let phased = format!("+i{input}"); - assert!( - std::panic::catch_unwind(|| { - crate::Phased::>::from(phased.as_str()) - }) - .is_err() - ); } for (input, expected) in [ ("", ""), diff --git a/crates/ppvm-pauli-word-2/src/phase.rs b/crates/ppvm-pauli-word-2/src/phase.rs deleted file mode 100644 index 444299ff1..000000000 --- a/crates/ppvm-pauli-word-2/src/phase.rs +++ /dev/null @@ -1,174 +0,0 @@ -// SPDX-FileCopyrightText: 2026 The PPVM Authors -// SPDX-License-Identifier: Apache-2.0 - -//! Phased words with explicit prefactors, parsing, display, and equality. - -use std::fmt; -use std::hash::BuildHasher; - -use crate::{HashFinalize, PauliStorage, PauliWord}; -use ppvm_traits_2::{Phase, Word}; - -/// A base word with a phase in `{+1, +i, -1, -i}`, representing `i^φ · g(w)`. -/// Equality and display include the phase; site inspection uses only the word. -/// Implements neither `Hash` nor `Indexable`, so it is not a map key. -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct Phased { - /// The bare base word (carries no phase of its own). - pub(crate) word: W, - /// The explicit `ℤ₄` prefactor `i^φ`. - pub(crate) phase: Phase, -} - -impl Phased { - /// Wrap `word` with the trivial phase `+1`. - #[inline] - pub fn new(word: W) -> Self { - Self { - word, - phase: Phase::one(), - } - } - - /// Wrap `word` with an explicit `phase`. - #[inline] - pub fn with_phase(word: W, phase: Phase) -> Self { - Self { word, phase } - } - - /// The `ℤ₄` prefactor `i^φ`. - #[inline] - pub fn phase(&self) -> Phase { - self.phase - } - - /// Borrow the bare base word (phase stripped). - #[inline] - pub fn word(&self) -> &W { - &self.word - } - - /// Consume the wrapper, returning the base word and its phase. - #[inline] - pub fn into_parts(self) -> (W, Phase) { - (self.word, self.phase) - } - - /// Multiply the stored phase by `delta` (the `ℤ₄` group product). - #[inline] - pub fn add_phase(&mut self, delta: Phase) { - self.phase *= delta; - } - - /// Whether the phase has a positive sign (`+1` or `+i`). - #[inline] - pub fn is_positive(&self) -> bool { - matches!(self.phase, Phase::Pos1 | Phase::PosI) - } -} - -/// Site inspection delegates to the inner word; the scalar phase changes no sites. -impl Word for Phased { - type Site = W::Site; - - #[inline] - fn n_sites(&self) -> usize { - self.word.n_sites() - } - - #[inline] - fn get(&self, index: usize) -> Self::Site { - self.word.get(index) - } - - #[inline] - fn weight(&self) -> usize { - self.word.weight() - } - - #[inline] - fn iter(&self) -> impl Iterator { - self.word.iter() - } -} - -/// Renders the phase as `+`, `+i`, `-`, or `-i`, matching the legacy display. -fn phase_prefix(phase: Phase) -> &'static str { - match phase { - Phase::Pos1 => "+", - Phase::PosI => "+i", - Phase::Neg1 => "-", - Phase::NegI => "-i", - } -} - -/// `Display` prints the phase prefix followed by the base word, e.g. `+iXYZI`. -impl fmt::Display for Phased { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}{}", phase_prefix(self.phase), self.word) - } -} - -impl From<&str> for Phased> -where - A: PauliStorage, - H: BuildHasher + Default + HashFinalize, -{ - /// Parses a required `+`/`-`, optional `i`, then a [`PauliWord`] string. - /// Panics if the sign is missing or the word is invalid. - fn from(s: &str) -> Self { - let mut chars = s.chars(); - let (phase, prefix_len) = match (chars.next(), chars.next()) { - (Some('+'), Some('i')) => (Phase::PosI, 2), - (Some('-'), Some('i')) => (Phase::NegI, 2), - (Some('+'), _) => (Phase::Pos1, 1), - (Some('-'), _) => (Phase::Neg1, 1), - _ => panic!("invalid phase format: {s:?} (expected a leading +/-)"), - }; - // Recognized prefixes are ASCII, so `prefix_len` is a UTF-8 boundary. - Self { - word: PauliWord::::from(&s[prefix_len..]), - phase, - } - } -} - -impl From for Phased> -where - A: PauliStorage, - H: BuildHasher + Default + HashFinalize, -{ - #[inline] - fn from(s: String) -> Self { - Self::from(s.as_str()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn phase_prefixes_roundtrip() { - for (prefix, phase) in [ - ("+", Phase::Pos1), - ("+i", Phase::PosI), - ("-", Phase::Neg1), - ("-i", Phase::NegI), - ] { - for body in ["", "XYZI"] { - let text = format!("{prefix}{body}"); - let word = Phased::::from(text.as_str()); - assert_eq!(word.phase(), phase); - assert_eq!(word.word().to_string(), body); - assert_eq!(word.to_string(), text); - } - } - } - - #[test] - #[should_panic(expected = "invalid Pauli character")] - fn unicode_body_is_rejected_by_word_parser() { - let _ = Phased::::from("+λ"); - } -} From bb2ab401f54ac891a3356c206a5ae99ad1dc79e4 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Wed, 4 Nov 2026 11:28:41 -0500 Subject: [PATCH 02/10] ppvm-phased-pauli-word-2 owns phase definition --- crates/ppvm-phased-pauli-word-2/Cargo.toml | 11 + crates/ppvm-phased-pauli-word-2/src/data.rs | 248 ++++++++++++++++++++ crates/ppvm-phased-pauli-word-2/src/lib.rs | 17 ++ 3 files changed, 276 insertions(+) create mode 100644 crates/ppvm-phased-pauli-word-2/Cargo.toml create mode 100644 crates/ppvm-phased-pauli-word-2/src/data.rs create mode 100644 crates/ppvm-phased-pauli-word-2/src/lib.rs diff --git a/crates/ppvm-phased-pauli-word-2/Cargo.toml b/crates/ppvm-phased-pauli-word-2/Cargo.toml new file mode 100644 index 000000000..d3f891f13 --- /dev/null +++ b/crates/ppvm-phased-pauli-word-2/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "ppvm-phased-pauli-word-2" +version = "0.1.0" +edition = "2024" + +[dependencies] +ppvm-traits-2 = { version = "0.1.0", path = "../ppvm-traits-2" } +ppvm-pauli-word-2 = { version = "0.1.0", path = "../ppvm-pauli-word-2" } + +[dev-dependencies] +ppvm-lossy-pauli-word-2 = { version = "0.1.0", path = "../ppvm-lossy-pauli-word-2" } diff --git a/crates/ppvm-phased-pauli-word-2/src/data.rs b/crates/ppvm-phased-pauli-word-2/src/data.rs new file mode 100644 index 000000000..511afb20c --- /dev/null +++ b/crates/ppvm-phased-pauli-word-2/src/data.rs @@ -0,0 +1,248 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Phased words with explicit prefactors, parsing, display, and equality. + +use std::fmt; +use std::hash::BuildHasher; + +use ppvm_pauli_word_2::{HashFinalize, PauliStorage, PauliWord}; +use ppvm_traits_2::{Phase, Word}; + +/// A base word with a phase in `{+1, +i, -1, -i}`, representing `i^φ · g(w)`. +/// Equality and display include the phase; site inspection uses only the word. +/// Implements neither `Hash` nor `Indexable`, so it is not a map key. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Phased { + /// The bare base word (carries no phase of its own). + pub(crate) word: W, + /// The explicit `ℤ₄` prefactor `i^φ`. + pub(crate) phase: Phase, +} + +impl Phased { + /// Wrap `word` with the trivial phase `+1`. + #[inline] + pub fn new(word: W) -> Self { + Self { + word, + phase: Phase::one(), + } + } + + /// Wrap `word` with an explicit `phase`. + #[inline] + pub fn with_phase(word: W, phase: Phase) -> Self { + Self { word, phase } + } + + /// The `ℤ₄` prefactor `i^φ`. + #[inline] + pub fn phase(&self) -> Phase { + self.phase + } + + /// Borrow the bare base word (phase stripped). + #[inline] + pub fn word(&self) -> &W { + &self.word + } + + /// Consume the wrapper, returning the base word and its phase. + #[inline] + pub fn into_parts(self) -> (W, Phase) { + (self.word, self.phase) + } + + /// Multiply the stored phase by `delta` (the `ℤ₄` group product). + #[inline] + pub fn add_phase(&mut self, delta: Phase) { + self.phase *= delta; + } + + /// Whether the phase has a positive sign (`+1` or `+i`). + #[inline] + pub fn is_positive(&self) -> bool { + matches!(self.phase, Phase::Pos1 | Phase::PosI) + } +} + +/// Site inspection delegates to the inner word; the scalar phase changes no sites. +impl Word for Phased { + type Site = W::Site; + + #[inline] + fn n_sites(&self) -> usize { + self.word.n_sites() + } + + #[inline] + fn get(&self, index: usize) -> Self::Site { + self.word.get(index) + } + + #[inline] + fn weight(&self) -> usize { + self.word.weight() + } + + #[inline] + fn iter(&self) -> impl Iterator { + self.word.iter() + } +} + +/// Renders the phase as `+`, `+i`, `-`, or `-i`, matching the legacy display. +fn phase_prefix(phase: Phase) -> &'static str { + match phase { + Phase::Pos1 => "+", + Phase::PosI => "+i", + Phase::Neg1 => "-", + Phase::NegI => "-i", + } +} + +/// `Display` prints the phase prefix followed by the base word, e.g. `+iXYZI`. +impl fmt::Display for Phased { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}{}", phase_prefix(self.phase), self.word) + } +} + +impl From<&str> for Phased> +where + A: PauliStorage, + H: BuildHasher + Default + HashFinalize, +{ + /// Parses a required `+`/`-`, optional `i`, then a [`PauliWord`] string. + /// Panics if the sign is missing or the word is invalid. + fn from(s: &str) -> Self { + let mut chars = s.chars(); + let (phase, prefix_len) = match (chars.next(), chars.next()) { + (Some('+'), Some('i')) => (Phase::PosI, 2), + (Some('-'), Some('i')) => (Phase::NegI, 2), + (Some('+'), _) => (Phase::Pos1, 1), + (Some('-'), _) => (Phase::Neg1, 1), + _ => panic!("invalid phase format: {s:?} (expected a leading +/-)"), + }; + // Recognized prefixes are ASCII, so `prefix_len` is a UTF-8 boundary. + Self { + word: PauliWord::::from(&s[prefix_len..]), + phase, + } + } +} + +impl From for Phased> +where + A: PauliStorage, + H: BuildHasher + Default + HashFinalize, +{ + #[inline] + fn from(s: String) -> Self { + Self::from(s.as_str()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ppvm_traits_2::Pauli; + + type Pw = Phased; + + #[test] + fn parsing_enforces_storage_capacity() { + for input in ["IIIIIIIII", "XXXXXXXXI", "IIIIIIIIX", "I_I_I_I_I_I_I_I_I"] { + let phased = format!("+i{input}"); + assert!( + std::panic::catch_unwind(|| { Phased::>::from(phased.as_str()) }) + .is_err() + ); + } + } + + #[test] + fn parse_sign_and_body() { + let cases = [ + ("+XYZI", Phase::Pos1), + ("-XYZI", Phase::Neg1), + ("+iXYZI", Phase::PosI), + ("-iXYZI", Phase::NegI), + ]; + for (s, want) in cases { + let pw: Pw = s.into(); + assert_eq!(pw.phase(), want, "{s} phase"); + assert_eq!(pw.n_sites(), 4, "{s} width"); + assert_eq!(pw.get(0), Pauli::X); + assert_eq!(pw.get(1), Pauli::Y); + assert_eq!(pw.get(2), Pauli::Z); + assert_eq!(pw.get(3), Pauli::I); + } + } + + #[test] + fn display_roundtrips_prefix() { + for s in ["+XYZI", "-XYZI", "+iXYZI", "-iXYZI"] { + let pw: Pw = s.into(); + assert_eq!(pw.to_string(), s); + } + } + + #[test] + fn is_positive_reads_sign() { + let pos: Pw = "+XY".into(); + let posi: Pw = "+iXY".into(); + let neg: Pw = "-XY".into(); + let negi: Pw = "-iXY".into(); + assert!(pos.is_positive() && posi.is_positive()); + assert!(!neg.is_positive() && !negi.is_positive()); + } + + #[test] + fn phase_participates_in_equality() { + let a: Pw = "+XY".into(); + let b: Pw = "-XY".into(); + let c: Pw = "+XY".into(); + assert_ne!(a, b, "different phase ⇒ distinct"); + assert_eq!(a, c); + } + + #[test] + fn weight_iter_delegate_to_word() { + let pw: Pw = "-iXIYZ".into(); + assert_eq!(pw.weight(), 3); + let via_iter: Vec = pw.iter().collect(); + assert_eq!(via_iter, vec![Pauli::X, Pauli::I, Pauli::Y, Pauli::Z]); + } + + #[test] + fn send_sync() { + fn assert_send_sync() {} + assert_send_sync::(); + } + + #[test] + fn phase_prefixes_roundtrip() { + for (prefix, phase) in [ + ("+", Phase::Pos1), + ("+i", Phase::PosI), + ("-", Phase::Neg1), + ("-i", Phase::NegI), + ] { + for body in ["", "XYZI"] { + let text = format!("{prefix}{body}"); + let word = Phased::::from(text.as_str()); + assert_eq!(word.phase(), phase); + assert_eq!(word.word().to_string(), body); + assert_eq!(word.to_string(), text); + } + } + } + + #[test] + #[should_panic(expected = "invalid Pauli character")] + fn unicode_body_is_rejected_by_word_parser() { + let _ = Phased::::from("+λ"); + } +} diff --git a/crates/ppvm-phased-pauli-word-2/src/lib.rs b/crates/ppvm-phased-pauli-word-2/src/lib.rs new file mode 100644 index 000000000..8281e238e --- /dev/null +++ b/crates/ppvm-phased-pauli-word-2/src/lib.rs @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +mod clifford; +mod data; +mod product; + +// ppvm-phased-pauli-word-2: The phased Pauli word wrapper. +// +// `Phased` wraps a base word `W` with an explicit `ℤ₄` phase — the standalone +// phased Pauli operator (`i^φ · g(w)`). Unlike the bare PauliWord / LossyPauliWord, +// whose clifford drops every conjugation sign, Phased carries a fused clifford impl +// that tracks the ℤ₄ sign (`HYH = −Y`, `SXS† = Y`, the CNOT/CZ deltas). + +pub use data::Phased; +pub type PhasedPauliWord = Phased; +pub use ppvm_pauli_word_2::PauliWord; From 8abadc8880d670f7b531af9e5c40627db004c542 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Wed, 4 Nov 2026 11:29:27 -0500 Subject: [PATCH 03/10] have to enforce that LossState trait is enforced as well for phased. Implemented clifford traits for it --- .../ppvm-phased-pauli-word-2/src/clifford.rs | 387 ++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 crates/ppvm-phased-pauli-word-2/src/clifford.rs diff --git a/crates/ppvm-phased-pauli-word-2/src/clifford.rs b/crates/ppvm-phased-pauli-word-2/src/clifford.rs new file mode 100644 index 000000000..2bcfc23fd --- /dev/null +++ b/crates/ppvm-phased-pauli-word-2/src/clifford.rs @@ -0,0 +1,387 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Phase-tracking Clifford conjugation. Gates touching lost sites are no-ops. +//! The base word must implement both `PauliBits` and `LossState`. + +use ppvm_traits_2::{Clifford, LossState, PauliBits, Phase}; + +use crate::data::Phased; + +impl Phased { + #[inline] + fn flip_sign_if(&mut self, cond: bool) { + if cond { + self.phase *= Phase::Neg1; + } + } +} + +impl Clifford for Phased { + #[inline] + fn x(&mut self, q: usize) { + if self.word.is_lost(q) { + return; + } + self.flip_sign_if(self.word.z_bit(q)); + } + + #[inline] + fn y(&mut self, q: usize) { + if self.word.is_lost(q) { + return; + } + self.flip_sign_if(self.word.x_bit(q) ^ self.word.z_bit(q)); + } + + #[inline] + fn z(&mut self, q: usize) { + if self.word.is_lost(q) { + return; + } + self.flip_sign_if(self.word.x_bit(q)); + } + + #[inline] + fn h(&mut self, q: usize) { + if self.word.is_lost(q) { + return; + } + let x = self.word.x_bit(q); + let z = self.word.z_bit(q); + self.word.set_xz_bits(q, z, x); + self.flip_sign_if(x && z); + } + + // Heisenberg convention: S† X S = -Y. + #[inline] + fn s(&mut self, q: usize) { + if self.word.is_lost(q) { + return; + } + let x = self.word.x_bit(q); + let z = self.word.z_bit(q); + self.word.set_z_bit(q, z ^ x); + self.flip_sign_if(x && !z); + } + + #[inline] + fn cnot(&mut self, ctrl: usize, tgt: usize) { + debug_assert_ne!(ctrl, tgt, "CNOT requires distinct qubits"); + debug_assert!( + ctrl < self.word.n_sites() && tgt < self.word.n_sites(), + "qubit out of bounds" + ); + if self.word.is_lost(ctrl) || self.word.is_lost(tgt) { + return; + } + let xc = self.word.x_bit(ctrl); + let zc = self.word.z_bit(ctrl); + let xt = self.word.x_bit(tgt); + let zt = self.word.z_bit(tgt); + self.word.set_x_bit_and_z_bit(tgt, xt ^ xc, ctrl, zc ^ zt); + self.flip_sign_if(xc && zt && (xt == zc)); + } + + #[inline] + fn cz(&mut self, a: usize, b: usize) { + debug_assert_ne!(a, b, "CZ requires distinct qubits"); + debug_assert!( + a < self.word.n_sites() && b < self.word.n_sites(), + "qubit out of bounds" + ); + if self.word.is_lost(a) || self.word.is_lost(b) { + return; + } + let xa = self.word.x_bit(a); + let za = self.word.z_bit(a); + let xb = self.word.x_bit(b); + let zb = self.word.z_bit(b); + self.word.set_z_bit_pair(a, za ^ xb, b, zb ^ xa); + self.flip_sign_if(xa && xb && (za ^ zb)); + } + + #[inline] + fn s_dag(&mut self, q: usize) { + if self.word.is_lost(q) { + return; + } + let x = self.word.x_bit(q); + let z = self.word.z_bit(q); + self.word.set_z_bit(q, z ^ x); + self.flip_sign_if(x && z); + } + + #[inline] + fn sqrt_x(&mut self, q: usize) { + if self.word.is_lost(q) { + return; + } + let x = self.word.x_bit(q); + let z = self.word.z_bit(q); + self.word.set_x_bit(q, x ^ z); + self.flip_sign_if(x && z); + } + + #[inline] + fn sqrt_x_dag(&mut self, q: usize) { + if self.word.is_lost(q) { + return; + } + let x = self.word.x_bit(q); + let z = self.word.z_bit(q); + self.word.set_x_bit(q, x ^ z); + self.flip_sign_if(!x && z); + } + + #[inline] + fn sqrt_y(&mut self, q: usize) { + if self.word.is_lost(q) { + return; + } + let x = self.word.x_bit(q); + let z = self.word.z_bit(q); + self.word.set_xz_bits(q, z, x); + self.flip_sign_if(!x && z); + } + + #[inline] + fn sqrt_y_dag(&mut self, q: usize) { + if self.word.is_lost(q) { + return; + } + let x = self.word.x_bit(q); + let z = self.word.z_bit(q); + self.word.set_xz_bits(q, z, x); + self.flip_sign_if(x && !z); + } + + #[inline] + fn cy(&mut self, control: usize, target: usize) { + debug_assert_ne!(control, target, "CY requires distinct qubits"); + debug_assert!( + control < self.word.n_sites() && target < self.word.n_sites(), + "qubit out of bounds" + ); + if self.word.is_lost(control) || self.word.is_lost(target) { + return; + } + let xc = self.word.x_bit(control); + let zc = self.word.z_bit(control); + let xt = self.word.x_bit(target); + let zt = self.word.z_bit(target); + self.word + .set_xz_bits2(control, xc, zc ^ xt ^ zt, target, xt ^ xc, zt ^ xc); + self.flip_sign_if(xc && (xt ^ zt) && !(zc ^ zt)); + } +} + +#[cfg(test)] +mod tests { + use crate::{Phased, PhasedPauliWord}; + use ppvm_lossy_pauli_word_2::LossyPauliWord; + use ppvm_traits_2::Clifford; + + type TestWord = Phased; + + #[test] + fn gates_touching_lost_sites_preserve_word_and_phase() { + use ppvm_traits_2::Phase; + + let single: [fn(&mut TestWord, usize); 10] = [ + TestWord::x, + TestWord::y, + TestWord::z, + TestWord::h, + TestWord::s, + TestWord::s_dag, + TestWord::sqrt_x, + TestWord::sqrt_x_dag, + TestWord::sqrt_y, + TestWord::sqrt_y_dag, + ]; + let pairs: [fn(&mut TestWord, usize, usize); 3] = + [TestWord::cnot, TestWord::cz, TestWord::cy]; + for phase in [Phase::Pos1, Phase::PosI, Phase::Neg1, Phase::NegI] { + for input in ["XL", "LY", "LL"] { + let original = Phased::with_phase(LossyPauliWord::from(input), phase); + let lost = if input.starts_with('L') { 0 } else { 1 }; + for gate in single { + let mut word = original.clone(); + gate(&mut word, lost); + assert_eq!(word, original); + } + for gate in pairs { + for (a, b) in [(0, 1), (1, 0)] { + let mut word = original.clone(); + gate(&mut word, a, b); + assert_eq!(word, original); + } + } + } + } + } + fn conj(input: &str, gate: impl Fn(&mut PhasedPauliWord)) -> String { + let mut w = PhasedPauliWord::from(input); + gate(&mut w); + w.to_string() + } + + #[test] + fn ordinary_word_loss_operations() { + use crate::PauliWord; + use ppvm_traits_2::{Indexable, LossState}; + use std::panic::{AssertUnwindSafe, catch_unwind}; + + let mut word: PauliWord = PauliWord::from("XYZ"); + assert_eq!(word.loss_weight(), 0); + for i in 0..3 { + assert!(!word.is_lost(i)); + } + let original = word; + let original_hash = word.key_hash(); + assert!(catch_unwind(AssertUnwindSafe(|| word.set_lost(1))).is_err()); + assert_eq!(word, original); + assert_eq!(word.key_hash(), original_hash); + + let cleared = word.loss_cleared(1); + assert_eq!(word, original); + let expected = PauliWord::from("XIZ"); + assert_eq!(cleared, expected); + assert_eq!(cleared.key_hash(), expected.key_hash()); + word.clear_lost(1); + assert_eq!(word, expected); + assert_eq!(word.key_hash(), expected.key_hash()); + } + + #[test] + fn single_qubit_gates_track_sign() { + // (gate, input → target) ported from the old phase/clifford.rs tests. + let x = [("+I", "+I"), ("+X", "+X"), ("+Y", "-Y"), ("+Z", "-Z")]; + let y = [("+I", "+I"), ("+X", "-X"), ("+Y", "+Y"), ("+Z", "-Z")]; + let z = [("+I", "+I"), ("+X", "-X"), ("+Y", "-Y"), ("+Z", "+Z")]; + let h = [("+I", "+I"), ("+X", "+Z"), ("+Y", "-Y"), ("+Z", "+X")]; + let s = [("+I", "+I"), ("+X", "-Y"), ("+Y", "+X"), ("+Z", "+Z")]; + for (i, t) in x { + assert_eq!(conj(i, |w| w.x(0)), t, "X {i}"); + } + for (i, t) in y { + assert_eq!(conj(i, |w| w.y(0)), t, "Y {i}"); + } + for (i, t) in z { + assert_eq!(conj(i, |w| w.z(0)), t, "Z {i}"); + } + for (i, t) in h { + assert_eq!(conj(i, |w| w.h(0)), t, "H {i}"); + } + for (i, t) in s { + assert_eq!(conj(i, |w| w.s(0)), t, "S {i}"); + } + } + + #[test] + fn cnot_tracks_sign() { + for (i, t) in [ + ("+II", "+II"), + ("+IX", "+IX"), + ("+IZ", "+ZZ"), + ("+IY", "+ZY"), + ("+XI", "+XX"), + ("+XX", "+XI"), + ("+XY", "+YZ"), + ("+XZ", "-YY"), + ("+ZI", "+ZI"), + ("+ZX", "+ZX"), + ("+ZY", "+IY"), + ("+ZZ", "+IZ"), + ("+YI", "+YX"), + ("+YX", "+YI"), + ("+YY", "-XZ"), + ("+YZ", "+XY"), + ] { + assert_eq!(conj(i, |w| w.cnot(0, 1)), t, "CNOT {i}"); + } + } + + #[test] + fn cz_tracks_sign() { + for (i, t) in [ + ("+II", "+II"), + ("+IX", "+ZX"), + ("+IY", "+ZY"), + ("+IZ", "+IZ"), + ("+XI", "+XZ"), + ("+XX", "+YY"), + ("+XY", "-YX"), + ("+XZ", "+XI"), + ("+ZI", "+ZI"), + ("+ZX", "+IX"), + ("+ZY", "+IY"), + ("+ZZ", "+ZZ"), + ("+YI", "+YZ"), + ("+YX", "-XY"), + ("+YY", "+XX"), + ("+YZ", "+YI"), + ] { + assert_eq!(conj(i, |w| w.cz(0, 1)), t, "CZ {i}"); + } + } + + #[test] + fn phase_carries_through_gate() { + // A pre-existing +i factor is preserved under a signed conjugation: + // H (−iY) = −i·(−Y) = +iY. + assert_eq!(conj("-iY", |w| w.h(0)), "+iY"); + } + + #[test] + fn extension_gates_track_sign() { + // The single-qubit table of `ppvm-traits/src/traits/clifford.rs`. + let s_dag = [("+I", "+I"), ("+X", "+Y"), ("+Y", "-X"), ("+Z", "+Z")]; + let sqrt_x = [("+I", "+I"), ("+X", "+X"), ("+Y", "-Z"), ("+Z", "+Y")]; + let sqrt_x_dag = [("+I", "+I"), ("+X", "+X"), ("+Y", "+Z"), ("+Z", "-Y")]; + let sqrt_y = [("+I", "+I"), ("+X", "+Z"), ("+Y", "+Y"), ("+Z", "-X")]; + let sqrt_y_dag = [("+I", "+I"), ("+X", "-Z"), ("+Y", "+Y"), ("+Z", "+X")]; + for (i, t) in s_dag { + assert_eq!(conj(i, |w| w.s_dag(0)), t, "S† {i}"); + } + for (i, t) in sqrt_x { + assert_eq!(conj(i, |w| w.sqrt_x(0)), t, "√X {i}"); + } + for (i, t) in sqrt_x_dag { + assert_eq!(conj(i, |w| w.sqrt_x_dag(0)), t, "√X† {i}"); + } + for (i, t) in sqrt_y { + assert_eq!(conj(i, |w| w.sqrt_y(0)), t, "√Y {i}"); + } + for (i, t) in sqrt_y_dag { + assert_eq!(conj(i, |w| w.sqrt_y_dag(0)), t, "√Y† {i}"); + } + } + + #[test] + fn cy_tracks_sign() { + // The 16-entry CY table of `ppvm-traits/src/traits/clifford.rs`. + for (i, t) in [ + ("+II", "+II"), + ("+IX", "+ZX"), + ("+IY", "+IY"), + ("+IZ", "+ZZ"), + ("+XI", "+XY"), + ("+XX", "-YZ"), + ("+XY", "+XI"), + ("+XZ", "+YX"), + ("+YI", "+YY"), + ("+YX", "+XZ"), + ("+YY", "+YI"), + ("+YZ", "-XX"), + ("+ZI", "+ZI"), + ("+ZX", "+IX"), + ("+ZY", "+ZY"), + ("+ZZ", "+IZ"), + ] { + assert_eq!(conj(i, |w| w.cy(0, 1)), t, "CY {i}"); + assert_eq!(conj(i, |w| w.zcy(0, 1)), t, "ZCY {i}"); + } + } +} From d0c7eb1591bf158bdcc04c6286f1c66a6e7e3fb0 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Wed, 4 Nov 2026 11:29:50 -0500 Subject: [PATCH 04/10] inmplemented key product trait for product --- .../ppvm-phased-pauli-word-2/src/product.rs | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 crates/ppvm-phased-pauli-word-2/src/product.rs diff --git a/crates/ppvm-phased-pauli-word-2/src/product.rs b/crates/ppvm-phased-pauli-word-2/src/product.rs new file mode 100644 index 000000000..9d54feeb1 --- /dev/null +++ b/crates/ppvm-phased-pauli-word-2/src/product.rs @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2026 The PPVM Authors +// SPDX-License-Identifier: Apache-2.0 + +//! Multiplication for [`Phased`] where `W: KeyProduct`. +//! Reuses [`KeyProduct::key_mul`] and combines its residual phase with both prefactors. +//! `Mul` and `MulAssign` delegate to the same product implementation. +//! Lossy words do not implement `KeyProduct`, so they have no phased product. + +use std::ops::{Mul, MulAssign}; + +use ppvm_traits_2::KeyProduct; + +use crate::data::Phased; + +impl Phased { + /// Multiply the base words and combine both prefactors with the emitted phase. + /// `(φ, v) · (ψ, w) = (φ · ψ · iᵏ, v·w)`, where `key_mul` emits `iᵏ`. + #[inline] + pub fn mul(&self, rhs: &Self) -> Self { + let (word, emitted) = self.word.key_mul(&rhs.word); + Self { + word, + phase: self.phase * rhs.phase * emitted, + } + } +} + +impl Mul for Phased { + type Output = Phased; + + #[inline] + fn mul(self, rhs: Self) -> Self { + Phased::mul(&self, &rhs) + } +} + +impl Mul<&Phased> for &Phased { + type Output = Phased; + + #[inline] + fn mul(self, rhs: &Phased) -> Phased { + Phased::mul(self, rhs) + } +} + +impl MulAssign<&Phased> for Phased { + #[inline] + fn mul_assign(&mut self, rhs: &Phased) { + *self = Phased::mul(self, rhs); + } +} + +impl MulAssign for Phased { + #[inline] + fn mul_assign(&mut self, rhs: Phased) { + *self = Phased::mul(self, &rhs); + } +} + +#[cfg(test)] +mod tests { + use crate::PhasedPauliWord; + + fn product(lhs: &str, rhs: &str) -> String { + let x: PhasedPauliWord = lhs.into(); + let y: PhasedPauliWord = rhs.into(); + (x * y).to_string() + } + + #[test] + fn single_qubit_products() { + // Ported from the old phase/mul.rs tests. + for (lhs, rhs, ans) in [("+X", "+X", "+I"), ("+X", "+Y", "+iZ"), ("+X", "+Z", "-iY")] { + assert_eq!(product(lhs, rhs), ans, "{lhs}*{rhs}"); + } + } + + #[test] + fn multi_qubit_and_phase_accumulation() { + for (lhs, rhs, ans) in [ + ("+ZI", "-ZI", "-II"), + ("+II", "-ZI", "-ZI"), + ("+XI", "+iXI", "+iII"), + ("-XX", "-XX", "+II"), + ] { + assert_eq!(product(lhs, rhs), ans, "{lhs}*{rhs}"); + } + } + + #[test] + fn operator_forms_agree() { + let a: PhasedPauliWord = "+iXY".into(); + let b: PhasedPauliWord = "-ZX".into(); + let by_ref = (&a * &b).to_string(); + let mut acc = a.clone(); + acc *= &b; + assert_eq!(by_ref, acc.to_string()); + assert_eq!(by_ref, a.mul(&b).to_string()); + assert_eq!(by_ref, (a * b).to_string()); + } + + #[test] + fn square_is_identity_up_to_phase() { + // Every Pauli word with prefactor +1 squares to +I on all sites. + for s in ["+XYZI", "+YYXZ", "+ZZZZ", "+IXYX"] { + let w: PhasedPauliWord = s.into(); + let sq = w.mul(&w); + assert_eq!(sq.to_string(), "+IIII", "{s}²"); + } + } +} From de915a34224eaa9b299175a8f9c0837e71274e33 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Thu, 5 Nov 2026 10:40:14 -0500 Subject: [PATCH 05/10] publish = false on ppvm-phased-pauli-word-2 --- crates/ppvm-phased-pauli-word-2/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ppvm-phased-pauli-word-2/Cargo.toml b/crates/ppvm-phased-pauli-word-2/Cargo.toml index d3f891f13..177129008 100644 --- a/crates/ppvm-phased-pauli-word-2/Cargo.toml +++ b/crates/ppvm-phased-pauli-word-2/Cargo.toml @@ -2,6 +2,7 @@ name = "ppvm-phased-pauli-word-2" version = "0.1.0" edition = "2024" +publish = false [dependencies] ppvm-traits-2 = { version = "0.1.0", path = "../ppvm-traits-2" } From 30a05a3f7d840aa592add614a89302862f352211 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Thu, 5 Nov 2026 10:44:01 -0500 Subject: [PATCH 06/10] removed version specification in cargo.toml --- crates/ppvm-phased-pauli-word-2/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ppvm-phased-pauli-word-2/Cargo.toml b/crates/ppvm-phased-pauli-word-2/Cargo.toml index 177129008..e4a9836d0 100644 --- a/crates/ppvm-phased-pauli-word-2/Cargo.toml +++ b/crates/ppvm-phased-pauli-word-2/Cargo.toml @@ -5,8 +5,8 @@ edition = "2024" publish = false [dependencies] -ppvm-traits-2 = { version = "0.1.0", path = "../ppvm-traits-2" } -ppvm-pauli-word-2 = { version = "0.1.0", path = "../ppvm-pauli-word-2" } +ppvm-traits-2 = { path = "../ppvm-traits-2" } +ppvm-pauli-word-2 = { path = "../ppvm-pauli-word-2" } [dev-dependencies] -ppvm-lossy-pauli-word-2 = { version = "0.1.0", path = "../ppvm-lossy-pauli-word-2" } +ppvm-lossy-pauli-word-2 = { path = "../ppvm-lossy-pauli-word-2" } From 4943544621696e2438dd1c4bfd19f1b296869d70 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Thu, 5 Nov 2026 16:17:21 -0500 Subject: [PATCH 07/10] Split up the LossState trait into LossInfo and LossState. Enforced those trait implementations across ppvm-traits-2, ppvm-lossy-pauli-word-2, and the new phase one --- crates/ppvm-lossy-pauli-word-2/src/data.rs | 16 +++++----- crates/ppvm-pauli-word-2/src/data.rs | 21 +++---------- .../ppvm-phased-pauli-word-2/src/clifford.rs | 30 +++++-------------- crates/ppvm-traits-2/src/containers/batch.rs | 6 ++-- crates/ppvm-traits-2/src/loss.rs | 8 +++-- 5 files changed, 30 insertions(+), 51 deletions(-) diff --git a/crates/ppvm-lossy-pauli-word-2/src/data.rs b/crates/ppvm-lossy-pauli-word-2/src/data.rs index 743b223b6..165076e04 100644 --- a/crates/ppvm-lossy-pauli-word-2/src/data.rs +++ b/crates/ppvm-lossy-pauli-word-2/src/data.rs @@ -9,7 +9,7 @@ use std::{ use bitvec::array::BitArray; use ppvm_pauli_word_2::{DefaultStorage, PauliStorage}; -use ppvm_traits_2::{LossState, LossySite, Pauli, PauliBits, Word}; +use ppvm_traits_2::{LossState, LossySite, Pauli, PauliBits, Word, loss::LossInfo}; pub struct LossyPauliWord { pub(crate) xbits: BitArray, @@ -426,12 +426,19 @@ impl PauliBits for LossyPauliWord { } } -impl LossState for LossyPauliWord { +impl LossInfo for LossyPauliWord { #[inline] fn is_lost(&self, i: usize) -> bool { LossyPauliWord::is_lost(self, i) } + #[inline] + fn loss_weight(&self) -> usize { + LossyPauliWord::loss_weight(self) + } +} + +impl LossState for LossyPauliWord { #[inline] fn set_lost(&mut self, i: usize) { LossyPauliWord::set_lost(self, i); @@ -441,11 +448,6 @@ impl LossState for LossyPauliWord { fn clear_lost(&mut self, i: usize) { LossyPauliWord::clear_loss(self, i); } - - #[inline] - fn loss_weight(&self) -> usize { - LossyPauliWord::loss_weight(self) - } } #[cfg(test)] diff --git a/crates/ppvm-pauli-word-2/src/data.rs b/crates/ppvm-pauli-word-2/src/data.rs index 93f441269..64620727c 100644 --- a/crates/ppvm-pauli-word-2/src/data.rs +++ b/crates/ppvm-pauli-word-2/src/data.rs @@ -10,7 +10,7 @@ use std::marker::PhantomData; use bitvec::array::BitArray; use bitvec::view::BitView; use num::{One, Zero}; -use ppvm_traits_2::{LossState, Pauli, PauliBits, Word}; +use ppvm_traits_2::{Pauli, PauliBits, Word, loss::LossInfo}; use crate::hash::structural_hash; use crate::{DefaultStorage, HashFinalize, PauliStorage}; @@ -226,23 +226,10 @@ impl Word for PauliWord { } } -/// Ordinary words report no loss. Setting loss is unsupported; clearing resets a site. -impl LossState for PauliWord -where +/// Ordinary words report no loss. So, we just have NOOP LossInfo reporting. +impl LossInfo for PauliWord where A: PauliStorage, - H: BuildHasher + Default + HashFinalize, -{ - /// Panics because ordinary Pauli words cannot represent lost sites. - fn set_lost(&mut self, _i: usize) { - panic!("PauliWord cannot represent loss; use LossyPauliWord instead"); - } - - /// Resets the site to identity and refreshes the structural hash if needed. - #[inline] - fn clear_lost(&mut self, i: usize) { - self.set_xz_bits(i, false, false); - } -} + H: BuildHasher + Default + HashFinalize {} impl PauliBits for PauliWord where diff --git a/crates/ppvm-phased-pauli-word-2/src/clifford.rs b/crates/ppvm-phased-pauli-word-2/src/clifford.rs index 2bcfc23fd..652ce1464 100644 --- a/crates/ppvm-phased-pauli-word-2/src/clifford.rs +++ b/crates/ppvm-phased-pauli-word-2/src/clifford.rs @@ -2,13 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 //! Phase-tracking Clifford conjugation. Gates touching lost sites are no-ops. -//! The base word must implement both `PauliBits` and `LossState`. +//! The base word must implement both `PauliBits` and `LossInfo`. -use ppvm_traits_2::{Clifford, LossState, PauliBits, Phase}; +use ppvm_traits_2::{Clifford, PauliBits, Phase, loss::LossInfo}; use crate::data::Phased; -impl Phased { +impl Phased { #[inline] fn flip_sign_if(&mut self, cond: bool) { if cond { @@ -17,7 +17,7 @@ impl Phased { } } -impl Clifford for Phased { +impl Clifford for Phased { #[inline] fn x(&mut self, q: usize) { if self.word.is_lost(q) { @@ -221,6 +221,7 @@ mod tests { } } } + fn conj(input: &str, gate: impl Fn(&mut PhasedPauliWord)) -> String { let mut w = PhasedPauliWord::from(input); gate(&mut w); @@ -228,30 +229,15 @@ mod tests { } #[test] - fn ordinary_word_loss_operations() { + fn ordinary_word_reports_no_loss() { use crate::PauliWord; - use ppvm_traits_2::{Indexable, LossState}; - use std::panic::{AssertUnwindSafe, catch_unwind}; + use ppvm_traits_2::loss::LossInfo; - let mut word: PauliWord = PauliWord::from("XYZ"); + let word: PauliWord = PauliWord::from("XYZ"); assert_eq!(word.loss_weight(), 0); for i in 0..3 { assert!(!word.is_lost(i)); } - let original = word; - let original_hash = word.key_hash(); - assert!(catch_unwind(AssertUnwindSafe(|| word.set_lost(1))).is_err()); - assert_eq!(word, original); - assert_eq!(word.key_hash(), original_hash); - - let cleared = word.loss_cleared(1); - assert_eq!(word, original); - let expected = PauliWord::from("XIZ"); - assert_eq!(cleared, expected); - assert_eq!(cleared.key_hash(), expected.key_hash()); - word.clear_lost(1); - assert_eq!(word, expected); - assert_eq!(word.key_hash(), expected.key_hash()); } #[test] diff --git a/crates/ppvm-traits-2/src/containers/batch.rs b/crates/ppvm-traits-2/src/containers/batch.rs index 663e463b1..ccc2224db 100644 --- a/crates/ppvm-traits-2/src/containers/batch.rs +++ b/crates/ppvm-traits-2/src/containers/batch.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::containers::Indexable; -use crate::loss::LossState; +use crate::loss::LossInfo; use crate::word::PauliBits; /// An [`Indexable`] key with a structure-of-arrays column representation. @@ -86,12 +86,12 @@ where } } -/// Row-level loss access for columns whose keys implement [`LossState`]. +/// Row-level loss access for columns whose keys implement [`LossInfo`]. /// Defaults materialize the row; packed columns override. No blanket impl, for /// the same reason as [`PauliColumn`]. pub trait LossColumn: KeyColumn where - Self::Key: LossState, + Self::Key: LossInfo, { /// Read one row's loss bit. #[inline] diff --git a/crates/ppvm-traits-2/src/loss.rs b/crates/ppvm-traits-2/src/loss.rs index 3e2b8e604..2cf942066 100644 --- a/crates/ppvm-traits-2/src/loss.rs +++ b/crates/ppvm-traits-2/src/loss.rs @@ -8,8 +8,9 @@ pub enum LossySite { /// A lost site - the qubit no longer participates Lost, } -/// Loss state associated with word sites. -pub trait LossState { + +/// Read only loss gathering information +pub trait LossInfo { /// Whether site `i` is lost. Lossy words override this. fn is_lost(&self, _i: usize) -> bool { false @@ -19,7 +20,10 @@ pub trait LossState { fn loss_weight(&self) -> usize { 0 } +} +/// Loss state associated with word sites. +pub trait LossState { /// Mark index `i` lost. fn set_lost(&mut self, i: usize); From d2284e0aacea1dcb0972df4445f01163643e5590 Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Thu, 5 Nov 2026 16:45:47 -0500 Subject: [PATCH 08/10] changes to have an in place version of key_mul --- crates/ppvm-pauli-word-2/src/product.rs | 23 +++++++++++-------- .../ppvm-phased-pauli-word-2/src/product.rs | 18 +++++++-------- crates/ppvm-traits-2/src/algebra.rs | 1 + 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/crates/ppvm-pauli-word-2/src/product.rs b/crates/ppvm-pauli-word-2/src/product.rs index 0b5df5b75..15ea2b64f 100644 --- a/crates/ppvm-pauli-word-2/src/product.rs +++ b/crates/ppvm-pauli-word-2/src/product.rs @@ -23,19 +23,24 @@ where /// Returns the XORed word and its residual phase for the coefficient to absorb. /// Both inputs must have equal width; zero unused bits stay zero. fn key_mul(&self, other: &Self) -> (Self, Phase) { + let mut product = self.clone(); + let phase = product.key_mul_assign(other); + (product, phase) + } + + /// Multiplies in place, returning the residual phase and refreshing the hash once. + #[inline(always)] + fn key_mul_assign(&mut self, other: &Self) -> Phase { debug_assert_eq!( self.nqubits, other.nqubits, "twisted product requires equal-width words", ); - let lhs_x = self.xbits.data.as_raw_slice(); - let lhs_z = self.zbits.data.as_raw_slice(); + let lhs_x = self.xbits.data.as_raw_mut_slice(); + let lhs_z = self.zbits.data.as_raw_mut_slice(); let rhs_x = other.xbits.data.as_raw_slice(); let rhs_z = other.zbits.data.as_raw_slice(); - let mut product = Self::new(self.nqubits); - let product_x = product.xbits.data.as_raw_mut_slice(); - let product_z = product.zbits.data.as_raw_mut_slice(); let mut sign_count = 0u32; let mut imaginary_count = 0u32; @@ -56,13 +61,13 @@ where imaginary_count += imaginary_mask.count_ones(); // XOR gives the product's Pauli letters; the masks account for phase. - product_x[chunk] = left_x ^ right_x; - product_z[chunk] = left_z ^ right_z; + lhs_x[chunk] = left_x ^ right_x; + lhs_z[chunk] = left_z ^ right_z; } - product.refresh_hash(); + self.refresh_hash(); let phase_exponent = ((2 * sign_count + imaginary_count) % 4) as u8; - (product, Phase::from_exponent(phase_exponent)) + Phase::from_exponent(phase_exponent) } } diff --git a/crates/ppvm-phased-pauli-word-2/src/product.rs b/crates/ppvm-phased-pauli-word-2/src/product.rs index 9d54feeb1..7bdfda51d 100644 --- a/crates/ppvm-phased-pauli-word-2/src/product.rs +++ b/crates/ppvm-phased-pauli-word-2/src/product.rs @@ -17,11 +17,9 @@ impl Phased { /// `(φ, v) · (ψ, w) = (φ · ψ · iᵏ, v·w)`, where `key_mul` emits `iᵏ`. #[inline] pub fn mul(&self, rhs: &Self) -> Self { - let (word, emitted) = self.word.key_mul(&rhs.word); - Self { - word, - phase: self.phase * rhs.phase * emitted, - } + let mut product = self.clone(); + product *= rhs; + product } } @@ -29,8 +27,9 @@ impl Mul for Phased { type Output = Phased; #[inline] - fn mul(self, rhs: Self) -> Self { - Phased::mul(&self, &rhs) + fn mul(mut self, rhs: Self) -> Self { + self *= &rhs; + self } } @@ -46,14 +45,15 @@ impl Mul<&Phased> for &Phased { impl MulAssign<&Phased> for Phased { #[inline] fn mul_assign(&mut self, rhs: &Phased) { - *self = Phased::mul(self, rhs); + let emitted = self.word.key_mul_assign(&rhs.word); + self.phase *= rhs.phase * emitted; } } impl MulAssign for Phased { #[inline] fn mul_assign(&mut self, rhs: Phased) { - *self = Phased::mul(self, &rhs); + *self *= &rhs; } } diff --git a/crates/ppvm-traits-2/src/algebra.rs b/crates/ppvm-traits-2/src/algebra.rs index a61bf21cd..04ef525d6 100644 --- a/crates/ppvm-traits-2/src/algebra.rs +++ b/crates/ppvm-traits-2/src/algebra.rs @@ -94,6 +94,7 @@ impl core::ops::MulAssign for Phase { 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); + fn key_mul_assign(&mut self, other: &Self) -> Phase; } /// A commutative coefficient ring with an imaginary unit. From 5048886413f82d27c09ce8a64a1bf9cce14f0155 Mon Sep 17 00:00:00 2001 From: David Plankensteiner Date: Fri, 6 Nov 2026 14:08:30 +0100 Subject: [PATCH 09/10] style: fix pre-commit failures (whitespace, fmt, clippy clone_on_copy) Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/ppvm-pauli-word-2/src/data.rs | 7 +++++-- crates/ppvm-pauli-word-2/src/product.rs | 2 +- crates/ppvm-traits-2/src/loss.rs | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/ppvm-pauli-word-2/src/data.rs b/crates/ppvm-pauli-word-2/src/data.rs index 64620727c..0af30f10e 100644 --- a/crates/ppvm-pauli-word-2/src/data.rs +++ b/crates/ppvm-pauli-word-2/src/data.rs @@ -227,9 +227,12 @@ impl Word for PauliWord { } /// Ordinary words report no loss. So, we just have NOOP LossInfo reporting. -impl LossInfo for PauliWord where +impl LossInfo for PauliWord +where A: PauliStorage, - H: BuildHasher + Default + HashFinalize {} + H: BuildHasher + Default + HashFinalize, +{ +} impl PauliBits for PauliWord where diff --git a/crates/ppvm-pauli-word-2/src/product.rs b/crates/ppvm-pauli-word-2/src/product.rs index 15ea2b64f..a82b9245d 100644 --- a/crates/ppvm-pauli-word-2/src/product.rs +++ b/crates/ppvm-pauli-word-2/src/product.rs @@ -23,7 +23,7 @@ where /// Returns the XORed word and its residual phase for the coefficient to absorb. /// Both inputs must have equal width; zero unused bits stay zero. fn key_mul(&self, other: &Self) -> (Self, Phase) { - let mut product = self.clone(); + let mut product = *self; let phase = product.key_mul_assign(other); (product, phase) } diff --git a/crates/ppvm-traits-2/src/loss.rs b/crates/ppvm-traits-2/src/loss.rs index 2cf942066..e8b88081d 100644 --- a/crates/ppvm-traits-2/src/loss.rs +++ b/crates/ppvm-traits-2/src/loss.rs @@ -9,7 +9,7 @@ pub enum LossySite { Lost, } -/// Read only loss gathering information +/// Read only loss gathering information pub trait LossInfo { /// Whether site `i` is lost. Lossy words override this. fn is_lost(&self, _i: usize) -> bool { From e8f9f48d08dc0a0389fdda80b0d3821d0803c39c Mon Sep 17 00:00:00 2001 From: jonhas colina Date: Fri, 6 Nov 2026 10:24:34 -0500 Subject: [PATCH 10/10] bounded LossState to LossInfo --- crates/ppvm-traits-2/src/loss.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ppvm-traits-2/src/loss.rs b/crates/ppvm-traits-2/src/loss.rs index e8b88081d..96dce0146 100644 --- a/crates/ppvm-traits-2/src/loss.rs +++ b/crates/ppvm-traits-2/src/loss.rs @@ -23,7 +23,7 @@ pub trait LossInfo { } /// Loss state associated with word sites. -pub trait LossState { +pub trait LossState: LossInfo { /// Mark index `i` lost. fn set_lost(&mut self, i: usize);