@tellegen/svelte 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +269 -5
- package/dist/Sparkline.svelte +2 -2
- package/dist/TellegenMap.svelte +518 -202
- package/dist/TellegenMap.svelte.d.ts +45 -0
- package/dist/TellegenProvider.svelte +40 -4
- package/dist/TellegenShell.svelte +24 -46
- package/dist/TellegenViewer.svelte +12 -2
- package/dist/api.d.ts +18 -3
- package/dist/api.js +8 -2
- package/dist/basemap.d.ts +192 -0
- package/dist/basemap.js +259 -0
- package/dist/colors.d.ts +25 -7
- package/dist/colors.js +39 -13
- package/dist/components/AppFooter.svelte +3 -2
- package/dist/components/AppHeader.svelte +44 -19
- package/dist/components/BindingLines.svelte +4 -7
- package/dist/components/BusPicker.svelte +11 -5
- package/dist/components/ControlPanel.svelte +106 -343
- package/dist/components/DemandSlider.svelte +36 -29
- package/dist/components/DiagramCanvas.svelte +486 -0
- package/dist/components/DiagramCanvas.svelte.d.ts +13 -0
- package/dist/components/DisplayControls.svelte +8 -2
- package/dist/components/DropZone.svelte +1 -1
- package/dist/components/FormulationSelector.svelte +9 -4
- package/dist/components/LocalCaseDetails.svelte +61 -48
- package/dist/components/McResults.svelte +428 -0
- package/dist/components/McResults.svelte.d.ts +17 -0
- package/dist/components/ModelDetails.svelte +230 -0
- package/dist/components/ModelDetails.svelte.d.ts +7 -0
- package/dist/components/MulticonductorDetails.svelte +232 -83
- package/dist/components/NetworkStats.svelte +20 -18
- package/dist/components/NotificationCenter.svelte +245 -0
- package/dist/components/NotificationCenter.svelte.d.ts +3 -0
- package/dist/components/PanelCard.svelte +384 -0
- package/dist/components/PanelCard.svelte.d.ts +9 -0
- package/dist/components/PanelFrame.svelte +46 -0
- package/dist/components/PanelFrame.svelte.d.ts +16 -0
- package/dist/components/PanelHost.svelte +237 -0
- package/dist/components/PanelHost.svelte.d.ts +6 -0
- package/dist/components/PlacementCue.svelte +1 -1
- package/dist/components/PowerFlowDetails.svelte +99 -0
- package/dist/components/PowerFlowDetails.svelte.d.ts +3 -0
- package/dist/components/RatingSlider.svelte +19 -14
- package/dist/components/RestoreDefaultsButton.svelte +1 -1
- package/dist/components/SavedNetworkDetails.svelte +214 -0
- package/dist/components/SavedNetworkDetails.svelte.d.ts +3 -0
- package/dist/components/SensitivityReadout.svelte +26 -18
- package/dist/components/ShellEnvironment.svelte +51 -0
- package/dist/components/ShellEnvironment.svelte.d.ts +18 -0
- package/dist/components/SizeLegend.svelte +2 -2
- package/dist/components/SolveCard.svelte +48 -51
- package/dist/components/TopMovers.svelte +9 -4
- package/dist/components/TopMovers.svelte.d.ts +2 -17
- package/dist/components.d.ts +5 -0
- package/dist/components.js +5 -0
- package/dist/context.svelte.d.ts +8 -1
- package/dist/context.svelte.js +8 -3
- package/dist/controller.svelte.d.ts +117 -36
- package/dist/controller.svelte.js +1133 -253
- package/dist/diagram-labels.d.ts +9 -0
- package/dist/diagram-labels.js +40 -0
- package/dist/display.d.ts +3 -7
- package/dist/display.js +31 -27
- package/dist/format.d.ts +9 -0
- package/dist/format.js +17 -4
- package/dist/index.d.ts +15 -5
- package/dist/index.js +9 -2
- package/dist/map.d.ts +4 -2
- package/dist/map.js +2 -1
- package/dist/maplibre-worker.d.ts +5 -0
- package/dist/maplibre-worker.dev.d.ts +11 -0
- package/dist/maplibre-worker.dev.js +15 -0
- package/dist/maplibre-worker.js +8 -0
- package/dist/multiconductor.d.ts +11 -2
- package/dist/multiconductor.js +107 -14
- package/dist/notices.svelte.d.ts +29 -0
- package/dist/notices.svelte.js +86 -0
- package/dist/panels.svelte.d.ts +71 -0
- package/dist/panels.svelte.js +245 -0
- package/dist/slider.d.ts +12 -0
- package/dist/slider.js +31 -0
- package/dist/state.svelte.d.ts +107 -23
- package/dist/state.svelte.js +106 -20
- package/dist/styles.css +59 -6
- package/dist/synthetic-layout.js +1 -0
- package/package.json +29 -11
- package/dist/study-package.d.ts +0 -8
- package/dist/study-package.js +0 -17
|
@@ -2,40 +2,50 @@
|
|
|
2
2
|
import { getAppState, getController } from '../context.svelte.js';
|
|
3
3
|
import { fmt } from '../format.js';
|
|
4
4
|
import type { LocalCase } from '../state.svelte.js';
|
|
5
|
+
import { formatPowerIoDiagnostic, type PowerIoDiagnostic } from '@tellegen/engine';
|
|
5
6
|
|
|
6
7
|
const app = getAppState();
|
|
7
8
|
const ctrl = getController();
|
|
8
9
|
|
|
9
|
-
//
|
|
10
|
-
// `model-json` is powerio's own model document, not a case format.
|
|
10
|
+
// Grid exchange formats the committed state can export.
|
|
11
11
|
const EXPORT_FORMATS = [
|
|
12
12
|
{ token: 'matpower', label: 'MATPOWER (.m)' },
|
|
13
13
|
{ token: 'psse', label: 'PSS/E (.raw)' },
|
|
14
|
+
{ token: 'psse35', label: 'PSS/E v35 (.raw)' },
|
|
14
15
|
{ token: 'powerworld', label: 'PowerWorld (.aux)' },
|
|
15
16
|
{ token: 'pslf', label: 'PSLF (.epc)' },
|
|
16
17
|
{ token: 'powermodels-json', label: 'PowerModels (.json)' },
|
|
17
|
-
{ token: 'pandapower-json', label: 'pandapower (.json)' }
|
|
18
|
-
{ token: 'model-json', label: 'PowerIO model (.json)' }
|
|
18
|
+
{ token: 'pandapower-json', label: 'pandapower (.json)' }
|
|
19
19
|
];
|
|
20
20
|
|
|
21
21
|
let busy = $state(false);
|
|
22
22
|
let exportOpen = $state(false);
|
|
23
|
-
let
|
|
23
|
+
let exportDiagnostics = $state<PowerIoDiagnostic[]>([]);
|
|
24
24
|
|
|
25
|
-
async function
|
|
25
|
+
async function saveCase(lc: LocalCase) {
|
|
26
26
|
busy = true;
|
|
27
|
-
|
|
27
|
+
exportDiagnostics = [];
|
|
28
28
|
try {
|
|
29
|
-
await ctrl.
|
|
29
|
+
await ctrl.saveCaseModule(lc);
|
|
30
30
|
} finally {
|
|
31
31
|
busy = false;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
async function
|
|
35
|
+
async function saveSolution(lc: LocalCase) {
|
|
36
36
|
busy = true;
|
|
37
|
+
exportDiagnostics = [];
|
|
37
38
|
try {
|
|
38
|
-
|
|
39
|
+
await ctrl.saveSolutionModule(lc);
|
|
40
|
+
} finally {
|
|
41
|
+
busy = false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function exportCase(lc: LocalCase, token: string) {
|
|
46
|
+
busy = true;
|
|
47
|
+
try {
|
|
48
|
+
exportDiagnostics = await ctrl.exportCaseAs(lc, token);
|
|
39
49
|
exportOpen = false;
|
|
40
50
|
} finally {
|
|
41
51
|
busy = false;
|
|
@@ -94,20 +104,22 @@
|
|
|
94
104
|
<dd>{fmt.format(lc.summary.base_mva)}</dd>
|
|
95
105
|
</div>
|
|
96
106
|
</dl>
|
|
97
|
-
{#if lc.
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
{
|
|
107
|
+
{#if lc.displayMode === 'diagram' && lc.diagram}
|
|
108
|
+
<p class="footnote mono">Drawing: {lc.diagram.name}</p>
|
|
109
|
+
{#if lc.diagram.warnings.length}
|
|
110
|
+
<details>
|
|
111
|
+
<summary>Drawing details</summary>
|
|
112
|
+
<ul class="warnings mono">
|
|
113
|
+
{#each lc.diagram.warnings as warning, i (i)}<li>{warning}</li>{/each}
|
|
114
|
+
</ul>
|
|
115
|
+
</details>
|
|
116
|
+
{/if}
|
|
117
|
+
{:else if !lc.view}
|
|
108
118
|
<p class="footnote mono">
|
|
109
119
|
no coordinates in this file: click the map or drop a geographic file
|
|
110
120
|
</p>
|
|
121
|
+
{:else if lc.coordsKind === 'synthetic' && lc.summary.coords_method === 'generated'}
|
|
122
|
+
<p class="footnote mono">coordinates: synthetic grid sited on real geography</p>
|
|
111
123
|
{:else if lc.coordsKind === 'synthetic' || lc.coordsKind === 'manual'}
|
|
112
124
|
<p class="footnote mono">
|
|
113
125
|
coordinates: {lc.coordsKind} topology layout centered where it was placed
|
|
@@ -117,22 +129,17 @@
|
|
|
117
129
|
coordinates: geographic file data from {lc.geoSource}
|
|
118
130
|
</p>
|
|
119
131
|
{/if}
|
|
120
|
-
{#if lc.geoWarnings && lc.geoWarnings.length > 0}
|
|
121
|
-
<ul class="warnings mono">
|
|
122
|
-
{#each lc.geoWarnings.slice(0, 4) as w, i (i)}
|
|
123
|
-
<li>{w}</li>
|
|
124
|
-
{/each}
|
|
125
|
-
{#if lc.geoWarnings.length > 4}
|
|
126
|
-
<li>+{lc.geoWarnings.length - 4} more</li>
|
|
127
|
-
{/if}
|
|
128
|
-
</ul>
|
|
129
|
-
{/if}
|
|
130
132
|
<p class="footnote mono">parsed in your browser by powerio (wasm); never uploaded</p>
|
|
131
|
-
{#if lc.
|
|
132
|
-
<div class="
|
|
133
|
-
<button class="reset mono" disabled={busy} onclick={() =>
|
|
134
|
-
save
|
|
133
|
+
{#if lc.studyInputJson}
|
|
134
|
+
<div class="case-actions">
|
|
135
|
+
<button class="reset mono" disabled={busy} onclick={() => saveCase(lc)}>
|
|
136
|
+
save PowerIO module (.json)
|
|
135
137
|
</button>
|
|
138
|
+
{#if lc.formulation === 'dcopf' && lc.solution}
|
|
139
|
+
<button class="reset mono" disabled={busy} onclick={() => saveSolution(lc)}>
|
|
140
|
+
save exact solution (.json)
|
|
141
|
+
</button>
|
|
142
|
+
{/if}
|
|
136
143
|
<div class="export">
|
|
137
144
|
<button
|
|
138
145
|
class="reset mono"
|
|
@@ -146,7 +153,11 @@
|
|
|
146
153
|
<ul class="export-menu mono">
|
|
147
154
|
{#each EXPORT_FORMATS as f (f.token)}
|
|
148
155
|
<li>
|
|
149
|
-
<button
|
|
156
|
+
<button
|
|
157
|
+
class="reset mono"
|
|
158
|
+
disabled={busy}
|
|
159
|
+
onclick={() => exportCase(lc, f.token)}
|
|
160
|
+
>
|
|
150
161
|
{f.label}
|
|
151
162
|
</button>
|
|
152
163
|
</li>
|
|
@@ -154,25 +165,27 @@
|
|
|
154
165
|
</ul>
|
|
155
166
|
{/if}
|
|
156
167
|
</div>
|
|
157
|
-
{#if lc.view}
|
|
168
|
+
{#if lc.view || lc.diagram}
|
|
158
169
|
<button class="reset mono" disabled={busy} onclick={() => downloadLayout(lc)}>
|
|
159
|
-
|
|
170
|
+
{lc.displayMode === 'diagram'
|
|
171
|
+
? 'Download drawing (.geo.json)'
|
|
172
|
+
: 'Download geography (.geo.json)'}
|
|
160
173
|
</button>
|
|
161
174
|
{/if}
|
|
162
175
|
</div>
|
|
163
|
-
{#if
|
|
176
|
+
{#if exportDiagnostics.length > 0}
|
|
164
177
|
<ul class="warnings mono">
|
|
165
|
-
{#each
|
|
166
|
-
<li>{
|
|
178
|
+
{#each exportDiagnostics.slice(0, 4) as diagnostic, i (i)}
|
|
179
|
+
<li>{formatPowerIoDiagnostic(diagnostic)}</li>
|
|
167
180
|
{/each}
|
|
168
|
-
{#if
|
|
169
|
-
<li>+{
|
|
181
|
+
{#if exportDiagnostics.length > 4}
|
|
182
|
+
<li>+{exportDiagnostics.length - 4} more</li>
|
|
170
183
|
{/if}
|
|
171
184
|
</ul>
|
|
172
185
|
{/if}
|
|
173
186
|
{/if}
|
|
174
187
|
{/if}
|
|
175
|
-
{#if lc.topology && lc.coordsKind !== 'file'}
|
|
188
|
+
{#if lc.displayMode !== 'diagram' && lc.topology && lc.coordsKind !== 'file'}
|
|
176
189
|
<button class="reset mono" onclick={() => ctrl.moveLocalCase(lc)}>
|
|
177
190
|
{lc.coordsKind === 'synthetic_pending'
|
|
178
191
|
? 'place on map'
|
|
@@ -212,7 +225,7 @@
|
|
|
212
225
|
|
|
213
226
|
.footnote {
|
|
214
227
|
margin: 8px 0 0;
|
|
215
|
-
font-size:
|
|
228
|
+
font-size: 12px;
|
|
216
229
|
color: var(--text-tertiary);
|
|
217
230
|
letter-spacing: 0;
|
|
218
231
|
}
|
|
@@ -221,12 +234,12 @@
|
|
|
221
234
|
margin: 8px 0 0;
|
|
222
235
|
padding: 0;
|
|
223
236
|
list-style: none;
|
|
224
|
-
font-size:
|
|
237
|
+
font-size: 12.5px;
|
|
225
238
|
line-height: 1.5;
|
|
226
239
|
color: var(--text-accent);
|
|
227
240
|
}
|
|
228
241
|
|
|
229
|
-
.
|
|
242
|
+
.case-actions {
|
|
230
243
|
display: flex;
|
|
231
244
|
flex-wrap: wrap;
|
|
232
245
|
gap: 8px;
|
|
@@ -244,7 +257,7 @@
|
|
|
244
257
|
padding: 4px;
|
|
245
258
|
list-style: none;
|
|
246
259
|
min-width: 100%;
|
|
247
|
-
background: var(--surface,
|
|
260
|
+
background: var(--surface, var(--surface-menu));
|
|
248
261
|
border: 1px solid var(--border, rgba(0, 0, 0, 0.15));
|
|
249
262
|
border-radius: 6px;
|
|
250
263
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import {
|
|
3
|
+
mcPfResultDetail,
|
|
4
|
+
summarizeMcPfResult,
|
|
5
|
+
type DistGraph,
|
|
6
|
+
type McComplex,
|
|
7
|
+
type McPfDetail,
|
|
8
|
+
type McPfDetailQuery,
|
|
9
|
+
type McPfResult,
|
|
10
|
+
type McPfSummary
|
|
11
|
+
} from '@tellegen/engine';
|
|
12
|
+
import { neutralTerminal } from '../multiconductor.js';
|
|
13
|
+
import type { McEditTiming } from '../state.svelte.js';
|
|
14
|
+
let {
|
|
15
|
+
result,
|
|
16
|
+
detail,
|
|
17
|
+
timing = null,
|
|
18
|
+
elapsedMs,
|
|
19
|
+
selectedBus,
|
|
20
|
+
selectedEdge,
|
|
21
|
+
graph
|
|
22
|
+
}: {
|
|
23
|
+
/** A live session's summary, or a stored full result. */
|
|
24
|
+
result: McPfSummary | McPfResult;
|
|
25
|
+
/** Fetches one bus's terminals and a page of equipment ports. A full
|
|
26
|
+
* `result` serves its own pages when this is omitted. */
|
|
27
|
+
detail?: (query: McPfDetailQuery) => Promise<McPfDetail | null>;
|
|
28
|
+
timing?: McEditTiming | null;
|
|
29
|
+
elapsedMs: number | null;
|
|
30
|
+
selectedBus: string | null;
|
|
31
|
+
selectedEdge: string | null;
|
|
32
|
+
graph: DistGraph | null;
|
|
33
|
+
} = $props();
|
|
34
|
+
const summary = $derived('terminals' in result ? summarizeMcPfResult(result) : result);
|
|
35
|
+
const load = $derived.by(() => {
|
|
36
|
+
if (detail) return detail;
|
|
37
|
+
if (!('terminals' in result)) return null;
|
|
38
|
+
const full = result;
|
|
39
|
+
return (query: McPfDetailQuery) => Promise.resolve(mcPfResultDetail(full, query));
|
|
40
|
+
});
|
|
41
|
+
let busChoice = $state('');
|
|
42
|
+
let edgeChoice = $state('');
|
|
43
|
+
const busId = $derived(
|
|
44
|
+
selectedBus ??
|
|
45
|
+
(graph?.buses.some((b) => b.id === busChoice) ? busChoice : graph?.buses[0]?.id) ??
|
|
46
|
+
''
|
|
47
|
+
);
|
|
48
|
+
const bus = $derived(graph?.buses.find((b) => b.id === busId));
|
|
49
|
+
const neutralName = $derived(bus ? neutralTerminal(bus.terminals, bus.neutral_terminal) : null);
|
|
50
|
+
const edgeId = $derived(selectedEdge ?? edgeChoice);
|
|
51
|
+
let search = $state('');
|
|
52
|
+
let page = $state(0);
|
|
53
|
+
const size = 20;
|
|
54
|
+
let portPage = $state(0);
|
|
55
|
+
// Only the selected bus and one equipment page are fetched; the previous
|
|
56
|
+
// rows stay visible until the page for a new operating point arrives.
|
|
57
|
+
let current = $state.raw<McPfDetail | null>(null);
|
|
58
|
+
let requested = 0;
|
|
59
|
+
$effect(() => {
|
|
60
|
+
const fetchDetail = load;
|
|
61
|
+
const query: McPfDetailQuery = {
|
|
62
|
+
bus: busId || null,
|
|
63
|
+
element: edgeId || null,
|
|
64
|
+
port_offset: portPage * size,
|
|
65
|
+
port_limit: size
|
|
66
|
+
};
|
|
67
|
+
void result;
|
|
68
|
+
if (!fetchDetail) {
|
|
69
|
+
current = null;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const seq = ++requested;
|
|
73
|
+
fetchDetail(query).then(
|
|
74
|
+
(next) => {
|
|
75
|
+
if (seq !== requested || !next) return;
|
|
76
|
+
if (next.element_port_total > 0 && query.port_offset! >= next.element_port_total) {
|
|
77
|
+
portPage = 0;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
current = next;
|
|
81
|
+
},
|
|
82
|
+
() => {}
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
const neutral = $derived(
|
|
86
|
+
current?.terminals.find((t) => t.bus === busId && t.terminal === neutralName)?.voltage
|
|
87
|
+
);
|
|
88
|
+
const sourcePower = $derived(summary.source_power_into_network);
|
|
89
|
+
const passiveLoss = $derived(summary.passive_loss.re);
|
|
90
|
+
const terminals = $derived(
|
|
91
|
+
(current?.terminals ?? []).filter(
|
|
92
|
+
(row) =>
|
|
93
|
+
(!busId || row.bus === busId) &&
|
|
94
|
+
`${row.bus} ${row.terminal}`.toLowerCase().includes(search.toLowerCase())
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
const pageCount = $derived(Math.max(1, Math.ceil(terminals.length / size)));
|
|
98
|
+
const currentPage = $derived(Math.min(page, pageCount - 1));
|
|
99
|
+
const visible = $derived(terminals.slice(currentPage * size, (currentPage + 1) * size));
|
|
100
|
+
const portTotal = $derived(current?.element_port_total ?? 0);
|
|
101
|
+
const portPages = $derived(Math.max(1, Math.ceil(portTotal / size)));
|
|
102
|
+
const currentPortPage = $derived(Math.min(portPage, portPages - 1));
|
|
103
|
+
const visiblePorts = $derived(current?.element_ports ?? []);
|
|
104
|
+
const magnitude = (value: McComplex) => Math.hypot(value.re, value.im);
|
|
105
|
+
const fixed = (value: number) => (Number.isFinite(value) ? value.toFixed(3) : '-');
|
|
106
|
+
const ms = (value: number | null) => (value === null ? '-' : `${value.toFixed(1)} ms`);
|
|
107
|
+
const angle = (value: McComplex) =>
|
|
108
|
+
magnitude(value) > 1e-12 ? ((Math.atan2(value.im, value.re) * 180) / Math.PI).toFixed(2) : '-';
|
|
109
|
+
</script>
|
|
110
|
+
|
|
111
|
+
<section aria-label="AC power flow results" class="mc-results">
|
|
112
|
+
<div class="result-heading">
|
|
113
|
+
<strong>AC power flow</strong><span
|
|
114
|
+
>{summary.iterations} iterations{elapsedMs === null
|
|
115
|
+
? ''
|
|
116
|
+
: `, ${Math.round(elapsedMs)} ms`}</span
|
|
117
|
+
>
|
|
118
|
+
</div>
|
|
119
|
+
<dl class="totals">
|
|
120
|
+
<div>
|
|
121
|
+
<dt>Status</dt>
|
|
122
|
+
<dd>{summary.converged ? 'Converged' : 'Not converged'}</dd>
|
|
123
|
+
</div>
|
|
124
|
+
<div>
|
|
125
|
+
<dt>Voltage band</dt>
|
|
126
|
+
<dd>{summary.voltage_valid ? 'Valid' : `${summary.voltage_violation_count} violations`}</dd>
|
|
127
|
+
</div>
|
|
128
|
+
<div>
|
|
129
|
+
<dt>Load voltage range</dt>
|
|
130
|
+
<dd>{summary.min_voltage_pu == null || summary.max_voltage_pu == null ? 'n/a' : `${fixed(summary.min_voltage_pu)}–${fixed(summary.max_voltage_pu)} pu`}</dd>
|
|
131
|
+
</div>
|
|
132
|
+
<div>
|
|
133
|
+
<dt>Source P / Q</dt>
|
|
134
|
+
<dd>{fixed(sourcePower.re / 1000)} / {fixed(sourcePower.im / 1000)} kW / kvar</dd>
|
|
135
|
+
</div>
|
|
136
|
+
<div>
|
|
137
|
+
<dt>Passive loss</dt>
|
|
138
|
+
<dd>{fixed(passiveLoss / 1000)} kW</dd>
|
|
139
|
+
</div>
|
|
140
|
+
</dl>
|
|
141
|
+
<label class="search"
|
|
142
|
+
>Bus result<select
|
|
143
|
+
value={busId}
|
|
144
|
+
onchange={(event) => {
|
|
145
|
+
busChoice = event.currentTarget.value;
|
|
146
|
+
selectedBus = null;
|
|
147
|
+
page = 0;
|
|
148
|
+
}}
|
|
149
|
+
>{#each graph?.buses ?? [] as bus (bus.id)}<option value={bus.id}>{bus.id}</option
|
|
150
|
+
>{/each}</select
|
|
151
|
+
></label
|
|
152
|
+
>
|
|
153
|
+
<label class="search"
|
|
154
|
+
>Terminals<input
|
|
155
|
+
aria-label="Find terminal"
|
|
156
|
+
type="search"
|
|
157
|
+
placeholder="Bus or terminal"
|
|
158
|
+
bind:value={search}
|
|
159
|
+
oninput={() => (page = 0)}
|
|
160
|
+
/></label
|
|
161
|
+
>
|
|
162
|
+
<!-- Scrollable result tables accept keyboard navigation. -->
|
|
163
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
164
|
+
<div class="scroll" role="region" aria-label="Terminal result columns" tabindex="0">
|
|
165
|
+
<table aria-label="Terminal results">
|
|
166
|
+
<thead
|
|
167
|
+
><tr
|
|
168
|
+
><th>Terminal</th><th>To ground<br />V</th><th>Angle<br />deg</th><th
|
|
169
|
+
>Net current<br />A</th
|
|
170
|
+
></tr
|
|
171
|
+
></thead
|
|
172
|
+
>
|
|
173
|
+
<tbody
|
|
174
|
+
>{#each visible as row (JSON.stringify([row.bus, row.terminal]))}
|
|
175
|
+
<tr
|
|
176
|
+
><td>{row.terminal}</td><td>{fixed(magnitude(row.voltage))}</td><td
|
|
177
|
+
>{angle(row.voltage)}</td
|
|
178
|
+
><td>{fixed(magnitude(row.current_into_network))}</td></tr
|
|
179
|
+
>
|
|
180
|
+
{/each}</tbody
|
|
181
|
+
>
|
|
182
|
+
</table>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="pagination">
|
|
185
|
+
<span>{terminals.length} terminals{busId ? ` at bus ${busId}` : ''}</span
|
|
186
|
+
>{#if pageCount > 1}<button
|
|
187
|
+
disabled={currentPage === 0}
|
|
188
|
+
onclick={() => (page = currentPage - 1)}
|
|
189
|
+
aria-label="Previous terminals">Previous</button
|
|
190
|
+
><span>{currentPage + 1} / {pageCount}</span><button
|
|
191
|
+
disabled={currentPage + 1 >= pageCount}
|
|
192
|
+
onclick={() => (page = currentPage + 1)}
|
|
193
|
+
aria-label="Next terminals">Next</button
|
|
194
|
+
>{/if}
|
|
195
|
+
</div>
|
|
196
|
+
{#if neutral}
|
|
197
|
+
<details>
|
|
198
|
+
<summary>Voltages to neutral</summary>
|
|
199
|
+
<!-- Scrollable result tables accept keyboard navigation. -->
|
|
200
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
201
|
+
<div class="scroll" role="region" aria-label="Neutral voltage columns" tabindex="0">
|
|
202
|
+
<table aria-label="Neutral voltage results">
|
|
203
|
+
<thead><tr><th>Terminal</th><th>To neutral<br />V</th><th>Angle<br />deg</th></tr></thead>
|
|
204
|
+
<tbody
|
|
205
|
+
>{#each visible as row (JSON.stringify([row.bus, row.terminal]))}
|
|
206
|
+
{@const relative = {
|
|
207
|
+
re: row.voltage.re - neutral.re,
|
|
208
|
+
im: row.voltage.im - neutral.im
|
|
209
|
+
}}
|
|
210
|
+
<tr
|
|
211
|
+
><td>{row.terminal}</td><td>{fixed(magnitude(relative))}</td><td
|
|
212
|
+
>{angle(relative)}</td
|
|
213
|
+
></tr
|
|
214
|
+
>
|
|
215
|
+
{/each}</tbody
|
|
216
|
+
>
|
|
217
|
+
</table>
|
|
218
|
+
</div>
|
|
219
|
+
</details>
|
|
220
|
+
{/if}
|
|
221
|
+
<details>
|
|
222
|
+
<summary>Equipment currents and power</summary>
|
|
223
|
+
<label class="search"
|
|
224
|
+
>Branch result<select
|
|
225
|
+
value={edgeId}
|
|
226
|
+
onchange={(event) => {
|
|
227
|
+
edgeChoice = event.currentTarget.value;
|
|
228
|
+
selectedEdge = null;
|
|
229
|
+
portPage = 0;
|
|
230
|
+
}}
|
|
231
|
+
><option value="">All equipment</option>{#each graph?.edges ?? [] as edge (edge.id)}<option
|
|
232
|
+
value={edge.id}>{edge.from} - {edge.to}, {edge.id}</option
|
|
233
|
+
>{/each}</select
|
|
234
|
+
></label
|
|
235
|
+
>
|
|
236
|
+
<p>Current enters the named equipment at each bus terminal.</p>
|
|
237
|
+
<!-- Scrollable result tables accept keyboard navigation. -->
|
|
238
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
239
|
+
<div class="scroll" role="region" aria-label="Equipment current columns" tabindex="0">
|
|
240
|
+
<table aria-label="Equipment currents">
|
|
241
|
+
<thead
|
|
242
|
+
><tr
|
|
243
|
+
><th>Equipment</th><th>Bus</th><th>Terminal</th><th>Current<br />A</th><th>P<br />kW</th
|
|
244
|
+
><th>Q<br />kvar</th></tr
|
|
245
|
+
></thead
|
|
246
|
+
><tbody
|
|
247
|
+
>{#each visiblePorts as row (JSON.stringify( [row.kind, row.element, row.branch, row.bus, row.terminal] ))}<tr
|
|
248
|
+
><td>{row.kind} {row.element}</td><td>{row.bus}</td><td>{row.terminal}</td><td
|
|
249
|
+
>{fixed(magnitude(row.current_into_element))}</td
|
|
250
|
+
><td>{fixed(row.power_into_element.re / 1000)}</td><td
|
|
251
|
+
>{fixed(row.power_into_element.im / 1000)}</td
|
|
252
|
+
></tr
|
|
253
|
+
>{/each}</tbody
|
|
254
|
+
>
|
|
255
|
+
</table>
|
|
256
|
+
</div>
|
|
257
|
+
<div class="pagination">
|
|
258
|
+
<span>{portTotal} connections</span>{#if portPages > 1}<button
|
|
259
|
+
disabled={currentPortPage === 0}
|
|
260
|
+
onclick={() => (portPage = currentPortPage - 1)}
|
|
261
|
+
aria-label="Previous currents">Previous</button
|
|
262
|
+
><span>{currentPortPage + 1} / {portPages}</span><button
|
|
263
|
+
disabled={currentPortPage + 1 >= portPages}
|
|
264
|
+
onclick={() => (portPage = currentPortPage + 1)}
|
|
265
|
+
aria-label="Next currents">Next</button
|
|
266
|
+
>{/if}
|
|
267
|
+
</div>
|
|
268
|
+
</details>
|
|
269
|
+
<details>
|
|
270
|
+
<summary>Calculation details</summary>
|
|
271
|
+
<dl>
|
|
272
|
+
<div>
|
|
273
|
+
<dt>Maximum KCL residual</dt>
|
|
274
|
+
<dd>{summary.physical_kcl_residual.toExponential(3)} A</dd>
|
|
275
|
+
</div>
|
|
276
|
+
<div>
|
|
277
|
+
<dt>Scaled KCL residual</dt>
|
|
278
|
+
<dd>{summary.scaled_kcl_residual.toExponential(3)}</dd>
|
|
279
|
+
</div>
|
|
280
|
+
<div>
|
|
281
|
+
<dt>Voltage change</dt>
|
|
282
|
+
<dd>{summary.voltage_change.toExponential(3)} V</dd>
|
|
283
|
+
</div>
|
|
284
|
+
<div>
|
|
285
|
+
<dt>Matrix size</dt>
|
|
286
|
+
<dd>{summary.matrix_dimension}</dd>
|
|
287
|
+
</div>
|
|
288
|
+
<div>
|
|
289
|
+
<dt>Nonzero entries</dt>
|
|
290
|
+
<dd>{summary.matrix_nonzeros}</dd>
|
|
291
|
+
</div>
|
|
292
|
+
<div>
|
|
293
|
+
<dt>Factorizations</dt>
|
|
294
|
+
<dd>{summary.factorization_count}</dd>
|
|
295
|
+
</div>
|
|
296
|
+
{#if timing}
|
|
297
|
+
<div>
|
|
298
|
+
<dt>Engine solve</dt>
|
|
299
|
+
<dd>{ms(timing.engine_ms)}</dd>
|
|
300
|
+
</div>
|
|
301
|
+
<div>
|
|
302
|
+
<dt>Transfer and parse</dt>
|
|
303
|
+
<dd>
|
|
304
|
+
{ms(
|
|
305
|
+
timing.engine_ms === null
|
|
306
|
+
? null
|
|
307
|
+
: timing.round_trip_ms - timing.engine_ms + timing.parse_ms
|
|
308
|
+
)}
|
|
309
|
+
</dd>
|
|
310
|
+
</div>
|
|
311
|
+
<div>
|
|
312
|
+
<dt>Total update</dt>
|
|
313
|
+
<dd>{ms(timing.total_ms)}</dd>
|
|
314
|
+
</div>
|
|
315
|
+
{/if}
|
|
316
|
+
</dl>
|
|
317
|
+
<p>
|
|
318
|
+
Voltage is measured to ground. Net current is the injection into the network. The calculation
|
|
319
|
+
checks current balance at unconstrained terminals.
|
|
320
|
+
</p>
|
|
321
|
+
</details>
|
|
322
|
+
</section>
|
|
323
|
+
|
|
324
|
+
<style>
|
|
325
|
+
.mc-results {
|
|
326
|
+
margin: 14px 0;
|
|
327
|
+
padding-top: 12px;
|
|
328
|
+
border-top: 1px solid var(--line);
|
|
329
|
+
font-size: 11px;
|
|
330
|
+
}
|
|
331
|
+
.result-heading,
|
|
332
|
+
.pagination,
|
|
333
|
+
dl div {
|
|
334
|
+
display: flex;
|
|
335
|
+
align-items: center;
|
|
336
|
+
justify-content: space-between;
|
|
337
|
+
gap: 8px;
|
|
338
|
+
}
|
|
339
|
+
.result-heading span,
|
|
340
|
+
.pagination,
|
|
341
|
+
dt,
|
|
342
|
+
p {
|
|
343
|
+
color: var(--text-secondary);
|
|
344
|
+
}
|
|
345
|
+
.search {
|
|
346
|
+
display: flex;
|
|
347
|
+
flex-direction: column;
|
|
348
|
+
gap: 5px;
|
|
349
|
+
margin: 12px 0 8px;
|
|
350
|
+
}
|
|
351
|
+
input,
|
|
352
|
+
select,
|
|
353
|
+
button {
|
|
354
|
+
font: inherit;
|
|
355
|
+
color: var(--ink);
|
|
356
|
+
background: var(--paper);
|
|
357
|
+
border: 1px solid var(--line);
|
|
358
|
+
border-radius: 3px;
|
|
359
|
+
padding: 5px 7px;
|
|
360
|
+
}
|
|
361
|
+
button {
|
|
362
|
+
cursor: pointer;
|
|
363
|
+
}
|
|
364
|
+
button:disabled {
|
|
365
|
+
opacity: 0.4;
|
|
366
|
+
cursor: default;
|
|
367
|
+
}
|
|
368
|
+
input,
|
|
369
|
+
select {
|
|
370
|
+
width: 100%;
|
|
371
|
+
box-sizing: border-box;
|
|
372
|
+
}
|
|
373
|
+
.scroll {
|
|
374
|
+
overflow: auto;
|
|
375
|
+
max-width: 100%;
|
|
376
|
+
}
|
|
377
|
+
.scroll:focus-visible {
|
|
378
|
+
outline: 2px solid var(--accent);
|
|
379
|
+
outline-offset: 2px;
|
|
380
|
+
}
|
|
381
|
+
table {
|
|
382
|
+
width: 100%;
|
|
383
|
+
border-collapse: collapse;
|
|
384
|
+
font: 10px var(--font-mono);
|
|
385
|
+
}
|
|
386
|
+
th,
|
|
387
|
+
td {
|
|
388
|
+
padding: 6px 5px;
|
|
389
|
+
text-align: right;
|
|
390
|
+
white-space: nowrap;
|
|
391
|
+
border-bottom: 1px solid var(--line);
|
|
392
|
+
}
|
|
393
|
+
th:first-child,
|
|
394
|
+
td:first-child {
|
|
395
|
+
text-align: left;
|
|
396
|
+
}
|
|
397
|
+
th {
|
|
398
|
+
color: var(--text-secondary);
|
|
399
|
+
font-weight: 500;
|
|
400
|
+
}
|
|
401
|
+
.pagination {
|
|
402
|
+
margin: 8px 0;
|
|
403
|
+
justify-content: flex-end;
|
|
404
|
+
}
|
|
405
|
+
.pagination > span:first-child {
|
|
406
|
+
margin-right: auto;
|
|
407
|
+
}
|
|
408
|
+
details {
|
|
409
|
+
margin-top: 12px;
|
|
410
|
+
}
|
|
411
|
+
summary {
|
|
412
|
+
cursor: pointer;
|
|
413
|
+
font-weight: 500;
|
|
414
|
+
}
|
|
415
|
+
p {
|
|
416
|
+
line-height: 1.5;
|
|
417
|
+
margin: 8px 0;
|
|
418
|
+
}
|
|
419
|
+
dl {
|
|
420
|
+
font: 10px var(--font-mono);
|
|
421
|
+
}
|
|
422
|
+
dl div {
|
|
423
|
+
padding: 4px 0;
|
|
424
|
+
}
|
|
425
|
+
dd {
|
|
426
|
+
margin: 0;
|
|
427
|
+
}
|
|
428
|
+
</style>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type DistGraph, type McPfDetail, type McPfDetailQuery, type McPfResult, type McPfSummary } from '@tellegen/engine';
|
|
2
|
+
import type { McEditTiming } from '../state.svelte.js';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
/** A live session's summary, or a stored full result. */
|
|
5
|
+
result: McPfSummary | McPfResult;
|
|
6
|
+
/** Fetches one bus's terminals and a page of equipment ports. A full
|
|
7
|
+
* `result` serves its own pages when this is omitted. */
|
|
8
|
+
detail?: (query: McPfDetailQuery) => Promise<McPfDetail | null>;
|
|
9
|
+
timing?: McEditTiming | null;
|
|
10
|
+
elapsedMs: number | null;
|
|
11
|
+
selectedBus: string | null;
|
|
12
|
+
selectedEdge: string | null;
|
|
13
|
+
graph: DistGraph | null;
|
|
14
|
+
};
|
|
15
|
+
declare const McResults: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
16
|
+
type McResults = ReturnType<typeof McResults>;
|
|
17
|
+
export default McResults;
|