Download storm_damage.js from cardef/grid-lab: direct link, hf CLI and curl.
- Browser
- Download file 3.31 kB
-
https://huggingface.co/spaces/cardef/grid-lab/resolve/main/storm_damage.js
- Command line
-
hf download hf://spaces/cardef/grid-lab/storm_damage.js
-
curl -L -o storm_damage.js https://huggingface.co/spaces/cardef/grid-lab/resolve/main/storm_damage.js
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; | |