@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.
Files changed (44) hide show
  1. package/README.md +19 -14
  2. package/dist/generated/contracts.d.ts +25 -17
  3. package/dist/generated/contracts.d.ts.map +1 -1
  4. package/dist/generated/contracts.js +3 -3
  5. package/dist/generated/study-contracts.d.ts +426 -0
  6. package/dist/generated/study-contracts.d.ts.map +1 -0
  7. package/dist/generated/study-contracts.js +4 -0
  8. package/dist/generated/study-contracts.js.map +1 -0
  9. package/dist/generated/study-schema.d.ts +2 -0
  10. package/dist/generated/study-schema.d.ts.map +1 -0
  11. package/dist/generated/study-schema.js +3 -0
  12. package/dist/generated/study-schema.js.map +1 -0
  13. package/dist/host.d.ts +12 -0
  14. package/dist/host.d.ts.map +1 -1
  15. package/dist/host.js +58 -10
  16. package/dist/host.js.map +1 -1
  17. package/dist/index.d.ts +309 -119
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +359 -110
  20. package/dist/index.js.map +1 -1
  21. package/dist/input-limit.d.ts +1 -1
  22. package/dist/input-limit.js +1 -1
  23. package/dist/module.d.ts +1 -0
  24. package/dist/module.d.ts.map +1 -1
  25. package/dist/module.js.map +1 -1
  26. package/dist/protocol.d.ts +87 -26
  27. package/dist/protocol.d.ts.map +1 -1
  28. package/dist/protocol.js +60 -23
  29. package/dist/protocol.js.map +1 -1
  30. package/dist/study-document.d.ts +46 -0
  31. package/dist/study-document.d.ts.map +1 -0
  32. package/dist/study-document.js +183 -0
  33. package/dist/study-document.js.map +1 -0
  34. package/dist/study-document.test.d.ts +2 -0
  35. package/dist/study-document.test.d.ts.map +1 -0
  36. package/dist/study-document.test.js +119 -0
  37. package/dist/study-document.test.js.map +1 -0
  38. package/dist/wasm-pkg/tellegen.d.ts +167 -85
  39. package/dist/wasm-pkg/tellegen.js +667 -187
  40. package/dist/wasm-pkg/tellegen_bg.wasm +0 -0
  41. package/dist/wasm-pkg/tellegen_bg.wasm.d.ts +27 -9
  42. package/dist/worker.js +15 -3
  43. package/dist/worker.js.map +1 -1
  44. 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. Package markers
14
- * also pass the strict package reader so the payload family is authoritative. */
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(networkJson, layer) {
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
- network_json: networkJson,
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(networkJson, coords, kind) {
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
- network_json: networkJson,
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(networkJson) {
122
- return expectText(await engineHost().call({ op: "extract_geo", network_json: networkJson }));
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(networkJson, bytes) {
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
- network_json: networkJson,
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 solveJson(networkJson, request = {}) {
143
+ export async function solveModule(moduleJson, request = {}) {
139
144
  return JSON.parse(expectText(await engineHost().call({
140
- op: "solve_json",
141
- network_json: networkJson,
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 LMP, so these columns apply to all of them). The
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
- /** The default formulation: DC OPF, preserving the prior fixed behavior byte-for-byte. */
356
+ /** DC OPF is the default calculation. */
209
357
  export const DEFAULT_FORMULATION = "dcopf";
210
- /** The `Operand[]` JSON `Study.preview` watches (the LMP column). */
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 ∂LMP/∂demand column at a dense bus
213
- * index, or the ∂LMP/∂rating column at a dense branch index, or `[]` when there is no
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 ∂LMP/∂parameter column from the first requested `SensitivityMatrix` into
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: "lmp", units: m.units, values };
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 ?? 0,
257
- lmp: (out.lmp ?? []).map((e) => ({ bus: e.bus, usd_per_mwh: e.value })),
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) => ({ gen: d.gen, mw: d.pg })),
414
+ dispatch: (out.dispatch ?? []).map((d) => ({
415
+ gen: d.gen,
416
+ bus: d.bus,
417
+ mw: d.pg,
418
+ })),
266
419
  };
267
420
  }
268
- /** Build-once browser transport for the reactive demand drag. The network is
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
- * LMP ordering (each `lmp[i].bus` sits at dense index `i`). The engine keys `SensRequest`
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
- return JSON.parse(expectText(await this.#host.call({
296
- op: "study_replace_edits",
297
- study: this.#handle,
298
- edits: JSON.stringify(toEdits(deltas, rates)),
299
- sensitivities,
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 LMP / flows order — the same axis
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 ∂LMP/∂parameter
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 ∂LMP/∂parameter column at `target` for this study's formulation, computed by
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-order LMP preview for replacing the committed point with
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 ΔLMP and the predicted Δobjective. */
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 lmp = (out.operands[0]?.values ?? [])
534
+ const prices = (out.operands[0]?.values ?? [])
357
535
  .filter((v) => v.element.Bus !== undefined)
358
- .map((v) => ({ bus: v.element.Bus, usd_per_mwh: v.value }));
359
- return { lmp, objectiveDelta: out.objective_delta };
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
- /** Serialize this study as a `.pio.json` package: the base network payload, the
367
- * edit log as the study block, and the formulation and solve options under
368
- * `study.app["tellegen"]`. The returned text is ready to download or hand to
369
- * `exportStudy`. */
370
- async savePackage() {
371
- return expectText(await this.#host.call({ op: "study_save_package", study: this.#handle }));
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
- * `savePackage` or export carries the coordinates on screen. Locations are
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
- * networkJson changed, or the case was removed). Fire and forget: the
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 build-once Study over `networkJson` for `formulation`, parsing the network
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(networkJson, formulation = DEFAULT_FORMULATION) {
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
- await host.call({
405
- op: "study_new",
406
- study: handle,
407
- network_json: networkJson,
408
- formulation,
409
- });
410
- return new BrowserStudy(host, handle);
411
- }
412
- /** Restore a study saved by `BrowserStudy.savePackage`: the case, edit sliders,
413
- * formulation, and solve options in one step. Rejects on a malformed package or one
414
- * that is not a tellegen study (the engine fails the load closed). */
415
- export async function loadPackage(text) {
416
- assertEngineInputLength(text.length);
417
- return JSON.parse(expectText(await engineHost().call({ op: "load_package", text })));
418
- }
419
- /** Restore a dropped package without decoding it in JavaScript. */
420
- export async function loadPackageBytes(bytes) {
421
- assertEngineInputBytes(bytes);
422
- return JSON.parse(expectText(await engineHost().call({ op: "load_package_bytes", bytes })));
423
- }
424
- /** Export a saved study package at commit `commit` to a powerio `format` (`matpower`,
425
- * `psse`, `model-json`, ...). Returns the serialized case text, the writer's fidelity
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
- solveJson,
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