grid-lab / storm_damage.js
cardef's picture
A grid lab in your browser: the static app (whatif_static as of 2026-10-22, the 52 files of SPACE_FILES.txt)
5b7eb93 verified
Raw History Blame Contribute Delete
3.31 kB
// The hurricane damage generator: a seeded draw of what a storm takes out, from the failure probabilities of the
// Storm-to-Steel R3 ensemble (assets/data/storm_ensemble.json, written by tools/export_storm_ensemble.py).
//
// R3 is a model, not a list: given the track variant (shift across the track, wind-speed multiplier), line l fails iff a
// uniform draw falls under its probability p[l], independently of the other lines. The wind farms above their cut-out speed
// stop, and the damaged zone (the buses at the ends of lines with p >= 1%), are fixed by the variant. D44 kept the
// draws that leave the network connected; so does this: it draws again until the network is in one piece, which gives
// the same law as the kept R3 draws, for any variant (including those with no stored what-if).
// A pure module: no DOM, so that node can test it (tools/test_storm_damage.mjs).
import { connected } from "./weather.js";
export const variantOf = (E, shift, alpha) => E.shifts.indexOf(shift) * E.alphas.length + E.alphas.indexOf(alpha);
// An exact early exit. A bus whose every line fails is cut off, and that is what makes most draws unusable; the buses
// likeliest to be cut off (all their lines carry a probability) are tested first, on lines drawn only as needed, so a
// refused draw costs a few random numbers and not a graph search. It changes no result: the graph search still decides.
const GUARD_MIN = 1e-5;
function guardsOf(G, V) {
if (V.guards) return V.guards;
const pos = new Map(V.lines.map((l, i) => [l, i])), inc = Array.from({ length: G.nBus }, () => []);
for (let j = 0; j < G.nBranch; j++) if (G.branch.status[j] === 1) { inc[G.branch.from[j]].push(j); inc[G.branch.to[j]].push(j); }
const gs = [];
for (let b = 0; b < G.nBus; b++) {
if (!inc[b].length || !inc[b].every((j) => pos.has(j))) continue;
const ix = inc[b].map((j) => pos.get(j)), q = ix.reduce((a, i) => a * V.p[i] / 1e4, 1); // the chance that all fail
if (q >= GUARD_MIN) gs.push({ ix, q });
}
V.guards = gs.sort((a, b) => b.q - a.q).map((g) => g.ix);
return V.guards;
}
// One connected draw of variant V (an entry of storm_ensemble.json) with the uniform generator rnd: {branches, tries}
// (tries: the draws made, the refused ones included), or null if none of maxTries leaves the network connected.
export function sampleDamage(G, V, rnd, maxTries = 100000) {
const n = V.lines.length, gs = guardsOf(G, V), st = new Uint8Array(n); // 0 not drawn yet, 1 out, 2 stays
const draw = (i) => (st[i] = rnd() * 1e4 < V.p[i] ? 1 : 2); // p is in units of 1e-4
for (let t = 1; t <= maxTries; t++) {
st.fill(0);
let cut = false;
for (const ix of gs) {
let alive = false;
for (const i of ix) if ((st[i] || draw(i)) === 2) { alive = true; break; }
if (!alive) { cut = true; break; }
}
if (cut) continue;
const out = [];
for (let i = 0; i < n; i++) if ((st[i] || draw(i)) === 1) out.push(V.lines[i]);
if (connected(G, out)) return { branches: out, tries: t };
}
return null;
}
// the generator's seed: the same for the same storm, variant and draw number, different between them
export const damageSeed = (storm, variant, n) => (20261022 + (storm === "beryl" ? 1000003 : 0) + variant * 7919 + n * 104729) >>> 0;