Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
164 commits
Select commit Hold shift + click to select a range
177fbee
feat(python-native): Rust shim for adaptive Pauli-Lindbladian time ev…
AlexSchuckert May 26, 2026
962ba18
feat(python): adaptive Lindbladian demo, shim tests, lazy SciPy
AlexSchuckert May 27, 2026
1a7d054
feat(lindblad): extract ppvm-lindblad crate, non-Hermitian jumps, pur…
AlexSchuckert May 29, 2026
ea294e3
feat(python): Lindbladian pc_step, sigma_plus/minus jumps, convergenc…
AlexSchuckert May 29, 2026
5e70242
Fix SPDX header formatting in lindblad.py
Copilot Jun 2, 2026
e54b88c
review: address PR #98 review comments
AlexSchuckert Jun 2, 2026
9fd6ef5
review(ppvm-lindblad): swap custom CsrMatrix for sprs::CsMatI<f64, u3…
AlexSchuckert Jun 3, 2026
e043ae1
feat(ppvm-lindblad): add `drop_tol` to pc_step for in-Rust basis pruning
AlexSchuckert Jun 3, 2026
5ad4e83
Remove action_cache from LindbladSpec
AlexSchuckert Jun 4, 2026
53f7ff2
perf(ppvm-lindblad): mimalloc allocator + chunked leakage → ~50% peak…
AlexSchuckert Jun 4, 2026
d763aa8
feat(lindblad): K parameter for pc_step; default K=5
AlexSchuckert Jun 4, 2026
dfc3204
Merge remote-tracking branch 'origin/main' into lindblad-pr-fixes
AlexSchuckert Jun 6, 2026
a591310
feat(ppvm-lindblad): matrix-free pc_step, rk4_step, and memory opts
AlexSchuckert Jun 9, 2026
b012332
feat(symmetry): TranslationGroup primitive + pc_step / PauliSum merging
AlexSchuckert Jun 10, 2026
9e8551b
feat(symmetry): phase-aware extension for momentum-sector evolution
AlexSchuckert Jun 11, 2026
8c1bc1b
feat(symmetry): per-step orbit-rep evolution with complex CSR
AlexSchuckert Jun 11, 2026
edb77fd
feat(symmetry): momentum-sector (k!=0) merge for real PauliSum pairs
AlexSchuckert Jun 15, 2026
68f9451
docs(symmetry): document k!=0 merging; test momentum_merge vs exact d…
AlexSchuckert Jun 15, 2026
a8e4dcc
Update Julia version and make sure benchmarks are consistent
david-pl Jun 17, 2026
6dca8a5
Merge branch 'main' into david/update-julia-benches
david-pl Jun 17, 2026
13aaad4
More efficient weight calculation
david-pl Jun 17, 2026
1a67b31
Inline cutoff
david-pl Jun 17, 2026
1a2b8f8
Hard-code weight to avoid slicing overhead
david-pl Jun 17, 2026
8ea602f
Add an early return for max_weight == usize::MAX
david-pl Jun 17, 2026
0072256
Remove svg
david-pl Jun 17, 2026
dd0728a
Merge david/fix-weight-calc (#126, faster weight calc) into symmetry-…
AlexSchuckert Jun 17, 2026
2799af8
feat(ppvm-lindblad): drive expm action via quspin-expm (real + complex)
AlexSchuckert Jun 17, 2026
f73cd6d
refactor(ppvm-lindblad): remove retired expm engine and CSR-for-real …
AlexSchuckert Jun 17, 2026
33d58d7
perf(ppvm-lindblad): matrix-free (cache-action) orbit-rep expm
AlexSchuckert Jun 17, 2026
9e1df52
refactor(ppvm-lindblad): drop the dead complex-CSR orbit-rep reference
AlexSchuckert Jun 17, 2026
65fc28c
feat(ppvm-lindblad): promote max_basis to a required pc_step parameter
AlexSchuckert Jun 25, 2026
f73854c
feat(ppvm-lindblad): apply max_basis rank-cap to the orbit-rep pc_step
AlexSchuckert Jun 25, 2026
d0928c2
Merge origin/main into symmetry-merging
AlexSchuckert Jun 30, 2026
f5d9ac5
autotune(expm-pc-step): baseline + profile (expm apply = 97% of pc_step)
AlexSchuckert Jul 1, 2026
f93a230
perf(ppvm-lindblad): cache generator action in real-space expm_apply_mf
AlexSchuckert Jul 1, 2026
49ca31f
Revert "perf(ppvm-lindblad): cache generator action in real-space exp…
AlexSchuckert Jul 1, 2026
5dbc49a
autotune(expm-pc-step): record cache-action discard
AlexSchuckert Jul 1, 2026
8eab919
perf(ppvm-lindblad): cache generator action + tolerance-matched expm …
AlexSchuckert Jul 1, 2026
646f67d
autotune(expm-pc-step): record cache+loose keep (~10.7x); note stale-…
AlexSchuckert Jul 1, 2026
22e1757
autotune(expm-memory): profile + knob characterization (no code fix w…
AlexSchuckert Jul 1, 2026
cd5fb58
autotune(expm-memory): tuned N=12 corrects earlier expm-wins claim (n…
AlexSchuckert Jul 1, 2026
5554051
autotune(expm-memory): wide dt/drop/max_basis scan (clean) -> optimal…
AlexSchuckert Jul 1, 2026
0e31637
autotune(expm-memory): tight-accuracy (1e-4/1e-5) expm-vs-Trotter -> …
AlexSchuckert Jul 2, 2026
aa37fb6
autotune(ladder): baselines + profiles for Trotter and expm XY-ladder
AlexSchuckert Jul 2, 2026
15f9655
perf(ppvm-pauli-sum): parallelize map_insert gate application
AlexSchuckert Jul 2, 2026
abe2dda
Revert "perf(ppvm-pauli-sum): parallelize map_insert gate application"
AlexSchuckert Jul 2, 2026
29f20b8
autotune(trotter-ladder): record parallelize-map_insert discard (~30%…
AlexSchuckert Jul 2, 2026
fb5d588
Reapply "perf(ppvm-pauli-sum): parallelize map_insert gate application"
AlexSchuckert Jul 2, 2026
56801fe
autotune(trotter-ladder): KEEP parallel map_insert — 2.5x at N=12 (re…
AlexSchuckert Jul 2, 2026
2411240
perf(ppvm-lindblad): reuse predictor action cache in leakage2
AlexSchuckert Jul 2, 2026
42a5fa9
Revert "perf(ppvm-lindblad): reuse predictor action cache in leakage2"
AlexSchuckert Jul 2, 2026
a1c0927
autotune(expm-ladder): discard action-cache-sharing (~2.3x RSS for <=…
AlexSchuckert Jul 2, 2026
9ee8b3b
Revert "Reapply "perf(ppvm-pauli-sum): parallelize map_insert gate ap…
AlexSchuckert Jul 2, 2026
ccc248e
autotune(trotter-ladder): CORRECT verdict - parallel is ~1.1x/+50%RAM…
AlexSchuckert Jul 2, 2026
03595f4
autotune(ladder): conclude - both near-optimal, no net change kept th…
AlexSchuckert Jul 2, 2026
c54423f
feat(ppvm-lindblad): opt-in rate-based leakage admission (PPVM_K_LEAK…
AlexSchuckert Jul 3, 2026
1f633eb
autotune(expm-ladder): add admission-control scan harness (K vs max_b…
AlexSchuckert Jul 3, 2026
2e59529
autotune(expm-ladder): K-admission study - K~1 helps ~1.1-2x on both …
AlexSchuckert Jul 3, 2026
8b65909
autotune(expm-ladder): RAM is accuracy-set; admission filter is a wal…
AlexSchuckert Jul 3, 2026
613e54d
autotune(expm-ladder): CORRECTION - max_basis (per-chunk cap) limits …
AlexSchuckert Jul 3, 2026
d08a11a
feat(ppvm-lindblad): opt-in streaming (matrix-free) expm mode (PPVM_E…
AlexSchuckert Jul 3, 2026
bf18801
autotune(expm-ladder): record streaming-mode RAM<->wall dial (1.8x le…
AlexSchuckert Jul 3, 2026
50eae86
autotune(orbit-compare): stage 0 - verify both orbit-preserving metho…
AlexSchuckert Jul 3, 2026
858bbef
feat(ppvm-lindblad): orbit-rep streaming (matrix-free) expm via PPVM_…
AlexSchuckert Jul 3, 2026
0af9b4c
autotune(orbit-compare): L=5 k-resolved Trotter vs expm - wall~tied, …
AlexSchuckert Jul 3, 2026
cbff9fa
autotune(orbit-compare): at-scale harnesses - L=7 exact-ED ref, growt…
AlexSchuckert Jul 4, 2026
c91c57e
autotune(orbit-compare): record L=7 at-scale results - expm owns rel<…
AlexSchuckert Jul 4, 2026
48d9af9
autotune(orbit-compare): k=1 canonical row - expm sub-1e-3 (5.5e-4) v…
AlexSchuckert Jul 4, 2026
87e655a
autotune(orbit-compare): RAM comparison at matched precision; correct…
AlexSchuckert Jul 4, 2026
6866c3b
autotune(orbit-compare): xy-experiments harness scan - K=1 Pareto win…
AlexSchuckert Jul 4, 2026
2324407
autotune(orbit-compare): calibrated merged-vs-unmerged trotter dissec…
AlexSchuckert Jul 4, 2026
14f6642
autotune(orbit-compare): extended K=5 grid - pec matches Trotter's fr…
AlexSchuckert Jul 4, 2026
4a9c8db
autotune(orbit-compare): retract 'within 2x = competitive' framing; b…
AlexSchuckert Jul 4, 2026
caca41b
autotune(orbit-compare): adopt median pointwise rel metric - pec wins…
AlexSchuckert Jul 4, 2026
75e4947
autotune(orbit-compare): overlay plot of highest-precision curves vs …
AlexSchuckert Jul 4, 2026
0f80cdd
autotune(orbit-compare): merged-vs-unmerged revisited under median me…
AlexSchuckert Jul 4, 2026
fe2e1e2
autotune(orbit-compare): fixed-precision verdict at 1% median - merge…
AlexSchuckert Jul 4, 2026
046bee0
autotune(orbit-compare): per-step truncation for merged trotter dies …
AlexSchuckert Jul 4, 2026
01cdcca
autotune(orbit-compare): real-space MSD comparison - split verdict at 1%
AlexSchuckert Jul 4, 2026
ccf7f65
autotune(orbit-compare): real-space K=5 completion + streaming test
AlexSchuckert Jul 4, 2026
06ed102
autotune(orbit-compare): interior-drop sweep - pec 27x faster at near…
AlexSchuckert Jul 4, 2026
8d733d0
autotune(orbit-compare): K sweep 1..10 - flat plateau then cliff at t…
AlexSchuckert Jul 4, 2026
b83ac67
autotune(orbit-compare): 2D K x drop scan - cliff is a pure tau_add~4…
AlexSchuckert Jul 4, 2026
cb8eefb
feat(ppvm-lindblad): rate-based K leakage filter on the orbit-rep path
AlexSchuckert Jul 4, 2026
b4de269
autotune(orbit-compare): tau_add cliff is dt- and drop-independent - …
AlexSchuckert Jul 4, 2026
14bdef3
autotune(orbit-compare): per-dt heatmaps of median rel vs (drop_tol, …
AlexSchuckert Jul 4, 2026
4923cc5
autotune(orbit-compare): correct 'plateau' claim - measured knob sens…
AlexSchuckert Jul 4, 2026
b5b35ec
autotune(orbit-compare): addendum - efficient locus is ~fixed K at fi…
AlexSchuckert Jul 4, 2026
129b84f
autotune(orbit-compare): dt=0.025 verification - K'=tau_add/drop wind…
AlexSchuckert Jul 4, 2026
a1efd79
autotune(orbit-compare): max_basis rank cap is the primary knob - bea…
AlexSchuckert Jul 4, 2026
cfafbc0
autotune(orbit-compare): tau_add vs max_basis head-to-head at matched…
AlexSchuckert Jul 4, 2026
0abb9eb
autotune(orbit-compare): framing - fixed rank, churning membership; m…
AlexSchuckert Jul 4, 2026
20fdce2
autotune(orbit-compare): 2x2 at M300k - pure cap ties cap+tau; drop c…
AlexSchuckert Jul 4, 2026
d460658
autotune(orbit-compare): concluding summary - max_basis alone superse…
AlexSchuckert Jul 4, 2026
612b773
autotune(orbit-compare): correct MPS analogy - chi_max with negligibl…
AlexSchuckert Jul 4, 2026
f243ad3
autotune(orbit-compare): cap-primary pec vs Trotter in real space - p…
AlexSchuckert Jul 4, 2026
a8c84c7
autotune(orbit-compare): drop=0 vs drop=1e-5 at fixed cap - tiny prun…
AlexSchuckert Jul 4, 2026
ccd6f3e
autotune(orbit-compare): drop sweep at fixed cap - 1e-10 behaves like…
AlexSchuckert Jul 4, 2026
fe72f75
autotune(orbit-compare): mechanism resolved - cap is admission-only, …
AlexSchuckert Jul 4, 2026
aabcf9e
feat(ppvm-lindblad): admit_basis working-set bound on pc_step (top-B-…
AlexSchuckert Jul 4, 2026
bee4907
autotune(orbit-compare): max-rel analysis - valve quasi-freezes at la…
AlexSchuckert Jul 4, 2026
03c4854
autotune(orbit-compare): rel(t) plot - valve vs displacement late-tim…
AlexSchuckert Jul 4, 2026
83610e7
autotune(orbit-compare): T=10 every-step comparison - displacement wi…
AlexSchuckert Jul 5, 2026
fdb90ed
autotune(orbit-compare): raw MSD(t) plot to T=10, all schemes vs exact
AlexSchuckert Jul 5, 2026
2d1a273
autotune(orbit-compare): frozen B=200k/300k at T=10 - size cannot sub…
AlexSchuckert Jul 5, 2026
57e6264
autotune(orbit-compare): valve 1e-6 at T=10 + combined 3-panel figure
AlexSchuckert Jul 5, 2026
26dca4b
autotune(orbit-compare): threshold-only runs at T=10 - fail early and…
AlexSchuckert Jul 5, 2026
41e609e
autotune(orbit-compare): dt-artefact analysis - valve dt-limited, dis…
AlexSchuckert Jul 6, 2026
cdb001b
autotune(orbit-compare): dt-scan error-vs-time plot
AlexSchuckert Jul 6, 2026
ffbfa85
autotune(orbit-compare): displacement B=300k dt scan - anti-dt trend …
AlexSchuckert Jul 6, 2026
fe43d42
autotune(orbit-compare): late-time floor decomposed - revival compone…
AlexSchuckert Jul 6, 2026
78af50e
autotune(orbit-compare): revival mechanism tested exactly at N=10 - t…
AlexSchuckert Jul 6, 2026
e19cc7a
autotune(orbit-compare): Trotter+truncation dephasing test - same era…
AlexSchuckert Jul 6, 2026
7786ccd
autotune(orbit-compare): comparison up to the MSD maximum - all pec v…
AlexSchuckert Jul 6, 2026
fc7f248
autotune(orbit-compare): L=21 (N=42) MSD converged via displacement C…
AlexSchuckert Jul 6, 2026
e3afb4d
autotune(orbit-compare): all-runs L=21 figure (19 curves, rough throu…
AlexSchuckert Jul 6, 2026
8cf66d8
autotune(orbit-compare): L=21 Trotter dt x mac ladder - best rung 2.6…
AlexSchuckert Jul 6, 2026
a85ad01
autotune(orbit-compare): L=41 (82 qubits) matches external 2^-18 Paul…
AlexSchuckert Jul 6, 2026
9e493f8
autotune(orbit-compare): drop L=21 curve from the L=41 reference figure
AlexSchuckert Jul 6, 2026
4c193a1
autotune(orbit-compare): add digitized 2^-17 reference points to the …
AlexSchuckert Jul 6, 2026
d28baf2
autotune(orbit-compare): corrected reference axis (x*5/4) - L=41 comp…
AlexSchuckert Jul 6, 2026
48d1cbb
autotune(orbit-compare): finalized L=41 external cross-validation - 0…
AlexSchuckert Jul 6, 2026
28287d5
autotune(orbit-compare): reference = Begusic-Chan PRXQ 2025 Fig 2c; t…
AlexSchuckert Jul 6, 2026
353e930
autotune(orbit-compare): D(t) extraction two ways from L=41 data
AlexSchuckert Jul 6, 2026
77a711d
autotune(orbit-compare): sliding-window D extraction, L=41
AlexSchuckert Jul 6, 2026
91fd13e
autotune(orbit-compare): L=21 Trotter 1e-5 rung lands - 190x wall for…
AlexSchuckert Jul 6, 2026
11602c2
autotune(orbit-compare): L=21 summary figure (curves, deviations, Par…
AlexSchuckert Jul 7, 2026
29b3978
autotune(orbit-compare): deep-Trotter vs reference figure with self-u…
AlexSchuckert Jul 7, 2026
6f1d710
autotune(orbit-compare): A=K*B scan at L=41 B=300k
AlexSchuckert Jul 7, 2026
af757d7
autotune(orbit-compare): A=K*B scan at L=41 - converged at K=3, K=1 i…
AlexSchuckert Jul 7, 2026
074919d
autotune(orbit-compare): A=K*B scan at L=41 B=150k
AlexSchuckert Jul 7, 2026
dfef327
autotune(orbit-compare): A=K*B scan at B=150k - same structure, best …
AlexSchuckert Jul 7, 2026
0fbe4f9
refactor(ppvm-lindblad): orbit admit_basis; remove streaming paths; d…
AlexSchuckert Jul 7, 2026
99ae78f
autotune(orbit-compare): full K scan to K=10 at both B
AlexSchuckert Jul 7, 2026
bd23ef9
autotune(orbit-compare): K scan to 10 - K=3 is both convergence onset…
AlexSchuckert Jul 7, 2026
bf79136
refactor(ppvm-lindblad): remove pc_step_complex; keep full-space comp…
AlexSchuckert Jul 7, 2026
0ba5f55
refactor(ppvm-lindblad): remove rk4_step entirely
AlexSchuckert Jul 7, 2026
dfde4f9
autotune(orbit-compare): K=2 walls re-measured - contention artifact …
AlexSchuckert Jul 7, 2026
409b6ae
refactor(ppvm-lindblad): tau_add as explicit argument; remove PPVM_K_…
AlexSchuckert Jul 7, 2026
ff582d8
autotune(orbit-compare): drivers use explicit --tau_add; legacy K/str…
AlexSchuckert Jul 7, 2026
7c8e78e
autotune(orbit-compare): B=600k baseline - first-order B-convergence,…
AlexSchuckert Jul 7, 2026
97a50cf
autotune(orbit-compare): K/B convergence plot rebased on B=600k/K=3 a…
AlexSchuckert Jul 7, 2026
f71ed65
autotune(orbit-compare): accuracy-vs-RAM iso-plot - increasing B at K…
AlexSchuckert Jul 7, 2026
3361afe
autotune(orbit-compare): fixed-A K/B split - K>=3 is all that matters…
AlexSchuckert Jul 7, 2026
6d8bc7d
autotune(orbit-compare): correct over-read of noise-floor cells - fix…
AlexSchuckert Jul 7, 2026
0873e9b
autotune(orbit-compare): triage + delete dangling worktree branches (…
AlexSchuckert Jul 8, 2026
74c8aff
autotune(orbit-compare): triage perf/mimalloc-allocator (deleted, sup…
AlexSchuckert Jul 8, 2026
31536d2
autotune(orbit-compare): diagnose orbit-rep per-term canonicalization…
AlexSchuckert Jul 8, 2026
58fd298
autotune(orbit-compare): D extraction = [2.5,5] linregress (matches B…
AlexSchuckert Jul 8, 2026
14a84d2
autotune(orbit-compare): D vs B for dt=0.05/0.1/0.2 - small dt B-limi…
AlexSchuckert Jul 8, 2026
b441f5c
autotune(orbit-compare): larger B converges dt<=0.1 but not dt=0.2 (K…
AlexSchuckert Jul 8, 2026
dff8bc9
autotune(orbit-compare): B=1M confirms plateau - D flat 3.75-3.80 for…
AlexSchuckert Jul 8, 2026
bc43a7a
autotune(orbit-compare): B=100k D-outlier = deterministic non-monoton…
AlexSchuckert Jul 8, 2026
ea81c8c
autotune(orbit-compare): B=3M all dt converge to D~3.79; revise dt=0.…
AlexSchuckert Jul 8, 2026
261f5e4
autotune(orbit-compare): B=6M confirms D plateau at 3.76 (6x largest …
AlexSchuckert Jul 9, 2026
8739df7
autotune(orbit-compare): dt=0.05 B-spread ~4x dt=0.1 - more truncatio…
AlexSchuckert Jul 9, 2026
a1dc4d7
refactor(ppvm-lindblad): PcStepConfig + error.rs (PR #98 review patte…
AlexSchuckert Jul 16, 2026
c4ca094
Merge origin/main (65 commits: vihaco/cli crates, tableau features, n…
AlexSchuckert Jul 16, 2026
2f5c40e
docs(autotune): ledger entry for origin/main sync + SCAN_GAMMA env in…
AlexSchuckert Jul 16, 2026
85c0100
chore(python): regenerate uv.lock against public PyPI (drop jfrog reg…
AlexSchuckert Jul 16, 2026
4eee3ef
refactor: comment pass (factual, no history refs) + dead-code removal
AlexSchuckert Jul 16, 2026
0b0da59
docs(autotune): ledger entry for the pre-PR comment/simplification pass
AlexSchuckert Jul 16, 2026
3ace107
review: bump quspin pin to the MIT-licensed rev; fix Pauli-code doc
AlexSchuckert Jul 16, 2026
3fe8d03
review: validate symmetry_merge/momentum_merge inputs; fix docstring …
AlexSchuckert Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,277 changes: 1,226 additions & 51 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ members = [
"crates/ppvm-pauli-word",
"crates/ppvm-pauli-sum",
"crates/ppvm-sym",
"crates/ppvm-lindblad",
"crates/ppvm-python-native",
"crates/ppvm-tableau",
"crates/ppvm-stim", "crates/stim-parser",
Expand Down
25 changes: 25 additions & 0 deletions crates/ppvm-lindblad/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "ppvm-lindblad"
version = "0.1.0"
edition = "2024"
description = "Direct Heisenberg-picture Lindbladian evolution on an adaptive Pauli-string basis."

[dependencies]
fxhash = "0.2.1"
ndarray = "0.17"
num = "0.4.3"
ppvm-traits = { version = "0.1.0", path = "../ppvm-traits" }
ppvm-pauli-word = { version = "0.1.0", path = "../ppvm-pauli-word" }
ppvm-pauli-sum = { version = "0.1.0", path = "../ppvm-pauli-sum" }
rayon = "1.11"
sprs = { version = "0.11", default-features = false, features = ["multi_thread"] }
# Matrix-exponential action (Al-Mohy & Higham). QuSpin-rust is MIT-licensed;
# the pinned rev is the commit that added the LICENSE file.
quspin-expm = { git = "https://github.com/QuSpin/QuSpin-rust", rev = "a0ad6c9fe2e8063208f9ba1c6677150c993bb554" }
# `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 = "a0ad6c9fe2e8063208f9ba1c6677150c993bb554" }

[dev-dependencies]
approx = "0.5.1"
58 changes: 58 additions & 0 deletions crates/ppvm-lindblad/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//! Configuration objects for the predictor-corrector stepper.

/// Truncation and execution policy for a single predictor-corrector step
/// ([`crate::LindbladSpec::pc_step`], [`crate::LindbladSpec::pc_step_timed`],
/// [`crate::orbit_rep::pc_step_orbit_rep`]).
///
/// These are the per-run *tuning knobs*, kept separate from the per-call data
/// (`basis`, `coeffs`, `dt`, `protected`, and — on the orbit path — the
/// translation group and momentum).
///
/// `max_basis` is the primary accuracy/cost dial; `admit_basis` selects the
/// displacement scheme; `drop_tol` is the churn valve of the admission-bound
/// scheme; `tau_add` is a wall optimization at most.
#[derive(Debug, Clone, Copy)]
pub struct PcStepConfig {
/// Hard rank cap on the retained basis: after the corrector, only the
/// top-`max_basis` strings by `|coeff|` are kept (protected words always
/// survive). The primary convergence dial — verify by re-running at 2×.
pub max_basis: usize,
/// Working-set (admission) bound. When `Some(a)` with `a > max_basis`,
/// enrichment may grow the live basis to `a` and the final cap performs a
/// genuine top-`max_basis`-of-union rank displacement (the analog of
/// two-site TDVP truncation at `χ_max`); `drop_tol` is then not needed
/// for membership turnover. `None` bounds admission by `max_basis`
/// itself — the valve scheme, which requires `drop_tol > 0` to keep the
/// basis adapting once it fills.
pub admit_basis: Option<usize>,
/// Magnitude prune applied after the corrector: basis entries whose
/// `|coeff|` is below `drop_tol` are discarded (protected words are
/// always kept). `<= 0.0` disables pruning — valid only with
/// `admit_basis` set, otherwise the basis freezes once it fills the cap.
pub drop_tol: f64,
/// Optional absolute rate threshold on leakage admission: a candidate is
/// admitted only if its inflow rate exceeds `tau_add`. This is the
/// natural (dt- and drop_tol-independent) parameterization — the
/// admission accuracy cliff sits at a fixed `tau_add`. `None` = no
/// filter, the recommended default with cap-based truncation.
pub tau_add: Option<f64>,
/// When `Some(n)`, run the entire step inside a freshly built rayon
/// thread pool of `n` threads (useful for benchmarking parallel
/// scaling). When `None`, the global rayon pool is used.
pub num_threads: Option<usize>,
}

impl Default for PcStepConfig {
/// Uncapped, unfiltered, no pruning: the near-exact reference
/// configuration. Production runs should set `max_basis` (and usually
/// `admit_basis ≈ 2-3×` it).
fn default() -> Self {
Self {
max_basis: usize::MAX,
admit_basis: None,
drop_tol: 0.0,
tau_add: None,
num_threads: None,
}
}
}
73 changes: 73 additions & 0 deletions crates/ppvm-lindblad/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//! Error type for [`crate::LindbladSpec`] construction and stepping.

use crate::MAX_QUBITS;
use std::fmt;

/// Errors raised when constructing a [`crate::LindbladSpec`].
#[derive(Debug, Clone)]
pub enum Error {
TooManyQubits {
got: usize,
},
LengthMismatch {
what: &'static str,
a: usize,
b: usize,
},
InvalidPauliCode {
code: u8,
},
InvalidPauliChar {
c: char,
},
WrongLength {
expected: usize,
got: usize,
},
NegativeRate {
index: usize,
rate: f64,
},
EmptyLincomb {
index: usize,
},
Internal(String),
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::TooManyQubits { got } => {
write!(
f,
"LindbladSpec supports n_qubits ≤ {MAX_QUBITS}; got {got}"
)
}
Error::LengthMismatch { what, a, b } => {
write!(f, "{what}: expected matching lengths, got {a} and {b}")
}
Error::InvalidPauliCode { code } => write!(
f,
"Pauli code must be 0 (I), 1 (X), 2 (Z), or 3 (Y); got {code}"
),
Error::InvalidPauliChar { c } => {
write!(f, "invalid Pauli character '{c}'; expected I, X, Y, or Z")
}
Error::WrongLength { expected, got } => {
write!(f, "Pauli string has length {got} but n_qubits = {expected}")
}
Error::NegativeRate { index, rate } => {
write!(f, "jump rate must be non-negative; got γ_{index} = {rate}")
}
Error::EmptyLincomb { index } => {
write!(
f,
"jump {index}: lincomb must contain at least one Pauli term"
)
}
Error::Internal(msg) => write!(f, "internal error: {msg}"),
}
}
}

impl std::error::Error for Error {}
145 changes: 145 additions & 0 deletions crates/ppvm-lindblad/src/expm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// SPDX-FileCopyrightText: 2026 The PPVM Authors
// SPDX-License-Identifier: Apache-2.0

//! Taylor-partition selection for the `quspin-expm`-backed `exp(t·A)·b`
//! engine (driven from [`crate::mf_expm`]): the `(m, s)` selection tables
//! [`THETA`] / [`THETA_LOOSE`] from Al-Mohy & Higham (2011), used to pick
//! the partition handed to `quspin-expm`'s `from_parts`.

/// `θ_m` table from Al-Mohy & Higham (2011), Table A.3, for double
/// precision (unit roundoff `u = 2^{-53}`).
///
/// `θ_m` bounds `‖A‖₁` such that the degree-`m` Taylor polynomial
/// approximates `exp(A)` to within `u`. We pick `(m, s)` with
/// `s ≥ ⌈‖tA‖₁ / θ_m⌉` and minimise `m·s` (total SpMV count).
pub(crate) const THETA: &[(u32, f64)] = &[
(1, 2.29e-16),
(2, 2.58e-8),
(3, 1.39e-5),
(4, 3.40e-4),
(5, 2.40e-3),
(6, 9.07e-3),
(7, 2.38e-2),
(8, 5.00e-2),
(9, 8.96e-2),
(10, 1.44e-1),
(11, 2.14e-1),
(12, 3.00e-1),
(13, 4.00e-1),
(14, 5.14e-1),
(15, 6.41e-1),
(16, 7.81e-1),
(17, 9.31e-1),
(18, 1.09),
(19, 1.26),
(20, 1.44),
(21, 1.62),
(22, 1.82),
(23, 2.01),
(24, 2.22),
(25, 2.43),
(26, 2.64),
(27, 2.86),
(28, 3.08),
(29, 3.31),
(30, 3.54),
];

/// `θ_m` table for a relaxed backward-error tolerance `tol = 1e-6`, computed
/// with the same Al-Mohy & Higham (2011) construction as [`THETA`] (the
/// backward-error series `h_{m+1}(x) = log(e^{-x} T_m(x))`; validated by
/// reproducing the `u = 2^{-53}` table above to ~2 significant figures).
///
/// The predictor-corrector truncates the Pauli basis at `drop_tol` (typically
/// 1e-3), so computing `exp` to double-precision backward error (~1e-16) is
/// ~10 orders more accurate than the state it acts on. Using `tol = 1e-6`
/// (still ~1000x tighter than the truncation) admits a lower-degree Taylor
/// polynomial for the same `‖tA‖`, cutting the SpMV count (e.g. 23 -> 13 at
/// `‖tA‖ ≈ 2`) with no measurable effect on the truncated result.
pub(crate) const THETA_LOOSE: &[(u32, f64)] = &[
(1, 2.000e-06),
(2, 2.447e-03),
(3, 2.863e-02),
(4, 1.025e-01),
(5, 2.262e-01),
(6, 3.911e-01),
(7, 5.866e-01),
(8, 8.045e-01),
(9, 1.039),
(10, 1.285),
(11, 1.539),
(12, 1.801),
(13, 2.067),
(14, 2.337),
(15, 2.610),
(16, 2.885),
(17, 3.162),
(18, 3.441),
(19, 3.721),
(20, 4.001),
(21, 4.282),
(22, 4.564),
(23, 4.847),
(24, 5.129),
(25, 5.412),
(26, 5.696),
(27, 5.979),
(28, 6.263),
(29, 6.546),
(30, 6.830),
];

/// Pick `(m, s)` minimising `s·m` subject to `s ≥ ⌈t_norm / θ_m⌉, s ≥ 1`,
/// using the `θ_m` table `theta`. Restricted to the table's `m` range; for
/// larger norms `s` simply grows linearly.
fn select_ms_with(t_norm: f64, theta: &[(u32, f64)]) -> (u32, u32) {
if t_norm <= 0.0 {
return (1, 1);
}
let mut best_m = 1u32;
let mut best_s = 1u32;
let mut best_cost = u64::MAX;
for &(m, th) in theta {
let s_f = (t_norm / th).ceil();
let s = if s_f >= 1.0 { s_f as u32 } else { 1 };
let cost = (m as u64) * (s as u64);
if cost < best_cost {
best_cost = cost;
best_m = m;
best_s = s;
}
}
(best_m, best_s)
}

/// `(m, s)` selection at double-precision backward error ([`THETA`]).
pub(crate) fn select_ms(t_norm: f64) -> (u32, u32) {
select_ms_with(t_norm, THETA)
}

/// `(m, s)` selection at the relaxed `tol = 1e-6` backward error
/// ([`THETA_LOOSE`]) — fewer SpMVs, used on the truncated PC expm path.
pub(crate) fn select_ms_loose(t_norm: f64) -> (u32, u32) {
select_ms_with(t_norm, THETA_LOOSE)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn ms_selection_sane() {
// tiny norm → small m, s = 1
let (m, s) = select_ms(1e-9);
assert!(m <= 5, "expected small m for tiny norm, got m={m}");
assert_eq!(s, 1);

// moderate norm → m·s should be ~10-50
let (m, s) = select_ms(1.0);
assert!((m * s) <= 50, "moderate norm cost too high: m={m} s={s}");

// large norm → s grows
let (_m, s) = select_ms(100.0);
assert!(s >= 20, "large norm should require many steps, got s={s}");
}
}
Loading
Loading