@tellegen/svelte 0.4.3 → 0.4.5
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 +45 -0
- package/dist/TellegenMap.svelte +63 -22
- package/dist/TellegenProvider.svelte +7 -4
- package/dist/TellegenShell.svelte +12 -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/DiagnosticLogView.svelte +176 -0
- package/dist/components/DiagnosticLogView.svelte.d.ts +11 -0
- package/dist/components/FormulationSelector.svelte +1 -1
- package/dist/components/LocalCaseDetails.svelte +18 -3
- package/dist/components/LogEntryView.svelte +188 -0
- package/dist/components/LogEntryView.svelte.d.ts +11 -0
- package/dist/components/LogPanel.svelte +25 -0
- package/dist/components/LogPanel.svelte.d.ts +3 -0
- 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/NotificationCenter.svelte +37 -6
- 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 +4 -0
- package/dist/components.js +4 -0
- package/dist/context.svelte.d.ts +2 -0
- package/dist/context.svelte.js +2 -0
- package/dist/controller.svelte.d.ts +40 -5
- package/dist/controller.svelte.js +230 -25
- 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 +3 -0
- package/dist/index.js +2 -0
- package/dist/log.svelte.d.ts +77 -0
- package/dist/log.svelte.js +149 -0
- package/dist/notices.svelte.d.ts +3 -0
- package/dist/state.svelte.d.ts +42 -5
- package/dist/state.svelte.js +56 -4
- 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. */
|
|
@@ -20,6 +20,9 @@ type SavedCaseCapture = {
|
|
|
20
20
|
export declare function isDisplayOnlyElement(element: {
|
|
21
21
|
editable?: boolean;
|
|
22
22
|
} | null | undefined): boolean;
|
|
23
|
+
/** Trigger a browser download of in-memory text. Case files never leave the machine:
|
|
24
|
+
* a save is a local Blob download. */
|
|
25
|
+
export declare function downloadText(text: string, filename: string, mime: string): void;
|
|
23
26
|
export type { SolvableCase };
|
|
24
27
|
type DemandRangeAnchor = {
|
|
25
28
|
caseId: string;
|
|
@@ -34,6 +37,7 @@ export interface ControllerOptions {
|
|
|
34
37
|
/** Whether `load()` fetches the bundled cases. Default `true`. With
|
|
35
38
|
* `false`, `load()` only marks the shell loaded and frames the map. */
|
|
36
39
|
loadDefaultCases?: boolean;
|
|
40
|
+
acOpfWasmUrl?: string;
|
|
37
41
|
mcTransport?: Pick<EngineTransport, 'solveMcModule' | 'applyMcGeo'> & Partial<Pick<EngineTransport, 'solveMcStudy' | 'applyMcStudyGeo' | 'createMcPfSession'>>;
|
|
38
42
|
}
|
|
39
43
|
/** Input to `Controller.openBalancedModule` and `replaceLocalCase`: a
|
|
@@ -66,6 +70,12 @@ export declare class Controller {
|
|
|
66
70
|
abort: AbortController | null;
|
|
67
71
|
sensitivity429Until: number;
|
|
68
72
|
serverComputeOff: boolean;
|
|
73
|
+
/** The optional EPL-bearing WASI asset is never inferred or bundled. */
|
|
74
|
+
private readonly acOpfWasmUrl;
|
|
75
|
+
private acOpfProbeStarted;
|
|
76
|
+
private readonly acOpfProbeAbort;
|
|
77
|
+
private readonly acOpfSolves;
|
|
78
|
+
acOpfAvailable: boolean;
|
|
69
79
|
caseStudies: WeakMap<SolvableCase, {
|
|
70
80
|
study: BrowserStudy;
|
|
71
81
|
studyInputJson: string;
|
|
@@ -111,6 +121,9 @@ export declare class Controller {
|
|
|
111
121
|
/** True when two selection targets name the same bus or branch. */
|
|
112
122
|
sameTarget(a: SensTarget, b: SensTarget): boolean;
|
|
113
123
|
isPerturbed(c: SolvableCase | null): boolean;
|
|
124
|
+
formulationAvailable(c: SolvableCase, formulation: Formulation): boolean;
|
|
125
|
+
probeAcOpf: () => void;
|
|
126
|
+
caseExportUnavailableReason(c: SolvableCase): string | null;
|
|
114
127
|
bumpRevision(c: SolvableCase): void;
|
|
115
128
|
setNearbyRangeAnchor(c: SolvableCase, bus: number, delta?: number): void;
|
|
116
129
|
beginBusSelection(c: SolvableCase, busId: number): {
|
|
@@ -131,10 +144,10 @@ export declare class Controller {
|
|
|
131
144
|
getStudy(c: SolvableCase, studyInputJson: string): Promise<BrowserStudy | null>;
|
|
132
145
|
browserSensitivity(c: SolvableCase, studyInputJson: string, target: SensTarget): Promise<SensitivityColumn | null>;
|
|
133
146
|
/** 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. */
|
|
147
|
+
* Study, including one still being built, its AC OPF solve, each
|
|
148
|
+
* multiconductor session, with their pending edits and in-flight requests,
|
|
149
|
+
* and the AC OPF asset probe. `TellegenProvider` calls it when it unmounts;
|
|
150
|
+
* the controller builds no Study and keeps no new session afterwards. */
|
|
138
151
|
dispose(): void;
|
|
139
152
|
disposeStudy(c: SolvableCase): void;
|
|
140
153
|
acceptSensitivity(c: SolvableCase, col: SensitivityColumn | null, target: SensTarget | null, sensitivitySeq?: number): void;
|
|
@@ -354,6 +367,24 @@ export declare class Controller {
|
|
|
354
367
|
* operating point, from the live session or the stored full result. Null
|
|
355
368
|
* when the operating point changed before the page arrived. */
|
|
356
369
|
multiResultDetail: (c: MulticonductorCase, query: McPfDetailQuery) => Promise<McPfDetail | null>;
|
|
370
|
+
/** Above this many buses, reading and indexing the whole operating point takes a noticeable
|
|
371
|
+
* fraction of a second (evidence/diagram), so the overview is computed on request instead of
|
|
372
|
+
* after every solve. */
|
|
373
|
+
static readonly AUTO_OVERVIEW_MAX_BUSES = 25000;
|
|
374
|
+
private overviewSeq;
|
|
375
|
+
/** Whether this case computes its overview only on request. */
|
|
376
|
+
overviewOnRequest: (c: MulticonductorCase) => boolean;
|
|
377
|
+
/** Read the whole operating point into `c.overviewResult` for the overview and the diagram. The
|
|
378
|
+
* previous result stays visible until the read lands; a newer read supersedes it. */
|
|
379
|
+
readOverview: (c: MulticonductorCase) => void;
|
|
380
|
+
private startOverviewRead;
|
|
381
|
+
/** Follow the active case's solves: read its operating point once load edits settle, or clear it
|
|
382
|
+
* for a case that computes the overview on request. Call from an effect that tracks the case's
|
|
383
|
+
* `result` and `mcFullResult`; the returned function cancels a pending read. */
|
|
384
|
+
followOverview: (c: MulticonductorCase | null) => (() => void) | undefined;
|
|
385
|
+
/** The complete operating point for whole-network views: the stored result, or one read from
|
|
386
|
+
* the live session. Null when there is no result or it changed before the read finished. */
|
|
387
|
+
multiFullResult: (c: MulticonductorCase) => Promise<McPfResult | null>;
|
|
357
388
|
/** Every terminal's identity, voltage, and injected current, as numeric
|
|
358
389
|
* arrays of interleaved `[re, im]` values. */
|
|
359
390
|
multiTerminalTable: (c: MulticonductorCase) => Promise<{
|
|
@@ -388,6 +419,10 @@ export declare class Controller {
|
|
|
388
419
|
caseGeographyLayers(c: LocalCase): Promise<string[]>;
|
|
389
420
|
/** Capture a matching cached result without constructing or solving a model. */
|
|
390
421
|
captureSavedCase(c: SolvableCase): Promise<SavedCaseCapture>;
|
|
422
|
+
/** Record what PowerIO and the engine said while reading a case file. */
|
|
423
|
+
private logImport;
|
|
424
|
+
/** Record a geographic layer's reader findings and placement notes. */
|
|
425
|
+
private logCoordinates;
|
|
391
426
|
/** Build a local case from a balanced PowerIO ingest payload without activating
|
|
392
427
|
* it yet; co-dropped placement data is applied before the first solve. */
|
|
393
428
|
private localFromBalancedPayload;
|
|
@@ -3,10 +3,11 @@ import { previewScaleFor, scalarDomain, sensFlatColor, sensitivityDomain } from
|
|
|
3
3
|
import { caseDeltas as sharedCaseDeltas, caseRatings as sharedCaseRatings, displayMetaFor, displaySeriesFor } from './display.js';
|
|
4
4
|
import { CaseState, LocalCase, MulticonductorCase } from './state.svelte.js';
|
|
5
5
|
import { placeSyntheticTopology } from './synthetic-layout.js';
|
|
6
|
+
import { powerIoEntry, solveEntry } from './log.svelte.js';
|
|
6
7
|
import { distExtensionFormat, isGeoFileName } from './drop-classify.js';
|
|
7
8
|
import { DropBatchGate, readDropFileBytes, validateDropBatch } from './drop-limits.js';
|
|
8
9
|
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';
|
|
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
11
|
import { browserSolveFailure, errorText, extent, formulationLabel, rgbaCss } from './format.js';
|
|
11
12
|
import { DEAD_ZONE_MW } from './slider.js';
|
|
12
13
|
const HIDDEN_DEFAULT_CASES_KEY = 'tellegen.hiddenDefaultCases.v1';
|
|
@@ -52,7 +53,7 @@ function delayUntilFocusSettles(ms, signal) {
|
|
|
52
53
|
}
|
|
53
54
|
/** Trigger a browser download of in-memory text. Case files never leave the machine:
|
|
54
55
|
* a save is a local Blob download. */
|
|
55
|
-
function downloadText(text, filename, mime) {
|
|
56
|
+
export function downloadText(text, filename, mime) {
|
|
56
57
|
const url = URL.createObjectURL(new Blob([text], { type: mime }));
|
|
57
58
|
const anchor = document.createElement('a');
|
|
58
59
|
anchor.href = url;
|
|
@@ -83,6 +84,12 @@ export class Controller {
|
|
|
83
84
|
// Latched once any server fallback answers 403: compute is disabled on this
|
|
84
85
|
// deploy, so later fallbacks show the notice instead of firing doomed requests.
|
|
85
86
|
serverComputeOff = false;
|
|
87
|
+
/** The optional EPL-bearing WASI asset is never inferred or bundled. */
|
|
88
|
+
acOpfWasmUrl;
|
|
89
|
+
acOpfProbeStarted = false;
|
|
90
|
+
acOpfProbeAbort = new AbortController();
|
|
91
|
+
acOpfSolves = new Map();
|
|
92
|
+
acOpfAvailable = $state(false);
|
|
86
93
|
// Build-once browser Study per case: the retained PowerIO module is parsed and the model built
|
|
87
94
|
// when the Study is created, so a drag re-solves (commit) and previews without
|
|
88
95
|
// re-parsing. Kept in a WeakMap off the reactive/raw case payloads — the wasm
|
|
@@ -131,6 +138,7 @@ export class Controller {
|
|
|
131
138
|
this.api = options.api ?? createApiClient({ apiBase: options.apiBase });
|
|
132
139
|
this.loadDefaultCases = options.loadDefaultCases ?? true;
|
|
133
140
|
this.mcTransport = options.mcTransport ?? browserWasmTransport;
|
|
141
|
+
this.acOpfWasmUrl = options.acOpfWasmUrl ?? null;
|
|
134
142
|
}
|
|
135
143
|
// ===== helpers =====
|
|
136
144
|
isBackendCase(c) {
|
|
@@ -158,6 +166,31 @@ export class Controller {
|
|
|
158
166
|
isPerturbed(c) {
|
|
159
167
|
return c?.perturbed ?? false;
|
|
160
168
|
}
|
|
169
|
+
formulationAvailable(c, formulation) {
|
|
170
|
+
if (formulation === 'acopf')
|
|
171
|
+
return this.acOpfAvailable && !c.perturbed;
|
|
172
|
+
return !FORMULATIONS.find((entry) => entry.id === formulation)?.disabled;
|
|
173
|
+
}
|
|
174
|
+
probeAcOpf = () => {
|
|
175
|
+
if (this.disposed || this.acOpfProbeStarted || !this.acOpfWasmUrl)
|
|
176
|
+
return;
|
|
177
|
+
this.acOpfProbeStarted = true;
|
|
178
|
+
void probeAcOpfWorker(this.acOpfWasmUrl, undefined, this.acOpfProbeAbort.signal).then((available) => {
|
|
179
|
+
if (this.disposed)
|
|
180
|
+
return;
|
|
181
|
+
this.acOpfAvailable = available;
|
|
182
|
+
// A declared instance can be dropped while the optional asset is probing.
|
|
183
|
+
const c = this.app.activeLocal;
|
|
184
|
+
if (available && c?.formulation === 'acopf' && !c.solving) {
|
|
185
|
+
this.maybeStartLocalSolve(c.id);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
};
|
|
189
|
+
caseExportUnavailableReason(c) {
|
|
190
|
+
return c.formulation === 'acopf'
|
|
191
|
+
? 'Saving and exporting AC OPF cases is not supported yet.'
|
|
192
|
+
: null;
|
|
193
|
+
}
|
|
161
194
|
bumpRevision(c) {
|
|
162
195
|
c.revisionGeneration += 1;
|
|
163
196
|
}
|
|
@@ -352,18 +385,24 @@ export class Controller {
|
|
|
352
385
|
return study ? study.sensitivity(c.id, this.caseDeltas(c), this.caseRatings(c), target) : null;
|
|
353
386
|
}
|
|
354
387
|
/** 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. */
|
|
388
|
+
* Study, including one still being built, its AC OPF solve, each
|
|
389
|
+
* multiconductor session, with their pending edits and in-flight requests,
|
|
390
|
+
* and the AC OPF asset probe. `TellegenProvider` calls it when it unmounts;
|
|
391
|
+
* the controller builds no Study and keeps no new session afterwards. */
|
|
359
392
|
dispose() {
|
|
360
393
|
if (this.disposed)
|
|
361
394
|
return;
|
|
362
395
|
this.disposed = true;
|
|
363
396
|
this.abort?.abort();
|
|
364
397
|
this.abort = null;
|
|
365
|
-
|
|
398
|
+
this.acOpfProbeAbort.abort();
|
|
399
|
+
for (const cancellation of this.acOpfSolves.values())
|
|
400
|
+
cancellation.abort();
|
|
401
|
+
this.acOpfSolves.clear();
|
|
402
|
+
for (const c of [...this.app.cases, ...this.app.localCases]) {
|
|
403
|
+
c.solveSeq = (c.solveSeq ?? 0) + 1;
|
|
366
404
|
this.disposeStudy(c);
|
|
405
|
+
}
|
|
367
406
|
for (const c of this.app.multiCases) {
|
|
368
407
|
c.solveAbort?.abort();
|
|
369
408
|
if (c.mcEditTimer !== null)
|
|
@@ -375,6 +414,8 @@ export class Controller {
|
|
|
375
414
|
}
|
|
376
415
|
// Release a case's Study (if any) when the case is removed.
|
|
377
416
|
disposeStudy(c) {
|
|
417
|
+
this.acOpfSolves.get(c)?.abort();
|
|
418
|
+
this.acOpfSolves.delete(c);
|
|
378
419
|
const cached = this.caseStudies.get(c);
|
|
379
420
|
if (cached) {
|
|
380
421
|
cached.study.free();
|
|
@@ -403,6 +444,10 @@ export class Controller {
|
|
|
403
444
|
if (seq !== (c.solveSeq ?? 0))
|
|
404
445
|
return;
|
|
405
446
|
c.solving = false;
|
|
447
|
+
// Every solve of a case restates the same findings; they count once.
|
|
448
|
+
const findings = c.solution?.diagnostics ?? [];
|
|
449
|
+
if (findings.length)
|
|
450
|
+
this.app.log.record(findings.map((d) => solveEntry(d, { subject: this.caseName(c) })), { repeat: false });
|
|
406
451
|
const selectionLive = target !== null &&
|
|
407
452
|
this.isActiveSolveCase(c) &&
|
|
408
453
|
('bus' in target
|
|
@@ -557,7 +602,9 @@ export class Controller {
|
|
|
557
602
|
sliderBounds = $derived.by(() => this.demandBounds(this.app.demandRangeMode, this.selectedBusData, this.nearbyRangeCenter));
|
|
558
603
|
sliderMin = $derived(this.sliderBounds.min);
|
|
559
604
|
sliderMax = $derived(this.sliderBounds.max);
|
|
560
|
-
sliderDisabled = $derived(
|
|
605
|
+
sliderDisabled = $derived(this.activeFormulation === 'acopf' ||
|
|
606
|
+
!this.selectedBusData ||
|
|
607
|
+
isDisplayOnlyElement(this.selectedBusData));
|
|
561
608
|
committedRating = $derived.by(() => this.activeSolvable && this.app.selectedBranch !== null
|
|
562
609
|
? (this.caseRatings(this.activeSolvable)[this.app.selectedBranch] ?? 0)
|
|
563
610
|
: 0);
|
|
@@ -569,7 +616,7 @@ export class Controller {
|
|
|
569
616
|
// to perturb.
|
|
570
617
|
ratingBounds = $derived.by(() => {
|
|
571
618
|
const b = this.selectedBranchData;
|
|
572
|
-
if (!b || isDisplayOnlyElement(b) || b.rate_mw <= 0)
|
|
619
|
+
if (this.activeFormulation === 'acopf' || !b || isDisplayOnlyElement(b) || b.rate_mw <= 0)
|
|
573
620
|
return { min: 0, max: 0, disabled: true };
|
|
574
621
|
const span = Math.min(50, Math.max(5, 0.2 * b.rate_mw));
|
|
575
622
|
return {
|
|
@@ -681,6 +728,7 @@ export class Controller {
|
|
|
681
728
|
if (this.loading)
|
|
682
729
|
return this.loading;
|
|
683
730
|
this.loading = (async () => {
|
|
731
|
+
this.probeAcOpf();
|
|
684
732
|
// Learn the deploy's compute gate up front so fallback paths pick honest
|
|
685
733
|
// copy and skip doomed requests (the SSE stream cannot see a 403). On
|
|
686
734
|
// failure assume enabled; the 403 latch in fetchServerColumn still
|
|
@@ -842,7 +890,7 @@ export class Controller {
|
|
|
842
890
|
this.app.activeCaseId = null;
|
|
843
891
|
this.leaveMulti();
|
|
844
892
|
this.app.activeLocalId = c.id;
|
|
845
|
-
if (c.formulation === 'acpf' && this.app.displayMode === 'price')
|
|
893
|
+
if ((c.formulation === 'acpf' || c.formulation === 'acopf') && this.app.displayMode === 'price')
|
|
846
894
|
this.app.displayMode = 'voltage';
|
|
847
895
|
this.app.placingLocalId = c.coordsKind === 'synthetic_pending' && !c.diagram ? c.id : null;
|
|
848
896
|
if (c.view || c.diagram || c.substations)
|
|
@@ -868,7 +916,7 @@ export class Controller {
|
|
|
868
916
|
this.app.activeCaseId = null;
|
|
869
917
|
this.leaveMulti();
|
|
870
918
|
this.app.addLocal(c);
|
|
871
|
-
if (c.formulation === 'acpf' && this.app.displayMode === 'price')
|
|
919
|
+
if ((c.formulation === 'acpf' || c.formulation === 'acopf') && this.app.displayMode === 'price')
|
|
872
920
|
this.app.displayMode = 'voltage';
|
|
873
921
|
if (c.diagram)
|
|
874
922
|
this.app.placingLocalId = null;
|
|
@@ -976,6 +1024,7 @@ export class Controller {
|
|
|
976
1024
|
routes += payload.report.matched_branches;
|
|
977
1025
|
unmatched += payload.report.unmatched_features;
|
|
978
1026
|
warnings.push(...l.diagnostics.map((d) => `${l.name}: ${formatPowerIoDiagnostic(d)}`), ...payload.report.notes.map((n) => `${l.name}: ${n}`));
|
|
1027
|
+
this.logCoordinates(l.name, l.diagnostics, payload.report.notes);
|
|
979
1028
|
}
|
|
980
1029
|
if (!payload)
|
|
981
1030
|
return;
|
|
@@ -1049,6 +1098,7 @@ export class Controller {
|
|
|
1049
1098
|
copy.iterations = source.iterations;
|
|
1050
1099
|
copy.solveMs = source.solveMs;
|
|
1051
1100
|
copy.solveBackend = source.solveBackend;
|
|
1101
|
+
copy.solveDetail = source.solveDetail;
|
|
1052
1102
|
copy.network = source.network;
|
|
1053
1103
|
if (source.network) {
|
|
1054
1104
|
copy.view = {
|
|
@@ -1121,7 +1171,8 @@ export class Controller {
|
|
|
1121
1171
|
'baseSolution',
|
|
1122
1172
|
'iterations',
|
|
1123
1173
|
'solveMs',
|
|
1124
|
-
'solveBackend'
|
|
1174
|
+
'solveBackend',
|
|
1175
|
+
'solveDetail'
|
|
1125
1176
|
])
|
|
1126
1177
|
Object.assign(copy, { [key]: source[key] });
|
|
1127
1178
|
const captured = this.detachedCaseCaptures.get(source);
|
|
@@ -1276,7 +1327,7 @@ export class Controller {
|
|
|
1276
1327
|
if (ac.signal.aborted)
|
|
1277
1328
|
return;
|
|
1278
1329
|
}
|
|
1279
|
-
if (c.formulation === 'acpf')
|
|
1330
|
+
if (c.formulation === 'acpf' || c.formulation === 'acopf')
|
|
1280
1331
|
return;
|
|
1281
1332
|
try {
|
|
1282
1333
|
// The column from the browser Study (under the case's formulation). DC OPF
|
|
@@ -1412,7 +1463,7 @@ export class Controller {
|
|
|
1412
1463
|
this.app.placingLocalId = null;
|
|
1413
1464
|
const { ac, sensitivitySeq } = this.beginBusSelection(c, busId);
|
|
1414
1465
|
try {
|
|
1415
|
-
if (c.formulation === 'acpf')
|
|
1466
|
+
if (c.formulation === 'acpf' || c.formulation === 'acopf')
|
|
1416
1467
|
return;
|
|
1417
1468
|
const sensitivity = await this.browserSensitivity(c, c.studyInputJson, {
|
|
1418
1469
|
bus: busId
|
|
@@ -1471,7 +1522,7 @@ export class Controller {
|
|
|
1471
1522
|
if (ac.signal.aborted)
|
|
1472
1523
|
return;
|
|
1473
1524
|
}
|
|
1474
|
-
if (c.formulation === 'acpf')
|
|
1525
|
+
if (c.formulation === 'acpf' || c.formulation === 'acopf')
|
|
1475
1526
|
return;
|
|
1476
1527
|
const sensitivity = await this.browserSensitivity(c, c.studyInputJson, {
|
|
1477
1528
|
branch: branchId
|
|
@@ -1535,8 +1586,12 @@ export class Controller {
|
|
|
1535
1586
|
// fall back: the server solves at base ratings, so a Study failure there is
|
|
1536
1587
|
// terminal.
|
|
1537
1588
|
runSolve = (c, target) => {
|
|
1538
|
-
if (
|
|
1589
|
+
if (this.disposed)
|
|
1590
|
+
return;
|
|
1591
|
+
if (c.formulation === 'acpf' || c.formulation === 'acopf')
|
|
1539
1592
|
target = null;
|
|
1593
|
+
this.acOpfSolves.get(c)?.abort();
|
|
1594
|
+
this.acOpfSolves.delete(c);
|
|
1540
1595
|
// Cancel this case's own previous server stream, if any (backend only).
|
|
1541
1596
|
if (this.isBackendCase(c)) {
|
|
1542
1597
|
c.closeStream?.();
|
|
@@ -1547,11 +1602,12 @@ export class Controller {
|
|
|
1547
1602
|
this.app.error = null;
|
|
1548
1603
|
c.solving = true;
|
|
1549
1604
|
c.solveBackend = null;
|
|
1605
|
+
c.solveDetail = null;
|
|
1550
1606
|
c.solveFallbackReason = null;
|
|
1551
1607
|
c.iterations = [];
|
|
1552
1608
|
c.solveMs = null;
|
|
1553
1609
|
this.ensureStudyInputJson(c).then(async (studyInputJson) => {
|
|
1554
|
-
if (seq !== (c.solveSeq ?? 0))
|
|
1610
|
+
if (this.disposed || seq !== (c.solveSeq ?? 0))
|
|
1555
1611
|
return;
|
|
1556
1612
|
if (!studyInputJson) {
|
|
1557
1613
|
c.solveFallbackReason ??= 'PowerIO module unavailable';
|
|
@@ -1574,6 +1630,45 @@ export class Controller {
|
|
|
1574
1630
|
return;
|
|
1575
1631
|
}
|
|
1576
1632
|
const t0 = performance.now();
|
|
1633
|
+
if (c.formulation === 'acopf') {
|
|
1634
|
+
if (!this.acOpfAvailable || !this.acOpfWasmUrl) {
|
|
1635
|
+
c.solving = false;
|
|
1636
|
+
this.app.error = `${this.caseName(c)}: the experimental AC OPF worker is unavailable`;
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
if (c.perturbed) {
|
|
1640
|
+
c.solving = false;
|
|
1641
|
+
this.app.error = `${this.caseName(c)}: AC OPF currently solves the canonical base case only; reset demand and rating edits first`;
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
const cancellation = new AbortController();
|
|
1645
|
+
this.acOpfSolves.set(c, cancellation);
|
|
1646
|
+
c.solveBackend = 'pounce-wasi';
|
|
1647
|
+
try {
|
|
1648
|
+
const response = await solveAcOpfModule(this.acOpfWasmUrl, studyInputJson, cancellation.signal);
|
|
1649
|
+
if (seq !== (c.solveSeq ?? 0) || cancellation.signal.aborted)
|
|
1650
|
+
return;
|
|
1651
|
+
c.solution = solveResponseToSolution(response);
|
|
1652
|
+
const diagnostics = response.iterations;
|
|
1653
|
+
c.solveDetail =
|
|
1654
|
+
diagnostics && !Array.isArray(diagnostics) && 'nlp_iterations' in diagnostics
|
|
1655
|
+
? `${diagnostics.total_nlp_iterations ?? diagnostics.nlp_iterations} NLP iterations${diagnostics.recovered_by ? ' (recovered)' : ''}`
|
|
1656
|
+
: 'POUNCE WASI';
|
|
1657
|
+
c.solveMs = Math.round(performance.now() - t0);
|
|
1658
|
+
this.finishSolve(c, seq, null);
|
|
1659
|
+
}
|
|
1660
|
+
catch (error) {
|
|
1661
|
+
if (seq !== (c.solveSeq ?? 0) || cancellation.signal.aborted)
|
|
1662
|
+
return;
|
|
1663
|
+
c.solving = false;
|
|
1664
|
+
this.app.error = `${this.caseName(c)}: AC OPF failed in the isolated browser worker: ${errorText(error)}`;
|
|
1665
|
+
}
|
|
1666
|
+
finally {
|
|
1667
|
+
if (this.acOpfSolves.get(c) === cancellation)
|
|
1668
|
+
this.acOpfSolves.delete(c);
|
|
1669
|
+
}
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1577
1672
|
c.solveBackend = 'clarabel-wasm';
|
|
1578
1673
|
// Build-once Study path: commit the new operating point (no re-parse). The
|
|
1579
1674
|
// selected nodal value sensitivity column is computed in the same
|
|
@@ -1763,12 +1858,12 @@ export class Controller {
|
|
|
1763
1858
|
this.fail('This PowerIO instance requires its declared calculation.');
|
|
1764
1859
|
return;
|
|
1765
1860
|
}
|
|
1766
|
-
if (
|
|
1861
|
+
if (!this.formulationAvailable(c, next))
|
|
1767
1862
|
return;
|
|
1768
1863
|
c.formulation = next;
|
|
1769
1864
|
// Demand does not depend on the calculation, so a demand colouring stays.
|
|
1770
1865
|
if (this.app.displayMode !== 'demand')
|
|
1771
|
-
this.app.displayMode = next === 'acpf' ? 'voltage' : 'price';
|
|
1866
|
+
this.app.displayMode = next === 'acpf' || next === 'acopf' ? 'voltage' : 'price';
|
|
1772
1867
|
this.bumpRevision(c);
|
|
1773
1868
|
// The committed point carries over (same demand), but the model and its solution do
|
|
1774
1869
|
// not; drop the Study and the cached solutions so they rebuild under `next`.
|
|
@@ -1777,6 +1872,7 @@ export class Controller {
|
|
|
1777
1872
|
c.solution = null;
|
|
1778
1873
|
c.iterations = [];
|
|
1779
1874
|
c.solveMs = null;
|
|
1875
|
+
c.solveDetail = null;
|
|
1780
1876
|
c.predictedObjective = null;
|
|
1781
1877
|
this.app.previewPrices = null;
|
|
1782
1878
|
this.previewObjective = null;
|
|
@@ -1953,6 +2049,7 @@ export class Controller {
|
|
|
1953
2049
|
const label = summary.name && summary.name !== 'case'
|
|
1954
2050
|
? summary.name
|
|
1955
2051
|
: file.name.replace(/\.[^.]+$/, '');
|
|
2052
|
+
this.logImport(file.name, summary);
|
|
1956
2053
|
const local = new LocalCase({
|
|
1957
2054
|
id,
|
|
1958
2055
|
label,
|
|
@@ -2152,6 +2249,7 @@ export class Controller {
|
|
|
2152
2249
|
const view = coordsKind === 'geographic'
|
|
2153
2250
|
? buildGeographicView(graph, payload.geo_layer)
|
|
2154
2251
|
: buildDiagramView(graph, payload.geo_layer);
|
|
2252
|
+
this.logImport(fileName, summary);
|
|
2155
2253
|
const c = new MulticonductorCase({
|
|
2156
2254
|
id: `dist-${++this.localSeq}`,
|
|
2157
2255
|
label,
|
|
@@ -2299,6 +2397,64 @@ export class Controller {
|
|
|
2299
2397
|
const full = c.mcFullResult ?? c.mcSnapshot?.result ?? null;
|
|
2300
2398
|
return full ? mcPfResultDetail(full, query, summary.solve_count) : null;
|
|
2301
2399
|
};
|
|
2400
|
+
/** 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
|
|
2402
|
+
* after every solve. */
|
|
2403
|
+
static AUTO_OVERVIEW_MAX_BUSES = 25_000;
|
|
2404
|
+
overviewSeq = 0;
|
|
2405
|
+
/** Whether this case computes its overview only on request. */
|
|
2406
|
+
overviewOnRequest = (c) => (c.graph?.buses.length ?? 0) > Controller.AUTO_OVERVIEW_MAX_BUSES;
|
|
2407
|
+
/** Read the whole operating point into `c.overviewResult` for the overview and the diagram. The
|
|
2408
|
+
* previous result stays visible until the read lands; a newer read supersedes it. */
|
|
2409
|
+
readOverview = (c) => this.startOverviewRead(c, ++this.overviewSeq);
|
|
2410
|
+
startOverviewRead(c, seq) {
|
|
2411
|
+
c.overviewLoading = true;
|
|
2412
|
+
this.multiFullResult(c).then((full) => {
|
|
2413
|
+
if (seq !== this.overviewSeq)
|
|
2414
|
+
return;
|
|
2415
|
+
if (full)
|
|
2416
|
+
c.overviewResult = full;
|
|
2417
|
+
c.overviewLoading = false;
|
|
2418
|
+
}, () => {
|
|
2419
|
+
if (seq === this.overviewSeq)
|
|
2420
|
+
c.overviewLoading = false;
|
|
2421
|
+
});
|
|
2422
|
+
}
|
|
2423
|
+
/** Follow the active case's solves: read its operating point once load edits settle, or clear it
|
|
2424
|
+
* for a case that computes the overview on request. Call from an effect that tracks the case's
|
|
2425
|
+
* `result` and `mcFullResult`; the returned function cancels a pending read. */
|
|
2426
|
+
followOverview = (c) => {
|
|
2427
|
+
const seq = ++this.overviewSeq;
|
|
2428
|
+
if (!c)
|
|
2429
|
+
return;
|
|
2430
|
+
if (c.result?.solve_count === undefined || this.overviewOnRequest(c)) {
|
|
2431
|
+
c.overviewResult = null;
|
|
2432
|
+
c.overviewLoading = false;
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
c.overviewLoading = true;
|
|
2436
|
+
const timer = setTimeout(() => this.startOverviewRead(c, seq), 300);
|
|
2437
|
+
return () => {
|
|
2438
|
+
clearTimeout(timer);
|
|
2439
|
+
// A read still in flight is superseded by the next one, so this case is no longer loading.
|
|
2440
|
+
c.overviewLoading = false;
|
|
2441
|
+
};
|
|
2442
|
+
};
|
|
2443
|
+
/** The complete operating point for whole-network views: the stored result, or one read from
|
|
2444
|
+
* the live session. Null when there is no result or it changed before the read finished. */
|
|
2445
|
+
multiFullResult = async (c) => {
|
|
2446
|
+
if (!c.result)
|
|
2447
|
+
return null;
|
|
2448
|
+
const stored = c.mcFullResult ?? c.mcSnapshot?.result ?? null;
|
|
2449
|
+
const session = c.mcSession;
|
|
2450
|
+
if (stored || !session)
|
|
2451
|
+
return stored;
|
|
2452
|
+
if (c.mcEditRunning)
|
|
2453
|
+
return null;
|
|
2454
|
+
const solve = c.result.solve_count;
|
|
2455
|
+
const full = await session.result();
|
|
2456
|
+
return session === c.mcSession && c.result?.solve_count === solve ? full : null;
|
|
2457
|
+
};
|
|
2302
2458
|
/** Every terminal's identity, voltage, and injected current, as numeric
|
|
2303
2459
|
* arrays of interleaved `[re, im]` values. */
|
|
2304
2460
|
multiTerminalTable = async (c) => {
|
|
@@ -2369,6 +2525,7 @@ export class Controller {
|
|
|
2369
2525
|
next = payload.module_json;
|
|
2370
2526
|
matched += payload.report.matched_buses + payload.report.matched_branches;
|
|
2371
2527
|
notes.push(`${payload.report.matched_buses} buses, ${payload.report.matched_branches} routes matched; ${payload.report.unmatched_features} unmatched objects`, ...payload.report.notes, ...layer.diagnostics.map(formatPowerIoDiagnostic));
|
|
2528
|
+
this.logCoordinates(layer.name, layer.diagnostics, payload.report.notes);
|
|
2372
2529
|
}
|
|
2373
2530
|
if (!payload || !matched)
|
|
2374
2531
|
throw new Error('No coordinates matched the selected case');
|
|
@@ -2595,7 +2752,13 @@ export class Controller {
|
|
|
2595
2752
|
...placeSyntheticTopology(c.topology, { lon: 0, lat: 0 }),
|
|
2596
2753
|
coordinate_space: 'diagram'
|
|
2597
2754
|
};
|
|
2598
|
-
return {
|
|
2755
|
+
return {
|
|
2756
|
+
id: 'study',
|
|
2757
|
+
name: c.name,
|
|
2758
|
+
base_mva: c.base_mva,
|
|
2759
|
+
synthetic_coords: !c.view,
|
|
2760
|
+
...view
|
|
2761
|
+
};
|
|
2599
2762
|
}
|
|
2600
2763
|
async caseGeographyLayers(c) {
|
|
2601
2764
|
if (!c.studyInputJson || !c.view)
|
|
@@ -2633,15 +2796,46 @@ export class Controller {
|
|
|
2633
2796
|
if (snapshot.base_input !== base ||
|
|
2634
2797
|
JSON.stringify(snapshot.display_solution) !== JSON.stringify(c.solution))
|
|
2635
2798
|
throw new Error('The server case changed. Reload it before saving.');
|
|
2636
|
-
return {
|
|
2799
|
+
return {
|
|
2800
|
+
input: snapshot.input,
|
|
2801
|
+
solution: snapshot.solution,
|
|
2802
|
+
view: snapshot.view
|
|
2803
|
+
};
|
|
2637
2804
|
}
|
|
2638
2805
|
return { input: base };
|
|
2639
2806
|
}
|
|
2807
|
+
/** Record what PowerIO and the engine said while reading a case file. */
|
|
2808
|
+
logImport(fileName, summary) {
|
|
2809
|
+
this.app.log.record([
|
|
2810
|
+
...(summary.diagnostics ?? []).map((d) => powerIoEntry(d, { stage: 'Open', subject: fileName })),
|
|
2811
|
+
...(summary.warnings ?? []).map((message) => ({
|
|
2812
|
+
severity: 'note',
|
|
2813
|
+
stage: 'Open',
|
|
2814
|
+
source: 'tellegen',
|
|
2815
|
+
subject: fileName,
|
|
2816
|
+
message
|
|
2817
|
+
}))
|
|
2818
|
+
]);
|
|
2819
|
+
}
|
|
2820
|
+
/** Record a geographic layer's reader findings and placement notes. */
|
|
2821
|
+
logCoordinates(layer, diagnostics, notes) {
|
|
2822
|
+
this.app.log.record([
|
|
2823
|
+
...(diagnostics ?? []).map((d) => powerIoEntry(d, { stage: 'Coordinates', subject: layer })),
|
|
2824
|
+
...(notes ?? []).map((message) => ({
|
|
2825
|
+
severity: 'note',
|
|
2826
|
+
stage: 'Coordinates',
|
|
2827
|
+
source: 'tellegen',
|
|
2828
|
+
subject: layer,
|
|
2829
|
+
message
|
|
2830
|
+
}))
|
|
2831
|
+
]);
|
|
2832
|
+
}
|
|
2640
2833
|
/** Build a local case from a balanced PowerIO ingest payload without activating
|
|
2641
2834
|
* it yet; co-dropped placement data is applied before the first solve. */
|
|
2642
2835
|
localFromBalancedPayload = (fileName, payload) => {
|
|
2643
2836
|
const { module_json, topology, view, formulation, ...summary } = payload;
|
|
2644
2837
|
const label = summary.name && summary.name !== 'case' ? summary.name : fileName.replace(/\.[^.]+$/, '');
|
|
2838
|
+
this.logImport(fileName, summary);
|
|
2645
2839
|
return new LocalCase({
|
|
2646
2840
|
id: `local-${++this.localSeq}`,
|
|
2647
2841
|
label,
|
|
@@ -2684,7 +2878,11 @@ export class Controller {
|
|
|
2684
2878
|
local.formulation = input.formulation;
|
|
2685
2879
|
if (input.geoLayer) {
|
|
2686
2880
|
await this.applyGeoLayers(local, [
|
|
2687
|
-
{
|
|
2881
|
+
{
|
|
2882
|
+
name: `${input.name}.geo.json`,
|
|
2883
|
+
layer: input.geoLayer,
|
|
2884
|
+
diagnostics: []
|
|
2885
|
+
}
|
|
2688
2886
|
]);
|
|
2689
2887
|
}
|
|
2690
2888
|
this.placeTopologyAsDiagram(local);
|
|
@@ -2736,6 +2934,11 @@ export class Controller {
|
|
|
2736
2934
|
* save or export captures exactly what is on screen. Null (with an error set) when no
|
|
2737
2935
|
* PowerIO module or Study is available. */
|
|
2738
2936
|
syncedStudy = async (c) => {
|
|
2937
|
+
const unavailable = this.caseExportUnavailableReason(c);
|
|
2938
|
+
if (unavailable) {
|
|
2939
|
+
this.app.error = `${this.caseName(c)}: ${unavailable}`;
|
|
2940
|
+
return null;
|
|
2941
|
+
}
|
|
2739
2942
|
const studyInputJson = await this.ensureStudyInputJson(c);
|
|
2740
2943
|
if (!studyInputJson) {
|
|
2741
2944
|
this.app.error = `${this.caseName(c)}: no PowerIO module available to save`;
|
|
@@ -2787,8 +2990,10 @@ export class Controller {
|
|
|
2787
2990
|
return [];
|
|
2788
2991
|
try {
|
|
2789
2992
|
const exported = await study.export(format);
|
|
2790
|
-
|
|
2993
|
+
const fileName = `${this.caseFileStem(c)}.${exported.extension}`;
|
|
2994
|
+
downloadText(exported.text, fileName, 'text/plain');
|
|
2791
2995
|
this.app.error = null;
|
|
2996
|
+
this.app.log.record((exported.diagnostics ?? []).map((d) => powerIoEntry(d, { stage: 'Export', subject: fileName })), { repeat: false });
|
|
2792
2997
|
return exported.diagnostics;
|
|
2793
2998
|
}
|
|
2794
2999
|
catch (e) {
|
|
@@ -2818,7 +3023,7 @@ export class Controller {
|
|
|
2818
3023
|
// can't preview (browser fallback path, server-only cases): the map then
|
|
2819
3024
|
// falls back to the JS sensitivity-times-step preview.
|
|
2820
3025
|
runPreview = (c, bus, value) => {
|
|
2821
|
-
if (c.formulation === 'acpf')
|
|
3026
|
+
if (c.formulation === 'acpf' || c.formulation === 'acopf')
|
|
2822
3027
|
return;
|
|
2823
3028
|
// Fast path: the committed price/demand column (already solved at the committed point),
|
|
2824
3029
|
// scaled by the demand step, is the same first-order linearization the engine preview
|
|
@@ -2933,7 +3138,7 @@ export class Controller {
|
|
|
2933
3138
|
// point (preview is replacement-absolute, so the absolute ratings map is built),
|
|
2934
3139
|
// then scaled by the live step; null when no Study slope is available.
|
|
2935
3140
|
runRatingPreview = (c, branch, value) => {
|
|
2936
|
-
if (c.formulation === 'acpf')
|
|
3141
|
+
if (c.formulation === 'acpf' || c.formulation === 'acopf')
|
|
2937
3142
|
return;
|
|
2938
3143
|
const committedAtBranch = this.caseRatings(c)[branch] ?? 0;
|
|
2939
3144
|
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
|
+
};
|