Kossakowski-form dissipator - #222
Merged
Merged
Conversation
Add the general GKSL form
`D*(O) = Σ_nm K_nm (A_n† O A_m − ½{A_n† A_m, O})`, alongside the existing
jump-operator form. Each `(n, m)` entry is compiled once into a
`kossakowski::Pair`; Hermitian-conjugate pairs are folded into a single
upper-triangle entry, since both sandwiches produce the same output words
with conjugate phase and the action keeps only the real part.
`PairShape` distinguishes the diagonal `(n,n)` entry from a folded
off-diagonal one and carries the term list only the folded case needs, so
the off-diagonal-only path cannot be reached on a diagonal pair.
`precompute_ldagger_l` generalizes to `precompute_adag_b(a, b)` and moves
to `algebra` next to `PauliTerm`, which the new module shares.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…kloads `kossakowski` compares the pair-matrix path against the equivalent eigenmode-jump decomposition on a superradiant chain; `drug_dipolar` covers the 2-local rank-2 tensors of dipolar relaxation, where each `A_n` carries several Pauli terms. `drug_profile` is the same model wired to a plain timing loop for use under a profiler. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`Lindbladian(..., kossakowski=(ops, K))` forwards an operator family and its pair matrix to the native spec. Mathematically identical to passing the eigenmode jumps of `K = V diag(γ) V†`, but the per-string action cost scales with the number of nonzero `K_nm` entries rather than carrying an extra factor of `M`. Python validates `K` for Hermiticity and positive-semidefiniteness before the call. The core rejects a non-Hermitian `K` too, but the Python check is not redundant: `eigvalsh` reads only one triangle, so without it a non-Hermitian `K` would pass the PSD test on its symmetric part. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`test_kossakowski` checks the pair-matrix action against a dense superoperator and against the equivalent eigenmode-jump decomposition; `test_orbit_dissipative` covers the orbit-representative path on a ring of emitters, including the non-unital Z → I flow across orbits of different sizes. The orbit tests reconstruct a translation-invariant sum as `Σ |orbit(rep)| · c_rep`. Rep coefficients are member coefficients on every orbit since the stabilized-orbit fix in d8313e0, so the previous uniform `|G| ·` factor would over-count orbits with a non-trivial stabilizer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
This was referenced Sep 27, 2026
`LindbladSpec` was hard-capped at 128 qubits by the compile-time
`W_CHUNKS`, so e.g. a 130-qubit Lindbladian failed with
ValueError: LindbladSpec supports n_qubits ≤ 128; got 130
which `wide-pauli-words` (4c07999) had already fixed; the fix did not
make it into the split. Port it onto the current spec/basis/step layout,
including the Kossakowski dissipator added in this PR.
- `ppvm-lindblad` is const-generic in the chunk count: `Word<C>`,
`LindbladSpec<C>`, and every function touching words. Both default to
`C = W_CHUNKS` (128 qubits), so existing Rust callers are unchanged.
`max_qubits::<C>()`, `chunks_for(n)`, `WIDTHS` (128/256/512-qubit chunk
counts, in `Chunk` units so the wasm32 u32 layout keeps working) and
`MAX_SUPPORTED_QUBITS = 512` are exported. `Error::TooManyQubits` now
carries the width's capacity.
- PyO3: `LindbladSpec` holds an `AnySpec { W128, W256, W512 }` picked at
construction and dispatches with `with_spec!`; the symmetry bindings use
`with_width!`. Registers of ≤128 qubits monomorphize to exactly the old
layout. More than 512 qubits is a `ValueError`.
- The orbit path's masked-shift generators cover up to 1024 bits, so all
three widths take the fast canonicalizer.
Tests: `tests/word_width.rs` — capacities, the 130/512-qubit cases, the
width error, bit-identical real-path results for the same problem padded
into 128/256/512-qubit words (Kossakowski and orbit-rep to rounding: their
accumulation order follows word hashes, which depend on the width), and
an orbit-rep step at 130 qubits. Python: `test_word_width.py` builds and
steps 130–512-qubit Lindbladians, rejects 513, and runs the momentum-orbit
step across the 128-qubit boundary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmnGNkxQqpmFmGszAV7dm5
) Stacked on #222 (`split/5-kossakowski`). ## Summary `build_mf_cols` / `build_orbit_rep_cols` returned one `Vec` per column, each reserved at the full L* output count although only in-basis entries are kept. The expm generator cache is now a `BlockCsc`: blocks of up to 4096 columns, each an exactly sized `offsets`/`rows`/`vals` triple (structure of arrays, 12 B per real entry instead of 16 B). `CscOp::dot` keeps the same per-thread column partition, so results are bit-identical. Single commit, `crates/ppvm-lindblad/src/mf_expm.rs` only. ## Measurements `pc_step` on the two-leg XY ladder with a local probe (not part of this PR): L = 41 rungs (N = 82), `max_basis = 2^18`, `admit_basis = 3·2^18`, dt = 0.1, 25 steps, 4 threads, `drop_tol = 0`. Apple M4 MacBook Air (fanless, so wall times are only comparable within a rep). Max RSS from `/usr/bin/time -l`; live-heap peak from a counting global allocator. | mimalloc, 3 interleaved reps | wall (s) | max RSS (MiB) | live-heap peak (MiB) | |---|---|---|---| | base (#222) | 53.7 / 53.5 / 62.5 | 484 / 484 / 483 | 441.7 | | this PR | 46.2 / 48.9 / 60.1 | 501 / 491 / 470 | 275.2 | - The live-heap peak drops by 38% and wall time by 4–14%, with each expm call about 20–30% faster. - On macOS + mimalloc the heap saving shows up only weakly in the process footprint: max RSS moves by −3% to +4%, and peak footprint (`/usr/bin/time -l`) drops by about 3–13%. With the macOS system allocator, RSS is not reproducible (768–1237 MiB for the same binary), so the effect cannot be resolved there. - This has not been measured on Linux/glibc, where fragmentation from the per-column allocations of the old layout may matter more. - The final coefficients are bit-identical to the base in every run. Tests: `cargo test -p ppvm-lindblad` passes (8/8) with this change stacked together with the skip-empty-hop2 PR; not yet run on this branch alone (CI will). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: David Plankensteiner <dplankensteiner@quera.com>
…othing (#230) Stacked on #222 (`split/5-kossakowski`). ## Summary With `admit_basis` set, once the basis has reached `max_basis` the first leakage admission fills the whole room `admit − |basis|`. The second leakage call then ran with room = 0 and admitted nothing in every capped step, while still evaluating all its candidates, and the corrector repeated the predictor exactly on the same basis and input. `pc_step` and `pc_step_orbit_rep` now skip the second leakage pass when room = 0 and reuse the predicted state whenever the second hop admitted no string. Results are bit-identical; skipped phases report 0 in `PcStepTimings`. Default behaviour is otherwise unchanged: capped steps still carry no second-order admission, as before. Single commit, `crates/ppvm-lindblad/src/step.rs` only. ## Measurements `pc_step` on the two-leg XY ladder with a local probe (not part of this PR): L = 41 rungs (N = 82), `max_basis = 2^18`, `admit_basis = 3·2^18`, dt = 0.1, 25 steps, 4 threads, `drop_tol = 0`. Apple M4 MacBook Air (fanless, so wall times are only comparable within a rep). In the base, 21 of the 25 steps are capped and the second hop (leakage2 + expm2) accounts for 53% of the wall time (34.5 of 65.2 s). | | wall (s), per rep | |---|---| | base (#222), mimalloc | 53.7 / 53.5 / 62.5 | | this PR, mimalloc | 26.6 / 28.7 / 33.3 | | base (#222), system malloc | 65.6 / 79.1 | | this PR, system malloc | 32.4 / 38.4 | - The run is 1.9–2.0× faster end-to-end; memory is unchanged (live-heap peak 441.7 MiB in both). - The final coefficients are bit-identical to the base in every run. Tests: `cargo test -p ppvm-lindblad` passes (8/8) with this change stacked together with the blocked-CSC PR; not yet run on this branch alone (CI will). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: David Plankensteiner <david-pl@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the general GKSL form
D*(O) = Σ_nm K_nm (A_n† O A_m − ½{A_n† A_m, O})toppvm-lindblad,alongside the existing jump-operator form, with Python bindings, tests
and benchmarks.
Replaces #179, which was stacked on the pre-split #178 and could not be
reopened usefully: its branch had never been rebased, so its diff against
the split stack was 160 files and reverted work that #180–#182 had since
landed. This branch is the same feature ported onto
split/3and cleanedup; the original commits are preserved on
kossakowski-dissipator.Stacked on #182 — review after that one settles, see the note below.
What's here
7b52cf6bkossakowski.rs,PairShape,add_kossakowskie6266b52dc8c6234Lindbladian(..., kossakowski=(ops, K))Python surfaceb52d22ccConjugate pairs
(n,m)/(m,n)are folded into a single upper-triangleentry — both sandwiches produce the same output words with conjugate
phase and the action keeps only the real part — halving the pair count.
PairShapedistinguishes the diagonal entry from a folded off-diagonalone and carries the term list only the folded case needs, so the
off-diagonal-only path is unreachable on a diagonal pair.
Against the equivalent eigenmode-jump decomposition on a superradiant
chain, the pair-matrix path costs 27.7 ms vs 103 ms at n=10 and 96 ms vs
1.71 s at n=30: the per-string action scales with the number of nonzero
K_nmentries instead of carrying an extra factor ofM.Differences from #179
Ported into the
spec/basis/step/algebralayoutsplit/2andsplit/3introduced, rather than appended tolib.rs:kossakowski.rsmodule.spec.rsgrows 65 lines, not 520.add_kossakowski(147 lines) split intovalidate_k/parse_ops/compile_pair/compile; the ~90-line action arm becamePair::accumulate+ the one-sided and both-sided halves.PairShapeenum replaces anoff_diagbool that drove three separatebranches in the hot path. Benchmarked both shapes: the deltas
(+2.4% / −1.3% / +2.2%, n30 p=0.17) are smaller than the drift on the
eigenmode_*control benchmarks (+3.4% to +6.6%) in the same run, sothis is noise, not a regression.
allows removed, 0 added.split/3had none inppvm-lindblad; threeneedless_range_loopbecame iterator loops andfour
type_complexitybecame two named aliases.precompute_ldagger_lgeneralized toprecompute_adag_b(a, b), movedto
algebra.rsbesidePauliTermand a namedCOEFF_DROP_TOL(was a1e-14literal in five places).let _ = i;, and row-length errors now report which row.support_masklifted out of the pair loop, where it was being rebuiltper iteration.
k_opsfield —LindbladSpecstate that never outlivedthe constructor.
:func:/:meth:docstrings;split/3had already converted those to backticks per AGENTS.md.
One thing to check
test_orbit_dissipative.pyfailed on arrival.d8313e0eonsplit/3("orbit-rep evolution on stabilized orbits") made rep coefficients member
coefficients on every orbit, where stabilized orbits were previously
divided by
|G|; the tests encoded the old behaviour andc_Icame outexactly 6× the asserted value on a 6-site ring.
I rewrote the reconstruction as
Σ |orbit(rep)| · c_repand updated theidentity-bookkeeping expectation to
coeff_I. The new values match theclosed form exactly. If
d8313e0ewas not meant to change thatconvention, the bug is there and not in these tests — worth confirming
while reviewing #182.
Verification
cargo test -p ppvm-lindblad— 8 passedpytest test/lindblad/— 35 passed, 1 skippedcargo clippy -p ppvm-lindblad --all-targets— zero warnings🤖 Generated with Claude Code