@tellegen/engine 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -6
- package/dist/generated/contracts.d.ts +35 -17
- package/dist/generated/contracts.d.ts.map +1 -1
- package/dist/generated/contracts.js +3 -3
- package/dist/generated/study-contracts.d.ts +426 -0
- package/dist/generated/study-contracts.d.ts.map +1 -0
- package/dist/generated/study-contracts.js +4 -0
- package/dist/generated/study-contracts.js.map +1 -0
- package/dist/generated/study-schema.d.ts +2 -0
- package/dist/generated/study-schema.d.ts.map +1 -0
- package/dist/generated/study-schema.js +3 -0
- package/dist/generated/study-schema.js.map +1 -0
- package/dist/host.d.ts +12 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +71 -17
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +360 -96
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +407 -89
- package/dist/index.js.map +1 -1
- package/dist/input-limit.d.ts +8 -0
- package/dist/input-limit.d.ts.map +1 -0
- package/dist/input-limit.js +14 -0
- package/dist/input-limit.js.map +1 -0
- package/dist/module.d.ts +1 -0
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js.map +1 -1
- package/dist/protocol.d.ts +105 -20
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +67 -18
- package/dist/protocol.js.map +1 -1
- package/dist/study-document.d.ts +46 -0
- package/dist/study-document.d.ts.map +1 -0
- package/dist/study-document.js +183 -0
- package/dist/study-document.js.map +1 -0
- package/dist/study-document.test.d.ts +2 -0
- package/dist/study-document.test.d.ts.map +1 -0
- package/dist/study-document.test.js +119 -0
- package/dist/study-document.test.js.map +1 -0
- package/dist/wasm-pkg/tellegen.d.ts +191 -55
- package/dist/wasm-pkg/tellegen.js +754 -97
- package/dist/wasm-pkg/tellegen_bg.wasm +0 -0
- package/dist/wasm-pkg/tellegen_bg.wasm.d.ts +30 -6
- package/dist/worker.js +27 -3
- package/dist/worker.js.map +1 -1
- package/package.json +26 -4
package/dist/index.js
CHANGED
|
@@ -2,12 +2,31 @@
|
|
|
2
2
|
* browser allows one — a solve never blocks the page — and on the calling
|
|
3
3
|
* thread otherwise (see host.ts). Nothing downloads until the first engine
|
|
4
4
|
* call; dropped files are parsed locally and never leave the machine. */
|
|
5
|
-
import { engineHost } from "./host.js";
|
|
5
|
+
import { engineHost, isolatedEngineHost } from "./host.js";
|
|
6
6
|
import { isPermanentWasmLoadFailure } from "./errors.js";
|
|
7
|
+
import { assertEngineInputBytes, assertEngineInputLength, } from "./input-limit.js";
|
|
7
8
|
export { CONTRACT_SOURCE_SHA256, CONTRACT_VERSION, FORMULATION_IDS, SOLVE_STATUSES, } from "./generated/contracts.js";
|
|
9
|
+
export { MAX_ENGINE_INPUT_BYTES } from "./input-limit.js";
|
|
10
|
+
/** Compact display spelling for a structured PowerIO finding. */
|
|
11
|
+
export function formatPowerIoDiagnostic(diagnostic) {
|
|
12
|
+
return `${diagnostic.code}: ${diagnostic.message}`;
|
|
13
|
+
}
|
|
8
14
|
export async function preloadEngine() {
|
|
9
15
|
await engineHost().call({ op: "preload" });
|
|
10
16
|
}
|
|
17
|
+
/** Classify JSON bytes through powerio's Rust routing table. A stored module
|
|
18
|
+
* passes the strict module reader so the payload family is authoritative. */
|
|
19
|
+
export async function classifyJson(bytes) {
|
|
20
|
+
assertEngineInputBytes(bytes);
|
|
21
|
+
return JSON.parse(expectText(await engineHost().call({ op: "classify_json", bytes })));
|
|
22
|
+
}
|
|
23
|
+
/** Classify and ingest one dropped JSON document in the same engine request.
|
|
24
|
+
* This avoids parsing a recognized package once to classify it and again to
|
|
25
|
+
* consume it. */
|
|
26
|
+
export async function ingestJsonDrop(bytes) {
|
|
27
|
+
assertEngineInputBytes(bytes);
|
|
28
|
+
return JSON.parse(expectText(await engineHost().call({ op: "ingest_json_drop", bytes })));
|
|
29
|
+
}
|
|
11
30
|
/** Asserts a request that must carry a payload actually did. */
|
|
12
31
|
function expectText(value) {
|
|
13
32
|
if (value === null)
|
|
@@ -17,24 +36,45 @@ function expectText(value) {
|
|
|
17
36
|
/** powerio format token from a file name; null for non-case files. */
|
|
18
37
|
export function formatOf(name) {
|
|
19
38
|
const ext = name.split(".").pop()?.toLowerCase();
|
|
20
|
-
|
|
39
|
+
switch (ext) {
|
|
40
|
+
case "m":
|
|
41
|
+
case "raw":
|
|
42
|
+
case "aux":
|
|
43
|
+
case "pwb":
|
|
44
|
+
return ext;
|
|
45
|
+
case "epc":
|
|
46
|
+
return "pslf";
|
|
47
|
+
default:
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
21
50
|
}
|
|
22
|
-
|
|
23
|
-
|
|
51
|
+
/** Parse a balanced case file for viewing. Takes the upload's bytes rather than
|
|
52
|
+
* decoded text: powerio refuses a text format whose bytes are not UTF-8, where
|
|
53
|
+
* `File.text()` would have replaced each offending byte with U+FFFD. */
|
|
54
|
+
export async function ingestCase(bytes, format) {
|
|
55
|
+
assertEngineInputBytes(bytes);
|
|
56
|
+
return JSON.parse(expectText(await engineHost().call({ op: "ingest_case", bytes, format })));
|
|
24
57
|
}
|
|
25
58
|
/** Parse a multiconductor distribution case for viewing. `format` is a
|
|
26
59
|
* distribution reader token (`dss`, `bmopf`, `pmd`) or `pio` for a `.pio.json`
|
|
27
60
|
* package carrying a multiconductor payload. Rejects on malformed input or a
|
|
28
61
|
* balanced package (which the study-restore path handles). */
|
|
29
62
|
export async function ingestDistCase(text, format) {
|
|
63
|
+
assertEngineInputLength(text.length);
|
|
30
64
|
return JSON.parse(expectText(await engineHost().call({ op: "ingest_dist_case", text, format })));
|
|
31
65
|
}
|
|
66
|
+
/** Byte entry point for a dropped distribution case or package. */
|
|
67
|
+
export async function ingestDistCaseBytes(bytes, format) {
|
|
68
|
+
assertEngineInputBytes(bytes);
|
|
69
|
+
return JSON.parse(expectText(await engineHost().call({ op: "ingest_dist_case_bytes", bytes, format })));
|
|
70
|
+
}
|
|
32
71
|
/** True for binary display files (PowerWorld .pwd), read via parseDisplay.
|
|
33
72
|
* Kept separate from formatOf: a .pwd is display data, not a case format. */
|
|
34
73
|
export function isDisplayFile(name) {
|
|
35
74
|
return name.split(".").pop()?.toLowerCase() === "pwd";
|
|
36
75
|
}
|
|
37
76
|
export async function parseDisplay(bytes) {
|
|
77
|
+
assertEngineInputBytes(bytes);
|
|
38
78
|
return JSON.parse(expectText(await engineHost().call({ op: "parse_display", bytes, format: "pwd" })));
|
|
39
79
|
}
|
|
40
80
|
/** Parse a geographic sidecar (buscoords CSV, aliased CSV/JSON records,
|
|
@@ -42,53 +82,213 @@ export async function parseDisplay(bytes) {
|
|
|
42
82
|
* is the file name (picks CSV against JSON; "" sniffs). Rejects input with
|
|
43
83
|
* no usable coordinates. */
|
|
44
84
|
export async function parseGeo(bytes, hint) {
|
|
85
|
+
assertEngineInputBytes(bytes);
|
|
45
86
|
return JSON.parse(expectText(await engineHost().call({ op: "parse_geo", bytes, hint })));
|
|
46
87
|
}
|
|
47
88
|
/** Apply a parsed layer onto a case network (matching by uid, external id,
|
|
48
89
|
* name, and the unordered branch endpoint pair). Rejects when nothing
|
|
49
90
|
* matched. */
|
|
50
|
-
export async function applyGeo(
|
|
91
|
+
export async function applyGeo(moduleJson, layer) {
|
|
92
|
+
assertEngineInputLength(moduleJson.length);
|
|
51
93
|
return JSON.parse(expectText(await engineHost().call({
|
|
52
94
|
op: "apply_geo",
|
|
53
|
-
|
|
95
|
+
module_json: moduleJson,
|
|
54
96
|
layer,
|
|
55
97
|
})));
|
|
56
98
|
}
|
|
99
|
+
/** Apply geographic or drawing coordinates to a multiconductor case. */
|
|
100
|
+
export async function applyMcGeo(moduleJson, layer) {
|
|
101
|
+
assertEngineInputLength(moduleJson.length);
|
|
102
|
+
const result = JSON.parse(expectText(await engineHost().call({
|
|
103
|
+
op: "apply_geo",
|
|
104
|
+
module_json: moduleJson,
|
|
105
|
+
layer,
|
|
106
|
+
})));
|
|
107
|
+
if (result.model !== "multiconductor")
|
|
108
|
+
throw new Error("Expected a multiconductor case");
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
57
111
|
/** Stamp a computed layout (bus id => [lon, lat]) onto a case network with
|
|
58
112
|
* its provenance, so saves and exports carry it. */
|
|
59
|
-
export async function applyLayout(
|
|
113
|
+
export async function applyLayout(moduleJson, coords, kind) {
|
|
114
|
+
assertEngineInputLength(moduleJson.length);
|
|
60
115
|
return JSON.parse(expectText(await engineHost().call({
|
|
61
116
|
op: "apply_layout",
|
|
62
|
-
|
|
117
|
+
module_json: moduleJson,
|
|
63
118
|
coords: JSON.stringify(coords),
|
|
64
119
|
kind,
|
|
65
120
|
})));
|
|
66
121
|
}
|
|
67
122
|
/** A case's coordinates as a canonical `.geo.json` document. Rejects when the
|
|
68
123
|
* case carries none. */
|
|
69
|
-
export async function extractGeo(
|
|
70
|
-
|
|
124
|
+
export async function extractGeo(moduleJson) {
|
|
125
|
+
assertEngineInputLength(moduleJson.length);
|
|
126
|
+
return expectText(await engineHost().call({ op: "extract_geo", module_json: moduleJson }));
|
|
71
127
|
}
|
|
72
128
|
/** Fill case coordinates from a PowerWorld `.pwd` sibling: substation symbols
|
|
73
129
|
* project to approximate longitude/latitude and join onto buses through the
|
|
74
130
|
* `SubNum` extras key. Rejects when no bus joined. */
|
|
75
|
-
export async function applyDisplayGeo(
|
|
131
|
+
export async function applyDisplayGeo(moduleJson, bytes) {
|
|
132
|
+
assertEngineInputLength(moduleJson.length);
|
|
133
|
+
assertEngineInputBytes(bytes);
|
|
76
134
|
return JSON.parse(expectText(await engineHost().call({
|
|
77
135
|
op: "apply_display_geo",
|
|
78
|
-
|
|
136
|
+
module_json: moduleJson,
|
|
79
137
|
bytes,
|
|
80
138
|
})));
|
|
81
139
|
}
|
|
82
140
|
export async function capabilities() {
|
|
83
141
|
return JSON.parse(expectText(await engineHost().call({ op: "capabilities" })));
|
|
84
142
|
}
|
|
85
|
-
export async function
|
|
143
|
+
export async function solveModule(moduleJson, request = {}) {
|
|
86
144
|
return JSON.parse(expectText(await engineHost().call({
|
|
87
|
-
op: "
|
|
88
|
-
|
|
145
|
+
op: "solve_module",
|
|
146
|
+
module_json: moduleJson,
|
|
89
147
|
request: JSON.stringify(request),
|
|
90
148
|
})));
|
|
91
149
|
}
|
|
150
|
+
/** A live fixed-point current-injection solver retained in the shared engine
|
|
151
|
+
* worker. Load edits reuse its sparse LU and previous converged phasors. */
|
|
152
|
+
export class BrowserMcPfSession {
|
|
153
|
+
host;
|
|
154
|
+
handle;
|
|
155
|
+
constructor(host, handle) {
|
|
156
|
+
this.host = host;
|
|
157
|
+
this.handle = handle;
|
|
158
|
+
}
|
|
159
|
+
async result() {
|
|
160
|
+
return JSON.parse(expectText(await this.host.call({
|
|
161
|
+
op: "mc_pf_session_result",
|
|
162
|
+
session: this.handle,
|
|
163
|
+
})));
|
|
164
|
+
}
|
|
165
|
+
async loadBranches() {
|
|
166
|
+
return JSON.parse(expectText(await this.host.call({
|
|
167
|
+
op: "mc_pf_session_load_branches",
|
|
168
|
+
session: this.handle,
|
|
169
|
+
})));
|
|
170
|
+
}
|
|
171
|
+
async replaceLoadPowers(edits) {
|
|
172
|
+
return JSON.parse(expectText(await this.host.call({
|
|
173
|
+
op: "mc_pf_session_replace_load_powers",
|
|
174
|
+
session: this.handle,
|
|
175
|
+
edits: JSON.stringify(edits),
|
|
176
|
+
})));
|
|
177
|
+
}
|
|
178
|
+
async inputModule() {
|
|
179
|
+
return expectText(await this.host.call({
|
|
180
|
+
op: "mc_pf_session_input_module",
|
|
181
|
+
session: this.handle,
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
async snapshot(studyId, studyTitle) {
|
|
185
|
+
return JSON.parse(expectText(await this.host.call({
|
|
186
|
+
op: "mc_pf_session_snapshot",
|
|
187
|
+
session: this.handle,
|
|
188
|
+
study_id: studyId,
|
|
189
|
+
study_title: studyTitle,
|
|
190
|
+
})));
|
|
191
|
+
}
|
|
192
|
+
free() {
|
|
193
|
+
void this.host
|
|
194
|
+
.call({ op: "mc_pf_session_free", session: this.handle })
|
|
195
|
+
.catch(() => { });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
let mcPfSessionSeq = 0;
|
|
199
|
+
/** Parse, prepare, factor, and solve a persistent multiconductor PF session. */
|
|
200
|
+
export async function createMcPfSession(moduleJson, options = {}, signal) {
|
|
201
|
+
assertEngineInputLength(moduleJson.length);
|
|
202
|
+
signal?.throwIfAborted();
|
|
203
|
+
const host = engineHost();
|
|
204
|
+
const handle = ++mcPfSessionSeq;
|
|
205
|
+
await host.call({
|
|
206
|
+
op: "mc_pf_session_new",
|
|
207
|
+
session: handle,
|
|
208
|
+
module_json: moduleJson,
|
|
209
|
+
options: JSON.stringify(options),
|
|
210
|
+
});
|
|
211
|
+
const session = new BrowserMcPfSession(host, handle);
|
|
212
|
+
if (signal?.aborted) {
|
|
213
|
+
session.free();
|
|
214
|
+
signal.throwIfAborted();
|
|
215
|
+
}
|
|
216
|
+
return session;
|
|
217
|
+
}
|
|
218
|
+
/** Parse and solve a raw BMOPF multiconductor case in the wasm module. */
|
|
219
|
+
export async function solveMcBmopf(text, options = {}) {
|
|
220
|
+
assertEngineInputLength(text.length);
|
|
221
|
+
return JSON.parse(expectText(await engineHost().call({
|
|
222
|
+
op: "solve_mc_bmopf",
|
|
223
|
+
text,
|
|
224
|
+
options: JSON.stringify(options),
|
|
225
|
+
})));
|
|
226
|
+
}
|
|
227
|
+
/** Solve a stored PowerIO multiconductor module through the same PF engine. */
|
|
228
|
+
export async function solveMcModule(moduleJson, options = {}, signal) {
|
|
229
|
+
assertEngineInputLength(moduleJson.length);
|
|
230
|
+
signal?.throwIfAborted();
|
|
231
|
+
const host = isolatedEngineHost();
|
|
232
|
+
const abort = () => host.cancel?.(new Error("Multiconductor calculation cancelled"));
|
|
233
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
234
|
+
try {
|
|
235
|
+
const result = await host.call({
|
|
236
|
+
op: "solve_mc_module",
|
|
237
|
+
module_json: moduleJson,
|
|
238
|
+
options: JSON.stringify(options),
|
|
239
|
+
});
|
|
240
|
+
signal?.throwIfAborted();
|
|
241
|
+
return JSON.parse(expectText(result));
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
signal?.removeEventListener("abort", abort);
|
|
245
|
+
host.cancel?.(new Error("Multiconductor calculation finished"));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** Solve a supported multiconductor Study input and return a replayable
|
|
249
|
+
* snapshot containing the typed input, PowerIO solution, and rich result. */
|
|
250
|
+
export async function solveMcStudy(moduleJson, studyId, studyTitle, options = {}, signal) {
|
|
251
|
+
assertEngineInputLength(moduleJson.length);
|
|
252
|
+
signal?.throwIfAborted();
|
|
253
|
+
const host = isolatedEngineHost();
|
|
254
|
+
const abort = () => host.cancel?.(new Error("Multiconductor calculation cancelled"));
|
|
255
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
256
|
+
try {
|
|
257
|
+
const result = await host.call({
|
|
258
|
+
op: "solve_mc_study",
|
|
259
|
+
module_json: moduleJson,
|
|
260
|
+
study_id: studyId,
|
|
261
|
+
study_title: studyTitle,
|
|
262
|
+
options: JSON.stringify(options),
|
|
263
|
+
});
|
|
264
|
+
signal?.throwIfAborted();
|
|
265
|
+
return JSON.parse(expectText(result));
|
|
266
|
+
}
|
|
267
|
+
finally {
|
|
268
|
+
signal?.removeEventListener("abort", abort);
|
|
269
|
+
host.cancel?.(new Error("Multiconductor calculation finished"));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/** Validate and canonicalize a saved multiconductor Study snapshot without
|
|
273
|
+
* running the solver again. */
|
|
274
|
+
export async function replayMcStudy(snapshotJson) {
|
|
275
|
+
assertEngineInputLength(snapshotJson.length);
|
|
276
|
+
return JSON.parse(expectText(await engineHost().call({
|
|
277
|
+
op: "replay_mc_study",
|
|
278
|
+
snapshot: snapshotJson,
|
|
279
|
+
})));
|
|
280
|
+
}
|
|
281
|
+
/** Update both saved modules with matching positions while retaining the solved values. */
|
|
282
|
+
export async function applyMcStudyGeo(snapshot, layer) {
|
|
283
|
+
const text = JSON.stringify(snapshot);
|
|
284
|
+
assertEngineInputLength(text.length);
|
|
285
|
+
assertEngineInputLength(layer.length);
|
|
286
|
+
return JSON.parse(expectText(await engineHost().call({
|
|
287
|
+
op: "apply_mc_study_geo",
|
|
288
|
+
snapshot: text,
|
|
289
|
+
layer,
|
|
290
|
+
})));
|
|
291
|
+
}
|
|
92
292
|
export { errorText } from "./errors.js";
|
|
93
293
|
/** True when the engine wasm module has failed to load in a way it can never
|
|
94
294
|
* recover from in this browser. Nothing solves client side when this is true;
|
|
@@ -96,10 +296,13 @@ export { errorText } from "./errors.js";
|
|
|
96
296
|
export function isPermanentEngineFailure(message) {
|
|
97
297
|
return isPermanentWasmLoadFailure(message);
|
|
98
298
|
}
|
|
99
|
-
/**
|
|
100
|
-
*
|
|
299
|
+
/** A safely representable all-digit key uses the numeric wire form. Larger IDs
|
|
300
|
+
* remain strings so serde can parse the exact integer without JS rounding. */
|
|
101
301
|
function toElementKey(key) {
|
|
102
|
-
|
|
302
|
+
if (!/^\d+$/.test(key))
|
|
303
|
+
return key;
|
|
304
|
+
const numeric = Number(key);
|
|
305
|
+
return Number.isSafeInteger(numeric) ? numeric : key;
|
|
103
306
|
}
|
|
104
307
|
/** A `NetworkEdit[]` for the wasm Study, dropping zero deltas so an unchanged
|
|
105
308
|
* element is never sent. */
|
|
@@ -122,7 +325,7 @@ function toEdits(deltas, rates) {
|
|
|
122
325
|
* ∂(price, active) / ∂(demand, active) for a bus target, or ∂(price, active) /
|
|
123
326
|
* ∂(line limit) for a branch target. The *formulation* is no longer fixed here — it is
|
|
124
327
|
* a parameter threaded from the UI's selector through `createStudy` (every formulation the
|
|
125
|
-
* full wasm build carries returns
|
|
328
|
+
* full wasm build carries returns nodal marginal values, so these columns apply to all of them). The
|
|
126
329
|
* operand/parameters stay centralized so `createStudy` and the Study's `commit`/`preview`
|
|
127
330
|
* requests read one source. */
|
|
128
331
|
const STUDY_CAPABILITY = {
|
|
@@ -137,6 +340,7 @@ const STUDY_CAPABILITY = {
|
|
|
137
340
|
* is the menu order; `dcopf` is first and is the default. */
|
|
138
341
|
export const FORMULATIONS = [
|
|
139
342
|
{ id: "dcopf", label: "DC OPF", hint: "DC optimal power flow (the default)" },
|
|
343
|
+
{ id: "acpf", label: "AC power flow", hint: "Balanced AC power flow" },
|
|
140
344
|
{
|
|
141
345
|
id: "socwr",
|
|
142
346
|
label: "SOCWR",
|
|
@@ -149,12 +353,12 @@ export const FORMULATIONS = [
|
|
|
149
353
|
disabled: true,
|
|
150
354
|
},
|
|
151
355
|
];
|
|
152
|
-
/**
|
|
356
|
+
/** DC OPF is the default calculation. */
|
|
153
357
|
export const DEFAULT_FORMULATION = "dcopf";
|
|
154
|
-
/** The `Operand[]` JSON `Study.preview` watches (the
|
|
358
|
+
/** The `Operand[]` JSON `Study.preview` watches (the active nodal value column). */
|
|
155
359
|
const PREVIEW_OPERANDS_JSON = JSON.stringify([STUDY_CAPABILITY.operand]);
|
|
156
|
-
/** The `SensRequest[]` JSON for `Study.commit`: the
|
|
157
|
-
* index, or the
|
|
360
|
+
/** The `SensRequest[]` JSON for `Study.commit`: the nodal-value/demand column at a dense bus
|
|
361
|
+
* index, or the nodal-value/rating column at a dense branch index, or `[]` when there is no
|
|
158
362
|
* target. NOTE: `SensRequest.indices` are **dense positional** indices into the target's
|
|
159
363
|
* axis (0-based), *not* external ids — `BrowserStudy` translates the selected external id
|
|
160
364
|
* to its dense index before calling this. */
|
|
@@ -171,7 +375,7 @@ function sensitivitiesJson(target) {
|
|
|
171
375
|
},
|
|
172
376
|
]);
|
|
173
377
|
}
|
|
174
|
-
/** Extract the
|
|
378
|
+
/** Extract the nodal-value/parameter column from the first requested `SensitivityMatrix` into
|
|
175
379
|
* the `SensitivityColumn` the map and legend consume, the same shape the server serves.
|
|
176
380
|
* Rows are Price operands per bus; the single column is the selected parameter, so the
|
|
177
381
|
* column is `values[r][0]` keyed by each row's source bus id, and the source element is
|
|
@@ -186,7 +390,7 @@ function sensitivityColumn(caseId, matrices) {
|
|
|
186
390
|
const values = m.rows
|
|
187
391
|
.map((row, r) => ({ bus: row.element.Bus, value: m.values[r]?.[0] ?? 0 }))
|
|
188
392
|
.filter((v) => v.bus !== undefined);
|
|
189
|
-
const shared = { case: caseId, operand: "
|
|
393
|
+
const shared = { case: caseId, operand: "price", units: m.units, values };
|
|
190
394
|
if (el.Bus !== undefined) {
|
|
191
395
|
return { ...shared, parameter: "d", bus: el.Bus };
|
|
192
396
|
}
|
|
@@ -197,8 +401,9 @@ function sensitivityColumn(caseId, matrices) {
|
|
|
197
401
|
}
|
|
198
402
|
function solveResponseToSolution(out) {
|
|
199
403
|
return {
|
|
200
|
-
objective: out.objective ??
|
|
201
|
-
|
|
404
|
+
objective: out.objective ?? null,
|
|
405
|
+
prices: (out.lmp ?? []).map((e) => ({ bus: e.bus, value: e.value })),
|
|
406
|
+
vm: out.vm ?? undefined,
|
|
202
407
|
va: out.va ?? [],
|
|
203
408
|
w: out.w ?? [],
|
|
204
409
|
flows: (out.flows ?? []).map((f) => ({
|
|
@@ -206,11 +411,15 @@ function solveResponseToSolution(out) {
|
|
|
206
411
|
mw: f.pf,
|
|
207
412
|
loading: f.loading,
|
|
208
413
|
})),
|
|
209
|
-
dispatch: (out.dispatch ?? []).map((d) => ({
|
|
414
|
+
dispatch: (out.dispatch ?? []).map((d) => ({
|
|
415
|
+
gen: d.gen,
|
|
416
|
+
bus: d.bus,
|
|
417
|
+
mw: d.pg,
|
|
418
|
+
})),
|
|
210
419
|
};
|
|
211
420
|
}
|
|
212
|
-
/**
|
|
213
|
-
* parsed and the model built when the Study is created; `commit` solves exactly
|
|
421
|
+
/** Browser transport for the reactive demand drag. The retained PowerIO module
|
|
422
|
+
* is parsed and the model built when the Study is created; `commit` solves exactly
|
|
214
423
|
* at the UI's absolute demand delta state and `preview` returns a first-order
|
|
215
424
|
* linearization toward an absolute demand delta state, neither re-parsing the
|
|
216
425
|
* network. The wasm Study lives behind the engine host (a dedicated worker
|
|
@@ -219,36 +428,56 @@ export class BrowserStudy {
|
|
|
219
428
|
#host;
|
|
220
429
|
/** The caller-allocated handle naming the wasm Study on the host. */
|
|
221
430
|
#handle;
|
|
431
|
+
#formulation;
|
|
432
|
+
#planningHost;
|
|
222
433
|
/** External bus id -> dense positional bus index, built once from the committed solution's
|
|
223
|
-
*
|
|
434
|
+
* nodal value ordering (each wire `lmp[i].bus` sits at dense index `i`). The engine keys `SensRequest`
|
|
224
435
|
* by this dense index, not the external bus id, so the selected bus must be translated
|
|
225
436
|
* before a sensitivity request. Null until first needed; the bus set is solve-invariant. */
|
|
226
437
|
#busToIndex = null;
|
|
227
438
|
/** External branch id -> dense positional branch index, from the committed solution's
|
|
228
439
|
* flows ordering — the branch-axis counterpart of `#busToIndex`. */
|
|
229
440
|
#branchToIndex = null;
|
|
230
|
-
constructor(host, handle) {
|
|
441
|
+
constructor(host, handle, formulation = DEFAULT_FORMULATION, planningHost = isolatedEngineHost) {
|
|
231
442
|
this.#host = host;
|
|
232
443
|
this.#handle = handle;
|
|
444
|
+
this.#formulation = formulation;
|
|
445
|
+
this.#planningHost = planningHost;
|
|
233
446
|
}
|
|
234
447
|
async #solution() {
|
|
235
448
|
return JSON.parse(expectText(await this.#host.call({ op: "study_solution", study: this.#handle })));
|
|
236
449
|
}
|
|
237
|
-
async #replaceEdits(deltas, rates, target) {
|
|
450
|
+
async #replaceEdits(deltas, rates, target, signal) {
|
|
238
451
|
const sensitivities = sensitivitiesJson(await this.#senseTarget(target));
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
452
|
+
signal?.throwIfAborted();
|
|
453
|
+
const cancel = () => {
|
|
454
|
+
this.#host.cancel?.(new DOMException("solve cancelled", "AbortError"));
|
|
455
|
+
};
|
|
456
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
457
|
+
try {
|
|
458
|
+
const result = await this.#host.call({
|
|
459
|
+
op: "study_replace_edits",
|
|
460
|
+
study: this.#handle,
|
|
461
|
+
edits: JSON.stringify(toEdits(deltas, rates)),
|
|
462
|
+
sensitivities,
|
|
463
|
+
});
|
|
464
|
+
signal?.throwIfAborted();
|
|
465
|
+
return JSON.parse(expectText(result));
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
signal?.throwIfAborted();
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
finally {
|
|
472
|
+
signal?.removeEventListener("abort", cancel);
|
|
473
|
+
}
|
|
245
474
|
}
|
|
246
475
|
/** The dense-axis sensitivity target the engine expects for a selection (external
|
|
247
476
|
* ids), or null when nothing is selected or the id is unknown. Memoizes the
|
|
248
|
-
* id->index maps from the committed solution's
|
|
477
|
+
* id->index maps from the committed solution's nodal value and flow order — the same axis
|
|
249
478
|
* orders the engine's dense sensitivity columns use. */
|
|
250
479
|
async #senseTarget(target) {
|
|
251
|
-
if (target === null)
|
|
480
|
+
if (target === null || this.#formulation === "acpf")
|
|
252
481
|
return null;
|
|
253
482
|
if ("bus" in target) {
|
|
254
483
|
if (!this.#busToIndex) {
|
|
@@ -266,57 +495,135 @@ export class BrowserStudy {
|
|
|
266
495
|
return index === undefined ? null : { kind: "branch", index };
|
|
267
496
|
}
|
|
268
497
|
/** Exact solve at demand = base + `deltas` and ratings = base + `rates`, replacing
|
|
269
|
-
* the committed point. When `target` names a bus or branch, its
|
|
498
|
+
* the committed point. When `target` names a bus or branch, its nodal-value/parameter
|
|
270
499
|
* column is computed in the *same* solve and returned (no second round-trip);
|
|
271
500
|
* otherwise `sensitivity` is null. `caseId` labels the returned column. Returns the
|
|
272
501
|
* UI Solution, the solver iterates, and the column. */
|
|
273
|
-
async commit(caseId, deltas, rates, target) {
|
|
274
|
-
const out = await this.#replaceEdits(deltas, rates, target);
|
|
502
|
+
async commit(caseId, deltas, rates, target, signal) {
|
|
503
|
+
const out = await this.#replaceEdits(deltas, rates, target, signal);
|
|
275
504
|
return {
|
|
276
505
|
solution: solveResponseToSolution(out.solution),
|
|
277
506
|
iterations: out.iterations ?? [],
|
|
278
507
|
sensitivity: target === null ? null : sensitivityColumn(caseId, out.sensitivities),
|
|
279
508
|
};
|
|
280
509
|
}
|
|
281
|
-
/** The
|
|
510
|
+
/** The nodal-value/parameter column at `target` for this study's formulation, computed by
|
|
282
511
|
* an exact re-solve at the edited point (the same one-call path `commit` uses, but
|
|
283
512
|
* returning only the column). Used when a bus or branch is selected so the overlay
|
|
284
513
|
* matches the active formulation rather than always the DC sensitivity. `caseId`
|
|
285
514
|
* labels the column; returns null when no recognized column comes back. */
|
|
286
515
|
async sensitivity(caseId, deltas, rates, target) {
|
|
516
|
+
if (this.#formulation === "acpf")
|
|
517
|
+
return null;
|
|
287
518
|
const out = await this.#replaceEdits(deltas, rates, target);
|
|
288
519
|
return sensitivityColumn(caseId, out.sensitivities);
|
|
289
520
|
}
|
|
290
|
-
/** First
|
|
521
|
+
/** First order nodal value preview for replacing the committed point with
|
|
291
522
|
* demand = base + `deltas` and ratings = base + `rates`, with no re-solve:
|
|
292
|
-
* predicted per-bus
|
|
523
|
+
* predicted per-bus nodal value changes and the predicted objective change. */
|
|
293
524
|
async preview(deltas, rates = {}) {
|
|
525
|
+
if (this.#formulation === "acpf") {
|
|
526
|
+
return { prices: [], objectiveDelta: null, units: null };
|
|
527
|
+
}
|
|
294
528
|
const out = JSON.parse(expectText(await this.#host.call({
|
|
295
529
|
op: "study_preview",
|
|
296
530
|
study: this.#handle,
|
|
297
531
|
edits: JSON.stringify(toEdits(deltas, rates)),
|
|
298
532
|
operands: PREVIEW_OPERANDS_JSON,
|
|
299
533
|
})));
|
|
300
|
-
const
|
|
534
|
+
const prices = (out.operands[0]?.values ?? [])
|
|
301
535
|
.filter((v) => v.element.Bus !== undefined)
|
|
302
|
-
.map((v) => ({ bus: v.element.Bus,
|
|
303
|
-
return {
|
|
536
|
+
.map((v) => ({ bus: v.element.Bus, value: v.value }));
|
|
537
|
+
return {
|
|
538
|
+
prices,
|
|
539
|
+
objectiveDelta: out.objective_delta,
|
|
540
|
+
units: out.operands[0]?.units ?? null,
|
|
541
|
+
};
|
|
304
542
|
}
|
|
305
543
|
/** The Study's current exact solution. Called immediately after Study creation
|
|
306
544
|
* to cache the base point for formulation comparisons. */
|
|
545
|
+
async currentView() {
|
|
546
|
+
return JSON.parse(expectText(await this.#host.call({ op: "study_solution", study: this.#handle })));
|
|
547
|
+
}
|
|
307
548
|
async currentSolution() {
|
|
308
549
|
return solveResponseToSolution(await this.#solution());
|
|
309
550
|
}
|
|
310
|
-
/**
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
551
|
+
/** Materialize the current case as generation 2 PowerIO IR. */
|
|
552
|
+
async saveModule() {
|
|
553
|
+
return expectText(await this.#host.call({ op: "study_save_module", study: this.#handle }));
|
|
554
|
+
}
|
|
555
|
+
/** Preserve the materialized instance's inner objective and constraints in PowerIO IR. */
|
|
556
|
+
async saveInstanceModule() {
|
|
557
|
+
return expectText(await this.#host.call({
|
|
558
|
+
op: "study_save_instance_module",
|
|
559
|
+
study: this.#handle,
|
|
560
|
+
}));
|
|
561
|
+
}
|
|
562
|
+
/** Serialize the current exact DC OPF result as a PowerIO solution module.
|
|
563
|
+
* Its embedded problem instance contains the materialized case that was
|
|
564
|
+
* solved. */
|
|
565
|
+
async saveSolutionModule() {
|
|
566
|
+
return expectText(await this.#host.call({
|
|
567
|
+
op: "study_save_solution_module",
|
|
568
|
+
study: this.#handle,
|
|
569
|
+
}));
|
|
570
|
+
}
|
|
571
|
+
/** Write the current materialized case to one PowerIO target format. */
|
|
572
|
+
async export(format) {
|
|
573
|
+
return JSON.parse(expectText(await this.#host.call({
|
|
574
|
+
op: "study_export",
|
|
575
|
+
study: this.#handle,
|
|
576
|
+
format,
|
|
577
|
+
})));
|
|
578
|
+
}
|
|
579
|
+
/** Run a bounded differentiable planning search over the committed
|
|
580
|
+
* operating point. Read only: the committed case, edits, and revision are
|
|
581
|
+
* untouched; the returned outcome carries the unapplied proposal and the
|
|
582
|
+
* search trace (gradient, per step prediction, exact result, first
|
|
583
|
+
* order error, accept or reject reason). Rejects on a non-DC formulation
|
|
584
|
+
* or an unknown element key. Worker hosts stop the solve on cancellation;
|
|
585
|
+
* the synchronous fallback finishes the wasm call but rejects its result. */
|
|
586
|
+
async plan(spec, signal) {
|
|
587
|
+
signal?.throwIfAborted();
|
|
588
|
+
const moduleJson = await this.saveModule();
|
|
589
|
+
signal?.throwIfAborted();
|
|
590
|
+
const planningStudy = await createStudyOnHost(moduleJson, this.#formulation, this.#planningHost(), signal);
|
|
591
|
+
try {
|
|
592
|
+
return await planningStudy.#planInPlace(spec, signal);
|
|
593
|
+
}
|
|
594
|
+
finally {
|
|
595
|
+
planningStudy.free();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/** Run planning on this disposable Study. Public callers go through
|
|
599
|
+
* `plan`, which first clones the committed PowerIO module onto an isolated
|
|
600
|
+
* host so cancellation cannot invalidate the interactive Study. */
|
|
601
|
+
async #planInPlace(spec, signal) {
|
|
602
|
+
signal?.throwIfAborted();
|
|
603
|
+
const cancel = () => {
|
|
604
|
+
this.#host.cancel?.(new DOMException("planning cancelled", "AbortError"));
|
|
605
|
+
};
|
|
606
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
607
|
+
try {
|
|
608
|
+
const result = await this.#host.call({
|
|
609
|
+
op: "study_plan",
|
|
610
|
+
study: this.#handle,
|
|
611
|
+
spec: JSON.stringify(spec),
|
|
612
|
+
});
|
|
613
|
+
signal?.throwIfAborted();
|
|
614
|
+
return JSON.parse(expectText(result));
|
|
615
|
+
}
|
|
616
|
+
catch (error) {
|
|
617
|
+
signal?.throwIfAborted();
|
|
618
|
+
throw error;
|
|
619
|
+
}
|
|
620
|
+
finally {
|
|
621
|
+
signal?.removeEventListener("abort", cancel);
|
|
622
|
+
}
|
|
316
623
|
}
|
|
317
624
|
/** Apply a parsed geographic layer (the canonical `.geo.json` from
|
|
318
625
|
* `parseGeo` or `applyLayout`) onto this study's base network, so the next
|
|
319
|
-
* `
|
|
626
|
+
* `saveModule` or export carries the coordinates on screen. Locations are
|
|
320
627
|
* metadata the model never reads; nothing re-solves. */
|
|
321
628
|
async applyGeoLayer(layer) {
|
|
322
629
|
return JSON.parse(expectText(await this.#host.call({
|
|
@@ -326,7 +633,7 @@ export class BrowserStudy {
|
|
|
326
633
|
})));
|
|
327
634
|
}
|
|
328
635
|
/** Release the wasm Study; call when discarding it (e.g. the case's
|
|
329
|
-
*
|
|
636
|
+
* module input changed, or the case was removed). Fire and forget: the
|
|
330
637
|
* handle is invalid from this call on. */
|
|
331
638
|
free() {
|
|
332
639
|
void this.#host
|
|
@@ -334,60 +641,71 @@ export class BrowserStudy {
|
|
|
334
641
|
.catch(() => { });
|
|
335
642
|
}
|
|
336
643
|
}
|
|
337
|
-
/** Construct a
|
|
338
|
-
* and solving the base case once. `formulation` is a `Problem` tag (`dcopf`/`acopf`/`socwr`,
|
|
644
|
+
/** Construct a Study from a retained PowerIO module for `formulation`, parsing its
|
|
645
|
+
* problem instance and solving the base case once. `formulation` is a `Problem` tag (`dcopf`/`acopf`/`socwr`,
|
|
339
646
|
* defaulting to DC OPF); the full wasm build solves every one entirely in the browser.
|
|
340
647
|
* Throws if the engine module can't load (or the formulation is unknown/not built); the
|
|
341
648
|
* caller must catch and fall back (see `isPermanentEngineFailure`). */
|
|
342
649
|
let studySeq = 0;
|
|
343
|
-
export async function createStudy(
|
|
344
|
-
const host = engineHost();
|
|
650
|
+
export async function createStudy(moduleJson, formulation = DEFAULT_FORMULATION, options = {}) {
|
|
651
|
+
const host = options.isolated ? isolatedEngineHost() : engineHost();
|
|
652
|
+
return createStudyOnHost(moduleJson, formulation, host, options.signal);
|
|
653
|
+
}
|
|
654
|
+
async function createStudyOnHost(moduleJson, formulation, host, signal) {
|
|
345
655
|
// Handles are allocated here, not by the host, so a pending build replayed
|
|
346
656
|
// onto the fallback host keeps naming the same study.
|
|
347
657
|
const handle = ++studySeq;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
study
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
format,
|
|
371
|
-
})));
|
|
658
|
+
signal?.throwIfAborted();
|
|
659
|
+
const cancel = () => {
|
|
660
|
+
host.cancel?.(new DOMException("study creation cancelled", "AbortError"));
|
|
661
|
+
};
|
|
662
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
663
|
+
try {
|
|
664
|
+
await host.call({
|
|
665
|
+
op: "study_new",
|
|
666
|
+
study: handle,
|
|
667
|
+
module_json: moduleJson,
|
|
668
|
+
formulation,
|
|
669
|
+
});
|
|
670
|
+
signal?.throwIfAborted();
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
signal?.throwIfAborted();
|
|
674
|
+
throw error;
|
|
675
|
+
}
|
|
676
|
+
finally {
|
|
677
|
+
signal?.removeEventListener("abort", cancel);
|
|
678
|
+
}
|
|
679
|
+
return new BrowserStudy(host, handle, formulation);
|
|
372
680
|
}
|
|
373
681
|
export const browserWasmTransport = {
|
|
374
682
|
preloadEngine,
|
|
683
|
+
classifyJson,
|
|
684
|
+
ingestJsonDrop,
|
|
375
685
|
ingestCase,
|
|
376
686
|
ingestDistCase,
|
|
687
|
+
ingestDistCaseBytes,
|
|
377
688
|
parseDisplay,
|
|
378
689
|
parseGeo,
|
|
379
690
|
applyGeo,
|
|
691
|
+
applyMcGeo,
|
|
380
692
|
applyLayout,
|
|
381
693
|
extractGeo,
|
|
382
694
|
applyDisplayGeo,
|
|
383
695
|
capabilities,
|
|
384
|
-
|
|
696
|
+
solveModule,
|
|
697
|
+
solveMcBmopf,
|
|
698
|
+
solveMcModule,
|
|
699
|
+
solveMcStudy,
|
|
700
|
+
createMcPfSession,
|
|
701
|
+
replayMcStudy,
|
|
702
|
+
applyMcStudyGeo,
|
|
385
703
|
createStudy,
|
|
386
|
-
loadPackage,
|
|
387
|
-
exportStudy,
|
|
388
704
|
};
|
|
389
705
|
export function createBrowserWasmTransport() {
|
|
390
706
|
return browserWasmTransport;
|
|
391
707
|
}
|
|
392
708
|
export { BrowserStudy as Study };
|
|
709
|
+
export { IndexedDbStudyStore, StudyDocumentController, studyBackend, } from "./study-document.js";
|
|
710
|
+
export { studySchema } from "./generated/study-schema.js";
|
|
393
711
|
//# sourceMappingURL=index.js.map
|