Skip to content
Open
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
fa721aa
feat(lindblad): add compact matrix-free row gather backend
MSRudolph Oct 28, 2026
329681c
perf(lindblad): compile Pauli action parity and phase tables
MSRudolph Oct 28, 2026
a69e2d9
perf(lindblad): pack uniform generator rows without staging
MSRudolph Oct 28, 2026
4281392
perf(lindblad): reuse canonical leakage block buffers
MSRudolph Oct 28, 2026
514eac4
perf(lindblad): select leakage ranks by unsigned magnitude bits
MSRudolph Oct 28, 2026
7205396
perf(lindblad): index candidate payload without duplicate admission s…
MSRudolph Oct 28, 2026
0323d7c
perf(lindblad): compact candidate removals without sorting
MSRudolph Oct 28, 2026
cba835b
perf(lindblad): retain stable candidate slots with a free list
MSRudolph Oct 28, 2026
1f02264
perf(lindblad): scan candidate rates separately and admit without cop…
MSRudolph Oct 28, 2026
5801d64
refactor(lindblad): remove dominated and neutral experiment modes
MSRudolph Oct 28, 2026
66e5122
perf(lindblad): make candidate rank scans contiguous and branch free
MSRudolph Oct 28, 2026
ea3820d
perf(lindblad): count packed rows without norm staging
MSRudolph Oct 28, 2026
6842da6
perf(lindblad): reuse adaptive leakage payload buffers
MSRudolph Oct 28, 2026
6e8fb99
perf(lindblad): store packed transitions in one allocation
MSRudolph Oct 28, 2026
76d53ab
perf(lindblad): accumulate candidate words in the main basis tail
MSRudolph Oct 28, 2026
8ac7f66
perf(lindblad): reserve packed rows from active action bounds
MSRudolph Oct 28, 2026
6c3d8b9
perf(lindblad): reuse packed transitions and exponential work arrays
MSRudolph Oct 28, 2026
155363d
refactor(lindblad): revert neutral branch-free rank scan
MSRudolph Oct 28, 2026
76391ec
perf(lindblad): bound packed buffer capacity and report retained allo…
MSRudolph Oct 28, 2026
ecb41b0
perf(lindblad): build exact packed rows in reusable batches
MSRudolph Oct 28, 2026
dee5e5d
perf(lindblad): share candidate ranking storage with Taylor scratch
MSRudolph Oct 28, 2026
a95a83a
perf(lindblad): reserve shared numeric work from the admission bound
MSRudolph Oct 28, 2026
52ace91
perf(lindblad): bound leakage generation batches independently of pru…
MSRudolph Oct 28, 2026
298eec2
perf(lindblad): reuse numeric scratch for tail admission permutation
MSRudolph Oct 28, 2026
e0148ea
perf(lindblad): raise compact basis lookup load to three quarters
MSRudolph Oct 28, 2026
c7b140d
perf(lindblad): reuse segmented packed rows without full-cache staging
MSRudolph Oct 28, 2026
ec46e3c
perf(lindblad): evolve main coefficients in place with a retained-pre…
MSRudolph Oct 28, 2026
7c12491
perf(lindblad): filter compact basis probes with Swiss-table tags
MSRudolph Oct 28, 2026
ae09327
perf(lindblad): balance generation workspace with two-thousand-source…
MSRudolph Oct 28, 2026
f5a2c71
perf(lindblad): save pre-step coefficients only while correction has …
MSRudolph Oct 28, 2026
5c08851
perf(lindblad): test power-of-two capacities for reusable row segments
MSRudolph Oct 28, 2026
0c5b66a
refactor(lindblad): retain validated packed workspace and remove supe…
MSRudolph Oct 28, 2026
5e4f9b6
refactor(lindblad): remove superseded leakage modes
MSRudolph Oct 29, 2026
5c7a78b
refactor(lindblad): restore owned exponential fallback buffers
MSRudolph Oct 29, 2026
bd985a0
refactor(lindblad): consume final workspace borrow directly
MSRudolph Oct 29, 2026
f406353
perf(lindblad): integrate reusable general-coefficient steps as defaults
MSRudolph Oct 29, 2026
6a2e5c5
perf(lindblad): cache Hamiltonian unit maps without edge coefficients
MSRudolph Oct 29, 2026
8061684
refactor(lindblad): separate candidate proposers from borrowed state …
MSRudolph Oct 29, 2026
a7188ff
feat(lindblad): add bounded-memory candidate proposer strategies
MSRudolph Oct 29, 2026
f394fad
feat(lindblad): make shortlist-and-score the default proposer
MSRudolph Oct 29, 2026
ef754a9
perf(lindblad): release shortlist lookup tables after scoring
MSRudolph Nov 2, 2026
7275239
perf(lindblad): rank-cap magnitudes in the idle exponential work vector
MSRudolph Nov 2, 2026
e0e22b1
perf(lindblad): store Pauli words as two bare bit planes
MSRudolph Nov 4, 2026
0287e15
perf(lindblad): hash generated words once and carry the hash
MSRudolph Nov 4, 2026
0b4109f
perf(lindblad): store each antisymmetric unit pair once in 6-byte ent…
MSRudolph Nov 4, 2026
ecb8eed
allow non-integer growth rate of support per step
MSRudolph Nov 5, 2026
327e061
switch back default proposer, add forward-looking selection for support
MSRudolph Nov 6, 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
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/ppvm-lindblad/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ publish = false
description = "Direct Heisenberg-picture Lindbladian evolution on an adaptive Pauli-string basis."

[dependencies]
bitvec = "1.0.1"
bytemuck = { version = "1", features = ["min_const_generics"] }
fxhash = "0.2.1"
hashbrown = { version = "0.17.1", default-features = false }
ndarray = "0.17"
num = "0.4.3"
ppvm-traits = { version = "0.1.1", path = "../ppvm-traits" }
Expand Down
4 changes: 1 addition & 3 deletions crates/ppvm-lindblad/src/algebra.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
use crate::word::{Chunk, W_CHUNKS, Word};
use fxhash::FxHashMap;
use num::Complex;
use ppvm_traits::PauliWordTrait;

/// Magnitude below which an expanded Pauli coefficient is treated as
/// cancellation noise and dropped.
Expand Down Expand Up @@ -105,7 +104,7 @@ pub(crate) fn comm_product<const C: usize>(h: &Word<C>, p: &Word<C>) -> (Word<C>
/// is `ω · out` with `ω = i^phase`.
#[inline(always)]
pub(crate) fn pauli_mul<const C: usize>(p: &Word<C>, q: &Word<C>) -> (Word<C>, u8) {
let mut out = Word::<C>::new(p.n_qubits());
let mut out = Word::<C>::default();
let mut sign_count: u32 = 0;
let mut imag_count: u32 = 0;
for i in 0..C {
Expand All @@ -120,6 +119,5 @@ pub(crate) fn pauli_mul<const C: usize>(p: &Word<C>, q: &Word<C>) -> (Word<C>, u
out.xbits.data[i] = a ^ c;
out.zbits.data[i] = b ^ d;
}
out.rehash();
(out, ((2 * sign_count + imag_count) & 3) as u8)
}
171 changes: 115 additions & 56 deletions crates/ppvm-lindblad/src/basis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@

//! Basis-level `L*` operators: in-basis generator and off-basis leakage.

use crate::Error;
use crate::candidates::{CandidateBuffer, CandidateProposer, StreamingProposer};
use crate::sector::Sector;
use crate::spec::LindbladSpec;
use crate::truncate::{cap_map_to_room, order_by_desc_mag};
use crate::word::{Word, word_hash};
use crate::{Error, ShortlistScoreProposer};
use fxhash::{FxBuildHasher, FxHashMap, FxHashSet};
use num::Complex;
use rayon::prelude::*;
Expand All @@ -16,6 +17,62 @@ use rayon::prelude::*;
/// into the live map (and the room-cap applied) once per chunk.
const CHUNK_SIZE: usize = 4096;

/// Reusable scratch for adaptive real-coefficient evolution.
///
/// Keep one workspace per trajectory. It retains candidate lookup and source-order
/// storage, generator connections and exponential scratch. Candidate words occupy
/// the main state array's tail; streaming fallback retains its generation buffers.
#[derive(Default)]
pub struct PcStepWorkspace<const C: usize = { crate::W_CHUNKS }> {
pub(crate) proposer: ShortlistScoreProposer<C>,
pub(crate) generator: Option<crate::mf_expm::RealCache>,
pub(crate) taylor: Vec<f64>,
pub(crate) coefficient_auxiliary: Vec<f64>,
}

impl<const C: usize> PcStepWorkspace<C> {
/// Requested vector, shortlist-index and generator capacities retained between
/// steps. Excludes the main state, the protected-word hash set, and metadata
/// owned by a separately supplied candidate proposer.
pub fn allocation_bytes(&self) -> [(&'static str, usize); 12] {
let [terms, order, index] = self.proposer.allocation_bytes();
[
("shortlist_terms", terms),
("source_order", order),
("shortlist_index", index),
("candidate_words", 0),
("candidate_rates", 0),
(
"candidate_state",
self.proposer.streaming.indexed.state.capacity(),
),
(
"candidate_free",
self.proposer.streaming.indexed.free.capacity() * 4,
),
("candidate_rank", 0),
(
"generation",
self.proposer
.streaming
.outputs
.iter()
.map(|v| v.capacity() * std::mem::size_of::<(Word<C>, f64)>())
.sum(),
),
(
"generator",
self.generator.as_ref().map_or(0, |p| p.allocation_bytes()),
),
("numeric_scratch", self.taylor.capacity() * 8),
(
"coefficient_auxiliary",
self.coefficient_auxiliary.capacity() * 8,
),
]
}
}

/// Build a `word → row` map for a basis assumed to contain unique Pauli
/// words; debug-asserts the uniqueness invariant.
pub fn build_basis_index<const C: usize>(basis: &[Word<C>]) -> FxHashMap<Word<C>, u32> {
Expand Down Expand Up @@ -69,61 +126,12 @@ impl<const C: usize> LindbladSpec<C> {
b: coeffs.len(),
});
}
// Hash-only membership tables: storing 8-byte `u64` keys instead
// of 48-byte Words shrinks the in-basis structure ~6×, keeping it
// in L3 (and often L2) at basis sizes where the full-Word version
// would spill to DRAM.
let in_basis: FxHashMap<u64, ()> = basis.iter().map(|w| (word_hash(w), ())).collect();
let protected_set: FxHashMap<u64, ()> =
protected.iter().map(|w| (word_hash(w), ())).collect();

let order = order_by_desc_mag(coeffs);
let room = max_basis.saturating_sub(basis.len());
let n_qubits = self.n_qubits();
let mut merged: FxHashMap<Word<C>, f64> = FxHashMap::default();
for chunk_indices in order.chunks(CHUNK_SIZE) {
let local: Vec<Vec<(Word<C>, f64)>> = chunk_indices
.par_iter()
.map_init(
|| {
(
Vec::<u32>::with_capacity(n_qubits),
Vec::<u32>::with_capacity(128),
FxHashMap::<Word<C>, Complex<f64>>::with_capacity_and_hasher(
128,
FxBuildHasher::default(),
),
)
},
|(s1, s2, lm), &i| {
let p = &basis[i];
let c = coeffs[i];
let terms = self.compute_action_terms(p, s1, s2, lm);
let mut out = Vec::with_capacity(terms.len());
for (w, v) in terms.iter() {
let h = word_hash(w);
if !in_basis.contains_key(&h) && !protected_set.contains_key(&h) {
out.push((*w, c * *v));
}
}
out
},
)
.collect();
for v in local {
for (k, val) in v {
*merged.entry(k).or_insert(0.0) += val;
}
}
cap_map_to_room(&mut merged, room);
}
// Rate-based admission: keep only candidates whose leakage rate
// exceeds `tau_add`. `tau_add = 0` admits everything except exact
// zeros.
Ok(merged
.into_iter()
.filter(|(_, c)| c.abs() > tau_add)
.collect())
let mut words = Vec::new();
let mut numeric = Vec::new();
let mut buffer = CandidateBuffer::separate(basis, coeffs, &mut words, &mut numeric);
let mut proposer = StreamingProposer::default();
proposer.propose(self, &mut buffer, protected, max_basis)?;
Ok(proposer.indexed.selected_rates(&buffer, tau_add).collect())
}

/// Sparse generator matrix in COO form: returns `(row, col, val)`
Expand Down Expand Up @@ -329,3 +337,54 @@ impl<const C: usize> LindbladSpec<C> {
Ok(merged.into_iter().filter(|(_, c)| c.norm() > 0.0).collect())
}
}

#[cfg(test)]
mod tail_tests {
use super::*;
use crate::word_from_codes;

#[test]
fn tail_leakage_preserves_multiple_chunk_caps_and_source_order() {
let n = 7;
let mut h = Vec::new();
for site in 0..n - 1 {
for symbol in ['X', 'Y'] {
let mut term = vec!['I'; n];
term[site] = symbol;
term[site + 1] = symbol;
h.push((term.into_iter().collect(), 0.3));
}
}
let reference = LindbladSpec::<2>::new(n, &h, &[]).unwrap();
let tail = LindbladSpec::<2>::new(n, &h, &[]).unwrap();
let basis: Vec<_> = (0..14000)
.filter(|i| i % 3 != 0)
.map(|i| {
word_from_codes::<2>(
&(0..n)
.map(|q| ((i >> (2 * q)) & 3) as u8)
.collect::<Vec<_>>(),
)
.unwrap()
})
.collect();
let coefficients: Vec<_> = (0..basis.len()).map(|i| (i as f64).sin()).collect();
for room in [0, 100, 2000] {
let expected = reference
.leakage_reference_with_prune(&basis, &coefficients, &[], basis.len() + room, 0.0)
.unwrap();
for threads in [1, 2, 6] {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.unwrap();
let actual = pool
.install(|| {
tail.leakage_with_prune(&basis, &coefficients, &[], basis.len() + room, 0.0)
})
.unwrap();
assert_eq!(actual, expected, "room={room}, threads={threads}");
}
}
}
}
Loading
Loading