@tellegen/svelte 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +233 -0
- package/dist/Sparkline.svelte +2 -2
- package/dist/TellegenMap.svelte +201 -47
- package/dist/TellegenMap.svelte.d.ts +45 -0
- package/dist/TellegenProvider.svelte +36 -4
- package/dist/TellegenShell.svelte +14 -42
- package/dist/TellegenViewer.svelte +12 -2
- package/dist/api.d.ts +10 -2
- package/dist/api.js +2 -0
- package/dist/basemap.d.ts +192 -0
- package/dist/basemap.js +259 -0
- package/dist/colors.d.ts +18 -0
- package/dist/colors.js +26 -0
- package/dist/components/AppFooter.svelte +3 -2
- package/dist/components/AppHeader.svelte +21 -8
- package/dist/components/BindingLines.svelte +1 -1
- package/dist/components/BusPicker.svelte +3 -3
- package/dist/components/DemandSlider.svelte +16 -7
- package/dist/components/DiagramCanvas.svelte +1 -1
- package/dist/components/DisplayControls.svelte +5 -1
- package/dist/components/DropZone.svelte +1 -1
- package/dist/components/FormulationSelector.svelte +1 -1
- package/dist/components/LocalCaseDetails.svelte +6 -3
- package/dist/components/McResults.svelte +98 -35
- package/dist/components/McResults.svelte.d.ts +8 -2
- package/dist/components/ModelDetails.svelte +5 -5
- package/dist/components/MulticonductorDetails.svelte +9 -9
- package/dist/components/NotificationCenter.svelte +4 -2
- package/dist/components/PanelCard.svelte +72 -7
- package/dist/components/PanelHost.svelte +59 -34
- package/dist/components/PanelHost.svelte.d.ts +4 -1
- package/dist/components/PlacementCue.svelte +1 -1
- package/dist/components/PowerFlowDetails.svelte +1 -1
- package/dist/components/RatingSlider.svelte +14 -5
- package/dist/components/RestoreDefaultsButton.svelte +1 -1
- package/dist/components/SavedNetworkDetails.svelte +3 -3
- package/dist/components/SensitivityReadout.svelte +2 -2
- package/dist/components/ShellEnvironment.svelte +51 -0
- package/dist/components/ShellEnvironment.svelte.d.ts +18 -0
- package/dist/components/SizeLegend.svelte +2 -2
- package/dist/components/SolveCard.svelte +3 -3
- package/dist/components/TopMovers.svelte +1 -1
- package/dist/components.d.ts +3 -0
- package/dist/components.js +3 -0
- package/dist/context.svelte.d.ts +8 -1
- package/dist/context.svelte.js +8 -3
- package/dist/controller.svelte.d.ts +65 -5
- package/dist/controller.svelte.js +262 -76
- package/dist/format.d.ts +9 -0
- package/dist/format.js +12 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -0
- package/dist/map.d.ts +4 -2
- package/dist/map.js +2 -1
- package/dist/maplibre-worker.d.ts +5 -0
- package/dist/maplibre-worker.dev.d.ts +11 -0
- package/dist/maplibre-worker.dev.js +15 -0
- package/dist/maplibre-worker.js +8 -0
- package/dist/notices.svelte.js +5 -1
- package/dist/panels.svelte.d.ts +6 -0
- package/dist/panels.svelte.js +31 -1
- package/dist/slider.d.ts +12 -0
- package/dist/slider.js +31 -0
- package/dist/state.svelte.d.ts +19 -2
- package/dist/state.svelte.js +12 -0
- package/dist/styles.css +59 -6
- package/package.json +13 -8
|
@@ -6,11 +6,17 @@ 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 } from '@tellegen/engine';
|
|
10
|
-
import { errorText, extent, formulationLabel, rgbaCss } from './format.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 { browserSolveFailure, errorText, extent, formulationLabel, rgbaCss } from './format.js';
|
|
11
|
+
import { DEAD_ZONE_MW } from './slider.js';
|
|
11
12
|
const HIDDEN_DEFAULT_CASES_KEY = 'tellegen.hiddenDefaultCases.v1';
|
|
12
13
|
// Open on South Carolina by default: it has the most interesting price action.
|
|
13
14
|
const DEFAULT_CASE_ID = 'case500';
|
|
15
|
+
/** Whether a local case renders or will render a bus with this id. */
|
|
16
|
+
function localHasBus(c, busId) {
|
|
17
|
+
const buses = c.view?.buses ?? c.diagram?.view.buses ?? c.topology?.buses ?? [];
|
|
18
|
+
return buses.some((b) => b.id === busId);
|
|
19
|
+
}
|
|
14
20
|
/** Terminal copy when nothing can solve. Two facts decide the wording: whether
|
|
15
21
|
* this browser's engine failure is known permanent, and that the server
|
|
16
22
|
* declines compute. Server compute exists in the codebase but ships disabled;
|
|
@@ -64,7 +70,10 @@ function downloadText(text, filename, mime) {
|
|
|
64
70
|
}
|
|
65
71
|
export class Controller {
|
|
66
72
|
app;
|
|
73
|
+
/** `null` when the controller was created with `apiBase: null`; every
|
|
74
|
+
* server path then reports "compute off" instead of fetching. */
|
|
67
75
|
api;
|
|
76
|
+
loadDefaultCases;
|
|
68
77
|
mcTransport;
|
|
69
78
|
abort = null;
|
|
70
79
|
// While set (epoch ms), the server sensitivity fallback is rate limited: skip
|
|
@@ -119,6 +128,7 @@ export class Controller {
|
|
|
119
128
|
constructor(app, options = {}) {
|
|
120
129
|
this.app = app;
|
|
121
130
|
this.api = options.api ?? createApiClient({ apiBase: options.apiBase });
|
|
131
|
+
this.loadDefaultCases = options.loadDefaultCases ?? true;
|
|
122
132
|
this.mcTransport = options.mcTransport ?? browserWasmTransport;
|
|
123
133
|
}
|
|
124
134
|
// ===== helpers =====
|
|
@@ -388,8 +398,13 @@ export class Controller {
|
|
|
388
398
|
return c.studyInputJson;
|
|
389
399
|
if (!this.isBackendCase(c))
|
|
390
400
|
return null;
|
|
401
|
+
const api = this.api;
|
|
402
|
+
if (!api) {
|
|
403
|
+
c.solveFallbackReason = 'no API configured';
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
391
406
|
try {
|
|
392
|
-
const moduleJson = await
|
|
407
|
+
const moduleJson = await api.getCaseModuleJson(c.id);
|
|
393
408
|
c.studyInputJson = moduleJson;
|
|
394
409
|
return moduleJson;
|
|
395
410
|
}
|
|
@@ -403,7 +418,7 @@ export class Controller {
|
|
|
403
418
|
// reads as "no change at this bus".
|
|
404
419
|
previewDeltas(c, bus, value) {
|
|
405
420
|
const deltas = { ...this.caseDeltas(c) };
|
|
406
|
-
if (Math.abs(value) <
|
|
421
|
+
if (Math.abs(value) < DEAD_ZONE_MW)
|
|
407
422
|
delete deltas[bus];
|
|
408
423
|
else
|
|
409
424
|
deltas[bus] = value;
|
|
@@ -621,11 +636,20 @@ export class Controller {
|
|
|
621
636
|
previewing = $derived.by(() => Boolean(this.activeSolvable?.solving ||
|
|
622
637
|
this.app.previewActive ||
|
|
623
638
|
(this.app.previewDeltaMw !== null &&
|
|
624
|
-
Math.abs(this.sliderValue - this.committedDelta) >=
|
|
639
|
+
Math.abs(this.sliderValue - this.committedDelta) >= DEAD_ZONE_MW) ||
|
|
625
640
|
(this.app.previewRatingMw !== null &&
|
|
626
|
-
Math.abs(this.ratingSliderValue - this.committedRating) >=
|
|
641
|
+
Math.abs(this.ratingSliderValue - this.committedRating) >= DEAD_ZONE_MW)));
|
|
627
642
|
// ===== actions =====
|
|
628
643
|
load = () => {
|
|
644
|
+
// Default cases off, or no backend at all: nothing to fetch. The shell
|
|
645
|
+
// still needs its loaded state and a framed map, and a host that opens
|
|
646
|
+
// its own cases must never see a request go out.
|
|
647
|
+
const api = this.api;
|
|
648
|
+
if (!this.loadDefaultCases || api === null) {
|
|
649
|
+
this.casesLoaded = true;
|
|
650
|
+
void this.app.requestFrame('all');
|
|
651
|
+
return Promise.resolve();
|
|
652
|
+
}
|
|
629
653
|
// A remount can call load() again before the first getCases() resolves; dedupe
|
|
630
654
|
// so two concurrent loads can't double-fetch the case list and double-fit the map.
|
|
631
655
|
if (this.loading)
|
|
@@ -635,14 +659,14 @@ export class Controller {
|
|
|
635
659
|
// copy and skip doomed requests (the SSE stream cannot see a 403). On
|
|
636
660
|
// failure assume enabled; the 403 latch in fetchServerColumn still
|
|
637
661
|
// catches a disabled deploy.
|
|
638
|
-
void
|
|
662
|
+
void api
|
|
639
663
|
.getComputeStatus()
|
|
640
664
|
.then((s) => {
|
|
641
665
|
this.serverComputeOff = !s.enabled;
|
|
642
666
|
})
|
|
643
667
|
.catch(() => { });
|
|
644
668
|
try {
|
|
645
|
-
const summaries = await
|
|
669
|
+
const summaries = await api.getCases();
|
|
646
670
|
const hidden = this.readHiddenDefaultCases();
|
|
647
671
|
this.hiddenDefaults = new Set(hidden);
|
|
648
672
|
// Tear down the cases being replaced so their wasm Studies are freed and any
|
|
@@ -689,15 +713,20 @@ export class Controller {
|
|
|
689
713
|
this.app.requestFrame(c.id);
|
|
690
714
|
return;
|
|
691
715
|
}
|
|
716
|
+
const api = this.api;
|
|
717
|
+
if (!api) {
|
|
718
|
+
this.fail(`${c.name}: no API configured`);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
692
721
|
this.loadingBackendCase = c.id;
|
|
693
722
|
const requestedFormulation = c.formulation;
|
|
694
723
|
try {
|
|
695
724
|
const [network, dcBaseSolution] = await Promise.all([
|
|
696
|
-
c.network ? Promise.resolve(c.network) :
|
|
725
|
+
c.network ? Promise.resolve(c.network) : api.getNetwork(c.id),
|
|
697
726
|
// The server caches only the DC OPF base solution. Other formulations
|
|
698
727
|
// must hydrate from the browser Study so their objective, nodal values, flows, and
|
|
699
728
|
// voltage fields all come from the selected formulation.
|
|
700
|
-
requestedFormulation === 'dcopf' ?
|
|
729
|
+
requestedFormulation === 'dcopf' ? api.getSolution(c.id) : Promise.resolve(null)
|
|
701
730
|
]);
|
|
702
731
|
if (!this.app.byId(c.id))
|
|
703
732
|
return;
|
|
@@ -805,7 +834,9 @@ export class Controller {
|
|
|
805
834
|
this.disposeStudy(c);
|
|
806
835
|
await this.hydrateFallback(this.app.removeLocal(c.id));
|
|
807
836
|
};
|
|
808
|
-
|
|
837
|
+
/** Add a prepared local case, make it active, frame it (unless `frame` is
|
|
838
|
+
* false) and start its browser solve. */
|
|
839
|
+
addAndActivateLocal = (c, options = {}) => {
|
|
809
840
|
this.app.studyView = null;
|
|
810
841
|
this.clearSelection();
|
|
811
842
|
this.app.activeCaseId = null;
|
|
@@ -815,7 +846,7 @@ export class Controller {
|
|
|
815
846
|
this.app.displayMode = 'voltage';
|
|
816
847
|
if (c.diagram)
|
|
817
848
|
this.app.placingLocalId = null;
|
|
818
|
-
if (c.view || c.diagram || c.substations)
|
|
849
|
+
if ((options.frame ?? true) && (c.view || c.diagram || c.substations))
|
|
819
850
|
this.app.requestFrame(c.id);
|
|
820
851
|
this.maybeStartLocalSolve(c.id);
|
|
821
852
|
};
|
|
@@ -1291,7 +1322,8 @@ export class Controller {
|
|
|
1291
1322
|
const retryOp = 'bus' in target
|
|
1292
1323
|
? () => this.selectBus(c.id, target.bus)
|
|
1293
1324
|
: () => this.selectBranch(c.id, target.branch);
|
|
1294
|
-
|
|
1325
|
+
const api = this.api;
|
|
1326
|
+
if (this.serverComputeOff || !api) {
|
|
1295
1327
|
this.fail(this.computeOffNotice(c), retryOp);
|
|
1296
1328
|
return;
|
|
1297
1329
|
}
|
|
@@ -1301,8 +1333,8 @@ export class Controller {
|
|
|
1301
1333
|
}
|
|
1302
1334
|
try {
|
|
1303
1335
|
const col = 'bus' in target
|
|
1304
|
-
? await
|
|
1305
|
-
: await
|
|
1336
|
+
? await api.getSensitivity(c.id, target.bus, c.deltas, ac.signal)
|
|
1337
|
+
: await api.getBranchSensitivity(c.id, target.branch, c.deltas, ac.signal);
|
|
1306
1338
|
if (!ac.signal.aborted) {
|
|
1307
1339
|
// The solution on screen came from the browser or the served base
|
|
1308
1340
|
// solve; the column came from the server. Label the mix.
|
|
@@ -1497,6 +1529,13 @@ export class Controller {
|
|
|
1497
1529
|
return;
|
|
1498
1530
|
if (!studyInputJson) {
|
|
1499
1531
|
c.solveFallbackReason ??= 'PowerIO module unavailable';
|
|
1532
|
+
// The server fallback only implements DC OPF. Preserve the requested
|
|
1533
|
+
// calculation when its browser input cannot be loaded.
|
|
1534
|
+
if (c.formulation !== 'dcopf') {
|
|
1535
|
+
c.solving = false;
|
|
1536
|
+
this.app.error = `${this.caseName(c)}: ${formulationLabel(c.formulation)} requires its PowerIO module; ${c.solveFallbackReason}`;
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1500
1539
|
if (this.hasRatingEdits(c)) {
|
|
1501
1540
|
c.solving = false;
|
|
1502
1541
|
this.app.error = this.ratingEditsFallbackError(c);
|
|
@@ -1588,13 +1627,17 @@ export class Controller {
|
|
|
1588
1627
|
return;
|
|
1589
1628
|
}
|
|
1590
1629
|
c.solving = false;
|
|
1591
|
-
|
|
1630
|
+
const failure = browserSolveFailure(c.label, c.solveFallbackReason);
|
|
1631
|
+
this.app.error = failure.message;
|
|
1632
|
+
this.app.errorDetail = failure.detail;
|
|
1592
1633
|
});
|
|
1593
1634
|
};
|
|
1594
1635
|
serverSolve = (c, target, seq = c.solveSeq) => {
|
|
1595
|
-
// Known-disabled server compute: show the notice
|
|
1596
|
-
// stream that answers 403 without a readable body
|
|
1597
|
-
|
|
1636
|
+
// Known-disabled server compute (or no backend at all): show the notice
|
|
1637
|
+
// instead of opening a stream that answers 403 without a readable body
|
|
1638
|
+
// (EventSource hides it).
|
|
1639
|
+
const api = this.api;
|
|
1640
|
+
if (this.serverComputeOff || !api) {
|
|
1598
1641
|
if (seq !== c.solveSeq)
|
|
1599
1642
|
return;
|
|
1600
1643
|
c.solving = false;
|
|
@@ -1610,7 +1653,7 @@ export class Controller {
|
|
|
1610
1653
|
c.solveFallbackReason ??= 'browser solve unavailable';
|
|
1611
1654
|
// The server computes bus columns only; a branch target streams no sensitivity.
|
|
1612
1655
|
const sensBus = target && 'bus' in target ? target.bus : null;
|
|
1613
|
-
c.closeStream =
|
|
1656
|
+
c.closeStream = api.openSolveStream(c.id, c.deltas, sensBus, {
|
|
1614
1657
|
onsolution: (sol) => {
|
|
1615
1658
|
if (seq !== c.solveSeq)
|
|
1616
1659
|
return;
|
|
@@ -1659,7 +1702,7 @@ export class Controller {
|
|
|
1659
1702
|
finishDemandInput = (value) => {
|
|
1660
1703
|
if (this.sliderDisabled)
|
|
1661
1704
|
return;
|
|
1662
|
-
if (Math.abs(value - this.committedDelta) <
|
|
1705
|
+
if (Math.abs(value - this.committedDelta) < DEAD_ZONE_MW) {
|
|
1663
1706
|
if (!this.activeSolvable?.solving) {
|
|
1664
1707
|
this.app.previewActive = false;
|
|
1665
1708
|
this.app.previewDeltaMw = null;
|
|
@@ -1932,42 +1975,47 @@ export class Controller {
|
|
|
1932
1975
|
if (local.studyInputJson && displays.length > 0) {
|
|
1933
1976
|
await this.fillFromDisplaySibling(local, displays);
|
|
1934
1977
|
}
|
|
1935
|
-
|
|
1936
|
-
const view = {
|
|
1937
|
-
...placeSyntheticTopology(local.topology, { lon: 0, lat: 0 }),
|
|
1938
|
-
coordinate_space: 'diagram'
|
|
1939
|
-
};
|
|
1940
|
-
const layer = JSON.stringify({
|
|
1941
|
-
type: 'FeatureCollection',
|
|
1942
|
-
powerio_geo: { space: 'diagram', kind: 'synthetic' },
|
|
1943
|
-
features: [
|
|
1944
|
-
...view.buses.map((b) => ({
|
|
1945
|
-
type: 'Feature',
|
|
1946
|
-
properties: { target: 'bus', id: String(b.id) },
|
|
1947
|
-
geometry: { type: 'Point', coordinates: [b.lon, b.lat] }
|
|
1948
|
-
})),
|
|
1949
|
-
...view.branches.map((b) => ({
|
|
1950
|
-
type: 'Feature',
|
|
1951
|
-
properties: {
|
|
1952
|
-
target: 'branch',
|
|
1953
|
-
branch_id: String(b.id),
|
|
1954
|
-
from: String(b.from),
|
|
1955
|
-
to: String(b.to)
|
|
1956
|
-
},
|
|
1957
|
-
geometry: { type: 'LineString', coordinates: b.path }
|
|
1958
|
-
}))
|
|
1959
|
-
]
|
|
1960
|
-
});
|
|
1961
|
-
local.diagram = {
|
|
1962
|
-
view,
|
|
1963
|
-
layer,
|
|
1964
|
-
name: 'Network diagram',
|
|
1965
|
-
warnings: ['No geographic coordinates; positions follow network connectivity.']
|
|
1966
|
-
};
|
|
1967
|
-
local.displayMode = 'diagram';
|
|
1968
|
-
}
|
|
1978
|
+
this.placeTopologyAsDiagram(local);
|
|
1969
1979
|
return { geoLayersConsumed, error };
|
|
1970
1980
|
};
|
|
1981
|
+
/** A case that still has no geometry gets a connectivity layout as its
|
|
1982
|
+
* diagram, so it can render and solve without a placement step. */
|
|
1983
|
+
placeTopologyAsDiagram(local) {
|
|
1984
|
+
if (local.view || local.diagram || !local.topology)
|
|
1985
|
+
return;
|
|
1986
|
+
const view = {
|
|
1987
|
+
...placeSyntheticTopology(local.topology, { lon: 0, lat: 0 }),
|
|
1988
|
+
coordinate_space: 'diagram'
|
|
1989
|
+
};
|
|
1990
|
+
const layer = JSON.stringify({
|
|
1991
|
+
type: 'FeatureCollection',
|
|
1992
|
+
powerio_geo: { space: 'diagram', kind: 'synthetic' },
|
|
1993
|
+
features: [
|
|
1994
|
+
...view.buses.map((b) => ({
|
|
1995
|
+
type: 'Feature',
|
|
1996
|
+
properties: { target: 'bus', id: String(b.id) },
|
|
1997
|
+
geometry: { type: 'Point', coordinates: [b.lon, b.lat] }
|
|
1998
|
+
})),
|
|
1999
|
+
...view.branches.map((b) => ({
|
|
2000
|
+
type: 'Feature',
|
|
2001
|
+
properties: {
|
|
2002
|
+
target: 'branch',
|
|
2003
|
+
branch_id: String(b.id),
|
|
2004
|
+
from: String(b.from),
|
|
2005
|
+
to: String(b.to)
|
|
2006
|
+
},
|
|
2007
|
+
geometry: { type: 'LineString', coordinates: b.path }
|
|
2008
|
+
}))
|
|
2009
|
+
]
|
|
2010
|
+
});
|
|
2011
|
+
local.diagram = {
|
|
2012
|
+
view,
|
|
2013
|
+
layer,
|
|
2014
|
+
name: 'Network diagram',
|
|
2015
|
+
warnings: ['No geographic coordinates; positions follow network connectivity.']
|
|
2016
|
+
};
|
|
2017
|
+
local.displayMode = 'diagram';
|
|
2018
|
+
}
|
|
1971
2019
|
/** Retain diagram geometry separately from the electrical case's geographic data. */
|
|
1972
2020
|
fillFromDisplaySibling = async (c, displays) => {
|
|
1973
2021
|
if (!c.studyInputJson)
|
|
@@ -2120,20 +2168,22 @@ export class Controller {
|
|
|
2120
2168
|
input = await retainedAtStart.inputModule();
|
|
2121
2169
|
let snapshot = null;
|
|
2122
2170
|
let result;
|
|
2171
|
+
let fullResult = null;
|
|
2123
2172
|
let loadBranches = c.mcLoadBranches;
|
|
2124
2173
|
if (this.mcTransport.createMcPfSession) {
|
|
2174
|
+
// The session answers with its summary; terminal and equipment
|
|
2175
|
+
// detail is fetched only when a view asks for it.
|
|
2125
2176
|
nextSession = await this.mcTransport.createMcPfSession(input, options, abort.signal);
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
nextSession.loadBranches()
|
|
2129
|
-
]);
|
|
2177
|
+
result = nextSession.initialSummary;
|
|
2178
|
+
loadBranches = await nextSession.loadBranches();
|
|
2130
2179
|
}
|
|
2131
2180
|
else {
|
|
2132
2181
|
snapshot = this.mcTransport.solveMcStudy
|
|
2133
2182
|
? await this.mcTransport.solveMcStudy(input, crypto.randomUUID(), c.label, options, abort.signal)
|
|
2134
2183
|
: null;
|
|
2135
|
-
|
|
2184
|
+
fullResult =
|
|
2136
2185
|
snapshot?.result ?? (await this.mcTransport.solveMcModule(input, options, abort.signal));
|
|
2186
|
+
result = summarizeMcPfResult(fullResult);
|
|
2137
2187
|
}
|
|
2138
2188
|
if (abort.signal.aborted)
|
|
2139
2189
|
throw new DOMException('Calculation cancelled', 'AbortError');
|
|
@@ -2157,6 +2207,8 @@ export class Controller {
|
|
|
2157
2207
|
c.mcLoadEdits = [];
|
|
2158
2208
|
c.moduleJson = input;
|
|
2159
2209
|
c.result = result;
|
|
2210
|
+
c.mcFullResult = fullResult;
|
|
2211
|
+
c.mcTiming = null;
|
|
2160
2212
|
c.mcSnapshot = snapshot;
|
|
2161
2213
|
c.mcSavedAt = null;
|
|
2162
2214
|
c.solveMs = performance.now() - started;
|
|
@@ -2201,6 +2253,60 @@ export class Controller {
|
|
|
2201
2253
|
c.mcSnapshot = snapshot;
|
|
2202
2254
|
return snapshot;
|
|
2203
2255
|
};
|
|
2256
|
+
/** One bus's terminals and a page of equipment ports for the displayed
|
|
2257
|
+
* operating point, from the live session or the stored full result. Null
|
|
2258
|
+
* when the operating point changed before the page arrived. */
|
|
2259
|
+
multiResultDetail = async (c, query) => {
|
|
2260
|
+
const summary = c.result;
|
|
2261
|
+
if (!summary)
|
|
2262
|
+
return null;
|
|
2263
|
+
const session = c.mcSession;
|
|
2264
|
+
if (session && !c.mcFullResult) {
|
|
2265
|
+
const detail = await session.detail(query);
|
|
2266
|
+
return session === c.mcSession && c.result?.solve_count === detail.solve_count
|
|
2267
|
+
? detail
|
|
2268
|
+
: null;
|
|
2269
|
+
}
|
|
2270
|
+
const full = c.mcFullResult ?? c.mcSnapshot?.result ?? null;
|
|
2271
|
+
return full ? mcPfResultDetail(full, query, summary.solve_count) : null;
|
|
2272
|
+
};
|
|
2273
|
+
/** Every terminal's identity, voltage, and injected current, as numeric
|
|
2274
|
+
* arrays of interleaved `[re, im]` values. */
|
|
2275
|
+
multiTerminalTable = async (c) => {
|
|
2276
|
+
if (!c.result)
|
|
2277
|
+
return null;
|
|
2278
|
+
const session = c.mcSession;
|
|
2279
|
+
if (session && !c.mcFullResult) {
|
|
2280
|
+
// Arrays carry no solve count, and a running flush may discard its solve.
|
|
2281
|
+
if (c.mcEditRunning)
|
|
2282
|
+
return null;
|
|
2283
|
+
const solve = c.result.solve_count;
|
|
2284
|
+
const [ids, voltages, currents] = await Promise.all([
|
|
2285
|
+
session.terminalIds(),
|
|
2286
|
+
session.terminalVoltages(),
|
|
2287
|
+
session.terminalCurrents()
|
|
2288
|
+
]);
|
|
2289
|
+
return session === c.mcSession && c.result?.solve_count === solve
|
|
2290
|
+
? { ids, voltages, currents }
|
|
2291
|
+
: null;
|
|
2292
|
+
}
|
|
2293
|
+
const full = c.mcFullResult ?? c.mcSnapshot?.result ?? null;
|
|
2294
|
+
if (!full)
|
|
2295
|
+
return null;
|
|
2296
|
+
const voltages = new Float64Array(2 * full.terminals.length);
|
|
2297
|
+
const currents = new Float64Array(2 * full.terminals.length);
|
|
2298
|
+
full.terminals.forEach((terminal, k) => {
|
|
2299
|
+
voltages[2 * k] = terminal.voltage.re;
|
|
2300
|
+
voltages[2 * k + 1] = terminal.voltage.im;
|
|
2301
|
+
currents[2 * k] = terminal.current_into_network.re;
|
|
2302
|
+
currents[2 * k + 1] = terminal.current_into_network.im;
|
|
2303
|
+
});
|
|
2304
|
+
return {
|
|
2305
|
+
ids: full.terminals.map((terminal) => [terminal.bus, terminal.terminal]),
|
|
2306
|
+
voltages,
|
|
2307
|
+
currents
|
|
2308
|
+
};
|
|
2309
|
+
};
|
|
2204
2310
|
/** Apply coordinates atomically to the explicitly selected conductor-resolved case. */
|
|
2205
2311
|
applyMultiGeoLayers = async (c, layers) => {
|
|
2206
2312
|
if (!layers.length)
|
|
@@ -2251,6 +2357,8 @@ export class Controller {
|
|
|
2251
2357
|
}
|
|
2252
2358
|
c.mcSession?.free();
|
|
2253
2359
|
c.mcSession = null;
|
|
2360
|
+
// Without a session, result detail comes from the saved result.
|
|
2361
|
+
c.mcFullResult = snapshot?.result ?? c.mcFullResult;
|
|
2254
2362
|
c.mcLoadBranches = [];
|
|
2255
2363
|
c.mcLoadEdits = [];
|
|
2256
2364
|
c.moduleJson = next;
|
|
@@ -2320,19 +2428,20 @@ export class Controller {
|
|
|
2320
2428
|
const revision = c.mcEditRevision;
|
|
2321
2429
|
const edits = c.mcLoadEdits.map((edit) => ({ ...edit }));
|
|
2322
2430
|
const started = performance.now();
|
|
2323
|
-
const
|
|
2431
|
+
const summary = await session.replaceLoadPowers(edits);
|
|
2324
2432
|
if (session !== c.mcSession || !this.app.multiCases.includes(c))
|
|
2325
2433
|
return;
|
|
2326
2434
|
if (revision !== c.mcEditRevision)
|
|
2327
2435
|
continue;
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
c.
|
|
2332
|
-
c.result = result;
|
|
2436
|
+
// One round trip: the queued edit already set the branch state
|
|
2437
|
+
// this successful solve confirms. Only a failure refetches it.
|
|
2438
|
+
c.result = summary;
|
|
2439
|
+
c.mcFullResult = null;
|
|
2333
2440
|
c.mcSnapshot = null;
|
|
2334
2441
|
c.mcSavedAt = null;
|
|
2335
|
-
|
|
2442
|
+
const total = performance.now() - started;
|
|
2443
|
+
c.solveMs = total;
|
|
2444
|
+
c.mcTiming = session.lastTiming ? { ...session.lastTiming, total_ms: total } : null;
|
|
2336
2445
|
c.revisionGeneration++;
|
|
2337
2446
|
break;
|
|
2338
2447
|
}
|
|
@@ -2489,11 +2598,9 @@ export class Controller {
|
|
|
2489
2598
|
Object.values(this.caseRatings(c)).some((v) => v !== 0)) {
|
|
2490
2599
|
throw new Error('The displayed edits have no matching saved result. Finish solving before saving them.');
|
|
2491
2600
|
}
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
this.api.getSavedCase) {
|
|
2496
|
-
const snapshot = await this.api.getSavedCase(c.id);
|
|
2601
|
+
const api = this.api;
|
|
2602
|
+
if (c instanceof CaseState && c.solution && c.formulation === 'dcopf' && api?.getSavedCase) {
|
|
2603
|
+
const snapshot = await api.getSavedCase(c.id);
|
|
2497
2604
|
if (snapshot.base_input !== base ||
|
|
2498
2605
|
JSON.stringify(snapshot.display_solution) !== JSON.stringify(c.solution))
|
|
2499
2606
|
throw new Error('The server case changed. Reload it before saving.');
|
|
@@ -2519,6 +2626,83 @@ export class Controller {
|
|
|
2519
2626
|
view
|
|
2520
2627
|
});
|
|
2521
2628
|
};
|
|
2629
|
+
/** Ingest module text through the engine and require a balanced network. */
|
|
2630
|
+
async ingestBalancedModule(name, moduleJson) {
|
|
2631
|
+
if (moduleJson === undefined)
|
|
2632
|
+
throw new Error(`${name}: openBalancedModule needs moduleJson or payload`);
|
|
2633
|
+
const result = await ingestJsonDrop(new TextEncoder().encode(moduleJson));
|
|
2634
|
+
if (result.kind === 'transmission')
|
|
2635
|
+
return result.payload;
|
|
2636
|
+
if (result.kind === 'module' && !('model' in result.payload))
|
|
2637
|
+
return result.payload;
|
|
2638
|
+
throw new Error(`${name}: not a balanced network module (${result.kind})`);
|
|
2639
|
+
}
|
|
2640
|
+
/** Open a balanced powerio module from memory, the programmatic twin of
|
|
2641
|
+
* dropping a `.pio.json` file: ingest (unless `payload` is given), apply an
|
|
2642
|
+
* optional geographic layer, lay out a module without coordinates as a
|
|
2643
|
+
* diagram, then add and activate it. Resolves to the new case; throws when
|
|
2644
|
+
* the module is not a balanced network or the geographic layer matches
|
|
2645
|
+
* nothing. */
|
|
2646
|
+
openBalancedModule = async (input) => {
|
|
2647
|
+
this.app.parsingFile = true;
|
|
2648
|
+
try {
|
|
2649
|
+
const payload = input.payload ?? (await this.ingestBalancedModule(input.name, input.moduleJson));
|
|
2650
|
+
const local = this.localFromBalancedPayload(`${input.name}.pio.json`, {
|
|
2651
|
+
...payload,
|
|
2652
|
+
name: input.name
|
|
2653
|
+
});
|
|
2654
|
+
if (input.formulation)
|
|
2655
|
+
local.formulation = input.formulation;
|
|
2656
|
+
if (input.geoLayer) {
|
|
2657
|
+
await this.applyGeoLayers(local, [
|
|
2658
|
+
{ name: `${input.name}.geo.json`, layer: input.geoLayer, diagnostics: [] }
|
|
2659
|
+
]);
|
|
2660
|
+
}
|
|
2661
|
+
this.placeTopologyAsDiagram(local);
|
|
2662
|
+
if (input.activate !== false)
|
|
2663
|
+
this.addAndActivateLocal(local, { frame: input.frame });
|
|
2664
|
+
return local;
|
|
2665
|
+
}
|
|
2666
|
+
finally {
|
|
2667
|
+
this.app.parsingFile = false;
|
|
2668
|
+
}
|
|
2669
|
+
};
|
|
2670
|
+
/** Swap a local case for a new module in place: same slot in the case list,
|
|
2671
|
+
* same formulation unless `input.formulation` says otherwise, same display
|
|
2672
|
+
* mode when the new case supports it, and the selected bus kept when the
|
|
2673
|
+
* new case has it. The previous case's Study is freed. */
|
|
2674
|
+
replaceLocalCase = async (previous, input) => {
|
|
2675
|
+
const wasActive = this.app.activeLocalId === previous.id;
|
|
2676
|
+
const selectedBus = wasActive ? this.app.selectedBus : null;
|
|
2677
|
+
const displayMode = previous.displayMode;
|
|
2678
|
+
const next = await this.openBalancedModule({
|
|
2679
|
+
...input,
|
|
2680
|
+
formulation: input.formulation ?? previous.formulation,
|
|
2681
|
+
activate: false
|
|
2682
|
+
});
|
|
2683
|
+
if (displayMode === 'diagram' ? next.diagram : next.view)
|
|
2684
|
+
next.displayMode = displayMode;
|
|
2685
|
+
const slot = this.app.localCases.findIndex((c) => c.id === previous.id);
|
|
2686
|
+
// A seq bump invalidates any in-flight browser solve of the old case.
|
|
2687
|
+
previous.solveSeq = (previous.solveSeq ?? 0) + 1;
|
|
2688
|
+
this.disposeStudy(previous);
|
|
2689
|
+
if (wasActive) {
|
|
2690
|
+
this.addAndActivateLocal(next, { frame: input.frame });
|
|
2691
|
+
}
|
|
2692
|
+
else {
|
|
2693
|
+
this.app.localCases = [...this.app.localCases, next];
|
|
2694
|
+
this.maybeStartLocalSolve(next.id);
|
|
2695
|
+
}
|
|
2696
|
+
const cases = this.app.localCases.filter((c) => c.id !== previous.id && c.id !== next.id);
|
|
2697
|
+
cases.splice(slot < 0 ? cases.length : slot, 0, next);
|
|
2698
|
+
this.app.localCases = cases;
|
|
2699
|
+
if (this.app.placingLocalId === previous.id)
|
|
2700
|
+
this.app.placingLocalId = null;
|
|
2701
|
+
if (selectedBus !== null && localHasBus(next, selectedBus)) {
|
|
2702
|
+
void this.selectLocalBus(next.id, selectedBus);
|
|
2703
|
+
}
|
|
2704
|
+
return next;
|
|
2705
|
+
};
|
|
2522
2706
|
/** Build (or reuse) the case's Study and commit it at the current edit state, so a
|
|
2523
2707
|
* save or export captures exactly what is on screen. Null (with an error set) when no
|
|
2524
2708
|
* PowerIO module or Study is available. */
|
|
@@ -2615,7 +2799,7 @@ export class Controller {
|
|
|
2615
2799
|
const col = c.sensitivity;
|
|
2616
2800
|
if (col && col.bus === bus) {
|
|
2617
2801
|
const committedAtBus = this.caseDeltas(c)[bus] ?? 0;
|
|
2618
|
-
const step = (Math.abs(value) <
|
|
2802
|
+
const step = (Math.abs(value) < DEAD_ZONE_MW ? 0 : value) - committedAtBus;
|
|
2619
2803
|
const delta = new Map();
|
|
2620
2804
|
for (const v of col.values)
|
|
2621
2805
|
delta.set(v.bus, v.value * step);
|
|
@@ -2707,7 +2891,7 @@ export class Controller {
|
|
|
2707
2891
|
// tiny nudge reads as "no change at this line".
|
|
2708
2892
|
previewRatings(c, branch, value) {
|
|
2709
2893
|
const ratings = { ...this.caseRatings(c) };
|
|
2710
|
-
if (Math.abs(value) <
|
|
2894
|
+
if (Math.abs(value) < DEAD_ZONE_MW)
|
|
2711
2895
|
delete ratings[branch];
|
|
2712
2896
|
else
|
|
2713
2897
|
ratings[branch] = value;
|
|
@@ -2723,7 +2907,7 @@ export class Controller {
|
|
|
2723
2907
|
if (c.formulation === 'acpf')
|
|
2724
2908
|
return;
|
|
2725
2909
|
const committedAtBranch = this.caseRatings(c)[branch] ?? 0;
|
|
2726
|
-
const step = (Math.abs(value) <
|
|
2910
|
+
const step = (Math.abs(value) < DEAD_ZONE_MW ? 0 : value) - committedAtBranch;
|
|
2727
2911
|
const col = c.sensitivity;
|
|
2728
2912
|
if (col && col.branch === branch) {
|
|
2729
2913
|
const delta = new Map();
|
|
@@ -2827,7 +3011,7 @@ export class Controller {
|
|
|
2827
3011
|
this.runSolve(c, this.selectionTarget);
|
|
2828
3012
|
};
|
|
2829
3013
|
finishRatingInput = (value) => {
|
|
2830
|
-
if (Math.abs(value - this.committedRating) <
|
|
3014
|
+
if (Math.abs(value - this.committedRating) < DEAD_ZONE_MW) {
|
|
2831
3015
|
if (!this.activeSolvable?.solving) {
|
|
2832
3016
|
this.app.previewActive = false;
|
|
2833
3017
|
this.app.previewRatingMw = null;
|
|
@@ -2837,6 +3021,8 @@ export class Controller {
|
|
|
2837
3021
|
this.commitRating(value);
|
|
2838
3022
|
};
|
|
2839
3023
|
restoreDefaultCases = () => {
|
|
3024
|
+
if (!this.loadDefaultCases)
|
|
3025
|
+
return;
|
|
2840
3026
|
try {
|
|
2841
3027
|
if (typeof localStorage !== 'undefined')
|
|
2842
3028
|
localStorage.removeItem(HIDDEN_DEFAULT_CASES_KEY);
|
package/dist/format.d.ts
CHANGED
|
@@ -14,6 +14,15 @@ type SolveMeta = {
|
|
|
14
14
|
solveBackend: SolveBackend | null;
|
|
15
15
|
};
|
|
16
16
|
export declare function solveMetaLabel(c: SolveMeta): string;
|
|
17
|
+
export declare const INFEASIBLE_DISPATCH = "No feasible dispatch: demand cannot be served within line and generator limits";
|
|
18
|
+
/** The message for a local case whose browser solve failed, and the
|
|
19
|
+
* technical detail to disclose behind it. An infeasible DC OPF means the
|
|
20
|
+
* network cannot carry the demand, which deserves plain words. The engine
|
|
21
|
+
* spells it `DC OPF solve infeasible`, and an unbounded one as `unbounded`. */
|
|
22
|
+
export declare function browserSolveFailure(label: string, reason: string | null): {
|
|
23
|
+
message: string;
|
|
24
|
+
detail: string | null;
|
|
25
|
+
};
|
|
17
26
|
export declare function rgbaCss([r, g, b, a]: RGBA): string;
|
|
18
27
|
/** Min and max of a numeric array in one pass. Used instead of
|
|
19
28
|
* `Math.min(...values)` / `Math.max(...values)`, which throw a RangeError once the
|
package/dist/format.js
CHANGED
|
@@ -26,6 +26,18 @@ export function solveMetaLabel(c) {
|
|
|
26
26
|
return 'server price sensitivity';
|
|
27
27
|
return c.solveBackend === 'rust-server' ? 'server solve' : 'browser solve';
|
|
28
28
|
}
|
|
29
|
+
export const INFEASIBLE_DISPATCH = 'No feasible dispatch: demand cannot be served within line and generator limits';
|
|
30
|
+
/** The message for a local case whose browser solve failed, and the
|
|
31
|
+
* technical detail to disclose behind it. An infeasible DC OPF means the
|
|
32
|
+
* network cannot carry the demand, which deserves plain words. The engine
|
|
33
|
+
* spells it `DC OPF solve infeasible`, and an unbounded one as `unbounded`. */
|
|
34
|
+
export function browserSolveFailure(label, reason) {
|
|
35
|
+
const technical = `${label}: browser solve unavailable${reason ? `: ${reason}` : ''}`;
|
|
36
|
+
if (reason && /\binfeasible\b/i.test(reason)) {
|
|
37
|
+
return { message: INFEASIBLE_DISPATCH, detail: technical };
|
|
38
|
+
}
|
|
39
|
+
return { message: technical, detail: null };
|
|
40
|
+
}
|
|
29
41
|
export function rgbaCss([r, g, b, a]) {
|
|
30
42
|
return `rgba(${r}, ${g}, ${b}, ${(a / 255).toFixed(3)})`;
|
|
31
43
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export { default as TellegenViewer } from './TellegenViewer.svelte';
|
|
|
9
9
|
export { default as TellegenMap } from './TellegenMap.svelte';
|
|
10
10
|
export { DEFAULT_TELLEGEN_UI_CONFIG, getAppState, getController, getUiConfig, resolveTellegenUiConfig, setAppState, setController, setUiConfig } from './context.svelte.js';
|
|
11
11
|
export { ApiError, createApiClient } from './api.js';
|
|
12
|
+
export { CARTO_STYLES, OPENFREEMAP_STYLES, attachBasemapFallback, cartoStyleUrl, defaultBasemap, isOpenFreeMapStyle, openFreeMapEdits, resolveBasemap } from './basemap.js';
|
|
13
|
+
export { MAP_PALETTE } from './colors.js';
|
|
12
14
|
export { Controller, createController } from './controller.svelte.js';
|
|
13
15
|
export { AppState, CaseState, LocalCase, MulticonductorCase, createAppState } from './state.svelte.js';
|
|
14
16
|
export { attachmentColor, attachmentGlyph, edgeColor, edgeWidth, phaseColor, neutralTerminal, placeMultiView, buildGeographicView } from './multiconductor.js';
|
|
@@ -17,7 +19,9 @@ export { caseDeltas, caseRatings, displayMetaFor, displaySeriesFor } from './dis
|
|
|
17
19
|
export { DEFAULT_FORMULATION, FORMULATIONS, BrowserStudy, Study, capabilities, createStudy, errorText, formatOf, classifyJson, ingestJsonDrop, ingestCase, isDisplayFile, isPermanentEngineFailure, parseDisplay, solveModule } from '@tellegen/engine';
|
|
18
20
|
export type { BranchRatingDeltas, CaseSummary, DemandDeltas, Network, NetworkBranch, NetworkBus, SensitivityColumn, Solution, SolveIteration, SolveStreamHandlers, TellegenApiClient, TellegenApiClientOptions } from './api.js';
|
|
19
21
|
export type { TellegenUiConfig, TellegenUiOptions } from './context.svelte.js';
|
|
20
|
-
export type { ControllerOptions } from './controller.svelte.js';
|
|
22
|
+
export type { ControllerOptions, OpenBalancedModuleInput } from './controller.svelte.js';
|
|
23
|
+
export type { BasemapFallbackReason, BasemapMapLike, BasemapProvider, BasemapSpec, BasemapStyle, BasemapSwitch, BasemapTheme, DefaultBasemap, MapProjection, MapTheme, ResolvedBasemap, StyleEdit } from './basemap.js';
|
|
24
|
+
export type { MapPalette } from './colors.js';
|
|
21
25
|
export type { DisplayOption } from './display.js';
|
|
22
26
|
export type { CameraSnapshot, DiagramCameraSnapshot, DemandRangeMode, DisplayMode, FallbackTarget, LocalCaseInit, LocalSubstations, MultiCaseSummary, MultiCoordsKind, SolvableCase, StudyDisplaySnapshot, SolveBackend } from './state.svelte.js';
|
|
23
27
|
export type { MultiView, PlacedMultiBus, PlacedMultiEdge } from './multiconductor.js';
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,8 @@ export { default as TellegenViewer } from './TellegenViewer.svelte';
|
|
|
8
8
|
export { default as TellegenMap } from './TellegenMap.svelte';
|
|
9
9
|
export { DEFAULT_TELLEGEN_UI_CONFIG, getAppState, getController, getUiConfig, resolveTellegenUiConfig, setAppState, setController, setUiConfig } from './context.svelte.js';
|
|
10
10
|
export { ApiError, createApiClient } from './api.js';
|
|
11
|
+
export { CARTO_STYLES, OPENFREEMAP_STYLES, attachBasemapFallback, cartoStyleUrl, defaultBasemap, isOpenFreeMapStyle, openFreeMapEdits, resolveBasemap } from './basemap.js';
|
|
12
|
+
export { MAP_PALETTE } from './colors.js';
|
|
11
13
|
export { Controller, createController } from './controller.svelte.js';
|
|
12
14
|
export { AppState, CaseState, LocalCase, MulticonductorCase, createAppState } from './state.svelte.js';
|
|
13
15
|
export { attachmentColor, attachmentGlyph, edgeColor, edgeWidth, phaseColor, neutralTerminal, placeMultiView, buildGeographicView } from './multiconductor.js';
|
package/dist/map.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { ComponentProps } from 'svelte';
|
|
2
2
|
import TellegenMap from './TellegenMap.svelte';
|
|
3
3
|
export { default as TellegenMap } from './TellegenMap.svelte';
|
|
4
|
-
export {
|
|
4
|
+
export { CARTO_STYLES, OPENFREEMAP_STYLES, attachBasemapFallback, cartoStyleUrl, defaultBasemap, isOpenFreeMapStyle, openFreeMapEdits, resolveBasemap } from './basemap.js';
|
|
5
|
+
export type { BasemapFallbackReason, BasemapMapLike, BasemapProvider, BasemapSpec, BasemapStyle, BasemapSwitch, BasemapTheme, DefaultBasemap, MapProjection, MapTheme, ResolvedBasemap, StyleEdit } from './basemap.js';
|
|
6
|
+
export { MAP_PALETTE, branchColor, branchWidth, busNeutral, busRadius, priceColor, priceDomain, priceGradient, scalarDomain, sensColor, sensFlatColor, sensGradient, sensNeutral, sensitivityDomain } from './colors.js';
|
|
5
7
|
export { caseDeltas, displayMetaFor, displaySeriesFor } from './display.js';
|
|
6
|
-
export type { RGBA, SensitivityDomain } from './colors.js';
|
|
8
|
+
export type { MapPalette, RGBA, SensitivityDomain } from './colors.js';
|
|
7
9
|
export type { DisplayOption } from './display.js';
|
|
8
10
|
export type TellegenMapProps = ComponentProps<typeof TellegenMap>;
|