@tellegen/svelte 0.4.2 → 0.4.4
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 +20 -0
- package/dist/TellegenMap.svelte +63 -22
- package/dist/TellegenProvider.svelte +7 -4
- package/dist/TellegenShell.svelte +10 -1
- package/dist/TellegenViewer.svelte +2 -0
- package/dist/basemap.d.ts +14 -0
- package/dist/basemap.js +27 -0
- package/dist/colors.d.ts +6 -1
- package/dist/colors.js +11 -4
- package/dist/components/DemandSlider.svelte +0 -2
- package/dist/components/FormulationSelector.svelte +1 -1
- package/dist/components/LocalCaseDetails.svelte +18 -3
- package/dist/components/McResults.svelte +18 -3
- package/dist/components/McWorkbench.svelte +688 -0
- package/dist/components/McWorkbench.svelte.d.ts +38 -0
- package/dist/components/MultiWire.svelte +136 -0
- package/dist/components/MultiWire.svelte.d.ts +7 -0
- package/dist/components/MulticonductorDetails.svelte +7 -0
- package/dist/components/OmniSearch.svelte +163 -0
- package/dist/components/OmniSearch.svelte.d.ts +14 -0
- package/dist/components/ResultsTable.svelte +259 -0
- package/dist/components/ResultsTable.svelte.d.ts +13 -0
- package/dist/components/SchematicCanvas.svelte +1896 -0
- package/dist/components/SchematicCanvas.svelte.d.ts +13 -0
- package/dist/components/SolveCard.svelte +6 -0
- package/dist/components/schematic/PickCard.svelte +126 -0
- package/dist/components/schematic/PickCard.svelte.d.ts +56 -0
- package/dist/components/schematic/export.d.ts +21 -0
- package/dist/components/schematic/export.js +110 -0
- package/dist/components/schematic/layers.d.ts +161 -0
- package/dist/components/schematic/layers.js +756 -0
- package/dist/components/schematic/paint.d.ts +28 -0
- package/dist/components/schematic/paint.js +69 -0
- package/dist/components/schematic/palette.d.ts +34 -0
- package/dist/components/schematic/palette.js +54 -0
- package/dist/components/schematic/text.d.ts +83 -0
- package/dist/components/schematic/text.js +190 -0
- package/dist/components/schematic/view-prefs.svelte.d.ts +30 -0
- package/dist/components/schematic/view-prefs.svelte.js +77 -0
- package/dist/components.d.ts +1 -0
- package/dist/components.js +1 -0
- package/dist/context.svelte.d.ts +2 -0
- package/dist/context.svelte.js +2 -0
- package/dist/controller.svelte.d.ts +33 -5
- package/dist/controller.svelte.js +173 -20
- package/dist/diagram/basemap.d.ts +28 -0
- package/dist/diagram/basemap.js +39 -0
- package/dist/diagram/bus-stacks.d.ts +19 -0
- package/dist/diagram/bus-stacks.js +99 -0
- package/dist/diagram/colour-modes.d.ts +18 -0
- package/dist/diagram/colour-modes.js +33 -0
- package/dist/diagram/connections.d.ts +30 -0
- package/dist/diagram/connections.js +129 -0
- package/dist/diagram/display-tree.d.ts +21 -0
- package/dist/diagram/display-tree.js +65 -0
- package/dist/diagram/energisation.d.ts +10 -0
- package/dist/diagram/energisation.js +31 -0
- package/dist/diagram/feeders.d.ts +30 -0
- package/dist/diagram/feeders.js +57 -0
- package/dist/diagram/galvanic-zones.d.ts +60 -0
- package/dist/diagram/galvanic-zones.js +210 -0
- package/dist/diagram/graph-inputs.d.ts +18 -0
- package/dist/diagram/graph-inputs.js +103 -0
- package/dist/diagram/index.d.ts +28 -0
- package/dist/diagram/index.js +28 -0
- package/dist/diagram/landmarks.d.ts +50 -0
- package/dist/diagram/landmarks.js +153 -0
- package/dist/diagram/layout-morph.d.ts +15 -0
- package/dist/diagram/layout-morph.js +32 -0
- package/dist/diagram/multi-wire.d.ts +44 -0
- package/dist/diagram/multi-wire.js +65 -0
- package/dist/diagram/phase-bars.d.ts +35 -0
- package/dist/diagram/phase-bars.js +44 -0
- package/dist/diagram/results-table.d.ts +47 -0
- package/dist/diagram/results-table.js +173 -0
- package/dist/diagram/schematic-equipment.d.ts +133 -0
- package/dist/diagram/schematic-equipment.js +645 -0
- package/dist/diagram/schematic-layout.d.ts +51 -0
- package/dist/diagram/schematic-layout.js +240 -0
- package/dist/diagram/search.d.ts +36 -0
- package/dist/diagram/search.js +48 -0
- package/dist/diagram/shared-routes.d.ts +21 -0
- package/dist/diagram/shared-routes.js +40 -0
- package/dist/diagram/short-names.d.ts +11 -0
- package/dist/diagram/short-names.js +54 -0
- package/dist/diagram/snapshot.d.ts +115 -0
- package/dist/diagram/snapshot.js +242 -0
- package/dist/diagram/summary.d.ts +65 -0
- package/dist/diagram/summary.js +144 -0
- package/dist/diagram/svg-export.d.ts +80 -0
- package/dist/diagram/svg-export.js +108 -0
- package/dist/diagram/tree-walk.d.ts +13 -0
- package/dist/diagram/tree-walk.js +83 -0
- package/dist/diagram/value-labels.d.ts +24 -0
- package/dist/diagram/value-labels.js +69 -0
- package/dist/diagram/view-state.d.ts +12 -0
- package/dist/diagram/view-state.js +22 -0
- package/dist/diagram/voltage-levels.d.ts +14 -0
- package/dist/diagram/voltage-levels.js +20 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/state.svelte.d.ts +37 -1
- package/dist/state.svelte.js +35 -0
- package/dist/transformer-icon.d.ts +22 -13
- package/dist/transformer-icon.js +23 -13
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type TellegenApiClient } from './api.js';
|
|
2
2
|
import type { BranchRatingDeltas, Network, NetworkBus, SensitivityColumn, Solution } from './api.js';
|
|
3
3
|
import { CaseState, LocalCase, MulticonductorCase, type AppState, type DemandRangeMode, type SolvableCase } from './state.svelte.js';
|
|
4
|
-
import { type EngineTransport, type McPfDetail, type McPfDetailQuery, type McPfOptions, type McPfSummary, type McStudySnapshot, type BrowserStudy, type Formulation, type IngestedCase, type PowerIoDiagnostic, type SensTarget } from '@tellegen/engine';
|
|
4
|
+
import { type EngineTransport, type McPfDetail, type McPfDetailQuery, type McPfOptions, type McPfResult, type McPfSummary, type McStudySnapshot, type BrowserStudy, type Formulation, type IngestedCase, type PowerIoDiagnostic, type SensTarget } from '@tellegen/engine';
|
|
5
5
|
/** A parsed geographic sidecar awaiting application: the canonical `.geo.json`
|
|
6
6
|
* layer document from the engine's tolerant reader, its source file name, and
|
|
7
7
|
* the reader's notes on records it could not use. */
|
|
@@ -34,6 +34,7 @@ export interface ControllerOptions {
|
|
|
34
34
|
/** Whether `load()` fetches the bundled cases. Default `true`. With
|
|
35
35
|
* `false`, `load()` only marks the shell loaded and frames the map. */
|
|
36
36
|
loadDefaultCases?: boolean;
|
|
37
|
+
acOpfWasmUrl?: string;
|
|
37
38
|
mcTransport?: Pick<EngineTransport, 'solveMcModule' | 'applyMcGeo'> & Partial<Pick<EngineTransport, 'solveMcStudy' | 'applyMcStudyGeo' | 'createMcPfSession'>>;
|
|
38
39
|
}
|
|
39
40
|
/** Input to `Controller.openBalancedModule` and `replaceLocalCase`: a
|
|
@@ -66,6 +67,12 @@ export declare class Controller {
|
|
|
66
67
|
abort: AbortController | null;
|
|
67
68
|
sensitivity429Until: number;
|
|
68
69
|
serverComputeOff: boolean;
|
|
70
|
+
/** The optional EPL-bearing WASI asset is never inferred or bundled. */
|
|
71
|
+
private readonly acOpfWasmUrl;
|
|
72
|
+
private acOpfProbeStarted;
|
|
73
|
+
private readonly acOpfProbeAbort;
|
|
74
|
+
private readonly acOpfSolves;
|
|
75
|
+
acOpfAvailable: boolean;
|
|
69
76
|
caseStudies: WeakMap<SolvableCase, {
|
|
70
77
|
study: BrowserStudy;
|
|
71
78
|
studyInputJson: string;
|
|
@@ -111,6 +118,9 @@ export declare class Controller {
|
|
|
111
118
|
/** True when two selection targets name the same bus or branch. */
|
|
112
119
|
sameTarget(a: SensTarget, b: SensTarget): boolean;
|
|
113
120
|
isPerturbed(c: SolvableCase | null): boolean;
|
|
121
|
+
formulationAvailable(c: SolvableCase, formulation: Formulation): boolean;
|
|
122
|
+
probeAcOpf: () => void;
|
|
123
|
+
caseExportUnavailableReason(c: SolvableCase): string | null;
|
|
114
124
|
bumpRevision(c: SolvableCase): void;
|
|
115
125
|
setNearbyRangeAnchor(c: SolvableCase, bus: number, delta?: number): void;
|
|
116
126
|
beginBusSelection(c: SolvableCase, busId: number): {
|
|
@@ -131,10 +141,10 @@ export declare class Controller {
|
|
|
131
141
|
getStudy(c: SolvableCase, studyInputJson: string): Promise<BrowserStudy | null>;
|
|
132
142
|
browserSensitivity(c: SolvableCase, studyInputJson: string, target: SensTarget): Promise<SensitivityColumn | null>;
|
|
133
143
|
/** Release every engine handle this controller holds: each case's browser
|
|
134
|
-
* Study, including one still being built,
|
|
135
|
-
* session, with their pending edits and in-flight requests
|
|
136
|
-
* `TellegenProvider` calls it when it unmounts;
|
|
137
|
-
* Study and keeps no new session afterwards. */
|
|
144
|
+
* Study, including one still being built, its AC OPF solve, each
|
|
145
|
+
* multiconductor session, with their pending edits and in-flight requests,
|
|
146
|
+
* and the AC OPF asset probe. `TellegenProvider` calls it when it unmounts;
|
|
147
|
+
* the controller builds no Study and keeps no new session afterwards. */
|
|
138
148
|
dispose(): void;
|
|
139
149
|
disposeStudy(c: SolvableCase): void;
|
|
140
150
|
acceptSensitivity(c: SolvableCase, col: SensitivityColumn | null, target: SensTarget | null, sensitivitySeq?: number): void;
|
|
@@ -354,6 +364,24 @@ export declare class Controller {
|
|
|
354
364
|
* operating point, from the live session or the stored full result. Null
|
|
355
365
|
* when the operating point changed before the page arrived. */
|
|
356
366
|
multiResultDetail: (c: MulticonductorCase, query: McPfDetailQuery) => Promise<McPfDetail | null>;
|
|
367
|
+
/** Above this many buses, reading and indexing the whole operating point takes a noticeable
|
|
368
|
+
* fraction of a second (evidence/diagram), so the overview is computed on request instead of
|
|
369
|
+
* after every solve. */
|
|
370
|
+
static readonly AUTO_OVERVIEW_MAX_BUSES = 25000;
|
|
371
|
+
private overviewSeq;
|
|
372
|
+
/** Whether this case computes its overview only on request. */
|
|
373
|
+
overviewOnRequest: (c: MulticonductorCase) => boolean;
|
|
374
|
+
/** Read the whole operating point into `c.overviewResult` for the overview and the diagram. The
|
|
375
|
+
* previous result stays visible until the read lands; a newer read supersedes it. */
|
|
376
|
+
readOverview: (c: MulticonductorCase) => void;
|
|
377
|
+
private startOverviewRead;
|
|
378
|
+
/** Follow the active case's solves: read its operating point once load edits settle, or clear it
|
|
379
|
+
* for a case that computes the overview on request. Call from an effect that tracks the case's
|
|
380
|
+
* `result` and `mcFullResult`; the returned function cancels a pending read. */
|
|
381
|
+
followOverview: (c: MulticonductorCase | null) => (() => void) | undefined;
|
|
382
|
+
/** The complete operating point for whole-network views: the stored result, or one read from
|
|
383
|
+
* the live session. Null when there is no result or it changed before the read finished. */
|
|
384
|
+
multiFullResult: (c: MulticonductorCase) => Promise<McPfResult | null>;
|
|
357
385
|
/** Every terminal's identity, voltage, and injected current, as numeric
|
|
358
386
|
* arrays of interleaved `[re, im]` values. */
|
|
359
387
|
multiTerminalTable: (c: MulticonductorCase) => Promise<{
|
|
@@ -6,7 +6,7 @@ import { placeSyntheticTopology } from './synthetic-layout.js';
|
|
|
6
6
|
import { distExtensionFormat, isGeoFileName } from './drop-classify.js';
|
|
7
7
|
import { DropBatchGate, readDropFileBytes, validateDropBatch } from './drop-limits.js';
|
|
8
8
|
import { buildGeographicView, buildDiagramView, placeMultiView } from './multiconductor.js';
|
|
9
|
-
import { browserWasmTransport, applyDisplayGeo, applyGeo, applyLayout, createStudy, extractGeo, FORMULATIONS, formatPowerIoDiagnostic, formatOf, ingestCase, ingestDistCaseBytes, ingestJsonDrop, isDisplayFile, isPermanentEngineFailure, parseDisplay, parseGeo, mcPfResultDetail, summarizeMcPfResult } from '@tellegen/engine';
|
|
9
|
+
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
10
|
import { browserSolveFailure, errorText, extent, formulationLabel, rgbaCss } from './format.js';
|
|
11
11
|
import { DEAD_ZONE_MW } from './slider.js';
|
|
12
12
|
const HIDDEN_DEFAULT_CASES_KEY = 'tellegen.hiddenDefaultCases.v1';
|
|
@@ -83,6 +83,12 @@ export class Controller {
|
|
|
83
83
|
// Latched once any server fallback answers 403: compute is disabled on this
|
|
84
84
|
// deploy, so later fallbacks show the notice instead of firing doomed requests.
|
|
85
85
|
serverComputeOff = false;
|
|
86
|
+
/** The optional EPL-bearing WASI asset is never inferred or bundled. */
|
|
87
|
+
acOpfWasmUrl;
|
|
88
|
+
acOpfProbeStarted = false;
|
|
89
|
+
acOpfProbeAbort = new AbortController();
|
|
90
|
+
acOpfSolves = new Map();
|
|
91
|
+
acOpfAvailable = $state(false);
|
|
86
92
|
// Build-once browser Study per case: the retained PowerIO module is parsed and the model built
|
|
87
93
|
// when the Study is created, so a drag re-solves (commit) and previews without
|
|
88
94
|
// re-parsing. Kept in a WeakMap off the reactive/raw case payloads — the wasm
|
|
@@ -131,6 +137,7 @@ export class Controller {
|
|
|
131
137
|
this.api = options.api ?? createApiClient({ apiBase: options.apiBase });
|
|
132
138
|
this.loadDefaultCases = options.loadDefaultCases ?? true;
|
|
133
139
|
this.mcTransport = options.mcTransport ?? browserWasmTransport;
|
|
140
|
+
this.acOpfWasmUrl = options.acOpfWasmUrl ?? null;
|
|
134
141
|
}
|
|
135
142
|
// ===== helpers =====
|
|
136
143
|
isBackendCase(c) {
|
|
@@ -158,6 +165,31 @@ export class Controller {
|
|
|
158
165
|
isPerturbed(c) {
|
|
159
166
|
return c?.perturbed ?? false;
|
|
160
167
|
}
|
|
168
|
+
formulationAvailable(c, formulation) {
|
|
169
|
+
if (formulation === 'acopf')
|
|
170
|
+
return this.acOpfAvailable && !c.perturbed;
|
|
171
|
+
return !FORMULATIONS.find((entry) => entry.id === formulation)?.disabled;
|
|
172
|
+
}
|
|
173
|
+
probeAcOpf = () => {
|
|
174
|
+
if (this.disposed || this.acOpfProbeStarted || !this.acOpfWasmUrl)
|
|
175
|
+
return;
|
|
176
|
+
this.acOpfProbeStarted = true;
|
|
177
|
+
void probeAcOpfWorker(this.acOpfWasmUrl, undefined, this.acOpfProbeAbort.signal).then((available) => {
|
|
178
|
+
if (this.disposed)
|
|
179
|
+
return;
|
|
180
|
+
this.acOpfAvailable = available;
|
|
181
|
+
// A declared instance can be dropped while the optional asset is probing.
|
|
182
|
+
const c = this.app.activeLocal;
|
|
183
|
+
if (available && c?.formulation === 'acopf' && !c.solving) {
|
|
184
|
+
this.maybeStartLocalSolve(c.id);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
};
|
|
188
|
+
caseExportUnavailableReason(c) {
|
|
189
|
+
return c.formulation === 'acopf'
|
|
190
|
+
? 'Saving and exporting AC OPF cases is not supported yet.'
|
|
191
|
+
: null;
|
|
192
|
+
}
|
|
161
193
|
bumpRevision(c) {
|
|
162
194
|
c.revisionGeneration += 1;
|
|
163
195
|
}
|
|
@@ -352,18 +384,24 @@ export class Controller {
|
|
|
352
384
|
return study ? study.sensitivity(c.id, this.caseDeltas(c), this.caseRatings(c), target) : null;
|
|
353
385
|
}
|
|
354
386
|
/** Release every engine handle this controller holds: each case's browser
|
|
355
|
-
* Study, including one still being built,
|
|
356
|
-
* session, with their pending edits and in-flight requests
|
|
357
|
-
* `TellegenProvider` calls it when it unmounts;
|
|
358
|
-
* Study and keeps no new session afterwards. */
|
|
387
|
+
* Study, including one still being built, its AC OPF solve, each
|
|
388
|
+
* multiconductor session, with their pending edits and in-flight requests,
|
|
389
|
+
* and the AC OPF asset probe. `TellegenProvider` calls it when it unmounts;
|
|
390
|
+
* the controller builds no Study and keeps no new session afterwards. */
|
|
359
391
|
dispose() {
|
|
360
392
|
if (this.disposed)
|
|
361
393
|
return;
|
|
362
394
|
this.disposed = true;
|
|
363
395
|
this.abort?.abort();
|
|
364
396
|
this.abort = null;
|
|
365
|
-
|
|
397
|
+
this.acOpfProbeAbort.abort();
|
|
398
|
+
for (const cancellation of this.acOpfSolves.values())
|
|
399
|
+
cancellation.abort();
|
|
400
|
+
this.acOpfSolves.clear();
|
|
401
|
+
for (const c of [...this.app.cases, ...this.app.localCases]) {
|
|
402
|
+
c.solveSeq = (c.solveSeq ?? 0) + 1;
|
|
366
403
|
this.disposeStudy(c);
|
|
404
|
+
}
|
|
367
405
|
for (const c of this.app.multiCases) {
|
|
368
406
|
c.solveAbort?.abort();
|
|
369
407
|
if (c.mcEditTimer !== null)
|
|
@@ -375,6 +413,8 @@ export class Controller {
|
|
|
375
413
|
}
|
|
376
414
|
// Release a case's Study (if any) when the case is removed.
|
|
377
415
|
disposeStudy(c) {
|
|
416
|
+
this.acOpfSolves.get(c)?.abort();
|
|
417
|
+
this.acOpfSolves.delete(c);
|
|
378
418
|
const cached = this.caseStudies.get(c);
|
|
379
419
|
if (cached) {
|
|
380
420
|
cached.study.free();
|
|
@@ -557,7 +597,9 @@ export class Controller {
|
|
|
557
597
|
sliderBounds = $derived.by(() => this.demandBounds(this.app.demandRangeMode, this.selectedBusData, this.nearbyRangeCenter));
|
|
558
598
|
sliderMin = $derived(this.sliderBounds.min);
|
|
559
599
|
sliderMax = $derived(this.sliderBounds.max);
|
|
560
|
-
sliderDisabled = $derived(
|
|
600
|
+
sliderDisabled = $derived(this.activeFormulation === 'acopf' ||
|
|
601
|
+
!this.selectedBusData ||
|
|
602
|
+
isDisplayOnlyElement(this.selectedBusData));
|
|
561
603
|
committedRating = $derived.by(() => this.activeSolvable && this.app.selectedBranch !== null
|
|
562
604
|
? (this.caseRatings(this.activeSolvable)[this.app.selectedBranch] ?? 0)
|
|
563
605
|
: 0);
|
|
@@ -569,7 +611,7 @@ export class Controller {
|
|
|
569
611
|
// to perturb.
|
|
570
612
|
ratingBounds = $derived.by(() => {
|
|
571
613
|
const b = this.selectedBranchData;
|
|
572
|
-
if (!b || isDisplayOnlyElement(b) || b.rate_mw <= 0)
|
|
614
|
+
if (this.activeFormulation === 'acopf' || !b || isDisplayOnlyElement(b) || b.rate_mw <= 0)
|
|
573
615
|
return { min: 0, max: 0, disabled: true };
|
|
574
616
|
const span = Math.min(50, Math.max(5, 0.2 * b.rate_mw));
|
|
575
617
|
return {
|
|
@@ -681,6 +723,7 @@ export class Controller {
|
|
|
681
723
|
if (this.loading)
|
|
682
724
|
return this.loading;
|
|
683
725
|
this.loading = (async () => {
|
|
726
|
+
this.probeAcOpf();
|
|
684
727
|
// Learn the deploy's compute gate up front so fallback paths pick honest
|
|
685
728
|
// copy and skip doomed requests (the SSE stream cannot see a 403). On
|
|
686
729
|
// failure assume enabled; the 403 latch in fetchServerColumn still
|
|
@@ -842,7 +885,7 @@ export class Controller {
|
|
|
842
885
|
this.app.activeCaseId = null;
|
|
843
886
|
this.leaveMulti();
|
|
844
887
|
this.app.activeLocalId = c.id;
|
|
845
|
-
if (c.formulation === 'acpf' && this.app.displayMode === 'price')
|
|
888
|
+
if ((c.formulation === 'acpf' || c.formulation === 'acopf') && this.app.displayMode === 'price')
|
|
846
889
|
this.app.displayMode = 'voltage';
|
|
847
890
|
this.app.placingLocalId = c.coordsKind === 'synthetic_pending' && !c.diagram ? c.id : null;
|
|
848
891
|
if (c.view || c.diagram || c.substations)
|
|
@@ -868,7 +911,7 @@ export class Controller {
|
|
|
868
911
|
this.app.activeCaseId = null;
|
|
869
912
|
this.leaveMulti();
|
|
870
913
|
this.app.addLocal(c);
|
|
871
|
-
if (c.formulation === 'acpf' && this.app.displayMode === 'price')
|
|
914
|
+
if ((c.formulation === 'acpf' || c.formulation === 'acopf') && this.app.displayMode === 'price')
|
|
872
915
|
this.app.displayMode = 'voltage';
|
|
873
916
|
if (c.diagram)
|
|
874
917
|
this.app.placingLocalId = null;
|
|
@@ -1049,6 +1092,7 @@ export class Controller {
|
|
|
1049
1092
|
copy.iterations = source.iterations;
|
|
1050
1093
|
copy.solveMs = source.solveMs;
|
|
1051
1094
|
copy.solveBackend = source.solveBackend;
|
|
1095
|
+
copy.solveDetail = source.solveDetail;
|
|
1052
1096
|
copy.network = source.network;
|
|
1053
1097
|
if (source.network) {
|
|
1054
1098
|
copy.view = {
|
|
@@ -1121,7 +1165,8 @@ export class Controller {
|
|
|
1121
1165
|
'baseSolution',
|
|
1122
1166
|
'iterations',
|
|
1123
1167
|
'solveMs',
|
|
1124
|
-
'solveBackend'
|
|
1168
|
+
'solveBackend',
|
|
1169
|
+
'solveDetail'
|
|
1125
1170
|
])
|
|
1126
1171
|
Object.assign(copy, { [key]: source[key] });
|
|
1127
1172
|
const captured = this.detachedCaseCaptures.get(source);
|
|
@@ -1276,7 +1321,7 @@ export class Controller {
|
|
|
1276
1321
|
if (ac.signal.aborted)
|
|
1277
1322
|
return;
|
|
1278
1323
|
}
|
|
1279
|
-
if (c.formulation === 'acpf')
|
|
1324
|
+
if (c.formulation === 'acpf' || c.formulation === 'acopf')
|
|
1280
1325
|
return;
|
|
1281
1326
|
try {
|
|
1282
1327
|
// The column from the browser Study (under the case's formulation). DC OPF
|
|
@@ -1412,7 +1457,7 @@ export class Controller {
|
|
|
1412
1457
|
this.app.placingLocalId = null;
|
|
1413
1458
|
const { ac, sensitivitySeq } = this.beginBusSelection(c, busId);
|
|
1414
1459
|
try {
|
|
1415
|
-
if (c.formulation === 'acpf')
|
|
1460
|
+
if (c.formulation === 'acpf' || c.formulation === 'acopf')
|
|
1416
1461
|
return;
|
|
1417
1462
|
const sensitivity = await this.browserSensitivity(c, c.studyInputJson, {
|
|
1418
1463
|
bus: busId
|
|
@@ -1471,7 +1516,7 @@ export class Controller {
|
|
|
1471
1516
|
if (ac.signal.aborted)
|
|
1472
1517
|
return;
|
|
1473
1518
|
}
|
|
1474
|
-
if (c.formulation === 'acpf')
|
|
1519
|
+
if (c.formulation === 'acpf' || c.formulation === 'acopf')
|
|
1475
1520
|
return;
|
|
1476
1521
|
const sensitivity = await this.browserSensitivity(c, c.studyInputJson, {
|
|
1477
1522
|
branch: branchId
|
|
@@ -1535,8 +1580,12 @@ export class Controller {
|
|
|
1535
1580
|
// fall back: the server solves at base ratings, so a Study failure there is
|
|
1536
1581
|
// terminal.
|
|
1537
1582
|
runSolve = (c, target) => {
|
|
1538
|
-
if (
|
|
1583
|
+
if (this.disposed)
|
|
1584
|
+
return;
|
|
1585
|
+
if (c.formulation === 'acpf' || c.formulation === 'acopf')
|
|
1539
1586
|
target = null;
|
|
1587
|
+
this.acOpfSolves.get(c)?.abort();
|
|
1588
|
+
this.acOpfSolves.delete(c);
|
|
1540
1589
|
// Cancel this case's own previous server stream, if any (backend only).
|
|
1541
1590
|
if (this.isBackendCase(c)) {
|
|
1542
1591
|
c.closeStream?.();
|
|
@@ -1547,11 +1596,12 @@ export class Controller {
|
|
|
1547
1596
|
this.app.error = null;
|
|
1548
1597
|
c.solving = true;
|
|
1549
1598
|
c.solveBackend = null;
|
|
1599
|
+
c.solveDetail = null;
|
|
1550
1600
|
c.solveFallbackReason = null;
|
|
1551
1601
|
c.iterations = [];
|
|
1552
1602
|
c.solveMs = null;
|
|
1553
1603
|
this.ensureStudyInputJson(c).then(async (studyInputJson) => {
|
|
1554
|
-
if (seq !== (c.solveSeq ?? 0))
|
|
1604
|
+
if (this.disposed || seq !== (c.solveSeq ?? 0))
|
|
1555
1605
|
return;
|
|
1556
1606
|
if (!studyInputJson) {
|
|
1557
1607
|
c.solveFallbackReason ??= 'PowerIO module unavailable';
|
|
@@ -1574,6 +1624,45 @@ export class Controller {
|
|
|
1574
1624
|
return;
|
|
1575
1625
|
}
|
|
1576
1626
|
const t0 = performance.now();
|
|
1627
|
+
if (c.formulation === 'acopf') {
|
|
1628
|
+
if (!this.acOpfAvailable || !this.acOpfWasmUrl) {
|
|
1629
|
+
c.solving = false;
|
|
1630
|
+
this.app.error = `${this.caseName(c)}: the experimental AC OPF worker is unavailable`;
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
if (c.perturbed) {
|
|
1634
|
+
c.solving = false;
|
|
1635
|
+
this.app.error = `${this.caseName(c)}: AC OPF currently solves the canonical base case only; reset demand and rating edits first`;
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
const cancellation = new AbortController();
|
|
1639
|
+
this.acOpfSolves.set(c, cancellation);
|
|
1640
|
+
c.solveBackend = 'pounce-wasi';
|
|
1641
|
+
try {
|
|
1642
|
+
const response = await solveAcOpfModule(this.acOpfWasmUrl, studyInputJson, cancellation.signal);
|
|
1643
|
+
if (seq !== (c.solveSeq ?? 0) || cancellation.signal.aborted)
|
|
1644
|
+
return;
|
|
1645
|
+
c.solution = solveResponseToSolution(response);
|
|
1646
|
+
const diagnostics = response.iterations;
|
|
1647
|
+
c.solveDetail =
|
|
1648
|
+
diagnostics && !Array.isArray(diagnostics) && 'nlp_iterations' in diagnostics
|
|
1649
|
+
? `${diagnostics.total_nlp_iterations ?? diagnostics.nlp_iterations} NLP iterations${diagnostics.recovered_by ? ' (recovered)' : ''}`
|
|
1650
|
+
: 'POUNCE WASI';
|
|
1651
|
+
c.solveMs = Math.round(performance.now() - t0);
|
|
1652
|
+
this.finishSolve(c, seq, null);
|
|
1653
|
+
}
|
|
1654
|
+
catch (error) {
|
|
1655
|
+
if (seq !== (c.solveSeq ?? 0) || cancellation.signal.aborted)
|
|
1656
|
+
return;
|
|
1657
|
+
c.solving = false;
|
|
1658
|
+
this.app.error = `${this.caseName(c)}: AC OPF failed in the isolated browser worker: ${errorText(error)}`;
|
|
1659
|
+
}
|
|
1660
|
+
finally {
|
|
1661
|
+
if (this.acOpfSolves.get(c) === cancellation)
|
|
1662
|
+
this.acOpfSolves.delete(c);
|
|
1663
|
+
}
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1577
1666
|
c.solveBackend = 'clarabel-wasm';
|
|
1578
1667
|
// Build-once Study path: commit the new operating point (no re-parse). The
|
|
1579
1668
|
// selected nodal value sensitivity column is computed in the same
|
|
@@ -1763,12 +1852,12 @@ export class Controller {
|
|
|
1763
1852
|
this.fail('This PowerIO instance requires its declared calculation.');
|
|
1764
1853
|
return;
|
|
1765
1854
|
}
|
|
1766
|
-
if (
|
|
1855
|
+
if (!this.formulationAvailable(c, next))
|
|
1767
1856
|
return;
|
|
1768
1857
|
c.formulation = next;
|
|
1769
1858
|
// Demand does not depend on the calculation, so a demand colouring stays.
|
|
1770
1859
|
if (this.app.displayMode !== 'demand')
|
|
1771
|
-
this.app.displayMode = next === 'acpf' ? 'voltage' : 'price';
|
|
1860
|
+
this.app.displayMode = next === 'acpf' || next === 'acopf' ? 'voltage' : 'price';
|
|
1772
1861
|
this.bumpRevision(c);
|
|
1773
1862
|
// The committed point carries over (same demand), but the model and its solution do
|
|
1774
1863
|
// not; drop the Study and the cached solutions so they rebuild under `next`.
|
|
@@ -1777,6 +1866,7 @@ export class Controller {
|
|
|
1777
1866
|
c.solution = null;
|
|
1778
1867
|
c.iterations = [];
|
|
1779
1868
|
c.solveMs = null;
|
|
1869
|
+
c.solveDetail = null;
|
|
1780
1870
|
c.predictedObjective = null;
|
|
1781
1871
|
this.app.previewPrices = null;
|
|
1782
1872
|
this.previewObjective = null;
|
|
@@ -2299,6 +2389,64 @@ export class Controller {
|
|
|
2299
2389
|
const full = c.mcFullResult ?? c.mcSnapshot?.result ?? null;
|
|
2300
2390
|
return full ? mcPfResultDetail(full, query, summary.solve_count) : null;
|
|
2301
2391
|
};
|
|
2392
|
+
/** Above this many buses, reading and indexing the whole operating point takes a noticeable
|
|
2393
|
+
* fraction of a second (evidence/diagram), so the overview is computed on request instead of
|
|
2394
|
+
* after every solve. */
|
|
2395
|
+
static AUTO_OVERVIEW_MAX_BUSES = 25_000;
|
|
2396
|
+
overviewSeq = 0;
|
|
2397
|
+
/** Whether this case computes its overview only on request. */
|
|
2398
|
+
overviewOnRequest = (c) => (c.graph?.buses.length ?? 0) > Controller.AUTO_OVERVIEW_MAX_BUSES;
|
|
2399
|
+
/** Read the whole operating point into `c.overviewResult` for the overview and the diagram. The
|
|
2400
|
+
* previous result stays visible until the read lands; a newer read supersedes it. */
|
|
2401
|
+
readOverview = (c) => this.startOverviewRead(c, ++this.overviewSeq);
|
|
2402
|
+
startOverviewRead(c, seq) {
|
|
2403
|
+
c.overviewLoading = true;
|
|
2404
|
+
this.multiFullResult(c).then((full) => {
|
|
2405
|
+
if (seq !== this.overviewSeq)
|
|
2406
|
+
return;
|
|
2407
|
+
if (full)
|
|
2408
|
+
c.overviewResult = full;
|
|
2409
|
+
c.overviewLoading = false;
|
|
2410
|
+
}, () => {
|
|
2411
|
+
if (seq === this.overviewSeq)
|
|
2412
|
+
c.overviewLoading = false;
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
/** Follow the active case's solves: read its operating point once load edits settle, or clear it
|
|
2416
|
+
* for a case that computes the overview on request. Call from an effect that tracks the case's
|
|
2417
|
+
* `result` and `mcFullResult`; the returned function cancels a pending read. */
|
|
2418
|
+
followOverview = (c) => {
|
|
2419
|
+
const seq = ++this.overviewSeq;
|
|
2420
|
+
if (!c)
|
|
2421
|
+
return;
|
|
2422
|
+
if (c.result?.solve_count === undefined || this.overviewOnRequest(c)) {
|
|
2423
|
+
c.overviewResult = null;
|
|
2424
|
+
c.overviewLoading = false;
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
c.overviewLoading = true;
|
|
2428
|
+
const timer = setTimeout(() => this.startOverviewRead(c, seq), 300);
|
|
2429
|
+
return () => {
|
|
2430
|
+
clearTimeout(timer);
|
|
2431
|
+
// A read still in flight is superseded by the next one, so this case is no longer loading.
|
|
2432
|
+
c.overviewLoading = false;
|
|
2433
|
+
};
|
|
2434
|
+
};
|
|
2435
|
+
/** The complete operating point for whole-network views: the stored result, or one read from
|
|
2436
|
+
* the live session. Null when there is no result or it changed before the read finished. */
|
|
2437
|
+
multiFullResult = async (c) => {
|
|
2438
|
+
if (!c.result)
|
|
2439
|
+
return null;
|
|
2440
|
+
const stored = c.mcFullResult ?? c.mcSnapshot?.result ?? null;
|
|
2441
|
+
const session = c.mcSession;
|
|
2442
|
+
if (stored || !session)
|
|
2443
|
+
return stored;
|
|
2444
|
+
if (c.mcEditRunning)
|
|
2445
|
+
return null;
|
|
2446
|
+
const solve = c.result.solve_count;
|
|
2447
|
+
const full = await session.result();
|
|
2448
|
+
return session === c.mcSession && c.result?.solve_count === solve ? full : null;
|
|
2449
|
+
};
|
|
2302
2450
|
/** Every terminal's identity, voltage, and injected current, as numeric
|
|
2303
2451
|
* arrays of interleaved `[re, im]` values. */
|
|
2304
2452
|
multiTerminalTable = async (c) => {
|
|
@@ -2736,6 +2884,11 @@ export class Controller {
|
|
|
2736
2884
|
* save or export captures exactly what is on screen. Null (with an error set) when no
|
|
2737
2885
|
* PowerIO module or Study is available. */
|
|
2738
2886
|
syncedStudy = async (c) => {
|
|
2887
|
+
const unavailable = this.caseExportUnavailableReason(c);
|
|
2888
|
+
if (unavailable) {
|
|
2889
|
+
this.app.error = `${this.caseName(c)}: ${unavailable}`;
|
|
2890
|
+
return null;
|
|
2891
|
+
}
|
|
2739
2892
|
const studyInputJson = await this.ensureStudyInputJson(c);
|
|
2740
2893
|
if (!studyInputJson) {
|
|
2741
2894
|
this.app.error = `${this.caseName(c)}: no PowerIO module available to save`;
|
|
@@ -2818,7 +2971,7 @@ export class Controller {
|
|
|
2818
2971
|
// can't preview (browser fallback path, server-only cases): the map then
|
|
2819
2972
|
// falls back to the JS sensitivity-times-step preview.
|
|
2820
2973
|
runPreview = (c, bus, value) => {
|
|
2821
|
-
if (c.formulation === 'acpf')
|
|
2974
|
+
if (c.formulation === 'acpf' || c.formulation === 'acopf')
|
|
2822
2975
|
return;
|
|
2823
2976
|
// Fast path: the committed price/demand column (already solved at the committed point),
|
|
2824
2977
|
// scaled by the demand step, is the same first-order linearization the engine preview
|
|
@@ -2933,7 +3086,7 @@ export class Controller {
|
|
|
2933
3086
|
// point (preview is replacement-absolute, so the absolute ratings map is built),
|
|
2934
3087
|
// then scaled by the live step; null when no Study slope is available.
|
|
2935
3088
|
runRatingPreview = (c, branch, value) => {
|
|
2936
|
-
if (c.formulation === 'acpf')
|
|
3089
|
+
if (c.formulation === 'acpf' || c.formulation === 'acopf')
|
|
2937
3090
|
return;
|
|
2938
3091
|
const committedAtBranch = this.caseRatings(c)[branch] ?? 0;
|
|
2939
3092
|
const step = (Math.abs(value) < DEAD_ZONE_MW ? 0 : value) - committedAtBranch;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** The basemap under geographic positions: the main map's own vector style, drawn by a MapLibre map
|
|
2
|
+
* kept in step with the schematic camera. Geographic positions are Web Mercator metres (y down)
|
|
3
|
+
* relative to an origin, so the camera converts exactly. */
|
|
4
|
+
/** Shared with the main map, so both backgrounds look the same. */
|
|
5
|
+
export declare const BASEMAP_STYLE = "https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json";
|
|
6
|
+
export declare const BASEMAP_ATTRIBUTION = "\u00A9 CARTO \u00A9 OpenStreetMap contributors";
|
|
7
|
+
/** Longitude and latitude in degrees of a Web Mercator point in metres, y down. */
|
|
8
|
+
export declare function mercatorToLngLat(x: number, y: number): [number, number];
|
|
9
|
+
/** The MapLibre camera showing what a schematic camera shows: same centre, and a zoom at which
|
|
10
|
+
* one metre spans the same pixels (MapLibre's world is 512 px wide at zoom 0). */
|
|
11
|
+
export declare function basemapCamera(camera: {
|
|
12
|
+
target: [number, number, number] | [number, number];
|
|
13
|
+
zoom: number;
|
|
14
|
+
}, origin: [number, number]): {
|
|
15
|
+
center: [number, number];
|
|
16
|
+
zoom: number;
|
|
17
|
+
};
|
|
18
|
+
/** Web Mercator metres (y down) of a longitude and latitude in degrees. */
|
|
19
|
+
export declare function lngLatToMercator(lng: number, lat: number): [number, number];
|
|
20
|
+
/** The schematic camera showing what a MapLibre camera shows (the inverse of
|
|
21
|
+
* {@link basemapCamera}), for opening the diagram where the map was. */
|
|
22
|
+
export declare function cameraFromMap(map: {
|
|
23
|
+
center: [number, number];
|
|
24
|
+
zoom: number;
|
|
25
|
+
}, origin: [number, number]): {
|
|
26
|
+
target: [number, number, number];
|
|
27
|
+
zoom: number;
|
|
28
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** The basemap under geographic positions: the main map's own vector style, drawn by a MapLibre map
|
|
2
|
+
* kept in step with the schematic camera. Geographic positions are Web Mercator metres (y down)
|
|
3
|
+
* relative to an origin, so the camera converts exactly. */
|
|
4
|
+
const R = 6_378_137;
|
|
5
|
+
const WORLD = 2 * Math.PI * R;
|
|
6
|
+
/** Shared with the main map, so both backgrounds look the same. */
|
|
7
|
+
export const BASEMAP_STYLE = 'https://basemaps.cartocdn.com/gl/positron-nolabels-gl-style/style.json';
|
|
8
|
+
export const BASEMAP_ATTRIBUTION = '© CARTO © OpenStreetMap contributors';
|
|
9
|
+
/** Longitude and latitude in degrees of a Web Mercator point in metres, y down. */
|
|
10
|
+
export function mercatorToLngLat(x, y) {
|
|
11
|
+
const lng = ((x / R) * 180) / Math.PI;
|
|
12
|
+
const lat = ((2 * Math.atan(Math.exp(-y / R)) - Math.PI / 2) * 180) / Math.PI;
|
|
13
|
+
return [lng, lat];
|
|
14
|
+
}
|
|
15
|
+
/** The MapLibre camera showing what a schematic camera shows: same centre, and a zoom at which
|
|
16
|
+
* one metre spans the same pixels (MapLibre's world is 512 px wide at zoom 0). */
|
|
17
|
+
export function basemapCamera(camera, origin) {
|
|
18
|
+
return {
|
|
19
|
+
center: mercatorToLngLat(camera.target[0] + origin[0], camera.target[1] + origin[1]),
|
|
20
|
+
zoom: Math.log2((WORLD * 2 ** camera.zoom) / 512)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** Web Mercator metres (y down) of a longitude and latitude in degrees. */
|
|
24
|
+
export function lngLatToMercator(lng, lat) {
|
|
25
|
+
const clamped = Math.max(-85, Math.min(85, lat));
|
|
26
|
+
return [
|
|
27
|
+
(R * lng * Math.PI) / 180,
|
|
28
|
+
-R * Math.log(Math.tan(Math.PI / 4 + (clamped * Math.PI) / 360))
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
/** The schematic camera showing what a MapLibre camera shows (the inverse of
|
|
32
|
+
* {@link basemapCamera}), for opening the diagram where the map was. */
|
|
33
|
+
export function cameraFromMap(map, origin) {
|
|
34
|
+
const [x, y] = lngLatToMercator(map.center[0], map.center[1]);
|
|
35
|
+
return {
|
|
36
|
+
target: [x - origin[0], y - origin[1], 0],
|
|
37
|
+
zoom: Math.log2((512 * 2 ** map.zoom) / WORLD)
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DistGraph } from '@tellegen/engine';
|
|
2
|
+
import type { SchematicGeometry } from './schematic-layout.js';
|
|
3
|
+
/** Buses drawn at exactly the same point (an MV and an LV bus at one pole, say). */
|
|
4
|
+
export interface BusStacks {
|
|
5
|
+
/** Groups of two or more bus indices, in graph order. */
|
|
6
|
+
groups: number[][];
|
|
7
|
+
/** Per bus: its group index, or −1 when it stands alone. */
|
|
8
|
+
stackOf: Int32Array;
|
|
9
|
+
}
|
|
10
|
+
export declare function busStacks(geometry: SchematicGeometry): BusStacks;
|
|
11
|
+
/** Offsets in drawing units that spread a stack's members on a circle of `radiusPx` screen pixels
|
|
12
|
+
* (`scale` pixels per unit): two side by side, more around the circle from the top. */
|
|
13
|
+
export declare function spreadOffsets(count: number, scale: number, radiusPx?: number): [number, number][];
|
|
14
|
+
/** `geometry` with the given buses moved by their offsets, and every branch end at a moved bus
|
|
15
|
+
* moved with it (interior route points stay put, so routes bend from the spread bus back onto
|
|
16
|
+
* their path). A branch whose two ends share a point (a pole transformer between stacked buses)
|
|
17
|
+
* takes its first end as `from` and its last as `to`. Returns `geometry` itself when nothing
|
|
18
|
+
* moves. */
|
|
19
|
+
export declare function displaceBuses(graph: DistGraph, geometry: SchematicGeometry, offsets: Map<number, [number, number]>): SchematicGeometry;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export function busStacks(geometry) {
|
|
2
|
+
const n = geometry.positions.length / 3;
|
|
3
|
+
const byPoint = new Map();
|
|
4
|
+
for (let i = 0; i < n; i++) {
|
|
5
|
+
const key = `${geometry.positions[i * 3]},${geometry.positions[i * 3 + 1]}`;
|
|
6
|
+
const list = byPoint.get(key);
|
|
7
|
+
if (list)
|
|
8
|
+
list.push(i);
|
|
9
|
+
else
|
|
10
|
+
byPoint.set(key, [i]);
|
|
11
|
+
}
|
|
12
|
+
const groups = [];
|
|
13
|
+
const stackOf = new Int32Array(n).fill(-1);
|
|
14
|
+
for (const list of byPoint.values())
|
|
15
|
+
if (list.length > 1) {
|
|
16
|
+
for (const i of list)
|
|
17
|
+
stackOf[i] = groups.length;
|
|
18
|
+
groups.push(list);
|
|
19
|
+
}
|
|
20
|
+
return { groups, stackOf };
|
|
21
|
+
}
|
|
22
|
+
/** Offsets in drawing units that spread a stack's members on a circle of `radiusPx` screen pixels
|
|
23
|
+
* (`scale` pixels per unit): two side by side, more around the circle from the top. */
|
|
24
|
+
export function spreadOffsets(count, scale, radiusPx = 12) {
|
|
25
|
+
const r = radiusPx / scale;
|
|
26
|
+
if (count === 2)
|
|
27
|
+
return [
|
|
28
|
+
[-r, 0],
|
|
29
|
+
[r, 0]
|
|
30
|
+
];
|
|
31
|
+
return Array.from({ length: count }, (_, i) => {
|
|
32
|
+
const a = -Math.PI / 2 + (2 * Math.PI * i) / count;
|
|
33
|
+
return [r * Math.cos(a), r * Math.sin(a)];
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/** `geometry` with the given buses moved by their offsets, and every branch end at a moved bus
|
|
37
|
+
* moved with it (interior route points stay put, so routes bend from the spread bus back onto
|
|
38
|
+
* their path). A branch whose two ends share a point (a pole transformer between stacked buses)
|
|
39
|
+
* takes its first end as `from` and its last as `to`. Returns `geometry` itself when nothing
|
|
40
|
+
* moves. */
|
|
41
|
+
export function displaceBuses(graph, geometry, offsets) {
|
|
42
|
+
if (!offsets.size)
|
|
43
|
+
return geometry;
|
|
44
|
+
const positions = Float32Array.from(geometry.positions);
|
|
45
|
+
for (const [i, [dx, dy]] of offsets) {
|
|
46
|
+
positions[i * 3] += dx;
|
|
47
|
+
positions[i * 3 + 1] += dy;
|
|
48
|
+
}
|
|
49
|
+
const sources = Float32Array.from(geometry.sources);
|
|
50
|
+
const targets = Float32Array.from(geometry.targets);
|
|
51
|
+
const index = new Map(graph.buses.map((b, i) => [b.id.toLowerCase(), i]));
|
|
52
|
+
// First and last segment of each edge.
|
|
53
|
+
const first = new Map();
|
|
54
|
+
const last = new Map();
|
|
55
|
+
for (let s = 0; s < geometry.segmentEdge.length; s++) {
|
|
56
|
+
const k = geometry.segmentEdge[s];
|
|
57
|
+
if (!first.has(k))
|
|
58
|
+
first.set(k, s);
|
|
59
|
+
last.set(k, s);
|
|
60
|
+
}
|
|
61
|
+
const at = (i) => [
|
|
62
|
+
geometry.positions[i * 3],
|
|
63
|
+
geometry.positions[i * 3 + 1]
|
|
64
|
+
];
|
|
65
|
+
const near = (x, y, i) => {
|
|
66
|
+
const [px, py] = at(i);
|
|
67
|
+
return x === px && y === py;
|
|
68
|
+
};
|
|
69
|
+
graph.edges.forEach((e, k) => {
|
|
70
|
+
const s0 = first.get(k);
|
|
71
|
+
const s1 = last.get(k);
|
|
72
|
+
if (s0 === undefined || s1 === undefined)
|
|
73
|
+
return;
|
|
74
|
+
const a = index.get(e.from.toLowerCase()) ?? -1;
|
|
75
|
+
const b = index.get(e.to.toLowerCase()) ?? -1;
|
|
76
|
+
const sx = geometry.sources[s0 * 3];
|
|
77
|
+
const sy = geometry.sources[s0 * 3 + 1];
|
|
78
|
+
const tx = geometry.targets[s1 * 3];
|
|
79
|
+
const ty = geometry.targets[s1 * 3 + 1];
|
|
80
|
+
// Which bus each end belongs to: the one drawn there, `from` first when both are.
|
|
81
|
+
const startBus = a >= 0 && near(sx, sy, a) ? a : b >= 0 && near(sx, sy, b) ? b : -1;
|
|
82
|
+
const endBus = b >= 0 && near(tx, ty, b) && b !== startBus
|
|
83
|
+
? b
|
|
84
|
+
: a >= 0 && near(tx, ty, a) && a !== startBus
|
|
85
|
+
? a
|
|
86
|
+
: -1;
|
|
87
|
+
const ds = startBus >= 0 ? offsets.get(startBus) : undefined;
|
|
88
|
+
const de = endBus >= 0 ? offsets.get(endBus) : undefined;
|
|
89
|
+
if (ds) {
|
|
90
|
+
sources[s0 * 3] += ds[0];
|
|
91
|
+
sources[s0 * 3 + 1] += ds[1];
|
|
92
|
+
}
|
|
93
|
+
if (de) {
|
|
94
|
+
targets[s1 * 3] += de[0];
|
|
95
|
+
targets[s1 * 3 + 1] += de[1];
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return { ...geometry, positions, sources, targets };
|
|
99
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type RGBA } from '../colors.js';
|
|
2
|
+
import type { PairVoltage } from './snapshot.js';
|
|
3
|
+
export type ColourMode = 'none' | 'voltage' | 'voltage-band' | 'loading' | 'loading-heat';
|
|
4
|
+
export declare const COLOUR_MODES: Record<ColourMode, {
|
|
5
|
+
label: string;
|
|
6
|
+
continuous: boolean;
|
|
7
|
+
}>;
|
|
8
|
+
/** Threshold and continuous variants of one quantity share issue focus, warnings and presets. */
|
|
9
|
+
export declare function colourFamily(mode: ColourMode): 'voltage' | 'loading' | 'none';
|
|
10
|
+
/** Position in the declared band: −1 at min, 0 mid-band, +1 at max; beyond ±1 is a violation. */
|
|
11
|
+
export declare function bandPosition(p: PairVoltage): number | null;
|
|
12
|
+
/** The pair nearest a limit decides a bus colour, so a violation can never be averaged away. */
|
|
13
|
+
export declare function worstBand(pairs: PairVoltage[]): number | null;
|
|
14
|
+
/** The application's sequential ramp; light loads start part-way up so they stay visible on paper. */
|
|
15
|
+
export declare const loadingColour: (loading: number) => RGBA;
|
|
16
|
+
/** The application's diverging ramp over the declared band, clamped at the limits. */
|
|
17
|
+
export declare const bandColour: (t: number) => RGBA;
|
|
18
|
+
export declare const MISSING_COLOUR: RGBA;
|