Continuous-time Pauli propagation: adaptive Pauli-Lindbladian evolution (real-space + momentum-sector)#178
Continuous-time Pauli propagation: adaptive Pauli-Lindbladian evolution (real-space + momentum-sector)#178AlexSchuckert wants to merge 164 commits into
Conversation
…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>
…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>
There was a problem hiding this comment.
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.
| /// 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. |
There was a problem hiding this comment.
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.
| # 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" } |
There was a problem hiding this comment.
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.
|
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>
| 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). |
There was a problem hiding this comment.
Fixed in 3fe8d03 — plain backticks now.
| ppvm_pauli_sum::symmetry::canonicalize_pauli_sum_complex( | ||
| &mut basis, | ||
| &mut coeffs, | ||
| group.core(), | ||
| &momentum, | ||
| ); |
There was a problem hiding this comment.
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.
| pub fn symmetry_merge( | ||
| &mut self, | ||
| group: &crate::symmetry::TranslationGroup, | ||
| ) { | ||
| ppvm_pauli_sum::symmetry::symmetry_merge_pauli_sum( | ||
| &mut self.inner, | ||
| group.core(), | ||
| ); |
There was a problem hiding this comment.
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>
| 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 |
|
Split into a stacked chain of reviewable PRs, per review feedback:
#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. |
What this adds
Continuous-time Pauli propagation (CTPP): direct Heisenberg-picture evolution
O ← exp(dt·L*) Oof Pauli observables under a Lindbladian, on an adaptively truncated Pauli-string basis. The step is exact indtwithin 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-freequspin-expmexponential), andpc_step_orbit_rep(translation-symmetric variant, below). Truncation policy in onePcStepConfig:max_basisrank cap (primary dial), optionaladmit_basisworking-set bound (displacement truncation: final cap selects top-max_basisover the retained+admitted union),drop_tolmagnitude prune, optionaltau_addleakage-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).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).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:
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.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