Translation symmetry: TranslationGroup + orbit-representative Pauli-sum merging#180
Translation symmetry: TranslationGroup + orbit-representative Pauli-sum merging#180AlexSchuckert wants to merge 3 commits into
Conversation
…erging TranslationGroup (finite abelian permutation groups: 1D chains, 2D/3D tori, multi-leg ladders, arbitrary generators) with lex-min orbit canonicalization, shift counters, and momentum-sector characters. Pauli-sum merging in both sectors: canonicalize_pauli_sum / symmetry_merge_pauli_sum (k=0, real coefficients) and canonicalize_pauli_sum_complex (k≠0, character-weighted projection, 1/|G| normalization), plus check_momentum_sector to validate an input before projecting. Following Teng, Chang, Rudolph & Holmes (arXiv:2512.12094). Split 1/4 of the CTPP work (see PR body for the stack); full development history on branch continuous-time-pauli-propagation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
There was a problem hiding this comment.
Pull request overview
Adds a new Rust-only symmetry layer to ppvm-pauli-sum for translation-invariant (and momentum-resolved) Pauli-sum canonicalization/merging, intended to support upcoming continuous-time Pauli propagation work (#178 split).
Changes:
- Introduces
ppvm_pauli_sum::symmetrywithTranslationGroupplus orbit-canonicalization utilities. - Adds real (
k=0) and complex/momentum-sector merging/canonicalization helpers, along with momentum-sector validation. - Exposes the new module from
ppvm-pauli-sum’s public API and adds a comprehensive test suite for canonicalization/merge behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| crates/ppvm-pauli-sum/src/symmetry.rs | New symmetry module: translation group model, orbit canonicalization, (momentum) merging, and tests. |
| crates/ppvm-pauli-sum/src/lib.rs | Exposes the new symmetry module publicly. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Self { | ||
| n_qubits, | ||
| perms, | ||
| orders, | ||
| } |
| /// Total group order: `Π orders[g]`. | ||
| pub fn order(&self) -> usize { | ||
| self.orders.iter().map(|&o| o as usize).product() | ||
| } |
| let inv_g: f64 = 1.0 / (group.order() as f64); | ||
| let mut merged: FxHashMap<PauliWord<A, S, R>, Complex<f64>> = | ||
| FxHashMap::with_capacity_and_hasher(basis.len(), Default::default()); | ||
| for (w, &c) in basis.iter().zip(coeffs.iter()) { | ||
| let (rep, cnt) = group.canonicalize_with_shift(w); | ||
| let chi = group.character(k_modes, &cnt); | ||
| let contrib = inv_g * chi * c; | ||
| *merged.entry(rep).or_insert(Complex::new(0.0, 0.0)) += contrib; | ||
| } |
| /// For the `k_modes = [0, 0, …]` (trivial) sector this reduces to plain | ||
| /// [`canonicalize_pauli_sum`] (real coefficients work, but on complex | ||
| /// input the result is complex with vanishing imaginary part). |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| pub fn chain_1d(n: usize) -> Self { | ||
| let perm: Vec<u32> = (0..n).map(|q| ((q + 1) % n) as u32).collect(); | ||
| Self::from_generators(n, vec![perm], vec![n as u32]) | ||
| } |
| pub fn torus_2d(lx: usize, ly: usize) -> Self { | ||
| let n = lx * ly; |
| pub fn torus_3d(lx: usize, ly: usize, lz: usize) -> Self { | ||
| let n = lx * ly * lz; |
| pub fn ladder(l: usize, n_legs: usize) -> Self { | ||
| let n = l * n_legs; |
| for &o in &self.orders { | ||
| counters.push((rem as u32) % o); | ||
| rem /= o as usize; | ||
| } |
| for &o in &self.orders { | ||
| counter.push((rem as u32) % o); | ||
| rem /= o as usize; | ||
| } |
| for (g, &o) in self.orders.iter().enumerate() { | ||
| let c = (rem as u32) % o; | ||
| rem /= o as usize; |
| /// For the `k_modes = [0, 0, …]` (trivial) sector this reduces to plain | ||
| /// [`canonicalize_pauli_sum`] (real coefficients work, but on complex | ||
| /// input the result is complex with vanishing imaginary part). |
| for (g, perm) in perms.iter().enumerate() { | ||
| assert_eq!( | ||
| perm.len(), | ||
| n_qubits, | ||
| "generator {g} permutation has length {} != n_qubits {n_qubits}", | ||
| perm.len() | ||
| ); | ||
| let mut seen = vec![false; n_qubits]; | ||
| for &p in perm { | ||
| assert!( | ||
| (p as usize) < n_qubits, | ||
| "generator {g} maps to out-of-range position {p}" | ||
| ); | ||
| assert!( | ||
| !seen[p as usize], | ||
| "generator {g} is not a permutation (duplicate target {p})" | ||
| ); | ||
| seen[p as usize] = true; | ||
| } | ||
| } | ||
| Self { | ||
| n_qubits, | ||
| perms, | ||
| orders, | ||
| } |
|
@AlexSchuckert I think this is a pretty good boiled down PR here. I just saw that the single file here was pretty large. codex suggested some semantic split, see below. Codex output:
The I would not split the individual lattice constructors into separate files—the 1D/2D/3D/ladder constructors are cohesive parts of |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (6)
crates/ppvm-pauli-sum/src/symmetry.rs:101
from_generatorsacceptsordersthat can be zero. That leads to division/modulo by zero later (e.g.characterdivides byorders[g], and mixed-radix decoding uses% o). Add an upfrontorder > 0validation per generator.
pub fn from_generators(n_qubits: usize, perms: Vec<Vec<u32>>, orders: Vec<u32>) -> Self {
assert_eq!(perms.len(), orders.len(), "perms and orders must match");
for (g, perm) in perms.iter().enumerate() {
crates/ppvm-pauli-sum/src/symmetry.rs:219
TranslationGroup::order()multiplies generator orders withproduct(), which can silently overflowusizein release builds. That can makeorder()wrong and can even feed1.0 / orderin momentum merging. Use checked multiplication and fail fast on overflow.
/// Total group order: `Π orders[g]`.
pub fn order(&self) -> usize {
self.orders.iter().map(|&o| o as usize).product()
}
crates/ppvm-pauli-sum/src/symmetry.rs:133
- The convenience constructors can panic on zero dimensions (e.g.
chain_1d(0)does(q + 1) % n). Add explicitassert!(...)guards so invalid sizes fail with a clear message.
/// 1D chain of `n` sites with periodic boundary conditions.
/// Single generator: cyclic shift by one site.
pub fn chain_1d(n: usize) -> Self {
let perm: Vec<u32> = (0..n).map(|q| ((q + 1) % n) as u32).collect();
Self::from_generators(n, vec![perm], vec![n as u32])
}
crates/ppvm-pauli-sum/src/symmetry.rs:152
torus_2d(0, ly)/torus_2d(lx, 0)will panic due to modulo-by-zero in the permutation builders. Add explicit dimension assertions so the API fails fast with a clear message.
/// 2D `lx × ly` torus, qubit at `(i, j)` indexed as `j*lx + i`.
/// Two generators: x-shift (i → i+1 mod lx) and y-shift (j → j+1 mod ly).
pub fn torus_2d(lx: usize, ly: usize) -> Self {
let n = lx * ly;
let perm_x: Vec<u32> = (0..n)
.map(|q| {
let (i, j) = (q % lx, q / lx);
(j * lx + (i + 1) % lx) as u32
})
.collect();
let perm_y: Vec<u32> = (0..n)
.map(|q| {
let (i, j) = (q % lx, q / lx);
(((j + 1) % ly) * lx + i) as u32
})
.collect();
Self::from_generators(n, vec![perm_x, perm_y], vec![lx as u32, ly as u32])
}
crates/ppvm-pauli-sum/src/symmetry.rs:187
torus_3dhas the same modulo-by-zero hazard astorus_2dwhen any dimension is zero. Add explicit assertions forlx,ly, andlz> 0.
/// 3D `lx × ly × lz` torus, qubit at `(i, j, k)` indexed as
/// `k*lx*ly + j*lx + i`.
pub fn torus_3d(lx: usize, ly: usize, lz: usize) -> Self {
let n = lx * ly * lz;
let perm_x: Vec<u32> = (0..n)
.map(|q| {
let i = q % lx;
let j = (q / lx) % ly;
let k = q / (lx * ly);
(k * lx * ly + j * lx + (i + 1) % lx) as u32
})
.collect();
let perm_y: Vec<u32> = (0..n)
.map(|q| {
let i = q % lx;
let j = (q / lx) % ly;
let k = q / (lx * ly);
(k * lx * ly + ((j + 1) % ly) * lx + i) as u32
})
.collect();
let perm_z: Vec<u32> = (0..n)
.map(|q| {
let i = q % lx;
let j = (q / lx) % ly;
let k = q / (lx * ly);
(((k + 1) % lz) * lx * ly + j * lx + i) as u32
})
.collect();
Self::from_generators(
n,
vec![perm_x, perm_y, perm_z],
vec![lx as u32, ly as u32, lz as u32],
)
}
crates/ppvm-pauli-sum/src/symmetry.rs:204
ladder(l, n_legs)can panic whenl == 0due to(j + 1) % l, andn_legs == 0produces a degenerate 0-qubit group. Add input validation for clarity and to avoid modulo-by-zero.
/// Multi-leg ladder: `l` sites along the chain × `n_legs` legs.
/// Single generator: cyclic shift along the chain direction (all
/// legs simultaneously). Qubit at `(leg, j)` indexed as
/// `leg * l + j`. No translation along the leg axis (legs are
/// distinguished).
pub fn ladder(l: usize, n_legs: usize) -> Self {
let n = l * n_legs;
let perm: Vec<u32> = (0..n)
.map(|q| {
let leg = q / l;
let j = q % l;
(leg * l + (j + 1) % l) as u32
})
.collect();
Self::from_generators(n, vec![perm], vec![l as u32])
}
| debug_assert_eq!( | ||
| w.n_qubits(), | ||
| self.n_qubits, | ||
| "word and group must agree on n_qubits" | ||
| ); |
| A: PauliStorage, | ||
| S: BuildHasher + Clone + Default + HashFinalize, | ||
| { | ||
| debug_assert_eq!(w.n_qubits(), self.n_qubits); |
| impl<A: PauliStorage, S, const R: bool> std::fmt::Display for SectorCheckError<A, S, R> { | ||
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| write!( | ||
| f, | ||
| "input not in target momentum sector: orbit rep expected c={:?}, but \ | ||
| orbit member (shift {:?}, coeff {:?}) implies c={:?}", | ||
| self.expected, self.shift, self.offending_coeff, self.got_implied, | ||
| ) | ||
| } |
| /// Lex-min canonical representative of `w`'s translation orbit | ||
| /// under this group. Walks the full group via mixed-radix counters, | ||
| /// keeping the smallest word seen. | ||
| /// | ||
| /// Total cost: `O(|G| × n_qubits)` per call. | ||
| pub fn canonicalize<A, S, const R: bool>(&self, w: &PauliWord<A, S, R>) -> PauliWord<A, S, R> |
PR 1 of 4 splitting #178 into reviewable chunks (this → CTPP core → symmetry-merged evolution → ledgers). Full development history: branch
continuous-time-pauli-propagation.Adds
ppvm_pauli_sum::symmetry(pure Rust, no new dependencies):TranslationGroup: finite abelian permutation groups — 1D chains, 2D/3D tori, multi-leg ladders, or arbitrary generator lists — with lex-min orbit canonicalization, shift counters, and momentum-sector charactersχ_k(g).canonicalize_pauli_sum(Vec-pair form used by the upcoming adaptive evolution) andsymmetry_merge_pauli_sum(PauliSumform). Preserves all G-invariant expectation values for G-commuting dynamics (Theorem 1 of Teng, Chang, Rudolph & Holmes, arXiv:2512.12094).canonicalize_pauli_sum_complex(character-weighted projection with 1/|G| normalization) andcheck_momentum_sectorto validate inputs before projecting (silent projection discards physics).Tests: canonicalization/orbit properties on chains, tori, ladders; merge correctness; momentum-eigenstate round trips; a Trotter end-to-end check that per-step merging matches merge-at-end in the dt → 0 limit.
🤖 Generated with Claude Code