// 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;