1use std::collections::BTreeMap;
17use std::sync::Arc;
18
19use serde::{Deserialize, Serialize};
20
21use crate::geo::{GeoMeta, Location};
22
23pub type Extras = BTreeMap<String, serde_json::Value>;
24
25pub type Mat = Vec<Vec<f64>>;
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
30#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
31#[serde(rename_all = "kebab-case")]
32#[non_exhaustive]
33pub enum DistSourceFormat {
34 Dss,
35 BmopfJson,
36 PmdJson,
37}
38
39impl DistSourceFormat {
40 pub fn name(self) -> &'static str {
43 match self {
44 DistSourceFormat::Dss => "dss",
45 DistSourceFormat::PmdJson => "pmd-json",
46 DistSourceFormat::BmopfJson => "bmopf-json",
47 }
48 }
49}
50
51#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
52#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
53#[non_exhaustive]
54pub struct DistBus {
55 pub id: String,
56 pub terminals: Vec<String>,
58 pub grounded: Vec<String>,
60 pub v_min: Option<f64>,
66 pub v_max: Option<f64>,
67 pub vpn_min: Option<Vec<f64>>,
68 pub vpn_max: Option<Vec<f64>>,
69 pub vpp_min: Option<Vec<f64>>,
70 pub vpp_max: Option<Vec<f64>>,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub vpos_min: Option<f64>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub vpos_max: Option<f64>,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub vneg_max: Option<f64>,
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub vzero_max: Option<f64>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub vn_max: Option<f64>,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub location: Option<Location>,
84 pub extras: Extras,
85}
86
87impl DistBus {
88 #[must_use]
89 pub fn new(id: impl Into<String>, terminals: Vec<String>) -> Self {
90 Self {
91 id: id.into(),
92 terminals,
93 grounded: Vec::new(),
94 v_min: None,
95 v_max: None,
96 vpn_min: None,
97 vpn_max: None,
98 vpp_min: None,
99 vpp_max: None,
100 vpos_min: None,
101 vpos_max: None,
102 vneg_max: None,
103 vzero_max: None,
104 vn_max: None,
105 location: None,
106 extras: Extras::new(),
107 }
108 }
109}
110
111#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
112#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
113#[non_exhaustive]
114pub struct DistLineCode {
115 pub name: String,
116 pub n_conductors: usize,
117 pub r_series: Mat,
119 pub x_series: Mat,
120 pub g_from: Mat,
122 pub b_from: Mat,
123 pub g_to: Mat,
124 pub b_to: Mat,
125 #[serde(default, with = "crate::nonfinite::upper_bounds")]
127 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
128 pub i_max: Option<Vec<f64>>,
129 #[serde(default, with = "crate::nonfinite::upper_bounds")]
130 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
131 pub s_max: Option<Vec<f64>>,
132 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub source: Option<String>,
136 pub extras: Extras,
137}
138
139impl DistLineCode {
140 #[must_use]
141 pub fn new(name: impl Into<String>, r_series: Mat, x_series: Mat) -> Self {
142 let n_conductors = matrix_extent(&r_series).max(matrix_extent(&x_series));
143 Self {
144 name: name.into(),
145 n_conductors,
146 r_series,
147 x_series,
148 g_from: zero_mat(n_conductors),
149 b_from: zero_mat(n_conductors),
150 g_to: zero_mat(n_conductors),
151 b_to: zero_mat(n_conductors),
152 i_max: None,
153 s_max: None,
154 source: None,
155 extras: Extras::new(),
156 }
157 }
158}
159
160#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
161#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
162#[non_exhaustive]
163pub struct DistLine {
164 pub name: String,
165 pub bus_from: String,
166 pub bus_to: String,
167 pub terminal_map_from: Vec<String>,
168 pub terminal_map_to: Vec<String>,
169 pub linecode: String,
170 #[serde(with = "crate::nonfinite::nan_scalar")]
172 #[cfg_attr(
173 feature = "schema",
174 schemars(schema_with = "crate::nonfinite::nullable_number")
175 )]
176 pub length: f64,
177 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub route: Option<Vec<Location>>,
183 #[serde(
187 default,
188 skip_serializing_if = "Option::is_none",
189 with = "crate::nonfinite::upper_bounds"
190 )]
191 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
192 pub i_max: Option<Vec<f64>>,
193 #[serde(
194 default,
195 skip_serializing_if = "Option::is_none",
196 with = "crate::nonfinite::upper_bounds"
197 )]
198 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
199 pub s_max: Option<Vec<f64>>,
200 pub extras: Extras,
201}
202
203impl DistLine {
204 #[must_use]
205 pub fn new(
206 name: impl Into<String>,
207 bus_from: impl Into<String>,
208 bus_to: impl Into<String>,
209 terminal_map_from: Vec<String>,
210 terminal_map_to: Vec<String>,
211 linecode: impl Into<String>,
212 length: f64,
213 ) -> Self {
214 Self {
215 name: name.into(),
216 bus_from: bus_from.into(),
217 bus_to: bus_to.into(),
218 terminal_map_from,
219 terminal_map_to,
220 linecode: linecode.into(),
221 length,
222 route: None,
223 i_max: None,
224 s_max: None,
225 extras: Extras::new(),
226 }
227 }
228}
229
230#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
236#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
237#[non_exhaustive]
238pub struct DistCapacitor {
239 pub name: String,
240 pub bus: String,
241 pub terminal_map: Vec<String>,
242 pub configuration: Configuration,
243 #[serde(with = "crate::nonfinite::nan_scalar")]
245 #[cfg_attr(
246 feature = "schema",
247 schemars(schema_with = "crate::nonfinite::nullable_number")
248 )]
249 pub q_rated: f64,
250 #[serde(with = "crate::nonfinite::nan_scalar")]
253 #[cfg_attr(
254 feature = "schema",
255 schemars(schema_with = "crate::nonfinite::nullable_number")
256 )]
257 pub v_nom: f64,
258 pub extras: Extras,
259}
260
261impl DistCapacitor {
262 #[must_use]
263 pub fn new(
264 name: impl Into<String>,
265 bus: impl Into<String>,
266 terminal_map: Vec<String>,
267 configuration: Configuration,
268 q_rated: f64,
269 v_nom: f64,
270 ) -> Self {
271 Self {
272 name: name.into(),
273 bus: bus.into(),
274 terminal_map,
275 configuration,
276 q_rated,
277 v_nom,
278 extras: Extras::new(),
279 }
280 }
281}
282
283#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
284#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
285#[non_exhaustive]
286pub struct DistSwitch {
287 pub name: String,
288 pub bus_from: String,
289 pub bus_to: String,
290 pub terminal_map_from: Vec<String>,
291 pub terminal_map_to: Vec<String>,
292 pub open: bool,
293 #[serde(default, with = "crate::nonfinite::upper_bounds")]
295 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
296 pub i_max: Option<Vec<f64>>,
297 pub extras: Extras,
298}
299
300impl DistSwitch {
301 #[must_use]
302 pub fn new(
303 name: impl Into<String>,
304 bus_from: impl Into<String>,
305 bus_to: impl Into<String>,
306 terminal_map_from: Vec<String>,
307 terminal_map_to: Vec<String>,
308 open: bool,
309 ) -> Self {
310 Self {
311 name: name.into(),
312 bus_from: bus_from.into(),
313 bus_to: bus_to.into(),
314 terminal_map_from,
315 terminal_map_to,
316 open,
317 i_max: None,
318 extras: Extras::new(),
319 }
320 }
321}
322
323#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
324#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
325#[serde(rename_all = "snake_case")]
326#[non_exhaustive]
327pub enum Configuration {
328 Wye,
329 Delta,
330 SinglePhase,
331}
332
333#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
334#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
335#[non_exhaustive]
336pub struct DistLoad {
337 pub name: String,
338 pub bus: String,
339 pub terminal_map: Vec<String>,
340 pub configuration: Configuration,
341 pub p_nom: Vec<f64>,
343 pub q_nom: Vec<f64>,
345 pub voltage_model: DistLoadVoltageModel,
346 pub extras: Extras,
347}
348
349impl DistLoad {
350 #[must_use]
351 pub fn new(
352 name: impl Into<String>,
353 bus: impl Into<String>,
354 terminal_map: Vec<String>,
355 configuration: Configuration,
356 p_nom: Vec<f64>,
357 q_nom: Vec<f64>,
358 ) -> Self {
359 Self {
360 name: name.into(),
361 bus: bus.into(),
362 terminal_map,
363 configuration,
364 p_nom,
365 q_nom,
366 voltage_model: DistLoadVoltageModel::default(),
367 extras: Extras::new(),
368 }
369 }
370}
371
372#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
373#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
374#[serde(tag = "model", rename_all = "snake_case")]
375#[non_exhaustive]
376pub enum DistLoadVoltageModel {
377 ConstantPower { v_nom: Vec<f64> },
380 ConstantCurrent { v_nom: Vec<f64> },
382 ConstantImpedance { v_nom: Vec<f64> },
384 Zip {
388 v_nom: Vec<f64>,
389 alpha_z: Vec<f64>,
390 alpha_i: Vec<f64>,
391 alpha_p: Vec<f64>,
392 beta_z: Vec<f64>,
393 beta_i: Vec<f64>,
394 beta_p: Vec<f64>,
395 },
396 Exponential {
399 v_nom: Vec<f64>,
400 gamma_p: Vec<f64>,
401 gamma_q: Vec<f64>,
402 },
403}
404
405impl Default for DistLoadVoltageModel {
406 fn default() -> Self {
407 Self::ConstantPower { v_nom: Vec::new() }
408 }
409}
410
411impl DistLoadVoltageModel {
412 #[must_use]
413 pub fn v_nom(&self) -> &[f64] {
414 match self {
415 Self::ConstantPower { v_nom }
416 | Self::ConstantCurrent { v_nom }
417 | Self::ConstantImpedance { v_nom }
418 | Self::Zip { v_nom, .. }
419 | Self::Exponential { v_nom, .. } => v_nom,
420 }
421 }
422}
423
424#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
425#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
426#[non_exhaustive]
427pub struct DistGenerator {
428 pub name: String,
429 pub bus: String,
430 pub terminal_map: Vec<String>,
431 pub configuration: Configuration,
432 pub p_nom: Vec<f64>,
434 pub q_nom: Vec<f64>,
435 #[serde(default, with = "crate::nonfinite::lower_bounds")]
438 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
439 pub p_min: Option<Vec<f64>>,
440 #[serde(default, with = "crate::nonfinite::upper_bounds")]
441 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
442 pub p_max: Option<Vec<f64>>,
443 #[serde(default, with = "crate::nonfinite::lower_bounds")]
444 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
445 pub q_min: Option<Vec<f64>>,
446 #[serde(default, with = "crate::nonfinite::upper_bounds")]
447 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
448 pub q_max: Option<Vec<f64>>,
449 pub cost: Option<f64>,
451 #[serde(
454 default,
455 skip_serializing_if = "Option::is_none",
456 with = "crate::nonfinite::upper_bounds"
457 )]
458 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
459 pub s_max: Option<Vec<f64>>,
460 #[serde(
461 default,
462 skip_serializing_if = "Option::is_none",
463 with = "crate::nonfinite::upper_bounds"
464 )]
465 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
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 #[serde(with = "crate::nonfinite::upper_limits")]
541 #[cfg_attr(feature = "schema", schemars(with = "Vec<Option<f64>>"))]
542 pub s_max: Vec<f64>,
543 #[serde(default, with = "crate::nonfinite::upper_bounds")]
545 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
546 pub i_max: Option<Vec<f64>>,
547 pub p_avail: Option<f64>,
549 #[serde(default, with = "crate::nonfinite::lower_bounds")]
550 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
551 pub p_min: Option<Vec<f64>>,
552 #[serde(default, with = "crate::nonfinite::upper_bounds")]
553 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
554 pub p_max: Option<Vec<f64>>,
555 #[serde(default, with = "crate::nonfinite::lower_bounds")]
556 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
557 pub q_min: Option<Vec<f64>>,
558 #[serde(default, with = "crate::nonfinite::upper_bounds")]
559 #[cfg_attr(feature = "schema", schemars(with = "Option<Vec<Option<f64>>>"))]
560 pub q_max: Option<Vec<f64>>,
561 pub control_profile: Option<String>,
562 pub voltage_aggregation: Option<IbrVoltageAggregation>,
563 pub extras: Extras,
564}
565
566impl DistIbr {
567 #[must_use]
568 pub fn new(
569 name: impl Into<String>,
570 bus: impl Into<String>,
571 terminal_map: Vec<String>,
572 topology: IbrTopology,
573 prime_mover: IbrPrimeMover,
574 s_max: Vec<f64>,
575 ) -> Self {
576 Self {
577 name: name.into(),
578 bus: bus.into(),
579 terminal_map,
580 topology,
581 prime_mover,
582 s_max,
583 i_max: None,
584 p_avail: None,
585 p_min: None,
586 p_max: None,
587 q_min: None,
588 q_max: None,
589 control_profile: None,
590 voltage_aggregation: None,
591 extras: Extras::new(),
592 }
593 }
594}
595
596#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
597#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
598#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
599#[non_exhaustive]
600pub enum ControlVoltageReference {
601 PnPerPhase,
602 PpPerPhase,
603 PpAveraged,
604 PgAveraged,
605 PnAveraged,
606 PgPerPhase,
607}
608
609#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
610#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
611#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
612#[non_exhaustive]
613pub enum ReactivePowerUnit {
614 VaFraction,
615 Var,
616}
617
618#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
619#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
620#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
621#[non_exhaustive]
622pub enum ActivePowerUnit {
623 VaFraction,
624 W,
625}
626
627#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
628#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
629#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
630#[non_exhaustive]
631pub enum ReactivePowerReference {
632 VarMax,
633 VarAvailable,
634}
635
636#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
637#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
638#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
639#[non_exhaustive]
640pub enum ActivePowerReference {
641 PAvailable,
642 PMax,
643 SMax,
644}
645
646#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
647#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
648#[non_exhaustive]
649pub struct PowerFactorControl {
650 pub pf: f64,
651}
652
653#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
654#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
655#[non_exhaustive]
656pub struct VoltVarControl {
657 pub voltage_reference: Option<ControlVoltageReference>,
658 pub breakpoints: Vec<f64>,
659 pub q_limits: Vec<f64>,
660 pub q_unit: Option<ReactivePowerUnit>,
661 pub q_ref: Option<ReactivePowerReference>,
662 pub p_min_for_q: Option<f64>,
663 pub p_min_for_q_max: Option<f64>,
664}
665
666#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
667#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
668#[non_exhaustive]
669pub struct VoltWattControl {
670 pub voltage_reference: Option<ControlVoltageReference>,
671 pub breakpoints: Vec<f64>,
672 pub p_limits: Vec<f64>,
673 pub p_unit: Option<ActivePowerUnit>,
674 pub p_ref: Option<ActivePowerReference>,
675}
676
677#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
678#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
679#[non_exhaustive]
680pub struct DistControlProfile {
681 pub name: String,
682 pub power_factor: Option<PowerFactorControl>,
683 pub volt_var: Option<VoltVarControl>,
684 pub volt_watt: Option<VoltWattControl>,
685 pub extras: Extras,
686}
687
688impl DistControlProfile {
689 #[must_use]
690 pub fn new(name: impl Into<String>) -> Self {
691 Self {
692 name: name.into(),
693 power_factor: None,
694 volt_var: None,
695 volt_watt: None,
696 extras: Extras::new(),
697 }
698 }
699}
700
701#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
702#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
703#[non_exhaustive]
704pub struct DistShunt {
705 pub name: String,
706 pub bus: String,
707 pub terminal_map: Vec<String>,
708 pub g: Mat,
710 pub b: Mat,
711 pub extras: Extras,
712}
713
714impl DistShunt {
715 #[must_use]
716 pub fn new(
717 name: impl Into<String>,
718 bus: impl Into<String>,
719 terminal_map: Vec<String>,
720 g: Mat,
721 b: Mat,
722 ) -> Self {
723 Self {
724 name: name.into(),
725 bus: bus.into(),
726 terminal_map,
727 g,
728 b,
729 extras: Extras::new(),
730 }
731 }
732}
733
734#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
735#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
736#[serde(rename_all = "snake_case")]
737#[non_exhaustive]
738pub enum WindingConn {
739 Wye,
740 Delta,
741}
742
743#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
744#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
745#[non_exhaustive]
746pub struct Winding {
747 pub bus: String,
748 pub terminal_map: Vec<String>,
749 pub conn: WindingConn,
750 #[serde(with = "crate::nonfinite::nan_scalar")]
752 #[cfg_attr(
753 feature = "schema",
754 schemars(schema_with = "crate::nonfinite::nullable_number")
755 )]
756 pub v_ref: f64,
757 #[serde(with = "crate::nonfinite::nan_scalar")]
759 #[cfg_attr(
760 feature = "schema",
761 schemars(schema_with = "crate::nonfinite::nullable_number")
762 )]
763 pub s_rating: f64,
764 #[serde(with = "crate::nonfinite::nan_scalar")]
766 #[cfg_attr(
767 feature = "schema",
768 schemars(schema_with = "crate::nonfinite::nullable_number")
769 )]
770 pub r_pct: f64,
771 pub tap: f64,
772 #[serde(default, skip_serializing_if = "Option::is_none")]
773 pub r_neutral: Option<f64>,
774 #[serde(default, skip_serializing_if = "Option::is_none")]
775 pub x_neutral: Option<f64>,
776}
777
778impl Winding {
779 #[must_use]
780 pub fn new(
781 bus: impl Into<String>,
782 terminal_map: Vec<String>,
783 conn: WindingConn,
784 v_ref: f64,
785 s_rating: f64,
786 ) -> Self {
787 Self {
788 bus: bus.into(),
789 terminal_map,
790 conn,
791 v_ref,
792 s_rating,
793 r_pct: 0.0,
794 tap: 1.0,
795 r_neutral: None,
796 x_neutral: None,
797 }
798 }
799}
800
801#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
802#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
803#[non_exhaustive]
804pub struct DistTransformer {
805 pub name: String,
806 pub windings: Vec<Winding>,
807 pub xsc_pct: Vec<f64>,
810 pub phases: usize,
811 pub extras: Extras,
812}
813
814impl DistTransformer {
815 #[must_use]
816 pub fn new(
817 name: impl Into<String>,
818 windings: Vec<Winding>,
819 xsc_pct: Vec<f64>,
820 phases: usize,
821 ) -> Self {
822 Self {
823 name: name.into(),
824 windings,
825 xsc_pct,
826 phases,
827 extras: Extras::new(),
828 }
829 }
830}
831
832#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
833#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
834#[non_exhaustive]
835pub struct VoltageSource {
836 pub name: String,
837 pub bus: String,
838 pub terminal_map: Vec<String>,
839 pub v_magnitude: Vec<f64>,
841 pub v_angle: Vec<f64>,
843 pub extras: Extras,
844}
845
846impl VoltageSource {
847 #[must_use]
848 pub fn new(
849 name: impl Into<String>,
850 bus: impl Into<String>,
851 terminal_map: Vec<String>,
852 v_magnitude: Vec<f64>,
853 v_angle: Vec<f64>,
854 ) -> Self {
855 Self {
856 name: name.into(),
857 bus: bus.into(),
858 terminal_map,
859 v_magnitude,
860 v_angle,
861 extras: Extras::new(),
862 }
863 }
864}
865
866#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
869#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
870#[non_exhaustive]
871pub struct UntypedObject {
872 pub class: String,
873 pub name: String,
874 pub props: Vec<(Option<String>, String)>,
875}
876
877impl UntypedObject {
878 #[must_use]
879 pub fn new(
880 class: impl Into<String>,
881 name: impl Into<String>,
882 props: Vec<(Option<String>, String)>,
883 ) -> Self {
884 Self {
885 class: class.into(),
886 name: name.into(),
887 props,
888 }
889 }
890}
891
892#[derive(Clone, Debug, Serialize, Deserialize)]
898#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
899#[non_exhaustive]
900pub struct MulticonductorNetwork {
901 pub name: Option<String>,
902 pub base_frequency: f64,
904 #[serde(default, skip_serializing_if = "Option::is_none")]
905 pub geo: Option<GeoMeta>,
906 pub buses: Vec<DistBus>,
907 pub linecodes: Vec<DistLineCode>,
908 pub lines: Vec<DistLine>,
909 pub switches: Vec<DistSwitch>,
910 pub transformers: Vec<DistTransformer>,
911 pub loads: Vec<DistLoad>,
912 pub generators: Vec<DistGenerator>,
913 #[serde(default, skip_serializing_if = "Vec::is_empty")]
914 pub ibrs: Vec<DistIbr>,
915 #[serde(default, skip_serializing_if = "Vec::is_empty")]
916 pub control_profiles: Vec<DistControlProfile>,
917 pub shunts: Vec<DistShunt>,
918 #[serde(default, skip_serializing_if = "Vec::is_empty")]
919 pub capacitors: Vec<DistCapacitor>,
920 pub sources: Vec<VoltageSource>,
923 pub untyped: Vec<UntypedObject>,
924 pub commands: Vec<(String, String)>,
927 pub options: Vec<(String, String)>,
928 #[serde(skip)]
935 pub defaulted: BTreeMap<String, Vec<&'static str>>,
936 pub warnings: Vec<String>,
937 #[serde(skip)]
942 pub parse_diagnostics: Vec<crate::diagnostics::StructuredDiagnostic>,
943 #[serde(skip)]
948 pub source: Option<Arc<String>>,
949 pub source_format: Option<DistSourceFormat>,
950 pub extras: Extras,
951}
952
953impl Default for MulticonductorNetwork {
954 fn default() -> Self {
958 MulticonductorNetwork {
959 name: None,
960 base_frequency: crate::dss::defaults::BASE_FREQUENCY,
961 geo: None,
962 buses: Vec::new(),
963 linecodes: Vec::new(),
964 lines: Vec::new(),
965 switches: Vec::new(),
966 transformers: Vec::new(),
967 loads: Vec::new(),
968 generators: Vec::new(),
969 ibrs: Vec::new(),
970 control_profiles: Vec::new(),
971 shunts: Vec::new(),
972 capacitors: Vec::new(),
973 sources: Vec::new(),
974 untyped: Vec::new(),
975 commands: Vec::new(),
976 options: Vec::new(),
977 defaulted: BTreeMap::new(),
978 warnings: Vec::new(),
979 parse_diagnostics: Vec::new(),
980 source: None,
981 source_format: None,
982 extras: Extras::new(),
983 }
984 }
985}
986
987impl MulticonductorNetwork {
988 #[must_use]
989 pub fn new() -> Self {
990 Self::default()
991 }
992
993 #[must_use]
994 pub fn named(name: impl Into<String>) -> Self {
995 Self {
996 name: Some(name.into()),
997 ..Self::default()
998 }
999 }
1000
1001 pub fn bus(&self, id: &str) -> Option<&DistBus> {
1003 self.buses.iter().find(|b| b.id.eq_ignore_ascii_case(id))
1004 }
1005
1006 pub fn linecode(&self, name: &str) -> Option<&DistLineCode> {
1008 self.linecodes
1009 .iter()
1010 .find(|c| c.name.eq_ignore_ascii_case(name))
1011 }
1012}
1013
1014pub(crate) fn warn_defaulted_frequency(net: &mut MulticonductorNetwork, field: &str) {
1020 let charging = net.linecodes.iter().any(|c| {
1021 [&c.b_from, &c.b_to]
1022 .iter()
1023 .flat_map(|m| m.iter())
1024 .flatten()
1025 .any(|v| v.is_finite() && v.abs() > 0.0)
1026 });
1027 if charging {
1028 net.warnings.push(format!(
1029 "document states no {field} and carries line susceptance; read at {} Hz",
1030 net.base_frequency
1031 ));
1032 }
1033}
1034
1035pub(crate) fn warn_unresolved_references(net: &mut MulticonductorNetwork) {
1043 use std::collections::BTreeSet;
1044 let buses: BTreeSet<String> = net
1045 .buses
1046 .iter()
1047 .map(|b| b.id.to_ascii_lowercase())
1048 .collect();
1049 let linecodes: BTreeSet<String> = net
1050 .linecodes
1051 .iter()
1052 .map(|c| c.name.to_ascii_lowercase())
1053 .collect();
1054 let mut warnings = Vec::new();
1055 {
1056 let mut bus = |what: &str, field: &str, id: &str| {
1057 if id.is_empty() {
1058 warnings.push(format!("{what}: `{field}` reference is empty or missing"));
1059 } else if !buses.contains(&id.to_ascii_lowercase()) {
1060 warnings.push(format!("{what}: references undefined bus `{id}`"));
1061 }
1062 };
1063 for l in &net.lines {
1064 let what = format!("line {}", l.name);
1065 bus(&what, "bus_from", &l.bus_from);
1066 bus(&what, "bus_to", &l.bus_to);
1067 }
1068 for sw in &net.switches {
1069 let what = format!("switch {}", sw.name);
1070 bus(&what, "bus_from", &sw.bus_from);
1071 bus(&what, "bus_to", &sw.bus_to);
1072 }
1073 for t in &net.transformers {
1074 let what = format!("transformer {}", t.name);
1075 for w in &t.windings {
1076 bus(&what, "bus", &w.bus);
1077 }
1078 }
1079 for (what, id) in std::iter::empty()
1080 .chain(
1081 net.loads
1082 .iter()
1083 .map(|x| (format!("load {}", x.name), &x.bus)),
1084 )
1085 .chain(
1086 net.generators
1087 .iter()
1088 .map(|x| (format!("generator {}", x.name), &x.bus)),
1089 )
1090 .chain(
1091 net.shunts
1092 .iter()
1093 .map(|x| (format!("shunt {}", x.name), &x.bus)),
1094 )
1095 .chain(
1096 net.capacitors
1097 .iter()
1098 .map(|x| (format!("capacitor {}", x.name), &x.bus)),
1099 )
1100 .chain(net.ibrs.iter().map(|x| (format!("ibr {}", x.name), &x.bus)))
1101 .chain(
1102 net.sources
1103 .iter()
1104 .map(|x| (format!("voltage_source {}", x.name), &x.bus)),
1105 )
1106 {
1107 bus(&what, "bus", id);
1108 }
1109 }
1110 for l in &net.lines {
1111 if l.linecode.is_empty() {
1112 warnings.push(format!(
1113 "line {}: `linecode` reference is empty or missing",
1114 l.name
1115 ));
1116 } else if !linecodes.contains(&l.linecode.to_ascii_lowercase()) {
1117 warnings.push(format!(
1118 "line {}: references undefined linecode `{}`",
1119 l.name, l.linecode
1120 ));
1121 }
1122 }
1123 net.warnings.extend(warnings);
1124}
1125
1126fn zero_mat(n: usize) -> Mat {
1127 vec![vec![0.0; n]; n]
1128}
1129
1130fn matrix_extent(m: &Mat) -> usize {
1131 m.iter().map(Vec::len).fold(m.len(), usize::max)
1132}
1133
1134pub(crate) fn n_winding_phase_count(conn: WindingConn, terminal_map: &[String]) -> usize {
1138 match conn {
1139 WindingConn::Wye => terminal_map.len().saturating_sub(1).max(1),
1140 WindingConn::Delta => {
1141 if terminal_map.len() == 2 {
1142 1
1143 } else {
1144 terminal_map.len().max(1)
1145 }
1146 }
1147 }
1148}
1149
1150pub(crate) fn n_winding_impedance_base(phases: usize, v_nom: f64, s: f64) -> Option<f64> {
1153 let phases = phases as f64;
1154 (phases > 0.0 && v_nom.is_finite() && v_nom > 0.0 && s.is_finite() && s > 0.0)
1155 .then_some(phases * v_nom * v_nom / s)
1156}
1157
1158pub(crate) fn pair_keys(n: usize) -> Vec<(usize, usize)> {
1161 let mut pairs = Vec::new();
1162 for i in 0..n {
1163 for j in i + 1..n {
1164 pairs.push((i, j));
1165 }
1166 }
1167 pairs
1168}
1169
1170pub(crate) fn square_from_rows(rows: &[Vec<f64>], n: usize) -> Option<Mat> {
1173 let mut m = vec![vec![0.0; n]; n];
1174 if rows.len() != n {
1175 return None;
1176 }
1177 let lower = rows.iter().enumerate().all(|(i, r)| r.len() == i + 1);
1178 let full = rows.iter().all(|r| r.len() == n);
1179 if lower {
1180 for (i, row) in rows.iter().enumerate() {
1181 for (j, &v) in row.iter().enumerate() {
1182 m[i][j] = v;
1183 m[j][i] = v;
1184 }
1185 }
1186 } else if full {
1187 for (i, row) in rows.iter().enumerate() {
1188 m[i].clone_from_slice(&row[..n]);
1189 }
1190 } else {
1191 return None;
1192 }
1193 Some(m)
1194}
1195
1196#[cfg(test)]
1197mod tests {
1198 use super::*;
1199
1200 #[test]
1201 #[allow(clippy::float_cmp)]
1202 fn lower_triangle_completes_symmetrically() {
1203 let rows = vec![vec![1.0], vec![0.5, 2.0], vec![0.3, 0.4, 3.0]];
1204 let m = square_from_rows(&rows, 3).unwrap();
1205 assert_eq!(m[0][1], 0.5);
1206 assert_eq!(m[1][0], 0.5);
1207 assert_eq!(m[2][2], 3.0);
1208 assert_eq!(m[0][2], 0.3);
1209 }
1210
1211 #[test]
1212 #[allow(clippy::float_cmp)]
1213 fn full_rows_pass_through() {
1214 let rows = vec![vec![1.0, 9.0], vec![8.0, 2.0]];
1215 let m = square_from_rows(&rows, 2).unwrap();
1216 assert_eq!(m[0][1], 9.0);
1217 assert_eq!(m[1][0], 8.0);
1218 }
1219
1220 #[test]
1221 fn wrong_shape_is_rejected() {
1222 assert!(square_from_rows(&[vec![1.0], vec![2.0]], 2).is_none());
1223 assert!(square_from_rows(&[vec![1.0, 2.0]], 2).is_none());
1224 }
1225}