@tellegen/svelte 0.1.2 → 0.2.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 +15 -0
- package/dist/Sparkline.svelte.d.ts +0 -1
- package/dist/TellegenMap.svelte +3 -37
- package/dist/TellegenMap.svelte.d.ts +0 -1
- package/dist/TellegenProvider.svelte.d.ts +0 -1
- package/dist/TellegenShell.svelte.d.ts +0 -1
- package/dist/TellegenViewer.svelte.d.ts +0 -1
- package/dist/api.d.ts +0 -1
- package/dist/colors.d.ts +0 -1
- package/dist/components/AppFooter.svelte.d.ts +0 -1
- package/dist/components/AppHeader.svelte +4 -2
- package/dist/components/AppHeader.svelte.d.ts +0 -1
- package/dist/components/BindingLines.svelte.d.ts +0 -1
- package/dist/components/BusPicker.svelte.d.ts +0 -1
- package/dist/components/ControlPanel.svelte.d.ts +0 -1
- package/dist/components/DemandSlider.svelte +28 -8
- package/dist/components/DemandSlider.svelte.d.ts +0 -1
- package/dist/components/DisplayControls.svelte.d.ts +0 -1
- package/dist/components/DropZone.svelte.d.ts +0 -1
- package/dist/components/FormulationSelector.svelte.d.ts +0 -1
- package/dist/components/LocalCaseDetails.svelte +5 -2
- package/dist/components/LocalCaseDetails.svelte.d.ts +0 -1
- package/dist/components/MulticonductorDetails.svelte +3 -3
- package/dist/components/MulticonductorDetails.svelte.d.ts +0 -1
- package/dist/components/NetworkStats.svelte.d.ts +0 -1
- package/dist/components/PlacementCue.svelte.d.ts +0 -1
- package/dist/components/RatingSlider.svelte.d.ts +0 -1
- package/dist/components/RestoreDefaultsButton.svelte.d.ts +0 -1
- package/dist/components/SensitivityReadout.svelte.d.ts +0 -1
- package/dist/components/SizeLegend.svelte.d.ts +0 -1
- package/dist/components/SolveCard.svelte.d.ts +0 -1
- package/dist/components/TopMovers.svelte.d.ts +0 -1
- package/dist/components.d.ts +0 -1
- package/dist/context.svelte.d.ts +0 -1
- package/dist/controller.svelte.d.ts +24 -12
- package/dist/controller.svelte.js +218 -101
- package/dist/display.d.ts +0 -1
- package/dist/drop-classify.d.ts +5 -42
- package/dist/drop-classify.js +5 -78
- package/dist/drop-limits.d.ts +27 -0
- package/dist/drop-limits.js +83 -0
- package/dist/format.d.ts +0 -1
- package/dist/index.d.ts +3 -5
- package/dist/index.js +2 -2
- package/dist/map-bounds.d.ts +5 -0
- package/dist/map-bounds.js +41 -0
- package/dist/map.d.ts +0 -1
- package/dist/multiconductor.d.ts +0 -1
- package/dist/state.svelte.d.ts +0 -1
- package/dist/study-package.d.ts +0 -1
- package/dist/synthetic-layout.d.ts +0 -1
- package/dist/synthetic-layout.js +16 -2
- package/dist/transformer-icon.d.ts +0 -1
- package/dist/types.d.ts +0 -1
- package/package.json +14 -11
- package/dist/Sparkline.svelte.d.ts.map +0 -1
- package/dist/TellegenMap.svelte.d.ts.map +0 -1
- package/dist/TellegenProvider.svelte.d.ts.map +0 -1
- package/dist/TellegenShell.svelte.d.ts.map +0 -1
- package/dist/TellegenViewer.svelte.d.ts.map +0 -1
- package/dist/api.d.ts.map +0 -1
- package/dist/colors.d.ts.map +0 -1
- package/dist/components/AppFooter.svelte.d.ts.map +0 -1
- package/dist/components/AppHeader.svelte.d.ts.map +0 -1
- package/dist/components/BindingLines.svelte.d.ts.map +0 -1
- package/dist/components/BusPicker.svelte.d.ts.map +0 -1
- package/dist/components/ControlPanel.svelte.d.ts.map +0 -1
- package/dist/components/DemandSlider.svelte.d.ts.map +0 -1
- package/dist/components/DisplayControls.svelte.d.ts.map +0 -1
- package/dist/components/DropZone.svelte.d.ts.map +0 -1
- package/dist/components/FormulationSelector.svelte.d.ts.map +0 -1
- package/dist/components/LocalCaseDetails.svelte.d.ts.map +0 -1
- package/dist/components/MulticonductorDetails.svelte.d.ts.map +0 -1
- package/dist/components/NetworkStats.svelte.d.ts.map +0 -1
- package/dist/components/PlacementCue.svelte.d.ts.map +0 -1
- package/dist/components/RatingSlider.svelte.d.ts.map +0 -1
- package/dist/components/RestoreDefaultsButton.svelte.d.ts.map +0 -1
- package/dist/components/SensitivityReadout.svelte.d.ts.map +0 -1
- package/dist/components/SizeLegend.svelte.d.ts.map +0 -1
- package/dist/components/SolveCard.svelte.d.ts.map +0 -1
- package/dist/components/TopMovers.svelte.d.ts.map +0 -1
- package/dist/components.d.ts.map +0 -1
- package/dist/context.svelte.d.ts.map +0 -1
- package/dist/controller.svelte.d.ts.map +0 -1
- package/dist/display.d.ts.map +0 -1
- package/dist/drop-classify.d.ts.map +0 -1
- package/dist/format.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/map.d.ts.map +0 -1
- package/dist/multiconductor.d.ts.map +0 -1
- package/dist/state.svelte.d.ts.map +0 -1
- package/dist/study-package.d.ts.map +0 -1
- package/dist/synthetic-layout.d.ts.map +0 -1
- package/dist/transformer-icon.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
|
@@ -4,9 +4,10 @@ import { caseDeltas as sharedCaseDeltas, caseRatings as sharedCaseRatings, displ
|
|
|
4
4
|
import { CaseState, LocalCase, MulticonductorCase } from './state.svelte.js';
|
|
5
5
|
import { placeSyntheticTopology } from './synthetic-layout.js';
|
|
6
6
|
import { studyCommitIndex } from './study-package.js';
|
|
7
|
-
import {
|
|
7
|
+
import { distExtensionFormat, isGeoFileName } from './drop-classify.js';
|
|
8
|
+
import { DropBatchGate, readDropFileBytes, validateDropBatch } from './drop-limits.js';
|
|
8
9
|
import { buildGeographicView, placeMultiView } from './multiconductor.js';
|
|
9
|
-
import { applyDisplayGeo, applyGeo, applyLayout, createStudy, exportStudy, extractGeo, FORMULATIONS, formatOf, ingestCase,
|
|
10
|
+
import { applyDisplayGeo, applyGeo, applyLayout, createStudy, exportStudy, extractGeo, FORMULATIONS, formatOf, ingestCase, ingestDistCaseBytes, ingestJsonDrop, isDisplayFile, isPermanentEngineFailure, parseDisplay, parseGeo } from '@tellegen/engine';
|
|
10
11
|
import { errorText, extent, formulationLabel, rgbaCss } from './format.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.
|
|
@@ -21,6 +22,11 @@ const COMPUTE_OFF_NOTICE = 'solving in this browser did not complete, and server
|
|
|
21
22
|
* starts anyway: a missing map or a cue that never appears must not stall the
|
|
22
23
|
* selection. */
|
|
23
24
|
const FOCUS_SETTLE_CAP_MS = 1200;
|
|
25
|
+
/** A lowered analysis row is view-only only when a current producer says so;
|
|
26
|
+
* absence keeps payloads from older compatible engine/server versions editable. */
|
|
27
|
+
export function isDisplayOnlyElement(element) {
|
|
28
|
+
return element?.editable === false;
|
|
29
|
+
}
|
|
24
30
|
function delayUntilFocusSettles(ms, signal) {
|
|
25
31
|
if (ms <= 0 || signal.aborted)
|
|
26
32
|
return Promise.resolve();
|
|
@@ -83,6 +89,9 @@ export class Controller {
|
|
|
83
89
|
// In-flight initial case-list load, so a remount during the first fetch dedupes
|
|
84
90
|
// instead of firing a second concurrent load().
|
|
85
91
|
loading = null;
|
|
92
|
+
// File reads and worker messages retain whole buffers. Admit one validated
|
|
93
|
+
// batch at a time so repeated drops cannot multiply the aggregate byte cap.
|
|
94
|
+
dropBatchGate = new DropBatchGate();
|
|
86
95
|
nearbyRangeAnchor = $state(null);
|
|
87
96
|
// Default cases the user has closed; drives the restore affordance. Seeded in load().
|
|
88
97
|
hiddenDefaults = $state(new Set());
|
|
@@ -270,7 +279,12 @@ export class Controller {
|
|
|
270
279
|
study.free();
|
|
271
280
|
return null;
|
|
272
281
|
}
|
|
273
|
-
this.caseStudies.set(c, {
|
|
282
|
+
this.caseStudies.set(c, {
|
|
283
|
+
study,
|
|
284
|
+
networkJson,
|
|
285
|
+
formulation,
|
|
286
|
+
baseSolution
|
|
287
|
+
});
|
|
274
288
|
return study;
|
|
275
289
|
}
|
|
276
290
|
catch (e) {
|
|
@@ -288,7 +302,12 @@ export class Controller {
|
|
|
288
302
|
this.studyBuilds.delete(c);
|
|
289
303
|
}
|
|
290
304
|
})();
|
|
291
|
-
this.studyBuilds.set(c, {
|
|
305
|
+
this.studyBuilds.set(c, {
|
|
306
|
+
networkJson,
|
|
307
|
+
formulation,
|
|
308
|
+
token,
|
|
309
|
+
promise: build
|
|
310
|
+
});
|
|
292
311
|
return build;
|
|
293
312
|
}
|
|
294
313
|
// The ∂LMP/∂parameter column at `target` for the case's active formulation, solved in
|
|
@@ -299,9 +318,7 @@ export class Controller {
|
|
|
299
318
|
// error.
|
|
300
319
|
async browserSensitivity(c, networkJson, target) {
|
|
301
320
|
const study = await this.getStudy(c, networkJson);
|
|
302
|
-
return study
|
|
303
|
-
? study.sensitivity(c.id, this.caseDeltas(c), this.caseRatings(c), target)
|
|
304
|
-
: null;
|
|
321
|
+
return study ? study.sensitivity(c.id, this.caseDeltas(c), this.caseRatings(c), target) : null;
|
|
305
322
|
}
|
|
306
323
|
// Release a case's Study (if any) when the case is removed.
|
|
307
324
|
disposeStudy(c) {
|
|
@@ -376,7 +393,7 @@ export class Controller {
|
|
|
376
393
|
return deltas;
|
|
377
394
|
}
|
|
378
395
|
demandBounds(mode, bus, center) {
|
|
379
|
-
if (!bus)
|
|
396
|
+
if (!bus || isDisplayOnlyElement(bus))
|
|
380
397
|
return { min: 0, max: 0, span: 0 };
|
|
381
398
|
// Floor, not ceil: -ceil(demand) can push base + delta below zero for
|
|
382
399
|
// non-integer demand, which the server rejects (400) and which is
|
|
@@ -384,7 +401,11 @@ export class Controller {
|
|
|
384
401
|
const physicalMin = -Math.floor(bus.demand_mw);
|
|
385
402
|
const physicalMax = Math.max(Math.ceil(bus.demand_mw), 50);
|
|
386
403
|
if (mode === 'full')
|
|
387
|
-
return {
|
|
404
|
+
return {
|
|
405
|
+
min: physicalMin,
|
|
406
|
+
max: physicalMax,
|
|
407
|
+
span: physicalMax - physicalMin
|
|
408
|
+
};
|
|
388
409
|
const span = Math.max(5, Math.min(25, 0.1 * Math.max(bus.demand_mw, 50)));
|
|
389
410
|
return {
|
|
390
411
|
min: Math.max(physicalMin, center - span),
|
|
@@ -417,7 +438,10 @@ export class Controller {
|
|
|
417
438
|
// Drive the values off the resolved mode, not app.displayMode, so the
|
|
418
439
|
// displayOptions[0] fallback holds while displayMode is stale relative to the
|
|
419
440
|
// formulation (e.g. leaving SOCWR before +page resets it to 'lmp').
|
|
420
|
-
return {
|
|
441
|
+
return {
|
|
442
|
+
...activeMeta,
|
|
443
|
+
values: displaySeriesFor(this.activeSolvable, activeMeta.mode)
|
|
444
|
+
};
|
|
421
445
|
});
|
|
422
446
|
displayStats = $derived.by(() => {
|
|
423
447
|
if (!this.activeDisplay)
|
|
@@ -471,6 +495,7 @@ export class Controller {
|
|
|
471
495
|
sliderBounds = $derived.by(() => this.demandBounds(this.app.demandRangeMode, this.selectedBusData, this.nearbyRangeCenter));
|
|
472
496
|
sliderMin = $derived(this.sliderBounds.min);
|
|
473
497
|
sliderMax = $derived(this.sliderBounds.max);
|
|
498
|
+
sliderDisabled = $derived(!this.selectedBusData || isDisplayOnlyElement(this.selectedBusData));
|
|
474
499
|
committedRating = $derived.by(() => this.activeSolvable && this.app.selectedBranch !== null
|
|
475
500
|
? (this.caseRatings(this.activeSolvable)[this.app.selectedBranch] ?? 0)
|
|
476
501
|
: 0);
|
|
@@ -482,10 +507,14 @@ export class Controller {
|
|
|
482
507
|
// to perturb.
|
|
483
508
|
ratingBounds = $derived.by(() => {
|
|
484
509
|
const b = this.selectedBranchData;
|
|
485
|
-
if (!b || b.rate_mw <= 0)
|
|
510
|
+
if (!b || isDisplayOnlyElement(b) || b.rate_mw <= 0)
|
|
486
511
|
return { min: 0, max: 0, disabled: true };
|
|
487
512
|
const span = Math.min(50, Math.max(5, 0.2 * b.rate_mw));
|
|
488
|
-
return {
|
|
513
|
+
return {
|
|
514
|
+
min: Math.max(-(b.rate_mw - 1), -span),
|
|
515
|
+
max: span,
|
|
516
|
+
disabled: false
|
|
517
|
+
};
|
|
489
518
|
});
|
|
490
519
|
selectedSensitivity = $derived.by(() => {
|
|
491
520
|
const c = this.activeSolvable;
|
|
@@ -838,8 +867,9 @@ export class Controller {
|
|
|
838
867
|
c.coordsKind = 'geofile';
|
|
839
868
|
c.syntheticCenter = undefined;
|
|
840
869
|
c.geoSource = sourceLabel;
|
|
870
|
+
const placedCanonicalBuses = payload.view?.buses.filter((bus) => bus.editable !== false).length ?? 0;
|
|
841
871
|
c.geoWarnings = [
|
|
842
|
-
`${
|
|
872
|
+
`${placedCanonicalBuses} of ${payload.n_bus} buses placed from ${placedFrom}`,
|
|
843
873
|
...extraWarnings,
|
|
844
874
|
...payload.warnings
|
|
845
875
|
];
|
|
@@ -1049,7 +1079,9 @@ export class Controller {
|
|
|
1049
1079
|
this.app.placingLocalId = null;
|
|
1050
1080
|
const { ac, sensitivitySeq } = this.beginBusSelection(c, busId);
|
|
1051
1081
|
try {
|
|
1052
|
-
const sensitivity = await this.browserSensitivity(c, c.networkJson, {
|
|
1082
|
+
const sensitivity = await this.browserSensitivity(c, c.networkJson, {
|
|
1083
|
+
bus: busId
|
|
1084
|
+
});
|
|
1053
1085
|
if (!ac.signal.aborted)
|
|
1054
1086
|
this.acceptSensitivity(c, sensitivity, { bus: busId }, sensitivitySeq);
|
|
1055
1087
|
if (!ac.signal.aborted && !sensitivity) {
|
|
@@ -1102,7 +1134,9 @@ export class Controller {
|
|
|
1102
1134
|
if (ac.signal.aborted)
|
|
1103
1135
|
return;
|
|
1104
1136
|
}
|
|
1105
|
-
const sensitivity = await this.browserSensitivity(c, c.networkJson, {
|
|
1137
|
+
const sensitivity = await this.browserSensitivity(c, c.networkJson, {
|
|
1138
|
+
branch: branchId
|
|
1139
|
+
});
|
|
1106
1140
|
if (ac.signal.aborted)
|
|
1107
1141
|
return;
|
|
1108
1142
|
if (sensitivity) {
|
|
@@ -1325,7 +1359,7 @@ export class Controller {
|
|
|
1325
1359
|
commitDelta = (value) => {
|
|
1326
1360
|
const c = this.activeSolvable;
|
|
1327
1361
|
const bus = this.app.selectedBus;
|
|
1328
|
-
if (!c || bus === null)
|
|
1362
|
+
if (!c || bus === null || this.sliderDisabled)
|
|
1329
1363
|
return;
|
|
1330
1364
|
// Refresh the engine preview at the commit value (a typed value may not have
|
|
1331
1365
|
// driven a drag), then score the commit with the engine's predicted Δobjective.
|
|
@@ -1337,6 +1371,8 @@ export class Controller {
|
|
|
1337
1371
|
this.runSolve(c, { bus });
|
|
1338
1372
|
};
|
|
1339
1373
|
finishDemandInput = (value) => {
|
|
1374
|
+
if (this.sliderDisabled)
|
|
1375
|
+
return;
|
|
1340
1376
|
if (Math.abs(value - this.committedDelta) < 0.25) {
|
|
1341
1377
|
if (!this.activeSolvable?.solving) {
|
|
1342
1378
|
this.app.previewActive = false;
|
|
@@ -1395,32 +1431,61 @@ export class Controller {
|
|
|
1395
1431
|
this.runSolve(c, this.selectionTarget);
|
|
1396
1432
|
};
|
|
1397
1433
|
/** Parse dropped files in the browser via the powerio wasm module. Case
|
|
1398
|
-
* files (.m, .raw, .aux) become local networks; geographic files can
|
|
1434
|
+
* files (.m, .raw, .aux, .epc, .pwb) become local networks; geographic files can
|
|
1399
1435
|
* place those networks; a PowerWorld .pwd becomes a substation point
|
|
1400
1436
|
* preview. Files run serially; nothing uploads. */
|
|
1401
1437
|
ingestFiles = async (files) => {
|
|
1402
|
-
const
|
|
1438
|
+
const release = this.dropBatchGate.enter();
|
|
1439
|
+
if (!release) {
|
|
1440
|
+
this.app.error = 'another file batch is still being parsed';
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
try {
|
|
1444
|
+
let list;
|
|
1445
|
+
try {
|
|
1446
|
+
list = validateDropBatch(files);
|
|
1447
|
+
}
|
|
1448
|
+
catch (e) {
|
|
1449
|
+
this.app.error = errorText(e);
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
await this.ingestFileBatch(list);
|
|
1453
|
+
}
|
|
1454
|
+
catch (e) {
|
|
1455
|
+
this.app.error = errorText(e);
|
|
1456
|
+
}
|
|
1457
|
+
finally {
|
|
1458
|
+
this.app.parsingFile = false;
|
|
1459
|
+
release();
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
ingestFileBatch = async (list) => {
|
|
1403
1463
|
let parsedCaseCount = 0;
|
|
1404
|
-
// True once a dropped case
|
|
1405
|
-
//
|
|
1464
|
+
// True once a dropped balanced case took the geographic sidecars. When no
|
|
1465
|
+
// case parses, the selected existing case may take them at the end.
|
|
1406
1466
|
let geoLayersConsumed = false;
|
|
1407
|
-
// Routable `.json` content (
|
|
1408
|
-
//
|
|
1409
|
-
//
|
|
1410
|
-
//
|
|
1411
|
-
//
|
|
1467
|
+
// Routable `.json` content (saved packages, model JSON, and transmission or
|
|
1468
|
+
// distribution documents) shares the extension with geographic files. Route
|
|
1469
|
+
// it up front, but stage recognized payloads until the geographic/display
|
|
1470
|
+
// sidecars below have parsed. Unrouted JSON retains its first byte buffer so
|
|
1471
|
+
// the geographic fallback does not read a potentially large file twice.
|
|
1412
1472
|
const rest = [];
|
|
1413
|
-
|
|
1473
|
+
const routedJson = [];
|
|
1474
|
+
const jsonBytes = new Map();
|
|
1414
1475
|
for (const file of list) {
|
|
1415
1476
|
if (file.name.toLowerCase().endsWith('.json')) {
|
|
1416
|
-
const
|
|
1417
|
-
if (outcome === '
|
|
1477
|
+
const route = await this.routeJsonDrop(file);
|
|
1478
|
+
if (route.outcome === 'balanced' ||
|
|
1479
|
+
route.outcome === 'restored' ||
|
|
1480
|
+
route.outcome === 'multiconductor') {
|
|
1418
1481
|
parsedCaseCount++;
|
|
1419
|
-
|
|
1482
|
+
routedJson.push({ file, route });
|
|
1420
1483
|
continue;
|
|
1421
1484
|
}
|
|
1422
|
-
if (outcome === 'failed')
|
|
1485
|
+
if (route.outcome === 'failed')
|
|
1423
1486
|
continue;
|
|
1487
|
+
if (route.bytes)
|
|
1488
|
+
jsonBytes.set(file, route.bytes);
|
|
1424
1489
|
}
|
|
1425
1490
|
rest.push(file);
|
|
1426
1491
|
}
|
|
@@ -1428,8 +1493,14 @@ export class Controller {
|
|
|
1428
1493
|
for (const file of rest.filter((f) => isGeoFileName(f.name))) {
|
|
1429
1494
|
this.app.parsingFile = true;
|
|
1430
1495
|
try {
|
|
1431
|
-
const
|
|
1432
|
-
|
|
1496
|
+
const bytes = await readDropFileBytes(file, jsonBytes.get(file));
|
|
1497
|
+
jsonBytes.delete(file);
|
|
1498
|
+
const parsed = await parseGeo(bytes, file.name);
|
|
1499
|
+
geoLayers.push({
|
|
1500
|
+
name: file.name,
|
|
1501
|
+
layer: parsed.layer,
|
|
1502
|
+
warnings: parsed.warnings
|
|
1503
|
+
});
|
|
1433
1504
|
this.app.error = null;
|
|
1434
1505
|
}
|
|
1435
1506
|
catch (e) {
|
|
@@ -1447,7 +1518,12 @@ export class Controller {
|
|
|
1447
1518
|
this.app.parsingFile = true;
|
|
1448
1519
|
try {
|
|
1449
1520
|
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
1450
|
-
displays.push({
|
|
1521
|
+
displays.push({
|
|
1522
|
+
file,
|
|
1523
|
+
bytes,
|
|
1524
|
+
preview: await parseDisplay(bytes),
|
|
1525
|
+
consumed: false
|
|
1526
|
+
});
|
|
1451
1527
|
this.app.error = null;
|
|
1452
1528
|
}
|
|
1453
1529
|
catch (e) {
|
|
@@ -1457,6 +1533,33 @@ export class Controller {
|
|
|
1457
1533
|
this.app.parsingFile = false;
|
|
1458
1534
|
}
|
|
1459
1535
|
}
|
|
1536
|
+
// Materialize routed JSON cases only after sidecars are ready. This keeps
|
|
1537
|
+
// placement ahead of the first solve and preserves the original JSON order.
|
|
1538
|
+
// `prepareDroppedLocal` is a wasm call that takes seconds on a large case,
|
|
1539
|
+
// and the drop gate refuses a second batch throughout, so the UI has to
|
|
1540
|
+
// keep saying so.
|
|
1541
|
+
if (routedJson.length > 0)
|
|
1542
|
+
this.app.parsingFile = true;
|
|
1543
|
+
try {
|
|
1544
|
+
for (const { file, route } of routedJson) {
|
|
1545
|
+
if (route.outcome === 'multiconductor') {
|
|
1546
|
+
this.addMultiCase(file.name, route.payload);
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
if (route.outcome !== 'balanced' && route.outcome !== 'restored')
|
|
1550
|
+
continue;
|
|
1551
|
+
const local = route.outcome === 'restored'
|
|
1552
|
+
? this.localFromPackage(file.name, route.payload)
|
|
1553
|
+
: this.localFromBalancedPayload(file.name, route.payload);
|
|
1554
|
+
const placement = await this.prepareDroppedLocal(local, geoLayers, displays);
|
|
1555
|
+
geoLayersConsumed ||= placement.geoLayersConsumed;
|
|
1556
|
+
this.addAndActivateLocal(local);
|
|
1557
|
+
this.app.error = placement.error;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
finally {
|
|
1561
|
+
this.app.parsingFile = false;
|
|
1562
|
+
}
|
|
1460
1563
|
for (const file of rest.filter((f) => !isGeoFileName(f.name) && !isDisplayFile(f.name))) {
|
|
1461
1564
|
// OpenDSS routes by extension into the multiconductor viewing path.
|
|
1462
1565
|
const distFormat = distExtensionFormat(file.name);
|
|
@@ -1467,13 +1570,13 @@ export class Controller {
|
|
|
1467
1570
|
}
|
|
1468
1571
|
const format = formatOf(file.name);
|
|
1469
1572
|
if (!format) {
|
|
1470
|
-
this.app.error = `${file.name}: not a case or geographic file (.m, .raw, .aux, .dss, .pwd, .csv, .json, .geojson)`;
|
|
1573
|
+
this.app.error = `${file.name}: not a case or geographic file (.m, .raw, .aux, .epc, .pwb, .dss, .pwd, .csv, .json, .geojson)`;
|
|
1471
1574
|
continue;
|
|
1472
1575
|
}
|
|
1473
1576
|
this.app.parsingFile = true;
|
|
1474
1577
|
try {
|
|
1475
|
-
const
|
|
1476
|
-
const { network_json, topology, view, ...summary } = await ingestCase(
|
|
1578
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
1579
|
+
const { network_json, topology, view, ...summary } = await ingestCase(bytes, format);
|
|
1477
1580
|
if (format === 'aux' && (summary.n_branch === 0 || summary.n_gen === 0)) {
|
|
1478
1581
|
this.app.error = `${file.name}: aux parsed, but no complete network; drop the matching .m or .raw case file`;
|
|
1479
1582
|
continue;
|
|
@@ -1492,29 +1595,11 @@ export class Controller {
|
|
|
1492
1595
|
coordsKind: summary.coords_kind,
|
|
1493
1596
|
view
|
|
1494
1597
|
});
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
// dropped alongside a case that already has coordinates takes effect
|
|
1498
|
-
// instead of being silently discarded. A failed apply keeps the parsed
|
|
1499
|
-
// case (placement still works) and surfaces the reason.
|
|
1500
|
-
let geoError = null;
|
|
1501
|
-
if (geoLayers.length > 0 && local.networkJson) {
|
|
1502
|
-
try {
|
|
1503
|
-
await this.applyGeoLayers(local, geoLayers);
|
|
1504
|
-
geoLayersConsumed = true;
|
|
1505
|
-
}
|
|
1506
|
-
catch (e) {
|
|
1507
|
-
geoError = `${geoLayers.map((l) => l.name).join(' + ')}: ${errorText(e)}; use place on map for manual placement`;
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
else if (local.coordsKind === 'synthetic_pending' && local.networkJson) {
|
|
1511
|
-
// No sidecar: a coordinate-less case may still borrow a co-dropped
|
|
1512
|
-
// .pwd's substation points through the SubNum join.
|
|
1513
|
-
await this.fillFromDisplaySibling(local, displays);
|
|
1514
|
-
}
|
|
1598
|
+
const placement = await this.prepareDroppedLocal(local, geoLayers, displays);
|
|
1599
|
+
geoLayersConsumed ||= placement.geoLayersConsumed;
|
|
1515
1600
|
this.addAndActivateLocal(local);
|
|
1516
1601
|
parsedCaseCount++;
|
|
1517
|
-
this.app.error =
|
|
1602
|
+
this.app.error = placement.error; // a successful parse clears a prior file's error
|
|
1518
1603
|
}
|
|
1519
1604
|
catch (e) {
|
|
1520
1605
|
this.app.error = `${file.name}: ${e instanceof Error ? e.message : e}`;
|
|
@@ -1541,13 +1626,34 @@ export class Controller {
|
|
|
1541
1626
|
substations: { points, approximate: true }
|
|
1542
1627
|
}));
|
|
1543
1628
|
}
|
|
1544
|
-
//
|
|
1545
|
-
// case
|
|
1546
|
-
|
|
1547
|
-
if (geoLayers.length > 0 && !geoLayersConsumed && (restoredAnyPackage || parsedCaseCount === 0)) {
|
|
1629
|
+
// When the drop carried only sidecars, apply them to the selected existing
|
|
1630
|
+
// case. Every balanced case parsed in this batch already took them above.
|
|
1631
|
+
if (geoLayers.length > 0 && !geoLayersConsumed && parsedCaseCount === 0) {
|
|
1548
1632
|
await this.applyGeoLayersToExisting(geoLayers);
|
|
1549
1633
|
}
|
|
1550
1634
|
};
|
|
1635
|
+
/** Apply co-dropped geographic data before a local case becomes active and
|
|
1636
|
+
* starts solving. Without geographic data, let a coordinate-less case borrow
|
|
1637
|
+
* the first matching PowerWorld display. A failed geographic join keeps the
|
|
1638
|
+
* parsed case and returns the user-facing placement error. */
|
|
1639
|
+
prepareDroppedLocal = async (local, geoLayers, displays) => {
|
|
1640
|
+
if (geoLayers.length > 0 && local.networkJson) {
|
|
1641
|
+
try {
|
|
1642
|
+
await this.applyGeoLayers(local, geoLayers);
|
|
1643
|
+
return { geoLayersConsumed: true, error: null };
|
|
1644
|
+
}
|
|
1645
|
+
catch (e) {
|
|
1646
|
+
return {
|
|
1647
|
+
geoLayersConsumed: false,
|
|
1648
|
+
error: `${geoLayers.map((l) => l.name).join(' + ')}: ${errorText(e)}; use place on map for manual placement`
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
if (local.coordsKind === 'synthetic_pending' && local.networkJson) {
|
|
1653
|
+
await this.fillFromDisplaySibling(local, displays);
|
|
1654
|
+
}
|
|
1655
|
+
return { geoLayersConsumed: false, error: null };
|
|
1656
|
+
};
|
|
1551
1657
|
/** Fill a coordinate-less case's positions from a co-dropped PowerWorld
|
|
1552
1658
|
* `.pwd`: the display's substation points project to approximate
|
|
1553
1659
|
* longitude/latitude and join onto buses through the `SubNum` extras key.
|
|
@@ -1568,47 +1674,43 @@ export class Controller {
|
|
|
1568
1674
|
}
|
|
1569
1675
|
}
|
|
1570
1676
|
};
|
|
1571
|
-
/** Route a dropped `.json` by its content
|
|
1572
|
-
*
|
|
1573
|
-
*
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
* single owner of the classification. */
|
|
1577
|
-
ingestJsonDrop = async (file) => {
|
|
1578
|
-
let text;
|
|
1677
|
+
/** Route a dropped `.json` by its content. Recognized payloads return without
|
|
1678
|
+
* mutating case state so co-dropped sidecars can be applied first; unrouted
|
|
1679
|
+
* content returns the bytes already read for the geographic fallback. */
|
|
1680
|
+
routeJsonDrop = async (file) => {
|
|
1681
|
+
let bytes;
|
|
1579
1682
|
try {
|
|
1580
|
-
|
|
1683
|
+
bytes = await readDropFileBytes(file);
|
|
1581
1684
|
}
|
|
1582
1685
|
catch {
|
|
1583
|
-
return '
|
|
1686
|
+
return { outcome: 'unrouted', bytes: null };
|
|
1584
1687
|
}
|
|
1585
|
-
const kind = classifyJson(text);
|
|
1586
|
-
if (kind === 'not-json')
|
|
1587
|
-
return 'not-package';
|
|
1588
1688
|
this.app.parsingFile = true;
|
|
1589
1689
|
try {
|
|
1590
|
-
|
|
1591
|
-
|
|
1690
|
+
const result = await ingestJsonDrop(bytes);
|
|
1691
|
+
if (result.kind === 'unknown')
|
|
1692
|
+
return { outcome: 'unrouted', bytes };
|
|
1693
|
+
if (result.kind === 'ambiguous') {
|
|
1694
|
+
this.app.error = `${file.name}: JSON markers name both transmission and distribution formats`;
|
|
1695
|
+
return { outcome: 'failed' };
|
|
1696
|
+
}
|
|
1697
|
+
if (result.kind === 'balanced-package') {
|
|
1592
1698
|
this.app.error = null;
|
|
1593
|
-
return 'restored';
|
|
1699
|
+
return { outcome: 'restored', payload: result.payload };
|
|
1594
1700
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
return 'not-package';
|
|
1605
|
-
this.addMultiCase(file.name, payload);
|
|
1606
|
-
this.app.error = null;
|
|
1607
|
-
return 'viewed';
|
|
1701
|
+
if (result.kind === 'model-json' || result.kind === 'transmission') {
|
|
1702
|
+
this.app.error = null;
|
|
1703
|
+
return { outcome: 'balanced', payload: result.payload };
|
|
1704
|
+
}
|
|
1705
|
+
if (result.kind === 'multiconductor-package' || result.kind === 'distribution') {
|
|
1706
|
+
this.app.error = null;
|
|
1707
|
+
return { outcome: 'multiconductor', payload: result.payload };
|
|
1708
|
+
}
|
|
1709
|
+
throw new Error('engine returned an unsupported JSON drop result');
|
|
1608
1710
|
}
|
|
1609
1711
|
catch (e) {
|
|
1610
1712
|
this.app.error = `${file.name}: ${errorText(e)}`;
|
|
1611
|
-
return 'failed';
|
|
1713
|
+
return { outcome: 'failed' };
|
|
1612
1714
|
}
|
|
1613
1715
|
finally {
|
|
1614
1716
|
this.app.parsingFile = false;
|
|
@@ -1619,7 +1721,8 @@ export class Controller {
|
|
|
1619
1721
|
ingestDistFile = async (file, format) => {
|
|
1620
1722
|
this.app.parsingFile = true;
|
|
1621
1723
|
try {
|
|
1622
|
-
|
|
1724
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
1725
|
+
this.addMultiCase(file.name, await ingestDistCaseBytes(bytes, format));
|
|
1623
1726
|
this.app.error = null;
|
|
1624
1727
|
return true;
|
|
1625
1728
|
}
|
|
@@ -1725,9 +1828,25 @@ export class Controller {
|
|
|
1725
1828
|
else
|
|
1726
1829
|
this.placeLocalCase(lon, lat);
|
|
1727
1830
|
};
|
|
1728
|
-
/** Build a local case from a
|
|
1729
|
-
*
|
|
1730
|
-
|
|
1831
|
+
/** Build a local case from a bare balanced ingest payload without activating
|
|
1832
|
+
* it yet; co-dropped placement data is applied before the first solve. */
|
|
1833
|
+
localFromBalancedPayload = (fileName, payload) => {
|
|
1834
|
+
const { network_json, topology, view, ...summary } = payload;
|
|
1835
|
+
const label = summary.name && summary.name !== 'case' ? summary.name : fileName.replace(/\.[^.]+$/, '');
|
|
1836
|
+
return new LocalCase({
|
|
1837
|
+
id: `local-${++this.localSeq}`,
|
|
1838
|
+
label,
|
|
1839
|
+
fileName,
|
|
1840
|
+
summary,
|
|
1841
|
+
networkJson: network_json,
|
|
1842
|
+
topology,
|
|
1843
|
+
coordsKind: summary.coords_kind,
|
|
1844
|
+
view
|
|
1845
|
+
});
|
|
1846
|
+
};
|
|
1847
|
+
/** Build a local case from a saved package, including its edit state and
|
|
1848
|
+
* formulation, without activating it until sidecars have been applied. */
|
|
1849
|
+
localFromPackage = (fileName, pkg) => {
|
|
1731
1850
|
const { network_json, topology, view, formulation, deltas, rates, ...summary } = pkg;
|
|
1732
1851
|
const id = `local-${++this.localSeq}`;
|
|
1733
1852
|
const label = summary.name && summary.name !== 'case' ? summary.name : fileName.replace(/\.[^.]+$/, '');
|
|
@@ -1744,7 +1863,7 @@ export class Controller {
|
|
|
1744
1863
|
local.deltas = deltas;
|
|
1745
1864
|
local.ratings = rates;
|
|
1746
1865
|
local.formulation = formulation;
|
|
1747
|
-
|
|
1866
|
+
return local;
|
|
1748
1867
|
};
|
|
1749
1868
|
/** Build (or reuse) the case's Study and commit it at the current edit state, so a
|
|
1750
1869
|
* save or export captures exactly what is on screen. Null (with an error set) when no
|
|
@@ -1867,9 +1986,7 @@ export class Controller {
|
|
|
1867
1986
|
const { lmp, objectiveDelta } = await study.preview(this.previewDeltas(c, bus, value), this.caseRatings(c));
|
|
1868
1987
|
// Apply only while this point is still the live drag target: a
|
|
1869
1988
|
// preview that resolves after a re-selection or a commit is stale.
|
|
1870
|
-
if (!this.app.previewActive ||
|
|
1871
|
-
this.activeSolvable !== c ||
|
|
1872
|
-
this.app.selectedBus !== bus)
|
|
1989
|
+
if (!this.app.previewActive || this.activeSolvable !== c || this.app.selectedBus !== bus)
|
|
1873
1990
|
continue;
|
|
1874
1991
|
const delta = new Map();
|
|
1875
1992
|
for (const e of lmp)
|
|
@@ -1891,7 +2008,7 @@ export class Controller {
|
|
|
1891
2008
|
}
|
|
1892
2009
|
}
|
|
1893
2010
|
setSliderPreview = (value) => {
|
|
1894
|
-
if (value === undefined)
|
|
2011
|
+
if (value === undefined || this.sliderDisabled)
|
|
1895
2012
|
return;
|
|
1896
2013
|
this.app.previewActive = true;
|
|
1897
2014
|
this.app.previewDeltaMw = value;
|
|
@@ -1996,7 +2113,7 @@ export class Controller {
|
|
|
1996
2113
|
commitRating = async (value) => {
|
|
1997
2114
|
const c = this.activeSolvable;
|
|
1998
2115
|
const branch = this.app.selectedBranch;
|
|
1999
|
-
if (!c || branch === null)
|
|
2116
|
+
if (!c || branch === null || isDisplayOnlyElement(this.selectedBranchData))
|
|
2000
2117
|
return;
|
|
2001
2118
|
// Refresh the engine preview at the commit value (a typed value may not have
|
|
2002
2119
|
// driven a drag), then score the commit with the engine's predicted Δobjective.
|
package/dist/display.d.ts
CHANGED
|
@@ -28,4 +28,3 @@ export declare function caseDeltas(c: SolvableCase): DemandDeltas;
|
|
|
28
28
|
/** The committed branch rating deltas for a case (MW from base, keyed by branch).
|
|
29
29
|
* The rating counterpart of `caseDeltas`. */
|
|
30
30
|
export declare function caseRatings(c: SolvableCase): BranchRatingDeltas;
|
|
31
|
-
//# sourceMappingURL=display.d.ts.map
|
package/dist/drop-classify.d.ts
CHANGED
|
@@ -1,48 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* the balanced `.m`/`.raw`/`.aux` via `formatOf` in the engine) is name-based;
|
|
4
|
-
* everything under `.json` is content-sniffed here so there is one place that
|
|
5
|
-
* decides package-vs-case and balanced-vs-multiconductor.
|
|
6
|
-
*
|
|
7
|
-
* The rules mirror the readers they feed:
|
|
8
|
-
* - a `.pio.json` package is recognized by its `schema` envelope field, then
|
|
9
|
-
* split by the authoritative `model_kind` (balanced restores a study;
|
|
10
|
-
* multiconductor is viewed);
|
|
11
|
-
* - a non-package document is distribution JSON, PMD when it carries the
|
|
12
|
-
* `data_model` marker and BMOPF otherwise (the same split
|
|
13
|
-
* `powerio_dist` uses for `.json`), except a GeoJSON FeatureCollection,
|
|
14
|
-
* which stays unrouted so the geo sidecar path keeps accepting it.
|
|
15
|
-
*
|
|
16
|
-
* Every function is total: malformed, truncated, or non-JSON input classifies
|
|
17
|
-
* as `not-json` rather than throwing. */
|
|
18
|
-
/** The schema URL prefix every `.pio.json` package envelope carries. */
|
|
19
|
-
export declare const PIO_PACKAGE_SCHEMA_PREFIX = "https://powerio.dev/schema/pio-package";
|
|
20
|
-
/** How a dropped JSON file should be ingested. */
|
|
21
|
-
export type JsonDropKind =
|
|
22
|
-
/** A saved balanced study package: restore the case, edits, and formulation. */
|
|
23
|
-
'balanced-package'
|
|
24
|
-
/** A package carrying a multiconductor payload: view it (no solve). */
|
|
25
|
-
| 'multiconductor-package'
|
|
26
|
-
/** A BMOPF JSON distribution case. */
|
|
27
|
-
| 'bmopf'
|
|
28
|
-
/** A PowerModelsDistribution ENGINEERING JSON case. */
|
|
29
|
-
| 'pmd'
|
|
30
|
-
/** Not JSON, or JSON we do not route (left to the generic error path). */
|
|
31
|
-
| 'not-json';
|
|
1
|
+
/** File name helpers for drop routing. JSON content classification lives in
|
|
2
|
+
* powerio's Rust/Wasm `classify_json_bytes` path, not in this TypeScript layer. */
|
|
32
3
|
/** The distribution format for a file recognized by extension, or null. Only
|
|
33
|
-
* `.dss` is name
|
|
34
|
-
*
|
|
4
|
+
* `.dss` is name routed; PMD and BMOPF share the `.json` extension and are
|
|
5
|
+
* classified by the engine. */
|
|
35
6
|
export declare function distExtensionFormat(name: string): 'dss' | null;
|
|
36
7
|
/** True for geographic sidecar files by extension (`.csv`, `.json`,
|
|
37
8
|
* `.geojson`). Routable `.json` content (packages, distribution documents) is
|
|
38
|
-
* consumed by
|
|
9
|
+
* consumed by the engine classifier before this applies; what remains parses
|
|
39
10
|
* through the engine's tolerant geo reader. */
|
|
40
11
|
export declare function isGeoFileName(name: string): boolean;
|
|
41
|
-
/** Whether `text` is a `.pio.json` package envelope, by its `schema` field. */
|
|
42
|
-
export declare function isPackageEnvelope(obj: Record<string, unknown> | null): boolean;
|
|
43
|
-
/** Classify a dropped JSON document into its ingest path. */
|
|
44
|
-
export declare function classifyJson(text: string): JsonDropKind;
|
|
45
|
-
/** Whether `text` is a saved study package (a balanced `.pio.json`). Kept for
|
|
46
|
-
* the study-restore path; multiconductor packages are viewed, not restored. */
|
|
47
|
-
export declare function isStudyPackageText(text: string): boolean;
|
|
48
|
-
//# sourceMappingURL=drop-classify.d.ts.map
|