@tellegen/svelte 0.4.7 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +25 -1
  2. package/dist/TellegenMap.svelte +47 -13
  3. package/dist/TellegenMap.svelte.d.ts +3 -1
  4. package/dist/antimeridian.d.ts +21 -0
  5. package/dist/antimeridian.js +40 -0
  6. package/dist/basemap.d.ts +10 -0
  7. package/dist/basemap.js +20 -0
  8. package/dist/bus-kinds.d.ts +9 -0
  9. package/dist/bus-kinds.js +33 -0
  10. package/dist/components/DiagramCanvas.svelte +5 -1
  11. package/dist/components/McModelQualification.svelte +67 -0
  12. package/dist/components/McModelQualification.svelte.d.ts +8 -0
  13. package/dist/components/McNetworkScope.svelte +90 -0
  14. package/dist/components/McNetworkScope.svelte.d.ts +9 -0
  15. package/dist/components/McPfSettings.svelte +158 -0
  16. package/dist/components/McPfSettings.svelte.d.ts +9 -0
  17. package/dist/components/McResults.svelte +16 -1
  18. package/dist/components/McResults.svelte.d.ts +1 -0
  19. package/dist/components/McWorkbench.svelte +2 -0
  20. package/dist/components/MulticonductorDetails.svelte +25 -6
  21. package/dist/components.d.ts +3 -0
  22. package/dist/components.js +3 -0
  23. package/dist/controller.svelte.d.ts +9 -5
  24. package/dist/controller.svelte.js +83 -18
  25. package/dist/diagram/schematic-equipment.d.ts +4 -3
  26. package/dist/diagram/schematic-equipment.js +13 -1
  27. package/dist/diagram/snapshot.d.ts +6 -3
  28. package/dist/diagram/snapshot.js +56 -35
  29. package/dist/index.d.ts +3 -0
  30. package/dist/index.js +3 -0
  31. package/dist/map-bounds.d.ts +8 -0
  32. package/dist/map-bounds.js +21 -8
  33. package/dist/multiconductor.js +38 -4
  34. package/dist/state.svelte.d.ts +33 -8
  35. package/dist/state.svelte.js +35 -5
  36. package/package.json +10 -10
@@ -1,4 +1,4 @@
1
- import type { DistAttachmentKind, DistConnection, DistGraph } from '@tellegen/engine';
1
+ import type { DistAttachmentKind, DistConnection, DistGraph, McPfResultTables } from '@tellegen/engine';
2
2
  import type { SchematicGeometry } from './schematic-layout.js';
3
3
  /** A stroked glyph: polylines in symbol units, x along the connection, y down. */
4
4
  export type Glyph = [number, number][][];
@@ -108,8 +108,9 @@ export declare function flowArrows(symbols: EquipmentSymbols, ends: ({
108
108
  /** Complex power into each element (loads, generators, IBRs, shunts) by element id, and for
109
109
  * sources the power they draw (the negative of what they inject), so every attachment reads with
110
110
  * the same convention: positive is consumption. Branch ports are left out: names are unique only
111
- * within a family, and a line sharing a load's name must not add to it. */
112
- export declare function elementPowers(result: {
111
+ * within a family, and a line sharing a load's name must not add to it. Reads a full result's
112
+ * objects or its columns. */
113
+ export declare function elementPowers(result: McPfResultTables | {
113
114
  element_ports: {
114
115
  element: string;
115
116
  kind?: string;
@@ -620,7 +620,8 @@ const BRANCH_PORT_KINDS = new Set(['line', 'switch', 'transformer']);
620
620
  /** Complex power into each element (loads, generators, IBRs, shunts) by element id, and for
621
621
  * sources the power they draw (the negative of what they inject), so every attachment reads with
622
622
  * the same convention: positive is consumption. Branch ports are left out: names are unique only
623
- * within a family, and a line sharing a load's name must not add to it. */
623
+ * within a family, and a line sharing a load's name must not add to it. Reads a full result's
624
+ * objects or its columns. */
624
625
  export function elementPowers(result) {
625
626
  const out = new Map();
626
627
  const add = (id, p, q) => {
@@ -629,6 +630,17 @@ export function elementPowers(result) {
629
630
  row.q += q;
630
631
  out.set(id, row);
631
632
  };
633
+ if ('strings' in result) {
634
+ const names = result.strings;
635
+ const power = result.portPower;
636
+ for (let k = 0; k < result.portElement.length; k++)
637
+ if (!BRANCH_PORT_KINDS.has(names[result.portKind[k]]))
638
+ add(names[result.portElement[k]], power[2 * k], power[2 * k + 1]);
639
+ const source = result.sourcePower;
640
+ for (let k = 0; k < result.sourceName.length; k++)
641
+ add(names[result.sourceName[k]], -source[2 * k], -source[2 * k + 1]);
642
+ return out;
643
+ }
632
644
  for (const port of result.element_ports)
633
645
  if (port.kind === undefined || !BRANCH_PORT_KINDS.has(port.kind))
634
646
  add(port.element, port.power_into_element.re, port.power_into_element.im);
@@ -1,4 +1,4 @@
1
- import type { McComplex, McPfResult } from '@tellegen/engine';
1
+ import { type McComplex, type McPfResult, type McPfResultTables } from '@tellegen/engine';
2
2
  /** Which reference a pair voltage is measured against. */
3
3
  export type PairZone = 'phase–neutral' | 'phase–phase' | 'phase–ground';
4
4
  /** The second member of a phase–ground pair. */
@@ -52,6 +52,8 @@ export interface BranchSnapshot {
52
52
  export interface ResultSnapshot {
53
53
  kind: 'solved' | 'stress';
54
54
  label: string;
55
+ /** Qualification and effective settings retained by CSV exports. */
56
+ provenance?: string[];
55
57
  iterations: number | null;
56
58
  buses: BusSnapshot[];
57
59
  branches: BranchSnapshot[];
@@ -107,9 +109,10 @@ export interface SnapshotBranch {
107
109
  /** Voltage unbalance factor |V2| / |V1| in percent, from three phasors in phase order. The ratio
108
110
  * is the same for phase–neutral and phase–phase voltages, so either reference works. */
109
111
  export declare function sequenceUnbalance(a: McComplex, b: McComplex, c: McComplex): number | null;
110
- /** Project a full multiconductor power-flow result onto the drawing's bus and branch order.
112
+ /** Project a full multiconductor power-flow result onto the drawing's bus and branch order, from
113
+ * its object form or its columns (`McPfResultTables`, what a live session transfers).
111
114
  * Missing ports leave an end `null`; nothing is interpolated. */
112
- export declare function snapshotFromMcPf(result: McPfResult, buses: SnapshotBus[], branches: SnapshotBranch[], label?: string): ResultSnapshot;
115
+ export declare function snapshotFromMcPf(result: McPfResult | McPfResultTables, buses: SnapshotBus[], branches: SnapshotBranch[], label?: string): ResultSnapshot;
113
116
  /** Bus results from a live session's transferred terminal arrays (`[re, im]` interleaved), for when
114
117
  * the full result with element ports has not been fetched. Branches are left without results. */
115
118
  export declare function snapshotBusesFromTerminals(ids: ArrayLike<[string, string]>, voltages: ArrayLike<number>, buses: SnapshotBus[]): BusSnapshot[];
@@ -1,3 +1,4 @@
1
+ import { mcPfResultTables, mcLoadModelLabel, mcNetworkScopeLabel } from '@tellegen/engine';
1
2
  import { neutralTerminal } from '../multiconductor.js';
2
3
  /** The second member of a phase–ground pair. */
3
4
  export const GROUND = 'ground';
@@ -89,27 +90,34 @@ function busSnapshot(bus, voltage) {
89
90
  const vuf = phasors.length === 3 ? sequenceUnbalance(phasors[0], phasors[1], phasors[2]) : null;
90
91
  return { pairs, unbalance: vuf === null ? null : { percent: vuf, definition: 'VUF' } };
91
92
  }
92
- /** Project a full multiconductor power-flow result onto the drawing's bus and branch order.
93
+ /** Project a full multiconductor power-flow result onto the drawing's bus and branch order, from
94
+ * its object form or its columns (`McPfResultTables`, what a live session transfers).
93
95
  * Missing ports leave an end `null`; nothing is interpolated. */
94
96
  export function snapshotFromMcPf(result, buses, branches, label = 'Tellegen multiconductor power flow') {
95
- // Nested maps keyed by bus, then element: no composite string keys on a 100k-bus result.
97
+ const t = 'strings' in result ? result : mcPfResultTables(result);
98
+ const names = t.strings;
99
+ // Nested maps keyed by bus, then terminal or element, holding row numbers into the tables: no
100
+ // composite string keys and no object per row on a 100k-bus result.
96
101
  const voltages = new Map();
97
- for (const t of result.terminals) {
98
- let bus = voltages.get(t.bus);
102
+ for (let k = 0; k < t.terminalBus.length; k++) {
103
+ const id = names[t.terminalBus[k]];
104
+ let bus = voltages.get(id);
99
105
  if (!bus)
100
- voltages.set(t.bus, (bus = new Map()));
101
- bus.set(t.terminal, t.voltage);
106
+ voltages.set(id, (bus = new Map()));
107
+ bus.set(names[t.terminalName[k]], k);
102
108
  }
103
109
  const ports = new Map();
104
- for (const p of result.element_ports) {
105
- let element = ports.get(p.element);
110
+ for (let k = 0; k < t.portElement.length; k++) {
111
+ const id = names[t.portElement[k]];
112
+ let element = ports.get(id);
106
113
  if (!element)
107
- ports.set(p.element, (element = new Map()));
108
- const rows = element.get(p.bus);
114
+ ports.set(id, (element = new Map()));
115
+ const bus = names[t.portBus[k]];
116
+ const rows = element.get(bus);
109
117
  if (rows)
110
- rows.push(p);
118
+ rows.push(k);
111
119
  else
112
- element.set(p.bus, [p]);
120
+ element.set(bus, [k]);
113
121
  }
114
122
  /** The element's port rows, of its own kind when a line and a switch share a name. A closed
115
123
  * switch has no ports of its own (the solver merges its buses), so it must not borrow another
@@ -118,8 +126,10 @@ export function snapshotFromMcPf(result, buses, branches, label = 'Tellegen mult
118
126
  const all = ports.get(branch.id)?.get(bus);
119
127
  if (!all || branch.kind === undefined)
120
128
  return all;
121
- return all.filter((r) => r.kind === branch.kind);
129
+ return all.filter((k) => names[t.portKind[k]] === branch.kind);
122
130
  };
131
+ const power = t.portPower;
132
+ const current = t.portCurrent;
123
133
  const seen = new Set();
124
134
  const branchSnapshots = branches.map((branch) => {
125
135
  const key = `${branch.kind ?? ''}\u0000${branch.id}`;
@@ -132,33 +142,44 @@ export function snapshotFromMcPf(result, buses, branches, label = 'Tellegen mult
132
142
  let p = 0;
133
143
  let q = 0;
134
144
  const byTerminal = new Map();
135
- for (const r of rows) {
136
- p += r.power_into_element.re;
137
- q += r.power_into_element.im;
138
- byTerminal.set(r.terminal, r);
145
+ for (const k of rows) {
146
+ p += power[2 * k];
147
+ q += power[2 * k + 1];
148
+ byTerminal.set(names[t.portTerminal[k]], k);
139
149
  }
140
150
  const currents = [];
141
151
  for (const terminal of side ? branch.toTerminals : branch.fromTerminals) {
142
- const row = byTerminal.get(terminal);
143
- if (row)
144
- currents.push({ terminal, amps: magnitude(row.current_into_element) });
152
+ const k = byTerminal.get(terminal);
153
+ if (k !== undefined)
154
+ currents.push({ terminal, amps: Math.hypot(current[2 * k], current[2 * k + 1]) });
145
155
  }
146
156
  return { p: p / 1000, q: q / 1000, currents };
147
157
  });
148
158
  return {
149
159
  ends,
150
- ...loadingOf(branch, ends, elementPorts),
160
+ ...loadingOf(branch, ends, elementPorts, power),
151
161
  primary,
152
- loss: primary ? equipmentLoss(branch, elementPorts) : null
162
+ loss: primary ? equipmentLoss(branch, elementPorts, power) : null
153
163
  };
154
164
  });
165
+ const voltage = t.terminalVoltage;
155
166
  return {
156
167
  kind: 'solved',
157
- label,
158
- iterations: result.iterations,
168
+ label: `${label}: ${mcLoadModelLabel(t.load_model)}; ${mcNetworkScopeLabel(t.network_scope)}`,
169
+ provenance: [
170
+ JSON.stringify({
171
+ load_model: t.load_model ?? null,
172
+ network_scope: t.network_scope ?? null,
173
+ excluded_islands: t.excluded_islands ?? []
174
+ })
175
+ ],
176
+ iterations: t.iterations,
159
177
  buses: buses.map((bus) => {
160
178
  const terminals = voltages.get(bus.id);
161
- return busSnapshot(bus, (t) => terminals?.get(t));
179
+ return busSnapshot(bus, (terminal) => {
180
+ const k = terminals?.get(terminal);
181
+ return k === undefined ? undefined : { re: voltage[2 * k], im: voltage[2 * k + 1] };
182
+ });
162
183
  }),
163
184
  branches: branchSnapshots
164
185
  };
@@ -175,18 +196,18 @@ export function snapshotBusesFromTerminals(ids, voltages, buses) {
175
196
  }));
176
197
  }
177
198
  /** Power summed over a set of port rows, in kW and kvar. */
178
- function portPower(rows) {
199
+ function portPower(rows, power) {
179
200
  let p = 0;
180
201
  let q = 0;
181
- for (const r of rows) {
182
- p += r.power_into_element.re;
183
- q += r.power_into_element.im;
202
+ for (const k of rows) {
203
+ p += power[2 * k];
204
+ q += power[2 * k + 1];
184
205
  }
185
206
  return { p: p / 1000, q: q / 1000 };
186
207
  }
187
208
  /** Both ends' ports are signed into the equipment, so their sum over every bus it connects is what
188
209
  * it consumes. Each bus counts once, however many winding pairs join it. */
189
- function equipmentLoss(branch, rows) {
210
+ function equipmentLoss(branch, rows, power) {
190
211
  if (!branch.inService)
191
212
  return null;
192
213
  const buses = new Set([branch.from, branch.to]);
@@ -199,13 +220,13 @@ function equipmentLoss(branch, rows) {
199
220
  const at = rows(branch, bus);
200
221
  if (!at?.length)
201
222
  return null;
202
- const power = portPower(at);
203
- p += power.p;
204
- q += power.q;
223
+ const sum = portPower(at, power);
224
+ p += sum.p;
225
+ q += sum.q;
205
226
  }
206
227
  return { p, q };
207
228
  }
208
- function loadingOf(branch, ends, rows) {
229
+ function loadingOf(branch, ends, rows, power) {
209
230
  const rating = branch.rating;
210
231
  if (!rating || !ends[0] || !ends[1])
211
232
  return { loading: null, basis: null };
@@ -228,7 +249,7 @@ function loadingOf(branch, ends, rows) {
228
249
  const at = rows(branch, bus);
229
250
  if (!(va > 0) || !at?.length)
230
251
  continue;
231
- const { p, q } = portPower(at);
252
+ const { p, q } = portPower(at, power);
232
253
  const percent = (100 * Math.hypot(p, q) * 1000) / va;
233
254
  if (loading === null || percent > loading)
234
255
  loading = percent;
package/dist/index.d.ts CHANGED
@@ -30,3 +30,6 @@ export type { MultiView, PlacedMultiBus, PlacedMultiEdge } from './multiconducto
30
30
  export type { CaseFileSummary, DistAttachmentKind, DistEdgeKind, DistGraph, DistGraphAttachment, DistGraphBus, DistGraphEdge, DisplayPreview, Formulation, IngestedCase, IngestedDistCase, IngestedJsonDrop, JsonDropClassification, JsonDropKind, CapacityPlanBusWeightJson, CapacityPlanIterationJson, CapacityPlanOutcomeJson, CapacityPlanRatingChangeJson, CapacityPlanResultSummaryJson, CapacityPlanSpecJson, ImplicitObjectiveJson, SensTarget, Topology, TopologyBranch, TopologyBus } from '@tellegen/engine';
31
31
  export { default as McResults } from './components/McResults.svelte';
32
32
  export { default as McWorkbench } from './components/McWorkbench.svelte';
33
+ export { default as McPfSettings } from './components/McPfSettings.svelte';
34
+ export { default as McModelQualification } from './components/McModelQualification.svelte';
35
+ export { default as McNetworkScope } from './components/McNetworkScope.svelte';
package/dist/index.js CHANGED
@@ -19,3 +19,6 @@ export { caseDeltas, caseRatings, displayMetaFor, displaySeriesFor } from './dis
19
19
  export { DEFAULT_FORMULATION, FORMULATIONS, BrowserStudy, Study, capabilities, createStudy, errorText, formatOf, classifyJson, ingestJsonDrop, ingestCase, isDisplayFile, isPermanentEngineFailure, parseDisplay, solveModule } from '@tellegen/engine';
20
20
  export { default as McResults } from './components/McResults.svelte';
21
21
  export { default as McWorkbench } from './components/McWorkbench.svelte';
22
+ export { default as McPfSettings } from './components/McPfSettings.svelte';
23
+ export { default as McModelQualification } from './components/McModelQualification.svelte';
24
+ export { default as McNetworkScope } from './components/McNetworkScope.svelte';
@@ -1,5 +1,13 @@
1
1
  export type MapBounds = [[number, number], [number, number]];
2
+ /** Whether a point can be framed on a Web Mercator map: finite, longitude in
3
+ * [-180, 180], latitude inside the projection's ±85.05° limit. */
4
+ export declare function isMapPoint([lon, lat]: readonly [number, number]): boolean;
2
5
  /** Fold finite Web Mercator coordinates into map bounds. Invalid points are
3
6
  * ignored so one malformed row cannot hide an otherwise usable network, and a
4
7
  * single surviving point still yields a box `fitBounds` can frame. */
5
8
  export declare function foldMapBounds(points: Iterable<readonly [number, number]>): MapBounds | null;
9
+ /** The box around points already checked, or `null` when there are none. An
10
+ * axis that collapsed to a single value is padded so `fitBounds` can frame
11
+ * it. Longitudes are taken as they are, so an unwrapped route may give a west
12
+ * edge below -180 or an east edge above 180. */
13
+ export declare function spanBounds(points: Iterable<readonly [number, number]>): MapBounds | null;
@@ -2,23 +2,36 @@
2
2
  * zero-span box makes `fitBounds` divide by zero, clamp to the map's maximum
3
3
  * zoom, and slam the camera onto one point instead of framing it. */
4
4
  const DEGENERATE_PAD_DEGREES = 0.005;
5
+ /** Whether a point can be framed on a Web Mercator map: finite, longitude in
6
+ * [-180, 180], latitude inside the projection's ±85.05° limit. */
7
+ export function isMapPoint([lon, lat]) {
8
+ return (Number.isFinite(lon) &&
9
+ Number.isFinite(lat) &&
10
+ lon >= -180 &&
11
+ lon <= 180 &&
12
+ lat >= -85.05 &&
13
+ lat <= 85.05);
14
+ }
5
15
  /** Fold finite Web Mercator coordinates into map bounds. Invalid points are
6
16
  * ignored so one malformed row cannot hide an otherwise usable network, and a
7
17
  * single surviving point still yields a box `fitBounds` can frame. */
8
18
  export function foldMapBounds(points) {
19
+ const kept = [];
20
+ for (const point of points)
21
+ if (isMapPoint(point))
22
+ kept.push(point);
23
+ return spanBounds(kept);
24
+ }
25
+ /** The box around points already checked, or `null` when there are none. An
26
+ * axis that collapsed to a single value is padded so `fitBounds` can frame
27
+ * it. Longitudes are taken as they are, so an unwrapped route may give a west
28
+ * edge below -180 or an east edge above 180. */
29
+ export function spanBounds(points) {
9
30
  let minLon = Infinity;
10
31
  let minLat = Infinity;
11
32
  let maxLon = -Infinity;
12
33
  let maxLat = -Infinity;
13
34
  for (const [lon, lat] of points) {
14
- if (!Number.isFinite(lon) ||
15
- !Number.isFinite(lat) ||
16
- lon < -180 ||
17
- lon > 180 ||
18
- lat < -85.05 ||
19
- lat > 85.05) {
20
- continue;
21
- }
22
35
  minLon = Math.min(minLon, lon);
23
36
  minLat = Math.min(minLat, lat);
24
37
  maxLon = Math.max(maxLon, lon);
@@ -110,6 +110,43 @@ function withRoutes(view, layer) {
110
110
  return view;
111
111
  const routes = new Map();
112
112
  const same = (a, b) => typeof a === 'string' && a.toLowerCase() === b.toLowerCase();
113
+ // Index the edges once by lowercased id and by lowercased directed endpoint
114
+ // pair. A city case carries tens of thousands of routes and edges, so a scan
115
+ // of every edge per feature is quadratic.
116
+ const byId = new Map();
117
+ const byEnds = new Map();
118
+ for (const edge of view.edges) {
119
+ const id = edge.id.toLowerCase();
120
+ const from = edge.from.toLowerCase();
121
+ const to = edge.to.toLowerCase();
122
+ const withId = byId.get(id);
123
+ if (withId)
124
+ withId.push(edge);
125
+ else
126
+ byId.set(id, [edge]);
127
+ let fromEnds = byEnds.get(from);
128
+ if (!fromEnds)
129
+ byEnds.set(from, (fromEnds = new Map()));
130
+ const between = fromEnds.get(to);
131
+ if (between)
132
+ between.push(edge);
133
+ else
134
+ fromEnds.set(to, [edge]);
135
+ }
136
+ const directed = (from, to) => byEnds.get(from)?.get(to) ?? [];
137
+ // The edges `same` would match: by id when the feature names one, otherwise
138
+ // by its endpoints in either direction. A self loop's two directions are one
139
+ // set, so it is not counted twice.
140
+ const matching = (id, from, to) => {
141
+ if (id !== undefined)
142
+ return typeof id === 'string' ? (byId.get(id.toLowerCase()) ?? []) : [];
143
+ if (typeof from !== 'string' || typeof to !== 'string')
144
+ return [];
145
+ const f = from.toLowerCase();
146
+ const t = to.toLowerCase();
147
+ const forward = directed(f, t);
148
+ return f === t ? forward : [...forward, ...directed(t, f)];
149
+ };
113
150
  for (const feature of features) {
114
151
  const props = feature?.properties;
115
152
  const geometry = feature?.geometry;
@@ -126,10 +163,7 @@ function withRoutes(view, layer) {
126
163
  const from = props.from ?? props.bus_from;
127
164
  const to = props.to ?? props.bus_to;
128
165
  const id = props.id ?? props.uid;
129
- const matches = view.edges.filter((edge) => id !== undefined
130
- ? same(id, edge.id)
131
- : (same(from, edge.from) && same(to, edge.to)) ||
132
- (same(from, edge.to) && same(to, edge.from)));
166
+ const matches = matching(id, from, to);
133
167
  if (matches.length !== 1)
134
168
  continue;
135
169
  const edge = matches[0];
@@ -1,5 +1,5 @@
1
1
  import type { CaseSummary, DemandDeltas, Network, NetworkBranch, NetworkBus, SensitivityColumn, Solution, SolveIteration } from './api.js';
2
- import { type StudyView, type BranchRatingDeltas, type BrowserMcPfSession, type CaseFileSummary, type DistGraph, type Formulation, type IngestedDistCase, type McPfCallTiming, type McPfResult, type McPfSummary, type McLoadBranchState, type McLoadPowerEdit, type McStudySnapshot, type SensTarget, type Topology } from '@tellegen/engine';
2
+ import { type StudyView, type BranchRatingDeltas, type BrowserMcPfSession, type CaseFileSummary, type DistGraph, type Formulation, type IngestedDistCase, type McPfCallTiming, type McPfOptions, type McPfResult, type McPfResultTables, type McPfSummary, type McLoadBranchState, type McLoadPowerEdit, type McModuleInput, type McStudySnapshot, type SensTarget, type Topology } from '@tellegen/engine';
3
3
  import type { MultiView } from './multiconductor.js';
4
4
  import { DiagnosticLog } from './log.svelte.js';
5
5
  import { type GraphInputs } from './diagram/graph-inputs.js';
@@ -95,6 +95,11 @@ export declare class LocalCase {
95
95
  coordsKind: CoordsKind | undefined;
96
96
  /** Map geometry when the file carried or received coordinates. */
97
97
  view: LocalView | null;
98
+ /** Buses a generated grid marks as line taps. The map draws no marker for
99
+ * them; the bus picker and the selection still reach them. */
100
+ readonly tapBuses: ReadonlySet<number>;
101
+ /** `view.buses` less the line taps: the buses the map draws as markers. */
102
+ readonly markerBuses: NetworkBus[];
98
103
  syntheticCenter: {
99
104
  lon: number;
100
105
  lat: number;
@@ -181,9 +186,10 @@ export declare class CaseState {
181
186
  }
182
187
  /** A case the solver can run: a server-backed case or a browser-parsed local case. */
183
188
  export type SolvableCase = CaseState | LocalCase;
184
- /** The multiconductor ingest payload without the graph: the summary counts,
185
- * connected load/generation, coordinate provenance, and diagnostics. */
186
- export type MultiCaseSummary = Omit<IngestedDistCase, 'graph'>;
189
+ /** The multiconductor ingest payload without the graph or the retained
190
+ * input: the summary counts, connected load/generation, coordinate
191
+ * provenance, and diagnostics. */
192
+ export type MultiCaseSummary = Omit<IngestedDistCase, 'graph' | 'input'>;
187
193
  /** Geographic coordinates use the map; other positions use the diagram canvas. */
188
194
  export type MultiCoordsKind = 'geographic' | 'planar' | 'synthetic';
189
195
  /** Where one interactive load edit spent its time, in milliseconds: engine
@@ -197,7 +203,10 @@ export declare class MulticonductorCase {
197
203
  readonly id: string;
198
204
  readonly label: string;
199
205
  readonly fileName: string;
200
- moduleJson: string | null;
206
+ /** The electrical input calculations start from: the module the engine
207
+ * keeps from the import, or PowerIO IR text (a saved or re-placed module).
208
+ * A kept module is freed when it is replaced or the case is removed. */
209
+ mcInput: McModuleInput | null;
201
210
  geoLayer: string | null;
202
211
  geoWarnings: string[];
203
212
  /** Summary of the displayed operating point: from the live session after
@@ -208,6 +217,15 @@ export declare class MulticonductorCase {
208
217
  mcFullResult: McPfResult | null;
209
218
  /** Latency breakdown of the latest load-edit solve. */
210
219
  mcTiming: McEditTiming | null;
220
+ /** Explicit selection for the next solve. The graph and module retain every bus. */
221
+ mcEnergizedOnly: boolean;
222
+ /** Successful explicit solve to reveal in Studies; load edits do not advance it. */
223
+ mcCompletedSolveSeq: number;
224
+ /** Detected on import, revalidated when preparing a solve; never removed from the graph. */
225
+ mcExcludedIslands: import('@tellegen/engine').McExcludedIsland[];
226
+ mcPreparedScope: import('@tellegen/engine').McNetworkScope | null;
227
+ /** Selected settings for the next explicit solve; results carry their own effective settings. */
228
+ mcOptions: McPfOptions;
211
229
  mcSnapshot: McStudySnapshot | null;
212
230
  mcSession: BrowserMcPfSession | null;
213
231
  mcLoadBranches: McLoadBranchState[];
@@ -248,9 +266,10 @@ export declare class MulticonductorCase {
248
266
  /** Bumped to ask the views to bring the current selection into view again (an issue clicked
249
267
  * that is already selected). */
250
268
  revealSeq: number;
251
- /** The full operating point read for whole-network views (overview, schematic); null until read,
252
- * and cleared when a large case's operating point changes. */
253
- overviewResult: McPfResult | null;
269
+ /** The full operating point read for whole-network views (overview, schematic): a stored
270
+ * result, or a live session's as columns. Null until read, and cleared when a large case's
271
+ * operating point changes. */
272
+ overviewResult: McPfResult | McPfResultTables | null;
254
273
  overviewLoading: boolean;
255
274
  /** The graph's buses and branches as the overview and the diagram read them: rebuilt when the
256
275
  * graph is replaced, never after a solve. Both views read this one copy. */
@@ -270,11 +289,17 @@ export declare class MulticonductorCase {
270
289
  label: string;
271
290
  fileName: string;
272
291
  summary: MultiCaseSummary;
292
+ /** The module the engine kept from the import, in place of the
293
+ * summary's `module_json`. */
294
+ input?: McModuleInput | null;
273
295
  graph: DistGraph;
274
296
  coordsKind: MultiCoordsKind;
275
297
  view?: MultiView | null;
276
298
  });
277
299
  get mcPfReason(): string | null;
300
+ /** Import detects scope; only a solve action opts into the selection. */
301
+ get mcRequiresEnergizedSolve(): boolean;
302
+ get mcSolveLabel(): string;
278
303
  get mcPfSupported(): boolean;
279
304
  /** Whether the case is placed and ready to render. */
280
305
  get placed(): boolean;
@@ -1,3 +1,5 @@
1
+ import { DEFAULT_MC_PF_OPTIONS, summarizeMcExcludedIslands } from '@tellegen/engine';
2
+ import { tapBusIds, withoutTaps } from './bus-kinds.js';
1
3
  import { DEFAULT_FORMULATION } from '@tellegen/engine';
2
4
  import { DiagnosticLog, errorEntry } from './log.svelte.js';
3
5
  import { graphInputs } from './diagram/graph-inputs.js';
@@ -26,6 +28,11 @@ export class LocalCase {
26
28
  coordsKind = $state.raw(undefined);
27
29
  /** Map geometry when the file carried or received coordinates. */
28
30
  view = $state.raw(null);
31
+ /** Buses a generated grid marks as line taps. The map draws no marker for
32
+ * them; the bus picker and the selection still reach them. */
33
+ tapBuses = $derived(tapBusIds(this.studyInputJson));
34
+ /** `view.buses` less the line taps: the buses the map draws as markers. */
35
+ markerBuses = $derived(withoutTaps(this.view?.buses ?? [], this.tapBuses));
29
36
  syntheticCenter = $state.raw(undefined);
30
37
  diagram = $state.raw(null);
31
38
  displayMode = $state('geographic');
@@ -127,7 +134,10 @@ export class MulticonductorCase {
127
134
  id;
128
135
  label;
129
136
  fileName;
130
- moduleJson = $state.raw(null);
137
+ /** The electrical input calculations start from: the module the engine
138
+ * keeps from the import, or PowerIO IR text (a saved or re-placed module).
139
+ * A kept module is freed when it is replaced or the case is removed. */
140
+ mcInput = $state.raw(null);
131
141
  geoLayer = $state.raw(null);
132
142
  geoWarnings = $state.raw([]);
133
143
  /** Summary of the displayed operating point: from the live session after
@@ -138,6 +148,15 @@ export class MulticonductorCase {
138
148
  mcFullResult = $state.raw(null);
139
149
  /** Latency breakdown of the latest load-edit solve. */
140
150
  mcTiming = $state.raw(null);
151
+ /** Explicit selection for the next solve. The graph and module retain every bus. */
152
+ mcEnergizedOnly = $state(false);
153
+ /** Successful explicit solve to reveal in Studies; load edits do not advance it. */
154
+ mcCompletedSolveSeq = $state(0);
155
+ /** Detected on import, revalidated when preparing a solve; never removed from the graph. */
156
+ mcExcludedIslands = $state.raw([]);
157
+ mcPreparedScope = $state.raw(null);
158
+ /** Selected settings for the next explicit solve; results carry their own effective settings. */
159
+ mcOptions = $state({ ...DEFAULT_MC_PF_OPTIONS });
141
160
  mcSnapshot = $state.raw(null);
142
161
  mcSession = $state.raw(null);
143
162
  mcLoadBranches = $state.raw([]);
@@ -172,8 +191,9 @@ export class MulticonductorCase {
172
191
  /** Bumped to ask the views to bring the current selection into view again (an issue clicked
173
192
  * that is already selected). */
174
193
  revealSeq = $state(0);
175
- /** The full operating point read for whole-network views (overview, schematic); null until read,
176
- * and cleared when a large case's operating point changes. */
194
+ /** The full operating point read for whole-network views (overview, schematic): a stored
195
+ * result, or a live session's as columns. Null until read, and cleared when a large case's
196
+ * operating point changes. */
177
197
  overviewResult = $state.raw(null);
178
198
  overviewLoading = $state(false);
179
199
  /** The graph's buses and branches as the overview and the diagram read them: rebuilt when the
@@ -196,14 +216,17 @@ export class MulticonductorCase {
196
216
  this.label = init.label;
197
217
  this.fileName = init.fileName;
198
218
  this.summary = init.summary;
199
- this.moduleJson = init.summary.module_json ?? null;
219
+ this.mcInput = init.input ?? init.summary.module_json ?? null;
220
+ this.mcEnergizedOnly = init.summary.mc_energized_only ?? false;
221
+ this.mcExcludedIslands = init.summary.mc_excluded_islands ?? [];
222
+ this.mcPreparedScope = summarizeMcExcludedIslands(this.mcExcludedIslands);
200
223
  this.geoLayer = init.summary.geo_layer ?? null;
201
224
  this.graph = init.graph;
202
225
  this.coordsKind = init.coordsKind;
203
226
  this.view = init.view ?? null;
204
227
  }
205
228
  get mcPfReason() {
206
- if (!this.moduleJson)
229
+ if (!this.mcInput)
207
230
  return 'This case has no retained electrical input';
208
231
  if (this.summary?.mc_pf_enabled !== true)
209
232
  return 'AC power flow is unavailable in this build';
@@ -211,6 +234,13 @@ export class MulticonductorCase {
211
234
  this.summary.mc_pf_unavailable_reason ??
212
235
  (this.summary.mc_pf_supported !== true ? 'AC power flow is unavailable for this case' : null));
213
236
  }
237
+ /** Import detects scope; only a solve action opts into the selection. */
238
+ get mcRequiresEnergizedSolve() {
239
+ return this.mcExcludedIslands.length > 0;
240
+ }
241
+ get mcSolveLabel() {
242
+ return this.mcRequiresEnergizedSolve ? 'Solve energized network' : 'Run power flow';
243
+ }
214
244
  get mcPfSupported() {
215
245
  return this.mcPfReason === null;
216
246
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tellegen/svelte",
3
- "version": "0.4.7",
3
+ "version": "0.5.1",
4
4
  "description": "Svelte components for tellegen power system maps, local case files, and browser solves.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -67,7 +67,7 @@
67
67
  "pack:dry-run": "npm pack --dry-run --cache ../../node_modules/.cache/npm-pack"
68
68
  },
69
69
  "dependencies": {
70
- "@tellegen/engine": "^0.4.6"
70
+ "@tellegen/engine": "^0.5.1"
71
71
  },
72
72
  "peerDependencies": {
73
73
  "@deck.gl/core": "^9.4.0",
@@ -77,16 +77,16 @@
77
77
  "svelte": ">=5.30 <6"
78
78
  },
79
79
  "devDependencies": {
80
- "@deck.gl/core": "^9.4.0",
81
- "@deck.gl/layers": "^9.4.0",
82
- "@deck.gl/maplibre": "^9.4.0",
83
- "@sveltejs/package": "^3.0.1",
80
+ "@deck.gl/core": "^9.4.1",
81
+ "@deck.gl/layers": "^9.4.1",
82
+ "@deck.gl/maplibre": "^9.4.1",
83
+ "@sveltejs/package": "^3.0.2",
84
84
  "@sveltejs/vite-plugin-svelte": "^7.3.1",
85
- "maplibre-gl": "^6.15.0",
86
- "svelte": "^5.57.2",
87
- "svelte-check": "^4.7.7",
85
+ "maplibre-gl": "^6.16.0",
86
+ "svelte": "^5.57.3",
87
+ "svelte-check": "^4.7.8",
88
88
  "typescript": "^6.0.3",
89
89
  "vite": "^8.3.1",
90
- "vitest": "^5.0.2"
90
+ "vitest": "^5.1.0"
91
91
  }
92
92
  }