|
| 1 | +// SPDX-FileCopyrightText: 2026 The PPVM Authors |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +//! Per-step cost of the Kossakowski-form dissipator on a *molecular dipolar |
| 5 | +//! relaxation* workload (the ZULF-NMR drug-FID application), which stresses |
| 6 | +//! the path differently from the superradiance chain in `kossakowski.rs`: |
| 7 | +//! |
| 8 | +//! - operators are **2-local rank-2 tensors** with ~4 Pauli terms each (one |
| 9 | +//! channel per site pair, per spatial harmonic `m`), not single-site σ⁻; |
| 10 | +//! - `K` is **block-diagonal** over the 5 spatial components `m ∈ −2..2`, |
| 11 | +//! each block a dense `P×P` Gram matrix over the `P = C(N,2)` pairs; |
| 12 | +//! - the basis strings are dense/high-weight, so most candidate pairs hit |
| 13 | +//! the **both-sided sandwich** (12-product) path — the arm the 2026-07-16 |
| 14 | +//! ledger flagged as remaining headroom. |
| 15 | +//! |
| 16 | +//! Both specs (eigenmode jumps vs Kossakowski) generate the identical action; |
| 17 | +//! the benchmark measures representation cost on one full `pc_step`. |
| 18 | +
|
| 19 | +use criterion::{Criterion, criterion_group, criterion_main}; |
| 20 | +use num::Complex; |
| 21 | +use ppvm_lindblad::{JumpInput, LindbladSpec, PcStepConfig, W_CHUNKS, Word, parse_pauli_string}; |
| 22 | +use std::hint::black_box; |
| 23 | + |
| 24 | +const B: usize = 4096; |
| 25 | +const GROW_STEPS: usize = 3; |
| 26 | +const DT: f64 = 1e-3; |
| 27 | +const N_M: usize = 5; |
| 28 | + |
| 29 | +/// Dipolar-pair geometry: the `(a, b)` site pairs, each pair's coupling |
| 30 | +/// magnitude, and its unit separation vector. |
| 31 | +type Geometry = (Vec<(usize, usize)>, Vec<f64>, Vec<[f64; 3]>); |
| 32 | + |
| 33 | +/// A Kossakowski dissipator as handed to `LindbladSpec::add_kossakowski`: |
| 34 | +/// the operators `A_n` as Pauli lincombs, and the pair matrix `K`. |
| 35 | +type KossakowskiModel = (Vec<Vec<(String, Complex<f64>)>>, Vec<Vec<Complex<f64>>>); |
| 36 | +// spatial harmonics m = -2..2 |
| 37 | + |
| 38 | +fn pstr(n: usize, sites: &[(usize, char)]) -> String { |
| 39 | + let mut s = vec!['I'; n]; |
| 40 | + for &(q, c) in sites { |
| 41 | + s[q] = c; |
| 42 | + } |
| 43 | + s.into_iter().collect() |
| 44 | +} |
| 45 | + |
| 46 | +/// Deterministic pseudo-random 3D unit direction + distance for pair (a,b), |
| 47 | +/// so the model is reproducible without an RNG dependency in the bench. |
| 48 | +fn hashf(mut x: u64) -> f64 { |
| 49 | + x ^= x >> 33; |
| 50 | + x = x.wrapping_mul(0xff51afd7ed558ccd); |
| 51 | + x ^= x >> 33; |
| 52 | + (x >> 11) as f64 / (1u64 << 53) as f64 |
| 53 | +} |
| 54 | + |
| 55 | +/// Dipolar coupling `b` and unit vector for every pair, from placing spins on |
| 56 | +/// a jittered chain (real molecules: `b ∝ 1/r³`, generic directions). |
| 57 | +fn geometry(n: usize) -> Geometry { |
| 58 | + let pos: Vec<[f64; 3]> = (0..n) |
| 59 | + .map(|i| { |
| 60 | + [ |
| 61 | + i as f64 + 0.3 * hashf(i as u64 * 3 + 1), |
| 62 | + 0.4 * hashf(i as u64 * 3 + 2), |
| 63 | + 0.4 * hashf(i as u64 * 3 + 3), |
| 64 | + ] |
| 65 | + }) |
| 66 | + .collect(); |
| 67 | + let mut pairs = Vec::new(); |
| 68 | + let mut bmag = Vec::new(); |
| 69 | + let mut dir = Vec::new(); |
| 70 | + for a in 0..n { |
| 71 | + for b in (a + 1)..n { |
| 72 | + let d = [ |
| 73 | + pos[a][0] - pos[b][0], |
| 74 | + pos[a][1] - pos[b][1], |
| 75 | + pos[a][2] - pos[b][2], |
| 76 | + ]; |
| 77 | + let r = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt(); |
| 78 | + pairs.push((a, b)); |
| 79 | + bmag.push(1.0 / (r * r * r)); |
| 80 | + dir.push([d[0] / r, d[1] / r, d[2] / r]); |
| 81 | + } |
| 82 | + } |
| 83 | + (pairs, bmag, dir) |
| 84 | +} |
| 85 | + |
| 86 | +/// Real rank-2 spherical harmonics (up to normalization) of a unit vector, |
| 87 | +/// ordered m = -2,-1,0,1,2 — the spatial factors that make `Γ` rank 5. |
| 88 | +fn y2(u: &[f64; 3]) -> [f64; N_M] { |
| 89 | + let (x, y, z) = (u[0], u[1], u[2]); |
| 90 | + [ |
| 91 | + x * y, |
| 92 | + y * z, |
| 93 | + (3.0 * z * z - 1.0) / 2.0, |
| 94 | + x * z, |
| 95 | + (x * x - y * y) / 2.0, |
| 96 | + ] |
| 97 | +} |
| 98 | + |
| 99 | +/// The 2-local rank-2 tensor operator on pair (a,b) for tensor component |
| 100 | +/// `mt` — a representative 4-term Pauli lincomb matching the high-field |
| 101 | +/// dressed-tensor forms (T^(2,±2): XX∓YY ± i(XY±YX), etc.). The exact |
| 102 | +/// coefficients are immaterial to the cost profile; the term *count* and |
| 103 | +/// 2-locality are what matter. |
| 104 | +fn tensor_op(n: usize, a: usize, b: usize, mt: usize) -> Vec<(String, Complex<f64>)> { |
| 105 | + let (i, j) = (Complex::new(0.0, 1.0), Complex::new(1.0, 0.0)); |
| 106 | + match mt { |
| 107 | + 0 | 4 => { |
| 108 | + let s = if mt == 0 { i } else { -i }; // ±2 components |
| 109 | + vec![ |
| 110 | + (pstr(n, &[(a, 'X'), (b, 'X')]), j), |
| 111 | + (pstr(n, &[(a, 'Y'), (b, 'Y')]), -j), |
| 112 | + (pstr(n, &[(a, 'X'), (b, 'Y')]), s), |
| 113 | + (pstr(n, &[(a, 'Y'), (b, 'X')]), s), |
| 114 | + ] |
| 115 | + } |
| 116 | + 1 | 3 => { |
| 117 | + let s = if mt == 1 { i } else { -i }; // ±1 components |
| 118 | + vec![ |
| 119 | + (pstr(n, &[(a, 'X'), (b, 'Z')]), j), |
| 120 | + (pstr(n, &[(a, 'Y'), (b, 'Z')]), s), |
| 121 | + (pstr(n, &[(a, 'Z'), (b, 'X')]), j), |
| 122 | + (pstr(n, &[(a, 'Z'), (b, 'Y')]), s), |
| 123 | + ] |
| 124 | + } |
| 125 | + _ => vec![ |
| 126 | + // m = 0 |
| 127 | + (pstr(n, &[(a, 'X'), (b, 'X')]), j), |
| 128 | + (pstr(n, &[(a, 'Y'), (b, 'Y')]), j), |
| 129 | + (pstr(n, &[(a, 'Z'), (b, 'Z')]), Complex::new(2.0, 0.0)), |
| 130 | + ], |
| 131 | + } |
| 132 | +} |
| 133 | + |
| 134 | +fn hamiltonian_terms(n: usize, pairs: &[(usize, usize)], bmag: &[f64]) -> Vec<(String, f64)> { |
| 135 | + let mut h = Vec::new(); |
| 136 | + for (k, &(a, b)) in pairs.iter().enumerate() { |
| 137 | + let jc = 0.1 * bmag[k]; // scalar J-coupling, XX+YY |
| 138 | + h.push((pstr(n, &[(a, 'X'), (b, 'X')]), jc)); |
| 139 | + h.push((pstr(n, &[(a, 'Y'), (b, 'Y')]), jc)); |
| 140 | + } |
| 141 | + h |
| 142 | +} |
| 143 | + |
| 144 | +/// Kossakowski ops (`N_M` blocks of `P` pair tensors) and the block-diagonal |
| 145 | +/// `K = blockdiag(Γ_m)`, `Γ_m[μν] = Σ_{m'} c_μ^{m'} c_ν^{m'}` with |
| 146 | +/// `c_μ^{m'} = b_μ Y_2^{m'}(r̂_μ)` — a rank-5 Gram block, exactly as the drug |
| 147 | +/// pickles decompose. |
| 148 | +fn kossakowski_model(n: usize) -> KossakowskiModel { |
| 149 | + let (pairs, bmag, dir) = geometry(n); |
| 150 | + let p = pairs.len(); |
| 151 | + let c: Vec<[f64; N_M]> = (0..p) |
| 152 | + .map(|k| { |
| 153 | + let y = y2(&dir[k]); |
| 154 | + std::array::from_fn(|mp| bmag[k] * y[mp]) |
| 155 | + }) |
| 156 | + .collect(); |
| 157 | + let mut ops = Vec::with_capacity(N_M * p); |
| 158 | + for mt in 0..N_M { |
| 159 | + for &(a, b) in &pairs { |
| 160 | + ops.push(tensor_op(n, a, b, mt)); |
| 161 | + } |
| 162 | + } |
| 163 | + let m_ops = N_M * p; |
| 164 | + let mut k = vec![vec![Complex::new(0.0, 0.0); m_ops]; m_ops]; |
| 165 | + for mt in 0..N_M { |
| 166 | + let off = mt * p; |
| 167 | + for mu in 0..p { |
| 168 | + for nu in 0..p { |
| 169 | + let g: f64 = (0..N_M).map(|mp| c[mu][mp] * c[nu][mp]).sum(); |
| 170 | + k[off + mu][off + nu] = Complex::new(g, 0.0); |
| 171 | + } |
| 172 | + } |
| 173 | + } |
| 174 | + (ops, k) |
| 175 | +} |
| 176 | + |
| 177 | +/// Eigenmode jumps of the block-diagonal `K` (the dense representation the |
| 178 | +/// Kossakowski path replaces): per block, `L_ν = √γ_ν Σ_μ V_μν T_μ`. |
| 179 | +fn eigenmode_jumps(ops: &[Vec<(String, Complex<f64>)>], k: &[Vec<Complex<f64>>]) -> Vec<JumpInput> { |
| 180 | + let m_ops = ops.len(); |
| 181 | + let p = m_ops / N_M; |
| 182 | + let mut jumps = Vec::new(); |
| 183 | + for mt in 0..N_M { |
| 184 | + let off = mt * p; |
| 185 | + let block = nalgebra::DMatrix::from_fn(p, p, |a, b| k[off + a][off + b].re); |
| 186 | + let eig = nalgebra::SymmetricEigen::new(block); |
| 187 | + for nu in 0..p { |
| 188 | + let g = eig.eigenvalues[nu]; |
| 189 | + if g < 1e-12 { |
| 190 | + continue; |
| 191 | + } |
| 192 | + let mut lin = Vec::new(); |
| 193 | + for mu in 0..p { |
| 194 | + let v = eig.eigenvectors[(mu, nu)]; |
| 195 | + if v.abs() > 1e-14 { |
| 196 | + for (s, cc) in &ops[off + mu] { |
| 197 | + lin.push((s.clone(), cc * Complex::new(v, 0.0))); |
| 198 | + } |
| 199 | + } |
| 200 | + } |
| 201 | + jumps.push(JumpInput { |
| 202 | + lincomb: lin, |
| 203 | + rate: g, |
| 204 | + }); |
| 205 | + } |
| 206 | + } |
| 207 | + jumps |
| 208 | +} |
| 209 | + |
| 210 | +/// Initial observable: γ-weighted transverse magnetization Σ_i X_i (the coil |
| 211 | +/// quadrature), a sparse single-site sum like the drug FID initial operator. |
| 212 | +fn observable(n: usize) -> (Vec<Word>, Vec<f64>) { |
| 213 | + let mut basis = Vec::new(); |
| 214 | + let mut coeffs = Vec::new(); |
| 215 | + for a in 0..n { |
| 216 | + basis.push( |
| 217 | + parse_pauli_string::<W_CHUNKS>(&pstr(n, &[(a, 'X')]), n) |
| 218 | + .unwrap() |
| 219 | + .0, |
| 220 | + ); |
| 221 | + coeffs.push(1.0); |
| 222 | + } |
| 223 | + (basis, coeffs) |
| 224 | +} |
| 225 | + |
| 226 | +fn bench_drug(c: &mut Criterion) { |
| 227 | + let mut group = c.benchmark_group("pc_step_drug_dipolar"); |
| 228 | + group.sample_size(10); |
| 229 | + for n in [10usize, 20, 32] { |
| 230 | + let (pairs, bmag, _) = geometry(n); |
| 231 | + let h = hamiltonian_terms(n, &pairs, &bmag); |
| 232 | + let (ops, k) = kossakowski_model(n); |
| 233 | + |
| 234 | + let spec_eig = <LindbladSpec>::new(n, &h, &eigenmode_jumps(&ops, &k)).unwrap(); |
| 235 | + let mut spec_koss = <LindbladSpec>::new(n, &h, &[]).unwrap(); |
| 236 | + spec_koss.add_kossakowski(&ops, &k).unwrap(); |
| 237 | + |
| 238 | + let cfg = PcStepConfig { |
| 239 | + max_basis: B, |
| 240 | + admit_basis: Some(3 * B), |
| 241 | + ..Default::default() |
| 242 | + }; |
| 243 | + let (mut basis, mut coeffs) = observable(n); |
| 244 | + for _ in 0..GROW_STEPS { |
| 245 | + spec_koss |
| 246 | + .pc_step(&mut basis, &mut coeffs, DT, &[], &cfg) |
| 247 | + .unwrap(); |
| 248 | + } |
| 249 | + |
| 250 | + for (label, spec) in [("eigenmode", &spec_eig), ("kossakowski", &spec_koss)] { |
| 251 | + group.bench_function(format!("{label}_n{n}"), |bch| { |
| 252 | + bch.iter(|| { |
| 253 | + let mut b = basis.clone(); |
| 254 | + let mut cf = coeffs.clone(); |
| 255 | + spec.pc_step(&mut b, &mut cf, DT, &[], &cfg).unwrap(); |
| 256 | + black_box(cf.len()) |
| 257 | + }) |
| 258 | + }); |
| 259 | + } |
| 260 | + } |
| 261 | + group.finish(); |
| 262 | +} |
| 263 | + |
| 264 | +criterion_group!(benches, bench_drug); |
| 265 | +criterion_main!(benches); |
0 commit comments