Translation symmetry: TranslationGroup + orbit-representative Pauli-sum merging - #180
Translation symmetry: TranslationGroup + orbit-representative Pauli-sum merging#180AlexSchuckert wants to merge 5 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.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@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])
}
This implements the split suggested in #180 (comment) and fixes some issues that surfaced during the split or from copilot findings. Should be merged before #181 cc @AlexSchuckert --------- Co-authored-by: Cursor <cursoragent@cursor.com>
| } => write!( | ||
| f, | ||
| "input not in target momentum sector: orbit rep {rep} expected c={expected:?}, \ | ||
| but orbit member {offending_pauli} (shift {shift:?}) has c={actual:?}" | ||
| ), |
There was a problem hiding this comment.
🟡 Changes recommended
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
crates/ppvm-pauli-sum/src/symmetry/momentum.rs:325
SectorCheckError::CoefficientMismatch's Display message is misleading: it says the orbit representative "expected c=…" butexpectedis the expected coefficient for the offending orbit member (after applying the sector relation). This makes debugging confusing.
Self::CoefficientMismatch {
rep,
offending_pauli,
expected,
actual,
shift,
} => write!(
f,
"input not in target momentum sector: orbit rep {rep} expected c={expected:?}, \
but orbit member {offending_pauli} (shift {shift:?}) has c={actual:?}"
),
- Files reviewed: 7/7 changed files
- Comments generated: 1
- Review effort level: Lite
| /// Returns `Ok(())` on pass; `Err(SectorCheckError)` on fail with the | ||
| /// offending orbit-rep, expected coefficient, and actual coefficient. | ||
| /// | ||
| /// Use this on a user-supplied initial state before feeding it to a | ||
| /// phase-aware merging pipeline — silently projecting a wrongly-typed | ||
| /// input throws away meaningful physics. | ||
| pub fn check_momentum_sector<A, S, const R: bool>( | ||
| basis: &[PauliWord<A, S, R>], | ||
| coeffs: &[Complex<f64>], | ||
| group: &TranslationGroup, | ||
| k_modes: &[i32], | ||
| tol: f64, | ||
| ) -> Result<(), SectorCheckError<A, S, R>> | ||
| where | ||
| A: PauliStorage, | ||
| S: BuildHasher + Clone + Default + HashFinalize, | ||
| { | ||
| assert_eq!(basis.len(), coeffs.len()); | ||
| assert_eq!(k_modes.len(), group.n_generators()); |
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