Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
f5487af
feat(ppvm-lindblad): continuous-time Pauli propagation — real-space a…
AlexSchuckert Jul 16, 2026
7194e0c
feat: symmetry-merged evolution — Trotter merging + momentum-sector CTPP
AlexSchuckert Jul 16, 2026
ad6dc1f
docs(autotune): CTPP benchmarking-campaign ledgers (text only)
AlexSchuckert Jul 16, 2026
bc6f4ea
feat(ppvm-lindblad): Kossakowski-form dissipator + dissipative orbit-…
AlexSchuckert Jul 16, 2026
d3a8ffa
docs(autotune): Kossakowski optimization-experiment ledger
AlexSchuckert Jul 16, 2026
4188fd0
Merge branch 'split/1-translation-symmetry' into split/2-ctpp-core
AlexSchuckert Jul 16, 2026
e440fba
style: cargo fmt + ruff format; SPDX headers on config.rs/error.rs
AlexSchuckert Jul 16, 2026
4d72eaf
Merge branch 'split/2-ctpp-core' into split/3-symmetric-evolution
AlexSchuckert Jul 16, 2026
c86c206
style: cargo fmt + ruff format
AlexSchuckert Jul 16, 2026
096a823
Merge branch 'split/3-symmetric-evolution' into split/4-autotune-ledgers
AlexSchuckert Jul 16, 2026
25d3924
Merge branch 'split/3-symmetric-evolution' into kossakowski-dissipator
AlexSchuckert Jul 16, 2026
be4587e
style: cargo fmt + ruff format
AlexSchuckert Jul 16, 2026
9053580
chore: drop unused deps (sprs; python-native ppvm-traits/ppvm-pauli-w…
AlexSchuckert Jul 16, 2026
c91781f
Merge branch 'split/2-ctpp-core' into split/3-symmetric-evolution
AlexSchuckert Jul 16, 2026
68ef857
Merge branch 'split/2-ctpp-core' into split/4-autotune-ledgers
AlexSchuckert Jul 16, 2026
eb90987
Merge branch 'split/2-ctpp-core' into kossakowski-dissipator
AlexSchuckert Jul 16, 2026
42f9f0c
style: ruff check fixes (import sorting)
AlexSchuckert Jul 16, 2026
25d971b
Merge branch 'split/2-ctpp-core' into split/3-symmetric-evolution
AlexSchuckert Jul 16, 2026
1e1220e
Merge branch 'split/2-ctpp-core' into kossakowski-dissipator
AlexSchuckert Jul 16, 2026
ca188b2
style: ruff check fixes
AlexSchuckert Jul 16, 2026
39df05e
Merge branch 'split/3-symmetric-evolution' into split/4-autotune-ledgers
AlexSchuckert Jul 16, 2026
7e2f0e9
style: ruff RUF005 (unpack instead of concatenation)
AlexSchuckert Jul 16, 2026
a7bb93d
Merge branch 'split/3-symmetric-evolution' into split/4-autotune-ledgers
AlexSchuckert Jul 16, 2026
d578d72
Merge branch 'split/3-symmetric-evolution' into kossakowski-dissipator
AlexSchuckert Jul 16, 2026
66982d8
types: LindbladSpec stub in _core.pyi; precise jump_op typing (ty)
AlexSchuckert Jul 16, 2026
80b73de
Merge branch 'split/2-ctpp-core' into split/3-symmetric-evolution
AlexSchuckert Jul 16, 2026
113f055
types: TranslationGroup / merge / orbit-step stubs in _core.pyi
AlexSchuckert Jul 16, 2026
b317948
Merge branch 'split/3-symmetric-evolution' into split/4-autotune-ledgers
AlexSchuckert Jul 16, 2026
2bf1903
types: kossakowski kwargs in the LindbladSpec stub
AlexSchuckert Jul 16, 2026
ca1ab82
fix: resolve merge-conflict markers committed in error (lindblad.py)
AlexSchuckert Jul 16, 2026
806a9b1
types: assert non-None before subscripting in dt-scaling test (ty)
AlexSchuckert Jul 16, 2026
c51699e
Merge branch 'split/2-ctpp-core' into split/3-symmetric-evolution
AlexSchuckert Jul 16, 2026
0b394f5
Merge branch 'split/3-symmetric-evolution' into split/4-autotune-ledgers
AlexSchuckert Jul 16, 2026
a8bd1d5
Merge branch 'split/3-symmetric-evolution' into kossakowski-dissipator
AlexSchuckert Jul 16, 2026
64d248e
fix: 32-bit-safe Pauli-word storage (wasm32 build)
AlexSchuckert Jul 16, 2026
7644fb0
Merge branch 'split/2-ctpp-core' into split/3-symmetric-evolution
AlexSchuckert Jul 16, 2026
da115e8
Merge branch 'split/3-symmetric-evolution' into split/4-autotune-ledgers
AlexSchuckert Jul 16, 2026
a4eedfd
Merge branch 'split/3-symmetric-evolution' into kossakowski-dissipator
AlexSchuckert Jul 16, 2026
2209325
Merge split/3 (32-bit-safe word storage); chunk-typed Kossakowski masks
AlexSchuckert Jul 16, 2026
36cb95d
style: type alias for the orbit-step return (clippy type_complexity)
AlexSchuckert Jul 16, 2026
3bda5e6
Merge branch 'split/3-symmetric-evolution' into split/4-autotune-ledgers
AlexSchuckert Jul 16, 2026
5acd63e
Merge branch 'split/3-symmetric-evolution' into kossakowski-dissipator
AlexSchuckert Jul 16, 2026
1e9ab12
chore: end-of-file newline on autotune metric.toml (pre-commit)
AlexSchuckert Jul 16, 2026
2ab855b
Merge branch 'split/4-autotune-ledgers' into kossakowski-dissipator
AlexSchuckert Jul 16, 2026
4992d1e
chore: end-of-file newline on kossakowski metric.toml (pre-commit)
AlexSchuckert Jul 16, 2026
ec55ce7
bench(lindblad): drug dipolar-relaxation Kossakowski workload + profiler
AlexSchuckert Jul 17, 2026
2a24cd2
perf(lindblad): reuse P_a.p in Kossakowski both-sided sandwich
AlexSchuckert Jul 17, 2026
5a254d0
perf(lindblad): fold conjugate Kossakowski pairs (n,m)+(m,n)
AlexSchuckert Jul 17, 2026
4ec82e9
docs(autotune): 2026-07-17-drug-kossakowski ledger + clippy clean
AlexSchuckert Jul 17, 2026
2f4e7f5
style(lindblad): rustfmt the drug-kossakowski additions
AlexSchuckert Jul 17, 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,411 changes: 1,370 additions & 41 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
34 changes: 34 additions & 0 deletions crates/ppvm-lindblad/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[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"
# 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"
criterion = "0.7.0"
nalgebra = "0.34"

[[bench]]
name = "kossakowski"
harness = false

[[bench]]
name = "drug_dipolar"
harness = false
254 changes: 254 additions & 0 deletions crates/ppvm-lindblad/benches/drug_dipolar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
// SPDX-FileCopyrightText: 2026 The PPVM Authors
// SPDX-License-Identifier: Apache-2.0

//! Per-step cost of the Kossakowski-form dissipator on a *molecular dipolar
//! relaxation* workload (the ZULF-NMR drug-FID application), which stresses
//! the path differently from the superradiance chain in `kossakowski.rs`:
//!
//! - operators are **2-local rank-2 tensors** with ~4 Pauli terms each (one
//! channel per site pair, per spatial harmonic `m`), not single-site σ⁻;
//! - `K` is **block-diagonal** over the 5 spatial components `m ∈ −2..2`,
//! each block a dense `P×P` Gram matrix over the `P = C(N,2)` pairs;
//! - the basis strings are dense/high-weight, so most candidate pairs hit
//! the **both-sided sandwich** (12-product) path — the arm the 2026-07-16
//! ledger flagged as remaining headroom.
//!
//! Both specs (eigenmode jumps vs Kossakowski) generate the identical action;
//! the benchmark measures representation cost on one full `pc_step`.

use criterion::{Criterion, criterion_group, criterion_main};
use num::Complex;
use ppvm_lindblad::{JumpInput, LindbladSpec, PcStepConfig, Word, parse_pauli_string};
use std::hint::black_box;

const B: usize = 4096;
const GROW_STEPS: usize = 3;
const DT: f64 = 1e-3;
const N_M: usize = 5; // spatial harmonics m = -2..2

fn pstr(n: usize, sites: &[(usize, char)]) -> String {
let mut s = vec!['I'; n];
for &(q, c) in sites {
s[q] = c;
}
s.into_iter().collect()
}

/// Deterministic pseudo-random 3D unit direction + distance for pair (a,b),
/// so the model is reproducible without an RNG dependency in the bench.
fn hashf(mut x: u64) -> f64 {
x ^= x >> 33;
x = x.wrapping_mul(0xff51afd7ed558ccd);
x ^= x >> 33;
(x >> 11) as f64 / (1u64 << 53) as f64
}

/// Dipolar coupling `b` and unit vector for every pair, from placing spins on
/// a jittered chain (real molecules: `b ∝ 1/r³`, generic directions).
#[allow(clippy::type_complexity)]
fn geometry(n: usize) -> (Vec<(usize, usize)>, Vec<f64>, Vec<[f64; 3]>) {
let pos: Vec<[f64; 3]> = (0..n)
.map(|i| {
[
i as f64 + 0.3 * hashf(i as u64 * 3 + 1),
0.4 * hashf(i as u64 * 3 + 2),
0.4 * hashf(i as u64 * 3 + 3),
]
})
.collect();
let mut pairs = Vec::new();
let mut bmag = Vec::new();
let mut dir = Vec::new();
for a in 0..n {
for b in (a + 1)..n {
let d = [
pos[a][0] - pos[b][0],
pos[a][1] - pos[b][1],
pos[a][2] - pos[b][2],
];
let r = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
pairs.push((a, b));
bmag.push(1.0 / (r * r * r));
dir.push([d[0] / r, d[1] / r, d[2] / r]);
}
}
(pairs, bmag, dir)
}

/// Real rank-2 spherical harmonics (up to normalization) of a unit vector,
/// ordered m = -2,-1,0,1,2 — the spatial factors that make `Γ` rank 5.
fn y2(u: &[f64; 3]) -> [f64; N_M] {
let (x, y, z) = (u[0], u[1], u[2]);
[
x * y,
y * z,
(3.0 * z * z - 1.0) / 2.0,
x * z,
(x * x - y * y) / 2.0,
]
}

/// The 2-local rank-2 tensor operator on pair (a,b) for tensor component
/// `mt` — a representative 4-term Pauli lincomb matching the high-field
/// dressed-tensor forms (T^(2,±2): XX∓YY ± i(XY±YX), etc.). The exact
/// coefficients are immaterial to the cost profile; the term *count* and
/// 2-locality are what matter.
fn tensor_op(n: usize, a: usize, b: usize, mt: usize) -> Vec<(String, Complex<f64>)> {
let (i, j) = (Complex::new(0.0, 1.0), Complex::new(1.0, 0.0));
match mt {
0 | 4 => {
let s = if mt == 0 { i } else { -i }; // ±2 components
vec![
(pstr(n, &[(a, 'X'), (b, 'X')]), j),
(pstr(n, &[(a, 'Y'), (b, 'Y')]), -j),
(pstr(n, &[(a, 'X'), (b, 'Y')]), s),
(pstr(n, &[(a, 'Y'), (b, 'X')]), s),
]
}
1 | 3 => {
let s = if mt == 1 { i } else { -i }; // ±1 components
vec![
(pstr(n, &[(a, 'X'), (b, 'Z')]), j),
(pstr(n, &[(a, 'Y'), (b, 'Z')]), s),
(pstr(n, &[(a, 'Z'), (b, 'X')]), j),
(pstr(n, &[(a, 'Z'), (b, 'Y')]), s),
]
}
_ => vec![
// m = 0
(pstr(n, &[(a, 'X'), (b, 'X')]), j),
(pstr(n, &[(a, 'Y'), (b, 'Y')]), j),
(pstr(n, &[(a, 'Z'), (b, 'Z')]), Complex::new(2.0, 0.0)),
],
}
}

fn hamiltonian_terms(n: usize, pairs: &[(usize, usize)], bmag: &[f64]) -> Vec<(String, f64)> {
let mut h = Vec::new();
for (k, &(a, b)) in pairs.iter().enumerate() {
let jc = 0.1 * bmag[k]; // scalar J-coupling, XX+YY
h.push((pstr(n, &[(a, 'X'), (b, 'X')]), jc));
h.push((pstr(n, &[(a, 'Y'), (b, 'Y')]), jc));
}
h
}

/// Kossakowski ops (`N_M` blocks of `P` pair tensors) and the block-diagonal
/// `K = blockdiag(Γ_m)`, `Γ_m[μν] = Σ_{m'} c_μ^{m'} c_ν^{m'}` with
/// `c_μ^{m'} = b_μ Y_2^{m'}(r̂_μ)` — a rank-5 Gram block, exactly as the drug
/// pickles decompose.
#[allow(clippy::type_complexity)]
fn kossakowski_model(n: usize) -> (Vec<Vec<(String, Complex<f64>)>>, Vec<Vec<Complex<f64>>>) {
let (pairs, bmag, dir) = geometry(n);
let p = pairs.len();
let c: Vec<[f64; N_M]> = (0..p)
.map(|k| {
let y = y2(&dir[k]);
std::array::from_fn(|mp| bmag[k] * y[mp])
})
.collect();
let mut ops = Vec::with_capacity(N_M * p);
for mt in 0..N_M {
for &(a, b) in &pairs {
ops.push(tensor_op(n, a, b, mt));
}
}
let m_ops = N_M * p;
let mut k = vec![vec![Complex::new(0.0, 0.0); m_ops]; m_ops];
for mt in 0..N_M {
let off = mt * p;
for mu in 0..p {
for nu in 0..p {
let g: f64 = (0..N_M).map(|mp| c[mu][mp] * c[nu][mp]).sum();
k[off + mu][off + nu] = Complex::new(g, 0.0);
}
}
}
(ops, k)
}

/// Eigenmode jumps of the block-diagonal `K` (the dense representation the
/// Kossakowski path replaces): per block, `L_ν = √γ_ν Σ_μ V_μν T_μ`.
fn eigenmode_jumps(ops: &[Vec<(String, Complex<f64>)>], k: &[Vec<Complex<f64>>]) -> Vec<JumpInput> {
let m_ops = ops.len();
let p = m_ops / N_M;
let mut jumps = Vec::new();
for mt in 0..N_M {
let off = mt * p;
let block = nalgebra::DMatrix::from_fn(p, p, |a, b| k[off + a][off + b].re);
let eig = nalgebra::SymmetricEigen::new(block);
for nu in 0..p {
let g = eig.eigenvalues[nu];
if g < 1e-12 {
continue;
}
let mut lin = Vec::new();
for mu in 0..p {
let v = eig.eigenvectors[(mu, nu)];
if v.abs() > 1e-14 {
for (s, cc) in &ops[off + mu] {
lin.push((s.clone(), cc * Complex::new(v, 0.0)));
}
}
}
jumps.push(JumpInput {
lincomb: lin,
rate: g,
});
}
}
jumps
}

/// Initial observable: γ-weighted transverse magnetization Σ_i X_i (the coil
/// quadrature), a sparse single-site sum like the drug FID initial operator.
fn observable(n: usize) -> (Vec<Word>, Vec<f64>) {
let mut basis = Vec::new();
let mut coeffs = Vec::new();
for a in 0..n {
basis.push(parse_pauli_string(&pstr(n, &[(a, 'X')]), n).unwrap().0);
coeffs.push(1.0);
}
(basis, coeffs)
}

fn bench_drug(c: &mut Criterion) {
let mut group = c.benchmark_group("pc_step_drug_dipolar");
group.sample_size(10);
for n in [10usize, 20, 32] {
let (pairs, bmag, _) = geometry(n);
let h = hamiltonian_terms(n, &pairs, &bmag);
let (ops, k) = kossakowski_model(n);

let spec_eig = LindbladSpec::new(n, &h, &eigenmode_jumps(&ops, &k)).unwrap();
let mut spec_koss = LindbladSpec::new(n, &h, &[]).unwrap();
spec_koss.add_kossakowski(&ops, &k).unwrap();

let cfg = PcStepConfig {
max_basis: B,
admit_basis: Some(3 * B),
..Default::default()
};
let (mut basis, mut coeffs) = observable(n);
for _ in 0..GROW_STEPS {
spec_koss
.pc_step(&mut basis, &mut coeffs, DT, &[], &cfg)
.unwrap();
}

for (label, spec) in [("eigenmode", &spec_eig), ("kossakowski", &spec_koss)] {
group.bench_function(format!("{label}_n{n}"), |bch| {
bch.iter(|| {
let mut b = basis.clone();
let mut cf = coeffs.clone();
spec.pc_step(&mut b, &mut cf, DT, &[], &cfg).unwrap();
black_box(cf.len())
})
});
}
}
group.finish();
}

criterion_group!(benches, bench_drug);
criterion_main!(benches);
Loading
Loading