@tellegen/svelte 0.2.0 → 0.3.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 +36 -5
- package/dist/TellegenMap.svelte +325 -163
- package/dist/TellegenProvider.svelte +4 -0
- package/dist/TellegenShell.svelte +19 -13
- package/dist/api.d.ts +8 -1
- package/dist/api.js +6 -2
- package/dist/colors.d.ts +7 -7
- package/dist/colors.js +13 -13
- package/dist/components/AppHeader.svelte +23 -11
- package/dist/components/BindingLines.svelte +3 -6
- package/dist/components/BusPicker.svelte +8 -2
- package/dist/components/ControlPanel.svelte +106 -343
- package/dist/components/DemandSlider.svelte +21 -23
- package/dist/components/DiagramCanvas.svelte +486 -0
- package/dist/components/DiagramCanvas.svelte.d.ts +13 -0
- package/dist/components/DisplayControls.svelte +4 -2
- package/dist/components/FormulationSelector.svelte +8 -3
- package/dist/components/LocalCaseDetails.svelte +55 -45
- package/dist/components/McResults.svelte +365 -0
- package/dist/components/McResults.svelte.d.ts +11 -0
- package/dist/components/ModelDetails.svelte +230 -0
- package/dist/components/ModelDetails.svelte.d.ts +7 -0
- package/dist/components/MulticonductorDetails.svelte +227 -78
- package/dist/components/NetworkStats.svelte +20 -18
- package/dist/components/NotificationCenter.svelte +243 -0
- package/dist/components/NotificationCenter.svelte.d.ts +3 -0
- package/dist/components/PanelCard.svelte +319 -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 +212 -0
- package/dist/components/PanelHost.svelte.d.ts +3 -0
- package/dist/components/PowerFlowDetails.svelte +99 -0
- package/dist/components/PowerFlowDetails.svelte.d.ts +3 -0
- package/dist/components/RatingSlider.svelte +5 -9
- package/dist/components/SavedNetworkDetails.svelte +214 -0
- package/dist/components/SavedNetworkDetails.svelte.d.ts +3 -0
- package/dist/components/SensitivityReadout.svelte +24 -16
- package/dist/components/SolveCard.svelte +46 -49
- package/dist/components/TopMovers.svelte +8 -3
- package/dist/components/TopMovers.svelte.d.ts +2 -17
- package/dist/components.d.ts +2 -0
- package/dist/components.js +2 -0
- package/dist/controller.svelte.d.ts +54 -33
- package/dist/controller.svelte.js +927 -233
- 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.js +5 -4
- package/dist/index.d.ts +10 -4
- package/dist/index.js +7 -2
- package/dist/map.d.ts +1 -1
- package/dist/map.js +1 -1
- 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 +82 -0
- package/dist/panels.svelte.d.ts +65 -0
- package/dist/panels.svelte.js +215 -0
- package/dist/state.svelte.d.ts +90 -23
- package/dist/state.svelte.js +94 -20
- package/dist/synthetic-layout.js +1 -0
- package/package.json +22 -9
- package/dist/study-package.d.ts +0 -8
- package/dist/study-package.js +0 -17
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ModelDetails as ModelDetailsRecord } from "@tellegen/engine";
|
|
3
|
+
|
|
4
|
+
let { details }: { details?: ModelDetailsRecord | null } = $props();
|
|
5
|
+
let open = $state(false);
|
|
6
|
+
let page = $state(0);
|
|
7
|
+
let search = $state("");
|
|
8
|
+
const pageSize = 20;
|
|
9
|
+
const method = $derived(
|
|
10
|
+
details?.method === "convex_quadratic_fit"
|
|
11
|
+
? "Convex quadratic fit"
|
|
12
|
+
: details?.method === "exact"
|
|
13
|
+
? "Source model"
|
|
14
|
+
: String(details?.method ?? "").replaceAll("_", " "),
|
|
15
|
+
);
|
|
16
|
+
const rows = $derived(
|
|
17
|
+
open
|
|
18
|
+
? (details?.approximations.filter((row) =>
|
|
19
|
+
`${row.generator} ${row.bus}`.includes(search.trim()),
|
|
20
|
+
) ?? [])
|
|
21
|
+
: [],
|
|
22
|
+
);
|
|
23
|
+
const pageCount = $derived(Math.max(1, Math.ceil(rows.length / pageSize)));
|
|
24
|
+
const currentPage = $derived(Math.min(page, pageCount - 1));
|
|
25
|
+
const number = (value: number) =>
|
|
26
|
+
value.toLocaleString(undefined, { maximumSignificantDigits: 4 });
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
{#if details}
|
|
30
|
+
<details class="model-details" bind:open>
|
|
31
|
+
<summary>Model details</summary>
|
|
32
|
+
{#if open}
|
|
33
|
+
<dl>
|
|
34
|
+
<div>
|
|
35
|
+
<dt>Method</dt>
|
|
36
|
+
<dd>{method}</dd>
|
|
37
|
+
</div>
|
|
38
|
+
<div>
|
|
39
|
+
<dt>Quantity</dt>
|
|
40
|
+
<dd>{details.quantity}</dd>
|
|
41
|
+
</div>
|
|
42
|
+
<div>
|
|
43
|
+
<dt>Approximated curves</dt>
|
|
44
|
+
<dd>{details.approximations.length.toLocaleString()}</dd>
|
|
45
|
+
</div>
|
|
46
|
+
</dl>
|
|
47
|
+
{#if details.approximations.length}
|
|
48
|
+
<label
|
|
49
|
+
>Find generator or bus<input
|
|
50
|
+
type="search"
|
|
51
|
+
value={search}
|
|
52
|
+
oninput={(event) => {
|
|
53
|
+
search = event.currentTarget.value;
|
|
54
|
+
page = 0;
|
|
55
|
+
}}
|
|
56
|
+
/></label
|
|
57
|
+
>
|
|
58
|
+
<div class="table-scroll">
|
|
59
|
+
<table>
|
|
60
|
+
<caption>Errors at source breakpoints ({details.units})</caption>
|
|
61
|
+
<thead
|
|
62
|
+
><tr
|
|
63
|
+
><th>Generator</th><th>Bus</th><th
|
|
64
|
+
><abbr title="Root mean square error">RMS</abbr></th
|
|
65
|
+
><th>Maximum</th></tr
|
|
66
|
+
></thead
|
|
67
|
+
>
|
|
68
|
+
<tbody
|
|
69
|
+
>{#each rows.slice(currentPage * pageSize, (currentPage + 1) * pageSize) as row (row.generator)}<tr
|
|
70
|
+
><th>{row.generator}</th><td>{row.bus}</td><td
|
|
71
|
+
>{number(row.rms_breakpoint_error)}</td
|
|
72
|
+
><td>{number(row.max_breakpoint_error)}</td></tr
|
|
73
|
+
>{/each}</tbody
|
|
74
|
+
>
|
|
75
|
+
</table>
|
|
76
|
+
</div>
|
|
77
|
+
{#if !rows.length}<p>No matching generators.</p>{/if}
|
|
78
|
+
{#if pageCount > 1}<div class="pagination">
|
|
79
|
+
<button
|
|
80
|
+
disabled={currentPage === 0}
|
|
81
|
+
onclick={() => (page = currentPage - 1)}>Previous</button
|
|
82
|
+
><span
|
|
83
|
+
>{currentPage * pageSize + 1}-{Math.min(
|
|
84
|
+
(currentPage + 1) * pageSize,
|
|
85
|
+
rows.length,
|
|
86
|
+
)} of {rows.length}</span
|
|
87
|
+
><button
|
|
88
|
+
disabled={currentPage + 1 >= pageCount}
|
|
89
|
+
onclick={() => (page = currentPage + 1)}>Next</button
|
|
90
|
+
>
|
|
91
|
+
</div>{/if}
|
|
92
|
+
{/if}
|
|
93
|
+
<details class="references">
|
|
94
|
+
<summary>Source and model IDs</summary>
|
|
95
|
+
<dl>
|
|
96
|
+
<div>
|
|
97
|
+
<dt>Source</dt>
|
|
98
|
+
<dd><code>{details.source}</code></dd>
|
|
99
|
+
</div>
|
|
100
|
+
<div>
|
|
101
|
+
<dt>Model</dt>
|
|
102
|
+
<dd><code>{details.model}</code></dd>
|
|
103
|
+
</div>
|
|
104
|
+
</dl>
|
|
105
|
+
</details>
|
|
106
|
+
{/if}
|
|
107
|
+
</details>
|
|
108
|
+
{/if}
|
|
109
|
+
|
|
110
|
+
<style>
|
|
111
|
+
.model-details {
|
|
112
|
+
border-top: 1px solid var(--line);
|
|
113
|
+
padding: 12px 0;
|
|
114
|
+
font: 12px/1.45 var(--font-display);
|
|
115
|
+
color: var(--ink);
|
|
116
|
+
}
|
|
117
|
+
summary {
|
|
118
|
+
cursor: pointer;
|
|
119
|
+
font-weight: 400;
|
|
120
|
+
}
|
|
121
|
+
dl {
|
|
122
|
+
display: grid;
|
|
123
|
+
gap: 7px;
|
|
124
|
+
margin: 14px 0;
|
|
125
|
+
}
|
|
126
|
+
dl > div {
|
|
127
|
+
display: grid;
|
|
128
|
+
grid-template-columns: 110px minmax(0, 1fr);
|
|
129
|
+
gap: 10px;
|
|
130
|
+
}
|
|
131
|
+
dt {
|
|
132
|
+
color: var(--text-secondary);
|
|
133
|
+
}
|
|
134
|
+
dd {
|
|
135
|
+
margin: 0;
|
|
136
|
+
}
|
|
137
|
+
label {
|
|
138
|
+
display: grid;
|
|
139
|
+
gap: 6px;
|
|
140
|
+
margin: 12px 0;
|
|
141
|
+
font-size: 11px;
|
|
142
|
+
}
|
|
143
|
+
input {
|
|
144
|
+
min-width: 0;
|
|
145
|
+
width: 100%;
|
|
146
|
+
box-sizing: border-box;
|
|
147
|
+
background: var(--paper);
|
|
148
|
+
border: 1px solid var(--line);
|
|
149
|
+
padding: 6px 8px;
|
|
150
|
+
border-radius: 4px;
|
|
151
|
+
color: inherit;
|
|
152
|
+
font: inherit;
|
|
153
|
+
}
|
|
154
|
+
.table-scroll {
|
|
155
|
+
max-height: 250px;
|
|
156
|
+
overflow: auto;
|
|
157
|
+
}
|
|
158
|
+
table {
|
|
159
|
+
width: 100%;
|
|
160
|
+
border-collapse: collapse;
|
|
161
|
+
font-size: 10px;
|
|
162
|
+
}
|
|
163
|
+
caption {
|
|
164
|
+
text-align: left;
|
|
165
|
+
color: var(--text-secondary);
|
|
166
|
+
padding-bottom: 8px;
|
|
167
|
+
}
|
|
168
|
+
th,
|
|
169
|
+
td {
|
|
170
|
+
padding: 7px 4px;
|
|
171
|
+
text-align: right;
|
|
172
|
+
border-bottom: 1px solid var(--line);
|
|
173
|
+
font-variant-numeric: tabular-nums;
|
|
174
|
+
}
|
|
175
|
+
th:first-child {
|
|
176
|
+
text-align: left;
|
|
177
|
+
}
|
|
178
|
+
tbody th {
|
|
179
|
+
font-weight: 400;
|
|
180
|
+
}
|
|
181
|
+
thead th {
|
|
182
|
+
position: sticky;
|
|
183
|
+
top: 0;
|
|
184
|
+
background: var(--paper);
|
|
185
|
+
font-weight: 500;
|
|
186
|
+
}
|
|
187
|
+
abbr {
|
|
188
|
+
text-decoration-style: dotted;
|
|
189
|
+
text-underline-offset: 3px;
|
|
190
|
+
}
|
|
191
|
+
.pagination {
|
|
192
|
+
display: flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
justify-content: space-between;
|
|
195
|
+
gap: 8px;
|
|
196
|
+
margin: 10px 0;
|
|
197
|
+
font-size: 10px;
|
|
198
|
+
color: var(--text-secondary);
|
|
199
|
+
}
|
|
200
|
+
button {
|
|
201
|
+
padding: 4px 6px;
|
|
202
|
+
font: inherit;
|
|
203
|
+
border: 1px solid var(--line);
|
|
204
|
+
background: transparent;
|
|
205
|
+
border-radius: 4px;
|
|
206
|
+
cursor: pointer;
|
|
207
|
+
color: inherit;
|
|
208
|
+
}
|
|
209
|
+
button:disabled {
|
|
210
|
+
opacity: 0.45;
|
|
211
|
+
cursor: default;
|
|
212
|
+
}
|
|
213
|
+
button:focus-visible,
|
|
214
|
+
input:focus-visible,
|
|
215
|
+
summary:focus-visible {
|
|
216
|
+
outline: 2px solid var(--focus-ring);
|
|
217
|
+
outline-offset: 2px;
|
|
218
|
+
}
|
|
219
|
+
.references {
|
|
220
|
+
margin-top: 12px;
|
|
221
|
+
font-size: 10px;
|
|
222
|
+
}
|
|
223
|
+
.references dl > div {
|
|
224
|
+
grid-template-columns: 42px minmax(0, 1fr);
|
|
225
|
+
}
|
|
226
|
+
code {
|
|
227
|
+
overflow-wrap: anywhere;
|
|
228
|
+
font: 10px/1.5 var(--font-mono);
|
|
229
|
+
}
|
|
230
|
+
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ModelDetails as ModelDetailsRecord } from "@tellegen/engine";
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
details?: ModelDetailsRecord | null;
|
|
4
|
+
};
|
|
5
|
+
declare const ModelDetails: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type ModelDetails = ReturnType<typeof ModelDetails>;
|
|
7
|
+
export default ModelDetails;
|
|
@@ -8,19 +8,35 @@
|
|
|
8
8
|
isPhaseTerminal,
|
|
9
9
|
phaseColor
|
|
10
10
|
} from '../multiconductor.js';
|
|
11
|
-
import
|
|
11
|
+
import { getPanelLayout } from '../panels.svelte.js';
|
|
12
|
+
import { getNoticeCenter } from '../notices.svelte.js';
|
|
13
|
+
import { type DistAttachmentKind } from '@tellegen/engine';
|
|
12
14
|
|
|
13
15
|
const app = getAppState();
|
|
14
16
|
const ctrl = getController();
|
|
17
|
+
const notices = getNoticeCenter();
|
|
18
|
+
const panels = getPanelLayout();
|
|
19
|
+
function openResults() {
|
|
20
|
+
const panel = panels.panels.find((p) => p.id === 'studies');
|
|
21
|
+
if (panel) {
|
|
22
|
+
const network = panels.panels.find((p) => p.id === 'network');
|
|
23
|
+
if (network) panels.close(network);
|
|
24
|
+
panel.setOpen(true);
|
|
25
|
+
panels.activate(panel);
|
|
26
|
+
if (panels.compact) panels.drawer = panel.id;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
15
29
|
|
|
16
30
|
const NEUTRAL_RGBA = [120, 114, 102, 255] as const;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
'
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
|
|
32
|
+
/** A cleared or unparsable number field is not a request for zero load;
|
|
33
|
+
* only a finite value counts as an edit. */
|
|
34
|
+
function parseLoadInput(raw: string): number | null {
|
|
35
|
+
if (raw.trim() === '') return null;
|
|
36
|
+
const value = Number(raw);
|
|
37
|
+
return Number.isFinite(value) ? value : null;
|
|
38
|
+
}
|
|
39
|
+
const ATTACHMENT_LEGEND: DistAttachmentKind[] = ['source', 'generator', 'ibr', 'load', 'shunt'];
|
|
24
40
|
|
|
25
41
|
function terminalColor(t: string): string {
|
|
26
42
|
return rgbaCss(isPhaseTerminal(t) ? phaseColor(t) : [...NEUTRAL_RGBA]);
|
|
@@ -32,60 +48,76 @@
|
|
|
32
48
|
{@const s = mc.summary}
|
|
33
49
|
<h2>{mc.label} <span class="region mono">via {mc.fileName}</span></h2>
|
|
34
50
|
{#if s}
|
|
35
|
-
<p class="tag mono">
|
|
36
|
-
<
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
<dd>{s.n_bus}</dd>
|
|
40
|
-
</div>
|
|
41
|
-
<div>
|
|
42
|
-
<dt>lines / switches / xfmrs</dt>
|
|
43
|
-
<dd>{s.n_line} / {s.n_switch} / {s.n_transformer}</dd>
|
|
44
|
-
</div>
|
|
45
|
-
<div>
|
|
46
|
-
<dt>load</dt>
|
|
47
|
-
<dd>{fmt.format(s.load_kw)} kW</dd>
|
|
48
|
-
</div>
|
|
49
|
-
<div>
|
|
50
|
-
<dt>gen capacity</dt>
|
|
51
|
-
<dd>{fmt.format(s.gen_kw)} kW</dd>
|
|
52
|
-
</div>
|
|
53
|
-
<div>
|
|
54
|
-
<dt>sources / loads / gens</dt>
|
|
55
|
-
<dd>{s.n_source} / {s.n_load} / {s.n_generator}</dd>
|
|
56
|
-
</div>
|
|
57
|
-
{#if s.n_ibr > 0 || s.n_shunt > 0 || (s.n_capacitor ?? 0) > 0}
|
|
51
|
+
<p class="tag mono">Multiconductor</p>
|
|
52
|
+
<details class="case-details">
|
|
53
|
+
<summary>{s.n_bus} buses, {s.n_edge} {s.n_edge === 1 ? 'branch' : 'branches'}</summary>
|
|
54
|
+
<dl class="mono">
|
|
58
55
|
<div>
|
|
59
|
-
<dt>
|
|
60
|
-
<dd>{s.
|
|
56
|
+
<dt>buses</dt>
|
|
57
|
+
<dd>{s.n_bus}</dd>
|
|
61
58
|
</div>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
<
|
|
59
|
+
<div>
|
|
60
|
+
<dt>Lines / switches / transformers</dt>
|
|
61
|
+
<dd>{s.n_line} / {s.n_switch} / {s.n_transformer}</dd>
|
|
62
|
+
</div>
|
|
63
|
+
<div>
|
|
64
|
+
<dt>load</dt>
|
|
65
|
+
<dd>{fmt.format(s.load_kw)} kW</dd>
|
|
66
|
+
</div>
|
|
67
|
+
<div>
|
|
68
|
+
<dt>Generation capacity</dt>
|
|
69
|
+
<dd>{fmt.format(s.gen_kw)} kW</dd>
|
|
70
|
+
</div>
|
|
71
|
+
<div>
|
|
72
|
+
<dt>sources / loads / gens</dt>
|
|
73
|
+
<dd>{s.n_source} / {s.n_load} / {s.n_generator}</dd>
|
|
74
|
+
</div>
|
|
75
|
+
{#if s.n_ibr > 0 || s.n_shunt > 0 || (s.n_capacitor ?? 0) > 0}
|
|
76
|
+
<div>
|
|
77
|
+
<dt>IBRs / shunts / caps</dt>
|
|
78
|
+
<dd>{s.n_ibr} / {s.n_shunt} / {s.n_capacitor ?? 0}</dd>
|
|
79
|
+
</div>
|
|
72
80
|
{/if}
|
|
73
|
-
</
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
</dl>
|
|
82
|
+
</details>
|
|
83
|
+
|
|
84
|
+
<p class="footnote mono">
|
|
85
|
+
{mc.coordsKind === 'geographic'
|
|
86
|
+
? 'Geographic coordinates'
|
|
87
|
+
: mc.coordsKind === 'planar'
|
|
88
|
+
? 'Drawing coordinates'
|
|
89
|
+
: 'Generated diagram'}
|
|
90
|
+
</p>
|
|
91
|
+
<div class="calculation-actions">
|
|
92
|
+
{#if mc.solving}
|
|
93
|
+
<span role="status">Calculating...</span>{#if mc.solveAbort}<button
|
|
94
|
+
onclick={() => mc.solveAbort?.abort()}>Cancel</button
|
|
95
|
+
>{/if}
|
|
96
|
+
{:else}
|
|
97
|
+
<button
|
|
98
|
+
disabled={!mc.mcPfSupported}
|
|
99
|
+
onclick={() => {
|
|
100
|
+
void ctrl
|
|
101
|
+
.solveMultiCase(mc)
|
|
102
|
+
.then(openResults)
|
|
103
|
+
.catch(() => {});
|
|
104
|
+
}}>Solve AC power flow</button
|
|
105
|
+
>
|
|
106
|
+
{#if !mc.mcPfSupported}
|
|
107
|
+
<button
|
|
108
|
+
class="quiet"
|
|
109
|
+
onclick={() =>
|
|
110
|
+
notices.push({
|
|
111
|
+
kind: 'warning',
|
|
112
|
+
title: 'AC power flow unavailable',
|
|
113
|
+
details: mc.mcPfReason ?? 'This case cannot run AC power flow in this browser'
|
|
114
|
+
})}>Why unavailable</button
|
|
115
|
+
>
|
|
116
|
+
{/if}
|
|
117
|
+
{/if}
|
|
118
|
+
</div>
|
|
119
|
+
{#if mc.result}<p class="footnote">Converged, {mc.result.iterations} iterations</p>
|
|
120
|
+
<button class="reset mono" onclick={openResults}>View results in Studies</button>{/if}
|
|
89
121
|
{/if}
|
|
90
122
|
|
|
91
123
|
{#if mc.selectedBus}
|
|
@@ -115,17 +147,74 @@
|
|
|
115
147
|
{:else}
|
|
116
148
|
<p class="footnote mono">no attachments on this bus</p>
|
|
117
149
|
{/if}
|
|
150
|
+
{@const loadBranches = mc.mcLoadBranches.filter((load) => load.bus === b.id)}
|
|
151
|
+
{#if loadBranches.length > 0}
|
|
152
|
+
<div class="load-editor">
|
|
153
|
+
<h4>Load power</h4>
|
|
154
|
+
{#each loadBranches as load (`${load.load}:${load.branch}`)}
|
|
155
|
+
<div class="load-row">
|
|
156
|
+
<span class="mono"
|
|
157
|
+
>{load.load}{loadBranches.length > 1 ? ` · ${load.branch + 1}` : ''}</span
|
|
158
|
+
>
|
|
159
|
+
<label
|
|
160
|
+
>P kW<input
|
|
161
|
+
type="number"
|
|
162
|
+
step="any"
|
|
163
|
+
value={load.p_w / 1000}
|
|
164
|
+
onchange={(event) => {
|
|
165
|
+
const kw = parseLoadInput(event.currentTarget.value);
|
|
166
|
+
if (kw === null) {
|
|
167
|
+
event.currentTarget.value = String(load.p_w / 1000);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
ctrl.queueMultiLoadPower(mc, load.load, load.branch, kw * 1000, load.q_var);
|
|
171
|
+
}}
|
|
172
|
+
/></label
|
|
173
|
+
>
|
|
174
|
+
<label
|
|
175
|
+
>Q kvar<input
|
|
176
|
+
type="number"
|
|
177
|
+
step="any"
|
|
178
|
+
value={load.q_var / 1000}
|
|
179
|
+
onchange={(event) => {
|
|
180
|
+
const kvar = parseLoadInput(event.currentTarget.value);
|
|
181
|
+
if (kvar === null) {
|
|
182
|
+
event.currentTarget.value = String(load.q_var / 1000);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
ctrl.queueMultiLoadPower(mc, load.load, load.branch, load.p_w, kvar * 1000);
|
|
186
|
+
}}
|
|
187
|
+
/></label
|
|
188
|
+
>
|
|
189
|
+
<button
|
|
190
|
+
class="quiet"
|
|
191
|
+
disabled={load.p_w === load.base_p_w && load.q_var === load.base_q_var}
|
|
192
|
+
onclick={() =>
|
|
193
|
+
ctrl.queueMultiLoadPower(
|
|
194
|
+
mc,
|
|
195
|
+
load.load,
|
|
196
|
+
load.branch,
|
|
197
|
+
load.base_p_w,
|
|
198
|
+
load.base_q_var
|
|
199
|
+
)}>Reset</button
|
|
200
|
+
>
|
|
201
|
+
</div>
|
|
202
|
+
{/each}
|
|
203
|
+
<p class="footnote">Changes re-solve automatically using the retained factorization.</p>
|
|
204
|
+
</div>
|
|
205
|
+
{/if}
|
|
118
206
|
{:else if mc.selectedEdge}
|
|
119
207
|
{@const e = mc.selectedEdge}
|
|
120
208
|
<hr />
|
|
121
209
|
<h3 class="mono">
|
|
122
210
|
<i class="swatch" style={`--sc:${rgbaCss([...edgeColor(e.kind, e.closed)])}`}></i>
|
|
123
|
-
{e.kind}
|
|
211
|
+
{e.kind}
|
|
212
|
+
{e.from} – {e.to}
|
|
124
213
|
</h3>
|
|
125
214
|
<dl class="mono">
|
|
126
215
|
<div>
|
|
127
216
|
<dt>phases</dt>
|
|
128
|
-
<dd>{e.n_phases}φ
|
|
217
|
+
<dd>{e.n_phases}φ , {e.conductors.length} conductors</dd>
|
|
129
218
|
</div>
|
|
130
219
|
{#if e.kind === 'switch'}
|
|
131
220
|
<div>
|
|
@@ -156,22 +245,53 @@
|
|
|
156
245
|
</span>
|
|
157
246
|
<span class="legend-row">
|
|
158
247
|
{#each ATTACHMENT_LEGEND as kind (kind)}
|
|
159
|
-
<i class="swatch" style={`--sc:${rgbaCss([...attachmentColor(kind)])}`}
|
|
160
|
-
|
|
161
|
-
)}
|
|
248
|
+
<i class="swatch" style={`--sc:${rgbaCss([...attachmentColor(kind)])}`}
|
|
249
|
+
></i>{attachmentGlyph(kind)}
|
|
162
250
|
{/each}
|
|
163
251
|
</span>
|
|
164
252
|
</div>
|
|
165
253
|
|
|
166
|
-
{
|
|
167
|
-
<button class="reset mono" onclick={() => ctrl.moveMultiCase(mc)}>place on map</button>
|
|
168
|
-
{:else}
|
|
169
|
-
<button class="reset mono" onclick={() => ctrl.moveMultiCase(mc)}>move layout</button>
|
|
170
|
-
{/if}
|
|
254
|
+
<button class="reset mono" onclick={() => app.requestFrame(mc.id)}>Fit case</button>
|
|
171
255
|
<button class="reset mono" onclick={() => ctrl.removeMultiCase(mc)}>remove</button>
|
|
172
256
|
{/if}
|
|
173
257
|
|
|
174
258
|
<style>
|
|
259
|
+
.case-details {
|
|
260
|
+
margin: 10px 0;
|
|
261
|
+
font-size: 12px;
|
|
262
|
+
}
|
|
263
|
+
.case-details summary {
|
|
264
|
+
cursor: pointer;
|
|
265
|
+
color: var(--text-secondary);
|
|
266
|
+
}
|
|
267
|
+
.case-details dl {
|
|
268
|
+
margin-top: 8px;
|
|
269
|
+
}
|
|
270
|
+
.calculation-actions {
|
|
271
|
+
display: flex;
|
|
272
|
+
flex-wrap: wrap;
|
|
273
|
+
gap: 8px;
|
|
274
|
+
align-items: center;
|
|
275
|
+
margin: 14px 0;
|
|
276
|
+
}
|
|
277
|
+
.calculation-actions button {
|
|
278
|
+
font: inherit;
|
|
279
|
+
padding: 7px 10px;
|
|
280
|
+
border: 1px solid var(--line);
|
|
281
|
+
border-radius: 3px;
|
|
282
|
+
background: var(--ink);
|
|
283
|
+
color: var(--paper);
|
|
284
|
+
cursor: pointer;
|
|
285
|
+
}
|
|
286
|
+
.calculation-actions button:disabled {
|
|
287
|
+
opacity: 0.5;
|
|
288
|
+
cursor: default;
|
|
289
|
+
}
|
|
290
|
+
.calculation-actions button.quiet {
|
|
291
|
+
background: transparent;
|
|
292
|
+
color: var(--ink);
|
|
293
|
+
}
|
|
294
|
+
|
|
175
295
|
h2 {
|
|
176
296
|
margin: 0 0 8px;
|
|
177
297
|
font-size: 16px;
|
|
@@ -261,6 +381,44 @@
|
|
|
261
381
|
letter-spacing: 0;
|
|
262
382
|
}
|
|
263
383
|
|
|
384
|
+
.load-editor {
|
|
385
|
+
margin-top: 14px;
|
|
386
|
+
padding-top: 10px;
|
|
387
|
+
border-top: 1px solid var(--line);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.load-editor h4 {
|
|
391
|
+
margin: 0 0 8px;
|
|
392
|
+
font-size: 11px;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.load-row {
|
|
396
|
+
display: grid;
|
|
397
|
+
grid-template-columns: minmax(70px, 1fr) 72px 72px auto;
|
|
398
|
+
gap: 6px;
|
|
399
|
+
align-items: end;
|
|
400
|
+
margin-bottom: 7px;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.load-row label {
|
|
404
|
+
display: flex;
|
|
405
|
+
flex-direction: column;
|
|
406
|
+
gap: 2px;
|
|
407
|
+
font-size: 9px;
|
|
408
|
+
color: var(--text-secondary);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.load-row input {
|
|
412
|
+
min-width: 0;
|
|
413
|
+
width: 100%;
|
|
414
|
+
box-sizing: border-box;
|
|
415
|
+
font: 10px var(--font-mono);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.load-row button {
|
|
419
|
+
font-size: 9px;
|
|
420
|
+
}
|
|
421
|
+
|
|
264
422
|
/* The global stylesheet's .legend is the 6px color-ramp strip; its height
|
|
265
423
|
* leaks into a scoped .legend, so this container carries its own name. */
|
|
266
424
|
.mc-legend {
|
|
@@ -316,15 +474,6 @@
|
|
|
316
474
|
letter-spacing: 0;
|
|
317
475
|
}
|
|
318
476
|
|
|
319
|
-
.warnings {
|
|
320
|
-
margin: 8px 0 0;
|
|
321
|
-
padding: 0;
|
|
322
|
-
list-style: none;
|
|
323
|
-
font-size: 10.5px;
|
|
324
|
-
line-height: 1.5;
|
|
325
|
-
color: var(--text-accent);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
477
|
hr {
|
|
329
478
|
border: 0;
|
|
330
479
|
border-top: 1px solid var(--line);
|
|
@@ -20,25 +20,27 @@
|
|
|
20
20
|
<dt>lines</dt>
|
|
21
21
|
<dd>{stats.branches}</dd>
|
|
22
22
|
</div>
|
|
23
|
-
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
{#if ctrl.isPerturbed(ctrl.activeSolvable) && deltaObjective !== null}
|
|
38
|
-
<div class="delta">
|
|
39
|
-
<dt>vs base</dt>
|
|
40
|
-
<dd>{signed(deltaObjective)} $/h</dd>
|
|
23
|
+
{#if ctrl.activeFormulation !== 'acpf'}
|
|
24
|
+
<div>
|
|
25
|
+
<dt>binding lines</dt>
|
|
26
|
+
<dd>{stats.binding ?? '...'}</dd>
|
|
27
|
+
</div>
|
|
28
|
+
<div>
|
|
29
|
+
<dt>OPF objective</dt>
|
|
30
|
+
<dd>
|
|
31
|
+
{#if stats.objective === null}
|
|
32
|
+
<span class="blink">solving...</span>
|
|
33
|
+
{:else}
|
|
34
|
+
{fmt.format(stats.objective)}
|
|
35
|
+
{/if}
|
|
36
|
+
</dd>
|
|
41
37
|
</div>
|
|
38
|
+
{#if ctrl.isPerturbed(ctrl.activeSolvable) && deltaObjective !== null}
|
|
39
|
+
<div class="delta">
|
|
40
|
+
<dt>vs base</dt>
|
|
41
|
+
<dd>{signed(deltaObjective)}</dd>
|
|
42
|
+
</div>
|
|
43
|
+
{/if}
|
|
42
44
|
{/if}
|
|
43
45
|
</dl>
|
|
44
46
|
{/if}
|