1use std::collections::BTreeMap;
17
18use serde::{Deserialize, Serialize};
19
20use crate::geo::{DistGeoMeta, DistLocation};
21
22pub type Extras = BTreeMap<String, serde_json::Value>;
23
24pub type ConductorMatrix = Vec<Vec<f64>>;
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
29#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
30#[serde(rename_all = "kebab-case")]
31#[non_exhaustive]
32pub enum DistSourceFormat {
33 Dss,
34 BmopfJson,
35 PmdJson,
36}
37
38impl DistSourceFormat {
39 pub fn name(self) -> &'static str {
42 match self {
43 DistSourceFormat::Dss => "dss",
44 DistSourceFormat::PmdJson => "pmd-json",
45 DistSourceFormat::BmopfJson => "bmopf-json",
46 }
47 }
48}
49
50#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
51#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
52#[non_exhaustive]
53pub struct DistBus {
54 pub id: String,
55 pub terminals: Vec<String>,
57 pub grounded: Vec<String>,
59 pub v_min: Option<f64>,
65 pub v_max: Option<f64>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub v_min_phase: Option<Vec<f64>>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub v_max_phase: Option<Vec<f64>>,
72 pub vpn_min: Option<Vec<f64>>,
73 pub vpn_max: Option<Vec<f64>>,
74 pub vpp_min: Option<Vec<f64>>,
75 pub vpp_max: Option<Vec<f64>>,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub vpos_min: Option<f64>,
78 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub vpos_max: Option<f64>,
80 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub vneg_max: Option<f64>,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub vzero_max: Option<f64>,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub vn_max: Option<f64>,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub location: Option<DistLocation>,
89 pub extras: Extras,
90}
91
92impl DistBus {
93 #[must_use]
95 pub fn phase_indices(&self, conventions: Option<&serde_json::Value>) -> Vec<usize> {
96 let numeric_four = self.terminals.len() == 4
97 && ["1", "2", "3", "4"]
98 .iter()
99 .all(|t| self.terminals.iter().any(|v| v == t));
100 self.terminals
101 .iter()
102 .enumerate()
103 .filter_map(|(index, terminal)| {
104 let nonphase = if let Some(roles) = conventions {
105 ["neutral", "earth"].iter().any(|role| {
106 roles
107 .get(role)
108 .and_then(serde_json::Value::as_array)
109 .is_some_and(|names| {
110 names
111 .iter()
112 .any(|name| name.as_str() == Some(terminal.as_str()))
113 })
114 })
115 } else {
116 terminal.eq_ignore_ascii_case("n") || (numeric_four && terminal == "4")
117 };
118 (!nonphase).then_some(index)
119 })
120 .collect()
121 }
122
123 #[must_use]
124 pub fn new(id: impl Into<String>, terminals: Vec<String>) -> Self {
125 Self {
126 id: id.into(),
127 terminals,
128 grounded: Vec::new(),
129 v_min: None,
130 v_max: None,
131 v_min_phase: None,
132 v_max_phase: None,
133 vpn_min: None,
134 vpn_max: None,
135 vpp_min: None,
136 vpp_max: None,
137 vpos_min: None,
138 vpos_max: None,
139 vneg_max: None,
140 vzero_max: None,
141 vn_max: None,
142 location: None,
143 extras: Extras::new(),
144 }
145 }
146}
147
148#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
149#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
150#[non_exhaustive]
151pub struct DistLineCode {
152 pub name: String,
153 pub n_conductors: usize,
154 pub r_series: ConductorMatrix,
156 pub x_series: ConductorMatrix,
157 pub g_from: ConductorMatrix,
159 pub b_from: ConductorMatrix,
160 pub g_to: ConductorMatrix,
161 pub b_to: ConductorMatrix,
162 #[serde(default)]
164 pub i_max: Option<Vec<f64>>,
165 #[serde(default)]
166 pub s_max: Option<Vec<f64>>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub source: Option<String>,
171 pub extras: Extras,
172}
173
174impl DistLineCode {
175 #[must_use]
176 pub fn new(
177 name: impl Into<String>,
178 r_series: ConductorMatrix,
179 x_series: ConductorMatrix,
180 ) -> Self {
181 let n_conductors = matrix_extent(&r_series).max(matrix_extent(&x_series));
182 Self {
183 name: name.into(),
184 n_conductors,
185 r_series,
186 x_series,
187 g_from: zero_mat(n_conductors),
188 b_from: zero_mat(n_conductors),
189 g_to: zero_mat(n_conductors),
190 b_to: zero_mat(n_conductors),
191 i_max: None,
192 s_max: None,
193 source: None,
194 extras: Extras::new(),
195 }
196 }
197}
198
199#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
200#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
201#[non_exhaustive]
202pub struct DistLine {
203 pub name: String,
204 pub bus_from: String,
205 pub bus_to: String,
206 pub terminal_map_from: Vec<String>,
207 pub terminal_map_to: Vec<String>,
208 pub linecode: String,
209 pub length: f64,
211 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub route: Option<Vec<DistLocation>>,
217 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub i_max: Option<Vec<f64>>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub s_max: Option<Vec<f64>>,
223 pub extras: Extras,
224}
225
226impl DistLine {
227 #[must_use]
228 pub fn new(
229 name: impl Into<String>,
230 bus_from: impl Into<String>,
231 bus_to: impl Into<String>,
232 terminal_map_from: Vec<String>,
233 terminal_map_to: Vec<String>,
234 linecode: impl Into<String>,
235 length: f64,
236 ) -> Self {
237 Self {
238 name: name.into(),
239 bus_from: bus_from.into(),
240 bus_to: bus_to.into(),
241 terminal_map_from,
242 terminal_map_to,
243 linecode: linecode.into(),
244 length,
245 route: None,
246 i_max: None,
247 s_max: None,
248 extras: Extras::new(),
249 }
250 }
251}
252
253#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
259#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
260#[non_exhaustive]
261pub struct DistCapacitor {
262 pub name: String,
263 pub bus: String,
264 pub terminal_map: Vec<String>,
265 pub configuration: Configuration,
266 pub q_rated: f64,
268 pub v_nom: f64,
271 pub extras: Extras,
272}
273
274impl DistCapacitor {
275 #[must_use]
276 pub fn new(
277 name: impl Into<String>,
278 bus: impl Into<String>,
279 terminal_map: Vec<String>,
280 configuration: Configuration,
281 q_rated: f64,
282 v_nom: f64,
283 ) -> Self {
284 Self {
285 name: name.into(),
286 bus: bus.into(),
287 terminal_map,
288 configuration,
289 q_rated,
290 v_nom,
291 extras: Extras::new(),
292 }
293 }
294}
295
296#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
297#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
298#[non_exhaustive]
299pub struct DistSwitch {
300 pub name: String,
301 pub bus_from: String,
302 pub bus_to: String,
303 pub terminal_map_from: Vec<String>,
304 pub terminal_map_to: Vec<String>,
305 pub open: bool,
306 #[serde(default)]
308 pub i_max: Option<Vec<f64>>,
309 pub extras: Extras,
310}
311
312impl DistSwitch {
313 #[must_use]
314 pub fn new(
315 name: impl Into<String>,
316 bus_from: impl Into<String>,
317 bus_to: impl Into<String>,
318 terminal_map_from: Vec<String>,
319 terminal_map_to: Vec<String>,
320 open: bool,
321 ) -> Self {
322 Self {
323 name: name.into(),
324 bus_from: bus_from.into(),
325 bus_to: bus_to.into(),
326 terminal_map_from,
327 terminal_map_to,
328 open,
329 i_max: None,
330 extras: Extras::new(),
331 }
332 }
333}
334
335#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
336#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
337#[serde(rename_all = "snake_case")]
338#[non_exhaustive]
339pub enum Configuration {
340 Wye,
341 Delta,
342 SinglePhase,
343}
344
345#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
346#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
347#[non_exhaustive]
348pub struct DistLoad {
349 pub name: String,
350 pub bus: String,
351 pub terminal_map: Vec<String>,
352 pub configuration: Configuration,
353 pub p_nom: Vec<f64>,
355 pub q_nom: Vec<f64>,
357 pub voltage_model: DistLoadVoltageModel,
358 pub extras: Extras,
359}
360
361impl DistLoad {
362 #[must_use]
363 pub fn new(
364 name: impl Into<String>,
365 bus: impl Into<String>,
366 terminal_map: Vec<String>,
367 configuration: Configuration,
368 p_nom: Vec<f64>,
369 q_nom: Vec<f64>,
370 ) -> Self {
371 Self {
372 name: name.into(),
373 bus: bus.into(),
374 terminal_map,
375 configuration,
376 p_nom,
377 q_nom,
378 voltage_model: DistLoadVoltageModel::default(),
379 extras: Extras::new(),
380 }
381 }
382}
383
384#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
385#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
386#[serde(tag = "model", rename_all = "snake_case")]
387#[non_exhaustive]
388pub enum DistLoadVoltageModel {
389 ConstantPower { v_nom: Vec<f64> },
392 ConstantCurrent { v_nom: Vec<f64> },
394 ConstantImpedance { v_nom: Vec<f64> },
396 Zip {
400 v_nom: Vec<f64>,
401 alpha_z: Vec<f64>,
402 alpha_i: Vec<f64>,
403 alpha_p: Vec<f64>,
404 beta_z: Vec<f64>,
405 beta_i: Vec<f64>,
406 beta_p: Vec<f64>,
407 },
408 Exponential {
411 v_nom: Vec<f64>,
412 gamma_p: Vec<f64>,
413 gamma_q: Vec<f64>,
414 },
415}
416
417impl Default for DistLoadVoltageModel {
418 fn default() -> Self {
419 Self::ConstantPower { v_nom: Vec::new() }
420 }
421}
422
423impl DistLoadVoltageModel {
424 #[must_use]
425 pub fn v_nom(&self) -> &[f64] {
426 match self {
427 Self::ConstantPower { v_nom }
428 | Self::ConstantCurrent { v_nom }
429 | Self::ConstantImpedance { v_nom }
430 | Self::Zip { v_nom, .. }
431 | Self::Exponential { v_nom, .. } => v_nom,
432 }
433 }
434}
435
436#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
437#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
438#[non_exhaustive]
439pub struct DistGenerator {
440 pub name: String,
441 pub bus: String,
442 pub terminal_map: Vec<String>,
443 pub configuration: Configuration,
444 pub p_nom: Vec<f64>,
446 pub q_nom: Vec<f64>,
447 #[serde(default)]
449 pub p_min: Option<Vec<f64>>,
450 #[serde(default)]
451 pub p_max: Option<Vec<f64>>,
452 #[serde(default)]
453 pub q_min: Option<Vec<f64>>,
454 #[serde(default)]
455 pub q_max: Option<Vec<f64>>,
456 pub cost: Option<Vec<f64>>,
461 #[serde(default, skip_serializing_if = "Option::is_none")]
464 pub s_max: Option<Vec<f64>>,
465 #[serde(default, skip_serializing_if = "Option::is_none")]
466 pub i_max: Option<Vec<f64>>,
467 pub extras: Extras,
468}
469
470impl DistGenerator {
471 #[must_use]
472 pub fn new(
473 name: impl Into<String>,
474 bus: impl Into<String>,
475 terminal_map: Vec<String>,
476 configuration: Configuration,
477 p_nom: Vec<f64>,
478 q_nom: Vec<f64>,
479 ) -> Self {
480 Self {
481 name: name.into(),
482 bus: bus.into(),
483 terminal_map,
484 configuration,
485 p_nom,
486 q_nom,
487 p_min: None,
488 p_max: None,
489 q_min: None,
490 q_max: None,
491 cost: None,
492 s_max: None,
493 i_max: None,
494 extras: Extras::new(),
495 }
496 }
497}
498
499#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
500#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
501#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
502#[non_exhaustive]
503pub enum IbrTopology {
504 SinglePhase,
505 ThreeLeg,
506 FourLeg,
507}
508
509#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
510#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
511#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
512#[non_exhaustive]
513pub enum IbrPrimeMover {
514 Pv,
515 Battery,
516 Generic,
517 Statcom,
518 Dstatcom,
519}
520
521#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
522#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
523#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
524#[non_exhaustive]
525pub enum IbrVoltageAggregation {
526 PerPhase,
527 Average,
528}
529
530#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
531#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
532#[non_exhaustive]
533pub struct DistIbr {
534 pub name: String,
535 pub bus: String,
536 pub terminal_map: Vec<String>,
537 pub topology: IbrTopology,
538 pub prime_mover: IbrPrimeMover,
539 pub s_max: Vec<f64>,
541 #[serde(default)]
543 pub i_max: Option<Vec<f64>>,
544 pub p_avail: Option<f64>,
546 #[serde(default)]
547 pub p_min: Option<Vec<f64>>,
548 #[serde(default)]
549 pub p_max: Option<Vec<f64>>,
550 #[serde(default)]
551 pub q_min: Option<Vec<f64>>,
552 #[serde(default)]
553 pub q_max: Option<Vec<f64>>,
554 pub control_profile: Option<String>,
555 pub voltage_aggregation: Option<IbrVoltageAggregation>,
556 pub extras: Extras,
557}
558
559impl DistIbr {
560 #[must_use]
561 pub fn new(
562 name: impl Into<String>,
563 bus: impl Into<String>,
564 terminal_map: Vec<String>,
565 topology: IbrTopology,
566 prime_mover: IbrPrimeMover,
567 s_max: Vec<f64>,
568 ) -> Self {
569 Self {
570 name: name.into(),
571 bus: bus.into(),
572 terminal_map,
573 topology,
574 prime_mover,
575 s_max,
576 i_max: None,
577 p_avail: None,
578 p_min: None,
579 p_max: None,
580 q_min: None,
581 q_max: None,
582 control_profile: None,
583 voltage_aggregation: None,
584 extras: Extras::new(),
585 }
586 }
587}
588
589#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
590#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
591#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
592#[non_exhaustive]
593pub enum ControlVoltageReference {
594 PnPerPhase,
595 PpPerPhase,
596 PpAveraged,
597 PgAveraged,
598 PnAveraged,
599 PgPerPhase,
600}
601
602#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
603#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
604#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
605#[non_exhaustive]
606pub enum ReactivePowerUnit {
607 VaFraction,
608 Var,
609}
610
611#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
612#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
613#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
614#[non_exhaustive]
615pub enum ActivePowerUnit {
616 VaFraction,
617 W,
618}
619
620#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
621#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
622#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
623#[non_exhaustive]
624pub enum ReactivePowerReference {
625 VarMax,
626 VarAvailable,
627}
628
629#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
630#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
631#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
632#[non_exhaustive]
633pub enum ActivePowerReference {
634 PAvailable,
635 PMax,
636 SMax,
637}
638
639#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
640#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
641#[non_exhaustive]
642pub struct PowerFactorControl {
643 pub pf: f64,
644}
645
646#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
647#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
648#[non_exhaustive]
649pub struct VoltVarControl {
650 pub voltage_reference: Option<ControlVoltageReference>,
651 pub breakpoints: Vec<f64>,
652 pub q_limits: Vec<f64>,
653 pub q_unit: Option<ReactivePowerUnit>,
654 pub q_ref: Option<ReactivePowerReference>,
655 pub p_min_for_q: Option<f64>,
656 pub p_min_for_q_max: Option<f64>,
657}
658
659#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
660#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
661#[non_exhaustive]
662pub struct VoltWattControl {
663 pub voltage_reference: Option<ControlVoltageReference>,
664 pub breakpoints: Vec<f64>,
665 pub p_limits: Vec<f64>,
666 pub p_unit: Option<ActivePowerUnit>,
667 pub p_ref: Option<ActivePowerReference>,
668}
669
670#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
671#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
672#[non_exhaustive]
673pub struct DistControlProfile {
674 pub name: String,
675 pub power_factor: Option<PowerFactorControl>,
676 pub volt_var: Option<VoltVarControl>,
677 pub volt_watt: Option<VoltWattControl>,
678 pub extras: Extras,
679}
680
681impl DistControlProfile {
682 #[must_use]
683 pub fn new(name: impl Into<String>) -> Self {
684 Self {
685 name: name.into(),
686 power_factor: None,
687 volt_var: None,
688 volt_watt: None,
689 extras: Extras::new(),
690 }
691 }
692}
693
694#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
695#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
696#[non_exhaustive]
697pub struct DistShunt {
698 pub name: String,
699 pub bus: String,
700 pub terminal_map: Vec<String>,
701 pub g: ConductorMatrix,
703 pub b: ConductorMatrix,
704 pub extras: Extras,
705}
706
707impl DistShunt {
708 #[must_use]
709 pub fn new(
710 name: impl Into<String>,
711 bus: impl Into<String>,
712 terminal_map: Vec<String>,
713 g: ConductorMatrix,
714 b: ConductorMatrix,
715 ) -> Self {
716 Self {
717 name: name.into(),
718 bus: bus.into(),
719 terminal_map,
720 g,
721 b,
722 extras: Extras::new(),
723 }
724 }
725}
726
727#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
728#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
729#[serde(rename_all = "snake_case")]
730#[non_exhaustive]
731pub enum DistWindingConn {
732 Wye,
733 Delta,
734}
735
736#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
737#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
738#[non_exhaustive]
739pub struct DistWinding {
740 pub bus: String,
741 pub terminal_map: Vec<String>,
742 pub conn: DistWindingConn,
743 pub v_ref: f64,
745 pub s_rating: f64,
747 pub r_pct: f64,
749 pub tap: f64,
750 #[serde(default, skip_serializing_if = "Option::is_none")]
751 pub r_neutral: Option<f64>,
752 #[serde(default, skip_serializing_if = "Option::is_none")]
753 pub x_neutral: Option<f64>,
754}
755
756impl DistWinding {
757 #[must_use]
758 pub fn new(
759 bus: impl Into<String>,
760 terminal_map: Vec<String>,
761 conn: DistWindingConn,
762 v_ref: f64,
763 s_rating: f64,
764 ) -> Self {
765 Self {
766 bus: bus.into(),
767 terminal_map,
768 conn,
769 v_ref,
770 s_rating,
771 r_pct: 0.0,
772 tap: 1.0,
773 r_neutral: None,
774 x_neutral: None,
775 }
776 }
777}
778
779#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
780#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
781#[non_exhaustive]
782pub struct DistTransformer {
783 pub name: String,
784 pub windings: Vec<DistWinding>,
785 pub xsc_pct: Vec<f64>,
789 pub phases: usize,
790 pub extras: Extras,
791}
792
793impl DistTransformer {
794 #[must_use]
795 pub fn new(
796 name: impl Into<String>,
797 windings: Vec<DistWinding>,
798 xsc_pct: Vec<f64>,
799 phases: usize,
800 ) -> Self {
801 Self {
802 name: name.into(),
803 windings,
804 xsc_pct,
805 phases,
806 extras: Extras::new(),
807 }
808 }
809}
810
811#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
812#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
813#[non_exhaustive]
814pub struct VoltageSource {
815 pub name: String,
816 pub bus: String,
817 pub terminal_map: Vec<String>,
818 pub v_magnitude: Vec<f64>,
820 pub v_angle: Vec<f64>,
822 #[serde(default, skip_serializing_if = "Option::is_none")]
824 pub energy_cost_rate: Option<Vec<f64>>,
825 pub extras: Extras,
826}
827
828impl VoltageSource {
829 #[must_use]
830 pub fn new(
831 name: impl Into<String>,
832 bus: impl Into<String>,
833 terminal_map: Vec<String>,
834 v_magnitude: Vec<f64>,
835 v_angle: Vec<f64>,
836 ) -> Self {
837 Self {
838 name: name.into(),
839 bus: bus.into(),
840 terminal_map,
841 v_magnitude,
842 v_angle,
843 energy_cost_rate: None,
844 extras: Extras::new(),
845 }
846 }
847}
848
849#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
852#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
853#[non_exhaustive]
854pub struct UntypedObject {
855 pub class: String,
856 pub name: String,
857 pub props: Vec<(Option<String>, String)>,
858}
859
860impl UntypedObject {
861 #[must_use]
862 pub fn new(
863 class: impl Into<String>,
864 name: impl Into<String>,
865 props: Vec<(Option<String>, String)>,
866 ) -> Self {
867 Self {
868 class: class.into(),
869 name: name.into(),
870 props,
871 }
872 }
873}
874
875#[derive(Debug, Clone)]
888pub struct MulticonductorNetwork {
889 tables: std::sync::Arc<MulticonductorNetworkTables>,
890}
891
892impl MulticonductorNetwork {
893 pub(crate) fn from_tables(tables: MulticonductorNetworkTables) -> Self {
894 Self {
895 tables: std::sync::Arc::new(tables),
896 }
897 }
898
899 pub(crate) fn tables_mut(&mut self) -> &mut MulticonductorNetworkTables {
902 std::sync::Arc::make_mut(&mut self.tables)
903 }
904}
905
906#[derive(Clone, Debug, Serialize, Deserialize)]
913#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
914#[cfg_attr(feature = "schema", schemars(rename = "MulticonductorNetwork"))]
915#[serde(remote = "Self")]
916pub(crate) struct MulticonductorNetworkTables {
917 pub name: Option<String>,
918 pub base_frequency: f64,
920 #[serde(default, skip_serializing_if = "Option::is_none")]
921 pub geo: Option<DistGeoMeta>,
922 pub buses: Vec<DistBus>,
923 #[serde(rename = "linecodes")]
924 pub line_codes: Vec<DistLineCode>,
925 pub lines: Vec<DistLine>,
926 pub switches: Vec<DistSwitch>,
927 pub transformers: Vec<DistTransformer>,
928 pub loads: Vec<DistLoad>,
929 pub generators: Vec<DistGenerator>,
930 #[serde(default, skip_serializing_if = "Vec::is_empty")]
931 pub ibrs: Vec<DistIbr>,
932 #[serde(default, skip_serializing_if = "Vec::is_empty")]
933 pub control_profiles: Vec<DistControlProfile>,
934 pub shunts: Vec<DistShunt>,
935 #[serde(default, skip_serializing_if = "Vec::is_empty")]
936 pub capacitors: Vec<DistCapacitor>,
937 pub sources: Vec<VoltageSource>,
940 #[serde(rename = "untyped")]
941 pub untyped_objects: Vec<UntypedObject>,
942 pub commands: Vec<(String, String)>,
945 pub options: Vec<(String, String)>,
946 #[serde(skip)]
953 pub defaulted: BTreeMap<String, Vec<&'static str>>,
954 pub source_format: Option<DistSourceFormat>,
955 pub extras: Extras,
956}
957
958impl serde::Serialize for MulticonductorNetwork {
963 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
964 MulticonductorNetworkTables::serialize(
965 &self.tables,
966 powerio_core::__implementation::nonfinite::NonFiniteSer(serializer),
967 )
968 }
969}
970
971impl<'de> serde::Deserialize<'de> for MulticonductorNetwork {
972 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
973 MulticonductorNetworkTables::deserialize(
974 powerio_core::__implementation::nonfinite::NonFiniteDe(deserializer),
975 )
976 .map(MulticonductorNetwork::from_tables)
977 }
978}
979
980#[cfg(feature = "schema")]
981impl schemars::JsonSchema for MulticonductorNetwork {
982 fn schema_name() -> std::borrow::Cow<'static, str> {
983 <MulticonductorNetworkTables as schemars::JsonSchema>::schema_name()
984 }
985
986 fn schema_id() -> std::borrow::Cow<'static, str> {
987 <MulticonductorNetworkTables as schemars::JsonSchema>::schema_id()
988 }
989
990 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
991 <MulticonductorNetworkTables as schemars::JsonSchema>::json_schema(generator)
992 }
993}
994
995macro_rules! dist_table_accessors {
996 ($($field:ident, $field_mut:ident: $ty:ty;)+) => {
997 impl MulticonductorNetwork {
998 $(
999 #[must_use]
1000 pub fn $field(&self) -> &$ty {
1001 &self.tables.$field
1002 }
1003
1004 #[must_use]
1008 pub fn $field_mut(&mut self) -> &mut $ty {
1009 &mut self.tables_mut().$field
1010 }
1011 )+
1012 }
1013 };
1014}
1015
1016dist_table_accessors! {
1017 name, name_mut: Option<String>;
1018 geo, geo_mut: Option<DistGeoMeta>;
1019 buses, buses_mut: Vec<DistBus>;
1020 line_codes, line_codes_mut: Vec<DistLineCode>;
1021 lines, lines_mut: Vec<DistLine>;
1022 switches, switches_mut: Vec<DistSwitch>;
1023 transformers, transformers_mut: Vec<DistTransformer>;
1024 loads, loads_mut: Vec<DistLoad>;
1025 generators, generators_mut: Vec<DistGenerator>;
1026 ibrs, ibrs_mut: Vec<DistIbr>;
1027 control_profiles, control_profiles_mut: Vec<DistControlProfile>;
1028 shunts, shunts_mut: Vec<DistShunt>;
1029 capacitors, capacitors_mut: Vec<DistCapacitor>;
1030 sources, sources_mut: Vec<VoltageSource>;
1031 untyped_objects, untyped_objects_mut: Vec<UntypedObject>;
1032 commands, commands_mut: Vec<(String, String)>;
1033 options, options_mut: Vec<(String, String)>;
1034 defaulted, defaulted_mut: BTreeMap<String, Vec<&'static str>>;
1035 source_format, source_format_mut: Option<DistSourceFormat>;
1036 extras, extras_mut: Extras;
1037}
1038
1039impl MulticonductorNetwork {
1040 #[must_use]
1042 pub fn base_frequency(&self) -> f64 {
1043 self.tables.base_frequency
1044 }
1045
1046 #[must_use]
1047 pub fn base_frequency_mut(&mut self) -> &mut f64 {
1048 &mut self.tables_mut().base_frequency
1049 }
1050}
1051
1052impl Default for MulticonductorNetwork {
1053 fn default() -> Self {
1054 MulticonductorNetwork::from_tables(MulticonductorNetworkTables::default())
1055 }
1056}
1057
1058impl Default for MulticonductorNetworkTables {
1059 fn default() -> Self {
1063 MulticonductorNetworkTables {
1064 name: None,
1065 base_frequency: crate::dss::defaults::BASE_FREQUENCY,
1066 geo: None,
1067 buses: Vec::new(),
1068 line_codes: Vec::new(),
1069 lines: Vec::new(),
1070 switches: Vec::new(),
1071 transformers: Vec::new(),
1072 loads: Vec::new(),
1073 generators: Vec::new(),
1074 ibrs: Vec::new(),
1075 control_profiles: Vec::new(),
1076 shunts: Vec::new(),
1077 capacitors: Vec::new(),
1078 sources: Vec::new(),
1079 untyped_objects: Vec::new(),
1080 commands: Vec::new(),
1081 options: Vec::new(),
1082 defaulted: BTreeMap::new(),
1083 source_format: None,
1084 extras: Extras::new(),
1085 }
1086 }
1087}
1088
1089impl MulticonductorNetwork {
1090 #[must_use]
1091 pub fn new() -> Self {
1092 Self::default()
1093 }
1094
1095 #[must_use]
1096 pub fn named(name: impl Into<String>) -> Self {
1097 MulticonductorNetwork::from_tables(MulticonductorNetworkTables {
1098 name: Some(name.into()),
1099 ..MulticonductorNetworkTables::default()
1100 })
1101 }
1102
1103 pub fn bus(&self, id: &str) -> Option<&DistBus> {
1105 self.buses().iter().find(|b| b.id.eq_ignore_ascii_case(id))
1106 }
1107
1108 pub fn linecode(&self, name: &str) -> Option<&DistLineCode> {
1110 self.line_codes()
1111 .iter()
1112 .find(|c| c.name.eq_ignore_ascii_case(name))
1113 }
1114}
1115
1116pub(crate) fn warn_defaulted_frequency(
1122 net: &MulticonductorNetwork,
1123 field: &str,
1124 diags: &mut crate::collect::Diagnostics,
1125) {
1126 let charging = net.line_codes().iter().any(|c| {
1127 [&c.b_from, &c.b_to]
1128 .iter()
1129 .flat_map(|m| m.iter())
1130 .flatten()
1131 .any(|v| v.is_finite() && v.abs() > 0.0)
1132 });
1133 if charging {
1134 diags.push(
1135 &crate::diagnostics::codes::READ_MULTICONDUCTOR_VALUE_DEFAULTED,
1136 format!(
1137 "document states no {field} and carries line susceptance; read at {} Hz",
1138 net.base_frequency()
1139 ),
1140 );
1141 }
1142}
1143
1144#[must_use]
1155pub fn find_unresolved_references(net: &MulticonductorNetwork) -> Vec<String> {
1156 let mut diags = crate::collect::Diagnostics::new();
1157 warn_unresolved_references(net, &mut diags);
1158 diags
1159 .into_records()
1160 .into_iter()
1161 .map(|record| record.message().to_owned())
1162 .collect()
1163}
1164
1165pub(crate) fn warn_unresolved_references(
1166 net: &MulticonductorNetwork,
1167 diags: &mut crate::collect::Diagnostics,
1168) {
1169 use std::collections::BTreeSet;
1170 let buses: BTreeSet<String> = net
1171 .buses()
1172 .iter()
1173 .map(|b| b.id.to_ascii_lowercase())
1174 .collect();
1175 let linecodes: BTreeSet<String> = net
1176 .line_codes()
1177 .iter()
1178 .map(|c| c.name.to_ascii_lowercase())
1179 .collect();
1180 let mut warnings = Vec::new();
1181 {
1182 let mut bus = |what: &str, field: &str, id: &str| {
1183 if id.is_empty() {
1184 warnings.push(format!("{what}: `{field}` reference is empty or missing"));
1185 } else if !buses.contains(&id.to_ascii_lowercase()) {
1186 warnings.push(format!("{what}: references undefined bus `{id}`"));
1187 }
1188 };
1189 for l in net.lines() {
1190 let what = format!("line {}", l.name);
1191 bus(&what, "bus_from", &l.bus_from);
1192 bus(&what, "bus_to", &l.bus_to);
1193 }
1194 for sw in net.switches() {
1195 let what = format!("switch {}", sw.name);
1196 bus(&what, "bus_from", &sw.bus_from);
1197 bus(&what, "bus_to", &sw.bus_to);
1198 }
1199 for t in net.transformers() {
1200 let what = format!("transformer {}", t.name);
1201 for w in &t.windings {
1202 bus(&what, "bus", &w.bus);
1203 }
1204 }
1205 for (what, id) in std::iter::empty()
1206 .chain(
1207 net.loads()
1208 .iter()
1209 .map(|x| (format!("load {}", x.name), &x.bus)),
1210 )
1211 .chain(
1212 net.generators()
1213 .iter()
1214 .map(|x| (format!("generator {}", x.name), &x.bus)),
1215 )
1216 .chain(
1217 net.shunts()
1218 .iter()
1219 .map(|x| (format!("shunt {}", x.name), &x.bus)),
1220 )
1221 .chain(
1222 net.capacitors()
1223 .iter()
1224 .map(|x| (format!("capacitor {}", x.name), &x.bus)),
1225 )
1226 .chain(
1227 net.ibrs()
1228 .iter()
1229 .map(|x| (format!("ibr {}", x.name), &x.bus)),
1230 )
1231 .chain(
1232 net.sources()
1233 .iter()
1234 .map(|x| (format!("voltage_source {}", x.name), &x.bus)),
1235 )
1236 {
1237 bus(&what, "bus", id);
1238 }
1239 }
1240 for l in net.lines() {
1241 if l.linecode.is_empty() {
1242 warnings.push(format!(
1243 "line {}: `linecode` reference is empty or missing",
1244 l.name
1245 ));
1246 } else if !linecodes.contains(&l.linecode.to_ascii_lowercase()) {
1247 warnings.push(format!(
1248 "line {}: references undefined linecode `{}`",
1249 l.name, l.linecode
1250 ));
1251 }
1252 }
1253 for message in warnings {
1254 diags.push(
1255 &crate::diagnostics::codes::VALIDATE_MULTICONDUCTOR_REFERENCE_UNDEFINED,
1256 message,
1257 );
1258 }
1259}
1260
1261fn zero_mat(n: usize) -> ConductorMatrix {
1262 vec![vec![0.0; n]; n]
1263}
1264
1265fn matrix_extent(m: &ConductorMatrix) -> usize {
1266 m.iter().map(Vec::len).fold(m.len(), usize::max)
1267}
1268
1269pub(crate) fn n_winding_phase_count(conn: DistWindingConn, terminal_map: &[String]) -> usize {
1273 match conn {
1274 DistWindingConn::Wye => terminal_map.len().saturating_sub(1).max(1),
1275 DistWindingConn::Delta => {
1276 if terminal_map.len() == 2 {
1277 1
1278 } else {
1279 terminal_map.len().max(1)
1280 }
1281 }
1282 }
1283}
1284
1285pub(crate) fn n_winding_impedance_base(phases: usize, v_nom: f64, s: f64) -> Option<f64> {
1288 let phases = phases as f64;
1289 (phases > 0.0 && v_nom.is_finite() && v_nom > 0.0 && s.is_finite() && s > 0.0)
1290 .then_some(phases * v_nom * v_nom / s)
1291}
1292
1293pub(crate) fn winding_phase_pair(winding: &DistWinding) -> Option<(u8, u8)> {
1296 let [first, second] = winding.terminal_map.as_slice() else {
1297 return None;
1298 };
1299 let phase = |term: &String| term.parse::<u8>().ok().filter(|n| (1..=3).contains(n));
1300 match (phase(first), phase(second)) {
1301 (Some(lead), Some(lag)) if lead != lag => Some((lead, lag)),
1302 _ => None,
1303 }
1304}
1305
1306pub(crate) fn open_delta_connection(a: (u8, u8), b: (u8, u8)) -> Option<(&'static str, bool)> {
1310 match (a, b) {
1311 ((1, 2), (2, 3)) => Some(("ABBC", false)),
1312 ((2, 3), (1, 2)) => Some(("ABBC", true)),
1313 ((2, 3), (1, 3)) => Some(("BCAC", false)),
1314 ((1, 3), (2, 3)) => Some(("BCAC", true)),
1315 ((3, 1), (2, 1)) => Some(("CABA", false)),
1316 ((2, 1), (3, 1)) => Some(("CABA", true)),
1317 _ => None,
1318 }
1319}
1320
1321pub(crate) fn open_delta_pairable(a: &DistTransformer, b: &DistTransformer) -> bool {
1325 let ([a0, a1], [b0, b1]) = (a.windings.as_slice(), b.windings.as_slice()) else {
1326 return false;
1327 };
1328 let eq = |x: f64, y: f64| x.to_bits() == y.to_bits();
1329 eq(a0.v_ref, b0.v_ref)
1330 && eq(a1.v_ref, b1.v_ref)
1331 && eq(a0.s_rating, b0.s_rating)
1332 && eq(a1.s_rating, b1.s_rating)
1333 && eq(a0.r_pct, b0.r_pct)
1334 && eq(a1.r_pct, b1.r_pct)
1335 && a.xsc_pct.len() == b.xsc_pct.len()
1336 && a.xsc_pct.iter().zip(&b.xsc_pct).all(|(x, y)| eq(*x, *y))
1337 && ["%noloadloss", "%imag", "g_no_load", "b_no_load"]
1338 .iter()
1339 .all(|k| a.extras.get(*k) == b.extras.get(*k))
1340}
1341
1342pub(crate) fn pair_keys(n: usize) -> Vec<(usize, usize)> {
1345 let mut pairs = Vec::new();
1346 for i in 0..n {
1347 for j in i + 1..n {
1348 pairs.push((i, j));
1349 }
1350 }
1351 pairs
1352}
1353
1354pub(crate) fn square_from_rows(rows: &[Vec<f64>], n: usize) -> Option<ConductorMatrix> {
1357 let mut m = vec![vec![0.0; n]; n];
1358 if rows.len() != n {
1359 return None;
1360 }
1361 let lower = rows.iter().enumerate().all(|(i, r)| r.len() == i + 1);
1362 let full = rows.iter().all(|r| r.len() == n);
1363 if lower {
1364 for (i, row) in rows.iter().enumerate() {
1365 for (j, &v) in row.iter().enumerate() {
1366 m[i][j] = v;
1367 m[j][i] = v;
1368 }
1369 }
1370 } else if full {
1371 for (i, row) in rows.iter().enumerate() {
1372 m[i].clone_from_slice(&row[..n]);
1373 }
1374 } else {
1375 return None;
1376 }
1377 Some(m)
1378}
1379
1380pub(crate) fn winding_coil_voltage(winding: &DistWinding, phases: usize) -> f64 {
1382 let divisor = if winding.conn == DistWindingConn::Wye && matches!(phases, 2 | 3) {
1383 3f64.sqrt()
1384 } else {
1385 1.0
1386 };
1387 winding.v_ref * winding.tap / divisor
1388}
1389
1390pub(crate) fn transformer_no_load_percentages(t: &DistTransformer) -> Option<(f64, f64)> {
1393 let shunt = t.extras.get("no_load_shunt")?;
1394 if shunt.get("winding")?.as_u64()? != 2 {
1395 return None;
1396 }
1397 let g = shunt.get("g")?.as_f64()?;
1398 let b = shunt.get("b")?.as_f64()?;
1399 let voltage = winding_coil_voltage(t.windings.get(1)?, t.phases);
1400 let rating = t.windings.first()?.s_rating;
1401 if !voltage.is_finite()
1402 || voltage <= 0.0
1403 || !rating.is_finite()
1404 || rating <= 0.0
1405 || g < 0.0
1406 || b > 0.0
1407 {
1408 return None;
1409 }
1410 let scale = 100.0 * t.phases.max(1) as f64 * voltage.powi(2) / rating;
1411 Some((g * scale, -b * scale))
1412}
1413
1414#[cfg(test)]
1415mod tests {
1416 use super::*;
1417
1418 #[test]
1419 #[allow(clippy::float_cmp)]
1420 fn lower_triangle_completes_symmetrically() {
1421 let rows = vec![vec![1.0], vec![0.5, 2.0], vec![0.3, 0.4, 3.0]];
1422 let m = square_from_rows(&rows, 3).unwrap();
1423 assert_eq!(m[0][1], 0.5);
1424 assert_eq!(m[1][0], 0.5);
1425 assert_eq!(m[2][2], 3.0);
1426 assert_eq!(m[0][2], 0.3);
1427 }
1428
1429 #[test]
1430 #[allow(clippy::float_cmp)]
1431 fn full_rows_pass_through() {
1432 let rows = vec![vec![1.0, 9.0], vec![8.0, 2.0]];
1433 let m = square_from_rows(&rows, 2).unwrap();
1434 assert_eq!(m[0][1], 9.0);
1435 assert_eq!(m[1][0], 8.0);
1436 }
1437
1438 #[test]
1439 fn wrong_shape_is_rejected() {
1440 assert!(square_from_rows(&[vec![1.0], vec![2.0]], 2).is_none());
1441 assert!(square_from_rows(&[vec![1.0, 2.0]], 2).is_none());
1442 }
1443
1444 #[test]
1445 fn multiconductor_json_uses_only_final_nonfinite_spellings() {
1446 let mut network = MulticonductorNetwork::named("strict-ir");
1447 let mut code = DistLineCode::new("lc", vec![vec![0.1]], vec![vec![0.2]]);
1448 code.i_max = Some(vec![f64::INFINITY]);
1449 network.line_codes_mut().push(code);
1450
1451 let text = serde_json::to_string(&network).unwrap();
1452 assert!(text.contains(r#""i_max":["Infinity"]"#), "{text}");
1453
1454 let invalid = text.replace(r#""i_max":["Infinity"]"#, r#""i_max":[null]"#);
1455 let error = serde_json::from_str::<MulticonductorNetwork>(&invalid).unwrap_err();
1456 assert!(error.to_string().contains("cannot be null"), "{error}");
1457 }
1458}