@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.
Files changed (88) hide show
  1. package/README.md +269 -5
  2. package/dist/Sparkline.svelte +2 -2
  3. package/dist/TellegenMap.svelte +518 -202
  4. package/dist/TellegenMap.svelte.d.ts +45 -0
  5. package/dist/TellegenProvider.svelte +40 -4
  6. package/dist/TellegenShell.svelte +24 -46
  7. package/dist/TellegenViewer.svelte +12 -2
  8. package/dist/api.d.ts +18 -3
  9. package/dist/api.js +8 -2
  10. package/dist/basemap.d.ts +192 -0
  11. package/dist/basemap.js +259 -0
  12. package/dist/colors.d.ts +25 -7
  13. package/dist/colors.js +39 -13
  14. package/dist/components/AppFooter.svelte +3 -2
  15. package/dist/components/AppHeader.svelte +44 -19
  16. package/dist/components/BindingLines.svelte +4 -7
  17. package/dist/components/BusPicker.svelte +11 -5
  18. package/dist/components/ControlPanel.svelte +106 -343
  19. package/dist/components/DemandSlider.svelte +36 -29
  20. package/dist/components/DiagramCanvas.svelte +486 -0
  21. package/dist/components/DiagramCanvas.svelte.d.ts +13 -0
  22. package/dist/components/DisplayControls.svelte +8 -2
  23. package/dist/components/DropZone.svelte +1 -1
  24. package/dist/components/FormulationSelector.svelte +9 -4
  25. package/dist/components/LocalCaseDetails.svelte +61 -48
  26. package/dist/components/McResults.svelte +428 -0
  27. package/dist/components/McResults.svelte.d.ts +17 -0
  28. package/dist/components/ModelDetails.svelte +230 -0
  29. package/dist/components/ModelDetails.svelte.d.ts +7 -0
  30. package/dist/components/MulticonductorDetails.svelte +232 -83
  31. package/dist/components/NetworkStats.svelte +20 -18
  32. package/dist/components/NotificationCenter.svelte +245 -0
  33. package/dist/components/NotificationCenter.svelte.d.ts +3 -0
  34. package/dist/components/PanelCard.svelte +384 -0
  35. package/dist/components/PanelCard.svelte.d.ts +9 -0
  36. package/dist/components/PanelFrame.svelte +46 -0
  37. package/dist/components/PanelFrame.svelte.d.ts +16 -0
  38. package/dist/components/PanelHost.svelte +237 -0
  39. package/dist/components/PanelHost.svelte.d.ts +6 -0
  40. package/dist/components/PlacementCue.svelte +1 -1
  41. package/dist/components/PowerFlowDetails.svelte +99 -0
  42. package/dist/components/PowerFlowDetails.svelte.d.ts +3 -0
  43. package/dist/components/RatingSlider.svelte +19 -14
  44. package/dist/components/RestoreDefaultsButton.svelte +1 -1
  45. package/dist/components/SavedNetworkDetails.svelte +214 -0
  46. package/dist/components/SavedNetworkDetails.svelte.d.ts +3 -0
  47. package/dist/components/SensitivityReadout.svelte +26 -18
  48. package/dist/components/ShellEnvironment.svelte +51 -0
  49. package/dist/components/ShellEnvironment.svelte.d.ts +18 -0
  50. package/dist/components/SizeLegend.svelte +2 -2
  51. package/dist/components/SolveCard.svelte +48 -51
  52. package/dist/components/TopMovers.svelte +9 -4
  53. package/dist/components/TopMovers.svelte.d.ts +2 -17
  54. package/dist/components.d.ts +5 -0
  55. package/dist/components.js +5 -0
  56. package/dist/context.svelte.d.ts +8 -1
  57. package/dist/context.svelte.js +8 -3
  58. package/dist/controller.svelte.d.ts +117 -36
  59. package/dist/controller.svelte.js +1133 -253
  60. package/dist/diagram-labels.d.ts +9 -0
  61. package/dist/diagram-labels.js +40 -0
  62. package/dist/display.d.ts +3 -7
  63. package/dist/display.js +31 -27
  64. package/dist/format.d.ts +9 -0
  65. package/dist/format.js +17 -4
  66. package/dist/index.d.ts +15 -5
  67. package/dist/index.js +9 -2
  68. package/dist/map.d.ts +4 -2
  69. package/dist/map.js +2 -1
  70. package/dist/maplibre-worker.d.ts +5 -0
  71. package/dist/maplibre-worker.dev.d.ts +11 -0
  72. package/dist/maplibre-worker.dev.js +15 -0
  73. package/dist/maplibre-worker.js +8 -0
  74. package/dist/multiconductor.d.ts +11 -2
  75. package/dist/multiconductor.js +107 -14
  76. package/dist/notices.svelte.d.ts +29 -0
  77. package/dist/notices.svelte.js +86 -0
  78. package/dist/panels.svelte.d.ts +71 -0
  79. package/dist/panels.svelte.js +245 -0
  80. package/dist/slider.d.ts +12 -0
  81. package/dist/slider.js +31 -0
  82. package/dist/state.svelte.d.ts +107 -23
  83. package/dist/state.svelte.js +106 -20
  84. package/dist/styles.css +59 -6
  85. package/dist/synthetic-layout.js +1 -0
  86. package/package.json +29 -11
  87. package/dist/study-package.d.ts +0 -8
  88. 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
- // Export tokens the committed state writes to; labels carry their extensions.
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 exportWarnings = $state<string[]>([]);
23
+ let exportDiagnostics = $state<PowerIoDiagnostic[]>([]);
24
24
 
25
- async function saveStudy(lc: LocalCase) {
25
+ async function saveCase(lc: LocalCase) {
26
26
  busy = true;
27
- exportWarnings = [];
27
+ exportDiagnostics = [];
28
28
  try {
29
- await ctrl.saveStudyPackage(lc);
29
+ await ctrl.saveCaseModule(lc);
30
30
  } finally {
31
31
  busy = false;
32
32
  }
33
33
  }
34
34
 
35
- async function exportStudy(lc: LocalCase, token: string) {
35
+ async function saveSolution(lc: LocalCase) {
36
36
  busy = true;
37
+ exportDiagnostics = [];
37
38
  try {
38
- exportWarnings = await ctrl.exportStudyAs(lc, token);
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.summary.warnings.length > 0}
98
- <ul class="warnings mono">
99
- {#each lc.summary.warnings.slice(0, 4) as w, i (i)}
100
- <li>{w}</li>
101
- {/each}
102
- {#if lc.summary.warnings.length > 4}
103
- <li>+{lc.summary.warnings.length - 4} more</li>
104
- {/if}
105
- </ul>
106
- {/if}
107
- {#if !lc.view}
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.networkJson}
132
- <div class="study">
133
- <button class="reset mono" disabled={busy} onclick={() => saveStudy(lc)}>
134
- save study (.pio.json)
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 class="reset mono" disabled={busy} onclick={() => exportStudy(lc, f.token)}>
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
- download layout (.geo.json)
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 exportWarnings.length > 0}
176
+ {#if exportDiagnostics.length > 0}
164
177
  <ul class="warnings mono">
165
- {#each exportWarnings.slice(0, 4) as w, i (i)}
166
- <li>{w}</li>
178
+ {#each exportDiagnostics.slice(0, 4) as diagnostic, i (i)}
179
+ <li>{formatPowerIoDiagnostic(diagnostic)}</li>
167
180
  {/each}
168
- {#if exportWarnings.length > 4}
169
- <li>+{exportWarnings.length - 4} more</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: 10px;
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: 10.5px;
237
+ font-size: 12.5px;
225
238
  line-height: 1.5;
226
239
  color: var(--text-accent);
227
240
  }
228
241
 
229
- .study {
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, #fff);
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;