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
25mod transformer_3w_serde;
26
27pub type Extras = BTreeMap<String, Value>;
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
34#[non_exhaustive]
35pub struct ComponentAlias {
36 pub value: String,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub alias_type: Option<String>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
43#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
44#[non_exhaustive]
45pub struct ExternalIdentifier {
46 pub value: String,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub authority: Option<String>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
54#[non_exhaustive]
55pub struct ComponentMetadata {
56 pub component: ComponentId,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub name: Option<String>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub equipment_container: Option<ComponentId>,
63 #[serde(default)]
64 pub aliases: Vec<ComponentAlias>,
65 #[serde(default)]
66 pub external_identifiers: Vec<ExternalIdentifier>,
67 #[serde(default)]
68 pub properties: BTreeMap<String, String>,
69 #[serde(default)]
70 pub fictitious: bool,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
75#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
76#[non_exhaustive]
77pub struct Substation {
78 pub component: ComponentId,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub country: Option<String>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 pub operator: Option<String>,
83 #[serde(default)]
84 pub geographical_tags: Vec<String>,
85}
86
87#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
89#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
90#[serde(rename_all = "snake_case")]
91#[non_exhaustive]
92pub enum TopologyKind {
93 BusBreaker,
94 NodeBreaker,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
99#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
100#[non_exhaustive]
101pub struct VoltageLevel {
102 pub component: ComponentId,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub substation: Option<ComponentId>,
105 pub nominal_kv: f64,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
107 pub low_voltage_limit_kv: Option<f64>,
108 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub high_voltage_limit_kv: Option<f64>,
110 pub topology_kind: TopologyKind,
111 #[serde(default)]
113 pub buses: Vec<BusId>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
118#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
119#[non_exhaustive]
120pub struct ConnectivityNode {
121 pub component: ComponentId,
122 pub voltage_level: ComponentId,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub node_number: Option<i32>,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
129 pub calculated_bus: Option<BusId>,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
135#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
136#[non_exhaustive]
137pub struct BusBreakerBus {
138 pub component: ComponentId,
139 pub voltage_level: ComponentId,
140 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub calculated_bus: Option<BusId>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub voltage_kv: Option<f64>,
144 #[serde(default, skip_serializing_if = "Option::is_none")]
145 pub angle_degrees: Option<f64>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
154#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
155#[non_exhaustive]
156pub struct CalculatedBus {
157 pub voltage_level: ComponentId,
158 pub calculated_bus: BusId,
159 pub nodes: Vec<ComponentId>,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub voltage_kv: Option<f64>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub angle_degrees: Option<f64>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
168#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
169#[non_exhaustive]
170pub struct BusbarSection {
171 pub component: ComponentId,
172 pub voltage_level: ComponentId,
173 pub node: ComponentId,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
181#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
182#[non_exhaustive]
183pub struct Junction {
184 pub component: ComponentId,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
189#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
190#[non_exhaustive]
191pub struct Terminal {
192 #[serde(default, skip_serializing_if = "Option::is_none")]
194 pub component: Option<ComponentId>,
195 pub equipment: ComponentId,
196 pub terminal: u8,
198 pub voltage_level: ComponentId,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub bus: Option<ComponentId>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub connectable_bus: Option<ComponentId>,
203 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub node: Option<ComponentId>,
205 pub connected: bool,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub active_power_mw: Option<f64>,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
211 pub reactive_power_mvar: Option<f64>,
212}
213
214#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
216#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
217#[serde(rename_all = "snake_case")]
218#[non_exhaustive]
219pub enum SwitchKind {
220 Breaker,
221 Disconnector,
222 LoadBreakSwitch,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
227#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
228#[serde(tag = "kind", content = "component", rename_all = "snake_case")]
229#[non_exhaustive]
230pub enum TopologyEndpoint {
231 Bus(ComponentId),
232 Node(ComponentId),
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
237#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
238#[non_exhaustive]
239pub struct TopologySwitch {
240 pub component: ComponentId,
241 pub voltage_level: ComponentId,
242 pub kind: SwitchKind,
243 pub endpoint1: TopologyEndpoint,
244 pub endpoint2: TopologyEndpoint,
245 pub open: bool,
246 #[serde(default)]
247 pub retained: bool,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
252#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
253#[non_exhaustive]
254pub struct InternalConnection {
255 pub voltage_level: ComponentId,
256 pub node1: ComponentId,
257 pub node2: ComponentId,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
262#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
263#[non_exhaustive]
264pub struct TerminalReference {
265 pub equipment: ComponentId,
266 pub terminal: u8,
267}
268
269#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
271#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
272#[serde(rename_all = "snake_case")]
273#[non_exhaustive]
274pub enum DcConverterOperatingMode {
275 Bipolar,
276 MonopolarGroundReturn,
277 MonopolarMetallicReturn,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
282#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
283#[non_exhaustive]
284pub struct DcConverterUnit {
285 pub component: ComponentId,
286 #[serde(default, skip_serializing_if = "Option::is_none")]
287 pub substation: Option<ComponentId>,
288 pub operation_mode: DcConverterOperatingMode,
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
293#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
294#[non_exhaustive]
295pub struct DcTopologicalNode {
296 pub component: ComponentId,
297 #[serde(default, skip_serializing_if = "Option::is_none")]
298 pub dc_converter_unit: Option<ComponentId>,
299}
300
301#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
303#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
304#[serde(rename_all = "snake_case")]
305#[non_exhaustive]
306pub enum DcPolarity {
307 Positive,
308 Middle,
309 Negative,
310}
311
312#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
314#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
315#[non_exhaustive]
316pub struct DcTerminal {
317 #[serde(default, skip_serializing_if = "Option::is_none")]
319 pub component: Option<ComponentId>,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
321 pub sequence_number: Option<u32>,
322 #[serde(default, skip_serializing_if = "Option::is_none")]
323 pub dc_node: Option<ComponentId>,
324 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub dc_topological_node: Option<ComponentId>,
326 #[serde(default, skip_serializing_if = "Option::is_none")]
327 pub polarity: Option<DcPolarity>,
328 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub connected: Option<bool>,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub active_power_mw: Option<f64>,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub current_a: Option<f64>,
336}
337
338#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
340#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
341#[non_exhaustive]
342pub struct DcNode {
343 pub component: ComponentId,
344 #[serde(default, skip_serializing_if = "Option::is_none")]
346 pub nominal_voltage_kv: Option<f64>,
347 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub dc_converter_unit: Option<ComponentId>,
349 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub dc_topological_node: Option<ComponentId>,
351 #[serde(default, skip_serializing_if = "Option::is_none")]
352 pub voltage_kv: Option<f64>,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
357#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
358#[non_exhaustive]
359pub struct DcGround {
360 pub component: ComponentId,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
362 pub equipment_container: Option<ComponentId>,
363 pub dc_terminal: DcTerminal,
364 #[serde(default, skip_serializing_if = "Option::is_none")]
365 pub rated_dc_voltage_kv: Option<f64>,
366 #[serde(default, skip_serializing_if = "Option::is_none")]
367 pub resistance_ohm: Option<f64>,
368 #[serde(default, skip_serializing_if = "Option::is_none")]
369 pub inductance_h: Option<f64>,
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
374#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
375#[non_exhaustive]
376pub struct DcBusbar {
377 pub component: ComponentId,
378 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub equipment_container: Option<ComponentId>,
380 pub dc_terminal: DcTerminal,
381 #[serde(default, skip_serializing_if = "Option::is_none")]
382 pub rated_dc_voltage_kv: Option<f64>,
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
387#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
388#[non_exhaustive]
389pub struct DcLine {
390 pub component: ComponentId,
391 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub equipment_container: Option<ComponentId>,
393 pub dc_terminal1: DcTerminal,
394 pub dc_terminal2: DcTerminal,
395 #[serde(default, skip_serializing_if = "Option::is_none")]
396 pub rated_dc_voltage_kv: Option<f64>,
397 #[serde(default, skip_serializing_if = "Option::is_none")]
398 pub resistance_ohm: Option<f64>,
399 #[serde(default, skip_serializing_if = "Option::is_none")]
400 pub inductance_h: Option<f64>,
401 #[serde(default, skip_serializing_if = "Option::is_none")]
402 pub capacitance_f: Option<f64>,
403 #[serde(default, skip_serializing_if = "Option::is_none")]
404 pub length_km: Option<f64>,
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
409#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
410#[non_exhaustive]
411pub struct DcSeriesDevice {
412 pub component: ComponentId,
413 #[serde(default, skip_serializing_if = "Option::is_none")]
414 pub equipment_container: Option<ComponentId>,
415 pub dc_terminal1: DcTerminal,
416 pub dc_terminal2: DcTerminal,
417 #[serde(default, skip_serializing_if = "Option::is_none")]
418 pub rated_dc_voltage_kv: Option<f64>,
419 #[serde(default, skip_serializing_if = "Option::is_none")]
420 pub resistance_ohm: Option<f64>,
421 #[serde(default, skip_serializing_if = "Option::is_none")]
422 pub inductance_h: Option<f64>,
423}
424
425#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
427#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
428#[serde(rename_all = "snake_case")]
429#[non_exhaustive]
430pub enum DcSwitchKind {
431 Switch,
432 Breaker,
433 Disconnector,
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
438#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
439#[non_exhaustive]
440pub struct DcSwitch {
441 pub component: ComponentId,
442 #[serde(default, skip_serializing_if = "Option::is_none")]
443 pub equipment_container: Option<ComponentId>,
444 pub dc_terminal1: DcTerminal,
445 pub dc_terminal2: DcTerminal,
446 pub kind: DcSwitchKind,
447 #[serde(default, skip_serializing_if = "Option::is_none")]
448 pub rated_dc_voltage_kv: Option<f64>,
449 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub open: Option<bool>,
451 #[serde(default, skip_serializing_if = "Option::is_none")]
452 pub resistance_ohm: Option<f64>,
453}
454
455#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
457#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
458#[serde(rename_all = "snake_case")]
459#[non_exhaustive]
460pub enum AcDcConverterControlMode {
461 ActivePowerAtPcc,
463 DcVoltage,
465 DcCurrent,
467 ActivePowerAtPccAndDcVoltageDroopCurve,
470 ActivePowerAtPccAndDcVoltageDroop,
472 ActivePowerAtPccAndDcVoltageDroopWithCompensation,
474 ActivePowerAtPccAndDcVoltageDroopPilot,
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
480#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
481#[non_exhaustive]
482pub struct DroopCurveSegment {
483 pub minimum_voltage_kv: f64,
484 pub maximum_voltage_kv: f64,
485 pub k: f64,
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
490#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
491#[non_exhaustive]
492pub struct DroopCurve {
493 pub segments: Vec<DroopCurveSegment>,
494}
495
496#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
498#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
499#[non_exhaustive]
500pub struct ReactiveCapabilityCurvePoint {
501 pub active_power_mw: f64,
502 pub minimum_reactive_power_mvar: f64,
503 pub maximum_reactive_power_mvar: f64,
504 #[serde(default)]
505 pub properties: BTreeMap<String, String>,
506}
507
508#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
510#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
511#[serde(rename_all = "snake_case")]
512#[non_exhaustive]
513pub enum CurveStyle {
514 ConstantYValue,
516 StraightLineYValues,
518}
519
520#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
522#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
523#[non_exhaustive]
524pub struct ReactiveCapabilityCurve {
525 pub curve_style: CurveStyle,
526 #[serde(default)]
527 pub properties: BTreeMap<String, String>,
528 pub points: Vec<ReactiveCapabilityCurvePoint>,
529}
530
531#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
533#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
534#[non_exhaustive]
535pub struct MinMaxReactiveLimits {
536 pub minimum_reactive_power_mvar: f64,
537 pub maximum_reactive_power_mvar: f64,
538 #[serde(default)]
539 pub properties: BTreeMap<String, String>,
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
544#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
545#[serde(tag = "kind", content = "limits", rename_all = "snake_case")]
546#[non_exhaustive]
547pub enum ReactiveLimits {
548 MinMax(MinMaxReactiveLimits),
549 CapabilityCurve(ReactiveCapabilityCurve),
550}
551
552#[allow(clippy::float_cmp, clippy::manual_midpoint)]
556pub(crate) fn calc_reactive_limits_at_active_power(
557 owner: &str,
558 limits: &ReactiveLimits,
559 active_power_mw: f64,
560) -> std::result::Result<(f64, f64), String> {
561 match limits {
562 ReactiveLimits::MinMax(limits) => {
563 if limits.minimum_reactive_power_mvar > limits.maximum_reactive_power_mvar {
564 return Err(format!("{owner} has minQ greater than maxQ"));
565 }
566 Ok((
567 limits.minimum_reactive_power_mvar,
568 limits.maximum_reactive_power_mvar,
569 ))
570 }
571 ReactiveLimits::CapabilityCurve(curve) => {
572 if curve.points.len() < 2 {
573 return Err(format!(
574 "{owner} reactiveCapabilityCurve has fewer than two points"
575 ));
576 }
577 if !active_power_mw.is_finite() {
578 return Err(format!(
579 "{owner} reactiveCapabilityCurve cannot be evaluated at a nonfinite active power"
580 ));
581 }
582 let mut points = curve.points.iter().collect::<Vec<_>>();
583 points
584 .sort_by(|first, second| first.active_power_mw.total_cmp(&second.active_power_mw));
585 for pair in points.windows(2) {
586 if pair[0].active_power_mw == pair[1].active_power_mw {
587 return Err(format!(
588 "{owner} reactiveCapabilityCurve has duplicate active power points"
589 ));
590 }
591 }
592 let (first, second) = if active_power_mw <= points[0].active_power_mw {
593 (points[0], points[0])
594 } else if active_power_mw >= points[points.len() - 1].active_power_mw {
595 (points[points.len() - 1], points[points.len() - 1])
596 } else {
597 let upper = points.partition_point(|point| point.active_power_mw < active_power_mw);
598 (points[upper - 1], points[upper])
599 };
600 let (minimum, maximum) = if std::ptr::eq(first, second) {
601 (
602 first.minimum_reactive_power_mvar,
603 first.maximum_reactive_power_mvar,
604 )
605 } else {
606 let fraction = (active_power_mw - first.active_power_mw)
607 / (second.active_power_mw - first.active_power_mw);
608 (
609 first.minimum_reactive_power_mvar
610 + fraction
611 * (second.minimum_reactive_power_mvar
612 - first.minimum_reactive_power_mvar),
613 first.maximum_reactive_power_mvar
614 + fraction
615 * (second.maximum_reactive_power_mvar
616 - first.maximum_reactive_power_mvar),
617 )
618 };
619 if minimum <= maximum {
620 Ok((minimum, maximum))
621 } else {
622 let midpoint = (minimum + maximum) / 2.0;
623 Ok((midpoint, midpoint))
624 }
625 }
626 }
627}
628
629#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
632#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
633#[non_exhaustive]
634pub struct EquipmentReactiveLimits {
635 pub equipment: ComponentId,
636 pub limits: ReactiveLimits,
637}
638
639#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
641#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
642#[non_exhaustive]
643pub struct BoundaryLineGeneration {
644 pub voltage_regulation_on: bool,
645 #[serde(default, skip_serializing_if = "Option::is_none")]
646 pub minimum_active_power_mw: Option<f64>,
647 #[serde(default, skip_serializing_if = "Option::is_none")]
648 pub maximum_active_power_mw: Option<f64>,
649 #[serde(default, skip_serializing_if = "Option::is_none")]
650 pub target_active_power_mw: Option<f64>,
651 #[serde(default, skip_serializing_if = "Option::is_none")]
652 pub target_reactive_power_mvar: Option<f64>,
653 #[serde(default, skip_serializing_if = "Option::is_none")]
654 pub target_voltage_kv: Option<f64>,
655 #[serde(default, skip_serializing_if = "Option::is_none")]
656 pub reactive_limits: Option<ReactiveLimits>,
657}
658
659#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
661#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
662#[non_exhaustive]
663pub struct BoundaryLine {
664 pub component: ComponentId,
665 pub voltage_level: ComponentId,
666 pub active_power_setpoint_mw: f64,
667 pub reactive_power_setpoint_mvar: f64,
668 pub resistance_ohm: f64,
669 pub reactance_ohm: f64,
670 pub conductance_siemens: f64,
671 pub susceptance_siemens: f64,
672 #[serde(default, skip_serializing_if = "Option::is_none")]
673 pub pairing_key: Option<String>,
674 #[serde(default, skip_serializing_if = "Option::is_none")]
675 pub generation: Option<BoundaryLineGeneration>,
676 #[serde(default, skip_serializing_if = "Option::is_none")]
677 pub calculation_load: Option<ComponentId>,
678 #[serde(default, skip_serializing_if = "Option::is_none")]
679 pub calculation_generator: Option<ComponentId>,
680}
681
682#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
684#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
685#[non_exhaustive]
686pub struct TieLine {
687 pub component: ComponentId,
688 pub boundary_line1: ComponentId,
689 pub boundary_line2: ComponentId,
690 #[serde(default, skip_serializing_if = "Option::is_none")]
691 pub calculation_branch: Option<ComponentId>,
692}
693
694#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
696#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
697#[non_exhaustive]
698pub struct VoltageSourceConverter {
699 pub component: ComponentId,
700 #[serde(default, skip_serializing_if = "Option::is_none")]
701 pub dc_converter_unit: Option<ComponentId>,
702 pub dc_terminal1: DcTerminal,
703 pub dc_terminal2: DcTerminal,
704 #[serde(default, skip_serializing_if = "Option::is_none")]
705 pub base_apparent_power_mva: Option<f64>,
706 #[serde(default, skip_serializing_if = "Option::is_none")]
707 pub minimum_active_power_mw: Option<f64>,
708 #[serde(default, skip_serializing_if = "Option::is_none")]
709 pub maximum_active_power_mw: Option<f64>,
710 #[serde(default, skip_serializing_if = "Option::is_none")]
711 pub minimum_dc_voltage_kv: Option<f64>,
712 #[serde(default, skip_serializing_if = "Option::is_none")]
713 pub maximum_dc_voltage_kv: Option<f64>,
714 #[serde(default, skip_serializing_if = "Option::is_none")]
715 pub rated_dc_voltage_kv: Option<f64>,
716 #[serde(default, skip_serializing_if = "Option::is_none")]
717 pub valve_u0_kv: Option<f64>,
718 #[serde(default, skip_serializing_if = "Option::is_none")]
719 pub number_of_valves: Option<u32>,
720 #[serde(default, skip_serializing_if = "Option::is_none")]
721 pub idle_loss_mw: Option<f64>,
722 #[serde(default, skip_serializing_if = "Option::is_none")]
723 pub switching_loss_mw_per_ampere: Option<f64>,
724 #[serde(default, skip_serializing_if = "Option::is_none")]
725 pub resistive_loss_ohm: Option<f64>,
726 #[serde(default, skip_serializing_if = "Option::is_none")]
727 pub control_mode: Option<AcDcConverterControlMode>,
728 #[serde(default, skip_serializing_if = "Option::is_none")]
730 pub active_power_at_pcc_mw: Option<f64>,
731 #[serde(default, skip_serializing_if = "Option::is_none")]
733 pub reactive_power_at_pcc_mvar: Option<f64>,
734 #[serde(default, skip_serializing_if = "Option::is_none")]
736 pub target_active_power_mw: Option<f64>,
737 #[serde(default, skip_serializing_if = "Option::is_none")]
738 pub target_dc_voltage_kv: Option<f64>,
739 #[serde(default, skip_serializing_if = "Option::is_none")]
740 pub pcc_terminal: Option<TerminalReference>,
741 #[serde(default, skip_serializing_if = "Option::is_none")]
742 pub droop_curve: Option<DroopCurve>,
743 #[serde(default, skip_serializing_if = "Option::is_none")]
745 pub droop: Option<f64>,
746 #[serde(default, skip_serializing_if = "Option::is_none")]
747 pub droop_compensation: Option<f64>,
748 #[serde(default, skip_serializing_if = "Option::is_none")]
749 pub q_share: Option<f64>,
750 #[serde(default, skip_serializing_if = "Option::is_none")]
751 pub maximum_modulation_index: Option<f64>,
752 #[serde(default, skip_serializing_if = "Option::is_none")]
753 pub maximum_valve_current_a: Option<f64>,
754 #[serde(default, skip_serializing_if = "Option::is_none")]
755 pub voltage_regulator_on: Option<bool>,
756 #[serde(default, skip_serializing_if = "Option::is_none")]
757 pub voltage_setpoint_kv: Option<f64>,
758 #[serde(default, skip_serializing_if = "Option::is_none")]
759 pub reactive_power_setpoint_mvar: Option<f64>,
760 #[serde(default, skip_serializing_if = "Option::is_none")]
761 pub reactive_limits: Option<ReactiveLimits>,
762 #[serde(default, skip_serializing_if = "Option::is_none")]
763 pub pole_loss_active_power_mw: Option<f64>,
764 #[serde(default, skip_serializing_if = "Option::is_none")]
765 pub dc_current_a: Option<f64>,
766 #[serde(default, skip_serializing_if = "Option::is_none")]
767 pub ac_voltage_kv: Option<f64>,
768 #[serde(default, skip_serializing_if = "Option::is_none")]
769 pub dc_voltage_kv: Option<f64>,
770 #[serde(default, skip_serializing_if = "Option::is_none")]
771 pub delta_degrees: Option<f64>,
772 #[serde(default, skip_serializing_if = "Option::is_none")]
773 pub uf_kv: Option<f64>,
774 #[serde(default, skip_serializing_if = "Option::is_none")]
775 pub uv_kv: Option<f64>,
776}
777
778#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
780#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
781#[serde(rename_all = "snake_case")]
782#[non_exhaustive]
783pub enum LineCommutatedConverterReactiveModel {
784 FixedPowerFactor,
785 CalculatedPowerFactor,
786}
787
788#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
790#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
791#[serde(rename_all = "snake_case")]
792#[non_exhaustive]
793pub enum LineCommutatedConverterOperatingMode {
794 Rectifier,
795 Inverter,
796}
797
798#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
800#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
801#[non_exhaustive]
802pub struct LineCommutatedConverter {
803 pub component: ComponentId,
804 #[serde(default, skip_serializing_if = "Option::is_none")]
805 pub dc_converter_unit: Option<ComponentId>,
806 pub dc_terminal1: DcTerminal,
807 pub dc_terminal2: DcTerminal,
808 #[serde(default, skip_serializing_if = "Option::is_none")]
809 pub base_apparent_power_mva: Option<f64>,
810 #[serde(default, skip_serializing_if = "Option::is_none")]
811 pub minimum_active_power_mw: Option<f64>,
812 #[serde(default, skip_serializing_if = "Option::is_none")]
813 pub maximum_active_power_mw: Option<f64>,
814 #[serde(default, skip_serializing_if = "Option::is_none")]
815 pub minimum_dc_voltage_kv: Option<f64>,
816 #[serde(default, skip_serializing_if = "Option::is_none")]
817 pub maximum_dc_voltage_kv: Option<f64>,
818 #[serde(default, skip_serializing_if = "Option::is_none")]
819 pub rated_dc_voltage_kv: Option<f64>,
820 #[serde(default, skip_serializing_if = "Option::is_none")]
821 pub valve_u0_kv: Option<f64>,
822 #[serde(default, skip_serializing_if = "Option::is_none")]
823 pub number_of_valves: Option<u32>,
824 #[serde(default, skip_serializing_if = "Option::is_none")]
825 pub idle_loss_mw: Option<f64>,
826 #[serde(default, skip_serializing_if = "Option::is_none")]
827 pub switching_loss_mw_per_ampere: Option<f64>,
828 #[serde(default, skip_serializing_if = "Option::is_none")]
829 pub resistive_loss_ohm: Option<f64>,
830 #[serde(default, skip_serializing_if = "Option::is_none")]
831 pub control_mode: Option<AcDcConverterControlMode>,
832 #[serde(default, skip_serializing_if = "Option::is_none")]
834 pub active_power_at_pcc_mw: Option<f64>,
835 #[serde(default, skip_serializing_if = "Option::is_none")]
837 pub reactive_power_at_pcc_mvar: Option<f64>,
838 #[serde(default, skip_serializing_if = "Option::is_none")]
840 pub target_active_power_mw: Option<f64>,
841 #[serde(default, skip_serializing_if = "Option::is_none")]
842 pub target_dc_voltage_kv: Option<f64>,
843 #[serde(default, skip_serializing_if = "Option::is_none")]
844 pub pcc_terminal: Option<TerminalReference>,
845 #[serde(default, skip_serializing_if = "Option::is_none")]
846 pub droop_curve: Option<DroopCurve>,
847 #[serde(default, skip_serializing_if = "Option::is_none")]
848 pub reactive_model: Option<LineCommutatedConverterReactiveModel>,
849 #[serde(default, skip_serializing_if = "Option::is_none")]
850 pub power_factor: Option<f64>,
851 #[serde(default, skip_serializing_if = "Option::is_none")]
852 pub operating_mode: Option<LineCommutatedConverterOperatingMode>,
853 #[serde(default, skip_serializing_if = "Option::is_none")]
854 pub rated_dc_current_a: Option<f64>,
855 #[serde(default, skip_serializing_if = "Option::is_none")]
856 pub minimum_alpha_degrees: Option<f64>,
857 #[serde(default, skip_serializing_if = "Option::is_none")]
858 pub maximum_alpha_degrees: Option<f64>,
859 #[serde(default, skip_serializing_if = "Option::is_none")]
860 pub minimum_gamma_degrees: Option<f64>,
861 #[serde(default, skip_serializing_if = "Option::is_none")]
862 pub maximum_gamma_degrees: Option<f64>,
863 #[serde(default, skip_serializing_if = "Option::is_none")]
864 pub target_alpha_degrees: Option<f64>,
865 #[serde(default, skip_serializing_if = "Option::is_none")]
866 pub target_gamma_degrees: Option<f64>,
867 #[serde(default, skip_serializing_if = "Option::is_none")]
868 pub target_dc_current_a: Option<f64>,
869 #[serde(default, skip_serializing_if = "Option::is_none")]
870 pub pole_loss_active_power_mw: Option<f64>,
871 #[serde(default, skip_serializing_if = "Option::is_none")]
872 pub dc_current_a: Option<f64>,
873 #[serde(default, skip_serializing_if = "Option::is_none")]
874 pub ac_voltage_kv: Option<f64>,
875 #[serde(default, skip_serializing_if = "Option::is_none")]
876 pub dc_voltage_kv: Option<f64>,
877 #[serde(default, skip_serializing_if = "Option::is_none")]
878 pub alpha_degrees: Option<f64>,
879 #[serde(default, skip_serializing_if = "Option::is_none")]
880 pub gamma_degrees: Option<f64>,
881}
882
883#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
885#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
886#[non_exhaustive]
887pub struct TemporaryLimit {
888 pub name: String,
889 pub value: f64,
890 pub acceptable_duration_seconds: u64,
891 #[serde(default)]
892 pub fictitious: bool,
893}
894
895#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
901#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
902#[non_exhaustive]
903pub struct LoadingLimits {
904 #[serde(default, skip_serializing_if = "Option::is_none")]
905 pub permanent_limit: Option<f64>,
906 #[serde(default, skip_serializing_if = "Option::is_none")]
907 pub permanent_limit_name: Option<String>,
908 #[serde(default)]
909 pub temporary_limits: Vec<TemporaryLimit>,
910}
911
912#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
914#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
915#[non_exhaustive]
916pub struct OperationalLimitGroup {
917 pub equipment: ComponentId,
918 pub terminal: u8,
919 pub id: String,
920 #[serde(default)]
921 pub properties: BTreeMap<String, String>,
922 #[serde(default)]
923 pub selected: bool,
924 #[serde(default, skip_serializing_if = "Option::is_none")]
925 pub current_limits: Option<LoadingLimits>,
926 #[serde(default, skip_serializing_if = "Option::is_none")]
927 pub active_power_limits: Option<LoadingLimits>,
928 #[serde(default, skip_serializing_if = "Option::is_none")]
929 pub apparent_power_limits: Option<LoadingLimits>,
930}
931
932#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
934#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
935#[serde(rename_all = "snake_case")]
936#[non_exhaustive]
937pub enum TapChangerKind {
938 Ratio,
939 Phase,
940}
941
942#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
944#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
945#[serde(rename_all = "snake_case")]
946#[non_exhaustive]
947pub enum TapChangerRegulationMode {
948 Voltage,
949 ReactivePower,
950 ActivePower,
951 Current,
952}
953
954#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
956#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
957#[non_exhaustive]
958pub struct TapChangerStep {
959 pub position: i32,
960 pub rho: f64,
962 pub alpha_degrees: f64,
964 pub resistance_deviation_percent: f64,
966 pub reactance_deviation_percent: f64,
968 pub conductance_deviation_percent: f64,
970 pub susceptance_deviation_percent: f64,
972}
973
974#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
976#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
977#[non_exhaustive]
978pub struct TapChanger {
979 #[serde(default, skip_serializing_if = "Option::is_none")]
981 pub component: Option<ComponentId>,
982 pub transformer: ComponentId,
983 pub winding: u8,
986 pub kind: TapChangerKind,
987 #[serde(default, skip_serializing_if = "Option::is_none")]
990 pub tap_position: Option<i32>,
991 #[serde(default, skip_serializing_if = "Option::is_none")]
994 pub solved_tap_position: Option<i32>,
995 pub low_tap_position: i32,
996 #[serde(default, skip_serializing_if = "Option::is_none")]
998 pub neutral_tap_position: Option<i32>,
999 #[serde(default, skip_serializing_if = "Option::is_none")]
1001 pub normal_tap_position: Option<i32>,
1002 #[serde(default, skip_serializing_if = "Option::is_none")]
1005 pub voltage_step_increment_percent: Option<f64>,
1006 pub load_tap_changing_capabilities: bool,
1007 pub regulating: bool,
1008 #[serde(default, skip_serializing_if = "Option::is_none")]
1009 pub regulation_mode: Option<TapChangerRegulationMode>,
1010 #[serde(default, skip_serializing_if = "Option::is_none")]
1012 pub regulation_value: Option<f64>,
1013 #[serde(default, skip_serializing_if = "Option::is_none")]
1015 pub target_deadband: Option<f64>,
1016 #[serde(default, skip_serializing_if = "Option::is_none")]
1017 pub regulation_terminal: Option<TerminalReference>,
1018 pub steps: Vec<TapChangerStep>,
1019}
1020
1021#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
1023#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1024#[non_exhaustive]
1025pub struct CaseMetadata {
1026 #[serde(default, skip_serializing_if = "Option::is_none")]
1027 pub case_date: Option<String>,
1028 #[serde(default, skip_serializing_if = "Option::is_none")]
1029 pub forecast_distance: Option<i32>,
1030 #[serde(default, skip_serializing_if = "Option::is_none")]
1031 pub source_model_format: Option<String>,
1032 #[serde(default, skip_serializing_if = "Option::is_none")]
1033 pub minimum_validation_level: Option<String>,
1034}
1035
1036#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1038#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1039#[non_exhaustive]
1040pub struct Subnetwork {
1041 pub component: ComponentId,
1042 pub parent: ComponentId,
1043 pub case_metadata: CaseMetadata,
1044 #[serde(default)]
1045 pub components: Vec<ComponentId>,
1046}
1047
1048#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
1054#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1055#[serde(rename_all = "snake_case")]
1056#[non_exhaustive]
1057pub enum OmittedFieldName {
1058 ActivePower,
1059 ReactivePower,
1060 VoltageSetpoint,
1061 RatedApparentPower,
1062 ShuntConductancePerSection,
1063}
1064
1065#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
1067#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1068#[non_exhaustive]
1069pub struct OmittedField {
1070 pub component: ComponentId,
1071 pub field: OmittedFieldName,
1072}
1073
1074impl OmittedField {
1075 #[must_use]
1076 pub fn new(component: ComponentId, field: OmittedFieldName) -> Self {
1077 Self { component, field }
1078 }
1079}
1080
1081#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
1084#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1085#[non_exhaustive]
1086pub struct DetailedConnectivity {
1087 #[serde(default)]
1090 pub omitted_fields: Vec<OmittedField>,
1091 #[serde(default)]
1092 pub component_metadata: Vec<ComponentMetadata>,
1093 #[serde(default)]
1094 pub subnetworks: Vec<Subnetwork>,
1095 #[serde(default)]
1096 pub substations: Vec<Substation>,
1097 #[serde(default)]
1098 pub voltage_levels: Vec<VoltageLevel>,
1099 #[serde(default)]
1100 pub bus_breaker_buses: Vec<BusBreakerBus>,
1101 #[serde(default)]
1102 pub calculated_buses: Vec<CalculatedBus>,
1103 #[serde(default)]
1104 pub connectivity_nodes: Vec<ConnectivityNode>,
1105 #[serde(default)]
1106 pub busbar_sections: Vec<BusbarSection>,
1107 #[serde(default)]
1108 pub junctions: Vec<Junction>,
1109 #[serde(default)]
1110 pub terminals: Vec<Terminal>,
1111 #[serde(default)]
1112 pub switches: Vec<TopologySwitch>,
1113 #[serde(default)]
1114 pub internal_connections: Vec<InternalConnection>,
1115 #[serde(default)]
1116 pub operational_limit_groups: Vec<OperationalLimitGroup>,
1117 #[serde(default)]
1118 pub tap_changers: Vec<TapChanger>,
1119 #[serde(default)]
1120 pub equipment_reactive_limits: Vec<EquipmentReactiveLimits>,
1121 #[serde(default)]
1122 pub boundary_lines: Vec<BoundaryLine>,
1123 #[serde(default)]
1124 pub tie_lines: Vec<TieLine>,
1125 #[serde(default)]
1126 pub dc_converter_units: Vec<DcConverterUnit>,
1127 #[serde(default)]
1128 pub dc_topological_nodes: Vec<DcTopologicalNode>,
1129 #[serde(default)]
1130 pub dc_nodes: Vec<DcNode>,
1131 #[serde(default)]
1132 pub dc_grounds: Vec<DcGround>,
1133 #[serde(default)]
1134 pub dc_busbars: Vec<DcBusbar>,
1135 #[serde(default)]
1136 pub dc_lines: Vec<DcLine>,
1137 #[serde(default)]
1138 pub dc_series_devices: Vec<DcSeriesDevice>,
1139 #[serde(default)]
1140 pub dc_switches: Vec<DcSwitch>,
1141 #[serde(default)]
1142 pub voltage_source_converters: Vec<VoltageSourceConverter>,
1143 #[serde(default)]
1144 pub line_commutated_converters: Vec<LineCommutatedConverter>,
1145}
1146
1147pub const DEFAULT_BASE_FREQUENCY: f64 = 60.0;
1151
1152fn default_base_frequency() -> f64 {
1156 DEFAULT_BASE_FREQUENCY
1157}
1158
1159#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
1166#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1167#[serde(transparent)]
1168pub struct BusId(pub usize);
1169
1170impl BusId {
1171 pub const MAX: Self = Self(i64::MAX as usize);
1175
1176 #[must_use]
1177 pub const fn new(id: usize) -> Self {
1178 Self(id)
1179 }
1180}
1181
1182impl std::fmt::Display for BusId {
1183 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1184 self.0.fmt(f)
1185 }
1186}
1187
1188#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1190#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1191#[serde(rename_all = "UPPERCASE")]
1192#[repr(u8)]
1193#[non_exhaustive]
1194pub enum BusType {
1195 Pq = 1,
1196 Pv = 2,
1197 Ref = 3,
1198 Isolated = 4,
1199}
1200
1201impl BusType {
1202 pub(crate) fn from_f64(v: f64) -> Self {
1204 match v as i32 {
1205 2 => Self::Pv,
1206 3 => Self::Ref,
1207 4 => Self::Isolated,
1208 _ => Self::Pq,
1209 }
1210 }
1211
1212 #[must_use]
1215 pub fn as_str(self) -> &'static str {
1216 match self {
1217 Self::Pq => "PQ",
1218 Self::Pv => "PV",
1219 Self::Ref => "REF",
1220 Self::Isolated => "ISOLATED",
1221 }
1222 }
1223}
1224
1225#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1227#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1228#[non_exhaustive]
1229pub struct GenCost {
1230 pub model: u8,
1232 pub startup: f64,
1233 pub shutdown: f64,
1234 pub ncost: usize,
1236 pub coeffs: Vec<f64>,
1239}
1240
1241impl GenCost {
1242 #[must_use]
1250 pub fn new(model: u8, startup: f64, shutdown: f64, coeffs: Vec<f64>) -> Self {
1251 let ncost = if model == 1 {
1252 coeffs.len() / 2
1253 } else {
1254 coeffs.len()
1255 };
1256 Self {
1257 model,
1258 startup,
1259 shutdown,
1260 ncost,
1261 coeffs,
1262 }
1263 }
1264
1265 #[must_use]
1266 pub fn with_ncost(
1267 model: u8,
1268 startup: f64,
1269 shutdown: f64,
1270 ncost: usize,
1271 coeffs: Vec<f64>,
1272 ) -> Self {
1273 Self {
1274 model,
1275 startup,
1276 shutdown,
1277 ncost,
1278 coeffs,
1279 }
1280 }
1281
1282 pub fn calc_quadratic(&self) -> Option<(f64, f64)> {
1287 self.calc_quadratic_with_constant().map(|(q, c, _)| (q, c))
1288 }
1289
1290 pub fn calc_quadratic_with_constant(&self) -> Option<(f64, f64, f64)> {
1296 if self.model != 2 {
1297 return None;
1298 }
1299 if self.coeffs.len() < self.ncost {
1302 return None;
1303 }
1304 match self.ncost {
1308 3 => Some((2.0 * self.coeffs[0], self.coeffs[1], self.coeffs[2])),
1309 2 => Some((0.0, self.coeffs[0], self.coeffs[1])),
1310 1 => Some((0.0, 0.0, self.coeffs[0])),
1311 _ => None,
1312 }
1313 }
1314
1315 pub const LEADING_COEFF_TOL: f64 = 1e-12;
1319
1320 pub fn calc_quadratic_with_constant_tol(&self, tol: f64) -> Option<(f64, f64, f64)> {
1330 if self.model != 2 {
1331 return None;
1332 }
1333 if self.coeffs.len() < self.ncost {
1334 return None;
1335 }
1336 let row = &self.coeffs[..self.ncost];
1337 let mut first = 0;
1338 while first + 1 < row.len() && row[first].abs() <= tol {
1339 first += 1;
1340 }
1341 match row.len() - first {
1342 3 => Some((2.0 * row[first], row[first + 1], row[first + 2])),
1343 2 => Some((0.0, row[first], row[first + 1])),
1344 1 => Some((0.0, 0.0, row[first])),
1345 _ => None,
1346 }
1347 }
1348}
1349
1350#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1356#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1357#[non_exhaustive]
1358pub enum SourceFormat {
1359 #[serde(rename = "matpower")]
1360 Matpower,
1361 #[serde(rename = "powermodels-json")]
1362 PowerModelsJson,
1363 #[serde(rename = "egret-json")]
1364 EgretJson,
1365 #[serde(rename = "psse")]
1366 Psse,
1367 #[serde(rename = "psse-rawx")]
1369 PsseRawx,
1370 #[serde(rename = "powerworld")]
1371 PowerWorld,
1372 #[serde(rename = "pandapower-json")]
1373 PandapowerJson,
1374 #[serde(rename = "pslf")]
1378 Pslf,
1379 #[serde(rename = "powerworld-pwb")]
1383 PowerWorldBinary,
1384 #[serde(rename = "in-memory")]
1386 InMemory,
1387 #[serde(rename = "normalized")]
1393 Normalized,
1394 #[serde(rename = "gridfm")]
1398 Gridfm,
1399 #[serde(rename = "pypsa-csv")]
1402 PypsaCsv,
1403 #[serde(rename = "goc3-json")]
1407 Goc3Json,
1408 #[serde(rename = "surge-json")]
1410 SurgeJson,
1411 #[serde(rename = "opfdata-json")]
1416 DeepMindOpfDataJson,
1417 #[serde(rename = "xiidm")]
1419 Xiidm,
1420 #[serde(rename = "jiidm")]
1422 Jiidm,
1423 #[serde(rename = "cgmes")]
1425 Cgmes,
1426 #[serde(rename = "ucte")]
1429 Ucte,
1430 #[serde(rename = "ieee-cdf")]
1433 IeeeCdf,
1434 #[serde(rename = "sincal")]
1436 Sincal,
1437 #[serde(rename = "ravens-json")]
1440 RavensJson,
1441}
1442
1443impl SourceFormat {
1444 #[must_use]
1449 pub fn name(self) -> &'static str {
1450 match self {
1451 SourceFormat::Sincal => "sincal",
1452 SourceFormat::Matpower => "matpower",
1453 SourceFormat::PowerModelsJson => "powermodels-json",
1454 SourceFormat::EgretJson => "egret-json",
1455 SourceFormat::Psse => "psse",
1456 SourceFormat::PsseRawx => "psse-rawx",
1457 SourceFormat::PowerWorld => "powerworld",
1458 SourceFormat::PandapowerJson => "pandapower-json",
1459 SourceFormat::Pslf => "pslf",
1460 SourceFormat::PowerWorldBinary => "powerworld-pwb",
1461 SourceFormat::InMemory => "in-memory",
1462 SourceFormat::Normalized => "normalized",
1463 SourceFormat::Gridfm => "gridfm",
1464 SourceFormat::PypsaCsv => "pypsa-csv",
1465 SourceFormat::Goc3Json => "goc3-json",
1466 SourceFormat::SurgeJson => "surge-json",
1467 SourceFormat::RavensJson => "ravens-json",
1468 SourceFormat::DeepMindOpfDataJson => "opfdata-json",
1469 SourceFormat::Xiidm => "xiidm",
1470 SourceFormat::Jiidm => "jiidm",
1471 SourceFormat::Cgmes => "cgmes",
1472 SourceFormat::Ucte => "ucte",
1473 SourceFormat::IeeeCdf => "ieee-cdf",
1474 }
1475 }
1476}
1477
1478#[derive(Debug, Clone)]
1488pub struct BalancedNetwork {
1489 tables: std::sync::Arc<BalancedNetworkTables>,
1490}
1491
1492impl BalancedNetwork {
1493 pub(crate) fn from_tables(tables: BalancedNetworkTables) -> Self {
1494 Self {
1495 tables: std::sync::Arc::new(tables),
1496 }
1497 }
1498
1499 pub(crate) fn tables_mut(&mut self) -> &mut BalancedNetworkTables {
1502 std::sync::Arc::make_mut(&mut self.tables)
1503 }
1504}
1505
1506#[derive(Debug, Clone, Serialize, Deserialize)]
1513#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1514#[cfg_attr(feature = "schema", schemars(rename = "BalancedNetwork"))]
1515#[serde(remote = "Self")]
1516pub(crate) struct BalancedNetworkTables {
1517 pub name: String,
1518 pub base_mva: f64,
1519 #[serde(default = "default_base_frequency")]
1526 pub base_frequency: f64,
1527 #[serde(default, skip_serializing_if = "Option::is_none")]
1528 pub geo: Option<GeoMeta>,
1529 #[serde(default)]
1531 pub case_metadata: CaseMetadata,
1532 #[serde(default, skip_serializing_if = "Option::is_none")]
1535 pub detailed_connectivity: Option<std::sync::Arc<DetailedConnectivity>>,
1536 #[serde(default, skip_serializing_if = "generated_uids_is_empty")]
1540 pub generated_uids: std::sync::Arc<BTreeSet<String>>,
1541 pub buses: std::sync::Arc<Vec<Bus>>,
1542 pub loads: std::sync::Arc<Vec<Load>>,
1543 pub shunts: std::sync::Arc<Vec<Shunt>>,
1544 #[serde(default)]
1545 pub static_var_compensators: std::sync::Arc<Vec<StaticVarCompensator>>,
1546 pub branches: std::sync::Arc<Vec<Branch>>,
1547 #[serde(default)]
1548 pub switches: std::sync::Arc<Vec<Switch>>,
1549 pub generators: std::sync::Arc<Vec<Generator>>,
1550 pub storage: std::sync::Arc<Vec<Storage>>,
1551 pub hvdc: std::sync::Arc<Vec<Hvdc>>,
1552 #[serde(default)]
1561 pub transformers_3w: std::sync::Arc<Vec<Transformer3W>>,
1562 #[serde(default)]
1567 pub areas: std::sync::Arc<Vec<Area>>,
1568 #[serde(default)]
1571 pub solver: Option<SolverParams>,
1572 pub source_format: SourceFormat,
1573}
1574
1575impl Serialize for BalancedNetwork {
1576 fn serialize<S: serde::Serializer>(
1577 &self,
1578 serializer: S,
1579 ) -> std::result::Result<S::Ok, S::Error> {
1580 BalancedNetworkTables::serialize(
1581 &self.tables,
1582 powerio_core::__implementation::nonfinite::NonFiniteSer(serializer),
1583 )
1584 }
1585}
1586
1587impl<'de> Deserialize<'de> for BalancedNetwork {
1588 fn deserialize<D: serde::Deserializer<'de>>(
1589 deserializer: D,
1590 ) -> std::result::Result<Self, D::Error> {
1591 BalancedNetworkTables::deserialize(powerio_core::__implementation::nonfinite::NonFiniteDe(
1592 deserializer,
1593 ))
1594 .map(BalancedNetwork::from_tables)
1595 }
1596}
1597
1598#[cfg(feature = "schema")]
1599impl schemars::JsonSchema for BalancedNetwork {
1600 fn schema_name() -> std::borrow::Cow<'static, str> {
1601 <BalancedNetworkTables as schemars::JsonSchema>::schema_name()
1602 }
1603
1604 fn schema_id() -> std::borrow::Cow<'static, str> {
1605 <BalancedNetworkTables as schemars::JsonSchema>::schema_id()
1606 }
1607
1608 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1609 <BalancedNetworkTables as schemars::JsonSchema>::json_schema(generator)
1610 }
1611}
1612
1613macro_rules! table_accessors {
1614 ($($(#[$doc:meta])* $field:ident, $field_mut:ident: $ty:ty;)+) => {
1615 impl BalancedNetwork {
1616 $(
1617 $(#[$doc])*
1618 #[must_use]
1619 pub fn $field(&self) -> &$ty {
1620 &self.tables.$field
1621 }
1622
1623 #[must_use]
1627 pub fn $field_mut(&mut self) -> &mut $ty {
1628 &mut self.tables_mut().$field
1629 }
1630 )+
1631 }
1632 };
1633}
1634
1635table_accessors! {
1636 name, name_mut: String;
1638 geo, geo_mut: Option<GeoMeta>;
1640 case_metadata, case_metadata_mut: CaseMetadata;
1642 solver, solver_mut: Option<SolverParams>;
1644}
1645
1646impl BalancedNetwork {
1647 #[must_use]
1649 pub fn detailed_connectivity(&self) -> &Option<std::sync::Arc<DetailedConnectivity>> {
1650 &self.tables.detailed_connectivity
1651 }
1652
1653 #[must_use]
1657 pub fn detailed_connectivity_mut(
1658 &mut self,
1659 ) -> &mut Option<std::sync::Arc<DetailedConnectivity>> {
1660 let tables = self.tables_mut();
1661 if let Some(detailed) = tables.detailed_connectivity.as_mut()
1662 && !detailed.omitted_fields.is_empty()
1663 {
1664 std::sync::Arc::make_mut(detailed).omitted_fields.clear();
1665 }
1666 &mut tables.detailed_connectivity
1667 }
1668
1669 fn clear_omitted_fields(&mut self, component_type: &str, fields: &[OmittedFieldName]) {
1670 let Some(detailed) = self.tables_mut().detailed_connectivity.as_mut() else {
1671 return;
1672 };
1673 let affected = |omitted: &OmittedField| {
1674 omitted.component.component_type() == component_type && fields.contains(&omitted.field)
1675 };
1676 if detailed.omitted_fields.iter().any(affected) {
1678 std::sync::Arc::make_mut(detailed)
1679 .omitted_fields
1680 .retain(|omitted| !affected(omitted));
1681 }
1682 }
1683
1684 fn clear_omitted_field(&mut self, component: &ComponentId, field: OmittedFieldName) {
1685 let Some(detailed) = self.tables_mut().detailed_connectivity.as_mut() else {
1686 return;
1687 };
1688 let affected =
1689 |omitted: &OmittedField| omitted.component == *component && omitted.field == field;
1690 if detailed.omitted_fields.iter().any(affected) {
1691 std::sync::Arc::make_mut(detailed)
1692 .omitted_fields
1693 .retain(|omitted| !affected(omitted));
1694 }
1695 }
1696}
1697
1698macro_rules! shared_table_accessors {
1703 ($($(#[$doc:meta])* $field:ident, $field_mut:ident: $ty:ty;)+) => {
1704 impl BalancedNetwork {
1705 $(
1706 $(#[$doc])*
1707 #[must_use]
1708 pub fn $field(&self) -> &$ty {
1709 &self.tables.$field
1710 }
1711
1712 #[must_use]
1717 pub fn $field_mut(&mut self) -> &mut $ty {
1718 std::sync::Arc::make_mut(&mut self.tables_mut().$field)
1719 }
1720 )+
1721 }
1722 };
1723}
1724
1725impl BalancedNetwork {
1726 pub fn share_equal_tables(&mut self, donor: &Self) {
1732 macro_rules! share {
1733 ($($field:ident),+) => {
1734 $(
1735 if !std::sync::Arc::ptr_eq(&self.tables.$field, &donor.tables.$field)
1736 && self.tables.$field == donor.tables.$field
1737 {
1738 self.tables_mut().$field = donor.tables.$field.clone();
1739 }
1740 )+
1741 };
1742 }
1743 share!(
1744 buses,
1745 loads,
1746 shunts,
1747 static_var_compensators,
1748 branches,
1749 switches,
1750 generators,
1751 storage,
1752 hvdc,
1753 transformers_3w,
1754 areas
1755 );
1756 }
1757}
1758
1759shared_table_accessors! {
1760 buses, buses_mut: Vec<Bus>;
1761 static_var_compensators, static_var_compensators_mut: Vec<StaticVarCompensator>;
1762 branches, branches_mut: Vec<Branch>;
1763 switches, switches_mut: Vec<Switch>;
1764 hvdc, hvdc_mut: Vec<Hvdc>;
1765 transformers_3w, transformers_3w_mut: Vec<Transformer3W>;
1767 areas, areas_mut: Vec<Area>;
1769}
1770
1771impl BalancedNetwork {
1772 #[must_use]
1773 pub fn loads(&self) -> &Vec<Load> {
1774 &self.tables.loads
1775 }
1776
1777 #[must_use]
1780 pub fn loads_mut(&mut self) -> &mut Vec<Load> {
1781 self.clear_omitted_fields(
1782 "load",
1783 &[
1784 OmittedFieldName::ActivePower,
1785 OmittedFieldName::ReactivePower,
1786 ],
1787 );
1788 std::sync::Arc::make_mut(&mut self.tables_mut().loads)
1789 }
1790
1791 #[doc(hidden)]
1795 pub(crate) fn edit_load_assignment<R>(
1796 &mut self,
1797 index: usize,
1798 component: &ComponentId,
1799 field: OmittedFieldName,
1800 edit: impl FnOnce(&mut Load) -> R,
1801 ) -> R {
1802 debug_assert_eq!(component.component_type(), "load");
1803 debug_assert!(matches!(
1804 field,
1805 OmittedFieldName::ActivePower | OmittedFieldName::ReactivePower
1806 ));
1807 self.clear_omitted_field(component, field);
1808 edit(&mut std::sync::Arc::make_mut(&mut self.tables_mut().loads)[index])
1809 }
1810
1811 #[must_use]
1812 pub fn generators(&self) -> &Vec<Generator> {
1813 &self.tables.generators
1814 }
1815
1816 #[must_use]
1819 pub fn generators_mut(&mut self) -> &mut Vec<Generator> {
1820 self.clear_omitted_fields(
1821 "generator",
1822 &[
1823 OmittedFieldName::ActivePower,
1824 OmittedFieldName::ReactivePower,
1825 OmittedFieldName::VoltageSetpoint,
1826 OmittedFieldName::RatedApparentPower,
1827 ],
1828 );
1829 std::sync::Arc::make_mut(&mut self.tables_mut().generators)
1830 }
1831
1832 #[doc(hidden)]
1836 pub(crate) fn edit_generator_assignment<R>(
1837 &mut self,
1838 index: usize,
1839 component: &ComponentId,
1840 field: OmittedFieldName,
1841 edit: impl FnOnce(&mut Generator) -> R,
1842 ) -> R {
1843 debug_assert_eq!(component.component_type(), "generator");
1844 debug_assert!(matches!(
1845 field,
1846 OmittedFieldName::ActivePower
1847 | OmittedFieldName::ReactivePower
1848 | OmittedFieldName::VoltageSetpoint
1849 ));
1850 self.clear_omitted_field(component, field);
1851 edit(&mut std::sync::Arc::make_mut(&mut self.tables_mut().generators)[index])
1852 }
1853
1854 #[must_use]
1855 pub fn storage(&self) -> &Vec<Storage> {
1856 &self.tables.storage
1857 }
1858
1859 #[must_use]
1862 pub fn storage_mut(&mut self) -> &mut Vec<Storage> {
1863 self.clear_omitted_fields(
1864 "storage",
1865 &[
1866 OmittedFieldName::ActivePower,
1867 OmittedFieldName::ReactivePower,
1868 ],
1869 );
1870 std::sync::Arc::make_mut(&mut self.tables_mut().storage)
1871 }
1872
1873 #[must_use]
1874 pub fn shunts(&self) -> &Vec<Shunt> {
1875 &self.tables.shunts
1876 }
1877
1878 #[must_use]
1881 pub fn shunts_mut(&mut self) -> &mut Vec<Shunt> {
1882 self.clear_omitted_fields("shunt", &[OmittedFieldName::ShuntConductancePerSection]);
1883 std::sync::Arc::make_mut(&mut self.tables_mut().shunts)
1884 }
1885}
1886
1887impl BalancedNetwork {
1888 #[must_use]
1890 pub fn base_mva(&self) -> f64 {
1891 self.tables.base_mva
1892 }
1893
1894 #[must_use]
1895 pub fn base_mva_mut(&mut self) -> &mut f64 {
1896 &mut self.tables_mut().base_mva
1897 }
1898
1899 #[must_use]
1901 pub fn base_frequency(&self) -> f64 {
1902 self.tables.base_frequency
1903 }
1904
1905 #[must_use]
1906 pub fn base_frequency_mut(&mut self) -> &mut f64 {
1907 &mut self.tables_mut().base_frequency
1908 }
1909
1910 #[must_use]
1912 pub fn source_format(&self) -> SourceFormat {
1913 self.tables.source_format
1914 }
1915
1916 #[must_use]
1917 pub fn source_format_mut(&mut self) -> &mut SourceFormat {
1918 &mut self.tables_mut().source_format
1919 }
1920
1921 pub fn assign_missing_component_ids(&mut self) {
1939 let mut used = self.component_ids_in_use();
1940 let mut next_suffix = HashMap::new();
1941 macro_rules! assign {
1942 ($table:ident, $table_mut:ident, $set_uid:expr, $stem:expr) => {
1943 if self.$table().iter().any(|value| value.uid.is_none()) {
1944 let generated = assign_missing_ids(
1945 self.$table_mut(),
1946 |value| value.uid.as_deref(),
1947 $set_uid,
1948 $stem,
1949 &mut used,
1950 &mut next_suffix,
1951 );
1952 std::sync::Arc::make_mut(&mut self.tables_mut().generated_uids)
1953 .extend(generated);
1954 }
1955 };
1956 }
1957
1958 assign!(
1959 buses,
1960 buses_mut,
1961 |bus: &mut Bus, uid| bus.uid = Some(uid),
1962 |bus: &Bus| bus.id.to_string()
1963 );
1964 assign!(
1965 loads,
1966 loads_mut,
1967 |load: &mut Load, uid| load.uid = Some(uid),
1968 |load: &Load| format!("bus-{}", load.bus)
1969 );
1970 assign!(
1971 shunts,
1972 shunts_mut,
1973 |shunt: &mut Shunt, uid| shunt.uid = Some(uid),
1974 |shunt: &Shunt| format!("bus-{}", shunt.bus)
1975 );
1976 assign!(
1977 static_var_compensators,
1978 static_var_compensators_mut,
1979 |svc: &mut StaticVarCompensator, uid| svc.uid = Some(uid),
1980 |svc: &StaticVarCompensator| format!("bus-{}", svc.bus)
1981 );
1982 assign!(
1983 branches,
1984 branches_mut,
1985 |branch: &mut Branch, uid| branch.uid = Some(uid),
1986 |branch: &Branch| format!("{}-{}", branch.from, branch.to)
1987 );
1988 assign!(
1989 switches,
1990 switches_mut,
1991 |switch: &mut Switch, uid| switch.uid = Some(uid),
1992 |switch: &Switch| format!("{}-{}", switch.from, switch.to)
1993 );
1994 assign!(
1995 generators,
1996 generators_mut,
1997 |generator: &mut Generator, uid| generator.uid = Some(uid),
1998 |generator: &Generator| format!("bus-{}", generator.bus)
1999 );
2000 assign!(
2001 storage,
2002 storage_mut,
2003 |storage: &mut Storage, uid| storage.uid = Some(uid),
2004 |storage: &Storage| format!("bus-{}", storage.bus)
2005 );
2006 assign!(
2007 hvdc,
2008 hvdc_mut,
2009 |line: &mut Hvdc, uid| line.uid = Some(uid),
2010 |line: &Hvdc| format!("{}-{}", line.from, line.to)
2011 );
2012 assign!(
2013 transformers_3w,
2014 transformers_3w_mut,
2015 |transformer: &mut Transformer3W, uid| transformer.uid = Some(uid),
2016 |transformer: &Transformer3W| {
2017 let [first, second, third] = &transformer.windings;
2018 format!("{}-{}-{}", first.bus, second.bus, third.bus)
2019 }
2020 );
2021 }
2022
2023 #[must_use]
2025 pub fn uid_is_generated(&self, uid: Option<&str>) -> bool {
2026 uid.is_some_and(|uid| self.tables.generated_uids.contains(uid))
2027 }
2028
2029 pub(crate) fn generated_uids(&self) -> &std::sync::Arc<BTreeSet<String>> {
2030 &self.tables.generated_uids
2031 }
2032
2033 pub(crate) fn component_ids_in_use(&self) -> HashSet<String> {
2035 let mut used = HashSet::new();
2036 macro_rules! collect {
2037 ($($table:ident),+) => {
2038 $(used.extend(
2039 self.$table()
2040 .iter()
2041 .filter_map(|value| value.uid.as_deref().map(str::to_owned)),
2042 );)+
2043 };
2044 }
2045 collect!(
2046 buses,
2047 loads,
2048 shunts,
2049 static_var_compensators,
2050 branches,
2051 switches,
2052 generators,
2053 storage,
2054 hvdc,
2055 transformers_3w
2056 );
2057 used
2058 }
2059}
2060
2061fn generated_uids_is_empty(uids: &std::sync::Arc<BTreeSet<String>>) -> bool {
2062 uids.is_empty()
2063}
2064
2065fn assign_missing_ids<T>(
2066 values: &mut [T],
2067 uid: impl for<'a> Fn(&'a T) -> Option<&'a str>,
2068 mut set_uid: impl FnMut(&mut T, String),
2069 stem: impl Fn(&T) -> String,
2070 used: &mut HashSet<String>,
2071 next_suffix: &mut HashMap<String, usize>,
2072) -> Vec<String> {
2073 let mut generated = Vec::new();
2074 for value in values {
2075 if uid(value).is_some() {
2076 continue;
2077 }
2078 let stem = stem(value);
2079 let suffix = match next_suffix.get_mut(&stem) {
2080 Some(suffix) => suffix,
2081 None => next_suffix.entry(stem.clone()).or_insert(1),
2082 };
2083 loop {
2084 let candidate = if *suffix == 1 {
2085 stem.clone()
2086 } else {
2087 format!("{stem}-{suffix}")
2088 };
2089 *suffix += 1;
2090 if !used.contains(&candidate) {
2091 used.insert(candidate.clone());
2092 set_uid(value, candidate.clone());
2093 generated.push(candidate);
2094 break;
2095 }
2096 }
2097 }
2098 generated
2099}
2100
2101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2102#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2103#[non_exhaustive]
2104pub struct Bus {
2105 pub id: BusId,
2107 pub kind: BusType,
2108 pub vm: f64,
2110 pub va: f64,
2112 pub base_kv: f64,
2113 pub vmax: f64,
2114 pub vmin: f64,
2115 #[serde(default)]
2121 pub evhi: Option<f64>,
2122 #[serde(default)]
2123 pub evlo: Option<f64>,
2124 pub area: usize,
2125 pub zone: usize,
2126 pub name: Option<String>,
2127 #[serde(default, skip_serializing_if = "Option::is_none")]
2132 pub uid: Option<String>,
2133 #[serde(default, skip_serializing_if = "Option::is_none")]
2135 pub location: Option<Location>,
2136 pub extras: Extras,
2137}
2138
2139impl Bus {
2140 #[must_use]
2141 pub fn new(id: BusId, kind: BusType, base_kv: f64) -> Self {
2142 Self {
2143 id,
2144 kind,
2145 vm: 1.0,
2146 va: 0.0,
2147 base_kv,
2148 vmax: 1.1,
2149 vmin: 0.9,
2150 evhi: None,
2151 evlo: None,
2152 area: 1,
2153 zone: 1,
2154 name: None,
2155 uid: None,
2156 location: None,
2157 extras: Extras::new(),
2158 }
2159 }
2160}
2161
2162#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2163#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2164#[non_exhaustive]
2165pub struct Load {
2166 pub bus: BusId,
2167 pub p: f64,
2169 pub q: f64,
2171 #[serde(default)]
2173 pub voltage_model: Option<LoadVoltageModel>,
2174 pub in_service: bool,
2175 #[serde(default, skip_serializing_if = "Option::is_none")]
2177 pub uid: Option<String>,
2178 pub extras: Extras,
2179}
2180
2181impl Load {
2182 #[must_use]
2183 pub fn new(bus: BusId, p: f64, q: f64) -> Self {
2184 Self {
2185 bus,
2186 p,
2187 q,
2188 voltage_model: None,
2189 in_service: true,
2190 uid: None,
2191 extras: Extras::new(),
2192 }
2193 }
2194}
2195
2196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2198#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2199#[serde(tag = "kind", rename_all = "snake_case")]
2200#[non_exhaustive]
2201pub enum LoadVoltageModel {
2202 ConstantPower,
2204 Zip {
2207 p_constant_power: f64,
2208 q_constant_power: f64,
2209 p_constant_current: f64,
2210 q_constant_current: f64,
2211 p_constant_impedance: f64,
2212 q_constant_impedance: f64,
2213 #[serde(default)]
2214 v_nom: Option<f64>,
2215 #[serde(default)]
2218 load_type: Option<i32>,
2219 #[serde(default)]
2221 scaling: Option<f64>,
2222 },
2223 Exponential {
2226 p: f64,
2227 q: f64,
2228 #[serde(default)]
2229 v_nom: Option<f64>,
2230 gamma_p: f64,
2231 gamma_q: f64,
2232 },
2233}
2234
2235impl LoadVoltageModel {
2236 #[must_use]
2237 pub fn has_non_matpower_fields(&self) -> bool {
2238 match self {
2239 Self::ConstantPower => false,
2240 Self::Zip {
2241 p_constant_current,
2242 q_constant_current,
2243 p_constant_impedance,
2244 q_constant_impedance,
2245 v_nom,
2246 load_type,
2247 scaling,
2248 ..
2249 } => {
2250 *p_constant_current != 0.0
2251 || *q_constant_current != 0.0
2252 || *p_constant_impedance != 0.0
2253 || *q_constant_impedance != 0.0
2254 || v_nom.is_some()
2255 || load_type.is_some()
2256 || scaling.is_some()
2257 }
2258 Self::Exponential { .. } => true,
2259 }
2260 }
2261}
2262
2263#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2264#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2265#[non_exhaustive]
2266pub struct Shunt {
2267 pub bus: BusId,
2268 pub g: f64,
2270 pub b: f64,
2273 pub in_service: bool,
2274 #[serde(default, skip_serializing_if = "Option::is_none")]
2277 pub section_count: Option<u32>,
2278 #[serde(default)]
2282 pub control: Option<SwitchedShuntControl>,
2283 #[serde(default, skip_serializing_if = "Option::is_none")]
2285 pub uid: Option<String>,
2286 pub extras: Extras,
2287}
2288
2289impl Shunt {
2290 #[must_use]
2291 pub fn new(bus: BusId, g: f64, b: f64) -> Self {
2292 Self {
2293 bus,
2294 g,
2295 b,
2296 in_service: true,
2297 section_count: None,
2298 control: None,
2299 uid: None,
2300 extras: Extras::new(),
2301 }
2302 }
2303}
2304
2305#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2313#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2314#[serde(rename_all = "snake_case")]
2315#[non_exhaustive]
2316pub enum SwitchedShuntMode {
2317 Locked,
2319 #[cfg_attr(
2322 feature = "schema",
2323 schemars(description = "Continuous adjustment within the block range (`MODSW` 1).")
2324 )]
2325 Continuous,
2326 #[cfg_attr(
2333 feature = "schema",
2334 schemars(description = "Discrete adjustment in fixed steps (`MODSW` 2 and up).")
2335 )]
2336 Discrete,
2337}
2338
2339#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2341#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2342#[non_exhaustive]
2343pub struct ShuntBlock {
2344 pub steps: u32,
2345 pub g: f64,
2347 pub b: f64,
2349}
2350
2351impl ShuntBlock {
2352 #[must_use]
2353 pub const fn new(steps: u32, b: f64) -> Self {
2354 Self { steps, g: 0.0, b }
2355 }
2356
2357 #[must_use]
2358 pub const fn with_admittance(steps: u32, g: f64, b: f64) -> Self {
2359 Self { steps, g, b }
2360 }
2361}
2362
2363#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2368#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2369#[non_exhaustive]
2370pub struct SwitchedShuntControl {
2371 pub mode: SwitchedShuntMode,
2372 pub vhigh: f64,
2374 pub vlow: f64,
2375 pub control_bus: Option<BusId>,
2377 #[serde(default, skip_serializing_if = "Option::is_none")]
2379 pub regulating_terminal: Option<TerminalReference>,
2380 pub rmpct: f64,
2382 pub blocks: Vec<ShuntBlock>,
2383}
2384
2385impl SwitchedShuntControl {
2386 #[must_use]
2387 pub fn new(mode: SwitchedShuntMode, vhigh: f64, vlow: f64, blocks: Vec<ShuntBlock>) -> Self {
2388 Self {
2389 mode,
2390 vhigh,
2391 vlow,
2392 control_bus: None,
2393 regulating_terminal: None,
2394 rmpct: 100.0,
2395 blocks,
2396 }
2397 }
2398}
2399
2400#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2402#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2403#[serde(rename_all = "snake_case")]
2404#[non_exhaustive]
2405pub enum StaticVarCompensatorRegulationMode {
2406 Voltage,
2407 ReactivePower,
2408}
2409
2410#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2412#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2413#[non_exhaustive]
2414pub struct StaticVarCompensator {
2415 pub bus: BusId,
2416 pub b_min_siemens: f64,
2418 pub b_max_siemens: f64,
2419 pub voltage_setpoint_kv: f64,
2420 pub reactive_power_setpoint_mvar: f64,
2421 pub regulation_mode: StaticVarCompensatorRegulationMode,
2422 pub regulating: bool,
2423 #[serde(default, skip_serializing_if = "Option::is_none")]
2424 pub regulating_terminal: Option<TerminalReference>,
2425 pub p: f64,
2427 pub q: f64,
2428 pub in_service: bool,
2429 #[serde(default, skip_serializing_if = "Option::is_none")]
2430 pub uid: Option<String>,
2431 pub extras: Extras,
2432}
2433
2434impl StaticVarCompensator {
2435 #[must_use]
2436 pub fn new(bus: BusId, b_min_siemens: f64, b_max_siemens: f64) -> Self {
2437 Self {
2438 bus,
2439 b_min_siemens,
2440 b_max_siemens,
2441 voltage_setpoint_kv: 0.0,
2442 reactive_power_setpoint_mvar: 0.0,
2443 regulation_mode: StaticVarCompensatorRegulationMode::Voltage,
2444 regulating: false,
2445 regulating_terminal: None,
2446 p: 0.0,
2447 q: 0.0,
2448 in_service: true,
2449 uid: None,
2450 extras: Extras::new(),
2451 }
2452 }
2453}
2454
2455#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2456#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2457#[non_exhaustive]
2458pub struct Branch {
2459 #[serde(default, skip_serializing_if = "Option::is_none")]
2461 pub name: Option<String>,
2462 pub from: BusId,
2463 pub to: BusId,
2464 pub r: f64,
2466 pub x: f64,
2468 pub b: f64,
2472 #[serde(default)]
2475 pub charging: Option<BranchCharging>,
2476 pub rate_a: f64,
2477 pub rate_b: f64,
2478 pub rate_c: f64,
2479 #[serde(default)]
2482 pub rating_sets: Vec<BranchRatingSet>,
2483 #[serde(default)]
2485 pub current_ratings: Option<BranchCurrentRatings>,
2486 pub tap: f64,
2488 pub shift: f64,
2490 pub in_service: bool,
2491 pub angmin: f64,
2492 pub angmax: f64,
2493 #[serde(default)]
2498 pub control: Option<TransformerControl>,
2499 #[serde(default)]
2501 pub solution: Option<BranchSolution>,
2502 #[serde(default, skip_serializing_if = "Option::is_none")]
2504 pub uid: Option<String>,
2505 #[serde(default, skip_serializing_if = "Option::is_none")]
2510 pub route: Option<Vec<Location>>,
2511 pub extras: Extras,
2512}
2513
2514pub(crate) const IMPEDANCE_CORRECTION_EXTRA: &str = "impedance_correction";
2518pub(crate) const IMPEDANCE_CORRECTION_NOMINAL_EXTRA: &str = "impedance_correction_nominal";
2521pub(crate) const IMPEDANCE_CORRECTION_APPLIED_EXTRA: &str = "impedance_correction_applied";
2525
2526pub(crate) fn current_correction_nominal<T>(extras: &Extras, current: &T) -> Option<T>
2533where
2534 T: PartialEq + serde::de::DeserializeOwned,
2535{
2536 let nominal = T::deserialize(extras.get(IMPEDANCE_CORRECTION_NOMINAL_EXTRA)?).ok()?;
2537 let applied = extras
2538 .get(IMPEDANCE_CORRECTION_APPLIED_EXTRA)
2539 .and_then(|value| T::deserialize(value).ok());
2540 applied
2541 .is_none_or(|applied| applied == *current)
2542 .then_some(nominal)
2543}
2544
2545pub(crate) fn correction_nominal_edited<T>(extras: &Extras, current: &T) -> bool
2548where
2549 T: PartialEq + serde::de::DeserializeOwned,
2550{
2551 extras.contains_key(IMPEDANCE_CORRECTION_NOMINAL_EXTRA)
2552 && current_correction_nominal(extras, current).is_none()
2553}
2554
2555#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2561#[non_exhaustive]
2562pub struct ImpedanceCorrection {
2563 pub table: u32,
2565 pub variable: ImpedanceCorrectionVariable,
2567 pub points: Vec<ImpedanceCorrectionPoint>,
2569}
2570
2571#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2573#[serde(rename_all = "snake_case")]
2574#[non_exhaustive]
2575pub enum ImpedanceCorrectionVariable {
2576 Ratio,
2578 Angle,
2580}
2581
2582#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
2586#[non_exhaustive]
2587pub struct ImpedanceCorrectionPoint {
2588 pub value: f64,
2590 pub factor_re: f64,
2591 pub factor_im: f64,
2592}
2593
2594impl ImpedanceCorrectionPoint {
2595 #[must_use]
2596 pub fn new(value: f64, factor_re: f64, factor_im: f64) -> Self {
2597 Self {
2598 value,
2599 factor_re,
2600 factor_im,
2601 }
2602 }
2603}
2604
2605impl ImpedanceCorrection {
2606 #[must_use]
2607 pub fn new(
2608 table: u32,
2609 variable: ImpedanceCorrectionVariable,
2610 points: Vec<ImpedanceCorrectionPoint>,
2611 ) -> Self {
2612 Self {
2613 table,
2614 variable,
2615 points,
2616 }
2617 }
2618
2619 #[must_use]
2624 pub fn calc_factor(&self, value: f64) -> Option<(f64, f64)> {
2625 let mut points = self.points.clone();
2626 points.sort_by(|a, b| a.value.total_cmp(&b.value));
2627 let first = points.first()?;
2628 let last = points.last()?;
2629 if value <= first.value {
2630 return Some((first.factor_re, first.factor_im));
2631 }
2632 if value >= last.value {
2633 return Some((last.factor_re, last.factor_im));
2634 }
2635 let upper = points.iter().position(|point| point.value >= value)?;
2636 let (a, b) = (points[upper - 1], points[upper]);
2637 let weight = (value - a.value) / (b.value - a.value);
2638 let lerp = |x: f64, y: f64| x + weight * (y - x);
2639 Some((
2640 lerp(a.factor_re, b.factor_re),
2641 lerp(a.factor_im, b.factor_im),
2642 ))
2643 }
2644}
2645
2646#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2648#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2649#[non_exhaustive]
2650pub struct BranchRatingSet {
2651 pub name: String,
2652 pub rate_mva: f64,
2653}
2654
2655impl BranchRatingSet {
2656 #[must_use]
2657 pub fn new(name: impl Into<String>, rate_mva: f64) -> Self {
2658 Self {
2659 name: name.into(),
2660 rate_mva,
2661 }
2662 }
2663}
2664
2665#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
2668#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2669#[non_exhaustive]
2670pub struct BranchCharging {
2671 pub g_fr: f64,
2672 pub b_fr: f64,
2673 pub g_to: f64,
2674 pub b_to: f64,
2675}
2676
2677impl BranchCharging {
2678 #[must_use]
2679 pub const fn new(g_fr: f64, b_fr: f64, g_to: f64, b_to: f64) -> Self {
2680 Self {
2681 g_fr,
2682 b_fr,
2683 g_to,
2684 b_to,
2685 }
2686 }
2687
2688 #[must_use]
2689 pub fn from_total_b(b: f64) -> Self {
2690 Self {
2691 g_fr: 0.0,
2692 b_fr: b / 2.0,
2693 g_to: 0.0,
2694 b_to: b / 2.0,
2695 }
2696 }
2697
2698 #[must_use]
2699 pub fn calc_total_b(self) -> f64 {
2700 self.b_fr + self.b_to
2701 }
2702
2703 #[must_use]
2704 pub fn calc_total_g(self) -> f64 {
2705 self.g_fr + self.g_to
2706 }
2707
2708 #[must_use]
2709 pub fn is_matpower_symmetric(self) -> bool {
2710 self.g_fr.abs() <= f64::EPSILON
2711 && self.g_to.abs() <= f64::EPSILON
2712 && (self.b_fr - self.b_to).abs() <= f64::EPSILON
2713 }
2714}
2715
2716#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
2718#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2719#[non_exhaustive]
2720pub struct BranchCurrentRatings {
2721 pub c_rating_a: f64,
2722 pub c_rating_b: f64,
2723 pub c_rating_c: f64,
2724}
2725
2726impl BranchCurrentRatings {
2727 #[must_use]
2728 pub const fn new(c_rating_a: f64, c_rating_b: f64, c_rating_c: f64) -> Self {
2729 Self {
2730 c_rating_a,
2731 c_rating_b,
2732 c_rating_c,
2733 }
2734 }
2735}
2736
2737#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
2739#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2740#[non_exhaustive]
2741pub struct BranchSolution {
2742 pub pf: f64,
2743 pub qf: f64,
2744 pub pt: f64,
2745 pub qt: f64,
2746}
2747
2748impl BranchSolution {
2749 #[must_use]
2750 pub const fn new(pf: f64, qf: f64, pt: f64, qt: f64) -> Self {
2751 Self { pf, qf, pt, qt }
2752 }
2753}
2754
2755impl Branch {
2756 #[must_use]
2757 pub fn new(from: BusId, to: BusId, r: f64, x: f64) -> Self {
2758 Self {
2759 name: None,
2760 from,
2761 to,
2762 r,
2763 x,
2764 b: 0.0,
2765 charging: None,
2766 rate_a: 0.0,
2767 rate_b: 0.0,
2768 rate_c: 0.0,
2769 rating_sets: Vec::new(),
2770 current_ratings: None,
2771 tap: 0.0,
2772 shift: 0.0,
2773 in_service: true,
2774 angmin: -360.0,
2775 angmax: 360.0,
2776 control: None,
2777 solution: None,
2778 uid: None,
2779 route: None,
2780 extras: Extras::new(),
2781 }
2782 }
2783
2784 #[must_use]
2786 pub fn calc_effective_tap(&self) -> f64 {
2787 if self.tap == 0.0 { 1.0 } else { self.tap }
2788 }
2789
2790 pub fn calc_divisible_tap(&self, row: usize) -> Result<f64> {
2799 let tap = self.calc_effective_tap();
2800 if !tap.is_finite() || tap.abs() < crate::dc::MIN_DIVISIBLE_MAGNITUDE {
2801 return Err(Error::DegenerateTap { row, tap });
2802 }
2803 Ok(tap)
2804 }
2805
2806 #[must_use]
2809 pub fn calc_terminal_charging(&self) -> BranchCharging {
2810 self.charging
2811 .unwrap_or_else(|| BranchCharging::from_total_b(self.b))
2812 }
2813
2814 pub fn calc_series_admittance(&self, row: usize) -> Result<Option<(f64, f64)>> {
2826 calc_series_admittance_of(self.r, self.x, row)
2827 }
2828
2829 #[must_use]
2855 pub fn synthesize_rate_a(
2856 &self,
2857 angle_window_rad: f64,
2858 (fr_vmin, fr_vmax): (f64, f64),
2859 (to_vmin, to_vmax): (f64, f64),
2860 ) -> f64 {
2861 let zmag = self.r.hypot(self.x);
2864 if zmag < crate::dc::MIN_DIVISIBLE_MAGNITUDE {
2865 return 0.0;
2866 }
2867 let window = angle_window_rad.abs().min(std::f64::consts::PI);
2868 let cos_window = window.cos();
2869 let separation = |vf: f64, vt: f64| {
2873 (vf * vf + vt * vt - 2.0 * vf * vt * cos_window)
2874 .max(0.0)
2875 .sqrt()
2876 };
2877 let widest = separation(fr_vmax, to_vmax)
2878 .max(separation(fr_vmax, to_vmin))
2879 .max(separation(fr_vmin, to_vmax))
2880 .max(separation(fr_vmin, to_vmin));
2881 fr_vmax.max(to_vmax) * widest / zmag
2882 }
2883
2884 #[must_use]
2887 pub fn calc_total_charging_b(&self) -> f64 {
2888 self.calc_terminal_charging().calc_total_b()
2889 }
2890
2891 #[must_use]
2893 pub fn has_non_matpower_charging(&self) -> bool {
2894 self.charging
2895 .is_some_and(|charging| !charging.is_matpower_symmetric())
2896 }
2897
2898 #[must_use]
2901 pub fn is_transformer(&self) -> bool {
2902 self.tap != 0.0 || self.shift != 0.0
2903 }
2904
2905 #[must_use]
2909 pub fn has_angle_limits(&self) -> bool {
2910 self.angmin > -360.0 || self.angmax < 360.0
2911 }
2912
2913 #[must_use]
2918 pub fn impedance_correction(&self) -> Option<ImpedanceCorrection> {
2919 self.extras
2920 .get(IMPEDANCE_CORRECTION_EXTRA)
2921 .and_then(|value| ImpedanceCorrection::deserialize(value).ok())
2922 }
2923
2924 pub fn set_impedance_correction(&mut self, correction: Option<ImpedanceCorrection>) {
2926 set_impedance_correction_extra(&mut self.extras, correction);
2927 }
2928}
2929
2930fn set_impedance_correction_extra(extras: &mut Extras, correction: Option<ImpedanceCorrection>) {
2931 match correction {
2932 Some(correction) => {
2933 let value = serde_json::to_value(correction).expect("a correction serializes to JSON");
2934 extras.insert(IMPEDANCE_CORRECTION_EXTRA.to_owned(), value);
2935 }
2936 None => {
2937 extras.remove(IMPEDANCE_CORRECTION_EXTRA);
2938 }
2939 }
2940}
2941
2942pub fn calc_series_admittance_of(r: f64, x: f64, row: usize) -> Result<Option<(f64, f64)>> {
2959 let magnitude = r.hypot(x);
2960 if magnitude < crate::dc::MIN_DIVISIBLE_MAGNITUDE {
2961 return Ok(None);
2962 }
2963 if !magnitude.is_finite() {
2964 return Err(Error::NonFiniteSusceptance { row });
2965 }
2966 Ok(Some(crate::dc::series_admittance_parts(r, x)))
2967}
2968
2969#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2972#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2973#[non_exhaustive]
2974pub struct Switch {
2975 pub from: BusId,
2976 pub to: BusId,
2977 pub closed: bool,
2978 #[serde(default)]
2979 pub thermal_rating: Option<f64>,
2980 #[serde(default)]
2981 pub current_rating: Option<f64>,
2982 #[serde(default)]
2983 pub pf: Option<f64>,
2984 #[serde(default)]
2985 pub qf: Option<f64>,
2986 #[serde(default)]
2987 pub pt: Option<f64>,
2988 #[serde(default)]
2989 pub qt: Option<f64>,
2990 #[serde(default, skip_serializing_if = "Option::is_none")]
2992 pub uid: Option<String>,
2993 pub extras: Extras,
2994}
2995
2996impl Switch {
2997 #[must_use]
2998 pub fn new(from: BusId, to: BusId, closed: bool) -> Self {
2999 Self {
3000 from,
3001 to,
3002 closed,
3003 thermal_rating: None,
3004 current_rating: None,
3005 pf: None,
3006 qf: None,
3007 pt: None,
3008 qt: None,
3009 uid: None,
3010 extras: Extras::new(),
3011 }
3012 }
3013}
3014
3015#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3018#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3019#[serde(rename_all = "snake_case")]
3020#[non_exhaustive]
3021pub enum TransformerControlMode {
3022 Fixed,
3024 Voltage,
3026 ReactiveFlow,
3028 ActiveFlow,
3030 DcLineQuantity,
3032 AsymmetricActiveFlow,
3034}
3035
3036#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3046#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3047#[non_exhaustive]
3048pub struct TransformerControl {
3049 pub mode: TransformerControlMode,
3050 pub enabled: bool,
3053 pub controlled_bus: Option<BusId>,
3054 #[serde(default)]
3057 pub controlled_bus_on_winding_side: bool,
3058 #[serde(default, skip_serializing_if = "Option::is_none")]
3060 pub regulating_terminal: Option<TerminalReference>,
3061 pub tap_min: f64,
3062 pub tap_max: f64,
3063 pub band_min: f64,
3064 pub band_max: f64,
3065 pub ntp: u32,
3066 pub mva_base: f64,
3067 #[serde(default, skip_serializing_if = "Option::is_none")]
3070 pub winding_connection_angle: Option<f64>,
3071}
3072
3073impl Default for TransformerControl {
3074 fn default() -> Self {
3075 TransformerControl {
3077 mode: TransformerControlMode::Fixed,
3078 enabled: false,
3079 controlled_bus: None,
3080 controlled_bus_on_winding_side: false,
3081 regulating_terminal: None,
3082 tap_min: 0.9,
3083 tap_max: 1.1,
3084 band_min: 0.9,
3085 band_max: 1.1,
3086 ntp: 33,
3087 mva_base: 0.0,
3088 winding_connection_angle: None,
3089 }
3090 }
3091}
3092
3093impl TransformerControl {
3094 #[must_use]
3095 pub fn new(mode: TransformerControlMode) -> Self {
3096 Self {
3097 mode,
3098 enabled: mode != TransformerControlMode::Fixed,
3099 ..Self::default()
3100 }
3101 }
3102}
3103
3104#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3114#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3115#[non_exhaustive]
3116pub struct ActivePowerControl {
3117 pub participate: bool,
3118 #[serde(default, skip_serializing_if = "Option::is_none")]
3119 pub droop_percent: Option<f64>,
3120 #[serde(default, skip_serializing_if = "Option::is_none")]
3121 pub participation_factor: Option<f64>,
3122 #[serde(default, skip_serializing_if = "Option::is_none")]
3123 pub minimum_target_active_power_mw: Option<f64>,
3124 #[serde(default, skip_serializing_if = "Option::is_none")]
3125 pub maximum_target_active_power_mw: Option<f64>,
3126}
3127
3128impl ActivePowerControl {
3129 #[must_use]
3130 pub fn new(participate: bool) -> Self {
3131 Self {
3132 participate,
3133 droop_percent: None,
3134 participation_factor: None,
3135 minimum_target_active_power_mw: None,
3136 maximum_target_active_power_mw: None,
3137 }
3138 }
3139}
3140
3141#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3142#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3143#[non_exhaustive]
3144pub struct Generator {
3145 pub bus: BusId,
3146 #[serde(default)]
3148 pub energy_source: GeneratorEnergySource,
3149 pub pg: f64,
3151 pub qg: f64,
3153 pub pmax: f64,
3154 pub pmin: f64,
3155 pub qmax: f64,
3156 pub qmin: f64,
3157 pub vg: f64,
3159 pub mbase: f64,
3160 pub in_service: bool,
3161 pub cost: Option<GenCost>,
3162 #[serde(default = "default_caps", with = "caps_serde")]
3171 #[cfg_attr(feature = "schema", schemars(with = "BTreeMap<String, f64>"))]
3172 pub caps: GenCaps,
3173 #[serde(default = "default_voltage_regulation_on")]
3177 pub voltage_regulation_on: bool,
3178 #[serde(default, skip_serializing_if = "Option::is_none")]
3182 pub regulating_terminal: Option<TerminalReference>,
3183 #[serde(default)]
3190 pub regulated_bus: Option<BusId>,
3191 #[serde(default, skip_serializing_if = "Option::is_none")]
3193 pub active_power_control: Option<ActivePowerControl>,
3194 #[serde(default, skip_serializing_if = "Option::is_none")]
3196 pub uid: Option<String>,
3197}
3198
3199impl Generator {
3200 #[must_use]
3201 pub fn new(bus: BusId) -> Self {
3202 Self {
3203 bus,
3204 energy_source: GeneratorEnergySource::Other,
3205 pg: 0.0,
3206 qg: 0.0,
3207 pmax: 0.0,
3208 pmin: 0.0,
3209 qmax: 0.0,
3210 qmin: 0.0,
3211 vg: 1.0,
3212 mbase: 0.0,
3213 in_service: true,
3214 cost: None,
3215 caps: default_caps(),
3216 voltage_regulation_on: true,
3217 regulating_terminal: None,
3218 regulated_bus: None,
3219 active_power_control: None,
3220 uid: None,
3221 }
3222 }
3223
3224 #[must_use]
3227 pub fn has_caps(&self) -> bool {
3228 self.caps.iter().any(Option::is_some)
3229 }
3230}
3231
3232#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
3234#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3235#[serde(rename_all = "snake_case")]
3236#[non_exhaustive]
3237pub enum GeneratorEnergySource {
3238 Hydro,
3239 Nuclear,
3240 Wind,
3241 Thermal,
3242 Solar,
3243 #[default]
3244 Other,
3245}
3246
3247const fn default_voltage_regulation_on() -> bool {
3248 true
3249}
3250
3251pub type GenCaps = [Option<f64>; GEN_EXTRA_KEYS.len()];
3253
3254fn default_caps() -> GenCaps {
3256 [None; GEN_EXTRA_KEYS.len()]
3257}
3258
3259mod caps_serde {
3270 use super::{GEN_EXTRA_KEYS, GenCaps};
3271 use serde::de::{Deserialize, Deserializer};
3272 use serde::ser::{SerializeMap, Serializer};
3273 use std::collections::BTreeMap;
3274
3275 pub(super) fn serialize<S: Serializer>(caps: &GenCaps, s: S) -> Result<S::Ok, S::Error> {
3276 let present = caps.iter().filter(|v| v.is_some()).count();
3277 let mut map = s.serialize_map(Some(present))?;
3278 for (key, slot) in GEN_EXTRA_KEYS.iter().zip(caps.iter()) {
3279 if let Some(value) = slot {
3280 map.serialize_entry(key, value)?;
3281 }
3282 }
3283 map.end()
3284 }
3285
3286 pub(super) fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<GenCaps, D::Error> {
3287 let named = Option::<BTreeMap<String, f64>>::deserialize(d)?.unwrap_or_default();
3292 let mut caps: GenCaps = [None; GEN_EXTRA_KEYS.len()];
3293 for (slot, key) in caps.iter_mut().zip(GEN_EXTRA_KEYS.iter()) {
3294 *slot = named.get(*key).copied();
3295 }
3296 Ok(caps)
3297 }
3298}
3299
3300#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3301#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3302#[non_exhaustive]
3303pub struct Storage {
3304 pub bus: BusId,
3305 pub ps: f64,
3306 pub qs: f64,
3307 pub energy: f64,
3308 pub energy_rating: f64,
3309 pub charge_rating: f64,
3310 pub discharge_rating: f64,
3311 pub charge_efficiency: f64,
3312 pub discharge_efficiency: f64,
3313 pub thermal_rating: f64,
3314 #[serde(default)]
3315 pub current_rating: Option<f64>,
3316 pub qmin: f64,
3317 pub qmax: f64,
3318 pub r: f64,
3319 pub x: f64,
3320 pub p_loss: f64,
3321 pub q_loss: f64,
3322 pub in_service: bool,
3323 #[serde(default, skip_serializing_if = "Option::is_none")]
3325 pub active_power_control: Option<ActivePowerControl>,
3326 #[serde(default, skip_serializing_if = "Option::is_none")]
3328 pub uid: Option<String>,
3329 pub extras: Extras,
3330}
3331
3332impl Storage {
3333 #[must_use]
3334 pub fn new(bus: BusId) -> Self {
3335 Self {
3336 bus,
3337 ps: 0.0,
3338 qs: 0.0,
3339 energy: 0.0,
3340 energy_rating: 0.0,
3341 charge_rating: 0.0,
3342 discharge_rating: 0.0,
3343 charge_efficiency: 1.0,
3344 discharge_efficiency: 1.0,
3345 thermal_rating: 0.0,
3346 current_rating: None,
3347 qmin: 0.0,
3348 qmax: 0.0,
3349 r: 0.0,
3350 x: 0.0,
3351 p_loss: 0.0,
3352 q_loss: 0.0,
3353 in_service: true,
3354 active_power_control: None,
3355 uid: None,
3356 extras: Extras::new(),
3357 }
3358 }
3359}
3360
3361#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
3369#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3370#[serde(rename_all = "snake_case")]
3371#[non_exhaustive]
3372pub enum HvdcConvertersMode {
3373 Side1RectifierSide2Inverter,
3374 Side1InverterSide2Rectifier,
3375}
3376
3377#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
3379#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3380#[serde(rename_all = "snake_case")]
3381#[non_exhaustive]
3382pub enum HvdcConverterKind {
3383 Vsc,
3384 Lcc,
3385}
3386
3387#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3389#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3390#[non_exhaustive]
3391pub struct HvdcConverter {
3392 pub component: ComponentId,
3393 pub kind: HvdcConverterKind,
3394 pub loss_factor_percent: f64,
3396 #[serde(default, skip_serializing_if = "Option::is_none")]
3397 pub voltage_regulator_on: Option<bool>,
3398 #[serde(default, skip_serializing_if = "Option::is_none")]
3399 pub voltage_setpoint_kv: Option<f64>,
3400 #[serde(default, skip_serializing_if = "Option::is_none")]
3401 pub reactive_power_setpoint_mvar: Option<f64>,
3402 #[serde(default, skip_serializing_if = "Option::is_none")]
3403 pub power_factor: Option<f64>,
3404 #[serde(default, skip_serializing_if = "Option::is_none")]
3405 pub regulating_terminal: Option<TerminalReference>,
3406}
3407
3408#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3409#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3410#[non_exhaustive]
3411pub struct Hvdc {
3412 pub from: BusId,
3413 pub to: BusId,
3414 pub in_service: bool,
3415 pub pf: f64,
3416 pub pt: f64,
3417 pub qf: f64,
3418 pub qt: f64,
3419 pub vf: f64,
3420 pub vt: f64,
3421 pub pmin: f64,
3422 pub pmax: f64,
3423 pub qminf: f64,
3424 pub qmaxf: f64,
3425 pub qmint: f64,
3426 pub qmaxt: f64,
3427 pub loss0: f64,
3428 pub loss1: f64,
3429 #[serde(default, skip_serializing_if = "Option::is_none")]
3432 pub resistance_ohm: Option<f64>,
3433 #[serde(default, skip_serializing_if = "Option::is_none")]
3434 pub nominal_voltage_kv: Option<f64>,
3435 #[serde(default, skip_serializing_if = "Option::is_none")]
3436 pub converters_mode: Option<HvdcConvertersMode>,
3437 #[serde(default, skip_serializing_if = "Option::is_none")]
3438 pub converter1: Option<HvdcConverter>,
3439 #[serde(default, skip_serializing_if = "Option::is_none")]
3440 pub converter2: Option<HvdcConverter>,
3441 #[serde(default)]
3442 pub cost: Option<GenCost>,
3443 #[serde(default, skip_serializing_if = "Option::is_none")]
3445 pub uid: Option<String>,
3446 pub extras: Extras,
3447}
3448
3449impl Hvdc {
3450 #[must_use]
3459 pub fn calc_delivered_power(pf: f64, loss0: f64, loss1: f64) -> f64 {
3460 pf - loss0 - loss1 * pf
3461 }
3462
3463 #[must_use]
3467 pub fn pt_matches_loss_model(&self, tol: f64) -> bool {
3468 (self.pt - Self::calc_delivered_power(self.pf, self.loss0, self.loss1)).abs() <= tol
3469 }
3470
3471 #[must_use]
3472 pub fn new(from: BusId, to: BusId) -> Self {
3473 Self {
3474 from,
3475 to,
3476 in_service: true,
3477 pf: 0.0,
3478 pt: 0.0,
3479 qf: 0.0,
3480 qt: 0.0,
3481 vf: 1.0,
3482 vt: 1.0,
3483 pmin: 0.0,
3484 pmax: 0.0,
3485 qminf: 0.0,
3486 qmaxf: 0.0,
3487 qmint: 0.0,
3488 qmaxt: 0.0,
3489 loss0: 0.0,
3490 loss1: 0.0,
3491 resistance_ohm: None,
3492 nominal_voltage_kv: None,
3493 converters_mode: None,
3494 converter1: None,
3495 converter2: None,
3496 cost: None,
3497 uid: None,
3498 extras: Extras::new(),
3499 }
3500 }
3501}
3502
3503#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3510#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3511#[non_exhaustive]
3512pub struct Area {
3513 pub number: usize,
3514 pub slack_bus: Option<BusId>,
3516 pub net_interchange: f64,
3518 pub tolerance: f64,
3520 pub name: Option<String>,
3521 #[serde(default, skip_serializing_if = "Option::is_none")]
3524 pub uid: Option<String>,
3525 #[serde(default, skip_serializing_if = "Option::is_none")]
3528 pub area_type: Option<String>,
3529}
3530
3531impl Area {
3532 #[must_use]
3533 pub fn new(number: usize) -> Self {
3534 Self {
3535 number,
3536 slack_bus: None,
3537 net_interchange: 0.0,
3538 tolerance: 0.0,
3539 name: None,
3540 uid: None,
3541 area_type: None,
3542 }
3543 }
3544}
3545
3546#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
3555#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3556#[non_exhaustive]
3557pub struct SolverParams {
3558 pub newton_tolerance: Option<f64>,
3560 pub max_iterations: Option<u32>,
3562 pub zero_impedance_threshold: Option<f64>,
3564 pub adjust_taps: Option<bool>,
3566 pub adjust_area_interchange: Option<bool>,
3568 pub adjust_phase_shift: Option<bool>,
3570 pub adjust_dc_taps: Option<bool>,
3572 pub adjust_switched_shunt: Option<bool>,
3574}
3575
3576impl SolverParams {
3577 #[must_use]
3578 pub fn new() -> Self {
3579 Self::default()
3580 }
3581
3582 #[must_use]
3584 pub fn is_empty(&self) -> bool {
3585 *self == SolverParams::default()
3586 }
3587}
3588
3589#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
3601#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3602#[non_exhaustive]
3603pub struct Impedance {
3604 pub r: f64,
3605 pub x: f64,
3606 pub base_mva: f64,
3607}
3608
3609impl Impedance {
3610 #[must_use]
3611 pub const fn new(r: f64, x: f64, base_mva: f64) -> Self {
3612 Self { r, x, base_mva }
3613 }
3614}
3615
3616pub(crate) const WINDING_IN_SERVICE_EXTRA: &str = "winding_in_service";
3619pub(crate) const WINDING_RATING_SETS_EXTRA: &str = "winding_rating_sets";
3622
3623#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3626#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3627#[non_exhaustive]
3628pub struct Winding {
3629 pub bus: BusId,
3630 pub tap: f64,
3632 pub shift: f64,
3634 pub nominal_kv: f64,
3636 pub rate_a: f64,
3637 pub rate_b: f64,
3638 pub rate_c: f64,
3639 #[serde(default, skip_serializing_if = "Option::is_none")]
3641 pub control: Option<TransformerControl>,
3642}
3643
3644impl Winding {
3645 #[must_use]
3646 pub fn new(bus: BusId) -> Self {
3647 Self {
3648 bus,
3649 tap: 1.0,
3650 shift: 0.0,
3651 nominal_kv: 0.0,
3652 rate_a: 0.0,
3653 rate_b: 0.0,
3654 rate_c: 0.0,
3655 control: None,
3656 }
3657 }
3658}
3659
3660#[derive(Debug, Clone, PartialEq)]
3670#[non_exhaustive]
3671pub struct Transformer3W {
3672 pub windings: [Winding; 3],
3674 pub z: [Impedance; 3],
3678 pub star_vm: f64,
3680 pub star_va: f64,
3681 pub mag_g: f64,
3683 pub mag_b: f64,
3684 pub in_service: bool,
3685 pub name: Option<String>,
3686 pub uid: Option<String>,
3688 pub extras: Extras,
3689}
3690
3691impl Transformer3W {
3692 #[must_use]
3693 pub fn new(windings: [Winding; 3], z: [Impedance; 3]) -> Self {
3694 Self {
3695 windings,
3696 z,
3697 star_vm: 1.0,
3698 star_va: 0.0,
3699 mag_g: 0.0,
3700 mag_b: 0.0,
3701 in_service: true,
3702 name: None,
3703 uid: None,
3704 extras: Extras::new(),
3705 }
3706 }
3707
3708 #[must_use]
3715 pub fn calc_star_impedances(&self) -> [(f64, f64); 3] {
3716 let [z12, z23, z31] = self.z;
3717 let half = |a: f64, b: f64, c: f64| (a + b - c) / 2.0;
3718 [
3719 (half(z12.r, z31.r, z23.r), half(z12.x, z31.x, z23.x)),
3720 (half(z12.r, z23.r, z31.r), half(z12.x, z23.x, z31.x)),
3721 (half(z23.r, z31.r, z12.r), half(z23.x, z31.x, z12.x)),
3722 ]
3723 }
3724
3725 #[must_use]
3734 pub fn winding_in_service(&self) -> [bool; 3] {
3735 self.extras
3736 .get(WINDING_IN_SERVICE_EXTRA)
3737 .and_then(|value| <[bool; 3]>::deserialize(value).ok())
3738 .unwrap_or([true; 3])
3739 }
3740
3741 pub fn set_winding_in_service(&mut self, in_service: [bool; 3]) {
3743 if in_service == [true; 3] {
3744 self.extras.remove(WINDING_IN_SERVICE_EXTRA);
3745 } else {
3746 self.extras.insert(
3747 WINDING_IN_SERVICE_EXTRA.to_owned(),
3748 Value::from(in_service.to_vec()),
3749 );
3750 }
3751 }
3752
3753 #[must_use]
3764 pub fn winding_rating_sets(&self, winding: usize) -> Vec<BranchRatingSet> {
3765 let mut sets = self.stored_winding_rating_sets();
3766 std::mem::take(&mut sets[winding])
3767 }
3768
3769 pub fn set_winding_rating_sets(&mut self, winding: usize, rating_sets: Vec<BranchRatingSet>) {
3774 let mut sets = self.stored_winding_rating_sets();
3775 sets[winding] = rating_sets;
3776 if sets.iter().all(Vec::is_empty) {
3777 self.extras.remove(WINDING_RATING_SETS_EXTRA);
3778 } else {
3779 let value = serde_json::to_value(sets).expect("rating sets serialize to JSON");
3780 self.extras
3781 .insert(WINDING_RATING_SETS_EXTRA.to_owned(), value);
3782 }
3783 }
3784
3785 fn stored_winding_rating_sets(&self) -> [Vec<BranchRatingSet>; 3] {
3786 self.extras
3787 .get(WINDING_RATING_SETS_EXTRA)
3788 .and_then(|value| <[Vec<BranchRatingSet>; 3]>::deserialize(value).ok())
3789 .unwrap_or_default()
3790 }
3791
3792 #[must_use]
3798 pub fn impedance_correction_control_winding(&self) -> Option<usize> {
3799 let mut adjustable = self
3800 .windings
3801 .iter()
3802 .enumerate()
3803 .filter(|(_, winding)| {
3804 winding
3805 .control
3806 .as_ref()
3807 .is_some_and(|control| control.mode != TransformerControlMode::Fixed)
3808 })
3809 .map(|(index, _)| index);
3810 let winding = adjustable.next()?;
3811 adjustable.next().is_none().then_some(winding)
3812 }
3813
3814 #[must_use]
3824 pub fn winding_impedance_correction(&self, winding: usize) -> Option<ImpedanceCorrection> {
3825 let mut corrections = self.stored_winding_impedance_corrections();
3826 corrections[winding].take()
3827 }
3828
3829 pub fn set_winding_impedance_correction(
3835 &mut self,
3836 winding: usize,
3837 correction: Option<ImpedanceCorrection>,
3838 ) {
3839 let mut corrections = self.stored_winding_impedance_corrections();
3840 corrections[winding] = correction;
3841 if corrections.iter().all(Option::is_none) {
3842 self.extras.remove(IMPEDANCE_CORRECTION_EXTRA);
3843 } else {
3844 let value = serde_json::to_value(corrections).expect("corrections serialize to JSON");
3845 self.extras
3846 .insert(IMPEDANCE_CORRECTION_EXTRA.to_owned(), value);
3847 }
3848 }
3849
3850 fn stored_winding_impedance_corrections(&self) -> [Option<ImpedanceCorrection>; 3] {
3851 self.extras
3852 .get(IMPEDANCE_CORRECTION_EXTRA)
3853 .and_then(|value| <[Option<ImpedanceCorrection>; 3]>::deserialize(value).ok())
3854 .unwrap_or_default()
3855 }
3856
3857 #[must_use]
3873 pub fn to_star_expansion(&self, star_id: BusId) -> (Bus, [Branch; 3]) {
3874 let star = Bus {
3875 id: star_id,
3876 kind: BusType::Pq,
3877 vm: self.star_vm,
3878 va: self.star_va,
3879 base_kv: self.windings[0].nominal_kv,
3880 vmax: 1.1,
3881 vmin: 0.9,
3882 evhi: None,
3883 evlo: None,
3884 area: 0,
3885 zone: 0,
3886 name: self.name.clone(),
3887 uid: self.uid.clone(),
3888 location: None,
3889 extras: Extras::new(),
3890 };
3891 let zs = self.calc_star_impedances();
3892 let branch = |w: &Winding, (r, x): (f64, f64)| Branch {
3893 name: None,
3894 from: w.bus,
3895 to: star_id,
3896 r,
3897 x,
3898 b: 0.0,
3899 charging: None,
3900 rate_a: w.rate_a,
3901 rate_b: w.rate_b,
3902 rate_c: w.rate_c,
3903 rating_sets: Vec::new(),
3904 current_ratings: None,
3905 tap: w.tap,
3906 shift: w.shift,
3907 in_service: self.in_service,
3908 angmin: -360.0,
3909 angmax: 360.0,
3910 control: w.control.clone(),
3911 solution: None,
3912 uid: None,
3913 route: None,
3914 extras: Extras::new(),
3915 };
3916 let mut branches = [
3917 branch(&self.windings[0], zs[0]),
3918 branch(&self.windings[1], zs[1]),
3919 branch(&self.windings[2], zs[2]),
3920 ];
3921 let windings_in_service = self.winding_in_service();
3922 let independent = self.extras.get("psse_zcod").and_then(Value::as_i64) != Some(1);
3923 let pairwise = self.z.map(|z| [z.r, z.x]);
3924 let nominal = current_correction_nominal::<[[f64; 2]; 3]>(&self.extras, &pairwise);
3925 for (k, branch) in branches.iter_mut().enumerate() {
3926 branch.in_service &= windings_in_service[k];
3927 branch.rating_sets = self.winding_rating_sets(k);
3928 let Some(correction) = self.winding_impedance_correction(k).filter(|_| independent)
3929 else {
3930 continue;
3931 };
3932 branch.set_impedance_correction(Some(correction));
3933 if let Some(z) = nominal {
3934 let (a, b, c) = [(0, 2, 1), (0, 1, 2), (1, 2, 0)][k];
3936 let leg: [f64; 2] =
3937 std::array::from_fn(|part| (z[a][part] + z[b][part] - z[c][part]) / 2.0);
3938 branch.extras.insert(
3939 IMPEDANCE_CORRECTION_NOMINAL_EXTRA.into(),
3940 serde_json::json!(leg),
3941 );
3942 branch.extras.insert(
3943 IMPEDANCE_CORRECTION_APPLIED_EXTRA.into(),
3944 serde_json::json!([branch.r, branch.x]),
3945 );
3946 }
3947 }
3948 (star, branches)
3949 }
3950}
3951
3952pub const GEN_EXTRA_KEYS: [&str; 11] = [
3955 "pc1", "pc2", "qc1min", "qc1max", "qc2min", "qc2max", "ramp_agc", "ramp_10", "ramp_30",
3956 "ramp_q", "apf",
3957];
3958
3959#[derive(Debug, Clone, PartialEq)]
3966pub(crate) struct ValueFinding {
3967 pub element: String,
3969 pub table: &'static str,
3974 pub index: usize,
3977 pub field: &'static str,
3978 pub old: f64,
3979 pub new: f64,
3980 pub reason: &'static str,
3981}
3982
3983impl ValueFinding {
3984 pub(crate) fn into_diagnostic(self) -> crate::Diagnostic {
3988 let mut details = serde_json::Map::new();
3989 details.insert("element".to_owned(), serde_json::json!(self.element));
3990 details.insert("field".to_owned(), serde_json::json!(self.field));
3991 details.insert("value".to_owned(), serde_json::json!(self.old));
3992 details.insert("repaired_value".to_owned(), serde_json::json!(self.new));
3993 details.insert("reason".to_owned(), serde_json::json!(self.reason));
3994 crate::Diagnostic::of(
3995 &crate::diagnostics::codes::VALIDATE_BALANCED_VALUE_DOMAIN,
3996 format!(
3997 "{}: `{}` is {} ({}); the repair sets {}",
3998 self.element, self.field, self.old, self.reason, self.new
3999 ),
4000 )
4001 .with_target(format!("/{}/{}/{}", self.table, self.index, self.field))
4002 .expect("scan-built targets are nonempty and bounded")
4003 .with_details(details)
4004 .expect("scan-built details stay within the record bounds")
4005 }
4006}
4007
4008pub fn repair_values(
4019 module: powerio_core::PioModule<BalancedNetwork>,
4020) -> std::result::Result<powerio_core::PioModule<BalancedNetwork>, powerio_core::Error> {
4021 let repair_ordinal = module
4022 .history()
4023 .iter()
4024 .filter(|entry| entry.kind() == powerio_core::HistoryKind::Repair)
4025 .count();
4026 let mut network_findings = Vec::new();
4027 let mut module = module.map_value(|mut network| {
4028 network_findings = network.repair_in_place();
4029 network
4030 });
4031 if network_findings.is_empty() {
4032 return Ok(module);
4033 }
4034 let mut parameters = std::collections::BTreeMap::new();
4035 parameters.insert(
4036 "repairs".to_owned(),
4037 serde_json::json!(
4038 network_findings
4039 .iter()
4040 .map(|finding| {
4041 serde_json::json!({
4042 "element": finding.element,
4043 "field": finding.field,
4044 "value": finding.old,
4045 "repaired_value": finding.new,
4046 })
4047 })
4048 .collect::<Vec<_>>()
4049 ),
4050 );
4051 let entry = powerio_core::HistoryEntry::new(
4052 powerio_core::HistoryId::new(format!("repair{repair_ordinal}"))?,
4053 powerio_core::HistoryKind::Repair,
4054 "value_domain_repair",
4055 )?
4056 .with_parameters(parameters)?;
4057 module.add_history_entry(entry)?;
4058 for finding in network_findings {
4059 module.add_diagnostic(finding.into_diagnostic())?;
4060 }
4061 module = module.sever_source();
4062 Ok(module)
4063}
4064
4065fn repair_vm(vm: f64) -> Option<f64> {
4069 (!vm.is_finite() || vm <= 0.0 || vm > 2.0).then_some(1.0)
4070}
4071
4072fn repair_va(va: f64) -> Option<f64> {
4074 (!va.is_finite() || va.abs() > 2000.0).then_some(0.0)
4075}
4076
4077fn repair_mbase(mbase: f64, sbase: f64) -> Option<f64> {
4079 (!mbase.is_finite() || mbase <= 0.0).then_some(sbase)
4080}
4081
4082fn repair_vg(vg: f64) -> Option<f64> {
4084 (!vg.is_finite() || vg <= 0.0).then_some(1.0)
4085}
4086
4087#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4092pub(crate) struct LoweredLengths {
4093 pub(crate) buses: usize,
4094 pub(crate) branches: usize,
4095 pub(crate) shunts: usize,
4096}
4097
4098impl BalancedNetwork {
4099 #[must_use]
4100 pub fn new(name: impl Into<String>, base_mva: f64) -> BalancedNetwork {
4101 BalancedNetwork::from_tables(BalancedNetworkTables {
4102 name: name.into(),
4103 base_mva,
4104 base_frequency: DEFAULT_BASE_FREQUENCY,
4105 geo: None,
4106 case_metadata: CaseMetadata::default(),
4107 detailed_connectivity: None,
4108 generated_uids: std::sync::Arc::default(),
4109 buses: Vec::new().into(),
4110 loads: Vec::new().into(),
4111 shunts: Vec::new().into(),
4112 static_var_compensators: Vec::new().into(),
4113 branches: Vec::new().into(),
4114 switches: Vec::new().into(),
4115 generators: Vec::new().into(),
4116 storage: Vec::new().into(),
4117 hvdc: Vec::new().into(),
4118 transformers_3w: Vec::new().into(),
4119 areas: Vec::new().into(),
4120 solver: None,
4121 source_format: SourceFormat::InMemory,
4122 })
4123 }
4124
4125 #[must_use]
4131 pub fn in_memory(
4132 name: impl Into<String>,
4133 base_mva: f64,
4134 buses: Vec<Bus>,
4135 branches: Vec<Branch>,
4136 ) -> BalancedNetwork {
4137 let mut net = Self::new(name, base_mva);
4138 *net.buses_mut() = buses;
4139 *net.branches_mut() = branches;
4140 net.assign_missing_component_ids();
4141 net
4142 }
4143
4144 #[must_use]
4149 pub fn is_normalized(&self) -> bool {
4150 self.source_format() == SourceFormat::Normalized
4151 }
4152
4153 pub fn check_base_mva(&self) -> crate::Result<()> {
4159 if self.base_mva().is_finite() && self.base_mva() > 0.0 {
4160 Ok(())
4161 } else {
4162 Err(crate::Error::InvalidBaseMva {
4163 base: self.base_mva(),
4164 })
4165 }
4166 }
4167
4168 #[must_use]
4180 pub fn validate_values(&self) -> Vec<crate::Diagnostic> {
4181 self.value_findings()
4182 .into_iter()
4183 .map(ValueFinding::into_diagnostic)
4184 .collect()
4185 }
4186
4187 pub(crate) fn value_findings(&self) -> Vec<ValueFinding> {
4188 let mut out = Vec::new();
4189 for (index, b) in self.buses().iter().enumerate() {
4190 if let Some(new) = repair_vm(b.vm) {
4191 out.push(ValueFinding {
4192 element: format!("bus {}", b.id),
4193 table: "buses",
4194 index,
4195 field: "vm",
4196 old: b.vm,
4197 new,
4198 reason: "voltage magnitude outside [0, 2] p.u.",
4199 });
4200 }
4201 if let Some(new) = repair_va(b.va) {
4202 out.push(ValueFinding {
4203 element: format!("bus {}", b.id),
4204 table: "buses",
4205 index,
4206 field: "va",
4207 old: b.va,
4208 new,
4209 reason: "voltage angle outside ±2000°",
4210 });
4211 }
4212 }
4213 for (index, g) in self.generators().iter().enumerate() {
4214 if let Some(new) = repair_mbase(g.mbase, self.base_mva()) {
4215 out.push(ValueFinding {
4216 element: format!("generator at bus {}", g.bus),
4217 table: "generators",
4218 index,
4219 field: "mbase",
4220 old: g.mbase,
4221 new,
4222 reason: "non-positive generator MVA base",
4223 });
4224 }
4225 if let Some(new) = repair_vg(g.vg) {
4226 out.push(ValueFinding {
4227 element: format!("generator at bus {}", g.bus),
4228 table: "generators",
4229 index,
4230 field: "vg",
4231 old: g.vg,
4232 new,
4233 reason: "non-positive voltage setpoint",
4234 });
4235 }
4236 }
4237 out
4238 }
4239
4240 pub(crate) fn repair_in_place(&mut self) -> Vec<ValueFinding> {
4247 let findings = self.value_findings();
4248 let sbase = self.base_mva();
4249 for b in self.buses_mut() {
4250 if let Some(new) = repair_vm(b.vm) {
4251 b.vm = new;
4252 }
4253 if let Some(new) = repair_va(b.va) {
4254 b.va = new;
4255 }
4256 }
4257 for g in self.generators_mut() {
4258 if let Some(new) = repair_mbase(g.mbase, sbase) {
4259 g.mbase = new;
4260 }
4261 if let Some(new) = repair_vg(g.vg) {
4262 g.vg = new;
4263 }
4264 }
4265 findings
4266 }
4267
4268 pub(crate) fn lowered_lengths(&self) -> LoweredLengths {
4274 let mut lengths = LoweredLengths {
4275 buses: self.buses().len(),
4276 branches: self.branches().len(),
4277 shunts: self.shunts().len(),
4278 };
4279 for t in self.transformers_3w().iter().filter(|t| t.in_service) {
4280 lengths.buses += 1;
4281 lengths.branches += 3;
4282 if t.mag_g != 0.0 || t.mag_b != 0.0 {
4283 lengths.shunts += 1;
4284 }
4285 }
4286 lengths
4287 }
4288
4289 pub(crate) fn expand_transformers_3w(&self) -> std::borrow::Cow<'_, BalancedNetwork> {
4301 if !self.transformers_3w().iter().any(|t| t.in_service) {
4302 return std::borrow::Cow::Borrowed(self);
4303 }
4304 let mut net = self.clone();
4305 let scale = if net.is_normalized() {
4310 1.0
4311 } else {
4312 net.base_mva()
4313 };
4314 let base_id = net
4319 .buses()
4320 .iter()
4321 .map(|b| b.id.0)
4322 .max()
4323 .unwrap_or(0)
4324 .checked_add(1)
4325 .expect("bus id space exhausted for star expansion");
4326 for (k, (source_row, t)) in self
4327 .transformers_3w()
4328 .iter()
4329 .enumerate()
4330 .filter(|(_, t)| t.in_service)
4331 .enumerate()
4332 {
4333 let star_id = BusId(
4334 base_id
4335 .checked_add(k)
4336 .expect("bus id space exhausted for star expansion"),
4337 );
4338 let (star, mut branches) = t.to_star_expansion(star_id);
4339 let transformer_identity = t
4340 .uid
4341 .clone()
4342 .unwrap_or_else(|| format!("transformers_3w:{source_row}"));
4343 for (winding, branch) in branches.iter_mut().enumerate() {
4344 branch.uid = Some(format!("{transformer_identity}/winding:{}", winding + 1));
4345 }
4346 net.buses_mut().push(star);
4347 net.branches_mut().extend(branches);
4348 if t.mag_g != 0.0 || t.mag_b != 0.0 {
4349 net.shunts_mut().push(Shunt {
4350 bus: star_id,
4351 g: t.mag_g * scale,
4352 b: t.mag_b * scale,
4353 in_service: true,
4354 section_count: None,
4355 control: None,
4356 uid: None,
4357 extras: Extras::new(),
4358 });
4359 }
4360 }
4361 net.transformers_3w_mut().clear();
4362 std::borrow::Cow::Owned(net)
4363 }
4364
4365 pub fn validate(&self) -> crate::Result<()> {
4371 self.check_references("network")
4372 }
4373
4374 #[allow(clippy::too_many_lines)]
4379 pub(crate) fn check_references(&self, format: &'static str) -> crate::Result<()> {
4380 let mut ids = std::collections::HashSet::with_capacity(self.buses().len());
4385 for b in self.buses() {
4386 if b.id > BusId::MAX {
4391 return Err(Error::FormatRead {
4392 format,
4393 message: format!("bus id {} is outside the int64 id space", b.id),
4394 });
4395 }
4396 if !ids.insert(b.id) {
4397 return Err(Error::FormatRead {
4398 format,
4399 message: format!("duplicate bus id {}", b.id),
4400 });
4401 }
4402 }
4403 let check = |bus: BusId, what: &str| -> crate::Result<()> {
4404 if ids.contains(&bus) {
4405 Ok(())
4406 } else {
4407 Err(Error::FormatRead {
4408 format,
4409 message: format!("{what} references unknown bus {bus}"),
4410 })
4411 }
4412 };
4413 for (i, br) in self.branches().iter().enumerate() {
4415 for bus in [br.from, br.to] {
4416 if !ids.contains(&bus) {
4417 return Err(Error::FormatRead {
4418 format,
4419 message: format!("branch {i} references unknown bus {bus}"),
4420 });
4421 }
4422 }
4423 if let Some(control) = br.control.as_ref() {
4424 if control.controlled_bus_on_winding_side
4425 && control.controlled_bus.is_none_or(|bus| bus.0 == 0)
4426 {
4427 return Err(Error::FormatRead {
4428 format,
4429 message: format!(
4430 "transformer control on branch {i} marks its controlled bus as lying on the winding side but has no nonzero controlled bus"
4431 ),
4432 });
4433 }
4434 if let Some(bus) = control.controlled_bus {
4435 check(bus, "transformer control")?;
4436 }
4437 }
4438 }
4439 for (i, sw) in self.switches().iter().enumerate() {
4440 for bus in [sw.from, sw.to] {
4441 if !ids.contains(&bus) {
4442 return Err(Error::FormatRead {
4443 format,
4444 message: format!("switch {i} references unknown bus {bus}"),
4445 });
4446 }
4447 }
4448 }
4449 for l in self.loads() {
4450 check(l.bus, "load")?;
4451 }
4452 for s in self.shunts() {
4453 check(s.bus, "shunt")?;
4454 if let Some(bus) = s.control.as_ref().and_then(|c| c.control_bus) {
4455 check(bus, "switched-shunt control")?;
4456 }
4457 }
4458 for svc in self.static_var_compensators() {
4459 check(svc.bus, "static VAR compensator")?;
4460 }
4461 for g in self.generators() {
4462 check(g.bus, "generator")?;
4463 if let Some(bus) = g.regulated_bus {
4464 check(bus, "generator voltage control")?;
4465 }
4466 }
4467 for d in self.hvdc() {
4468 check(d.from, "dcline")?;
4469 check(d.to, "dcline")?;
4470 }
4471 for s in self.storage() {
4472 check(s.bus, "storage")?;
4473 }
4474 for a in self.areas() {
4475 if let Some(slack) = a.slack_bus {
4476 check(slack, "area swing")?;
4477 }
4478 }
4479 for t in self.transformers_3w() {
4480 for w in &t.windings {
4481 check(w.bus, "3-winding transformer")?;
4482 if let Some(control) = w.control.as_ref() {
4483 if control.controlled_bus_on_winding_side
4484 && control.controlled_bus.is_none_or(|bus| bus.0 == 0)
4485 {
4486 return Err(Error::FormatRead {
4487 format,
4488 message: format!(
4489 "3-winding transformer control at bus {} marks its controlled bus as lying on the winding side but has no nonzero controlled bus",
4490 w.bus
4491 ),
4492 });
4493 }
4494 if let Some(bus) = control.controlled_bus {
4495 check(bus, "3-winding transformer control")?;
4496 }
4497 }
4498 }
4499 }
4500 if let Some(detailed) = self.detailed_connectivity().as_ref() {
4501 let nodes = detailed
4502 .connectivity_nodes
4503 .iter()
4504 .map(|node| &node.component)
4505 .collect::<std::collections::HashSet<_>>();
4506 for calculated in &detailed.calculated_buses {
4507 check(calculated.calculated_bus, "calculated bus")?;
4508 if calculated.nodes.is_empty() {
4509 return Err(Error::FormatRead {
4510 format,
4511 message: "calculated bus has no connectivity nodes".into(),
4512 });
4513 }
4514 for node in &calculated.nodes {
4515 if !nodes.contains(node) {
4516 return Err(Error::FormatRead {
4517 format,
4518 message: format!(
4519 "calculated bus references unknown connectivity node `{}`",
4520 node.local_id()
4521 ),
4522 });
4523 }
4524 }
4525 }
4526 let declared_components = detailed
4527 .component_metadata
4528 .iter()
4529 .map(|metadata| &metadata.component)
4530 .chain(
4531 detailed
4532 .substations
4533 .iter()
4534 .map(|substation| &substation.component),
4535 )
4536 .chain(detailed.voltage_levels.iter().map(|level| &level.component))
4537 .chain(
4538 detailed
4539 .dc_converter_units
4540 .iter()
4541 .map(|unit| &unit.component),
4542 )
4543 .collect::<std::collections::HashSet<_>>();
4544 for metadata in &detailed.component_metadata {
4545 if let Some(container) = metadata.equipment_container.as_ref()
4546 && !declared_components.contains(container)
4547 {
4548 return Err(Error::FormatRead {
4549 format,
4550 message: format!(
4551 "component `{}` references unknown equipment container `{container}`",
4552 metadata.component
4553 ),
4554 });
4555 }
4556 }
4557 let mut junctions = std::collections::HashSet::new();
4558 let terminal_equipment: std::collections::HashSet<&ComponentId> = detailed
4559 .terminals
4560 .iter()
4561 .map(|terminal| &terminal.equipment)
4562 .collect();
4563 for junction in &detailed.junctions {
4564 if !junctions.insert(&junction.component) {
4565 return Err(Error::FormatRead {
4566 format,
4567 message: format!("duplicate Junction `{}`", junction.component.local_id()),
4568 });
4569 }
4570 if !declared_components.contains(&junction.component) {
4571 return Err(Error::FormatRead {
4572 format,
4573 message: format!(
4574 "Junction `{}` has no component metadata",
4575 junction.component.local_id()
4576 ),
4577 });
4578 }
4579 if !terminal_equipment.contains(&junction.component) {
4580 return Err(Error::FormatRead {
4581 format,
4582 message: format!(
4583 "Junction `{}` has no terminal",
4584 junction.component.local_id()
4585 ),
4586 });
4587 }
4588 }
4589 }
4590 self.check_detailed_ac_references(format, &ids)?;
4591 self.check_detailed_dc_references(format)?;
4592 self.check_star_expansion_headroom(format)
4593 }
4594
4595 #[allow(clippy::too_many_lines)]
4596 fn check_detailed_ac_references(
4597 &self,
4598 format: &'static str,
4599 balanced_buses: &std::collections::HashSet<BusId>,
4600 ) -> crate::Result<()> {
4601 let Some(detailed) = self.detailed_connectivity().as_ref() else {
4602 return Ok(());
4603 };
4604 let error = |message: String| Error::FormatRead { format, message };
4605
4606 let mut metadata_components = std::collections::HashSet::new();
4607 for metadata in &detailed.component_metadata {
4608 if !metadata_components.insert(metadata.component.clone()) {
4609 return Err(error(format!(
4610 "duplicate component metadata `{}`",
4611 metadata.component
4612 )));
4613 }
4614 }
4615
4616 let mut substations = std::collections::HashSet::new();
4617 for substation in &detailed.substations {
4618 if !substations.insert(substation.component.clone()) {
4619 return Err(error(format!(
4620 "duplicate substation `{}`",
4621 substation.component
4622 )));
4623 }
4624 }
4625
4626 let mut voltage_levels = std::collections::HashSet::new();
4627 let mut voltage_level_topology = std::collections::HashMap::new();
4628 let mut calculated_bus_levels = std::collections::HashMap::new();
4629 for level in &detailed.voltage_levels {
4630 if !voltage_levels.insert(level.component.clone()) {
4631 return Err(error(format!(
4632 "duplicate voltage level `{}`",
4633 level.component
4634 )));
4635 }
4636 voltage_level_topology.insert(level.component.clone(), level.topology_kind);
4637 if let Some(substation) = &level.substation
4638 && !substations.contains(substation)
4639 {
4640 return Err(error(format!(
4641 "voltage level `{}` references unknown substation `{substation}`",
4642 level.component
4643 )));
4644 }
4645 let mut level_buses = std::collections::HashSet::new();
4646 for bus in &level.buses {
4647 if !balanced_buses.contains(bus) {
4648 return Err(error(format!(
4649 "voltage level `{}` references unknown calculated bus {bus}",
4650 level.component
4651 )));
4652 }
4653 if !level_buses.insert(*bus) {
4654 return Err(error(format!(
4655 "voltage level `{}` repeats calculated bus {bus}",
4656 level.component
4657 )));
4658 }
4659 if let Some(first_level) =
4660 calculated_bus_levels.insert(*bus, level.component.clone())
4661 && first_level != level.component
4662 {
4663 return Err(error(format!(
4664 "calculated bus {bus} belongs to both voltage levels `{first_level}` and `{}`",
4665 level.component
4666 )));
4667 }
4668 }
4669 }
4670 let known_container = |container: &ComponentId| {
4671 voltage_levels.contains(container) || metadata_components.contains(container)
4672 };
4673
4674 let mut configured_buses = std::collections::HashSet::new();
4675 let mut configured_bus_levels = std::collections::HashMap::new();
4676 for bus in &detailed.bus_breaker_buses {
4677 if !configured_buses.insert(bus.component.clone()) {
4678 return Err(error(format!(
4679 "duplicate bus breaker bus `{}`",
4680 bus.component
4681 )));
4682 }
4683 if !known_container(&bus.voltage_level) {
4684 let detail = if bus.calculated_bus.is_none() {
4685 " has no calculated bus and"
4686 } else {
4687 ""
4688 };
4689 return Err(error(format!(
4690 "TopologicalNode `{}`{detail} references unknown voltage level or connectivity container `{}`",
4691 bus.component, bus.voltage_level
4692 )));
4693 }
4694 configured_bus_levels.insert(bus.component.clone(), bus.voltage_level.clone());
4695 if let Some(calculated) = bus.calculated_bus
4696 && !balanced_buses.contains(&calculated)
4697 {
4698 return Err(error(format!(
4699 "bus breaker bus `{}` references unknown calculated bus {calculated}",
4700 bus.component
4701 )));
4702 }
4703 if voltage_levels.contains(&bus.voltage_level)
4704 && let Some(calculated) = bus.calculated_bus
4705 && let Some(first_level) =
4706 calculated_bus_levels.insert(calculated, bus.voltage_level.clone())
4707 && first_level != bus.voltage_level
4708 {
4709 return Err(error(format!(
4710 "calculated bus {calculated} belongs to both connectivity containers `{first_level}` and `{}`",
4711 bus.voltage_level
4712 )));
4713 }
4714 }
4715
4716 let mut connectivity_nodes = std::collections::HashSet::new();
4717 let mut connectivity_node_levels = std::collections::HashMap::new();
4718 let mut connectivity_node_calculated_buses = std::collections::HashMap::new();
4719 for node in &detailed.connectivity_nodes {
4720 if !connectivity_nodes.insert(node.component.clone()) {
4721 return Err(error(format!(
4722 "duplicate connectivity node `{}`",
4723 node.component
4724 )));
4725 }
4726 if !known_container(&node.voltage_level) {
4727 let detail = if node.calculated_bus.is_none() {
4728 " has no calculated bus and"
4729 } else {
4730 ""
4731 };
4732 return Err(error(format!(
4733 "ConnectivityNode `{}`{detail} references unknown voltage level or connectivity container `{}`",
4734 node.component, node.voltage_level
4735 )));
4736 }
4737 connectivity_node_levels.insert(node.component.clone(), node.voltage_level.clone());
4738 connectivity_node_calculated_buses.insert(node.component.clone(), node.calculated_bus);
4739 if let Some(calculated) = node.calculated_bus
4740 && !balanced_buses.contains(&calculated)
4741 {
4742 return Err(error(format!(
4743 "connectivity node `{}` references unknown calculated bus {calculated}",
4744 node.component
4745 )));
4746 }
4747 if voltage_levels.contains(&node.voltage_level)
4748 && let Some(calculated) = node.calculated_bus
4749 && let Some(first_level) =
4750 calculated_bus_levels.insert(calculated, node.voltage_level.clone())
4751 && first_level != node.voltage_level
4752 {
4753 return Err(error(format!(
4754 "calculated bus {calculated} belongs to both connectivity containers `{first_level}` and `{}`",
4755 node.voltage_level
4756 )));
4757 }
4758 }
4759
4760 let mut calculated_buses = std::collections::HashSet::new();
4761 let mut calculated_bus_nodes = std::collections::HashMap::new();
4762 for calculated in &detailed.calculated_buses {
4763 if !calculated_buses.insert(calculated.calculated_bus) {
4764 return Err(error(format!(
4765 "duplicate calculated bus {} in detailed connectivity",
4766 calculated.calculated_bus
4767 )));
4768 }
4769 if !balanced_buses.contains(&calculated.calculated_bus) {
4770 return Err(error(format!(
4771 "detailed connectivity references unknown calculated bus {}",
4772 calculated.calculated_bus
4773 )));
4774 }
4775 if !known_container(&calculated.voltage_level) {
4776 return Err(error(format!(
4777 "calculated bus {} references unknown voltage level or connectivity container `{}`",
4778 calculated.calculated_bus, calculated.voltage_level
4779 )));
4780 }
4781 if voltage_levels.contains(&calculated.voltage_level)
4782 && let Some(first_level) = calculated_bus_levels
4783 .insert(calculated.calculated_bus, calculated.voltage_level.clone())
4784 && first_level != calculated.voltage_level
4785 {
4786 return Err(error(format!(
4787 "calculated bus {} belongs to both connectivity containers `{first_level}` and `{}`",
4788 calculated.calculated_bus, calculated.voltage_level
4789 )));
4790 }
4791 for node in &calculated.nodes {
4792 let Some(node_level) = connectivity_node_levels.get(node) else {
4793 return Err(error(format!(
4794 "calculated bus {} references unknown connectivity node `{node}`",
4795 calculated.calculated_bus
4796 )));
4797 };
4798 if node_level != &calculated.voltage_level {
4799 return Err(error(format!(
4800 "calculated bus {} in `{}` contains connectivity node `{node}` from `{node_level}`",
4801 calculated.calculated_bus, calculated.voltage_level
4802 )));
4803 }
4804 if let Some(Some(node_bus)) = connectivity_node_calculated_buses.get(node)
4805 && *node_bus != calculated.calculated_bus
4806 {
4807 return Err(error(format!(
4808 "connectivity node `{node}` names calculated bus {node_bus}, but calculated bus {} also claims that node",
4809 calculated.calculated_bus
4810 )));
4811 }
4812 if let Some(first_bus) =
4813 calculated_bus_nodes.insert(node.clone(), calculated.calculated_bus)
4814 {
4815 return Err(error(format!(
4816 "connectivity node `{node}` is listed by calculated buses {first_bus} and {}",
4817 calculated.calculated_bus
4818 )));
4819 }
4820 }
4821 }
4822
4823 let mut busbars = std::collections::HashSet::new();
4824 for busbar in &detailed.busbar_sections {
4825 if !busbars.insert(busbar.component.clone()) {
4826 return Err(error(format!(
4827 "duplicate busbar section `{}`",
4828 busbar.component
4829 )));
4830 }
4831 if !known_container(&busbar.voltage_level) {
4832 return Err(error(format!(
4833 "busbar section `{}` references unknown voltage level or connectivity container `{}`",
4834 busbar.component, busbar.voltage_level
4835 )));
4836 }
4837 let Some(node_level) = connectivity_node_levels.get(&busbar.node) else {
4838 return Err(error(format!(
4839 "busbar section `{}` references unknown connectivity node `{}`",
4840 busbar.component, busbar.node
4841 )));
4842 };
4843 if voltage_levels.contains(&busbar.voltage_level)
4844 && voltage_levels.contains(node_level)
4845 && node_level != &busbar.voltage_level
4846 {
4847 return Err(error(format!(
4848 "busbar section `{}` in `{}` references connectivity node `{}` from `{node_level}`",
4849 busbar.component, busbar.voltage_level, busbar.node
4850 )));
4851 }
4852 }
4853
4854 let mut terminal_components = std::collections::HashSet::new();
4855 let mut terminal_keys = std::collections::HashSet::new();
4856 for terminal in &detailed.terminals {
4857 if terminal.terminal == 0 {
4858 return Err(error(format!(
4859 "equipment `{}` has terminal 0",
4860 terminal.equipment
4861 )));
4862 }
4863 if !terminal_keys.insert((terminal.equipment.clone(), terminal.terminal)) {
4864 return Err(error(format!(
4865 "equipment `{}` repeats terminal {}",
4866 terminal.equipment, terminal.terminal
4867 )));
4868 }
4869 if let Some(component) = &terminal.component
4870 && !terminal_components.insert(component.clone())
4871 {
4872 return Err(error(format!("duplicate terminal identity `{component}`")));
4873 }
4874 if !known_container(&terminal.voltage_level) {
4875 return Err(error(format!(
4876 "equipment `{}` terminal {} references unknown voltage level or connectivity container `{}`",
4877 terminal.equipment, terminal.terminal, terminal.voltage_level
4878 )));
4879 }
4880 for bus in [terminal.bus.as_ref(), terminal.connectable_bus.as_ref()]
4881 .into_iter()
4882 .flatten()
4883 {
4884 let Some(bus_level) = configured_bus_levels.get(bus) else {
4885 return Err(error(format!(
4886 "equipment `{}` terminal {} references unknown bus breaker bus `{bus}`",
4887 terminal.equipment, terminal.terminal
4888 )));
4889 };
4890 if bus_level != &terminal.voltage_level {
4891 return Err(error(format!(
4892 "equipment `{}` terminal {} in `{}` references bus `{bus}` from `{bus_level}`",
4893 terminal.equipment, terminal.terminal, terminal.voltage_level
4894 )));
4895 }
4896 }
4897 if let Some(node) = &terminal.node {
4898 let Some(node_level) = connectivity_node_levels.get(node) else {
4899 return Err(error(format!(
4900 "equipment `{}` terminal {} references unknown connectivity node `{node}`",
4901 terminal.equipment, terminal.terminal
4902 )));
4903 };
4904 if node_level != &terminal.voltage_level {
4905 return Err(error(format!(
4906 "equipment `{}` terminal {} in `{}` references connectivity node `{node}` from `{node_level}`",
4907 terminal.equipment, terminal.terminal, terminal.voltage_level
4908 )));
4909 }
4910 }
4911 }
4912
4913 let mut switches = std::collections::HashSet::new();
4914 for switch in &detailed.switches {
4915 if !switches.insert(switch.component.clone()) {
4916 return Err(error(format!(
4917 "duplicate topology switch `{}`",
4918 switch.component
4919 )));
4920 }
4921 if !voltage_levels.contains(&switch.voltage_level)
4922 && !metadata_components.contains(&switch.voltage_level)
4923 {
4924 return Err(error(format!(
4925 "topology switch `{}` references unknown voltage level or connectivity container `{}`",
4926 switch.component, switch.voltage_level
4927 )));
4928 }
4929 for endpoint in [&switch.endpoint1, &switch.endpoint2] {
4930 match endpoint {
4931 TopologyEndpoint::Bus(bus) => {
4932 if !configured_bus_levels.contains_key(bus) {
4933 return Err(error(format!(
4934 "topology switch `{}` references unknown bus breaker bus `{bus}`",
4935 switch.component
4936 )));
4937 }
4938 if voltage_level_topology.get(&switch.voltage_level)
4939 == Some(&TopologyKind::NodeBreaker)
4940 {
4941 return Err(error(format!(
4942 "topology switch `{}` uses a bus endpoint in node breaker voltage level `{}`",
4943 switch.component, switch.voltage_level
4944 )));
4945 }
4946 }
4947 TopologyEndpoint::Node(node) => {
4948 if !connectivity_node_levels.contains_key(node) {
4949 return Err(error(format!(
4950 "topology switch `{}` references unknown connectivity node `{node}`",
4951 switch.component
4952 )));
4953 }
4954 if voltage_level_topology.get(&switch.voltage_level)
4955 == Some(&TopologyKind::BusBreaker)
4956 {
4957 return Err(error(format!(
4958 "topology switch `{}` uses a node endpoint in bus breaker voltage level `{}`",
4959 switch.component, switch.voltage_level
4960 )));
4961 }
4962 }
4963 }
4964 }
4965 }
4966
4967 for connection in &detailed.internal_connections {
4968 if !voltage_levels.contains(&connection.voltage_level) {
4969 return Err(error(format!(
4970 "internal connection references unknown voltage level `{}`",
4971 connection.voltage_level
4972 )));
4973 }
4974 for node in [&connection.node1, &connection.node2] {
4975 let Some(node_level) = connectivity_node_levels.get(node) else {
4976 return Err(error(format!(
4977 "internal connection references unknown connectivity node `{node}`"
4978 )));
4979 };
4980 if node_level != &connection.voltage_level {
4981 return Err(error(format!(
4982 "internal connection in `{}` references connectivity node `{node}` from `{node_level}`",
4983 connection.voltage_level
4984 )));
4985 }
4986 }
4987 }
4988
4989 let mut tap_components = std::collections::HashSet::new();
4990 let mut tap_keys = std::collections::HashSet::new();
4991 for tap in &detailed.tap_changers {
4992 if tap.winding == 0 {
4993 return Err(error(format!(
4994 "transformer `{}` has a tap changer on winding 0",
4995 tap.transformer
4996 )));
4997 }
4998 let kind = match tap.kind {
4999 TapChangerKind::Ratio => 0_u8,
5000 TapChangerKind::Phase => 1_u8,
5001 };
5002 if !tap_keys.insert((tap.transformer.clone(), tap.winding, kind)) {
5003 return Err(error(format!(
5004 "transformer `{}` repeats its {:?} tap changer on winding {}",
5005 tap.transformer, tap.kind, tap.winding
5006 )));
5007 }
5008 if let Some(component) = &tap.component
5009 && !tap_components.insert(component.clone())
5010 {
5011 return Err(error(format!(
5012 "duplicate tap changer identity `{component}`"
5013 )));
5014 }
5015 let mut positions = std::collections::HashSet::new();
5016 for step in &tap.steps {
5017 if !positions.insert(step.position) {
5018 return Err(error(format!(
5019 "transformer `{}` winding {} repeats tap position {}",
5020 tap.transformer, tap.winding, step.position
5021 )));
5022 }
5023 }
5024 if let Some(reference) = &tap.regulation_terminal
5025 && !terminal_keys.contains(&(reference.equipment.clone(), reference.terminal))
5026 {
5027 return Err(error(format!(
5028 "transformer `{}` winding {} regulates unknown equipment terminal `{}` terminal {}",
5029 tap.transformer, tap.winding, reference.equipment, reference.terminal
5030 )));
5031 }
5032 }
5033
5034 Ok(())
5035 }
5036
5037 #[allow(clippy::too_many_lines)]
5038 fn check_detailed_dc_references(&self, format: &'static str) -> crate::Result<()> {
5039 let Some(detailed) = self.detailed_connectivity().as_ref() else {
5040 return Ok(());
5041 };
5042
5043 let substations = detailed
5044 .substations
5045 .iter()
5046 .map(|substation| &substation.component)
5047 .collect::<std::collections::HashSet<_>>();
5048 let mut converter_units =
5049 std::collections::HashSet::with_capacity(detailed.dc_converter_units.len());
5050 for unit in &detailed.dc_converter_units {
5051 if !converter_units.insert(&unit.component) {
5052 return Err(Error::FormatRead {
5053 format,
5054 message: format!("duplicate DCConverterUnit `{}`", unit.component.local_id()),
5055 });
5056 }
5057 if let Some(substation) = unit.substation.as_ref()
5058 && !substations.contains(substation)
5059 {
5060 return Err(Error::FormatRead {
5061 format,
5062 message: format!(
5063 "DCConverterUnit `{}` references unknown Substation `{}`",
5064 unit.component.local_id(),
5065 substation.local_id()
5066 ),
5067 });
5068 }
5069 }
5070 let known_components = detailed
5071 .component_metadata
5072 .iter()
5073 .map(|metadata| &metadata.component)
5074 .chain(converter_units.iter().copied())
5075 .collect::<std::collections::HashSet<_>>();
5076
5077 let mut nodes = std::collections::HashSet::with_capacity(detailed.dc_nodes.len());
5078 for node in &detailed.dc_nodes {
5079 if !nodes.insert(&node.component) {
5080 return Err(Error::FormatRead {
5081 format,
5082 message: format!("duplicate DCNode `{}`", node.component.local_id()),
5083 });
5084 }
5085 }
5086 let mut topological_nodes =
5087 std::collections::HashSet::with_capacity(detailed.dc_topological_nodes.len());
5088 for node in &detailed.dc_topological_nodes {
5089 if !topological_nodes.insert(&node.component) {
5090 return Err(Error::FormatRead {
5091 format,
5092 message: format!(
5093 "duplicate DCTopologicalNode `{}`",
5094 node.component.local_id()
5095 ),
5096 });
5097 }
5098 }
5099
5100 let check_unit = |class: &str,
5101 component: &ComponentId,
5102 unit: Option<&ComponentId>|
5103 -> crate::Result<()> {
5104 if let Some(unit) = unit
5105 && !converter_units.contains(unit)
5106 {
5107 return Err(Error::FormatRead {
5108 format,
5109 message: format!(
5110 "{class} `{}` references unknown DCConverterUnit `{}`",
5111 component.local_id(),
5112 unit.local_id()
5113 ),
5114 });
5115 }
5116 Ok(())
5117 };
5118 let check_container = |class: &str,
5119 component: &ComponentId,
5120 container: Option<&ComponentId>|
5121 -> crate::Result<()> {
5122 if let Some(container) = container
5123 && !known_components.contains(container)
5124 {
5125 return Err(Error::FormatRead {
5126 format,
5127 message: format!(
5128 "{class} `{}` references unknown equipment container `{}`",
5129 component.local_id(),
5130 container.local_id()
5131 ),
5132 });
5133 }
5134 Ok(())
5135 };
5136 for node in &detailed.dc_topological_nodes {
5137 check_unit(
5138 "DCTopologicalNode",
5139 &node.component,
5140 node.dc_converter_unit.as_ref(),
5141 )?;
5142 }
5143 for node in &detailed.dc_nodes {
5144 check_unit("DCNode", &node.component, node.dc_converter_unit.as_ref())?;
5145 if let Some(topological_node) = node.dc_topological_node.as_ref()
5146 && !topological_nodes.contains(topological_node)
5147 {
5148 return Err(Error::FormatRead {
5149 format,
5150 message: format!(
5151 "DCNode `{}` references unknown DCTopologicalNode `{}`",
5152 node.component.local_id(),
5153 topological_node.local_id()
5154 ),
5155 });
5156 }
5157 }
5158
5159 let mut terminal_ids = std::collections::HashSet::<ComponentId>::new();
5160 let mut check_terminal = |class: &str,
5161 equipment: &ComponentId,
5162 terminal: &DcTerminal|
5163 -> crate::Result<()> {
5164 if let Some(terminal_id) = terminal.component.as_ref()
5165 && !terminal_ids.insert(terminal_id.clone())
5166 {
5167 return Err(Error::FormatRead {
5168 format,
5169 message: format!("duplicate DCTerminal `{}`", terminal_id.local_id()),
5170 });
5171 }
5172 if terminal.dc_node.is_none() && terminal.dc_topological_node.is_none() {
5173 return Err(Error::FormatRead {
5174 format,
5175 message: format!(
5176 "DCTerminal on {class} `{}` references neither a DCNode nor a DCTopologicalNode",
5177 equipment.local_id()
5178 ),
5179 });
5180 }
5181 if let Some(node) = terminal.dc_node.as_ref()
5182 && !nodes.contains(node)
5183 {
5184 return Err(Error::FormatRead {
5185 format,
5186 message: format!(
5187 "DCTerminal on {class} `{}` references unknown DCNode `{}`",
5188 equipment.local_id(),
5189 node.local_id()
5190 ),
5191 });
5192 }
5193 if let Some(node) = terminal.dc_topological_node.as_ref()
5194 && !topological_nodes.contains(node)
5195 {
5196 return Err(Error::FormatRead {
5197 format,
5198 message: format!(
5199 "DCTerminal on {class} `{}` references unknown DCTopologicalNode `{}`",
5200 equipment.local_id(),
5201 node.local_id()
5202 ),
5203 });
5204 }
5205 Ok(())
5206 };
5207
5208 for ground in &detailed.dc_grounds {
5209 check_container(
5210 "DCGround",
5211 &ground.component,
5212 ground.equipment_container.as_ref(),
5213 )?;
5214 check_terminal("DCGround", &ground.component, &ground.dc_terminal)?;
5215 }
5216 for busbar in &detailed.dc_busbars {
5217 check_container(
5218 "DCBusbar",
5219 &busbar.component,
5220 busbar.equipment_container.as_ref(),
5221 )?;
5222 check_terminal("DCBusbar", &busbar.component, &busbar.dc_terminal)?;
5223 }
5224 for line in &detailed.dc_lines {
5225 check_container(
5226 "DCLineSegment",
5227 &line.component,
5228 line.equipment_container.as_ref(),
5229 )?;
5230 check_terminal("DCLineSegment", &line.component, &line.dc_terminal1)?;
5231 check_terminal("DCLineSegment", &line.component, &line.dc_terminal2)?;
5232 }
5233 for device in &detailed.dc_series_devices {
5234 check_container(
5235 "DCSeriesDevice",
5236 &device.component,
5237 device.equipment_container.as_ref(),
5238 )?;
5239 check_terminal("DCSeriesDevice", &device.component, &device.dc_terminal1)?;
5240 check_terminal("DCSeriesDevice", &device.component, &device.dc_terminal2)?;
5241 }
5242 for switch in &detailed.dc_switches {
5243 check_container(
5244 "DCSwitch",
5245 &switch.component,
5246 switch.equipment_container.as_ref(),
5247 )?;
5248 check_terminal("DCSwitch", &switch.component, &switch.dc_terminal1)?;
5249 check_terminal("DCSwitch", &switch.component, &switch.dc_terminal2)?;
5250 }
5251 for converter in &detailed.voltage_source_converters {
5252 check_unit(
5253 "VsConverter",
5254 &converter.component,
5255 converter.dc_converter_unit.as_ref(),
5256 )?;
5257 check_terminal("VsConverter", &converter.component, &converter.dc_terminal1)?;
5258 check_terminal("VsConverter", &converter.component, &converter.dc_terminal2)?;
5259 }
5260 for converter in &detailed.line_commutated_converters {
5261 check_unit(
5262 "CsConverter",
5263 &converter.component,
5264 converter.dc_converter_unit.as_ref(),
5265 )?;
5266 check_terminal("CsConverter", &converter.component, &converter.dc_terminal1)?;
5267 check_terminal("CsConverter", &converter.component, &converter.dc_terminal2)?;
5268 }
5269
5270 let ac_terminals = detailed
5271 .terminals
5272 .iter()
5273 .map(|terminal| (&terminal.equipment, terminal.terminal))
5274 .collect::<std::collections::HashSet<_>>();
5275 let check_regulating_terminal =
5276 |what: &str, reference: Option<&TerminalReference>| -> crate::Result<()> {
5277 if let Some(reference) = reference
5278 && !ac_terminals.contains(&(&reference.equipment, reference.terminal))
5279 {
5280 return Err(Error::FormatRead {
5281 format,
5282 message: format!(
5283 "{what} references undeclared regulating Terminal `{}` number {}",
5284 reference.equipment.local_id(),
5285 reference.terminal
5286 ),
5287 });
5288 }
5289 Ok(())
5290 };
5291 for (index, generator) in self.generators().iter().enumerate() {
5292 check_regulating_terminal(
5293 &format!("generator {index}"),
5294 generator.regulating_terminal.as_ref(),
5295 )?;
5296 }
5297 for (index, branch) in self.branches().iter().enumerate() {
5298 if let Some(control) = &branch.control {
5299 check_regulating_terminal(
5300 &format!("transformer branch {index}"),
5301 control.regulating_terminal.as_ref(),
5302 )?;
5303 }
5304 }
5305 for (index, shunt) in self.shunts().iter().enumerate() {
5306 if let Some(control) = &shunt.control {
5307 check_regulating_terminal(
5308 &format!("switched shunt {index}"),
5309 control.regulating_terminal.as_ref(),
5310 )?;
5311 }
5312 }
5313 for (transformer_index, transformer) in self.transformers_3w().iter().enumerate() {
5314 for (winding_index, winding) in transformer.windings.iter().enumerate() {
5315 if let Some(control) = &winding.control {
5316 check_regulating_terminal(
5317 &format!(
5318 "three winding transformer {transformer_index} winding {winding_index}"
5319 ),
5320 control.regulating_terminal.as_ref(),
5321 )?;
5322 }
5323 }
5324 }
5325 let check_pcc_terminal = |class: &str,
5326 component: &ComponentId,
5327 pcc: Option<&TerminalReference>|
5328 -> crate::Result<()> {
5329 if let Some(pcc) = pcc
5330 && !ac_terminals.contains(&(&pcc.equipment, pcc.terminal))
5331 {
5332 return Err(Error::FormatRead {
5333 format,
5334 message: format!(
5335 "{class} `{}` references undeclared PCC Terminal `{}` number {}",
5336 component.local_id(),
5337 pcc.equipment.local_id(),
5338 pcc.terminal
5339 ),
5340 });
5341 }
5342 Ok(())
5343 };
5344 for converter in &detailed.voltage_source_converters {
5345 check_pcc_terminal(
5346 "VoltageSourceConverter",
5347 &converter.component,
5348 converter.pcc_terminal.as_ref(),
5349 )?;
5350 }
5351 for converter in &detailed.line_commutated_converters {
5352 check_pcc_terminal(
5353 "LineCommutatedConverter",
5354 &converter.component,
5355 converter.pcc_terminal.as_ref(),
5356 )?;
5357 }
5358 Ok(())
5359 }
5360
5361 fn check_star_expansion_headroom(&self, format: &'static str) -> crate::Result<()> {
5369 if self.transformers_3w().is_empty() {
5370 return Ok(());
5371 }
5372 let Some(max_id) = self.buses().iter().map(|b| b.id.0).max() else {
5373 return Ok(());
5374 };
5375 let needed = self
5376 .transformers_3w()
5377 .iter()
5378 .filter(|t| t.in_service)
5379 .count()
5380 .max(1);
5381 if max_id
5382 .checked_add(needed)
5383 .is_none_or(|top| top > BusId::MAX.0)
5384 {
5385 return Err(Error::FormatRead {
5386 format,
5387 message: format!(
5388 "bus id {max_id} leaves no room to allocate synthetic star bus ids \
5389 for 3-winding transformers"
5390 ),
5391 });
5392 }
5393 Ok(())
5394 }
5395}
5396
5397#[cfg(test)]
5401pub(crate) fn serde_round_trip(network: &BalancedNetwork) -> BalancedNetwork {
5402 serde_json::from_str(&serde_json::to_string(network).unwrap()).unwrap()
5403}
5404
5405#[cfg(test)]
5406mod tests {
5407 use super::*;
5408
5409 fn close(actual: f64, expected: f64) {
5410 assert!((actual - expected).abs() < 1e-12, "{actual} != {expected}");
5411 }
5412
5413 fn component(component_type: &str, local_id: &str) -> ComponentId {
5414 ComponentId::new(component_type, local_id).unwrap()
5415 }
5416
5417 fn reference_test_dc_terminal(local_id: &str, node: &ComponentId) -> DcTerminal {
5418 DcTerminal {
5419 component: Some(component("dc_terminal", local_id)),
5420 sequence_number: None,
5421 dc_node: Some(node.clone()),
5422 dc_topological_node: None,
5423 polarity: None,
5424 connected: None,
5425 active_power_mw: None,
5426 current_a: None,
5427 }
5428 }
5429
5430 #[allow(clippy::too_many_lines)]
5431 fn detailed_dc_reference_test_network() -> BalancedNetwork {
5432 let substation = component("substation", "S");
5433 let converter_unit = component("dc_converter_unit", "U");
5434 let topological_node = component("dc_topological_node", "T");
5435 let node = component("dc_node", "N");
5436 let pcc_equipment = component("ac_equipment", "PCC");
5437 let voltage_level = component("voltage_level", "VL");
5438
5439 let voltage_source_converter: VoltageSourceConverter =
5440 serde_json::from_value(serde_json::json!({
5441 "component": component("voltage_source_converter", "VSC"),
5442 "dc_converter_unit": converter_unit.clone(),
5443 "dc_terminal1": reference_test_dc_terminal("VSC-1", &node),
5444 "dc_terminal2": reference_test_dc_terminal("VSC-2", &node),
5445 "pcc_terminal": {
5446 "equipment": pcc_equipment.clone(),
5447 "terminal": 1
5448 }
5449 }))
5450 .unwrap();
5451 let line_commutated_converter: LineCommutatedConverter =
5452 serde_json::from_value(serde_json::json!({
5453 "component": component("line_commutated_converter", "LCC"),
5454 "dc_converter_unit": converter_unit.clone(),
5455 "dc_terminal1": reference_test_dc_terminal("LCC-1", &node),
5456 "dc_terminal2": reference_test_dc_terminal("LCC-2", &node),
5457 "pcc_terminal": {
5458 "equipment": pcc_equipment.clone(),
5459 "terminal": 1
5460 }
5461 }))
5462 .unwrap();
5463
5464 let detailed = DetailedConnectivity {
5465 substations: vec![Substation {
5466 component: substation.clone(),
5467 country: None,
5468 operator: None,
5469 geographical_tags: Vec::new(),
5470 }],
5471 voltage_levels: vec![VoltageLevel {
5472 component: voltage_level.clone(),
5473 substation: Some(substation.clone()),
5474 nominal_kv: 230.0,
5475 low_voltage_limit_kv: None,
5476 high_voltage_limit_kv: None,
5477 topology_kind: TopologyKind::BusBreaker,
5478 buses: Vec::new(),
5479 }],
5480 terminals: vec![Terminal {
5481 component: None,
5482 equipment: pcc_equipment,
5483 terminal: 1,
5484 voltage_level,
5485 bus: None,
5486 connectable_bus: None,
5487 node: None,
5488 connected: true,
5489 active_power_mw: None,
5490 reactive_power_mvar: None,
5491 }],
5492 dc_converter_units: vec![DcConverterUnit {
5493 component: converter_unit.clone(),
5494 substation: Some(substation),
5495 operation_mode: DcConverterOperatingMode::Bipolar,
5496 }],
5497 dc_topological_nodes: vec![DcTopologicalNode {
5498 component: topological_node.clone(),
5499 dc_converter_unit: Some(converter_unit.clone()),
5500 }],
5501 dc_nodes: vec![DcNode {
5502 component: node.clone(),
5503 nominal_voltage_kv: None,
5504 dc_converter_unit: Some(converter_unit.clone()),
5505 dc_topological_node: Some(topological_node),
5506 voltage_kv: None,
5507 }],
5508 dc_grounds: vec![DcGround {
5509 component: component("dc_ground", "G"),
5510 equipment_container: Some(converter_unit.clone()),
5511 dc_terminal: reference_test_dc_terminal("G-1", &node),
5512 rated_dc_voltage_kv: None,
5513 resistance_ohm: None,
5514 inductance_h: None,
5515 }],
5516 dc_lines: vec![DcLine {
5517 component: component("dc_line", "L"),
5518 equipment_container: Some(converter_unit.clone()),
5519 dc_terminal1: reference_test_dc_terminal("L-1", &node),
5520 dc_terminal2: reference_test_dc_terminal("L-2", &node),
5521 rated_dc_voltage_kv: None,
5522 resistance_ohm: None,
5523 inductance_h: None,
5524 capacitance_f: None,
5525 length_km: None,
5526 }],
5527 dc_switches: vec![DcSwitch {
5528 component: component("dc_switch", "SW"),
5529 equipment_container: Some(converter_unit),
5530 dc_terminal1: reference_test_dc_terminal("SW-1", &node),
5531 dc_terminal2: reference_test_dc_terminal("SW-2", &node),
5532 kind: DcSwitchKind::Switch,
5533 rated_dc_voltage_kv: None,
5534 open: None,
5535 resistance_ohm: None,
5536 }],
5537 voltage_source_converters: vec![voltage_source_converter],
5538 line_commutated_converters: vec![line_commutated_converter],
5539 ..DetailedConnectivity::default()
5540 };
5541 let mut network = BalancedNetwork::new("dc-reference-test", 100.0);
5542 *network.detailed_connectivity_mut() = Some(std::sync::Arc::new(detailed));
5543 network
5544 }
5545
5546 fn reference_test_detailed_mut(network: &mut BalancedNetwork) -> &mut DetailedConnectivity {
5547 std::sync::Arc::make_mut(
5548 network
5549 .detailed_connectivity_mut()
5550 .as_mut()
5551 .expect("reference test has detailed connectivity"),
5552 )
5553 }
5554
5555 fn assert_invalid(network: &BalancedNetwork, expected: &str) {
5556 let error = network.validate().unwrap_err().to_string();
5557 assert!(
5558 error.contains(expected),
5559 "expected `{expected}` in `{error}`"
5560 );
5561 }
5562
5563 fn assert_duplicate_dc_converter_unit_rejected(valid: &BalancedNetwork) {
5564 let mut network = valid.clone();
5565 let unit = reference_test_detailed_mut(&mut network).dc_converter_units[0].clone();
5566 reference_test_detailed_mut(&mut network)
5567 .dc_converter_units
5568 .push(unit);
5569 assert_invalid(&network, "duplicate DCConverterUnit");
5570 }
5571
5572 #[test]
5573 fn detailed_dc_references_are_complete_and_checked() {
5574 let valid = detailed_dc_reference_test_network();
5575 valid.validate().unwrap();
5576
5577 assert_duplicate_dc_converter_unit_rejected(&valid);
5578
5579 let mut missing_substation = valid.clone();
5580 reference_test_detailed_mut(&mut missing_substation).dc_converter_units[0].substation =
5581 Some(component("substation", "missing"));
5582 assert_invalid(&missing_substation, "references unknown Substation");
5583
5584 let mut duplicate_topological_node = valid.clone();
5585 let node = reference_test_detailed_mut(&mut duplicate_topological_node)
5586 .dc_topological_nodes[0]
5587 .clone();
5588 reference_test_detailed_mut(&mut duplicate_topological_node)
5589 .dc_topological_nodes
5590 .push(node);
5591 assert_invalid(&duplicate_topological_node, "duplicate DCTopologicalNode");
5592
5593 let mut missing_topological_unit = valid.clone();
5594 reference_test_detailed_mut(&mut missing_topological_unit).dc_topological_nodes[0]
5595 .dc_converter_unit = Some(component("dc_converter_unit", "missing"));
5596 assert_invalid(
5597 &missing_topological_unit,
5598 "DCTopologicalNode `T` references unknown DCConverterUnit",
5599 );
5600
5601 let mut missing_node_unit = valid.clone();
5602 reference_test_detailed_mut(&mut missing_node_unit).dc_nodes[0].dc_converter_unit =
5603 Some(component("dc_converter_unit", "missing"));
5604 assert_invalid(
5605 &missing_node_unit,
5606 "DCNode `N` references unknown DCConverterUnit",
5607 );
5608
5609 let mut missing_node_topology = valid.clone();
5610 reference_test_detailed_mut(&mut missing_node_topology).dc_nodes[0].dc_topological_node =
5611 Some(component("dc_topological_node", "missing"));
5612 assert_invalid(
5613 &missing_node_topology,
5614 "DCNode `N` references unknown DCTopologicalNode",
5615 );
5616
5617 let mut missing_container = valid.clone();
5618 reference_test_detailed_mut(&mut missing_container).dc_lines[0].equipment_container =
5619 Some(component("dc_converter_unit", "missing"));
5620 assert_invalid(
5621 &missing_container,
5622 "DCLineSegment `L` references unknown equipment container",
5623 );
5624
5625 let mut missing_converter_unit = valid.clone();
5626 reference_test_detailed_mut(&mut missing_converter_unit).voltage_source_converters[0]
5627 .dc_converter_unit = Some(component("dc_converter_unit", "missing"));
5628 assert_invalid(
5629 &missing_converter_unit,
5630 "VsConverter `VSC` references unknown DCConverterUnit",
5631 );
5632
5633 let mut terminal_without_node = valid.clone();
5634 let terminal =
5635 &mut reference_test_detailed_mut(&mut terminal_without_node).dc_grounds[0].dc_terminal;
5636 terminal.dc_node = None;
5637 terminal.dc_topological_node = None;
5638 assert_invalid(
5639 &terminal_without_node,
5640 "references neither a DCNode nor a DCTopologicalNode",
5641 );
5642
5643 let mut missing_physical_node = valid.clone();
5644 reference_test_detailed_mut(&mut missing_physical_node).dc_grounds[0]
5645 .dc_terminal
5646 .dc_node = Some(component("dc_node", "missing"));
5647 assert_invalid(&missing_physical_node, "references unknown DCNode");
5648
5649 let mut missing_terminal_topology = valid.clone();
5650 let terminal = &mut reference_test_detailed_mut(&mut missing_terminal_topology).dc_grounds
5651 [0]
5652 .dc_terminal;
5653 terminal.dc_node = None;
5654 terminal.dc_topological_node = Some(component("dc_topological_node", "missing"));
5655 assert_invalid(
5656 &missing_terminal_topology,
5657 "references unknown DCTopologicalNode",
5658 );
5659
5660 let mut duplicate_terminal = valid.clone();
5661 let terminal_id = reference_test_detailed_mut(&mut duplicate_terminal).dc_grounds[0]
5662 .dc_terminal
5663 .component
5664 .clone();
5665 reference_test_detailed_mut(&mut duplicate_terminal).dc_lines[0]
5666 .dc_terminal1
5667 .component = terminal_id;
5668 assert_invalid(&duplicate_terminal, "duplicate DCTerminal");
5669
5670 let mut missing_pcc_terminal = valid;
5671 reference_test_detailed_mut(&mut missing_pcc_terminal).voltage_source_converters[0]
5672 .pcc_terminal
5673 .as_mut()
5674 .unwrap()
5675 .terminal = 2;
5676 assert_invalid(&missing_pcc_terminal, "references undeclared PCC Terminal");
5677 }
5678
5679 #[test]
5680 #[allow(clippy::too_many_lines)] fn detailed_connectivity_dc_equipment_round_trips_and_defaults() {
5682 let empty: DetailedConnectivity = serde_json::from_str("{}").unwrap();
5683 assert_eq!(empty, DetailedConnectivity::default());
5684
5685 let node1 = component("dc_node", "dc-1");
5686 let node2 = component("dc_node", "dc-2");
5687 let terminal = |dc_node: ComponentId, connected: bool, power| DcTerminal {
5688 component: None,
5689 sequence_number: None,
5690 dc_node: Some(dc_node),
5691 dc_topological_node: None,
5692 polarity: None,
5693 connected: Some(connected),
5694 active_power_mw: power,
5695 current_a: power.map(|value| value / 320.0),
5696 };
5697 let unit = component("dc_converter_unit", "unit");
5698 let topological_node = component("dc_topological_node", "dc-topology");
5699 let pcc_terminal = TerminalReference {
5700 equipment: component("branch", "pcc"),
5701 terminal: 2,
5702 };
5703 let droop_curve = DroopCurve {
5704 segments: vec![DroopCurveSegment {
5705 minimum_voltage_kv: 300.0,
5706 maximum_voltage_kv: 340.0,
5707 k: 0.4,
5708 }],
5709 };
5710 let mut curve_properties = BTreeMap::new();
5711 curve_properties.insert("source".into(), "test".into());
5712 let reactive_limits = ReactiveLimits::CapabilityCurve(ReactiveCapabilityCurve {
5713 curve_style: CurveStyle::StraightLineYValues,
5714 properties: curve_properties,
5715 points: vec![
5716 ReactiveCapabilityCurvePoint {
5717 active_power_mw: -100.0,
5718 minimum_reactive_power_mvar: -50.0,
5719 maximum_reactive_power_mvar: 40.0,
5720 properties: BTreeMap::new(),
5721 },
5722 ReactiveCapabilityCurvePoint {
5723 active_power_mw: 100.0,
5724 minimum_reactive_power_mvar: -40.0,
5725 maximum_reactive_power_mvar: 50.0,
5726 properties: BTreeMap::new(),
5727 },
5728 ],
5729 });
5730 let detailed = DetailedConnectivity {
5731 terminals: vec![Terminal {
5732 component: None,
5733 equipment: component("voltage_source_converter", "vsc"),
5734 terminal: 1,
5735 voltage_level: component("voltage_level", "vl"),
5736 bus: None,
5737 connectable_bus: None,
5738 node: Some(component("connectivity_node", "n1")),
5739 connected: true,
5740 active_power_mw: Some(-95.0),
5741 reactive_power_mvar: Some(12.0),
5742 }],
5743 dc_converter_units: vec![DcConverterUnit {
5744 component: unit.clone(),
5745 substation: Some(component("substation", "station")),
5746 operation_mode: DcConverterOperatingMode::Bipolar,
5747 }],
5748 dc_topological_nodes: vec![DcTopologicalNode {
5749 component: topological_node.clone(),
5750 dc_converter_unit: Some(unit.clone()),
5751 }],
5752 dc_nodes: vec![
5753 DcNode {
5754 component: node1.clone(),
5755 nominal_voltage_kv: Some(320.0),
5756 dc_converter_unit: Some(unit.clone()),
5757 dc_topological_node: Some(topological_node.clone()),
5758 voltage_kv: Some(318.0),
5759 },
5760 DcNode {
5761 component: node2.clone(),
5762 nominal_voltage_kv: Some(320.0),
5763 dc_converter_unit: Some(unit.clone()),
5764 dc_topological_node: Some(topological_node),
5765 voltage_kv: None,
5766 },
5767 ],
5768 dc_grounds: vec![DcGround {
5769 component: component("dc_ground", "ground"),
5770 equipment_container: Some(unit.clone()),
5771 dc_terminal: terminal(node1.clone(), true, Some(0.5)),
5772 rated_dc_voltage_kv: Some(320.0),
5773 resistance_ohm: Some(2.0),
5774 inductance_h: Some(0.01),
5775 }],
5776 dc_lines: vec![DcLine {
5777 component: component("dc_line", "line"),
5778 equipment_container: Some(unit.clone()),
5779 dc_terminal1: terminal(node1.clone(), true, Some(100.0)),
5780 dc_terminal2: terminal(node2.clone(), false, Some(-99.0)),
5781 rated_dc_voltage_kv: Some(320.0),
5782 resistance_ohm: Some(1.5),
5783 inductance_h: Some(0.02),
5784 capacitance_f: Some(0.001),
5785 length_km: Some(20.0),
5786 }],
5787 dc_switches: vec![DcSwitch {
5788 component: component("dc_switch", "breaker"),
5789 equipment_container: Some(unit.clone()),
5790 dc_terminal1: terminal(node1.clone(), true, None),
5791 dc_terminal2: terminal(node2.clone(), true, None),
5792 kind: DcSwitchKind::Breaker,
5793 rated_dc_voltage_kv: Some(320.0),
5794 open: Some(false),
5795 resistance_ohm: Some(0.01),
5796 }],
5797 voltage_source_converters: vec![VoltageSourceConverter {
5798 component: component("voltage_source_converter", "vsc"),
5799 dc_converter_unit: Some(unit.clone()),
5800 dc_terminal1: terminal(node1.clone(), true, Some(-100.0)),
5801 dc_terminal2: terminal(node2.clone(), true, Some(100.0)),
5802 base_apparent_power_mva: Some(200.0),
5803 minimum_active_power_mw: Some(-150.0),
5804 maximum_active_power_mw: Some(150.0),
5805 minimum_dc_voltage_kv: Some(300.0),
5806 maximum_dc_voltage_kv: Some(340.0),
5807 rated_dc_voltage_kv: Some(320.0),
5808 valve_u0_kv: Some(0.1),
5809 number_of_valves: Some(4),
5810 idle_loss_mw: Some(1.0),
5811 switching_loss_mw_per_ampere: Some(0.002),
5812 resistive_loss_ohm: Some(0.1),
5813 control_mode: Some(
5814 AcDcConverterControlMode::ActivePowerAtPccAndDcVoltageDroopCurve,
5815 ),
5816 active_power_at_pcc_mw: Some(95.0),
5817 reactive_power_at_pcc_mvar: Some(12.0),
5818 target_active_power_mw: Some(95.0),
5819 target_dc_voltage_kv: Some(320.0),
5820 pcc_terminal: Some(pcc_terminal.clone()),
5821 droop_curve: Some(droop_curve.clone()),
5822 droop: Some(2.0),
5823 droop_compensation: Some(0.1),
5824 q_share: Some(0.5),
5825 maximum_modulation_index: Some(0.9),
5826 maximum_valve_current_a: Some(800.0),
5827 voltage_regulator_on: Some(true),
5828 voltage_setpoint_kv: Some(230.0),
5829 reactive_power_setpoint_mvar: None,
5830 reactive_limits: Some(reactive_limits),
5831 pole_loss_active_power_mw: Some(1.2),
5832 dc_current_a: Some(300.0),
5833 ac_voltage_kv: Some(230.0),
5834 dc_voltage_kv: Some(318.0),
5835 delta_degrees: Some(1.0),
5836 uf_kv: Some(229.0),
5837 uv_kv: Some(231.0),
5838 }],
5839 line_commutated_converters: vec![LineCommutatedConverter {
5840 component: component("line_commutated_converter", "lcc"),
5841 dc_converter_unit: Some(unit),
5842 dc_terminal1: terminal(node1, true, Some(-75.0)),
5843 dc_terminal2: terminal(node2, true, Some(75.0)),
5844 base_apparent_power_mva: Some(160.0),
5845 minimum_active_power_mw: Some(-120.0),
5846 maximum_active_power_mw: Some(120.0),
5847 minimum_dc_voltage_kv: Some(300.0),
5848 maximum_dc_voltage_kv: Some(340.0),
5849 rated_dc_voltage_kv: Some(320.0),
5850 valve_u0_kv: Some(0.2),
5851 number_of_valves: Some(6),
5852 idle_loss_mw: Some(0.5),
5853 switching_loss_mw_per_ampere: Some(0.001),
5854 resistive_loss_ohm: Some(0.2),
5855 control_mode: Some(AcDcConverterControlMode::DcVoltage),
5856 active_power_at_pcc_mw: Some(-75.0),
5857 reactive_power_at_pcc_mvar: Some(-20.0),
5858 target_active_power_mw: None,
5859 target_dc_voltage_kv: Some(320.0),
5860 pcc_terminal: Some(pcc_terminal),
5861 droop_curve: Some(droop_curve),
5862 reactive_model: Some(LineCommutatedConverterReactiveModel::FixedPowerFactor),
5863 power_factor: Some(0.95),
5864 operating_mode: Some(LineCommutatedConverterOperatingMode::Rectifier),
5865 rated_dc_current_a: Some(500.0),
5866 minimum_alpha_degrees: Some(5.0),
5867 maximum_alpha_degrees: Some(30.0),
5868 minimum_gamma_degrees: Some(10.0),
5869 maximum_gamma_degrees: Some(35.0),
5870 target_alpha_degrees: Some(15.0),
5871 target_gamma_degrees: Some(20.0),
5872 target_dc_current_a: Some(250.0),
5873 pole_loss_active_power_mw: Some(0.8),
5874 dc_current_a: Some(250.0),
5875 ac_voltage_kv: Some(230.0),
5876 dc_voltage_kv: Some(319.0),
5877 alpha_degrees: Some(14.0),
5878 gamma_degrees: Some(19.0),
5879 }],
5880 ..DetailedConnectivity::default()
5881 };
5882
5883 let json = serde_json::to_string(&detailed).unwrap();
5884 let restored: DetailedConnectivity = serde_json::from_str(&json).unwrap();
5885 assert_eq!(restored, detailed);
5886
5887 let mut older_terminal = serde_json::to_value(&detailed.terminals[0]).unwrap();
5888 let older_terminal = older_terminal.as_object_mut().unwrap();
5889 older_terminal.remove("active_power_mw");
5890 older_terminal.remove("reactive_power_mvar");
5891 let older_terminal: Terminal =
5892 serde_json::from_value(serde_json::Value::Object(older_terminal.clone())).unwrap();
5893 assert_eq!(older_terminal.active_power_mw, None);
5894 assert_eq!(older_terminal.reactive_power_mvar, None);
5895
5896 let min_max = ReactiveLimits::MinMax(MinMaxReactiveLimits {
5897 minimum_reactive_power_mvar: -25.0,
5898 maximum_reactive_power_mvar: 30.0,
5899 properties: BTreeMap::new(),
5900 });
5901 let json = serde_json::to_string(&min_max).unwrap();
5902 assert_eq!(
5903 serde_json::from_str::<ReactiveLimits>(&json).unwrap(),
5904 min_max
5905 );
5906 }
5907
5908 #[test]
5909 fn converter_droop_control_modes_have_distinct_serialized_names() {
5910 for (mode, name) in [
5911 (
5912 AcDcConverterControlMode::ActivePowerAtPccAndDcVoltageDroopCurve,
5913 "active_power_at_pcc_and_dc_voltage_droop_curve",
5914 ),
5915 (
5916 AcDcConverterControlMode::ActivePowerAtPccAndDcVoltageDroop,
5917 "active_power_at_pcc_and_dc_voltage_droop",
5918 ),
5919 (
5920 AcDcConverterControlMode::ActivePowerAtPccAndDcVoltageDroopWithCompensation,
5921 "active_power_at_pcc_and_dc_voltage_droop_with_compensation",
5922 ),
5923 (
5924 AcDcConverterControlMode::ActivePowerAtPccAndDcVoltageDroopPilot,
5925 "active_power_at_pcc_and_dc_voltage_droop_pilot",
5926 ),
5927 ] {
5928 let value = serde_json::to_value(mode).unwrap();
5929 assert_eq!(value, serde_json::Value::String(name.to_owned()));
5930 assert_eq!(
5931 serde_json::from_value::<AcDcConverterControlMode>(value).unwrap(),
5932 mode
5933 );
5934 }
5935 }
5936
5937 #[test]
5938 fn assigned_component_ids_follow_records_after_reordering() {
5939 let mut network = BalancedNetwork::in_memory(
5940 "ids",
5941 100.0,
5942 vec![
5943 Bus::new(BusId(10), BusType::Ref, 230.0),
5944 Bus::new(BusId(20), BusType::Pq, 230.0),
5945 ],
5946 vec![
5947 Branch::new(BusId(10), BusId(20), 0.0, 0.1),
5948 Branch::new(BusId(10), BusId(20), 0.0, 0.2),
5949 ],
5950 );
5951 network.loads_mut().extend([
5952 Load::new(BusId(20), 10.0, 1.0),
5953 Load::new(BusId(20), 20.0, 2.0),
5954 ]);
5955 network.assign_missing_component_ids();
5956
5957 assert_eq!(network.buses()[0].uid.as_deref(), Some("10"));
5958 assert_eq!(network.branches()[0].uid.as_deref(), Some("10-20"));
5959 assert_eq!(network.branches()[1].uid.as_deref(), Some("10-20-2"));
5960 assert_eq!(network.loads()[0].uid.as_deref(), Some("bus-20"));
5961 assert_eq!(network.loads()[1].uid.as_deref(), Some("bus-20-2"));
5962 assert!(network.uid_is_generated(network.loads()[0].uid.as_deref()));
5963
5964 network.loads_mut().swap(0, 1);
5965 network.branches_mut().swap(0, 1);
5966 network.assign_missing_component_ids();
5967 assert_eq!(network.loads()[0].uid.as_deref(), Some("bus-20-2"));
5968 assert_eq!(network.loads()[1].uid.as_deref(), Some("bus-20"));
5969 assert_eq!(network.branches()[0].uid.as_deref(), Some("10-20-2"));
5970 assert_eq!(network.branches()[1].uid.as_deref(), Some("10-20"));
5971
5972 let restored = serde_round_trip(&network);
5973 assert_eq!(restored.loads()[0].uid, network.loads()[0].uid);
5974 assert_eq!(restored.branches()[0].uid, network.branches()[0].uid);
5975 assert!(restored.uid_is_generated(restored.loads()[0].uid.as_deref()));
5976 }
5977
5978 #[test]
5979 fn source_and_generated_component_identities_remain_distinct() {
5980 let mut explicit = Bus::new(BusId(10), BusType::Ref, 230.0);
5981 explicit.uid = Some("source-bus".to_owned());
5982 let mut network = BalancedNetwork::new("identity provenance", 100.0);
5983 network
5984 .buses_mut()
5985 .extend([explicit, Bus::new(BusId(20), BusType::Pq, 230.0)]);
5986 network.assign_missing_component_ids();
5987
5988 assert!(!network.uid_is_generated(network.buses()[0].uid.as_deref()));
5989 assert!(network.uid_is_generated(network.buses()[1].uid.as_deref()));
5990 }
5991
5992 #[test]
5995 fn generated_identities_are_unique_across_tables() {
5996 let mut network = BalancedNetwork::in_memory(
5997 "cross table ids",
5998 100.0,
5999 vec![
6000 Bus::new(BusId(10), BusType::Ref, 230.0),
6001 Bus::new(BusId(20), BusType::Pq, 230.0),
6002 ],
6003 vec![Branch::new(BusId(10), BusId(20), 0.0, 0.1)],
6004 );
6005 network.loads_mut().push(Load::new(BusId(20), 10.0, 1.0));
6006 network.shunts_mut().push(Shunt::new(BusId(20), 0.0, 0.5));
6007 network.generators_mut().push(Generator::new(BusId(20)));
6008 network.assign_missing_component_ids();
6009
6010 let mut identities = vec![
6011 network.loads()[0].uid.clone().unwrap(),
6012 network.shunts()[0].uid.clone().unwrap(),
6013 network.generators()[0].uid.clone().unwrap(),
6014 ];
6015 identities.sort();
6016 assert_eq!(identities, ["bus-20", "bus-20-2", "bus-20-3"]);
6017
6018 let before = identities.clone();
6021 network.assign_missing_component_ids();
6022 let mut after = vec![
6023 network.loads()[0].uid.clone().unwrap(),
6024 network.shunts()[0].uid.clone().unwrap(),
6025 network.generators()[0].uid.clone().unwrap(),
6026 ];
6027 after.sort();
6028 assert_eq!(after, before);
6029 }
6030
6031 #[test]
6032 fn source_format_serializes_as_its_name_token() {
6033 let all = [
6036 SourceFormat::Sincal,
6037 SourceFormat::Matpower,
6038 SourceFormat::PowerModelsJson,
6039 SourceFormat::EgretJson,
6040 SourceFormat::Psse,
6041 SourceFormat::PsseRawx,
6042 SourceFormat::PowerWorld,
6043 SourceFormat::PandapowerJson,
6044 SourceFormat::Pslf,
6045 SourceFormat::PowerWorldBinary,
6046 SourceFormat::InMemory,
6047 SourceFormat::Normalized,
6048 SourceFormat::Gridfm,
6049 SourceFormat::PypsaCsv,
6050 SourceFormat::Goc3Json,
6051 SourceFormat::SurgeJson,
6052 SourceFormat::RavensJson,
6053 SourceFormat::DeepMindOpfDataJson,
6054 SourceFormat::Xiidm,
6055 SourceFormat::Jiidm,
6056 SourceFormat::Cgmes,
6057 SourceFormat::Ucte,
6058 SourceFormat::IeeeCdf,
6059 ];
6060 for f in all {
6061 match f {
6062 SourceFormat::Matpower
6063 | SourceFormat::PowerModelsJson
6064 | SourceFormat::EgretJson
6065 | SourceFormat::Psse
6066 | SourceFormat::PsseRawx
6067 | SourceFormat::PowerWorld
6068 | SourceFormat::PandapowerJson
6069 | SourceFormat::Pslf
6070 | SourceFormat::PowerWorldBinary
6071 | SourceFormat::InMemory
6072 | SourceFormat::Normalized
6073 | SourceFormat::Gridfm
6074 | SourceFormat::PypsaCsv
6075 | SourceFormat::Goc3Json
6076 | SourceFormat::SurgeJson
6077 | SourceFormat::RavensJson
6078 | SourceFormat::DeepMindOpfDataJson
6079 | SourceFormat::Xiidm
6080 | SourceFormat::Jiidm
6081 | SourceFormat::Cgmes
6082 | SourceFormat::Ucte
6083 | SourceFormat::IeeeCdf
6084 | SourceFormat::Sincal => {}
6085 }
6086 let token = serde_json::to_value(f).unwrap();
6087 assert_eq!(token, serde_json::Value::String(f.name().to_owned()));
6088 let back: SourceFormat = serde_json::from_value(token).unwrap();
6089 assert_eq!(back, f);
6090 }
6091 }
6092
6093 #[test]
6094 fn quadratic_with_constant_keeps_c0_across_ncost() {
6095 let full = GenCost::new(2, 0.0, 0.0, vec![1.5, 2.0, 5.0]);
6096 assert_eq!(full.calc_quadratic_with_constant(), Some((3.0, 2.0, 5.0)));
6097 assert_eq!(full.calc_quadratic(), Some((3.0, 2.0)));
6098 assert_eq!(
6099 full.calc_quadratic_with_constant(),
6100 full.calc_quadratic_with_constant()
6101 );
6102 assert_eq!(full.calc_quadratic(), full.calc_quadratic());
6103
6104 let linear = GenCost::new(2, 0.0, 0.0, vec![2.0, 5.0]);
6105 assert_eq!(linear.calc_quadratic_with_constant(), Some((0.0, 2.0, 5.0)));
6106
6107 let constant = GenCost::new(2, 0.0, 0.0, vec![5.0]);
6108 assert_eq!(
6109 constant.calc_quadratic_with_constant(),
6110 Some((0.0, 0.0, 5.0))
6111 );
6112
6113 let piecewise = GenCost::new(1, 0.0, 0.0, vec![0.0, 0.0, 1.0, 1.0]);
6114 assert_eq!(piecewise.calc_quadratic_with_constant(), None);
6115
6116 let cubic = GenCost::new(2, 0.0, 0.0, vec![1.0, 1.0, 1.0, 1.0]);
6117 assert_eq!(cubic.calc_quadratic_with_constant(), None);
6118
6119 let truncated = GenCost::with_ncost(2, 0.0, 0.0, 3, vec![1.0]);
6120 assert_eq!(truncated.calc_quadratic_with_constant(), None);
6121 }
6122
6123 #[test]
6124 fn a_leading_coefficient_below_the_tolerance_comes_off_the_row() {
6125 let artifact = GenCost::new(2, 0.0, 0.0, vec![1e-17, 2.0, 5.0]);
6126 assert_eq!(
6127 artifact.calc_quadratic_with_constant(),
6128 Some((2e-17, 2.0, 5.0)),
6129 "the untouched reader keeps the artifact"
6130 );
6131 assert_eq!(
6132 artifact.calc_quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
6133 Some((0.0, 2.0, 5.0))
6134 );
6135 assert_eq!(
6136 artifact.calc_quadratic_with_constant_tol(0.0),
6137 Some((2e-17, 2.0, 5.0)),
6138 "a zero tolerance strips an exact zero alone"
6139 );
6140
6141 let padded = GenCost::new(2, 0.0, 0.0, vec![0.0, 1.5, 2.0, 5.0]);
6144 assert_eq!(padded.calc_quadratic_with_constant(), None);
6145 assert_eq!(
6146 padded.calc_quadratic_with_constant_tol(0.0),
6147 Some((3.0, 2.0, 5.0))
6148 );
6149
6150 let flat = GenCost::new(2, 0.0, 0.0, vec![1e-17, 1e-17, 1e-17]);
6151 assert_eq!(
6152 flat.calc_quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
6153 Some((0.0, 0.0, 1e-17)),
6154 "the last coefficient stays, whatever its magnitude"
6155 );
6156
6157 let piecewise = GenCost::new(1, 0.0, 0.0, vec![0.0, 0.0, 1.0, 1.0]);
6158 assert_eq!(
6159 piecewise.calc_quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
6160 None
6161 );
6162
6163 let truncated = GenCost::with_ncost(2, 0.0, 0.0, 3, vec![1.0]);
6164 assert_eq!(
6165 truncated.calc_quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
6166 None
6167 );
6168
6169 let quartic = GenCost::new(2, 0.0, 0.0, vec![1.0, 1.0, 1.0, 1.0, 1.0]);
6170 assert_eq!(
6171 quartic.calc_quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
6172 None
6173 );
6174 }
6175
6176 fn expected_rate(window: f64, fr: f64, to: f64, zmag: f64) -> f64 {
6179 let separation = (fr * fr + to * to - 2.0 * fr * to * window.cos()).sqrt();
6180 fr.max(to) * separation / zmag
6181 }
6182
6183 #[test]
6184 fn synthesized_rate_follows_the_angle_window_and_the_voltage_bands() {
6185 let br = Branch::new(BusId(1), BusId(2), 0.03, 0.04);
6186 let expected = |window: f64, fr: f64, to: f64| expected_rate(window, fr, to, 0.05);
6187 let at = |v: f64| (v, v);
6190 close(
6191 br.synthesize_rate_a(0.5, at(1.1), at(1.06)),
6192 expected(0.5, 1.1, 1.06),
6193 );
6194
6195 assert!(
6197 br.synthesize_rate_a(0.8, at(1.1), at(1.06))
6198 > br.synthesize_rate_a(0.5, at(1.1), at(1.06))
6199 );
6200
6201 close(
6203 br.synthesize_rate_a(-0.5, at(1.1), at(1.06)),
6204 expected(0.5, 1.1, 1.06),
6205 );
6206 for window in [6.0, 2.0 * std::f64::consts::PI, -360.0] {
6207 close(
6208 br.synthesize_rate_a(window, at(1.1), at(1.06)),
6209 expected(std::f64::consts::PI, 1.1, 1.06),
6210 );
6211 }
6212
6213 let ideal = Branch::new(BusId(1), BusId(2), 0.0, 0.0);
6214 close(ideal.synthesize_rate_a(0.5, at(1.1), at(1.1)), 0.0);
6215 }
6216
6217 #[test]
6218 fn a_narrow_window_bounds_at_the_mixed_voltage_corner() {
6219 let br = Branch::new(BusId(1), BusId(2), 0.0, 0.01);
6226 let (vmin, vmax) = (0.9, 1.1);
6227 let corner = |window: f64, fr: f64, to: f64| expected_rate(window, fr, to, 0.01);
6228
6229 let narrow = 2.0_f64.to_radians();
6230 let bound = br.synthesize_rate_a(narrow, (vmin, vmax), (vmin, vmax));
6231 close(bound, corner(narrow, vmax, vmin));
6232 assert!(
6233 bound > 5.0 * corner(narrow, vmax, vmax),
6234 "the mixed corner dominates here: {bound} vs {}",
6235 corner(narrow, vmax, vmax)
6236 );
6237
6238 let wide = 30.0_f64.to_radians();
6240 close(
6241 br.synthesize_rate_a(wide, (vmin, vmax), (vmin, vmax)),
6242 corner(wide, vmax, vmax),
6243 );
6244 }
6245
6246 fn bus(id: usize) -> Bus {
6247 Bus {
6248 id: BusId(id),
6249 kind: BusType::Pq,
6250 vm: 1.0,
6251 va: 0.0,
6252 base_kv: 230.0,
6253 vmax: 1.1,
6254 vmin: 0.9,
6255 evhi: None,
6256 evlo: None,
6257 area: 1,
6258 zone: 1,
6259 name: None,
6260 uid: None,
6261 location: None,
6262 extras: Extras::new(),
6263 }
6264 }
6265
6266 fn winding(b: usize) -> Winding {
6267 Winding {
6268 bus: BusId(b),
6269 tap: 1.0,
6270 shift: 0.0,
6271 nominal_kv: 230.0,
6272 rate_a: 100.0,
6273 rate_b: 0.0,
6274 rate_c: 0.0,
6275 control: None,
6276 }
6277 }
6278
6279 fn transformer_3w() -> Transformer3W {
6280 let z = |r, x| Impedance {
6281 r,
6282 x,
6283 base_mva: 100.0,
6284 };
6285 Transformer3W {
6286 windings: [winding(1), winding(2), winding(3)],
6287 z: [z(0.01, 0.10), z(0.02, 0.20), z(0.03, 0.30)],
6288 star_vm: 0.98,
6289 star_va: -1.5,
6290 mag_g: 0.0,
6291 mag_b: 0.0,
6292 in_service: true,
6293 name: Some("T1".into()),
6294 uid: None,
6295 extras: Extras::new(),
6296 }
6297 }
6298
6299 #[test]
6300 fn star_impedances_split_the_pairwise_values() {
6301 let [(r1, x1), (r2, x2), (r3, x3)] = transformer_3w().calc_star_impedances();
6303 close(r1, 0.01);
6304 close(x1, 0.10);
6305 close(r2, 0.0);
6306 close(x2, 0.0);
6307 close(r3, 0.02);
6308 close(x3, 0.20);
6309 }
6310
6311 #[test]
6312 fn star_expansion_builds_a_star_bus_and_three_branches() {
6313 let t = transformer_3w();
6314 let (star, branches) = t.to_star_expansion(BusId(99));
6315
6316 assert_eq!(star.id, BusId(99));
6317 close(star.vm, 0.98);
6318 close(star.va, -1.5);
6319 for (i, br) in branches.iter().enumerate() {
6322 assert_eq!(br.from, t.windings[i].bus);
6323 assert_eq!(br.to, BusId(99));
6324 close(br.tap, 1.0);
6325 close(br.rate_a, 100.0);
6326 }
6327 close(branches[2].r, 0.02);
6328 close(branches[2].x, 0.20);
6329 }
6330
6331 #[test]
6332 fn star_expansion_carries_winding_status_and_rating_sets() {
6333 let mut t = transformer_3w();
6334 t.set_winding_in_service([true, false, true]);
6335 t.set_winding_rating_sets(2, vec![BranchRatingSet::new("RATE4", 75.0)]);
6336 let (_, branches) = t.to_star_expansion(BusId(99));
6337 assert_eq!(
6338 branches.each_ref().map(|br| br.in_service),
6339 [true, false, true]
6340 );
6341 assert_eq!(
6342 branches[2].rating_sets,
6343 [BranchRatingSet::new("RATE4", 75.0)]
6344 );
6345 assert_eq!(branches[0].rating_sets, []);
6346
6347 t.in_service = false;
6349 let (_, branches) = t.to_star_expansion(BusId(99));
6350 assert!(branches.iter().all(|br| !br.in_service));
6351
6352 t.in_service = true;
6355 let mut net =
6356 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
6357 net.transformers_3w_mut().push(t);
6358 let lowered = net.expand_transformers_3w();
6359 assert_eq!(
6360 lowered
6361 .branches()
6362 .iter()
6363 .map(|br| (br.from, br.in_service))
6364 .collect::<Vec<_>>(),
6365 [(BusId(1), true), (BusId(2), false), (BusId(3), true)]
6366 );
6367 assert_eq!(net.lowered_lengths().branches, lowered.branches().len());
6368 }
6369
6370 #[test]
6371 fn winding_status_and_rating_sets_live_in_extras_only_when_stated() {
6372 let mut t = transformer_3w();
6373 assert_eq!(t.winding_in_service(), [true; 3]);
6374 assert_eq!(t.winding_rating_sets(1), []);
6375
6376 t.set_winding_in_service([false, true, true]);
6377 t.set_winding_rating_sets(1, vec![BranchRatingSet::new("RATE4", 75.0)]);
6378 assert_eq!(
6379 t.extras["winding_in_service"],
6380 serde_json::json!([false, true, true])
6381 );
6382 assert_eq!(
6383 t.extras["winding_rating_sets"],
6384 serde_json::json!([[], [{"name": "RATE4", "rate_mva": 75.0}], []])
6385 );
6386 let mut net =
6387 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
6388 net.transformers_3w_mut().push(t.clone());
6389 let back = serde_round_trip(&net);
6390 assert_eq!(
6391 back.transformers_3w()[0].winding_in_service(),
6392 [false, true, true]
6393 );
6394 assert_eq!(
6395 back.transformers_3w()[0].winding_rating_sets(1),
6396 [BranchRatingSet::new("RATE4", 75.0)]
6397 );
6398
6399 t.set_winding_in_service([true; 3]);
6401 t.set_winding_rating_sets(1, Vec::new());
6402 assert!(t.extras.is_empty(), "{:?}", t.extras);
6403
6404 t.extras
6406 .insert("winding_in_service".to_owned(), Value::from("open"));
6407 assert_eq!(t.winding_in_service(), [true; 3]);
6408 }
6409
6410 #[test]
6411 fn three_winding_transformer_survives_serde_round_trip() {
6412 let mut net =
6413 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
6414 net.transformers_3w_mut().push(transformer_3w());
6415 net.validate().unwrap();
6416
6417 let back = serde_round_trip(&net);
6418 assert_eq!(back.transformers_3w().len(), 1);
6419 close(back.transformers_3w()[0].z[1].x, 0.20);
6420 assert_eq!(back.transformers_3w()[0].windings[2].bus, BusId(3));
6421 }
6422
6423 #[test]
6424 fn a_network_with_only_out_of_service_three_winding_units_is_not_copied() {
6425 let mut out_of_service = transformer_3w();
6426 out_of_service.in_service = false;
6427 let mut net =
6428 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
6429 net.transformers_3w_mut().push(out_of_service);
6430 assert!(matches!(
6431 net.expand_transformers_3w(),
6432 std::borrow::Cow::Borrowed(_)
6433 ));
6434 let core = crate::IndexCore::build(&net);
6436 let view = crate::IndexedNetwork::with_core(&net, &core);
6437 assert!(std::ptr::eq(view.network(), std::ptr::from_ref(&net)));
6438 net.transformers_3w_mut()[0].in_service = true;
6439 assert!(matches!(
6440 net.expand_transformers_3w(),
6441 std::borrow::Cow::Owned(_)
6442 ));
6443 }
6444
6445 #[test]
6446 fn lowered_lengths_match_the_expansion() {
6447 let mut magnetizing = transformer_3w();
6452 magnetizing.mag_b = 0.02;
6453 let mut out_of_service = transformer_3w();
6454 out_of_service.in_service = false;
6455 out_of_service.mag_g = 0.01;
6456
6457 for units in [
6458 vec![],
6459 vec![transformer_3w()],
6460 vec![magnetizing.clone()],
6461 vec![out_of_service.clone()],
6462 vec![transformer_3w(), magnetizing, out_of_service],
6463 ] {
6464 let mut net =
6465 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
6466 net.shunts_mut().push(Shunt::new(BusId(1), 0.0, 0.5));
6467 *net.transformers_3w_mut() = units;
6468 let counted = net.lowered_lengths();
6469 let built = net.expand_transformers_3w();
6470 assert_eq!(counted.buses, built.buses().len());
6471 assert_eq!(counted.branches, built.branches().len());
6472 assert_eq!(counted.shunts, built.shunts().len());
6473 }
6474 }
6475
6476 #[test]
6477 fn check_references_rejects_bus_ids_without_star_expansion_headroom() {
6478 let mut net = BalancedNetwork::in_memory(
6482 "t",
6483 100.0,
6484 vec![bus(1), bus(2), bus(3), bus(i64::MAX as usize)],
6485 Vec::new(),
6486 );
6487 net.transformers_3w_mut().push(transformer_3w());
6488 let err = net.validate().unwrap_err().to_string();
6489 assert!(
6490 err.contains("no room to allocate synthetic star bus ids"),
6491 "got {err}"
6492 );
6493 }
6494
6495 #[test]
6496 fn star_expansion_headroom_counts_only_in_service_transformers() {
6497 let mut net = BalancedNetwork::in_memory(
6503 "t",
6504 100.0,
6505 vec![bus(1), bus(2), bus(3), bus(i64::MAX as usize - 1)],
6506 Vec::new(),
6507 );
6508 net.transformers_3w_mut().push(transformer_3w());
6509 let mut out_of_service = transformer_3w();
6510 out_of_service.in_service = false;
6511 net.transformers_3w_mut().push(out_of_service);
6512 net.validate()
6513 .expect("in-service count fits; must not be rejected");
6514 }
6515
6516 #[test]
6517 fn check_references_rejects_a_bus_id_past_the_int64_ceiling() {
6518 let mut net = BalancedNetwork::in_memory(
6523 "t",
6524 100.0,
6525 vec![bus(1), bus(i64::MAX as usize + 1)],
6526 Vec::new(),
6527 );
6528 let err = net.validate().unwrap_err().to_string();
6529 assert!(err.contains("outside the int64 id space"), "got {err}");
6530
6531 net.buses_mut()[1].id = BusId(i64::MAX as usize);
6533 net.validate().expect("the ceiling itself is representable");
6534 }
6535
6536 #[test]
6537 fn check_references_rejects_a_dangling_winding_bus() {
6538 let mut net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
6539 net.transformers_3w_mut().push(transformer_3w()); let err = net.validate().unwrap_err().to_string();
6541 assert!(
6542 err.contains("3-winding transformer references unknown bus 3"),
6543 "got {err}"
6544 );
6545 }
6546
6547 #[test]
6548 fn check_references_rejects_a_dangling_winding_control_bus() {
6549 let mut net =
6550 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
6551 let mut transformer = transformer_3w();
6552 let mut control = TransformerControl::new(TransformerControlMode::Voltage);
6553 control.controlled_bus = Some(BusId(4));
6554 transformer.windings[2].control = Some(control);
6555 net.transformers_3w_mut().push(transformer);
6556 let err = net.validate().unwrap_err().to_string();
6557 assert!(
6558 err.contains("3-winding transformer control references unknown bus 4"),
6559 "got {err}"
6560 );
6561 }
6562
6563 fn regulating_branch(reg: usize) -> Branch {
6565 Branch {
6566 name: None,
6567 from: BusId(1),
6568 to: BusId(2),
6569 r: 0.0,
6570 x: 0.1,
6571 b: 0.0,
6572 charging: None,
6573 rate_a: 0.0,
6574 rate_b: 0.0,
6575 rate_c: 0.0,
6576 rating_sets: Vec::new(),
6577 current_ratings: None,
6578 tap: 1.0,
6579 shift: 0.0,
6580 in_service: true,
6581 angmin: -360.0,
6582 angmax: 360.0,
6583 control: Some(TransformerControl {
6584 mode: TransformerControlMode::Voltage,
6585 enabled: true,
6586 controlled_bus: Some(BusId(reg)),
6587 controlled_bus_on_winding_side: false,
6588 regulating_terminal: None,
6589 tap_min: 0.95,
6590 tap_max: 1.05,
6591 band_min: 1.0,
6592 band_max: 1.02,
6593 ntp: 17,
6594 mva_base: 100.0,
6595 winding_connection_angle: None,
6596 }),
6597 solution: None,
6598 uid: None,
6599 route: None,
6600 extras: Extras::new(),
6601 }
6602 }
6603
6604 #[test]
6605 fn transformer_control_survives_serde_round_trip() {
6606 let mut net =
6607 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
6608 net.branches_mut().push(regulating_branch(3));
6609 net.validate().unwrap();
6610
6611 let back = serde_round_trip(&net);
6612 let c = back.branches()[0].control.as_ref().unwrap();
6613 assert_eq!(c.mode, TransformerControlMode::Voltage);
6614 assert_eq!(c.controlled_bus, Some(BusId(3)));
6615 close(c.tap_max, 1.05);
6616 assert_eq!(c.ntp, 17);
6617 }
6618
6619 #[test]
6620 fn gen_caps_serialize_as_a_named_map_that_grows_additively() {
6621 let mut caps: GenCaps = [None; GEN_EXTRA_KEYS.len()];
6622 caps[8] = Some(1.5); caps[10] = Some(0.5); let g = Generator {
6625 bus: BusId(1),
6626 energy_source: GeneratorEnergySource::default(),
6627 pg: 10.0,
6628 qg: 0.0,
6629 pmax: 100.0,
6630 pmin: 0.0,
6631 qmax: 50.0,
6632 qmin: -50.0,
6633 vg: 1.0,
6634 mbase: 100.0,
6635 in_service: true,
6636 cost: None,
6637 caps,
6638 voltage_regulation_on: false,
6639 regulating_terminal: Some(TerminalReference {
6640 equipment: ComponentId::new("load", "L1").unwrap(),
6641 terminal: 1,
6642 }),
6643 regulated_bus: Some(BusId(2)),
6644 active_power_control: None,
6645 uid: None,
6646 };
6647
6648 let json = serde_json::to_string(&g).unwrap();
6651 assert!(json.contains(r#""caps":{"#), "caps is an object: {json}");
6652 assert!(json.contains(r#""ramp_30":1.5"#) && json.contains(r#""apf":0.5"#));
6653 let back: Generator = serde_json::from_str(&json).unwrap();
6654 assert_eq!(back.caps, g.caps);
6655 assert!(!back.voltage_regulation_on);
6656 assert_eq!(back.regulating_terminal, g.regulating_terminal);
6657 assert_eq!(back.regulated_bus, Some(BusId(2)));
6658
6659 let with_future = r#"{"bus":1,"pg":10,"qg":0,"pmax":100,"pmin":0,"qmax":50,"qmin":-50,
6662 "vg":1,"mbase":100,"in_service":true,"cost":null,
6663 "caps":{"ramp_30":1.5,"future_ramp":9.9}}"#;
6664 let g2: Generator = serde_json::from_str(with_future).unwrap();
6665 assert_eq!(g2.caps[8], Some(1.5));
6666 assert_eq!(g2.caps.iter().filter(|v| v.is_some()).count(), 1);
6667 let no_caps = r#"{"bus":1,"pg":10,"qg":0,"pmax":100,"pmin":0,"qmax":50,"qmin":-50,
6668 "vg":1,"mbase":100,"in_service":true,"cost":null}"#;
6669 let g3: Generator = serde_json::from_str(no_caps).unwrap();
6670 assert!(!g3.has_caps());
6671 assert!(g3.voltage_regulation_on);
6672 assert_eq!(g3.regulating_terminal, None);
6673
6674 let null_caps = r#"{"bus":1,"pg":10,"qg":0,"pmax":100,"pmin":0,"qmax":50,"qmin":-50,
6676 "vg":1,"mbase":100,"in_service":true,"cost":null,"caps":null}"#;
6677 let g4: Generator = serde_json::from_str(null_caps).unwrap();
6678 assert!(!g4.has_caps());
6679 }
6680
6681 #[test]
6682 #[allow(clippy::float_cmp)]
6683 fn nonfinite_values_round_trip_through_serde() {
6684 let bus = |id, vm| Bus {
6685 id: BusId(id),
6686 kind: BusType::Pq,
6687 vm,
6688 va: 0.0,
6689 base_kv: 230.0,
6690 vmax: 1.1,
6691 vmin: 0.9,
6692 evhi: None,
6693 evlo: None,
6694 area: 1,
6695 zone: 1,
6696 name: None,
6697 uid: None,
6698 location: None,
6699 extras: Extras::new(),
6700 };
6701 let branch = Branch {
6702 name: None,
6703 from: BusId(1),
6704 to: BusId(2),
6705 r: 0.0,
6706 x: f64::INFINITY,
6707 b: 0.0,
6708 charging: None,
6709 rate_a: 0.0,
6710 rate_b: 0.0,
6711 rate_c: 0.0,
6712 rating_sets: Vec::new(),
6713 current_ratings: None,
6714 tap: 0.0,
6715 shift: 0.0,
6716 in_service: true,
6717 angmin: -360.0,
6718 angmax: 360.0,
6719 control: None,
6720 solution: None,
6721 uid: None,
6722 route: None,
6723 extras: Extras::new(),
6724 };
6725 let mut g = Generator {
6728 bus: BusId(1),
6729 energy_source: GeneratorEnergySource::default(),
6730 pg: 0.0,
6731 qg: 0.0,
6732 pmax: 0.0,
6733 pmin: 0.0,
6734 qmax: 0.0,
6735 qmin: 0.0,
6736 vg: 1.0,
6737 mbase: 100.0,
6738 in_service: true,
6739 cost: None,
6740 caps: GenCaps::default(),
6741 voltage_regulation_on: true,
6742 regulating_terminal: None,
6743 regulated_bus: None,
6744 active_power_control: None,
6745 uid: None,
6746 };
6747 g.caps[8] = Some(f64::INFINITY); let mut net = BalancedNetwork::in_memory(
6752 "nf",
6753 100.0,
6754 vec![bus(1, f64::NAN), bus(2, 1.0)],
6755 vec![branch],
6756 );
6757 net.generators_mut().push(g);
6758
6759 let text = serde_json::to_string(&net).unwrap();
6760 assert!(text.contains(r#""vm":"NaN""#), "{text}");
6761 assert!(text.contains(r#""x":"Infinity""#), "{text}");
6762 assert!(text.contains(r#""ramp_30":"Infinity""#), "{text}");
6763
6764 let back: BalancedNetwork = serde_json::from_str(&text).unwrap();
6765 assert!(back.buses()[0].vm.is_nan());
6766 assert_eq!(back.branches()[0].x, f64::INFINITY);
6767 assert_eq!(back.generators()[0].caps[8], Some(f64::INFINITY));
6768
6769 assert_eq!(serde_json::to_string(&back).unwrap(), text);
6771 }
6772
6773 #[test]
6774 fn a_null_at_a_float_position_is_rejected() {
6775 let net = BalancedNetwork::in_memory("nf", 100.0, vec![bus(1), bus(2)], Vec::new());
6776 let text = serde_json::to_string(&net)
6777 .unwrap()
6778 .replacen("\"vm\":1.0", "\"vm\":null", 1);
6779 assert!(text.contains("\"vm\":null"), "fixture edit failed: {text}");
6780 let err = serde_json::from_str::<BalancedNetwork>(&text)
6781 .unwrap_err()
6782 .to_string();
6783 assert!(err.contains("cannot be null"), "{err}");
6784 }
6785
6786 #[test]
6787 fn check_references_rejects_a_dangling_controlled_bus() {
6788 let mut net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
6789 net.branches_mut().push(regulating_branch(9)); let err = net.validate().unwrap_err().to_string();
6791 assert!(
6792 err.contains("transformer control references unknown bus 9"),
6793 "got {err}"
6794 );
6795 }
6796
6797 #[test]
6798 fn check_references_rejects_winding_side_without_a_controlled_bus() {
6799 let mut net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
6800 let mut branch = regulating_branch(2);
6801 let control = branch.control.as_mut().unwrap();
6802 control.controlled_bus = None;
6803 control.controlled_bus_on_winding_side = true;
6804 net.branches_mut().push(branch);
6805
6806 let err = net.validate().unwrap_err().to_string();
6807 assert!(
6808 err.contains("winding side but has no nonzero controlled bus"),
6809 "got {err}"
6810 );
6811 }
6812
6813 fn switched_shunt(reg: usize) -> Shunt {
6815 Shunt {
6816 bus: BusId(1),
6817 g: 0.0,
6818 b: 19.0,
6819 in_service: true,
6820 section_count: None,
6821 control: Some(SwitchedShuntControl {
6822 mode: SwitchedShuntMode::Discrete,
6823 vhigh: 1.05,
6824 vlow: 0.95,
6825 control_bus: Some(BusId(reg)),
6826 regulating_terminal: None,
6827 rmpct: 100.0,
6828 blocks: vec![ShuntBlock::new(2, 25.0), ShuntBlock::new(1, 50.0)],
6829 }),
6830 uid: None,
6831 extras: Extras::new(),
6832 }
6833 }
6834
6835 #[test]
6836 fn switched_shunt_control_survives_serde_round_trip() {
6837 let mut net =
6838 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
6839 net.shunts_mut().push(switched_shunt(3));
6840 net.validate().unwrap();
6841
6842 let back = serde_round_trip(&net);
6843 let c = back.shunts()[0].control.as_ref().unwrap();
6844 assert_eq!(c.mode, SwitchedShuntMode::Discrete);
6845 assert_eq!(c.control_bus, Some(BusId(3)));
6846 assert_eq!(c.blocks.len(), 2);
6847 close(c.blocks[1].b, 50.0);
6848 }
6849
6850 #[test]
6851 fn check_references_rejects_a_dangling_switched_shunt_control_bus() {
6852 let mut net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
6853 net.shunts_mut().push(switched_shunt(9)); let err = net.validate().unwrap_err().to_string();
6855 assert!(
6856 err.contains("switched-shunt control references unknown bus 9"),
6857 "got {err}"
6858 );
6859 }
6860
6861 #[test]
6862 fn validate_values_flags_and_repair_clamps_out_of_domain_values() {
6863 let mut net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
6864 net.buses_mut()[0].vm = 0.0; net.buses_mut()[1].va = 9000.0; net.generators_mut().push(Generator {
6867 bus: BusId(1),
6868 energy_source: GeneratorEnergySource::default(),
6869 pg: 10.0,
6870 qg: 0.0,
6871 pmax: 100.0,
6872 pmin: 0.0,
6873 qmax: 50.0,
6874 qmin: -50.0,
6875 vg: 0.0, mbase: 0.0, in_service: true,
6878 cost: None,
6879 voltage_regulation_on: true,
6880 regulating_terminal: None,
6881 caps: Default::default(),
6882 regulated_bus: None,
6883 active_power_control: None,
6884 uid: None,
6885 });
6886
6887 let diags = net.validate_values();
6888 let fields: std::collections::BTreeSet<_> = diags
6889 .iter()
6890 .map(|d| d.details()["field"].as_str().unwrap().to_owned())
6891 .collect();
6892 assert_eq!(
6893 fields,
6894 ["mbase", "va", "vg", "vm"]
6895 .into_iter()
6896 .map(str::to_owned)
6897 .collect(),
6898 "all four out-of-domain fields reported"
6899 );
6900 assert!(
6901 diags
6902 .iter()
6903 .all(|d| d.code() == "VALIDATE.BALANCED.VALUE_DOMAIN" && d.target().is_some())
6904 );
6905 close(net.buses()[0].vm, 0.0);
6907
6908 let module = powerio_core::PioModule::new(net);
6910 let module = repair_values(module).unwrap();
6911 let net = &module.value();
6912 close(net.buses()[0].vm, 1.0);
6913 close(net.buses()[1].va, 0.0);
6914 close(net.generators()[0].mbase, 100.0); close(net.generators()[0].vg, 1.0);
6916 assert_eq!(net.validate_values().as_slice(), []);
6919 let entries = module.history();
6920 assert_eq!(entries.len(), 1);
6921 assert_eq!(entries[0].kind(), powerio_core::HistoryKind::Repair);
6922 assert_eq!(
6923 entries[0].parameters()["repairs"].as_array().unwrap().len(),
6924 diags.len()
6925 );
6926 assert_eq!(module.diagnostics().len(), diags.len());
6927 let module = repair_values(module).unwrap();
6928 assert_eq!(module.history().len(), 1);
6929 }
6930
6931 #[test]
6932 fn validate_values_is_empty_for_a_clean_network() {
6933 let net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
6934 assert_eq!(net.validate_values().as_slice(), []);
6935 }
6936
6937 #[test]
6938 fn capability_curve_rejects_a_nonfinite_active_power() {
6939 let limits = ReactiveLimits::CapabilityCurve(ReactiveCapabilityCurve {
6940 curve_style: CurveStyle::StraightLineYValues,
6941 properties: BTreeMap::new(),
6942 points: vec![
6943 ReactiveCapabilityCurvePoint {
6944 active_power_mw: 0.0,
6945 minimum_reactive_power_mvar: -10.0,
6946 maximum_reactive_power_mvar: 10.0,
6947 properties: BTreeMap::new(),
6948 },
6949 ReactiveCapabilityCurvePoint {
6950 active_power_mw: 100.0,
6951 minimum_reactive_power_mvar: -5.0,
6952 maximum_reactive_power_mvar: 5.0,
6953 properties: BTreeMap::new(),
6954 },
6955 ],
6956 });
6957
6958 let error = calc_reactive_limits_at_active_power("generator 1", &limits, f64::NAN)
6959 .expect_err("NaN must not be used to index a capability curve");
6960 assert!(error.contains("nonfinite active power"), "{error}");
6961 }
6962 #[test]
6963 fn impedance_correction_factor_interpolates_and_holds_the_end_points() {
6964 let point = ImpedanceCorrectionPoint::new;
6965 let table = ImpedanceCorrection::new(
6966 1,
6967 ImpedanceCorrectionVariable::Angle,
6968 vec![
6969 point(30.0, 1.2, -0.2),
6970 point(-30.0, 1.2, 0.2),
6971 point(0.0, 1.0, 0.0),
6972 ],
6973 );
6974 assert_eq!(table.calc_factor(-45.0), Some((1.2, 0.2)));
6975 assert_eq!(table.calc_factor(45.0), Some((1.2, -0.2)));
6976 let (re, im) = table.calc_factor(15.0).unwrap();
6977 close(re, 1.1);
6978 close(im, -0.1);
6979 let single = ImpedanceCorrection::new(
6980 2,
6981 ImpedanceCorrectionVariable::Ratio,
6982 vec![point(1.0, 0.9, 0.0)],
6983 );
6984 assert_eq!(single.calc_factor(1.1), Some((0.9, 0.0)));
6985 let empty = ImpedanceCorrection::new(3, ImpedanceCorrectionVariable::Ratio, Vec::new());
6986 assert_eq!(empty.calc_factor(1.0), None);
6987 }
6988}