Download engine_api.js from cardef/grid-lab: direct link, hf CLI and curl.
- Browser
- Download file 11.1 kB
-
https://huggingface.co/spaces/cardef/grid-lab/resolve/main/engine_api.js
- Command line
-
hf download hf://spaces/cardef/grid-lab/engine_api.js
-
curl -L -o engine_api.js https://huggingface.co/spaces/cardef/grid-lab/resolve/main/engine_api.js
11.1 kB
| // The page's side of the engine: a Web Worker (engine_worker.js) that runs GENCO (ONNX, onnxruntime-web) and the | |
| // repair and fast correction (Pyodide). The contract is in ENGINE_CONTRACT.md; this file is its executable form. | |
| // | |
| // Knobs (JSON, the fields of whatif_app's whatif.scenario.Knobs dataclass): | |
| // domes [[lat, lon, radius_km, amplitude_degC], ...] 0-3 heat domes (the UI offers 3) | |
| // weather [[lat, lon, dT_degC], ...] a real-weather anomaly field (scenario.field_anomaly), [] for none | |
| // heat_load_wind false: r and wind are real data that already include the heat (real days); default true | |
| // r load level, share of the summer peak UI 0.5-1.1 (training 0.6-1.0) | |
| // wind_area 8 numbers, wind availability per area 0-1, the case's area order (grid.json meta.areas) | |
| // solar_area 8 numbers, solar availability per area 0-1 | |
| // gas_m, coal_m fuel price multipliers UI 0.25-5 and 0.25-2 | |
| // branches_out branch indices (0-based, case order) the network must stay connected (the page checks) | |
| // gens_out unit indices (0-based, case order) | |
| // noise null (always: the browser never draws the generator's per-bus load noise) | |
| // dark [] (bus indices of a hurricane's damaged zone; the heat tab never sets it) | |
| // | |
| // Results (one per call; typed arrays are transferred, not copied): | |
| // preview(knobs) -> {cost, max_loading, n_viol, viol_detail, feasible, time_ms, | |
| // line_loading: Float32Array(n_branch), bus_vm: Float32Array(n_bus), gen_p: Float32Array(n_gen), | |
| // v_out?: Int32Array} (v_out optional: buses out of band at the gate) | |
| // check(knobs) -> the same, plus loops; or {status: "cancelled", loops: 0} when a newer request stopped it | |
| // exact(knobs, {start: "check" | "genco" | "flat" | "mid", limit_s}) | |
| // -> {status: "optimal" | "not_certified" | "cancelled", start, cost, iterations, time_ms (solve only), | |
| // build_ms, package_ms, total_ms, feasible, gap_vs_check, line_loading, bus_vm, gen_p} | |
| // (the exact AC-OPF, interior point from the checked point (the check run first if needed; | |
| // its time in check_ms), GENCO's prediction, or a standard start without GENCO; hard limit 20 s | |
| // from the pipeline, 60 s from scratch; arrays may be NaN when not certified) | |
| // cancel() -> stops a running exact solve, which then resolves with {status: "cancelled"}; a preview, check or | |
| // sent while an exact is outstanding stops it the same way (Cancellation in the contract) | |
| // preview results may also carry forward_ms, repair_ms, raw_max_mismatch_mw, raw_cost, raw_line_loading (GENCO's | |
| // own prediction before the repair) | |
| // init(opts) -> {ready, load_ms, threads, can_cancel, interrupt_buffer (SharedArrayBuffer of 2 Int32, or null)} | |
| // progress <- {type: "progress", id, stage: "check" | "solve" | "setup"}: the request's timed work starts (check, | |
| // exact's interior point) or its one-time setup does; passed to check()'s or exact()'s onProgress | |
| // every result also gets rt_ms here: its round trip as the page saw it | |
| // Units: cost $/h; loading as a fraction of the (heat-adjusted) rating, NaN for a branch out of service or without a | |
| // rating; |V| p.u.; Pg MW (0 for units out); time_ms the engine's own time for the call. | |
| export const WORKER_URL = "engine_worker.js"; | |
| export const WORKER_TYPE = "module"; // the worker is an ES module (import onnxruntime-web / pyodide.mjs) | |
| // a private Space signs the page (?__sign=): the worker's requests pass only if the worker's own URL carries the signature | |
| const SIGN = typeof location === "undefined" ? null : new URLSearchParams(location.search).get("__sign"); | |
| const signed = (u) => (SIGN ? `${u}${String(u).includes("?") ? "&" : "?"}__sign=${encodeURIComponent(SIGN)}` : u); | |
| export function normalizeKnobs(k) { | |
| const n = (x, d) => (Number.isFinite(+x) ? +x : d); | |
| return { | |
| domes: (k.domes || []).map((d) => d.map(Number)), | |
| r: n(k.r, 0.9), | |
| wind_area: (k.wind_area || Array(8).fill(0.5)).map(Number), | |
| solar_area: (k.solar_area || Array(8).fill(0.5)).map(Number), | |
| gas_m: n(k.gas_m, 1.0), | |
| coal_m: n(k.coal_m, 1.0), | |
| gens_out: [...new Set((k.gens_out || []).map(Number))].sort((a, b) => a - b), | |
| branches_out: [...new Set((k.branches_out || []).map(Number))].sort((a, b) => a - b), | |
| noise: Array.isArray(k.noise) && k.noise.length === 2 ? k.noise.map(Number) : null, // a stored hurricane draw: [seed, i] | |
| dark: (k.dark || []).map(Number), | |
| weather: (k.weather || []).map((p) => p.map(Number)), | |
| heat_load_wind: k.heat_load_wind !== false, | |
| }; | |
| } | |
| // Throws if a result breaks the contract (used by the page in development and by tools/ui_test.py through the mock). | |
| export function validateResult(r, dims, withLoops = false) { | |
| const need = ["cost", "max_loading", "n_viol", "viol_detail", "feasible", "time_ms"]; | |
| for (const f of need) if (!(f in r)) throw new Error(`engine result lacks ${f}`); | |
| const arr = [["line_loading", dims.nBranch], ["bus_vm", dims.nBus], ["gen_p", dims.nGen]]; | |
| for (const [f, len] of arr) { | |
| if (!(r[f] instanceof Float32Array)) throw new Error(`${f} must be a Float32Array`); | |
| if (r[f].length !== len) throw new Error(`${f} has ${r[f].length} entries, expected ${len}`); | |
| } | |
| if (withLoops && !Number.isInteger(r.loops)) throw new Error("check result lacks integer loops"); | |
| return r; | |
| } | |
| // exact() times (ENGINE_CONTRACT.md): time_ms is the solve only; build_ms the model build and package_ms the package load | |
| // on this call (0 afterwards); total_ms the call's wall time. An engine that reports timing: {solve_s, build_s} instead | |
| // is read the same way. time_ms is never adjusted by the build. | |
| export function normalizeExact(r) { | |
| if (!r || typeof r !== "object") return r; | |
| const t = r.timing || {}; | |
| if (!Number.isFinite(r.build_ms) && Number.isFinite(t.build_s)) r.build_ms = t.build_s * 1000; | |
| if (!Number.isFinite(r.time_ms)) { | |
| if (Number.isFinite(t.solve_s)) r.time_ms = t.solve_s * 1000; | |
| else if (Number.isFinite(r.total_ms)) r.time_ms = r.total_ms; | |
| } | |
| return r; | |
| } | |
| // An exact() result: a status always; when optimal, the cost and the three arrays as for a preview. | |
| export function validateExact(r, dims) { | |
| if (!r || !["optimal", "not_certified", "cancelled"].includes(r.status)) throw new Error("exact result lacks a valid status"); | |
| if (r.status === "optimal") { | |
| for (const f of ["cost", "time_ms"]) if (!Number.isFinite(r[f])) throw new Error(`exact result lacks ${f}`); | |
| for (const [f, len] of [["line_loading", dims.nBranch], ["bus_vm", dims.nBus], ["gen_p", dims.nGen]]) { | |
| if (!(r[f] instanceof Float32Array) || r[f].length !== len) throw new Error(`exact ${f} must be a Float32Array(${len})`); | |
| } | |
| } | |
| return r; | |
| } | |
| export class Engine { | |
| constructor(url = WORKER_URL, type = WORKER_TYPE) { | |
| this.worker = new Worker(signed(url), { type }); | |
| this.n = 0; | |
| this.pending = new Map(); | |
| this.statusListeners = []; | |
| this.ready = false; | |
| this.iv = null; // the interrupt buffer: [cancel flag, cancellation counter] | |
| this.slowOut = 0; // exact and check calls sent and not yet answered | |
| this.worker.onmessage = (e) => this._onMessage(e.data); | |
| this.worker.onerror = (e) => { | |
| const err = new Error(`engine worker error: ${e.message || e}`); | |
| for (const [, p] of this.pending) p.reject(err); | |
| this.pending.clear(); | |
| this.statusListeners.forEach((cb) => cb({ type: "status", stage: "error", detail: err.message })); | |
| }; | |
| } | |
| onStatus(cb) { this.statusListeners.push(cb); } | |
| _onMessage(m) { | |
| if (m.type === "status") { this.statusListeners.forEach((cb) => cb(m)); return; } | |
| const p = this.pending.get(m.id); | |
| if (!p) return; | |
| if (m.type === "progress") { if (p.onProgress) p.onProgress(m); return; } // {type: "progress", id, stage} | |
| this.pending.delete(m.id); | |
| // rt_ms: this request's round trip as the page saw it (queue, setup, the work, messaging); the result's own times | |
| // say which part was the work | |
| if (m.ok && m.result && typeof m.result === "object") m.result.rt_ms = performance.now() - p.t0; | |
| if (m.ok) p.resolve(m.result); else p.reject(new Error(m.error || "engine error")); | |
| } | |
| _call(type, payload = {}, onProgress = null) { | |
| const id = ++this.n; | |
| return new Promise((resolve, reject) => { | |
| this.pending.set(id, { resolve, reject, onProgress, t0: performance.now() }); | |
| this.worker.postMessage({ id, type, ...payload }); | |
| }); | |
| } | |
| async init(opts = {}) { | |
| const r = await this._call("init", { opts }); | |
| this.ready = !!r.ready; | |
| this.iv = r.interrupt_buffer ? new Int32Array(r.interrupt_buffer) : null; | |
| return r; | |
| } | |
| // A newer request stops outstanding slow work (an exact; a check too where the engine supports it): bump the counter | |
| // and raise the flag before posting it. A running solve polls the buffer at safe points; queued work whose gen is | |
| // behind the counter is skipped. Writing the buffer when nothing reads it is harmless (ENGINE_CONTRACT.md). | |
| _interrupt() { if (this.iv && this.slowOut > 0) { Atomics.add(this.iv, 1, 1); Atomics.store(this.iv, 0, 2); } } | |
| _slow(type, payload, onProgress) { | |
| const gen = this.iv ? Atomics.load(this.iv, 1) : 0; | |
| this.slowOut += 1; | |
| const done = () => { this.slowOut -= 1; }; | |
| const p = this._call(type, { ...payload, gen }, onProgress); | |
| p.then(done, done); | |
| return p; | |
| } | |
| preview(knobs) { this._interrupt(); return this._call("preview", { knobs: normalizeKnobs(knobs) }); } | |
| // onProgress(m): called with {stage: "check" | "solve" | "setup"} when the work the result times starts | |
| check(knobs, { onProgress = null } = {}) { this._interrupt(); return this._slow("check", { knobs: normalizeKnobs(knobs) }, onProgress); } | |
| exact(knobs, opts = { start: "genco" }) { | |
| const start = ["check", "genco", "flat", "mid"].includes(opts.start) ? opts.start : "genco"; | |
| const lim = Number.isFinite(opts.limit_s) ? { limit_s: opts.limit_s } : {}; // the solve's hard limit (engine default if absent) | |
| return this._slow("exact", { knobs: normalizeKnobs(knobs), opts: { start, ...lim } }, opts.onProgress || null).then(normalizeExact); | |
| } | |
| // Stop a running exact solve (in practice the from-scratch one): it then resolves with {status: "cancelled", start}. | |
| cancel() { this._interrupt(); return this._call("cancel", {}); } | |
| } | |
| // Transferable buffers of a result (the worker passes these to postMessage). | |
| export function transferables(result) { | |
| const out = []; | |
| const walk = (o) => { | |
| if (!o || typeof o !== "object") return; | |
| if (ArrayBuffer.isView(o)) { out.push(o.buffer); return; } | |
| for (const v of Object.values(o)) walk(v); | |
| }; | |
| walk(result); | |
| return [...new Set(out)]; | |
| } | |