@tellegen/svelte 0.4.7 → 0.5.1
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 +25 -1
- package/dist/TellegenMap.svelte +47 -13
- package/dist/TellegenMap.svelte.d.ts +3 -1
- package/dist/antimeridian.d.ts +21 -0
- package/dist/antimeridian.js +40 -0
- package/dist/basemap.d.ts +10 -0
- package/dist/basemap.js +20 -0
- package/dist/bus-kinds.d.ts +9 -0
- package/dist/bus-kinds.js +33 -0
- 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/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/map-bounds.d.ts +8 -0
- package/dist/map-bounds.js +21 -8
- package/dist/multiconductor.js +38 -4
- package/dist/state.svelte.d.ts +33 -8
- package/dist/state.svelte.js +35 -5
- package/package.json +10 -10
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { DEFAULT_MC_PF_OPTIONS, validateMcPfOptions, type McPfOptions } from '@tellegen/engine';
|
|
3
|
+
let {
|
|
4
|
+
options = $bindable<McPfOptions>({ ...DEFAULT_MC_PF_OPTIONS }),
|
|
5
|
+
disabled = false,
|
|
6
|
+
onapply
|
|
7
|
+
}: {
|
|
8
|
+
options: McPfOptions;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
onapply?: (options: McPfOptions) => Promise<unknown>;
|
|
11
|
+
} = $props();
|
|
12
|
+
let failure = $state('');
|
|
13
|
+
const validation = $derived(validateMcPfOptions(options));
|
|
14
|
+
// Keep one shared set of next-run settings for every solve action. Fill
|
|
15
|
+
// omitted options when opening an older snapshot with partial settings.
|
|
16
|
+
$effect(() => {
|
|
17
|
+
if (Object.keys(DEFAULT_MC_PF_OPTIONS).some((key) => !(key in options))) {
|
|
18
|
+
options = { ...DEFAULT_MC_PF_OPTIONS, ...options };
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
async function apply() {
|
|
22
|
+
failure = '';
|
|
23
|
+
if (validation) return;
|
|
24
|
+
try {
|
|
25
|
+
await onapply?.({ ...options });
|
|
26
|
+
} catch (error) {
|
|
27
|
+
failure = error instanceof Error ? error.message : String(error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<details class="settings">
|
|
33
|
+
<summary>Power flow settings</summary>
|
|
34
|
+
<p>
|
|
35
|
+
These settings apply to both Run power flow and Solve energized network. Editing them does not
|
|
36
|
+
start a calculation. A failed run retains the previous result and the settings used for that
|
|
37
|
+
result.
|
|
38
|
+
</p>
|
|
39
|
+
<fieldset {disabled}>
|
|
40
|
+
<legend>Load-model envelope</legend>
|
|
41
|
+
<label class="toggle"
|
|
42
|
+
><input type="checkbox" bind:checked={options.voltage_envelope} />Use load-model envelope</label
|
|
43
|
+
>
|
|
44
|
+
<p>
|
|
45
|
+
Outside the normal band, the envelope changes the load equations to help convergence. Modified
|
|
46
|
+
results are labelled explicitly. These thresholds are not operating voltage limits.
|
|
47
|
+
</p>
|
|
48
|
+
<label>Vlow (pu)<input type="number" step="any" bind:value={options.v_low_pu} /></label>
|
|
49
|
+
<label>Vmin (pu)<input type="number" step="any" bind:value={options.v_min_pu} /></label>
|
|
50
|
+
<label>Vmax (pu)<input type="number" step="any" bind:value={options.v_max_pu} /></label>
|
|
51
|
+
<p>
|
|
52
|
+
With the envelope disabled, the original load equations are used at every voltage; convergence
|
|
53
|
+
is not guaranteed.
|
|
54
|
+
</p>
|
|
55
|
+
</fieldset>
|
|
56
|
+
<fieldset {disabled}>
|
|
57
|
+
<legend>Numerical solver</legend>
|
|
58
|
+
<label
|
|
59
|
+
>Maximum iterations<input
|
|
60
|
+
type="number"
|
|
61
|
+
min="1"
|
|
62
|
+
step="1"
|
|
63
|
+
bind:value={options.max_iterations}
|
|
64
|
+
/></label
|
|
65
|
+
>
|
|
66
|
+
<label
|
|
67
|
+
>Damping<input type="number" min="0" max="1" step="any" bind:value={options.damping} /></label
|
|
68
|
+
>
|
|
69
|
+
<details>
|
|
70
|
+
<summary>Advanced tolerances</summary>
|
|
71
|
+
<label
|
|
72
|
+
>Voltage-change tolerance (V)<input
|
|
73
|
+
type="number"
|
|
74
|
+
step="any"
|
|
75
|
+
bind:value={options.tolerance}
|
|
76
|
+
/></label
|
|
77
|
+
>
|
|
78
|
+
<label
|
|
79
|
+
>Zero-voltage tolerance (V)<input
|
|
80
|
+
type="number"
|
|
81
|
+
step="any"
|
|
82
|
+
bind:value={options.zero_voltage_tolerance}
|
|
83
|
+
/></label
|
|
84
|
+
>
|
|
85
|
+
<label
|
|
86
|
+
>Absolute KCL tolerance (A)<input
|
|
87
|
+
type="number"
|
|
88
|
+
step="any"
|
|
89
|
+
bind:value={options.absolute_kcl_tolerance}
|
|
90
|
+
/></label
|
|
91
|
+
>
|
|
92
|
+
<label
|
|
93
|
+
>Relative KCL tolerance<input
|
|
94
|
+
type="number"
|
|
95
|
+
step="any"
|
|
96
|
+
bind:value={options.relative_kcl_tolerance}
|
|
97
|
+
/></label
|
|
98
|
+
>
|
|
99
|
+
</details>
|
|
100
|
+
</fieldset>
|
|
101
|
+
{#if validation}<p role="alert">{validation}</p>{/if}
|
|
102
|
+
{#if failure}<p role="alert">{failure}</p>{/if}
|
|
103
|
+
<div class="actions">
|
|
104
|
+
{#if onapply}<button disabled={disabled || !!validation} onclick={() => void apply()}
|
|
105
|
+
>Apply settings and run</button
|
|
106
|
+
>{/if}
|
|
107
|
+
<button
|
|
108
|
+
{disabled}
|
|
109
|
+
onclick={() => {
|
|
110
|
+
options = { ...DEFAULT_MC_PF_OPTIONS };
|
|
111
|
+
failure = '';
|
|
112
|
+
}}>Restore defaults</button
|
|
113
|
+
>
|
|
114
|
+
</div>
|
|
115
|
+
</details>
|
|
116
|
+
|
|
117
|
+
<style>
|
|
118
|
+
.settings {
|
|
119
|
+
margin: 0.75rem 0;
|
|
120
|
+
font-size: 12px;
|
|
121
|
+
}
|
|
122
|
+
summary {
|
|
123
|
+
cursor: pointer;
|
|
124
|
+
font-weight: 600;
|
|
125
|
+
}
|
|
126
|
+
fieldset {
|
|
127
|
+
margin: 0.75rem 0;
|
|
128
|
+
border: 1px solid var(--line);
|
|
129
|
+
border-radius: 4px;
|
|
130
|
+
}
|
|
131
|
+
label {
|
|
132
|
+
display: flex;
|
|
133
|
+
align-items: center;
|
|
134
|
+
justify-content: space-between;
|
|
135
|
+
gap: 0.5rem;
|
|
136
|
+
margin: 0.5rem 0;
|
|
137
|
+
}
|
|
138
|
+
input[type='number'] {
|
|
139
|
+
width: 7rem;
|
|
140
|
+
min-width: 0;
|
|
141
|
+
}
|
|
142
|
+
.toggle {
|
|
143
|
+
justify-content: flex-start;
|
|
144
|
+
}
|
|
145
|
+
p {
|
|
146
|
+
line-height: 1.5;
|
|
147
|
+
}
|
|
148
|
+
.actions {
|
|
149
|
+
display: flex;
|
|
150
|
+
flex-wrap: wrap;
|
|
151
|
+
gap: 0.5rem;
|
|
152
|
+
}
|
|
153
|
+
[role='alert'] {
|
|
154
|
+
color: var(--text-primary);
|
|
155
|
+
border-left: 3px solid #b7791f;
|
|
156
|
+
padding-left: 0.5rem;
|
|
157
|
+
}
|
|
158
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type McPfOptions } from '@tellegen/engine';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
options: McPfOptions;
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
onapply?: (options: McPfOptions) => Promise<unknown>;
|
|
6
|
+
};
|
|
7
|
+
declare const McPfSettings: import("svelte").Component<$$ComponentProps, {}, "options">;
|
|
8
|
+
type McPfSettings = ReturnType<typeof McPfSettings>;
|
|
9
|
+
export default McPfSettings;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import McNetworkScope from './McNetworkScope.svelte';
|
|
3
|
+
import McModelQualification from './McModelQualification.svelte';
|
|
2
4
|
import {
|
|
3
5
|
mcPfResultDetail,
|
|
4
6
|
summarizeMcPfResult,
|
|
@@ -13,6 +15,7 @@
|
|
|
13
15
|
import type { McEditTiming } from '../state.svelte.js';
|
|
14
16
|
let {
|
|
15
17
|
result,
|
|
18
|
+
excludedIslands = [],
|
|
16
19
|
detail,
|
|
17
20
|
timing = null,
|
|
18
21
|
elapsedMs,
|
|
@@ -22,6 +25,7 @@
|
|
|
22
25
|
}: {
|
|
23
26
|
/** A live session's summary, or a stored full result. */
|
|
24
27
|
result: McPfSummary | McPfResult;
|
|
28
|
+
excludedIslands?: import('@tellegen/engine').McExcludedIsland[];
|
|
25
29
|
/** Fetches one bus's terminals and a page of equipment ports. A full
|
|
26
30
|
* `result` serves its own pages when this is omitted. */
|
|
27
31
|
detail?: (query: McPfDetailQuery) => Promise<McPfDetail | null>;
|
|
@@ -120,6 +124,11 @@
|
|
|
120
124
|
</script>
|
|
121
125
|
|
|
122
126
|
<section aria-label="AC power flow results" class="mc-results">
|
|
127
|
+
<McNetworkScope
|
|
128
|
+
scope={summary.network_scope}
|
|
129
|
+
islands={'excluded_islands' in result ? result.excluded_islands : excludedIslands}
|
|
130
|
+
/>
|
|
131
|
+
<McModelQualification model={summary.load_model} />
|
|
123
132
|
<div class="result-heading">
|
|
124
133
|
<strong>AC power flow</strong><span
|
|
125
134
|
>{summary.iterations} iterations{elapsedMs === null
|
|
@@ -133,7 +142,7 @@
|
|
|
133
142
|
<dd>{summary.converged ? 'Converged' : 'Not converged'}</dd>
|
|
134
143
|
</div>
|
|
135
144
|
<div>
|
|
136
|
-
<dt>
|
|
145
|
+
<dt>Load-model voltage band</dt>
|
|
137
146
|
<dd>{summary.voltage_valid ? 'Valid' : `${summary.voltage_violation_count} violations`}</dd>
|
|
138
147
|
</div>
|
|
139
148
|
<div>
|
|
@@ -165,6 +174,12 @@
|
|
|
165
174
|
>{/each}</select
|
|
166
175
|
></label
|
|
167
176
|
>
|
|
177
|
+
{#if excludedIslands.some((island) => island.buses.includes(busId ?? ''))}
|
|
178
|
+
<p role="status">
|
|
179
|
+
This bus is excluded from the calculation because its island has no voltage source. No result
|
|
180
|
+
is available.
|
|
181
|
+
</p>
|
|
182
|
+
{/if}
|
|
168
183
|
<label class="search"
|
|
169
184
|
>Terminals<input
|
|
170
185
|
aria-label="Find terminal"
|
|
@@ -3,6 +3,7 @@ import type { McEditTiming } from '../state.svelte.js';
|
|
|
3
3
|
type $$ComponentProps = {
|
|
4
4
|
/** A live session's summary, or a stored full result. */
|
|
5
5
|
result: McPfSummary | McPfResult;
|
|
6
|
+
excludedIslands?: import('@tellegen/engine').McExcludedIsland[];
|
|
6
7
|
/** Fetches one bus's terminals and a page of equipment ports. A full
|
|
7
8
|
* `result` serves its own pages when this is omitted. */
|
|
8
9
|
detail?: (query: McPfDetailQuery) => Promise<McPfDetail | null>;
|
|
@@ -206,6 +206,7 @@
|
|
|
206
206
|
`Tellegen export ${new Date().toISOString()}`,
|
|
207
207
|
`Case: ${label}`,
|
|
208
208
|
`${snapshot.label}; ${snapshot.iterations ?? '?'} iterations`,
|
|
209
|
+
...(snapshot.provenance ?? []),
|
|
209
210
|
'P/Q are positive into the equipment at each end; voltages are terminal-pair magnitudes'
|
|
210
211
|
]);
|
|
211
212
|
const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
|
|
@@ -227,6 +228,7 @@
|
|
|
227
228
|
onclick={() => onschematic(!schematic)}>{schematic ? 'Show map' : 'Show diagram'}</button
|
|
228
229
|
>{/if}
|
|
229
230
|
</div>
|
|
231
|
+
{#if snapshot}<p class="result-provenance">{snapshot.label}</p>{/if}
|
|
230
232
|
<OmniSearch
|
|
231
233
|
index={search}
|
|
232
234
|
hint={searchHint}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import McNetworkScope from './McNetworkScope.svelte';
|
|
3
|
+
import McModelQualification from './McModelQualification.svelte';
|
|
2
4
|
import { getAppState, getController } from '../context.svelte.js';
|
|
3
5
|
import { fmt, rgbaCss } from '../format.js';
|
|
4
6
|
import {
|
|
@@ -10,7 +12,7 @@
|
|
|
10
12
|
} from '../multiconductor.js';
|
|
11
13
|
import { getPanelLayout } from '../panels.svelte.js';
|
|
12
14
|
import { getNoticeCenter } from '../notices.svelte.js';
|
|
13
|
-
import { type DistAttachmentKind } from '@tellegen/engine';
|
|
15
|
+
import { validateMcPfOptions, type DistAttachmentKind } from '@tellegen/engine';
|
|
14
16
|
|
|
15
17
|
const app = getAppState();
|
|
16
18
|
const ctrl = getController();
|
|
@@ -50,7 +52,10 @@
|
|
|
50
52
|
{#if s}
|
|
51
53
|
<p class="tag">Multiconductor</p>
|
|
52
54
|
<details class="case-details">
|
|
53
|
-
<summary
|
|
55
|
+
<summary
|
|
56
|
+
>{s.n_bus} buses, {s.n_edge}
|
|
57
|
+
{s.n_edge === 1 ? 'branch' : 'branches'}</summary
|
|
58
|
+
>
|
|
54
59
|
<dl>
|
|
55
60
|
<div>
|
|
56
61
|
<dt>Buses</dt>
|
|
@@ -97,6 +102,15 @@
|
|
|
97
102
|
</dd>
|
|
98
103
|
</div>
|
|
99
104
|
</dl>
|
|
105
|
+
<McNetworkScope
|
|
106
|
+
scope={mc.mcPreparedScope}
|
|
107
|
+
islands={mc.mcExcludedIslands}
|
|
108
|
+
preview={!mc.result}
|
|
109
|
+
/>
|
|
110
|
+
{#if !mc.mcPfSupported}<p class="footnote">{mc.mcPfReason}</p>{/if}
|
|
111
|
+
{#if validateMcPfOptions(mc.mcOptions)}<p class="footnote">
|
|
112
|
+
{validateMcPfOptions(mc.mcOptions)} Update Power flow settings in Studies before solving.
|
|
113
|
+
</p>{/if}
|
|
100
114
|
<div class="calculation-actions">
|
|
101
115
|
{#if mc.solving}
|
|
102
116
|
<span role="status">Calculating…</span>{#if mc.solveAbort}<button
|
|
@@ -104,13 +118,13 @@
|
|
|
104
118
|
>{/if}
|
|
105
119
|
{:else}
|
|
106
120
|
<button
|
|
107
|
-
disabled={!mc.mcPfSupported}
|
|
121
|
+
disabled={!mc.mcPfSupported || !!validateMcPfOptions(mc.mcOptions)}
|
|
108
122
|
onclick={() => {
|
|
109
123
|
void ctrl
|
|
110
|
-
.solveMultiCase(mc)
|
|
124
|
+
.solveMultiCase(mc, mc.mcOptions, undefined, mc.mcRequiresEnergizedSolve)
|
|
111
125
|
.then(openResults)
|
|
112
126
|
.catch(() => {});
|
|
113
|
-
}}>
|
|
127
|
+
}}>{mc.mcSolveLabel}</button
|
|
114
128
|
>
|
|
115
129
|
{#if !mc.mcPfSupported}
|
|
116
130
|
<button
|
|
@@ -125,12 +139,17 @@
|
|
|
125
139
|
{/if}
|
|
126
140
|
{/if}
|
|
127
141
|
</div>
|
|
128
|
-
{#if mc.result}<
|
|
142
|
+
{#if mc.result}<McModelQualification model={mc.result.load_model} compact />
|
|
143
|
+
<p class="footnote">Converged, {mc.result.iterations} iterations</p>
|
|
129
144
|
<button class="reset" onclick={openResults}>View results in Studies</button>{/if}
|
|
130
145
|
{/if}
|
|
131
146
|
|
|
132
147
|
{#if mc.selectedBus}
|
|
133
148
|
{@const b = mc.selectedBus}
|
|
149
|
+
{#if mc.mcExcludedIslands.some((island) => island.buses.includes(b.id))}<p role="status">
|
|
150
|
+
This bus belongs to an island with no voltage source and will be excluded from
|
|
151
|
+
energized-network calculations. No power-flow result is available for it.
|
|
152
|
+
</p>{/if}
|
|
134
153
|
<hr />
|
|
135
154
|
<h3>Bus <span class="mono">{b.id}</span></h3>
|
|
136
155
|
<div class="terminals">
|
package/dist/components.d.ts
CHANGED
|
@@ -25,3 +25,6 @@ export { default as TopMovers } from './components/TopMovers.svelte';
|
|
|
25
25
|
export { default as Sparkline } from './Sparkline.svelte';
|
|
26
26
|
export { default as McResults } from './components/McResults.svelte';
|
|
27
27
|
export { default as McWorkbench } from './components/McWorkbench.svelte';
|
|
28
|
+
export { default as McPfSettings } from './components/McPfSettings.svelte';
|
|
29
|
+
export { default as McModelQualification } from './components/McModelQualification.svelte';
|
|
30
|
+
export { default as McNetworkScope } from './components/McNetworkScope.svelte';
|
package/dist/components.js
CHANGED
|
@@ -25,3 +25,6 @@ export { default as TopMovers } from './components/TopMovers.svelte';
|
|
|
25
25
|
export { default as Sparkline } from './Sparkline.svelte';
|
|
26
26
|
export { default as McResults } from './components/McResults.svelte';
|
|
27
27
|
export { default as McWorkbench } from './components/McWorkbench.svelte';
|
|
28
|
+
export { default as McPfSettings } from './components/McPfSettings.svelte';
|
|
29
|
+
export { default as McModelQualification } from './components/McModelQualification.svelte';
|
|
30
|
+
export { default as McNetworkScope } from './components/McNetworkScope.svelte';
|
|
@@ -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;
|