Skip to content

Continuous-time Pauli propagation: adaptive Pauli-Lindbladian evolution (real-space + momentum-sector)#178

Closed
AlexSchuckert wants to merge 164 commits into
mainfrom
continuous-time-pauli-propagation
Closed

Continuous-time Pauli propagation: adaptive Pauli-Lindbladian evolution (real-space + momentum-sector)#178
AlexSchuckert wants to merge 164 commits into
mainfrom
continuous-time-pauli-propagation

Conversation

@AlexSchuckert

Copy link
Copy Markdown
Collaborator

What this adds

Continuous-time Pauli propagation (CTPP): direct Heisenberg-picture evolution O ← exp(dt·L*) O of Pauli observables under a Lindbladian, on an adaptively truncated Pauli-string basis. The step is exact in dt within the working basis — no Trotter splitting; the only approximation is basis truncation. Supports Hermitian-Pauli jumps (fast diagonal path) and general complex Pauli-sum jumps (σ±, amplitude damping) via a precomputed L†L expansion.

New crates / modules

  • ppvm-lindblad: the core engine. LindbladSpec (precompiled adjoint Lindbladian), pc_step (predictor-corrector step with two-hop leakage enrichment and a cached-CSC matrix-free quspin-expm exponential), and pc_step_orbit_rep (translation-symmetric variant, below). Truncation policy in one PcStepConfig: max_basis rank cap (primary dial), optional admit_basis working-set bound (displacement truncation: final cap selects top-max_basis over the retained+admitted union), drop_tol magnitude prune, optional tau_add leakage-admission rate filter.
  • ppvm-pauli-sum::symmetry: TranslationGroup (1D/2D/3D tori, ladders, arbitrary generators), lex-min orbit canonicalization, plain and momentum-sector (character-weighted) Pauli-sum merging, sector validation. Following Teng, Chang, Rudolph & Holmes (arXiv:2512.12094).
  • Orbit-rep evolution: the state lives permanently in orbit-representative form (one rep per translation orbit, complex coefficients, phase-aware action) — basis ~|G|× smaller than full-basis evolution, persisting through every step. Momentum-resolved observables come out per k-mode.
  • Python API (ppvm.Lindbladian, ppvm._core.TranslationGroup, PauliSum.symmetry_merge / momentum_merge): numpy-array hot path plus string-keyed convenience API; two jupytext demos; test suite with exact-reference kernels (dense Liouvillian, closed bilinear solution, ED cross-checks).
  • mimalloc as the python-native global allocator (~50% peak-RSS cut on the allocation-heavy adaptive paths).

Why (benchmarks)

Benchmarked against per-bond-truncated Trotter Pauli propagation on XX-ladder transport (MSD / momentum correlators), exact-ED referenced at L=5/7, at matched accuracy:

  • L=7 (N=14): CTPP dominates every precision class in wall and RAM (e.g. ~2e-3 median rel: 5.6 s / 283 MB vs Trotter 209 s / 415 MB), and reaches sub-1e-3 where Trotter saturates.
  • L=21 (N=42): 78 s / 0.4 GB / 150k strings vs Trotter 4.1 h / 2 GB / 11.1M strings at matched ~2.8e-3 — 190× wall, 5× RAM.
  • L=41 (N=82): agrees with Begušić & Chan, PRX Quantum 6, 020302 (2025), Fig. 2(c) at 0.5% median in 209 s / 650 MB on a laptop (their reported cost: 43–84 h on 6 cores).

Truncation acts as weak continuous dephasing of coherent recurrences (verified against exact N=10 dynamics): hydrodynamics is preserved, revivals are damped — the regime the method targets.

The full benchmarking ledgers (every attempt, kept or discarded) are under docs/autotune/.

Verification

  • cargo test: ppvm-lindblad 7/7, ppvm-pauli-sum 36/36; python suite 23/23; zero clippy warnings in the new crates.
  • Orbit-rep path validated against full-basis complex evolution + end projection (exact match); complex path validated against the real path at k=0; both validated against exact ED.
  • Merged with current main; all regression gates reproduce bit-identically.

Supersedes #98 (the early lindblad-shim PR): its review requests (config struct, error module, sprs/CsMat feedback) are incorporated here on the final API.

🤖 Generated with Claude Code

AlexSchuckert and others added 30 commits June 2, 2026 12:58
…olution

Adds a LindbladSpec PyClass that exposes three primitives needed for direct
Heisenberg-picture evolution of a generic Pauli Lindbladian (Hermitian
Hamiltonian sum + Hermitian Pauli jumps) on an adaptive Pauli-string basis:

  - action(p): L*(p) for a single Pauli string
  - leakage(basis, coeffs): off-basis component of L*(Sum c_j p_j)
  - generator(basis): sparse generator matrix in COO form

The hot path stores Pauli words as packed [u8; W] X/Z bit arrays and runs
a single fused per-byte multiply that computes both r = h XOR p and the
product phase in one pass. Active-site iteration (per-qubit inverted index
of which H/jump terms touch each qubit) restricts the inner loop to
non-trivially-acting terms; an action cache (DashMap, FxHash) memoises
per-input contribution lists; rayon parallelises the basis loops.

The numpy boundary takes uint8 code arrays (0=I 1=X 2=Z 3=Y) end-to-end,
so the Python driver never builds per-row strings on the hot path. A thin
Python wrapper (ppvm.Lindbladian) exposes both ndarray and string-keyed
APIs for callers who don't need the ndarray fast path.

End-to-end agreement with a pure-Python reference is within 1e-16 in
max |Ck|. At L=51, kmax=1, dt=0.2, 20 steps, the shim runs in 17s vs
481s for the pure-Python adaptive driver (~28x), with the action cache
and ndarray-native wrapper jointly responsible for the speedup over
naive Rust.
- demo/lindblad_adaptive.py: adaptive Pauli-Lindbladian time evolution on a
  leakage-grown Pauli-string basis (all-to-all XY + single-site Z dephasing),
  with an optional predictor-corrector (lifts the dt-error from O(dt^2) to
  O(dt^3)) and a discarded-weight a-posteriori error monitor. Exercises
  ppvm.Lindbladian (action / leakage / generator) end to end.
- test/test_lindblad.py: cross-check action / leakage / generator / the
  protected set against an independent dense Pauli-multiplication-table
  reference, on XY+Z-dephasing and TFIM+X-dephasing Lindbladians.
- Declare numpy as a runtime dependency (the shim wrapper uses it) and import
  SciPy lazily inside generator()/generator_arr(); `import ppvm` and the
  action/leakage primitives no longer require SciPy -- only the sparse-matrix
  convenience does.
…e-Rust PC step

Split the adjoint Pauli-Lindbladian algorithm out of `ppvm-python-native`
into a standalone `ppvm-lindblad` crate backed by `PauliWord<[u64; 2]>`
from `ppvm-runtime`, and extend it with:

- Non-Hermitian Pauli-sum jumps via `JumpInput` / `JumpKind::{HermitianPauli,
  General}`. The general path implements the full GKSL sandwich
  `Σ_{a,b} λ_a* λ_b P_a O P_b - 1/2 {L†L, O}` with the `L†L` Pauli expansion
  precomputed at construction. Same fast diagonal path for the common
  Hermitian-Pauli jump case.

- Pure-Rust matrix-exponential action `expm_multiply` implementing Al-Mohy
  & Higham (2011) Algorithm 3.2: degree-`m` Horner-form Taylor with
  scaling-and-squaring, `(m, s)` from a precomputed `θ_m` table, optional
  early termination. Backed by a minimal CSR matrix type with serial and
  rayon-parallel SpMV (parallel above a configurable nnz threshold).

- `LindbladSpec::pc_step` runs the entire predictor-corrector step in Rust
  (first-hop leakage expansion → predictor expm → second-hop leakage
  expansion → corrector expm), so the Python driver no longer needs scipy
  for the inner loop.

- `pc_step_timed` returns a per-phase microsecond breakdown
  (leakage1/expand1/gencsr1/expm1/...) for profiling parallel scaling.

- `num_threads: Option<usize>` parameter wraps the entire PC step in a
  freshly-built rayon `pool.install`, isolating the requested core count
  for benchmarking.

- Performance: parallel-scatter CSR build (skips `from_triplets`'s
  sequential count/scatter); `rayon::map_init` thread-local scratch
  (hashmap + scratch vectors reused across rayon tasks instead of
  per-task allocations); hash-only `in_basis`/`protected_set` membership
  tables (`FxHashMap<u64, ()>` instead of `<Word, ()>` shrinks the
  working set ~6× and keeps it in L2/L3 past basis 10⁶).

At L=51, long-range XY α=1, γ=1, T=1 (basis grows to 577K), 20 PC steps
runs end-to-end in ~512 s wall on 4 threads — down from ~734 s
pre-optimisation (1.43× faster).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e tests, scaling demo

Wrapper updates and tests for the new `ppvm-lindblad` crate.

API additions on `Lindbladian`:

- `pc_step` / `pc_step_arr`: thin Python wrappers around the pure-Rust
  predictor-corrector step. Expose `parallel_threshold`, `num_threads`,
  and `expm_tol` knobs for benchmarking parallel scaling without
  re-importing scipy.

- Accept jumps as either a Pauli string (Hermitian-Pauli fast path) or a
  complex Pauli linear combination `Iterable[(str, complex)]` for
  non-Hermitian dissipators.

- `sigma_plus(site, n_qubits)` and `sigma_minus(site, n_qubits)` helpers
  produce the `(X ± iY)/2` Pauli sums for amplitude damping / thermal
  excitation. Re-exported from the top-level `ppvm` package.

Tests:

- Amplitude damping (`test_amplitude_damping_action`,
  `test_amplitude_damping_generator_and_leakage`) and thermal σ± mix
  (`test_thermal_excitation_damping_action`) cross-checked against a
  dense Liouvillian super-operator reference.

- Adaptive shim convergence vs the closed Jordan-Wigner bilinear ODE for
  NN-XY + Z-dephasing on OBC
  (`test_adaptive_converges_to_nn_xy_z_dephasing_bilinear`).

- Predictor-corrector raises the dt-scaling from quadratic to cubic
  (`test_predictor_corrector_lifts_dt_scaling_to_cubic`).

- Pure-Rust pc_step matches scipy-based PC at FP precision
  (`test_pc_step_rust_matches_scipy_pc`) and exhibits the same cubic
  dt-scaling against the bilinear reference
  (`test_pc_step_rust_dt_scaling_is_cubic`).

- Length-1 real lincomb routes to the Hermitian fast path
  (`test_lincomb_single_term_matches_hermitian_fast_path`).

Demo:

- `demo/lindblad_pc_scaling.py`: end-to-end wall-time scaling sweep with
  `--max-cores`, `--model {nn,long-range}`, `--alpha`, `--gamma`, `--tau`
  CLI for HPC nodes. Reports first-step + steady-state ms per thread
  count with the entire PC step pinned to a per-call rayon pool.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Correctness
- generator()/pc_step()/pc_step_timed(): reject duplicate basis Pauli words
  at the PyO3 boundary with a clear ValueError. The underlying row-index
  map silently overwrote earlier rows on collision, producing a wrong
  sparse generator. ppvm-lindblad now factors the index-build with
  uniqueness debug_assert into `build_basis_index(...)`, called from both
  `generator` and `generator_csr`. New regression test
  `test_generator_rejects_duplicate_basis`.
- `_string_to_codes`: raise `ValueError` naming the bad character; strip
  `_` separators to match the Rust `parse_pauli_string` parser.

API surface
- Rename the four codec helpers (string_to_codes, codes_to_string,
  basis_to_codes, codes_to_basis) to `_`-prefixed module-private names.

Dependencies — drop scipy from runtime + tests
- Lindbladian.generator{,_arr} now return COO triples (rows, cols, vals)
  instead of a scipy.sparse.csc_matrix. Users wanting scipy can wrap;
  see docstring.
- Tests replace `scipy.sparse.linalg.expm_multiply` with a numpy
  eigendecomposition reference (`expm_mv_dense`).
- pyproject.toml: scipy removed from `dev`; moved to a new `demo` group
  (demos still use it for the reference matrix exponential).

Test layout
- Split the 723-line `test/test_lindblad.py` into `test/lindblad/`:
  * `_helpers.py` — shared Pauli-table reference, dense Liouvillian
    reference, bilinear NN-XY+Z-dephasing solver, adaptive PC drivers
  * `test_action_generator.py` — Hermitian-Pauli action/leakage/generator
  * `test_non_hermitian_jumps.py` — non-Hermitian (σ-/σ+/thermal) jumps
  * `test_adaptive_pc.py` — adaptive PC convergence vs bilinear ref
  * `test_pc_step_rust.py` — pure-Rust pc_step parity + cubic dt-scaling
  Each test file now ≤ 121 lines.

Demos
- Both `demo/lindblad_adaptive.py` and `demo/lindblad_pc_scaling.py`
  rewritten as jupytext percent-format notebooks with markdown narrative
  cells and end-of-notebook plots, replacing CLI arg parsing with a
  top-of-file parameter cell.
- `lindblad_adaptive.py`: split the monolithic per-step body into
  `init_mode`, `add_leakage_to_basis`, `prune_and_cap`, `run_mode`;
  dropped the unnecessary `target_arr = basis_arr.copy()` /
  `protected_arr = basis_arr.copy()` calls (nothing mutates basis_arr
  in place — `vstack`/slice rebind).
- `lindblad_pc_scaling.py`: dropped the `importlib.util.spec_from_file_location`
  bypass that was working around `demo/stim.py` shadowing the real `stim`
  package on `python demo/foo.py` invocations.
- Rename `demo/stim.py` → `demo/stim_msd.py` (it's an MSD-circuit demo
  that happened to share a name with the `stim` package).

Benches
- Convert `examples/spmv_scaling.rs` (std::time + manual print) to a
  proper criterion `benches/spmv_scaling.rs` with one entry per thread
  count `{1, 2, 3, 4, 6, 8, 10}`, each running SpMV inside a
  `rayon::ThreadPool::install`. Added `criterion` to dev-deps +
  `[[bench]]` to `ppvm-lindblad/Cargo.toml`.

CSR + expm_multiply replacement (left as-is)
- Surveyed `sprs`, `nalgebra-sparse`, `russell_sparse`, `faer-rs`: all
  ship CSR + serial SpMV but none offer rayon-parallel SpMV with the
  nnz-threshold gating used here. No Rust crate provides Al-Mohy & Higham
  `expm_multiply` (matrix-free action). Decision documented in PR review
  replies; happy to revisit if a suitable crate appears.

Verification
- cargo test -p ppvm-lindblad --lib: 10/10
- pytest test/: 115/115

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…2, usize>

Roger asked on PR #98 whether the hand-rolled CSR could be replaced with
an existing Rust crate. It can: this commit drops the custom CsrMatrix
struct + impl block (~90 LOC) and uses `sprs::CsMatI<f64, u32, usize>`
for storage, keeping our free-function helpers for the small bits sprs
doesn't provide (parallel SpMV with nnz-threshold gating, 1-norm).

Type choice: `CsMatI<f64, u32, usize>` keeps `u32` column indices to
match the previous memory layout (4 bytes per nonzero index). The default
`sprs::CsMat<f64>` uses `usize` indices (8 bytes), which causes a 17–32%
regression at bandwidth-bound matrix sizes (8M+ nonzeros) from the
doubled index traffic per SpMV. The wider `usize` indptr stays — it's
small (n+1 entries) and accommodates nnz > 2^32.

Bench (`benches/spmv_scaling.rs`) on M4, 4 sizes × 4 thread counts —
all deltas within ±5% of the previous custom code; 4-thread speedups of
~2.7× preserved across L2-resident, L3-boundary, and DRAM-bandwidth
regimes.

  100 k × 4 threads:  87.6 µs → 87.0 µs   (-1%)
  500 k × 4 threads:   415 µs →  418 µs   (+1%)
    8 M × 4 threads:  7.09 ms → 6.73 ms   (-5%)

The expm_multiply algorithm itself stays ours — no Rust crate implements
Al-Mohy & Higham matrix-free `expm_multiply`. That part of Roger's
question was already addressed in the PR-review reply.

Net: ~25 LOC removed, one new dep (`sprs` with default-features = false +
"multi_thread"), storage type now comes from the standard ecosystem.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`pc_step` now takes an optional `drop_tol` parameter. At the end of the
predictor-corrector step (after the corrector expm_multiply), any basis
entry whose absolute coefficient is below `drop_tol` is removed from the
basis, unless the corresponding Pauli word appears in `protected`.

Implemented as a small in-place compaction (FxHashSet lookup on
`protected`, single pass swap-and-truncate over basis+coeffs). No-op
when `drop_tol ≤ 0`, preserving the existing add-only growth behaviour.

Motivation: at γ = 0 (pure unitary Hamiltonian dynamics) there is no
physical damping of small-coefficient strings, so the basis grows
monotonically with only `tau_add` as a brake. `drop_tol > 0` lets the
caller actively prune entries whose coefficient has decayed, keeping
the basis bounded and making γ-extrapolation experiments feasible.

Plumbed through `ppvm_lindblad::Lindbladian::pc_step{,_timed}`, the
PyO3 binding (new optional kwarg, defaults to 0.0), and the Python
shim `ppvm.lindblad.Lindbladian.pc_step{,_arr}`.

Verified:
- All 10 Rust lib tests still pass.
- All 122 Python tests still pass.
- Smoke test: at L=6, NN XY, γ=0, seed = Z_2:
    no drop_tol            → basis saturates at 36 strings
    drop_tol = 0.05        → basis stays at 9
    drop_tol = 0.5         → basis stays at 1 (only protected Z_2)
    drop_tol = 1.5         → basis stays at 1 (protected never dropped)
  Protected words are never removed regardless of magnitude.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Empirical benchmarks (xy-experiments/bench_cache_*.py, see PR thread)
showed that the per-Pauli `L*(p)` memoisation cache:

- HURTS wall time for sparse-local Hamiltonians (e.g. XX ladder): hash-
  map lookup latency at million-entry scale matches the cost of just
  recomputing the few-bond commutator. cache_ON 195s vs cache_OFF 149s
  at L=20 ladder, |basis|=3.7M (-24%).

- Helps modestly for dense long-range Hamiltonians (LR-XY, α=1): ~25%
  speedup, BUT at ~5 KB per cached entry. At basis sizes that matter
  (10⁶+), this consumes tens of GB and pushes runs into OOM well before
  the basis itself does.

In both regimes — and especially for the L=41 sweeps that just OOM'd
on Perlmutter at γ=0 / tight add_tol — the cache is a net loss. Pauli
propagation is intrinsically memory-limited (basis + transient action
lists), so any per-Pauli auxiliary storage just shrinks the reachable
problem size.

Strips:
- `action_cache: DashMap` field + `cache_enabled: AtomicBool` flag
- `clear_cache()`, `cache_size()`, `set_cache_enabled()` on LindbladSpec
- corresponding PyO3 bindings and Python wrapper methods
- `dashmap` Cargo dep (no other code in the crate uses it)
- `L_op.clear_cache()` calls in demos and tests (now no-ops anyway)

All 10 Rust lib tests + 122 Python tests still pass. End-to-end
behaviour unchanged; just lower memory + (often) faster.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… RSS cut

Two memory optimisations on top of the adaptive-Pauli evolution paths:

1. **Switch ppvm-python-native to the mimalloc global allocator.**
   The system allocator (macOS / glibc malloc) holds onto freed pages in
   arena freelists, so the OS-reported RSS stays high even after Rust
   has freed allocations. mimalloc returns pages to the kernel much more
   aggressively. Drop-in, ~5 lines.

2. **Chunk `leakage_inner`'s parallel pass into batches of 4096 basis
   rows.** Previously `basis.par_iter().map_init(...).collect()`
   materialised the full `Vec<Vec<(Word, f64)>>` (~N × A × 24 B) in
   memory simultaneously before merging — ~430 MB at L=51 LR-XY, N=14 K,
   A≈1275. Now each chunk's `Vec<Vec>` is merged into the global
   `FxHashMap` and dropped before the next chunk starts; peak in-flight
   is bounded to `CHUNK_SIZE · A · 24 B`. Chunk of 4096 still gives
   ~400 items per rayon worker so parallelism stays healthy.

Benchmark on macOS at two configs (separate process per measurement so
ru_maxrss is clean):

```
                    config          wall(s)  peak RSS (MB)
                                    before  after  before  after
  LR-XY L=51 γ=1   add_tol=2e-4     18.51   16.51    3160    1759   (-44%)
  LR-XY L=12 γ=0.1 add_tol=2e-4     13.10   11.56    4583    1797   (-61%)
```

Memory ~halved; wall ~10-15 % faster (mimalloc is itself faster than
the system allocator). All 10 Rust lib tests + 122 Python tests pass.

This lets the L=41 / γ=0 / tight-tol sweeps that were OOM-ing at 80 GB
on Perlmutter shared nodes likely fit, and roughly doubles the largest
basis we can reach on a fixed memory budget.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tie the leakage-rate add threshold to the post-step drop threshold via
  tau_add = K * drop_tol / dt
with K=5 as the default — Pareto-optimal in the L=51 LR-XY γ=1 study
(K=5, drop=4e-6 matches the K=1, drop=2e-5 accuracy floor at 3× lower
RSS+wall, and dominates K=10 at high accuracy). Callers can still pass
tau_add explicitly; the new policy only kicks in when tau_add is omitted
and drop_tol > 0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds three new code paths plus three memory optimizations on the
existing pc_step:

  - matrix_free flag on pc_step: skip CSR build, do each Krylov-Taylor
    SpMV by recomputing L*(p) on the fly. New spmv_matrix_free,
    one_norm_matrix_free, expm_multiply_mf. Trades wall (4× at L=6) for
    memory (modest at L=6, expected to dominate at L≥10).
  - rk4_step: classical 4th-order Runge-Kutta on the adjoint
    Lindbladian, matrix-free, with leakage-driven natural basis growth
    and post-step drop_tol prune. New compute_action_sum helper. Doc
    flags the dt < 2.78/||L*|| stability boundary with explicit warning
    about silent norm-conserving / observable-diverging failure mode.
  - leakage_with_prune: streaming Cauchy-Schwarz remaining-budget
    prune. Exact (drops only candidates whose true sum cannot exceed
    tau_add). Uses new max_action_coef per-operator bound.
  - pc_step_inner: collapse coeffs_pre + coeffs_pre_padded clones into
    a single in-place coeffs buffer, saving 2 × n × 8B per step.
  - ExpmOpts.max_krylov_m: cap on Krylov-Taylor degree m_star. Trades
    more outer scaling-and-squaring iterations for less Krylov scratch.

All four exposed through Python pc_step_arr and rk4_step_arr.

Correctness: CSR (new opts) and matrix-free pc_step trajectories are
bit-identical (4e-16 max delta over 50 steps at drop=1e-6, L=6).
Adds a general-purpose lattice translation-symmetry framework for
operator-space Pauli evolution, following Teng/Chang/Rudolph/Holmes
arXiv:2512.12094 (plain k=0 merging — phase-aware k!=0 extension to
come later).

ppvm-runtime/symmetry.rs:
  - TranslationGroup: finite abelian group of qubit permutations,
    specified by generator perms + cyclic orders.
  - Constructors: chain_1d, torus_2d, torus_3d, ladder, from_generators.
  - canonicalize(w): lex-min orbit representative; O(|G|*n_qubits).
  - orbit(w): iterator over the |G| group-acted versions of w.
  - canonicalize_pauli_sum(basis, coeffs, group): drop-in merge for
    the (Vec<Word>, Vec<f64>) representation used by ppvm-lindblad's
    pc_step / rk4_step.
  - symmetry_merge_pauli_sum(psum, group): same idea for the Trotter
    PauliSum representation, via map_add.

Tests (8 in ppvm-runtime/lib + 1 in ppvm-lindblad/lib, all passing):
  - cyclic-shift canonicalization, lex-min property, orbit-size
    correctness, 2D torus, ladder.
  - canonicalize_pauli_sum merges/keeps-distinct correctly.
  - pc_step_matches_symmetry_merged_on_small_chain: n=4 XY chain PBC,
    no truncation, 10 pc_step iterations, with-merging matches
    without-merging-then-canonicalize to 1e-9.
  - pauli_sum_symmetry_merge_matches_plain_trotter: same setup, Trotter
    at dt=1e-5 (small so first-order Trotter's translation-non-
    equivariant commutator error stays below FP noise), 1e-7.

ppvm-python-native/symmetry.rs:
  - TranslationGroup PyO3 class with the same constructors.
  - canonicalize_basis_arr free function on numpy arrays
    (matches LindbladSpec.pc_step_arr / rk4_step_arr signature).

PauliSum symmetry_merge for Trotter Python bindings deferred — would
require modifying the create_interface! macro to inject the method on
all 32 generated PauliSum classes; out of scope for this commit.

Bit-rotation fast paths for 1D/2D/3D torus also deferred; current
canonicalize uses the generic per-qubit permutation walk. Sufficient
for L<=20; revisit if profiling shows it as a bottleneck.
Complex-coefficient pipeline supporting evolution of a single
momentum sector with optional orbit-rep projection at snapshot points.
Following Teng/Chang/Rudolph/Holmes arXiv:2512.12094 §III for the
phase-aware extension.

ppvm-runtime/symmetry.rs:
  - canonicalize_with_shift(w): lex-min rep + mixed-radix counter of
    the group element g such that g.r = w. Used for phase factor in
    momentum-aware merge.
  - character(k_modes, counter): exp(2πi · Σ k·c / orders).
  - canonicalize_pauli_sum_complex(basis, complex_coeffs, group, k):
    project to orbit-rep form at momentum sector k. For input already
    in sector k, output is the orbit-rep coefficients unchanged
    (modulo 1/|G| convention).
  - check_momentum_sector(basis, complex_coeffs, group, k, tol):
    validates input is a k-eigenstate; returns SectorCheckError with
    diagnostic info on fail.

ppvm-lindblad:
  - expm.rs: spmv_complex + expm_multiply_complex (real CSR matrix,
    complex vector — two real SpMVs internally).
  - LindbladSpec::compute_action_sum_complex, leakage_complex,
    pc_step_complex (full-basis complex pc_step with optional
    pre-step sector check).
  - prune_basis_complex: drop on |c| norm.

Tests (10 in ppvm-runtime/lib + 3 in ppvm-lindblad, all passing):
  - canonicalize_with_shift round-trip
  - character properties (trivial, unit-modulus)
  - momentum-zero complex merge matches real merge
  - momentum-eigenstate sector check passes / fails appropriately
  - pc_step_complex matches real pc_step at k=0
  - pc_step_complex sector preserved through evolution
  - pc_step_complex rejects wrong-sector input

Python bindings (ppvm-python-native + ppvm-python):
  - TranslationGroup unchanged (constructors / canonicalize already
    work for both paths).
  - canonicalize_basis_arr_complex, check_momentum_sector_arr free
    functions on numpy uint8/complex128 arrays.
  - Lindbladian.pc_step_complex method (delegates to _spec.pc_step_complex).

End-to-end Python sanity test (test_phase_aware_pyend.py): n=4 XY
chain k=1 eigenstate evolves preserving Hilbert-Schmidt norm and
sector membership; projection collapses 12 entries → 3 orbit reps.

DESIGN LIMITATION: per-step orbit-rep evolution is NOT supported by
this commit. Merging is "at snapshot points" only — the user
evolves in full-basis representation, projects when reading
observables. Per-step memory reduction would require phase-aware
action with complex CSR matrix elements (matrix elements become
complex in orbit-rep form due to phase factors), which is the next
follow-up.
State lives entirely in orbit-representative form throughout pc_step.
Phase-aware action: each output Pauli q of L*(r) canonicalizes to
its orbit rep r_q with shift counter cnt_q, contributing
χ_k(g_{cnt_q}) * v to the orbit-rep matrix element [M]_{r_q, r}.
The CSR becomes complex (matrix elements carry the χ_k(g) phase).

Adds:
  - expm.rs: CsrCx (complex CSR), csr_cx_one_norm, spmv_cx_{serial,parallel},
    spmv_cx, expm_multiply_cx (Al-Mohy & Higham on complex matrix +
    complex vector).
  - new orbit_rep.rs module:
    - canonicalize_basis_to_rep: rewrite each basis word to its
      canonical orbit rep (enforces the orbit-rep invariant).
    - generator_csr_orbit_rep: build complex CSR with phase-aware
      matrix elements; deduplicates and sorts per row.
    - leakage_orbit_rep: phase-aware leakage producing orbit reps
      with momentum-character weighted complex contributions.
    - pc_step_orbit_rep: full predictor-corrector with phase-aware
      action + complex CSR throughout. drop_tol applied on |c|.

Python bindings:
  - LindbladSpec.pc_step_orbit_rep (with `canonicalize_first` kwarg).
  - Forwarded through Lindbladian.pc_step_orbit_rep in ppvm-python.

Tests:
  - pc_step_orbit_rep_matches_full_basis_projection: n=4 XY chain
    k=1, 3 steps. Orbit-rep matches full-basis-projected to 1e-9.

End-to-end Python demo (test_orbit_rep_pyend.py): n=8 1D XY chain
k=2 eigenstate, 5 pc steps.
  - Full-basis: peak basis 72 entries.
  - Orbit-rep: peak basis 9 entries (8× reduction = n).
  - Final state agreement: max |Δc| = 2.22e-16 (FP noise).

This delivers the full memory benefit the symmetry-merging paper
promises: per-step basis size reduced by |G|×, throughout every
step, with the trajectory bit-identical to the reference projection.
Add `PauliSum.momentum_merge(other, group, momentum)`: a phase-aware
orbit-rep merge for a complex operator carried as a real pair
(`self` = real part, `other` = imaginary part of O = self + i·other).
Both sums are folded onto orbit representatives in momentum sector
`momentum` and overwritten in place, generalizing `symmetry_merge`
(k=0 only) to non-trivial sectors while keeping real coefficients on
the Python side.

Implementation reuses the tested `canonicalize_pauli_sum_complex` for
the character-weighted fold, then rescales by |G| so the merge is a
*summing* projector (idempotent on already-merged input, like
`symmetry_merge`) — stable when applied after every Trotter step
(the 1/|G| prefactor would otherwise shrink coefficients each call).

Enables L×-compressed, k-resolved gate-based Trotter evolution with
flat memory (no CSR / leakage buffers). Verified against the exact
adaptive `pc_step_orbit_rep` path: k=0 exact, k!=0 converges as O(dt²).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…iagonalization

- Update the symmetry module doc: the k!=0 extension is no longer "a
  follow-up" — point to canonicalize_pauli_sum_complex and the real-pair
  PauliSum.momentum_merge path.
- Add test_momentum_merge.py validating against EXACT references (not any
  other propagation scheme):
    * momentum_merge is an idempotent sector projector and annihilates
      operators from other momentum sectors;
    * k-resolved symmetry-compressed Trotter reproduces the operator
      autocorrelator C_k(t) from exact dense diagonalization (k=0 exact;
      k!=0 converges to exact as dt->0);
    * compressed (merge-each-step) matches the unmerged evolution up to
      the O(dt^2) equivariance error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…merging

Brings the bytemuck-based weight() speedup + early-return for uncapped
MaxPauliWeight (and the rest of #126) into the symmetry-merging branch.

Conflicts resolved: keep stim_demo.py (byte-identical to the stim_msd.py
rename); regenerate uv.lock against the merged pyproject.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the hand-rolled Al-Mohy & Higham scaling-and-squaring engine on all
three evolution paths with the external `quspin-expm` crate, via borrowed
`LinearOperator`s fed to `ExpmOp::from_parts` (which supplies the shift mu,
partition count s, and Taylor order m* directly, so only `dot` runs in the
Taylor loop -- no estimator, no dot_transpose):

  * expm_step (real): matrix-free `MfOp` over the in-basis action
    (`spmv_matrix_free`); mu and the exact column 1-norm of M - mu*I are
    gathered in one matrix-free pass via `action()`. No CSR materialised.
  * expm_step_complex (real matrix, complex vector): two matrix-free real
    applies on the re/im parts, recombined. No CSR.
  * orbit-rep (genuinely complex generator): the momentum-character phases
    have no matrix-free action, so the complex `CsrCx` is still built by
    `generator_csr_orbit_rep`, then wrapped in a CSR-backed `CsrCxOp` and
    exponentiated through quspin-expm (zero shift, `csr_cx_one_norm`).

(m*, s) reuse the existing `select_ms` table. The old `expm` engine
(expm_multiply / _mf / _complex / _cx, spmv*, Csr/CsrCx) is retained but
off the evolution paths -- used now only by the parity regression tests.

Validated: parity vs the retired engine to <1e-15 (real 2.2e-16; complex
orbit-rep 1.1e-16 over n=1,3,7,12); cargo test --workspace --exclude
ppvm-python-native (all green); Python lindblad + momentum-merge + preserve
suites (30 passed, incl. pc_step_orbit_rep vs full-basis).

NOTE: QuSpin-rust currently ships no LICENSE (all-rights-reserved). This pin
is a placeholder pending a permissive license being added upstream; the
quspin-expm authors are co-authors of the ppvm paper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…path

Now that evolution runs entirely on quspin-expm (matrix-free for the real
and complex-vector paths, CSR-backed only for the complex orbit-rep
generator), delete the dead hand-rolled engine and every remnant of the
CSR-for-real path:

  * expm.rs: drop expm_multiply / _mf / _complex / _cx, the real Csr type,
    csr_from_triplets, csr_one_norm, spmv / spmv_parallel / spmv_complex,
    and the ExpmOpts options struct. Kept: select_ms/THETA (used by the new
    engine), and the complex CsrCx / csr_cx_one_norm / spmv_cx (orbit-rep).
  * lib.rs: remove LindbladSpec::generator_csr, one_norm_matrix_free, the
    now-dead SendPtr scatter helper, and the retired parity test. Drop the
    `opts: ExpmOpts` and `matrix_free: bool` parameters from pc_step,
    pc_step_timed, pc_step_complex (+ inner) and expm_step — the real path
    is unconditionally matrix-free now.
  * orbit_rep.rs: drop the unused `_opts` parameter.
  * benches/spmv_scaling.rs: removed (benched the deleted real Csr/SpMV);
    drop its [[bench]] entry and the now-unused criterion dev-dep.

Python bindings + wrapper: drop the expm_tol / parallel_threshold /
matrix_free / max_krylov_m kwargs (they only configured the removed engine)
from pc_step / pc_step_timed / pc_step_complex / pc_step_orbit_rep; num_threads
is retained. Updated the lindblad_pc_scaling demo accordingly.

Net -1042 / +35 lines. Validated: cargo test --workspace --exclude
ppvm-python-native (all green, warning-clean) and the full Python suite
(146 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The orbit-rep evolution path (pc_step_orbit_rep) previously materialized a
full complex CSR of the in-basis generator twice per step
(generator_csr_orbit_rep) to feed the Krylov expm. Profiling the symmetry-
merged ladder (main_k_pec_ladder) showed this CSR dominated peak memory:
the per-column phase-aware action `cols`, a transposed row-major copy, and
the held CSR together ran ~3x the rest of the working set.

Replace it with a cache-the-action hybrid, mirroring the matrix-free
quspin-expm design used on the real path:

  * build_orbit_rep_cols: the per-column phase-aware action
    (compute_action_terms + canonicalize_with_shift + character) is computed
    ONCE per expm call into Vec<Vec<(row, chi*v)>> (factored out and shared
    with generator_csr_orbit_rep so the CSR reference stays bit-identical).
  * OrbitRepCscOp: a quspin LinearOperator whose dot() is a CSC matvec
    directly against the cached cols (no per-matvec action recompute), driven
    by ExpmOp::from_parts with mu = tr/n and the same (m*, s) norm bound as
    expm_apply_mf. No CSR is materialized.

vs the old CSR path (L=6 ladder, drop_tol=1e-6): peak RSS 953 -> 499 MB
(1.9x less, below the Trotter reference) AND slightly faster (41 -> 37 s),
since the transpose/sort/dedup/row-major build is gone while the action is
still amortized across the Krylov matvecs. (A pure recompute-every-matvec
matrix-free variant hit 341 MB but ~8x slower; the cache-action hybrid is
the sweet spot.)

generator_csr_orbit_rep / expm_apply_csr_cx are retained (dead-code-allowed)
as the parity reference: parity_orbit_rep_cached_vs_csr matches them
bit-exactly (max|d|=0) at k=0 and k=1.

Validated: cargo test --workspace --exclude ppvm-python-native (all green,
warning-clean) and Python momentum-merge + lindblad suites (23 passed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The orbit-rep path is fully matrix-free now (expm_apply_orbit_rep_cached,
cache-action). The complex-CSR code was retained only as a parity reference;
remove it:

  * orbit_rep.rs: generator_csr_orbit_rep + the parity_orbit_rep_cached_vs_csr
    test (the cache path is exercised end-to-end by
    pc_step_orbit_rep_matches_full_basis_projection, which stays).
  * mf_expm.rs: expm_apply_csr_cx + CsrCxOp.
  * expm.rs: the now-unused complex-CSR primitives CsrCx, csr_cx_one_norm,
    spmv_cx{,_serial,_parallel}. expm.rs is now just select_ms + THETA.

Also folded the file-local orbit_rep::build_basis_index duplicate onto the
crate-level build_basis_index (compiler-flagged dead after the removal).

Net -370 lines. Validated: cargo test --workspace --exclude
ppvm-python-native (all green, warning-clean) and the Python momentum-merge
+ lindblad suites (23 passed). No CSR is materialized on any evolution path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AlexSchuckert and others added 10 commits July 8, 2026 16:05
…e MSD overshoot, not statistical noise

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…2 (larger B DOES fix it at K=3, just slowly)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…basis)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n events at smaller dt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rn on the current API)

Config struct bundles max_basis/admit_basis/drop_tol/tau_add/num_threads
with measured-role docs; Error extracted to error.rs; step signatures
reduced to (basis, coeffs, dt, protected, &cfg); python kwargs unchanged
(binding builds the config). Removes every too_many_arguments allow.
Also cleans doc-comment debris orphaned by the rk4/pc_step_complex
removals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ative type stubs) into autotune/ladder-tuning

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

# Conflicts:
#	Cargo.lock
#	crates/ppvm-python-native/Cargo.toml
… the MSD scan driver

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…istry refs, matching upstream lockfile hygiene)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comments: strip campaign/ledger/PR references and stale descriptions
(rk4/pc_step_complex/PPVM_K_LEAKAGE remnants, 'complex CSR' wording for
the cached-CSC engine, ppvm_python_native module paths, 'shim' naming).

Dead code removed from ppvm-lindblad: max_action_coef, spmv_matrix_free,
compute_action_sum{,_complex}, prune_basis_complex (lib copy), the unused
max_m parameter of select_ms{,_loose}, the never-written gencsr timing
fields, and the orphaned PPVM_K_LEAKAGE doc block that attached to
MAX_QUBITS.

Simplifications: comm_product now wraps pauli_mul (same bit kernel);
CachedCscOp/OrbitRepCscOp unified into one generic CscOp<T>; the binding
crates share one decode_basis; pc_step_timed now honors admit_basis and
tau_add like pc_step (kwargs added); expm_apply_mf_cxvec gated to tests;
expm module made pub(crate).

Verified: crate tests 7/7 + 36 pauli-sum, python suite 23/23, L=5
momentum gate bit-identical, tau_add cell 5.19e-3/77,820 and displacement
cell 1.99e-3/1.20e-2 exact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 16, 2026 10:08

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 continuous-time Pauli propagation (CTPP) capability by introducing a Lindbladian-based adaptive predictor–corrector evolution engine, plus translation-symmetry compression (including momentum sectors) and Python bindings/tests/demos to exercise and validate the new functionality.

Changes:

  • Introduces ppvm-lindblad (Rust) with matrix-exponential–based PC stepping (real-space and orbit-rep) and supporting config/error modules.
  • Adds translation-group symmetry merging (k=0) and momentum-sector merging (k≠0) for Pauli sums, exposed through Rust→Python bindings and validated with new tests.
  • Updates Python package exports/deps and adds demos + benchmarking/autotune scripts under docs/autotune/.

Reviewed changes

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

Show a summary per file
File Description
Cargo.toml Adds crates/ppvm-lindblad to the workspace.
crates/ppvm-lindblad/Cargo.toml New crate manifest (includes quspin-* git deps for expm).
crates/ppvm-lindblad/src/config.rs Adds PcStepConfig for truncation/execution policy.
crates/ppvm-lindblad/src/error.rs Defines error type for Lindblad spec construction/stepping.
crates/ppvm-lindblad/src/expm.rs Implements Al‑Mohy & Higham (m,s) selection tables (incl. relaxed tolerance).
crates/ppvm-lindblad/src/mf_expm.rs Implements cached CSC-style matrix-free exp(dt·M)·v and a LinearOperator.
crates/ppvm-pauli-sum/src/lib.rs Exposes new symmetry module.
crates/ppvm-python-native/Cargo.toml Adds deps for Lindblad/symmetry bindings (incl. mimalloc, numpy, new crate deps).
crates/ppvm-python-native/src/interface.rs Adds PyO3-exposed symmetry_merge / momentum_merge methods for PauliSum interfaces.
crates/ppvm-python-native/src/lib.rs Installs mimalloc as global allocator; exports Lindblad and symmetry PyO3 symbols.
crates/ppvm-python-native/src/lindblad.rs Adds PyO3 wrapper LindbladSpec with action/leakage/generator/pc_step/pc_step_orbit_rep.
crates/ppvm-python-native/src/symmetry.rs Adds PyO3 TranslationGroup and canonicalize_basis_arr(_complex) + sector check helpers.
docs/autotune/2026-07-01-expm-memory/log.md Benchmark/memory investigation notes for expm path.
docs/autotune/2026-07-01-expm-memory/scan3d.py Parameter scan driver for expm (dt/drop/max_basis).
docs/autotune/2026-07-01-expm-memory/tight.py Tight-accuracy expm scan script.
docs/autotune/2026-07-01-expm-memory/tight2.py Additional tight-accuracy expm scan script.
docs/autotune/2026-07-01-expm-memory/trot_tight.py Tight-accuracy Trotter scan script for comparison.
docs/autotune/2026-07-01-expm-pc-step/cache-action/prompts.md Notes/prompts for cache-action approach.
docs/autotune/2026-07-01-expm-pc-step/log.md Benchmark log for expm PC-step optimization.
docs/autotune/2026-07-01-expm-pc-step/metric.toml Recorded metrics for expm PC-step optimization.
docs/autotune/2026-07-02-expm-ladder/log.md Benchmark log for expm ladder experiments.
docs/autotune/2026-07-02-expm-ladder/metric.toml Recorded metrics for expm ladder experiments.
docs/autotune/2026-07-02-expm-ladder/scan_admission.py Admission-control scan script (expm).
docs/autotune/2026-07-02-trotter-ladder/log.md Benchmark log for Trotter ladder experiments.
docs/autotune/2026-07-02-trotter-ladder/metric.toml Recorded metrics for Trotter ladder experiments.
docs/autotune/2026-07-03-orbit-compare/exact_msd_L7.py Exact-reference generator for MSD (sector-reduced ED).
docs/autotune/2026-07-03-orbit-compare/exact_ref_L7.py Exact-reference generator for momentum-k correlator (blocked ED).
docs/autotune/2026-07-03-orbit-compare/growth_probe.py Basis-growth probe script for orbit-rep expm at scale.
docs/autotune/2026-07-03-orbit-compare/orbit_bench.py Orbit-preserving benchmark driver comparing expm vs Trotter.
docs/autotune/2026-07-03-orbit-compare/orbit_bench_L7.py L=7 benchmark driver with notes about legacy env vars.
docs/autotune/2026-07-03-orbit-compare/orbit_verify.py Verification script comparing real-space vs orbit-rep vs exact ED.
docs/autotune/2026-07-03-orbit-compare/reference_digitized_L41.py Digitized reference data used in benchmarking comparisons.
docs/autotune/2026-07-03-orbit-compare/revival_test_N10.py Exact revival-carrier hypothesis test script.
docs/autotune/2026-07-03-orbit-compare/scan_realspace_msd.py Real-space MSD scan driver for method comparison.
docs/autotune/2026-07-03-orbit-compare/scan_xy_mid.py Mid-precision scan driver for ladder momentum correlators.
docs/autotune/2026-07-03-orbit-compare/trotter_orbit_verify.py Orbit-merged Trotter verification script against exact ED.
ppvm-python/demo/lindblad_adaptive.py Jupytext demo of adaptive Lindbladian evolution (SciPy reference expm).
ppvm-python/demo/lindblad_pc_scaling.py Jupytext demo measuring pc_step scaling vs rayon thread count.
ppvm-python/pyproject.toml Adds runtime numpy dependency; adds optional demo (SciPy) and doc groups.
ppvm-python/src/ppvm/init.py Exports Lindbladian, sigma_minus, sigma_plus from top-level ppvm.
ppvm-python/src/ppvm/paulisum.py Adds PauliSum.symmetry_merge and PauliSum.momentum_merge APIs.
ppvm-python/test/lindblad/_helpers.py Adds shared reference kernels (Hermitian table + dense Liouvillian + bilinear ref).
ppvm-python/test/lindblad/test_action_generator.py Adds cross-check tests for action/generator/leakage and duplicate-basis rejection.
ppvm-python/test/lindblad/test_adaptive_pc.py Adds end-to-end convergence tests for adaptive evolution and PC dt-scaling.
ppvm-python/test/lindblad/test_non_hermitian_jumps.py Adds dense-reference tests for non-Hermitian jumps (σ±, amplitude damping).
ppvm-python/test/lindblad/test_pc_step_rust.py Adds tests pinning Rust pc_step against numpy eig reference and dt-scaling.
ppvm-python/test/lindblad/init.py Initializes lindblad test package.
ppvm-python/test/test_momentum_merge.py Adds tests for momentum-sector symmetry merging and k-resolved Trotter convergence.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +144 to +146
/// Return the canonical (lex-min) orbit representative of `pauli`.
/// `pauli` is a length-`n_qubits` uint8 array with the encoding
/// `I=0, X=1, Y=2, Z=3`. Result is the same shape.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 3ace107 — the docstring now states the actual encoding (0=I, 1=X, 2=Z, 3=Y), matching the Lindblad bindings and the Rust parser.

Comment thread crates/ppvm-lindblad/Cargo.toml Outdated
Comment on lines +16 to +23
# Matrix-exponential action (Al-Mohy & Higham). NOTE: QuSpin-rust currently has
# no LICENSE file; this pin is a placeholder pending a permissive license being
# added upstream (the authors are co-authors of the ppvm paper).
quspin-expm = { git = "https://github.com/QuSpin/QuSpin-rust", rev = "6f4a1581e14d5258c9aeb684b4aec964bdff4e34" }
# `QuSpinError` (the error type returned by the `LinearOperator` trait methods
# we implement in `mf_expm.rs`) is not re-exported from `quspin-expm`'s root,
# so we depend on `quspin-types` directly. Same git rev as `quspin-expm`.
quspin-types = { git = "https://github.com/QuSpin/QuSpin-rust", rev = "6f4a1581e14d5258c9aeb684b4aec964bdff4e34" }

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 3ace107: QuSpin-rust added an MIT LICENSE upstream. The pin is bumped to that exact commit (a0ad6c9), the immediate successor of the previous pin — zero code drift, verified bit-identical on the regression gates.

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-16 12:24 UTC

Addresses the two Copilot comments on #178: QuSpin-rust added an MIT
LICENSE upstream (a0ad6c9, the immediate successor of the previous pin —
zero code drift, verified bit-identical), and the TranslationGroup
.canonicalize docstring stated the wrong Pauli-code encoding (Y/Z were
swapped vs the actual 0=I, 1=X, 2=Z, 3=Y).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 16, 2026 10:42

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 51 out of 80 changed files in this pull request and generated 5 comments.

Comment thread ppvm-python/src/ppvm/paulisum.py
Comment thread ppvm-python/src/ppvm/paulisum.py Outdated
Comment on lines +417 to +429
Generalizes :meth:`symmetry_merge` to non-trivial momentum sectors
(``k != 0``) while keeping real coefficients on both PauliSums — the
only complex arithmetic is the internal character-weighted fold.
``self`` and ``other`` must be distinct objects with the same qubit
count. Exact after a translation-covariant gate layer; under a
generic Trotter step it carries the same ``O(dt^{p+1})`` equivariance
error as the ``k=0`` merge.

Args:
other: the PauliSum holding the imaginary component (modified in place).
group: a :class:`ppvm._core.TranslationGroup`.
momentum: sequence of integer modes, one per group generator
(e.g. ``[k]`` for a 1D chain; ``[0, ...]`` is the trivial sector).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 3fe8d03 — plain backticks now.

Comment thread crates/ppvm-python-native/src/interface.rs Outdated
Comment on lines +131 to +136
ppvm_pauli_sum::symmetry::canonicalize_pauli_sum_complex(
&mut basis,
&mut coeffs,
group.core(),
&momentum,
);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 3fe8d03 — momentum_merge now returns PyResult and validates momentum length plus the n_qubits of both PauliSums against the group before any canonicalization; all three violations raise ValueError.

Comment on lines +70 to +77
pub fn symmetry_merge(
&mut self,
group: &crate::symmetry::TranslationGroup,
) {
ppvm_pauli_sum::symmetry::symmetry_merge_pauli_sum(
&mut self.inner,
group.core(),
);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 3fe8d03 — symmetry_merge validates PauliSum vs group n_qubits and raises ValueError.

…paths and RST roles

Copilot review of #178: mismatched TranslationGroup n_qubits or momentum
length now raises ValueError instead of panicking across the FFI
boundary; ppvm.symmetry.TranslationGroup docstring path corrected to
ppvm._core.TranslationGroup; remaining :class:/:meth: RST roles in
paulisum.py replaced with plain backticks (the docs pipeline renders
Markdown).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 16, 2026 11:54

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 51 out of 80 changed files in this pull request and generated 1 comment.

Comment on lines +149 to +155
n, T = 4, 0.3
bonds = chain_bonds(n)
# exact reference at the matching times for two step sizes
err = {}
for dt in (0.04, 0.02):
steps = round(T / dt)
ts = np.arange(steps + 1) * dt
@AlexSchuckert

Copy link
Copy Markdown
Collaborator Author

Split into a stacked chain of reviewable PRs, per review feedback:

  1. Translation symmetry: TranslationGroup + orbit-representative Pauli-sum merging #180 — Translation symmetry: TranslationGroup + orbit-representative Pauli-sum merging (~950 lines, Rust, no new deps)
  2. Continuous-time Pauli propagation: real-space adaptive Lindbladian evolution #181 — CTPP core: real-space adaptive Lindbladian evolution (crate + Python API + tests + demos)
  3. Symmetry-merged evolution: Trotter merging + momentum-sector CTPP #182 — Symmetry-merged evolution: PauliSum.symmetry_merge/momentum_merge (Trotter) + pc_step_orbit_rep (momentum-sector CTPP)
  4. docs: CTPP benchmarking-campaign ledgers (text only) #183 — Benchmarking-campaign ledgers, text only (plots/npz stay on this branch)

#179 (Kossakowski dissipator) is rebased onto #182's branch and remains open.

The stack reproduces this PR's tree exactly (verified: the only diff is the excluded binary plot/reference files). This branch is kept as the archive of the full 160-commit development history; each split PR links back here. Review bottom-up: #180#181#182#183; merging bottom-up retargets the stack automatically.

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.

4 participants