Skip to content

Translation symmetry: TranslationGroup + orbit-representative Pauli-sum merging#180

Open
AlexSchuckert wants to merge 3 commits into
mainfrom
split/1-translation-symmetry
Open

Translation symmetry: TranslationGroup + orbit-representative Pauli-sum merging#180
AlexSchuckert wants to merge 3 commits into
mainfrom
split/1-translation-symmetry

Conversation

@AlexSchuckert

Copy link
Copy Markdown
Collaborator

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).
  • k = 0 merging: canonicalize_pauli_sum (Vec-pair form used by the upcoming adaptive evolution) and symmetry_merge_pauli_sum (PauliSum form). Preserves all G-invariant expectation values for G-commuting dynamics (Theorem 1 of Teng, Chang, Rudolph & Holmes, arXiv:2512.12094).
  • k ≠ 0: canonicalize_pauli_sum_complex (character-weighted projection with 1/|G| normalization) and check_momentum_sector to 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

…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>
@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://QuEraComputing.github.io/ppvm/pr-preview/pr-180/

Built to branch gh-pages at 2026-07-20 10:14 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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::symmetry with TranslationGroup plus 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.

Comment on lines +125 to +129
Self {
n_qubits,
perms,
orders,
}
Comment on lines +220 to +223
/// Total group order: `Π orders[g]`.
pub fn order(&self) -> usize {
self.orders.iter().map(|&o| o as usize).product()
}
Comment on lines +495 to +503
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;
}
Comment on lines +475 to +477
/// 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>
Copilot AI review requested due to automatic review settings July 16, 2026 12:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 9 comments.

Comment on lines +130 to +133
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])
}
Comment on lines +137 to +138
pub fn torus_2d(lx: usize, ly: usize) -> Self {
let n = lx * ly;
Comment on lines +156 to +157
pub fn torus_3d(lx: usize, ly: usize, lz: usize) -> Self {
let n = lx * ly * lz;
Comment on lines +194 to +195
pub fn ladder(l: usize, n_legs: usize) -> Self {
let n = l * n_legs;
Comment on lines +297 to +300
for &o in &self.orders {
counters.push((rem as u32) % o);
rem /= o as usize;
}
Comment on lines +347 to +350
for &o in &self.orders {
counter.push((rem as u32) % o);
rem /= o as usize;
}
Comment on lines +407 to +409
for (g, &o) in self.orders.iter().enumerate() {
let c = (rem as u32) % o;
rem /= o as usize;
Comment on lines +472 to +474
/// 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).
Comment on lines +101 to +125
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,
}
@david-pl

david-pl commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

@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:

symmetry.rs is approaching 1,000 lines, and I think there is a worthwhile semantic split here beyond simply moving code around. I would suggest preserving the existing ppvm_pauli_sum::symmetry::* API through re-exports, but organizing the implementation as:

symmetry/
├── mod.rs       # module docs and public re-exports
├── group.rs     # TranslationGroup, constructors, orbit/canonicalization
├── merge.rs     # k=0 Vec/PauliSum orbit merging
├── momentum.rs  # characters, complex projection, sector validation/error
└── tests.rs     # current tests

The group versus reduction boundary is already quite clear in the current file. I think momentum also merits its own module because canonicalize_pauli_sum_complex is a normalized sector projection, not merely the complex-coefficient equivalent of the plain merge; colocating it with character, check_momentum_sector, and SectorCheckError would make that semantic distinction easier to maintain.

I would not split the individual lattice constructors into separate files—the 1D/2D/3D/ladder constructors are cohesive parts of TranslationGroup. Also, about a third of the current file is tests, so moving those out alone would make the production code substantially easier to scan.

Copilot AI review requested due to automatic review settings July 20, 2026 10:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_generators accepts orders that can be zero. That leads to division/modulo by zero later (e.g. character divides by orders[g], and mixed-radix decoding uses % o). Add an upfront order > 0 validation 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 with product(), which can silently overflow usize in release builds. That can make order() wrong and can even feed 1.0 / order in 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 explicit assert!(...) 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_3d has the same modulo-by-zero hazard as torus_2d when any dimension is zero. Add explicit assertions for lx, ly, and lz > 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 when l == 0 due to (j + 1) % l, and n_legs == 0 produces 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])
    }

Comment on lines +271 to +275
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);
Comment on lines +601 to +609
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,
)
}
Comment on lines +261 to +266
/// 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants