@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,212 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, untrack } from 'svelte';
|
|
3
|
+
import { getAppState } from '../context.svelte.js';
|
|
4
|
+
import { getPanelLayout } from '../panels.svelte.js';
|
|
5
|
+
import { getNoticeCenter } from '../notices.svelte.js';
|
|
6
|
+
import PanelCard from './PanelCard.svelte';
|
|
7
|
+
const app = getAppState();
|
|
8
|
+
const layout = getPanelLayout();
|
|
9
|
+
const notices = getNoticeCenter();
|
|
10
|
+
const bottomReserve = $derived(notices.entries.length ? 120 : 88);
|
|
11
|
+
const left = $derived(layout.ordered('left').filter((p) => p.open));
|
|
12
|
+
const right = $derived(layout.ordered('right').filter((p) => p.open));
|
|
13
|
+
const floating = $derived(
|
|
14
|
+
layout.ordered().filter((p) => p.open && layout.position(p) === 'floating')
|
|
15
|
+
);
|
|
16
|
+
const drawer = $derived(layout.panels.find((p) => p.id === layout.drawer));
|
|
17
|
+
const toolbarTop = $derived(app.headerInset + 6);
|
|
18
|
+
const panelTop = $derived(toolbarTop + 42);
|
|
19
|
+
const drawerHeight = $derived(
|
|
20
|
+
Math.max(
|
|
21
|
+
1,
|
|
22
|
+
Math.min(
|
|
23
|
+
layout.viewportHeight * 0.6,
|
|
24
|
+
(layout.viewportHeight - panelTop - bottomReserve - 8) * 0.75
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
function resize() {
|
|
29
|
+
layout.resizeViewport(window.innerWidth, window.innerHeight, panelTop);
|
|
30
|
+
}
|
|
31
|
+
onMount(() => {
|
|
32
|
+
resize();
|
|
33
|
+
try {
|
|
34
|
+
layout.initialize(window.localStorage);
|
|
35
|
+
} catch {
|
|
36
|
+
layout.initialize();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
$effect(() => {
|
|
40
|
+
const top = panelTop;
|
|
41
|
+
const bottom = bottomReserve;
|
|
42
|
+
untrack(() => {
|
|
43
|
+
layout.bottom = bottom;
|
|
44
|
+
layout.resizeViewport(window.innerWidth, window.innerHeight, top);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
$effect(() => {
|
|
48
|
+
app.sheetInset = layout.compact && drawer ? drawerHeight + bottomReserve : 0;
|
|
49
|
+
});
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<svelte:window
|
|
53
|
+
onresize={resize}
|
|
54
|
+
onpointermove={(event) => layout.moveGesture(event.clientX, event.clientY)}
|
|
55
|
+
onpointerup={() => layout.finishGesture()}
|
|
56
|
+
onpointercancel={() => layout.finishGesture()}
|
|
57
|
+
/>
|
|
58
|
+
<div class="panel-toolbar" style:top="{toolbarTop}px" aria-label="Panels">
|
|
59
|
+
<div class="panel-buttons">
|
|
60
|
+
{#each layout.ordered().filter((p) => p.side === 'left') as panel (panel.id)}
|
|
61
|
+
<button
|
|
62
|
+
aria-label={panel.title}
|
|
63
|
+
aria-pressed={layout.compact ? layout.drawer === panel.id : panel.open}
|
|
64
|
+
onclick={() => layout.toggle(panel)}>{panel.title}</button
|
|
65
|
+
>
|
|
66
|
+
{/each}
|
|
67
|
+
</div>
|
|
68
|
+
<div class="panel-buttons right">
|
|
69
|
+
{#each layout.ordered().filter((p) => p.side === 'right') as panel (panel.id)}
|
|
70
|
+
<button
|
|
71
|
+
aria-label={panel.title}
|
|
72
|
+
aria-pressed={layout.compact ? layout.drawer === panel.id : panel.open}
|
|
73
|
+
onclick={() => layout.toggle(panel)}>{panel.title}</button
|
|
74
|
+
>
|
|
75
|
+
{/each}
|
|
76
|
+
<button
|
|
77
|
+
class="reset-layout"
|
|
78
|
+
onclick={() => layout.reset()}
|
|
79
|
+
title="Restore the default panel positions">Reset layout</button
|
|
80
|
+
>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
{#if layout.storageError}<p class="layout-error" role="status" style:top="{panelTop}px">
|
|
84
|
+
{layout.storageError}
|
|
85
|
+
</p>{/if}
|
|
86
|
+
{#if layout.compact}
|
|
87
|
+
{#if drawer}<div class="drawer" style:bottom="{bottomReserve}px" style:height="{drawerHeight}px">
|
|
88
|
+
<PanelCard panel={drawer} mobile />
|
|
89
|
+
</div>{/if}
|
|
90
|
+
{:else}
|
|
91
|
+
<div
|
|
92
|
+
class="dock left"
|
|
93
|
+
style:bottom="{Math.max(100, bottomReserve)}px"
|
|
94
|
+
data-panel-dock="left"
|
|
95
|
+
style:top="{panelTop}px"
|
|
96
|
+
style:width="{layout.dockWidth('left')}px"
|
|
97
|
+
>
|
|
98
|
+
{#each left as panel (panel.id)}<PanelCard {panel} />{/each}
|
|
99
|
+
</div>
|
|
100
|
+
<div
|
|
101
|
+
class="dock right"
|
|
102
|
+
style:bottom="{Math.max(100, bottomReserve)}px"
|
|
103
|
+
data-panel-dock="right"
|
|
104
|
+
style:top="{panelTop}px"
|
|
105
|
+
style:width="{layout.dockWidth('right')}px"
|
|
106
|
+
>
|
|
107
|
+
{#each right as panel (panel.id)}<PanelCard {panel} />{/each}
|
|
108
|
+
</div>
|
|
109
|
+
{#each floating as panel (panel.id)}<PanelCard {panel} floating />{/each}
|
|
110
|
+
{/if}
|
|
111
|
+
|
|
112
|
+
<style>
|
|
113
|
+
.panel-toolbar {
|
|
114
|
+
position: fixed;
|
|
115
|
+
z-index: 30;
|
|
116
|
+
left: 20px;
|
|
117
|
+
right: 20px;
|
|
118
|
+
display: flex;
|
|
119
|
+
justify-content: space-between;
|
|
120
|
+
align-items: center;
|
|
121
|
+
gap: 12px;
|
|
122
|
+
pointer-events: none;
|
|
123
|
+
height: 34px;
|
|
124
|
+
}
|
|
125
|
+
.panel-buttons {
|
|
126
|
+
display: flex;
|
|
127
|
+
gap: 6px;
|
|
128
|
+
align-items: center;
|
|
129
|
+
pointer-events: auto;
|
|
130
|
+
}
|
|
131
|
+
button {
|
|
132
|
+
border: 1px solid var(--line);
|
|
133
|
+
border-radius: 4px;
|
|
134
|
+
background: var(--paper);
|
|
135
|
+
color: var(--ink);
|
|
136
|
+
font: 12px/1.4 var(--font-display);
|
|
137
|
+
min-height: 32px;
|
|
138
|
+
padding: 6px 12px;
|
|
139
|
+
cursor: pointer;
|
|
140
|
+
box-shadow: var(--elev-1);
|
|
141
|
+
}
|
|
142
|
+
button[aria-pressed='true'] {
|
|
143
|
+
border-color: var(--accent);
|
|
144
|
+
background: var(--accent-soft);
|
|
145
|
+
}
|
|
146
|
+
button:focus-visible {
|
|
147
|
+
outline: 2px solid var(--focus-ring);
|
|
148
|
+
outline-offset: 2px;
|
|
149
|
+
}
|
|
150
|
+
button:hover {
|
|
151
|
+
border-color: var(--accent);
|
|
152
|
+
}
|
|
153
|
+
.reset-layout {
|
|
154
|
+
color: var(--text-secondary);
|
|
155
|
+
font-size: 11px;
|
|
156
|
+
padding: 6px 8px;
|
|
157
|
+
background: var(--panel);
|
|
158
|
+
}
|
|
159
|
+
.dock {
|
|
160
|
+
position: fixed;
|
|
161
|
+
z-index: 25;
|
|
162
|
+
bottom: 100px;
|
|
163
|
+
display: flex;
|
|
164
|
+
flex-direction: column;
|
|
165
|
+
align-items: stretch;
|
|
166
|
+
gap: 12px;
|
|
167
|
+
pointer-events: none;
|
|
168
|
+
}
|
|
169
|
+
.dock.left {
|
|
170
|
+
left: 20px;
|
|
171
|
+
}
|
|
172
|
+
.dock.right {
|
|
173
|
+
right: 20px;
|
|
174
|
+
}
|
|
175
|
+
.drawer {
|
|
176
|
+
position: fixed;
|
|
177
|
+
bottom: 88px;
|
|
178
|
+
left: 12px;
|
|
179
|
+
right: 12px;
|
|
180
|
+
z-index: 25;
|
|
181
|
+
}
|
|
182
|
+
.layout-error {
|
|
183
|
+
position: fixed;
|
|
184
|
+
left: 50%;
|
|
185
|
+
transform: translateX(-50%);
|
|
186
|
+
z-index: 42;
|
|
187
|
+
max-width: 400px;
|
|
188
|
+
margin: 0;
|
|
189
|
+
padding: 8px 12px;
|
|
190
|
+
font: 12px/1.5 var(--font-display);
|
|
191
|
+
color: var(--ink);
|
|
192
|
+
background: var(--paper);
|
|
193
|
+
border: 1px solid var(--line);
|
|
194
|
+
}
|
|
195
|
+
@media (max-width: 960px) {
|
|
196
|
+
.panel-toolbar {
|
|
197
|
+
left: 12px;
|
|
198
|
+
right: 12px;
|
|
199
|
+
gap: 6px;
|
|
200
|
+
}
|
|
201
|
+
.panel-buttons {
|
|
202
|
+
gap: 4px;
|
|
203
|
+
}
|
|
204
|
+
button {
|
|
205
|
+
padding: 6px 8px;
|
|
206
|
+
font-size: 11px;
|
|
207
|
+
}
|
|
208
|
+
.reset-layout {
|
|
209
|
+
display: none;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
</style>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getController } from '../context.svelte.js';
|
|
3
|
+
import { caseDeltas } from '../display.js';
|
|
4
|
+
import { fmt } from '../format.js';
|
|
5
|
+
|
|
6
|
+
const ctrl = getController();
|
|
7
|
+
const active = $derived(ctrl.activeSolvable);
|
|
8
|
+
const bus = $derived(ctrl.selectedBusData);
|
|
9
|
+
const branch = $derived(ctrl.selectedBranchData);
|
|
10
|
+
const voltage = $derived(active?.solution?.vm?.find((value) => value.bus === bus?.id)?.value);
|
|
11
|
+
const angle = $derived(active?.solution?.va.find((value) => value.bus === bus?.id)?.value);
|
|
12
|
+
const flow = $derived(active?.solution?.flows.find((value) => value.branch === branch?.id));
|
|
13
|
+
const demand = $derived.by(() => {
|
|
14
|
+
if (!bus || !active) return null;
|
|
15
|
+
const deltas = caseDeltas(active);
|
|
16
|
+
return bus.demand_mw + (deltas[bus.uid ?? bus.id] ?? deltas[bus.id] ?? 0);
|
|
17
|
+
});
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
{#if bus || branch}
|
|
21
|
+
<section class="equipment" aria-label="Power-flow equipment results">
|
|
22
|
+
{#if bus}
|
|
23
|
+
<h3>
|
|
24
|
+
Bus {bus.id}{#if bus.name}<span>{bus.name}</span>{/if}
|
|
25
|
+
</h3>
|
|
26
|
+
<dl>
|
|
27
|
+
{#if demand !== null}<div>
|
|
28
|
+
<dt>Demand</dt>
|
|
29
|
+
<dd>{fmt.format(demand)} MW</dd>
|
|
30
|
+
</div>{/if}
|
|
31
|
+
{#if voltage != null}<div>
|
|
32
|
+
<dt>Voltage</dt>
|
|
33
|
+
<dd>{voltage.toFixed(4)} pu</dd>
|
|
34
|
+
</div>{/if}
|
|
35
|
+
{#if angle != null}<div>
|
|
36
|
+
<dt>Voltage angle</dt>
|
|
37
|
+
<dd>{angle.toFixed(4)} rad</dd>
|
|
38
|
+
</div>{/if}
|
|
39
|
+
</dl>
|
|
40
|
+
{:else if branch}
|
|
41
|
+
<h3>Line {branch.id}<span>Bus {branch.from} to bus {branch.to}</span></h3>
|
|
42
|
+
<dl>
|
|
43
|
+
<div>
|
|
44
|
+
<dt>Capacity</dt>
|
|
45
|
+
<dd>{branch.rate_mw > 0 ? `${fmt.format(branch.rate_mw)} MVA` : 'Unlimited'}</dd>
|
|
46
|
+
</div>
|
|
47
|
+
{#if flow}
|
|
48
|
+
<div>
|
|
49
|
+
<dt>Active power</dt>
|
|
50
|
+
<dd>{fmt.format(flow.mw)} MW</dd>
|
|
51
|
+
</div>
|
|
52
|
+
{#if branch.rate_mw > 0}<div>
|
|
53
|
+
<dt>Loading</dt>
|
|
54
|
+
<dd>{fmt.format(flow.loading * 100)}%</dd>
|
|
55
|
+
</div>{/if}
|
|
56
|
+
{/if}
|
|
57
|
+
</dl>
|
|
58
|
+
{/if}
|
|
59
|
+
</section>
|
|
60
|
+
{/if}
|
|
61
|
+
|
|
62
|
+
<style>
|
|
63
|
+
.equipment {
|
|
64
|
+
border-top: 1px solid var(--line);
|
|
65
|
+
margin-top: 14px;
|
|
66
|
+
padding-top: 14px;
|
|
67
|
+
}
|
|
68
|
+
h3 {
|
|
69
|
+
margin: 0 0 12px;
|
|
70
|
+
font-size: 13px;
|
|
71
|
+
font-weight: 600;
|
|
72
|
+
}
|
|
73
|
+
h3 span {
|
|
74
|
+
display: block;
|
|
75
|
+
margin-top: 3px;
|
|
76
|
+
font-size: 11px;
|
|
77
|
+
font-weight: 400;
|
|
78
|
+
color: var(--text-secondary);
|
|
79
|
+
}
|
|
80
|
+
dl {
|
|
81
|
+
margin: 0;
|
|
82
|
+
display: grid;
|
|
83
|
+
gap: 6px;
|
|
84
|
+
font: 12px/1.5 var(--font-mono);
|
|
85
|
+
}
|
|
86
|
+
dl > div {
|
|
87
|
+
display: flex;
|
|
88
|
+
justify-content: space-between;
|
|
89
|
+
align-items: baseline;
|
|
90
|
+
gap: 12px;
|
|
91
|
+
}
|
|
92
|
+
dt {
|
|
93
|
+
color: var(--text-secondary);
|
|
94
|
+
}
|
|
95
|
+
dd {
|
|
96
|
+
margin: 0;
|
|
97
|
+
text-align: right;
|
|
98
|
+
}
|
|
99
|
+
</style>
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
'The black tick marks the last committed rating change. Drag the knob to preview a new thermal rating; release to solve and move the tick to that point.';
|
|
26
26
|
const disabledTip = "this line's rating is synthesized; not adjustable";
|
|
27
27
|
const scoreTip =
|
|
28
|
-
'Gradient
|
|
28
|
+
'Gradient estimates the declared objective change versus base before the solve finishes, from the engine preview at the committed point. Exact is the resolved OPF objective change.';
|
|
29
29
|
</script>
|
|
30
30
|
|
|
31
31
|
{#if ctrl.activeSolvable}
|
|
@@ -39,9 +39,7 @@
|
|
|
39
39
|
<div class="line-row mono">
|
|
40
40
|
{#if branch}
|
|
41
41
|
<span class="dim"
|
|
42
|
-
>line {branch.from} – {branch.to}
|
|
43
|
-
branch.rate_mw
|
|
44
|
-
)} MW</span
|
|
42
|
+
>line {branch.from} – {branch.to}, {fmt.format(branch.rate_mw)} MW</span
|
|
45
43
|
>
|
|
46
44
|
{/if}
|
|
47
45
|
<span class="dim">
|
|
@@ -75,7 +73,7 @@
|
|
|
75
73
|
<div class="rating-feedback" class:idle={!ctrl.previewing && !ctrl.isPerturbed(c)}>
|
|
76
74
|
<p class="pred mono dim" aria-hidden={!(ctrl.predictedDeltaObj !== null && ctrl.previewing)}>
|
|
77
75
|
{#if ctrl.predictedDeltaObj !== null && ctrl.previewing}
|
|
78
|
-
predicted Δ
|
|
76
|
+
predicted Δobjective {signed(ctrl.predictedDeltaObj)}
|
|
79
77
|
{:else}
|
|
80
78
|
|
|
81
79
|
{/if}
|
|
@@ -83,10 +81,8 @@
|
|
|
83
81
|
<p class="score mono" aria-hidden={!(ctrl.gradientScore && ctrl.isPerturbed(c))}>
|
|
84
82
|
{#if ctrl.gradientScore && ctrl.isPerturbed(c)}
|
|
85
83
|
<span title={scoreTip}>
|
|
86
|
-
gradient {signed(ctrl.gradientScore.pred)}
|
|
87
|
-
|
|
88
|
-
)}
|
|
89
|
-
$/h
|
|
84
|
+
gradient {signed(ctrl.gradientScore.pred)}, exact {signed(ctrl.gradientScore.exact)} objective
|
|
85
|
+
units
|
|
90
86
|
</span>
|
|
91
87
|
{:else}
|
|
92
88
|
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getAppState } from '../context.svelte.js';
|
|
3
|
+
import { fmt, formulationLabel, signed } from '../format.js';
|
|
4
|
+
import BusPicker from './BusPicker.svelte';
|
|
5
|
+
import ModelDetails from './ModelDetails.svelte';
|
|
6
|
+
|
|
7
|
+
const app = getAppState();
|
|
8
|
+
const saved = $derived(app.studyView);
|
|
9
|
+
const bus = $derived(saved?.network.buses.find((value) => value.id === app.selectedBus));
|
|
10
|
+
const branch = $derived(saved?.network.branches.find((value) => value.id === app.selectedBranch));
|
|
11
|
+
const lmp = $derived(saved?.solution?.lmp?.find((value) => value.bus === bus?.id)?.value);
|
|
12
|
+
const voltage = $derived(saved?.solution?.vm?.find((value) => value.bus === bus?.id)?.value);
|
|
13
|
+
const squaredVoltage = $derived(
|
|
14
|
+
saved?.solution?.w?.find((value) => value.bus === bus?.id)?.value
|
|
15
|
+
);
|
|
16
|
+
const angle = $derived(saved?.solution?.va?.find((value) => value.bus === bus?.id)?.value);
|
|
17
|
+
const flow = $derived(saved?.solution?.flows?.find((value) => value.branch === branch?.id));
|
|
18
|
+
const baseDemand = $derived(bus ? saved?.baseDemandMw?.[String(bus.id)] : undefined);
|
|
19
|
+
function live() {
|
|
20
|
+
app.studyView = null;
|
|
21
|
+
app.selectedBus = null;
|
|
22
|
+
app.selectedBranch = null;
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
{#if saved}
|
|
27
|
+
<section aria-label="Saved network details">
|
|
28
|
+
<h2>{saved.network.name}</h2>
|
|
29
|
+
<p class="state-label">{saved.label}<span>Saved state</span></p>
|
|
30
|
+
<dl>
|
|
31
|
+
<div>
|
|
32
|
+
<dt>Buses</dt>
|
|
33
|
+
<dd>{saved.network.buses.length.toLocaleString()}</dd>
|
|
34
|
+
</div>
|
|
35
|
+
<div>
|
|
36
|
+
<dt>Lines</dt>
|
|
37
|
+
<dd>{saved.network.branches.length.toLocaleString()}</dd>
|
|
38
|
+
</div>
|
|
39
|
+
<div>
|
|
40
|
+
<dt>Calculation</dt>
|
|
41
|
+
<dd>{formulationLabel(saved.formulation)}</dd>
|
|
42
|
+
</div>
|
|
43
|
+
<div>
|
|
44
|
+
<dt>Result</dt>
|
|
45
|
+
<dd>{saved.solution?.status ?? 'Not solved'}</dd>
|
|
46
|
+
</div>
|
|
47
|
+
{#if saved.solution?.objective != null}
|
|
48
|
+
<div>
|
|
49
|
+
<dt>OPF objective</dt>
|
|
50
|
+
<dd>{fmt.format(saved.solution.objective)}</dd>
|
|
51
|
+
</div>
|
|
52
|
+
{/if}
|
|
53
|
+
</dl>
|
|
54
|
+
<button class="live" onclick={live}>Return to live case</button>
|
|
55
|
+
<ModelDetails details={saved.network.model_details} />
|
|
56
|
+
<BusPicker inline />
|
|
57
|
+
{#if saved.solution}
|
|
58
|
+
<label class="map-values"
|
|
59
|
+
>{saved.network.coordinate_space === 'diagram' ? 'Diagram values' : 'Map values'}
|
|
60
|
+
<select bind:value={app.displayMode}>
|
|
61
|
+
{#if saved.solution.lmp?.length}<option value="price">LMP</option>{/if}
|
|
62
|
+
{#if saved.solution.va?.length}<option value="angle">Voltage angle</option>{/if}
|
|
63
|
+
{#if saved.solution.vm?.length || saved.solution.w?.length}<option value="voltage"
|
|
64
|
+
>Voltage magnitude</option
|
|
65
|
+
>{/if}
|
|
66
|
+
</select>
|
|
67
|
+
</label>
|
|
68
|
+
{/if}
|
|
69
|
+
{#if bus}
|
|
70
|
+
<div class="equipment">
|
|
71
|
+
<h3>
|
|
72
|
+
Bus {bus.id}{#if bus.name}<span>{bus.name}</span>{/if}
|
|
73
|
+
</h3>
|
|
74
|
+
<dl>
|
|
75
|
+
{#if baseDemand != null}<div>
|
|
76
|
+
<dt>Base demand</dt>
|
|
77
|
+
<dd>{fmt.format(baseDemand)} MW</dd>
|
|
78
|
+
</div>{/if}
|
|
79
|
+
<div>
|
|
80
|
+
<dt>Current demand</dt>
|
|
81
|
+
<dd>{fmt.format(bus.demand_mw)} MW</dd>
|
|
82
|
+
</div>
|
|
83
|
+
{#if baseDemand != null}<div>
|
|
84
|
+
<dt>Change</dt>
|
|
85
|
+
<dd>{signed(bus.demand_mw - baseDemand)} MW</dd>
|
|
86
|
+
</div>{/if}
|
|
87
|
+
{#if lmp != null}<div>
|
|
88
|
+
<dt>LMP</dt>
|
|
89
|
+
<dd>{fmt.format(lmp)}<small>objective units/MW</small></dd>
|
|
90
|
+
</div>{/if}
|
|
91
|
+
{#if voltage != null || squaredVoltage != null}<div>
|
|
92
|
+
<dt>Voltage</dt>
|
|
93
|
+
<dd>{(voltage ?? Math.sqrt(Math.max(0, squaredVoltage!))).toFixed(4)} pu</dd>
|
|
94
|
+
</div>{/if}
|
|
95
|
+
{#if angle != null}<div>
|
|
96
|
+
<dt>Voltage angle</dt>
|
|
97
|
+
<dd>{angle.toFixed(4)} rad</dd>
|
|
98
|
+
</div>{/if}
|
|
99
|
+
</dl>
|
|
100
|
+
</div>
|
|
101
|
+
{:else if branch}
|
|
102
|
+
<div class="equipment">
|
|
103
|
+
<h3>Line {branch.id}<span>Bus {branch.from} to bus {branch.to}</span></h3>
|
|
104
|
+
<dl>
|
|
105
|
+
<div>
|
|
106
|
+
<dt>Capacity</dt>
|
|
107
|
+
<dd>
|
|
108
|
+
{branch.rate_mw > 0
|
|
109
|
+
? `${fmt.format(branch.rate_mw)} ${saved.formulation === 'dcopf' ? 'MW' : 'MVA'}`
|
|
110
|
+
: 'Unlimited'}
|
|
111
|
+
</dd>
|
|
112
|
+
</div>
|
|
113
|
+
{#if flow}<div>
|
|
114
|
+
<dt>Active power</dt>
|
|
115
|
+
<dd>{fmt.format(flow.pf)} MW</dd>
|
|
116
|
+
</div>
|
|
117
|
+
<div>
|
|
118
|
+
<dt>Loading</dt>
|
|
119
|
+
<dd>{fmt.format(flow.loading * 100)}%</dd>
|
|
120
|
+
</div>{/if}
|
|
121
|
+
</dl>
|
|
122
|
+
</div>
|
|
123
|
+
{/if}
|
|
124
|
+
</section>
|
|
125
|
+
{/if}
|
|
126
|
+
|
|
127
|
+
<style>
|
|
128
|
+
h2 {
|
|
129
|
+
margin: 0 0 8px;
|
|
130
|
+
font-size: 15px;
|
|
131
|
+
font-weight: 600;
|
|
132
|
+
}
|
|
133
|
+
.state-label {
|
|
134
|
+
display: flex;
|
|
135
|
+
justify-content: space-between;
|
|
136
|
+
gap: 12px;
|
|
137
|
+
margin: 0 0 16px;
|
|
138
|
+
font-size: 12px;
|
|
139
|
+
}
|
|
140
|
+
.state-label span {
|
|
141
|
+
flex: none;
|
|
142
|
+
color: var(--text-secondary);
|
|
143
|
+
font-size: 11px;
|
|
144
|
+
}
|
|
145
|
+
dl {
|
|
146
|
+
margin: 0;
|
|
147
|
+
display: grid;
|
|
148
|
+
gap: 6px;
|
|
149
|
+
font: 12px/1.5 var(--font-mono);
|
|
150
|
+
}
|
|
151
|
+
dl > div {
|
|
152
|
+
display: flex;
|
|
153
|
+
justify-content: space-between;
|
|
154
|
+
align-items: baseline;
|
|
155
|
+
gap: 12px;
|
|
156
|
+
}
|
|
157
|
+
dt {
|
|
158
|
+
color: var(--text-secondary);
|
|
159
|
+
}
|
|
160
|
+
dd {
|
|
161
|
+
margin: 0;
|
|
162
|
+
text-align: right;
|
|
163
|
+
}
|
|
164
|
+
small {
|
|
165
|
+
display: block;
|
|
166
|
+
font-size: 10px;
|
|
167
|
+
color: var(--text-secondary);
|
|
168
|
+
}
|
|
169
|
+
.live {
|
|
170
|
+
margin: 16px 0;
|
|
171
|
+
}
|
|
172
|
+
button,
|
|
173
|
+
select {
|
|
174
|
+
border: 1px solid var(--line);
|
|
175
|
+
border-radius: 4px;
|
|
176
|
+
color: var(--ink);
|
|
177
|
+
background: var(--paper);
|
|
178
|
+
padding: 6px 8px;
|
|
179
|
+
font: 12px/1.4 var(--font-display);
|
|
180
|
+
}
|
|
181
|
+
button {
|
|
182
|
+
cursor: pointer;
|
|
183
|
+
}
|
|
184
|
+
button:focus-visible,
|
|
185
|
+
select:focus-visible {
|
|
186
|
+
outline: 2px solid var(--focus-ring);
|
|
187
|
+
outline-offset: 2px;
|
|
188
|
+
}
|
|
189
|
+
.map-values {
|
|
190
|
+
display: flex;
|
|
191
|
+
justify-content: space-between;
|
|
192
|
+
align-items: center;
|
|
193
|
+
gap: 12px;
|
|
194
|
+
margin: 16px 0;
|
|
195
|
+
font-size: 12px;
|
|
196
|
+
}
|
|
197
|
+
.equipment {
|
|
198
|
+
border-top: 1px solid var(--line);
|
|
199
|
+
margin-top: 16px;
|
|
200
|
+
padding-top: 16px;
|
|
201
|
+
}
|
|
202
|
+
h3 {
|
|
203
|
+
margin: 0 0 12px;
|
|
204
|
+
font-size: 13px;
|
|
205
|
+
font-weight: 600;
|
|
206
|
+
}
|
|
207
|
+
h3 span {
|
|
208
|
+
display: block;
|
|
209
|
+
margin-top: 3px;
|
|
210
|
+
color: var(--text-secondary);
|
|
211
|
+
font-size: 11px;
|
|
212
|
+
font-weight: 400;
|
|
213
|
+
}
|
|
214
|
+
</style>
|
|
@@ -5,19 +5,24 @@
|
|
|
5
5
|
|
|
6
6
|
const app = getAppState();
|
|
7
7
|
const ctrl = getController();
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
const reportedUnits = $derived(
|
|
9
|
+
(ctrl.selectedSensitivity?.units ?? '(objective units/MW)/MW').replaceAll(
|
|
10
|
+
'objective_unit',
|
|
11
|
+
'objective units'
|
|
12
|
+
)
|
|
13
|
+
);
|
|
14
|
+
const busUnitTitle = $derived(
|
|
15
|
+
`LMP response to active demand, in ${reportedUnits}. The response is small while the active set stays unchanged.`
|
|
16
|
+
);
|
|
17
|
+
const branchUnitTitle = $derived(
|
|
18
|
+
`LMP response to the thermal rating, in ${reportedUnits}. The column is zero when the limit is inactive.`
|
|
19
|
+
);
|
|
15
20
|
|
|
16
|
-
// Branch mode: the selection is a line and the column is
|
|
21
|
+
// Branch mode: the selection is a line and the column is price/rating; the
|
|
17
22
|
// legend and preview pipeline are identical (both columns are bus-keyed).
|
|
18
23
|
const branchMode = $derived(app.selectedBranch !== null);
|
|
19
24
|
const unitTitle = $derived(branchMode ? branchUnitTitle : busUnitTitle);
|
|
20
|
-
const
|
|
25
|
+
const previewUnits = $derived(app.previewPrices?.units ?? 'objective units/MW');
|
|
21
26
|
|
|
22
27
|
const previewStep = $derived(
|
|
23
28
|
branchMode
|
|
@@ -54,33 +59,36 @@
|
|
|
54
59
|
{#if ctrl.sensSummary?.flat}
|
|
55
60
|
<div class="legend flat" style:background={ctrl.flatSensBackground}></div>
|
|
56
61
|
<div class="legend-labels mono single">
|
|
57
|
-
<span
|
|
62
|
+
<span
|
|
63
|
+
>uniform {signedExp(ctrl.sensSummary.mean * previewStep)}
|
|
64
|
+
{previewUnits}</span
|
|
65
|
+
>
|
|
58
66
|
</div>
|
|
59
67
|
{:else if ctrl.previewScale}
|
|
60
68
|
<!-- The bounds are fixed for the whole drag (column scale × full slider
|
|
61
69
|
deflection), so the ramp ends label the colors: full green/purple is
|
|
62
|
-
the predicted
|
|
70
|
+
the predicted LMP change at full deflection, and intensity grows with the
|
|
63
71
|
step instead of renormalizing every frame. -->
|
|
64
72
|
<div class="legend" style:background={sensGradient}></div>
|
|
65
73
|
<div class="legend-labels mono">
|
|
66
74
|
<span>−{ctrl.previewScale.toExponential(1)}</span>
|
|
67
|
-
<span>ΔLMP
|
|
75
|
+
<span>Δ LMP {previewUnits}</span>
|
|
68
76
|
<span>+{ctrl.previewScale.toExponential(1)}</span>
|
|
69
77
|
</div>
|
|
70
78
|
{/if}
|
|
71
79
|
{:else}
|
|
72
80
|
<p class="dim small sensitivity-copy" title={unitTitle}>
|
|
73
81
|
{#if branchMode}
|
|
74
|
-
LMP response
|
|
82
|
+
LMP response to the rating on line {branchLabel}.
|
|
75
83
|
{:else}
|
|
76
|
-
LMP response
|
|
84
|
+
LMP response to demand at bus {app.selectedBus}.
|
|
77
85
|
{/if}
|
|
78
86
|
<span class="hint-dot mono" title={unitTitle} aria-label={unitTitle}>i</span>
|
|
79
87
|
</p>
|
|
80
88
|
{#if ctrl.sensSummary?.flat}
|
|
81
89
|
<div class="legend flat" style:background={ctrl.flatSensBackground}></div>
|
|
82
90
|
<div class="legend-labels mono single">
|
|
83
|
-
<span>uniform {signedExp(ctrl.sensSummary.mean)} {
|
|
91
|
+
<span>uniform {signedExp(ctrl.sensSummary.mean)} {reportedUnits}</span>
|
|
84
92
|
</div>
|
|
85
93
|
{:else if ctrl.sensSummary}
|
|
86
94
|
<div class="legend" style:background={sensGradient}></div>
|
|
@@ -93,7 +101,7 @@
|
|
|
93
101
|
<div class="legend" style:background="var(--line)" style:opacity="0.4"></div>
|
|
94
102
|
<div class="legend-labels mono single">
|
|
95
103
|
<span class="blink">
|
|
96
|
-
computing {#if branchMode}∂
|
|
104
|
+
computing {#if branchMode}∂value/∂fmax{:else}∂value/∂d{/if}…
|
|
97
105
|
</span>
|
|
98
106
|
</div>
|
|
99
107
|
{/if}
|