Skip to content

Commit d4ce286

Browse files
committed
fix(perturbation-sim): degenerate-grid + key-cardinality guards (review #511)
Addresses the open review findings on the merged #511: - examples/calibrate.rs: guard degenerate grids up front. `k = 24.min(m)` is 0 when m==0, so `m / k` panics (divide by zero); `eigenvector(1)` / `m - 1` also break on n<2. Exit cleanly with a message instead. (CodeRabbit Major) - src/hhtl.rs: basin_lambda2 asserts keys.len() == grid.n. A mismatched slice silently produced wrong basin groupings + lambda2; fail fast at the API boundary. (CodeRabbit Major) - .claude/board/TECH_DEBT.md: reflow the wrapped `#507` so it is not parsed as an ATX heading (markdownlint MD018). (CodeRabbit Minor) Already-fixed before merge (no change needed): Codex P2 — `infight` is 4-bit (columns.rs `spec("infight", 4, ...)`, the certified width per the §10 note). Deferred: the TECH_DEBT.md append-only addendum-placement nit (CodeRabbit Major) is a board-hygiene reorganization, left to a focused governance pass rather than churned inside a code-fix PR. Gates: fmt + clippy -D warnings + test green (75 lib tests) on the standalone perturbation-sim crate. https://claude.ai/code/session_016b33swuXE23hKtqxsHu9p1
1 parent c3dddfc commit d4ce286

3 files changed

Lines changed: 19 additions & 4 deletions

File tree

.claude/board/TECH_DEBT.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,9 @@ flags. Separately, #507 left `intervene_counterfactual.rs:133/165` calling the
175175
`8131c480` lives on the unmerged `claude/continue-ndarray-x0Oaw`) — that WARNS, does
176176
not fail (v1 default routes through the canonical mapping per I-LEGACY-API-FEATURE-
177177
GATED); tracked here as a separate latent item, not fixed on this CI branch.
178-
Cross-ref: `.github/workflows/rust-test.yml` (now both jobs at `debuginfo=0`); PR
179-
#507 (`0c6ef02c`); `claude/continue-ndarray-x0Oaw` (the pending ce64-v2 consumer
180-
migration).
178+
Cross-ref: `.github/workflows/rust-test.yml` (now both jobs at `debuginfo=0`);
179+
PR #507 (`0c6ef02c`); `claude/continue-ndarray-x0Oaw` (the pending ce64-v2
180+
consumer migration).
181181

182182
### TD-UNBUNDLE-FROM-1 — `unbundle_from` is NOT the inverse of `bundle_into` (2026-06-07)
183183

crates/perturbation-sim/examples/calibrate.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,19 @@ fn main() {
123123
};
124124
let n = grid.n;
125125
let alive = vec![true; grid.edges.len()];
126+
let m = grid.edges.len();
127+
// Degenerate grids would divide by zero (`k = 24.min(m) = 0` → `m / k`) and
128+
// break the eigenvector / `m - 1` assumptions below. Guard up front.
129+
if n < 2 || m == 0 {
130+
eprintln!(
131+
"calibrate requires a connected grid with at least 2 buses and 1 line (got n={n}, m={m})"
132+
);
133+
std::process::exit(2);
134+
}
126135

127136
// Ground truth = the study's 5-factor contingency matrix on the real core.
128137
let base = symmetric_eigen(&grid.laplacian_of(&alive), n);
129138
let v2 = base.eigenvector(1);
130-
let m = grid.edges.len();
131139
let mut sens: Vec<(usize, f64)> = (0..m)
132140
.map(|e| {
133141
let d = v2[grid.edges[e].from] - v2[grid.edges[e].to];

crates/perturbation-sim/src/hhtl.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ pub fn hhtl_keys(grid: &Grid) -> Vec<HhtlKey> {
106106
/// Per-leaf-basin algebraic connectivity `λ₂` keyed by HHTL address — the topology
107107
/// "value" the key indexes (read once from the spectrum, deterministic).
108108
pub fn basin_lambda2(grid: &Grid, keys: &[HhtlKey]) -> HashMap<HhtlKey, f64> {
109+
assert_eq!(
110+
keys.len(),
111+
grid.n,
112+
"basin_lambda2 requires exactly one HHTL key per grid node (got {} keys for {} nodes)",
113+
keys.len(),
114+
grid.n
115+
);
109116
let mut groups: HashMap<HhtlKey, Vec<usize>> = HashMap::new();
110117
for (n, k) in keys.iter().enumerate() {
111118
groups.entry(*k).or_default().push(n);

0 commit comments

Comments
 (0)