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 DistNetwork {
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
953pub type MulticonductorNetwork = DistNetwork;
955
956impl Default for DistNetwork {
957 fn default() -> Self {
961 DistNetwork {
962 name: None,
963 base_frequency: crate::dss::defaults::BASE_FREQUENCY,
964 geo: None,
965 buses: Vec::new(),
966 linecodes: Vec::new(),
967 lines: Vec::new(),
968 switches: Vec::new(),
969 transformers: Vec::new(),
970 loads: Vec::new(),
971 generators: Vec::new(),
972 ibrs: Vec::new(),
973 control_profiles: Vec::new(),
974 shunts: Vec::new(),
975 capacitors: Vec::new(),
976 sources: Vec::new(),
977 untyped: Vec::new(),
978 commands: Vec::new(),
979 options: Vec::new(),
980 defaulted: BTreeMap::new(),
981 warnings: Vec::new(),
982 parse_diagnostics: Vec::new(),
983 source: None,
984 source_format: None,
985 extras: Extras::new(),
986 }
987 }
988}
989
990impl DistNetwork {
991 #[must_use]
992 pub fn new() -> Self {
993 Self::default()
994 }
995
996 #[must_use]
997 pub fn named(name: impl Into<String>) -> Self {
998 Self {
999 name: Some(name.into()),
1000 ..Self::default()
1001 }
1002 }
1003
1004 pub fn bus(&self, id: &str) -> Option<&DistBus> {
1006 self.buses.iter().find(|b| b.id.eq_ignore_ascii_case(id))
1007 }
1008
1009 pub fn linecode(&self, name: &str) -> Option<&DistLineCode> {
1011 self.linecodes
1012 .iter()
1013 .find(|c| c.name.eq_ignore_ascii_case(name))
1014 }
1015}
1016
1017pub(crate) fn warn_defaulted_frequency(net: &mut DistNetwork, field: &str) {
1023 let charging = net.linecodes.iter().any(|c| {
1024 [&c.b_from, &c.b_to]
1025 .iter()
1026 .flat_map(|m| m.iter())
1027 .flatten()
1028 .any(|v| v.is_finite() && v.abs() > 0.0)
1029 });
1030 if charging {
1031 net.warnings.push(format!(
1032 "document states no {field} and carries line susceptance; read at {} Hz",
1033 net.base_frequency
1034 ));
1035 }
1036}
1037
1038pub(crate) fn warn_unresolved_references(net: &mut DistNetwork) {
1046 use std::collections::BTreeSet;
1047 let buses: BTreeSet<String> = net
1048 .buses
1049 .iter()
1050 .map(|b| b.id.to_ascii_lowercase())
1051 .collect();
1052 let linecodes: BTreeSet<String> = net
1053 .linecodes
1054 .iter()
1055 .map(|c| c.name.to_ascii_lowercase())
1056 .collect();
1057 let mut warnings = Vec::new();
1058 {
1059 let mut bus = |what: &str, field: &str, id: &str| {
1060 if id.is_empty() {
1061 warnings.push(format!("{what}: `{field}` reference is empty or missing"));
1062 } else if !buses.contains(&id.to_ascii_lowercase()) {
1063 warnings.push(format!("{what}: references undefined bus `{id}`"));
1064 }
1065 };
1066 for l in &net.lines {
1067 let what = format!("line {}", l.name);
1068 bus(&what, "bus_from", &l.bus_from);
1069 bus(&what, "bus_to", &l.bus_to);
1070 }
1071 for sw in &net.switches {
1072 let what = format!("switch {}", sw.name);
1073 bus(&what, "bus_from", &sw.bus_from);
1074 bus(&what, "bus_to", &sw.bus_to);
1075 }
1076 for t in &net.transformers {
1077 let what = format!("transformer {}", t.name);
1078 for w in &t.windings {
1079 bus(&what, "bus", &w.bus);
1080 }
1081 }
1082 for (what, id) in std::iter::empty()
1083 .chain(
1084 net.loads
1085 .iter()
1086 .map(|x| (format!("load {}", x.name), &x.bus)),
1087 )
1088 .chain(
1089 net.generators
1090 .iter()
1091 .map(|x| (format!("generator {}", x.name), &x.bus)),
1092 )
1093 .chain(
1094 net.shunts
1095 .iter()
1096 .map(|x| (format!("shunt {}", x.name), &x.bus)),
1097 )
1098 .chain(
1099 net.capacitors
1100 .iter()
1101 .map(|x| (format!("capacitor {}", x.name), &x.bus)),
1102 )
1103 .chain(net.ibrs.iter().map(|x| (format!("ibr {}", x.name), &x.bus)))
1104 .chain(
1105 net.sources
1106 .iter()
1107 .map(|x| (format!("voltage_source {}", x.name), &x.bus)),
1108 )
1109 {
1110 bus(&what, "bus", id);
1111 }
1112 }
1113 for l in &net.lines {
1114 if l.linecode.is_empty() {
1115 warnings.push(format!(
1116 "line {}: `linecode` reference is empty or missing",
1117 l.name
1118 ));
1119 } else if !linecodes.contains(&l.linecode.to_ascii_lowercase()) {
1120 warnings.push(format!(
1121 "line {}: references undefined linecode `{}`",
1122 l.name, l.linecode
1123 ));
1124 }
1125 }
1126 net.warnings.extend(warnings);
1127}
1128
1129fn zero_mat(n: usize) -> Mat {
1130 vec![vec![0.0; n]; n]
1131}
1132
1133fn matrix_extent(m: &Mat) -> usize {
1134 m.iter().map(Vec::len).fold(m.len(), usize::max)
1135}
1136
1137pub(crate) fn n_winding_phase_count(conn: WindingConn, terminal_map: &[String]) -> usize {
1141 match conn {
1142 WindingConn::Wye => terminal_map.len().saturating_sub(1).max(1),
1143 WindingConn::Delta => {
1144 if terminal_map.len() == 2 {
1145 1
1146 } else {
1147 terminal_map.len().max(1)
1148 }
1149 }
1150 }
1151}
1152
1153pub(crate) fn n_winding_impedance_base(phases: usize, v_nom: f64, s: f64) -> Option<f64> {
1156 let phases = phases as f64;
1157 (phases > 0.0 && v_nom.is_finite() && v_nom > 0.0 && s.is_finite() && s > 0.0)
1158 .then_some(phases * v_nom * v_nom / s)
1159}
1160
1161pub(crate) fn pair_keys(n: usize) -> Vec<(usize, usize)> {
1164 let mut pairs = Vec::new();
1165 for i in 0..n {
1166 for j in i + 1..n {
1167 pairs.push((i, j));
1168 }
1169 }
1170 pairs
1171}
1172
1173pub(crate) fn square_from_rows(rows: &[Vec<f64>], n: usize) -> Option<Mat> {
1176 let mut m = vec![vec![0.0; n]; n];
1177 if rows.len() != n {
1178 return None;
1179 }
1180 let lower = rows.iter().enumerate().all(|(i, r)| r.len() == i + 1);
1181 let full = rows.iter().all(|r| r.len() == n);
1182 if lower {
1183 for (i, row) in rows.iter().enumerate() {
1184 for (j, &v) in row.iter().enumerate() {
1185 m[i][j] = v;
1186 m[j][i] = v;
1187 }
1188 }
1189 } else if full {
1190 for (i, row) in rows.iter().enumerate() {
1191 m[i].clone_from_slice(&row[..n]);
1192 }
1193 } else {
1194 return None;
1195 }
1196 Some(m)
1197}
1198
1199#[cfg(test)]
1200mod tests {
1201 use super::*;
1202
1203 #[test]
1204 #[allow(clippy::float_cmp)]
1205 fn lower_triangle_completes_symmetrically() {
1206 let rows = vec![vec![1.0], vec![0.5, 2.0], vec![0.3, 0.4, 3.0]];
1207 let m = square_from_rows(&rows, 3).unwrap();
1208 assert_eq!(m[0][1], 0.5);
1209 assert_eq!(m[1][0], 0.5);
1210 assert_eq!(m[2][2], 3.0);
1211 assert_eq!(m[0][2], 0.3);
1212 }
1213
1214 #[test]
1215 #[allow(clippy::float_cmp)]
1216 fn full_rows_pass_through() {
1217 let rows = vec![vec![1.0, 9.0], vec![8.0, 2.0]];
1218 let m = square_from_rows(&rows, 2).unwrap();
1219 assert_eq!(m[0][1], 9.0);
1220 assert_eq!(m[1][0], 8.0);
1221 }
1222
1223 #[test]
1224 fn wrong_shape_is_rejected() {
1225 assert!(square_from_rows(&[vec![1.0], vec![2.0]], 2).is_none());
1226 assert!(square_from_rows(&[vec![1.0, 2.0]], 2).is_none());
1227 }
1228}