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