@tellegen/svelte 0.4.0 → 0.4.2
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 +38 -10
- package/dist/Sparkline.svelte +4 -4
- package/dist/TellegenMap.svelte +80 -66
- package/dist/TellegenProvider.svelte +29 -13
- package/dist/colors.d.ts +37 -1
- package/dist/colors.js +63 -0
- package/dist/components/AppFooter.svelte +4 -4
- package/dist/components/AppHeader.svelte +15 -19
- package/dist/components/BindingLines.svelte +10 -9
- package/dist/components/BusPicker.svelte +5 -6
- package/dist/components/ControlPanel.svelte +16 -11
- package/dist/components/ControlPanel.svelte.d.ts +6 -1
- package/dist/components/DemandSlider.svelte +26 -29
- package/dist/components/DiagramCanvas.svelte +16 -4
- package/dist/components/DisplayControls.svelte +5 -24
- package/dist/components/DropZone.svelte +7 -3
- package/dist/components/DropZone.svelte.d.ts +6 -17
- package/dist/components/FormulationSelector.svelte +2 -7
- package/dist/components/LocalCaseDetails.svelte +126 -126
- package/dist/components/LocalCaseDetails.svelte.d.ts +6 -1
- package/dist/components/MulticonductorDetails.svelte +48 -38
- package/dist/components/NetworkStats.svelte +14 -16
- package/dist/components/PanelCard.svelte +1 -1
- package/dist/components/PlacementCue.svelte +1 -1
- package/dist/components/RatingSlider.svelte +22 -23
- package/dist/components/RestoreDefaultsButton.svelte +2 -3
- package/dist/components/SensitivityReadout.svelte +9 -27
- package/dist/components/SolveCard.svelte +69 -35
- package/dist/components/TopMovers.svelte +5 -5
- package/dist/context.svelte.d.ts +6 -2
- package/dist/context.svelte.js +4 -2
- package/dist/controller.svelte.d.ts +7 -0
- package/dist/controller.svelte.js +37 -8
- package/dist/display.d.ts +3 -2
- package/dist/display.js +28 -11
- package/dist/format.d.ts +1 -9
- package/dist/format.js +1 -14
- package/dist/index.d.ts +1 -1
- package/dist/map.d.ts +1 -1
- package/dist/map.js +1 -1
- package/dist/state.svelte.d.ts +6 -1
- package/dist/state.svelte.js +1 -0
- package/dist/styles.css +3 -0
- package/dist/types.d.ts +1 -1
- package/package.json +10 -6
|
@@ -22,34 +22,33 @@
|
|
|
22
22
|
const neutralPos = $derived(thumbAlignedPos(neutralFrac));
|
|
23
23
|
const fillLo = $derived(valFrac < neutralFrac ? valPos : neutralPos);
|
|
24
24
|
const fillHi = $derived(valFrac < neutralFrac ? neutralPos : valPos);
|
|
25
|
-
const sliderTip =
|
|
26
|
-
|
|
27
|
-
const disabledTip = "this line's rating is synthesized; not adjustable";
|
|
25
|
+
const sliderTip = 'Drag to preview, release to solve. The tick marks the committed rating.';
|
|
26
|
+
const disabledTip = 'Synthesized rating; not adjustable';
|
|
28
27
|
const scoreTip =
|
|
29
|
-
'Gradient
|
|
28
|
+
'Gradient: first-order estimate from the committed point. Exact: solved objective change.';
|
|
30
29
|
</script>
|
|
31
30
|
|
|
32
31
|
{#if ctrl.activeSolvable}
|
|
33
32
|
{@const c = ctrl.activeSolvable}
|
|
34
33
|
{@const branch = ctrl.selectedBranchData}
|
|
35
34
|
<div class="slider-block">
|
|
36
|
-
<div class="slider-head
|
|
37
|
-
<span
|
|
38
|
-
<span class="val">{signed(ctrl.ratingSliderValue)} MW</span>
|
|
35
|
+
<div class="slider-head">
|
|
36
|
+
<span>Rating change</span>
|
|
37
|
+
<span class="val mono">{signed(ctrl.ratingSliderValue)} MW</span>
|
|
39
38
|
</div>
|
|
40
|
-
<div class="line-row
|
|
39
|
+
<div class="line-row">
|
|
41
40
|
{#if branch}
|
|
42
41
|
<span class="dim"
|
|
43
|
-
>
|
|
42
|
+
>Line <span class="mono"
|
|
43
|
+
>{branch.from} – {branch.to}, {fmt.format(branch.rate_mw)} MW</span
|
|
44
|
+
></span
|
|
44
45
|
>
|
|
45
46
|
{/if}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
{
|
|
50
|
-
|
|
51
|
-
{/if}
|
|
52
|
-
</span>
|
|
47
|
+
{#if bounds.disabled}
|
|
48
|
+
<span class="dim">Not adjustable</span>
|
|
49
|
+
{:else}
|
|
50
|
+
<span class="mono dim">{fmt.format(bounds.min)} to +{fmt.format(bounds.max)} MW</span>
|
|
51
|
+
{/if}
|
|
53
52
|
</div>
|
|
54
53
|
<div
|
|
55
54
|
class="slider-track"
|
|
@@ -63,7 +62,7 @@
|
|
|
63
62
|
step="any"
|
|
64
63
|
disabled={bounds.disabled}
|
|
65
64
|
bind:value={ctrl.ratingSliderCurrent, ctrl.setRatingPreview}
|
|
66
|
-
aria-label="
|
|
65
|
+
aria-label="Rating delta at selected line"
|
|
67
66
|
onpointerdown={() => ctrl.setRatingPreview(ctrl.ratingSliderValue)}
|
|
68
67
|
onkeydown={(e) => {
|
|
69
68
|
const next = sliderKeyValue(e.key, ctrl.ratingSliderValue, bounds.min, bounds.max);
|
|
@@ -80,18 +79,18 @@
|
|
|
80
79
|
/>
|
|
81
80
|
</div>
|
|
82
81
|
<div class="rating-feedback" class:idle={!ctrl.previewing && !ctrl.isPerturbed(c)}>
|
|
83
|
-
<p class="pred
|
|
82
|
+
<p class="pred dim" aria-hidden={!(ctrl.predictedDeltaObj !== null && ctrl.previewing)}>
|
|
84
83
|
{#if ctrl.predictedDeltaObj !== null && ctrl.previewing}
|
|
85
|
-
|
|
84
|
+
Predicted objective change <span class="mono">{signed(ctrl.predictedDeltaObj)}</span>
|
|
86
85
|
{:else}
|
|
87
86
|
|
|
88
87
|
{/if}
|
|
89
88
|
</p>
|
|
90
|
-
<p class="score
|
|
89
|
+
<p class="score" aria-hidden={!(ctrl.gradientScore && ctrl.isPerturbed(c))}>
|
|
91
90
|
{#if ctrl.gradientScore && ctrl.isPerturbed(c)}
|
|
92
91
|
<span title={scoreTip}>
|
|
93
|
-
|
|
94
|
-
units
|
|
92
|
+
Gradient <span class="mono">{signed(ctrl.gradientScore.pred)}</span>, exact
|
|
93
|
+
<span class="mono">{signed(ctrl.gradientScore.exact)}</span> objective units
|
|
95
94
|
</span>
|
|
96
95
|
{:else}
|
|
97
96
|
|
|
@@ -99,7 +98,7 @@
|
|
|
99
98
|
</p>
|
|
100
99
|
<div class="reset-row">
|
|
101
100
|
{#if ctrl.isPerturbed(c)}
|
|
102
|
-
<button class="reset
|
|
101
|
+
<button class="reset" onclick={() => ctrl.resetCase(c)}>Reset rating</button>
|
|
103
102
|
{/if}
|
|
104
103
|
</div>
|
|
105
104
|
</div>
|
|
@@ -6,9 +6,7 @@
|
|
|
6
6
|
</script>
|
|
7
7
|
|
|
8
8
|
{#if config.loadDefaultCases && ctrl.casesLoaded && ctrl.hiddenDefaults.size > 0}
|
|
9
|
-
<button class="restore-defaults
|
|
10
|
-
↺ restore default cases
|
|
11
|
-
</button>
|
|
9
|
+
<button class="restore-defaults" onclick={ctrl.restoreDefaultCases}>Restore default cases</button>
|
|
12
10
|
{/if}
|
|
13
11
|
|
|
14
12
|
<style>
|
|
@@ -22,6 +20,7 @@
|
|
|
22
20
|
border: 1px solid var(--line);
|
|
23
21
|
border-radius: 3px;
|
|
24
22
|
color: var(--text-secondary);
|
|
23
|
+
font-family: inherit;
|
|
25
24
|
font-size: 11px;
|
|
26
25
|
cursor: pointer;
|
|
27
26
|
box-shadow: var(--elev-1);
|
|
@@ -11,17 +11,11 @@
|
|
|
11
11
|
'objective units'
|
|
12
12
|
)
|
|
13
13
|
);
|
|
14
|
-
const
|
|
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
|
-
);
|
|
14
|
+
const unitTitle = $derived(`Units: ${reportedUnits}`);
|
|
20
15
|
|
|
21
16
|
// Branch mode: the selection is a line and the column is price/rating; the
|
|
22
17
|
// legend and preview pipeline are identical (both columns are bus-keyed).
|
|
23
18
|
const branchMode = $derived(app.selectedBranch !== null);
|
|
24
|
-
const unitTitle = $derived(branchMode ? branchUnitTitle : busUnitTitle);
|
|
25
19
|
const previewUnits = $derived(app.previewPrices?.units ?? 'objective units/MW');
|
|
26
20
|
|
|
27
21
|
const previewStep = $derived(
|
|
@@ -42,25 +36,22 @@
|
|
|
42
36
|
<span class="chip">
|
|
43
37
|
{ctrl.previewing ? 'LMP preview' : branchMode ? '∂LMP/∂rating' : '∂LMP/∂d'}
|
|
44
38
|
</span>
|
|
45
|
-
<span class="
|
|
46
|
-
{#if branchMode}
|
|
39
|
+
<span class="dim">
|
|
40
|
+
{#if branchMode}Line <span class="mono">{branchLabel}</span>{:else}Bus
|
|
41
|
+
<span class="mono">{app.selectedBus}</span>{/if}
|
|
47
42
|
</span>
|
|
48
|
-
<button
|
|
49
|
-
><span class="key-hint">esc </span>clear</button
|
|
50
|
-
>
|
|
43
|
+
<button onclick={ctrl.clearSelection} aria-keyshortcuts="Escape" title="Esc">Clear</button>
|
|
51
44
|
</div>
|
|
52
45
|
<div class="sensitivity-readout" aria-live="polite">
|
|
53
46
|
{#if ctrl.previewing}
|
|
54
47
|
<p class="dim small">
|
|
55
|
-
{ctrl.activeSolvable?.solving
|
|
56
|
-
? 'Exact solve running; the map keeps the LMP preview.'
|
|
57
|
-
: 'First order LMP preview. Release for the exact solve.'}
|
|
48
|
+
{ctrl.activeSolvable?.solving ? 'Solving…' : 'First-order preview. Release to solve.'}
|
|
58
49
|
</p>
|
|
59
50
|
{#if ctrl.sensSummary?.flat}
|
|
60
51
|
<div class="legend flat" style:background={ctrl.flatSensBackground}></div>
|
|
61
52
|
<div class="legend-labels mono single">
|
|
62
53
|
<span
|
|
63
|
-
>
|
|
54
|
+
>Uniform {signedExp(ctrl.sensSummary.mean * previewStep)}
|
|
64
55
|
{previewUnits}</span
|
|
65
56
|
>
|
|
66
57
|
</div>
|
|
@@ -88,7 +79,7 @@
|
|
|
88
79
|
{#if ctrl.sensSummary?.flat}
|
|
89
80
|
<div class="legend flat" style:background={ctrl.flatSensBackground}></div>
|
|
90
81
|
<div class="legend-labels mono single">
|
|
91
|
-
<span>
|
|
82
|
+
<span>Uniform {signedExp(ctrl.sensSummary.mean)} {reportedUnits}</span>
|
|
92
83
|
</div>
|
|
93
84
|
{:else if ctrl.sensSummary}
|
|
94
85
|
<div class="legend" style:background={sensGradient}></div>
|
|
@@ -100,9 +91,7 @@
|
|
|
100
91
|
{:else if app.sensitivityLoading}
|
|
101
92
|
<div class="legend" style:background="var(--line)" style:opacity="0.4"></div>
|
|
102
93
|
<div class="legend-labels mono single">
|
|
103
|
-
<span class="blink">
|
|
104
|
-
computing {#if branchMode}∂value/∂fmax{:else}∂value/∂d{/if}…
|
|
105
|
-
</span>
|
|
94
|
+
<span class="blink">Computing…</span>
|
|
106
95
|
</div>
|
|
107
96
|
{/if}
|
|
108
97
|
{/if}
|
|
@@ -133,11 +122,4 @@
|
|
|
133
122
|
line-height: 1.35;
|
|
134
123
|
white-space: nowrap;
|
|
135
124
|
}
|
|
136
|
-
|
|
137
|
-
/* No Esc key to name on a touch device. */
|
|
138
|
-
@media (hover: none), (pointer: coarse) {
|
|
139
|
-
.key-hint {
|
|
140
|
-
display: none;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
125
|
</style>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getAppState, getController } from '../context.svelte.js';
|
|
3
|
-
import { formulationLabel
|
|
3
|
+
import { formulationLabel } from '../format.js';
|
|
4
4
|
import Sparkline from '../Sparkline.svelte';
|
|
5
5
|
import PanelFrame from './PanelFrame.svelte';
|
|
6
6
|
let open = $state(true);
|
|
@@ -16,67 +16,101 @@
|
|
|
16
16
|
: []
|
|
17
17
|
: (active?.iterations ?? [])
|
|
18
18
|
);
|
|
19
|
+
/** Named only when a server took part; the browser engine is the default. */
|
|
20
|
+
const server = $derived(
|
|
21
|
+
active?.solveBackend === 'rust-server'
|
|
22
|
+
? 'Server'
|
|
23
|
+
: active?.solveBackend === 'clarabel-wasm-server-sensitivity'
|
|
24
|
+
? 'Server price sensitivity'
|
|
25
|
+
: null
|
|
26
|
+
);
|
|
19
27
|
</script>
|
|
20
28
|
|
|
21
29
|
{#if saved || (active && (active.solving || active.solveMs != null))}
|
|
22
30
|
<PanelFrame id="solver" title="Solver" side="right" order={10} width={320} bind:open>
|
|
23
31
|
<div class="solvecard">
|
|
24
|
-
<div class="solve-label mono">
|
|
25
|
-
{saved ? 'Saved result' : active?.formulation === 'acpf' ? 'Power flow' : 'OPF solve'}
|
|
26
|
-
</div>
|
|
27
32
|
{#if iterations.length > 1}
|
|
28
33
|
{#key iterations}
|
|
29
34
|
<Sparkline {iterations} />
|
|
30
35
|
{/key}
|
|
31
36
|
{/if}
|
|
32
37
|
{#if saved}
|
|
33
|
-
<
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
<dl>
|
|
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
|
+
<div>
|
|
48
|
+
<dt>Saved state</dt>
|
|
49
|
+
<dd>{saved.label}</dd>
|
|
50
|
+
</div>
|
|
51
|
+
</dl>
|
|
38
52
|
{:else if active}
|
|
39
|
-
<
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
<dl>
|
|
54
|
+
<div>
|
|
55
|
+
<dt>Calculation</dt>
|
|
56
|
+
<dd>{formulationLabel(active.formulation)}</dd>
|
|
57
|
+
</div>
|
|
58
|
+
{#if iterations.length > 1}
|
|
59
|
+
<div>
|
|
60
|
+
<dt>Iterations</dt>
|
|
61
|
+
<dd class="mono">{iterations.length}</dd>
|
|
62
|
+
</div>
|
|
63
|
+
{/if}
|
|
64
|
+
{#if server}
|
|
65
|
+
<div>
|
|
66
|
+
<dt>Solver</dt>
|
|
67
|
+
<dd>{server}</dd>
|
|
68
|
+
</div>
|
|
69
|
+
{/if}
|
|
70
|
+
{#if active.solveMs != null}
|
|
71
|
+
<div>
|
|
72
|
+
<dt>Time</dt>
|
|
73
|
+
<dd class="mono">{active.solveMs} ms</dd>
|
|
74
|
+
</div>
|
|
75
|
+
{/if}
|
|
76
|
+
{#if active.solveBackend === 'rust-server' && active.solveFallbackReason}
|
|
77
|
+
<div>
|
|
78
|
+
<dt>Fallback</dt>
|
|
79
|
+
<dd title={active.solveFallbackReason}>{active.solveFallbackReason}</dd>
|
|
80
|
+
</div>
|
|
81
|
+
{/if}
|
|
82
|
+
</dl>
|
|
49
83
|
{/if}
|
|
50
84
|
</div>
|
|
51
85
|
</PanelFrame>
|
|
52
86
|
{/if}
|
|
53
87
|
|
|
54
88
|
<style>
|
|
55
|
-
.solve-label {
|
|
56
|
-
font-size: 13px;
|
|
57
|
-
margin-bottom: 8px;
|
|
58
|
-
}
|
|
59
89
|
.solvecard {
|
|
60
90
|
padding: 13px 15px 11px;
|
|
61
91
|
}
|
|
62
92
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
align-items: center;
|
|
66
|
-
flex-wrap: wrap;
|
|
67
|
-
gap: 10px 14px;
|
|
93
|
+
dl {
|
|
94
|
+
margin: 6px 0 0;
|
|
68
95
|
font-size: 12px;
|
|
69
|
-
margin-top: 6px;
|
|
70
96
|
}
|
|
71
97
|
|
|
72
|
-
|
|
73
|
-
|
|
98
|
+
dl div {
|
|
99
|
+
display: flex;
|
|
100
|
+
justify-content: space-between;
|
|
101
|
+
gap: 12px;
|
|
102
|
+
padding: 2px 0;
|
|
74
103
|
}
|
|
75
104
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
105
|
+
dt {
|
|
106
|
+
flex: none;
|
|
107
|
+
color: var(--text-secondary);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
dd {
|
|
111
|
+
min-width: 0;
|
|
112
|
+
margin: 0;
|
|
113
|
+
text-align: right;
|
|
80
114
|
overflow-wrap: anywhere;
|
|
81
115
|
}
|
|
82
116
|
</style>
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
|
|
15
15
|
<div class="movers-block">
|
|
16
16
|
{#if !ctrl.previewing && ctrl.topMovers.length > 0}
|
|
17
|
-
<table
|
|
18
|
-
<caption class="
|
|
19
|
-
|
|
17
|
+
<table>
|
|
18
|
+
<caption class="dim" title={unitTitle}>
|
|
19
|
+
Largest LMP response <span class="mono unit">{units}</span>
|
|
20
20
|
</caption>
|
|
21
21
|
<tbody>
|
|
22
22
|
{#each ctrl.topMovers as mover (mover.bus)}
|
|
23
23
|
<tr>
|
|
24
|
-
<td>
|
|
25
|
-
<td class:pos={mover.value > 0} class:neg={mover.value < 0}>
|
|
24
|
+
<td>Bus <span class="mono">{mover.bus}</span></td>
|
|
25
|
+
<td class="mono" class:pos={mover.value > 0} class:neg={mover.value < 0}>
|
|
26
26
|
{signedExp(mover.value)}
|
|
27
27
|
</td>
|
|
28
28
|
</tr>
|
package/dist/context.svelte.d.ts
CHANGED
|
@@ -9,15 +9,19 @@ export interface TellegenUiConfig {
|
|
|
9
9
|
orgLabel: string;
|
|
10
10
|
showFooter: boolean;
|
|
11
11
|
/** `'dark'` sets `data-theme="dark"` on the document root while the
|
|
12
|
-
* provider is mounted, which switches the `styles.css` tokens.
|
|
12
|
+
* provider is mounted, which switches the `styles.css` tokens. The
|
|
13
|
+
* provider keeps it in step with its prop. */
|
|
13
14
|
theme: 'light' | 'dark';
|
|
14
15
|
/** CARTO basemap key for the default shell's map, trimmed; empty for
|
|
15
|
-
* none. See `TellegenMap`'s `cartoApiKey`.
|
|
16
|
+
* none. See `TellegenMap`'s `cartoApiKey`. The provider keeps it in step
|
|
17
|
+
* with its prop. */
|
|
16
18
|
cartoApiKey: string;
|
|
17
19
|
}
|
|
18
20
|
export type TellegenUiOptions = Partial<TellegenUiConfig>;
|
|
19
21
|
export declare const DEFAULT_TELLEGEN_UI_CONFIG: TellegenUiConfig;
|
|
20
22
|
export declare function resolveTellegenUiConfig(options?: TellegenUiOptions): TellegenUiConfig;
|
|
23
|
+
export declare const resolveTheme: (theme: TellegenUiOptions["theme"]) => TellegenUiConfig["theme"];
|
|
24
|
+
export declare const resolveCartoApiKey: (key: TellegenUiOptions["cartoApiKey"]) => string;
|
|
21
25
|
export declare const setAppState: (a: AppState) => AppState;
|
|
22
26
|
export declare const setController: (c: Controller) => Controller;
|
|
23
27
|
export declare const setUiConfig: (config: TellegenUiConfig) => TellegenUiConfig;
|
package/dist/context.svelte.js
CHANGED
|
@@ -21,10 +21,12 @@ export function resolveTellegenUiConfig(options = {}) {
|
|
|
21
21
|
orgHref: options.orgHref ?? DEFAULT_TELLEGEN_UI_CONFIG.orgHref,
|
|
22
22
|
orgLabel: options.orgLabel ?? DEFAULT_TELLEGEN_UI_CONFIG.orgLabel,
|
|
23
23
|
showFooter: options.showFooter ?? DEFAULT_TELLEGEN_UI_CONFIG.showFooter,
|
|
24
|
-
theme: options.theme
|
|
25
|
-
cartoApiKey:
|
|
24
|
+
theme: resolveTheme(options.theme),
|
|
25
|
+
cartoApiKey: resolveCartoApiKey(options.cartoApiKey)
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
export const resolveTheme = (theme) => theme ?? DEFAULT_TELLEGEN_UI_CONFIG.theme;
|
|
29
|
+
export const resolveCartoApiKey = (key) => normalizeCartoApiKey(key) ?? DEFAULT_TELLEGEN_UI_CONFIG.cartoApiKey;
|
|
28
30
|
export const setAppState = (a) => setContext(APP, a);
|
|
29
31
|
export const setController = (c) => setContext(CTRL, c);
|
|
30
32
|
export const setUiConfig = (config) => setContext(CONFIG, config);
|
|
@@ -83,6 +83,7 @@ export declare class Controller {
|
|
|
83
83
|
localSeq: number;
|
|
84
84
|
loading: Promise<void> | null;
|
|
85
85
|
private readonly dropBatchGate;
|
|
86
|
+
disposed: boolean;
|
|
86
87
|
nearbyRangeAnchor: DemandRangeAnchor | null;
|
|
87
88
|
hiddenDefaults: Set<string>;
|
|
88
89
|
casesLoaded: boolean;
|
|
@@ -129,6 +130,12 @@ export declare class Controller {
|
|
|
129
130
|
maybeStartLocalSolve(id: string): void;
|
|
130
131
|
getStudy(c: SolvableCase, studyInputJson: string): Promise<BrowserStudy | null>;
|
|
131
132
|
browserSensitivity(c: SolvableCase, studyInputJson: string, target: SensTarget): Promise<SensitivityColumn | null>;
|
|
133
|
+
/** Release every engine handle this controller holds: each case's browser
|
|
134
|
+
* Study, including one still being built, and each multiconductor
|
|
135
|
+
* session, with their pending edits and in-flight requests.
|
|
136
|
+
* `TellegenProvider` calls it when it unmounts; the controller builds no
|
|
137
|
+
* Study and keeps no new session afterwards. */
|
|
138
|
+
dispose(): void;
|
|
132
139
|
disposeStudy(c: SolvableCase): void;
|
|
133
140
|
acceptSensitivity(c: SolvableCase, col: SensitivityColumn | null, target: SensTarget | null, sensitivitySeq?: number): void;
|
|
134
141
|
finishSolve(c: SolvableCase, seq: number, target: SensTarget | null): void;
|
|
@@ -19,10 +19,9 @@ function localHasBus(c, busId) {
|
|
|
19
19
|
}
|
|
20
20
|
/** Terminal copy when nothing can solve. Two facts decide the wording: whether
|
|
21
21
|
* this browser's engine failure is known permanent, and that the server
|
|
22
|
-
* declines compute.
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
const COMPUTE_OFF_NOTICE = 'solving in this browser did not complete, and server side compute is disabled in this demo. retry to run the browser engine again, or email Samuel Talkington at talks@umich.edu to express interest in server compute';
|
|
22
|
+
* declines compute. */
|
|
23
|
+
const WASM_REQUIRED_NOTICE = 'This browser cannot run the WebAssembly engine, and server compute is disabled';
|
|
24
|
+
const COMPUTE_OFF_NOTICE = 'The browser solve did not complete, and server compute is disabled. Retry to run it again';
|
|
26
25
|
/** Upper bound on waiting for a camera fly-to before the sensitivity solve
|
|
27
26
|
* starts anyway: a missing map or a cue that never appears must not stall the
|
|
28
27
|
* selection. */
|
|
@@ -107,6 +106,8 @@ export class Controller {
|
|
|
107
106
|
// File reads and worker messages retain whole buffers. Admit one validated
|
|
108
107
|
// batch at a time so repeated drops cannot multiply the aggregate byte cap.
|
|
109
108
|
dropBatchGate = new DropBatchGate();
|
|
109
|
+
// Set by dispose(). Not $state; nothing renders it.
|
|
110
|
+
disposed = false;
|
|
110
111
|
nearbyRangeAnchor = $state(null);
|
|
111
112
|
// Default cases the user has closed; drives the restore affordance. Seeded in load().
|
|
112
113
|
hiddenDefaults = $state(new Set());
|
|
@@ -268,6 +269,9 @@ export class Controller {
|
|
|
268
269
|
// under that formulation. Returns null when the sens module can't load; the caller then
|
|
269
270
|
// falls back to the server, surfacing solveFallbackReason.
|
|
270
271
|
async getStudy(c, studyInputJson) {
|
|
272
|
+
// A disposed controller builds nothing that would outlive it.
|
|
273
|
+
if (this.disposed)
|
|
274
|
+
return null;
|
|
271
275
|
const latched = this.studyUnavailable.get(c);
|
|
272
276
|
if (latched) {
|
|
273
277
|
c.solveFallbackReason ??= latched;
|
|
@@ -347,6 +351,28 @@ export class Controller {
|
|
|
347
351
|
const study = await this.getStudy(c, studyInputJson);
|
|
348
352
|
return study ? study.sensitivity(c.id, this.caseDeltas(c), this.caseRatings(c), target) : null;
|
|
349
353
|
}
|
|
354
|
+
/** Release every engine handle this controller holds: each case's browser
|
|
355
|
+
* Study, including one still being built, and each multiconductor
|
|
356
|
+
* session, with their pending edits and in-flight requests.
|
|
357
|
+
* `TellegenProvider` calls it when it unmounts; the controller builds no
|
|
358
|
+
* Study and keeps no new session afterwards. */
|
|
359
|
+
dispose() {
|
|
360
|
+
if (this.disposed)
|
|
361
|
+
return;
|
|
362
|
+
this.disposed = true;
|
|
363
|
+
this.abort?.abort();
|
|
364
|
+
this.abort = null;
|
|
365
|
+
for (const c of [...this.app.cases, ...this.app.localCases])
|
|
366
|
+
this.disposeStudy(c);
|
|
367
|
+
for (const c of this.app.multiCases) {
|
|
368
|
+
c.solveAbort?.abort();
|
|
369
|
+
if (c.mcEditTimer !== null)
|
|
370
|
+
clearTimeout(c.mcEditTimer);
|
|
371
|
+
c.mcEditTimer = null;
|
|
372
|
+
c.mcSession?.free();
|
|
373
|
+
c.mcSession = null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
350
376
|
// Release a case's Study (if any) when the case is removed.
|
|
351
377
|
disposeStudy(c) {
|
|
352
378
|
const cached = this.caseStudies.get(c);
|
|
@@ -487,7 +513,7 @@ export class Controller {
|
|
|
487
513
|
return null;
|
|
488
514
|
const domain = scalarDomain(this.activeDisplay.mode, values);
|
|
489
515
|
const { min, max } = extent(values);
|
|
490
|
-
const flatThreshold = this.activeDisplay.mode === 'price' ? 1 : 1e-5;
|
|
516
|
+
const flatThreshold = this.activeDisplay.mode === 'price' || this.activeDisplay.mode === 'demand' ? 1 : 1e-5;
|
|
491
517
|
return {
|
|
492
518
|
lo: { value: domain.lo, clamped: min < domain.lo - flatThreshold / 20 },
|
|
493
519
|
hi: { value: domain.hi, clamped: max > domain.hi + flatThreshold / 20 },
|
|
@@ -1740,7 +1766,9 @@ export class Controller {
|
|
|
1740
1766
|
if (FORMULATIONS.find((f) => f.id === next)?.disabled)
|
|
1741
1767
|
return;
|
|
1742
1768
|
c.formulation = next;
|
|
1743
|
-
|
|
1769
|
+
// Demand does not depend on the calculation, so a demand colouring stays.
|
|
1770
|
+
if (this.app.displayMode !== 'demand')
|
|
1771
|
+
this.app.displayMode = next === 'acpf' ? 'voltage' : 'price';
|
|
1744
1772
|
this.bumpRevision(c);
|
|
1745
1773
|
// The committed point carries over (same demand), but the model and its solution do
|
|
1746
1774
|
// not; drop the Study and the cached solutions so they rebuild under `next`.
|
|
@@ -2142,7 +2170,7 @@ export class Controller {
|
|
|
2142
2170
|
}
|
|
2143
2171
|
/** Compute terminal quantities only for the retained input and current case revision. */
|
|
2144
2172
|
solveMultiCase = async (c, options = {}, signal) => {
|
|
2145
|
-
if (signal?.aborted)
|
|
2173
|
+
if (signal?.aborted || this.disposed)
|
|
2146
2174
|
throw new DOMException('Calculation cancelled', 'AbortError');
|
|
2147
2175
|
if (c.solving)
|
|
2148
2176
|
throw new Error('A calculation is already running for this case');
|
|
@@ -2185,7 +2213,8 @@ export class Controller {
|
|
|
2185
2213
|
snapshot?.result ?? (await this.mcTransport.solveMcModule(input, options, abort.signal));
|
|
2186
2214
|
result = summarizeMcPfResult(fullResult);
|
|
2187
2215
|
}
|
|
2188
|
-
|
|
2216
|
+
// dispose() aborts too; a session that arrives after it is freed below.
|
|
2217
|
+
if (abort.signal.aborted || this.disposed)
|
|
2189
2218
|
throw new DOMException('Calculation cancelled', 'AbortError');
|
|
2190
2219
|
if (seq !== c.solveSeq ||
|
|
2191
2220
|
(retainedAtStart ? retainedAtStart !== c.mcSession : input !== c.moduleJson) ||
|
package/dist/display.d.ts
CHANGED
|
@@ -12,8 +12,9 @@ export type DisplayOption = {
|
|
|
12
12
|
};
|
|
13
13
|
/** Variables present in the current result. Power flow has no LMP values. */
|
|
14
14
|
export declare function displayMetaFor(c: SolvableCase | null): DisplayOption[];
|
|
15
|
-
/** Bus LMPs, voltage angles in radians,
|
|
16
|
-
*
|
|
15
|
+
/** Bus LMPs, voltage angles in radians, voltage magnitudes in per unit, or
|
|
16
|
+
* demand in MW with the case's committed changes. SOCWR supplies squared
|
|
17
|
+
* voltages, which are converted to magnitudes here. */
|
|
17
18
|
export declare function displaySeriesFor(c: SolvableCase | null, mode: DisplayMode): {
|
|
18
19
|
bus: number;
|
|
19
20
|
value: number;
|
package/dist/display.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { priceGradient } from './colors.js';
|
|
2
|
-
import { priceCopy } from './format.js';
|
|
1
|
+
import { demandGradient, priceGradient } from './colors.js';
|
|
3
2
|
import { CaseState } from './state.svelte.js';
|
|
4
3
|
/** Variables present in the current result. Power flow has no LMP values. */
|
|
5
4
|
export function displayMetaFor(c) {
|
|
@@ -11,7 +10,9 @@ export function displayMetaFor(c) {
|
|
|
11
10
|
mode: 'price',
|
|
12
11
|
label: 'LMP',
|
|
13
12
|
unit: 'objective units/MW',
|
|
14
|
-
copy:
|
|
13
|
+
copy: c.formulation === 'socwr'
|
|
14
|
+
? 'Locational marginal price, SOCWR relaxation'
|
|
15
|
+
: 'Locational marginal price',
|
|
15
16
|
gradient: priceGradient
|
|
16
17
|
});
|
|
17
18
|
}
|
|
@@ -20,29 +21,45 @@ export function displayMetaFor(c) {
|
|
|
20
21
|
mode: 'voltage',
|
|
21
22
|
label: '|V|',
|
|
22
23
|
unit: 'pu',
|
|
23
|
-
copy: c.formulation === 'socwr'
|
|
24
|
-
? 'SOCWR voltage magnitude from the current relaxed solution.'
|
|
25
|
-
: 'Bus voltage magnitude from the current power-flow result.',
|
|
24
|
+
copy: c.formulation === 'socwr' ? 'Voltage magnitude, SOCWR relaxation' : 'Voltage magnitude',
|
|
26
25
|
gradient: priceGradient
|
|
27
26
|
});
|
|
28
27
|
}
|
|
29
28
|
if (c.solution.va.length > 0) {
|
|
30
29
|
options.push({
|
|
31
30
|
mode: 'angle',
|
|
32
|
-
label: '
|
|
31
|
+
label: 'Angle',
|
|
33
32
|
unit: 'rad',
|
|
34
|
-
copy: '
|
|
33
|
+
copy: 'Voltage angle',
|
|
35
34
|
gradient: priceGradient
|
|
36
35
|
});
|
|
37
36
|
}
|
|
37
|
+
// Last, so a mode the result cannot show still falls back to a result variable.
|
|
38
|
+
if (c.network?.buses.length) {
|
|
39
|
+
options.push({
|
|
40
|
+
mode: 'demand',
|
|
41
|
+
label: 'Demand',
|
|
42
|
+
unit: 'MW',
|
|
43
|
+
copy: 'Demand, square-root scale',
|
|
44
|
+
gradient: demandGradient
|
|
45
|
+
});
|
|
46
|
+
}
|
|
38
47
|
return options;
|
|
39
48
|
}
|
|
40
|
-
/** Bus LMPs, voltage angles in radians,
|
|
41
|
-
*
|
|
49
|
+
/** Bus LMPs, voltage angles in radians, voltage magnitudes in per unit, or
|
|
50
|
+
* demand in MW with the case's committed changes. SOCWR supplies squared
|
|
51
|
+
* voltages, which are converted to magnitudes here. */
|
|
42
52
|
export function displaySeriesFor(c, mode) {
|
|
43
53
|
const sol = c?.solution;
|
|
44
|
-
if (!sol)
|
|
54
|
+
if (!c || !sol)
|
|
45
55
|
return [];
|
|
56
|
+
if (mode === 'demand') {
|
|
57
|
+
const deltas = caseDeltas(c);
|
|
58
|
+
return (c.network?.buses ?? []).map((b) => ({
|
|
59
|
+
bus: b.id,
|
|
60
|
+
value: b.demand_mw + (deltas[b.id] ?? 0)
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
46
63
|
if (mode === 'angle')
|
|
47
64
|
return sol.va;
|
|
48
65
|
if (mode === 'voltage') {
|
package/dist/format.d.ts
CHANGED
|
@@ -1,19 +1,11 @@
|
|
|
1
|
-
import type { SolveIteration } from './api.js';
|
|
2
1
|
import type { RGBA } from './colors.js';
|
|
3
|
-
import type { DisplayMode
|
|
2
|
+
import type { DisplayMode } from './state.svelte.js';
|
|
4
3
|
import { type Formulation } from '@tellegen/engine';
|
|
5
4
|
export { errorText } from '@tellegen/engine';
|
|
6
5
|
/** The short menu label for a formulation tag (e.g. `acopf` -> `AC OPF`). */
|
|
7
6
|
export declare function formulationLabel(id: Formulation): string;
|
|
8
7
|
export declare function formulationHint(id: Formulation): string;
|
|
9
|
-
export declare function priceCopy(id: Formulation): string;
|
|
10
8
|
export declare function splitName(name: string): [string, string];
|
|
11
|
-
/** Structural shape `solveMetaLabel` needs, satisfied by `CaseState` and `LocalCase`. */
|
|
12
|
-
type SolveMeta = {
|
|
13
|
-
iterations?: SolveIteration[];
|
|
14
|
-
solveBackend: SolveBackend | null;
|
|
15
|
-
};
|
|
16
|
-
export declare function solveMetaLabel(c: SolveMeta): string;
|
|
17
9
|
export declare const INFEASIBLE_DISPATCH = "No feasible dispatch: demand cannot be served within line and generator limits";
|
|
18
10
|
/** The message for a local case whose browser solve failed, and the
|
|
19
11
|
* technical detail to disclose behind it. An infeasible DC OPF means the
|
package/dist/format.js
CHANGED
|
@@ -9,23 +9,10 @@ export function formulationLabel(id) {
|
|
|
9
9
|
export function formulationHint(id) {
|
|
10
10
|
return FORMULATIONS.find((f) => f.id === id)?.hint ?? id;
|
|
11
11
|
}
|
|
12
|
-
export function priceCopy(id) {
|
|
13
|
-
const definition = 'Locational marginal price (LMP): the change in the OPF objective for an additional MW at a bus, in objective units/MW.';
|
|
14
|
-
return id === 'socwr'
|
|
15
|
-
? `${definition} Prices come from the SOCWR relaxation. Select a bus to see their sensitivity.`
|
|
16
|
-
: `${definition} Select a bus to see its demand sensitivity.`;
|
|
17
|
-
}
|
|
18
12
|
export function splitName(name) {
|
|
19
13
|
const m = name.match(/^(.*?)\s*\((.*)\)$/);
|
|
20
14
|
return m ? [m[1], m[2]] : [name, ''];
|
|
21
15
|
}
|
|
22
|
-
export function solveMetaLabel(c) {
|
|
23
|
-
if ((c.iterations ?? []).length > 1)
|
|
24
|
-
return `${c.iterations?.length} iterations`;
|
|
25
|
-
if (c.solveBackend === 'clarabel-wasm-server-sensitivity')
|
|
26
|
-
return 'server price sensitivity';
|
|
27
|
-
return c.solveBackend === 'rust-server' ? 'server solve' : 'browser solve';
|
|
28
|
-
}
|
|
29
16
|
export const INFEASIBLE_DISPATCH = 'No feasible dispatch: demand cannot be served within line and generator limits';
|
|
30
17
|
/** The message for a local case whose browser solve failed, and the
|
|
31
18
|
* technical detail to disclose behind it. An infeasible DC OPF means the
|
|
@@ -59,4 +46,4 @@ export function extent(values) {
|
|
|
59
46
|
export const fmt = new Intl.NumberFormat('en-US', { maximumFractionDigits: 1 });
|
|
60
47
|
export const signed = (v) => `${v < 0 ? '−' : '+'}${fmt.format(Math.abs(v))}`;
|
|
61
48
|
export const signedExp = (v) => `${v < 0 ? '−' : '+'}${Math.abs(v).toExponential(2)}`;
|
|
62
|
-
export const displayFmt = (mode, value) => mode === 'price' ? fmt.format(value) : value.toFixed(3);
|
|
49
|
+
export const displayFmt = (mode, value) => mode === 'price' || mode === 'demand' ? fmt.format(value) : value.toFixed(3);
|