@tellegen/svelte 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +233 -0
- package/dist/Sparkline.svelte +2 -2
- package/dist/TellegenMap.svelte +201 -47
- package/dist/TellegenMap.svelte.d.ts +45 -0
- package/dist/TellegenProvider.svelte +36 -4
- package/dist/TellegenShell.svelte +14 -42
- package/dist/TellegenViewer.svelte +12 -2
- package/dist/api.d.ts +10 -2
- package/dist/api.js +2 -0
- package/dist/basemap.d.ts +192 -0
- package/dist/basemap.js +259 -0
- package/dist/colors.d.ts +18 -0
- package/dist/colors.js +26 -0
- package/dist/components/AppFooter.svelte +3 -2
- package/dist/components/AppHeader.svelte +21 -8
- package/dist/components/BindingLines.svelte +1 -1
- package/dist/components/BusPicker.svelte +3 -3
- package/dist/components/DemandSlider.svelte +16 -7
- package/dist/components/DiagramCanvas.svelte +1 -1
- package/dist/components/DisplayControls.svelte +5 -1
- package/dist/components/DropZone.svelte +1 -1
- package/dist/components/FormulationSelector.svelte +1 -1
- package/dist/components/LocalCaseDetails.svelte +6 -3
- package/dist/components/McResults.svelte +98 -35
- package/dist/components/McResults.svelte.d.ts +8 -2
- package/dist/components/ModelDetails.svelte +5 -5
- package/dist/components/MulticonductorDetails.svelte +9 -9
- package/dist/components/NotificationCenter.svelte +4 -2
- package/dist/components/PanelCard.svelte +72 -7
- package/dist/components/PanelHost.svelte +59 -34
- package/dist/components/PanelHost.svelte.d.ts +4 -1
- package/dist/components/PlacementCue.svelte +1 -1
- package/dist/components/PowerFlowDetails.svelte +1 -1
- package/dist/components/RatingSlider.svelte +14 -5
- package/dist/components/RestoreDefaultsButton.svelte +1 -1
- package/dist/components/SavedNetworkDetails.svelte +3 -3
- package/dist/components/SensitivityReadout.svelte +2 -2
- package/dist/components/ShellEnvironment.svelte +51 -0
- package/dist/components/ShellEnvironment.svelte.d.ts +18 -0
- package/dist/components/SizeLegend.svelte +2 -2
- package/dist/components/SolveCard.svelte +3 -3
- package/dist/components/TopMovers.svelte +1 -1
- package/dist/components.d.ts +3 -0
- package/dist/components.js +3 -0
- package/dist/context.svelte.d.ts +8 -1
- package/dist/context.svelte.js +8 -3
- package/dist/controller.svelte.d.ts +65 -5
- package/dist/controller.svelte.js +262 -76
- package/dist/format.d.ts +9 -0
- package/dist/format.js +12 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -0
- package/dist/map.d.ts +4 -2
- package/dist/map.js +2 -1
- package/dist/maplibre-worker.d.ts +5 -0
- package/dist/maplibre-worker.dev.d.ts +11 -0
- package/dist/maplibre-worker.dev.js +15 -0
- package/dist/maplibre-worker.js +8 -0
- package/dist/notices.svelte.js +5 -1
- package/dist/panels.svelte.d.ts +6 -0
- package/dist/panels.svelte.js +31 -1
- package/dist/slider.d.ts +12 -0
- package/dist/slider.js +31 -0
- package/dist/state.svelte.d.ts +19 -2
- package/dist/state.svelte.js +12 -0
- package/dist/styles.css +59 -6
- package/package.json +13 -8
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
padding: 8px 20px;
|
|
32
32
|
font-size: 10.5px;
|
|
33
33
|
color: var(--text-secondary);
|
|
34
|
-
background: linear-gradient(
|
|
34
|
+
background: linear-gradient(rgb(var(--bg-rgb) / 0), rgb(var(--bg-rgb) / 0.9));
|
|
35
35
|
animation: rise 0.5s 0.24s ease-out both;
|
|
36
36
|
pointer-events: none;
|
|
37
37
|
}
|
|
@@ -53,10 +53,12 @@
|
|
|
53
53
|
color: var(--text-accent);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/* Inside the Network panel on narrow screens, panel text size applies. */
|
|
56
57
|
footer.inline {
|
|
57
58
|
position: static;
|
|
58
59
|
margin-top: 14px;
|
|
59
60
|
padding: 0;
|
|
61
|
+
font-size: 12px;
|
|
60
62
|
background: none;
|
|
61
63
|
animation: none;
|
|
62
64
|
pointer-events: auto;
|
|
@@ -71,7 +73,6 @@
|
|
|
71
73
|
|
|
72
74
|
footer.inline {
|
|
73
75
|
margin-top: 4px;
|
|
74
|
-
font-size: 11px;
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
<header bind:this={headerEl}>
|
|
27
27
|
<div class="brand">
|
|
28
28
|
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true">
|
|
29
|
-
<path d="M4 18 L12 6 L20 18" stroke
|
|
30
|
-
<circle cx="4" cy="18" r="2.4"
|
|
31
|
-
<circle cx="12" cy="6" r="2.4"
|
|
32
|
-
<circle cx="20" cy="18" r="2.4"
|
|
29
|
+
<path class="brand-accent-stroke" d="M4 18 L12 6 L20 18" stroke-width="1.6" fill="none" />
|
|
30
|
+
<circle class="brand-accent" cx="4" cy="18" r="2.4" />
|
|
31
|
+
<circle class="brand-ink" cx="12" cy="6" r="2.4" />
|
|
32
|
+
<circle class="brand-accent" cx="20" cy="18" r="2.4" />
|
|
33
33
|
</svg>
|
|
34
34
|
<h1>tellegen</h1>
|
|
35
35
|
</div>
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
justify-content: space-between;
|
|
131
131
|
gap: 16px;
|
|
132
132
|
padding: 10px 20px;
|
|
133
|
-
background: linear-gradient(
|
|
133
|
+
background: linear-gradient(rgb(var(--bg-rgb) / 0.95), rgb(var(--bg-rgb) / 0));
|
|
134
134
|
animation: drop 0.5s ease-out both;
|
|
135
135
|
}
|
|
136
136
|
|
|
@@ -141,6 +141,19 @@
|
|
|
141
141
|
gap: 10px;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/* The brand mark follows the theme: accent legs, ink apex. */
|
|
145
|
+
.brand-accent {
|
|
146
|
+
fill: var(--accent);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.brand-accent-stroke {
|
|
150
|
+
stroke: var(--accent);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.brand-ink {
|
|
154
|
+
fill: var(--ink);
|
|
155
|
+
}
|
|
156
|
+
|
|
144
157
|
h1 {
|
|
145
158
|
margin: 0;
|
|
146
159
|
font-size: 22px;
|
|
@@ -305,8 +318,8 @@
|
|
|
305
318
|
|
|
306
319
|
/* Ghost chip: standing invitation to drop or pick a case file. */
|
|
307
320
|
.cases > button.ghost {
|
|
308
|
-
background:
|
|
309
|
-
border: 1px dashed
|
|
321
|
+
background: rgb(var(--paper-rgb) / 0.36);
|
|
322
|
+
border: 1px dashed rgb(var(--accent-rgb) / 0.55);
|
|
310
323
|
color: var(--text-secondary);
|
|
311
324
|
box-shadow: inset 0 0 0 1px rgba(212, 116, 34, 0.08);
|
|
312
325
|
}
|
|
@@ -351,7 +364,7 @@
|
|
|
351
364
|
flex-wrap: wrap;
|
|
352
365
|
gap: 8px;
|
|
353
366
|
padding: 8px 10px 12px;
|
|
354
|
-
background: linear-gradient(
|
|
367
|
+
background: linear-gradient(rgb(var(--bg-rgb) / 0.97), rgb(var(--bg-rgb) / 0.72));
|
|
355
368
|
}
|
|
356
369
|
|
|
357
370
|
.brand {
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
|
|
156
156
|
label {
|
|
157
157
|
display: block;
|
|
158
|
-
font-size:
|
|
158
|
+
font-size: 12px;
|
|
159
159
|
text-transform: uppercase;
|
|
160
160
|
margin-bottom: 3px;
|
|
161
161
|
}
|
|
@@ -220,7 +220,7 @@
|
|
|
220
220
|
background: var(--panel);
|
|
221
221
|
border: 1px solid var(--line);
|
|
222
222
|
border-radius: var(--radius-xs, 2px);
|
|
223
|
-
box-shadow:
|
|
223
|
+
box-shadow: var(--elev-menu);
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
li {
|
|
@@ -244,7 +244,7 @@
|
|
|
244
244
|
border: 1px solid var(--line);
|
|
245
245
|
border-radius: var(--radius-xs, 2px);
|
|
246
246
|
box-shadow: var(--elev-1);
|
|
247
|
-
font-size:
|
|
247
|
+
font-size: 12.5px;
|
|
248
248
|
margin: 4px 0 0;
|
|
249
249
|
}
|
|
250
250
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getAppState, getController } from '../context.svelte.js';
|
|
3
3
|
import { fmt, signed } from '../format.js';
|
|
4
|
+
import { sliderKeyValue } from '../slider.js';
|
|
4
5
|
|
|
5
6
|
const app = getAppState();
|
|
6
7
|
const ctrl = getController();
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
<span class="val">{signed(ctrl.sliderValue)} MW</span>
|
|
38
39
|
</div>
|
|
39
40
|
<div class="range-mode">
|
|
40
|
-
<div class="segment mono" aria-label="demand range">
|
|
41
|
+
<div class="segment mono" role="group" aria-label="demand range">
|
|
41
42
|
<button
|
|
42
43
|
type="button"
|
|
43
44
|
class:active={app.demandRangeMode === 'local'}
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
type="button"
|
|
52
53
|
class:active={app.demandRangeMode === 'full'}
|
|
53
54
|
aria-pressed={app.demandRangeMode === 'full'}
|
|
54
|
-
aria-label="full demand
|
|
55
|
+
aria-label="full range of demand"
|
|
55
56
|
{disabled}
|
|
56
57
|
title="range from zero load to the local physical limit"
|
|
57
58
|
onclick={() => ctrl.setDemandRangeMode('full')}>full range</button
|
|
@@ -79,7 +80,15 @@
|
|
|
79
80
|
bind:value={ctrl.sliderCurrent, ctrl.setSliderPreview}
|
|
80
81
|
aria-label="demand delta at selected bus"
|
|
81
82
|
onpointerdown={() => ctrl.setSliderPreview(ctrl.sliderValue)}
|
|
82
|
-
onkeydown={() =>
|
|
83
|
+
onkeydown={(e) => {
|
|
84
|
+
const next = sliderKeyValue(e.key, ctrl.sliderValue, ctrl.sliderMin, ctrl.sliderMax);
|
|
85
|
+
if (next === null) {
|
|
86
|
+
ctrl.setSliderPreview(ctrl.sliderValue);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
ctrl.setSliderPreview(next);
|
|
91
|
+
}}
|
|
83
92
|
onpointerup={(e) => ctrl.finishDemandInput(Number(e.currentTarget.value))}
|
|
84
93
|
onkeyup={(e) => ctrl.finishDemandInput(Number(e.currentTarget.value))}
|
|
85
94
|
onchange={(e) => ctrl.finishDemandInput(Number(e.currentTarget.value))}
|
|
@@ -120,7 +129,7 @@
|
|
|
120
129
|
.slider-head {
|
|
121
130
|
display: flex;
|
|
122
131
|
justify-content: space-between;
|
|
123
|
-
font-size:
|
|
132
|
+
font-size: 13.5px;
|
|
124
133
|
color: var(--text-secondary);
|
|
125
134
|
margin-bottom: 4px;
|
|
126
135
|
}
|
|
@@ -135,7 +144,7 @@
|
|
|
135
144
|
justify-content: space-between;
|
|
136
145
|
gap: 8px;
|
|
137
146
|
margin: 6px 0 7px;
|
|
138
|
-
font-size:
|
|
147
|
+
font-size: 12.5px;
|
|
139
148
|
}
|
|
140
149
|
|
|
141
150
|
.slider-track {
|
|
@@ -256,13 +265,13 @@
|
|
|
256
265
|
|
|
257
266
|
.pred {
|
|
258
267
|
margin: 2px 0 0;
|
|
259
|
-
font-size:
|
|
268
|
+
font-size: 13px;
|
|
260
269
|
min-height: 16px;
|
|
261
270
|
}
|
|
262
271
|
|
|
263
272
|
.score {
|
|
264
273
|
margin: 8px 0 0;
|
|
265
|
-
font-size:
|
|
274
|
+
font-size: 13px;
|
|
266
275
|
color: var(--ink);
|
|
267
276
|
min-height: 16px;
|
|
268
277
|
}
|
|
@@ -350,7 +350,7 @@
|
|
|
350
350
|
cy={bus.lat}
|
|
351
351
|
r={Math.max(5, busRadius(Math.max(bus.demand_mw, bus.gen_mw))) / scale}
|
|
352
352
|
fill={color(bus)}
|
|
353
|
-
stroke={selectedBus === bus.id ? '#2f6fbb' : '
|
|
353
|
+
style:stroke={selectedBus === bus.id ? '#2f6fbb' : 'var(--diagram-bus-stroke)'}
|
|
354
354
|
stroke-width={selectedBus === bus.id ? 3 : 1}
|
|
355
355
|
vector-effect="non-scaling-stroke"
|
|
356
356
|
/>
|
|
@@ -59,10 +59,14 @@
|
|
|
59
59
|
</span>
|
|
60
60
|
{/if}
|
|
61
61
|
</div>
|
|
62
|
-
{:else}
|
|
62
|
+
{:else if ctrl.activeSolvable?.solving}
|
|
63
63
|
<p class="dim small blink">
|
|
64
64
|
Solving {formulationLabel(ctrl.activeFormulation)}…
|
|
65
65
|
</p>
|
|
66
|
+
{:else}
|
|
67
|
+
<p class="dim small">
|
|
68
|
+
No {formulationLabel(ctrl.activeFormulation)} results available.
|
|
69
|
+
</p>
|
|
66
70
|
{/if}
|
|
67
71
|
|
|
68
72
|
<style>
|
|
@@ -11,6 +11,7 @@
|
|
|
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)' },
|
|
@@ -117,6 +118,8 @@
|
|
|
117
118
|
<p class="footnote mono">
|
|
118
119
|
no coordinates in this file: click the map or drop a geographic file
|
|
119
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>
|
|
120
123
|
{:else if lc.coordsKind === 'synthetic' || lc.coordsKind === 'manual'}
|
|
121
124
|
<p class="footnote mono">
|
|
122
125
|
coordinates: {lc.coordsKind} topology layout centered where it was placed
|
|
@@ -222,7 +225,7 @@
|
|
|
222
225
|
|
|
223
226
|
.footnote {
|
|
224
227
|
margin: 8px 0 0;
|
|
225
|
-
font-size:
|
|
228
|
+
font-size: 12px;
|
|
226
229
|
color: var(--text-tertiary);
|
|
227
230
|
letter-spacing: 0;
|
|
228
231
|
}
|
|
@@ -231,7 +234,7 @@
|
|
|
231
234
|
margin: 8px 0 0;
|
|
232
235
|
padding: 0;
|
|
233
236
|
list-style: none;
|
|
234
|
-
font-size:
|
|
237
|
+
font-size: 12.5px;
|
|
235
238
|
line-height: 1.5;
|
|
236
239
|
color: var(--text-accent);
|
|
237
240
|
}
|
|
@@ -254,7 +257,7 @@
|
|
|
254
257
|
padding: 4px;
|
|
255
258
|
list-style: none;
|
|
256
259
|
min-width: 100%;
|
|
257
|
-
background: var(--surface,
|
|
260
|
+
background: var(--surface, var(--surface-menu));
|
|
258
261
|
border: 1px solid var(--border, rgba(0, 0, 0, 0.15));
|
|
259
262
|
border-radius: 6px;
|
|
260
263
|
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
|
|
@@ -1,19 +1,43 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
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';
|
|
3
12
|
import { neutralTerminal } from '../multiconductor.js';
|
|
13
|
+
import type { McEditTiming } from '../state.svelte.js';
|
|
4
14
|
let {
|
|
5
15
|
result,
|
|
16
|
+
detail,
|
|
17
|
+
timing = null,
|
|
6
18
|
elapsedMs,
|
|
7
19
|
selectedBus,
|
|
8
20
|
selectedEdge,
|
|
9
21
|
graph
|
|
10
22
|
}: {
|
|
11
|
-
result
|
|
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;
|
|
12
29
|
elapsedMs: number | null;
|
|
13
30
|
selectedBus: string | null;
|
|
14
31
|
selectedEdge: string | null;
|
|
15
32
|
graph: DistGraph | null;
|
|
16
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
|
+
});
|
|
17
41
|
let busChoice = $state('');
|
|
18
42
|
let edgeChoice = $state('');
|
|
19
43
|
const busId = $derived(
|
|
@@ -23,29 +47,48 @@
|
|
|
23
47
|
);
|
|
24
48
|
const bus = $derived(graph?.buses.find((b) => b.id === busId));
|
|
25
49
|
const neutralName = $derived(bus ? neutralTerminal(bus.terminals, bus.neutral_terminal) : null);
|
|
26
|
-
const neutral = $derived(
|
|
27
|
-
result.terminals.find((t) => t.bus === busId && t.terminal === neutralName)?.voltage
|
|
28
|
-
);
|
|
29
50
|
const edgeId = $derived(selectedEdge ?? edgeChoice);
|
|
30
|
-
const sourcePower = $derived(
|
|
31
|
-
result.source_reactions.reduce(
|
|
32
|
-
(sum, port) => ({
|
|
33
|
-
re: sum.re + port.power_into_network.re,
|
|
34
|
-
im: sum.im + port.power_into_network.im
|
|
35
|
-
}),
|
|
36
|
-
{ re: 0, im: 0 }
|
|
37
|
-
)
|
|
38
|
-
);
|
|
39
|
-
const passiveLoss = $derived(
|
|
40
|
-
result.element_ports
|
|
41
|
-
.filter((port) => !['load', 'generator', 'ibr'].includes(port.kind))
|
|
42
|
-
.reduce((sum, port) => sum + port.power_into_element.re, 0)
|
|
43
|
-
);
|
|
44
51
|
let search = $state('');
|
|
45
52
|
let page = $state(0);
|
|
46
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);
|
|
47
90
|
const terminals = $derived(
|
|
48
|
-
|
|
91
|
+
(current?.terminals ?? []).filter(
|
|
49
92
|
(row) =>
|
|
50
93
|
(!busId || row.bus === busId) &&
|
|
51
94
|
`${row.bus} ${row.terminal}`.toLowerCase().includes(search.toLowerCase())
|
|
@@ -54,13 +97,13 @@
|
|
|
54
97
|
const pageCount = $derived(Math.max(1, Math.ceil(terminals.length / size)));
|
|
55
98
|
const currentPage = $derived(Math.min(page, pageCount - 1));
|
|
56
99
|
const visible = $derived(terminals.slice(currentPage * size, (currentPage + 1) * size));
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
const portPages = $derived(Math.max(1, Math.ceil(ports.length / size)));
|
|
100
|
+
const portTotal = $derived(current?.element_port_total ?? 0);
|
|
101
|
+
const portPages = $derived(Math.max(1, Math.ceil(portTotal / size)));
|
|
60
102
|
const currentPortPage = $derived(Math.min(portPage, portPages - 1));
|
|
61
|
-
const visiblePorts = $derived(
|
|
103
|
+
const visiblePorts = $derived(current?.element_ports ?? []);
|
|
62
104
|
const magnitude = (value: McComplex) => Math.hypot(value.re, value.im);
|
|
63
105
|
const fixed = (value: number) => (Number.isFinite(value) ? value.toFixed(3) : '-');
|
|
106
|
+
const ms = (value: number | null) => (value === null ? '-' : `${value.toFixed(1)} ms`);
|
|
64
107
|
const angle = (value: McComplex) =>
|
|
65
108
|
magnitude(value) > 1e-12 ? ((Math.atan2(value.im, value.re) * 180) / Math.PI).toFixed(2) : '-';
|
|
66
109
|
</script>
|
|
@@ -68,7 +111,7 @@
|
|
|
68
111
|
<section aria-label="AC power flow results" class="mc-results">
|
|
69
112
|
<div class="result-heading">
|
|
70
113
|
<strong>AC power flow</strong><span
|
|
71
|
-
>{
|
|
114
|
+
>{summary.iterations} iterations{elapsedMs === null
|
|
72
115
|
? ''
|
|
73
116
|
: `, ${Math.round(elapsedMs)} ms`}</span
|
|
74
117
|
>
|
|
@@ -76,15 +119,15 @@
|
|
|
76
119
|
<dl class="totals">
|
|
77
120
|
<div>
|
|
78
121
|
<dt>Status</dt>
|
|
79
|
-
<dd>{
|
|
122
|
+
<dd>{summary.converged ? 'Converged' : 'Not converged'}</dd>
|
|
80
123
|
</div>
|
|
81
124
|
<div>
|
|
82
125
|
<dt>Voltage band</dt>
|
|
83
|
-
<dd>{
|
|
126
|
+
<dd>{summary.voltage_valid ? 'Valid' : `${summary.voltage_violation_count} violations`}</dd>
|
|
84
127
|
</div>
|
|
85
128
|
<div>
|
|
86
129
|
<dt>Load voltage range</dt>
|
|
87
|
-
<dd>{
|
|
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>
|
|
88
131
|
</div>
|
|
89
132
|
<div>
|
|
90
133
|
<dt>Source P / Q</dt>
|
|
@@ -212,7 +255,7 @@
|
|
|
212
255
|
</table>
|
|
213
256
|
</div>
|
|
214
257
|
<div class="pagination">
|
|
215
|
-
<span>{
|
|
258
|
+
<span>{portTotal} connections</span>{#if portPages > 1}<button
|
|
216
259
|
disabled={currentPortPage === 0}
|
|
217
260
|
onclick={() => (portPage = currentPortPage - 1)}
|
|
218
261
|
aria-label="Previous currents">Previous</button
|
|
@@ -228,28 +271,48 @@
|
|
|
228
271
|
<dl>
|
|
229
272
|
<div>
|
|
230
273
|
<dt>Maximum KCL residual</dt>
|
|
231
|
-
<dd>{
|
|
274
|
+
<dd>{summary.physical_kcl_residual.toExponential(3)} A</dd>
|
|
232
275
|
</div>
|
|
233
276
|
<div>
|
|
234
277
|
<dt>Scaled KCL residual</dt>
|
|
235
|
-
<dd>{
|
|
278
|
+
<dd>{summary.scaled_kcl_residual.toExponential(3)}</dd>
|
|
236
279
|
</div>
|
|
237
280
|
<div>
|
|
238
281
|
<dt>Voltage change</dt>
|
|
239
|
-
<dd>{
|
|
282
|
+
<dd>{summary.voltage_change.toExponential(3)} V</dd>
|
|
240
283
|
</div>
|
|
241
284
|
<div>
|
|
242
285
|
<dt>Matrix size</dt>
|
|
243
|
-
<dd>{
|
|
286
|
+
<dd>{summary.matrix_dimension}</dd>
|
|
244
287
|
</div>
|
|
245
288
|
<div>
|
|
246
289
|
<dt>Nonzero entries</dt>
|
|
247
|
-
<dd>{
|
|
290
|
+
<dd>{summary.matrix_nonzeros}</dd>
|
|
248
291
|
</div>
|
|
249
292
|
<div>
|
|
250
293
|
<dt>Factorizations</dt>
|
|
251
|
-
<dd>{
|
|
294
|
+
<dd>{summary.factorization_count}</dd>
|
|
252
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}
|
|
253
316
|
</dl>
|
|
254
317
|
<p>
|
|
255
318
|
Voltage is measured to ground. Net current is the injection into the network. The calculation
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type DistGraph, type McPfDetail, type McPfDetailQuery, type McPfResult, type McPfSummary } from '@tellegen/engine';
|
|
2
|
+
import type { McEditTiming } from '../state.svelte.js';
|
|
2
3
|
type $$ComponentProps = {
|
|
3
|
-
result
|
|
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;
|
|
4
10
|
elapsedMs: number | null;
|
|
5
11
|
selectedBus: string | null;
|
|
6
12
|
selectedEdge: string | null;
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
display: grid;
|
|
139
139
|
gap: 6px;
|
|
140
140
|
margin: 12px 0;
|
|
141
|
-
font-size:
|
|
141
|
+
font-size: 13px;
|
|
142
142
|
}
|
|
143
143
|
input {
|
|
144
144
|
min-width: 0;
|
|
@@ -158,7 +158,7 @@
|
|
|
158
158
|
table {
|
|
159
159
|
width: 100%;
|
|
160
160
|
border-collapse: collapse;
|
|
161
|
-
font-size:
|
|
161
|
+
font-size: 12px;
|
|
162
162
|
}
|
|
163
163
|
caption {
|
|
164
164
|
text-align: left;
|
|
@@ -194,7 +194,7 @@
|
|
|
194
194
|
justify-content: space-between;
|
|
195
195
|
gap: 8px;
|
|
196
196
|
margin: 10px 0;
|
|
197
|
-
font-size:
|
|
197
|
+
font-size: 12px;
|
|
198
198
|
color: var(--text-secondary);
|
|
199
199
|
}
|
|
200
200
|
button {
|
|
@@ -218,13 +218,13 @@
|
|
|
218
218
|
}
|
|
219
219
|
.references {
|
|
220
220
|
margin-top: 12px;
|
|
221
|
-
font-size:
|
|
221
|
+
font-size: 12px;
|
|
222
222
|
}
|
|
223
223
|
.references dl > div {
|
|
224
224
|
grid-template-columns: 42px minmax(0, 1fr);
|
|
225
225
|
}
|
|
226
226
|
code {
|
|
227
227
|
overflow-wrap: anywhere;
|
|
228
|
-
font:
|
|
228
|
+
font: 12px/1.5 var(--font-mono);
|
|
229
229
|
}
|
|
230
230
|
</style>
|
|
@@ -306,7 +306,7 @@
|
|
|
306
306
|
|
|
307
307
|
.tag {
|
|
308
308
|
margin: 0 0 10px;
|
|
309
|
-
font-size:
|
|
309
|
+
font-size: 12px;
|
|
310
310
|
color: var(--text-secondary);
|
|
311
311
|
letter-spacing: 0;
|
|
312
312
|
text-transform: uppercase;
|
|
@@ -353,7 +353,7 @@
|
|
|
353
353
|
background: color-mix(in srgb, var(--tc) 26%, transparent);
|
|
354
354
|
color: var(--ink);
|
|
355
355
|
font-family: var(--font-mono);
|
|
356
|
-
font-size:
|
|
356
|
+
font-size: 13px;
|
|
357
357
|
}
|
|
358
358
|
|
|
359
359
|
.term.grounded {
|
|
@@ -377,7 +377,7 @@
|
|
|
377
377
|
border-radius: 3px;
|
|
378
378
|
background: var(--bc);
|
|
379
379
|
color: #fff;
|
|
380
|
-
font-size:
|
|
380
|
+
font-size: 12px;
|
|
381
381
|
letter-spacing: 0;
|
|
382
382
|
}
|
|
383
383
|
|
|
@@ -389,7 +389,7 @@
|
|
|
389
389
|
|
|
390
390
|
.load-editor h4 {
|
|
391
391
|
margin: 0 0 8px;
|
|
392
|
-
font-size:
|
|
392
|
+
font-size: 13px;
|
|
393
393
|
}
|
|
394
394
|
|
|
395
395
|
.load-row {
|
|
@@ -404,7 +404,7 @@
|
|
|
404
404
|
display: flex;
|
|
405
405
|
flex-direction: column;
|
|
406
406
|
gap: 2px;
|
|
407
|
-
font-size:
|
|
407
|
+
font-size: 12px;
|
|
408
408
|
color: var(--text-secondary);
|
|
409
409
|
}
|
|
410
410
|
|
|
@@ -412,11 +412,11 @@
|
|
|
412
412
|
min-width: 0;
|
|
413
413
|
width: 100%;
|
|
414
414
|
box-sizing: border-box;
|
|
415
|
-
font:
|
|
415
|
+
font: 12px var(--font-mono);
|
|
416
416
|
}
|
|
417
417
|
|
|
418
418
|
.load-row button {
|
|
419
|
-
font-size:
|
|
419
|
+
font-size: 12px;
|
|
420
420
|
}
|
|
421
421
|
|
|
422
422
|
/* The global stylesheet's .legend is the 6px color-ramp strip; its height
|
|
@@ -426,7 +426,7 @@
|
|
|
426
426
|
flex-direction: column;
|
|
427
427
|
gap: 4px;
|
|
428
428
|
margin-top: 12px;
|
|
429
|
-
font-size:
|
|
429
|
+
font-size: 12px;
|
|
430
430
|
color: var(--text-secondary);
|
|
431
431
|
}
|
|
432
432
|
|
|
@@ -469,7 +469,7 @@
|
|
|
469
469
|
|
|
470
470
|
.footnote {
|
|
471
471
|
margin: 8px 0 0;
|
|
472
|
-
font-size:
|
|
472
|
+
font-size: 12px;
|
|
473
473
|
color: var(--text-tertiary);
|
|
474
474
|
letter-spacing: 0;
|
|
475
475
|
}
|
|
@@ -12,14 +12,16 @@
|
|
|
12
12
|
const observed = new WeakSet<object>();
|
|
13
13
|
$effect(() => {
|
|
14
14
|
const message = app.error;
|
|
15
|
+
const detail = app.errorDetail;
|
|
15
16
|
const revision = app.errorRevision;
|
|
16
17
|
const retry = app.errorRetry;
|
|
17
18
|
if (message)
|
|
18
19
|
untrack(() =>
|
|
19
20
|
notices.push({
|
|
20
21
|
kind: 'error',
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
// A message with its own detail is already written for people.
|
|
23
|
+
title: detail ? message : errorNoticeTitle(message),
|
|
24
|
+
details: detail ?? message,
|
|
23
25
|
retry: () => ctrl.retryError(),
|
|
24
26
|
canRetry: () =>
|
|
25
27
|
app.errorRevision === revision && app.error === message && app.errorRetry === retry
|