1use std::collections::BTreeMap;
17use std::sync::Arc;
18
19use serde::{Deserialize, Serialize};
20use serde_json::Value;
21
22use crate::geo::{GeoMeta, Location};
23use crate::{Error, Result};
24
25pub type Extras = BTreeMap<String, Value>;
28
29pub const DEFAULT_BASE_FREQUENCY: f64 = 60.0;
33
34fn default_base_frequency() -> f64 {
38 DEFAULT_BASE_FREQUENCY
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
48#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
49#[serde(transparent)]
50pub struct BusId(pub usize);
51
52impl BusId {
53 #[must_use]
54 pub const fn new(id: usize) -> Self {
55 Self(id)
56 }
57}
58
59impl std::fmt::Display for BusId {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 self.0.fmt(f)
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
67#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
68#[serde(rename_all = "UPPERCASE")]
69#[repr(u8)]
70#[non_exhaustive]
71pub enum BusType {
72 Pq = 1,
73 Pv = 2,
74 Ref = 3,
75 Isolated = 4,
76}
77
78impl BusType {
79 pub(crate) fn from_f64(v: f64) -> Self {
81 match v as i32 {
82 2 => Self::Pv,
83 3 => Self::Ref,
84 4 => Self::Isolated,
85 _ => Self::Pq,
86 }
87 }
88
89 #[must_use]
92 pub fn as_str(self) -> &'static str {
93 match self {
94 Self::Pq => "PQ",
95 Self::Pv => "PV",
96 Self::Ref => "REF",
97 Self::Isolated => "ISOLATED",
98 }
99 }
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
104#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
105#[non_exhaustive]
106pub struct GenCost {
107 pub model: u8,
109 pub startup: f64,
110 pub shutdown: f64,
111 pub ncost: usize,
113 pub coeffs: Vec<f64>,
116}
117
118impl GenCost {
119 #[must_use]
127 pub fn new(model: u8, startup: f64, shutdown: f64, coeffs: Vec<f64>) -> Self {
128 let ncost = if model == 1 {
129 coeffs.len() / 2
130 } else {
131 coeffs.len()
132 };
133 Self {
134 model,
135 startup,
136 shutdown,
137 ncost,
138 coeffs,
139 }
140 }
141
142 #[must_use]
143 pub fn with_ncost(
144 model: u8,
145 startup: f64,
146 shutdown: f64,
147 ncost: usize,
148 coeffs: Vec<f64>,
149 ) -> Self {
150 Self {
151 model,
152 startup,
153 shutdown,
154 ncost,
155 coeffs,
156 }
157 }
158
159 pub fn quadratic(&self) -> Option<(f64, f64)> {
164 self.quadratic_with_constant().map(|(q, c, _)| (q, c))
165 }
166
167 pub fn quadratic_with_constant(&self) -> Option<(f64, f64, f64)> {
173 if self.model != 2 {
174 return None;
175 }
176 if self.coeffs.len() < self.ncost {
179 return None;
180 }
181 match self.ncost {
185 3 => Some((2.0 * self.coeffs[0], self.coeffs[1], self.coeffs[2])),
186 2 => Some((0.0, self.coeffs[0], self.coeffs[1])),
187 1 => Some((0.0, 0.0, self.coeffs[0])),
188 _ => None,
189 }
190 }
191
192 pub const LEADING_COEFF_TOL: f64 = 1e-12;
196
197 pub fn quadratic_with_constant_tol(&self, tol: f64) -> Option<(f64, f64, f64)> {
207 if self.model != 2 {
208 return None;
209 }
210 if self.coeffs.len() < self.ncost {
211 return None;
212 }
213 let row = &self.coeffs[..self.ncost];
214 let mut first = 0;
215 while first + 1 < row.len() && row[first].abs() <= tol {
216 first += 1;
217 }
218 match row.len() - first {
219 3 => Some((2.0 * row[first], row[first + 1], row[first + 2])),
220 2 => Some((0.0, row[first], row[first + 1])),
221 1 => Some((0.0, 0.0, row[first])),
222 _ => None,
223 }
224 }
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
230#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
231#[non_exhaustive]
232pub enum SourceFormat {
233 Matpower,
234 PowerModelsJson,
235 EgretJson,
236 Psse,
237 PowerWorld,
238 PandapowerJson,
239 Pslf,
244 PowerWorldBinary,
248 InMemory,
250 Normalized,
256 Gridfm,
262 PypsaCsv,
265 Goc3Json,
269 SurgeJson,
271 DeepMindOpfDataJson,
276}
277
278impl SourceFormat {
279 #[must_use]
284 pub fn name(self) -> &'static str {
285 match self {
286 SourceFormat::Matpower => "matpower",
287 SourceFormat::PowerModelsJson => "powermodels-json",
288 SourceFormat::EgretJson => "egret-json",
289 SourceFormat::Psse => "psse",
290 SourceFormat::PowerWorld => "powerworld",
291 SourceFormat::PandapowerJson => "pandapower-json",
292 SourceFormat::Pslf => "pslf",
293 SourceFormat::PowerWorldBinary => "powerworld-pwb",
294 SourceFormat::InMemory => "in-memory",
295 SourceFormat::Normalized => "normalized",
296 SourceFormat::Gridfm => "gridfm",
297 SourceFormat::PypsaCsv => "pypsa-csv",
298 SourceFormat::Goc3Json => "goc3-json",
299 SourceFormat::SurgeJson => "surge-json",
300 SourceFormat::DeepMindOpfDataJson => "opfdata-json",
301 }
302 }
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
307#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
308#[non_exhaustive]
309pub struct BalancedNetwork {
310 pub name: String,
311 pub base_mva: f64,
312 #[serde(default = "default_base_frequency")]
319 pub base_frequency: f64,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
321 pub geo: Option<GeoMeta>,
322 pub buses: Vec<Bus>,
323 pub loads: Vec<Load>,
324 pub shunts: Vec<Shunt>,
325 pub branches: Vec<Branch>,
326 #[serde(default)]
327 pub switches: Vec<Switch>,
328 pub generators: Vec<Generator>,
329 pub storage: Vec<Storage>,
330 pub hvdc: Vec<Hvdc>,
331 #[serde(default)]
340 pub transformers_3w: Vec<Transformer3W>,
341 #[serde(default)]
346 pub areas: Vec<Area>,
347 #[serde(default)]
350 pub solver: Option<SolverParams>,
351 pub source_format: SourceFormat,
352 #[serde(skip)]
363 pub source: Option<Arc<String>>,
364}
365
366#[derive(Debug, Clone, Serialize, Deserialize)]
367#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
368#[non_exhaustive]
369pub struct Bus {
370 pub id: BusId,
372 pub kind: BusType,
373 pub vm: f64,
375 pub va: f64,
377 pub base_kv: f64,
378 pub vmax: f64,
379 pub vmin: f64,
380 #[serde(default)]
386 pub evhi: Option<f64>,
387 #[serde(default)]
388 pub evlo: Option<f64>,
389 pub area: usize,
390 pub zone: usize,
391 pub name: Option<String>,
392 #[serde(default, skip_serializing_if = "Option::is_none")]
397 pub uid: Option<String>,
398 #[serde(default, skip_serializing_if = "Option::is_none")]
400 pub location: Option<Location>,
401 pub extras: Extras,
402}
403
404impl Bus {
405 #[must_use]
406 pub fn new(id: BusId, kind: BusType, base_kv: f64) -> Self {
407 Self {
408 id,
409 kind,
410 vm: 1.0,
411 va: 0.0,
412 base_kv,
413 vmax: 1.1,
414 vmin: 0.9,
415 evhi: None,
416 evlo: None,
417 area: 1,
418 zone: 1,
419 name: None,
420 uid: None,
421 location: None,
422 extras: Extras::new(),
423 }
424 }
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize)]
428#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
429#[non_exhaustive]
430pub struct Load {
431 pub bus: BusId,
432 pub p: f64,
434 pub q: f64,
436 #[serde(default)]
438 pub voltage_model: Option<LoadVoltageModel>,
439 pub in_service: bool,
440 #[serde(default, skip_serializing_if = "Option::is_none")]
442 pub uid: Option<String>,
443 pub extras: Extras,
444}
445
446impl Load {
447 #[must_use]
448 pub fn new(bus: BusId, p: f64, q: f64) -> Self {
449 Self {
450 bus,
451 p,
452 q,
453 voltage_model: None,
454 in_service: true,
455 uid: None,
456 extras: Extras::new(),
457 }
458 }
459}
460
461#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
463#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
464#[serde(tag = "kind", rename_all = "snake_case")]
465#[non_exhaustive]
466pub enum LoadVoltageModel {
467 ConstantPower,
469 Zip {
472 p_constant_power: f64,
473 q_constant_power: f64,
474 p_constant_current: f64,
475 q_constant_current: f64,
476 p_constant_impedance: f64,
477 q_constant_impedance: f64,
478 #[serde(default)]
479 v_nom: Option<f64>,
480 #[serde(default)]
483 load_type: Option<i32>,
484 #[serde(default)]
486 scaling: Option<f64>,
487 },
488 Exponential {
491 p: f64,
492 q: f64,
493 #[serde(default)]
494 v_nom: Option<f64>,
495 gamma_p: f64,
496 gamma_q: f64,
497 },
498}
499
500impl LoadVoltageModel {
501 #[must_use]
502 pub fn has_non_matpower_fields(&self) -> bool {
503 match self {
504 Self::ConstantPower => false,
505 Self::Zip {
506 p_constant_current,
507 q_constant_current,
508 p_constant_impedance,
509 q_constant_impedance,
510 v_nom,
511 load_type,
512 scaling,
513 ..
514 } => {
515 *p_constant_current != 0.0
516 || *q_constant_current != 0.0
517 || *p_constant_impedance != 0.0
518 || *q_constant_impedance != 0.0
519 || v_nom.is_some()
520 || load_type.is_some()
521 || scaling.is_some()
522 }
523 Self::Exponential { .. } => true,
524 }
525 }
526}
527
528#[derive(Debug, Clone, Serialize, Deserialize)]
529#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
530#[non_exhaustive]
531pub struct Shunt {
532 pub bus: BusId,
533 pub g: f64,
535 pub b: f64,
538 pub in_service: bool,
539 #[serde(default)]
543 pub control: Option<SwitchedShuntControl>,
544 #[serde(default, skip_serializing_if = "Option::is_none")]
546 pub uid: Option<String>,
547 pub extras: Extras,
548}
549
550impl Shunt {
551 #[must_use]
552 pub fn new(bus: BusId, g: f64, b: f64) -> Self {
553 Self {
554 bus,
555 g,
556 b,
557 in_service: true,
558 control: None,
559 uid: None,
560 extras: Extras::new(),
561 }
562 }
563}
564
565#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
567#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
568#[serde(rename_all = "snake_case")]
569#[non_exhaustive]
570pub enum SwitchedShuntMode {
571 Locked,
573 Continuous,
575 Discrete,
577}
578
579#[derive(Debug, Clone, Serialize, Deserialize)]
581#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
582#[non_exhaustive]
583pub struct ShuntBlock {
584 pub steps: u32,
585 pub b: f64,
587}
588
589impl ShuntBlock {
590 #[must_use]
591 pub const fn new(steps: u32, b: f64) -> Self {
592 Self { steps, b }
593 }
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize)]
601#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
602#[non_exhaustive]
603pub struct SwitchedShuntControl {
604 pub mode: SwitchedShuntMode,
605 pub vhigh: f64,
607 pub vlow: f64,
608 pub control_bus: Option<BusId>,
610 pub rmpct: f64,
612 pub blocks: Vec<ShuntBlock>,
613}
614
615impl SwitchedShuntControl {
616 #[must_use]
617 pub fn new(mode: SwitchedShuntMode, vhigh: f64, vlow: f64, blocks: Vec<ShuntBlock>) -> Self {
618 Self {
619 mode,
620 vhigh,
621 vlow,
622 control_bus: None,
623 rmpct: 100.0,
624 blocks,
625 }
626 }
627}
628
629#[derive(Debug, Clone, Serialize, Deserialize)]
630#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
631#[non_exhaustive]
632pub struct Branch {
633 pub from: BusId,
634 pub to: BusId,
635 pub r: f64,
637 pub x: f64,
639 pub b: f64,
643 #[serde(default)]
646 pub charging: Option<BranchCharging>,
647 pub rate_a: f64,
648 pub rate_b: f64,
649 pub rate_c: f64,
650 #[serde(default)]
653 pub rating_sets: Vec<BranchRatingSet>,
654 #[serde(default)]
656 pub current_ratings: Option<BranchCurrentRatings>,
657 pub tap: f64,
659 pub shift: f64,
661 pub in_service: bool,
662 pub angmin: f64,
663 pub angmax: f64,
664 #[serde(default)]
669 pub control: Option<TransformerControl>,
670 #[serde(default)]
672 pub solution: Option<BranchSolution>,
673 #[serde(default, skip_serializing_if = "Option::is_none")]
675 pub uid: Option<String>,
676 #[serde(default, skip_serializing_if = "Option::is_none")]
681 pub route: Option<Vec<Location>>,
682 pub extras: Extras,
683}
684
685#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
687#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
688#[non_exhaustive]
689pub struct BranchRatingSet {
690 pub name: String,
691 pub rate_mva: f64,
692}
693
694impl BranchRatingSet {
695 #[must_use]
696 pub fn new(name: impl Into<String>, rate_mva: f64) -> Self {
697 Self {
698 name: name.into(),
699 rate_mva,
700 }
701 }
702}
703
704#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
707#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
708#[non_exhaustive]
709pub struct BranchCharging {
710 pub g_fr: f64,
711 pub b_fr: f64,
712 pub g_to: f64,
713 pub b_to: f64,
714}
715
716impl BranchCharging {
717 #[must_use]
718 pub const fn new(g_fr: f64, b_fr: f64, g_to: f64, b_to: f64) -> Self {
719 Self {
720 g_fr,
721 b_fr,
722 g_to,
723 b_to,
724 }
725 }
726
727 #[must_use]
728 pub fn from_total_b(b: f64) -> Self {
729 Self {
730 g_fr: 0.0,
731 b_fr: b / 2.0,
732 g_to: 0.0,
733 b_to: b / 2.0,
734 }
735 }
736
737 #[must_use]
738 pub fn total_b(self) -> f64 {
739 self.b_fr + self.b_to
740 }
741
742 #[must_use]
743 pub fn total_g(self) -> f64 {
744 self.g_fr + self.g_to
745 }
746
747 #[must_use]
748 pub fn is_matpower_symmetric(self) -> bool {
749 self.g_fr.abs() <= f64::EPSILON
750 && self.g_to.abs() <= f64::EPSILON
751 && (self.b_fr - self.b_to).abs() <= f64::EPSILON
752 }
753}
754
755#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
757#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
758#[non_exhaustive]
759pub struct BranchCurrentRatings {
760 pub c_rating_a: f64,
761 pub c_rating_b: f64,
762 pub c_rating_c: f64,
763}
764
765impl BranchCurrentRatings {
766 #[must_use]
767 pub const fn new(c_rating_a: f64, c_rating_b: f64, c_rating_c: f64) -> Self {
768 Self {
769 c_rating_a,
770 c_rating_b,
771 c_rating_c,
772 }
773 }
774}
775
776#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
778#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
779#[non_exhaustive]
780pub struct BranchSolution {
781 pub pf: f64,
782 pub qf: f64,
783 pub pt: f64,
784 pub qt: f64,
785}
786
787impl BranchSolution {
788 #[must_use]
789 pub const fn new(pf: f64, qf: f64, pt: f64, qt: f64) -> Self {
790 Self { pf, qf, pt, qt }
791 }
792}
793
794impl Branch {
795 #[must_use]
796 pub fn new(from: BusId, to: BusId, r: f64, x: f64) -> Self {
797 Self {
798 from,
799 to,
800 r,
801 x,
802 b: 0.0,
803 charging: None,
804 rate_a: 0.0,
805 rate_b: 0.0,
806 rate_c: 0.0,
807 rating_sets: Vec::new(),
808 current_ratings: None,
809 tap: 0.0,
810 shift: 0.0,
811 in_service: true,
812 angmin: -360.0,
813 angmax: 360.0,
814 control: None,
815 solution: None,
816 uid: None,
817 route: None,
818 extras: Extras::new(),
819 }
820 }
821
822 #[must_use]
824 pub fn effective_tap(&self) -> f64 {
825 if self.tap == 0.0 { 1.0 } else { self.tap }
826 }
827
828 pub fn divisible_tap(&self, row: usize) -> Result<f64> {
837 let tap = self.effective_tap();
838 if !tap.is_finite() || tap.abs() < crate::dc::MIN_DIVISIBLE_MAGNITUDE {
839 return Err(Error::DegenerateTap { row, tap });
840 }
841 Ok(tap)
842 }
843
844 #[must_use]
847 pub fn terminal_charging(&self) -> BranchCharging {
848 self.charging
849 .unwrap_or_else(|| BranchCharging::from_total_b(self.b))
850 }
851
852 pub fn series_admittance(&self, row: usize) -> Result<Option<(f64, f64)>> {
864 series_admittance_of(self.r, self.x, row)
865 }
866
867 #[must_use]
886 pub fn synthesize_rate_a(&self, angle_window_rad: f64, fr_vmax: f64, to_vmax: f64) -> f64 {
887 let zmag = self.r.hypot(self.x);
890 if zmag < crate::dc::MIN_DIVISIBLE_MAGNITUDE {
891 return 0.0;
892 }
893 let window = angle_window_rad.abs().min(std::f64::consts::PI);
894 let separation =
895 (fr_vmax * fr_vmax + to_vmax * to_vmax - 2.0 * fr_vmax * to_vmax * window.cos()).sqrt();
896 fr_vmax.max(to_vmax) * separation / zmag
897 }
898
899 #[must_use]
902 pub fn total_charging_b(&self) -> f64 {
903 self.terminal_charging().total_b()
904 }
905
906 #[must_use]
908 pub fn has_non_matpower_charging(&self) -> bool {
909 self.charging
910 .is_some_and(|charging| !charging.is_matpower_symmetric())
911 }
912
913 #[must_use]
916 pub fn is_transformer(&self) -> bool {
917 self.tap != 0.0 || self.shift != 0.0
918 }
919
920 #[must_use]
924 pub fn has_angle_limits(&self) -> bool {
925 self.angmin > -360.0 || self.angmax < 360.0
926 }
927}
928
929pub fn series_admittance_of(r: f64, x: f64, row: usize) -> Result<Option<(f64, f64)>> {
946 if r.hypot(x) < crate::dc::MIN_DIVISIBLE_MAGNITUDE {
947 return Ok(None);
948 }
949 let denom = r * r + x * x;
950 if !denom.is_finite() {
951 return Err(Error::NonFiniteSusceptance { row });
952 }
953 Ok(Some((r / denom, -x / denom)))
954}
955
956#[derive(Debug, Clone, Serialize, Deserialize)]
959#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
960#[non_exhaustive]
961pub struct Switch {
962 pub from: BusId,
963 pub to: BusId,
964 pub closed: bool,
965 #[serde(default)]
966 pub thermal_rating: Option<f64>,
967 #[serde(default)]
968 pub current_rating: Option<f64>,
969 #[serde(default)]
970 pub pf: Option<f64>,
971 #[serde(default)]
972 pub qf: Option<f64>,
973 #[serde(default)]
974 pub pt: Option<f64>,
975 #[serde(default)]
976 pub qt: Option<f64>,
977 #[serde(default, skip_serializing_if = "Option::is_none")]
979 pub uid: Option<String>,
980 pub extras: Extras,
981}
982
983impl Switch {
984 #[must_use]
985 pub fn new(from: BusId, to: BusId, closed: bool) -> Self {
986 Self {
987 from,
988 to,
989 closed,
990 thermal_rating: None,
991 current_rating: None,
992 pf: None,
993 qf: None,
994 pt: None,
995 qt: None,
996 uid: None,
997 extras: Extras::new(),
998 }
999 }
1000}
1001
1002#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1005#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1006#[serde(rename_all = "snake_case")]
1007#[non_exhaustive]
1008pub enum TransformerControlMode {
1009 Fixed,
1011 Voltage,
1013 ReactiveFlow,
1015 ActiveFlow,
1017}
1018
1019#[derive(Debug, Clone, Serialize, Deserialize)]
1029#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1030#[non_exhaustive]
1031pub struct TransformerControl {
1032 pub mode: TransformerControlMode,
1033 pub controlled_bus: Option<BusId>,
1034 pub tap_min: f64,
1035 pub tap_max: f64,
1036 pub band_min: f64,
1037 pub band_max: f64,
1038 pub ntp: u32,
1039 pub mva_base: f64,
1040}
1041
1042impl Default for TransformerControl {
1043 fn default() -> Self {
1044 TransformerControl {
1046 mode: TransformerControlMode::Fixed,
1047 controlled_bus: None,
1048 tap_min: 0.9,
1049 tap_max: 1.1,
1050 band_min: 0.9,
1051 band_max: 1.1,
1052 ntp: 33,
1053 mva_base: 0.0,
1054 }
1055 }
1056}
1057
1058impl TransformerControl {
1059 #[must_use]
1060 pub fn new(mode: TransformerControlMode) -> Self {
1061 Self {
1062 mode,
1063 ..Self::default()
1064 }
1065 }
1066}
1067
1068#[derive(Debug, Clone, Serialize, Deserialize)]
1069#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1070#[non_exhaustive]
1071pub struct Generator {
1072 pub bus: BusId,
1073 pub pg: f64,
1075 pub qg: f64,
1077 pub pmax: f64,
1078 pub pmin: f64,
1079 pub qmax: f64,
1080 pub qmin: f64,
1081 pub vg: f64,
1083 pub mbase: f64,
1084 pub in_service: bool,
1085 pub cost: Option<GenCost>,
1086 #[serde(default = "default_caps", with = "caps_serde")]
1095 #[cfg_attr(feature = "schema", schemars(with = "BTreeMap<String, f64>"))]
1096 pub caps: GenCaps,
1097 #[serde(default)]
1104 pub regulated_bus: Option<BusId>,
1105 #[serde(default, skip_serializing_if = "Option::is_none")]
1107 pub uid: Option<String>,
1108}
1109
1110impl Generator {
1111 #[must_use]
1112 pub fn new(bus: BusId) -> Self {
1113 Self {
1114 bus,
1115 pg: 0.0,
1116 qg: 0.0,
1117 pmax: 0.0,
1118 pmin: 0.0,
1119 qmax: 0.0,
1120 qmin: 0.0,
1121 vg: 1.0,
1122 mbase: 0.0,
1123 in_service: true,
1124 cost: None,
1125 caps: default_caps(),
1126 regulated_bus: None,
1127 uid: None,
1128 }
1129 }
1130
1131 #[must_use]
1134 pub fn has_caps(&self) -> bool {
1135 self.caps.iter().any(Option::is_some)
1136 }
1137}
1138
1139pub type GenCaps = [Option<f64>; GEN_EXTRA_KEYS.len()];
1141
1142fn default_caps() -> GenCaps {
1144 [None; GEN_EXTRA_KEYS.len()]
1145}
1146
1147mod caps_serde {
1158 use super::{GEN_EXTRA_KEYS, GenCaps};
1159 use serde::de::{Deserialize, Deserializer};
1160 use serde::ser::{SerializeMap, Serializer};
1161 use std::collections::BTreeMap;
1162
1163 pub(super) fn serialize<S: Serializer>(caps: &GenCaps, s: S) -> Result<S::Ok, S::Error> {
1164 let present = caps.iter().filter(|v| v.is_some()).count();
1165 let mut map = s.serialize_map(Some(present))?;
1166 for (key, slot) in GEN_EXTRA_KEYS.iter().zip(caps.iter()) {
1167 if let Some(value) = slot {
1168 map.serialize_entry(key, value)?;
1169 }
1170 }
1171 map.end()
1172 }
1173
1174 pub(super) fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<GenCaps, D::Error> {
1175 let named = Option::<BTreeMap<String, f64>>::deserialize(d)?.unwrap_or_default();
1180 let mut caps: GenCaps = [None; GEN_EXTRA_KEYS.len()];
1181 for (slot, key) in caps.iter_mut().zip(GEN_EXTRA_KEYS.iter()) {
1182 *slot = named.get(*key).copied();
1183 }
1184 Ok(caps)
1185 }
1186}
1187
1188#[derive(Debug, Clone, Serialize, Deserialize)]
1189#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1190#[non_exhaustive]
1191pub struct Storage {
1192 pub bus: BusId,
1193 pub ps: f64,
1194 pub qs: f64,
1195 pub energy: f64,
1196 pub energy_rating: f64,
1197 pub charge_rating: f64,
1198 pub discharge_rating: f64,
1199 pub charge_efficiency: f64,
1200 pub discharge_efficiency: f64,
1201 pub thermal_rating: f64,
1202 #[serde(default)]
1203 pub current_rating: Option<f64>,
1204 pub qmin: f64,
1205 pub qmax: f64,
1206 pub r: f64,
1207 pub x: f64,
1208 pub p_loss: f64,
1209 pub q_loss: f64,
1210 pub in_service: bool,
1211 #[serde(default, skip_serializing_if = "Option::is_none")]
1213 pub uid: Option<String>,
1214 pub extras: Extras,
1215}
1216
1217impl Storage {
1218 #[must_use]
1219 pub fn new(bus: BusId) -> Self {
1220 Self {
1221 bus,
1222 ps: 0.0,
1223 qs: 0.0,
1224 energy: 0.0,
1225 energy_rating: 0.0,
1226 charge_rating: 0.0,
1227 discharge_rating: 0.0,
1228 charge_efficiency: 1.0,
1229 discharge_efficiency: 1.0,
1230 thermal_rating: 0.0,
1231 current_rating: None,
1232 qmin: 0.0,
1233 qmax: 0.0,
1234 r: 0.0,
1235 x: 0.0,
1236 p_loss: 0.0,
1237 q_loss: 0.0,
1238 in_service: true,
1239 uid: None,
1240 extras: Extras::new(),
1241 }
1242 }
1243}
1244
1245#[derive(Debug, Clone, Serialize, Deserialize)]
1253#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1254#[non_exhaustive]
1255pub struct Hvdc {
1256 pub from: BusId,
1257 pub to: BusId,
1258 pub in_service: bool,
1259 pub pf: f64,
1260 pub pt: f64,
1261 pub qf: f64,
1262 pub qt: f64,
1263 pub vf: f64,
1264 pub vt: f64,
1265 pub pmin: f64,
1266 pub pmax: f64,
1267 pub qminf: f64,
1268 pub qmaxf: f64,
1269 pub qmint: f64,
1270 pub qmaxt: f64,
1271 pub loss0: f64,
1272 pub loss1: f64,
1273 #[serde(default)]
1274 pub cost: Option<GenCost>,
1275 #[serde(default, skip_serializing_if = "Option::is_none")]
1277 pub uid: Option<String>,
1278 pub extras: Extras,
1279}
1280
1281impl Hvdc {
1282 #[must_use]
1283 pub fn new(from: BusId, to: BusId) -> Self {
1284 Self {
1285 from,
1286 to,
1287 in_service: true,
1288 pf: 0.0,
1289 pt: 0.0,
1290 qf: 0.0,
1291 qt: 0.0,
1292 vf: 1.0,
1293 vt: 1.0,
1294 pmin: 0.0,
1295 pmax: 0.0,
1296 qminf: 0.0,
1297 qmaxf: 0.0,
1298 qmint: 0.0,
1299 qmaxt: 0.0,
1300 loss0: 0.0,
1301 loss1: 0.0,
1302 cost: None,
1303 uid: None,
1304 extras: Extras::new(),
1305 }
1306 }
1307}
1308
1309#[derive(Debug, Clone, Serialize, Deserialize)]
1316#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1317#[non_exhaustive]
1318pub struct Area {
1319 pub number: usize,
1320 pub slack_bus: Option<BusId>,
1322 pub net_interchange: f64,
1324 pub tolerance: f64,
1326 pub name: Option<String>,
1327}
1328
1329impl Area {
1330 #[must_use]
1331 pub fn new(number: usize) -> Self {
1332 Self {
1333 number,
1334 slack_bus: None,
1335 net_interchange: 0.0,
1336 tolerance: 0.0,
1337 name: None,
1338 }
1339 }
1340}
1341
1342#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
1351#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1352#[non_exhaustive]
1353pub struct SolverParams {
1354 pub newton_tolerance: Option<f64>,
1356 pub max_iterations: Option<u32>,
1358 pub zero_impedance_threshold: Option<f64>,
1360 pub adjust_taps: Option<bool>,
1362 pub adjust_area_interchange: Option<bool>,
1364 pub adjust_phase_shift: Option<bool>,
1366 pub adjust_dc_taps: Option<bool>,
1368 pub adjust_switched_shunt: Option<bool>,
1370}
1371
1372impl SolverParams {
1373 #[must_use]
1374 pub fn new() -> Self {
1375 Self::default()
1376 }
1377
1378 #[must_use]
1380 pub fn is_empty(&self) -> bool {
1381 *self == SolverParams::default()
1382 }
1383}
1384
1385#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
1397#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1398#[non_exhaustive]
1399pub struct Impedance {
1400 pub r: f64,
1401 pub x: f64,
1402 pub base_mva: f64,
1403}
1404
1405impl Impedance {
1406 #[must_use]
1407 pub const fn new(r: f64, x: f64, base_mva: f64) -> Self {
1408 Self { r, x, base_mva }
1409 }
1410}
1411
1412#[derive(Debug, Clone, Serialize, Deserialize)]
1415#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1416#[non_exhaustive]
1417pub struct Winding {
1418 pub bus: BusId,
1419 pub tap: f64,
1421 pub shift: f64,
1423 pub nominal_kv: f64,
1425 pub rate_a: f64,
1426 pub rate_b: f64,
1427 pub rate_c: f64,
1428}
1429
1430impl Winding {
1431 #[must_use]
1432 pub fn new(bus: BusId) -> Self {
1433 Self {
1434 bus,
1435 tap: 1.0,
1436 shift: 0.0,
1437 nominal_kv: 0.0,
1438 rate_a: 0.0,
1439 rate_b: 0.0,
1440 rate_c: 0.0,
1441 }
1442 }
1443}
1444
1445#[derive(Debug, Clone, Serialize, Deserialize)]
1455#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1456#[non_exhaustive]
1457pub struct Transformer3W {
1458 pub windings: [Winding; 3],
1460 pub z: [Impedance; 3],
1464 pub star_vm: f64,
1466 pub star_va: f64,
1467 pub mag_g: f64,
1469 pub mag_b: f64,
1470 pub in_service: bool,
1471 pub name: Option<String>,
1472 #[serde(default, skip_serializing_if = "Option::is_none")]
1474 pub uid: Option<String>,
1475 pub extras: Extras,
1476}
1477
1478impl Transformer3W {
1479 #[must_use]
1480 pub fn new(windings: [Winding; 3], z: [Impedance; 3]) -> Self {
1481 Self {
1482 windings,
1483 z,
1484 star_vm: 1.0,
1485 star_va: 0.0,
1486 mag_g: 0.0,
1487 mag_b: 0.0,
1488 in_service: true,
1489 name: None,
1490 uid: None,
1491 extras: Extras::new(),
1492 }
1493 }
1494
1495 #[must_use]
1502 pub fn star_impedances(&self) -> [(f64, f64); 3] {
1503 let [z12, z23, z31] = self.z;
1504 let half = |a: f64, b: f64, c: f64| (a + b - c) / 2.0;
1505 [
1506 (half(z12.r, z31.r, z23.r), half(z12.x, z31.x, z23.x)),
1507 (half(z12.r, z23.r, z31.r), half(z12.x, z23.x, z31.x)),
1508 (half(z23.r, z31.r, z12.r), half(z23.x, z31.x, z12.x)),
1509 ]
1510 }
1511
1512 #[must_use]
1519 pub fn star_expansion(&self, star_id: BusId) -> (Bus, [Branch; 3]) {
1520 let star = Bus {
1521 id: star_id,
1522 kind: BusType::Pq,
1523 vm: self.star_vm,
1524 va: self.star_va,
1525 base_kv: self.windings[0].nominal_kv,
1526 vmax: 1.1,
1527 vmin: 0.9,
1528 evhi: None,
1529 evlo: None,
1530 area: 0,
1531 zone: 0,
1532 name: self.name.clone(),
1533 uid: self.uid.clone(),
1534 location: None,
1535 extras: Extras::new(),
1536 };
1537 let zs = self.star_impedances();
1538 let branch = |w: &Winding, (r, x): (f64, f64)| Branch {
1539 from: w.bus,
1540 to: star_id,
1541 r,
1542 x,
1543 b: 0.0,
1544 charging: None,
1545 rate_a: w.rate_a,
1546 rate_b: w.rate_b,
1547 rate_c: w.rate_c,
1548 rating_sets: Vec::new(),
1549 current_ratings: None,
1550 tap: w.tap,
1551 shift: w.shift,
1552 in_service: self.in_service,
1553 angmin: -360.0,
1554 angmax: 360.0,
1555 control: None,
1556 solution: None,
1557 uid: None,
1558 route: None,
1559 extras: Extras::new(),
1560 };
1561 let branches = [
1562 branch(&self.windings[0], zs[0]),
1563 branch(&self.windings[1], zs[1]),
1564 branch(&self.windings[2], zs[2]),
1565 ];
1566 (star, branches)
1567 }
1568}
1569
1570pub(crate) const GEN_EXTRA_KEYS: [&str; 11] = [
1573 "pc1", "pc2", "qc1min", "qc1max", "qc2min", "qc2max", "ramp_agc", "ramp_10", "ramp_30",
1574 "ramp_q", "apf",
1575];
1576
1577#[derive(Debug, Clone, PartialEq)]
1584#[non_exhaustive]
1585pub struct Diagnostic {
1586 pub element: String,
1588 pub field: &'static str,
1589 pub old: f64,
1590 pub new: f64,
1591 pub reason: &'static str,
1592}
1593
1594fn repair_vm(vm: f64) -> Option<f64> {
1598 (!vm.is_finite() || vm <= 0.0 || vm > 2.0).then_some(1.0)
1599}
1600
1601fn repair_va(va: f64) -> Option<f64> {
1603 (!va.is_finite() || va.abs() > 2000.0).then_some(0.0)
1604}
1605
1606fn repair_mbase(mbase: f64, sbase: f64) -> Option<f64> {
1608 (!mbase.is_finite() || mbase <= 0.0).then_some(sbase)
1609}
1610
1611fn repair_vg(vg: f64) -> Option<f64> {
1613 (!vg.is_finite() || vg <= 0.0).then_some(1.0)
1614}
1615
1616#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1621pub(crate) struct LoweredLengths {
1622 pub(crate) buses: usize,
1623 pub(crate) branches: usize,
1624 pub(crate) shunts: usize,
1625}
1626
1627impl BalancedNetwork {
1628 #[must_use]
1629 pub fn new(name: impl Into<String>, base_mva: f64) -> BalancedNetwork {
1630 BalancedNetwork {
1631 name: name.into(),
1632 base_mva,
1633 base_frequency: DEFAULT_BASE_FREQUENCY,
1634 geo: None,
1635 buses: Vec::new(),
1636 loads: Vec::new(),
1637 shunts: Vec::new(),
1638 branches: Vec::new(),
1639 switches: Vec::new(),
1640 generators: Vec::new(),
1641 storage: Vec::new(),
1642 hvdc: Vec::new(),
1643 transformers_3w: Vec::new(),
1644 areas: Vec::new(),
1645 solver: None,
1646 source_format: SourceFormat::InMemory,
1647 source: None,
1648 }
1649 }
1650
1651 #[must_use]
1657 pub fn in_memory(
1658 name: impl Into<String>,
1659 base_mva: f64,
1660 buses: Vec<Bus>,
1661 branches: Vec<Branch>,
1662 ) -> BalancedNetwork {
1663 let mut net = Self::new(name, base_mva);
1664 net.buses = buses;
1665 net.branches = branches;
1666 net
1667 }
1668
1669 pub fn to_json(&self) -> crate::Result<String> {
1686 serde_json::to_string(self).map_err(|e| Error::FormatRead {
1687 format: "JSON",
1688 message: e.to_string(),
1689 })
1690 }
1691
1692 #[allow(clippy::too_many_lines)]
1704 pub(crate) fn non_finite_fields(&self) -> Vec<String> {
1705 fn bad<'a>(
1706 fields: impl IntoIterator<Item = (&'a str, f64)>,
1707 ) -> impl Iterator<Item = &'a str> {
1708 fields
1709 .into_iter()
1710 .filter_map(|(name, v)| (!v.is_finite()).then_some(name))
1711 }
1712 let mut out = Vec::new();
1713 if !self.base_mva.is_finite() {
1714 out.push("base_mva".into());
1715 }
1716 if !self.base_frequency.is_finite() {
1717 out.push("base_frequency".into());
1718 }
1719 for (i, b) in self.buses.iter().enumerate() {
1720 #[rustfmt::skip]
1721 let Bus { id: _, kind: _, vm, va, base_kv, vmax, vmin, evhi: _, evlo: _, area: _, zone: _, name: _, uid: _, location, extras: _ } = b;
1722 let fields = [
1723 ("vm", *vm),
1724 ("va", *va),
1725 ("base_kv", *base_kv),
1726 ("vmax", *vmax),
1727 ("vmin", *vmin),
1728 ];
1729 out.extend(bad(fields).map(|f| format!("buses[{i}].{f}")));
1730 if let Some(location) = location {
1731 let fields = [("location.x", location.x), ("location.y", location.y)];
1732 out.extend(bad(fields).map(|f| format!("buses[{i}].{f}")));
1733 }
1734 }
1735 for (i, l) in self.loads.iter().enumerate() {
1736 let Load {
1737 bus: _,
1738 p,
1739 q,
1740 voltage_model,
1741 in_service: _,
1742 uid: _,
1743 extras: _,
1744 } = l;
1745 out.extend(bad([("p", *p), ("q", *q)]).map(|f| format!("loads[{i}].{f}")));
1746 if let Some(model) = voltage_model {
1747 match model {
1748 LoadVoltageModel::ConstantPower => {}
1749 LoadVoltageModel::Zip {
1750 p_constant_power,
1751 q_constant_power,
1752 p_constant_current,
1753 q_constant_current,
1754 p_constant_impedance,
1755 q_constant_impedance,
1756 v_nom,
1757 load_type: _,
1758 scaling,
1759 } => {
1760 let fields = [
1761 ("p_constant_power", *p_constant_power),
1762 ("q_constant_power", *q_constant_power),
1763 ("p_constant_current", *p_constant_current),
1764 ("q_constant_current", *q_constant_current),
1765 ("p_constant_impedance", *p_constant_impedance),
1766 ("q_constant_impedance", *q_constant_impedance),
1767 ];
1768 out.extend(bad(fields).map(|f| format!("loads[{i}].voltage_model.{f}")));
1769 if matches!(v_nom, Some(v) if !v.is_finite()) {
1770 out.push(format!("loads[{i}].voltage_model.v_nom"));
1771 }
1772 if matches!(scaling, Some(v) if !v.is_finite()) {
1773 out.push(format!("loads[{i}].voltage_model.scaling"));
1774 }
1775 }
1776 LoadVoltageModel::Exponential {
1777 p,
1778 q,
1779 v_nom,
1780 gamma_p,
1781 gamma_q,
1782 } => {
1783 out.extend(
1784 bad([
1785 ("p", *p),
1786 ("q", *q),
1787 ("gamma_p", *gamma_p),
1788 ("gamma_q", *gamma_q),
1789 ])
1790 .map(|f| format!("loads[{i}].voltage_model.{f}")),
1791 );
1792 if matches!(v_nom, Some(v) if !v.is_finite()) {
1793 out.push(format!("loads[{i}].voltage_model.v_nom"));
1794 }
1795 }
1796 }
1797 }
1798 }
1799 for (i, s) in self.shunts.iter().enumerate() {
1800 let Shunt {
1801 bus: _,
1802 g,
1803 b,
1804 in_service: _,
1805 control: _,
1806 uid: _,
1807 extras: _,
1808 } = s;
1809 out.extend(bad([("g", *g), ("b", *b)]).map(|f| format!("shunts[{i}].{f}")));
1810 }
1811 for (i, br) in self.branches.iter().enumerate() {
1812 #[rustfmt::skip]
1813 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: _, route: _, extras: _ } = br;
1814 let fields = [
1815 ("r", *r),
1816 ("x", *x),
1817 ("b", *b),
1818 ("rate_a", *rate_a),
1819 ("rate_b", *rate_b),
1820 ("rate_c", *rate_c),
1821 ("tap", *tap),
1822 ("shift", *shift),
1823 ("angmin", *angmin),
1824 ("angmax", *angmax),
1825 ];
1826 out.extend(bad(fields).map(|f| format!("branches[{i}].{f}")));
1827 out.extend(
1828 rating_sets
1829 .iter()
1830 .enumerate()
1831 .filter(|(_, r)| !r.rate_mva.is_finite())
1832 .map(|(j, _)| format!("branches[{i}].rating_sets[{j}].rate_mva")),
1833 );
1834 if let Some(charging) = charging {
1835 let BranchCharging {
1836 g_fr,
1837 b_fr,
1838 g_to,
1839 b_to,
1840 } = charging;
1841 let fields = [
1842 ("g_fr", *g_fr),
1843 ("b_fr", *b_fr),
1844 ("g_to", *g_to),
1845 ("b_to", *b_to),
1846 ];
1847 out.extend(bad(fields).map(|f| format!("branches[{i}].charging.{f}")));
1848 }
1849 if let Some(current) = current_ratings {
1850 let BranchCurrentRatings {
1851 c_rating_a,
1852 c_rating_b,
1853 c_rating_c,
1854 } = current;
1855 let fields = [
1856 ("c_rating_a", *c_rating_a),
1857 ("c_rating_b", *c_rating_b),
1858 ("c_rating_c", *c_rating_c),
1859 ];
1860 out.extend(bad(fields).map(|f| format!("branches[{i}].current_ratings.{f}")));
1861 }
1862 if let Some(solution) = solution {
1863 let BranchSolution { pf, qf, pt, qt } = solution;
1864 out.extend(
1865 bad([("pf", *pf), ("qf", *qf), ("pt", *pt), ("qt", *qt)])
1866 .map(|f| format!("branches[{i}].solution.{f}")),
1867 );
1868 }
1869 }
1870 for (i, sw) in self.switches.iter().enumerate() {
1871 let Switch {
1872 from: _,
1873 to: _,
1874 closed: _,
1875 thermal_rating,
1876 current_rating,
1877 pf,
1878 qf,
1879 pt,
1880 qt,
1881 uid: _,
1882 extras: _,
1883 } = sw;
1884 for (field, value) in [
1885 ("thermal_rating", *thermal_rating),
1886 ("current_rating", *current_rating),
1887 ("pf", *pf),
1888 ("qf", *qf),
1889 ("pt", *pt),
1890 ("qt", *qt),
1891 ] {
1892 if matches!(value, Some(v) if !v.is_finite()) {
1893 out.push(format!("switches[{i}].{field}"));
1894 }
1895 }
1896 }
1897 for (i, g) in self.generators.iter().enumerate() {
1898 #[rustfmt::skip]
1899 let Generator { bus: _, pg, qg, pmax, pmin, qmax, qmin, vg, mbase, in_service: _, cost, caps, regulated_bus: _, uid: _ } = g;
1900 let fields = [
1901 ("pg", *pg),
1902 ("qg", *qg),
1903 ("pmax", *pmax),
1904 ("pmin", *pmin),
1905 ("qmax", *qmax),
1906 ("qmin", *qmin),
1907 ("vg", *vg),
1908 ("mbase", *mbase),
1909 ];
1910 out.extend(bad(fields).map(|f| format!("generators[{i}].{f}")));
1911 if let Some(GenCost {
1912 model: _,
1913 startup,
1914 shutdown,
1915 ncost: _,
1916 coeffs,
1917 }) = cost
1918 {
1919 out.extend(
1920 bad([("startup", *startup), ("shutdown", *shutdown)])
1921 .map(|f| format!("generators[{i}].cost.{f}")),
1922 );
1923 if coeffs.iter().any(|c| !c.is_finite()) {
1924 out.push(format!("generators[{i}].cost.coeffs"));
1925 }
1926 }
1927 for (key, slot) in GEN_EXTRA_KEYS.iter().zip(caps.iter()) {
1931 if matches!(slot, Some(v) if !v.is_finite()) {
1932 out.push(format!("generators[{i}].caps.{key}"));
1933 }
1934 }
1935 }
1936 for (i, s) in self.storage.iter().enumerate() {
1937 #[rustfmt::skip]
1938 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;
1939 let fields = [
1940 ("ps", *ps),
1941 ("qs", *qs),
1942 ("energy", *energy),
1943 ("energy_rating", *energy_rating),
1944 ("charge_rating", *charge_rating),
1945 ("discharge_rating", *discharge_rating),
1946 ("charge_efficiency", *charge_efficiency),
1947 ("discharge_efficiency", *discharge_efficiency),
1948 ("thermal_rating", *thermal_rating),
1949 ("qmin", *qmin),
1950 ("qmax", *qmax),
1951 ("r", *r),
1952 ("x", *x),
1953 ("p_loss", *p_loss),
1954 ("q_loss", *q_loss),
1955 ];
1956 out.extend(bad(fields).map(|f| format!("storage[{i}].{f}")));
1957 if matches!(current_rating, Some(v) if !v.is_finite()) {
1958 out.push(format!("storage[{i}].current_rating"));
1959 }
1960 }
1961 for (i, h) in self.hvdc.iter().enumerate() {
1962 #[rustfmt::skip]
1963 let Hvdc { from: _, to: _, in_service: _, pf, pt, qf, qt, vf, vt, pmin, pmax, qminf, qmaxf, qmint, qmaxt, loss0, loss1, cost, uid: _, extras: _ } = h;
1964 let fields = [
1965 ("pf", *pf),
1966 ("pt", *pt),
1967 ("qf", *qf),
1968 ("qt", *qt),
1969 ("vf", *vf),
1970 ("vt", *vt),
1971 ("pmin", *pmin),
1972 ("pmax", *pmax),
1973 ("qminf", *qminf),
1974 ("qmaxf", *qmaxf),
1975 ("qmint", *qmint),
1976 ("qmaxt", *qmaxt),
1977 ("loss0", *loss0),
1978 ("loss1", *loss1),
1979 ];
1980 out.extend(bad(fields).map(|f| format!("hvdc[{i}].{f}")));
1981 if let Some(GenCost {
1982 model: _,
1983 startup,
1984 shutdown,
1985 ncost: _,
1986 coeffs,
1987 }) = cost
1988 {
1989 out.extend(
1990 bad([("startup", *startup), ("shutdown", *shutdown)])
1991 .map(|f| format!("hvdc[{i}].cost.{f}")),
1992 );
1993 if coeffs.iter().any(|c| !c.is_finite()) {
1994 out.push(format!("hvdc[{i}].cost.coeffs"));
1995 }
1996 }
1997 }
1998 out
1999 }
2000
2001 pub fn to_format(&self, format: crate::TargetFormat) -> crate::Result<crate::Conversion> {
2007 crate::write_as(self, format)
2008 }
2009
2010 pub fn to_format_with_options(
2015 &self,
2016 format: crate::TargetFormat,
2017 options: &crate::WriteOptions,
2018 ) -> crate::Result<crate::Conversion> {
2019 crate::write_as_with_options(self, format, options)
2020 }
2021
2022 #[must_use]
2027 pub fn to_matpower(&self) -> String {
2028 crate::write_matpower(self)
2029 }
2030
2031 pub fn from_json(text: &str) -> crate::Result<BalancedNetwork> {
2038 let text = text.trim_start_matches('\u{feff}');
2040 let net: BalancedNetwork = serde_json::from_str(text).map_err(|e| Error::FormatRead {
2041 format: "JSON",
2042 message: e.to_string(),
2043 })?;
2044 net.check_references("JSON")?;
2045 if net.buses.is_empty() {
2046 return Err(Error::FormatRead {
2047 format: "JSON",
2048 message: "case has no buses".into(),
2049 });
2050 }
2051 Ok(net)
2052 }
2053
2054 #[must_use]
2059 pub fn is_normalized(&self) -> bool {
2060 self.source_format == SourceFormat::Normalized
2061 }
2062
2063 pub fn check_base_mva(&self) -> crate::Result<()> {
2069 if self.base_mva.is_finite() && self.base_mva > 0.0 {
2070 Ok(())
2071 } else {
2072 Err(crate::Error::InvalidBaseMva {
2073 base: self.base_mva,
2074 })
2075 }
2076 }
2077
2078 #[must_use]
2090 pub fn validate_values(&self) -> Vec<Diagnostic> {
2091 let mut out = Vec::new();
2092 for b in &self.buses {
2093 if let Some(new) = repair_vm(b.vm) {
2094 out.push(Diagnostic {
2095 element: format!("bus {}", b.id),
2096 field: "vm",
2097 old: b.vm,
2098 new,
2099 reason: "voltage magnitude outside [0, 2] p.u.",
2100 });
2101 }
2102 if let Some(new) = repair_va(b.va) {
2103 out.push(Diagnostic {
2104 element: format!("bus {}", b.id),
2105 field: "va",
2106 old: b.va,
2107 new,
2108 reason: "voltage angle outside ±2000°",
2109 });
2110 }
2111 }
2112 for g in &self.generators {
2113 if let Some(new) = repair_mbase(g.mbase, self.base_mva) {
2114 out.push(Diagnostic {
2115 element: format!("generator at bus {}", g.bus),
2116 field: "mbase",
2117 old: g.mbase,
2118 new,
2119 reason: "non-positive generator MVA base",
2120 });
2121 }
2122 if let Some(new) = repair_vg(g.vg) {
2123 out.push(Diagnostic {
2124 element: format!("generator at bus {}", g.bus),
2125 field: "vg",
2126 old: g.vg,
2127 new,
2128 reason: "non-positive voltage setpoint",
2129 });
2130 }
2131 }
2132 out
2133 }
2134
2135 pub(crate) fn invalidate_source(&mut self) {
2141 self.source = None;
2142 }
2143
2144 pub fn repair(&mut self) -> Vec<Diagnostic> {
2149 let findings = self.validate_values();
2150 let sbase = self.base_mva;
2151 for b in &mut self.buses {
2152 if let Some(new) = repair_vm(b.vm) {
2153 b.vm = new;
2154 }
2155 if let Some(new) = repair_va(b.va) {
2156 b.va = new;
2157 }
2158 }
2159 for g in &mut self.generators {
2160 if let Some(new) = repair_mbase(g.mbase, sbase) {
2161 g.mbase = new;
2162 }
2163 if let Some(new) = repair_vg(g.vg) {
2164 g.vg = new;
2165 }
2166 }
2167 if !findings.is_empty() {
2169 self.invalidate_source();
2170 }
2171 findings
2172 }
2173
2174 pub(crate) fn lowered_lengths(&self) -> LoweredLengths {
2180 let mut lengths = LoweredLengths {
2181 buses: self.buses.len(),
2182 branches: self.branches.len(),
2183 shunts: self.shunts.len(),
2184 };
2185 for t in self.transformers_3w.iter().filter(|t| t.in_service) {
2186 lengths.buses += 1;
2187 lengths.branches += 3;
2188 if t.mag_g != 0.0 || t.mag_b != 0.0 {
2189 lengths.shunts += 1;
2190 }
2191 }
2192 lengths
2193 }
2194
2195 pub(crate) fn expand_transformers_3w(&self) -> std::borrow::Cow<'_, BalancedNetwork> {
2206 if self.transformers_3w.is_empty() {
2207 return std::borrow::Cow::Borrowed(self);
2208 }
2209 let mut net = self.clone();
2210 let scale = if net.is_normalized() {
2215 1.0
2216 } else {
2217 net.base_mva
2218 };
2219 let base_id = net
2224 .buses
2225 .iter()
2226 .map(|b| b.id.0)
2227 .max()
2228 .unwrap_or(0)
2229 .checked_add(1)
2230 .expect("bus id space exhausted for star expansion");
2231 for (k, t) in self
2232 .transformers_3w
2233 .iter()
2234 .filter(|t| t.in_service)
2235 .enumerate()
2236 {
2237 let star_id = BusId(
2238 base_id
2239 .checked_add(k)
2240 .expect("bus id space exhausted for star expansion"),
2241 );
2242 let (star, branches) = t.star_expansion(star_id);
2243 net.buses.push(star);
2244 net.branches.extend(branches);
2245 if t.mag_g != 0.0 || t.mag_b != 0.0 {
2246 net.shunts.push(Shunt {
2247 bus: star_id,
2248 g: t.mag_g * scale,
2249 b: t.mag_b * scale,
2250 in_service: true,
2251 control: None,
2252 uid: None,
2253 extras: Extras::new(),
2254 });
2255 }
2256 }
2257 net.transformers_3w.clear();
2258 std::borrow::Cow::Owned(net)
2259 }
2260
2261 pub fn validate(&self) -> crate::Result<()> {
2267 self.check_references("network")
2268 }
2269
2270 pub(crate) fn check_references(&self, format: &'static str) -> crate::Result<()> {
2275 let mut ids = std::collections::HashSet::with_capacity(self.buses.len());
2280 for b in &self.buses {
2281 if !ids.insert(b.id) {
2282 return Err(Error::FormatRead {
2283 format,
2284 message: format!("duplicate bus id {}", b.id),
2285 });
2286 }
2287 }
2288 let check = |bus: BusId, what: &str| -> crate::Result<()> {
2289 if ids.contains(&bus) {
2290 Ok(())
2291 } else {
2292 Err(Error::FormatRead {
2293 format,
2294 message: format!("{what} references unknown bus {bus}"),
2295 })
2296 }
2297 };
2298 for (i, br) in self.branches.iter().enumerate() {
2300 for bus in [br.from, br.to] {
2301 if !ids.contains(&bus) {
2302 return Err(Error::FormatRead {
2303 format,
2304 message: format!("branch {i} references unknown bus {bus}"),
2305 });
2306 }
2307 }
2308 if let Some(bus) = br.control.as_ref().and_then(|c| c.controlled_bus) {
2309 check(bus, "transformer control")?;
2310 }
2311 }
2312 for (i, sw) in self.switches.iter().enumerate() {
2313 for bus in [sw.from, sw.to] {
2314 if !ids.contains(&bus) {
2315 return Err(Error::FormatRead {
2316 format,
2317 message: format!("switch {i} references unknown bus {bus}"),
2318 });
2319 }
2320 }
2321 }
2322 for l in &self.loads {
2323 check(l.bus, "load")?;
2324 }
2325 for s in &self.shunts {
2326 check(s.bus, "shunt")?;
2327 if let Some(bus) = s.control.as_ref().and_then(|c| c.control_bus) {
2328 check(bus, "switched-shunt control")?;
2329 }
2330 }
2331 for g in &self.generators {
2332 check(g.bus, "generator")?;
2333 if let Some(bus) = g.regulated_bus {
2334 check(bus, "generator voltage control")?;
2335 }
2336 }
2337 for d in &self.hvdc {
2338 check(d.from, "dcline")?;
2339 check(d.to, "dcline")?;
2340 }
2341 for s in &self.storage {
2342 check(s.bus, "storage")?;
2343 }
2344 for a in &self.areas {
2345 if let Some(slack) = a.slack_bus {
2346 check(slack, "area swing")?;
2347 }
2348 }
2349 for t in &self.transformers_3w {
2350 for w in &t.windings {
2351 check(w.bus, "3-winding transformer")?;
2352 }
2353 }
2354 if !self.transformers_3w.is_empty()
2362 && let Some(max_id) = self.buses.iter().map(|b| b.id.0).max()
2363 {
2364 let needed = self
2365 .transformers_3w
2366 .iter()
2367 .filter(|t| t.in_service)
2368 .count()
2369 .max(1);
2370 if max_id.checked_add(needed).is_none() {
2371 return Err(Error::FormatRead {
2372 format,
2373 message: format!(
2374 "bus id {max_id} leaves no room to allocate synthetic star bus ids \
2375 for 3-winding transformers"
2376 ),
2377 });
2378 }
2379 }
2380 Ok(())
2381 }
2382}
2383
2384#[cfg(test)]
2385mod tests {
2386 use super::*;
2387
2388 fn close(actual: f64, expected: f64) {
2389 assert!((actual - expected).abs() < 1e-12, "{actual} != {expected}");
2390 }
2391
2392 #[test]
2393 fn quadratic_with_constant_keeps_c0_across_ncost() {
2394 let full = GenCost::new(2, 0.0, 0.0, vec![1.5, 2.0, 5.0]);
2395 assert_eq!(full.quadratic_with_constant(), Some((3.0, 2.0, 5.0)));
2396 assert_eq!(full.quadratic(), Some((3.0, 2.0)));
2397
2398 let linear = GenCost::new(2, 0.0, 0.0, vec![2.0, 5.0]);
2399 assert_eq!(linear.quadratic_with_constant(), Some((0.0, 2.0, 5.0)));
2400
2401 let constant = GenCost::new(2, 0.0, 0.0, vec![5.0]);
2402 assert_eq!(constant.quadratic_with_constant(), Some((0.0, 0.0, 5.0)));
2403
2404 let piecewise = GenCost::new(1, 0.0, 0.0, vec![0.0, 0.0, 1.0, 1.0]);
2405 assert_eq!(piecewise.quadratic_with_constant(), None);
2406
2407 let cubic = GenCost::new(2, 0.0, 0.0, vec![1.0, 1.0, 1.0, 1.0]);
2408 assert_eq!(cubic.quadratic_with_constant(), None);
2409
2410 let truncated = GenCost::with_ncost(2, 0.0, 0.0, 3, vec![1.0]);
2411 assert_eq!(truncated.quadratic_with_constant(), None);
2412 }
2413
2414 #[test]
2415 fn a_leading_coefficient_below_the_tolerance_comes_off_the_row() {
2416 let artifact = GenCost::new(2, 0.0, 0.0, vec![1e-17, 2.0, 5.0]);
2417 assert_eq!(
2418 artifact.quadratic_with_constant(),
2419 Some((2e-17, 2.0, 5.0)),
2420 "the untouched reader keeps the artifact"
2421 );
2422 assert_eq!(
2423 artifact.quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
2424 Some((0.0, 2.0, 5.0))
2425 );
2426 assert_eq!(
2427 artifact.quadratic_with_constant_tol(0.0),
2428 Some((2e-17, 2.0, 5.0)),
2429 "a zero tolerance strips an exact zero alone"
2430 );
2431
2432 let padded = GenCost::new(2, 0.0, 0.0, vec![0.0, 1.5, 2.0, 5.0]);
2435 assert_eq!(padded.quadratic_with_constant(), None);
2436 assert_eq!(
2437 padded.quadratic_with_constant_tol(0.0),
2438 Some((3.0, 2.0, 5.0))
2439 );
2440
2441 let flat = GenCost::new(2, 0.0, 0.0, vec![1e-17, 1e-17, 1e-17]);
2442 assert_eq!(
2443 flat.quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
2444 Some((0.0, 0.0, 1e-17)),
2445 "the last coefficient stays, whatever its magnitude"
2446 );
2447
2448 let piecewise = GenCost::new(1, 0.0, 0.0, vec![0.0, 0.0, 1.0, 1.0]);
2449 assert_eq!(
2450 piecewise.quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
2451 None
2452 );
2453
2454 let truncated = GenCost::with_ncost(2, 0.0, 0.0, 3, vec![1.0]);
2455 assert_eq!(
2456 truncated.quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
2457 None
2458 );
2459
2460 let quartic = GenCost::new(2, 0.0, 0.0, vec![1.0, 1.0, 1.0, 1.0, 1.0]);
2461 assert_eq!(
2462 quartic.quadratic_with_constant_tol(GenCost::LEADING_COEFF_TOL),
2463 None
2464 );
2465 }
2466
2467 #[test]
2468 fn synthesized_rate_follows_the_angle_window_and_the_voltage_ceilings() {
2469 let br = Branch::new(BusId(1), BusId(2), 0.03, 0.04);
2470 let expected = |window: f64, fr: f64, to: f64| {
2471 let separation = (fr * fr + to * to - 2.0 * fr * to * window.cos()).sqrt();
2472 fr.max(to) * separation / 0.05
2473 };
2474 close(
2475 br.synthesize_rate_a(0.5, 1.1, 1.06),
2476 expected(0.5, 1.1, 1.06),
2477 );
2478
2479 assert!(br.synthesize_rate_a(0.8, 1.1, 1.06) > br.synthesize_rate_a(0.5, 1.1, 1.06));
2481
2482 close(
2484 br.synthesize_rate_a(-0.5, 1.1, 1.06),
2485 expected(0.5, 1.1, 1.06),
2486 );
2487 for window in [6.0, 2.0 * std::f64::consts::PI, -360.0] {
2488 close(
2489 br.synthesize_rate_a(window, 1.1, 1.06),
2490 expected(std::f64::consts::PI, 1.1, 1.06),
2491 );
2492 }
2493
2494 let ideal = Branch::new(BusId(1), BusId(2), 0.0, 0.0);
2495 close(ideal.synthesize_rate_a(0.5, 1.1, 1.1), 0.0);
2496 }
2497
2498 fn bus(id: usize) -> Bus {
2499 Bus {
2500 id: BusId(id),
2501 kind: BusType::Pq,
2502 vm: 1.0,
2503 va: 0.0,
2504 base_kv: 230.0,
2505 vmax: 1.1,
2506 vmin: 0.9,
2507 evhi: None,
2508 evlo: None,
2509 area: 1,
2510 zone: 1,
2511 name: None,
2512 uid: None,
2513 location: None,
2514 extras: Extras::new(),
2515 }
2516 }
2517
2518 fn winding(b: usize) -> Winding {
2519 Winding {
2520 bus: BusId(b),
2521 tap: 1.0,
2522 shift: 0.0,
2523 nominal_kv: 230.0,
2524 rate_a: 100.0,
2525 rate_b: 0.0,
2526 rate_c: 0.0,
2527 }
2528 }
2529
2530 fn transformer_3w() -> Transformer3W {
2531 let z = |r, x| Impedance {
2532 r,
2533 x,
2534 base_mva: 100.0,
2535 };
2536 Transformer3W {
2537 windings: [winding(1), winding(2), winding(3)],
2538 z: [z(0.01, 0.10), z(0.02, 0.20), z(0.03, 0.30)],
2539 star_vm: 0.98,
2540 star_va: -1.5,
2541 mag_g: 0.0,
2542 mag_b: 0.0,
2543 in_service: true,
2544 name: Some("T1".into()),
2545 uid: None,
2546 extras: Extras::new(),
2547 }
2548 }
2549
2550 #[test]
2551 fn star_impedances_split_the_pairwise_values() {
2552 let [(r1, x1), (r2, x2), (r3, x3)] = transformer_3w().star_impedances();
2554 close(r1, 0.01);
2555 close(x1, 0.10);
2556 close(r2, 0.0);
2557 close(x2, 0.0);
2558 close(r3, 0.02);
2559 close(x3, 0.20);
2560 }
2561
2562 #[test]
2563 fn star_expansion_builds_a_star_bus_and_three_branches() {
2564 let t = transformer_3w();
2565 let (star, branches) = t.star_expansion(BusId(99));
2566
2567 assert_eq!(star.id, BusId(99));
2568 close(star.vm, 0.98);
2569 close(star.va, -1.5);
2570 for (i, br) in branches.iter().enumerate() {
2573 assert_eq!(br.from, t.windings[i].bus);
2574 assert_eq!(br.to, BusId(99));
2575 close(br.tap, 1.0);
2576 close(br.rate_a, 100.0);
2577 }
2578 close(branches[2].r, 0.02);
2579 close(branches[2].x, 0.20);
2580 }
2581
2582 #[test]
2583 fn three_winding_transformer_survives_json_transport() {
2584 let mut net =
2585 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
2586 net.transformers_3w.push(transformer_3w());
2587 net.validate().unwrap();
2588
2589 let back = BalancedNetwork::from_json(&net.to_json().unwrap()).unwrap();
2590 assert_eq!(back.transformers_3w.len(), 1);
2591 close(back.transformers_3w[0].z[1].x, 0.20);
2592 assert_eq!(back.transformers_3w[0].windings[2].bus, BusId(3));
2593 }
2594
2595 #[test]
2596 fn lowered_lengths_match_the_expansion() {
2597 let mut magnetizing = transformer_3w();
2602 magnetizing.mag_b = 0.02;
2603 let mut out_of_service = transformer_3w();
2604 out_of_service.in_service = false;
2605 out_of_service.mag_g = 0.01;
2606
2607 for units in [
2608 vec![],
2609 vec![transformer_3w()],
2610 vec![magnetizing.clone()],
2611 vec![out_of_service.clone()],
2612 vec![transformer_3w(), magnetizing, out_of_service],
2613 ] {
2614 let mut net =
2615 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
2616 net.shunts.push(Shunt::new(BusId(1), 0.0, 0.5));
2617 net.transformers_3w = units;
2618
2619 let counted = net.lowered_lengths();
2620 let built = net.expand_transformers_3w();
2621 assert_eq!(counted.buses, built.buses.len());
2622 assert_eq!(counted.branches, built.branches.len());
2623 assert_eq!(counted.shunts, built.shunts.len());
2624 }
2625 }
2626
2627 #[test]
2628 fn check_references_rejects_bus_ids_without_star_expansion_headroom() {
2629 let mut net = BalancedNetwork::in_memory(
2633 "t",
2634 100.0,
2635 vec![bus(1), bus(2), bus(3), bus(usize::MAX)],
2636 Vec::new(),
2637 );
2638 net.transformers_3w.push(transformer_3w());
2639 let err = net.validate().unwrap_err().to_string();
2640 assert!(
2641 err.contains("no room to allocate synthetic star bus ids"),
2642 "got {err}"
2643 );
2644 }
2645
2646 #[test]
2647 fn star_expansion_headroom_counts_only_in_service_transformers() {
2648 let mut net = BalancedNetwork::in_memory(
2654 "t",
2655 100.0,
2656 vec![bus(1), bus(2), bus(3), bus(usize::MAX - 1)],
2657 Vec::new(),
2658 );
2659 net.transformers_3w.push(transformer_3w());
2660 let mut out_of_service = transformer_3w();
2661 out_of_service.in_service = false;
2662 net.transformers_3w.push(out_of_service);
2663 net.validate()
2664 .expect("in-service count fits; must not be rejected");
2665 }
2666
2667 #[test]
2668 fn check_references_rejects_a_dangling_winding_bus() {
2669 let mut net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
2670 net.transformers_3w.push(transformer_3w()); let err = net.validate().unwrap_err().to_string();
2672 assert!(
2673 err.contains("3-winding transformer references unknown bus 3"),
2674 "got {err}"
2675 );
2676 }
2677
2678 fn regulating_branch(reg: usize) -> Branch {
2680 Branch {
2681 from: BusId(1),
2682 to: BusId(2),
2683 r: 0.0,
2684 x: 0.1,
2685 b: 0.0,
2686 charging: None,
2687 rate_a: 0.0,
2688 rate_b: 0.0,
2689 rate_c: 0.0,
2690 rating_sets: Vec::new(),
2691 current_ratings: None,
2692 tap: 1.0,
2693 shift: 0.0,
2694 in_service: true,
2695 angmin: -360.0,
2696 angmax: 360.0,
2697 control: Some(TransformerControl {
2698 mode: TransformerControlMode::Voltage,
2699 controlled_bus: Some(BusId(reg)),
2700 tap_min: 0.95,
2701 tap_max: 1.05,
2702 band_min: 1.0,
2703 band_max: 1.02,
2704 ntp: 17,
2705 mva_base: 100.0,
2706 }),
2707 solution: None,
2708 uid: None,
2709 route: None,
2710 extras: Extras::new(),
2711 }
2712 }
2713
2714 #[test]
2715 fn transformer_control_survives_json_transport() {
2716 let mut net =
2717 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
2718 net.branches.push(regulating_branch(3));
2719 net.validate().unwrap();
2720
2721 let back = BalancedNetwork::from_json(&net.to_json().unwrap()).unwrap();
2722 let c = back.branches[0].control.as_ref().unwrap();
2723 assert_eq!(c.mode, TransformerControlMode::Voltage);
2724 assert_eq!(c.controlled_bus, Some(BusId(3)));
2725 close(c.tap_max, 1.05);
2726 assert_eq!(c.ntp, 17);
2727 }
2728
2729 #[test]
2730 fn gen_caps_serialize_as_a_named_map_that_grows_additively() {
2731 let mut caps: GenCaps = [None; GEN_EXTRA_KEYS.len()];
2732 caps[8] = Some(1.5); caps[10] = Some(0.5); let g = Generator {
2735 bus: BusId(1),
2736 pg: 10.0,
2737 qg: 0.0,
2738 pmax: 100.0,
2739 pmin: 0.0,
2740 qmax: 50.0,
2741 qmin: -50.0,
2742 vg: 1.0,
2743 mbase: 100.0,
2744 in_service: true,
2745 cost: None,
2746 caps,
2747 regulated_bus: None,
2748 uid: None,
2749 };
2750
2751 let json = serde_json::to_string(&g).unwrap();
2754 assert!(json.contains(r#""caps":{"#), "caps is an object: {json}");
2755 assert!(json.contains(r#""ramp_30":1.5"#) && json.contains(r#""apf":0.5"#));
2756 let back: Generator = serde_json::from_str(&json).unwrap();
2757 assert_eq!(back.caps, g.caps);
2758
2759 let with_future = r#"{"bus":1,"pg":10,"qg":0,"pmax":100,"pmin":0,"qmax":50,"qmin":-50,
2762 "vg":1,"mbase":100,"in_service":true,"cost":null,
2763 "caps":{"ramp_30":1.5,"future_ramp":9.9}}"#;
2764 let g2: Generator = serde_json::from_str(with_future).unwrap();
2765 assert_eq!(g2.caps[8], Some(1.5));
2766 assert_eq!(g2.caps.iter().filter(|v| v.is_some()).count(), 1);
2767 let no_caps = r#"{"bus":1,"pg":10,"qg":0,"pmax":100,"pmin":0,"qmax":50,"qmin":-50,
2768 "vg":1,"mbase":100,"in_service":true,"cost":null}"#;
2769 let g3: Generator = serde_json::from_str(no_caps).unwrap();
2770 assert!(!g3.has_caps());
2771
2772 let null_caps = r#"{"bus":1,"pg":10,"qg":0,"pmax":100,"pmin":0,"qmax":50,"qmin":-50,
2774 "vg":1,"mbase":100,"in_service":true,"cost":null,"caps":null}"#;
2775 let g4: Generator = serde_json::from_str(null_caps).unwrap();
2776 assert!(!g4.has_caps());
2777 }
2778
2779 #[test]
2780 fn non_finite_fields_lists_every_offender_not_just_the_first() {
2781 let bus = |id, vm| Bus {
2782 id: BusId(id),
2783 kind: BusType::Pq,
2784 vm,
2785 va: 0.0,
2786 base_kv: 230.0,
2787 vmax: 1.1,
2788 vmin: 0.9,
2789 evhi: None,
2790 evlo: None,
2791 area: 1,
2792 zone: 1,
2793 name: None,
2794 uid: None,
2795 location: None,
2796 extras: Extras::new(),
2797 };
2798 let branch = Branch {
2799 from: BusId(1),
2800 to: BusId(2),
2801 r: 0.0,
2802 x: f64::INFINITY,
2803 b: 0.0,
2804 charging: None,
2805 rate_a: 0.0,
2806 rate_b: 0.0,
2807 rate_c: 0.0,
2808 rating_sets: Vec::new(),
2809 current_ratings: None,
2810 tap: 0.0,
2811 shift: 0.0,
2812 in_service: true,
2813 angmin: -360.0,
2814 angmax: 360.0,
2815 control: None,
2816 solution: None,
2817 uid: None,
2818 route: None,
2819 extras: Extras::new(),
2820 };
2821 let mut g = Generator {
2824 bus: BusId(1),
2825 pg: 0.0,
2826 qg: 0.0,
2827 pmax: 0.0,
2828 pmin: 0.0,
2829 qmax: 0.0,
2830 qmin: 0.0,
2831 vg: 1.0,
2832 mbase: 100.0,
2833 in_service: true,
2834 cost: None,
2835 caps: GenCaps::default(),
2836 regulated_bus: None,
2837 uid: None,
2838 };
2839 g.caps[8] = Some(f64::INFINITY); let mut net = BalancedNetwork::in_memory(
2843 "nf",
2844 100.0,
2845 vec![bus(1, f64::NAN), bus(2, 1.0)],
2846 vec![branch],
2847 );
2848 net.generators.push(g);
2849 let fields = net.non_finite_fields();
2850 assert!(fields.contains(&"buses[0].vm".to_string()), "{fields:?}");
2851 assert!(fields.contains(&"branches[0].x".to_string()), "{fields:?}");
2852 assert!(
2853 fields.contains(&"generators[0].caps.ramp_30".to_string()),
2854 "caps reported at key precision: {fields:?}"
2855 );
2856 assert_eq!(
2857 fields.len(),
2858 3,
2859 "exactly the three offenders, no more: {fields:?}"
2860 );
2861 }
2862
2863 #[test]
2864 fn check_references_rejects_a_dangling_controlled_bus() {
2865 let mut net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
2866 net.branches.push(regulating_branch(9)); let err = net.validate().unwrap_err().to_string();
2868 assert!(
2869 err.contains("transformer control references unknown bus 9"),
2870 "got {err}"
2871 );
2872 }
2873
2874 fn switched_shunt(reg: usize) -> Shunt {
2876 Shunt {
2877 bus: BusId(1),
2878 g: 0.0,
2879 b: 19.0,
2880 in_service: true,
2881 control: Some(SwitchedShuntControl {
2882 mode: SwitchedShuntMode::Discrete,
2883 vhigh: 1.05,
2884 vlow: 0.95,
2885 control_bus: Some(BusId(reg)),
2886 rmpct: 100.0,
2887 blocks: vec![
2888 ShuntBlock { steps: 2, b: 25.0 },
2889 ShuntBlock { steps: 1, b: 50.0 },
2890 ],
2891 }),
2892 uid: None,
2893 extras: Extras::new(),
2894 }
2895 }
2896
2897 #[test]
2898 fn switched_shunt_control_survives_json_transport() {
2899 let mut net =
2900 BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
2901 net.shunts.push(switched_shunt(3));
2902 net.validate().unwrap();
2903
2904 let back = BalancedNetwork::from_json(&net.to_json().unwrap()).unwrap();
2905 let c = back.shunts[0].control.as_ref().unwrap();
2906 assert_eq!(c.mode, SwitchedShuntMode::Discrete);
2907 assert_eq!(c.control_bus, Some(BusId(3)));
2908 assert_eq!(c.blocks.len(), 2);
2909 close(c.blocks[1].b, 50.0);
2910 }
2911
2912 #[test]
2913 fn check_references_rejects_a_dangling_switched_shunt_control_bus() {
2914 let mut net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
2915 net.shunts.push(switched_shunt(9)); let err = net.validate().unwrap_err().to_string();
2917 assert!(
2918 err.contains("switched-shunt control references unknown bus 9"),
2919 "got {err}"
2920 );
2921 }
2922
2923 #[test]
2924 fn validate_values_flags_and_repair_clamps_out_of_domain_values() {
2925 let mut net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
2926 net.buses[0].vm = 0.0; net.buses[1].va = 9000.0; net.generators.push(Generator {
2929 bus: BusId(1),
2930 pg: 10.0,
2931 qg: 0.0,
2932 pmax: 100.0,
2933 pmin: 0.0,
2934 qmax: 50.0,
2935 qmin: -50.0,
2936 vg: 0.0, mbase: 0.0, in_service: true,
2939 cost: None,
2940 caps: Default::default(),
2941 regulated_bus: None,
2942 uid: None,
2943 });
2944
2945 let diags = net.validate_values();
2946 let fields: std::collections::BTreeSet<_> = diags.iter().map(|d| d.field).collect();
2947 assert_eq!(
2948 fields,
2949 ["mbase", "va", "vg", "vm"].into_iter().collect(),
2950 "all four out-of-domain fields reported"
2951 );
2952 close(net.buses[0].vm, 0.0);
2954
2955 let applied = net.repair();
2956 assert_eq!(applied.len(), diags.len());
2957 close(net.buses[0].vm, 1.0);
2958 close(net.buses[1].va, 0.0);
2959 close(net.generators[0].mbase, 100.0); close(net.generators[0].vg, 1.0);
2961 assert!(net.validate_values().is_empty());
2963 }
2964
2965 #[test]
2966 fn validate_values_is_empty_for_a_clean_network() {
2967 let net = BalancedNetwork::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
2968 assert!(net.validate_values().is_empty());
2969 }
2970}