From 1ac69b5c41f68654306fd579550bfbc10c580878 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 06:44:26 +0000 Subject: [PATCH] perf(lindblad): skip the second hop and corrector when hop 2 admits nothing With admit_basis set, once the basis has reached max_basis the first leakage admission fills the whole room admit - |basis|, so the second leakage call ran with room = 0 and admitted nothing in every capped step while still evaluating all its candidates, and the corrector then 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. Co-Authored-By: Claude Opus 5.5 --- crates/ppvm-lindblad/src/step.rs | 74 ++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 22 deletions(-) diff --git a/crates/ppvm-lindblad/src/step.rs b/crates/ppvm-lindblad/src/step.rs index 7a168a77e..c988657c3 100644 --- a/crates/ppvm-lindblad/src/step.rs +++ b/crates/ppvm-lindblad/src/step.rs @@ -58,6 +58,14 @@ impl LindbladSpec { /// rank cap) per [`PcStepConfig`]. Exact in `dt` within the working /// basis — the only error is basis truncation. /// + /// When the second hop admits no string (in particular when the first + /// hop already filled the admission room `admit_basis − |basis|`, the + /// usual case once the basis has reached `max_basis`), the corrector + /// would repeat the predictor exactly; the second leakage pass (if + /// `room = 0`) and the corrector exponential are then skipped, with + /// bit-identical results. The step's timings report 0 for skipped + /// phases. + /// /// `protected` words are never dropped. All tuning knobs live in `cfg`. pub fn pc_step( &self, @@ -149,23 +157,35 @@ impl LindbladSpec { let coeffs_predict = self.expm_step(basis, dt, coeffs, drop_tol); p.stop(&mut t.expm1_us); - // 3. Second-hop expansion from the predicted state. After leakage2 - // we no longer need `coeffs_predict`. Extend `coeffs` with zeros for - // any newly-added second-hop strings so it remains a valid input - // (pre-step state) for the corrector. - let p = Phase::start(timed); - let leak2 = self.leakage_with_prune(basis, &coeffs_predict, protected, admit, tau_add)?; - p.stop(&mut t.leakage2_us); - drop(coeffs_predict); + // 3. Second-hop expansion from the predicted state. Extend `coeffs` + // with zeros for any newly-added second-hop strings so it remains a + // valid input (pre-step state) for the corrector. Once the basis is + // full, the first hop usually fills the whole admission room; with + // `room = 0` the second hop can admit nothing, so its leakage pass + // is skipped. + let n_predict = basis.len(); + if admit > n_predict { + let p = Phase::start(timed); + let leak2 = + self.leakage_with_prune(basis, &coeffs_predict, protected, admit, tau_add)?; + p.stop(&mut t.leakage2_us); - let p = Phase::start(timed); - add_leakage_capped(basis, coeffs, leak2, admit); - p.stop(&mut t.expand2_us); + let p = Phase::start(timed); + add_leakage_capped(basis, coeffs, leak2, admit); + p.stop(&mut t.expand2_us); + } // 4. Corrector: redo from pre-step state on the doubly-enlarged basis. - let p = Phase::start(timed); - *coeffs = self.expm_step(basis, dt, coeffs, drop_tol); - p.stop(&mut t.expm2_us); + // If the second hop admitted nothing, the corrector would repeat the + // predictor's computation exactly, so the predicted state is kept. + if basis.len() == n_predict { + *coeffs = coeffs_predict; + } else { + drop(coeffs_predict); + let p = Phase::start(timed); + *coeffs = self.expm_step(basis, dt, coeffs, drop_tol); + p.stop(&mut t.expm2_us); + } // 5. Prune basis entries below `drop_tol` (protected words never dropped). prune_basis(basis, coeffs, drop_tol, protected); @@ -250,16 +270,26 @@ impl LindbladSpec { // across every matvec. let coeffs_predict = mf_expm::expm_apply_orbit_rep(self, basis, sector, dt, coeffs); - // 3. Second-hop leakage from the predicted state. - let mut leak2 = self.leakage_orbit_rep(basis, &coeffs_predict, protected, sector, admit)?; - drop(coeffs_predict); - if tau_add > 0.0 { - leak2.retain(|(_, c)| c.norm() > tau_add); + // 3. Second-hop leakage from the predicted state, skipped when the + // first hop left no admission room (see `pc_step_inner`). + let n_predict = basis.len(); + if admit > n_predict { + let mut leak2 = + self.leakage_orbit_rep(basis, &coeffs_predict, protected, sector, admit)?; + if tau_add > 0.0 { + leak2.retain(|(_, c)| c.norm() > tau_add); + } + add_leakage_capped(basis, coeffs, leak2, admit); } - add_leakage_capped(basis, coeffs, leak2, admit); - // 4. Corrector: redo from the pre-step state (the basis grew). - *coeffs = mf_expm::expm_apply_orbit_rep(self, basis, sector, dt, coeffs); + // 4. Corrector: redo from the pre-step state if the basis grew; + // otherwise it would reproduce the predictor exactly. + if basis.len() == n_predict { + *coeffs = coeffs_predict; + } else { + drop(coeffs_predict); + *coeffs = mf_expm::expm_apply_orbit_rep(self, basis, sector, dt, coeffs); + } // 5. Prune by magnitude, then rank-cap to max_basis. prune_basis(basis, coeffs, drop_tol, protected);