@tellegen/svelte 0.4.7 → 0.5.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.
@@ -7,7 +7,7 @@ import { powerIoEntry, solveEntry } from './log.svelte.js';
7
7
  import { distExtensionFormat, isGeoFileName } from './drop-classify.js';
8
8
  import { DropBatchGate, readDropFileBytes, validateDropBatch } from './drop-limits.js';
9
9
  import { buildGeographicView, buildDiagramView, placeMultiView } from './multiconductor.js';
10
- import { browserWasmTransport, applyDisplayGeo, applyGeo, applyLayout, createStudy, extractGeo, FORMULATIONS, formatPowerIoDiagnostic, formatOf, ingestCase, ingestDistCaseBytes, ingestJsonDrop, isDisplayFile, isPermanentEngineFailure, parseDisplay, parseGeo, probeAcOpfWorker, solveAcOpfModule, solveResponseToSolution, mcPfResultDetail, summarizeMcPfResult } from '@tellegen/engine';
10
+ import { browserWasmTransport, applyDisplayGeo, applyGeo, applyLayout, createStudy, extractGeo, FORMULATIONS, formatPowerIoDiagnostic, formatOf, ingestCase, ingestDistCaseBytes, ingestJsonDrop, isDisplayFile, isPermanentEngineFailure, parseDisplay, parseGeo, probeAcOpfWorker, solveAcOpfModule, solveResponseToSolution, mcPfResultDetail, summarizeMcPfResult, validateMcPfOptions, DEFAULT_MC_PF_OPTIONS } from '@tellegen/engine';
11
11
  import { browserSolveFailure, errorText, extent, formulationLabel, rgbaCss } from './format.js';
12
12
  import { DEAD_ZONE_MW } from './slider.js';
13
13
  const HIDDEN_DEFAULT_CASES_KEY = 'tellegen.hiddenDefaultCases.v1';
@@ -18,6 +18,15 @@ function localHasBus(c, busId) {
18
18
  const buses = c.view?.buses ?? c.diagram?.view.buses ?? c.topology?.buses ?? [];
19
19
  return buses.some((b) => b.id === busId);
20
20
  }
21
+ /** Free a case input the engine keeps; text needs no release. */
22
+ function releaseMcInput(input) {
23
+ if (input && typeof input !== 'string')
24
+ input.free();
25
+ }
26
+ /** PowerIO IR text for a case input, built by the engine for a kept module. */
27
+ function mcInputJson(input) {
28
+ return typeof input === 'string' ? Promise.resolve(input) : input.moduleJson();
29
+ }
21
30
  /** Terminal copy when nothing can solve. Two facts decide the wording: whether
22
31
  * this browser's engine failure is known permanent, and that the server
23
32
  * declines compute. */
@@ -410,6 +419,8 @@ export class Controller {
410
419
  c.mcEditTimer = null;
411
420
  c.mcSession?.free();
412
421
  c.mcSession = null;
422
+ releaseMcInput(c.mcInput);
423
+ c.mcInput = null;
413
424
  }
414
425
  }
415
426
  // Release a case's Study (if any) when the case is removed.
@@ -2189,7 +2200,9 @@ export class Controller {
2189
2200
  }
2190
2201
  this.app.parsingFile = true;
2191
2202
  try {
2192
- const result = await ingestJsonDrop(bytes);
2203
+ // A multiconductor module stays in the engine; only its handle
2204
+ // comes back, never its IR text.
2205
+ const result = await ingestJsonDrop(bytes, { retain: true });
2193
2206
  if (result.kind === 'unknown')
2194
2207
  return { outcome: 'unrouted', bytes };
2195
2208
  if (result.kind === 'ambiguous') {
@@ -2229,7 +2242,7 @@ export class Controller {
2229
2242
  this.app.parsingFile = true;
2230
2243
  try {
2231
2244
  const bytes = new Uint8Array(await file.arrayBuffer());
2232
- this.addMultiCase(file.name, await ingestDistCaseBytes(bytes, format));
2245
+ this.addMultiCase(file.name, await ingestDistCaseBytes(bytes, format, { retain: true }));
2233
2246
  this.app.error = null;
2234
2247
  return true;
2235
2248
  }
@@ -2243,7 +2256,7 @@ export class Controller {
2243
2256
  };
2244
2257
  /** Retain parsed electrical inputs and draw each declared coordinate space directly. */
2245
2258
  addMultiCase(fileName, payload) {
2246
- const { graph, ...summary } = payload;
2259
+ const { graph, input, ...summary } = payload;
2247
2260
  const coordsKind = payload.coords_kind;
2248
2261
  const label = summary.name && summary.name !== 'case' ? summary.name : fileName.replace(/\.[^.]+$/, '');
2249
2262
  const view = coordsKind === 'geographic'
@@ -2255,6 +2268,7 @@ export class Controller {
2255
2268
  label,
2256
2269
  fileName,
2257
2270
  summary,
2271
+ input,
2258
2272
  graph,
2259
2273
  coordsKind,
2260
2274
  view
@@ -2267,17 +2281,24 @@ export class Controller {
2267
2281
  return c;
2268
2282
  }
2269
2283
  /** Compute terminal quantities only for the retained input and current case revision. */
2270
- solveMultiCase = async (c, options = {}, signal) => {
2284
+ solveMultiCase = async (c, options = c.mcOptions, signal, energizedOnly = c.mcEnergizedOnly) => {
2271
2285
  if (signal?.aborted || this.disposed)
2272
2286
  throw new DOMException('Calculation cancelled', 'AbortError');
2273
2287
  if (c.solving)
2274
2288
  throw new Error('A calculation is already running for this case');
2289
+ options = { ...DEFAULT_MC_PF_OPTIONS, ...c.mcOptions, ...options };
2290
+ const settingsError = validateMcPfOptions(options);
2291
+ if (settingsError)
2292
+ throw new Error(settingsError);
2293
+ c.mcOptions = { ...options };
2294
+ c.mcEnergizedOnly = energizedOnly;
2275
2295
  const unavailable = c.mcPfReason;
2276
2296
  if (unavailable) {
2277
2297
  this.app.error = unavailable;
2278
2298
  throw new Error(unavailable);
2279
2299
  }
2280
- let input = c.moduleJson;
2300
+ const originalInput = c.mcInput;
2301
+ let input = originalInput;
2281
2302
  const retainedAtStart = c.mcSession;
2282
2303
  const revision = c.revisionGeneration;
2283
2304
  const seq = ++c.solveSeq;
@@ -2289,9 +2310,27 @@ export class Controller {
2289
2310
  this.app.error = null;
2290
2311
  const started = performance.now();
2291
2312
  let nextSession = null;
2313
+ // An input this call took from the live session; freed unless it
2314
+ // becomes the case's input.
2315
+ let editedInput = null;
2292
2316
  try {
2317
+ // The edited operating point stays in the engine; it is never
2318
+ // written out as IR text to be read back.
2293
2319
  if (retainedAtStart)
2294
- input = await retainedAtStart.inputModule();
2320
+ input = editedInput = await retainedAtStart.input();
2321
+ if (energizedOnly) {
2322
+ if (!this.mcTransport.prepareMcEnergizedNetwork)
2323
+ throw new Error('Energized-network preparation is unavailable');
2324
+ const prepared = await this.mcTransport.prepareMcEnergizedNetwork(input);
2325
+ const selectedInput = 'input' in prepared ? prepared.input : prepared.module_json;
2326
+ if (editedInput !== selectedInput)
2327
+ releaseMcInput(editedInput);
2328
+ input = editedInput = selectedInput;
2329
+ if (abort.signal.aborted || seq !== c.solveSeq || revision !== c.revisionGeneration)
2330
+ throw new DOMException('Calculation cancelled', 'AbortError');
2331
+ c.mcExcludedIslands = prepared.excluded_islands;
2332
+ c.mcPreparedScope = prepared.network_scope;
2333
+ }
2295
2334
  let snapshot = null;
2296
2335
  let result;
2297
2336
  let fullResult = null;
@@ -2304,18 +2343,19 @@ export class Controller {
2304
2343
  loadBranches = await nextSession.loadBranches();
2305
2344
  }
2306
2345
  else {
2346
+ const text = typeof input === 'string' ? input : await input.moduleJson();
2307
2347
  snapshot = this.mcTransport.solveMcStudy
2308
- ? await this.mcTransport.solveMcStudy(input, crypto.randomUUID(), c.label, options, abort.signal)
2348
+ ? await this.mcTransport.solveMcStudy(text, crypto.randomUUID(), c.label, options, abort.signal)
2309
2349
  : null;
2310
2350
  fullResult =
2311
- snapshot?.result ?? (await this.mcTransport.solveMcModule(input, options, abort.signal));
2351
+ snapshot?.result ?? (await this.mcTransport.solveMcModule(text, options, abort.signal));
2312
2352
  result = summarizeMcPfResult(fullResult);
2313
2353
  }
2314
2354
  // dispose() aborts too; a session that arrives after it is freed below.
2315
2355
  if (abort.signal.aborted || this.disposed)
2316
2356
  throw new DOMException('Calculation cancelled', 'AbortError');
2317
2357
  if (seq !== c.solveSeq ||
2318
- (retainedAtStart ? retainedAtStart !== c.mcSession : input !== c.moduleJson) ||
2358
+ (retainedAtStart ? retainedAtStart !== c.mcSession : originalInput !== c.mcInput) ||
2319
2359
  revision !== c.revisionGeneration ||
2320
2360
  !this.app.multiCases.includes(c))
2321
2361
  throw new Error('The case changed while calculating. Run AC power flow again');
@@ -2332,7 +2372,10 @@ export class Controller {
2332
2372
  nextSession = null;
2333
2373
  c.mcLoadBranches = loadBranches;
2334
2374
  c.mcLoadEdits = [];
2335
- c.moduleJson = input;
2375
+ if (c.mcInput !== input)
2376
+ releaseMcInput(c.mcInput);
2377
+ c.mcInput = input;
2378
+ editedInput = null;
2336
2379
  c.result = result;
2337
2380
  c.mcFullResult = fullResult;
2338
2381
  c.mcTiming = null;
@@ -2340,10 +2383,12 @@ export class Controller {
2340
2383
  c.mcSavedAt = null;
2341
2384
  c.solveMs = performance.now() - started;
2342
2385
  c.revisionGeneration++;
2386
+ c.mcCompletedSolveSeq = seq;
2343
2387
  return result;
2344
2388
  }
2345
2389
  catch (error) {
2346
2390
  nextSession?.free();
2391
+ releaseMcInput(editedInput);
2347
2392
  if (!abort.signal.aborted &&
2348
2393
  !(error instanceof DOMException && error.name === 'AbortError')) {
2349
2394
  this.app.error = errorText(error);
@@ -2398,7 +2443,7 @@ export class Controller {
2398
2443
  return full ? mcPfResultDetail(full, query, summary.solve_count) : null;
2399
2444
  };
2400
2445
  /** Above this many buses, reading and indexing the whole operating point takes a noticeable
2401
- * fraction of a second (evidence/diagram), so the overview is computed on request instead of
2446
+ * fraction of a second (docs/evidence/diagram), so the overview is computed on request instead of
2402
2447
  * after every solve. */
2403
2448
  static AUTO_OVERVIEW_MAX_BUSES = 25_000;
2404
2449
  overviewSeq = 0;
@@ -2409,7 +2454,7 @@ export class Controller {
2409
2454
  readOverview = (c) => this.startOverviewRead(c, ++this.overviewSeq);
2410
2455
  startOverviewRead(c, seq) {
2411
2456
  c.overviewLoading = true;
2412
- this.multiFullResult(c).then((full) => {
2457
+ this.multiOverviewResult(c).then((full) => {
2413
2458
  if (seq !== this.overviewSeq)
2414
2459
  return;
2415
2460
  if (full)
@@ -2440,6 +2485,22 @@ export class Controller {
2440
2485
  c.overviewLoading = false;
2441
2486
  };
2442
2487
  };
2488
+ /** The complete operating point in the form whole-network views read: the stored result, or
2489
+ * the live session's as columns, which hold no object per terminal or port. Null when there is
2490
+ * no result or it changed before the read finished. */
2491
+ multiOverviewResult = async (c) => {
2492
+ if (!c.result)
2493
+ return null;
2494
+ const stored = c.mcFullResult ?? c.mcSnapshot?.result ?? null;
2495
+ const session = c.mcSession;
2496
+ if (stored || !session)
2497
+ return stored;
2498
+ if (c.mcEditRunning)
2499
+ return null;
2500
+ const solve = c.result.solve_count;
2501
+ const tables = await session.resultTables();
2502
+ return session === c.mcSession && c.result?.solve_count === solve ? tables : null;
2503
+ };
2443
2504
  /** The complete operating point for whole-network views: the stored result, or one read from
2444
2505
  * the live session. Null when there is no result or it changed before the read finished. */
2445
2506
  multiFullResult = async (c) => {
@@ -2500,14 +2561,15 @@ export class Controller {
2500
2561
  throw new Error('Select the matching case before attaching coordinates');
2501
2562
  if (c.solving)
2502
2563
  throw new Error('Wait for the calculation before attaching coordinates');
2503
- if (!c.moduleJson || !this.mcTransport.applyMcGeo)
2564
+ if (!c.mcInput || !this.mcTransport.applyMcGeo)
2504
2565
  throw new Error('Coordinate attachment is unavailable for this case');
2505
- const baseInput = c.moduleJson;
2566
+ const baseInput = c.mcInput;
2506
2567
  const retainedAtStart = c.mcSession;
2507
2568
  let snapshot = c.mcSnapshot;
2508
2569
  if (!snapshot && c.mcSession && c.result)
2509
2570
  snapshot = await this.snapshotMultiCase(c);
2510
- const input = snapshot?.input_module ?? c.moduleJson;
2571
+ // Coordinates attach to IR text, so a kept module is written out here.
2572
+ const input = snapshot?.input_module ?? (await mcInputJson(baseInput));
2511
2573
  const revision = c.revisionGeneration;
2512
2574
  let next = input;
2513
2575
  let payload;
@@ -2531,7 +2593,7 @@ export class Controller {
2531
2593
  throw new Error('No coordinates matched the selected case');
2532
2594
  if (this.app.studyView ||
2533
2595
  this.app.activeMulti !== c ||
2534
- c.moduleJson !== baseInput ||
2596
+ c.mcInput !== baseInput ||
2535
2597
  c.mcSession !== retainedAtStart ||
2536
2598
  c.revisionGeneration !== revision ||
2537
2599
  c.solving)
@@ -2547,7 +2609,8 @@ export class Controller {
2547
2609
  c.mcFullResult = snapshot?.result ?? c.mcFullResult;
2548
2610
  c.mcLoadBranches = [];
2549
2611
  c.mcLoadEdits = [];
2550
- c.moduleJson = next;
2612
+ releaseMcInput(c.mcInput);
2613
+ c.mcInput = next;
2551
2614
  c.mcSnapshot = snapshot ? { ...snapshot, id: crypto.randomUUID() } : null;
2552
2615
  c.mcSavedAt = null;
2553
2616
  c.geoLayer = payload.geo_layer ?? null;
@@ -2727,6 +2790,8 @@ export class Controller {
2727
2790
  clearTimeout(c.mcEditTimer);
2728
2791
  c.mcSession?.free();
2729
2792
  c.mcSession = null;
2793
+ releaseMcInput(c.mcInput);
2794
+ c.mcInput = null;
2730
2795
  if (this.app.activeMultiId === c.id) {
2731
2796
  c.selectedBusId = null;
2732
2797
  c.selectedEdgeId = null;
@@ -1,4 +1,4 @@
1
- import type { DistAttachmentKind, DistConnection, DistGraph } from '@tellegen/engine';
1
+ import type { DistAttachmentKind, DistConnection, DistGraph, McPfResultTables } from '@tellegen/engine';
2
2
  import type { SchematicGeometry } from './schematic-layout.js';
3
3
  /** A stroked glyph: polylines in symbol units, x along the connection, y down. */
4
4
  export type Glyph = [number, number][][];
@@ -108,8 +108,9 @@ export declare function flowArrows(symbols: EquipmentSymbols, ends: ({
108
108
  /** Complex power into each element (loads, generators, IBRs, shunts) by element id, and for
109
109
  * sources the power they draw (the negative of what they inject), so every attachment reads with
110
110
  * the same convention: positive is consumption. Branch ports are left out: names are unique only
111
- * within a family, and a line sharing a load's name must not add to it. */
112
- export declare function elementPowers(result: {
111
+ * within a family, and a line sharing a load's name must not add to it. Reads a full result's
112
+ * objects or its columns. */
113
+ export declare function elementPowers(result: McPfResultTables | {
113
114
  element_ports: {
114
115
  element: string;
115
116
  kind?: string;
@@ -620,7 +620,8 @@ const BRANCH_PORT_KINDS = new Set(['line', 'switch', 'transformer']);
620
620
  /** Complex power into each element (loads, generators, IBRs, shunts) by element id, and for
621
621
  * sources the power they draw (the negative of what they inject), so every attachment reads with
622
622
  * the same convention: positive is consumption. Branch ports are left out: names are unique only
623
- * within a family, and a line sharing a load's name must not add to it. */
623
+ * within a family, and a line sharing a load's name must not add to it. Reads a full result's
624
+ * objects or its columns. */
624
625
  export function elementPowers(result) {
625
626
  const out = new Map();
626
627
  const add = (id, p, q) => {
@@ -629,6 +630,17 @@ export function elementPowers(result) {
629
630
  row.q += q;
630
631
  out.set(id, row);
631
632
  };
633
+ if ('strings' in result) {
634
+ const names = result.strings;
635
+ const power = result.portPower;
636
+ for (let k = 0; k < result.portElement.length; k++)
637
+ if (!BRANCH_PORT_KINDS.has(names[result.portKind[k]]))
638
+ add(names[result.portElement[k]], power[2 * k], power[2 * k + 1]);
639
+ const source = result.sourcePower;
640
+ for (let k = 0; k < result.sourceName.length; k++)
641
+ add(names[result.sourceName[k]], -source[2 * k], -source[2 * k + 1]);
642
+ return out;
643
+ }
632
644
  for (const port of result.element_ports)
633
645
  if (port.kind === undefined || !BRANCH_PORT_KINDS.has(port.kind))
634
646
  add(port.element, port.power_into_element.re, port.power_into_element.im);
@@ -1,4 +1,4 @@
1
- import type { McComplex, McPfResult } from '@tellegen/engine';
1
+ import { type McComplex, type McPfResult, type McPfResultTables } from '@tellegen/engine';
2
2
  /** Which reference a pair voltage is measured against. */
3
3
  export type PairZone = 'phase–neutral' | 'phase–phase' | 'phase–ground';
4
4
  /** The second member of a phase–ground pair. */
@@ -52,6 +52,8 @@ export interface BranchSnapshot {
52
52
  export interface ResultSnapshot {
53
53
  kind: 'solved' | 'stress';
54
54
  label: string;
55
+ /** Qualification and effective settings retained by CSV exports. */
56
+ provenance?: string[];
55
57
  iterations: number | null;
56
58
  buses: BusSnapshot[];
57
59
  branches: BranchSnapshot[];
@@ -107,9 +109,10 @@ export interface SnapshotBranch {
107
109
  /** Voltage unbalance factor |V2| / |V1| in percent, from three phasors in phase order. The ratio
108
110
  * is the same for phase–neutral and phase–phase voltages, so either reference works. */
109
111
  export declare function sequenceUnbalance(a: McComplex, b: McComplex, c: McComplex): number | null;
110
- /** Project a full multiconductor power-flow result onto the drawing's bus and branch order.
112
+ /** Project a full multiconductor power-flow result onto the drawing's bus and branch order, from
113
+ * its object form or its columns (`McPfResultTables`, what a live session transfers).
111
114
  * Missing ports leave an end `null`; nothing is interpolated. */
112
- export declare function snapshotFromMcPf(result: McPfResult, buses: SnapshotBus[], branches: SnapshotBranch[], label?: string): ResultSnapshot;
115
+ export declare function snapshotFromMcPf(result: McPfResult | McPfResultTables, buses: SnapshotBus[], branches: SnapshotBranch[], label?: string): ResultSnapshot;
113
116
  /** Bus results from a live session's transferred terminal arrays (`[re, im]` interleaved), for when
114
117
  * the full result with element ports has not been fetched. Branches are left without results. */
115
118
  export declare function snapshotBusesFromTerminals(ids: ArrayLike<[string, string]>, voltages: ArrayLike<number>, buses: SnapshotBus[]): BusSnapshot[];
@@ -1,3 +1,4 @@
1
+ import { mcPfResultTables, mcLoadModelLabel, mcNetworkScopeLabel } from '@tellegen/engine';
1
2
  import { neutralTerminal } from '../multiconductor.js';
2
3
  /** The second member of a phase–ground pair. */
3
4
  export const GROUND = 'ground';
@@ -89,27 +90,34 @@ function busSnapshot(bus, voltage) {
89
90
  const vuf = phasors.length === 3 ? sequenceUnbalance(phasors[0], phasors[1], phasors[2]) : null;
90
91
  return { pairs, unbalance: vuf === null ? null : { percent: vuf, definition: 'VUF' } };
91
92
  }
92
- /** Project a full multiconductor power-flow result onto the drawing's bus and branch order.
93
+ /** Project a full multiconductor power-flow result onto the drawing's bus and branch order, from
94
+ * its object form or its columns (`McPfResultTables`, what a live session transfers).
93
95
  * Missing ports leave an end `null`; nothing is interpolated. */
94
96
  export function snapshotFromMcPf(result, buses, branches, label = 'Tellegen multiconductor power flow') {
95
- // Nested maps keyed by bus, then element: no composite string keys on a 100k-bus result.
97
+ const t = 'strings' in result ? result : mcPfResultTables(result);
98
+ const names = t.strings;
99
+ // Nested maps keyed by bus, then terminal or element, holding row numbers into the tables: no
100
+ // composite string keys and no object per row on a 100k-bus result.
96
101
  const voltages = new Map();
97
- for (const t of result.terminals) {
98
- let bus = voltages.get(t.bus);
102
+ for (let k = 0; k < t.terminalBus.length; k++) {
103
+ const id = names[t.terminalBus[k]];
104
+ let bus = voltages.get(id);
99
105
  if (!bus)
100
- voltages.set(t.bus, (bus = new Map()));
101
- bus.set(t.terminal, t.voltage);
106
+ voltages.set(id, (bus = new Map()));
107
+ bus.set(names[t.terminalName[k]], k);
102
108
  }
103
109
  const ports = new Map();
104
- for (const p of result.element_ports) {
105
- let element = ports.get(p.element);
110
+ for (let k = 0; k < t.portElement.length; k++) {
111
+ const id = names[t.portElement[k]];
112
+ let element = ports.get(id);
106
113
  if (!element)
107
- ports.set(p.element, (element = new Map()));
108
- const rows = element.get(p.bus);
114
+ ports.set(id, (element = new Map()));
115
+ const bus = names[t.portBus[k]];
116
+ const rows = element.get(bus);
109
117
  if (rows)
110
- rows.push(p);
118
+ rows.push(k);
111
119
  else
112
- element.set(p.bus, [p]);
120
+ element.set(bus, [k]);
113
121
  }
114
122
  /** The element's port rows, of its own kind when a line and a switch share a name. A closed
115
123
  * switch has no ports of its own (the solver merges its buses), so it must not borrow another
@@ -118,8 +126,10 @@ export function snapshotFromMcPf(result, buses, branches, label = 'Tellegen mult
118
126
  const all = ports.get(branch.id)?.get(bus);
119
127
  if (!all || branch.kind === undefined)
120
128
  return all;
121
- return all.filter((r) => r.kind === branch.kind);
129
+ return all.filter((k) => names[t.portKind[k]] === branch.kind);
122
130
  };
131
+ const power = t.portPower;
132
+ const current = t.portCurrent;
123
133
  const seen = new Set();
124
134
  const branchSnapshots = branches.map((branch) => {
125
135
  const key = `${branch.kind ?? ''}\u0000${branch.id}`;
@@ -132,33 +142,44 @@ export function snapshotFromMcPf(result, buses, branches, label = 'Tellegen mult
132
142
  let p = 0;
133
143
  let q = 0;
134
144
  const byTerminal = new Map();
135
- for (const r of rows) {
136
- p += r.power_into_element.re;
137
- q += r.power_into_element.im;
138
- byTerminal.set(r.terminal, r);
145
+ for (const k of rows) {
146
+ p += power[2 * k];
147
+ q += power[2 * k + 1];
148
+ byTerminal.set(names[t.portTerminal[k]], k);
139
149
  }
140
150
  const currents = [];
141
151
  for (const terminal of side ? branch.toTerminals : branch.fromTerminals) {
142
- const row = byTerminal.get(terminal);
143
- if (row)
144
- currents.push({ terminal, amps: magnitude(row.current_into_element) });
152
+ const k = byTerminal.get(terminal);
153
+ if (k !== undefined)
154
+ currents.push({ terminal, amps: Math.hypot(current[2 * k], current[2 * k + 1]) });
145
155
  }
146
156
  return { p: p / 1000, q: q / 1000, currents };
147
157
  });
148
158
  return {
149
159
  ends,
150
- ...loadingOf(branch, ends, elementPorts),
160
+ ...loadingOf(branch, ends, elementPorts, power),
151
161
  primary,
152
- loss: primary ? equipmentLoss(branch, elementPorts) : null
162
+ loss: primary ? equipmentLoss(branch, elementPorts, power) : null
153
163
  };
154
164
  });
165
+ const voltage = t.terminalVoltage;
155
166
  return {
156
167
  kind: 'solved',
157
- label,
158
- iterations: result.iterations,
168
+ label: `${label}: ${mcLoadModelLabel(t.load_model)}; ${mcNetworkScopeLabel(t.network_scope)}`,
169
+ provenance: [
170
+ JSON.stringify({
171
+ load_model: t.load_model ?? null,
172
+ network_scope: t.network_scope ?? null,
173
+ excluded_islands: t.excluded_islands ?? []
174
+ })
175
+ ],
176
+ iterations: t.iterations,
159
177
  buses: buses.map((bus) => {
160
178
  const terminals = voltages.get(bus.id);
161
- return busSnapshot(bus, (t) => terminals?.get(t));
179
+ return busSnapshot(bus, (terminal) => {
180
+ const k = terminals?.get(terminal);
181
+ return k === undefined ? undefined : { re: voltage[2 * k], im: voltage[2 * k + 1] };
182
+ });
162
183
  }),
163
184
  branches: branchSnapshots
164
185
  };
@@ -175,18 +196,18 @@ export function snapshotBusesFromTerminals(ids, voltages, buses) {
175
196
  }));
176
197
  }
177
198
  /** Power summed over a set of port rows, in kW and kvar. */
178
- function portPower(rows) {
199
+ function portPower(rows, power) {
179
200
  let p = 0;
180
201
  let q = 0;
181
- for (const r of rows) {
182
- p += r.power_into_element.re;
183
- q += r.power_into_element.im;
202
+ for (const k of rows) {
203
+ p += power[2 * k];
204
+ q += power[2 * k + 1];
184
205
  }
185
206
  return { p: p / 1000, q: q / 1000 };
186
207
  }
187
208
  /** Both ends' ports are signed into the equipment, so their sum over every bus it connects is what
188
209
  * it consumes. Each bus counts once, however many winding pairs join it. */
189
- function equipmentLoss(branch, rows) {
210
+ function equipmentLoss(branch, rows, power) {
190
211
  if (!branch.inService)
191
212
  return null;
192
213
  const buses = new Set([branch.from, branch.to]);
@@ -199,13 +220,13 @@ function equipmentLoss(branch, rows) {
199
220
  const at = rows(branch, bus);
200
221
  if (!at?.length)
201
222
  return null;
202
- const power = portPower(at);
203
- p += power.p;
204
- q += power.q;
223
+ const sum = portPower(at, power);
224
+ p += sum.p;
225
+ q += sum.q;
205
226
  }
206
227
  return { p, q };
207
228
  }
208
- function loadingOf(branch, ends, rows) {
229
+ function loadingOf(branch, ends, rows, power) {
209
230
  const rating = branch.rating;
210
231
  if (!rating || !ends[0] || !ends[1])
211
232
  return { loading: null, basis: null };
@@ -228,7 +249,7 @@ function loadingOf(branch, ends, rows) {
228
249
  const at = rows(branch, bus);
229
250
  if (!(va > 0) || !at?.length)
230
251
  continue;
231
- const { p, q } = portPower(at);
252
+ const { p, q } = portPower(at, power);
232
253
  const percent = (100 * Math.hypot(p, q) * 1000) / va;
233
254
  if (loading === null || percent > loading)
234
255
  loading = percent;
package/dist/index.d.ts CHANGED
@@ -30,3 +30,6 @@ export type { MultiView, PlacedMultiBus, PlacedMultiEdge } from './multiconducto
30
30
  export type { CaseFileSummary, DistAttachmentKind, DistEdgeKind, DistGraph, DistGraphAttachment, DistGraphBus, DistGraphEdge, DisplayPreview, Formulation, IngestedCase, IngestedDistCase, IngestedJsonDrop, JsonDropClassification, JsonDropKind, CapacityPlanBusWeightJson, CapacityPlanIterationJson, CapacityPlanOutcomeJson, CapacityPlanRatingChangeJson, CapacityPlanResultSummaryJson, CapacityPlanSpecJson, ImplicitObjectiveJson, SensTarget, Topology, TopologyBranch, TopologyBus } from '@tellegen/engine';
31
31
  export { default as McResults } from './components/McResults.svelte';
32
32
  export { default as McWorkbench } from './components/McWorkbench.svelte';
33
+ export { default as McPfSettings } from './components/McPfSettings.svelte';
34
+ export { default as McModelQualification } from './components/McModelQualification.svelte';
35
+ export { default as McNetworkScope } from './components/McNetworkScope.svelte';
package/dist/index.js CHANGED
@@ -19,3 +19,6 @@ export { caseDeltas, caseRatings, displayMetaFor, displaySeriesFor } from './dis
19
19
  export { DEFAULT_FORMULATION, FORMULATIONS, BrowserStudy, Study, capabilities, createStudy, errorText, formatOf, classifyJson, ingestJsonDrop, ingestCase, isDisplayFile, isPermanentEngineFailure, parseDisplay, solveModule } from '@tellegen/engine';
20
20
  export { default as McResults } from './components/McResults.svelte';
21
21
  export { default as McWorkbench } from './components/McWorkbench.svelte';
22
+ export { default as McPfSettings } from './components/McPfSettings.svelte';
23
+ export { default as McModelQualification } from './components/McModelQualification.svelte';
24
+ export { default as McNetworkScope } from './components/McNetworkScope.svelte';
@@ -110,6 +110,43 @@ function withRoutes(view, layer) {
110
110
  return view;
111
111
  const routes = new Map();
112
112
  const same = (a, b) => typeof a === 'string' && a.toLowerCase() === b.toLowerCase();
113
+ // Index the edges once by lowercased id and by lowercased directed endpoint
114
+ // pair. A city case carries tens of thousands of routes and edges, so a scan
115
+ // of every edge per feature is quadratic.
116
+ const byId = new Map();
117
+ const byEnds = new Map();
118
+ for (const edge of view.edges) {
119
+ const id = edge.id.toLowerCase();
120
+ const from = edge.from.toLowerCase();
121
+ const to = edge.to.toLowerCase();
122
+ const withId = byId.get(id);
123
+ if (withId)
124
+ withId.push(edge);
125
+ else
126
+ byId.set(id, [edge]);
127
+ let fromEnds = byEnds.get(from);
128
+ if (!fromEnds)
129
+ byEnds.set(from, (fromEnds = new Map()));
130
+ const between = fromEnds.get(to);
131
+ if (between)
132
+ between.push(edge);
133
+ else
134
+ fromEnds.set(to, [edge]);
135
+ }
136
+ const directed = (from, to) => byEnds.get(from)?.get(to) ?? [];
137
+ // The edges `same` would match: by id when the feature names one, otherwise
138
+ // by its endpoints in either direction. A self loop's two directions are one
139
+ // set, so it is not counted twice.
140
+ const matching = (id, from, to) => {
141
+ if (id !== undefined)
142
+ return typeof id === 'string' ? (byId.get(id.toLowerCase()) ?? []) : [];
143
+ if (typeof from !== 'string' || typeof to !== 'string')
144
+ return [];
145
+ const f = from.toLowerCase();
146
+ const t = to.toLowerCase();
147
+ const forward = directed(f, t);
148
+ return f === t ? forward : [...forward, ...directed(t, f)];
149
+ };
113
150
  for (const feature of features) {
114
151
  const props = feature?.properties;
115
152
  const geometry = feature?.geometry;
@@ -126,10 +163,7 @@ function withRoutes(view, layer) {
126
163
  const from = props.from ?? props.bus_from;
127
164
  const to = props.to ?? props.bus_to;
128
165
  const id = props.id ?? props.uid;
129
- const matches = view.edges.filter((edge) => id !== undefined
130
- ? same(id, edge.id)
131
- : (same(from, edge.from) && same(to, edge.to)) ||
132
- (same(from, edge.to) && same(to, edge.from)));
166
+ const matches = matching(id, from, to);
133
167
  if (matches.length !== 1)
134
168
  continue;
135
169
  const edge = matches[0];