@tellegen/svelte 0.4.6 → 0.5.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.
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import 'maplibre-gl/dist/maplibre-gl.css';
3
3
  import { tick, untrack } from 'svelte';
4
- import type { Layer, PickingInfo } from '@deck.gl/core';
4
+ import type { Layer, LayerExtension, PickingInfo } from '@deck.gl/core';
5
5
  import type { IconLayer, IconLayerProps, PathLayer, ScatterplotLayer } from '@deck.gl/layers';
6
6
  import type { MapLibreOverlay } from '@deck.gl/maplibre';
7
7
  import type { LngLatBoundsLike, Map as MapLibreMap } from 'maplibre-gl';
@@ -54,6 +54,7 @@
54
54
  transformerIconAtlas,
55
55
  transformerIconMapping
56
56
  } from './transformer-icon.js';
57
+ import { createGlobeHorizon, withGlobeHorizon } from './globe-horizon.js';
57
58
  import { foldMapBounds } from './map-bounds.js';
58
59
  import { getAppState, getController } from './context.svelte.js';
59
60
  import { displayFmt, fmt, rgbaCss } from './format.js';
@@ -239,6 +240,8 @@
239
240
  isHovering: boolean;
240
241
  };
241
242
  let layerCtors = $state.raw<LayerCtors | null>(null);
243
+ // Hides the far side of the globe; built with the deck modules.
244
+ let globeHorizon: LayerExtension | null = null;
242
245
 
243
246
  interface CaseDisplay {
244
247
  prices: Map<number, number>;
@@ -818,9 +821,10 @@
818
821
  }
819
822
 
820
823
  async function loadMapModules() {
821
- const [maplibre, deckMap, layers, workerUrl] = await Promise.all([
824
+ const [maplibre, deckMap, deckCore, layers, workerUrl] = await Promise.all([
822
825
  import('maplibre-gl'),
823
826
  import('@deck.gl/maplibre'),
827
+ import('@deck.gl/core'),
824
828
  import('@deck.gl/layers'),
825
829
  // A package export, not a relative import: its `development`
826
830
  // condition keeps the worker's `?worker&url` import out of the dev
@@ -832,6 +836,7 @@
832
836
  return {
833
837
  maplibregl: maplibre,
834
838
  MapLibreOverlay: deckMap.MapLibreOverlay,
839
+ horizon: createGlobeHorizon(deckCore.LayerExtension),
835
840
  PathLayer: layers.PathLayer,
836
841
  ScatterplotLayer: layers.ScatterplotLayer,
837
842
  IconLayer: layers.IconLayer
@@ -844,26 +849,19 @@
844
849
  // maplibre's own WebGL2 context instead of a second canvas, so the page
845
850
  // holds one GL context rather than two. Safari evicts WebGL contexts under
846
851
  // memory and tab pressure, and one context survives that far better; the
847
- // deck layers also inherit maplibre's 4096 canvas clamp. The flat basemap
848
- // writes no depth, so on the flat map the overlay always draws on top. On
849
- // the globe it tests against the depth maplibre writes for the earth, so a
850
- // network on the far hemisphere hides behind it instead of showing through.
851
- function overlayParameters(type: MapProjection) {
852
- return {
853
- depthWriteEnabled: false,
854
- depthCompare: type === 'globe' ? ('less-equal' as const) : ('always' as const)
855
- };
856
- }
857
-
858
- function buildOverlay(
859
- MapLibreOverlay: MapModules['MapLibreOverlay'],
860
- type: MapProjection
861
- ): MapLibreOverlay {
852
+ // deck layers also inherit maplibre's 4096 canvas clamp. The basemap
853
+ // writes no depth we can trust (on the globe, deck's sphere and maplibre's
854
+ // disagree near the surface), so the overlay always draws on top, and on
855
+ // the globe the horizon extension hides what lies on the far side.
856
+ function buildOverlay(MapLibreOverlay: MapModules['MapLibreOverlay']): MapLibreOverlay {
862
857
  const touch = coarsePointer();
863
858
  return new MapLibreOverlay({
864
859
  interleaved: true,
865
860
  layers: [],
866
- parameters: overlayParameters(type),
861
+ parameters: {
862
+ depthWriteEnabled: false,
863
+ depthCompare: 'always'
864
+ },
867
865
  // a small bus draws ~9px across, well under a fingertip
868
866
  pickingRadius: touch ? 12 : 0,
869
867
  onClick: (info: PickingInfo) => {
@@ -894,9 +892,10 @@
894
892
  let cleanup = () => {};
895
893
  let cancelled = false;
896
894
  void loadMapModules()
897
- .then(({ maplibregl, MapLibreOverlay, PathLayer, ScatterplotLayer, IconLayer }) => {
895
+ .then(({ maplibregl, MapLibreOverlay, horizon, PathLayer, ScatterplotLayer, IconLayer }) => {
898
896
  if (cancelled) return;
899
897
  layerCtors = { PathLayer, ScatterplotLayer, IconLayer };
898
+ globeHorizon = horizon;
900
899
  const m = new maplibregl.Map({
901
900
  container,
902
901
  style: spec.primary,
@@ -924,10 +923,7 @@
924
923
  onFallback: (reason) => onBasemapFallback?.(reason),
925
924
  onFailure: (reason) => onBasemapError?.(reason)
926
925
  });
927
- const o = buildOverlay(
928
- MapLibreOverlay,
929
- untrack(() => projection)
930
- );
926
+ const o = buildOverlay(MapLibreOverlay);
931
927
  m.addControl(o);
932
928
  m.on('click', (e) => {
933
929
  if (app.placingId) onplacecase(e.lngLat.lng, e.lngLat.lat);
@@ -1069,7 +1065,6 @@
1069
1065
  // MapLibre throws, and the `style.load` handler above applies it instead.
1070
1066
  $effect(() => {
1071
1067
  const type = projection;
1072
- overlay?.setProps({ parameters: overlayParameters(type) });
1073
1068
  const m = map;
1074
1069
  if (!m) return;
1075
1070
  try {
@@ -1158,7 +1153,7 @@
1158
1153
  updateTriggers: { getFillColor: [study, mode, theme], getRadius: [app.busSizing] }
1159
1154
  })
1160
1155
  );
1161
- overlay.setProps({ layers: [...layers, ...extraLayers] });
1156
+ overlay.setProps({ layers: withGlobeHorizon([...layers, ...extraLayers], globeHorizon) });
1162
1157
  return;
1163
1158
  }
1164
1159
  for (const c of app.cases) {
@@ -1431,7 +1426,7 @@
1431
1426
  })
1432
1427
  );
1433
1428
  }
1434
- overlay.setProps({ layers: [...layers, ...extraLayers] });
1429
+ overlay.setProps({ layers: withGlobeHorizon([...layers, ...extraLayers], globeHorizon) });
1435
1430
  });
1436
1431
 
1437
1432
  $effect(() => {
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { mcNetworkScopeLabel, mcLoadModelLabel } from '@tellegen/engine';
2
3
  import { untrack } from 'svelte';
3
4
  import { on } from 'svelte/events';
4
5
  import type { Network, NetworkBus } from '../api.js';
@@ -395,7 +396,10 @@
395
396
  >
396
397
  </div>
397
398
  </div>
398
- <p class="coordinate-label">Diagram coordinates</p>
399
+ <p class="coordinate-label">
400
+ Diagram coordinates{#if multiconductor?.result}
401
+ · {mcLoadModelLabel(multiconductor.result.load_model)} · {mcNetworkScopeLabel(multiconductor.result.network_scope)}{/if}
402
+ </p>
399
403
  </div>
400
404
 
401
405
  <style>
@@ -0,0 +1,67 @@
1
+ <script lang="ts">
2
+ import { mcLoadModelLabel, type McLoadModelAssessment } from '@tellegen/engine';
3
+ let { model, compact = false }: { model?: McLoadModelAssessment | null; compact?: boolean } =
4
+ $props();
5
+ </script>
6
+
7
+ <aside class:warning={model?.status !== 'original'} aria-label="Load-model qualification">
8
+ <strong>{mcLoadModelLabel(model)}</strong>
9
+ {#if model?.status === 'modified'}
10
+ <p>
11
+ The calculation converged using modified load equations on {model.modified_branch_count}
12
+ load {model.modified_branch_count === 1 ? 'branch' : 'branches'}. These values are not a
13
+ verified solution of the original load model. The qualification is retained when saving or
14
+ exporting.
15
+ </p>
16
+ {:else if model?.status !== 'original'}
17
+ <p>
18
+ These results have no verified solution of the original load equations. Rerun to assess the
19
+ load model.
20
+ </p>
21
+ {/if}
22
+ {#if model && !compact}
23
+ <details>
24
+ <summary>Model assessment and settings used</summary>
25
+ <p>
26
+ Original-model KCL residual: {model.original_kcl_residual === null
27
+ ? 'undefined at this voltage'
28
+ : `${model.original_kcl_residual.toExponential(3)} A`}. Scaled residual: {model.original_scaled_kcl_residual ===
29
+ null
30
+ ? 'unavailable'
31
+ : model.original_scaled_kcl_residual.toPrecision(4)} (at most 1 passes).
32
+ </p>
33
+ <p>
34
+ Load-model envelope {model.options.voltage_envelope ? 'enabled' : 'disabled'}; Vlow {model
35
+ .options.v_low_pu}, Vmin {model.options.v_min_pu}, Vmax {model.options.v_max_pu} pu. These thresholds
36
+ change load equations; they are not operating voltage limits.
37
+ </p>
38
+ <p>
39
+ Maximum iterations {model.options.max_iterations}; damping {model.options.damping};
40
+ voltage-change tolerance {model.options.tolerance} V; zero-voltage tolerance {model.options
41
+ .zero_voltage_tolerance} V; KCL tolerances {model.options.absolute_kcl_tolerance} A absolute
42
+ and {model.options.relative_kcl_tolerance} relative.
43
+ </p>
44
+ </details>
45
+ {/if}
46
+ </aside>
47
+
48
+ <style>
49
+ aside {
50
+ margin: 0.75rem 0;
51
+ padding: 0.65rem;
52
+ border: 1px solid var(--line);
53
+ border-radius: 4px;
54
+ font-size: 12px;
55
+ }
56
+ .warning {
57
+ border-left: 4px solid #b7791f;
58
+ }
59
+ p {
60
+ margin: 0.4rem 0;
61
+ line-height: 1.5;
62
+ }
63
+ summary {
64
+ cursor: pointer;
65
+ margin-top: 0.4rem;
66
+ }
67
+ </style>
@@ -0,0 +1,8 @@
1
+ import { type McLoadModelAssessment } from '@tellegen/engine';
2
+ type $$ComponentProps = {
3
+ model?: McLoadModelAssessment | null;
4
+ compact?: boolean;
5
+ };
6
+ declare const McModelQualification: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type McModelQualification = ReturnType<typeof McModelQualification>;
8
+ export default McModelQualification;
@@ -0,0 +1,90 @@
1
+ <script lang="ts">
2
+ import {
3
+ mcNetworkScopeLabel,
4
+ type McNetworkScope as NetworkScope,
5
+ type McExcludedIsland
6
+ } from '@tellegen/engine';
7
+ let {
8
+ scope,
9
+ islands = [],
10
+ preview = false
11
+ }: {
12
+ scope?: NetworkScope | null;
13
+ islands?: McExcludedIsland[];
14
+ preview?: boolean;
15
+ } = $props();
16
+ </script>
17
+
18
+ {#if scope}
19
+ <aside aria-label="Network calculation scope">
20
+ <strong>{preview ? 'Unpowered islands detected' : mcNetworkScopeLabel(scope)}</strong>
21
+ {#if preview}<p>
22
+ {scope.excluded_island_count}
23
+ {scope.excluded_island_count === 1 ? 'island has' : 'islands have'} no voltage source.
24
+ </p>{/if}
25
+ <p>
26
+ {scope.excluded_bus_count}
27
+ {scope.excluded_bus_count === 1 ? 'bus' : 'buses'} and {scope.excluded_equipment_count}
28
+ equipment
29
+ {scope.excluded_equipment_count === 1 ? 'record' : 'records'}
30
+ {preview
31
+ ? 'will be excluded when you solve the energized network'
32
+ : 'are excluded from the calculation'}
33
+ because their {scope.excluded_island_count === 1 ? 'island has' : 'islands have'} no voltage source.
34
+ The full network remains available for inspection. Excluded equipment has no calculated result;
35
+ its voltage and power are not reported as zero.
36
+ </p>
37
+ {#if scope.excluded_load_count}<p>
38
+ {scope.excluded_load_count}
39
+ {scope.excluded_load_count === 1 ? 'load has' : 'loads have'} no voltage source and {preview
40
+ ? 'will be omitted'
41
+ : scope.excluded_load_count === 1
42
+ ? 'is absent'
43
+ : 'are absent'} from solved totals.
44
+ </p>{/if}
45
+ {#if islands.length}
46
+ <details>
47
+ <summary
48
+ >{preview
49
+ ? 'Islands and equipment to exclude'
50
+ : 'Excluded islands and equipment'}</summary
51
+ >
52
+ {#each islands as island, i}
53
+ <h4>Island {i + 1}: no voltage source</h4>
54
+ <p>Buses: {island.buses.join(', ')}</p>
55
+ <ul>
56
+ {#each island.equipment as item}<li>
57
+ {item.kind}: {item.name}
58
+ </li>{/each}
59
+ </ul>
60
+ {/each}
61
+ </details>
62
+ {/if}
63
+ </aside>
64
+ {/if}
65
+
66
+ <style>
67
+ aside {
68
+ margin: 0.75rem 0;
69
+ padding: 0.65rem;
70
+ border: 1px solid var(--line);
71
+ border-left: 4px solid #b7791f;
72
+ border-radius: 4px;
73
+ font-size: 12px;
74
+ overflow-wrap: anywhere;
75
+ }
76
+ p {
77
+ margin: 0.4rem 0;
78
+ line-height: 1.5;
79
+ }
80
+ summary {
81
+ cursor: pointer;
82
+ }
83
+ details {
84
+ max-height: 20rem;
85
+ overflow: auto;
86
+ }
87
+ h4 {
88
+ margin: 0.75rem 0 0.25rem;
89
+ }
90
+ </style>
@@ -0,0 +1,9 @@
1
+ import { type McNetworkScope as NetworkScope, type McExcludedIsland } from '@tellegen/engine';
2
+ type $$ComponentProps = {
3
+ scope?: NetworkScope | null;
4
+ islands?: McExcludedIsland[];
5
+ preview?: boolean;
6
+ };
7
+ declare const McNetworkScope: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type McNetworkScope = ReturnType<typeof McNetworkScope>;
9
+ export default McNetworkScope;
@@ -0,0 +1,158 @@
1
+ <script lang="ts">
2
+ import { DEFAULT_MC_PF_OPTIONS, validateMcPfOptions, type McPfOptions } from '@tellegen/engine';
3
+ let {
4
+ options = $bindable<McPfOptions>({ ...DEFAULT_MC_PF_OPTIONS }),
5
+ disabled = false,
6
+ onapply
7
+ }: {
8
+ options: McPfOptions;
9
+ disabled?: boolean;
10
+ onapply?: (options: McPfOptions) => Promise<unknown>;
11
+ } = $props();
12
+ let failure = $state('');
13
+ const validation = $derived(validateMcPfOptions(options));
14
+ // Keep one shared set of next-run settings for every solve action. Fill
15
+ // omitted options when opening an older snapshot with partial settings.
16
+ $effect(() => {
17
+ if (Object.keys(DEFAULT_MC_PF_OPTIONS).some((key) => !(key in options))) {
18
+ options = { ...DEFAULT_MC_PF_OPTIONS, ...options };
19
+ }
20
+ });
21
+ async function apply() {
22
+ failure = '';
23
+ if (validation) return;
24
+ try {
25
+ await onapply?.({ ...options });
26
+ } catch (error) {
27
+ failure = error instanceof Error ? error.message : String(error);
28
+ }
29
+ }
30
+ </script>
31
+
32
+ <details class="settings">
33
+ <summary>Power flow settings</summary>
34
+ <p>
35
+ These settings apply to both Run power flow and Solve energized network. Editing them does not
36
+ start a calculation. A failed run retains the previous result and the settings used for that
37
+ result.
38
+ </p>
39
+ <fieldset {disabled}>
40
+ <legend>Load-model envelope</legend>
41
+ <label class="toggle"
42
+ ><input type="checkbox" bind:checked={options.voltage_envelope} />Use load-model envelope</label
43
+ >
44
+ <p>
45
+ Outside the normal band, the envelope changes the load equations to help convergence. Modified
46
+ results are labelled explicitly. These thresholds are not operating voltage limits.
47
+ </p>
48
+ <label>Vlow (pu)<input type="number" step="any" bind:value={options.v_low_pu} /></label>
49
+ <label>Vmin (pu)<input type="number" step="any" bind:value={options.v_min_pu} /></label>
50
+ <label>Vmax (pu)<input type="number" step="any" bind:value={options.v_max_pu} /></label>
51
+ <p>
52
+ With the envelope disabled, the original load equations are used at every voltage; convergence
53
+ is not guaranteed.
54
+ </p>
55
+ </fieldset>
56
+ <fieldset {disabled}>
57
+ <legend>Numerical solver</legend>
58
+ <label
59
+ >Maximum iterations<input
60
+ type="number"
61
+ min="1"
62
+ step="1"
63
+ bind:value={options.max_iterations}
64
+ /></label
65
+ >
66
+ <label
67
+ >Damping<input type="number" min="0" max="1" step="any" bind:value={options.damping} /></label
68
+ >
69
+ <details>
70
+ <summary>Advanced tolerances</summary>
71
+ <label
72
+ >Voltage-change tolerance (V)<input
73
+ type="number"
74
+ step="any"
75
+ bind:value={options.tolerance}
76
+ /></label
77
+ >
78
+ <label
79
+ >Zero-voltage tolerance (V)<input
80
+ type="number"
81
+ step="any"
82
+ bind:value={options.zero_voltage_tolerance}
83
+ /></label
84
+ >
85
+ <label
86
+ >Absolute KCL tolerance (A)<input
87
+ type="number"
88
+ step="any"
89
+ bind:value={options.absolute_kcl_tolerance}
90
+ /></label
91
+ >
92
+ <label
93
+ >Relative KCL tolerance<input
94
+ type="number"
95
+ step="any"
96
+ bind:value={options.relative_kcl_tolerance}
97
+ /></label
98
+ >
99
+ </details>
100
+ </fieldset>
101
+ {#if validation}<p role="alert">{validation}</p>{/if}
102
+ {#if failure}<p role="alert">{failure}</p>{/if}
103
+ <div class="actions">
104
+ {#if onapply}<button disabled={disabled || !!validation} onclick={() => void apply()}
105
+ >Apply settings and run</button
106
+ >{/if}
107
+ <button
108
+ {disabled}
109
+ onclick={() => {
110
+ options = { ...DEFAULT_MC_PF_OPTIONS };
111
+ failure = '';
112
+ }}>Restore defaults</button
113
+ >
114
+ </div>
115
+ </details>
116
+
117
+ <style>
118
+ .settings {
119
+ margin: 0.75rem 0;
120
+ font-size: 12px;
121
+ }
122
+ summary {
123
+ cursor: pointer;
124
+ font-weight: 600;
125
+ }
126
+ fieldset {
127
+ margin: 0.75rem 0;
128
+ border: 1px solid var(--line);
129
+ border-radius: 4px;
130
+ }
131
+ label {
132
+ display: flex;
133
+ align-items: center;
134
+ justify-content: space-between;
135
+ gap: 0.5rem;
136
+ margin: 0.5rem 0;
137
+ }
138
+ input[type='number'] {
139
+ width: 7rem;
140
+ min-width: 0;
141
+ }
142
+ .toggle {
143
+ justify-content: flex-start;
144
+ }
145
+ p {
146
+ line-height: 1.5;
147
+ }
148
+ .actions {
149
+ display: flex;
150
+ flex-wrap: wrap;
151
+ gap: 0.5rem;
152
+ }
153
+ [role='alert'] {
154
+ color: var(--text-primary);
155
+ border-left: 3px solid #b7791f;
156
+ padding-left: 0.5rem;
157
+ }
158
+ </style>
@@ -0,0 +1,9 @@
1
+ import { type McPfOptions } from '@tellegen/engine';
2
+ type $$ComponentProps = {
3
+ options: McPfOptions;
4
+ disabled?: boolean;
5
+ onapply?: (options: McPfOptions) => Promise<unknown>;
6
+ };
7
+ declare const McPfSettings: import("svelte").Component<$$ComponentProps, {}, "options">;
8
+ type McPfSettings = ReturnType<typeof McPfSettings>;
9
+ export default McPfSettings;
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import McNetworkScope from './McNetworkScope.svelte';
3
+ import McModelQualification from './McModelQualification.svelte';
2
4
  import {
3
5
  mcPfResultDetail,
4
6
  summarizeMcPfResult,
@@ -13,6 +15,7 @@
13
15
  import type { McEditTiming } from '../state.svelte.js';
14
16
  let {
15
17
  result,
18
+ excludedIslands = [],
16
19
  detail,
17
20
  timing = null,
18
21
  elapsedMs,
@@ -22,6 +25,7 @@
22
25
  }: {
23
26
  /** A live session's summary, or a stored full result. */
24
27
  result: McPfSummary | McPfResult;
28
+ excludedIslands?: import('@tellegen/engine').McExcludedIsland[];
25
29
  /** Fetches one bus's terminals and a page of equipment ports. A full
26
30
  * `result` serves its own pages when this is omitted. */
27
31
  detail?: (query: McPfDetailQuery) => Promise<McPfDetail | null>;
@@ -120,6 +124,11 @@
120
124
  </script>
121
125
 
122
126
  <section aria-label="AC power flow results" class="mc-results">
127
+ <McNetworkScope
128
+ scope={summary.network_scope}
129
+ islands={'excluded_islands' in result ? result.excluded_islands : excludedIslands}
130
+ />
131
+ <McModelQualification model={summary.load_model} />
123
132
  <div class="result-heading">
124
133
  <strong>AC power flow</strong><span
125
134
  >{summary.iterations} iterations{elapsedMs === null
@@ -133,7 +142,7 @@
133
142
  <dd>{summary.converged ? 'Converged' : 'Not converged'}</dd>
134
143
  </div>
135
144
  <div>
136
- <dt>Voltage band</dt>
145
+ <dt>Load-model voltage band</dt>
137
146
  <dd>{summary.voltage_valid ? 'Valid' : `${summary.voltage_violation_count} violations`}</dd>
138
147
  </div>
139
148
  <div>
@@ -165,6 +174,12 @@
165
174
  >{/each}</select
166
175
  ></label
167
176
  >
177
+ {#if excludedIslands.some((island) => island.buses.includes(busId ?? ''))}
178
+ <p role="status">
179
+ This bus is excluded from the calculation because its island has no voltage source. No result
180
+ is available.
181
+ </p>
182
+ {/if}
168
183
  <label class="search"
169
184
  >Terminals<input
170
185
  aria-label="Find terminal"
@@ -3,6 +3,7 @@ import type { McEditTiming } from '../state.svelte.js';
3
3
  type $$ComponentProps = {
4
4
  /** A live session's summary, or a stored full result. */
5
5
  result: McPfSummary | McPfResult;
6
+ excludedIslands?: import('@tellegen/engine').McExcludedIsland[];
6
7
  /** Fetches one bus's terminals and a page of equipment ports. A full
7
8
  * `result` serves its own pages when this is omitted. */
8
9
  detail?: (query: McPfDetailQuery) => Promise<McPfDetail | null>;
@@ -206,6 +206,7 @@
206
206
  `Tellegen export ${new Date().toISOString()}`,
207
207
  `Case: ${label}`,
208
208
  `${snapshot.label}; ${snapshot.iterations ?? '?'} iterations`,
209
+ ...(snapshot.provenance ?? []),
209
210
  'P/Q are positive into the equipment at each end; voltages are terminal-pair magnitudes'
210
211
  ]);
211
212
  const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
@@ -227,6 +228,7 @@
227
228
  onclick={() => onschematic(!schematic)}>{schematic ? 'Show map' : 'Show diagram'}</button
228
229
  >{/if}
229
230
  </div>
231
+ {#if snapshot}<p class="result-provenance">{snapshot.label}</p>{/if}
230
232
  <OmniSearch
231
233
  index={search}
232
234
  hint={searchHint}
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import McNetworkScope from './McNetworkScope.svelte';
3
+ import McModelQualification from './McModelQualification.svelte';
2
4
  import { getAppState, getController } from '../context.svelte.js';
3
5
  import { fmt, rgbaCss } from '../format.js';
4
6
  import {
@@ -10,7 +12,7 @@
10
12
  } from '../multiconductor.js';
11
13
  import { getPanelLayout } from '../panels.svelte.js';
12
14
  import { getNoticeCenter } from '../notices.svelte.js';
13
- import { type DistAttachmentKind } from '@tellegen/engine';
15
+ import { validateMcPfOptions, type DistAttachmentKind } from '@tellegen/engine';
14
16
 
15
17
  const app = getAppState();
16
18
  const ctrl = getController();
@@ -50,7 +52,10 @@
50
52
  {#if s}
51
53
  <p class="tag">Multiconductor</p>
52
54
  <details class="case-details">
53
- <summary>{s.n_bus} buses, {s.n_edge} {s.n_edge === 1 ? 'branch' : 'branches'}</summary>
55
+ <summary
56
+ >{s.n_bus} buses, {s.n_edge}
57
+ {s.n_edge === 1 ? 'branch' : 'branches'}</summary
58
+ >
54
59
  <dl>
55
60
  <div>
56
61
  <dt>Buses</dt>
@@ -97,6 +102,15 @@
97
102
  </dd>
98
103
  </div>
99
104
  </dl>
105
+ <McNetworkScope
106
+ scope={mc.mcPreparedScope}
107
+ islands={mc.mcExcludedIslands}
108
+ preview={!mc.result}
109
+ />
110
+ {#if !mc.mcPfSupported}<p class="footnote">{mc.mcPfReason}</p>{/if}
111
+ {#if validateMcPfOptions(mc.mcOptions)}<p class="footnote">
112
+ {validateMcPfOptions(mc.mcOptions)} Update Power flow settings in Studies before solving.
113
+ </p>{/if}
100
114
  <div class="calculation-actions">
101
115
  {#if mc.solving}
102
116
  <span role="status">Calculating&hellip;</span>{#if mc.solveAbort}<button
@@ -104,13 +118,13 @@
104
118
  >{/if}
105
119
  {:else}
106
120
  <button
107
- disabled={!mc.mcPfSupported}
121
+ disabled={!mc.mcPfSupported || !!validateMcPfOptions(mc.mcOptions)}
108
122
  onclick={() => {
109
123
  void ctrl
110
- .solveMultiCase(mc)
124
+ .solveMultiCase(mc, mc.mcOptions, undefined, mc.mcRequiresEnergizedSolve)
111
125
  .then(openResults)
112
126
  .catch(() => {});
113
- }}>Solve AC power flow</button
127
+ }}>{mc.mcSolveLabel}</button
114
128
  >
115
129
  {#if !mc.mcPfSupported}
116
130
  <button
@@ -125,12 +139,17 @@
125
139
  {/if}
126
140
  {/if}
127
141
  </div>
128
- {#if mc.result}<p class="footnote">Converged, {mc.result.iterations} iterations</p>
142
+ {#if mc.result}<McModelQualification model={mc.result.load_model} compact />
143
+ <p class="footnote">Converged, {mc.result.iterations} iterations</p>
129
144
  <button class="reset" onclick={openResults}>View results in Studies</button>{/if}
130
145
  {/if}
131
146
 
132
147
  {#if mc.selectedBus}
133
148
  {@const b = mc.selectedBus}
149
+ {#if mc.mcExcludedIslands.some((island) => island.buses.includes(b.id))}<p role="status">
150
+ This bus belongs to an island with no voltage source and will be excluded from
151
+ energized-network calculations. No power-flow result is available for it.
152
+ </p>{/if}
134
153
  <hr />
135
154
  <h3>Bus <span class="mono">{b.id}</span></h3>
136
155
  <div class="terminals">
@@ -25,3 +25,6 @@ export { default as TopMovers } from './components/TopMovers.svelte';
25
25
  export { default as Sparkline } from './Sparkline.svelte';
26
26
  export { default as McResults } from './components/McResults.svelte';
27
27
  export { default as McWorkbench } from './components/McWorkbench.svelte';
28
+ export { default as McPfSettings } from './components/McPfSettings.svelte';
29
+ export { default as McModelQualification } from './components/McModelQualification.svelte';
30
+ export { default as McNetworkScope } from './components/McNetworkScope.svelte';
@@ -25,3 +25,6 @@ export { default as TopMovers } from './components/TopMovers.svelte';
25
25
  export { default as Sparkline } from './Sparkline.svelte';
26
26
  export { default as McResults } from './components/McResults.svelte';
27
27
  export { default as McWorkbench } from './components/McWorkbench.svelte';
28
+ export { default as McPfSettings } from './components/McPfSettings.svelte';
29
+ export { default as McModelQualification } from './components/McModelQualification.svelte';
30
+ export { default as McNetworkScope } from './components/McNetworkScope.svelte';