@tellegen/engine 0.2.0 → 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 +19 -14
- package/dist/generated/contracts.d.ts +25 -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 +58 -10
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +309 -119
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +359 -110
- package/dist/index.js.map +1 -1
- package/dist/input-limit.d.ts +1 -1
- package/dist/input-limit.js +1 -1
- 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 +87 -26
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +60 -23
- 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 +167 -85
- package/dist/wasm-pkg/tellegen.js +667 -187
- package/dist/wasm-pkg/tellegen_bg.wasm +0 -0
- package/dist/wasm-pkg/tellegen_bg.wasm.d.ts +27 -9
- package/dist/worker.js +15 -3
- package/dist/worker.js.map +1 -1
- package/package.json +20 -3
package/dist/index.js
CHANGED
|
@@ -2,16 +2,20 @@
|
|
|
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
7
|
import { assertEngineInputBytes, assertEngineInputLength, } from "./input-limit.js";
|
|
8
8
|
export { CONTRACT_SOURCE_SHA256, CONTRACT_VERSION, FORMULATION_IDS, SOLVE_STATUSES, } from "./generated/contracts.js";
|
|
9
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
|
+
}
|
|
10
14
|
export async function preloadEngine() {
|
|
11
15
|
await engineHost().call({ op: "preload" });
|
|
12
16
|
}
|
|
13
|
-
/** Classify JSON bytes through powerio's Rust routing table.
|
|
14
|
-
*
|
|
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. */
|
|
15
19
|
export async function classifyJson(bytes) {
|
|
16
20
|
assertEngineInputBytes(bytes);
|
|
17
21
|
return JSON.parse(expectText(await engineHost().call({ op: "classify_json", bytes })));
|
|
@@ -51,21 +55,6 @@ export async function ingestCase(bytes, format) {
|
|
|
51
55
|
assertEngineInputBytes(bytes);
|
|
52
56
|
return JSON.parse(expectText(await engineHost().call({ op: "ingest_case", bytes, format })));
|
|
53
57
|
}
|
|
54
|
-
/** Parse powerio's own model JSON (the `model-json` export, or the payload a
|
|
55
|
-
* package carries) for viewing. It is not a case format, so it does not go
|
|
56
|
-
* through `ingestCase`. */
|
|
57
|
-
export async function ingestModelJson(networkJson) {
|
|
58
|
-
assertEngineInputLength(networkJson.length);
|
|
59
|
-
return JSON.parse(expectText(await engineHost().call({
|
|
60
|
-
op: "ingest_model_json",
|
|
61
|
-
network_json: networkJson,
|
|
62
|
-
})));
|
|
63
|
-
}
|
|
64
|
-
/** Byte entry point for a dropped model JSON document. */
|
|
65
|
-
export async function ingestModelJsonBytes(bytes) {
|
|
66
|
-
assertEngineInputBytes(bytes);
|
|
67
|
-
return JSON.parse(expectText(await engineHost().call({ op: "ingest_model_json_bytes", bytes })));
|
|
68
|
-
}
|
|
69
58
|
/** Parse a multiconductor distribution case for viewing. `format` is a
|
|
70
59
|
* distribution reader token (`dss`, `bmopf`, `pmd`) or `pio` for a `.pio.json`
|
|
71
60
|
* package carrying a multiconductor payload. Rejects on malformed input or a
|
|
@@ -99,49 +88,207 @@ export async function parseGeo(bytes, hint) {
|
|
|
99
88
|
/** Apply a parsed layer onto a case network (matching by uid, external id,
|
|
100
89
|
* name, and the unordered branch endpoint pair). Rejects when nothing
|
|
101
90
|
* matched. */
|
|
102
|
-
export async function applyGeo(
|
|
91
|
+
export async function applyGeo(moduleJson, layer) {
|
|
92
|
+
assertEngineInputLength(moduleJson.length);
|
|
103
93
|
return JSON.parse(expectText(await engineHost().call({
|
|
104
94
|
op: "apply_geo",
|
|
105
|
-
|
|
95
|
+
module_json: moduleJson,
|
|
106
96
|
layer,
|
|
107
97
|
})));
|
|
108
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
|
+
}
|
|
109
111
|
/** Stamp a computed layout (bus id => [lon, lat]) onto a case network with
|
|
110
112
|
* its provenance, so saves and exports carry it. */
|
|
111
|
-
export async function applyLayout(
|
|
113
|
+
export async function applyLayout(moduleJson, coords, kind) {
|
|
114
|
+
assertEngineInputLength(moduleJson.length);
|
|
112
115
|
return JSON.parse(expectText(await engineHost().call({
|
|
113
116
|
op: "apply_layout",
|
|
114
|
-
|
|
117
|
+
module_json: moduleJson,
|
|
115
118
|
coords: JSON.stringify(coords),
|
|
116
119
|
kind,
|
|
117
120
|
})));
|
|
118
121
|
}
|
|
119
122
|
/** A case's coordinates as a canonical `.geo.json` document. Rejects when the
|
|
120
123
|
* case carries none. */
|
|
121
|
-
export async function extractGeo(
|
|
122
|
-
|
|
124
|
+
export async function extractGeo(moduleJson) {
|
|
125
|
+
assertEngineInputLength(moduleJson.length);
|
|
126
|
+
return expectText(await engineHost().call({ op: "extract_geo", module_json: moduleJson }));
|
|
123
127
|
}
|
|
124
128
|
/** Fill case coordinates from a PowerWorld `.pwd` sibling: substation symbols
|
|
125
129
|
* project to approximate longitude/latitude and join onto buses through the
|
|
126
130
|
* `SubNum` extras key. Rejects when no bus joined. */
|
|
127
|
-
export async function applyDisplayGeo(
|
|
131
|
+
export async function applyDisplayGeo(moduleJson, bytes) {
|
|
132
|
+
assertEngineInputLength(moduleJson.length);
|
|
128
133
|
assertEngineInputBytes(bytes);
|
|
129
134
|
return JSON.parse(expectText(await engineHost().call({
|
|
130
135
|
op: "apply_display_geo",
|
|
131
|
-
|
|
136
|
+
module_json: moduleJson,
|
|
132
137
|
bytes,
|
|
133
138
|
})));
|
|
134
139
|
}
|
|
135
140
|
export async function capabilities() {
|
|
136
141
|
return JSON.parse(expectText(await engineHost().call({ op: "capabilities" })));
|
|
137
142
|
}
|
|
138
|
-
export async function
|
|
143
|
+
export async function solveModule(moduleJson, request = {}) {
|
|
139
144
|
return JSON.parse(expectText(await engineHost().call({
|
|
140
|
-
op: "
|
|
141
|
-
|
|
145
|
+
op: "solve_module",
|
|
146
|
+
module_json: moduleJson,
|
|
142
147
|
request: JSON.stringify(request),
|
|
143
148
|
})));
|
|
144
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
|
+
}
|
|
145
292
|
export { errorText } from "./errors.js";
|
|
146
293
|
/** True when the engine wasm module has failed to load in a way it can never
|
|
147
294
|
* recover from in this browser. Nothing solves client side when this is true;
|
|
@@ -178,7 +325,7 @@ function toEdits(deltas, rates) {
|
|
|
178
325
|
* ∂(price, active) / ∂(demand, active) for a bus target, or ∂(price, active) /
|
|
179
326
|
* ∂(line limit) for a branch target. The *formulation* is no longer fixed here — it is
|
|
180
327
|
* a parameter threaded from the UI's selector through `createStudy` (every formulation the
|
|
181
|
-
* full wasm build carries returns
|
|
328
|
+
* full wasm build carries returns nodal marginal values, so these columns apply to all of them). The
|
|
182
329
|
* operand/parameters stay centralized so `createStudy` and the Study's `commit`/`preview`
|
|
183
330
|
* requests read one source. */
|
|
184
331
|
const STUDY_CAPABILITY = {
|
|
@@ -193,6 +340,7 @@ const STUDY_CAPABILITY = {
|
|
|
193
340
|
* is the menu order; `dcopf` is first and is the default. */
|
|
194
341
|
export const FORMULATIONS = [
|
|
195
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" },
|
|
196
344
|
{
|
|
197
345
|
id: "socwr",
|
|
198
346
|
label: "SOCWR",
|
|
@@ -205,12 +353,12 @@ export const FORMULATIONS = [
|
|
|
205
353
|
disabled: true,
|
|
206
354
|
},
|
|
207
355
|
];
|
|
208
|
-
/**
|
|
356
|
+
/** DC OPF is the default calculation. */
|
|
209
357
|
export const DEFAULT_FORMULATION = "dcopf";
|
|
210
|
-
/** The `Operand[]` JSON `Study.preview` watches (the
|
|
358
|
+
/** The `Operand[]` JSON `Study.preview` watches (the active nodal value column). */
|
|
211
359
|
const PREVIEW_OPERANDS_JSON = JSON.stringify([STUDY_CAPABILITY.operand]);
|
|
212
|
-
/** The `SensRequest[]` JSON for `Study.commit`: the
|
|
213
|
-
* 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
|
|
214
362
|
* target. NOTE: `SensRequest.indices` are **dense positional** indices into the target's
|
|
215
363
|
* axis (0-based), *not* external ids — `BrowserStudy` translates the selected external id
|
|
216
364
|
* to its dense index before calling this. */
|
|
@@ -227,7 +375,7 @@ function sensitivitiesJson(target) {
|
|
|
227
375
|
},
|
|
228
376
|
]);
|
|
229
377
|
}
|
|
230
|
-
/** Extract the
|
|
378
|
+
/** Extract the nodal-value/parameter column from the first requested `SensitivityMatrix` into
|
|
231
379
|
* the `SensitivityColumn` the map and legend consume, the same shape the server serves.
|
|
232
380
|
* Rows are Price operands per bus; the single column is the selected parameter, so the
|
|
233
381
|
* column is `values[r][0]` keyed by each row's source bus id, and the source element is
|
|
@@ -242,7 +390,7 @@ function sensitivityColumn(caseId, matrices) {
|
|
|
242
390
|
const values = m.rows
|
|
243
391
|
.map((row, r) => ({ bus: row.element.Bus, value: m.values[r]?.[0] ?? 0 }))
|
|
244
392
|
.filter((v) => v.bus !== undefined);
|
|
245
|
-
const shared = { case: caseId, operand: "
|
|
393
|
+
const shared = { case: caseId, operand: "price", units: m.units, values };
|
|
246
394
|
if (el.Bus !== undefined) {
|
|
247
395
|
return { ...shared, parameter: "d", bus: el.Bus };
|
|
248
396
|
}
|
|
@@ -253,8 +401,9 @@ function sensitivityColumn(caseId, matrices) {
|
|
|
253
401
|
}
|
|
254
402
|
function solveResponseToSolution(out) {
|
|
255
403
|
return {
|
|
256
|
-
objective: out.objective ??
|
|
257
|
-
|
|
404
|
+
objective: out.objective ?? null,
|
|
405
|
+
prices: (out.lmp ?? []).map((e) => ({ bus: e.bus, value: e.value })),
|
|
406
|
+
vm: out.vm ?? undefined,
|
|
258
407
|
va: out.va ?? [],
|
|
259
408
|
w: out.w ?? [],
|
|
260
409
|
flows: (out.flows ?? []).map((f) => ({
|
|
@@ -262,11 +411,15 @@ function solveResponseToSolution(out) {
|
|
|
262
411
|
mw: f.pf,
|
|
263
412
|
loading: f.loading,
|
|
264
413
|
})),
|
|
265
|
-
dispatch: (out.dispatch ?? []).map((d) => ({
|
|
414
|
+
dispatch: (out.dispatch ?? []).map((d) => ({
|
|
415
|
+
gen: d.gen,
|
|
416
|
+
bus: d.bus,
|
|
417
|
+
mw: d.pg,
|
|
418
|
+
})),
|
|
266
419
|
};
|
|
267
420
|
}
|
|
268
|
-
/**
|
|
269
|
-
* 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
|
|
270
423
|
* at the UI's absolute demand delta state and `preview` returns a first-order
|
|
271
424
|
* linearization toward an absolute demand delta state, neither re-parsing the
|
|
272
425
|
* network. The wasm Study lives behind the engine host (a dedicated worker
|
|
@@ -275,36 +428,56 @@ export class BrowserStudy {
|
|
|
275
428
|
#host;
|
|
276
429
|
/** The caller-allocated handle naming the wasm Study on the host. */
|
|
277
430
|
#handle;
|
|
431
|
+
#formulation;
|
|
432
|
+
#planningHost;
|
|
278
433
|
/** External bus id -> dense positional bus index, built once from the committed solution's
|
|
279
|
-
*
|
|
434
|
+
* nodal value ordering (each wire `lmp[i].bus` sits at dense index `i`). The engine keys `SensRequest`
|
|
280
435
|
* by this dense index, not the external bus id, so the selected bus must be translated
|
|
281
436
|
* before a sensitivity request. Null until first needed; the bus set is solve-invariant. */
|
|
282
437
|
#busToIndex = null;
|
|
283
438
|
/** External branch id -> dense positional branch index, from the committed solution's
|
|
284
439
|
* flows ordering — the branch-axis counterpart of `#busToIndex`. */
|
|
285
440
|
#branchToIndex = null;
|
|
286
|
-
constructor(host, handle) {
|
|
441
|
+
constructor(host, handle, formulation = DEFAULT_FORMULATION, planningHost = isolatedEngineHost) {
|
|
287
442
|
this.#host = host;
|
|
288
443
|
this.#handle = handle;
|
|
444
|
+
this.#formulation = formulation;
|
|
445
|
+
this.#planningHost = planningHost;
|
|
289
446
|
}
|
|
290
447
|
async #solution() {
|
|
291
448
|
return JSON.parse(expectText(await this.#host.call({ op: "study_solution", study: this.#handle })));
|
|
292
449
|
}
|
|
293
|
-
async #replaceEdits(deltas, rates, target) {
|
|
450
|
+
async #replaceEdits(deltas, rates, target, signal) {
|
|
294
451
|
const sensitivities = sensitivitiesJson(await this.#senseTarget(target));
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
+
}
|
|
301
474
|
}
|
|
302
475
|
/** The dense-axis sensitivity target the engine expects for a selection (external
|
|
303
476
|
* ids), or null when nothing is selected or the id is unknown. Memoizes the
|
|
304
|
-
* 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
|
|
305
478
|
* orders the engine's dense sensitivity columns use. */
|
|
306
479
|
async #senseTarget(target) {
|
|
307
|
-
if (target === null)
|
|
480
|
+
if (target === null || this.#formulation === "acpf")
|
|
308
481
|
return null;
|
|
309
482
|
if ("bus" in target) {
|
|
310
483
|
if (!this.#busToIndex) {
|
|
@@ -322,57 +495,135 @@ export class BrowserStudy {
|
|
|
322
495
|
return index === undefined ? null : { kind: "branch", index };
|
|
323
496
|
}
|
|
324
497
|
/** Exact solve at demand = base + `deltas` and ratings = base + `rates`, replacing
|
|
325
|
-
* 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
|
|
326
499
|
* column is computed in the *same* solve and returned (no second round-trip);
|
|
327
500
|
* otherwise `sensitivity` is null. `caseId` labels the returned column. Returns the
|
|
328
501
|
* UI Solution, the solver iterates, and the column. */
|
|
329
|
-
async commit(caseId, deltas, rates, target) {
|
|
330
|
-
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);
|
|
331
504
|
return {
|
|
332
505
|
solution: solveResponseToSolution(out.solution),
|
|
333
506
|
iterations: out.iterations ?? [],
|
|
334
507
|
sensitivity: target === null ? null : sensitivityColumn(caseId, out.sensitivities),
|
|
335
508
|
};
|
|
336
509
|
}
|
|
337
|
-
/** The
|
|
510
|
+
/** The nodal-value/parameter column at `target` for this study's formulation, computed by
|
|
338
511
|
* an exact re-solve at the edited point (the same one-call path `commit` uses, but
|
|
339
512
|
* returning only the column). Used when a bus or branch is selected so the overlay
|
|
340
513
|
* matches the active formulation rather than always the DC sensitivity. `caseId`
|
|
341
514
|
* labels the column; returns null when no recognized column comes back. */
|
|
342
515
|
async sensitivity(caseId, deltas, rates, target) {
|
|
516
|
+
if (this.#formulation === "acpf")
|
|
517
|
+
return null;
|
|
343
518
|
const out = await this.#replaceEdits(deltas, rates, target);
|
|
344
519
|
return sensitivityColumn(caseId, out.sensitivities);
|
|
345
520
|
}
|
|
346
|
-
/** First
|
|
521
|
+
/** First order nodal value preview for replacing the committed point with
|
|
347
522
|
* demand = base + `deltas` and ratings = base + `rates`, with no re-solve:
|
|
348
|
-
* predicted per-bus
|
|
523
|
+
* predicted per-bus nodal value changes and the predicted objective change. */
|
|
349
524
|
async preview(deltas, rates = {}) {
|
|
525
|
+
if (this.#formulation === "acpf") {
|
|
526
|
+
return { prices: [], objectiveDelta: null, units: null };
|
|
527
|
+
}
|
|
350
528
|
const out = JSON.parse(expectText(await this.#host.call({
|
|
351
529
|
op: "study_preview",
|
|
352
530
|
study: this.#handle,
|
|
353
531
|
edits: JSON.stringify(toEdits(deltas, rates)),
|
|
354
532
|
operands: PREVIEW_OPERANDS_JSON,
|
|
355
533
|
})));
|
|
356
|
-
const
|
|
534
|
+
const prices = (out.operands[0]?.values ?? [])
|
|
357
535
|
.filter((v) => v.element.Bus !== undefined)
|
|
358
|
-
.map((v) => ({ bus: v.element.Bus,
|
|
359
|
-
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
|
+
};
|
|
360
542
|
}
|
|
361
543
|
/** The Study's current exact solution. Called immediately after Study creation
|
|
362
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
|
+
}
|
|
363
548
|
async currentSolution() {
|
|
364
549
|
return solveResponseToSolution(await this.#solution());
|
|
365
550
|
}
|
|
366
|
-
/**
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
+
}
|
|
372
623
|
}
|
|
373
624
|
/** Apply a parsed geographic layer (the canonical `.geo.json` from
|
|
374
625
|
* `parseGeo` or `applyLayout`) onto this study's base network, so the next
|
|
375
|
-
* `
|
|
626
|
+
* `saveModule` or export carries the coordinates on screen. Locations are
|
|
376
627
|
* metadata the model never reads; nothing re-solves. */
|
|
377
628
|
async applyGeoLayer(layer) {
|
|
378
629
|
return JSON.parse(expectText(await this.#host.call({
|
|
@@ -382,7 +633,7 @@ export class BrowserStudy {
|
|
|
382
633
|
})));
|
|
383
634
|
}
|
|
384
635
|
/** Release the wasm Study; call when discarding it (e.g. the case's
|
|
385
|
-
*
|
|
636
|
+
* module input changed, or the case was removed). Fire and forget: the
|
|
386
637
|
* handle is invalid from this call on. */
|
|
387
638
|
free() {
|
|
388
639
|
void this.#host
|
|
@@ -390,73 +641,71 @@ export class BrowserStudy {
|
|
|
390
641
|
.catch(() => { });
|
|
391
642
|
}
|
|
392
643
|
}
|
|
393
|
-
/** Construct a
|
|
394
|
-
* 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`,
|
|
395
646
|
* defaulting to DC OPF); the full wasm build solves every one entirely in the browser.
|
|
396
647
|
* Throws if the engine module can't load (or the formulation is unknown/not built); the
|
|
397
648
|
* caller must catch and fall back (see `isPermanentEngineFailure`). */
|
|
398
649
|
let studySeq = 0;
|
|
399
|
-
export async function createStudy(
|
|
400
|
-
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) {
|
|
401
655
|
// Handles are allocated here, not by the host, so a pending build replayed
|
|
402
656
|
// onto the fallback host keeps naming the same study.
|
|
403
657
|
const handle = ++studySeq;
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
study
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
* warnings, and the format token and file extension. */
|
|
427
|
-
export async function exportStudy(packageJson, commit, format) {
|
|
428
|
-
assertEngineInputLength(packageJson.length);
|
|
429
|
-
return JSON.parse(expectText(await engineHost().call({
|
|
430
|
-
op: "export_study",
|
|
431
|
-
package_json: packageJson,
|
|
432
|
-
commit,
|
|
433
|
-
format,
|
|
434
|
-
})));
|
|
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);
|
|
435
680
|
}
|
|
436
681
|
export const browserWasmTransport = {
|
|
437
682
|
preloadEngine,
|
|
438
683
|
classifyJson,
|
|
439
684
|
ingestJsonDrop,
|
|
440
685
|
ingestCase,
|
|
441
|
-
ingestModelJson,
|
|
442
|
-
ingestModelJsonBytes,
|
|
443
686
|
ingestDistCase,
|
|
444
687
|
ingestDistCaseBytes,
|
|
445
688
|
parseDisplay,
|
|
446
689
|
parseGeo,
|
|
447
690
|
applyGeo,
|
|
691
|
+
applyMcGeo,
|
|
448
692
|
applyLayout,
|
|
449
693
|
extractGeo,
|
|
450
694
|
applyDisplayGeo,
|
|
451
695
|
capabilities,
|
|
452
|
-
|
|
696
|
+
solveModule,
|
|
697
|
+
solveMcBmopf,
|
|
698
|
+
solveMcModule,
|
|
699
|
+
solveMcStudy,
|
|
700
|
+
createMcPfSession,
|
|
701
|
+
replayMcStudy,
|
|
702
|
+
applyMcStudyGeo,
|
|
453
703
|
createStudy,
|
|
454
|
-
loadPackage,
|
|
455
|
-
loadPackageBytes,
|
|
456
|
-
exportStudy,
|
|
457
704
|
};
|
|
458
705
|
export function createBrowserWasmTransport() {
|
|
459
706
|
return browserWasmTransport;
|
|
460
707
|
}
|
|
461
708
|
export { BrowserStudy as Study };
|
|
709
|
+
export { IndexedDbStudyStore, StudyDocumentController, studyBackend, } from "./study-document.js";
|
|
710
|
+
export { studySchema } from "./generated/study-schema.js";
|
|
462
711
|
//# sourceMappingURL=index.js.map
|