1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
17
18use powerio_core::ComponentId;
19use serde::{Deserialize, Serialize};
20use serde_json::Value;
21
22use crate::geo::{GeoMeta, Location};
23use crate::{Error, Result};
24
25pub type Extras = BTreeMap<String, Value>;
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
31#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
32#[non_exhaustive]
33pub struct ComponentAlias {
34 pub value: String,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub alias_type: Option<String>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
41#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
42#[non_exhaustive]
43pub struct ExternalIdentifier {
44 pub value: String,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub authority: Option<String>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
51#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
52#[non_exhaustive]
53pub struct ComponentMetadata {
54 pub component: ComponentId,
55 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub name: Option<String>,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub equipment_container: Option<ComponentId>,
61 #[serde(default)]
62 pub aliases: Vec<ComponentAlias>,
63 #[serde(default)]
64 pub external_identifiers: Vec<ExternalIdentifier>,
65 #[serde(default)]
66 pub properties: BTreeMap<String, String>,
67 #[serde(default)]
68 pub fictitious: bool,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
73#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
74#[non_exhaustive]
75pub struct Substation {
76 pub component: ComponentId,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub country: Option<String>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub operator: Option<String>,
81 #[serde(default)]
82 pub geographical_tags: Vec<String>,
83}
84
85#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
87#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
88#[serde(rename_all = "snake_case")]
89#[non_exhaustive]
90pub enum TopologyKind {
91 BusBreaker,
92 NodeBreaker,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
97#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
98#[non_exhaustive]
99pub struct VoltageLevel {
100 pub component: ComponentId,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub substation: Option<ComponentId>,
103 pub nominal_kv: f64,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub low_voltage_limit_kv: Option<f64>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub high_voltage_limit_kv: Option<f64>,
108 pub topology_kind: TopologyKind,
109 #[serde(default)]
111 pub buses: Vec<BusId>,
112}
113
114#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
116#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
117#[non_exhaustive]
118pub struct ConnectivityNode {
119 pub component: ComponentId,
120 pub voltage_level: ComponentId,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub node_number: Option<i32>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub calculated_bus: Option<BusId>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
133#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
134#[non_exhaustive]
135pub struct BusBreakerBus {
136 pub component: ComponentId,
137 pub voltage_level: ComponentId,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub calculated_bus: Option<BusId>,
140 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub voltage_kv: Option<f64>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub angle_degrees: Option<f64>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
152#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
153#[non_exhaustive]
154pub struct CalculatedBus {
155 pub voltage_level: ComponentId,
156 pub calculated_bus: BusId,
157 pub nodes: Vec<ComponentId>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub voltage_kv: Option<f64>,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub angle_degrees: Option<f64>,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
166#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
167#[non_exhaustive]
168pub struct BusbarSection {
169 pub component: ComponentId,
170 pub voltage_level: ComponentId,
171 pub node: ComponentId,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
179#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
180#[non_exhaustive]
181pub struct Junction {
182 pub component: ComponentId,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
187#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
188#[non_exhaustive]
189pub struct Terminal {
190 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub component: Option<ComponentId>,
193 pub equipment: ComponentId,
194 pub terminal: u8,
196 pub voltage_level: ComponentId,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub bus: Option<ComponentId>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub connectable_bus: Option<ComponentId>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub node: Option<ComponentId>,
203 pub connected: bool,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub active_power_mw: Option<f64>,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
209 pub reactive_power_mvar: Option<f64>,
210}
211
212#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
214#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
215#[serde(rename_all = "snake_case")]
216#[non_exhaustive]
217pub enum SwitchKind {
218 Breaker,
219 Disconnector,
220 LoadBreakSwitch,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
225#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
226#[serde(tag = "kind", content = "component", rename_all = "snake_case")]
227#[non_exhaustive]
228pub enum TopologyEndpoint {
229 Bus(ComponentId),
230 Node(ComponentId),
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
235#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
236#[non_exhaustive]
237pub struct TopologySwitch {
238 pub component: ComponentId,
239 pub voltage_level: ComponentId,
240 pub kind: SwitchKind,
241 pub endpoint1: TopologyEndpoint,
242 pub endpoint2: TopologyEndpoint,
243 pub open: bool,
244 #[serde(default)]
245 pub retained: bool,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
250#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
251#[non_exhaustive]
252pub struct InternalConnection {
253 pub voltage_level: ComponentId,
254 pub node1: ComponentId,
255 pub node2: ComponentId,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
260#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
261#[non_exhaustive]
262pub struct TerminalReference {
263 pub equipment: ComponentId,
264 pub terminal: u8,
265}
266
267#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
269#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
270#[serde(rename_all = "snake_case")]
271#[non_exhaustive]
272pub enum DcConverterOperatingMode {
273 Bipolar,
274 MonopolarGroundReturn,
275 MonopolarMetallicReturn,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
280#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
281#[non_exhaustive]
282pub struct DcConverterUnit {
283 pub component: ComponentId,
284 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub substation: Option<ComponentId>,
286 pub operation_mode: DcConverterOperatingMode,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
291#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
292#[non_exhaustive]
293pub struct DcTopologicalNode {
294 pub component: ComponentId,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub dc_converter_unit: Option<ComponentId>,
297}
298
299#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
301#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
302#[serde(rename_all = "snake_case")]
303#[non_exhaustive]
304pub enum DcPolarity {
305 Positive,
306 Middle,
307 Negative,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
312#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
313#[non_exhaustive]
314pub struct DcTerminal {
315 #[serde(default, skip_serializing_if = "Option::is_none")]
317 pub component: Option<ComponentId>,
318 #[serde(default, skip_serializing_if = "Option::is_none")]
319 pub sequence_number: Option<u32>,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
321 pub dc_node: Option<ComponentId>,
322 #[serde(default, skip_serializing_if = "Option::is_none")]
323 pub dc_topological_node: Option<ComponentId>,
324 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub polarity: Option<DcPolarity>,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
327 pub connected: Option<bool>,
328 #[serde(default, skip_serializing_if = "Option::is_none")]
330 pub active_power_mw: Option<f64>,
331 #[serde(default, skip_serializing_if = "Option::is_none")]
333 pub current_a: Option<f64>,
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
338#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
339#[non_exhaustive]
340pub struct DcNode {
341 pub component: ComponentId,
342 #[serde(default, skip_serializing_if = "Option::is_none")]
344 pub nominal_voltage_kv: Option<f64>,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
346 pub dc_converter_unit: Option<ComponentId>,
347 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub dc_topological_node: Option<ComponentId>,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub voltage_kv: Option<f64>,
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
355#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
356#[non_exhaustive]
357pub struct DcGround {
358 pub component: ComponentId,
359 #[serde(default, skip_serializing_if = "Option::is_none")]
360 pub equipment_container: Option<ComponentId>,
361 pub dc_terminal: DcTerminal,
362 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub rated_dc_voltage_kv: Option<f64>,
364 #[serde(default, skip_serializing_if = "Option::is_none")]
365 pub resistance_ohm: Option<f64>,
366 #[serde(default, skip_serializing_if = "Option::is_none")]
367 pub inductance_h: Option<f64>,
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
372#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
373#[non_exhaustive]
374pub struct DcBusbar {
375 pub component: ComponentId,
376 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub equipment_container: Option<ComponentId>,
378 pub dc_terminal: DcTerminal,
379 #[serde(default, skip_serializing_if = "Option::is_none")]
380 pub rated_dc_voltage_kv: Option<f64>,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
385#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
386#[non_exhaustive]
387pub struct DcLine {
388 pub component: ComponentId,
389 #[serde(default, skip_serializing_if = "Option::is_none")]
390 pub equipment_container: Option<ComponentId>,
391 pub dc_terminal1: DcTerminal,
392 pub dc_terminal2: DcTerminal,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
394 pub rated_dc_voltage_kv: Option<f64>,
395 #[serde(default, skip_serializing_if = "Option::is_none")]
396 pub resistance_ohm: Option<f64>,
397 #[serde(default, skip_serializing_if = "Option::is_none")]
398 pub inductance_h: Option<f64>,
399 #[serde(default, skip_serializing_if = "Option::is_none")]
400 pub capacitance_f: Option<f64>,
401 #[serde(default, skip_serializing_if = "Option::is_none")]
402 pub length_km: Option<f64>,
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
407#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
408#[non_exhaustive]
409pub struct DcSeriesDevice {
410 pub component: ComponentId,
411 #[serde(default, skip_serializing_if = "Option::is_none")]
412 pub equipment_container: Option<ComponentId>,
413 pub dc_terminal1: DcTerminal,
414 pub dc_terminal2: DcTerminal,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
416 pub rated_dc_voltage_kv: Option<f64>,
417 #[serde(default, skip_serializing_if = "Option::is_none")]
418 pub resistance_ohm: Option<f64>,
419 #[serde(default, skip_serializing_if = "Option::is_none")]
420 pub inductance_h: Option<f64>,
421}
422
423#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
425#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
426#[serde(rename_all = "snake_case")]
427#[non_exhaustive]
428pub enum DcSwitchKind {
429 Switch,
430 Breaker,
431 Disconnector,
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
436#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
437#[non_exhaustive]
438pub struct DcSwitch {
439 pub component: ComponentId,
440 #[serde(default, skip_serializing_if = "Option::is_none")]
441 pub equipment_container: Option<ComponentId>,
442 pub dc_terminal1: DcTerminal,
443 pub dc_terminal2: DcTerminal,
444 pub kind: DcSwitchKind,
445 #[serde(default, skip_serializing_if = "Option::is_none")]
446 pub rated_dc_voltage_kv: Option<f64>,
447 #[serde(default, skip_serializing_if = "Option::is_none")]
448 pub open: Option<bool>,
449 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub resistance_ohm: Option<f64>,
451}
452
453#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
455#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
456#[serde(rename_all = "snake_case")]
457#[non_exhaustive]
458pub enum AcDcConverterControlMode {
459 ActivePowerAtPcc,
461 DcVoltage,
463 DcCurrent,
465 ActivePowerAtPccAndDcVoltageDroopCurve,
468 ActivePowerAtPccAndDcVoltageDroop,
470 ActivePowerAtPccAndDcVoltageDroopWithCompensation,
472 ActivePowerAtPccAndDcVoltageDroopPilot,
474}
475
476#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
478#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
479#[non_exhaustive]
480pub struct DroopCurveSegment {
481 pub minimum_voltage_kv: f64,
482 pub maximum_voltage_kv: f64,
483 pub k: f64,
484}
485
486#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
488#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
489#[non_exhaustive]
490pub struct DroopCurve {
491 pub segments: Vec<DroopCurveSegment>,
492}
493
494#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
496#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
497#[non_exhaustive]
498pub struct ReactiveCapabilityCurvePoint {
499 pub active_power_mw: f64,
500 pub minimum_reactive_power_mvar: f64,
501 pub maximum_reactive_power_mvar: f64,
502 #[serde(default)]
503 pub properties: BTreeMap<String, String>,
504}
505
506#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
508#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
509#[serde(rename_all = "snake_case")]
510#[non_exhaustive]
511pub enum CurveStyle {
512 ConstantYValue,
514 StraightLineYValues,
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
520#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
521#[non_exhaustive]
522pub struct ReactiveCapabilityCurve {
523 pub curve_style: CurveStyle,
524 #[serde(default)]
525 pub properties: BTreeMap<String, String>,
526 pub points: Vec<ReactiveCapabilityCurvePoint>,
527}
528
529#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
531#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
532#[non_exhaustive]
533pub struct MinMaxReactiveLimits {
534 pub minimum_reactive_power_mvar: f64,
535 pub maximum_reactive_power_mvar: f64,
536 #[serde(default)]
537 pub properties: BTreeMap<String, String>,
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
542#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
543#[serde(tag = "kind", content = "limits", rename_all = "snake_case")]
544#[non_exhaustive]
545pub enum ReactiveLimits {
546 MinMax(MinMaxReactiveLimits),
547 CapabilityCurve(ReactiveCapabilityCurve),
548}
549
550#[allow(clippy::float_cmp, clippy::manual_midpoint)]
554pub(crate) fn calc_reactive_limits_at_active_power(
555 owner: &str,
556 limits: &ReactiveLimits,
557 active_power_mw: f64,
558) -> std::result::Result<(f64, f64), String> {
559 match limits {
560 ReactiveLimits::MinMax(limits) => {
561 if limits.minimum_reactive_power_mvar > limits.maximum_reactive_power_mvar {
562 return Err(format!("{owner} has minQ greater than maxQ"));
563 }
564 Ok((
565 limits.minimum_reactive_power_mvar,
566 limits.maximum_reactive_power_mvar,
567 ))
568 }
569 ReactiveLimits::CapabilityCurve(curve) => {
570 if curve.points.len() < 2 {
571 return Err(format!(
572 "{owner} reactiveCapabilityCurve has fewer than two points"
573 ));
574 }
575 if !active_power_mw.is_finite() {
576 return Err(format!(
577 "{owner} reactiveCapabilityCurve cannot be evaluated at a nonfinite active power"
578 ));
579 }
580 let mut points = curve.points.iter().collect::<Vec<_>>();
581 points
582 .sort_by(|first, second| first.active_power_mw.total_cmp(&second.active_power_mw));
583 for pair in points.windows(2) {
584 if pair[0].active_power_mw == pair[1].active_power_mw {
585 return Err(format!(
586 "{owner} reactiveCapabilityCurve has duplicate active power points"
587 ));
588 }
589 }
590 let (first, second) = if active_power_mw <= points[0].active_power_mw {
591 (points[0], points[0])
592 } else if active_power_mw >= points[points.len() - 1].active_power_mw {
593 (points[points.len() - 1], points[points.len() - 1])
594 } else {
595 let upper = points.partition_point(|point| point.active_power_mw < active_power_mw);
596 (points[upper - 1], points[upper])
597 };
598 let (minimum, maximum) = if std::ptr::eq(first, second) {
599 (
600 first.minimum_reactive_power_mvar,
601 first.maximum_reactive_power_mvar,
602 )
603 } else {
604 let fraction = (active_power_mw - first.active_power_mw)
605 / (second.active_power_mw - first.active_power_mw);
606 (
607 first.minimum_reactive_power_mvar
608 + fraction
609 * (second.minimum_reactive_power_mvar
610 - first.minimum_reactive_power_mvar),
611 first.maximum_reactive_power_mvar
612 + fraction
613 * (second.maximum_reactive_power_mvar
614 - first.maximum_reactive_power_mvar),
615 )
616 };
617 if minimum <= maximum {
618 Ok((minimum, maximum))
619 } else {
620 let midpoint = (minimum + maximum) / 2.0;
621 Ok((midpoint, midpoint))
622 }
623 }
624 }
625}
626
627#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
630#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
631#[non_exhaustive]
632pub struct EquipmentReactiveLimits {
633 pub equipment: ComponentId,
634 pub limits: ReactiveLimits,
635}
636
637#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
639#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
640#[non_exhaustive]
641pub struct BoundaryLineGeneration {
642 pub voltage_regulation_on: bool,
643 #[serde(default, skip_serializing_if = "Option::is_none")]
644 pub minimum_active_power_mw: Option<f64>,
645 #[serde(default, skip_serializing_if = "Option::is_none")]
646 pub maximum_active_power_mw: Option<f64>,
647 #[serde(default, skip_serializing_if = "Option::is_none")]
648 pub target_active_power_mw: Option<f64>,
649 #[serde(default, skip_serializing_if = "Option::is_none")]
650 pub target_reactive_power_mvar: Option<f64>,
651 #[serde(default, skip_serializing_if = "Option::is_none")]
652 pub target_voltage_kv: Option<f64>,
653 #[serde(default, skip_serializing_if = "Option::is_none")]
654 pub reactive_limits: Option<ReactiveLimits>,
655}
656
657#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
659#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
660#[non_exhaustive]
661pub struct BoundaryLine {
662 pub component: ComponentId,
663 pub voltage_level: ComponentId,
664 pub active_power_setpoint_mw: f64,
665 pub reactive_power_setpoint_mvar: f64,
666 pub resistance_ohm: f64,
667 pub reactance_ohm: f64,
668 pub conductance_siemens: f64,
669 pub susceptance_siemens: f64,
670 #[serde(default, skip_serializing_if = "Option::is_none")]
671 pub pairing_key: Option<String>,
672 #[serde(default, skip_serializing_if = "Option::is_none")]
673 pub generation: Option<BoundaryLineGeneration>,
674 #[serde(default, skip_serializing_if = "Option::is_none")]
675 pub calculation_load: Option<ComponentId>,
676 #[serde(default, skip_serializing_if = "Option::is_none")]
677 pub calculation_generator: Option<ComponentId>,
678}
679
680#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
682#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
683#[non_exhaustive]
684pub struct TieLine {
685 pub component: ComponentId,
686 pub boundary_line1: ComponentId,
687 pub boundary_line2: ComponentId,
688 #[serde(default, skip_serializing_if = "Option::is_none")]
689 pub calculation_branch: Option<ComponentId>,
690}
691
692#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
694#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
695#[non_exhaustive]
696pub struct VoltageSourceConverter {
697 pub component: ComponentId,
698 #[serde(default, skip_serializing_if = "Option::is_none")]
699 pub dc_converter_unit: Option<ComponentId>,
700 pub dc_terminal1: DcTerminal,
701 pub dc_terminal2: DcTerminal,
702 #[serde(default, skip_serializing_if = "Option::is_none")]
703 pub base_apparent_power_mva: Option<f64>,
704 #[serde(default, skip_serializing_if = "Option::is_none")]
705 pub minimum_active_power_mw: Option<f64>,
706 #[serde(default, skip_serializing_if = "Option::is_none")]
707 pub maximum_active_power_mw: Option<f64>,
708 #[serde(default, skip_serializing_if = "Option::is_none")]
709 pub minimum_dc_voltage_kv: Option<f64>,
710 #[serde(default, skip_serializing_if = "Option::is_none")]
711 pub maximum_dc_voltage_kv: Option<f64>,
712 #[serde(default, skip_serializing_if = "Option::is_none")]
713 pub rated_dc_voltage_kv: Option<f64>,
714 #[serde(default, skip_serializing_if = "Option::is_none")]
715 pub valve_u0_kv: Option<f64>,
716 #[serde(default, skip_serializing_if = "Option::is_none")]
717 pub number_of_valves: Option<u32>,
718 #[serde(default, skip_serializing_if = "Option::is_none")]
719 pub idle_loss_mw: Option<f64>,
720 #[serde(default, skip_serializing_if = "Option::is_none")]
721 pub switching_loss_mw_per_ampere: Option<f64>,
722 #[serde(default, skip_serializing_if = "Option::is_none")]
723 pub resistive_loss_ohm: Option<f64>,
724 #[serde(default, skip_serializing_if = "Option::is_none")]
725 pub control_mode: Option<AcDcConverterControlMode>,
726 #[serde(default, skip_serializing_if = "Option::is_none")]
728 pub active_power_at_pcc_mw: Option<f64>,
729 #[serde(default, skip_serializing_if = "Option::is_none")]
731 pub reactive_power_at_pcc_mvar: Option<f64>,
732 #[serde(default, skip_serializing_if = "Option::is_none")]
734 pub target_active_power_mw: Option<f64>,
735 #[serde(default, skip_serializing_if = "Option::is_none")]
736 pub target_dc_voltage_kv: Option<f64>,
737 #[serde(default, skip_serializing_if = "Option::is_none")]
738 pub pcc_terminal: Option<TerminalReference>,
739 #[serde(default, skip_serializing_if = "Option::is_none")]
740 pub droop_curve: Option<DroopCurve>,
741 #[serde(default, skip_serializing_if = "Option::is_none")]
743 pub droop: Option<f64>,
744 #[serde(default, skip_serializing_if = "Option::is_none")]
745 pub droop_compensation: Option<f64>,
746 #[serde(default, skip_serializing_if = "Option::is_none")]
747 pub q_share: Option<f64>,
748 #[serde(default, skip_serializing_if = "Option::is_none")]
749 pub maximum_modulation_index: Option<f64>,
750 #[serde(default, skip_serializing_if = "Option::is_none")]
751 pub maximum_valve_current_a: Option<f64>,
752 #[serde(default, skip_serializing_if = "Option::is_none")]
753 pub voltage_regulator_on: Option<bool>,
754 #[serde(default, skip_serializing_if = "Option::is_none")]
755 pub voltage_setpoint_kv: Option<f64>,
756 #[serde(default, skip_serializing_if = "Option::is_none")]
757 pub reactive_power_setpoint_mvar: Option<f64>,
758 #[serde(default, skip_serializing_if = "Option::is_none")]
759 pub reactive_limits: Option<ReactiveLimits>,
760 #[serde(default, skip_serializing_if = "Option::is_none")]
761 pub pole_loss_active_power_mw: Option<f64>,
762 #[serde(default, skip_serializing_if = "Option::is_none")]
763 pub dc_current_a: Option<f64>,
764 #[serde(default, skip_serializing_if = "Option::is_none")]
765 pub ac_voltage_kv: Option<f64>,
766 #[serde(default, skip_serializing_if = "Option::is_none")]
767 pub dc_voltage_kv: Option<f64>,
768 #[serde(default, skip_serializing_if = "Option::is_none")]
769 pub delta_degrees: Option<f64>,
770 #[serde(default, skip_serializing_if = "Option::is_none")]
771 pub uf_kv: Option<f64>,
772 #[serde(default, skip_serializing_if = "Option::is_none")]
773 pub uv_kv: Option<f64>,
774}
775
776#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
778#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
779#[serde(rename_all = "snake_case")]
780#[non_exhaustive]
781pub enum LineCommutatedConverterReactiveModel {
782 FixedPowerFactor,
783 CalculatedPowerFactor,
784}
785
786#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
788#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
789#[serde(rename_all = "snake_case")]
790#[non_exhaustive]
791pub enum LineCommutatedConverterOperatingMode {
792 Rectifier,
793 Inverter,
794}
795
796#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
798#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
799#[non_exhaustive]
800pub struct LineCommutatedConverter {
801 pub component: ComponentId,
802 #[serde(default, skip_serializing_if = "Option::is_none")]
803 pub dc_converter_unit: Option<ComponentId>,
804 pub dc_terminal1: DcTerminal,
805 pub dc_terminal2: DcTerminal,
806 #[serde(default, skip_serializing_if = "Option::is_none")]
807 pub base_apparent_power_mva: Option<f64>,
808 #[serde(default, skip_serializing_if = "Option::is_none")]
809 pub minimum_active_power_mw: Option<f64>,
810 #[serde(default, skip_serializing_if = "Option::is_none")]
811 pub maximum_active_power_mw: Option<f64>,
812 #[serde(default, skip_serializing_if = "Option::is_none")]
813 pub minimum_dc_voltage_kv: Option<f64>,
814 #[serde(default, skip_serializing_if = "Option::is_none")]
815 pub maximum_dc_voltage_kv: Option<f64>,
816 #[serde(default, skip_serializing_if = "Option::is_none")]
817 pub rated_dc_voltage_kv: Option<f64>,
818 #[serde(default, skip_serializing_if = "Option::is_none")]
819 pub valve_u0_kv: Option<f64>,
820 #[serde(default, skip_serializing_if = "Option::is_none")]
821 pub number_of_valves: Option<u32>,
822 #[serde(default, skip_serializing_if = "Option::is_none")]
823 pub idle_loss_mw: Option<f64>,
824 #[serde(default, skip_serializing_if = "Option::is_none")]
825 pub switching_loss_mw_per_ampere: Option<f64>,
826 #[serde(default, skip_serializing_if = "Option::is_none")]
827 pub resistive_loss_ohm: Option<f64>,
828 #[serde(default, skip_serializing_if = "Option::is_none")]
829 pub control_mode: Option<AcDcConverterControlMode>,
830 #[serde(default, skip_serializing_if = "Option::is_none")]
832 pub active_power_at_pcc_mw: Option<f64>,
833 #[serde(default, skip_serializing_if = "Option::is_none")]
835 pub reactive_power_at_pcc_mvar: Option<f64>,
836 #[serde(default, skip_serializing_if = "Option::is_none")]
838 pub target_active_power_mw: Option<f64>,
839 #[serde(default, skip_serializing_if = "Option::is_none")]
840 pub target_dc_voltage_kv: Option<f64>,
841 #[serde(default, skip_serializing_if = "Option::is_none")]
842 pub pcc_terminal: Option<TerminalReference>,
843 #[serde(default, skip_serializing_if = "Option::is_none")]
844 pub droop_curve: Option<DroopCurve>,
845 #[serde(default, skip_serializing_if = "Option::is_none")]
846 pub reactive_model: Option<LineCommutatedConverterReactiveModel>,
847 #[serde(default, skip_serializing_if = "Option::is_none")]
848 pub power_factor: Option<f64>,
849 #[serde(default, skip_serializing_if = "Option::is_none")]
850 pub operating_mode: Option<LineCommutatedConverterOperatingMode>,
851 #[serde(default, skip_serializing_if = "Option::is_none")]
852 pub rated_dc_current_a: Option<f64>,
853 #[serde(default, skip_serializing_if = "Option::is_none")]
854 pub minimum_alpha_degrees: Option<f64>,
855 #[serde(default, skip_serializing_if = "Option::is_none")]
856 pub maximum_alpha_degrees: Option<f64>,
857 #[serde(default, skip_serializing_if = "Option::is_none")]
858 pub minimum_gamma_degrees: Option<f64>,
859 #[serde(default, skip_serializing_if = "Option::is_none")]
860 pub maximum_gamma_degrees: Option<f64>,
861 #[serde(default, skip_serializing_if = "Option::is_none")]
862 pub target_alpha_degrees: Option<f64>,
863 #[serde(default, skip_serializing_if = "Option::is_none")]
864 pub target_gamma_degrees: Option<f64>,
865 #[serde(default, skip_serializing_if = "Option::is_none")]
866 pub target_dc_current_a: Option<f64>,
867 #[serde(default, skip_serializing_if = "Option::is_none")]
868 pub pole_loss_active_power_mw: Option<f64>,
869 #[serde(default, skip_serializing_if = "Option::is_none")]
870 pub dc_current_a: Option<f64>,
871 #[serde(default, skip_serializing_if = "Option::is_none")]
872 pub ac_voltage_kv: Option<f64>,
873 #[serde(default, skip_serializing_if = "Option::is_none")]
874 pub dc_voltage_kv: Option<f64>,
875 #[serde(default, skip_serializing_if = "Option::is_none")]
876 pub alpha_degrees: Option<f64>,
877 #[serde(default, skip_serializing_if = "Option::is_none")]
878 pub gamma_degrees: Option<f64>,
879}
880
881#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
883#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
884#[non_exhaustive]
885pub struct TemporaryLimit {
886 pub name: String,
887 pub value: f64,
888 pub acceptable_duration_seconds: u64,
889 #[serde(default)]
890 pub fictitious: bool,
891}
892
893#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
899#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
900#[non_exhaustive]
901pub struct LoadingLimits {
902 #[serde(default, skip_serializing_if = "Option::is_none")]
903 pub permanent_limit: Option<f64>,
904 #[serde(default, skip_serializing_if = "Option::is_none")]
905 pub permanent_limit_name: Option<String>,
906 #[serde(default)]
907 pub temporary_limits: Vec<TemporaryLimit>,
908}
909
910#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
912#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
913#[non_exhaustive]
914pub struct OperationalLimitGroup {
915 pub equipment: ComponentId,
916 pub terminal: u8,
917 pub id: String,
918 #[serde(default)]
919 pub properties: BTreeMap<String, String>,
920 #[serde(default)]
921 pub selected: bool,
922 #[serde(default, skip_serializing_if = "Option::is_none")]
923 pub current_limits: Option<LoadingLimits>,
924 #[serde(default, skip_serializing_if = "Option::is_none")]
925 pub active_power_limits: Option<LoadingLimits>,
926 #[serde(default, skip_serializing_if = "Option::is_none")]
927 pub apparent_power_limits: Option<LoadingLimits>,
928}
929
930#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
932#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
933#[serde(rename_all = "snake_case")]
934#[non_exhaustive]
935pub enum TapChangerKind {
936 Ratio,
937 Phase,
938}
939
940#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
942#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
943#[serde(rename_all = "snake_case")]
944#[non_exhaustive]
945pub enum TapChangerRegulationMode {
946 Voltage,
947 ReactivePower,
948 ActivePower,
949 Current,
950}
951
952#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
954#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
955#[non_exhaustive]
956pub struct TapChangerStep {
957 pub position: i32,
958 pub rho: f64,
960 pub alpha_degrees: f64,
962 pub resistance_deviation_percent: f64,
964 pub reactance_deviation_percent: f64,
966 pub conductance_deviation_percent: f64,
968 pub susceptance_deviation_percent: f64,
970}
971
972#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
974#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
975#[non_exhaustive]
976pub struct TapChanger {
977 #[serde(default, skip_serializing_if = "Option::is_none")]
979 pub component: Option<ComponentId>,
980 pub transformer: ComponentId,
981 pub winding: u8,
984 pub kind: TapChangerKind,
985 #[serde(default, skip_serializing_if = "Option::is_none")]
988 pub tap_position: Option<i32>,
989 #[serde(default, skip_serializing_if = "Option::is_none")]
992 pub solved_tap_position: Option<i32>,
993 pub low_tap_position: i32,
994 #[serde(default, skip_serializing_if = "Option::is_none")]
996 pub neutral_tap_position: Option<i32>,
997 #[serde(default, skip_serializing_if = "Option::is_none")]
999 pub normal_tap_position: Option<i32>,
1000 #[serde(default, skip_serializing_if = "Option::is_none")]
1003 pub voltage_step_increment_percent: Option<f64>,
1004 pub load_tap_changing_capabilities: bool,
1005 pub regulating: bool,
1006 #[serde(default, skip_serializing_if = "Option::is_none")]
1007 pub regulation_mode: Option<TapChangerRegulationMode>,
1008 #[serde(default, skip_serializing_if = "Option::is_none")]
1010 pub regulation_value: Option<f64>,
1011 #[serde(default, skip_serializing_if = "Option::is_none")]
1013 pub target_deadband: Option<f64>,
1014 #[serde(default, skip_serializing_if = "Option::is_none")]
1015 pub regulation_terminal: Option<TerminalReference>,
1016 pub steps: Vec<TapChangerStep>,
1017}
1018
1019#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1021#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1022#[non_exhaustive]
1023pub struct CaseMetadata {
1024 #[serde(default, skip_serializing_if = "Option::is_none")]
1025 pub case_date: Option<String>,
1026 #[serde(default, skip_serializing_if = "Option::is_none")]
1027 pub forecast_distance: Option<i32>,
1028 #[serde(default, skip_serializing_if = "Option::is_none")]
1029 pub source_model_format: Option<String>,
1030 #[serde(default, skip_serializing_if = "Option::is_none")]
1031 pub minimum_validation_level: Option<String>,
1032}
1033
1034#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1036#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1037#[non_exhaustive]
1038pub struct Subnetwork {
1039 pub component: ComponentId,
1040 pub parent: ComponentId,
1041 pub case_metadata: CaseMetadata,
1042 #[serde(default)]
1043 pub components: Vec<ComponentId>,
1044}
1045
1046#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
1052#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1053#[serde(rename_all = "snake_case")]
1054#[non_exhaustive]
1055pub enum OmittedFieldName {
1056 ActivePower,
1057 ReactivePower,
1058 VoltageSetpoint,
1059 RatedApparentPower,
1060 ShuntConductancePerSection,
1061}
1062
1063#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
1065#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1066#[non_exhaustive]
1067pub struct OmittedField {
1068 pub component: ComponentId,
1069 pub field: OmittedFieldName,
1070}
1071
1072impl OmittedField {
1073 #[must_use]
1074 pub fn new(component: ComponentId, field: OmittedFieldName) -> Self {
1075 Self { component, field }
1076 }
1077}
1078
1079#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1082#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1083#[non_exhaustive]
1084pub struct DetailedConnectivity {
1085 #[serde(default)]
1088 pub omitted_fields: Vec<OmittedField>,
1089 #[serde(default)]
1090 pub component_metadata: Vec<ComponentMetadata>,
1091 #[serde(default)]
1092 pub subnetworks: Vec<Subnetwork>,
1093 #[serde(default)]
1094 pub substations: Vec<Substation>,
1095 #[serde(default)]
1096 pub voltage_levels: Vec<VoltageLevel>,
1097 #[serde(default)]
1098 pub bus_breaker_buses: Vec<BusBreakerBus>,
1099 #[serde(default)]
1100 pub calculated_buses: Vec<CalculatedBus>,
1101 #[serde(default)]
1102 pub connectivity_nodes: Vec<ConnectivityNode>,
1103 #[serde(default)]
1104 pub busbar_sections: Vec<BusbarSection>,
1105 #[serde(default)]
1106 pub junctions: Vec<Junction>,
1107 #[serde(default)]
1108 pub terminals: Vec<Terminal>,
1109 #[serde(default)]
1110 pub switches: Vec<TopologySwitch>,
1111 #[serde(default)]
1112 pub internal_connections: Vec<InternalConnection>,
1113 #[serde(default)]
1114 pub operational_limit_groups: Vec<OperationalLimitGroup>,
1115 #[serde(default)]
1116 pub tap_changers: Vec<TapChanger>,
1117 #[serde(default)]
1118 pub equipment_reactive_limits: Vec<EquipmentReactiveLimits>,
1119 #[serde(default)]
1120 pub boundary_lines: Vec<BoundaryLine>,
1121 #[serde(default)]
1122 pub tie_lines: Vec<TieLine>,
1123 #[serde(default)]
1124 pub dc_converter_units: Vec<DcConverterUnit>,
1125 #[serde(default)]
1126 pub dc_topological_nodes: Vec<DcTopologicalNode>,
1127 #[serde(default)]
1128 pub dc_nodes: Vec<DcNode>,
1129 #[serde(default)]
1130 pub dc_grounds: Vec<DcGround>,
1131 #[serde(default)]
1132 pub dc_busbars: Vec<DcBusbar>,
1133 #[serde(default)]
1134 pub dc_lines: Vec<DcLine>,
1135 #[serde(default)]
1136 pub dc_series_devices: Vec<DcSeriesDevice>,
1137 #[serde(default)]
1138 pub dc_switches: Vec<DcSwitch>,
1139 #[serde(default)]
1140 pub voltage_source_converters: Vec<VoltageSourceConverter>,
1141 #[serde(default)]
1142 pub line_commutated_converters: Vec<LineCommutatedConverter>,
1143}
1144
1145pub const DEFAULT_BASE_FREQUENCY: f64 = 60.0;
1149
1150fn default_base_frequency() -> f64 {
1154 DEFAULT_BASE_FREQUENCY
1155}
1156
1157#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
1164#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1165#[serde(transparent)]
1166pub struct BusId(pub usize);
1167
1168impl BusId {
1169 pub const MAX: Self = Self(i64::MAX as usize);
1173
1174 #[must_use]
1175 pub const fn new(id: usize) -> Self {
1176 Self(id)
1177 }
1178}
1179
1180impl std::fmt::Display for BusId {
1181 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1182 self.0.fmt(f)
1183 }
1184}
1185
1186#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1188#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1189#[serde(rename_all = "UPPERCASE")]
1190#[repr(u8)]
1191#[non_exhaustive]
1192pub enum BusType {
1193 Pq = 1,
1194 Pv = 2,
1195 Ref = 3,
1196 Isolated = 4,
1197}
1198
1199impl BusType {
1200 pub(crate) fn from_f64(v: f64) -> Self {
1202 match v as i32 {
1203 2 => Self::Pv,
1204 3 => Self::Ref,
1205 4 => Self::Isolated,
1206 _ => Self::Pq,
1207 }
1208 }
1209
1210 #[must_use]
1213 pub fn as_str(self) -> &'static str {
1214 match self {
1215 Self::Pq => "PQ",
1216 Self::Pv => "PV",
1217 Self::Ref => "REF",
1218 Self::Isolated => "ISOLATED",
1219 }
1220 }
1221}
1222
1223#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1225#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1226#[non_exhaustive]
1227pub struct GenCost {
1228 pub model: u8,
1230 pub startup: f64,
1231 pub shutdown: f64,
1232 pub ncost: usize,
1234 pub coeffs: Vec<f64>,
1237}
1238
1239impl GenCost {
1240 #[must_use]
1248 pub fn new(model: u8, startup: f64, shutdown: f64, coeffs: Vec<f64>) -> Self {
1249 let ncost = if model == 1 {
1250 coeffs.len() / 2
1251 } else {
1252 coeffs.len()
1253 };
1254 Self {
1255 model,
1256 startup,
1257 shutdown,
1258 ncost,
1259 coeffs,
1260 }
1261 }
1262
1263 #[must_use]
1264 pub fn with_ncost(
1265 model: u8,
1266 startup: f64,
1267 shutdown: f64,
1268 ncost: usize,
1269 coeffs: Vec<f64>,
1270 ) -> Self {
1271 Self {
1272 model,
1273 startup,
1274 shutdown,
1275 ncost,
1276 coeffs,
1277 }
1278 }
1279
1280 pub fn calc_quadratic(&self) -> Option<(f64, f64)> {
1285 self.calc_quadratic_with_constant().map(|(q, c, _)| (q, c))
1286 }
1287
1288 pub fn calc_quadratic_with_constant(&self) -> Option<(f64, f64, f64)> {
1294 if self.model != 2 {
1295 return None;
1296 }
1297 if self.coeffs.len() < self.ncost {
1300 return None;
1301 }
1302 match self.ncost {
1306 3 => Some((2.0 * self.coeffs[0], self.coeffs[1], self.coeffs[2])),
1307 2 => Some((0.0, self.coeffs[0], self.coeffs[1])),
1308 1 => Some((0.0, 0.0, self.coeffs[0])),
1309 _ => None,
1310 }
1311 }
1312
1313 pub const LEADING_COEFF_TOL: f64 = 1e-12;
1317
1318 pub fn calc_quadratic_with_constant_tol(&self, tol: f64) -> Option<(f64, f64, f64)> {
1328 if self.model != 2 {
1329 return None;
1330 }
1331 if self.coeffs.len() < self.ncost {
1332 return None;
1333 }
1334 let row = &self.coeffs[..self.ncost];
1335 let mut first = 0;
1336 while first + 1 < row.len() && row[first].abs() <= tol {
1337 first += 1;
1338 }
1339 match row.len() - first {
1340 3 => Some((2.0 * row[first], row[first + 1], row[first + 2])),
1341 2 => Some((0.0, row[first], row[first + 1])),
1342 1 => Some((0.0, 0.0, row[first])),
1343 _ => None,
1344 }
1345 }
1346}
1347
1348#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1354#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1355#[non_exhaustive]
1356pub enum SourceFormat {
1357 #[serde(rename = "matpower")]
1358 Matpower,
1359 #[serde(rename = "powermodels-json")]
1360 PowerModelsJson,
1361 #[serde(rename = "egret-json")]
1362 EgretJson,
1363 #[serde(rename = "psse")]
1364 Psse,
1365 #[serde(rename = "psse-rawx")]
1367 PsseRawx,
1368 #[serde(rename = "powerworld")]
1369 PowerWorld,
1370 #[serde(rename = "pandapower-json")]
1371 PandapowerJson,
1372 #[serde(rename = "pslf")]
1376 Pslf,
1377 #[serde(rename = "powerworld-pwb")]
1381 PowerWorldBinary,
1382 #[serde(rename = "in-memory")]
1384 InMemory,
1385 #[serde(rename = "normalized")]
1391 Normalized,
1392 #[serde(rename = "gridfm")]
1396 Gridfm,
1397 #[serde(rename = "pypsa-csv")]
1400 PypsaCsv,
1401 #[serde(rename = "goc3-json")]
1405 Goc3Json,
1406 #[serde(rename = "surge-json")]
1408 SurgeJson,
1409 #[serde(rename = "opfdata-json")]
1414 DeepMindOpfDataJson,
1415 #[serde(rename = "xiidm")]
1417 Xiidm,
1418 #[serde(rename = "jiidm")]
1420 Jiidm,
1421 #[serde(rename = "cgmes")]
1423 Cgmes,
1424 #[serde(rename = "ucte")]
1427 Ucte,
1428 #[serde(rename = "ieee-cdf")]
1431 IeeeCdf,
1432}
1433
1434impl SourceFormat {
1435 #[must_use]
1440 pub fn name(self) -> &'static str {
1441 match self {
1442 SourceFormat::Matpower => "matpower",
1443 SourceFormat::PowerModelsJson => "powermodels-json",
1444 SourceFormat::EgretJson => "egret-json",
1445 SourceFormat::Psse => "psse",
1446 SourceFormat::PsseRawx => "psse-rawx",
1447 SourceFormat::PowerWorld => "powerworld",
1448 SourceFormat::PandapowerJson => "pandapower-json",
1449 SourceFormat::Pslf => "pslf",
1450 SourceFormat::PowerWorldBinary => "powerworld-pwb",
1451 SourceFormat::InMemory => "in-memory",
1452 SourceFormat::Normalized => "normalized",
1453 SourceFormat::Gridfm => "gridfm",
1454 SourceFormat::PypsaCsv => "pypsa-csv",
1455 SourceFormat::Goc3Json => "goc3-json",
1456 SourceFormat::SurgeJson => "surge-json",
1457 SourceFormat::DeepMindOpfDataJson => "opfdata-json",
1458 SourceFormat::Xiidm => "xiidm",
1459 SourceFormat::Jiidm => "jiidm",
1460 SourceFormat::Cgmes => "cgmes",
1461 SourceFormat::Ucte => "ucte",
1462 SourceFormat::IeeeCdf => "ieee-cdf",
1463 }
1464 }
1465}
1466
1467#[derive(Debug, Clone)]
1477pub struct BalancedNetwork {
1478 tables: std::sync::Arc<BalancedNetworkTables>,
1479}
1480
1481impl BalancedNetwork {
1482 pub(crate) fn from_tables(tables: BalancedNetworkTables) -> Self {
1483 Self {
1484 tables: std::sync::Arc::new(tables),
1485 }
1486 }
1487
1488 pub(crate) fn tables_mut(&mut self) -> &mut BalancedNetworkTables {
1491 std::sync::Arc::make_mut(&mut self.tables)
1492 }
1493}
1494
1495#[derive(Debug, Clone, Serialize, Deserialize)]
1502#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1503#[cfg_attr(feature = "schema", schemars(rename = "BalancedNetwork"))]
1504#[serde(remote = "Self")]
1505pub(crate) struct BalancedNetworkTables {
1506 pub name: String,
1507 pub base_mva: f64,
1508 #[serde(default = "default_base_frequency")]
1515 pub base_frequency: f64,
1516 #[serde(default, skip_serializing_if = "Option::is_none")]
1517 pub geo: Option<GeoMeta>,
1518 #[serde(default)]
1520 pub case_metadata: CaseMetadata,
1521 #[serde(default, skip_serializing_if = "Option::is_none")]
1524 pub detailed_connectivity: Option<std::sync::Arc<DetailedConnectivity>>,
1525 #[serde(default, skip_serializing_if = "generated_uids_is_empty")]
1529 pub generated_uids: std::sync::Arc<BTreeSet<String>>,
1530 pub buses: std::sync::Arc<Vec<Bus>>,
1531 pub loads: std::sync::Arc<Vec<Load>>,
1532 pub shunts: std::sync::Arc<Vec<Shunt>>,
1533 #[serde(default)]
1534 pub static_var_compensators: std::sync::Arc<Vec<StaticVarCompensator>>,
1535 pub branches: std::sync::Arc<Vec<Branch>>,
1536 #[serde(default)]
1537 pub switches: std::sync::Arc<Vec<Switch>>,
1538 pub generators: std::sync::Arc<Vec<Generator>>,
1539 pub storage: std::sync::Arc<Vec<Storage>>,
1540 pub hvdc: std::sync::Arc<Vec<Hvdc>>,
1541 #[serde(default)]
1550 pub transformers_3w: std::sync::Arc<Vec<Transformer3W>>,
1551 #[serde(default)]
1556 pub areas: std::sync::Arc<Vec<Area>>,
1557 #[serde(default)]
1560 pub solver: Option<SolverParams>,
1561 pub source_format: SourceFormat,
1562}
1563
1564impl Serialize for BalancedNetwork {
1565 fn serialize<S: serde::Serializer>(
1566 &self,
1567 serializer: S,
1568 ) -> std::result::Result<S::Ok, S::Error> {
1569 BalancedNetworkTables::serialize(
1570 &self.tables,
1571 powerio_core::__implementation::nonfinite::NonFiniteSer(serializer),
1572 )
1573 }
1574}
1575
1576impl<'de> Deserialize<'de> for BalancedNetwork {
1577 fn deserialize<D: serde::Deserializer<'de>>(
1578 deserializer: D,
1579 ) -> std::result::Result<Self, D::Error> {
1580 BalancedNetworkTables::deserialize(powerio_core::__implementation::nonfinite::NonFiniteDe(
1581 deserializer,
1582 ))
1583 .map(BalancedNetwork::from_tables)
1584 }
1585}
1586
1587#[cfg(feature = "schema")]
1588impl schemars::JsonSchema for BalancedNetwork {
1589 fn schema_name() -> std::borrow::Cow<'static, str> {
1590 <BalancedNetworkTables as schemars::JsonSchema>::schema_name()
1591 }
1592
1593 fn schema_id() -> std::borrow::Cow<'static, str> {
1594 <BalancedNetworkTables as schemars::JsonSchema>::schema_id()
1595 }
1596
1597 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1598 <BalancedNetworkTables as schemars::JsonSchema>::json_schema(generator)
1599 }
1600}
1601
1602macro_rules! table_accessors {
1603 ($($(#[$doc:meta])* $field:ident, $field_mut:ident: $ty:ty;)+) => {
1604 impl BalancedNetwork {
1605 $(
1606 $(#[$doc])*
1607 #[must_use]
1608 pub fn $field(&self) -> &$ty {
1609 &self.tables.$field
1610 }
1611
1612 #[must_use]
1616 pub fn $field_mut(&mut self) -> &mut $ty {
1617 &mut self.tables_mut().$field
1618 }
1619 )+
1620 }
1621 };
1622}
1623
1624table_accessors! {
1625 name, name_mut: String;
1627 geo, geo_mut: Option<GeoMeta>;
1629 case_metadata, case_metadata_mut: CaseMetadata;
1631 solver, solver_mut: Option<SolverParams>;
1633}
1634
1635impl BalancedNetwork {
1636 #[must_use]
1638 pub fn detailed_connectivity(&self) -> &Option<std::sync::Arc<DetailedConnectivity>> {
1639 &self.tables.detailed_connectivity
1640 }
1641
1642 #[must_use]
1646 pub fn detailed_connectivity_mut(
1647 &mut self,
1648 ) -> &mut Option<std::sync::Arc<DetailedConnectivity>> {
1649 let tables = self.tables_mut();
1650 if let Some(detailed) = tables.detailed_connectivity.as_mut()
1651 && !detailed.omitted_fields.is_empty()
1652 {
1653 std::sync::Arc::make_mut(detailed).omitted_fields.clear();
1654 }
1655 &mut tables.detailed_connectivity
1656 }
1657
1658 fn clear_omitted_fields(&mut self, component_type: &str, fields: &[OmittedFieldName]) {
1659 let Some(detailed) = self.tables_mut().detailed_connectivity.as_mut() else {
1660 return;
1661 };
1662 let affected = |omitted: &OmittedField| {
1663 omitted.component.component_type() == component_type && fields.contains(&omitted.field)
1664 };
1665 if detailed.omitted_fields.iter().any(affected) {
1667 std::sync::Arc::make_mut(detailed)
1668 .omitted_fields
1669 .retain(|omitted| !affected(omitted));
1670 }
1671 }
1672
1673 fn clear_omitted_field(&mut self, component: &ComponentId, field: OmittedFieldName) {
1674 let Some(detailed) = self.tables_mut().detailed_connectivity.as_mut() else {
1675 return;
1676 };
1677 let affected =
1678 |omitted: &OmittedField| omitted.component == *component && omitted.field == field;
1679 if detailed.omitted_fields.iter().any(affected) {
1680 std::sync::Arc::make_mut(detailed)
1681 .omitted_fields
1682 .retain(|omitted| !affected(omitted));
1683 }
1684 }
1685}
1686
1687macro_rules! shared_table_accessors {
1692 ($($(#[$doc:meta])* $field:ident, $field_mut:ident: $ty:ty;)+) => {
1693 impl BalancedNetwork {
1694 $(
1695 $(#[$doc])*
1696 #[must_use]
1697 pub fn $field(&self) -> &$ty {
1698 &self.tables.$field
1699 }
1700
1701 #[must_use]
1706 pub fn $field_mut(&mut self) -> &mut $ty {
1707 std::sync::Arc::make_mut(&mut self.tables_mut().$field)
1708 }
1709 )+
1710 }
1711 };
1712}
1713
1714impl BalancedNetwork {
1715 pub fn share_equal_tables(&mut self, donor: &Self) {
1721 macro_rules! share {
1722 ($($field:ident),+) => {
1723 $(
1724 if !std::sync::Arc::ptr_eq(&self.tables.$field, &donor.tables.$field)
1725 && self.tables.$field == donor.tables.$field
1726 {
1727 self.tables_mut().$field = donor.tables.$field.clone();
1728 }
1729 )+
1730 };
1731 }
1732 share!(
1733 buses,
1734 loads,
1735 shunts,
1736 static_var_compensators,
1737 branches,
1738 switches,
1739 generators,
1740 storage,
1741 hvdc,
1742 transformers_3w,
1743 areas
1744 );
1745 }
1746}
1747
1748shared_table_accessors! {
1749 buses, buses_mut: Vec<Bus>;
1750 static_var_compensators, static_var_compensators_mut: Vec<StaticVarCompensator>;
1751 branches, branches_mut: Vec<Branch>;
1752 switches, switches_mut: Vec<Switch>;
1753 hvdc, hvdc_mut: Vec<Hvdc>;
1754 transformers_3w, transformers_3w_mut: Vec<Transformer3W>;
1756 areas, areas_mut: Vec<Area>;
1758}
1759
1760impl BalancedNetwork {
1761 #[must_use]
1762 pub fn loads(&self) -> &Vec<Load> {
1763 &self.tables.loads
1764 }
1765
1766 #[must_use]
1769 pub fn loads_mut(&mut self) -> &mut Vec<Load> {
1770 self.clear_omitted_fields(
1771 "load",
1772 &[
1773 OmittedFieldName::ActivePower,
1774 OmittedFieldName::ReactivePower,
1775 ],
1776 );
1777 std::sync::Arc::make_mut(&mut self.tables_mut().loads)
1778 }
1779
1780 #[doc(hidden)]
1784 pub(crate) fn edit_load_assignment<R>(
1785 &mut self,
1786 index: usize,
1787 component: &ComponentId,
1788 field: OmittedFieldName,
1789 edit: impl FnOnce(&mut Load) -> R,
1790 ) -> R {
1791 debug_assert_eq!(component.component_type(), "load");
1792 debug_assert!(matches!(
1793 field,
1794 OmittedFieldName::ActivePower | OmittedFieldName::ReactivePower
1795 ));
1796 self.clear_omitted_field(component, field);
1797 edit(&mut std::sync::Arc::make_mut(&mut self.tables_mut().loads)[index])
1798 }
1799
1800 #[must_use]
1801 pub fn generators(&self) -> &Vec<Generator> {
1802 &self.tables.generators
1803 }
1804
1805 #[must_use]
1808 pub fn generators_mut(&mut self) -> &mut Vec<Generator> {
1809 self.clear_omitted_fields(
1810 "generator",
1811 &[
1812 OmittedFieldName::ActivePower,
1813 OmittedFieldName::ReactivePower,
1814 OmittedFieldName::VoltageSetpoint,
1815 OmittedFieldName::RatedApparentPower,
1816 ],
1817 );
1818 std::sync::Arc::make_mut(&mut self.tables_mut().generators)
1819 }
1820
1821 #[doc(hidden)]
1825 pub(crate) fn edit_generator_assignment<R>(
1826 &mut self,
1827 index: usize,
1828 component: &ComponentId,
1829 field: OmittedFieldName,
1830 edit: impl FnOnce(&mut Generator) -> R,
1831 ) -> R {
1832 debug_assert_eq!(component.component_type(), "generator");
1833 debug_assert!(matches!(
1834 field,
1835 OmittedFieldName::ActivePower
1836 | OmittedFieldName::ReactivePower
1837 | OmittedFieldName::VoltageSetpoint
1838 ));
1839 self.clear_omitted_field(component, field);
1840 edit(&mut std::sync::Arc::make_mut(&mut self.tables_mut().generators)[index])
1841 }
1842
1843 #[must_use]
1844 pub fn storage(&self) -> &Vec<Storage> {
1845 &self.tables.storage
1846 }
1847
1848 #[must_use]
1851 pub fn storage_mut(&mut self) -> &mut Vec<Storage> {
1852 self.clear_omitted_fields(
1853 "storage",
1854 &[
1855 OmittedFieldName::ActivePower,
1856 OmittedFieldName::ReactivePower,
1857 ],
1858 );
1859 std::sync::Arc::make_mut(&mut self.tables_mut().storage)
1860 }
1861
1862 #[must_use]
1863 pub fn shunts(&self) -> &Vec<Shunt> {
1864 &self.tables.shunts
1865 }
1866
1867 #[must_use]
1870 pub fn shunts_mut(&mut self) -> &mut Vec<Shunt> {
1871 self.clear_omitted_fields("shunt", &[OmittedFieldName::ShuntConductancePerSection]);
1872 std::sync::Arc::make_mut(&mut self.tables_mut().shunts)
1873 }
1874}
1875
1876impl BalancedNetwork {
1877 #[must_use]
1879 pub fn base_mva(&self) -> f64 {
1880 self.tables.base_mva
1881 }
1882
1883 #[must_use]
1884 pub fn base_mva_mut(&mut self) -> &mut f64 {
1885 &mut self.tables_mut().base_mva
1886 }
1887
1888 #[must_use]
1890 pub fn base_frequency(&self) -> f64 {
1891 self.tables.base_frequency
1892 }
1893
1894 #[must_use]
1895 pub fn base_frequency_mut(&mut self) -> &mut f64 {
1896 &mut self.tables_mut().base_frequency
1897 }
1898
1899 #[must_use]
1901 pub fn source_format(&self) -> SourceFormat {
1902 self.tables.source_format
1903 }
1904
1905 #[must_use]
1906 pub fn source_format_mut(&mut self) -> &mut SourceFormat {
1907 &mut self.tables_mut().source_format
1908 }
1909
1910 pub fn assign_missing_component_ids(&mut self) {
1928 let mut used = self.component_ids_in_use();
1929 let mut next_suffix = HashMap::new();
1930 macro_rules! assign {
1931 ($table:ident, $table_mut:ident, $set_uid:expr, $stem:expr) => {
1932 if self.$table().iter().any(|value| value.uid.is_none()) {
1933 let generated = assign_missing_ids(
1934 self.$table_mut(),
1935 |value| value.uid.as_deref(),
1936 $set_uid,
1937 $stem,
1938 &mut used,
1939 &mut next_suffix,
1940 );
1941 std::sync::Arc::make_mut(&mut self.tables_mut().generated_uids)
1942 .extend(generated);
1943 }
1944 };
1945 }
1946
1947 assign!(
1948 buses,
1949 buses_mut,
1950 |bus: &mut Bus, uid| bus.uid = Some(uid),
1951 |bus: &Bus| bus.id.to_string()
1952 );
1953 assign!(
1954 loads,
1955 loads_mut,
1956 |load: &mut Load, uid| load.uid = Some(uid),
1957 |load: &Load| format!("bus-{}", load.bus)
1958 );
1959 assign!(
1960 shunts,
1961 shunts_mut,
1962 |shunt: &mut Shunt, uid| shunt.uid = Some(uid),
1963 |shunt: &Shunt| format!("bus-{}", shunt.bus)
1964 );
1965 assign!(
1966 static_var_compensators,
1967 static_var_compensators_mut,
1968 |svc: &mut StaticVarCompensator, uid| svc.uid = Some(uid),
1969 |svc: &StaticVarCompensator| format!("bus-{}", svc.bus)
1970 );
1971 assign!(
1972 branches,
1973 branches_mut,
1974 |branch: &mut Branch, uid| branch.uid = Some(uid),
1975 |branch: &Branch| format!("{}-{}", branch.from, branch.to)
1976 );
1977 assign!(
1978 switches,
1979 switches_mut,
1980 |switch: &mut Switch, uid| switch.uid = Some(uid),
1981 |switch: &Switch| format!("{}-{}", switch.from, switch.to)
1982 );
1983 assign!(
1984 generators,
1985 generators_mut,
1986 |generator: &mut Generator, uid| generator.uid = Some(uid),
1987 |generator: &Generator| format!("bus-{}", generator.bus)
1988 );
1989 assign!(
1990 storage,
1991 storage_mut,
1992 |storage: &mut Storage, uid| storage.uid = Some(uid),
1993 |storage: &Storage| format!("bus-{}", storage.bus)
1994 );
1995 assign!(
1996 hvdc,
1997 hvdc_mut,
1998 |line: &mut Hvdc, uid| line.uid = Some(uid),
1999 |line: &Hvdc| format!("{}-{}", line.from, line.to)
2000 );
2001 assign!(
2002 transformers_3w,
2003 transformers_3w_mut,
2004 |transformer: &mut Transformer3W, uid| transformer.uid = Some(uid),
2005 |transformer: &Transformer3W| {
2006 let [first, second, third] = &transformer.windings;
2007 format!("{}-{}-{}", first.bus, second.bus, third.bus)
2008 }
2009 );
2010 }
2011
2012 #[must_use]
2014 pub fn uid_is_generated(&self, uid: Option<&str>) -> bool {
2015 uid.is_some_and(|uid| self.tables.generated_uids.contains(uid))
2016 }
2017
2018 pub(crate) fn generated_uids(&self) -> &std::sync::Arc<BTreeSet<String>> {
2019 &self.tables.generated_uids
2020 }
2021
2022 pub(crate) fn component_ids_in_use(&self) -> HashSet<String> {
2024 let mut used = HashSet::new();
2025 macro_rules! collect {
2026 ($($table:ident),+) => {
2027 $(used.extend(
2028 self.$table()
2029 .iter()
2030 .filter_map(|value| value.uid.as_deref().map(str::to_owned)),
2031 );)+
2032 };
2033 }
2034 collect!(
2035 buses,
2036 loads,
2037 shunts,
2038 static_var_compensators,
2039 branches,
2040 switches,
2041 generators,
2042 storage,
2043 hvdc,
2044 transformers_3w
2045 );
2046 used
2047 }
2048}
2049
2050fn generated_uids_is_empty(uids: &std::sync::Arc<BTreeSet<String>>) -> bool {
2051 uids.is_empty()
2052}
2053
2054fn assign_missing_ids<T>(
2055 values: &mut [T],
2056 uid: impl for<'a> Fn(&'a T) -> Option<&'a str>,
2057 mut set_uid: impl FnMut(&mut T, String),
2058 stem: impl Fn(&T) -> String,
2059 used: &mut HashSet<String>,
2060 next_suffix: &mut HashMap<String, usize>,
2061) -> Vec<String> {
2062 let mut generated = Vec::new();
2063 for value in values {
2064 if uid(value).is_some() {
2065 continue;
2066 }
2067 let stem = stem(value);
2068 let suffix = match next_suffix.get_mut(&stem) {
2069 Some(suffix) => suffix,
2070 None => next_suffix.entry(stem.clone()).or_insert(1),
2071 };
2072 loop {
2073 let candidate = if *suffix == 1 {
2074 stem.clone()
2075 } else {
2076 format!("{stem}-{suffix}")
2077 };
2078 *suffix += 1;
2079 if !used.contains(&candidate) {
2080 used.insert(candidate.clone());
2081 set_uid(value, candidate.clone());
2082 generated.push(candidate);
2083 break;
2084 }
2085 }
2086 }
2087 generated
2088}
2089
2090#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2091#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2092#[non_exhaustive]
2093pub struct Bus {
2094 pub id: BusId,
2096 pub kind: BusType,
2097 pub vm: f64,
2099 pub va: f64,
2101 pub base_kv: f64,
2102 pub vmax: f64,
2103 pub vmin: f64,
2104 #[serde(default)]
2110 pub evhi: Option<f64>,
2111 #[serde(default)]
2112 pub evlo: Option<f64>,
2113 pub area: usize,
2114 pub zone: usize,
2115 pub name: Option<String>,
2116 #[serde(default, skip_serializing_if = "Option::is_none")]
2121 pub uid: Option<String>,
2122 #[serde(default, skip_serializing_if = "Option::is_none")]
2124 pub location: Option<Location>,
2125 pub extras: Extras,
2126}
2127
2128impl Bus {
2129 #[must_use]
2130 pub fn new(id: BusId, kind: BusType, base_kv: f64) -> Self {
2131 Self {
2132 id,
2133 kind,
2134 vm: 1.0,
2135 va: 0.0,
2136 base_kv,
2137 vmax: 1.1,
2138 vmin: 0.9,
2139 evhi: None,
2140 evlo: None,
2141 area: 1,
2142 zone: 1,
2143 name: None,
2144 uid: None,
2145 location: None,
2146 extras: Extras::new(),
2147 }
2148 }
2149}
2150
2151#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2152#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2153#[non_exhaustive]
2154pub struct Load {
2155 pub bus: BusId,
2156 pub p: f64,
2158 pub q: f64,
2160 #[serde(default)]
2162 pub voltage_model: Option<LoadVoltageModel>,
2163 pub in_service: bool,
2164 #[serde(default, skip_serializing_if = "Option::is_none")]
2166 pub uid: Option<String>,
2167 pub extras: Extras,
2168}
2169
2170impl Load {
2171 #[must_use]
2172 pub fn new(bus: BusId, p: f64, q: f64) -> Self {
2173 Self {
2174 bus,
2175 p,
2176 q,
2177 voltage_model: None,
2178 in_service: true,
2179 uid: None,
2180 extras: Extras::new(),
2181 }
2182 }
2183}
2184
2185#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2187#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2188#[serde(tag = "kind", rename_all = "snake_case")]
2189#[non_exhaustive]
2190pub enum LoadVoltageModel {
2191 ConstantPower,
2193 Zip {
2196 p_constant_power: f64,
2197 q_constant_power: f64,
2198 p_constant_current: f64,
2199 q_constant_current: f64,
2200 p_constant_impedance: f64,
2201 q_constant_impedance: f64,
2202 #[serde(default)]
2203 v_nom: Option<f64>,
2204 #[serde(default)]
2207 load_type: Option<i32>,
2208 #[serde(default)]
2210 scaling: Option<f64>,
2211 },
2212 Exponential {
2215 p: f64,
2216 q: f64,
2217 #[serde(default)]
2218 v_nom: Option<f64>,
2219 gamma_p: f64,
2220 gamma_q: f64,
2221 },
2222}
2223
2224impl LoadVoltageModel {
2225 #[must_use]
2226 pub fn has_non_matpower_fields(&self) -> bool {
2227 match self {
2228 Self::ConstantPower => false,
2229 Self::Zip {
2230 p_constant_current,
2231 q_constant_current,
2232 p_constant_impedance,
2233 q_constant_impedance,
2234 v_nom,
2235 load_type,
2236 scaling,
2237 ..
2238 } => {
2239 *p_constant_current != 0.0
2240 || *q_constant_current != 0.0
2241 || *p_constant_impedance != 0.0
2242 || *q_constant_impedance != 0.0
2243 || v_nom.is_some()
2244 || load_type.is_some()
2245 || scaling.is_some()
2246 }
2247 Self::Exponential { .. } => true,
2248 }
2249 }
2250}
2251
2252#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2253#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2254#[non_exhaustive]
2255pub struct Shunt {
2256 pub bus: BusId,
2257 pub g: f64,
2259 pub b: f64,
2262 pub in_service: bool,
2263 #[serde(default, skip_serializing_if = "Option::is_none")]
2266 pub section_count: Option<u32>,
2267 #[serde(default)]
2271 pub control: Option<SwitchedShuntControl>,
2272 #[serde(default, skip_serializing_if = "Option::is_none")]
2274 pub uid: Option<String>,
2275 pub extras: Extras,
2276}
2277
2278impl Shunt {
2279 #[must_use]
2280 pub fn new(bus: BusId, g: f64, b: f64) -> Self {
2281 Self {
2282 bus,
2283 g,
2284 b,
2285 in_service: true,
2286 section_count: None,
2287 control: None,
2288 uid: None,
2289 extras: Extras::new(),
2290 }
2291 }
2292}
2293
2294#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2296#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2297#[serde(rename_all = "snake_case")]
2298#[non_exhaustive]
2299pub enum SwitchedShuntMode {
2300 Locked,
2302 Continuous,
2304 Discrete,
2306}
2307
2308#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2310#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2311#[non_exhaustive]
2312pub struct ShuntBlock {
2313 pub steps: u32,
2314 pub g: f64,
2316 pub b: f64,
2318}
2319
2320impl ShuntBlock {
2321 #[must_use]
2322 pub const fn new(steps: u32, b: f64) -> Self {
2323 Self { steps, g: 0.0, b }
2324 }
2325
2326 #[must_use]
2327 pub const fn with_admittance(steps: u32, g: f64, b: f64) -> Self {
2328 Self { steps, g, b }
2329 }
2330}
2331
2332#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2337#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2338#[non_exhaustive]
2339pub struct SwitchedShuntControl {
2340 pub mode: SwitchedShuntMode,
2341 pub vhigh: f64,
2343 pub vlow: f64,
2344 pub control_bus: Option<BusId>,
2346 #[serde(default, skip_serializing_if = "Option::is_none")]
2348 pub regulating_terminal: Option<TerminalReference>,
2349 pub rmpct: f64,
2351 pub blocks: Vec<ShuntBlock>,
2352}
2353
2354impl SwitchedShuntControl {
2355 #[must_use]
2356 pub fn new(mode: SwitchedShuntMode, vhigh: f64, vlow: f64, blocks: Vec<ShuntBlock>) -> Self {
2357 Self {
2358 mode,
2359 vhigh,
2360 vlow,
2361 control_bus: None,
2362 regulating_terminal: None,
2363 rmpct: 100.0,
2364 blocks,
2365 }
2366 }
2367}
2368
2369#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2371#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2372#[serde(rename_all = "snake_case")]
2373#[non_exhaustive]
2374pub enum StaticVarCompensatorRegulationMode {
2375 Voltage,
2376 ReactivePower,
2377}
2378
2379#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2381#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2382#[non_exhaustive]
2383pub struct StaticVarCompensator {
2384 pub bus: BusId,
2385 pub b_min_siemens: f64,
2387 pub b_max_siemens: f64,
2388 pub voltage_setpoint_kv: f64,
2389 pub reactive_power_setpoint_mvar: f64,
2390 pub regulation_mode: StaticVarCompensatorRegulationMode,
2391 pub regulating: bool,
2392 #[serde(default, skip_serializing_if = "Option::is_none")]
2393 pub regulating_terminal: Option<TerminalReference>,
2394 pub p: f64,
2396 pub q: f64,
2397 pub in_service: bool,
2398 #[serde(default, skip_serializing_if = "Option::is_none")]
2399 pub uid: Option<String>,
2400 pub extras: Extras,
2401}
2402
2403impl StaticVarCompensator {
2404 #[must_use]
2405 pub fn new(bus: BusId, b_min_siemens: f64, b_max_siemens: f64) -> Self {
2406 Self {
2407 bus,
2408 b_min_siemens,
2409 b_max_siemens,
2410 voltage_setpoint_kv: 0.0,
2411 reactive_power_setpoint_mvar: 0.0,
2412 regulation_mode: StaticVarCompensatorRegulationMode::Voltage,
2413 regulating: false,
2414 regulating_terminal: None,
2415 p: 0.0,
2416 q: 0.0,
2417 in_service: true,
2418 uid: None,
2419 extras: Extras::new(),
2420 }
2421 }
2422}
2423
2424#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2425#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2426#[non_exhaustive]
2427pub struct Branch {
2428 #[serde(default, skip_serializing_if = "Option::is_none")]
2430 pub name: Option<String>,
2431 pub from: BusId,
2432 pub to: BusId,
2433 pub r: f64,
2435 pub x: f64,
2437 pub b: f64,
2441 #[serde(default)]
2444 pub charging: Option<BranchCharging>,
2445 pub rate_a: f64,
2446 pub rate_b: f64,
2447 pub rate_c: f64,
2448 #[serde(default)]
2451 pub rating_sets: Vec<BranchRatingSet>,
2452 #[serde(default)]
2454 pub current_ratings: Option<BranchCurrentRatings>,
2455 pub tap: f64,
2457 pub shift: f64,
2459 pub in_service: bool,
2460 pub angmin: f64,
2461 pub angmax: f64,
2462 #[serde(default)]
2467 pub control: Option<TransformerControl>,
2468 #[serde(default)]
2470 pub solution: Option<BranchSolution>,
2471 #[serde(default, skip_serializing_if = "Option::is_none")]
2473 pub uid: Option<String>,
2474 #[serde(default, skip_serializing_if = "Option::is_none")]
2479 pub route: Option<Vec<Location>>,
2480 pub extras: Extras,
2481}
2482
2483#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2485#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2486#[non_exhaustive]
2487pub struct BranchRatingSet {
2488 pub name: String,
2489 pub rate_mva: f64,
2490}
2491
2492impl BranchRatingSet {
2493 #[must_use]
2494 pub fn new(name: impl Into<String>, rate_mva: f64) -> Self {
2495 Self {
2496 name: name.into(),
2497 rate_mva,
2498 }
2499 }
2500}
2501
2502#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
2505#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2506#[non_exhaustive]
2507pub struct BranchCharging {
2508 pub g_fr: f64,
2509 pub b_fr: f64,
2510 pub g_to: f64,
2511 pub b_to: f64,
2512}
2513
2514impl BranchCharging {
2515 #[must_use]
2516 pub const fn new(g_fr: f64, b_fr: f64, g_to: f64, b_to: f64) -> Self {
2517 Self {
2518 g_fr,
2519 b_fr,
2520 g_to,
2521 b_to,
2522 }
2523 }
2524
2525 #[must_use]
2526 pub fn from_total_b(b: f64) -> Self {
2527 Self {
2528 g_fr: 0.0,
2529 b_fr: b / 2.0,
2530 g_to: 0.0,
2531 b_to: b / 2.0,
2532 }
2533 }
2534
2535 #[must_use]
2536 pub fn calc_total_b(self) -> f64 {
2537 self.b_fr + self.b_to
2538 }
2539
2540 #[must_use]
2541 pub fn calc_total_g(self) -> f64 {
2542 self.g_fr + self.g_to
2543 }
2544
2545 #[must_use]
2546 pub fn is_matpower_symmetric(self) -> bool {
2547 self.g_fr.abs() <= f64::EPSILON
2548 && self.g_to.abs() <= f64::EPSILON
2549 && (self.b_fr - self.b_to).abs() <= f64::EPSILON
2550 }
2551}
2552
2553#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
2555#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2556#[non_exhaustive]
2557pub struct BranchCurrentRatings {
2558 pub c_rating_a: f64,
2559 pub c_rating_b: f64,
2560 pub c_rating_c: f64,
2561}
2562
2563impl BranchCurrentRatings {
2564 #[must_use]
2565 pub const fn new(c_rating_a: f64, c_rating_b: f64, c_rating_c: f64) -> Self {
2566 Self {
2567 c_rating_a,
2568 c_rating_b,
2569 c_rating_c,
2570 }
2571 }
2572}
2573
2574#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
2576#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2577#[non_exhaustive]
2578pub struct BranchSolution {
2579 pub pf: f64,
2580 pub qf: f64,
2581 pub pt: f64,
2582 pub qt: f64,
2583}
2584
2585impl BranchSolution {
2586 #[must_use]
2587 pub const fn new(pf: f64, qf: f64, pt: f64, qt: f64) -> Self {
2588 Self { pf, qf, pt, qt }
2589 }
2590}
2591
2592impl Branch {
2593 #[must_use]
2594 pub fn new(from: BusId, to: BusId, r: f64, x: f64) -> Self {
2595 Self {
2596 name: None,
2597 from,
2598 to,
2599 r,
2600 x,
2601 b: 0.0,
2602 charging: None,
2603 rate_a: 0.0,
2604 rate_b: 0.0,
2605 rate_c: 0.0,
2606 rating_sets: Vec::new(),
2607 current_ratings: None,
2608 tap: 0.0,
2609 shift: 0.0,
2610 in_service: true,
2611 angmin: -360.0,
2612 angmax: 360.0,
2613 control: None,
2614 solution: None,
2615 uid: None,
2616 route: None,
2617 extras: Extras::new(),
2618 }
2619 }
2620
2621 #[must_use]
2623 pub fn calc_effective_tap(&self) -> f64 {
2624 if self.tap == 0.0 { 1.0 } else { self.tap }
2625 }
2626
2627 pub fn calc_divisible_tap(&self, row: usize) -> Result<f64> {
2636 let tap = self.calc_effective_tap();
2637 if !tap.is_finite() || tap.abs() < crate::dc::MIN_DIVISIBLE_MAGNITUDE {
2638 return Err(Error::DegenerateTap { row, tap });
2639 }
2640 Ok(tap)
2641 }
2642
2643 #[must_use]
2646 pub fn calc_terminal_charging(&self) -> BranchCharging {
2647 self.charging
2648 .unwrap_or_else(|| BranchCharging::from_total_b(self.b))
2649 }
2650
2651 pub fn calc_series_admittance(&self, row: usize) -> Result<Option<(f64, f64)>> {
2663 calc_series_admittance_of(self.r, self.x, row)
2664 }
2665
2666 #[must_use]
2692 pub fn synthesize_rate_a(
2693 &self,
2694 angle_window_rad: f64,
2695 (fr_vmin, fr_vmax): (f64, f64),
2696 (to_vmin, to_vmax): (f64, f64),
2697 ) -> f64 {
2698 let zmag = self.r.hypot(self.x);
2701 if zmag < crate::dc::MIN_DIVISIBLE_MAGNITUDE {
2702 return 0.0;
2703 }
2704 let window = angle_window_rad.abs().min(std::f64::consts::PI);
2705 let cos_window = window.cos();
2706 let separation = |vf: f64, vt: f64| {
2710 (vf * vf + vt * vt - 2.0 * vf * vt * cos_window)
2711 .max(0.0)
2712 .sqrt()
2713 };
2714 let widest = separation(fr_vmax, to_vmax)
2715 .max(separation(fr_vmax, to_vmin))
2716 .max(separation(fr_vmin, to_vmax))
2717 .max(separation(fr_vmin, to_vmin));
2718 fr_vmax.max(to_vmax) * widest / zmag
2719 }
2720
2721 #[must_use]
2724 pub fn calc_total_charging_b(&self) -> f64 {
2725 self.calc_terminal_charging().calc_total_b()
2726 }
2727
2728 #[must_use]
2730 pub fn has_non_matpower_charging(&self) -> bool {
2731 self.charging
2732 .is_some_and(|charging| !charging.is_matpower_symmetric())
2733 }
2734
2735 #[must_use]
2738 pub fn is_transformer(&self) -> bool {
2739 self.tap != 0.0 || self.shift != 0.0
2740 }
2741
2742 #[must_use]
2746 pub fn has_angle_limits(&self) -> bool {
2747 self.angmin > -360.0 || self.angmax < 360.0
2748 }
2749}
2750
2751pub fn calc_series_admittance_of(r: f64, x: f64, row: usize) -> Result<Option<(f64, f64)>> {
2768 let magnitude = r.hypot(x);
2769 if magnitude < crate::dc::MIN_DIVISIBLE_MAGNITUDE {
2770 return Ok(None);
2771 }
2772 if !magnitude.is_finite() {
2773 return Err(Error::NonFiniteSusceptance { row });
2774 }
2775 Ok(Some(crate::dc::series_admittance_parts(r, x)))
2776}
2777
2778#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2781#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2782#[non_exhaustive]
2783pub struct Switch {
2784 pub from: BusId,
2785 pub to: BusId,
2786 pub closed: bool,
2787 #[serde(default)]
2788 pub thermal_rating: Option<f64>,
2789 #[serde(default)]
2790 pub current_rating: Option<f64>,
2791 #[serde(default)]
2792 pub pf: Option<f64>,
2793 #[serde(default)]
2794 pub qf: Option<f64>,
2795 #[serde(default)]
2796 pub pt: Option<f64>,
2797 #[serde(default)]
2798 pub qt: Option<f64>,
2799 #[serde(default, skip_serializing_if = "Option::is_none")]
2801 pub uid: Option<String>,
2802 pub extras: Extras,
2803}
2804
2805impl Switch {
2806 #[must_use]
2807 pub fn new(from: BusId, to: BusId, closed: bool) -> Self {
2808 Self {
2809 from,
2810 to,
2811 closed,
2812 thermal_rating: None,
2813 current_rating: None,
2814 pf: None,
2815 qf: None,
2816 pt: None,
2817 qt: None,
2818 uid: None,
2819 extras: Extras::new(),
2820 }
2821 }
2822}
2823
2824#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2827#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2828#[serde(rename_all = "snake_case")]
2829#[non_exhaustive]
2830pub enum TransformerControlMode {
2831 Fixed,
2833 Voltage,
2835 ReactiveFlow,
2837 ActiveFlow,
2839 DcLineQuantity,
2841 AsymmetricActiveFlow,
2843}
2844
2845#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2855#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2856#[non_exhaustive]
2857pub struct TransformerControl {
2858 pub mode: TransformerControlMode,
2859 pub enabled: bool,
2862 pub controlled_bus: Option<BusId>,
2863 #[serde(default)]
2866 pub controlled_bus_on_winding_side: bool,
2867 #[serde(default, skip_serializing_if = "Option::is_none")]
2869 pub regulating_terminal: Option<TerminalReference>,
2870 pub tap_min: f64,
2871 pub tap_max: f64,
2872 pub band_min: f64,
2873 pub band_max: f64,
2874 pub ntp: u32,
2875 pub mva_base: f64,
2876 #[serde(default, skip_serializing_if = "Option::is_none")]
2879 pub winding_connection_angle: Option<f64>,
2880}
2881
2882impl Default for TransformerControl {
2883 fn default() -> Self {
2884 TransformerControl {
2886 mode: TransformerControlMode::Fixed,
2887 enabled: false,
2888 controlled_bus: None,
2889 controlled_bus_on_winding_side: false,
2890 regulating_terminal: None,
2891 tap_min: 0.9,
2892 tap_max: 1.1,
2893 band_min: 0.9,
2894 band_max: 1.1,
2895 ntp: 33,
2896 mva_base: 0.0,
2897 winding_connection_angle: None,
2898 }
2899 }
2900}
2901
2902impl TransformerControl {
2903 #[must_use]
2904 pub fn new(mode: TransformerControlMode) -> Self {
2905 Self {
2906 mode,
2907 enabled: mode != TransformerControlMode::Fixed,
2908 ..Self::default()
2909 }
2910 }
2911}
2912
2913#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2923#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2924#[non_exhaustive]
2925pub struct ActivePowerControl {
2926 pub participate: bool,
2927 #[serde(default, skip_serializing_if = "Option::is_none")]
2928 pub droop_percent: Option<f64>,
2929 #[serde(default, skip_serializing_if = "Option::is_none")]
2930 pub participation_factor: Option<f64>,
2931 #[serde(default, skip_serializing_if = "Option::is_none")]
2932 pub minimum_target_active_power_mw: Option<f64>,
2933 #[serde(default, skip_serializing_if = "Option::is_none")]
2934 pub maximum_target_active_power_mw: Option<f64>,
2935}
2936
2937impl ActivePowerControl {
2938 #[must_use]
2939 pub fn new(participate: bool) -> Self {
2940 Self {
2941 participate,
2942 droop_percent: None,
2943 participation_factor: None,
2944 minimum_target_active_power_mw: None,
2945 maximum_target_active_power_mw: None,
2946 }
2947 }
2948}
2949
2950#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2951#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2952#[non_exhaustive]
2953pub struct Generator {
2954 pub bus: BusId,
2955 #[serde(default)]
2957 pub energy_source: GeneratorEnergySource,
2958 pub pg: f64,
2960 pub qg: f64,
2962 pub pmax: f64,
2963 pub pmin: f64,
2964 pub qmax: f64,
2965 pub qmin: f64,
2966 pub vg: f64,
2968 pub mbase: f64,
2969 pub in_service: bool,
2970 pub cost: Option<GenCost>,
2971 #[serde(default = "default_caps", with = "caps_serde")]
2980 #[cfg_attr(feature = "schema", schemars(with = "BTreeMap<String, f64>"))]
2981 pub caps: GenCaps,
2982 #[serde(default = "default_voltage_regulation_on")]
2986 pub voltage_regulation_on: bool,
2987 #[serde(default, skip_serializing_if = "Option::is_none")]
2991 pub regulating_terminal: Option<TerminalReference>,
2992 #[serde(default)]
2999 pub regulated_bus: Option<BusId>,
3000 #[serde(default, skip_serializing_if = "Option::is_none")]
3002 pub active_power_control: Option<ActivePowerControl>,
3003 #[serde(default, skip_serializing_if = "Option::is_none")]
3005 pub uid: Option<String>,
3006}
3007
3008impl Generator {
3009 #[must_use]
3010 pub fn new(bus: BusId) -> Self {
3011 Self {
3012 bus,
3013 energy_source: GeneratorEnergySource::Other,
3014 pg: 0.0,
3015 qg: 0.0,
3016 pmax: 0.0,
3017 pmin: 0.0,
3018 qmax: 0.0,
3019 qmin: 0.0,
3020 vg: 1.0,
3021 mbase: 0.0,
3022 in_service: true,
3023 cost: None,
3024 caps: default_caps(),
3025 voltage_regulation_on: true,
3026 regulating_terminal: None,
3027 regulated_bus: None,
3028 active_power_control: None,
3029 uid: None,
3030 }
3031 }
3032
3033 #[must_use]
3036 pub fn has_caps(&self) -> bool {
3037 self.caps.iter().any(Option::is_some)
3038 }
3039}
3040
3041#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
3043#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3044#[serde(rename_all = "snake_case")]
3045#[non_exhaustive]
3046pub enum GeneratorEnergySource {
3047 Hydro,
3048 Nuclear,
3049 Wind,
3050 Thermal,
3051 Solar,
3052 #[default]
3053 Other,
3054}
3055
3056const fn default_voltage_regulation_on() -> bool {
3057 true
3058}
3059
3060pub type GenCaps = [Option<f64>; GEN_EXTRA_KEYS.len()];
3062
3063fn default_caps() -> GenCaps {
3065 [None; GEN_EXTRA_KEYS.len()]
3066}
3067
3068mod caps_serde {
3079 use super::{GEN_EXTRA_KEYS, GenCaps};
3080 use serde::de::{Deserialize, Deserializer};
3081 use serde::ser::{SerializeMap, Serializer};
3082 use std::collections::BTreeMap;
3083
3084 pub(super) fn serialize<S: Serializer>(caps: &GenCaps, s: S) -> Result<S::Ok, S::Error> {
3085 let present = caps.iter().filter(|v| v.is_some()).count();
3086 let mut map = s.serialize_map(Some(present))?;
3087 for (key, slot) in GEN_EXTRA_KEYS.iter().zip(caps.iter()) {
3088 if let Some(value) = slot {
3089 map.serialize_entry(key, value)?;
3090 }
3091 }
3092 map.end()
3093 }
3094
3095 pub(super) fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<GenCaps, D::Error> {
3096 let named = Option::<BTreeMap<String, f64>>::deserialize(d)?.unwrap_or_default();
3101 let mut caps: GenCaps = [None; GEN_EXTRA_KEYS.len()];
3102 for (slot, key) in caps.iter_mut().zip(GEN_EXTRA_KEYS.iter()) {
3103 *slot = named.get(*key).copied();
3104 }
3105 Ok(caps)
3106 }
3107}
3108
3109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3110#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3111#[non_exhaustive]
3112pub struct Storage {
3113 pub bus: BusId,
3114 pub ps: f64,
3115 pub qs: f64,
3116 pub energy: f64,
3117 pub energy_rating: f64,
3118 pub charge_rating: f64,
3119 pub discharge_rating: f64,
3120 pub charge_efficiency: f64,
3121 pub discharge_efficiency: f64,
3122 pub thermal_rating: f64,
3123 #[serde(default)]
3124 pub current_rating: Option<f64>,
3125 pub qmin: f64,
3126 pub qmax: f64,
3127 pub r: f64,
3128 pub x: f64,
3129 pub p_loss: f64,
3130 pub q_loss: f64,
3131 pub in_service: bool,
3132 #[serde(default, skip_serializing_if = "Option::is_none")]
3134 pub active_power_control: Option<ActivePowerControl>,
3135 #[serde(default, skip_serializing_if = "Option::is_none")]
3137 pub uid: Option<String>,
3138 pub extras: Extras,
3139}
3140
3141impl Storage {
3142 #[must_use]
3143 pub fn new(bus: BusId) -> Self {
3144 Self {
3145 bus,
3146 ps: 0.0,
3147 qs: 0.0,
3148 energy: 0.0,
3149 energy_rating: 0.0,
3150 charge_rating: 0.0,
3151 discharge_rating: 0.0,
3152 charge_efficiency: 1.0,
3153 discharge_efficiency: 1.0,
3154 thermal_rating: 0.0,
3155 current_rating: None,
3156 qmin: 0.0,
3157 qmax: 0.0,
3158 r: 0.0,
3159 x: 0.0,
3160 p_loss: 0.0,
3161 q_loss: 0.0,
3162 in_service: true,
3163 active_power_control: None,
3164 uid: None,
3165 extras: Extras::new(),
3166 }
3167 }
3168}
3169
3170#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
3178#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3179#[serde(rename_all = "snake_case")]
3180#[non_exhaustive]
3181pub enum HvdcConvertersMode {
3182 Side1RectifierSide2Inverter,
3183 Side1InverterSide2Rectifier,
3184}
3185
3186#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
3188#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3189#[serde(rename_all = "snake_case")]
3190#[non_exhaustive]
3191pub enum HvdcConverterKind {
3192 Vsc,
3193 Lcc,
3194}
3195
3196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3198#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3199#[non_exhaustive]
3200pub struct HvdcConverter {
3201 pub component: ComponentId,
3202 pub kind: HvdcConverterKind,
3203 pub loss_factor_percent: f64,
3205 #[serde(default, skip_serializing_if = "Option::is_none")]
3206 pub voltage_regulator_on: Option<bool>,
3207 #[serde(default, skip_serializing_if = "Option::is_none")]
3208 pub voltage_setpoint_kv: Option<f64>,
3209 #[serde(default, skip_serializing_if = "Option::is_none")]
3210 pub reactive_power_setpoint_mvar: Option<f64>,
3211 #[serde(default, skip_serializing_if = "Option::is_none")]
3212 pub power_factor: Option<f64>,
3213 #[serde(default, skip_serializing_if = "Option::is_none")]
3214 pub regulating_terminal: Option<TerminalReference>,
3215}
3216
3217#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3218#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3219#[non_exhaustive]
3220pub struct Hvdc {
3221 pub from: BusId,
3222 pub to: BusId,
3223 pub in_service: bool,
3224 pub pf: f64,
3225 pub pt: f64,
3226 pub qf: f64,
3227 pub qt: f64,
3228 pub vf: f64,
3229 pub vt: f64,
3230 pub pmin: f64,
3231 pub pmax: f64,
3232 pub qminf: f64,
3233 pub qmaxf: f64,
3234 pub qmint: f64,
3235 pub qmaxt: f64,
3236 pub loss0: f64,
3237 pub loss1: f64,
3238 #[serde(default, skip_serializing_if = "Option::is_none")]
3241 pub resistance_ohm: Option<f64>,
3242 #[serde(default, skip_serializing_if = "Option::is_none")]
3243 pub nominal_voltage_kv: Option<f64>,
3244 #[serde(default, skip_serializing_if = "Option::is_none")]
3245 pub converters_mode: Option<HvdcConvertersMode>,
3246 #[serde(default, skip_serializing_if = "Option::is_none")]
3247 pub converter1: Option<HvdcConverter>,
3248 #[serde(default, skip_serializing_if = "Option::is_none")]
3249 pub converter2: Option<HvdcConverter>,
3250 #[serde(default)]
3251 pub cost: Option<GenCost>,
3252 #[serde(default, skip_serializing_if = "Option::is_none")]
3254 pub uid: Option<String>,
3255 pub extras: Extras,
3256}
3257
3258impl Hvdc {
3259 #[must_use]
3268 pub fn calc_delivered_power(pf: f64, loss0: f64, loss1: f64) -> f64 {
3269 pf - loss0 - loss1 * pf
3270 }
3271
3272 #[must_use]
3276 pub fn pt_matches_loss_model(&self, tol: f64) -> bool {
3277 (self.pt - Self::calc_delivered_power(self.pf, self.loss0, self.loss1)).abs() <= tol
3278 }
3279
3280 #[must_use]
3281 pub fn new(from: BusId, to: BusId) -> Self {
3282 Self {
3283 from,
3284 to,
3285 in_service: true,
3286 pf: 0.0,
3287 pt: 0.0,
3288 qf: 0.0,
3289 qt: 0.0,
3290 vf: 1.0,
3291 vt: 1.0,
3292 pmin: 0.0,
3293 pmax: 0.0,
3294 qminf: 0.0,
3295 qmaxf: 0.0,
3296 qmint: 0.0,
3297 qmaxt: 0.0,
3298 loss0: 0.0,
3299 loss1: 0.0,
3300 resistance_ohm: None,
3301 nominal_voltage_kv: None,
3302 converters_mode: None,
3303 converter1: None,
3304 converter2: None,
3305 cost: None,
3306 uid: None,
3307 extras: Extras::new(),
3308 }
3309 }
3310}
3311
3312#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3319#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3320#[non_exhaustive]
3321pub struct Area {
3322 pub number: usize,
3323 pub slack_bus: Option<BusId>,
3325 pub net_interchange: f64,
3327 pub tolerance: f64,
3329 pub name: Option<String>,
3330 #[serde(default, skip_serializing_if = "Option::is_none")]
3333 pub uid: Option<String>,
3334 #[serde(default, skip_serializing_if = "Option::is_none")]
3337 pub area_type: Option<String>,
3338}
3339
3340impl Area {
3341 #[must_use]
3342 pub fn new(number: usize) -> Self {
3343 Self {
3344 number,
3345 slack_bus: None,
3346 net_interchange: 0.0,
3347 tolerance: 0.0,
3348 name: None,
3349 uid: None,
3350 area_type: None,
3351 }
3352 }
3353}
3354
3355#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
3364#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3365#[non_exhaustive]
3366pub struct SolverParams {
3367 pub newton_tolerance: Option<f64>,
3369 pub max_iterations: Option<u32>,
3371 pub zero_impedance_threshold: Option<f64>,
3373 pub adjust_taps: Option<bool>,
3375 pub adjust_area_interchange: Option<bool>,
3377 pub adjust_phase_shift: Option<bool>,
3379 pub adjust_dc_taps: Option<bool>,
3381 pub adjust_switched_shunt: Option<bool>,
3383}
3384
3385impl SolverParams {
3386 #[must_use]
3387 pub fn new() -> Self {
3388 Self::default()
3389 }
3390
3391 #[must_use]
3393 pub fn is_empty(&self) -> bool {
3394 *self == SolverParams::default()
3395 }
3396}
3397
3398#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
3410#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3411#[non_exhaustive]
3412pub struct Impedance {
3413 pub r: f64,
3414 pub x: f64,
3415 pub base_mva: f64,
3416}
3417
3418impl Impedance {
3419 #[must_use]
3420 pub const fn new(r: f64, x: f64, base_mva: f64) -> Self {
3421 Self { r, x, base_mva }
3422 }
3423}
3424
3425#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3428#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3429#[non_exhaustive]
3430pub struct Winding {
3431 pub bus: BusId,
3432 pub tap: f64,
3434 pub shift: f64,
3436 pub nominal_kv: f64,
3438 pub rate_a: f64,
3439 pub rate_b: f64,
3440 pub rate_c: f64,
3441 #[serde(default, skip_serializing_if = "Option::is_none")]
3443 pub control: Option<TransformerControl>,
3444}
3445
3446impl Winding {
3447 #[must_use]
3448 pub fn new(bus: BusId) -> Self {
3449 Self {
3450 bus,
3451 tap: 1.0,
3452 shift: 0.0,
3453 nominal_kv: 0.0,
3454 rate_a: 0.0,
3455 rate_b: 0.0,
3456 rate_c: 0.0,
3457 control: None,
3458 }
3459 }
3460}
3461
3462#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3472#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3473#[non_exhaustive]
3474pub struct Transformer3W {
3475 pub windings: [Winding; 3],
3477 pub z: [Impedance; 3],
3481 pub star_vm: f64,
3483 pub star_va: f64,
3484 pub mag_g: f64,
3486 pub mag_b: f64,
3487 pub in_service: bool,
3488 pub name: Option<String>,
3489 #[serde(default, skip_serializing_if = "Option::is_none")]
3491 pub uid: Option<String>,
3492 pub extras: Extras,
3493}
3494
3495impl Transformer3W {
3496 #[must_use]
3497 pub fn new(windings: [Winding; 3], z: [Impedance; 3]) -> Self {
3498 Self {
3499 windings,
3500 z,
3501 star_vm: 1.0,
3502 star_va: 0.0,
3503 mag_g: 0.0,
3504 mag_b: 0.0,
3505 in_service: true,
3506 name: None,
3507 uid: None,
3508 extras: Extras::new(),
3509 }
3510 }
3511
3512 #[must_use]
3519 pub fn calc_star_impedances(&self) -> [(f64, f64); 3] {
3520 let [z12, z23, z31] = self.z;
3521 let half = |a: f64, b: f64, c: f64| (a + b - c) / 2.0;
3522 [
3523 (half(z12.r, z31.r, z23.r), half(z12.x, z31.x, z23.x)),
3524 (half(z12.r, z23.r, z31.r), half(z12.x, z23.x, z31.x)),
3525 (half(z23.r, z31.r, z12.r), half(z23.x, z31.x, z12.x)),
3526 ]
3527 }
3528
3529 #[must_use]
3536 pub fn to_star_expansion(&self, star_id: BusId) -> (Bus, [Branch; 3]) {
3537 let star = Bus {
3538 id: star_id,
3539 kind: BusType::Pq,
3540 vm: self.star_vm,
3541 va: self.star_va,
3542 base_kv: self.windings[0].nominal_kv,
3543 vmax: 1.1,
3544 vmin: 0.9,
3545 evhi: None,
3546 evlo: None,
3547 area: 0,
3548 zone: 0,
3549 name: self.name.clone(),
3550 uid: self.uid.clone(),
3551 location: None,
3552 extras: Extras::new(),
3553 };
3554 let zs = self.calc_star_impedances();
3555 let branch = |w: &Winding, (r, x): (f64, f64)| Branch {
3556 name: None,
3557 from: w.bus,
3558 to: star_id,
3559 r,
3560 x,
3561 b: 0.0,
3562 charging: None,
3563 rate_a: w.rate_a,
3564 rate_b: w.rate_b,
3565 rate_c: w.rate_c,
3566 rating_sets: Vec::new(),
3567 current_ratings: None,
3568 tap: w.tap,
3569 shift: w.shift,
3570 in_service: self.in_service,
3571 angmin: -360.0,
3572 angmax: 360.0,
3573 control: w.control.clone(),
3574 solution: None,
3575 uid: None,
3576 route: None,
3577 extras: Extras::new(),
3578 };
3579 let branches = [
3580 branch(&self.windings[0], zs[0]),
3581 branch(&self.windings[1], zs[1]),
3582 branch(&self.windings[2], zs[2]),
3583 ];
3584 (star, branches)
3585 }
3586}
3587
3588pub const GEN_EXTRA_KEYS: [&str; 11] = [
3591 "pc1", "pc2", "qc1min", "qc1max", "qc2min", "qc2max", "ramp_agc", "ramp_10", "ramp_30",
3592 "ramp_q", "apf",
3593];
3594
3595#[derive(Debug, Clone, PartialEq)]
3602pub(crate) struct ValueFinding {
3603 pub element: String,
3605 pub table: &'static str,
3610 pub index: usize,
3613 pub field: &'static str,
3614 pub old: f64,
3615 pub new: f64,
3616 pub reason: &'static str,
3617}
3618
3619impl ValueFinding {
3620 pub(crate) fn into_diagnostic(self) -> crate::Diagnostic {
3624 let mut details = serde_json::Map::new();
3625 details.insert("element".to_owned(), serde_json::json!(self.element));
3626 details.insert("field".to_owned(), serde_json::json!(self.field));
3627 details.insert("value".to_owned(), serde_json::json!(self.old));
3628 details.insert("repaired_value".to_owned(), serde_json::json!(self.new));
3629 details.insert("reason".to_owned(), serde_json::json!(self.reason));
3630 crate::Diagnostic::of(
3631 &crate::diagnostics::codes::VALIDATE_BALANCED_VALUE_DOMAIN,
3632 format!(
3633 "{}: `{}` is {} ({}); the repair sets {}",
3634 self.element, self.field, self.old, self.reason, self.new
3635 ),
3636 )
3637 .with_target(format!("/{}/{}/{}", self.table, self.index, self.field))
3638 .expect("scan-built targets are nonempty and bounded")
3639 .with_details(details)
3640 .expect("scan-built details stay within the record bounds")
3641 }
3642}
3643
3644pub fn repair_values(
3655 module: powerio_core::PioModule<BalancedNetwork>,
3656) -> std::result::Result<powerio_core::PioModule<BalancedNetwork>, powerio_core::Error> {
3657 let repair_ordinal = module
3658 .history()
3659 .iter()
3660 .filter(|entry| entry.kind() == powerio_core::HistoryKind::Repair)
3661 .count();
3662 let mut network_findings = Vec::new();
3663 let mut module = module.map_value(|mut network| {
3664 network_findings = network.repair_in_place();
3665 network
3666 });
3667 if network_findings.is_empty() {
3668 return Ok(module);
3669 }
3670 let mut parameters = std::collections::BTreeMap::new();
3671 parameters.insert(
3672 "repairs".to_owned(),
3673 serde_json::json!(
3674 network_findings
3675 .iter()
3676 .map(|finding| {
3677 serde_json::json!({
3678 "element": finding.element,
3679 "field": finding.field,
3680 "value": finding.old,
3681 "repaired_value": finding.new,
3682 })
3683 })
3684 .collect::<Vec<_>>()
3685 ),
3686 );
3687 let entry = powerio_core::HistoryEntry::new(
3688 powerio_core::HistoryId::new(format!("repair{repair_ordinal}"))?,
3689 powerio_core::HistoryKind::Repair,
3690 "value_domain_repair",
3691 )?
3692 .with_parameters(parameters)?;
3693 module.add_history_entry(entry)?;
3694 for finding in network_findings {
3695 module.add_diagnostic(finding.into_diagnostic())?;
3696 }
3697 module = module.sever_source();
3698 Ok(module)
3699}
3700
3701fn repair_vm(vm: f64) -> Option<f64> {
3705 (!vm.is_finite() || vm <= 0.0 || vm > 2.0).then_some(1.0)
3706}
3707
3708fn repair_va(va: f64) -> Option<f64> {
3710 (!va.is_finite() || va.abs() > 2000.0).then_some(0.0)
3711}
3712
3713fn repair_mbase(mbase: f64, sbase: f64) -> Option<f64> {
3715 (!mbase.is_finite() || mbase <= 0.0).then_some(sbase)
3716}
3717
3718fn repair_vg(vg: f64) -> Option<f64> {
3720 (!vg.is_finite() || vg <= 0.0).then_some(1.0)
3721}
3722
3723#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3728pub(crate) struct LoweredLengths {
3729 pub(crate) buses: usize,
3730 pub(crate) branches: usize,
3731 pub(crate) shunts: usize,
3732}
3733
3734impl BalancedNetwork {
3735 #[must_use]
3736 pub fn new(name: impl Into<String>, base_mva: f64) -> BalancedNetwork {
3737 BalancedNetwork::from_tables(BalancedNetworkTables {
3738 name: name.into(),
3739 base_mva,
3740 base_frequency: DEFAULT_BASE_FREQUENCY,
3741 geo: None,
3742 case_metadata: CaseMetadata::default(),
3743 detailed_connectivity: None,
3744 generated_uids: std::sync::Arc::default(),
3745 buses: Vec::new().into(),
3746 loads: Vec::new().into(),
3747 shunts: Vec::new().into(),
3748 static_var_compensators: Vec::new().into(),
3749 branches: Vec::new().into(),
3750 switches: Vec::new().into(),
3751 generators: Vec::new().into(),
3752 storage: Vec::new().into(),
3753 hvdc: Vec::new().into(),
3754 transformers_3w: Vec::new().into(),
3755 areas: Vec::new().into(),
3756 solver: None,
3757 source_format: SourceFormat::InMemory,
3758 })
3759 }
3760
3761 #[must_use]
3767 pub fn in_memory(
3768 name: impl Into<String>,
3769 base_mva: f64,
3770 buses: Vec<Bus>,
3771 branches: Vec<Branch>,
3772 ) -> BalancedNetwork {
3773 let mut net = Self::new(name, base_mva);
3774 *net.buses_mut() = buses;
3775 *net.branches_mut() = branches;
3776 net.assign_missing_component_ids();
3777 net
3778 }
3779
3780 #[must_use]
3785 pub fn is_normalized(&self) -> bool {
3786 self.source_format() == SourceFormat::Normalized
3787 }
3788
3789 pub fn check_base_mva(&self) -> crate::Result<()> {
3795 if self.base_mva().is_finite() && self.base_mva() > 0.0 {
3796 Ok(())
3797 } else {
3798 Err(crate::Error::InvalidBaseMva {
3799 base: self.base_mva(),
3800 })
3801 }
3802 }
3803
3804 #[must_use]
3816 pub fn validate_values(&self) -> Vec<crate::Diagnostic> {
3817 self.value_findings()
3818 .into_iter()
3819 .map(ValueFinding::into_diagnostic)
3820 .collect()
3821 }
3822
3823 pub(crate) fn value_findings(&self) -> Vec<ValueFinding> {
3824 let mut out = Vec::new();
3825 for (index, b) in self.buses().iter().enumerate() {
3826 if let Some(new) = repair_vm(b.vm) {
3827 out.push(ValueFinding {
3828 element: format!("bus {}", b.id),
3829 table: "buses",
3830 index,
3831 field: "vm",
3832 old: b.vm,
3833 new,
3834 reason: "voltage magnitude outside [0, 2] p.u.",
3835 });
3836 }
3837 if let Some(new) = repair_va(b.va) {
3838 out.push(ValueFinding {
3839 element: format!("bus {}", b.id),
3840 table: "buses",
3841 index,
3842 field: "va",
3843 old: b.va,
3844 new,
3845 reason: "voltage angle outside ±2000°",
3846 });
3847 }
3848 }
3849 for (index, g) in self.generators().iter().enumerate() {
3850 if let Some(new) = repair_mbase(g.mbase, self.base_mva()) {
3851 out.push(ValueFinding {
3852 element: format!("generator at bus {}", g.bus),
3853 table: "generators",
3854 index,
3855 field: "mbase",
3856 old: g.mbase,
3857 new,
3858 reason: "non-positive generator MVA base",
3859 });
3860 }
3861 if let Some(new) = repair_vg(g.vg) {
3862 out.push(ValueFinding {
3863 element: format!("generator at bus {}", g.bus),
3864 table: "generators",
3865 index,
3866 field: "vg",
3867 old: g.vg,
3868 new,
3869 reason: "non-positive voltage setpoint",
3870 });
3871 }
3872 }
3873 out
3874 }
3875
3876 pub(crate) fn repair_in_place(&mut self) -> Vec<ValueFinding> {
3883 let findings = self.value_findings();
3884 let sbase = self.base_mva();
3885 for b in self.buses_mut() {
3886 if let Some(new) = repair_vm(b.vm) {
3887 b.vm = new;
3888 }
3889 if let Some(new) = repair_va(b.va) {
3890 b.va = new;
3891 }
3892 }
3893 for g in self.generators_mut() {
3894 if let Some(new) = repair_mbase(g.mbase, sbase) {
3895 g.mbase = new;
3896 }
3897 if let Some(new) = repair_vg(g.vg) {
3898 g.vg = new;
3899 }
3900 }
3901 findings
3902 }
3903
3904 pub(crate) fn lowered_lengths(&self) -> LoweredLengths {
3910 let mut lengths = LoweredLengths {
3911 buses: self.buses().len(),
3912 branches: self.branches().len(),
3913 shunts: self.shunts().len(),
3914 };
3915 for t in self.transformers_3w().iter().filter(|t| t.in_service) {
3916 lengths.buses += 1;
3917 lengths.branches += 3;
3918 if t.mag_g != 0.0 || t.mag_b != 0.0 {
3919 lengths.shunts += 1;
3920 }
3921 }
3922 lengths
3923 }
3924
3925 pub(crate) fn expand_transformers_3w(&self) -> std::borrow::Cow<'_, BalancedNetwork> {
3936 if self.transformers_3w().is_empty() {
3937 return std::borrow::Cow::Borrowed(self);
3938 }
3939 let mut net = self.clone();
3940 let scale = if net.is_normalized() {
3945 1.0
3946 } else {
3947 net.base_mva()
3948 };
3949 let base_id = net
3954 .buses()
3955 .iter()
3956 .map(|b| b.id.0)
3957 .max()
3958 .unwrap_or(0)
3959 .checked_add(1)
3960 .expect("bus id space exhausted for star expansion");
3961 for (k, (source_row, t)) in self
3962 .transformers_3w()
3963 .iter()
3964 .enumerate()
3965 .filter(|(_, t)| t.in_service)
3966 .enumerate()
3967 {
3968 let star_id = BusId(
3969 base_id
3970 .checked_add(k)
3971 .expect("bus id space exhausted for star expansion"),
3972 );
3973 let (star, mut branches) = t.to_star_expansion(star_id);
3974 let transformer_identity = t
3975 .uid
3976 .clone()
3977 .unwrap_or_else(|| format!("transformers_3w:{source_row}"));
3978 for (winding, branch) in branches.iter_mut().enumerate() {
3979 branch.uid = Some(format!("{transformer_identity}/winding:{}", winding + 1));
3980 }
3981 net.buses_mut().push(star);
3982 net.branches_mut().extend(branches);
3983 if t.mag_g != 0.0 || t.mag_b != 0.0 {
3984 net.shunts_mut().push(Shunt {
3985 bus: star_id,
3986 g: t.mag_g * scale,
3987 b: t.mag_b * scale,
3988 in_service: true,
3989 section_count: None,
3990 control: None,
3991 uid: None,
3992 extras: Extras::new(),
3993 });
3994 }
3995 }
3996 net.transformers_3w_mut().clear();
3997 std::borrow::Cow::Owned(net)
3998 }
3999
4000 pub fn validate(&self) -> crate::Result<()> {
4006 self.check_references("network")
4007 }
4008
4009 #[allow(clippy::too_many_lines)]
4014 pub(crate) fn check_references(&self, format: &'static str) -> crate::Result<()> {
4015 let mut ids = std::collections::HashSet::with_capacity(self.buses().len());
4020 for b in self.buses() {
4021 if b.id > BusId::MAX {
4026 return Err(Error::FormatRead {
4027 format,
4028 message: format!("bus id {} is outside the int64 id space", b.id),
4029 });
4030 }
4031 if !ids.insert(b.id) {
4032 return Err(Error::FormatRead {
4033 format,
4034 message: format!("duplicate bus id {}", b.id),
4035 });
4036 }
4037 }
4038 let check = |bus: BusId, what: &str| -> crate::Result<()> {
4039 if ids.contains(&bus) {
4040 Ok(())
4041 } else {
4042 Err(Error::FormatRead {
4043 format,
4044 message: format!("{what} references unknown bus {bus}"),
4045 })
4046 }
4047 };
4048 for (i, br) in self.branches().iter().enumerate() {
4050 for bus in [br.from, br.to] {
4051 if !ids.contains(&bus) {
4052 return Err(Error::FormatRead {
4053 format,
4054 message: format!("branch {i} references unknown bus {bus}"),
4055 });
4056 }
4057 }
4058 if let Some(control) = br.control.as_ref() {
4059 if control.controlled_bus_on_winding_side
4060 && control.controlled_bus.is_none_or(|bus| bus.0 == 0)
4061 {
4062 return Err(Error::FormatRead {
4063 format,
4064 message: format!(
4065 "transformer control on branch {i} marks its controlled bus as lying on the winding side but has no nonzero controlled bus"
4066 ),
4067 });
4068 }
4069 if let Some(bus) = control.controlled_bus {
4070 check(bus, "transformer control")?;
4071 }
4072 }
4073 }
4074 for (i, sw) in self.switches().iter().enumerate() {
4075 for bus in [sw.from, sw.to] {
4076 if !ids.contains(&bus) {
4077 return Err(Error::FormatRead {
4078 format,
4079 message: format!("switch {i} references unknown bus {bus}"),
4080 });
4081 }
4082 }
4083 }
4084 for l in self.loads() {
4085 check(l.bus, "load")?;
4086 }
4087 for s in self.shunts() {
4088 check(s.bus, "shunt")?;
4089 if let Some(bus) = s.control.as_ref().and_then(|c| c.control_bus) {
4090 check(bus, "switched-shunt control")?;
4091 }
4092 }
4093 for svc in self.static_var_compensators() {
4094 check(svc.bus, "static VAR compensator")?;
4095 }
4096 for g in self.generators() {
4097 check(g.bus, "generator")?;
4098 if let Some(bus) = g.regulated_bus {
4099 check(bus, "generator voltage control")?;
4100 }
4101 }
4102 for d in self.hvdc() {
4103 check(d.from, "dcline")?;
4104 check(d.to, "dcline")?;
4105 }
4106 for s in self.storage() {
4107 check(s.bus, "storage")?;
4108 }
4109 for a in self.areas() {
4110 if let Some(slack) = a.slack_bus {
4111 check(slack, "area swing")?;
4112 }
4113 }
4114 for t in self.transformers_3w() {
4115 for w in &t.windings {
4116 check(w.bus, "3-winding transformer")?;
4117 if let Some(control) = w.control.as_ref() {
4118 if control.controlled_bus_on_winding_side
4119 && control.controlled_bus.is_none_or(|bus| bus.0 == 0)
4120 {
4121 return Err(Error::FormatRead {
4122 format,
4123 message: format!(
4124 "3-winding transformer control at bus {} marks its controlled bus as lying on the winding side but has no nonzero controlled bus",
4125 w.bus
4126 ),
4127 });
4128 }
4129 if let Some(bus) = control.controlled_bus {
4130 check(bus, "3-winding transformer control")?;
4131 }
4132 }
4133 }
4134 }
4135 if let Some(detailed) = self.detailed_connectivity().as_ref() {
4136 let nodes = detailed
4137 .connectivity_nodes
4138 .iter()
4139 .map(|node| &node.component)
4140 .collect::<std::collections::HashSet<_>>();
4141 for calculated in &detailed.calculated_buses {
4142 check(calculated.calculated_bus, "calculated bus")?;
4143 if calculated.nodes.is_empty() {
4144 return Err(Error::FormatRead {
4145 format,
4146 message: "calculated bus has no connectivity nodes".into(),
4147 });
4148 }
4149 for node in &calculated.nodes {
4150 if !nodes.contains(node) {
4151 return Err(Error::FormatRead {
4152 format,
4153 message: format!(
4154 "calculated bus references unknown connectivity node `{}`",
4155 node.local_id()
4156 ),
4157 });
4158 }
4159 }
4160 }
4161 let declared_components = detailed
4162 .component_metadata
4163 .iter()
4164 .map(|metadata| &metadata.component)
4165 .chain(
4166 detailed
4167 .substations
4168 .iter()
4169 .map(|substation| &substation.component),
4170 )
4171 .chain(detailed.voltage_levels.iter().map(|level| &level.component))
4172 .chain(
4173 detailed
4174 .dc_converter_units
4175 .iter()
4176 .map(|unit| &unit.component),
4177 )
4178 .collect::<std::collections::HashSet<_>>();
4179 for metadata in &detailed.component_metadata {
4180 if let Some(container) = metadata.equipment_container.as_ref()
4181 && !declared_components.contains(container)
4182 {
4183 return Err(Error::FormatRead {
4184 format,
4185 message: format!(
4186 "component `{}` references unknown equipment container `{container}`",
4187 metadata.component
4188 ),
4189 });
4190 }
4191 }
4192 let mut junctions = std::collections::HashSet::new();
4193 let terminal_equipment: std::collections::HashSet<&ComponentId> = detailed
4194 .terminals
4195 .iter()
4196 .map(|terminal| &terminal.equipment)
4197 .collect();
4198 for junction in &detailed.junctions {
4199 if !junctions.insert(&junction.component) {
4200 return Err(Error::FormatRead {
4201 format,
4202 message: format!("duplicate Junction `{}`", junction.component.local_id()),
4203 });
4204 }
4205 if !declared_components.contains(&junction.component) {
4206 return Err(Error::FormatRead {
4207 format,
4208 message: format!(
4209 "Junction `{}` has no component metadata",
4210 junction.component.local_id()
4211 ),
4212 });
4213 }
4214 if !terminal_equipment.contains(&junction.component) {
4215 return Err(Error::FormatRead {
4216 format,
4217 message: format!(
4218 "Junction `{}` has no terminal",
4219 junction.component.local_id()
4220 ),
4221 });
4222 }
4223 }
4224 }
4225 self.check_detailed_ac_references(format, &ids)?;
4226 self.check_detailed_dc_references(format)?;
4227 self.check_star_expansion_headroom(format)
4228 }
4229
4230 #[allow(clippy::too_many_lines)]
4231 fn check_detailed_ac_references(
4232 &self,
4233 format: &'static str,
4234 balanced_buses: &std::collections::HashSet<BusId>,
4235 ) -> crate::Result<()> {
4236 let Some(detailed) = self.detailed_connectivity().as_ref() else {
4237 return Ok(());
4238 };
4239 let error = |message: String| Error::FormatRead { format, message };
4240
4241 let mut metadata_components = std::collections::HashSet::new();
4242 for metadata in &detailed.component_metadata {
4243 if !metadata_components.insert(metadata.component.clone()) {
4244 return Err(error(format!(
4245 "duplicate component metadata `{}`",
4246 metadata.component
4247 )));
4248 }
4249 }
4250
4251 let mut substations = std::collections::HashSet::new();
4252 for substation in &detailed.substations {
4253 if !substations.insert(substation.component.clone()) {
4254 return Err(error(format!(
4255 "duplicate substation `{}`",
4256 substation.component
4257 )));
4258 }
4259 }
4260
4261 let mut voltage_levels = std::collections::HashSet::new();
4262 let mut voltage_level_topology = std::collections::HashMap::new();
4263 let mut calculated_bus_levels = std::collections::HashMap::new();
4264 for level in &detailed.voltage_levels {
4265 if !voltage_levels.insert(level.component.clone()) {
4266 return Err(error(format!(
4267 "duplicate voltage level `{}`",
4268 level.component
4269 )));
4270 }
4271 voltage_level_topology.insert(level.component.clone(), level.topology_kind);
4272 if let Some(substation) = &level.substation
4273 && !substations.contains(substation)
4274 {
4275 return Err(error(format!(
4276 "voltage level `{}` references unknown substation `{substation}`",
4277 level.component
4278 )));
4279 }
4280 let mut level_buses = std::collections::HashSet::new();
4281 for bus in &level.buses {
4282 if !balanced_buses.contains(bus) {
4283 return Err(error(format!(
4284 "voltage level `{}` references unknown calculated bus {bus}",
4285 level.component
4286 )));
4287 }
4288 if !level_buses.insert(*bus) {
4289 return Err(error(format!(
4290 "voltage level `{}` repeats calculated bus {bus}",
4291 level.component
4292 )));
4293 }
4294 if let Some(first_level) =
4295 calculated_bus_levels.insert(*bus, level.component.clone())
4296 && first_level != level.component
4297 {
4298 return Err(error(format!(
4299 "calculated bus {bus} belongs to both voltage levels `{first_level}` and `{}`",
4300 level.component
4301 )));
4302 }
4303 }
4304 }
4305 let known_container = |container: &ComponentId| {
4306 voltage_levels.contains(container) || metadata_components.contains(container)
4307 };
4308
4309 let mut configured_buses = std::collections::HashSet::new();
4310 let mut configured_bus_levels = std::collections::HashMap::new();
4311 for bus in &detailed.bus_breaker_buses {
4312 if !configured_buses.insert(bus.component.clone()) {
4313 return Err(error(format!(
4314 "duplicate bus breaker bus `{}`",
4315 bus.component
4316 )));
4317 }
4318 if !known_container(&bus.voltage_level) {
4319 let detail = if bus.calculated_bus.is_none() {
4320 " has no calculated bus and"
4321 } else {
4322 ""
4323 };
4324 return Err(error(format!(
4325 "TopologicalNode `{}`{detail} references unknown voltage level or connectivity container `{}`",
4326 bus.component, bus.voltage_level
4327 )));
4328 }
4329 configured_bus_levels.insert(bus.component.clone(), bus.voltage_level.clone());
4330 if let Some(calculated) = bus.calculated_bus
4331 && !balanced_buses.contains(&calculated)
4332 {
4333 return Err(error(format!(
4334 "bus breaker bus `{}` references unknown calculated bus {calculated}",
4335 bus.component
4336 )));
4337 }
4338 if voltage_levels.contains(&bus.voltage_level)
4339 && let Some(calculated) = bus.calculated_bus
4340 && let Some(first_level) =
4341 calculated_bus_levels.insert(calculated, bus.voltage_level.clone())
4342 && first_level != bus.voltage_level
4343 {
4344 return Err(error(format!(
4345 "calculated bus {calculated} belongs to both connectivity containers `{first_level}` and `{}`",
4346 bus.voltage_level
4347 )));
4348 }
4349 }
4350
4351 let mut connectivity_nodes = std::collections::HashSet::new();
4352 let mut connectivity_node_levels = std::collections::HashMap::new();
4353 let mut connectivity_node_calculated_buses = std::collections::HashMap::new();
4354 for node in &detailed.connectivity_nodes {
4355 if !connectivity_nodes.insert(node.component.clone()) {
4356 return Err(error(format!(
4357 "duplicate connectivity node `{}`",
4358 node.component
4359 )));
4360 }
4361 if !known_container(&node.voltage_level) {
4362 let detail = if node.calculated_bus.is_none() {
4363 " has no calculated bus and"
4364 } else {
4365 ""
4366 };
4367 return Err(error(format!(
4368 "ConnectivityNode `{}`{detail} references unknown voltage level or connectivity container `{}`",
4369 node.component, node.voltage_level
4370 )));
4371 }
4372 connectivity_node_levels.insert(node.component.clone(), node.voltage_level.clone());
4373 connectivity_node_calculated_buses.insert(node.component.clone(), node.calculated_bus);
4374 if let Some(calculated) = node.calculated_bus
4375 && !balanced_buses.contains(&calculated)
4376 {
4377 return Err(error(format!(
4378 "connectivity node `{}` references unknown calculated bus {calculated}",
4379 node.component
4380 )));
4381 }
4382 if voltage_levels.contains(&node.voltage_level)
4383 && let Some(calculated) = node.calculated_bus
4384 && let Some(first_level) =
4385 calculated_bus_levels.insert(calculated, node.voltage_level.clone())
4386 && first_level != node.voltage_level
4387 {
4388 return Err(error(format!(
4389 "calculated bus {calculated} belongs to both connectivity containers `{first_level}` and `{}`",
4390 node.voltage_level
4391 )));
4392 }
4393 }
4394
4395 let mut calculated_buses = std::collections::HashSet::new();
4396 let mut calculated_bus_nodes = std::collections::HashMap::new();
4397 for calculated in &detailed.calculated_buses {
4398 if !calculated_buses.insert(calculated.calculated_bus) {
4399 return Err(error(format!(
4400 "duplicate calculated bus {} in detailed connectivity",
4401 calculated.calculated_bus
4402 )));
4403 }
4404 if !balanced_buses.contains(&calculated.calculated_bus) {
4405 return Err(error(format!(
4406 "detailed connectivity references unknown calculated bus {}",
4407 calculated.calculated_bus
4408 )));
4409 }
4410 if !known_container(&calculated.voltage_level) {
4411 return Err(error(format!(
4412 "calculated bus {} references unknown voltage level or connectivity container `{}`",
4413 calculated.calculated_bus, calculated.voltage_level
4414 )));
4415 }
4416 if voltage_levels.contains(&calculated.voltage_level)
4417 && let Some(first_level) = calculated_bus_levels
4418 .insert(calculated.calculated_bus, calculated.voltage_level.clone())
4419 && first_level != calculated.voltage_level
4420 {
4421 return Err(error(format!(
4422 "calculated bus {} belongs to both connectivity containers `{first_level}` and `{}`",
4423 calculated.calculated_bus, calculated.voltage_level
4424 )));
4425 }
4426 for node in &calculated.nodes {
4427 let Some(node_level) = connectivity_node_levels.get(node) else {
4428 return Err(error(format!(
4429 "calculated bus {} references unknown connectivity node `{node}`",
4430 calculated.calculated_bus
4431 )));
4432 };
4433 if node_level != &calculated.voltage_level {
4434 return Err(error(format!(
4435 "calculated bus {} in `{}` contains connectivity node `{node}` from `{node_level}`",
4436 calculated.calculated_bus, calculated.voltage_level
4437 )));
4438 }
4439 if let Some(Some(node_bus)) = connectivity_node_calculated_buses.get(node)
4440 && *node_bus != calculated.calculated_bus
4441 {
4442 return Err(error(format!(
4443 "connectivity node `{node}` names calculated bus {node_bus}, but calculated bus {} also claims that node",
4444 calculated.calculated_bus
4445 )));
4446 }
4447 if let Some(first_bus) =
4448 calculated_bus_nodes.insert(node.clone(), calculated.calculated_bus)
4449 {
4450 return Err(error(format!(
4451 "connectivity node `{node}` is listed by calculated buses {first_bus} and {}",
4452 calculated.calculated_bus
4453 )));
4454 }
4455 }
4456 }
4457
4458 let mut busbars = std::collections::HashSet::new();
4459 for busbar in &detailed.busbar_sections {
4460 if !busbars.insert(busbar.component.clone()) {
4461 return Err(error(format!(
4462 "duplicate busbar section `{}`",
4463 busbar.component
4464 )));
4465 }
4466 if !known_container(&busbar.voltage_level) {
4467 return Err(error(format!(
4468 "busbar section `{}` references unknown voltage level or connectivity container `{}`",
4469 busbar.component, busbar.voltage_level
4470 )));
4471 }
4472 let Some(node_level) = connectivity_node_levels.get(&busbar.node) else {
4473 return Err(error(format!(
4474 "busbar section `{}` references unknown connectivity node `{}`",
4475 busbar.component, busbar.node
4476 )));
4477 };
4478 if voltage_levels.contains(&busbar.voltage_level)
4479 && voltage_levels.contains(node_level)
4480 && node_level != &busbar.voltage_level
4481 {
4482 return Err(error(format!(
4483 "busbar section `{}` in `{}` references connectivity node `{}` from `{node_level}`",
4484 busbar.component, busbar.voltage_level, busbar.node
4485 )));
4486 }
4487 }
4488
4489 let mut terminal_components = std::collections::HashSet::new();
4490 let mut terminal_keys = std::collections::HashSet::new();
4491 for terminal in &detailed.terminals {
4492 if terminal.terminal == 0 {
4493 return Err(error(format!(
4494 "equipment `{}` has terminal 0",
4495 terminal.equipment
4496 )));
4497 }
4498 if !terminal_keys.insert((terminal.equipment.clone(), terminal.terminal)) {
4499 return Err(error(format!(
4500 "equipment `{}` repeats terminal {}",
4501 terminal.equipment, terminal.terminal
4502 )));
4503 }
4504 if let Some(component) = &terminal.component
4505 && !terminal_components.insert(component.clone())
4506 {
4507 return Err(error(format!("duplicate terminal identity `{component}`")));
4508 }
4509 if !known_container(&terminal.voltage_level) {
4510 return Err(error(format!(
4511 "equipment `{}` terminal {} references unknown voltage level or connectivity container `{}`",
4512 terminal.equipment, terminal.terminal, terminal.voltage_level
4513 )));
4514 }
4515 for bus in [terminal.bus.as_ref(), terminal.connectable_bus.as_ref()]
4516 .into_iter()
4517 .flatten()
4518 {
4519 let Some(bus_level) = configured_bus_levels.get(bus) else {
4520 return Err(error(format!(
4521 "equipment `{}` terminal {} references unknown bus breaker bus `{bus}`",
4522 terminal.equipment, terminal.terminal
4523 )));
4524 };
4525 if bus_level != &terminal.voltage_level {
4526 return Err(error(format!(
4527 "equipment `{}` terminal {} in `{}` references bus `{bus}` from `{bus_level}`",
4528 terminal.equipment, terminal.terminal, terminal.voltage_level
4529 )));
4530 }
4531 }
4532 if let Some(node) = &terminal.node {
4533 let Some(node_level) = connectivity_node_levels.get(node) else {
4534 return Err(error(format!(
4535 "equipment `{}` terminal {} references unknown connectivity node `{node}`",
4536 terminal.equipment, terminal.terminal
4537 )));
4538 };
4539 if node_level != &terminal.voltage_level {
4540 return Err(error(format!(
4541 "equipment `{}` terminal {} in `{}` references connectivity node `{node}` from `{node_level}`",
4542 terminal.equipment, terminal.terminal, terminal.voltage_level
4543 )));
4544 }
4545 }
4546 }
4547
4548 let mut switches = std::collections::HashSet::new();
4549 for switch in &detailed.switches {
4550 if !switches.insert(switch.component.clone()) {
4551 return Err(error(format!(
4552 "duplicate topology switch `{}`",
4553 switch.component
4554 )));
4555 }
4556 if !voltage_levels.contains(&switch.voltage_level)
4557 && !metadata_components.contains(&switch.voltage_level)
4558 {
4559 return Err(error(format!(
4560 "topology switch `{}` references unknown voltage level or connectivity container `{}`",
4561 switch.component, switch.voltage_level
4562 )));
4563 }
4564 for endpoint in [&switch.endpoint1, &switch.endpoint2] {
4565 match endpoint {
4566 TopologyEndpoint::Bus(bus) => {
4567 if !configured_bus_levels.contains_key(bus) {
4568 return Err(error(format!(
4569 "topology switch `{}` references unknown bus breaker bus `{bus}`",
4570 switch.component
4571 )));
4572 }
4573 if voltage_level_topology.get(&switch.voltage_level)
4574 == Some(&TopologyKind::NodeBreaker)
4575 {
4576 return Err(error(format!(
4577 "topology switch `{}` uses a bus endpoint in node breaker voltage level `{}`",
4578 switch.component, switch.voltage_level
4579 )));
4580 }
4581 }
4582 TopologyEndpoint::Node(node) => {
4583 if !connectivity_node_levels.contains_key(node) {
4584 return Err(error(format!(
4585 "topology switch `{}` references unknown connectivity node `{node}`",
4586 switch.component
4587 )));
4588 }
4589 if voltage_level_topology.get(&switch.voltage_level)
4590 == Some(&TopologyKind::BusBreaker)
4591 {
4592 return Err(error(format!(
4593 "topology switch `{}` uses a node endpoint in bus breaker voltage level `{}`",
4594 switch.component, switch.voltage_level
4595 )));
4596 }
4597 }
4598 }
4599 }
4600 }
4601
4602 for connection in &detailed.internal_connections {
4603 if !voltage_levels.contains(&connection.voltage_level) {
4604 return Err(error(format!(
4605 "internal connection references unknown voltage level `{}`",
4606 connection.voltage_level
4607 )));
4608 }
4609 for node in [&connection.node1, &connection.node2] {
4610 let Some(node_level) = connectivity_node_levels.get(node) else {
4611 return Err(error(format!(
4612 "internal connection references unknown connectivity node `{node}`"
4613 )));
4614 };
4615 if node_level != &connection.voltage_level {
4616 return Err(error(format!(
4617 "internal connection in `{}` references connectivity node `{node}` from `{node_level}`",
4618 connection.voltage_level
4619 )));
4620 }
4621 }
4622 }
4623
4624 let mut tap_components = std::collections::HashSet::new();
4625 let mut tap_keys = std::collections::HashSet::new();
4626 for tap in &detailed.tap_changers {
4627 if tap.winding == 0 {
4628 return Err(error(format!(
4629 "transformer `{}` has a tap changer on winding 0",
4630 tap.transformer
4631 )));
4632 }
4633 let kind = match tap.kind {
4634 TapChangerKind::Ratio => 0_u8,
4635 TapChangerKind::Phase => 1_u8,
4636 };
4637 if !tap_keys.insert((tap.transformer.clone(), tap.winding, kind)) {
4638 return Err(error(format!(
4639 "transformer `{}` repeats its {:?} tap changer on winding {}",
4640 tap.transformer, tap.kind, tap.winding
4641 )));
4642 }
4643 if let Some(component) = &tap.component
4644 && !tap_components.insert(component.clone())
4645 {
4646 return Err(error(format!(
4647 "duplicate tap changer identity `{component}`"
4648 )));
4649 }
4650 let mut positions = std::collections::HashSet::new();
4651 for step in &tap.steps {
4652 if !positions.insert(step.position) {
4653 return Err(error(format!(
4654 "transformer `{}` winding {} repeats tap position {}",
4655 tap.transformer, tap.winding, step.position
4656 )));
4657 }
4658 }
4659 if let Some(reference) = &tap.regulation_terminal
4660 && !terminal_keys.contains(&(reference.equipment.clone(), reference.terminal))
4661 {
4662 return Err(error(format!(
4663 "transformer `{}` winding {} regulates unknown equipment terminal `{}` terminal {}",
4664 tap.transformer, tap.winding, reference.equipment, reference.terminal
4665 )));
4666 }
4667 }
4668
4669 Ok(())
4670 }
4671
4672 #[allow(clippy::too_many_lines)]
4673 fn check_detailed_dc_references(&self, format: &'static str) -> crate::Result<()> {
4674 let Some(detailed) = self.detailed_connectivity().as_ref() else {
4675 return Ok(());
4676 };
4677
4678 let substations = detailed
4679 .substations
4680 .iter()
4681 .map(|substation| &substation.component)
4682 .collect::<std::collections::HashSet<_>>();
4683 let mut converter_units =
4684 std::collections::HashSet::with_capacity(detailed.dc_converter_units.len());
4685 for unit in &detailed.dc_converter_units {
4686 if !converter_units.insert(&unit.component) {
4687 return Err(Error::FormatRead {
4688 format,
4689 message: format!("duplicate DCConverterUnit `{}`", unit.component.local_id()),
4690 });
4691 }
4692 if let Some(substation) = unit.substation.as_ref()
4693 && !substations.contains(substation)
4694 {
4695 return Err(Error::FormatRead {
4696 format,
4697 message: format!(
4698 "DCConverterUnit `{}` references unknown Substation `{}`",
4699 unit.component.local_id(),
4700 substation.local_id()
4701 ),
4702 });
4703 }
4704 }
4705 let known_components = detailed
4706 .component_metadata
4707 .iter()
4708 .map(|metadata| &metadata.component)
4709 .chain(converter_units.iter().copied())
4710 .collect::<std::collections::HashSet<_>>();
4711
4712 let mut nodes = std::collections::HashSet::with_capacity(detailed.dc_nodes.len());
4713 for node in &detailed.dc_nodes {
4714 if !nodes.insert(&node.component) {
4715 return Err(Error::FormatRead {
4716 format,
4717 message: format!("duplicate DCNode `{}`", node.component.local_id()),
4718 });
4719 }
4720 }
4721 let mut topological_nodes =
4722 std::collections::HashSet::with_capacity(detailed.dc_topological_nodes.len());
4723 for node in &detailed.dc_topological_nodes {
4724 if !topological_nodes.insert(&node.component) {
4725 return Err(Error::FormatRead {
4726 format,
4727 message: format!(
4728 "duplicate DCTopologicalNode `{}`",
4729 node.component.local_id()
4730 ),
4731 });
4732 }
4733 }
4734
4735 let check_unit = |class: &str,
4736 component: &ComponentId,
4737 unit: Option<&ComponentId>|
4738 -> crate::Result<()> {
4739 if let Some(unit) = unit
4740 && !converter_units.contains(unit)
4741 {
4742 return Err(Error::FormatRead {
4743 format,
4744 message: format!(
4745 "{class} `{}` references unknown DCConverterUnit `{}`",
4746 component.local_id(),
4747 unit.local_id()
4748 ),
4749 });
4750 }
4751 Ok(())
4752 };
4753 let check_container = |class: &str,
4754 component: &ComponentId,
4755 container: Option<&ComponentId>|
4756 -> crate::Result<()> {
4757 if let Some(container) = container
4758 && !known_components.contains(container)
4759 {
4760 return Err(Error::FormatRead {
4761 format,
4762 message: format!(
4763 "{class} `{}` references unknown equipment container `{}`",
4764 component.local_id(),
4765 container.local_id()
4766 ),
4767 });
4768 }
4769 Ok(())
4770 };
4771 for node in &detailed.dc_topological_nodes {
4772 check_unit(
4773 "DCTopologicalNode",
4774 &node.component,
4775 node.dc_converter_unit.as_ref(),
4776 )?;
4777 }
4778 for node in &detailed.dc_nodes {
4779 check_unit("DCNode", &node.component, node.dc_converter_unit.as_ref())?;
4780 if let Some(topological_node) = node.dc_topological_node.as_ref()
4781 && !topological_nodes.contains(topological_node)
4782 {
4783 return Err(Error::FormatRead {
4784 format,
4785 message: format!(
4786 "DCNode `{}` references unknown DCTopologicalNode `{}`",
4787 node.component.local_id(),
4788 topological_node.local_id()
4789 ),
4790 });
4791 }
4792 }
4793
4794 let mut terminal_ids = std::collections::HashSet::<ComponentId>::new();
4795 let mut check_terminal = |class: &str,
4796 equipment: &ComponentId,
4797 terminal: &DcTerminal|
4798 -> crate::Result<()> {
4799 if let Some(terminal_id) = terminal.component.as_ref()
4800 && !terminal_ids.insert(terminal_id.clone())
4801 {
4802 return Err(Error::FormatRead {
4803 format,
4804 message: format!("duplicate DCTerminal `{}`", terminal_id.local_id()),
4805 });
4806 }
4807 if terminal.dc_node.is_none() && terminal.dc_topological_node.is_none() {
4808 return Err(Error::FormatRead {
4809 format,
4810 message: format!(
4811 "DCTerminal on {class} `{}` references neither a DCNode nor a DCTopologicalNode",
4812 equipment.local_id()
4813 ),
4814 });
4815 }
4816 if let Some(node) = terminal.dc_node.as_ref()
4817 && !nodes.contains(node)
4818 {
4819 return Err(Error::FormatRead {
4820 format,
4821 message: format!(
4822 "DCTerminal on {class} `{}` references unknown DCNode `{}`",
4823 equipment.local_id(),
4824 node.local_id()
4825 ),
4826 });
4827 }
4828 if let Some(node) = terminal.dc_topological_node.as_ref()
4829 && !topological_nodes.contains(node)
4830 {
4831 return Err(Error::FormatRead {
4832 format,
4833 message: format!(
4834 "DCTerminal on {class} `{}` references unknown DCTopologicalNode `{}`",
4835 equipment.local_id(),
4836 node.local_id()
4837 ),
4838 });
4839 }
4840 Ok(())
4841 };
4842
4843 for ground in &detailed.dc_grounds {
4844 check_container(
4845 "DCGround",
4846 &ground.component,
4847 ground.equipment_container.as_ref(),
4848 )?;
4849 check_terminal("DCGround", &ground.component, &ground.dc_terminal)?;
4850 }
4851 for busbar in &detailed.dc_busbars {
4852 check_container(
4853 "DCBusbar",
4854 &busbar.component,
4855 busbar.equipment_container.as_ref(),
4856 )?;
4857 check_terminal("DCBusbar", &busbar.component, &busbar.dc_terminal)?;
4858 }
4859 for line in &detailed.dc_lines {
4860 check_container(
4861 "DCLineSegment",
4862 &line.component,
4863 line.equipment_container.as_ref(),
4864 )?;
4865 check_terminal("DCLineSegment", &line.component, &line.dc_terminal1)?;
4866 check_terminal("DCLineSegment", &line.component, &line.dc_terminal2)?;
4867 }
4868 for device in &detailed.dc_series_devices {
4869 check_container(
4870 "DCSeriesDevice",
4871 &device.component,
4872 device.equipment_container.as_ref(),
4873 )?;
4874 check_terminal("DCSeriesDevice", &device.component, &device.dc_terminal1)?;
4875 check_terminal("DCSeriesDevice", &device.component, &device.dc_terminal2)?;
4876 }
4877 for switch in &detailed.dc_switches {
4878 check_container(
4879 "DCSwitch",
4880 &switch.component,
4881 switch.equipment_container.as_ref(),
4882 )?;
4883 check_terminal("DCSwitch", &switch.component, &switch.dc_terminal1)?;
4884 check_terminal("DCSwitch", &switch.component, &switch.dc_terminal2)?;
4885 }
4886 for converter in &detailed.voltage_source_converters {
4887 check_unit(
4888 "VsConverter",
4889 &converter.component,
4890 converter.dc_converter_unit.as_ref(),
4891 )?;
4892 check_terminal("VsConverter", &converter.component, &converter.dc_terminal1)?;
4893 check_terminal("VsConverter", &converter.component, &converter.dc_terminal2)?;
4894 }
4895 for converter in &detailed.line_commutated_converters {
4896 check_unit(
4897 "CsConverter",
4898 &converter.component,
4899 converter.dc_converter_unit.as_ref(),
4900 )?;
4901 check_terminal("CsConverter", &converter.component, &converter.dc_terminal1)?;
4902 check_terminal("CsConverter", &converter.component, &converter.dc_terminal2)?;
4903 }
4904
4905 let ac_terminals = detailed
4906 .terminals
4907 .iter()
4908 .map(|terminal| (&terminal.equipment, terminal.terminal))
4909 .collect::<std::collections::HashSet<_>>();
4910 let check_regulating_terminal =
4911 |what: &str, reference: Option<&TerminalReference>| -> crate::Result<()> {
4912 if let Some(reference) = reference
4913 && !ac_terminals.contains(&(&reference.equipment, reference.terminal))
4914 {
4915 return Err(Error::FormatRead {
4916 format,
4917 message: format!(
4918 "{what} references undeclared regulating Terminal `{}` number {}",
4919 reference.equipment.local_id(),
4920 reference.terminal
4921 ),
4922 });
4923 }
4924 Ok(())
4925 };
4926 for (index, generator) in self.generators().iter().enumerate() {
4927 check_regulating_terminal(
4928 &format!("generator {index}"),
4929 generator.regulating_terminal.as_ref(),
4930 )?;
4931 }
4932 for (index, branch) in self.branches().iter().enumerate() {
4933 if let Some(control) = &branch.control {
4934 check_regulating_terminal(
4935 &format!("transformer branch {index}"),
4936 control.regulating_terminal.as_ref(),
4937 )?;
4938 }
4939 }
4940 for (index, shunt) in self.shunts().iter().enumerate() {
4941 if let Some(control) = &shunt.control {
4942 check_regulating_terminal(
4943 &format!("switched shunt {index}"),
4944 control.regulating_terminal.as_ref(),
4945 )?;
4946 }
4947 }
4948 for (transformer_index, transformer) in self.transformers_3w().iter().enumerate() {
4949 for (winding_index, winding) in transformer.windings.iter().enumerate() {
4950 if let Some(control) = &winding.control {
4951 check_regulating_terminal(
4952 &format!(
4953 "three winding transformer {transformer_index} winding {winding_index}"
4954 ),
4955 control.regulating_terminal.as_ref(),
4956 )?;
4957 }
4958 }
4959 }
4960 let check_pcc_terminal = |class: &str,
4961 component: &ComponentId,
4962 pcc: Option<&TerminalReference>|
4963 -> crate::Result<()> {
4964 if let Some(pcc) = pcc
4965 && !ac_terminals.contains(&(&pcc.equipment, pcc.terminal))
4966 {
4967 return Err(Error::FormatRead {
4968 format,
4969 message: format!(
4970 "{class} `{}` references undeclared PCC Terminal `{}` number {}",
4971 component.local_id(),
4972 pcc.equipment.local_id(),
4973 pcc.terminal
4974 ),
4975 });
4976 }
4977 Ok(())
4978 };
4979 for converter in &detailed.voltage_source_converters {
4980 check_pcc_terminal(
4981 "VoltageSourceConverter",
4982 &converter.component,
4983 converter.pcc_terminal.as_ref(),
4984 )?;
4985 }
4986 for converter in &detailed.line_commutated_converters {
4987 check_pcc_terminal(
4988 "LineCommutatedConverter",
4989 &converter.component,
4990 converter.pcc_terminal.as_ref(),
4991 )?;
4992 }
4993 Ok(())
4994 }
4995
4996 fn check_star_expansion_headroom(&self, format: &'static str) -> crate::Result<()> {
5004 if self.transformers_3w().is_empty() {
5005 return Ok(());
5006 }
5007 let Some(max_id) = self.buses().iter().map(|b| b.id.0).max() else {
5008 return Ok(());
5009 };
5010 let needed = self
5011 .transformers_3w()
5012 .iter()
5013 .filter(|t| t.in_service)
5014 .count()
5015 .max(1);
5016 if max_id
5017 .checked_add(needed)
5018 .is_none_or(|top| top > BusId::MAX.0)
5019 {
5020 return Err(Error::FormatRead {
5021 format,
5022 message: format!(
5023 "bus id {max_id} leaves no room to allocate synthetic star bus ids \
5024 for 3-winding transformers"
5025 ),
5026 });
5027 }
5028 Ok(())
5029 }
5030}
5031
5032#[cfg(test)]
5036pub(crate) fn serde_round_trip(network: &BalancedNetwork) -> BalancedNetwork {
5037 serde_json::from_str(&serde_json::to_string(network).unwrap()).unwrap()
5038}
5039
5040#[cfg(test)]
5041mod tests {
5042 use super::*;
5043
5044 fn close(actual: f64, expected: f64) {
5045 assert!((actual - expected).abs() < 1e-12, "{actual} != {expected}");
5046 }
5047
5048 fn component(component_type: &str, local_id: &str) -> ComponentId {
5049 ComponentId::new(component_type, local_id).unwrap()
5050 }
5051
5052 fn reference_test_dc_terminal(local_id: &str, node: &ComponentId) -> DcTerminal {
5053 DcTerminal {
5054 component: Some(component("dc_terminal", local_id)),
5055 sequence_number: None,
5056 dc_node: Some(node.clone()),
5057 dc_topological_node: None,
5058 polarity: None,
5059 connected: None,
5060 active_power_mw: None,
5061 current_a: None,
5062 }
5063 }
5064
5065 #[allow(clippy::too_many_lines)]
5066 fn detailed_dc_reference_test_network() -> BalancedNetwork {
5067 let substation = component("substation", "S");
5068 let converter_unit = component("dc_converter_unit", "U");
5069 let topological_node = component("dc_topological_node", "T");
5070 let node = component("dc_node", "N");
5071 let pcc_equipment = component("ac_equipment", "PCC");
5072 let voltage_level = component("voltage_level", "VL");
5073
5074 let voltage_source_converter: VoltageSourceConverter =
5075 serde_json::from_value(serde_json::json!({
5076 "component": component("voltage_source_converter", "VSC"),
5077 "dc_converter_unit": converter_unit.clone(),
5078 "dc_terminal1": reference_test_dc_terminal("VSC-1", &node),
5079 "dc_terminal2": reference_test_dc_terminal("VSC-2", &node),
5080 "pcc_terminal": {
5081 "equipment": pcc_equipment.clone(),
5082 "terminal": 1
5083 }
5084 }))
5085 .unwrap();
5086 let line_commutated_converter: LineCommutatedConverter =
5087 serde_json::from_value(serde_json::json!({
5088 "component": component("line_commutated_converter", "LCC"),
5089 "dc_converter_unit": converter_unit.clone(),
5090 "dc_terminal1": reference_test_dc_terminal("LCC-1", &node),
5091 "dc_terminal2": reference_test_dc_terminal("LCC-2", &node),
5092 "pcc_terminal": {
5093 "equipment": pcc_equipment.clone(),
5094 "terminal": 1
5095 }
5096 }))
5097 .unwrap();
5098
5099 let detailed = DetailedConnectivity {
5100 substations: vec![Substation {
5101 component: substation.clone(),
5102 country: None,
5103 operator: None,
5104 geographical_tags: Vec::new(),
5105 }],
5106 voltage_levels: vec![VoltageLevel {
5107 component: voltage_level.clone(),
5108 substation: Some(substation.clone()),
5109 nominal_kv: 230.0,
5110 low_voltage_limit_kv: None,
5111 high_voltage_limit_kv: None,
5112 topology_kind: TopologyKind::BusBreaker,
5113 buses: Vec::new(),
5114 }],
5115 terminals: vec![Terminal {
5116 component: None,
5117 equipment: pcc_equipment,
5118 terminal: 1,
5119 voltage_level,
5120 bus: None,
5121 connectable_bus: None,
5122 node: None,
5123 connected: true,
5124 active_power_mw: None,
5125 reactive_power_mvar: None,
5126 }],
5127 dc_converter_units: vec![DcConverterUnit {
5128 component: converter_unit.clone(),
5129 substation: Some(substation),
5130 operation_mode: DcConverterOperatingMode::Bipolar,
5131 }],
5132 dc_topological_nodes: vec![DcTopologicalNode {
5133 component: topological_node.clone(),
5134 dc_converter_unit: Some(converter_unit.clone()),
5135 }],
5136 dc_nodes: vec![DcNode {
5137 component: node.clone(),
5138 nominal_voltage_kv: None,
5139 dc_converter_unit: Some(converter_unit.clone()),
5140 dc_topological_node: Some(topological_node),
5141 voltage_kv: None,
5142 }],
5143 dc_grounds: vec![DcGround {
5144 component: component("dc_ground", "G"),
5145 equipment_container: Some(converter_unit.clone()),
5146 dc_terminal: reference_test_dc_terminal("G-1", &node),
5147 rated_dc_voltage_kv: None,
5148 resistance_ohm: None,
5149 inductance_h: None,
5150 }],
5151 dc_lines: vec![DcLine {
5152 component: component("dc_line", "L"),
5153 equipment_container: Some(converter_unit.clone()),
5154 dc_terminal1: reference_test_dc_terminal("L-1", &node),
5155 dc_terminal2: reference_test_dc_terminal("L-2", &node),
5156 rated_dc_voltage_kv: None,
5157 resistance_ohm: None,
5158 inductance_h: None,
5159 capacitance_f: None,
5160 length_km: None,
5161 }],
5162 dc_switches: vec![DcSwitch {
5163 component: component("dc_switch", "SW"),
5164 equipment_container: Some(converter_unit),
5165 dc_terminal1: reference_test_dc_terminal("SW-1", &node),
5166 dc_terminal2: reference_test_dc_terminal("SW-2", &node),
5167 kind: DcSwitchKind::Switch,
5168 rated_dc_voltage_kv: None,
5169 open: None,
5170 resistance_ohm: None,
5171 }],
5172 voltage_source_converters: vec![voltage_source_converter],
5173 line_commutated_converters: vec![line_commutated_converter],
5174 ..DetailedConnectivity::default()
5175 };
5176 let mut network = BalancedNetwork::new("dc-reference-test", 100.0);
5177 *network.detailed_connectivity_mut() = Some(std::sync::Arc::new(detailed));
5178 network
5179 }
5180
5181 fn reference_test_detailed_mut(network: &mut BalancedNetwork) -> &mut DetailedConnectivity {
5182 std::sync::Arc::make_mut(
5183 network
5184 .detailed_connectivity_mut()
5185 .as_mut()
5186 .expect("reference test has detailed connectivity"),
5187 )
5188 }
5189
5190 fn assert_invalid(network: &BalancedNetwork, expected: &str) {
5191 let error = network.validate().unwrap_err().to_string();
5192 assert!(
5193 error.contains(expected),
5194 "expected `{expected}` in `{error}`"
5195 );
5196 }
5197
5198 fn assert_duplicate_dc_converter_unit_rejected(valid: &BalancedNetwork) {
5199 let mut network = valid.clone();
5200 let unit = reference_test_detailed_mut(&mut network).dc_converter_units[0].clone();
5201 reference_test_detailed_mut(&mut network)
5202 .dc_converter_units
5203 .push(unit);
5204 assert_invalid(&network, "duplicate DCConverterUnit");
5205 }
5206
5207 #[test]
5208 fn detailed_dc_references_are_complete_and_checked() {
5209 let valid = detailed_dc_reference_test_network();
5210 valid.validate().unwrap();
5211
5212 assert_duplicate_dc_converter_unit_rejected(&valid);
5213
5214 let mut missing_substation = valid.clone();
5215 reference_test_detailed_mut(&mut missing_substation).dc_converter_units[0].substation =
5216 Some(component("substation", "missing"));
5217 assert_invalid(&missing_substation, "references unknown Substation");
5218
5219 let mut duplicate_topological_node = valid.clone();
5220 let node = reference_test_detailed_mut(&mut duplicate_topological_node)
5221 .dc_topological_nodes[0]
5222 .clone();
5223 reference_test_detailed_mut(&mut duplicate_topological_node)
5224 .dc_topological_nodes
5225 .push(node);
5226 assert_invalid(&duplicate_topological_node, "duplicate DCTopologicalNode");
5227
5228 let mut missing_topological_unit = valid.clone();
5229 reference_test_detailed_mut(&mut missing_topological_unit).dc_topological_nodes[0]
5230 .dc_converter_unit = Some(component("dc_converter_unit", "missing"));
5231 assert_invalid(
5232 &missing_topological_unit,
5233 "DCTopologicalNode `T` references unknown DCConverterUnit",
5234 );
5235
5236 let mut missing_node_unit = valid.clone();
5237 reference_test_detailed_mut(&mut missing_node_unit).dc_nodes[0].dc_converter_unit =
5238 Some(component("dc_converter_unit", "missing"));
5239 assert_invalid(
5240 &missing_node_unit,
5241 "DCNode `N` references unknown DCConverterUnit",
5242 );
5243
5244 let mut missing_node_topology = valid.clone();
5245 reference_test_detailed_mut(&mut missing_node_topology).dc_nodes[0].dc_topological_node =
5246 Some(component("dc_topological_node", "missing"));
5247 assert_invalid(
5248 &missing_node_topology,
5249 "DCNode `N` references unknown DCTopologicalNode",
5250 );
5251
5252 let mut missing_container = valid.clone();
5253 reference_test_detailed_mut(&mut missing_container).dc_lines[0].equipment_container =
5254 Some(component("dc_converter_unit", "missing"));
5255 assert_invalid(
5256 &missing_container,
5257 "DCLineSegment `L` references unknown equipment container",
5258 );
5259
5260 let mut missing_converter_unit = valid.clone();
5261 reference_test_detailed_mut(&mut missing_converter_unit).voltage_source_converters[0]
5262 .dc_converter_unit = Some(component("dc_converter_unit", "missing"));
5263 assert_invalid(
5264 &missing_converter_unit,
5265 "VsConverter `VSC` references unknown DCConverterUnit",
5266 );
5267
5268 let mut terminal_without_node = valid.clone();
5269 let terminal =
5270 &mut reference_test_detailed_mut(&mut terminal_without_node).dc_grounds[0].dc_terminal;
5271 terminal.dc_node = None;
5272 terminal.dc_topological_node = None;
5273 assert_invalid(
5274 &terminal_without_node,
5275 "references neither a DCNode nor a DCTopologicalNode",
5276 );
5277
5278 let mut missing_physical_node = valid.clone();
5279 reference_test_detailed_mut(&mut missing_physical_node).dc_grounds[0]
5280 .dc_terminal
5281 .dc_node = Some(component("dc_node", "missing"));
5282 assert_invalid(&missing_physical_node, "references unknown DCNode");
5283
5284 let mut missing_terminal_topology = valid.clone();
5285 let terminal = &mut reference_test_detailed_mut(&mut missing_terminal_topology).dc_grounds
5286 [0]
5287 .dc_terminal;
5288 terminal.dc_node = None;
5289 terminal.dc_topological_node = Some(component("dc_topological_node", "missing"));
5290 assert_invalid(
5291 &missing_terminal_topology,
5292 "references unknown DCTopologicalNode",
5293 );
5294
5295 let mut duplicate_terminal = valid.clone();
5296 let terminal_id = reference_test_detailed_mut(&mut duplicate_terminal).dc_grounds[0]
5297 .dc_terminal
5298 .component
5299 .clone();
5300 reference_test_detailed_mut(&mut duplicate_terminal).dc_lines[0]
5301 .dc_terminal1
5302 .component = terminal_id;
5303 assert_invalid(&duplicate_terminal, "duplicate DCTerminal");
5304
5305 let mut missing_pcc_terminal = valid;
5306 reference_test_detailed_mut(&mut missing_pcc_terminal).voltage_source_converters[0]
5307 .pcc_terminal
5308 .as_mut()
5309 .unwrap()
5310 .terminal = 2;
5311 assert_invalid(&missing_pcc_terminal, "references undeclared PCC Terminal");
5312 }
5313
5314 #[test]
5315 #[allow(clippy::too_many_lines)] fn detailed_connectivity_dc_equipment_round_trips_and_defaults() {
5317 let empty: DetailedConnectivity = serde_json::from_str("{}").unwrap();
5318 assert_eq!(empty, DetailedConnectivity::default());
5319
5320 let node1 = component("dc_node", "dc-1");
5321 let node2 = component("dc_node", "dc-2");
5322 let terminal = |dc_node: ComponentId, connected: bool, power| DcTerminal {
5323 component: None,
5324 sequence_number: None,
5325 dc_node: Some(dc_node),
5326 dc_topological_node: None,
5327 polarity: None,
5328 connected: Some(connected),
5329 active_power_mw: power,
5330 current_a: power.map(|value| value / 320.0),
5331 };
5332 let unit = component("dc_converter_unit", "unit");
5333 let topological_node = component("dc_topological_node", "dc-topology");
5334 let pcc_terminal = TerminalReference {
5335 equipment: component("branch", "pcc"),
5336 terminal: 2,
5337 };
5338 let droop_curve = DroopCurve {
5339 segments: vec![DroopCurveSegment {
5340 minimum_voltage_kv: 300.0,
5341 maximum_voltage_kv: 340.0,
5342 k: 0.4,
5343 }],
5344 };
5345 let mut curve_properties = BTreeMap::new();
5346 curve_properties.insert("source".into(), "test".into());
5347 let reactive_limits = ReactiveLimits::CapabilityCurve(ReactiveCapabilityCurve {
5348 curve_style: CurveStyle::StraightLineYValues,
5349 properties: curve_properties,
5350 points: vec![
5351 ReactiveCapabilityCurvePoint {
5352 active_power_mw: -100.0,
5353 minimum_reactive_power_mvar: -50.0,
5354 maximum_reactive_power_mvar: 40.0,
5355 properties: BTreeMap::new(),
5356 },
5357 ReactiveCapabilityCurvePoint {
5358 active_power_mw: 100.0,
5359 minimum_reactive_power_mvar: -40.0,
5360 maximum_reactive_power_mvar: 50.0,
5361 properties: BTreeMap::new(),
5362 },
5363 ],
5364 });
5365 let detailed = DetailedConnectivity {
5366 terminals: vec![Terminal {
5367 component: None,
5368 equipment: component("voltage_source_converter", "vsc"),
5369 terminal: 1,
5370 voltage_level: component("voltage_level", "vl"),
5371 bus: None,
5372 connectable_bus: None,
5373 node: Some(component("connectivity_node", "n1")),
5374 connected: true,
5375 active_power_mw: Some(-95.0),
5376 reactive_power_mvar: Some(12.0),
5377 }],
5378 dc_converter_units: vec![DcConverterUnit {
5379 component: unit.clone(),
5380 substation: Some(component("substation", "station")),
5381 operation_mode: DcConverterOperatingMode::Bipolar,
5382 }],
5383 dc_topological_nodes: vec![DcTopologicalNode {
5384 component: topological_node.clone(),
5385 dc_converter_unit: Some(unit.clone()),
5386 }],
5387 dc_nodes: vec![
5388 DcNode {
5389 component: node1.clone(),
5390 nominal_voltage_kv: Some(320.0),
5391 dc_converter_unit: Some(unit.clone()),
5392 dc_topological_node: Some(topological_node.clone()),
5393 voltage_kv: Some(318.0),
5394 },
5395 DcNode {
5396 component: node2.clone(),
5397 nominal_voltage_kv: Some(320.0),
5398 dc_converter_unit: Some(unit.clone()),
5399 dc_topological_node: Some(topological_node),
5400 voltage_kv: None,
5401 },
5402 ],
5403 dc_grounds: vec![DcGround {
5404 component: component("dc_ground", "ground"),
5405 equipment_container: Some(unit.clone()),
5406 dc_terminal: terminal(node1.clone(), true, Some(0.5)),
5407 rated_dc_voltage_kv: Some(320.0),
5408 resistance_ohm: Some(2.0),
5409 inductance_h: Some(0.01),
5410 }],
5411 dc_lines: vec![DcLine {
5412 component: component("dc_line", "line"),
5413 equipment_container: Some(unit.clone()),
5414 dc_terminal1: terminal(node1.clone(), true, Some(100.0)),
5415 dc_terminal2: terminal(node2.clone(), false, Some(-99.0)),
5416 rated_dc_voltage_kv: Some(320.0),
5417 resistance_ohm: Some(1.5),
5418 inductance_h: Some(0.02),
5419 capacitance_f: Some(0.001),
5420 length_km: Some(20.0),
5421 }],
5422 dc_switches: vec![DcSwitch {
5423 component: component("dc_switch", "breaker"),
5424 equipment_container: Some(unit.clone()),
5425 dc_terminal1: terminal(node1.clone(), true, None),
5426 dc_terminal2: terminal(node2.clone(), true, None),
5427 kind: DcSwitchKind::Breaker,
5428 rated_dc_voltage_kv: Some(320.0),
5429 open: Some(false),
5430 resistance_ohm: Some(0.01),
5431 }],
5432 voltage_source_converters: vec![VoltageSourceConverter {
5433 component: component("voltage_source_converter", "vsc"),
5434 dc_converter_unit: Some(unit.clone()),
5435 dc_terminal1: terminal(node1.clone(), true, Some(-100.0)),
5436 dc_terminal2: terminal(node2.clone(), true, Some(100.0)),
5437 base_apparent_power_mva: Some(200.0),
5438 minimum_active_power_mw: Some(-150.0),
5439 maximum_active_power_mw: Some(150.0),
5440 minimum_dc_voltage_kv: Some(300.0),
5441 maximum_dc_voltage_kv: Some(340.0),
5442 rated_dc_voltage_kv: Some(320.0),
5443 valve_u0_kv: Some(0.1),
5444 number_of_valves: Some(4),
5445 idle_loss_mw: Some(1.0),
5446 switching_loss_mw_per_ampere: Some(0.002),
5447 resistive_loss_ohm: Some(0.1),
5448 control_mode: Some(
5449 AcDcConverterControlMode::ActivePowerAtPccAndDcVoltageDroopCurve,
5450 ),
5451 active_power_at_pcc_mw: Some(95.0),
5452 reactive_power_at_pcc_mvar: Some(12.0),
5453 target_active_power_mw: Some(95.0),
5454 target_dc_voltage_kv: Some(320.0),
5455 pcc_terminal: Some(pcc_terminal.clone()),
5456 droop_curve: Some(droop_curve.clone()),
5457 droop: Some(2.0),
5458 droop_compensation: Some(0.1),
5459 q_share: Some(0.5),
5460 maximum_modulation_index: Some(0.9),
5461 maximum_valve_current_a: Some(800.0),
5462 voltage_regulator_on: Some(true),
5463 voltage_setpoint_kv: Some(230.0),
5464 reactive_power_setpoint_mvar: None,
5465 reactive_limits: Some(reactive_limits),
5466 pole_loss_active_power_mw: Some(1.2),
5467 dc_current_a: Some(300.0),
5468 ac_voltage_kv: Some(230.0),
5469 dc_voltage_kv: Some(318.0),
5470 delta_degrees: Some(1.0),
5471 uf_kv: Some(229.0),
5472 uv_kv: Some(231.0),
5473 }],
5474 line_commutated_converters: vec![LineCommutatedConverter {
5475 component: component("line_commutated_converter", "lcc"),
5476 dc_converter_unit: Some(unit),
5477 dc_terminal1: terminal(node1, true, Some(-75.0)),
5478 dc_terminal2: terminal(node2, true, Some(75.0)),
5479 base_apparent_power_mva: Some(160.0),
5480 minimum_active_power_mw: Some(-120.0),
5481 maximum_active_power_mw: Some(120.0),
5482 minimum_dc_voltage_kv: Some(300.0),
5483 maximum_dc_voltage_kv: Some(340.0),
5484 rated_dc_voltage_kv: Some(320.0),
5485 valve_u0_kv: Some(0.2),
5486 number_of_valves: Some(6),
5487 idle_loss_mw: Some(0.5),
5488 switching_loss_mw_per_ampere: Some(0.001),
5489 resistive_loss_ohm: Some(0.2),
5490 control_mode: Some(AcDcConverterControlMode::DcVoltage),
5491 active_power_at_pcc_mw: Some(-75.0),
5492 reactive_power_at_pcc_mvar: Some(-20.0),
5493 target_active_power_mw: None,
5494 target_dc_voltage_kv: Some(320.0),
5495 pcc_terminal: Some(pcc_terminal),
5496 droop_curve: Some(droop_curve),
5497 reactive_model: Some(LineCommutatedConverterReactiveModel::FixedPowerFactor),
5498 power_factor: Some(0.95),
5499 operating_mode: Some(LineCommutatedConverterOperatingMode::Rectifier),
5500 rated_dc_current_a: Some(500.0),
5501 minimum_alpha_degrees: Some(5.0),
5502 maximum_alpha_degrees: Some(30.0),
5503 minimum_gamma_degrees: Some(10.0),
5504 maximum_gamma_degrees: Some(35.0),
5505 target_alpha_degrees: Some(15.0),
5506 target_gamma_degrees: Some(20.0),
5507 target_dc_current_a: Some(250.0),
5508 pole_loss_active_power_mw: Some(0.8),
5509 dc_current_a: Some(250.0),
5510 ac_voltage_kv: Some(230.0),
5511 dc_voltage_kv: Some(319.0),
5512 alpha_degrees: Some(14.0),
5513 gamma_degrees: Some(19.0),
5514 }],
5515 ..DetailedConnectivity::default()
5516 };
5517
5518 let json = serde_json::to_string(&detailed).unwrap();
5519 let restored: DetailedConnectivity = serde_json::from_str(&json).unwrap();
5520 assert_eq!(restored, detailed);
5521
5522 let mut older_terminal = serde_json::to_value(&detailed.terminals[0]).unwrap();
5523 let older_terminal = older_terminal.as_object_mut().unwrap();
5524 older_terminal.remove("active_power_mw");
5525 older_terminal.remove("reactive_power_mvar");
5526 let older_terminal: Terminal =
5527 serde_json::from_value(serde_json::Value::Object(older_terminal.clone())).unwrap();
5528 assert_eq!(older_terminal.active_power_mw, None);
5529 assert_eq!(older_terminal.reactive_power_mvar, None);
5530
5531 let min_max = ReactiveLimits::MinMax(MinMaxReactiveLimits {
5532 minimum_reactive_power_mvar: -25.0,
5533 maximum_reactive_power_mvar: 30.0,
5534 properties: BTreeMap::new(),
5535 });
5536 let json = serde_json::to_string(&min_max).unwrap();
5537 assert_eq!(
5538 serde_json::from_str::<ReactiveLimits>(&json).unwrap(),
5539 min_max
5540 );
5541 }
5542
5543 #[test]
5544 fn converter_droop_control_modes_have_distinct_serialized_names() {
5545 for (mode, name) in [
5546 (
5547 AcDcConverterControlMode::ActivePowerAtPccAndDcVoltageDroopCurve,
5548 "active_power_at_pcc_and_dc_voltage_droop_curve",
5549 ),
5550 (
5551 AcDcConverterControlMode::ActivePowerAtPccAndDcVoltageDroop,
5552 "active_power_at_pcc_and_dc_voltage_droop",
5553 ),
5554 (
5555 AcDcConverterControlMode::ActivePowerAtPccAndDcVoltageDroopWithCompensation,
5556 "active_power_at_pcc_and_dc_voltage_droop_with_compensation",
5557 ),
5558 (
5559 AcDcConverterControlMode::ActivePowerAtPccAndDcVoltageDroopPilot,
5560 "active_power_at_pcc_and_dc_voltage_droop_pilot",
5561 ),
5562 ] {
5563 let value = serde_json::to_value(mode).unwrap();
5564 assert_eq!(value, serde_json::Value::String(name.to_owned()));
5565 assert_eq!(
5566 serde_json::from_value::<AcDcConverterControlMode>(value).unwrap(),
5567 mode
5568 );
5569 }
5570 }
5571
5572 #[test]
5573 fn assigned_component_ids_follow_records_after_reordering() {
5574 let mut network = BalancedNetwork::in_memory(
5575 "ids",
5576 100.0,
5577 vec![
5578 Bus::new(BusId(10), BusType::Ref, 230.0),
5579 Bus::new(BusId(20), BusType::Pq, 230.0),
5580 ],
5581 vec![
5582 Branch::new(BusId(10), BusId(20), 0.0, 0.1),
5583 Branch::new(BusId(10), BusId(20), 0.0, 0.2),
5584 ],
5585 );
5586 network.loads_mut().extend([
5587 Load::new(BusId(20), 10.0, 1.0),
5588 Load::new(BusId(20), 20.0, 2.0),
5589 ]);
5590 network.assign_missing_component_ids();
5591
5592 assert_eq!(network.buses()[0].uid.as_deref(), Some("10"));
5593 assert_eq!(network.branches()[0].uid.as_deref(), Some("10-20"));
5594 assert_eq!(network.branches()[1].uid.as_deref(), Some("10-20-2"));
5595 assert_eq!(network.loads()[0].uid.as_deref(), Some("bus-20"));
5596 assert_eq!(network.loads()[1].uid.as_deref(), Some("bus-20-2"));
5597 assert!(network.uid_is_generated(network.loads()[0].uid.as_deref()));
5598
5599 network.loads_mut().swap(0, 1);
5600 network.branches_mut().swap(0, 1);
5601 network.assign_missing_component_ids();
5602 assert_eq!(network.loads()[0].uid.as_deref(), Some("bus-20-2"));
5603 assert_eq!(network.loads()[1].uid.as_deref(), Some("bus-20"));
5604 assert_eq!(network.branches()[0].uid.as_deref(), Some("10-20-2"));
5605 assert_eq!(network.branches()[1].uid.as_deref(), Some("10-20"));
5606
5607 let restored = serde_round_trip(&network);
5608 assert_eq!(restored.loads()[0].uid, network.loads()[0].uid);
5609 assert_eq!(restored.branches()[0].uid, network.branches()[0].uid);
5610 assert!(restored.uid_is_generated(restored.loads()[0].uid.as_deref()));
5611 }
5612
5613 #[test]
5614 fn source_and_generated_component_identities_remain_distinct() {
5615 let mut explicit = Bus::new(BusId(10), BusType::Ref, 230.0);
5616 explicit.uid = Some("source-bus".to_owned());
5617 let mut network = BalancedNetwork::new("identity provenance", 100.0);
5618 network
5619 .buses_mut()
5620 .extend([explicit, Bus::new(BusId(20), BusType::Pq, 230.0)]);
5621 network.assign_missing_component_ids();
5622
5623 assert!(!network.uid_is_generated(network.buses()[0].uid.as_deref()));
5624 assert!(network.uid_is_generated(network.buses()[1].uid.as_deref()));
5625 }
5626
5627 #[test]
5630 fn generated_identities_are_unique_across_tables() {
5631 let mut network = BalancedNetwork::in_memory(
5632 "cross table ids",
5633 100.0,
5634 vec![
5635 Bus::new(BusId(10), BusType::Ref, 230.0),
5636 Bus::new(BusId(20), BusType::Pq, 230.0),
5637 ],
5638 vec![Branch::new(BusId(10), BusId(20), 0.0, 0.1)],
5639 );
5640 network.loads_mut().push(Load::new(BusId(20), 10.0, 1.0));
5641 network.shunts_mut().push(Shunt::new(BusId(20), 0.0, 0.5));
5642 network.generators_mut().push(Generator::new(BusId(20)));
5643 network.assign_missing_component_ids();
5644
5645 let mut identities = vec![
5646 network.loads()[0].uid.clone().unwrap(),
5647 network.shunts()[0].uid.clone().unwrap(),
5648 network.generators()[0].uid.clone().unwrap(),
5649 ];
5650 identities.sort();
5651 assert_eq!(identities, ["bus-20", "bus-20-2", "bus-20-3"]);
5652
5653 let before = identities.clone();
5656 network.assign_missing_component_ids();
5657 let mut after = vec![
5658 network.loads()[0].uid.clone().unwrap(),
5659 network.shunts()[0].uid.clone().unwrap(),
5660 network.generators()[0].uid.clone().unwrap(),
5661 ];
5662 after.sort();
5663 assert_eq!(after, before);
5664 }
5665
5666 #[test]
5667 fn source_format_serializes_as_its_name_token() {
5668 let all = [
5671 SourceFormat::Matpower,
5672 SourceFormat::PowerModelsJson,
5673 SourceFormat::EgretJson,
5674 SourceFormat::Psse,
5675 SourceFormat::PsseRawx,
5676 SourceFormat::PowerWorld,
5677 SourceFormat::PandapowerJson,
5678 SourceFormat::Pslf,
5679 SourceFormat::PowerWorldBinary,
5680 SourceFormat::InMemory,
5681 SourceFormat::Normalized,
5682 SourceFormat::Gridfm,
5683 SourceFormat::PypsaCsv,
5684 SourceFormat::Goc3Json,
5685 SourceFormat::SurgeJson,
5686 SourceFormat::DeepMindOpfDataJson,
5687 SourceFormat::Xiidm,
5688 SourceFormat::Jiidm,
5689 SourceFormat::Cgmes,
5690 SourceFormat::Ucte,
5691 SourceFormat::IeeeCdf,
5692 ];
5693 for f in all {
5694 match f {
5695 SourceFormat::Matpower
5696 | SourceFormat::PowerModelsJson
5697 | SourceFormat::EgretJson
5698 | SourceFormat::Psse
5699 | SourceFormat::PsseRawx
5700 | SourceFormat::PowerWorld
5701 | SourceFormat::PandapowerJson
5702 | SourceFormat::Pslf
5703 | SourceFormat::PowerWorldBinary
5704 | SourceFormat::InMemory
5705 | SourceFormat::Normalized
5706 | SourceFormat::Gridfm
5707 | SourceFormat::PypsaCsv
5708 | SourceFormat::Goc3Json
5709 | SourceFormat::SurgeJson
5710 | SourceFormat::DeepMindOpfDataJson
5711 | SourceFormat::Xiidm
5712 | SourceFormat::Jiidm
5713 | SourceFormat::Cgmes
5714 | SourceFormat::Ucte
5715 | SourceFormat::IeeeCdf => {}
5716 }
5717 let token = serde_json::to_value(f).unwrap();
5718 assert_eq!(token, serde_json::Value::String(f.name().to_owned()));
5719 let back: SourceFormat = serde_json::from_value(token).unwrap();
5720 assert_eq!(back, f);
5721 }
5722 }
5723
5724 #[test]
5725 fn quadratic_with_constant_keeps_c0_across_ncost() {
5726 let full = GenCost::new(2, 0.0, 0.0, vec![1.5, 2.0, 5.0]);
5727 assert_eq!(full.calc_quadratic_with_constant(), Some((3.0, 2.0, 5.0)));
5728 assert_eq!(full.calc_quadratic(), Some((3.0, 2.0)));
5729 assert_eq!(
5730 full.calc_quadratic_with_constant(),
5731 full.calc_quadratic_with_constant()
5732 );
5733 assert_eq!(full.calc_quadratic(), full.calc_quadratic());
5734
5735 let linear = GenCost::new(2, 0.0, 0.0, vec![2.0, 5.0]);
5736 assert_eq!(linear.calc_quadratic_with_constant(), Some((0.0, 2.0, 5.0)));
5737
5738 let constant = GenCost::new(2, 0.0, 0.0, vec![5.0]);
5739 assert_eq!(
5740 constant.calc_quadratic_with_constant(),
5741 Some((0.0, 0.0, 5.0))
5742 );
5743
5744 let piecewise = GenCost::new(1, 0.0, 0.0, vec![0.0, 0.0, 1.0, 1.0]);
5745 assert_eq!(piecewise.calc_quadratic_with_constant(), None);
5746
5747 let cubic = GenCost::new(2, 0.0, 0.0, vec![1.0, 1.0, 1.0, 1.0]);
5748 assert_eq!(cubic.calc_quadratic_with_constant(), None);
5749
5750 let truncated = GenCost::with_ncost(2, 0.0, 0.0, 3, vec![1.0]);
5751 assert_eq!(truncated.calc_quadratic_with_constant(), None);
5752 }
5753
5754 #[test]
5755 fn a_leading_coefficient_below_the_tolerance_comes_off_the_row() {
5756 let artifact = GenCost::new(2, 0.0, 0.0, vec![1e-17, 2.0, 5.0]);
5757 assert_eq!(
5758 artifact.calc_quadratic_with_constant(),
5759 Some((2e-17, 2.0, 5.0)),
5760 "the untouched reader keeps the artifact"
5761 );
5762 assert_eq!(
5763 artifact.calc_quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
5764 Some((0.0, 2.0, 5.0))
5765 );
5766 assert_eq!(
5767 artifact.calc_quadratic_with_constant_tol(0.0),
5768 Some((2e-17, 2.0, 5.0)),
5769 "a zero tolerance strips an exact zero alone"
5770 );
5771
5772 let padded = GenCost::new(2, 0.0, 0.0, vec![0.0, 1.5, 2.0, 5.0]);
5775 assert_eq!(padded.calc_quadratic_with_constant(), None);
5776 assert_eq!(
5777 padded.calc_quadratic_with_constant_tol(0.0),
5778 Some((3.0, 2.0, 5.0))
5779 );
5780
5781 let flat = GenCost::new(2, 0.0, 0.0, vec![1e-17, 1e-17, 1e-17]);
5782 assert_eq!(
5783 flat.calc_quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
5784 Some((0.0, 0.0, 1e-17)),
5785 "the last coefficient stays, whatever its magnitude"
5786 );
5787
5788 let piecewise = GenCost::new(1, 0.0, 0.0, vec![0.0, 0.0, 1.0, 1.0]);
5789 assert_eq!(
5790 piecewise.calc_quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
5791 None
5792 );
5793
5794 let truncated = GenCost::with_ncost(2, 0.0, 0.0, 3, vec![1.0]);
5795 assert_eq!(
5796 truncated.calc_quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
5797 None
5798 );
5799
5800 let quartic = GenCost::new(2, 0.0, 0.0, vec![1.0, 1.0, 1.0, 1.0, 1.0]);
5801 assert_eq!(
5802 quartic.calc_quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
5803 None
5804 );
5805 }
5806
5807 fn expected_rate(window: f64, fr: f64, to: f64, zmag: f64) -> f64 {
5810 let separation = (fr * fr + to * to - 2.0 * fr * to * window.cos()).sqrt();
5811 fr.max(to) * separation / zmag
5812 }
5813
5814 #[test]
5815 fn synthesized_rate_follows_the_angle_window_and_the_voltage_bands() {
5816 let br = Branch::new(BusId(1), BusId(2), 0.03, 0.04);
5817 let expected = |window: f64, fr: f64, to: f64| expected_rate(window, fr, to, 0.05);
5818 let at = |v: f64| (v, v);
5821 close(
5822 br.synthesize_rate_a(0.5, at(1.1), at(1.06)),
5823 expected(0.5, 1.1, 1.06),
5824 );
5825
5826 assert!(
5828 br.synthesize_rate_a(0.8, at(1.1), at(1.06))
5829 > br.synthesize_rate_a(0.5, at(1.1), at(1.06))
5830 );
5831
5832 close(
5834 br.synthesize_rate_a(-0.5, at(1.1), at(1.06)),
5835 expected(0.5, 1.1, 1.06),
5836 );
5837 for window in [6.0, 2.0 * std::f64::consts::PI, -360.0] {
5838 close(
5839 br.synthesize_rate_a(window, at(1.1), at(1.06)),
5840 expected(std::f64::consts::PI, 1.1, 1.06),
5841 );
5842 }
5843
5844 let ideal = Branch::new(BusId(1), BusId(2), 0.0, 0.0);
5845 close(ideal.synthesize_rate_a(0.5, at(1.1), at(1.1)), 0.0);
5846 }
5847
5848 #[test]
5849 fn a_narrow_window_bounds_at_the_mixed_voltage_corner() {
5850 let br = Branch::new(BusId(1), BusId(2), 0.0, 0.01);
5857 let (vmin, vmax) = (0.9, 1.1);
5858 let corner = |window: f64, fr: f64, to: f64| expected_rate(window, fr, to, 0.01);
5859
5860 let narrow = 2.0_f64.to_radians();
5861 let bound = br.synthesize_rate_a(narrow, (vmin, vmax), (vmin, vmax));
5862 close(bound, corner(narrow, vmax, vmin));
5863 assert!(
5864 bound > 5.0 * corner(narrow, vmax, vmax),
5865 "the mixed corner dominates here: {bound} vs {}",
5866 corner(narrow, vmax, vmax)
5867 );
5868
5869 let wide = 30.0_f64.to_radians();
5871 close(
5872 br.synthesize_rate_a(wide, (vmin, vmax), (vmin, vmax)),
5873 corner(wide, vmax, vmax),
5874 );
5875 }
5876
5877 fn bus(id: usize) -> Bus {
5878 Bus {
5879 id: BusId(id),
5880 kind: BusType::Pq,
5881 vm: 1.0,
5882 va: 0.0,
5883 base_kv: 230.0,
5884 vmax: 1.1,
5885 vmin: 0.9,
5886 evhi: None,
5887 evlo: None,
5888 area: 1,
5889 zone: 1,
5890 name: None,
5891 uid: None,
5892 location: None,
5893 extras: Extras::new(),
5894 }
5895 }
5896
5897 fn winding(b: usize) -> Winding {
5898 Winding {
5899 bus: BusId(b),
5900 tap: 1.0,
5901 shift: 0.0,
5902 nominal_kv: 230.0,
5903 rate_a: 100.0,
5904 rate_b: 0.0,
5905 rate_c: 0.0,
5906 control: None,
5907 }
5908 }
5909
5910 fn transformer_3w() -> Transformer3W {
5911 let z = |r, x| Impedance {
5912 r,
5913 x,
5914 base_mva: 100.0,
5915 };
5916 Transformer3W {
5917 windings: [winding(1), winding(2), winding(3)],
5918 z: [z(0.01, 0.10), z(0.02, 0.20), z(0.03, 0.30)],
5919 star_vm: 0.98,
5920 star_va: -1.5,
5921 mag_g: 0.0,
5922 mag_b: 0.0,
5923 in_service: true,
5924 name: Some("T1".into()),
5925 uid: None,
5926 extras: Extras::new(),
5927 }
5928 }
5929
5930 #[test]
5931 fn star_impedances_split_the_pairwise_values() {
5932 let [(r1, x1), (r2, x2), (r3, x3)] = transformer_3w().calc_star_impedances();
5934 close(r1, 0.01);
5935 close(x1, 0.10);
5936 close(r2, 0.0);
5937 close(x2, 0.0);
5938 close(r3, 0.02);
5939 close(x3, 0.20);
5940 }
5941
5942 #[test]
5943 fn star_expansion_builds_a_star_bus_and_three_branches() {
5944 let t = transformer_3w();
5945 let (star, branches) = t.to_star_expansion(BusId(99));
5946
5947 assert_eq!(star.id, BusId(99));
5948 close(star.vm, 0.98);
5949 close(star.va, -1.5);
5950 for (i, br) in branches.iter().enumerate() {
5953 assert_eq!(br.from, t.windings[i].bus);
5954 assert_eq!(br.to, BusId(99));
5955 close(br.tap, 1.0);
5956 close(br.rate_a, 100.0);
5957 }
5958 close(branches[2].r, 0.02);
5959 close(branches[2].x, 0.20);
5960 }
5961
5962 #[test]
5963 fn three_winding_transformer_survives_serde_round_trip() {
5964 let mut net =
5965 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
5966 net.transformers_3w_mut().push(transformer_3w());
5967 net.validate().unwrap();
5968
5969 let back = serde_round_trip(&net);
5970 assert_eq!(back.transformers_3w().len(), 1);
5971 close(back.transformers_3w()[0].z[1].x, 0.20);
5972 assert_eq!(back.transformers_3w()[0].windings[2].bus, BusId(3));
5973 }
5974
5975 #[test]
5976 fn lowered_lengths_match_the_expansion() {
5977 let mut magnetizing = transformer_3w();
5982 magnetizing.mag_b = 0.02;
5983 let mut out_of_service = transformer_3w();
5984 out_of_service.in_service = false;
5985 out_of_service.mag_g = 0.01;
5986
5987 for units in [
5988 vec![],
5989 vec![transformer_3w()],
5990 vec![magnetizing.clone()],
5991 vec![out_of_service.clone()],
5992 vec![transformer_3w(), magnetizing, out_of_service],
5993 ] {
5994 let mut net =
5995 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
5996 net.shunts_mut().push(Shunt::new(BusId(1), 0.0, 0.5));
5997 *net.transformers_3w_mut() = units;
5998 let counted = net.lowered_lengths();
5999 let built = net.expand_transformers_3w();
6000 assert_eq!(counted.buses, built.buses().len());
6001 assert_eq!(counted.branches, built.branches().len());
6002 assert_eq!(counted.shunts, built.shunts().len());
6003 }
6004 }
6005
6006 #[test]
6007 fn check_references_rejects_bus_ids_without_star_expansion_headroom() {
6008 let mut net = BalancedNetwork::in_memory(
6012 "t",
6013 100.0,
6014 vec![bus(1), bus(2), bus(3), bus(i64::MAX as usize)],
6015 Vec::new(),
6016 );
6017 net.transformers_3w_mut().push(transformer_3w());
6018 let err = net.validate().unwrap_err().to_string();
6019 assert!(
6020 err.contains("no room to allocate synthetic star bus ids"),
6021 "got {err}"
6022 );
6023 }
6024
6025 #[test]
6026 fn star_expansion_headroom_counts_only_in_service_transformers() {
6027 let mut net = BalancedNetwork::in_memory(
6033 "t",
6034 100.0,
6035 vec![bus(1), bus(2), bus(3), bus(i64::MAX as usize - 1)],
6036 Vec::new(),
6037 );
6038 net.transformers_3w_mut().push(transformer_3w());
6039 let mut out_of_service = transformer_3w();
6040 out_of_service.in_service = false;
6041 net.transformers_3w_mut().push(out_of_service);
6042 net.validate()
6043 .expect("in-service count fits; must not be rejected");
6044 }
6045
6046 #[test]
6047 fn check_references_rejects_a_bus_id_past_the_int64_ceiling() {
6048 let mut net = BalancedNetwork::in_memory(
6053 "t",
6054 100.0,
6055 vec![bus(1), bus(i64::MAX as usize + 1)],
6056 Vec::new(),
6057 );
6058 let err = net.validate().unwrap_err().to_string();
6059 assert!(err.contains("outside the int64 id space"), "got {err}");
6060
6061 net.buses_mut()[1].id = BusId(i64::MAX as usize);
6063 net.validate().expect("the ceiling itself is representable");
6064 }
6065
6066 #[test]
6067 fn check_references_rejects_a_dangling_winding_bus() {
6068 let mut net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
6069 net.transformers_3w_mut().push(transformer_3w()); let err = net.validate().unwrap_err().to_string();
6071 assert!(
6072 err.contains("3-winding transformer references unknown bus 3"),
6073 "got {err}"
6074 );
6075 }
6076
6077 #[test]
6078 fn check_references_rejects_a_dangling_winding_control_bus() {
6079 let mut net =
6080 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
6081 let mut transformer = transformer_3w();
6082 let mut control = TransformerControl::new(TransformerControlMode::Voltage);
6083 control.controlled_bus = Some(BusId(4));
6084 transformer.windings[2].control = Some(control);
6085 net.transformers_3w_mut().push(transformer);
6086 let err = net.validate().unwrap_err().to_string();
6087 assert!(
6088 err.contains("3-winding transformer control references unknown bus 4"),
6089 "got {err}"
6090 );
6091 }
6092
6093 fn regulating_branch(reg: usize) -> Branch {
6095 Branch {
6096 name: None,
6097 from: BusId(1),
6098 to: BusId(2),
6099 r: 0.0,
6100 x: 0.1,
6101 b: 0.0,
6102 charging: None,
6103 rate_a: 0.0,
6104 rate_b: 0.0,
6105 rate_c: 0.0,
6106 rating_sets: Vec::new(),
6107 current_ratings: None,
6108 tap: 1.0,
6109 shift: 0.0,
6110 in_service: true,
6111 angmin: -360.0,
6112 angmax: 360.0,
6113 control: Some(TransformerControl {
6114 mode: TransformerControlMode::Voltage,
6115 enabled: true,
6116 controlled_bus: Some(BusId(reg)),
6117 controlled_bus_on_winding_side: false,
6118 regulating_terminal: None,
6119 tap_min: 0.95,
6120 tap_max: 1.05,
6121 band_min: 1.0,
6122 band_max: 1.02,
6123 ntp: 17,
6124 mva_base: 100.0,
6125 winding_connection_angle: None,
6126 }),
6127 solution: None,
6128 uid: None,
6129 route: None,
6130 extras: Extras::new(),
6131 }
6132 }
6133
6134 #[test]
6135 fn transformer_control_survives_serde_round_trip() {
6136 let mut net =
6137 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
6138 net.branches_mut().push(regulating_branch(3));
6139 net.validate().unwrap();
6140
6141 let back = serde_round_trip(&net);
6142 let c = back.branches()[0].control.as_ref().unwrap();
6143 assert_eq!(c.mode, TransformerControlMode::Voltage);
6144 assert_eq!(c.controlled_bus, Some(BusId(3)));
6145 close(c.tap_max, 1.05);
6146 assert_eq!(c.ntp, 17);
6147 }
6148
6149 #[test]
6150 fn gen_caps_serialize_as_a_named_map_that_grows_additively() {
6151 let mut caps: GenCaps = [None; GEN_EXTRA_KEYS.len()];
6152 caps[8] = Some(1.5); caps[10] = Some(0.5); let g = Generator {
6155 bus: BusId(1),
6156 energy_source: GeneratorEnergySource::default(),
6157 pg: 10.0,
6158 qg: 0.0,
6159 pmax: 100.0,
6160 pmin: 0.0,
6161 qmax: 50.0,
6162 qmin: -50.0,
6163 vg: 1.0,
6164 mbase: 100.0,
6165 in_service: true,
6166 cost: None,
6167 caps,
6168 voltage_regulation_on: false,
6169 regulating_terminal: Some(TerminalReference {
6170 equipment: ComponentId::new("load", "L1").unwrap(),
6171 terminal: 1,
6172 }),
6173 regulated_bus: Some(BusId(2)),
6174 active_power_control: None,
6175 uid: None,
6176 };
6177
6178 let json = serde_json::to_string(&g).unwrap();
6181 assert!(json.contains(r#""caps":{"#), "caps is an object: {json}");
6182 assert!(json.contains(r#""ramp_30":1.5"#) && json.contains(r#""apf":0.5"#));
6183 let back: Generator = serde_json::from_str(&json).unwrap();
6184 assert_eq!(back.caps, g.caps);
6185 assert!(!back.voltage_regulation_on);
6186 assert_eq!(back.regulating_terminal, g.regulating_terminal);
6187 assert_eq!(back.regulated_bus, Some(BusId(2)));
6188
6189 let with_future = r#"{"bus":1,"pg":10,"qg":0,"pmax":100,"pmin":0,"qmax":50,"qmin":-50,
6192 "vg":1,"mbase":100,"in_service":true,"cost":null,
6193 "caps":{"ramp_30":1.5,"future_ramp":9.9}}"#;
6194 let g2: Generator = serde_json::from_str(with_future).unwrap();
6195 assert_eq!(g2.caps[8], Some(1.5));
6196 assert_eq!(g2.caps.iter().filter(|v| v.is_some()).count(), 1);
6197 let no_caps = r#"{"bus":1,"pg":10,"qg":0,"pmax":100,"pmin":0,"qmax":50,"qmin":-50,
6198 "vg":1,"mbase":100,"in_service":true,"cost":null}"#;
6199 let g3: Generator = serde_json::from_str(no_caps).unwrap();
6200 assert!(!g3.has_caps());
6201 assert!(g3.voltage_regulation_on);
6202 assert_eq!(g3.regulating_terminal, None);
6203
6204 let null_caps = r#"{"bus":1,"pg":10,"qg":0,"pmax":100,"pmin":0,"qmax":50,"qmin":-50,
6206 "vg":1,"mbase":100,"in_service":true,"cost":null,"caps":null}"#;
6207 let g4: Generator = serde_json::from_str(null_caps).unwrap();
6208 assert!(!g4.has_caps());
6209 }
6210
6211 #[test]
6212 #[allow(clippy::float_cmp)]
6213 fn nonfinite_values_round_trip_through_serde() {
6214 let bus = |id, vm| Bus {
6215 id: BusId(id),
6216 kind: BusType::Pq,
6217 vm,
6218 va: 0.0,
6219 base_kv: 230.0,
6220 vmax: 1.1,
6221 vmin: 0.9,
6222 evhi: None,
6223 evlo: None,
6224 area: 1,
6225 zone: 1,
6226 name: None,
6227 uid: None,
6228 location: None,
6229 extras: Extras::new(),
6230 };
6231 let branch = Branch {
6232 name: None,
6233 from: BusId(1),
6234 to: BusId(2),
6235 r: 0.0,
6236 x: f64::INFINITY,
6237 b: 0.0,
6238 charging: None,
6239 rate_a: 0.0,
6240 rate_b: 0.0,
6241 rate_c: 0.0,
6242 rating_sets: Vec::new(),
6243 current_ratings: None,
6244 tap: 0.0,
6245 shift: 0.0,
6246 in_service: true,
6247 angmin: -360.0,
6248 angmax: 360.0,
6249 control: None,
6250 solution: None,
6251 uid: None,
6252 route: None,
6253 extras: Extras::new(),
6254 };
6255 let mut g = Generator {
6258 bus: BusId(1),
6259 energy_source: GeneratorEnergySource::default(),
6260 pg: 0.0,
6261 qg: 0.0,
6262 pmax: 0.0,
6263 pmin: 0.0,
6264 qmax: 0.0,
6265 qmin: 0.0,
6266 vg: 1.0,
6267 mbase: 100.0,
6268 in_service: true,
6269 cost: None,
6270 caps: GenCaps::default(),
6271 voltage_regulation_on: true,
6272 regulating_terminal: None,
6273 regulated_bus: None,
6274 active_power_control: None,
6275 uid: None,
6276 };
6277 g.caps[8] = Some(f64::INFINITY); let mut net = BalancedNetwork::in_memory(
6282 "nf",
6283 100.0,
6284 vec![bus(1, f64::NAN), bus(2, 1.0)],
6285 vec![branch],
6286 );
6287 net.generators_mut().push(g);
6288
6289 let text = serde_json::to_string(&net).unwrap();
6290 assert!(text.contains(r#""vm":"NaN""#), "{text}");
6291 assert!(text.contains(r#""x":"Infinity""#), "{text}");
6292 assert!(text.contains(r#""ramp_30":"Infinity""#), "{text}");
6293
6294 let back: BalancedNetwork = serde_json::from_str(&text).unwrap();
6295 assert!(back.buses()[0].vm.is_nan());
6296 assert_eq!(back.branches()[0].x, f64::INFINITY);
6297 assert_eq!(back.generators()[0].caps[8], Some(f64::INFINITY));
6298
6299 assert_eq!(serde_json::to_string(&back).unwrap(), text);
6301 }
6302
6303 #[test]
6304 fn a_null_at_a_float_position_is_rejected() {
6305 let net = BalancedNetwork::in_memory("nf", 100.0, vec![bus(1), bus(2)], Vec::new());
6306 let text = serde_json::to_string(&net)
6307 .unwrap()
6308 .replacen("\"vm\":1.0", "\"vm\":null", 1);
6309 assert!(text.contains("\"vm\":null"), "fixture edit failed: {text}");
6310 let err = serde_json::from_str::<BalancedNetwork>(&text)
6311 .unwrap_err()
6312 .to_string();
6313 assert!(err.contains("cannot be null"), "{err}");
6314 }
6315
6316 #[test]
6317 fn check_references_rejects_a_dangling_controlled_bus() {
6318 let mut net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
6319 net.branches_mut().push(regulating_branch(9)); let err = net.validate().unwrap_err().to_string();
6321 assert!(
6322 err.contains("transformer control references unknown bus 9"),
6323 "got {err}"
6324 );
6325 }
6326
6327 #[test]
6328 fn check_references_rejects_winding_side_without_a_controlled_bus() {
6329 let mut net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
6330 let mut branch = regulating_branch(2);
6331 let control = branch.control.as_mut().unwrap();
6332 control.controlled_bus = None;
6333 control.controlled_bus_on_winding_side = true;
6334 net.branches_mut().push(branch);
6335
6336 let err = net.validate().unwrap_err().to_string();
6337 assert!(
6338 err.contains("winding side but has no nonzero controlled bus"),
6339 "got {err}"
6340 );
6341 }
6342
6343 fn switched_shunt(reg: usize) -> Shunt {
6345 Shunt {
6346 bus: BusId(1),
6347 g: 0.0,
6348 b: 19.0,
6349 in_service: true,
6350 section_count: None,
6351 control: Some(SwitchedShuntControl {
6352 mode: SwitchedShuntMode::Discrete,
6353 vhigh: 1.05,
6354 vlow: 0.95,
6355 control_bus: Some(BusId(reg)),
6356 regulating_terminal: None,
6357 rmpct: 100.0,
6358 blocks: vec![ShuntBlock::new(2, 25.0), ShuntBlock::new(1, 50.0)],
6359 }),
6360 uid: None,
6361 extras: Extras::new(),
6362 }
6363 }
6364
6365 #[test]
6366 fn switched_shunt_control_survives_serde_round_trip() {
6367 let mut net =
6368 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
6369 net.shunts_mut().push(switched_shunt(3));
6370 net.validate().unwrap();
6371
6372 let back = serde_round_trip(&net);
6373 let c = back.shunts()[0].control.as_ref().unwrap();
6374 assert_eq!(c.mode, SwitchedShuntMode::Discrete);
6375 assert_eq!(c.control_bus, Some(BusId(3)));
6376 assert_eq!(c.blocks.len(), 2);
6377 close(c.blocks[1].b, 50.0);
6378 }
6379
6380 #[test]
6381 fn check_references_rejects_a_dangling_switched_shunt_control_bus() {
6382 let mut net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
6383 net.shunts_mut().push(switched_shunt(9)); let err = net.validate().unwrap_err().to_string();
6385 assert!(
6386 err.contains("switched-shunt control references unknown bus 9"),
6387 "got {err}"
6388 );
6389 }
6390
6391 #[test]
6392 fn validate_values_flags_and_repair_clamps_out_of_domain_values() {
6393 let mut net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
6394 net.buses_mut()[0].vm = 0.0; net.buses_mut()[1].va = 9000.0; net.generators_mut().push(Generator {
6397 bus: BusId(1),
6398 energy_source: GeneratorEnergySource::default(),
6399 pg: 10.0,
6400 qg: 0.0,
6401 pmax: 100.0,
6402 pmin: 0.0,
6403 qmax: 50.0,
6404 qmin: -50.0,
6405 vg: 0.0, mbase: 0.0, in_service: true,
6408 cost: None,
6409 voltage_regulation_on: true,
6410 regulating_terminal: None,
6411 caps: Default::default(),
6412 regulated_bus: None,
6413 active_power_control: None,
6414 uid: None,
6415 });
6416
6417 let diags = net.validate_values();
6418 let fields: std::collections::BTreeSet<_> = diags
6419 .iter()
6420 .map(|d| d.details()["field"].as_str().unwrap().to_owned())
6421 .collect();
6422 assert_eq!(
6423 fields,
6424 ["mbase", "va", "vg", "vm"]
6425 .into_iter()
6426 .map(str::to_owned)
6427 .collect(),
6428 "all four out-of-domain fields reported"
6429 );
6430 assert!(
6431 diags
6432 .iter()
6433 .all(|d| d.code() == "VALIDATE.BALANCED.VALUE_DOMAIN" && d.target().is_some())
6434 );
6435 close(net.buses()[0].vm, 0.0);
6437
6438 let module = powerio_core::PioModule::new(net);
6440 let module = repair_values(module).unwrap();
6441 let net = &module.value();
6442 close(net.buses()[0].vm, 1.0);
6443 close(net.buses()[1].va, 0.0);
6444 close(net.generators()[0].mbase, 100.0); close(net.generators()[0].vg, 1.0);
6446 assert!(net.validate_values().is_empty());
6449 let entries = module.history();
6450 assert_eq!(entries.len(), 1);
6451 assert_eq!(entries[0].kind(), powerio_core::HistoryKind::Repair);
6452 assert_eq!(
6453 entries[0].parameters()["repairs"].as_array().unwrap().len(),
6454 diags.len()
6455 );
6456 assert_eq!(module.diagnostics().len(), diags.len());
6457 let module = repair_values(module).unwrap();
6458 assert_eq!(module.history().len(), 1);
6459 }
6460
6461 #[test]
6462 fn validate_values_is_empty_for_a_clean_network() {
6463 let net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
6464 assert!(net.validate_values().is_empty());
6465 }
6466
6467 #[test]
6468 fn capability_curve_rejects_a_nonfinite_active_power() {
6469 let limits = ReactiveLimits::CapabilityCurve(ReactiveCapabilityCurve {
6470 curve_style: CurveStyle::StraightLineYValues,
6471 properties: BTreeMap::new(),
6472 points: vec![
6473 ReactiveCapabilityCurvePoint {
6474 active_power_mw: 0.0,
6475 minimum_reactive_power_mvar: -10.0,
6476 maximum_reactive_power_mvar: 10.0,
6477 properties: BTreeMap::new(),
6478 },
6479 ReactiveCapabilityCurvePoint {
6480 active_power_mw: 100.0,
6481 minimum_reactive_power_mvar: -5.0,
6482 maximum_reactive_power_mvar: 5.0,
6483 properties: BTreeMap::new(),
6484 },
6485 ],
6486 });
6487
6488 let error = calc_reactive_limits_at_active_power("generator 1", &limits, f64::NAN)
6489 .expect_err("NaN must not be used to index a capability curve");
6490 assert!(error.contains("nonfinite active power"), "{error}");
6491 }
6492}