1use std::collections::BTreeMap;
23use std::sync::Arc;
24
25use serde::{Deserialize, Serialize};
26use serde_json::Value;
27
28use crate::Error;
29
30pub type Extras = BTreeMap<String, Value>;
33
34pub const DEFAULT_BASE_FREQUENCY: f64 = 60.0;
38
39fn default_base_frequency() -> f64 {
43 DEFAULT_BASE_FREQUENCY
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
58#[serde(transparent)]
59pub struct BusId(pub usize);
60
61impl BusId {
62 #[must_use]
63 pub const fn new(id: usize) -> Self {
64 Self(id)
65 }
66}
67
68impl std::fmt::Display for BusId {
69 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70 self.0.fmt(f)
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "UPPERCASE")]
77#[repr(u8)]
78#[non_exhaustive]
79pub enum BusType {
80 Pq = 1,
81 Pv = 2,
82 Ref = 3,
83 Isolated = 4,
84}
85
86impl BusType {
87 pub(crate) fn from_f64(v: f64) -> Self {
89 match v as i32 {
90 2 => Self::Pv,
91 3 => Self::Ref,
92 4 => Self::Isolated,
93 _ => Self::Pq,
94 }
95 }
96
97 #[must_use]
100 pub fn as_str(self) -> &'static str {
101 match self {
102 Self::Pq => "PQ",
103 Self::Pv => "PV",
104 Self::Ref => "REF",
105 Self::Isolated => "ISOLATED",
106 }
107 }
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
112#[non_exhaustive]
113pub struct GenCost {
114 pub model: u8,
116 pub startup: f64,
117 pub shutdown: f64,
118 pub ncost: usize,
120 pub coeffs: Vec<f64>,
123}
124
125impl GenCost {
126 #[must_use]
134 pub fn new(model: u8, startup: f64, shutdown: f64, coeffs: Vec<f64>) -> Self {
135 let ncost = if model == 1 {
136 coeffs.len() / 2
137 } else {
138 coeffs.len()
139 };
140 Self {
141 model,
142 startup,
143 shutdown,
144 ncost,
145 coeffs,
146 }
147 }
148
149 #[must_use]
150 pub fn with_ncost(
151 model: u8,
152 startup: f64,
153 shutdown: f64,
154 ncost: usize,
155 coeffs: Vec<f64>,
156 ) -> Self {
157 Self {
158 model,
159 startup,
160 shutdown,
161 ncost,
162 coeffs,
163 }
164 }
165
166 pub fn quadratic(&self) -> Option<(f64, f64)> {
171 if self.model != 2 {
172 return None;
173 }
174 if self.coeffs.len() < self.ncost {
177 return None;
178 }
179 match self.ncost {
180 3 => Some((2.0 * self.coeffs[0], self.coeffs[1])),
181 2 => Some((0.0, self.coeffs[0])),
182 1 => Some((0.0, 0.0)),
183 _ => None,
184 }
185 }
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
191#[non_exhaustive]
192pub enum SourceFormat {
193 Matpower,
194 PowerModelsJson,
195 EgretJson,
196 Psse,
197 PowerWorld,
198 PandapowerJson,
199 Pslf,
204 PowerWorldBinary,
208 InMemory,
210 Normalized,
216 Gridfm,
222 PypsaCsv,
225 Goc3Json,
229 SurgeJson,
231}
232
233impl SourceFormat {
234 #[must_use]
239 pub fn name(self) -> &'static str {
240 match self {
241 SourceFormat::Matpower => "matpower",
242 SourceFormat::PowerModelsJson => "powermodels-json",
243 SourceFormat::EgretJson => "egret-json",
244 SourceFormat::Psse => "psse",
245 SourceFormat::PowerWorld => "powerworld",
246 SourceFormat::PandapowerJson => "pandapower-json",
247 SourceFormat::Pslf => "pslf",
248 SourceFormat::PowerWorldBinary => "powerworld-pwb",
249 SourceFormat::InMemory => "in-memory",
250 SourceFormat::Normalized => "normalized",
251 SourceFormat::Gridfm => "gridfm",
252 SourceFormat::PypsaCsv => "pypsa-csv",
253 SourceFormat::Goc3Json => "goc3-json",
254 SourceFormat::SurgeJson => "surge-json",
255 }
256 }
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
261#[non_exhaustive]
262pub struct Network {
263 pub name: String,
264 pub base_mva: f64,
265 #[serde(default = "default_base_frequency")]
272 pub base_frequency: f64,
273 pub buses: Vec<Bus>,
274 pub loads: Vec<Load>,
275 pub shunts: Vec<Shunt>,
276 pub branches: Vec<Branch>,
277 #[serde(default)]
278 pub switches: Vec<Switch>,
279 pub generators: Vec<Generator>,
280 pub storage: Vec<Storage>,
281 pub hvdc: Vec<Hvdc>,
282 #[serde(default)]
291 pub transformers_3w: Vec<Transformer3W>,
292 #[serde(default)]
297 pub areas: Vec<Area>,
298 #[serde(default)]
301 pub solver: Option<SolverParams>,
302 pub source_format: SourceFormat,
303 #[serde(skip)]
314 pub source: Option<Arc<String>>,
315}
316
317pub type BalancedNetwork = Network;
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
321#[non_exhaustive]
322pub struct Bus {
323 pub id: BusId,
325 pub kind: BusType,
326 pub vm: f64,
328 pub va: f64,
330 pub base_kv: f64,
331 pub vmax: f64,
332 pub vmin: f64,
333 #[serde(default)]
339 pub evhi: Option<f64>,
340 #[serde(default)]
341 pub evlo: Option<f64>,
342 pub area: usize,
343 pub zone: usize,
344 pub name: Option<String>,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
350 pub uid: Option<String>,
351 pub extras: Extras,
352}
353
354impl Bus {
355 #[must_use]
356 pub fn new(id: BusId, kind: BusType, base_kv: f64) -> Self {
357 Self {
358 id,
359 kind,
360 vm: 1.0,
361 va: 0.0,
362 base_kv,
363 vmax: 1.1,
364 vmin: 0.9,
365 evhi: None,
366 evlo: None,
367 area: 1,
368 zone: 1,
369 name: None,
370 uid: None,
371 extras: Extras::new(),
372 }
373 }
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize)]
377#[non_exhaustive]
378pub struct Load {
379 pub bus: BusId,
380 pub p: f64,
382 pub q: f64,
384 #[serde(default)]
386 pub voltage_model: Option<LoadVoltageModel>,
387 pub in_service: bool,
388 #[serde(default, skip_serializing_if = "Option::is_none")]
390 pub uid: Option<String>,
391 pub extras: Extras,
392}
393
394impl Load {
395 #[must_use]
396 pub fn new(bus: BusId, p: f64, q: f64) -> Self {
397 Self {
398 bus,
399 p,
400 q,
401 voltage_model: None,
402 in_service: true,
403 uid: None,
404 extras: Extras::new(),
405 }
406 }
407}
408
409#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
411#[serde(tag = "kind", rename_all = "snake_case")]
412#[non_exhaustive]
413pub enum LoadVoltageModel {
414 ConstantPower,
416 Zip {
419 p_constant_power: f64,
420 q_constant_power: f64,
421 p_constant_current: f64,
422 q_constant_current: f64,
423 p_constant_impedance: f64,
424 q_constant_impedance: f64,
425 #[serde(default)]
426 v_nom: Option<f64>,
427 #[serde(default)]
430 load_type: Option<i32>,
431 #[serde(default)]
433 scaling: Option<f64>,
434 },
435 Exponential {
438 p: f64,
439 q: f64,
440 #[serde(default)]
441 v_nom: Option<f64>,
442 gamma_p: f64,
443 gamma_q: f64,
444 },
445}
446
447impl LoadVoltageModel {
448 #[must_use]
449 pub fn has_non_matpower_fields(&self) -> bool {
450 match self {
451 Self::ConstantPower => false,
452 Self::Zip {
453 p_constant_current,
454 q_constant_current,
455 p_constant_impedance,
456 q_constant_impedance,
457 v_nom,
458 load_type,
459 scaling,
460 ..
461 } => {
462 *p_constant_current != 0.0
463 || *q_constant_current != 0.0
464 || *p_constant_impedance != 0.0
465 || *q_constant_impedance != 0.0
466 || v_nom.is_some()
467 || load_type.is_some()
468 || scaling.is_some()
469 }
470 Self::Exponential { .. } => true,
471 }
472 }
473}
474
475#[derive(Debug, Clone, Serialize, Deserialize)]
476#[non_exhaustive]
477pub struct Shunt {
478 pub bus: BusId,
479 pub g: f64,
481 pub b: f64,
484 pub in_service: bool,
485 #[serde(default)]
489 pub control: Option<SwitchedShuntControl>,
490 #[serde(default, skip_serializing_if = "Option::is_none")]
492 pub uid: Option<String>,
493 pub extras: Extras,
494}
495
496impl Shunt {
497 #[must_use]
498 pub fn new(bus: BusId, g: f64, b: f64) -> Self {
499 Self {
500 bus,
501 g,
502 b,
503 in_service: true,
504 control: None,
505 uid: None,
506 extras: Extras::new(),
507 }
508 }
509}
510
511#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
513#[serde(rename_all = "snake_case")]
514#[non_exhaustive]
515pub enum SwitchedShuntMode {
516 Locked,
518 Continuous,
520 Discrete,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
526#[non_exhaustive]
527pub struct ShuntBlock {
528 pub steps: u32,
529 pub b: f64,
531}
532
533impl ShuntBlock {
534 #[must_use]
535 pub const fn new(steps: u32, b: f64) -> Self {
536 Self { steps, b }
537 }
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize)]
545#[non_exhaustive]
546pub struct SwitchedShuntControl {
547 pub mode: SwitchedShuntMode,
548 pub vhigh: f64,
550 pub vlow: f64,
551 pub control_bus: Option<BusId>,
553 pub rmpct: f64,
555 pub blocks: Vec<ShuntBlock>,
556}
557
558impl SwitchedShuntControl {
559 #[must_use]
560 pub fn new(mode: SwitchedShuntMode, vhigh: f64, vlow: f64, blocks: Vec<ShuntBlock>) -> Self {
561 Self {
562 mode,
563 vhigh,
564 vlow,
565 control_bus: None,
566 rmpct: 100.0,
567 blocks,
568 }
569 }
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize)]
573#[non_exhaustive]
574pub struct Branch {
575 pub from: BusId,
576 pub to: BusId,
577 pub r: f64,
579 pub x: f64,
581 pub b: f64,
585 #[serde(default)]
588 pub charging: Option<BranchCharging>,
589 pub rate_a: f64,
590 pub rate_b: f64,
591 pub rate_c: f64,
592 #[serde(default)]
595 pub rating_sets: Vec<BranchRatingSet>,
596 #[serde(default)]
598 pub current_ratings: Option<BranchCurrentRatings>,
599 pub tap: f64,
601 pub shift: f64,
603 pub in_service: bool,
604 pub angmin: f64,
605 pub angmax: f64,
606 #[serde(default)]
611 pub control: Option<TransformerControl>,
612 #[serde(default)]
614 pub solution: Option<BranchSolution>,
615 #[serde(default, skip_serializing_if = "Option::is_none")]
617 pub uid: Option<String>,
618 pub extras: Extras,
619}
620
621#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
623#[non_exhaustive]
624pub struct BranchRatingSet {
625 pub name: String,
626 pub rate_mva: f64,
627}
628
629impl BranchRatingSet {
630 #[must_use]
631 pub fn new(name: impl Into<String>, rate_mva: f64) -> Self {
632 Self {
633 name: name.into(),
634 rate_mva,
635 }
636 }
637}
638
639#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
642#[non_exhaustive]
643pub struct BranchCharging {
644 pub g_fr: f64,
645 pub b_fr: f64,
646 pub g_to: f64,
647 pub b_to: f64,
648}
649
650impl BranchCharging {
651 #[must_use]
652 pub const fn new(g_fr: f64, b_fr: f64, g_to: f64, b_to: f64) -> Self {
653 Self {
654 g_fr,
655 b_fr,
656 g_to,
657 b_to,
658 }
659 }
660
661 #[must_use]
662 pub fn from_total_b(b: f64) -> Self {
663 Self {
664 g_fr: 0.0,
665 b_fr: b / 2.0,
666 g_to: 0.0,
667 b_to: b / 2.0,
668 }
669 }
670
671 #[must_use]
672 pub fn total_b(self) -> f64 {
673 self.b_fr + self.b_to
674 }
675
676 #[must_use]
677 pub fn total_g(self) -> f64 {
678 self.g_fr + self.g_to
679 }
680
681 #[must_use]
682 pub fn is_matpower_symmetric(self) -> bool {
683 self.g_fr.abs() <= f64::EPSILON
684 && self.g_to.abs() <= f64::EPSILON
685 && (self.b_fr - self.b_to).abs() <= f64::EPSILON
686 }
687}
688
689#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
691#[non_exhaustive]
692pub struct BranchCurrentRatings {
693 pub c_rating_a: f64,
694 pub c_rating_b: f64,
695 pub c_rating_c: f64,
696}
697
698impl BranchCurrentRatings {
699 #[must_use]
700 pub const fn new(c_rating_a: f64, c_rating_b: f64, c_rating_c: f64) -> Self {
701 Self {
702 c_rating_a,
703 c_rating_b,
704 c_rating_c,
705 }
706 }
707}
708
709#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
711#[non_exhaustive]
712pub struct BranchSolution {
713 pub pf: f64,
714 pub qf: f64,
715 pub pt: f64,
716 pub qt: f64,
717}
718
719impl BranchSolution {
720 #[must_use]
721 pub const fn new(pf: f64, qf: f64, pt: f64, qt: f64) -> Self {
722 Self { pf, qf, pt, qt }
723 }
724}
725
726impl Branch {
727 #[must_use]
728 pub fn new(from: BusId, to: BusId, r: f64, x: f64) -> Self {
729 Self {
730 from,
731 to,
732 r,
733 x,
734 b: 0.0,
735 charging: None,
736 rate_a: 0.0,
737 rate_b: 0.0,
738 rate_c: 0.0,
739 rating_sets: Vec::new(),
740 current_ratings: None,
741 tap: 0.0,
742 shift: 0.0,
743 in_service: true,
744 angmin: -360.0,
745 angmax: 360.0,
746 control: None,
747 solution: None,
748 uid: None,
749 extras: Extras::new(),
750 }
751 }
752
753 #[must_use]
755 pub fn effective_tap(&self) -> f64 {
756 if self.tap == 0.0 { 1.0 } else { self.tap }
757 }
758
759 #[must_use]
762 pub fn terminal_charging(&self) -> BranchCharging {
763 self.charging
764 .unwrap_or_else(|| BranchCharging::from_total_b(self.b))
765 }
766
767 #[must_use]
770 pub fn legacy_total_charging_b(&self) -> f64 {
771 self.terminal_charging().total_b()
772 }
773
774 #[must_use]
776 pub fn has_non_matpower_charging(&self) -> bool {
777 self.charging
778 .is_some_and(|charging| !charging.is_matpower_symmetric())
779 }
780
781 #[must_use]
784 pub fn is_transformer(&self) -> bool {
785 self.tap != 0.0 || self.shift != 0.0
786 }
787
788 #[must_use]
792 pub fn has_angle_limits(&self) -> bool {
793 self.angmin > -360.0 || self.angmax < 360.0
794 }
795}
796
797#[derive(Debug, Clone, Serialize, Deserialize)]
800#[non_exhaustive]
801pub struct Switch {
802 pub from: BusId,
803 pub to: BusId,
804 pub closed: bool,
805 #[serde(default)]
806 pub thermal_rating: Option<f64>,
807 #[serde(default)]
808 pub current_rating: Option<f64>,
809 #[serde(default)]
810 pub pf: Option<f64>,
811 #[serde(default)]
812 pub qf: Option<f64>,
813 #[serde(default)]
814 pub pt: Option<f64>,
815 #[serde(default)]
816 pub qt: Option<f64>,
817 #[serde(default, skip_serializing_if = "Option::is_none")]
819 pub uid: Option<String>,
820 pub extras: Extras,
821}
822
823impl Switch {
824 #[must_use]
825 pub fn new(from: BusId, to: BusId, closed: bool) -> Self {
826 Self {
827 from,
828 to,
829 closed,
830 thermal_rating: None,
831 current_rating: None,
832 pf: None,
833 qf: None,
834 pt: None,
835 qt: None,
836 uid: None,
837 extras: Extras::new(),
838 }
839 }
840}
841
842#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
845#[serde(rename_all = "snake_case")]
846#[non_exhaustive]
847pub enum TransformerControlMode {
848 Fixed,
850 Voltage,
852 ReactiveFlow,
854 ActiveFlow,
856}
857
858#[derive(Debug, Clone, Serialize, Deserialize)]
868#[non_exhaustive]
869pub struct TransformerControl {
870 pub mode: TransformerControlMode,
871 pub controlled_bus: Option<BusId>,
872 pub tap_min: f64,
873 pub tap_max: f64,
874 pub band_min: f64,
875 pub band_max: f64,
876 pub ntp: u32,
877 pub mva_base: f64,
878}
879
880impl Default for TransformerControl {
881 fn default() -> Self {
882 TransformerControl {
884 mode: TransformerControlMode::Fixed,
885 controlled_bus: None,
886 tap_min: 0.9,
887 tap_max: 1.1,
888 band_min: 0.9,
889 band_max: 1.1,
890 ntp: 33,
891 mva_base: 0.0,
892 }
893 }
894}
895
896impl TransformerControl {
897 #[must_use]
898 pub fn new(mode: TransformerControlMode) -> Self {
899 Self {
900 mode,
901 ..Self::default()
902 }
903 }
904}
905
906#[derive(Debug, Clone, Serialize, Deserialize)]
907#[non_exhaustive]
908pub struct Generator {
909 pub bus: BusId,
910 pub pg: f64,
912 pub qg: f64,
914 pub pmax: f64,
915 pub pmin: f64,
916 pub qmax: f64,
917 pub qmin: f64,
918 pub vg: f64,
920 pub mbase: f64,
921 pub in_service: bool,
922 pub cost: Option<GenCost>,
923 #[serde(default = "default_caps", with = "caps_serde")]
932 pub caps: GenCaps,
933 #[serde(default)]
940 pub regulated_bus: Option<BusId>,
941 #[serde(default, skip_serializing_if = "Option::is_none")]
943 pub uid: Option<String>,
944}
945
946impl Generator {
947 #[must_use]
948 pub fn new(bus: BusId) -> Self {
949 Self {
950 bus,
951 pg: 0.0,
952 qg: 0.0,
953 pmax: 0.0,
954 pmin: 0.0,
955 qmax: 0.0,
956 qmin: 0.0,
957 vg: 1.0,
958 mbase: 0.0,
959 in_service: true,
960 cost: None,
961 caps: default_caps(),
962 regulated_bus: None,
963 uid: None,
964 }
965 }
966
967 #[must_use]
970 pub fn has_caps(&self) -> bool {
971 self.caps.iter().any(Option::is_some)
972 }
973}
974
975pub type GenCaps = [Option<f64>; GEN_EXTRA_KEYS.len()];
977
978fn default_caps() -> GenCaps {
980 [None; GEN_EXTRA_KEYS.len()]
981}
982
983mod caps_serde {
994 use super::{GEN_EXTRA_KEYS, GenCaps};
995 use serde::de::{Deserialize, Deserializer};
996 use serde::ser::{SerializeMap, Serializer};
997 use std::collections::BTreeMap;
998
999 pub(super) fn serialize<S: Serializer>(caps: &GenCaps, s: S) -> Result<S::Ok, S::Error> {
1000 let present = caps.iter().filter(|v| v.is_some()).count();
1001 let mut map = s.serialize_map(Some(present))?;
1002 for (key, slot) in GEN_EXTRA_KEYS.iter().zip(caps.iter()) {
1003 if let Some(value) = slot {
1004 map.serialize_entry(key, value)?;
1005 }
1006 }
1007 map.end()
1008 }
1009
1010 pub(super) fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<GenCaps, D::Error> {
1011 let named = Option::<BTreeMap<String, f64>>::deserialize(d)?.unwrap_or_default();
1016 let mut caps: GenCaps = [None; GEN_EXTRA_KEYS.len()];
1017 for (slot, key) in caps.iter_mut().zip(GEN_EXTRA_KEYS.iter()) {
1018 *slot = named.get(*key).copied();
1019 }
1020 Ok(caps)
1021 }
1022}
1023
1024#[derive(Debug, Clone, Serialize, Deserialize)]
1025#[non_exhaustive]
1026pub struct Storage {
1027 pub bus: BusId,
1028 pub ps: f64,
1029 pub qs: f64,
1030 pub energy: f64,
1031 pub energy_rating: f64,
1032 pub charge_rating: f64,
1033 pub discharge_rating: f64,
1034 pub charge_efficiency: f64,
1035 pub discharge_efficiency: f64,
1036 pub thermal_rating: f64,
1037 #[serde(default)]
1038 pub current_rating: Option<f64>,
1039 pub qmin: f64,
1040 pub qmax: f64,
1041 pub r: f64,
1042 pub x: f64,
1043 pub p_loss: f64,
1044 pub q_loss: f64,
1045 pub in_service: bool,
1046 #[serde(default, skip_serializing_if = "Option::is_none")]
1048 pub uid: Option<String>,
1049 pub extras: Extras,
1050}
1051
1052impl Storage {
1053 #[must_use]
1054 pub fn new(bus: BusId) -> Self {
1055 Self {
1056 bus,
1057 ps: 0.0,
1058 qs: 0.0,
1059 energy: 0.0,
1060 energy_rating: 0.0,
1061 charge_rating: 0.0,
1062 discharge_rating: 0.0,
1063 charge_efficiency: 1.0,
1064 discharge_efficiency: 1.0,
1065 thermal_rating: 0.0,
1066 current_rating: None,
1067 qmin: 0.0,
1068 qmax: 0.0,
1069 r: 0.0,
1070 x: 0.0,
1071 p_loss: 0.0,
1072 q_loss: 0.0,
1073 in_service: true,
1074 uid: None,
1075 extras: Extras::new(),
1076 }
1077 }
1078}
1079
1080#[derive(Debug, Clone, Serialize, Deserialize)]
1088#[non_exhaustive]
1089pub struct Hvdc {
1090 pub from: BusId,
1091 pub to: BusId,
1092 pub in_service: bool,
1093 pub pf: f64,
1094 pub pt: f64,
1095 pub qf: f64,
1096 pub qt: f64,
1097 pub vf: f64,
1098 pub vt: f64,
1099 pub pmin: f64,
1100 pub pmax: f64,
1101 pub qminf: f64,
1102 pub qmaxf: f64,
1103 pub qmint: f64,
1104 pub qmaxt: f64,
1105 pub loss0: f64,
1106 pub loss1: f64,
1107 #[serde(default)]
1108 pub cost: Option<GenCost>,
1109 #[serde(default, skip_serializing_if = "Option::is_none")]
1111 pub uid: Option<String>,
1112 pub extras: Extras,
1113}
1114
1115impl Hvdc {
1116 #[must_use]
1117 pub fn new(from: BusId, to: BusId) -> Self {
1118 Self {
1119 from,
1120 to,
1121 in_service: true,
1122 pf: 0.0,
1123 pt: 0.0,
1124 qf: 0.0,
1125 qt: 0.0,
1126 vf: 1.0,
1127 vt: 1.0,
1128 pmin: 0.0,
1129 pmax: 0.0,
1130 qminf: 0.0,
1131 qmaxf: 0.0,
1132 qmint: 0.0,
1133 qmaxt: 0.0,
1134 loss0: 0.0,
1135 loss1: 0.0,
1136 cost: None,
1137 uid: None,
1138 extras: Extras::new(),
1139 }
1140 }
1141}
1142
1143#[derive(Debug, Clone, Serialize, Deserialize)]
1150#[non_exhaustive]
1151pub struct Area {
1152 pub number: usize,
1153 pub slack_bus: Option<BusId>,
1155 pub net_interchange: f64,
1157 pub tolerance: f64,
1159 pub name: Option<String>,
1160}
1161
1162impl Area {
1163 #[must_use]
1164 pub fn new(number: usize) -> Self {
1165 Self {
1166 number,
1167 slack_bus: None,
1168 net_interchange: 0.0,
1169 tolerance: 0.0,
1170 name: None,
1171 }
1172 }
1173}
1174
1175#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1184#[non_exhaustive]
1185pub struct SolverParams {
1186 pub newton_tolerance: Option<f64>,
1188 pub max_iterations: Option<u32>,
1190 pub zero_impedance_threshold: Option<f64>,
1192 pub adjust_taps: Option<bool>,
1194 pub adjust_area_interchange: Option<bool>,
1196 pub adjust_phase_shift: Option<bool>,
1198 pub adjust_dc_taps: Option<bool>,
1200 pub adjust_switched_shunt: Option<bool>,
1202}
1203
1204impl SolverParams {
1205 #[must_use]
1206 pub fn new() -> Self {
1207 Self::default()
1208 }
1209
1210 #[must_use]
1212 pub fn is_empty(&self) -> bool {
1213 *self == SolverParams::default()
1214 }
1215}
1216
1217#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
1229#[non_exhaustive]
1230pub struct Impedance {
1231 pub r: f64,
1232 pub x: f64,
1233 pub base_mva: f64,
1234}
1235
1236impl Impedance {
1237 #[must_use]
1238 pub const fn new(r: f64, x: f64, base_mva: f64) -> Self {
1239 Self { r, x, base_mva }
1240 }
1241}
1242
1243#[derive(Debug, Clone, Serialize, Deserialize)]
1246#[non_exhaustive]
1247pub struct Winding {
1248 pub bus: BusId,
1249 pub tap: f64,
1251 pub shift: f64,
1253 pub nominal_kv: f64,
1255 pub rate_a: f64,
1256 pub rate_b: f64,
1257 pub rate_c: f64,
1258}
1259
1260impl Winding {
1261 #[must_use]
1262 pub fn new(bus: BusId) -> Self {
1263 Self {
1264 bus,
1265 tap: 1.0,
1266 shift: 0.0,
1267 nominal_kv: 0.0,
1268 rate_a: 0.0,
1269 rate_b: 0.0,
1270 rate_c: 0.0,
1271 }
1272 }
1273}
1274
1275#[derive(Debug, Clone, Serialize, Deserialize)]
1286#[non_exhaustive]
1287pub struct Transformer3W {
1288 pub windings: [Winding; 3],
1290 pub z: [Impedance; 3],
1294 pub star_vm: f64,
1296 pub star_va: f64,
1297 pub mag_g: f64,
1299 pub mag_b: f64,
1300 pub in_service: bool,
1301 pub name: Option<String>,
1302 #[serde(default, skip_serializing_if = "Option::is_none")]
1304 pub uid: Option<String>,
1305 pub extras: Extras,
1306}
1307
1308impl Transformer3W {
1309 #[must_use]
1310 pub fn new(windings: [Winding; 3], z: [Impedance; 3]) -> Self {
1311 Self {
1312 windings,
1313 z,
1314 star_vm: 1.0,
1315 star_va: 0.0,
1316 mag_g: 0.0,
1317 mag_b: 0.0,
1318 in_service: true,
1319 name: None,
1320 uid: None,
1321 extras: Extras::new(),
1322 }
1323 }
1324
1325 #[must_use]
1332 pub fn star_impedances(&self) -> [(f64, f64); 3] {
1333 let [z12, z23, z31] = self.z;
1334 let half = |a: f64, b: f64, c: f64| (a + b - c) / 2.0;
1335 [
1336 (half(z12.r, z31.r, z23.r), half(z12.x, z31.x, z23.x)),
1337 (half(z12.r, z23.r, z31.r), half(z12.x, z23.x, z31.x)),
1338 (half(z23.r, z31.r, z12.r), half(z23.x, z31.x, z12.x)),
1339 ]
1340 }
1341
1342 #[must_use]
1349 pub fn star_expansion(&self, star_id: BusId) -> (Bus, [Branch; 3]) {
1350 let star = Bus {
1351 id: star_id,
1352 kind: BusType::Pq,
1353 vm: self.star_vm,
1354 va: self.star_va,
1355 base_kv: self.windings[0].nominal_kv,
1356 vmax: 1.1,
1357 vmin: 0.9,
1358 evhi: None,
1359 evlo: None,
1360 area: 0,
1361 zone: 0,
1362 name: self.name.clone(),
1363 uid: self.uid.clone(),
1364 extras: Extras::new(),
1365 };
1366 let zs = self.star_impedances();
1367 let branch = |w: &Winding, (r, x): (f64, f64)| Branch {
1368 from: w.bus,
1369 to: star_id,
1370 r,
1371 x,
1372 b: 0.0,
1373 charging: None,
1374 rate_a: w.rate_a,
1375 rate_b: w.rate_b,
1376 rate_c: w.rate_c,
1377 rating_sets: Vec::new(),
1378 current_ratings: None,
1379 tap: w.tap,
1380 shift: w.shift,
1381 in_service: self.in_service,
1382 angmin: -360.0,
1383 angmax: 360.0,
1384 control: None,
1385 solution: None,
1386 uid: None,
1387 extras: Extras::new(),
1388 };
1389 let branches = [
1390 branch(&self.windings[0], zs[0]),
1391 branch(&self.windings[1], zs[1]),
1392 branch(&self.windings[2], zs[2]),
1393 ];
1394 (star, branches)
1395 }
1396}
1397
1398pub(crate) const GEN_EXTRA_KEYS: [&str; 11] = [
1401 "pc1", "pc2", "qc1min", "qc1max", "qc2min", "qc2max", "ramp_agc", "ramp_10", "ramp_30",
1402 "ramp_q", "apf",
1403];
1404
1405#[derive(Debug, Clone, PartialEq)]
1412#[non_exhaustive]
1413pub struct Diagnostic {
1414 pub element: String,
1416 pub field: &'static str,
1417 pub old: f64,
1418 pub new: f64,
1419 pub reason: &'static str,
1420}
1421
1422fn repair_vm(vm: f64) -> Option<f64> {
1426 (!vm.is_finite() || vm <= 0.0 || vm > 2.0).then_some(1.0)
1427}
1428
1429fn repair_va(va: f64) -> Option<f64> {
1431 (!va.is_finite() || va.abs() > 2000.0).then_some(0.0)
1432}
1433
1434fn repair_mbase(mbase: f64, sbase: f64) -> Option<f64> {
1436 (!mbase.is_finite() || mbase <= 0.0).then_some(sbase)
1437}
1438
1439fn repair_vg(vg: f64) -> Option<f64> {
1441 (!vg.is_finite() || vg <= 0.0).then_some(1.0)
1442}
1443
1444impl Network {
1445 #[must_use]
1446 pub fn new(name: impl Into<String>, base_mva: f64) -> Network {
1447 Network {
1448 name: name.into(),
1449 base_mva,
1450 base_frequency: DEFAULT_BASE_FREQUENCY,
1451 buses: Vec::new(),
1452 loads: Vec::new(),
1453 shunts: Vec::new(),
1454 branches: Vec::new(),
1455 switches: Vec::new(),
1456 generators: Vec::new(),
1457 storage: Vec::new(),
1458 hvdc: Vec::new(),
1459 transformers_3w: Vec::new(),
1460 areas: Vec::new(),
1461 solver: None,
1462 source_format: SourceFormat::InMemory,
1463 source: None,
1464 }
1465 }
1466
1467 #[must_use]
1473 pub fn in_memory(
1474 name: impl Into<String>,
1475 base_mva: f64,
1476 buses: Vec<Bus>,
1477 branches: Vec<Branch>,
1478 ) -> Network {
1479 let mut net = Self::new(name, base_mva);
1480 net.buses = buses;
1481 net.branches = branches;
1482 net
1483 }
1484
1485 pub fn to_json(&self) -> crate::Result<String> {
1502 serde_json::to_string(self).map_err(|e| Error::FormatRead {
1503 format: "JSON",
1504 message: e.to_string(),
1505 })
1506 }
1507
1508 #[allow(clippy::too_many_lines)]
1520 pub(crate) fn non_finite_fields(&self) -> Vec<String> {
1521 fn bad<'a>(
1522 fields: impl IntoIterator<Item = (&'a str, f64)>,
1523 ) -> impl Iterator<Item = &'a str> {
1524 fields
1525 .into_iter()
1526 .filter_map(|(name, v)| (!v.is_finite()).then_some(name))
1527 }
1528 let mut out = Vec::new();
1529 if !self.base_mva.is_finite() {
1530 out.push("base_mva".into());
1531 }
1532 if !self.base_frequency.is_finite() {
1533 out.push("base_frequency".into());
1534 }
1535 for (i, b) in self.buses.iter().enumerate() {
1536 #[rustfmt::skip]
1537 let Bus { id: _, kind: _, vm, va, base_kv, vmax, vmin, evhi: _, evlo: _, area: _, zone: _, name: _, uid: _, extras: _ } = b;
1538 let fields = [
1539 ("vm", *vm),
1540 ("va", *va),
1541 ("base_kv", *base_kv),
1542 ("vmax", *vmax),
1543 ("vmin", *vmin),
1544 ];
1545 out.extend(bad(fields).map(|f| format!("buses[{i}].{f}")));
1546 }
1547 for (i, l) in self.loads.iter().enumerate() {
1548 let Load {
1549 bus: _,
1550 p,
1551 q,
1552 voltage_model,
1553 in_service: _,
1554 uid: _,
1555 extras: _,
1556 } = l;
1557 out.extend(bad([("p", *p), ("q", *q)]).map(|f| format!("loads[{i}].{f}")));
1558 if let Some(model) = voltage_model {
1559 match model {
1560 LoadVoltageModel::ConstantPower => {}
1561 LoadVoltageModel::Zip {
1562 p_constant_power,
1563 q_constant_power,
1564 p_constant_current,
1565 q_constant_current,
1566 p_constant_impedance,
1567 q_constant_impedance,
1568 v_nom,
1569 load_type: _,
1570 scaling,
1571 } => {
1572 let fields = [
1573 ("p_constant_power", *p_constant_power),
1574 ("q_constant_power", *q_constant_power),
1575 ("p_constant_current", *p_constant_current),
1576 ("q_constant_current", *q_constant_current),
1577 ("p_constant_impedance", *p_constant_impedance),
1578 ("q_constant_impedance", *q_constant_impedance),
1579 ];
1580 out.extend(bad(fields).map(|f| format!("loads[{i}].voltage_model.{f}")));
1581 if matches!(v_nom, Some(v) if !v.is_finite()) {
1582 out.push(format!("loads[{i}].voltage_model.v_nom"));
1583 }
1584 if matches!(scaling, Some(v) if !v.is_finite()) {
1585 out.push(format!("loads[{i}].voltage_model.scaling"));
1586 }
1587 }
1588 LoadVoltageModel::Exponential {
1589 p,
1590 q,
1591 v_nom,
1592 gamma_p,
1593 gamma_q,
1594 } => {
1595 out.extend(
1596 bad([
1597 ("p", *p),
1598 ("q", *q),
1599 ("gamma_p", *gamma_p),
1600 ("gamma_q", *gamma_q),
1601 ])
1602 .map(|f| format!("loads[{i}].voltage_model.{f}")),
1603 );
1604 if matches!(v_nom, Some(v) if !v.is_finite()) {
1605 out.push(format!("loads[{i}].voltage_model.v_nom"));
1606 }
1607 }
1608 }
1609 }
1610 }
1611 for (i, s) in self.shunts.iter().enumerate() {
1612 let Shunt {
1613 bus: _,
1614 g,
1615 b,
1616 in_service: _,
1617 control: _,
1618 uid: _,
1619 extras: _,
1620 } = s;
1621 out.extend(bad([("g", *g), ("b", *b)]).map(|f| format!("shunts[{i}].{f}")));
1622 }
1623 for (i, br) in self.branches.iter().enumerate() {
1624 #[rustfmt::skip]
1625 let Branch { from: _, to: _, r, x, b, charging, rate_a, rate_b, rate_c, rating_sets, current_ratings, tap, shift, in_service: _, angmin, angmax, control: _, solution, uid: _, extras: _ } = br;
1626 let fields = [
1627 ("r", *r),
1628 ("x", *x),
1629 ("b", *b),
1630 ("rate_a", *rate_a),
1631 ("rate_b", *rate_b),
1632 ("rate_c", *rate_c),
1633 ("tap", *tap),
1634 ("shift", *shift),
1635 ("angmin", *angmin),
1636 ("angmax", *angmax),
1637 ];
1638 out.extend(bad(fields).map(|f| format!("branches[{i}].{f}")));
1639 out.extend(
1640 rating_sets
1641 .iter()
1642 .enumerate()
1643 .filter(|(_, r)| !r.rate_mva.is_finite())
1644 .map(|(j, _)| format!("branches[{i}].rating_sets[{j}].rate_mva")),
1645 );
1646 if let Some(charging) = charging {
1647 let BranchCharging {
1648 g_fr,
1649 b_fr,
1650 g_to,
1651 b_to,
1652 } = charging;
1653 let fields = [
1654 ("g_fr", *g_fr),
1655 ("b_fr", *b_fr),
1656 ("g_to", *g_to),
1657 ("b_to", *b_to),
1658 ];
1659 out.extend(bad(fields).map(|f| format!("branches[{i}].charging.{f}")));
1660 }
1661 if let Some(current) = current_ratings {
1662 let BranchCurrentRatings {
1663 c_rating_a,
1664 c_rating_b,
1665 c_rating_c,
1666 } = current;
1667 let fields = [
1668 ("c_rating_a", *c_rating_a),
1669 ("c_rating_b", *c_rating_b),
1670 ("c_rating_c", *c_rating_c),
1671 ];
1672 out.extend(bad(fields).map(|f| format!("branches[{i}].current_ratings.{f}")));
1673 }
1674 if let Some(solution) = solution {
1675 let BranchSolution { pf, qf, pt, qt } = solution;
1676 out.extend(
1677 bad([("pf", *pf), ("qf", *qf), ("pt", *pt), ("qt", *qt)])
1678 .map(|f| format!("branches[{i}].solution.{f}")),
1679 );
1680 }
1681 }
1682 for (i, sw) in self.switches.iter().enumerate() {
1683 let Switch {
1684 from: _,
1685 to: _,
1686 closed: _,
1687 thermal_rating,
1688 current_rating,
1689 pf,
1690 qf,
1691 pt,
1692 qt,
1693 uid: _,
1694 extras: _,
1695 } = sw;
1696 for (field, value) in [
1697 ("thermal_rating", *thermal_rating),
1698 ("current_rating", *current_rating),
1699 ("pf", *pf),
1700 ("qf", *qf),
1701 ("pt", *pt),
1702 ("qt", *qt),
1703 ] {
1704 if matches!(value, Some(v) if !v.is_finite()) {
1705 out.push(format!("switches[{i}].{field}"));
1706 }
1707 }
1708 }
1709 for (i, g) in self.generators.iter().enumerate() {
1710 #[rustfmt::skip]
1711 let Generator { bus: _, pg, qg, pmax, pmin, qmax, qmin, vg, mbase, in_service: _, cost, caps, regulated_bus: _, uid: _ } = g;
1712 let fields = [
1713 ("pg", *pg),
1714 ("qg", *qg),
1715 ("pmax", *pmax),
1716 ("pmin", *pmin),
1717 ("qmax", *qmax),
1718 ("qmin", *qmin),
1719 ("vg", *vg),
1720 ("mbase", *mbase),
1721 ];
1722 out.extend(bad(fields).map(|f| format!("generators[{i}].{f}")));
1723 if let Some(GenCost {
1724 model: _,
1725 startup,
1726 shutdown,
1727 ncost: _,
1728 coeffs,
1729 }) = cost
1730 {
1731 out.extend(
1732 bad([("startup", *startup), ("shutdown", *shutdown)])
1733 .map(|f| format!("generators[{i}].cost.{f}")),
1734 );
1735 if coeffs.iter().any(|c| !c.is_finite()) {
1736 out.push(format!("generators[{i}].cost.coeffs"));
1737 }
1738 }
1739 for (key, slot) in GEN_EXTRA_KEYS.iter().zip(caps.iter()) {
1743 if matches!(slot, Some(v) if !v.is_finite()) {
1744 out.push(format!("generators[{i}].caps.{key}"));
1745 }
1746 }
1747 }
1748 for (i, s) in self.storage.iter().enumerate() {
1749 #[rustfmt::skip]
1750 let Storage { bus: _, ps, qs, energy, energy_rating, charge_rating, discharge_rating, charge_efficiency, discharge_efficiency, thermal_rating, current_rating, qmin, qmax, r, x, p_loss, q_loss, in_service: _, uid: _, extras: _ } = s;
1751 let fields = [
1752 ("ps", *ps),
1753 ("qs", *qs),
1754 ("energy", *energy),
1755 ("energy_rating", *energy_rating),
1756 ("charge_rating", *charge_rating),
1757 ("discharge_rating", *discharge_rating),
1758 ("charge_efficiency", *charge_efficiency),
1759 ("discharge_efficiency", *discharge_efficiency),
1760 ("thermal_rating", *thermal_rating),
1761 ("qmin", *qmin),
1762 ("qmax", *qmax),
1763 ("r", *r),
1764 ("x", *x),
1765 ("p_loss", *p_loss),
1766 ("q_loss", *q_loss),
1767 ];
1768 out.extend(bad(fields).map(|f| format!("storage[{i}].{f}")));
1769 if matches!(current_rating, Some(v) if !v.is_finite()) {
1770 out.push(format!("storage[{i}].current_rating"));
1771 }
1772 }
1773 for (i, h) in self.hvdc.iter().enumerate() {
1774 #[rustfmt::skip]
1775 let Hvdc { from: _, to: _, in_service: _, pf, pt, qf, qt, vf, vt, pmin, pmax, qminf, qmaxf, qmint, qmaxt, loss0, loss1, cost, uid: _, extras: _ } = h;
1776 let fields = [
1777 ("pf", *pf),
1778 ("pt", *pt),
1779 ("qf", *qf),
1780 ("qt", *qt),
1781 ("vf", *vf),
1782 ("vt", *vt),
1783 ("pmin", *pmin),
1784 ("pmax", *pmax),
1785 ("qminf", *qminf),
1786 ("qmaxf", *qmaxf),
1787 ("qmint", *qmint),
1788 ("qmaxt", *qmaxt),
1789 ("loss0", *loss0),
1790 ("loss1", *loss1),
1791 ];
1792 out.extend(bad(fields).map(|f| format!("hvdc[{i}].{f}")));
1793 if let Some(GenCost {
1794 model: _,
1795 startup,
1796 shutdown,
1797 ncost: _,
1798 coeffs,
1799 }) = cost
1800 {
1801 out.extend(
1802 bad([("startup", *startup), ("shutdown", *shutdown)])
1803 .map(|f| format!("hvdc[{i}].cost.{f}")),
1804 );
1805 if coeffs.iter().any(|c| !c.is_finite()) {
1806 out.push(format!("hvdc[{i}].cost.coeffs"));
1807 }
1808 }
1809 }
1810 out
1811 }
1812
1813 pub fn to_format(&self, format: crate::TargetFormat) -> crate::Result<crate::Conversion> {
1820 crate::write_as(self, format)
1821 }
1822
1823 pub fn to_format_with_options(
1828 &self,
1829 format: crate::TargetFormat,
1830 options: &crate::WriteOptions,
1831 ) -> crate::Result<crate::Conversion> {
1832 crate::write_as_with_options(self, format, options)
1833 }
1834
1835 #[must_use]
1840 pub fn to_matpower(&self) -> String {
1841 crate::write_matpower(self)
1842 }
1843
1844 pub fn from_json(text: &str) -> crate::Result<Network> {
1851 let net: Network = serde_json::from_str(text).map_err(|e| Error::FormatRead {
1852 format: "JSON",
1853 message: e.to_string(),
1854 })?;
1855 net.check_references("JSON")?;
1856 if net.buses.is_empty() {
1857 return Err(Error::FormatRead {
1858 format: "JSON",
1859 message: "case has no buses".into(),
1860 });
1861 }
1862 Ok(net)
1863 }
1864
1865 #[must_use]
1870 pub fn is_normalized(&self) -> bool {
1871 self.source_format == SourceFormat::Normalized
1872 }
1873
1874 pub fn check_base_mva(&self) -> crate::Result<()> {
1880 if self.base_mva.is_finite() && self.base_mva > 0.0 {
1881 Ok(())
1882 } else {
1883 Err(crate::Error::InvalidBaseMva {
1884 base: self.base_mva,
1885 })
1886 }
1887 }
1888
1889 #[must_use]
1901 pub fn validate_values(&self) -> Vec<Diagnostic> {
1902 let mut out = Vec::new();
1903 for b in &self.buses {
1904 if let Some(new) = repair_vm(b.vm) {
1905 out.push(Diagnostic {
1906 element: format!("bus {}", b.id),
1907 field: "vm",
1908 old: b.vm,
1909 new,
1910 reason: "voltage magnitude outside [0, 2] p.u.",
1911 });
1912 }
1913 if let Some(new) = repair_va(b.va) {
1914 out.push(Diagnostic {
1915 element: format!("bus {}", b.id),
1916 field: "va",
1917 old: b.va,
1918 new,
1919 reason: "voltage angle outside ±2000°",
1920 });
1921 }
1922 }
1923 for g in &self.generators {
1924 if let Some(new) = repair_mbase(g.mbase, self.base_mva) {
1925 out.push(Diagnostic {
1926 element: format!("generator at bus {}", g.bus),
1927 field: "mbase",
1928 old: g.mbase,
1929 new,
1930 reason: "non-positive generator MVA base",
1931 });
1932 }
1933 if let Some(new) = repair_vg(g.vg) {
1934 out.push(Diagnostic {
1935 element: format!("generator at bus {}", g.bus),
1936 field: "vg",
1937 old: g.vg,
1938 new,
1939 reason: "non-positive voltage setpoint",
1940 });
1941 }
1942 }
1943 out
1944 }
1945
1946 pub(crate) fn invalidate_source(&mut self) {
1952 self.source = None;
1953 }
1954
1955 pub fn repair(&mut self) -> Vec<Diagnostic> {
1960 let findings = self.validate_values();
1961 let sbase = self.base_mva;
1962 for b in &mut self.buses {
1963 if let Some(new) = repair_vm(b.vm) {
1964 b.vm = new;
1965 }
1966 if let Some(new) = repair_va(b.va) {
1967 b.va = new;
1968 }
1969 }
1970 for g in &mut self.generators {
1971 if let Some(new) = repair_mbase(g.mbase, sbase) {
1972 g.mbase = new;
1973 }
1974 if let Some(new) = repair_vg(g.vg) {
1975 g.vg = new;
1976 }
1977 }
1978 if !findings.is_empty() {
1980 self.invalidate_source();
1981 }
1982 findings
1983 }
1984
1985 pub(crate) fn expand_transformers_3w(&self) -> std::borrow::Cow<'_, Network> {
1996 if self.transformers_3w.is_empty() {
1997 return std::borrow::Cow::Borrowed(self);
1998 }
1999 let mut net = self.clone();
2000 let scale = if net.is_normalized() {
2005 1.0
2006 } else {
2007 net.base_mva
2008 };
2009 let base_id = net.buses.iter().map(|b| b.id.0).max().unwrap_or(0) + 1;
2010 for (k, t) in self
2011 .transformers_3w
2012 .iter()
2013 .filter(|t| t.in_service)
2014 .enumerate()
2015 {
2016 let star_id = BusId(base_id + k);
2017 let (star, branches) = t.star_expansion(star_id);
2018 net.buses.push(star);
2019 net.branches.extend(branches);
2020 if t.mag_g != 0.0 || t.mag_b != 0.0 {
2021 net.shunts.push(Shunt {
2022 bus: star_id,
2023 g: t.mag_g * scale,
2024 b: t.mag_b * scale,
2025 in_service: true,
2026 control: None,
2027 uid: None,
2028 extras: Extras::new(),
2029 });
2030 }
2031 }
2032 net.transformers_3w.clear();
2033 std::borrow::Cow::Owned(net)
2034 }
2035
2036 pub fn validate(&self) -> crate::Result<()> {
2042 self.check_references("network")
2043 }
2044
2045 pub(crate) fn check_references(&self, format: &'static str) -> crate::Result<()> {
2050 let mut ids = std::collections::HashSet::with_capacity(self.buses.len());
2055 for b in &self.buses {
2056 if !ids.insert(b.id) {
2057 return Err(Error::FormatRead {
2058 format,
2059 message: format!("duplicate bus id {}", b.id),
2060 });
2061 }
2062 }
2063 let check = |bus: BusId, what: &str| -> crate::Result<()> {
2064 if ids.contains(&bus) {
2065 Ok(())
2066 } else {
2067 Err(Error::FormatRead {
2068 format,
2069 message: format!("{what} references unknown bus {bus}"),
2070 })
2071 }
2072 };
2073 for (i, br) in self.branches.iter().enumerate() {
2075 for bus in [br.from, br.to] {
2076 if !ids.contains(&bus) {
2077 return Err(Error::FormatRead {
2078 format,
2079 message: format!("branch {i} references unknown bus {bus}"),
2080 });
2081 }
2082 }
2083 if let Some(bus) = br.control.as_ref().and_then(|c| c.controlled_bus) {
2084 check(bus, "transformer control")?;
2085 }
2086 }
2087 for (i, sw) in self.switches.iter().enumerate() {
2088 for bus in [sw.from, sw.to] {
2089 if !ids.contains(&bus) {
2090 return Err(Error::FormatRead {
2091 format,
2092 message: format!("switch {i} references unknown bus {bus}"),
2093 });
2094 }
2095 }
2096 }
2097 for l in &self.loads {
2098 check(l.bus, "load")?;
2099 }
2100 for s in &self.shunts {
2101 check(s.bus, "shunt")?;
2102 if let Some(bus) = s.control.as_ref().and_then(|c| c.control_bus) {
2103 check(bus, "switched-shunt control")?;
2104 }
2105 }
2106 for g in &self.generators {
2107 check(g.bus, "generator")?;
2108 if let Some(bus) = g.regulated_bus {
2109 check(bus, "generator voltage control")?;
2110 }
2111 }
2112 for d in &self.hvdc {
2113 check(d.from, "dcline")?;
2114 check(d.to, "dcline")?;
2115 }
2116 for s in &self.storage {
2117 check(s.bus, "storage")?;
2118 }
2119 for a in &self.areas {
2120 if let Some(slack) = a.slack_bus {
2121 check(slack, "area swing")?;
2122 }
2123 }
2124 for t in &self.transformers_3w {
2125 for w in &t.windings {
2126 check(w.bus, "3-winding transformer")?;
2127 }
2128 }
2129 Ok(())
2130 }
2131}
2132
2133#[cfg(test)]
2134mod tests {
2135 use super::*;
2136
2137 fn close(actual: f64, expected: f64) {
2138 assert!((actual - expected).abs() < 1e-12, "{actual} != {expected}");
2139 }
2140
2141 fn bus(id: usize) -> Bus {
2142 Bus {
2143 id: BusId(id),
2144 kind: BusType::Pq,
2145 vm: 1.0,
2146 va: 0.0,
2147 base_kv: 230.0,
2148 vmax: 1.1,
2149 vmin: 0.9,
2150 evhi: None,
2151 evlo: None,
2152 area: 1,
2153 zone: 1,
2154 name: None,
2155 uid: None,
2156 extras: Extras::new(),
2157 }
2158 }
2159
2160 fn winding(b: usize) -> Winding {
2161 Winding {
2162 bus: BusId(b),
2163 tap: 1.0,
2164 shift: 0.0,
2165 nominal_kv: 230.0,
2166 rate_a: 100.0,
2167 rate_b: 0.0,
2168 rate_c: 0.0,
2169 }
2170 }
2171
2172 fn transformer_3w() -> Transformer3W {
2173 let z = |r, x| Impedance {
2174 r,
2175 x,
2176 base_mva: 100.0,
2177 };
2178 Transformer3W {
2179 windings: [winding(1), winding(2), winding(3)],
2180 z: [z(0.01, 0.10), z(0.02, 0.20), z(0.03, 0.30)],
2181 star_vm: 0.98,
2182 star_va: -1.5,
2183 mag_g: 0.0,
2184 mag_b: 0.0,
2185 in_service: true,
2186 name: Some("T1".into()),
2187 uid: None,
2188 extras: Extras::new(),
2189 }
2190 }
2191
2192 #[test]
2193 fn star_impedances_split_the_pairwise_values() {
2194 let [(r1, x1), (r2, x2), (r3, x3)] = transformer_3w().star_impedances();
2196 close(r1, 0.01);
2197 close(x1, 0.10);
2198 close(r2, 0.0);
2199 close(x2, 0.0);
2200 close(r3, 0.02);
2201 close(x3, 0.20);
2202 }
2203
2204 #[test]
2205 fn star_expansion_builds_a_star_bus_and_three_branches() {
2206 let t = transformer_3w();
2207 let (star, branches) = t.star_expansion(BusId(99));
2208
2209 assert_eq!(star.id, BusId(99));
2210 close(star.vm, 0.98);
2211 close(star.va, -1.5);
2212 for (i, br) in branches.iter().enumerate() {
2215 assert_eq!(br.from, t.windings[i].bus);
2216 assert_eq!(br.to, BusId(99));
2217 close(br.tap, 1.0);
2218 close(br.rate_a, 100.0);
2219 }
2220 close(branches[2].r, 0.02);
2221 close(branches[2].x, 0.20);
2222 }
2223
2224 #[test]
2225 fn three_winding_transformer_survives_json_transport() {
2226 let mut net = Network::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
2227 net.transformers_3w.push(transformer_3w());
2228 net.validate().unwrap();
2229
2230 let back = Network::from_json(&net.to_json().unwrap()).unwrap();
2231 assert_eq!(back.transformers_3w.len(), 1);
2232 close(back.transformers_3w[0].z[1].x, 0.20);
2233 assert_eq!(back.transformers_3w[0].windings[2].bus, BusId(3));
2234 }
2235
2236 #[test]
2237 fn check_references_rejects_a_dangling_winding_bus() {
2238 let mut net = Network::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
2239 net.transformers_3w.push(transformer_3w()); let err = net.validate().unwrap_err().to_string();
2241 assert!(
2242 err.contains("3-winding transformer references unknown bus 3"),
2243 "got {err}"
2244 );
2245 }
2246
2247 fn regulating_branch(reg: usize) -> Branch {
2249 Branch {
2250 from: BusId(1),
2251 to: BusId(2),
2252 r: 0.0,
2253 x: 0.1,
2254 b: 0.0,
2255 charging: None,
2256 rate_a: 0.0,
2257 rate_b: 0.0,
2258 rate_c: 0.0,
2259 rating_sets: Vec::new(),
2260 current_ratings: None,
2261 tap: 1.0,
2262 shift: 0.0,
2263 in_service: true,
2264 angmin: -360.0,
2265 angmax: 360.0,
2266 control: Some(TransformerControl {
2267 mode: TransformerControlMode::Voltage,
2268 controlled_bus: Some(BusId(reg)),
2269 tap_min: 0.95,
2270 tap_max: 1.05,
2271 band_min: 1.0,
2272 band_max: 1.02,
2273 ntp: 17,
2274 mva_base: 100.0,
2275 }),
2276 solution: None,
2277 uid: None,
2278 extras: Extras::new(),
2279 }
2280 }
2281
2282 #[test]
2283 fn transformer_control_survives_json_transport() {
2284 let mut net = Network::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
2285 net.branches.push(regulating_branch(3));
2286 net.validate().unwrap();
2287
2288 let back = Network::from_json(&net.to_json().unwrap()).unwrap();
2289 let c = back.branches[0].control.as_ref().unwrap();
2290 assert_eq!(c.mode, TransformerControlMode::Voltage);
2291 assert_eq!(c.controlled_bus, Some(BusId(3)));
2292 close(c.tap_max, 1.05);
2293 assert_eq!(c.ntp, 17);
2294 }
2295
2296 #[test]
2297 fn gen_caps_serialize_as_a_named_map_that_grows_additively() {
2298 let mut caps: GenCaps = [None; GEN_EXTRA_KEYS.len()];
2299 caps[8] = Some(1.5); caps[10] = Some(0.5); let g = Generator {
2302 bus: BusId(1),
2303 pg: 10.0,
2304 qg: 0.0,
2305 pmax: 100.0,
2306 pmin: 0.0,
2307 qmax: 50.0,
2308 qmin: -50.0,
2309 vg: 1.0,
2310 mbase: 100.0,
2311 in_service: true,
2312 cost: None,
2313 caps,
2314 regulated_bus: None,
2315 uid: None,
2316 };
2317
2318 let json = serde_json::to_string(&g).unwrap();
2321 assert!(json.contains(r#""caps":{"#), "caps is an object: {json}");
2322 assert!(json.contains(r#""ramp_30":1.5"#) && json.contains(r#""apf":0.5"#));
2323 let back: Generator = serde_json::from_str(&json).unwrap();
2324 assert_eq!(back.caps, g.caps);
2325
2326 let with_future = r#"{"bus":1,"pg":10,"qg":0,"pmax":100,"pmin":0,"qmax":50,"qmin":-50,
2329 "vg":1,"mbase":100,"in_service":true,"cost":null,
2330 "caps":{"ramp_30":1.5,"future_ramp":9.9}}"#;
2331 let g2: Generator = serde_json::from_str(with_future).unwrap();
2332 assert_eq!(g2.caps[8], Some(1.5));
2333 assert_eq!(g2.caps.iter().filter(|v| v.is_some()).count(), 1);
2334 let no_caps = r#"{"bus":1,"pg":10,"qg":0,"pmax":100,"pmin":0,"qmax":50,"qmin":-50,
2335 "vg":1,"mbase":100,"in_service":true,"cost":null}"#;
2336 let g3: Generator = serde_json::from_str(no_caps).unwrap();
2337 assert!(!g3.has_caps());
2338
2339 let null_caps = r#"{"bus":1,"pg":10,"qg":0,"pmax":100,"pmin":0,"qmax":50,"qmin":-50,
2341 "vg":1,"mbase":100,"in_service":true,"cost":null,"caps":null}"#;
2342 let g4: Generator = serde_json::from_str(null_caps).unwrap();
2343 assert!(!g4.has_caps());
2344 }
2345
2346 #[test]
2347 fn non_finite_fields_lists_every_offender_not_just_the_first() {
2348 let bus = |id, vm| Bus {
2349 id: BusId(id),
2350 kind: BusType::Pq,
2351 vm,
2352 va: 0.0,
2353 base_kv: 230.0,
2354 vmax: 1.1,
2355 vmin: 0.9,
2356 evhi: None,
2357 evlo: None,
2358 area: 1,
2359 zone: 1,
2360 name: None,
2361 uid: None,
2362 extras: Extras::new(),
2363 };
2364 let branch = Branch {
2365 from: BusId(1),
2366 to: BusId(2),
2367 r: 0.0,
2368 x: f64::INFINITY,
2369 b: 0.0,
2370 charging: None,
2371 rate_a: 0.0,
2372 rate_b: 0.0,
2373 rate_c: 0.0,
2374 rating_sets: Vec::new(),
2375 current_ratings: None,
2376 tap: 0.0,
2377 shift: 0.0,
2378 in_service: true,
2379 angmin: -360.0,
2380 angmax: 360.0,
2381 control: None,
2382 solution: None,
2383 uid: None,
2384 extras: Extras::new(),
2385 };
2386 let mut g = Generator {
2389 bus: BusId(1),
2390 pg: 0.0,
2391 qg: 0.0,
2392 pmax: 0.0,
2393 pmin: 0.0,
2394 qmax: 0.0,
2395 qmin: 0.0,
2396 vg: 1.0,
2397 mbase: 100.0,
2398 in_service: true,
2399 cost: None,
2400 caps: GenCaps::default(),
2401 regulated_bus: None,
2402 uid: None,
2403 };
2404 g.caps[8] = Some(f64::INFINITY); let mut net = Network::in_memory(
2408 "nf",
2409 100.0,
2410 vec![bus(1, f64::NAN), bus(2, 1.0)],
2411 vec![branch],
2412 );
2413 net.generators.push(g);
2414 let fields = net.non_finite_fields();
2415 assert!(fields.contains(&"buses[0].vm".to_string()), "{fields:?}");
2416 assert!(fields.contains(&"branches[0].x".to_string()), "{fields:?}");
2417 assert!(
2418 fields.contains(&"generators[0].caps.ramp_30".to_string()),
2419 "caps reported at key precision: {fields:?}"
2420 );
2421 assert_eq!(
2422 fields.len(),
2423 3,
2424 "exactly the three offenders, no more: {fields:?}"
2425 );
2426 }
2427
2428 #[test]
2429 fn check_references_rejects_a_dangling_controlled_bus() {
2430 let mut net = Network::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
2431 net.branches.push(regulating_branch(9)); let err = net.validate().unwrap_err().to_string();
2433 assert!(
2434 err.contains("transformer control references unknown bus 9"),
2435 "got {err}"
2436 );
2437 }
2438
2439 fn switched_shunt(reg: usize) -> Shunt {
2441 Shunt {
2442 bus: BusId(1),
2443 g: 0.0,
2444 b: 19.0,
2445 in_service: true,
2446 control: Some(SwitchedShuntControl {
2447 mode: SwitchedShuntMode::Discrete,
2448 vhigh: 1.05,
2449 vlow: 0.95,
2450 control_bus: Some(BusId(reg)),
2451 rmpct: 100.0,
2452 blocks: vec![
2453 ShuntBlock { steps: 2, b: 25.0 },
2454 ShuntBlock { steps: 1, b: 50.0 },
2455 ],
2456 }),
2457 uid: None,
2458 extras: Extras::new(),
2459 }
2460 }
2461
2462 #[test]
2463 fn switched_shunt_control_survives_json_transport() {
2464 let mut net = Network::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
2465 net.shunts.push(switched_shunt(3));
2466 net.validate().unwrap();
2467
2468 let back = Network::from_json(&net.to_json().unwrap()).unwrap();
2469 let c = back.shunts[0].control.as_ref().unwrap();
2470 assert_eq!(c.mode, SwitchedShuntMode::Discrete);
2471 assert_eq!(c.control_bus, Some(BusId(3)));
2472 assert_eq!(c.blocks.len(), 2);
2473 close(c.blocks[1].b, 50.0);
2474 }
2475
2476 #[test]
2477 fn check_references_rejects_a_dangling_switched_shunt_control_bus() {
2478 let mut net = Network::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
2479 net.shunts.push(switched_shunt(9)); let err = net.validate().unwrap_err().to_string();
2481 assert!(
2482 err.contains("switched-shunt control references unknown bus 9"),
2483 "got {err}"
2484 );
2485 }
2486
2487 #[test]
2488 fn validate_values_flags_and_repair_clamps_out_of_domain_values() {
2489 let mut net = Network::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
2490 net.buses[0].vm = 0.0; net.buses[1].va = 9000.0; net.generators.push(Generator {
2493 bus: BusId(1),
2494 pg: 10.0,
2495 qg: 0.0,
2496 pmax: 100.0,
2497 pmin: 0.0,
2498 qmax: 50.0,
2499 qmin: -50.0,
2500 vg: 0.0, mbase: 0.0, in_service: true,
2503 cost: None,
2504 caps: Default::default(),
2505 regulated_bus: None,
2506 uid: None,
2507 });
2508
2509 let diags = net.validate_values();
2510 let fields: std::collections::BTreeSet<_> = diags.iter().map(|d| d.field).collect();
2511 assert_eq!(
2512 fields,
2513 ["mbase", "va", "vg", "vm"].into_iter().collect(),
2514 "all four out-of-domain fields reported"
2515 );
2516 close(net.buses[0].vm, 0.0);
2518
2519 let applied = net.repair();
2520 assert_eq!(applied.len(), diags.len());
2521 close(net.buses[0].vm, 1.0);
2522 close(net.buses[1].va, 0.0);
2523 close(net.generators[0].mbase, 100.0); close(net.generators[0].vg, 1.0);
2525 assert!(net.validate_values().is_empty());
2527 }
2528
2529 #[test]
2530 fn validate_values_is_empty_for_a_clean_network() {
2531 let net = Network::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
2532 assert!(net.validate_values().is_empty());
2533 }
2534}