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