Skip to content

Commit 967eb7b

Browse files
committed
fix(perturbation-sim): address Codex P2 review on #504
P2-1 (Cargo.toml): the optional ndarray dep was a PATH dep — Cargo reads the path manifest at resolution, so a clean checkout without ../../../ndarray failed even with ndarray-simd OFF, breaking the zero-dep default. Switched to the GIT source (helix pattern): it resolves remotely and is only fetched/built when the feature activates it. Verified: default `cargo test` now resolves the git dep without any local sibling — 49 tests pass. P2-2 (cascade.rs): edge_field recorded the all-lines BASE flow for lines that trip in later rounds, underreporting redistribution-driven trips (a line with ~0 base flow that trips on a large redistributed flow showed ~0 footprint). Now capture trip_flow[e] = the current-round flow at the round each line trips (seed/survivors default to base); edge_field for a tripped line uses that overload. New regression test round_trip_edge_field_is_the_overload_not_base asserts a round-≥1 trip's edge_field exceeds its limit. 49 tests pass; clippy -D warnings clean; fmt clean.
1 parent b6f4559 commit 967eb7b

3 files changed

Lines changed: 46 additions & 6 deletions

File tree

crates/perturbation-sim/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/perturbation-sim/Cargo.toml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ ndarray-simd = ["dep:ndarray"]
2020

2121
[dependencies]
2222
# The AdaWorldAPI fork ("The Foundation"), optional + behind `ndarray-simd`.
23-
# Path dep: perturbation-sim lives inside lance-graph, which already path-deps
24-
# this sibling for its core crates, so the fork is expected present. Clean-
25-
# checkout alternative (helix pattern): swap to
26-
# ndarray = { git = "https://github.com/AdaWorldAPI/ndarray.git", branch = "master", optional = true, default-features = false, features = ["std"] }
27-
ndarray = { path = "../../../ndarray", optional = true, default-features = false, features = ["std"] }
23+
# Sourced by GIT (not a local path): an optional *path* dep is read at manifest
24+
# resolution, so a clean checkout without the sibling `../../../ndarray` fails
25+
# even with the feature OFF — breaking the zero-dep default (codex P2 on #504,
26+
# the helix-#460 lesson). A git source resolves remotely and is only fetched/
27+
# built when `ndarray-simd` activates it. Local-dev alternative: swap to
28+
# ndarray = { path = "../../../ndarray", optional = true, default-features = false, features = ["std"] }
29+
ndarray = { git = "https://github.com/AdaWorldAPI/ndarray.git", branch = "master", optional = true, default-features = false, features = ["std"] }
2830

2931
[lib]
3032
name = "perturbation_sim"

crates/perturbation-sim/src/cascade.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ pub fn simulate_outage(
112112
alive[seed_line] = false;
113113
let mut trip_round = vec![-1i32; m];
114114
trip_round[seed_line] = 0;
115+
// Flow each line carried in the round it tripped (the overload that caused
116+
// it). Seeded with the base flow so the seed line — and any survivor —
117+
// defaults sensibly; round-trips overwrite with their current-round flow.
118+
let mut trip_flow = flow_base.clone();
115119

116120
// Assigned on every loop iteration before any break (the loop body always
117121
// runs at least once), so no initializer is needed.
@@ -149,6 +153,7 @@ pub fn simulate_outage(
149153
for e in new_trips {
150154
alive[e] = false;
151155
trip_round[e] = rounds as i32;
156+
trip_flow[e] = flow[e]; // the overloaded flow that tripped it
152157
}
153158
}
154159

@@ -159,7 +164,7 @@ pub fn simulate_outage(
159164
if alive[e] {
160165
(flow[e] - flow_base[e]).abs()
161166
} else {
162-
flow_base[e].abs()
167+
trip_flow[e].abs()
163168
}
164169
})
165170
.collect();
@@ -229,6 +234,38 @@ mod tests {
229234
assert!(r.shape.node_field.iter().any(|&x| x > 1e-9));
230235
}
231236

237+
#[test]
238+
fn round_trip_edge_field_is_the_overload_not_base() {
239+
// A line that trips in a later round tripped because its REDISTRIBUTED
240+
// flow exceeded its limit — so its edge_field must be that overload
241+
// (> limit), not its (smaller) base flow. Guards the Codex P2 fix.
242+
let g = Grid::new(
243+
4,
244+
vec![
245+
Edge::new(0, 1, 1.0, 0.6),
246+
Edge::new(1, 2, 1.0, 0.6),
247+
Edge::new(2, 3, 1.0, 0.6),
248+
Edge::new(3, 0, 1.0, 0.6),
249+
],
250+
);
251+
let p = vec![1.0, 0.0, -1.0, 0.0];
252+
let r = simulate_outage(&g, &p, 0, CascadeConfig::default());
253+
let mut checked = 0;
254+
for e in 0..g.edges.len() {
255+
if r.shape.trip_round[e] >= 1 {
256+
assert!(
257+
r.shape.edge_field[e] > g.edges[e].limit - 1e-9,
258+
"round-{} trip {e}: edge_field {} should be the overload (> limit {})",
259+
r.shape.trip_round[e],
260+
r.shape.edge_field[e],
261+
g.edges[e].limit
262+
);
263+
checked += 1;
264+
}
265+
}
266+
assert!(checked >= 1, "expected at least one round-≥1 trip to check");
267+
}
268+
232269
#[test]
233270
fn islanding_is_flagged() {
234271
// Two triangles + one bridge; tripping the bridge islands the grid.

0 commit comments

Comments
 (0)