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}
1438
1439impl SourceFormat {
1440 #[must_use]
1445 pub fn name(self) -> &'static str {
1446 match self {
1447 SourceFormat::Sincal => "sincal",
1448 SourceFormat::Matpower => "matpower",
1449 SourceFormat::PowerModelsJson => "powermodels-json",
1450 SourceFormat::EgretJson => "egret-json",
1451 SourceFormat::Psse => "psse",
1452 SourceFormat::PsseRawx => "psse-rawx",
1453 SourceFormat::PowerWorld => "powerworld",
1454 SourceFormat::PandapowerJson => "pandapower-json",
1455 SourceFormat::Pslf => "pslf",
1456 SourceFormat::PowerWorldBinary => "powerworld-pwb",
1457 SourceFormat::InMemory => "in-memory",
1458 SourceFormat::Normalized => "normalized",
1459 SourceFormat::Gridfm => "gridfm",
1460 SourceFormat::PypsaCsv => "pypsa-csv",
1461 SourceFormat::Goc3Json => "goc3-json",
1462 SourceFormat::SurgeJson => "surge-json",
1463 SourceFormat::DeepMindOpfDataJson => "opfdata-json",
1464 SourceFormat::Xiidm => "xiidm",
1465 SourceFormat::Jiidm => "jiidm",
1466 SourceFormat::Cgmes => "cgmes",
1467 SourceFormat::Ucte => "ucte",
1468 SourceFormat::IeeeCdf => "ieee-cdf",
1469 }
1470 }
1471}
1472
1473#[derive(Debug, Clone)]
1483pub struct BalancedNetwork {
1484 tables: std::sync::Arc<BalancedNetworkTables>,
1485}
1486
1487impl BalancedNetwork {
1488 pub(crate) fn from_tables(tables: BalancedNetworkTables) -> Self {
1489 Self {
1490 tables: std::sync::Arc::new(tables),
1491 }
1492 }
1493
1494 pub(crate) fn tables_mut(&mut self) -> &mut BalancedNetworkTables {
1497 std::sync::Arc::make_mut(&mut self.tables)
1498 }
1499}
1500
1501#[derive(Debug, Clone, Serialize, Deserialize)]
1508#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1509#[cfg_attr(feature = "schema", schemars(rename = "BalancedNetwork"))]
1510#[serde(remote = "Self")]
1511pub(crate) struct BalancedNetworkTables {
1512 pub name: String,
1513 pub base_mva: f64,
1514 #[serde(default = "default_base_frequency")]
1521 pub base_frequency: f64,
1522 #[serde(default, skip_serializing_if = "Option::is_none")]
1523 pub geo: Option<GeoMeta>,
1524 #[serde(default)]
1526 pub case_metadata: CaseMetadata,
1527 #[serde(default, skip_serializing_if = "Option::is_none")]
1530 pub detailed_connectivity: Option<std::sync::Arc<DetailedConnectivity>>,
1531 #[serde(default, skip_serializing_if = "generated_uids_is_empty")]
1535 pub generated_uids: std::sync::Arc<BTreeSet<String>>,
1536 pub buses: std::sync::Arc<Vec<Bus>>,
1537 pub loads: std::sync::Arc<Vec<Load>>,
1538 pub shunts: std::sync::Arc<Vec<Shunt>>,
1539 #[serde(default)]
1540 pub static_var_compensators: std::sync::Arc<Vec<StaticVarCompensator>>,
1541 pub branches: std::sync::Arc<Vec<Branch>>,
1542 #[serde(default)]
1543 pub switches: std::sync::Arc<Vec<Switch>>,
1544 pub generators: std::sync::Arc<Vec<Generator>>,
1545 pub storage: std::sync::Arc<Vec<Storage>>,
1546 pub hvdc: std::sync::Arc<Vec<Hvdc>>,
1547 #[serde(default)]
1556 pub transformers_3w: std::sync::Arc<Vec<Transformer3W>>,
1557 #[serde(default)]
1562 pub areas: std::sync::Arc<Vec<Area>>,
1563 #[serde(default)]
1566 pub solver: Option<SolverParams>,
1567 pub source_format: SourceFormat,
1568}
1569
1570impl Serialize for BalancedNetwork {
1571 fn serialize<S: serde::Serializer>(
1572 &self,
1573 serializer: S,
1574 ) -> std::result::Result<S::Ok, S::Error> {
1575 BalancedNetworkTables::serialize(
1576 &self.tables,
1577 powerio_core::__implementation::nonfinite::NonFiniteSer(serializer),
1578 )
1579 }
1580}
1581
1582impl<'de> Deserialize<'de> for BalancedNetwork {
1583 fn deserialize<D: serde::Deserializer<'de>>(
1584 deserializer: D,
1585 ) -> std::result::Result<Self, D::Error> {
1586 BalancedNetworkTables::deserialize(powerio_core::__implementation::nonfinite::NonFiniteDe(
1587 deserializer,
1588 ))
1589 .map(BalancedNetwork::from_tables)
1590 }
1591}
1592
1593#[cfg(feature = "schema")]
1594impl schemars::JsonSchema for BalancedNetwork {
1595 fn schema_name() -> std::borrow::Cow<'static, str> {
1596 <BalancedNetworkTables as schemars::JsonSchema>::schema_name()
1597 }
1598
1599 fn schema_id() -> std::borrow::Cow<'static, str> {
1600 <BalancedNetworkTables as schemars::JsonSchema>::schema_id()
1601 }
1602
1603 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1604 <BalancedNetworkTables as schemars::JsonSchema>::json_schema(generator)
1605 }
1606}
1607
1608macro_rules! table_accessors {
1609 ($($(#[$doc:meta])* $field:ident, $field_mut:ident: $ty:ty;)+) => {
1610 impl BalancedNetwork {
1611 $(
1612 $(#[$doc])*
1613 #[must_use]
1614 pub fn $field(&self) -> &$ty {
1615 &self.tables.$field
1616 }
1617
1618 #[must_use]
1622 pub fn $field_mut(&mut self) -> &mut $ty {
1623 &mut self.tables_mut().$field
1624 }
1625 )+
1626 }
1627 };
1628}
1629
1630table_accessors! {
1631 name, name_mut: String;
1633 geo, geo_mut: Option<GeoMeta>;
1635 case_metadata, case_metadata_mut: CaseMetadata;
1637 solver, solver_mut: Option<SolverParams>;
1639}
1640
1641impl BalancedNetwork {
1642 #[must_use]
1644 pub fn detailed_connectivity(&self) -> &Option<std::sync::Arc<DetailedConnectivity>> {
1645 &self.tables.detailed_connectivity
1646 }
1647
1648 #[must_use]
1652 pub fn detailed_connectivity_mut(
1653 &mut self,
1654 ) -> &mut Option<std::sync::Arc<DetailedConnectivity>> {
1655 let tables = self.tables_mut();
1656 if let Some(detailed) = tables.detailed_connectivity.as_mut()
1657 && !detailed.omitted_fields.is_empty()
1658 {
1659 std::sync::Arc::make_mut(detailed).omitted_fields.clear();
1660 }
1661 &mut tables.detailed_connectivity
1662 }
1663
1664 fn clear_omitted_fields(&mut self, component_type: &str, fields: &[OmittedFieldName]) {
1665 let Some(detailed) = self.tables_mut().detailed_connectivity.as_mut() else {
1666 return;
1667 };
1668 let affected = |omitted: &OmittedField| {
1669 omitted.component.component_type() == component_type && fields.contains(&omitted.field)
1670 };
1671 if detailed.omitted_fields.iter().any(affected) {
1673 std::sync::Arc::make_mut(detailed)
1674 .omitted_fields
1675 .retain(|omitted| !affected(omitted));
1676 }
1677 }
1678
1679 fn clear_omitted_field(&mut self, component: &ComponentId, field: OmittedFieldName) {
1680 let Some(detailed) = self.tables_mut().detailed_connectivity.as_mut() else {
1681 return;
1682 };
1683 let affected =
1684 |omitted: &OmittedField| omitted.component == *component && omitted.field == field;
1685 if detailed.omitted_fields.iter().any(affected) {
1686 std::sync::Arc::make_mut(detailed)
1687 .omitted_fields
1688 .retain(|omitted| !affected(omitted));
1689 }
1690 }
1691}
1692
1693macro_rules! shared_table_accessors {
1698 ($($(#[$doc:meta])* $field:ident, $field_mut:ident: $ty:ty;)+) => {
1699 impl BalancedNetwork {
1700 $(
1701 $(#[$doc])*
1702 #[must_use]
1703 pub fn $field(&self) -> &$ty {
1704 &self.tables.$field
1705 }
1706
1707 #[must_use]
1712 pub fn $field_mut(&mut self) -> &mut $ty {
1713 std::sync::Arc::make_mut(&mut self.tables_mut().$field)
1714 }
1715 )+
1716 }
1717 };
1718}
1719
1720impl BalancedNetwork {
1721 pub fn share_equal_tables(&mut self, donor: &Self) {
1727 macro_rules! share {
1728 ($($field:ident),+) => {
1729 $(
1730 if !std::sync::Arc::ptr_eq(&self.tables.$field, &donor.tables.$field)
1731 && self.tables.$field == donor.tables.$field
1732 {
1733 self.tables_mut().$field = donor.tables.$field.clone();
1734 }
1735 )+
1736 };
1737 }
1738 share!(
1739 buses,
1740 loads,
1741 shunts,
1742 static_var_compensators,
1743 branches,
1744 switches,
1745 generators,
1746 storage,
1747 hvdc,
1748 transformers_3w,
1749 areas
1750 );
1751 }
1752}
1753
1754shared_table_accessors! {
1755 buses, buses_mut: Vec<Bus>;
1756 static_var_compensators, static_var_compensators_mut: Vec<StaticVarCompensator>;
1757 branches, branches_mut: Vec<Branch>;
1758 switches, switches_mut: Vec<Switch>;
1759 hvdc, hvdc_mut: Vec<Hvdc>;
1760 transformers_3w, transformers_3w_mut: Vec<Transformer3W>;
1762 areas, areas_mut: Vec<Area>;
1764}
1765
1766impl BalancedNetwork {
1767 #[must_use]
1768 pub fn loads(&self) -> &Vec<Load> {
1769 &self.tables.loads
1770 }
1771
1772 #[must_use]
1775 pub fn loads_mut(&mut self) -> &mut Vec<Load> {
1776 self.clear_omitted_fields(
1777 "load",
1778 &[
1779 OmittedFieldName::ActivePower,
1780 OmittedFieldName::ReactivePower,
1781 ],
1782 );
1783 std::sync::Arc::make_mut(&mut self.tables_mut().loads)
1784 }
1785
1786 #[doc(hidden)]
1790 pub(crate) fn edit_load_assignment<R>(
1791 &mut self,
1792 index: usize,
1793 component: &ComponentId,
1794 field: OmittedFieldName,
1795 edit: impl FnOnce(&mut Load) -> R,
1796 ) -> R {
1797 debug_assert_eq!(component.component_type(), "load");
1798 debug_assert!(matches!(
1799 field,
1800 OmittedFieldName::ActivePower | OmittedFieldName::ReactivePower
1801 ));
1802 self.clear_omitted_field(component, field);
1803 edit(&mut std::sync::Arc::make_mut(&mut self.tables_mut().loads)[index])
1804 }
1805
1806 #[must_use]
1807 pub fn generators(&self) -> &Vec<Generator> {
1808 &self.tables.generators
1809 }
1810
1811 #[must_use]
1814 pub fn generators_mut(&mut self) -> &mut Vec<Generator> {
1815 self.clear_omitted_fields(
1816 "generator",
1817 &[
1818 OmittedFieldName::ActivePower,
1819 OmittedFieldName::ReactivePower,
1820 OmittedFieldName::VoltageSetpoint,
1821 OmittedFieldName::RatedApparentPower,
1822 ],
1823 );
1824 std::sync::Arc::make_mut(&mut self.tables_mut().generators)
1825 }
1826
1827 #[doc(hidden)]
1831 pub(crate) fn edit_generator_assignment<R>(
1832 &mut self,
1833 index: usize,
1834 component: &ComponentId,
1835 field: OmittedFieldName,
1836 edit: impl FnOnce(&mut Generator) -> R,
1837 ) -> R {
1838 debug_assert_eq!(component.component_type(), "generator");
1839 debug_assert!(matches!(
1840 field,
1841 OmittedFieldName::ActivePower
1842 | OmittedFieldName::ReactivePower
1843 | OmittedFieldName::VoltageSetpoint
1844 ));
1845 self.clear_omitted_field(component, field);
1846 edit(&mut std::sync::Arc::make_mut(&mut self.tables_mut().generators)[index])
1847 }
1848
1849 #[must_use]
1850 pub fn storage(&self) -> &Vec<Storage> {
1851 &self.tables.storage
1852 }
1853
1854 #[must_use]
1857 pub fn storage_mut(&mut self) -> &mut Vec<Storage> {
1858 self.clear_omitted_fields(
1859 "storage",
1860 &[
1861 OmittedFieldName::ActivePower,
1862 OmittedFieldName::ReactivePower,
1863 ],
1864 );
1865 std::sync::Arc::make_mut(&mut self.tables_mut().storage)
1866 }
1867
1868 #[must_use]
1869 pub fn shunts(&self) -> &Vec<Shunt> {
1870 &self.tables.shunts
1871 }
1872
1873 #[must_use]
1876 pub fn shunts_mut(&mut self) -> &mut Vec<Shunt> {
1877 self.clear_omitted_fields("shunt", &[OmittedFieldName::ShuntConductancePerSection]);
1878 std::sync::Arc::make_mut(&mut self.tables_mut().shunts)
1879 }
1880}
1881
1882impl BalancedNetwork {
1883 #[must_use]
1885 pub fn base_mva(&self) -> f64 {
1886 self.tables.base_mva
1887 }
1888
1889 #[must_use]
1890 pub fn base_mva_mut(&mut self) -> &mut f64 {
1891 &mut self.tables_mut().base_mva
1892 }
1893
1894 #[must_use]
1896 pub fn base_frequency(&self) -> f64 {
1897 self.tables.base_frequency
1898 }
1899
1900 #[must_use]
1901 pub fn base_frequency_mut(&mut self) -> &mut f64 {
1902 &mut self.tables_mut().base_frequency
1903 }
1904
1905 #[must_use]
1907 pub fn source_format(&self) -> SourceFormat {
1908 self.tables.source_format
1909 }
1910
1911 #[must_use]
1912 pub fn source_format_mut(&mut self) -> &mut SourceFormat {
1913 &mut self.tables_mut().source_format
1914 }
1915
1916 pub fn assign_missing_component_ids(&mut self) {
1934 let mut used = self.component_ids_in_use();
1935 let mut next_suffix = HashMap::new();
1936 macro_rules! assign {
1937 ($table:ident, $table_mut:ident, $set_uid:expr, $stem:expr) => {
1938 if self.$table().iter().any(|value| value.uid.is_none()) {
1939 let generated = assign_missing_ids(
1940 self.$table_mut(),
1941 |value| value.uid.as_deref(),
1942 $set_uid,
1943 $stem,
1944 &mut used,
1945 &mut next_suffix,
1946 );
1947 std::sync::Arc::make_mut(&mut self.tables_mut().generated_uids)
1948 .extend(generated);
1949 }
1950 };
1951 }
1952
1953 assign!(
1954 buses,
1955 buses_mut,
1956 |bus: &mut Bus, uid| bus.uid = Some(uid),
1957 |bus: &Bus| bus.id.to_string()
1958 );
1959 assign!(
1960 loads,
1961 loads_mut,
1962 |load: &mut Load, uid| load.uid = Some(uid),
1963 |load: &Load| format!("bus-{}", load.bus)
1964 );
1965 assign!(
1966 shunts,
1967 shunts_mut,
1968 |shunt: &mut Shunt, uid| shunt.uid = Some(uid),
1969 |shunt: &Shunt| format!("bus-{}", shunt.bus)
1970 );
1971 assign!(
1972 static_var_compensators,
1973 static_var_compensators_mut,
1974 |svc: &mut StaticVarCompensator, uid| svc.uid = Some(uid),
1975 |svc: &StaticVarCompensator| format!("bus-{}", svc.bus)
1976 );
1977 assign!(
1978 branches,
1979 branches_mut,
1980 |branch: &mut Branch, uid| branch.uid = Some(uid),
1981 |branch: &Branch| format!("{}-{}", branch.from, branch.to)
1982 );
1983 assign!(
1984 switches,
1985 switches_mut,
1986 |switch: &mut Switch, uid| switch.uid = Some(uid),
1987 |switch: &Switch| format!("{}-{}", switch.from, switch.to)
1988 );
1989 assign!(
1990 generators,
1991 generators_mut,
1992 |generator: &mut Generator, uid| generator.uid = Some(uid),
1993 |generator: &Generator| format!("bus-{}", generator.bus)
1994 );
1995 assign!(
1996 storage,
1997 storage_mut,
1998 |storage: &mut Storage, uid| storage.uid = Some(uid),
1999 |storage: &Storage| format!("bus-{}", storage.bus)
2000 );
2001 assign!(
2002 hvdc,
2003 hvdc_mut,
2004 |line: &mut Hvdc, uid| line.uid = Some(uid),
2005 |line: &Hvdc| format!("{}-{}", line.from, line.to)
2006 );
2007 assign!(
2008 transformers_3w,
2009 transformers_3w_mut,
2010 |transformer: &mut Transformer3W, uid| transformer.uid = Some(uid),
2011 |transformer: &Transformer3W| {
2012 let [first, second, third] = &transformer.windings;
2013 format!("{}-{}-{}", first.bus, second.bus, third.bus)
2014 }
2015 );
2016 }
2017
2018 #[must_use]
2020 pub fn uid_is_generated(&self, uid: Option<&str>) -> bool {
2021 uid.is_some_and(|uid| self.tables.generated_uids.contains(uid))
2022 }
2023
2024 pub(crate) fn generated_uids(&self) -> &std::sync::Arc<BTreeSet<String>> {
2025 &self.tables.generated_uids
2026 }
2027
2028 pub(crate) fn component_ids_in_use(&self) -> HashSet<String> {
2030 let mut used = HashSet::new();
2031 macro_rules! collect {
2032 ($($table:ident),+) => {
2033 $(used.extend(
2034 self.$table()
2035 .iter()
2036 .filter_map(|value| value.uid.as_deref().map(str::to_owned)),
2037 );)+
2038 };
2039 }
2040 collect!(
2041 buses,
2042 loads,
2043 shunts,
2044 static_var_compensators,
2045 branches,
2046 switches,
2047 generators,
2048 storage,
2049 hvdc,
2050 transformers_3w
2051 );
2052 used
2053 }
2054}
2055
2056fn generated_uids_is_empty(uids: &std::sync::Arc<BTreeSet<String>>) -> bool {
2057 uids.is_empty()
2058}
2059
2060fn assign_missing_ids<T>(
2061 values: &mut [T],
2062 uid: impl for<'a> Fn(&'a T) -> Option<&'a str>,
2063 mut set_uid: impl FnMut(&mut T, String),
2064 stem: impl Fn(&T) -> String,
2065 used: &mut HashSet<String>,
2066 next_suffix: &mut HashMap<String, usize>,
2067) -> Vec<String> {
2068 let mut generated = Vec::new();
2069 for value in values {
2070 if uid(value).is_some() {
2071 continue;
2072 }
2073 let stem = stem(value);
2074 let suffix = match next_suffix.get_mut(&stem) {
2075 Some(suffix) => suffix,
2076 None => next_suffix.entry(stem.clone()).or_insert(1),
2077 };
2078 loop {
2079 let candidate = if *suffix == 1 {
2080 stem.clone()
2081 } else {
2082 format!("{stem}-{suffix}")
2083 };
2084 *suffix += 1;
2085 if !used.contains(&candidate) {
2086 used.insert(candidate.clone());
2087 set_uid(value, candidate.clone());
2088 generated.push(candidate);
2089 break;
2090 }
2091 }
2092 }
2093 generated
2094}
2095
2096#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2097#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2098#[non_exhaustive]
2099pub struct Bus {
2100 pub id: BusId,
2102 pub kind: BusType,
2103 pub vm: f64,
2105 pub va: f64,
2107 pub base_kv: f64,
2108 pub vmax: f64,
2109 pub vmin: f64,
2110 #[serde(default)]
2116 pub evhi: Option<f64>,
2117 #[serde(default)]
2118 pub evlo: Option<f64>,
2119 pub area: usize,
2120 pub zone: usize,
2121 pub name: Option<String>,
2122 #[serde(default, skip_serializing_if = "Option::is_none")]
2127 pub uid: Option<String>,
2128 #[serde(default, skip_serializing_if = "Option::is_none")]
2130 pub location: Option<Location>,
2131 pub extras: Extras,
2132}
2133
2134impl Bus {
2135 #[must_use]
2136 pub fn new(id: BusId, kind: BusType, base_kv: f64) -> Self {
2137 Self {
2138 id,
2139 kind,
2140 vm: 1.0,
2141 va: 0.0,
2142 base_kv,
2143 vmax: 1.1,
2144 vmin: 0.9,
2145 evhi: None,
2146 evlo: None,
2147 area: 1,
2148 zone: 1,
2149 name: None,
2150 uid: None,
2151 location: None,
2152 extras: Extras::new(),
2153 }
2154 }
2155}
2156
2157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2158#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2159#[non_exhaustive]
2160pub struct Load {
2161 pub bus: BusId,
2162 pub p: f64,
2164 pub q: f64,
2166 #[serde(default)]
2168 pub voltage_model: Option<LoadVoltageModel>,
2169 pub in_service: bool,
2170 #[serde(default, skip_serializing_if = "Option::is_none")]
2172 pub uid: Option<String>,
2173 pub extras: Extras,
2174}
2175
2176impl Load {
2177 #[must_use]
2178 pub fn new(bus: BusId, p: f64, q: f64) -> Self {
2179 Self {
2180 bus,
2181 p,
2182 q,
2183 voltage_model: None,
2184 in_service: true,
2185 uid: None,
2186 extras: Extras::new(),
2187 }
2188 }
2189}
2190
2191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2193#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2194#[serde(tag = "kind", rename_all = "snake_case")]
2195#[non_exhaustive]
2196pub enum LoadVoltageModel {
2197 ConstantPower,
2199 Zip {
2202 p_constant_power: f64,
2203 q_constant_power: f64,
2204 p_constant_current: f64,
2205 q_constant_current: f64,
2206 p_constant_impedance: f64,
2207 q_constant_impedance: f64,
2208 #[serde(default)]
2209 v_nom: Option<f64>,
2210 #[serde(default)]
2213 load_type: Option<i32>,
2214 #[serde(default)]
2216 scaling: Option<f64>,
2217 },
2218 Exponential {
2221 p: f64,
2222 q: f64,
2223 #[serde(default)]
2224 v_nom: Option<f64>,
2225 gamma_p: f64,
2226 gamma_q: f64,
2227 },
2228}
2229
2230impl LoadVoltageModel {
2231 #[must_use]
2232 pub fn has_non_matpower_fields(&self) -> bool {
2233 match self {
2234 Self::ConstantPower => false,
2235 Self::Zip {
2236 p_constant_current,
2237 q_constant_current,
2238 p_constant_impedance,
2239 q_constant_impedance,
2240 v_nom,
2241 load_type,
2242 scaling,
2243 ..
2244 } => {
2245 *p_constant_current != 0.0
2246 || *q_constant_current != 0.0
2247 || *p_constant_impedance != 0.0
2248 || *q_constant_impedance != 0.0
2249 || v_nom.is_some()
2250 || load_type.is_some()
2251 || scaling.is_some()
2252 }
2253 Self::Exponential { .. } => true,
2254 }
2255 }
2256}
2257
2258#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2259#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2260#[non_exhaustive]
2261pub struct Shunt {
2262 pub bus: BusId,
2263 pub g: f64,
2265 pub b: f64,
2268 pub in_service: bool,
2269 #[serde(default, skip_serializing_if = "Option::is_none")]
2272 pub section_count: Option<u32>,
2273 #[serde(default)]
2277 pub control: Option<SwitchedShuntControl>,
2278 #[serde(default, skip_serializing_if = "Option::is_none")]
2280 pub uid: Option<String>,
2281 pub extras: Extras,
2282}
2283
2284impl Shunt {
2285 #[must_use]
2286 pub fn new(bus: BusId, g: f64, b: f64) -> Self {
2287 Self {
2288 bus,
2289 g,
2290 b,
2291 in_service: true,
2292 section_count: None,
2293 control: None,
2294 uid: None,
2295 extras: Extras::new(),
2296 }
2297 }
2298}
2299
2300#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2308#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2309#[serde(rename_all = "snake_case")]
2310#[non_exhaustive]
2311pub enum SwitchedShuntMode {
2312 Locked,
2314 #[cfg_attr(
2317 feature = "schema",
2318 schemars(description = "Continuous adjustment within the block range (`MODSW` 1).")
2319 )]
2320 Continuous,
2321 #[cfg_attr(
2328 feature = "schema",
2329 schemars(description = "Discrete adjustment in fixed steps (`MODSW` 2 and up).")
2330 )]
2331 Discrete,
2332}
2333
2334#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2336#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2337#[non_exhaustive]
2338pub struct ShuntBlock {
2339 pub steps: u32,
2340 pub g: f64,
2342 pub b: f64,
2344}
2345
2346impl ShuntBlock {
2347 #[must_use]
2348 pub const fn new(steps: u32, b: f64) -> Self {
2349 Self { steps, g: 0.0, b }
2350 }
2351
2352 #[must_use]
2353 pub const fn with_admittance(steps: u32, g: f64, b: f64) -> Self {
2354 Self { steps, g, b }
2355 }
2356}
2357
2358#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2363#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2364#[non_exhaustive]
2365pub struct SwitchedShuntControl {
2366 pub mode: SwitchedShuntMode,
2367 pub vhigh: f64,
2369 pub vlow: f64,
2370 pub control_bus: Option<BusId>,
2372 #[serde(default, skip_serializing_if = "Option::is_none")]
2374 pub regulating_terminal: Option<TerminalReference>,
2375 pub rmpct: f64,
2377 pub blocks: Vec<ShuntBlock>,
2378}
2379
2380impl SwitchedShuntControl {
2381 #[must_use]
2382 pub fn new(mode: SwitchedShuntMode, vhigh: f64, vlow: f64, blocks: Vec<ShuntBlock>) -> Self {
2383 Self {
2384 mode,
2385 vhigh,
2386 vlow,
2387 control_bus: None,
2388 regulating_terminal: None,
2389 rmpct: 100.0,
2390 blocks,
2391 }
2392 }
2393}
2394
2395#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
2397#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2398#[serde(rename_all = "snake_case")]
2399#[non_exhaustive]
2400pub enum StaticVarCompensatorRegulationMode {
2401 Voltage,
2402 ReactivePower,
2403}
2404
2405#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2407#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2408#[non_exhaustive]
2409pub struct StaticVarCompensator {
2410 pub bus: BusId,
2411 pub b_min_siemens: f64,
2413 pub b_max_siemens: f64,
2414 pub voltage_setpoint_kv: f64,
2415 pub reactive_power_setpoint_mvar: f64,
2416 pub regulation_mode: StaticVarCompensatorRegulationMode,
2417 pub regulating: bool,
2418 #[serde(default, skip_serializing_if = "Option::is_none")]
2419 pub regulating_terminal: Option<TerminalReference>,
2420 pub p: f64,
2422 pub q: f64,
2423 pub in_service: bool,
2424 #[serde(default, skip_serializing_if = "Option::is_none")]
2425 pub uid: Option<String>,
2426 pub extras: Extras,
2427}
2428
2429impl StaticVarCompensator {
2430 #[must_use]
2431 pub fn new(bus: BusId, b_min_siemens: f64, b_max_siemens: f64) -> Self {
2432 Self {
2433 bus,
2434 b_min_siemens,
2435 b_max_siemens,
2436 voltage_setpoint_kv: 0.0,
2437 reactive_power_setpoint_mvar: 0.0,
2438 regulation_mode: StaticVarCompensatorRegulationMode::Voltage,
2439 regulating: false,
2440 regulating_terminal: None,
2441 p: 0.0,
2442 q: 0.0,
2443 in_service: true,
2444 uid: None,
2445 extras: Extras::new(),
2446 }
2447 }
2448}
2449
2450#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2451#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2452#[non_exhaustive]
2453pub struct Branch {
2454 #[serde(default, skip_serializing_if = "Option::is_none")]
2456 pub name: Option<String>,
2457 pub from: BusId,
2458 pub to: BusId,
2459 pub r: f64,
2461 pub x: f64,
2463 pub b: f64,
2467 #[serde(default)]
2470 pub charging: Option<BranchCharging>,
2471 pub rate_a: f64,
2472 pub rate_b: f64,
2473 pub rate_c: f64,
2474 #[serde(default)]
2477 pub rating_sets: Vec<BranchRatingSet>,
2478 #[serde(default)]
2480 pub current_ratings: Option<BranchCurrentRatings>,
2481 pub tap: f64,
2483 pub shift: f64,
2485 pub in_service: bool,
2486 pub angmin: f64,
2487 pub angmax: f64,
2488 #[serde(default)]
2493 pub control: Option<TransformerControl>,
2494 #[serde(default)]
2496 pub solution: Option<BranchSolution>,
2497 #[serde(default, skip_serializing_if = "Option::is_none")]
2499 pub uid: Option<String>,
2500 #[serde(default, skip_serializing_if = "Option::is_none")]
2505 pub route: Option<Vec<Location>>,
2506 pub extras: Extras,
2507}
2508
2509pub(crate) const IMPEDANCE_CORRECTION_EXTRA: &str = "impedance_correction";
2513pub(crate) const IMPEDANCE_CORRECTION_NOMINAL_EXTRA: &str = "impedance_correction_nominal";
2516pub(crate) const IMPEDANCE_CORRECTION_APPLIED_EXTRA: &str = "impedance_correction_applied";
2520
2521pub(crate) fn current_correction_nominal<T>(extras: &Extras, current: &T) -> Option<T>
2528where
2529 T: PartialEq + serde::de::DeserializeOwned,
2530{
2531 let nominal = T::deserialize(extras.get(IMPEDANCE_CORRECTION_NOMINAL_EXTRA)?).ok()?;
2532 let applied = extras
2533 .get(IMPEDANCE_CORRECTION_APPLIED_EXTRA)
2534 .and_then(|value| T::deserialize(value).ok());
2535 applied
2536 .is_none_or(|applied| applied == *current)
2537 .then_some(nominal)
2538}
2539
2540pub(crate) fn correction_nominal_edited<T>(extras: &Extras, current: &T) -> bool
2543where
2544 T: PartialEq + serde::de::DeserializeOwned,
2545{
2546 extras.contains_key(IMPEDANCE_CORRECTION_NOMINAL_EXTRA)
2547 && current_correction_nominal(extras, current).is_none()
2548}
2549
2550#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2556#[non_exhaustive]
2557pub struct ImpedanceCorrection {
2558 pub table: u32,
2560 pub variable: ImpedanceCorrectionVariable,
2562 pub points: Vec<ImpedanceCorrectionPoint>,
2564}
2565
2566#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2568#[serde(rename_all = "snake_case")]
2569#[non_exhaustive]
2570pub enum ImpedanceCorrectionVariable {
2571 Ratio,
2573 Angle,
2575}
2576
2577#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
2581#[non_exhaustive]
2582pub struct ImpedanceCorrectionPoint {
2583 pub value: f64,
2585 pub factor_re: f64,
2586 pub factor_im: f64,
2587}
2588
2589impl ImpedanceCorrectionPoint {
2590 #[must_use]
2591 pub fn new(value: f64, factor_re: f64, factor_im: f64) -> Self {
2592 Self {
2593 value,
2594 factor_re,
2595 factor_im,
2596 }
2597 }
2598}
2599
2600impl ImpedanceCorrection {
2601 #[must_use]
2602 pub fn new(
2603 table: u32,
2604 variable: ImpedanceCorrectionVariable,
2605 points: Vec<ImpedanceCorrectionPoint>,
2606 ) -> Self {
2607 Self {
2608 table,
2609 variable,
2610 points,
2611 }
2612 }
2613
2614 #[must_use]
2619 pub fn calc_factor(&self, value: f64) -> Option<(f64, f64)> {
2620 let mut points = self.points.clone();
2621 points.sort_by(|a, b| a.value.total_cmp(&b.value));
2622 let first = points.first()?;
2623 let last = points.last()?;
2624 if value <= first.value {
2625 return Some((first.factor_re, first.factor_im));
2626 }
2627 if value >= last.value {
2628 return Some((last.factor_re, last.factor_im));
2629 }
2630 let upper = points.iter().position(|point| point.value >= value)?;
2631 let (a, b) = (points[upper - 1], points[upper]);
2632 let weight = (value - a.value) / (b.value - a.value);
2633 let lerp = |x: f64, y: f64| x + weight * (y - x);
2634 Some((
2635 lerp(a.factor_re, b.factor_re),
2636 lerp(a.factor_im, b.factor_im),
2637 ))
2638 }
2639}
2640
2641#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2643#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2644#[non_exhaustive]
2645pub struct BranchRatingSet {
2646 pub name: String,
2647 pub rate_mva: f64,
2648}
2649
2650impl BranchRatingSet {
2651 #[must_use]
2652 pub fn new(name: impl Into<String>, rate_mva: f64) -> Self {
2653 Self {
2654 name: name.into(),
2655 rate_mva,
2656 }
2657 }
2658}
2659
2660#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
2663#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2664#[non_exhaustive]
2665pub struct BranchCharging {
2666 pub g_fr: f64,
2667 pub b_fr: f64,
2668 pub g_to: f64,
2669 pub b_to: f64,
2670}
2671
2672impl BranchCharging {
2673 #[must_use]
2674 pub const fn new(g_fr: f64, b_fr: f64, g_to: f64, b_to: f64) -> Self {
2675 Self {
2676 g_fr,
2677 b_fr,
2678 g_to,
2679 b_to,
2680 }
2681 }
2682
2683 #[must_use]
2684 pub fn from_total_b(b: f64) -> Self {
2685 Self {
2686 g_fr: 0.0,
2687 b_fr: b / 2.0,
2688 g_to: 0.0,
2689 b_to: b / 2.0,
2690 }
2691 }
2692
2693 #[must_use]
2694 pub fn calc_total_b(self) -> f64 {
2695 self.b_fr + self.b_to
2696 }
2697
2698 #[must_use]
2699 pub fn calc_total_g(self) -> f64 {
2700 self.g_fr + self.g_to
2701 }
2702
2703 #[must_use]
2704 pub fn is_matpower_symmetric(self) -> bool {
2705 self.g_fr.abs() <= f64::EPSILON
2706 && self.g_to.abs() <= f64::EPSILON
2707 && (self.b_fr - self.b_to).abs() <= f64::EPSILON
2708 }
2709}
2710
2711#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
2713#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2714#[non_exhaustive]
2715pub struct BranchCurrentRatings {
2716 pub c_rating_a: f64,
2717 pub c_rating_b: f64,
2718 pub c_rating_c: f64,
2719}
2720
2721impl BranchCurrentRatings {
2722 #[must_use]
2723 pub const fn new(c_rating_a: f64, c_rating_b: f64, c_rating_c: f64) -> Self {
2724 Self {
2725 c_rating_a,
2726 c_rating_b,
2727 c_rating_c,
2728 }
2729 }
2730}
2731
2732#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
2734#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2735#[non_exhaustive]
2736pub struct BranchSolution {
2737 pub pf: f64,
2738 pub qf: f64,
2739 pub pt: f64,
2740 pub qt: f64,
2741}
2742
2743impl BranchSolution {
2744 #[must_use]
2745 pub const fn new(pf: f64, qf: f64, pt: f64, qt: f64) -> Self {
2746 Self { pf, qf, pt, qt }
2747 }
2748}
2749
2750impl Branch {
2751 #[must_use]
2752 pub fn new(from: BusId, to: BusId, r: f64, x: f64) -> Self {
2753 Self {
2754 name: None,
2755 from,
2756 to,
2757 r,
2758 x,
2759 b: 0.0,
2760 charging: None,
2761 rate_a: 0.0,
2762 rate_b: 0.0,
2763 rate_c: 0.0,
2764 rating_sets: Vec::new(),
2765 current_ratings: None,
2766 tap: 0.0,
2767 shift: 0.0,
2768 in_service: true,
2769 angmin: -360.0,
2770 angmax: 360.0,
2771 control: None,
2772 solution: None,
2773 uid: None,
2774 route: None,
2775 extras: Extras::new(),
2776 }
2777 }
2778
2779 #[must_use]
2781 pub fn calc_effective_tap(&self) -> f64 {
2782 if self.tap == 0.0 { 1.0 } else { self.tap }
2783 }
2784
2785 pub fn calc_divisible_tap(&self, row: usize) -> Result<f64> {
2794 let tap = self.calc_effective_tap();
2795 if !tap.is_finite() || tap.abs() < crate::dc::MIN_DIVISIBLE_MAGNITUDE {
2796 return Err(Error::DegenerateTap { row, tap });
2797 }
2798 Ok(tap)
2799 }
2800
2801 #[must_use]
2804 pub fn calc_terminal_charging(&self) -> BranchCharging {
2805 self.charging
2806 .unwrap_or_else(|| BranchCharging::from_total_b(self.b))
2807 }
2808
2809 pub fn calc_series_admittance(&self, row: usize) -> Result<Option<(f64, f64)>> {
2821 calc_series_admittance_of(self.r, self.x, row)
2822 }
2823
2824 #[must_use]
2850 pub fn synthesize_rate_a(
2851 &self,
2852 angle_window_rad: f64,
2853 (fr_vmin, fr_vmax): (f64, f64),
2854 (to_vmin, to_vmax): (f64, f64),
2855 ) -> f64 {
2856 let zmag = self.r.hypot(self.x);
2859 if zmag < crate::dc::MIN_DIVISIBLE_MAGNITUDE {
2860 return 0.0;
2861 }
2862 let window = angle_window_rad.abs().min(std::f64::consts::PI);
2863 let cos_window = window.cos();
2864 let separation = |vf: f64, vt: f64| {
2868 (vf * vf + vt * vt - 2.0 * vf * vt * cos_window)
2869 .max(0.0)
2870 .sqrt()
2871 };
2872 let widest = separation(fr_vmax, to_vmax)
2873 .max(separation(fr_vmax, to_vmin))
2874 .max(separation(fr_vmin, to_vmax))
2875 .max(separation(fr_vmin, to_vmin));
2876 fr_vmax.max(to_vmax) * widest / zmag
2877 }
2878
2879 #[must_use]
2882 pub fn calc_total_charging_b(&self) -> f64 {
2883 self.calc_terminal_charging().calc_total_b()
2884 }
2885
2886 #[must_use]
2888 pub fn has_non_matpower_charging(&self) -> bool {
2889 self.charging
2890 .is_some_and(|charging| !charging.is_matpower_symmetric())
2891 }
2892
2893 #[must_use]
2896 pub fn is_transformer(&self) -> bool {
2897 self.tap != 0.0 || self.shift != 0.0
2898 }
2899
2900 #[must_use]
2904 pub fn has_angle_limits(&self) -> bool {
2905 self.angmin > -360.0 || self.angmax < 360.0
2906 }
2907
2908 #[must_use]
2913 pub fn impedance_correction(&self) -> Option<ImpedanceCorrection> {
2914 self.extras
2915 .get(IMPEDANCE_CORRECTION_EXTRA)
2916 .and_then(|value| ImpedanceCorrection::deserialize(value).ok())
2917 }
2918
2919 pub fn set_impedance_correction(&mut self, correction: Option<ImpedanceCorrection>) {
2921 set_impedance_correction_extra(&mut self.extras, correction);
2922 }
2923}
2924
2925fn set_impedance_correction_extra(extras: &mut Extras, correction: Option<ImpedanceCorrection>) {
2926 match correction {
2927 Some(correction) => {
2928 let value = serde_json::to_value(correction).expect("a correction serializes to JSON");
2929 extras.insert(IMPEDANCE_CORRECTION_EXTRA.to_owned(), value);
2930 }
2931 None => {
2932 extras.remove(IMPEDANCE_CORRECTION_EXTRA);
2933 }
2934 }
2935}
2936
2937pub fn calc_series_admittance_of(r: f64, x: f64, row: usize) -> Result<Option<(f64, f64)>> {
2954 let magnitude = r.hypot(x);
2955 if magnitude < crate::dc::MIN_DIVISIBLE_MAGNITUDE {
2956 return Ok(None);
2957 }
2958 if !magnitude.is_finite() {
2959 return Err(Error::NonFiniteSusceptance { row });
2960 }
2961 Ok(Some(crate::dc::series_admittance_parts(r, x)))
2962}
2963
2964#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
2967#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
2968#[non_exhaustive]
2969pub struct Switch {
2970 pub from: BusId,
2971 pub to: BusId,
2972 pub closed: bool,
2973 #[serde(default)]
2974 pub thermal_rating: Option<f64>,
2975 #[serde(default)]
2976 pub current_rating: Option<f64>,
2977 #[serde(default)]
2978 pub pf: Option<f64>,
2979 #[serde(default)]
2980 pub qf: Option<f64>,
2981 #[serde(default)]
2982 pub pt: Option<f64>,
2983 #[serde(default)]
2984 pub qt: Option<f64>,
2985 #[serde(default, skip_serializing_if = "Option::is_none")]
2987 pub uid: Option<String>,
2988 pub extras: Extras,
2989}
2990
2991impl Switch {
2992 #[must_use]
2993 pub fn new(from: BusId, to: BusId, closed: bool) -> Self {
2994 Self {
2995 from,
2996 to,
2997 closed,
2998 thermal_rating: None,
2999 current_rating: None,
3000 pf: None,
3001 qf: None,
3002 pt: None,
3003 qt: None,
3004 uid: None,
3005 extras: Extras::new(),
3006 }
3007 }
3008}
3009
3010#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
3013#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3014#[serde(rename_all = "snake_case")]
3015#[non_exhaustive]
3016pub enum TransformerControlMode {
3017 Fixed,
3019 Voltage,
3021 ReactiveFlow,
3023 ActiveFlow,
3025 DcLineQuantity,
3027 AsymmetricActiveFlow,
3029}
3030
3031#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3041#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3042#[non_exhaustive]
3043pub struct TransformerControl {
3044 pub mode: TransformerControlMode,
3045 pub enabled: bool,
3048 pub controlled_bus: Option<BusId>,
3049 #[serde(default)]
3052 pub controlled_bus_on_winding_side: bool,
3053 #[serde(default, skip_serializing_if = "Option::is_none")]
3055 pub regulating_terminal: Option<TerminalReference>,
3056 pub tap_min: f64,
3057 pub tap_max: f64,
3058 pub band_min: f64,
3059 pub band_max: f64,
3060 pub ntp: u32,
3061 pub mva_base: f64,
3062 #[serde(default, skip_serializing_if = "Option::is_none")]
3065 pub winding_connection_angle: Option<f64>,
3066}
3067
3068impl Default for TransformerControl {
3069 fn default() -> Self {
3070 TransformerControl {
3072 mode: TransformerControlMode::Fixed,
3073 enabled: false,
3074 controlled_bus: None,
3075 controlled_bus_on_winding_side: false,
3076 regulating_terminal: None,
3077 tap_min: 0.9,
3078 tap_max: 1.1,
3079 band_min: 0.9,
3080 band_max: 1.1,
3081 ntp: 33,
3082 mva_base: 0.0,
3083 winding_connection_angle: None,
3084 }
3085 }
3086}
3087
3088impl TransformerControl {
3089 #[must_use]
3090 pub fn new(mode: TransformerControlMode) -> Self {
3091 Self {
3092 mode,
3093 enabled: mode != TransformerControlMode::Fixed,
3094 ..Self::default()
3095 }
3096 }
3097}
3098
3099#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3109#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3110#[non_exhaustive]
3111pub struct ActivePowerControl {
3112 pub participate: bool,
3113 #[serde(default, skip_serializing_if = "Option::is_none")]
3114 pub droop_percent: Option<f64>,
3115 #[serde(default, skip_serializing_if = "Option::is_none")]
3116 pub participation_factor: Option<f64>,
3117 #[serde(default, skip_serializing_if = "Option::is_none")]
3118 pub minimum_target_active_power_mw: Option<f64>,
3119 #[serde(default, skip_serializing_if = "Option::is_none")]
3120 pub maximum_target_active_power_mw: Option<f64>,
3121}
3122
3123impl ActivePowerControl {
3124 #[must_use]
3125 pub fn new(participate: bool) -> Self {
3126 Self {
3127 participate,
3128 droop_percent: None,
3129 participation_factor: None,
3130 minimum_target_active_power_mw: None,
3131 maximum_target_active_power_mw: None,
3132 }
3133 }
3134}
3135
3136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3137#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3138#[non_exhaustive]
3139pub struct Generator {
3140 pub bus: BusId,
3141 #[serde(default)]
3143 pub energy_source: GeneratorEnergySource,
3144 pub pg: f64,
3146 pub qg: f64,
3148 pub pmax: f64,
3149 pub pmin: f64,
3150 pub qmax: f64,
3151 pub qmin: f64,
3152 pub vg: f64,
3154 pub mbase: f64,
3155 pub in_service: bool,
3156 pub cost: Option<GenCost>,
3157 #[serde(default = "default_caps", with = "caps_serde")]
3166 #[cfg_attr(feature = "schema", schemars(with = "BTreeMap<String, f64>"))]
3167 pub caps: GenCaps,
3168 #[serde(default = "default_voltage_regulation_on")]
3172 pub voltage_regulation_on: bool,
3173 #[serde(default, skip_serializing_if = "Option::is_none")]
3177 pub regulating_terminal: Option<TerminalReference>,
3178 #[serde(default)]
3185 pub regulated_bus: Option<BusId>,
3186 #[serde(default, skip_serializing_if = "Option::is_none")]
3188 pub active_power_control: Option<ActivePowerControl>,
3189 #[serde(default, skip_serializing_if = "Option::is_none")]
3191 pub uid: Option<String>,
3192}
3193
3194impl Generator {
3195 #[must_use]
3196 pub fn new(bus: BusId) -> Self {
3197 Self {
3198 bus,
3199 energy_source: GeneratorEnergySource::Other,
3200 pg: 0.0,
3201 qg: 0.0,
3202 pmax: 0.0,
3203 pmin: 0.0,
3204 qmax: 0.0,
3205 qmin: 0.0,
3206 vg: 1.0,
3207 mbase: 0.0,
3208 in_service: true,
3209 cost: None,
3210 caps: default_caps(),
3211 voltage_regulation_on: true,
3212 regulating_terminal: None,
3213 regulated_bus: None,
3214 active_power_control: None,
3215 uid: None,
3216 }
3217 }
3218
3219 #[must_use]
3222 pub fn has_caps(&self) -> bool {
3223 self.caps.iter().any(Option::is_some)
3224 }
3225}
3226
3227#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Hash)]
3229#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3230#[serde(rename_all = "snake_case")]
3231#[non_exhaustive]
3232pub enum GeneratorEnergySource {
3233 Hydro,
3234 Nuclear,
3235 Wind,
3236 Thermal,
3237 Solar,
3238 #[default]
3239 Other,
3240}
3241
3242const fn default_voltage_regulation_on() -> bool {
3243 true
3244}
3245
3246pub type GenCaps = [Option<f64>; GEN_EXTRA_KEYS.len()];
3248
3249fn default_caps() -> GenCaps {
3251 [None; GEN_EXTRA_KEYS.len()]
3252}
3253
3254mod caps_serde {
3265 use super::{GEN_EXTRA_KEYS, GenCaps};
3266 use serde::de::{Deserialize, Deserializer};
3267 use serde::ser::{SerializeMap, Serializer};
3268 use std::collections::BTreeMap;
3269
3270 pub(super) fn serialize<S: Serializer>(caps: &GenCaps, s: S) -> Result<S::Ok, S::Error> {
3271 let present = caps.iter().filter(|v| v.is_some()).count();
3272 let mut map = s.serialize_map(Some(present))?;
3273 for (key, slot) in GEN_EXTRA_KEYS.iter().zip(caps.iter()) {
3274 if let Some(value) = slot {
3275 map.serialize_entry(key, value)?;
3276 }
3277 }
3278 map.end()
3279 }
3280
3281 pub(super) fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<GenCaps, D::Error> {
3282 let named = Option::<BTreeMap<String, f64>>::deserialize(d)?.unwrap_or_default();
3287 let mut caps: GenCaps = [None; GEN_EXTRA_KEYS.len()];
3288 for (slot, key) in caps.iter_mut().zip(GEN_EXTRA_KEYS.iter()) {
3289 *slot = named.get(*key).copied();
3290 }
3291 Ok(caps)
3292 }
3293}
3294
3295#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3296#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3297#[non_exhaustive]
3298pub struct Storage {
3299 pub bus: BusId,
3300 pub ps: f64,
3301 pub qs: f64,
3302 pub energy: f64,
3303 pub energy_rating: f64,
3304 pub charge_rating: f64,
3305 pub discharge_rating: f64,
3306 pub charge_efficiency: f64,
3307 pub discharge_efficiency: f64,
3308 pub thermal_rating: f64,
3309 #[serde(default)]
3310 pub current_rating: Option<f64>,
3311 pub qmin: f64,
3312 pub qmax: f64,
3313 pub r: f64,
3314 pub x: f64,
3315 pub p_loss: f64,
3316 pub q_loss: f64,
3317 pub in_service: bool,
3318 #[serde(default, skip_serializing_if = "Option::is_none")]
3320 pub active_power_control: Option<ActivePowerControl>,
3321 #[serde(default, skip_serializing_if = "Option::is_none")]
3323 pub uid: Option<String>,
3324 pub extras: Extras,
3325}
3326
3327impl Storage {
3328 #[must_use]
3329 pub fn new(bus: BusId) -> Self {
3330 Self {
3331 bus,
3332 ps: 0.0,
3333 qs: 0.0,
3334 energy: 0.0,
3335 energy_rating: 0.0,
3336 charge_rating: 0.0,
3337 discharge_rating: 0.0,
3338 charge_efficiency: 1.0,
3339 discharge_efficiency: 1.0,
3340 thermal_rating: 0.0,
3341 current_rating: None,
3342 qmin: 0.0,
3343 qmax: 0.0,
3344 r: 0.0,
3345 x: 0.0,
3346 p_loss: 0.0,
3347 q_loss: 0.0,
3348 in_service: true,
3349 active_power_control: None,
3350 uid: None,
3351 extras: Extras::new(),
3352 }
3353 }
3354}
3355
3356#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
3364#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3365#[serde(rename_all = "snake_case")]
3366#[non_exhaustive]
3367pub enum HvdcConvertersMode {
3368 Side1RectifierSide2Inverter,
3369 Side1InverterSide2Rectifier,
3370}
3371
3372#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
3374#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3375#[serde(rename_all = "snake_case")]
3376#[non_exhaustive]
3377pub enum HvdcConverterKind {
3378 Vsc,
3379 Lcc,
3380}
3381
3382#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3384#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3385#[non_exhaustive]
3386pub struct HvdcConverter {
3387 pub component: ComponentId,
3388 pub kind: HvdcConverterKind,
3389 pub loss_factor_percent: f64,
3391 #[serde(default, skip_serializing_if = "Option::is_none")]
3392 pub voltage_regulator_on: Option<bool>,
3393 #[serde(default, skip_serializing_if = "Option::is_none")]
3394 pub voltage_setpoint_kv: Option<f64>,
3395 #[serde(default, skip_serializing_if = "Option::is_none")]
3396 pub reactive_power_setpoint_mvar: Option<f64>,
3397 #[serde(default, skip_serializing_if = "Option::is_none")]
3398 pub power_factor: Option<f64>,
3399 #[serde(default, skip_serializing_if = "Option::is_none")]
3400 pub regulating_terminal: Option<TerminalReference>,
3401}
3402
3403#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3404#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3405#[non_exhaustive]
3406pub struct Hvdc {
3407 pub from: BusId,
3408 pub to: BusId,
3409 pub in_service: bool,
3410 pub pf: f64,
3411 pub pt: f64,
3412 pub qf: f64,
3413 pub qt: f64,
3414 pub vf: f64,
3415 pub vt: f64,
3416 pub pmin: f64,
3417 pub pmax: f64,
3418 pub qminf: f64,
3419 pub qmaxf: f64,
3420 pub qmint: f64,
3421 pub qmaxt: f64,
3422 pub loss0: f64,
3423 pub loss1: f64,
3424 #[serde(default, skip_serializing_if = "Option::is_none")]
3427 pub resistance_ohm: Option<f64>,
3428 #[serde(default, skip_serializing_if = "Option::is_none")]
3429 pub nominal_voltage_kv: Option<f64>,
3430 #[serde(default, skip_serializing_if = "Option::is_none")]
3431 pub converters_mode: Option<HvdcConvertersMode>,
3432 #[serde(default, skip_serializing_if = "Option::is_none")]
3433 pub converter1: Option<HvdcConverter>,
3434 #[serde(default, skip_serializing_if = "Option::is_none")]
3435 pub converter2: Option<HvdcConverter>,
3436 #[serde(default)]
3437 pub cost: Option<GenCost>,
3438 #[serde(default, skip_serializing_if = "Option::is_none")]
3440 pub uid: Option<String>,
3441 pub extras: Extras,
3442}
3443
3444impl Hvdc {
3445 #[must_use]
3454 pub fn calc_delivered_power(pf: f64, loss0: f64, loss1: f64) -> f64 {
3455 pf - loss0 - loss1 * pf
3456 }
3457
3458 #[must_use]
3462 pub fn pt_matches_loss_model(&self, tol: f64) -> bool {
3463 (self.pt - Self::calc_delivered_power(self.pf, self.loss0, self.loss1)).abs() <= tol
3464 }
3465
3466 #[must_use]
3467 pub fn new(from: BusId, to: BusId) -> Self {
3468 Self {
3469 from,
3470 to,
3471 in_service: true,
3472 pf: 0.0,
3473 pt: 0.0,
3474 qf: 0.0,
3475 qt: 0.0,
3476 vf: 1.0,
3477 vt: 1.0,
3478 pmin: 0.0,
3479 pmax: 0.0,
3480 qminf: 0.0,
3481 qmaxf: 0.0,
3482 qmint: 0.0,
3483 qmaxt: 0.0,
3484 loss0: 0.0,
3485 loss1: 0.0,
3486 resistance_ohm: None,
3487 nominal_voltage_kv: None,
3488 converters_mode: None,
3489 converter1: None,
3490 converter2: None,
3491 cost: None,
3492 uid: None,
3493 extras: Extras::new(),
3494 }
3495 }
3496}
3497
3498#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3505#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3506#[non_exhaustive]
3507pub struct Area {
3508 pub number: usize,
3509 pub slack_bus: Option<BusId>,
3511 pub net_interchange: f64,
3513 pub tolerance: f64,
3515 pub name: Option<String>,
3516 #[serde(default, skip_serializing_if = "Option::is_none")]
3519 pub uid: Option<String>,
3520 #[serde(default, skip_serializing_if = "Option::is_none")]
3523 pub area_type: Option<String>,
3524}
3525
3526impl Area {
3527 #[must_use]
3528 pub fn new(number: usize) -> Self {
3529 Self {
3530 number,
3531 slack_bus: None,
3532 net_interchange: 0.0,
3533 tolerance: 0.0,
3534 name: None,
3535 uid: None,
3536 area_type: None,
3537 }
3538 }
3539}
3540
3541#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
3550#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3551#[non_exhaustive]
3552pub struct SolverParams {
3553 pub newton_tolerance: Option<f64>,
3555 pub max_iterations: Option<u32>,
3557 pub zero_impedance_threshold: Option<f64>,
3559 pub adjust_taps: Option<bool>,
3561 pub adjust_area_interchange: Option<bool>,
3563 pub adjust_phase_shift: Option<bool>,
3565 pub adjust_dc_taps: Option<bool>,
3567 pub adjust_switched_shunt: Option<bool>,
3569}
3570
3571impl SolverParams {
3572 #[must_use]
3573 pub fn new() -> Self {
3574 Self::default()
3575 }
3576
3577 #[must_use]
3579 pub fn is_empty(&self) -> bool {
3580 *self == SolverParams::default()
3581 }
3582}
3583
3584#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
3596#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3597#[non_exhaustive]
3598pub struct Impedance {
3599 pub r: f64,
3600 pub x: f64,
3601 pub base_mva: f64,
3602}
3603
3604impl Impedance {
3605 #[must_use]
3606 pub const fn new(r: f64, x: f64, base_mva: f64) -> Self {
3607 Self { r, x, base_mva }
3608 }
3609}
3610
3611pub(crate) const WINDING_IN_SERVICE_EXTRA: &str = "winding_in_service";
3614pub(crate) const WINDING_RATING_SETS_EXTRA: &str = "winding_rating_sets";
3617
3618#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
3621#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
3622#[non_exhaustive]
3623pub struct Winding {
3624 pub bus: BusId,
3625 pub tap: f64,
3627 pub shift: f64,
3629 pub nominal_kv: f64,
3631 pub rate_a: f64,
3632 pub rate_b: f64,
3633 pub rate_c: f64,
3634 #[serde(default, skip_serializing_if = "Option::is_none")]
3636 pub control: Option<TransformerControl>,
3637}
3638
3639impl Winding {
3640 #[must_use]
3641 pub fn new(bus: BusId) -> Self {
3642 Self {
3643 bus,
3644 tap: 1.0,
3645 shift: 0.0,
3646 nominal_kv: 0.0,
3647 rate_a: 0.0,
3648 rate_b: 0.0,
3649 rate_c: 0.0,
3650 control: None,
3651 }
3652 }
3653}
3654
3655#[derive(Debug, Clone, PartialEq)]
3665#[non_exhaustive]
3666pub struct Transformer3W {
3667 pub windings: [Winding; 3],
3669 pub z: [Impedance; 3],
3673 pub star_vm: f64,
3675 pub star_va: f64,
3676 pub mag_g: f64,
3678 pub mag_b: f64,
3679 pub in_service: bool,
3680 pub name: Option<String>,
3681 pub uid: Option<String>,
3683 pub extras: Extras,
3684}
3685
3686impl Transformer3W {
3687 #[must_use]
3688 pub fn new(windings: [Winding; 3], z: [Impedance; 3]) -> Self {
3689 Self {
3690 windings,
3691 z,
3692 star_vm: 1.0,
3693 star_va: 0.0,
3694 mag_g: 0.0,
3695 mag_b: 0.0,
3696 in_service: true,
3697 name: None,
3698 uid: None,
3699 extras: Extras::new(),
3700 }
3701 }
3702
3703 #[must_use]
3710 pub fn calc_star_impedances(&self) -> [(f64, f64); 3] {
3711 let [z12, z23, z31] = self.z;
3712 let half = |a: f64, b: f64, c: f64| (a + b - c) / 2.0;
3713 [
3714 (half(z12.r, z31.r, z23.r), half(z12.x, z31.x, z23.x)),
3715 (half(z12.r, z23.r, z31.r), half(z12.x, z23.x, z31.x)),
3716 (half(z23.r, z31.r, z12.r), half(z23.x, z31.x, z12.x)),
3717 ]
3718 }
3719
3720 #[must_use]
3729 pub fn winding_in_service(&self) -> [bool; 3] {
3730 self.extras
3731 .get(WINDING_IN_SERVICE_EXTRA)
3732 .and_then(|value| <[bool; 3]>::deserialize(value).ok())
3733 .unwrap_or([true; 3])
3734 }
3735
3736 pub fn set_winding_in_service(&mut self, in_service: [bool; 3]) {
3738 if in_service == [true; 3] {
3739 self.extras.remove(WINDING_IN_SERVICE_EXTRA);
3740 } else {
3741 self.extras.insert(
3742 WINDING_IN_SERVICE_EXTRA.to_owned(),
3743 Value::from(in_service.to_vec()),
3744 );
3745 }
3746 }
3747
3748 #[must_use]
3759 pub fn winding_rating_sets(&self, winding: usize) -> Vec<BranchRatingSet> {
3760 let mut sets = self.stored_winding_rating_sets();
3761 std::mem::take(&mut sets[winding])
3762 }
3763
3764 pub fn set_winding_rating_sets(&mut self, winding: usize, rating_sets: Vec<BranchRatingSet>) {
3769 let mut sets = self.stored_winding_rating_sets();
3770 sets[winding] = rating_sets;
3771 if sets.iter().all(Vec::is_empty) {
3772 self.extras.remove(WINDING_RATING_SETS_EXTRA);
3773 } else {
3774 let value = serde_json::to_value(sets).expect("rating sets serialize to JSON");
3775 self.extras
3776 .insert(WINDING_RATING_SETS_EXTRA.to_owned(), value);
3777 }
3778 }
3779
3780 fn stored_winding_rating_sets(&self) -> [Vec<BranchRatingSet>; 3] {
3781 self.extras
3782 .get(WINDING_RATING_SETS_EXTRA)
3783 .and_then(|value| <[Vec<BranchRatingSet>; 3]>::deserialize(value).ok())
3784 .unwrap_or_default()
3785 }
3786
3787 pub(crate) fn impedance_correction_control_winding(&self) -> Option<usize> {
3791 let mut adjustable = self
3792 .windings
3793 .iter()
3794 .enumerate()
3795 .filter(|(_, winding)| {
3796 winding
3797 .control
3798 .as_ref()
3799 .is_some_and(|control| control.mode != TransformerControlMode::Fixed)
3800 })
3801 .map(|(index, _)| index);
3802 let winding = adjustable.next()?;
3803 adjustable.next().is_none().then_some(winding)
3804 }
3805
3806 #[must_use]
3816 pub fn winding_impedance_correction(&self, winding: usize) -> Option<ImpedanceCorrection> {
3817 let mut corrections = self.stored_winding_impedance_corrections();
3818 corrections[winding].take()
3819 }
3820
3821 pub fn set_winding_impedance_correction(
3827 &mut self,
3828 winding: usize,
3829 correction: Option<ImpedanceCorrection>,
3830 ) {
3831 let mut corrections = self.stored_winding_impedance_corrections();
3832 corrections[winding] = correction;
3833 if corrections.iter().all(Option::is_none) {
3834 self.extras.remove(IMPEDANCE_CORRECTION_EXTRA);
3835 } else {
3836 let value = serde_json::to_value(corrections).expect("corrections serialize to JSON");
3837 self.extras
3838 .insert(IMPEDANCE_CORRECTION_EXTRA.to_owned(), value);
3839 }
3840 }
3841
3842 fn stored_winding_impedance_corrections(&self) -> [Option<ImpedanceCorrection>; 3] {
3843 self.extras
3844 .get(IMPEDANCE_CORRECTION_EXTRA)
3845 .and_then(|value| <[Option<ImpedanceCorrection>; 3]>::deserialize(value).ok())
3846 .unwrap_or_default()
3847 }
3848
3849 #[must_use]
3857 pub fn to_star_expansion(&self, star_id: BusId) -> (Bus, [Branch; 3]) {
3858 let star = Bus {
3859 id: star_id,
3860 kind: BusType::Pq,
3861 vm: self.star_vm,
3862 va: self.star_va,
3863 base_kv: self.windings[0].nominal_kv,
3864 vmax: 1.1,
3865 vmin: 0.9,
3866 evhi: None,
3867 evlo: None,
3868 area: 0,
3869 zone: 0,
3870 name: self.name.clone(),
3871 uid: self.uid.clone(),
3872 location: None,
3873 extras: Extras::new(),
3874 };
3875 let zs = self.calc_star_impedances();
3876 let branch = |w: &Winding, (r, x): (f64, f64)| Branch {
3877 name: None,
3878 from: w.bus,
3879 to: star_id,
3880 r,
3881 x,
3882 b: 0.0,
3883 charging: None,
3884 rate_a: w.rate_a,
3885 rate_b: w.rate_b,
3886 rate_c: w.rate_c,
3887 rating_sets: Vec::new(),
3888 current_ratings: None,
3889 tap: w.tap,
3890 shift: w.shift,
3891 in_service: self.in_service,
3892 angmin: -360.0,
3893 angmax: 360.0,
3894 control: w.control.clone(),
3895 solution: None,
3896 uid: None,
3897 route: None,
3898 extras: Extras::new(),
3899 };
3900 let mut branches = [
3901 branch(&self.windings[0], zs[0]),
3902 branch(&self.windings[1], zs[1]),
3903 branch(&self.windings[2], zs[2]),
3904 ];
3905 let windings_in_service = self.winding_in_service();
3906 for (k, branch) in branches.iter_mut().enumerate() {
3907 branch.in_service &= windings_in_service[k];
3908 branch.rating_sets = self.winding_rating_sets(k);
3909 }
3910 (star, branches)
3911 }
3912}
3913
3914pub const GEN_EXTRA_KEYS: [&str; 11] = [
3917 "pc1", "pc2", "qc1min", "qc1max", "qc2min", "qc2max", "ramp_agc", "ramp_10", "ramp_30",
3918 "ramp_q", "apf",
3919];
3920
3921#[derive(Debug, Clone, PartialEq)]
3928pub(crate) struct ValueFinding {
3929 pub element: String,
3931 pub table: &'static str,
3936 pub index: usize,
3939 pub field: &'static str,
3940 pub old: f64,
3941 pub new: f64,
3942 pub reason: &'static str,
3943}
3944
3945impl ValueFinding {
3946 pub(crate) fn into_diagnostic(self) -> crate::Diagnostic {
3950 let mut details = serde_json::Map::new();
3951 details.insert("element".to_owned(), serde_json::json!(self.element));
3952 details.insert("field".to_owned(), serde_json::json!(self.field));
3953 details.insert("value".to_owned(), serde_json::json!(self.old));
3954 details.insert("repaired_value".to_owned(), serde_json::json!(self.new));
3955 details.insert("reason".to_owned(), serde_json::json!(self.reason));
3956 crate::Diagnostic::of(
3957 &crate::diagnostics::codes::VALIDATE_BALANCED_VALUE_DOMAIN,
3958 format!(
3959 "{}: `{}` is {} ({}); the repair sets {}",
3960 self.element, self.field, self.old, self.reason, self.new
3961 ),
3962 )
3963 .with_target(format!("/{}/{}/{}", self.table, self.index, self.field))
3964 .expect("scan-built targets are nonempty and bounded")
3965 .with_details(details)
3966 .expect("scan-built details stay within the record bounds")
3967 }
3968}
3969
3970pub fn repair_values(
3981 module: powerio_core::PioModule<BalancedNetwork>,
3982) -> std::result::Result<powerio_core::PioModule<BalancedNetwork>, powerio_core::Error> {
3983 let repair_ordinal = module
3984 .history()
3985 .iter()
3986 .filter(|entry| entry.kind() == powerio_core::HistoryKind::Repair)
3987 .count();
3988 let mut network_findings = Vec::new();
3989 let mut module = module.map_value(|mut network| {
3990 network_findings = network.repair_in_place();
3991 network
3992 });
3993 if network_findings.is_empty() {
3994 return Ok(module);
3995 }
3996 let mut parameters = std::collections::BTreeMap::new();
3997 parameters.insert(
3998 "repairs".to_owned(),
3999 serde_json::json!(
4000 network_findings
4001 .iter()
4002 .map(|finding| {
4003 serde_json::json!({
4004 "element": finding.element,
4005 "field": finding.field,
4006 "value": finding.old,
4007 "repaired_value": finding.new,
4008 })
4009 })
4010 .collect::<Vec<_>>()
4011 ),
4012 );
4013 let entry = powerio_core::HistoryEntry::new(
4014 powerio_core::HistoryId::new(format!("repair{repair_ordinal}"))?,
4015 powerio_core::HistoryKind::Repair,
4016 "value_domain_repair",
4017 )?
4018 .with_parameters(parameters)?;
4019 module.add_history_entry(entry)?;
4020 for finding in network_findings {
4021 module.add_diagnostic(finding.into_diagnostic())?;
4022 }
4023 module = module.sever_source();
4024 Ok(module)
4025}
4026
4027fn repair_vm(vm: f64) -> Option<f64> {
4031 (!vm.is_finite() || vm <= 0.0 || vm > 2.0).then_some(1.0)
4032}
4033
4034fn repair_va(va: f64) -> Option<f64> {
4036 (!va.is_finite() || va.abs() > 2000.0).then_some(0.0)
4037}
4038
4039fn repair_mbase(mbase: f64, sbase: f64) -> Option<f64> {
4041 (!mbase.is_finite() || mbase <= 0.0).then_some(sbase)
4042}
4043
4044fn repair_vg(vg: f64) -> Option<f64> {
4046 (!vg.is_finite() || vg <= 0.0).then_some(1.0)
4047}
4048
4049#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4054pub(crate) struct LoweredLengths {
4055 pub(crate) buses: usize,
4056 pub(crate) branches: usize,
4057 pub(crate) shunts: usize,
4058}
4059
4060impl BalancedNetwork {
4061 #[must_use]
4062 pub fn new(name: impl Into<String>, base_mva: f64) -> BalancedNetwork {
4063 BalancedNetwork::from_tables(BalancedNetworkTables {
4064 name: name.into(),
4065 base_mva,
4066 base_frequency: DEFAULT_BASE_FREQUENCY,
4067 geo: None,
4068 case_metadata: CaseMetadata::default(),
4069 detailed_connectivity: None,
4070 generated_uids: std::sync::Arc::default(),
4071 buses: Vec::new().into(),
4072 loads: Vec::new().into(),
4073 shunts: Vec::new().into(),
4074 static_var_compensators: Vec::new().into(),
4075 branches: Vec::new().into(),
4076 switches: Vec::new().into(),
4077 generators: Vec::new().into(),
4078 storage: Vec::new().into(),
4079 hvdc: Vec::new().into(),
4080 transformers_3w: Vec::new().into(),
4081 areas: Vec::new().into(),
4082 solver: None,
4083 source_format: SourceFormat::InMemory,
4084 })
4085 }
4086
4087 #[must_use]
4093 pub fn in_memory(
4094 name: impl Into<String>,
4095 base_mva: f64,
4096 buses: Vec<Bus>,
4097 branches: Vec<Branch>,
4098 ) -> BalancedNetwork {
4099 let mut net = Self::new(name, base_mva);
4100 *net.buses_mut() = buses;
4101 *net.branches_mut() = branches;
4102 net.assign_missing_component_ids();
4103 net
4104 }
4105
4106 #[must_use]
4111 pub fn is_normalized(&self) -> bool {
4112 self.source_format() == SourceFormat::Normalized
4113 }
4114
4115 pub fn check_base_mva(&self) -> crate::Result<()> {
4121 if self.base_mva().is_finite() && self.base_mva() > 0.0 {
4122 Ok(())
4123 } else {
4124 Err(crate::Error::InvalidBaseMva {
4125 base: self.base_mva(),
4126 })
4127 }
4128 }
4129
4130 #[must_use]
4142 pub fn validate_values(&self) -> Vec<crate::Diagnostic> {
4143 self.value_findings()
4144 .into_iter()
4145 .map(ValueFinding::into_diagnostic)
4146 .collect()
4147 }
4148
4149 pub(crate) fn value_findings(&self) -> Vec<ValueFinding> {
4150 let mut out = Vec::new();
4151 for (index, b) in self.buses().iter().enumerate() {
4152 if let Some(new) = repair_vm(b.vm) {
4153 out.push(ValueFinding {
4154 element: format!("bus {}", b.id),
4155 table: "buses",
4156 index,
4157 field: "vm",
4158 old: b.vm,
4159 new,
4160 reason: "voltage magnitude outside [0, 2] p.u.",
4161 });
4162 }
4163 if let Some(new) = repair_va(b.va) {
4164 out.push(ValueFinding {
4165 element: format!("bus {}", b.id),
4166 table: "buses",
4167 index,
4168 field: "va",
4169 old: b.va,
4170 new,
4171 reason: "voltage angle outside ±2000°",
4172 });
4173 }
4174 }
4175 for (index, g) in self.generators().iter().enumerate() {
4176 if let Some(new) = repair_mbase(g.mbase, self.base_mva()) {
4177 out.push(ValueFinding {
4178 element: format!("generator at bus {}", g.bus),
4179 table: "generators",
4180 index,
4181 field: "mbase",
4182 old: g.mbase,
4183 new,
4184 reason: "non-positive generator MVA base",
4185 });
4186 }
4187 if let Some(new) = repair_vg(g.vg) {
4188 out.push(ValueFinding {
4189 element: format!("generator at bus {}", g.bus),
4190 table: "generators",
4191 index,
4192 field: "vg",
4193 old: g.vg,
4194 new,
4195 reason: "non-positive voltage setpoint",
4196 });
4197 }
4198 }
4199 out
4200 }
4201
4202 pub(crate) fn repair_in_place(&mut self) -> Vec<ValueFinding> {
4209 let findings = self.value_findings();
4210 let sbase = self.base_mva();
4211 for b in self.buses_mut() {
4212 if let Some(new) = repair_vm(b.vm) {
4213 b.vm = new;
4214 }
4215 if let Some(new) = repair_va(b.va) {
4216 b.va = new;
4217 }
4218 }
4219 for g in self.generators_mut() {
4220 if let Some(new) = repair_mbase(g.mbase, sbase) {
4221 g.mbase = new;
4222 }
4223 if let Some(new) = repair_vg(g.vg) {
4224 g.vg = new;
4225 }
4226 }
4227 findings
4228 }
4229
4230 pub(crate) fn lowered_lengths(&self) -> LoweredLengths {
4236 let mut lengths = LoweredLengths {
4237 buses: self.buses().len(),
4238 branches: self.branches().len(),
4239 shunts: self.shunts().len(),
4240 };
4241 for t in self.transformers_3w().iter().filter(|t| t.in_service) {
4242 lengths.buses += 1;
4243 lengths.branches += 3;
4244 if t.mag_g != 0.0 || t.mag_b != 0.0 {
4245 lengths.shunts += 1;
4246 }
4247 }
4248 lengths
4249 }
4250
4251 pub(crate) fn expand_transformers_3w(&self) -> std::borrow::Cow<'_, BalancedNetwork> {
4263 if !self.transformers_3w().iter().any(|t| t.in_service) {
4264 return std::borrow::Cow::Borrowed(self);
4265 }
4266 let mut net = self.clone();
4267 let scale = if net.is_normalized() {
4272 1.0
4273 } else {
4274 net.base_mva()
4275 };
4276 let base_id = net
4281 .buses()
4282 .iter()
4283 .map(|b| b.id.0)
4284 .max()
4285 .unwrap_or(0)
4286 .checked_add(1)
4287 .expect("bus id space exhausted for star expansion");
4288 for (k, (source_row, t)) in self
4289 .transformers_3w()
4290 .iter()
4291 .enumerate()
4292 .filter(|(_, t)| t.in_service)
4293 .enumerate()
4294 {
4295 let star_id = BusId(
4296 base_id
4297 .checked_add(k)
4298 .expect("bus id space exhausted for star expansion"),
4299 );
4300 let (star, mut branches) = t.to_star_expansion(star_id);
4301 let transformer_identity = t
4302 .uid
4303 .clone()
4304 .unwrap_or_else(|| format!("transformers_3w:{source_row}"));
4305 for (winding, branch) in branches.iter_mut().enumerate() {
4306 branch.uid = Some(format!("{transformer_identity}/winding:{}", winding + 1));
4307 }
4308 net.buses_mut().push(star);
4309 net.branches_mut().extend(branches);
4310 if t.mag_g != 0.0 || t.mag_b != 0.0 {
4311 net.shunts_mut().push(Shunt {
4312 bus: star_id,
4313 g: t.mag_g * scale,
4314 b: t.mag_b * scale,
4315 in_service: true,
4316 section_count: None,
4317 control: None,
4318 uid: None,
4319 extras: Extras::new(),
4320 });
4321 }
4322 }
4323 net.transformers_3w_mut().clear();
4324 std::borrow::Cow::Owned(net)
4325 }
4326
4327 pub fn validate(&self) -> crate::Result<()> {
4333 self.check_references("network")
4334 }
4335
4336 #[allow(clippy::too_many_lines)]
4341 pub(crate) fn check_references(&self, format: &'static str) -> crate::Result<()> {
4342 let mut ids = std::collections::HashSet::with_capacity(self.buses().len());
4347 for b in self.buses() {
4348 if b.id > BusId::MAX {
4353 return Err(Error::FormatRead {
4354 format,
4355 message: format!("bus id {} is outside the int64 id space", b.id),
4356 });
4357 }
4358 if !ids.insert(b.id) {
4359 return Err(Error::FormatRead {
4360 format,
4361 message: format!("duplicate bus id {}", b.id),
4362 });
4363 }
4364 }
4365 let check = |bus: BusId, what: &str| -> crate::Result<()> {
4366 if ids.contains(&bus) {
4367 Ok(())
4368 } else {
4369 Err(Error::FormatRead {
4370 format,
4371 message: format!("{what} references unknown bus {bus}"),
4372 })
4373 }
4374 };
4375 for (i, br) in self.branches().iter().enumerate() {
4377 for bus in [br.from, br.to] {
4378 if !ids.contains(&bus) {
4379 return Err(Error::FormatRead {
4380 format,
4381 message: format!("branch {i} references unknown bus {bus}"),
4382 });
4383 }
4384 }
4385 if let Some(control) = br.control.as_ref() {
4386 if control.controlled_bus_on_winding_side
4387 && control.controlled_bus.is_none_or(|bus| bus.0 == 0)
4388 {
4389 return Err(Error::FormatRead {
4390 format,
4391 message: format!(
4392 "transformer control on branch {i} marks its controlled bus as lying on the winding side but has no nonzero controlled bus"
4393 ),
4394 });
4395 }
4396 if let Some(bus) = control.controlled_bus {
4397 check(bus, "transformer control")?;
4398 }
4399 }
4400 }
4401 for (i, sw) in self.switches().iter().enumerate() {
4402 for bus in [sw.from, sw.to] {
4403 if !ids.contains(&bus) {
4404 return Err(Error::FormatRead {
4405 format,
4406 message: format!("switch {i} references unknown bus {bus}"),
4407 });
4408 }
4409 }
4410 }
4411 for l in self.loads() {
4412 check(l.bus, "load")?;
4413 }
4414 for s in self.shunts() {
4415 check(s.bus, "shunt")?;
4416 if let Some(bus) = s.control.as_ref().and_then(|c| c.control_bus) {
4417 check(bus, "switched-shunt control")?;
4418 }
4419 }
4420 for svc in self.static_var_compensators() {
4421 check(svc.bus, "static VAR compensator")?;
4422 }
4423 for g in self.generators() {
4424 check(g.bus, "generator")?;
4425 if let Some(bus) = g.regulated_bus {
4426 check(bus, "generator voltage control")?;
4427 }
4428 }
4429 for d in self.hvdc() {
4430 check(d.from, "dcline")?;
4431 check(d.to, "dcline")?;
4432 }
4433 for s in self.storage() {
4434 check(s.bus, "storage")?;
4435 }
4436 for a in self.areas() {
4437 if let Some(slack) = a.slack_bus {
4438 check(slack, "area swing")?;
4439 }
4440 }
4441 for t in self.transformers_3w() {
4442 for w in &t.windings {
4443 check(w.bus, "3-winding transformer")?;
4444 if let Some(control) = w.control.as_ref() {
4445 if control.controlled_bus_on_winding_side
4446 && control.controlled_bus.is_none_or(|bus| bus.0 == 0)
4447 {
4448 return Err(Error::FormatRead {
4449 format,
4450 message: format!(
4451 "3-winding transformer control at bus {} marks its controlled bus as lying on the winding side but has no nonzero controlled bus",
4452 w.bus
4453 ),
4454 });
4455 }
4456 if let Some(bus) = control.controlled_bus {
4457 check(bus, "3-winding transformer control")?;
4458 }
4459 }
4460 }
4461 }
4462 if let Some(detailed) = self.detailed_connectivity().as_ref() {
4463 let nodes = detailed
4464 .connectivity_nodes
4465 .iter()
4466 .map(|node| &node.component)
4467 .collect::<std::collections::HashSet<_>>();
4468 for calculated in &detailed.calculated_buses {
4469 check(calculated.calculated_bus, "calculated bus")?;
4470 if calculated.nodes.is_empty() {
4471 return Err(Error::FormatRead {
4472 format,
4473 message: "calculated bus has no connectivity nodes".into(),
4474 });
4475 }
4476 for node in &calculated.nodes {
4477 if !nodes.contains(node) {
4478 return Err(Error::FormatRead {
4479 format,
4480 message: format!(
4481 "calculated bus references unknown connectivity node `{}`",
4482 node.local_id()
4483 ),
4484 });
4485 }
4486 }
4487 }
4488 let declared_components = detailed
4489 .component_metadata
4490 .iter()
4491 .map(|metadata| &metadata.component)
4492 .chain(
4493 detailed
4494 .substations
4495 .iter()
4496 .map(|substation| &substation.component),
4497 )
4498 .chain(detailed.voltage_levels.iter().map(|level| &level.component))
4499 .chain(
4500 detailed
4501 .dc_converter_units
4502 .iter()
4503 .map(|unit| &unit.component),
4504 )
4505 .collect::<std::collections::HashSet<_>>();
4506 for metadata in &detailed.component_metadata {
4507 if let Some(container) = metadata.equipment_container.as_ref()
4508 && !declared_components.contains(container)
4509 {
4510 return Err(Error::FormatRead {
4511 format,
4512 message: format!(
4513 "component `{}` references unknown equipment container `{container}`",
4514 metadata.component
4515 ),
4516 });
4517 }
4518 }
4519 let mut junctions = std::collections::HashSet::new();
4520 let terminal_equipment: std::collections::HashSet<&ComponentId> = detailed
4521 .terminals
4522 .iter()
4523 .map(|terminal| &terminal.equipment)
4524 .collect();
4525 for junction in &detailed.junctions {
4526 if !junctions.insert(&junction.component) {
4527 return Err(Error::FormatRead {
4528 format,
4529 message: format!("duplicate Junction `{}`", junction.component.local_id()),
4530 });
4531 }
4532 if !declared_components.contains(&junction.component) {
4533 return Err(Error::FormatRead {
4534 format,
4535 message: format!(
4536 "Junction `{}` has no component metadata",
4537 junction.component.local_id()
4538 ),
4539 });
4540 }
4541 if !terminal_equipment.contains(&junction.component) {
4542 return Err(Error::FormatRead {
4543 format,
4544 message: format!(
4545 "Junction `{}` has no terminal",
4546 junction.component.local_id()
4547 ),
4548 });
4549 }
4550 }
4551 }
4552 self.check_detailed_ac_references(format, &ids)?;
4553 self.check_detailed_dc_references(format)?;
4554 self.check_star_expansion_headroom(format)
4555 }
4556
4557 #[allow(clippy::too_many_lines)]
4558 fn check_detailed_ac_references(
4559 &self,
4560 format: &'static str,
4561 balanced_buses: &std::collections::HashSet<BusId>,
4562 ) -> crate::Result<()> {
4563 let Some(detailed) = self.detailed_connectivity().as_ref() else {
4564 return Ok(());
4565 };
4566 let error = |message: String| Error::FormatRead { format, message };
4567
4568 let mut metadata_components = std::collections::HashSet::new();
4569 for metadata in &detailed.component_metadata {
4570 if !metadata_components.insert(metadata.component.clone()) {
4571 return Err(error(format!(
4572 "duplicate component metadata `{}`",
4573 metadata.component
4574 )));
4575 }
4576 }
4577
4578 let mut substations = std::collections::HashSet::new();
4579 for substation in &detailed.substations {
4580 if !substations.insert(substation.component.clone()) {
4581 return Err(error(format!(
4582 "duplicate substation `{}`",
4583 substation.component
4584 )));
4585 }
4586 }
4587
4588 let mut voltage_levels = std::collections::HashSet::new();
4589 let mut voltage_level_topology = std::collections::HashMap::new();
4590 let mut calculated_bus_levels = std::collections::HashMap::new();
4591 for level in &detailed.voltage_levels {
4592 if !voltage_levels.insert(level.component.clone()) {
4593 return Err(error(format!(
4594 "duplicate voltage level `{}`",
4595 level.component
4596 )));
4597 }
4598 voltage_level_topology.insert(level.component.clone(), level.topology_kind);
4599 if let Some(substation) = &level.substation
4600 && !substations.contains(substation)
4601 {
4602 return Err(error(format!(
4603 "voltage level `{}` references unknown substation `{substation}`",
4604 level.component
4605 )));
4606 }
4607 let mut level_buses = std::collections::HashSet::new();
4608 for bus in &level.buses {
4609 if !balanced_buses.contains(bus) {
4610 return Err(error(format!(
4611 "voltage level `{}` references unknown calculated bus {bus}",
4612 level.component
4613 )));
4614 }
4615 if !level_buses.insert(*bus) {
4616 return Err(error(format!(
4617 "voltage level `{}` repeats calculated bus {bus}",
4618 level.component
4619 )));
4620 }
4621 if let Some(first_level) =
4622 calculated_bus_levels.insert(*bus, level.component.clone())
4623 && first_level != level.component
4624 {
4625 return Err(error(format!(
4626 "calculated bus {bus} belongs to both voltage levels `{first_level}` and `{}`",
4627 level.component
4628 )));
4629 }
4630 }
4631 }
4632 let known_container = |container: &ComponentId| {
4633 voltage_levels.contains(container) || metadata_components.contains(container)
4634 };
4635
4636 let mut configured_buses = std::collections::HashSet::new();
4637 let mut configured_bus_levels = std::collections::HashMap::new();
4638 for bus in &detailed.bus_breaker_buses {
4639 if !configured_buses.insert(bus.component.clone()) {
4640 return Err(error(format!(
4641 "duplicate bus breaker bus `{}`",
4642 bus.component
4643 )));
4644 }
4645 if !known_container(&bus.voltage_level) {
4646 let detail = if bus.calculated_bus.is_none() {
4647 " has no calculated bus and"
4648 } else {
4649 ""
4650 };
4651 return Err(error(format!(
4652 "TopologicalNode `{}`{detail} references unknown voltage level or connectivity container `{}`",
4653 bus.component, bus.voltage_level
4654 )));
4655 }
4656 configured_bus_levels.insert(bus.component.clone(), bus.voltage_level.clone());
4657 if let Some(calculated) = bus.calculated_bus
4658 && !balanced_buses.contains(&calculated)
4659 {
4660 return Err(error(format!(
4661 "bus breaker bus `{}` references unknown calculated bus {calculated}",
4662 bus.component
4663 )));
4664 }
4665 if voltage_levels.contains(&bus.voltage_level)
4666 && let Some(calculated) = bus.calculated_bus
4667 && let Some(first_level) =
4668 calculated_bus_levels.insert(calculated, bus.voltage_level.clone())
4669 && first_level != bus.voltage_level
4670 {
4671 return Err(error(format!(
4672 "calculated bus {calculated} belongs to both connectivity containers `{first_level}` and `{}`",
4673 bus.voltage_level
4674 )));
4675 }
4676 }
4677
4678 let mut connectivity_nodes = std::collections::HashSet::new();
4679 let mut connectivity_node_levels = std::collections::HashMap::new();
4680 let mut connectivity_node_calculated_buses = std::collections::HashMap::new();
4681 for node in &detailed.connectivity_nodes {
4682 if !connectivity_nodes.insert(node.component.clone()) {
4683 return Err(error(format!(
4684 "duplicate connectivity node `{}`",
4685 node.component
4686 )));
4687 }
4688 if !known_container(&node.voltage_level) {
4689 let detail = if node.calculated_bus.is_none() {
4690 " has no calculated bus and"
4691 } else {
4692 ""
4693 };
4694 return Err(error(format!(
4695 "ConnectivityNode `{}`{detail} references unknown voltage level or connectivity container `{}`",
4696 node.component, node.voltage_level
4697 )));
4698 }
4699 connectivity_node_levels.insert(node.component.clone(), node.voltage_level.clone());
4700 connectivity_node_calculated_buses.insert(node.component.clone(), node.calculated_bus);
4701 if let Some(calculated) = node.calculated_bus
4702 && !balanced_buses.contains(&calculated)
4703 {
4704 return Err(error(format!(
4705 "connectivity node `{}` references unknown calculated bus {calculated}",
4706 node.component
4707 )));
4708 }
4709 if voltage_levels.contains(&node.voltage_level)
4710 && let Some(calculated) = node.calculated_bus
4711 && let Some(first_level) =
4712 calculated_bus_levels.insert(calculated, node.voltage_level.clone())
4713 && first_level != node.voltage_level
4714 {
4715 return Err(error(format!(
4716 "calculated bus {calculated} belongs to both connectivity containers `{first_level}` and `{}`",
4717 node.voltage_level
4718 )));
4719 }
4720 }
4721
4722 let mut calculated_buses = std::collections::HashSet::new();
4723 let mut calculated_bus_nodes = std::collections::HashMap::new();
4724 for calculated in &detailed.calculated_buses {
4725 if !calculated_buses.insert(calculated.calculated_bus) {
4726 return Err(error(format!(
4727 "duplicate calculated bus {} in detailed connectivity",
4728 calculated.calculated_bus
4729 )));
4730 }
4731 if !balanced_buses.contains(&calculated.calculated_bus) {
4732 return Err(error(format!(
4733 "detailed connectivity references unknown calculated bus {}",
4734 calculated.calculated_bus
4735 )));
4736 }
4737 if !known_container(&calculated.voltage_level) {
4738 return Err(error(format!(
4739 "calculated bus {} references unknown voltage level or connectivity container `{}`",
4740 calculated.calculated_bus, calculated.voltage_level
4741 )));
4742 }
4743 if voltage_levels.contains(&calculated.voltage_level)
4744 && let Some(first_level) = calculated_bus_levels
4745 .insert(calculated.calculated_bus, calculated.voltage_level.clone())
4746 && first_level != calculated.voltage_level
4747 {
4748 return Err(error(format!(
4749 "calculated bus {} belongs to both connectivity containers `{first_level}` and `{}`",
4750 calculated.calculated_bus, calculated.voltage_level
4751 )));
4752 }
4753 for node in &calculated.nodes {
4754 let Some(node_level) = connectivity_node_levels.get(node) else {
4755 return Err(error(format!(
4756 "calculated bus {} references unknown connectivity node `{node}`",
4757 calculated.calculated_bus
4758 )));
4759 };
4760 if node_level != &calculated.voltage_level {
4761 return Err(error(format!(
4762 "calculated bus {} in `{}` contains connectivity node `{node}` from `{node_level}`",
4763 calculated.calculated_bus, calculated.voltage_level
4764 )));
4765 }
4766 if let Some(Some(node_bus)) = connectivity_node_calculated_buses.get(node)
4767 && *node_bus != calculated.calculated_bus
4768 {
4769 return Err(error(format!(
4770 "connectivity node `{node}` names calculated bus {node_bus}, but calculated bus {} also claims that node",
4771 calculated.calculated_bus
4772 )));
4773 }
4774 if let Some(first_bus) =
4775 calculated_bus_nodes.insert(node.clone(), calculated.calculated_bus)
4776 {
4777 return Err(error(format!(
4778 "connectivity node `{node}` is listed by calculated buses {first_bus} and {}",
4779 calculated.calculated_bus
4780 )));
4781 }
4782 }
4783 }
4784
4785 let mut busbars = std::collections::HashSet::new();
4786 for busbar in &detailed.busbar_sections {
4787 if !busbars.insert(busbar.component.clone()) {
4788 return Err(error(format!(
4789 "duplicate busbar section `{}`",
4790 busbar.component
4791 )));
4792 }
4793 if !known_container(&busbar.voltage_level) {
4794 return Err(error(format!(
4795 "busbar section `{}` references unknown voltage level or connectivity container `{}`",
4796 busbar.component, busbar.voltage_level
4797 )));
4798 }
4799 let Some(node_level) = connectivity_node_levels.get(&busbar.node) else {
4800 return Err(error(format!(
4801 "busbar section `{}` references unknown connectivity node `{}`",
4802 busbar.component, busbar.node
4803 )));
4804 };
4805 if voltage_levels.contains(&busbar.voltage_level)
4806 && voltage_levels.contains(node_level)
4807 && node_level != &busbar.voltage_level
4808 {
4809 return Err(error(format!(
4810 "busbar section `{}` in `{}` references connectivity node `{}` from `{node_level}`",
4811 busbar.component, busbar.voltage_level, busbar.node
4812 )));
4813 }
4814 }
4815
4816 let mut terminal_components = std::collections::HashSet::new();
4817 let mut terminal_keys = std::collections::HashSet::new();
4818 for terminal in &detailed.terminals {
4819 if terminal.terminal == 0 {
4820 return Err(error(format!(
4821 "equipment `{}` has terminal 0",
4822 terminal.equipment
4823 )));
4824 }
4825 if !terminal_keys.insert((terminal.equipment.clone(), terminal.terminal)) {
4826 return Err(error(format!(
4827 "equipment `{}` repeats terminal {}",
4828 terminal.equipment, terminal.terminal
4829 )));
4830 }
4831 if let Some(component) = &terminal.component
4832 && !terminal_components.insert(component.clone())
4833 {
4834 return Err(error(format!("duplicate terminal identity `{component}`")));
4835 }
4836 if !known_container(&terminal.voltage_level) {
4837 return Err(error(format!(
4838 "equipment `{}` terminal {} references unknown voltage level or connectivity container `{}`",
4839 terminal.equipment, terminal.terminal, terminal.voltage_level
4840 )));
4841 }
4842 for bus in [terminal.bus.as_ref(), terminal.connectable_bus.as_ref()]
4843 .into_iter()
4844 .flatten()
4845 {
4846 let Some(bus_level) = configured_bus_levels.get(bus) else {
4847 return Err(error(format!(
4848 "equipment `{}` terminal {} references unknown bus breaker bus `{bus}`",
4849 terminal.equipment, terminal.terminal
4850 )));
4851 };
4852 if bus_level != &terminal.voltage_level {
4853 return Err(error(format!(
4854 "equipment `{}` terminal {} in `{}` references bus `{bus}` from `{bus_level}`",
4855 terminal.equipment, terminal.terminal, terminal.voltage_level
4856 )));
4857 }
4858 }
4859 if let Some(node) = &terminal.node {
4860 let Some(node_level) = connectivity_node_levels.get(node) else {
4861 return Err(error(format!(
4862 "equipment `{}` terminal {} references unknown connectivity node `{node}`",
4863 terminal.equipment, terminal.terminal
4864 )));
4865 };
4866 if node_level != &terminal.voltage_level {
4867 return Err(error(format!(
4868 "equipment `{}` terminal {} in `{}` references connectivity node `{node}` from `{node_level}`",
4869 terminal.equipment, terminal.terminal, terminal.voltage_level
4870 )));
4871 }
4872 }
4873 }
4874
4875 let mut switches = std::collections::HashSet::new();
4876 for switch in &detailed.switches {
4877 if !switches.insert(switch.component.clone()) {
4878 return Err(error(format!(
4879 "duplicate topology switch `{}`",
4880 switch.component
4881 )));
4882 }
4883 if !voltage_levels.contains(&switch.voltage_level)
4884 && !metadata_components.contains(&switch.voltage_level)
4885 {
4886 return Err(error(format!(
4887 "topology switch `{}` references unknown voltage level or connectivity container `{}`",
4888 switch.component, switch.voltage_level
4889 )));
4890 }
4891 for endpoint in [&switch.endpoint1, &switch.endpoint2] {
4892 match endpoint {
4893 TopologyEndpoint::Bus(bus) => {
4894 if !configured_bus_levels.contains_key(bus) {
4895 return Err(error(format!(
4896 "topology switch `{}` references unknown bus breaker bus `{bus}`",
4897 switch.component
4898 )));
4899 }
4900 if voltage_level_topology.get(&switch.voltage_level)
4901 == Some(&TopologyKind::NodeBreaker)
4902 {
4903 return Err(error(format!(
4904 "topology switch `{}` uses a bus endpoint in node breaker voltage level `{}`",
4905 switch.component, switch.voltage_level
4906 )));
4907 }
4908 }
4909 TopologyEndpoint::Node(node) => {
4910 if !connectivity_node_levels.contains_key(node) {
4911 return Err(error(format!(
4912 "topology switch `{}` references unknown connectivity node `{node}`",
4913 switch.component
4914 )));
4915 }
4916 if voltage_level_topology.get(&switch.voltage_level)
4917 == Some(&TopologyKind::BusBreaker)
4918 {
4919 return Err(error(format!(
4920 "topology switch `{}` uses a node endpoint in bus breaker voltage level `{}`",
4921 switch.component, switch.voltage_level
4922 )));
4923 }
4924 }
4925 }
4926 }
4927 }
4928
4929 for connection in &detailed.internal_connections {
4930 if !voltage_levels.contains(&connection.voltage_level) {
4931 return Err(error(format!(
4932 "internal connection references unknown voltage level `{}`",
4933 connection.voltage_level
4934 )));
4935 }
4936 for node in [&connection.node1, &connection.node2] {
4937 let Some(node_level) = connectivity_node_levels.get(node) else {
4938 return Err(error(format!(
4939 "internal connection references unknown connectivity node `{node}`"
4940 )));
4941 };
4942 if node_level != &connection.voltage_level {
4943 return Err(error(format!(
4944 "internal connection in `{}` references connectivity node `{node}` from `{node_level}`",
4945 connection.voltage_level
4946 )));
4947 }
4948 }
4949 }
4950
4951 let mut tap_components = std::collections::HashSet::new();
4952 let mut tap_keys = std::collections::HashSet::new();
4953 for tap in &detailed.tap_changers {
4954 if tap.winding == 0 {
4955 return Err(error(format!(
4956 "transformer `{}` has a tap changer on winding 0",
4957 tap.transformer
4958 )));
4959 }
4960 let kind = match tap.kind {
4961 TapChangerKind::Ratio => 0_u8,
4962 TapChangerKind::Phase => 1_u8,
4963 };
4964 if !tap_keys.insert((tap.transformer.clone(), tap.winding, kind)) {
4965 return Err(error(format!(
4966 "transformer `{}` repeats its {:?} tap changer on winding {}",
4967 tap.transformer, tap.kind, tap.winding
4968 )));
4969 }
4970 if let Some(component) = &tap.component
4971 && !tap_components.insert(component.clone())
4972 {
4973 return Err(error(format!(
4974 "duplicate tap changer identity `{component}`"
4975 )));
4976 }
4977 let mut positions = std::collections::HashSet::new();
4978 for step in &tap.steps {
4979 if !positions.insert(step.position) {
4980 return Err(error(format!(
4981 "transformer `{}` winding {} repeats tap position {}",
4982 tap.transformer, tap.winding, step.position
4983 )));
4984 }
4985 }
4986 if let Some(reference) = &tap.regulation_terminal
4987 && !terminal_keys.contains(&(reference.equipment.clone(), reference.terminal))
4988 {
4989 return Err(error(format!(
4990 "transformer `{}` winding {} regulates unknown equipment terminal `{}` terminal {}",
4991 tap.transformer, tap.winding, reference.equipment, reference.terminal
4992 )));
4993 }
4994 }
4995
4996 Ok(())
4997 }
4998
4999 #[allow(clippy::too_many_lines)]
5000 fn check_detailed_dc_references(&self, format: &'static str) -> crate::Result<()> {
5001 let Some(detailed) = self.detailed_connectivity().as_ref() else {
5002 return Ok(());
5003 };
5004
5005 let substations = detailed
5006 .substations
5007 .iter()
5008 .map(|substation| &substation.component)
5009 .collect::<std::collections::HashSet<_>>();
5010 let mut converter_units =
5011 std::collections::HashSet::with_capacity(detailed.dc_converter_units.len());
5012 for unit in &detailed.dc_converter_units {
5013 if !converter_units.insert(&unit.component) {
5014 return Err(Error::FormatRead {
5015 format,
5016 message: format!("duplicate DCConverterUnit `{}`", unit.component.local_id()),
5017 });
5018 }
5019 if let Some(substation) = unit.substation.as_ref()
5020 && !substations.contains(substation)
5021 {
5022 return Err(Error::FormatRead {
5023 format,
5024 message: format!(
5025 "DCConverterUnit `{}` references unknown Substation `{}`",
5026 unit.component.local_id(),
5027 substation.local_id()
5028 ),
5029 });
5030 }
5031 }
5032 let known_components = detailed
5033 .component_metadata
5034 .iter()
5035 .map(|metadata| &metadata.component)
5036 .chain(converter_units.iter().copied())
5037 .collect::<std::collections::HashSet<_>>();
5038
5039 let mut nodes = std::collections::HashSet::with_capacity(detailed.dc_nodes.len());
5040 for node in &detailed.dc_nodes {
5041 if !nodes.insert(&node.component) {
5042 return Err(Error::FormatRead {
5043 format,
5044 message: format!("duplicate DCNode `{}`", node.component.local_id()),
5045 });
5046 }
5047 }
5048 let mut topological_nodes =
5049 std::collections::HashSet::with_capacity(detailed.dc_topological_nodes.len());
5050 for node in &detailed.dc_topological_nodes {
5051 if !topological_nodes.insert(&node.component) {
5052 return Err(Error::FormatRead {
5053 format,
5054 message: format!(
5055 "duplicate DCTopologicalNode `{}`",
5056 node.component.local_id()
5057 ),
5058 });
5059 }
5060 }
5061
5062 let check_unit = |class: &str,
5063 component: &ComponentId,
5064 unit: Option<&ComponentId>|
5065 -> crate::Result<()> {
5066 if let Some(unit) = unit
5067 && !converter_units.contains(unit)
5068 {
5069 return Err(Error::FormatRead {
5070 format,
5071 message: format!(
5072 "{class} `{}` references unknown DCConverterUnit `{}`",
5073 component.local_id(),
5074 unit.local_id()
5075 ),
5076 });
5077 }
5078 Ok(())
5079 };
5080 let check_container = |class: &str,
5081 component: &ComponentId,
5082 container: Option<&ComponentId>|
5083 -> crate::Result<()> {
5084 if let Some(container) = container
5085 && !known_components.contains(container)
5086 {
5087 return Err(Error::FormatRead {
5088 format,
5089 message: format!(
5090 "{class} `{}` references unknown equipment container `{}`",
5091 component.local_id(),
5092 container.local_id()
5093 ),
5094 });
5095 }
5096 Ok(())
5097 };
5098 for node in &detailed.dc_topological_nodes {
5099 check_unit(
5100 "DCTopologicalNode",
5101 &node.component,
5102 node.dc_converter_unit.as_ref(),
5103 )?;
5104 }
5105 for node in &detailed.dc_nodes {
5106 check_unit("DCNode", &node.component, node.dc_converter_unit.as_ref())?;
5107 if let Some(topological_node) = node.dc_topological_node.as_ref()
5108 && !topological_nodes.contains(topological_node)
5109 {
5110 return Err(Error::FormatRead {
5111 format,
5112 message: format!(
5113 "DCNode `{}` references unknown DCTopologicalNode `{}`",
5114 node.component.local_id(),
5115 topological_node.local_id()
5116 ),
5117 });
5118 }
5119 }
5120
5121 let mut terminal_ids = std::collections::HashSet::<ComponentId>::new();
5122 let mut check_terminal = |class: &str,
5123 equipment: &ComponentId,
5124 terminal: &DcTerminal|
5125 -> crate::Result<()> {
5126 if let Some(terminal_id) = terminal.component.as_ref()
5127 && !terminal_ids.insert(terminal_id.clone())
5128 {
5129 return Err(Error::FormatRead {
5130 format,
5131 message: format!("duplicate DCTerminal `{}`", terminal_id.local_id()),
5132 });
5133 }
5134 if terminal.dc_node.is_none() && terminal.dc_topological_node.is_none() {
5135 return Err(Error::FormatRead {
5136 format,
5137 message: format!(
5138 "DCTerminal on {class} `{}` references neither a DCNode nor a DCTopologicalNode",
5139 equipment.local_id()
5140 ),
5141 });
5142 }
5143 if let Some(node) = terminal.dc_node.as_ref()
5144 && !nodes.contains(node)
5145 {
5146 return Err(Error::FormatRead {
5147 format,
5148 message: format!(
5149 "DCTerminal on {class} `{}` references unknown DCNode `{}`",
5150 equipment.local_id(),
5151 node.local_id()
5152 ),
5153 });
5154 }
5155 if let Some(node) = terminal.dc_topological_node.as_ref()
5156 && !topological_nodes.contains(node)
5157 {
5158 return Err(Error::FormatRead {
5159 format,
5160 message: format!(
5161 "DCTerminal on {class} `{}` references unknown DCTopologicalNode `{}`",
5162 equipment.local_id(),
5163 node.local_id()
5164 ),
5165 });
5166 }
5167 Ok(())
5168 };
5169
5170 for ground in &detailed.dc_grounds {
5171 check_container(
5172 "DCGround",
5173 &ground.component,
5174 ground.equipment_container.as_ref(),
5175 )?;
5176 check_terminal("DCGround", &ground.component, &ground.dc_terminal)?;
5177 }
5178 for busbar in &detailed.dc_busbars {
5179 check_container(
5180 "DCBusbar",
5181 &busbar.component,
5182 busbar.equipment_container.as_ref(),
5183 )?;
5184 check_terminal("DCBusbar", &busbar.component, &busbar.dc_terminal)?;
5185 }
5186 for line in &detailed.dc_lines {
5187 check_container(
5188 "DCLineSegment",
5189 &line.component,
5190 line.equipment_container.as_ref(),
5191 )?;
5192 check_terminal("DCLineSegment", &line.component, &line.dc_terminal1)?;
5193 check_terminal("DCLineSegment", &line.component, &line.dc_terminal2)?;
5194 }
5195 for device in &detailed.dc_series_devices {
5196 check_container(
5197 "DCSeriesDevice",
5198 &device.component,
5199 device.equipment_container.as_ref(),
5200 )?;
5201 check_terminal("DCSeriesDevice", &device.component, &device.dc_terminal1)?;
5202 check_terminal("DCSeriesDevice", &device.component, &device.dc_terminal2)?;
5203 }
5204 for switch in &detailed.dc_switches {
5205 check_container(
5206 "DCSwitch",
5207 &switch.component,
5208 switch.equipment_container.as_ref(),
5209 )?;
5210 check_terminal("DCSwitch", &switch.component, &switch.dc_terminal1)?;
5211 check_terminal("DCSwitch", &switch.component, &switch.dc_terminal2)?;
5212 }
5213 for converter in &detailed.voltage_source_converters {
5214 check_unit(
5215 "VsConverter",
5216 &converter.component,
5217 converter.dc_converter_unit.as_ref(),
5218 )?;
5219 check_terminal("VsConverter", &converter.component, &converter.dc_terminal1)?;
5220 check_terminal("VsConverter", &converter.component, &converter.dc_terminal2)?;
5221 }
5222 for converter in &detailed.line_commutated_converters {
5223 check_unit(
5224 "CsConverter",
5225 &converter.component,
5226 converter.dc_converter_unit.as_ref(),
5227 )?;
5228 check_terminal("CsConverter", &converter.component, &converter.dc_terminal1)?;
5229 check_terminal("CsConverter", &converter.component, &converter.dc_terminal2)?;
5230 }
5231
5232 let ac_terminals = detailed
5233 .terminals
5234 .iter()
5235 .map(|terminal| (&terminal.equipment, terminal.terminal))
5236 .collect::<std::collections::HashSet<_>>();
5237 let check_regulating_terminal =
5238 |what: &str, reference: Option<&TerminalReference>| -> crate::Result<()> {
5239 if let Some(reference) = reference
5240 && !ac_terminals.contains(&(&reference.equipment, reference.terminal))
5241 {
5242 return Err(Error::FormatRead {
5243 format,
5244 message: format!(
5245 "{what} references undeclared regulating Terminal `{}` number {}",
5246 reference.equipment.local_id(),
5247 reference.terminal
5248 ),
5249 });
5250 }
5251 Ok(())
5252 };
5253 for (index, generator) in self.generators().iter().enumerate() {
5254 check_regulating_terminal(
5255 &format!("generator {index}"),
5256 generator.regulating_terminal.as_ref(),
5257 )?;
5258 }
5259 for (index, branch) in self.branches().iter().enumerate() {
5260 if let Some(control) = &branch.control {
5261 check_regulating_terminal(
5262 &format!("transformer branch {index}"),
5263 control.regulating_terminal.as_ref(),
5264 )?;
5265 }
5266 }
5267 for (index, shunt) in self.shunts().iter().enumerate() {
5268 if let Some(control) = &shunt.control {
5269 check_regulating_terminal(
5270 &format!("switched shunt {index}"),
5271 control.regulating_terminal.as_ref(),
5272 )?;
5273 }
5274 }
5275 for (transformer_index, transformer) in self.transformers_3w().iter().enumerate() {
5276 for (winding_index, winding) in transformer.windings.iter().enumerate() {
5277 if let Some(control) = &winding.control {
5278 check_regulating_terminal(
5279 &format!(
5280 "three winding transformer {transformer_index} winding {winding_index}"
5281 ),
5282 control.regulating_terminal.as_ref(),
5283 )?;
5284 }
5285 }
5286 }
5287 let check_pcc_terminal = |class: &str,
5288 component: &ComponentId,
5289 pcc: Option<&TerminalReference>|
5290 -> crate::Result<()> {
5291 if let Some(pcc) = pcc
5292 && !ac_terminals.contains(&(&pcc.equipment, pcc.terminal))
5293 {
5294 return Err(Error::FormatRead {
5295 format,
5296 message: format!(
5297 "{class} `{}` references undeclared PCC Terminal `{}` number {}",
5298 component.local_id(),
5299 pcc.equipment.local_id(),
5300 pcc.terminal
5301 ),
5302 });
5303 }
5304 Ok(())
5305 };
5306 for converter in &detailed.voltage_source_converters {
5307 check_pcc_terminal(
5308 "VoltageSourceConverter",
5309 &converter.component,
5310 converter.pcc_terminal.as_ref(),
5311 )?;
5312 }
5313 for converter in &detailed.line_commutated_converters {
5314 check_pcc_terminal(
5315 "LineCommutatedConverter",
5316 &converter.component,
5317 converter.pcc_terminal.as_ref(),
5318 )?;
5319 }
5320 Ok(())
5321 }
5322
5323 fn check_star_expansion_headroom(&self, format: &'static str) -> crate::Result<()> {
5331 if self.transformers_3w().is_empty() {
5332 return Ok(());
5333 }
5334 let Some(max_id) = self.buses().iter().map(|b| b.id.0).max() else {
5335 return Ok(());
5336 };
5337 let needed = self
5338 .transformers_3w()
5339 .iter()
5340 .filter(|t| t.in_service)
5341 .count()
5342 .max(1);
5343 if max_id
5344 .checked_add(needed)
5345 .is_none_or(|top| top > BusId::MAX.0)
5346 {
5347 return Err(Error::FormatRead {
5348 format,
5349 message: format!(
5350 "bus id {max_id} leaves no room to allocate synthetic star bus ids \
5351 for 3-winding transformers"
5352 ),
5353 });
5354 }
5355 Ok(())
5356 }
5357}
5358
5359#[cfg(test)]
5363pub(crate) fn serde_round_trip(network: &BalancedNetwork) -> BalancedNetwork {
5364 serde_json::from_str(&serde_json::to_string(network).unwrap()).unwrap()
5365}
5366
5367#[cfg(test)]
5368mod tests {
5369 use super::*;
5370
5371 fn close(actual: f64, expected: f64) {
5372 assert!((actual - expected).abs() < 1e-12, "{actual} != {expected}");
5373 }
5374
5375 fn component(component_type: &str, local_id: &str) -> ComponentId {
5376 ComponentId::new(component_type, local_id).unwrap()
5377 }
5378
5379 fn reference_test_dc_terminal(local_id: &str, node: &ComponentId) -> DcTerminal {
5380 DcTerminal {
5381 component: Some(component("dc_terminal", local_id)),
5382 sequence_number: None,
5383 dc_node: Some(node.clone()),
5384 dc_topological_node: None,
5385 polarity: None,
5386 connected: None,
5387 active_power_mw: None,
5388 current_a: None,
5389 }
5390 }
5391
5392 #[allow(clippy::too_many_lines)]
5393 fn detailed_dc_reference_test_network() -> BalancedNetwork {
5394 let substation = component("substation", "S");
5395 let converter_unit = component("dc_converter_unit", "U");
5396 let topological_node = component("dc_topological_node", "T");
5397 let node = component("dc_node", "N");
5398 let pcc_equipment = component("ac_equipment", "PCC");
5399 let voltage_level = component("voltage_level", "VL");
5400
5401 let voltage_source_converter: VoltageSourceConverter =
5402 serde_json::from_value(serde_json::json!({
5403 "component": component("voltage_source_converter", "VSC"),
5404 "dc_converter_unit": converter_unit.clone(),
5405 "dc_terminal1": reference_test_dc_terminal("VSC-1", &node),
5406 "dc_terminal2": reference_test_dc_terminal("VSC-2", &node),
5407 "pcc_terminal": {
5408 "equipment": pcc_equipment.clone(),
5409 "terminal": 1
5410 }
5411 }))
5412 .unwrap();
5413 let line_commutated_converter: LineCommutatedConverter =
5414 serde_json::from_value(serde_json::json!({
5415 "component": component("line_commutated_converter", "LCC"),
5416 "dc_converter_unit": converter_unit.clone(),
5417 "dc_terminal1": reference_test_dc_terminal("LCC-1", &node),
5418 "dc_terminal2": reference_test_dc_terminal("LCC-2", &node),
5419 "pcc_terminal": {
5420 "equipment": pcc_equipment.clone(),
5421 "terminal": 1
5422 }
5423 }))
5424 .unwrap();
5425
5426 let detailed = DetailedConnectivity {
5427 substations: vec![Substation {
5428 component: substation.clone(),
5429 country: None,
5430 operator: None,
5431 geographical_tags: Vec::new(),
5432 }],
5433 voltage_levels: vec![VoltageLevel {
5434 component: voltage_level.clone(),
5435 substation: Some(substation.clone()),
5436 nominal_kv: 230.0,
5437 low_voltage_limit_kv: None,
5438 high_voltage_limit_kv: None,
5439 topology_kind: TopologyKind::BusBreaker,
5440 buses: Vec::new(),
5441 }],
5442 terminals: vec![Terminal {
5443 component: None,
5444 equipment: pcc_equipment,
5445 terminal: 1,
5446 voltage_level,
5447 bus: None,
5448 connectable_bus: None,
5449 node: None,
5450 connected: true,
5451 active_power_mw: None,
5452 reactive_power_mvar: None,
5453 }],
5454 dc_converter_units: vec![DcConverterUnit {
5455 component: converter_unit.clone(),
5456 substation: Some(substation),
5457 operation_mode: DcConverterOperatingMode::Bipolar,
5458 }],
5459 dc_topological_nodes: vec![DcTopologicalNode {
5460 component: topological_node.clone(),
5461 dc_converter_unit: Some(converter_unit.clone()),
5462 }],
5463 dc_nodes: vec![DcNode {
5464 component: node.clone(),
5465 nominal_voltage_kv: None,
5466 dc_converter_unit: Some(converter_unit.clone()),
5467 dc_topological_node: Some(topological_node),
5468 voltage_kv: None,
5469 }],
5470 dc_grounds: vec![DcGround {
5471 component: component("dc_ground", "G"),
5472 equipment_container: Some(converter_unit.clone()),
5473 dc_terminal: reference_test_dc_terminal("G-1", &node),
5474 rated_dc_voltage_kv: None,
5475 resistance_ohm: None,
5476 inductance_h: None,
5477 }],
5478 dc_lines: vec![DcLine {
5479 component: component("dc_line", "L"),
5480 equipment_container: Some(converter_unit.clone()),
5481 dc_terminal1: reference_test_dc_terminal("L-1", &node),
5482 dc_terminal2: reference_test_dc_terminal("L-2", &node),
5483 rated_dc_voltage_kv: None,
5484 resistance_ohm: None,
5485 inductance_h: None,
5486 capacitance_f: None,
5487 length_km: None,
5488 }],
5489 dc_switches: vec![DcSwitch {
5490 component: component("dc_switch", "SW"),
5491 equipment_container: Some(converter_unit),
5492 dc_terminal1: reference_test_dc_terminal("SW-1", &node),
5493 dc_terminal2: reference_test_dc_terminal("SW-2", &node),
5494 kind: DcSwitchKind::Switch,
5495 rated_dc_voltage_kv: None,
5496 open: None,
5497 resistance_ohm: None,
5498 }],
5499 voltage_source_converters: vec![voltage_source_converter],
5500 line_commutated_converters: vec![line_commutated_converter],
5501 ..DetailedConnectivity::default()
5502 };
5503 let mut network = BalancedNetwork::new("dc-reference-test", 100.0);
5504 *network.detailed_connectivity_mut() = Some(std::sync::Arc::new(detailed));
5505 network
5506 }
5507
5508 fn reference_test_detailed_mut(network: &mut BalancedNetwork) -> &mut DetailedConnectivity {
5509 std::sync::Arc::make_mut(
5510 network
5511 .detailed_connectivity_mut()
5512 .as_mut()
5513 .expect("reference test has detailed connectivity"),
5514 )
5515 }
5516
5517 fn assert_invalid(network: &BalancedNetwork, expected: &str) {
5518 let error = network.validate().unwrap_err().to_string();
5519 assert!(
5520 error.contains(expected),
5521 "expected `{expected}` in `{error}`"
5522 );
5523 }
5524
5525 fn assert_duplicate_dc_converter_unit_rejected(valid: &BalancedNetwork) {
5526 let mut network = valid.clone();
5527 let unit = reference_test_detailed_mut(&mut network).dc_converter_units[0].clone();
5528 reference_test_detailed_mut(&mut network)
5529 .dc_converter_units
5530 .push(unit);
5531 assert_invalid(&network, "duplicate DCConverterUnit");
5532 }
5533
5534 #[test]
5535 fn detailed_dc_references_are_complete_and_checked() {
5536 let valid = detailed_dc_reference_test_network();
5537 valid.validate().unwrap();
5538
5539 assert_duplicate_dc_converter_unit_rejected(&valid);
5540
5541 let mut missing_substation = valid.clone();
5542 reference_test_detailed_mut(&mut missing_substation).dc_converter_units[0].substation =
5543 Some(component("substation", "missing"));
5544 assert_invalid(&missing_substation, "references unknown Substation");
5545
5546 let mut duplicate_topological_node = valid.clone();
5547 let node = reference_test_detailed_mut(&mut duplicate_topological_node)
5548 .dc_topological_nodes[0]
5549 .clone();
5550 reference_test_detailed_mut(&mut duplicate_topological_node)
5551 .dc_topological_nodes
5552 .push(node);
5553 assert_invalid(&duplicate_topological_node, "duplicate DCTopologicalNode");
5554
5555 let mut missing_topological_unit = valid.clone();
5556 reference_test_detailed_mut(&mut missing_topological_unit).dc_topological_nodes[0]
5557 .dc_converter_unit = Some(component("dc_converter_unit", "missing"));
5558 assert_invalid(
5559 &missing_topological_unit,
5560 "DCTopologicalNode `T` references unknown DCConverterUnit",
5561 );
5562
5563 let mut missing_node_unit = valid.clone();
5564 reference_test_detailed_mut(&mut missing_node_unit).dc_nodes[0].dc_converter_unit =
5565 Some(component("dc_converter_unit", "missing"));
5566 assert_invalid(
5567 &missing_node_unit,
5568 "DCNode `N` references unknown DCConverterUnit",
5569 );
5570
5571 let mut missing_node_topology = valid.clone();
5572 reference_test_detailed_mut(&mut missing_node_topology).dc_nodes[0].dc_topological_node =
5573 Some(component("dc_topological_node", "missing"));
5574 assert_invalid(
5575 &missing_node_topology,
5576 "DCNode `N` references unknown DCTopologicalNode",
5577 );
5578
5579 let mut missing_container = valid.clone();
5580 reference_test_detailed_mut(&mut missing_container).dc_lines[0].equipment_container =
5581 Some(component("dc_converter_unit", "missing"));
5582 assert_invalid(
5583 &missing_container,
5584 "DCLineSegment `L` references unknown equipment container",
5585 );
5586
5587 let mut missing_converter_unit = valid.clone();
5588 reference_test_detailed_mut(&mut missing_converter_unit).voltage_source_converters[0]
5589 .dc_converter_unit = Some(component("dc_converter_unit", "missing"));
5590 assert_invalid(
5591 &missing_converter_unit,
5592 "VsConverter `VSC` references unknown DCConverterUnit",
5593 );
5594
5595 let mut terminal_without_node = valid.clone();
5596 let terminal =
5597 &mut reference_test_detailed_mut(&mut terminal_without_node).dc_grounds[0].dc_terminal;
5598 terminal.dc_node = None;
5599 terminal.dc_topological_node = None;
5600 assert_invalid(
5601 &terminal_without_node,
5602 "references neither a DCNode nor a DCTopologicalNode",
5603 );
5604
5605 let mut missing_physical_node = valid.clone();
5606 reference_test_detailed_mut(&mut missing_physical_node).dc_grounds[0]
5607 .dc_terminal
5608 .dc_node = Some(component("dc_node", "missing"));
5609 assert_invalid(&missing_physical_node, "references unknown DCNode");
5610
5611 let mut missing_terminal_topology = valid.clone();
5612 let terminal = &mut reference_test_detailed_mut(&mut missing_terminal_topology).dc_grounds
5613 [0]
5614 .dc_terminal;
5615 terminal.dc_node = None;
5616 terminal.dc_topological_node = Some(component("dc_topological_node", "missing"));
5617 assert_invalid(
5618 &missing_terminal_topology,
5619 "references unknown DCTopologicalNode",
5620 );
5621
5622 let mut duplicate_terminal = valid.clone();
5623 let terminal_id = reference_test_detailed_mut(&mut duplicate_terminal).dc_grounds[0]
5624 .dc_terminal
5625 .component
5626 .clone();
5627 reference_test_detailed_mut(&mut duplicate_terminal).dc_lines[0]
5628 .dc_terminal1
5629 .component = terminal_id;
5630 assert_invalid(&duplicate_terminal, "duplicate DCTerminal");
5631
5632 let mut missing_pcc_terminal = valid;
5633 reference_test_detailed_mut(&mut missing_pcc_terminal).voltage_source_converters[0]
5634 .pcc_terminal
5635 .as_mut()
5636 .unwrap()
5637 .terminal = 2;
5638 assert_invalid(&missing_pcc_terminal, "references undeclared PCC Terminal");
5639 }
5640
5641 #[test]
5642 #[allow(clippy::too_many_lines)] fn detailed_connectivity_dc_equipment_round_trips_and_defaults() {
5644 let empty: DetailedConnectivity = serde_json::from_str("{}").unwrap();
5645 assert_eq!(empty, DetailedConnectivity::default());
5646
5647 let node1 = component("dc_node", "dc-1");
5648 let node2 = component("dc_node", "dc-2");
5649 let terminal = |dc_node: ComponentId, connected: bool, power| DcTerminal {
5650 component: None,
5651 sequence_number: None,
5652 dc_node: Some(dc_node),
5653 dc_topological_node: None,
5654 polarity: None,
5655 connected: Some(connected),
5656 active_power_mw: power,
5657 current_a: power.map(|value| value / 320.0),
5658 };
5659 let unit = component("dc_converter_unit", "unit");
5660 let topological_node = component("dc_topological_node", "dc-topology");
5661 let pcc_terminal = TerminalReference {
5662 equipment: component("branch", "pcc"),
5663 terminal: 2,
5664 };
5665 let droop_curve = DroopCurve {
5666 segments: vec![DroopCurveSegment {
5667 minimum_voltage_kv: 300.0,
5668 maximum_voltage_kv: 340.0,
5669 k: 0.4,
5670 }],
5671 };
5672 let mut curve_properties = BTreeMap::new();
5673 curve_properties.insert("source".into(), "test".into());
5674 let reactive_limits = ReactiveLimits::CapabilityCurve(ReactiveCapabilityCurve {
5675 curve_style: CurveStyle::StraightLineYValues,
5676 properties: curve_properties,
5677 points: vec![
5678 ReactiveCapabilityCurvePoint {
5679 active_power_mw: -100.0,
5680 minimum_reactive_power_mvar: -50.0,
5681 maximum_reactive_power_mvar: 40.0,
5682 properties: BTreeMap::new(),
5683 },
5684 ReactiveCapabilityCurvePoint {
5685 active_power_mw: 100.0,
5686 minimum_reactive_power_mvar: -40.0,
5687 maximum_reactive_power_mvar: 50.0,
5688 properties: BTreeMap::new(),
5689 },
5690 ],
5691 });
5692 let detailed = DetailedConnectivity {
5693 terminals: vec![Terminal {
5694 component: None,
5695 equipment: component("voltage_source_converter", "vsc"),
5696 terminal: 1,
5697 voltage_level: component("voltage_level", "vl"),
5698 bus: None,
5699 connectable_bus: None,
5700 node: Some(component("connectivity_node", "n1")),
5701 connected: true,
5702 active_power_mw: Some(-95.0),
5703 reactive_power_mvar: Some(12.0),
5704 }],
5705 dc_converter_units: vec![DcConverterUnit {
5706 component: unit.clone(),
5707 substation: Some(component("substation", "station")),
5708 operation_mode: DcConverterOperatingMode::Bipolar,
5709 }],
5710 dc_topological_nodes: vec![DcTopologicalNode {
5711 component: topological_node.clone(),
5712 dc_converter_unit: Some(unit.clone()),
5713 }],
5714 dc_nodes: vec![
5715 DcNode {
5716 component: node1.clone(),
5717 nominal_voltage_kv: Some(320.0),
5718 dc_converter_unit: Some(unit.clone()),
5719 dc_topological_node: Some(topological_node.clone()),
5720 voltage_kv: Some(318.0),
5721 },
5722 DcNode {
5723 component: node2.clone(),
5724 nominal_voltage_kv: Some(320.0),
5725 dc_converter_unit: Some(unit.clone()),
5726 dc_topological_node: Some(topological_node),
5727 voltage_kv: None,
5728 },
5729 ],
5730 dc_grounds: vec![DcGround {
5731 component: component("dc_ground", "ground"),
5732 equipment_container: Some(unit.clone()),
5733 dc_terminal: terminal(node1.clone(), true, Some(0.5)),
5734 rated_dc_voltage_kv: Some(320.0),
5735 resistance_ohm: Some(2.0),
5736 inductance_h: Some(0.01),
5737 }],
5738 dc_lines: vec![DcLine {
5739 component: component("dc_line", "line"),
5740 equipment_container: Some(unit.clone()),
5741 dc_terminal1: terminal(node1.clone(), true, Some(100.0)),
5742 dc_terminal2: terminal(node2.clone(), false, Some(-99.0)),
5743 rated_dc_voltage_kv: Some(320.0),
5744 resistance_ohm: Some(1.5),
5745 inductance_h: Some(0.02),
5746 capacitance_f: Some(0.001),
5747 length_km: Some(20.0),
5748 }],
5749 dc_switches: vec![DcSwitch {
5750 component: component("dc_switch", "breaker"),
5751 equipment_container: Some(unit.clone()),
5752 dc_terminal1: terminal(node1.clone(), true, None),
5753 dc_terminal2: terminal(node2.clone(), true, None),
5754 kind: DcSwitchKind::Breaker,
5755 rated_dc_voltage_kv: Some(320.0),
5756 open: Some(false),
5757 resistance_ohm: Some(0.01),
5758 }],
5759 voltage_source_converters: vec![VoltageSourceConverter {
5760 component: component("voltage_source_converter", "vsc"),
5761 dc_converter_unit: Some(unit.clone()),
5762 dc_terminal1: terminal(node1.clone(), true, Some(-100.0)),
5763 dc_terminal2: terminal(node2.clone(), true, Some(100.0)),
5764 base_apparent_power_mva: Some(200.0),
5765 minimum_active_power_mw: Some(-150.0),
5766 maximum_active_power_mw: Some(150.0),
5767 minimum_dc_voltage_kv: Some(300.0),
5768 maximum_dc_voltage_kv: Some(340.0),
5769 rated_dc_voltage_kv: Some(320.0),
5770 valve_u0_kv: Some(0.1),
5771 number_of_valves: Some(4),
5772 idle_loss_mw: Some(1.0),
5773 switching_loss_mw_per_ampere: Some(0.002),
5774 resistive_loss_ohm: Some(0.1),
5775 control_mode: Some(
5776 AcDcConverterControlMode::ActivePowerAtPccAndDcVoltageDroopCurve,
5777 ),
5778 active_power_at_pcc_mw: Some(95.0),
5779 reactive_power_at_pcc_mvar: Some(12.0),
5780 target_active_power_mw: Some(95.0),
5781 target_dc_voltage_kv: Some(320.0),
5782 pcc_terminal: Some(pcc_terminal.clone()),
5783 droop_curve: Some(droop_curve.clone()),
5784 droop: Some(2.0),
5785 droop_compensation: Some(0.1),
5786 q_share: Some(0.5),
5787 maximum_modulation_index: Some(0.9),
5788 maximum_valve_current_a: Some(800.0),
5789 voltage_regulator_on: Some(true),
5790 voltage_setpoint_kv: Some(230.0),
5791 reactive_power_setpoint_mvar: None,
5792 reactive_limits: Some(reactive_limits),
5793 pole_loss_active_power_mw: Some(1.2),
5794 dc_current_a: Some(300.0),
5795 ac_voltage_kv: Some(230.0),
5796 dc_voltage_kv: Some(318.0),
5797 delta_degrees: Some(1.0),
5798 uf_kv: Some(229.0),
5799 uv_kv: Some(231.0),
5800 }],
5801 line_commutated_converters: vec![LineCommutatedConverter {
5802 component: component("line_commutated_converter", "lcc"),
5803 dc_converter_unit: Some(unit),
5804 dc_terminal1: terminal(node1, true, Some(-75.0)),
5805 dc_terminal2: terminal(node2, true, Some(75.0)),
5806 base_apparent_power_mva: Some(160.0),
5807 minimum_active_power_mw: Some(-120.0),
5808 maximum_active_power_mw: Some(120.0),
5809 minimum_dc_voltage_kv: Some(300.0),
5810 maximum_dc_voltage_kv: Some(340.0),
5811 rated_dc_voltage_kv: Some(320.0),
5812 valve_u0_kv: Some(0.2),
5813 number_of_valves: Some(6),
5814 idle_loss_mw: Some(0.5),
5815 switching_loss_mw_per_ampere: Some(0.001),
5816 resistive_loss_ohm: Some(0.2),
5817 control_mode: Some(AcDcConverterControlMode::DcVoltage),
5818 active_power_at_pcc_mw: Some(-75.0),
5819 reactive_power_at_pcc_mvar: Some(-20.0),
5820 target_active_power_mw: None,
5821 target_dc_voltage_kv: Some(320.0),
5822 pcc_terminal: Some(pcc_terminal),
5823 droop_curve: Some(droop_curve),
5824 reactive_model: Some(LineCommutatedConverterReactiveModel::FixedPowerFactor),
5825 power_factor: Some(0.95),
5826 operating_mode: Some(LineCommutatedConverterOperatingMode::Rectifier),
5827 rated_dc_current_a: Some(500.0),
5828 minimum_alpha_degrees: Some(5.0),
5829 maximum_alpha_degrees: Some(30.0),
5830 minimum_gamma_degrees: Some(10.0),
5831 maximum_gamma_degrees: Some(35.0),
5832 target_alpha_degrees: Some(15.0),
5833 target_gamma_degrees: Some(20.0),
5834 target_dc_current_a: Some(250.0),
5835 pole_loss_active_power_mw: Some(0.8),
5836 dc_current_a: Some(250.0),
5837 ac_voltage_kv: Some(230.0),
5838 dc_voltage_kv: Some(319.0),
5839 alpha_degrees: Some(14.0),
5840 gamma_degrees: Some(19.0),
5841 }],
5842 ..DetailedConnectivity::default()
5843 };
5844
5845 let json = serde_json::to_string(&detailed).unwrap();
5846 let restored: DetailedConnectivity = serde_json::from_str(&json).unwrap();
5847 assert_eq!(restored, detailed);
5848
5849 let mut older_terminal = serde_json::to_value(&detailed.terminals[0]).unwrap();
5850 let older_terminal = older_terminal.as_object_mut().unwrap();
5851 older_terminal.remove("active_power_mw");
5852 older_terminal.remove("reactive_power_mvar");
5853 let older_terminal: Terminal =
5854 serde_json::from_value(serde_json::Value::Object(older_terminal.clone())).unwrap();
5855 assert_eq!(older_terminal.active_power_mw, None);
5856 assert_eq!(older_terminal.reactive_power_mvar, None);
5857
5858 let min_max = ReactiveLimits::MinMax(MinMaxReactiveLimits {
5859 minimum_reactive_power_mvar: -25.0,
5860 maximum_reactive_power_mvar: 30.0,
5861 properties: BTreeMap::new(),
5862 });
5863 let json = serde_json::to_string(&min_max).unwrap();
5864 assert_eq!(
5865 serde_json::from_str::<ReactiveLimits>(&json).unwrap(),
5866 min_max
5867 );
5868 }
5869
5870 #[test]
5871 fn converter_droop_control_modes_have_distinct_serialized_names() {
5872 for (mode, name) in [
5873 (
5874 AcDcConverterControlMode::ActivePowerAtPccAndDcVoltageDroopCurve,
5875 "active_power_at_pcc_and_dc_voltage_droop_curve",
5876 ),
5877 (
5878 AcDcConverterControlMode::ActivePowerAtPccAndDcVoltageDroop,
5879 "active_power_at_pcc_and_dc_voltage_droop",
5880 ),
5881 (
5882 AcDcConverterControlMode::ActivePowerAtPccAndDcVoltageDroopWithCompensation,
5883 "active_power_at_pcc_and_dc_voltage_droop_with_compensation",
5884 ),
5885 (
5886 AcDcConverterControlMode::ActivePowerAtPccAndDcVoltageDroopPilot,
5887 "active_power_at_pcc_and_dc_voltage_droop_pilot",
5888 ),
5889 ] {
5890 let value = serde_json::to_value(mode).unwrap();
5891 assert_eq!(value, serde_json::Value::String(name.to_owned()));
5892 assert_eq!(
5893 serde_json::from_value::<AcDcConverterControlMode>(value).unwrap(),
5894 mode
5895 );
5896 }
5897 }
5898
5899 #[test]
5900 fn assigned_component_ids_follow_records_after_reordering() {
5901 let mut network = BalancedNetwork::in_memory(
5902 "ids",
5903 100.0,
5904 vec![
5905 Bus::new(BusId(10), BusType::Ref, 230.0),
5906 Bus::new(BusId(20), BusType::Pq, 230.0),
5907 ],
5908 vec![
5909 Branch::new(BusId(10), BusId(20), 0.0, 0.1),
5910 Branch::new(BusId(10), BusId(20), 0.0, 0.2),
5911 ],
5912 );
5913 network.loads_mut().extend([
5914 Load::new(BusId(20), 10.0, 1.0),
5915 Load::new(BusId(20), 20.0, 2.0),
5916 ]);
5917 network.assign_missing_component_ids();
5918
5919 assert_eq!(network.buses()[0].uid.as_deref(), Some("10"));
5920 assert_eq!(network.branches()[0].uid.as_deref(), Some("10-20"));
5921 assert_eq!(network.branches()[1].uid.as_deref(), Some("10-20-2"));
5922 assert_eq!(network.loads()[0].uid.as_deref(), Some("bus-20"));
5923 assert_eq!(network.loads()[1].uid.as_deref(), Some("bus-20-2"));
5924 assert!(network.uid_is_generated(network.loads()[0].uid.as_deref()));
5925
5926 network.loads_mut().swap(0, 1);
5927 network.branches_mut().swap(0, 1);
5928 network.assign_missing_component_ids();
5929 assert_eq!(network.loads()[0].uid.as_deref(), Some("bus-20-2"));
5930 assert_eq!(network.loads()[1].uid.as_deref(), Some("bus-20"));
5931 assert_eq!(network.branches()[0].uid.as_deref(), Some("10-20-2"));
5932 assert_eq!(network.branches()[1].uid.as_deref(), Some("10-20"));
5933
5934 let restored = serde_round_trip(&network);
5935 assert_eq!(restored.loads()[0].uid, network.loads()[0].uid);
5936 assert_eq!(restored.branches()[0].uid, network.branches()[0].uid);
5937 assert!(restored.uid_is_generated(restored.loads()[0].uid.as_deref()));
5938 }
5939
5940 #[test]
5941 fn source_and_generated_component_identities_remain_distinct() {
5942 let mut explicit = Bus::new(BusId(10), BusType::Ref, 230.0);
5943 explicit.uid = Some("source-bus".to_owned());
5944 let mut network = BalancedNetwork::new("identity provenance", 100.0);
5945 network
5946 .buses_mut()
5947 .extend([explicit, Bus::new(BusId(20), BusType::Pq, 230.0)]);
5948 network.assign_missing_component_ids();
5949
5950 assert!(!network.uid_is_generated(network.buses()[0].uid.as_deref()));
5951 assert!(network.uid_is_generated(network.buses()[1].uid.as_deref()));
5952 }
5953
5954 #[test]
5957 fn generated_identities_are_unique_across_tables() {
5958 let mut network = BalancedNetwork::in_memory(
5959 "cross table ids",
5960 100.0,
5961 vec![
5962 Bus::new(BusId(10), BusType::Ref, 230.0),
5963 Bus::new(BusId(20), BusType::Pq, 230.0),
5964 ],
5965 vec![Branch::new(BusId(10), BusId(20), 0.0, 0.1)],
5966 );
5967 network.loads_mut().push(Load::new(BusId(20), 10.0, 1.0));
5968 network.shunts_mut().push(Shunt::new(BusId(20), 0.0, 0.5));
5969 network.generators_mut().push(Generator::new(BusId(20)));
5970 network.assign_missing_component_ids();
5971
5972 let mut identities = vec![
5973 network.loads()[0].uid.clone().unwrap(),
5974 network.shunts()[0].uid.clone().unwrap(),
5975 network.generators()[0].uid.clone().unwrap(),
5976 ];
5977 identities.sort();
5978 assert_eq!(identities, ["bus-20", "bus-20-2", "bus-20-3"]);
5979
5980 let before = identities.clone();
5983 network.assign_missing_component_ids();
5984 let mut after = vec![
5985 network.loads()[0].uid.clone().unwrap(),
5986 network.shunts()[0].uid.clone().unwrap(),
5987 network.generators()[0].uid.clone().unwrap(),
5988 ];
5989 after.sort();
5990 assert_eq!(after, before);
5991 }
5992
5993 #[test]
5994 fn source_format_serializes_as_its_name_token() {
5995 let all = [
5998 SourceFormat::Sincal,
5999 SourceFormat::Matpower,
6000 SourceFormat::PowerModelsJson,
6001 SourceFormat::EgretJson,
6002 SourceFormat::Psse,
6003 SourceFormat::PsseRawx,
6004 SourceFormat::PowerWorld,
6005 SourceFormat::PandapowerJson,
6006 SourceFormat::Pslf,
6007 SourceFormat::PowerWorldBinary,
6008 SourceFormat::InMemory,
6009 SourceFormat::Normalized,
6010 SourceFormat::Gridfm,
6011 SourceFormat::PypsaCsv,
6012 SourceFormat::Goc3Json,
6013 SourceFormat::SurgeJson,
6014 SourceFormat::DeepMindOpfDataJson,
6015 SourceFormat::Xiidm,
6016 SourceFormat::Jiidm,
6017 SourceFormat::Cgmes,
6018 SourceFormat::Ucte,
6019 SourceFormat::IeeeCdf,
6020 ];
6021 for f in all {
6022 match f {
6023 SourceFormat::Matpower
6024 | SourceFormat::PowerModelsJson
6025 | SourceFormat::EgretJson
6026 | SourceFormat::Psse
6027 | SourceFormat::PsseRawx
6028 | SourceFormat::PowerWorld
6029 | SourceFormat::PandapowerJson
6030 | SourceFormat::Pslf
6031 | SourceFormat::PowerWorldBinary
6032 | SourceFormat::InMemory
6033 | SourceFormat::Normalized
6034 | SourceFormat::Gridfm
6035 | SourceFormat::PypsaCsv
6036 | SourceFormat::Goc3Json
6037 | SourceFormat::SurgeJson
6038 | SourceFormat::DeepMindOpfDataJson
6039 | SourceFormat::Xiidm
6040 | SourceFormat::Jiidm
6041 | SourceFormat::Cgmes
6042 | SourceFormat::Ucte
6043 | SourceFormat::IeeeCdf
6044 | SourceFormat::Sincal => {}
6045 }
6046 let token = serde_json::to_value(f).unwrap();
6047 assert_eq!(token, serde_json::Value::String(f.name().to_owned()));
6048 let back: SourceFormat = serde_json::from_value(token).unwrap();
6049 assert_eq!(back, f);
6050 }
6051 }
6052
6053 #[test]
6054 fn quadratic_with_constant_keeps_c0_across_ncost() {
6055 let full = GenCost::new(2, 0.0, 0.0, vec![1.5, 2.0, 5.0]);
6056 assert_eq!(full.calc_quadratic_with_constant(), Some((3.0, 2.0, 5.0)));
6057 assert_eq!(full.calc_quadratic(), Some((3.0, 2.0)));
6058 assert_eq!(
6059 full.calc_quadratic_with_constant(),
6060 full.calc_quadratic_with_constant()
6061 );
6062 assert_eq!(full.calc_quadratic(), full.calc_quadratic());
6063
6064 let linear = GenCost::new(2, 0.0, 0.0, vec![2.0, 5.0]);
6065 assert_eq!(linear.calc_quadratic_with_constant(), Some((0.0, 2.0, 5.0)));
6066
6067 let constant = GenCost::new(2, 0.0, 0.0, vec![5.0]);
6068 assert_eq!(
6069 constant.calc_quadratic_with_constant(),
6070 Some((0.0, 0.0, 5.0))
6071 );
6072
6073 let piecewise = GenCost::new(1, 0.0, 0.0, vec![0.0, 0.0, 1.0, 1.0]);
6074 assert_eq!(piecewise.calc_quadratic_with_constant(), None);
6075
6076 let cubic = GenCost::new(2, 0.0, 0.0, vec![1.0, 1.0, 1.0, 1.0]);
6077 assert_eq!(cubic.calc_quadratic_with_constant(), None);
6078
6079 let truncated = GenCost::with_ncost(2, 0.0, 0.0, 3, vec![1.0]);
6080 assert_eq!(truncated.calc_quadratic_with_constant(), None);
6081 }
6082
6083 #[test]
6084 fn a_leading_coefficient_below_the_tolerance_comes_off_the_row() {
6085 let artifact = GenCost::new(2, 0.0, 0.0, vec![1e-17, 2.0, 5.0]);
6086 assert_eq!(
6087 artifact.calc_quadratic_with_constant(),
6088 Some((2e-17, 2.0, 5.0)),
6089 "the untouched reader keeps the artifact"
6090 );
6091 assert_eq!(
6092 artifact.calc_quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
6093 Some((0.0, 2.0, 5.0))
6094 );
6095 assert_eq!(
6096 artifact.calc_quadratic_with_constant_tol(0.0),
6097 Some((2e-17, 2.0, 5.0)),
6098 "a zero tolerance strips an exact zero alone"
6099 );
6100
6101 let padded = GenCost::new(2, 0.0, 0.0, vec![0.0, 1.5, 2.0, 5.0]);
6104 assert_eq!(padded.calc_quadratic_with_constant(), None);
6105 assert_eq!(
6106 padded.calc_quadratic_with_constant_tol(0.0),
6107 Some((3.0, 2.0, 5.0))
6108 );
6109
6110 let flat = GenCost::new(2, 0.0, 0.0, vec![1e-17, 1e-17, 1e-17]);
6111 assert_eq!(
6112 flat.calc_quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
6113 Some((0.0, 0.0, 1e-17)),
6114 "the last coefficient stays, whatever its magnitude"
6115 );
6116
6117 let piecewise = GenCost::new(1, 0.0, 0.0, vec![0.0, 0.0, 1.0, 1.0]);
6118 assert_eq!(
6119 piecewise.calc_quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
6120 None
6121 );
6122
6123 let truncated = GenCost::with_ncost(2, 0.0, 0.0, 3, vec![1.0]);
6124 assert_eq!(
6125 truncated.calc_quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
6126 None
6127 );
6128
6129 let quartic = GenCost::new(2, 0.0, 0.0, vec![1.0, 1.0, 1.0, 1.0, 1.0]);
6130 assert_eq!(
6131 quartic.calc_quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
6132 None
6133 );
6134 }
6135
6136 fn expected_rate(window: f64, fr: f64, to: f64, zmag: f64) -> f64 {
6139 let separation = (fr * fr + to * to - 2.0 * fr * to * window.cos()).sqrt();
6140 fr.max(to) * separation / zmag
6141 }
6142
6143 #[test]
6144 fn synthesized_rate_follows_the_angle_window_and_the_voltage_bands() {
6145 let br = Branch::new(BusId(1), BusId(2), 0.03, 0.04);
6146 let expected = |window: f64, fr: f64, to: f64| expected_rate(window, fr, to, 0.05);
6147 let at = |v: f64| (v, v);
6150 close(
6151 br.synthesize_rate_a(0.5, at(1.1), at(1.06)),
6152 expected(0.5, 1.1, 1.06),
6153 );
6154
6155 assert!(
6157 br.synthesize_rate_a(0.8, at(1.1), at(1.06))
6158 > br.synthesize_rate_a(0.5, at(1.1), at(1.06))
6159 );
6160
6161 close(
6163 br.synthesize_rate_a(-0.5, at(1.1), at(1.06)),
6164 expected(0.5, 1.1, 1.06),
6165 );
6166 for window in [6.0, 2.0 * std::f64::consts::PI, -360.0] {
6167 close(
6168 br.synthesize_rate_a(window, at(1.1), at(1.06)),
6169 expected(std::f64::consts::PI, 1.1, 1.06),
6170 );
6171 }
6172
6173 let ideal = Branch::new(BusId(1), BusId(2), 0.0, 0.0);
6174 close(ideal.synthesize_rate_a(0.5, at(1.1), at(1.1)), 0.0);
6175 }
6176
6177 #[test]
6178 fn a_narrow_window_bounds_at_the_mixed_voltage_corner() {
6179 let br = Branch::new(BusId(1), BusId(2), 0.0, 0.01);
6186 let (vmin, vmax) = (0.9, 1.1);
6187 let corner = |window: f64, fr: f64, to: f64| expected_rate(window, fr, to, 0.01);
6188
6189 let narrow = 2.0_f64.to_radians();
6190 let bound = br.synthesize_rate_a(narrow, (vmin, vmax), (vmin, vmax));
6191 close(bound, corner(narrow, vmax, vmin));
6192 assert!(
6193 bound > 5.0 * corner(narrow, vmax, vmax),
6194 "the mixed corner dominates here: {bound} vs {}",
6195 corner(narrow, vmax, vmax)
6196 );
6197
6198 let wide = 30.0_f64.to_radians();
6200 close(
6201 br.synthesize_rate_a(wide, (vmin, vmax), (vmin, vmax)),
6202 corner(wide, vmax, vmax),
6203 );
6204 }
6205
6206 fn bus(id: usize) -> Bus {
6207 Bus {
6208 id: BusId(id),
6209 kind: BusType::Pq,
6210 vm: 1.0,
6211 va: 0.0,
6212 base_kv: 230.0,
6213 vmax: 1.1,
6214 vmin: 0.9,
6215 evhi: None,
6216 evlo: None,
6217 area: 1,
6218 zone: 1,
6219 name: None,
6220 uid: None,
6221 location: None,
6222 extras: Extras::new(),
6223 }
6224 }
6225
6226 fn winding(b: usize) -> Winding {
6227 Winding {
6228 bus: BusId(b),
6229 tap: 1.0,
6230 shift: 0.0,
6231 nominal_kv: 230.0,
6232 rate_a: 100.0,
6233 rate_b: 0.0,
6234 rate_c: 0.0,
6235 control: None,
6236 }
6237 }
6238
6239 fn transformer_3w() -> Transformer3W {
6240 let z = |r, x| Impedance {
6241 r,
6242 x,
6243 base_mva: 100.0,
6244 };
6245 Transformer3W {
6246 windings: [winding(1), winding(2), winding(3)],
6247 z: [z(0.01, 0.10), z(0.02, 0.20), z(0.03, 0.30)],
6248 star_vm: 0.98,
6249 star_va: -1.5,
6250 mag_g: 0.0,
6251 mag_b: 0.0,
6252 in_service: true,
6253 name: Some("T1".into()),
6254 uid: None,
6255 extras: Extras::new(),
6256 }
6257 }
6258
6259 #[test]
6260 fn star_impedances_split_the_pairwise_values() {
6261 let [(r1, x1), (r2, x2), (r3, x3)] = transformer_3w().calc_star_impedances();
6263 close(r1, 0.01);
6264 close(x1, 0.10);
6265 close(r2, 0.0);
6266 close(x2, 0.0);
6267 close(r3, 0.02);
6268 close(x3, 0.20);
6269 }
6270
6271 #[test]
6272 fn star_expansion_builds_a_star_bus_and_three_branches() {
6273 let t = transformer_3w();
6274 let (star, branches) = t.to_star_expansion(BusId(99));
6275
6276 assert_eq!(star.id, BusId(99));
6277 close(star.vm, 0.98);
6278 close(star.va, -1.5);
6279 for (i, br) in branches.iter().enumerate() {
6282 assert_eq!(br.from, t.windings[i].bus);
6283 assert_eq!(br.to, BusId(99));
6284 close(br.tap, 1.0);
6285 close(br.rate_a, 100.0);
6286 }
6287 close(branches[2].r, 0.02);
6288 close(branches[2].x, 0.20);
6289 }
6290
6291 #[test]
6292 fn star_expansion_carries_winding_status_and_rating_sets() {
6293 let mut t = transformer_3w();
6294 t.set_winding_in_service([true, false, true]);
6295 t.set_winding_rating_sets(2, vec![BranchRatingSet::new("RATE4", 75.0)]);
6296 let (_, branches) = t.to_star_expansion(BusId(99));
6297 assert_eq!(
6298 branches.each_ref().map(|br| br.in_service),
6299 [true, false, true]
6300 );
6301 assert_eq!(
6302 branches[2].rating_sets,
6303 [BranchRatingSet::new("RATE4", 75.0)]
6304 );
6305 assert_eq!(branches[0].rating_sets, []);
6306
6307 t.in_service = false;
6309 let (_, branches) = t.to_star_expansion(BusId(99));
6310 assert!(branches.iter().all(|br| !br.in_service));
6311
6312 t.in_service = true;
6315 let mut net =
6316 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
6317 net.transformers_3w_mut().push(t);
6318 let lowered = net.expand_transformers_3w();
6319 assert_eq!(
6320 lowered
6321 .branches()
6322 .iter()
6323 .map(|br| (br.from, br.in_service))
6324 .collect::<Vec<_>>(),
6325 [(BusId(1), true), (BusId(2), false), (BusId(3), true)]
6326 );
6327 assert_eq!(net.lowered_lengths().branches, lowered.branches().len());
6328 }
6329
6330 #[test]
6331 fn winding_status_and_rating_sets_live_in_extras_only_when_stated() {
6332 let mut t = transformer_3w();
6333 assert_eq!(t.winding_in_service(), [true; 3]);
6334 assert_eq!(t.winding_rating_sets(1), []);
6335
6336 t.set_winding_in_service([false, true, true]);
6337 t.set_winding_rating_sets(1, vec![BranchRatingSet::new("RATE4", 75.0)]);
6338 assert_eq!(
6339 t.extras["winding_in_service"],
6340 serde_json::json!([false, true, true])
6341 );
6342 assert_eq!(
6343 t.extras["winding_rating_sets"],
6344 serde_json::json!([[], [{"name": "RATE4", "rate_mva": 75.0}], []])
6345 );
6346 let mut net =
6347 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
6348 net.transformers_3w_mut().push(t.clone());
6349 let back = serde_round_trip(&net);
6350 assert_eq!(
6351 back.transformers_3w()[0].winding_in_service(),
6352 [false, true, true]
6353 );
6354 assert_eq!(
6355 back.transformers_3w()[0].winding_rating_sets(1),
6356 [BranchRatingSet::new("RATE4", 75.0)]
6357 );
6358
6359 t.set_winding_in_service([true; 3]);
6361 t.set_winding_rating_sets(1, Vec::new());
6362 assert!(t.extras.is_empty(), "{:?}", t.extras);
6363
6364 t.extras
6366 .insert("winding_in_service".to_owned(), Value::from("open"));
6367 assert_eq!(t.winding_in_service(), [true; 3]);
6368 }
6369
6370 #[test]
6371 fn three_winding_transformer_survives_serde_round_trip() {
6372 let mut net =
6373 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
6374 net.transformers_3w_mut().push(transformer_3w());
6375 net.validate().unwrap();
6376
6377 let back = serde_round_trip(&net);
6378 assert_eq!(back.transformers_3w().len(), 1);
6379 close(back.transformers_3w()[0].z[1].x, 0.20);
6380 assert_eq!(back.transformers_3w()[0].windings[2].bus, BusId(3));
6381 }
6382
6383 #[test]
6384 fn a_network_with_only_out_of_service_three_winding_units_is_not_copied() {
6385 let mut out_of_service = transformer_3w();
6386 out_of_service.in_service = false;
6387 let mut net =
6388 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
6389 net.transformers_3w_mut().push(out_of_service);
6390 assert!(matches!(
6391 net.expand_transformers_3w(),
6392 std::borrow::Cow::Borrowed(_)
6393 ));
6394 let core = crate::IndexCore::build(&net);
6396 let view = crate::IndexedNetwork::with_core(&net, &core);
6397 assert!(std::ptr::eq(view.network(), std::ptr::from_ref(&net)));
6398 net.transformers_3w_mut()[0].in_service = true;
6399 assert!(matches!(
6400 net.expand_transformers_3w(),
6401 std::borrow::Cow::Owned(_)
6402 ));
6403 }
6404
6405 #[test]
6406 fn lowered_lengths_match_the_expansion() {
6407 let mut magnetizing = transformer_3w();
6412 magnetizing.mag_b = 0.02;
6413 let mut out_of_service = transformer_3w();
6414 out_of_service.in_service = false;
6415 out_of_service.mag_g = 0.01;
6416
6417 for units in [
6418 vec![],
6419 vec![transformer_3w()],
6420 vec![magnetizing.clone()],
6421 vec![out_of_service.clone()],
6422 vec![transformer_3w(), magnetizing, out_of_service],
6423 ] {
6424 let mut net =
6425 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
6426 net.shunts_mut().push(Shunt::new(BusId(1), 0.0, 0.5));
6427 *net.transformers_3w_mut() = units;
6428 let counted = net.lowered_lengths();
6429 let built = net.expand_transformers_3w();
6430 assert_eq!(counted.buses, built.buses().len());
6431 assert_eq!(counted.branches, built.branches().len());
6432 assert_eq!(counted.shunts, built.shunts().len());
6433 }
6434 }
6435
6436 #[test]
6437 fn check_references_rejects_bus_ids_without_star_expansion_headroom() {
6438 let mut net = BalancedNetwork::in_memory(
6442 "t",
6443 100.0,
6444 vec![bus(1), bus(2), bus(3), bus(i64::MAX as usize)],
6445 Vec::new(),
6446 );
6447 net.transformers_3w_mut().push(transformer_3w());
6448 let err = net.validate().unwrap_err().to_string();
6449 assert!(
6450 err.contains("no room to allocate synthetic star bus ids"),
6451 "got {err}"
6452 );
6453 }
6454
6455 #[test]
6456 fn star_expansion_headroom_counts_only_in_service_transformers() {
6457 let mut net = BalancedNetwork::in_memory(
6463 "t",
6464 100.0,
6465 vec![bus(1), bus(2), bus(3), bus(i64::MAX as usize - 1)],
6466 Vec::new(),
6467 );
6468 net.transformers_3w_mut().push(transformer_3w());
6469 let mut out_of_service = transformer_3w();
6470 out_of_service.in_service = false;
6471 net.transformers_3w_mut().push(out_of_service);
6472 net.validate()
6473 .expect("in-service count fits; must not be rejected");
6474 }
6475
6476 #[test]
6477 fn check_references_rejects_a_bus_id_past_the_int64_ceiling() {
6478 let mut net = BalancedNetwork::in_memory(
6483 "t",
6484 100.0,
6485 vec![bus(1), bus(i64::MAX as usize + 1)],
6486 Vec::new(),
6487 );
6488 let err = net.validate().unwrap_err().to_string();
6489 assert!(err.contains("outside the int64 id space"), "got {err}");
6490
6491 net.buses_mut()[1].id = BusId(i64::MAX as usize);
6493 net.validate().expect("the ceiling itself is representable");
6494 }
6495
6496 #[test]
6497 fn check_references_rejects_a_dangling_winding_bus() {
6498 let mut net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
6499 net.transformers_3w_mut().push(transformer_3w()); let err = net.validate().unwrap_err().to_string();
6501 assert!(
6502 err.contains("3-winding transformer references unknown bus 3"),
6503 "got {err}"
6504 );
6505 }
6506
6507 #[test]
6508 fn check_references_rejects_a_dangling_winding_control_bus() {
6509 let mut net =
6510 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
6511 let mut transformer = transformer_3w();
6512 let mut control = TransformerControl::new(TransformerControlMode::Voltage);
6513 control.controlled_bus = Some(BusId(4));
6514 transformer.windings[2].control = Some(control);
6515 net.transformers_3w_mut().push(transformer);
6516 let err = net.validate().unwrap_err().to_string();
6517 assert!(
6518 err.contains("3-winding transformer control references unknown bus 4"),
6519 "got {err}"
6520 );
6521 }
6522
6523 fn regulating_branch(reg: usize) -> Branch {
6525 Branch {
6526 name: None,
6527 from: BusId(1),
6528 to: BusId(2),
6529 r: 0.0,
6530 x: 0.1,
6531 b: 0.0,
6532 charging: None,
6533 rate_a: 0.0,
6534 rate_b: 0.0,
6535 rate_c: 0.0,
6536 rating_sets: Vec::new(),
6537 current_ratings: None,
6538 tap: 1.0,
6539 shift: 0.0,
6540 in_service: true,
6541 angmin: -360.0,
6542 angmax: 360.0,
6543 control: Some(TransformerControl {
6544 mode: TransformerControlMode::Voltage,
6545 enabled: true,
6546 controlled_bus: Some(BusId(reg)),
6547 controlled_bus_on_winding_side: false,
6548 regulating_terminal: None,
6549 tap_min: 0.95,
6550 tap_max: 1.05,
6551 band_min: 1.0,
6552 band_max: 1.02,
6553 ntp: 17,
6554 mva_base: 100.0,
6555 winding_connection_angle: None,
6556 }),
6557 solution: None,
6558 uid: None,
6559 route: None,
6560 extras: Extras::new(),
6561 }
6562 }
6563
6564 #[test]
6565 fn transformer_control_survives_serde_round_trip() {
6566 let mut net =
6567 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
6568 net.branches_mut().push(regulating_branch(3));
6569 net.validate().unwrap();
6570
6571 let back = serde_round_trip(&net);
6572 let c = back.branches()[0].control.as_ref().unwrap();
6573 assert_eq!(c.mode, TransformerControlMode::Voltage);
6574 assert_eq!(c.controlled_bus, Some(BusId(3)));
6575 close(c.tap_max, 1.05);
6576 assert_eq!(c.ntp, 17);
6577 }
6578
6579 #[test]
6580 fn gen_caps_serialize_as_a_named_map_that_grows_additively() {
6581 let mut caps: GenCaps = [None; GEN_EXTRA_KEYS.len()];
6582 caps[8] = Some(1.5); caps[10] = Some(0.5); let g = Generator {
6585 bus: BusId(1),
6586 energy_source: GeneratorEnergySource::default(),
6587 pg: 10.0,
6588 qg: 0.0,
6589 pmax: 100.0,
6590 pmin: 0.0,
6591 qmax: 50.0,
6592 qmin: -50.0,
6593 vg: 1.0,
6594 mbase: 100.0,
6595 in_service: true,
6596 cost: None,
6597 caps,
6598 voltage_regulation_on: false,
6599 regulating_terminal: Some(TerminalReference {
6600 equipment: ComponentId::new("load", "L1").unwrap(),
6601 terminal: 1,
6602 }),
6603 regulated_bus: Some(BusId(2)),
6604 active_power_control: None,
6605 uid: None,
6606 };
6607
6608 let json = serde_json::to_string(&g).unwrap();
6611 assert!(json.contains(r#""caps":{"#), "caps is an object: {json}");
6612 assert!(json.contains(r#""ramp_30":1.5"#) && json.contains(r#""apf":0.5"#));
6613 let back: Generator = serde_json::from_str(&json).unwrap();
6614 assert_eq!(back.caps, g.caps);
6615 assert!(!back.voltage_regulation_on);
6616 assert_eq!(back.regulating_terminal, g.regulating_terminal);
6617 assert_eq!(back.regulated_bus, Some(BusId(2)));
6618
6619 let with_future = r#"{"bus":1,"pg":10,"qg":0,"pmax":100,"pmin":0,"qmax":50,"qmin":-50,
6622 "vg":1,"mbase":100,"in_service":true,"cost":null,
6623 "caps":{"ramp_30":1.5,"future_ramp":9.9}}"#;
6624 let g2: Generator = serde_json::from_str(with_future).unwrap();
6625 assert_eq!(g2.caps[8], Some(1.5));
6626 assert_eq!(g2.caps.iter().filter(|v| v.is_some()).count(), 1);
6627 let no_caps = r#"{"bus":1,"pg":10,"qg":0,"pmax":100,"pmin":0,"qmax":50,"qmin":-50,
6628 "vg":1,"mbase":100,"in_service":true,"cost":null}"#;
6629 let g3: Generator = serde_json::from_str(no_caps).unwrap();
6630 assert!(!g3.has_caps());
6631 assert!(g3.voltage_regulation_on);
6632 assert_eq!(g3.regulating_terminal, None);
6633
6634 let null_caps = r#"{"bus":1,"pg":10,"qg":0,"pmax":100,"pmin":0,"qmax":50,"qmin":-50,
6636 "vg":1,"mbase":100,"in_service":true,"cost":null,"caps":null}"#;
6637 let g4: Generator = serde_json::from_str(null_caps).unwrap();
6638 assert!(!g4.has_caps());
6639 }
6640
6641 #[test]
6642 #[allow(clippy::float_cmp)]
6643 fn nonfinite_values_round_trip_through_serde() {
6644 let bus = |id, vm| Bus {
6645 id: BusId(id),
6646 kind: BusType::Pq,
6647 vm,
6648 va: 0.0,
6649 base_kv: 230.0,
6650 vmax: 1.1,
6651 vmin: 0.9,
6652 evhi: None,
6653 evlo: None,
6654 area: 1,
6655 zone: 1,
6656 name: None,
6657 uid: None,
6658 location: None,
6659 extras: Extras::new(),
6660 };
6661 let branch = Branch {
6662 name: None,
6663 from: BusId(1),
6664 to: BusId(2),
6665 r: 0.0,
6666 x: f64::INFINITY,
6667 b: 0.0,
6668 charging: None,
6669 rate_a: 0.0,
6670 rate_b: 0.0,
6671 rate_c: 0.0,
6672 rating_sets: Vec::new(),
6673 current_ratings: None,
6674 tap: 0.0,
6675 shift: 0.0,
6676 in_service: true,
6677 angmin: -360.0,
6678 angmax: 360.0,
6679 control: None,
6680 solution: None,
6681 uid: None,
6682 route: None,
6683 extras: Extras::new(),
6684 };
6685 let mut g = Generator {
6688 bus: BusId(1),
6689 energy_source: GeneratorEnergySource::default(),
6690 pg: 0.0,
6691 qg: 0.0,
6692 pmax: 0.0,
6693 pmin: 0.0,
6694 qmax: 0.0,
6695 qmin: 0.0,
6696 vg: 1.0,
6697 mbase: 100.0,
6698 in_service: true,
6699 cost: None,
6700 caps: GenCaps::default(),
6701 voltage_regulation_on: true,
6702 regulating_terminal: None,
6703 regulated_bus: None,
6704 active_power_control: None,
6705 uid: None,
6706 };
6707 g.caps[8] = Some(f64::INFINITY); let mut net = BalancedNetwork::in_memory(
6712 "nf",
6713 100.0,
6714 vec![bus(1, f64::NAN), bus(2, 1.0)],
6715 vec![branch],
6716 );
6717 net.generators_mut().push(g);
6718
6719 let text = serde_json::to_string(&net).unwrap();
6720 assert!(text.contains(r#""vm":"NaN""#), "{text}");
6721 assert!(text.contains(r#""x":"Infinity""#), "{text}");
6722 assert!(text.contains(r#""ramp_30":"Infinity""#), "{text}");
6723
6724 let back: BalancedNetwork = serde_json::from_str(&text).unwrap();
6725 assert!(back.buses()[0].vm.is_nan());
6726 assert_eq!(back.branches()[0].x, f64::INFINITY);
6727 assert_eq!(back.generators()[0].caps[8], Some(f64::INFINITY));
6728
6729 assert_eq!(serde_json::to_string(&back).unwrap(), text);
6731 }
6732
6733 #[test]
6734 fn a_null_at_a_float_position_is_rejected() {
6735 let net = BalancedNetwork::in_memory("nf", 100.0, vec![bus(1), bus(2)], Vec::new());
6736 let text = serde_json::to_string(&net)
6737 .unwrap()
6738 .replacen("\"vm\":1.0", "\"vm\":null", 1);
6739 assert!(text.contains("\"vm\":null"), "fixture edit failed: {text}");
6740 let err = serde_json::from_str::<BalancedNetwork>(&text)
6741 .unwrap_err()
6742 .to_string();
6743 assert!(err.contains("cannot be null"), "{err}");
6744 }
6745
6746 #[test]
6747 fn check_references_rejects_a_dangling_controlled_bus() {
6748 let mut net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
6749 net.branches_mut().push(regulating_branch(9)); let err = net.validate().unwrap_err().to_string();
6751 assert!(
6752 err.contains("transformer control references unknown bus 9"),
6753 "got {err}"
6754 );
6755 }
6756
6757 #[test]
6758 fn check_references_rejects_winding_side_without_a_controlled_bus() {
6759 let mut net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
6760 let mut branch = regulating_branch(2);
6761 let control = branch.control.as_mut().unwrap();
6762 control.controlled_bus = None;
6763 control.controlled_bus_on_winding_side = true;
6764 net.branches_mut().push(branch);
6765
6766 let err = net.validate().unwrap_err().to_string();
6767 assert!(
6768 err.contains("winding side but has no nonzero controlled bus"),
6769 "got {err}"
6770 );
6771 }
6772
6773 fn switched_shunt(reg: usize) -> Shunt {
6775 Shunt {
6776 bus: BusId(1),
6777 g: 0.0,
6778 b: 19.0,
6779 in_service: true,
6780 section_count: None,
6781 control: Some(SwitchedShuntControl {
6782 mode: SwitchedShuntMode::Discrete,
6783 vhigh: 1.05,
6784 vlow: 0.95,
6785 control_bus: Some(BusId(reg)),
6786 regulating_terminal: None,
6787 rmpct: 100.0,
6788 blocks: vec![ShuntBlock::new(2, 25.0), ShuntBlock::new(1, 50.0)],
6789 }),
6790 uid: None,
6791 extras: Extras::new(),
6792 }
6793 }
6794
6795 #[test]
6796 fn switched_shunt_control_survives_serde_round_trip() {
6797 let mut net =
6798 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
6799 net.shunts_mut().push(switched_shunt(3));
6800 net.validate().unwrap();
6801
6802 let back = serde_round_trip(&net);
6803 let c = back.shunts()[0].control.as_ref().unwrap();
6804 assert_eq!(c.mode, SwitchedShuntMode::Discrete);
6805 assert_eq!(c.control_bus, Some(BusId(3)));
6806 assert_eq!(c.blocks.len(), 2);
6807 close(c.blocks[1].b, 50.0);
6808 }
6809
6810 #[test]
6811 fn check_references_rejects_a_dangling_switched_shunt_control_bus() {
6812 let mut net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
6813 net.shunts_mut().push(switched_shunt(9)); let err = net.validate().unwrap_err().to_string();
6815 assert!(
6816 err.contains("switched-shunt control references unknown bus 9"),
6817 "got {err}"
6818 );
6819 }
6820
6821 #[test]
6822 fn validate_values_flags_and_repair_clamps_out_of_domain_values() {
6823 let mut net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
6824 net.buses_mut()[0].vm = 0.0; net.buses_mut()[1].va = 9000.0; net.generators_mut().push(Generator {
6827 bus: BusId(1),
6828 energy_source: GeneratorEnergySource::default(),
6829 pg: 10.0,
6830 qg: 0.0,
6831 pmax: 100.0,
6832 pmin: 0.0,
6833 qmax: 50.0,
6834 qmin: -50.0,
6835 vg: 0.0, mbase: 0.0, in_service: true,
6838 cost: None,
6839 voltage_regulation_on: true,
6840 regulating_terminal: None,
6841 caps: Default::default(),
6842 regulated_bus: None,
6843 active_power_control: None,
6844 uid: None,
6845 });
6846
6847 let diags = net.validate_values();
6848 let fields: std::collections::BTreeSet<_> = diags
6849 .iter()
6850 .map(|d| d.details()["field"].as_str().unwrap().to_owned())
6851 .collect();
6852 assert_eq!(
6853 fields,
6854 ["mbase", "va", "vg", "vm"]
6855 .into_iter()
6856 .map(str::to_owned)
6857 .collect(),
6858 "all four out-of-domain fields reported"
6859 );
6860 assert!(
6861 diags
6862 .iter()
6863 .all(|d| d.code() == "VALIDATE.BALANCED.VALUE_DOMAIN" && d.target().is_some())
6864 );
6865 close(net.buses()[0].vm, 0.0);
6867
6868 let module = powerio_core::PioModule::new(net);
6870 let module = repair_values(module).unwrap();
6871 let net = &module.value();
6872 close(net.buses()[0].vm, 1.0);
6873 close(net.buses()[1].va, 0.0);
6874 close(net.generators()[0].mbase, 100.0); close(net.generators()[0].vg, 1.0);
6876 assert_eq!(net.validate_values().as_slice(), []);
6879 let entries = module.history();
6880 assert_eq!(entries.len(), 1);
6881 assert_eq!(entries[0].kind(), powerio_core::HistoryKind::Repair);
6882 assert_eq!(
6883 entries[0].parameters()["repairs"].as_array().unwrap().len(),
6884 diags.len()
6885 );
6886 assert_eq!(module.diagnostics().len(), diags.len());
6887 let module = repair_values(module).unwrap();
6888 assert_eq!(module.history().len(), 1);
6889 }
6890
6891 #[test]
6892 fn validate_values_is_empty_for_a_clean_network() {
6893 let net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
6894 assert_eq!(net.validate_values().as_slice(), []);
6895 }
6896
6897 #[test]
6898 fn capability_curve_rejects_a_nonfinite_active_power() {
6899 let limits = ReactiveLimits::CapabilityCurve(ReactiveCapabilityCurve {
6900 curve_style: CurveStyle::StraightLineYValues,
6901 properties: BTreeMap::new(),
6902 points: vec![
6903 ReactiveCapabilityCurvePoint {
6904 active_power_mw: 0.0,
6905 minimum_reactive_power_mvar: -10.0,
6906 maximum_reactive_power_mvar: 10.0,
6907 properties: BTreeMap::new(),
6908 },
6909 ReactiveCapabilityCurvePoint {
6910 active_power_mw: 100.0,
6911 minimum_reactive_power_mvar: -5.0,
6912 maximum_reactive_power_mvar: 5.0,
6913 properties: BTreeMap::new(),
6914 },
6915 ],
6916 });
6917
6918 let error = calc_reactive_limits_at_active_power("generator 1", &limits, f64::NAN)
6919 .expect_err("NaN must not be used to index a capability curve");
6920 assert!(error.contains("nonfinite active power"), "{error}");
6921 }
6922 #[test]
6923 fn impedance_correction_factor_interpolates_and_holds_the_end_points() {
6924 let point = ImpedanceCorrectionPoint::new;
6925 let table = ImpedanceCorrection::new(
6926 1,
6927 ImpedanceCorrectionVariable::Angle,
6928 vec![
6929 point(30.0, 1.2, -0.2),
6930 point(-30.0, 1.2, 0.2),
6931 point(0.0, 1.0, 0.0),
6932 ],
6933 );
6934 assert_eq!(table.calc_factor(-45.0), Some((1.2, 0.2)));
6935 assert_eq!(table.calc_factor(45.0), Some((1.2, -0.2)));
6936 let (re, im) = table.calc_factor(15.0).unwrap();
6937 close(re, 1.1);
6938 close(im, -0.1);
6939 let single = ImpedanceCorrection::new(
6940 2,
6941 ImpedanceCorrectionVariable::Ratio,
6942 vec![point(1.0, 0.9, 0.0)],
6943 );
6944 assert_eq!(single.calc_factor(1.1), Some((0.9, 0.0)));
6945 let empty = ImpedanceCorrection::new(3, ImpedanceCorrectionVariable::Ratio, Vec::new());
6946 assert_eq!(empty.calc_factor(1.0), None);
6947 }
6948}