1use std::collections::BTreeMap;
17use std::sync::Arc;
18
19use serde::{Deserialize, Serialize};
20
21pub type Extras = BTreeMap<String, serde_json::Value>;
22
23pub type Mat = Vec<Vec<f64>>;
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "kebab-case")]
29#[non_exhaustive]
30pub enum DistSourceFormat {
31 Dss,
32 BmopfJson,
33 PmdJson,
34}
35
36impl DistSourceFormat {
37 pub fn name(self) -> &'static str {
40 match self {
41 DistSourceFormat::Dss => "dss",
42 DistSourceFormat::PmdJson => "pmd-json",
43 DistSourceFormat::BmopfJson => "bmopf-json",
44 }
45 }
46}
47
48#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)]
49#[non_exhaustive]
50pub struct DistBus {
51 pub id: String,
52 pub terminals: Vec<String>,
54 pub grounded: Vec<String>,
56 pub v_min: Option<f64>,
60 pub v_max: Option<f64>,
61 pub vpn_min: Option<Vec<f64>>,
62 pub vpn_max: Option<Vec<f64>>,
63 pub vpp_min: Option<Vec<f64>>,
64 pub vpp_max: Option<Vec<f64>>,
65 pub vsym_min: Option<Vec<f64>>,
66 pub vsym_max: Option<Vec<f64>>,
67 pub extras: Extras,
68}
69
70impl DistBus {
71 #[must_use]
72 pub fn new(id: impl Into<String>, terminals: Vec<String>) -> Self {
73 Self {
74 id: id.into(),
75 terminals,
76 grounded: Vec::new(),
77 v_min: None,
78 v_max: None,
79 vpn_min: None,
80 vpn_max: None,
81 vpp_min: None,
82 vpp_max: None,
83 vsym_min: None,
84 vsym_max: None,
85 extras: Extras::new(),
86 }
87 }
88}
89
90#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
91#[non_exhaustive]
92pub struct DistLineCode {
93 pub name: String,
94 pub n_conductors: usize,
95 pub r_series: Mat,
97 pub x_series: Mat,
98 pub g_from: Mat,
100 pub b_from: Mat,
101 pub g_to: Mat,
102 pub b_to: Mat,
103 pub i_max: Option<Vec<f64>>,
105 pub s_max: Option<Vec<f64>>,
106 pub extras: Extras,
107}
108
109impl DistLineCode {
110 #[must_use]
111 pub fn new(name: impl Into<String>, r_series: Mat, x_series: Mat) -> Self {
112 let n_conductors = matrix_extent(&r_series).max(matrix_extent(&x_series));
113 Self {
114 name: name.into(),
115 n_conductors,
116 r_series,
117 x_series,
118 g_from: zero_mat(n_conductors),
119 b_from: zero_mat(n_conductors),
120 g_to: zero_mat(n_conductors),
121 b_to: zero_mat(n_conductors),
122 i_max: None,
123 s_max: None,
124 extras: Extras::new(),
125 }
126 }
127}
128
129#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
130#[non_exhaustive]
131pub struct DistLine {
132 pub name: String,
133 pub bus_from: String,
134 pub bus_to: String,
135 pub terminal_map_from: Vec<String>,
136 pub terminal_map_to: Vec<String>,
137 pub linecode: String,
138 pub length: f64,
140 pub extras: Extras,
141}
142
143impl DistLine {
144 #[must_use]
145 pub fn new(
146 name: impl Into<String>,
147 bus_from: impl Into<String>,
148 bus_to: impl Into<String>,
149 terminal_map_from: Vec<String>,
150 terminal_map_to: Vec<String>,
151 linecode: impl Into<String>,
152 length: f64,
153 ) -> Self {
154 Self {
155 name: name.into(),
156 bus_from: bus_from.into(),
157 bus_to: bus_to.into(),
158 terminal_map_from,
159 terminal_map_to,
160 linecode: linecode.into(),
161 length,
162 extras: Extras::new(),
163 }
164 }
165}
166
167#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
168#[non_exhaustive]
169pub struct DistSwitch {
170 pub name: String,
171 pub bus_from: String,
172 pub bus_to: String,
173 pub terminal_map_from: Vec<String>,
174 pub terminal_map_to: Vec<String>,
175 pub open: bool,
176 pub i_max: Option<Vec<f64>>,
177 pub extras: Extras,
178}
179
180impl DistSwitch {
181 #[must_use]
182 pub fn new(
183 name: impl Into<String>,
184 bus_from: impl Into<String>,
185 bus_to: impl Into<String>,
186 terminal_map_from: Vec<String>,
187 terminal_map_to: Vec<String>,
188 open: bool,
189 ) -> Self {
190 Self {
191 name: name.into(),
192 bus_from: bus_from.into(),
193 bus_to: bus_to.into(),
194 terminal_map_from,
195 terminal_map_to,
196 open,
197 i_max: None,
198 extras: Extras::new(),
199 }
200 }
201}
202
203#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
204#[serde(rename_all = "snake_case")]
205#[non_exhaustive]
206pub enum Configuration {
207 Wye,
208 Delta,
209 SinglePhase,
210}
211
212#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
213#[non_exhaustive]
214pub struct DistLoad {
215 pub name: String,
216 pub bus: String,
217 pub terminal_map: Vec<String>,
218 pub configuration: Configuration,
219 pub p_nom: Vec<f64>,
221 pub q_nom: Vec<f64>,
223 pub voltage_model: DistLoadVoltageModel,
224 pub extras: Extras,
225}
226
227impl DistLoad {
228 #[must_use]
229 pub fn new(
230 name: impl Into<String>,
231 bus: impl Into<String>,
232 terminal_map: Vec<String>,
233 configuration: Configuration,
234 p_nom: Vec<f64>,
235 q_nom: Vec<f64>,
236 ) -> Self {
237 Self {
238 name: name.into(),
239 bus: bus.into(),
240 terminal_map,
241 configuration,
242 p_nom,
243 q_nom,
244 voltage_model: DistLoadVoltageModel::default(),
245 extras: Extras::new(),
246 }
247 }
248}
249
250#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
251#[serde(tag = "model", rename_all = "snake_case")]
252#[non_exhaustive]
253pub enum DistLoadVoltageModel {
254 ConstantPower { v_nom: Vec<f64> },
257 ConstantCurrent { v_nom: Vec<f64> },
259 ConstantImpedance { v_nom: Vec<f64> },
261 Zip {
265 v_nom: Vec<f64>,
266 alpha_z: Vec<f64>,
267 alpha_i: Vec<f64>,
268 alpha_p: Vec<f64>,
269 beta_z: Vec<f64>,
270 beta_i: Vec<f64>,
271 beta_p: Vec<f64>,
272 },
273 Exponential {
276 v_nom: Vec<f64>,
277 gamma_p: Vec<f64>,
278 gamma_q: Vec<f64>,
279 },
280}
281
282impl Default for DistLoadVoltageModel {
283 fn default() -> Self {
284 Self::ConstantPower { v_nom: Vec::new() }
285 }
286}
287
288impl DistLoadVoltageModel {
289 #[must_use]
290 pub fn v_nom(&self) -> &[f64] {
291 match self {
292 Self::ConstantPower { v_nom }
293 | Self::ConstantCurrent { v_nom }
294 | Self::ConstantImpedance { v_nom }
295 | Self::Zip { v_nom, .. }
296 | Self::Exponential { v_nom, .. } => v_nom,
297 }
298 }
299}
300
301#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
302#[non_exhaustive]
303pub struct DistGenerator {
304 pub name: String,
305 pub bus: String,
306 pub terminal_map: Vec<String>,
307 pub configuration: Configuration,
308 pub p_nom: Vec<f64>,
310 pub q_nom: Vec<f64>,
311 pub p_min: Option<Vec<f64>>,
312 pub p_max: Option<Vec<f64>>,
313 pub q_min: Option<Vec<f64>>,
314 pub q_max: Option<Vec<f64>>,
315 pub cost: Option<f64>,
317 pub extras: Extras,
318}
319
320impl DistGenerator {
321 #[must_use]
322 pub fn new(
323 name: impl Into<String>,
324 bus: impl Into<String>,
325 terminal_map: Vec<String>,
326 configuration: Configuration,
327 p_nom: Vec<f64>,
328 q_nom: Vec<f64>,
329 ) -> Self {
330 Self {
331 name: name.into(),
332 bus: bus.into(),
333 terminal_map,
334 configuration,
335 p_nom,
336 q_nom,
337 p_min: None,
338 p_max: None,
339 q_min: None,
340 q_max: None,
341 cost: None,
342 extras: Extras::new(),
343 }
344 }
345}
346
347#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
348#[non_exhaustive]
349pub struct DistShunt {
350 pub name: String,
351 pub bus: String,
352 pub terminal_map: Vec<String>,
353 pub g: Mat,
355 pub b: Mat,
356 pub extras: Extras,
357}
358
359impl DistShunt {
360 #[must_use]
361 pub fn new(
362 name: impl Into<String>,
363 bus: impl Into<String>,
364 terminal_map: Vec<String>,
365 g: Mat,
366 b: Mat,
367 ) -> Self {
368 Self {
369 name: name.into(),
370 bus: bus.into(),
371 terminal_map,
372 g,
373 b,
374 extras: Extras::new(),
375 }
376 }
377}
378
379#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
380#[serde(rename_all = "snake_case")]
381#[non_exhaustive]
382pub enum WindingConn {
383 Wye,
384 Delta,
385}
386
387#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
388#[non_exhaustive]
389pub struct Winding {
390 pub bus: String,
391 pub terminal_map: Vec<String>,
392 pub conn: WindingConn,
393 pub v_ref: f64,
395 pub s_rating: f64,
397 pub r_pct: f64,
399 pub tap: f64,
400}
401
402impl Winding {
403 #[must_use]
404 pub fn new(
405 bus: impl Into<String>,
406 terminal_map: Vec<String>,
407 conn: WindingConn,
408 v_ref: f64,
409 s_rating: f64,
410 ) -> Self {
411 Self {
412 bus: bus.into(),
413 terminal_map,
414 conn,
415 v_ref,
416 s_rating,
417 r_pct: 0.0,
418 tap: 1.0,
419 }
420 }
421}
422
423#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
424#[non_exhaustive]
425pub struct DistTransformer {
426 pub name: String,
427 pub windings: Vec<Winding>,
428 pub xsc_pct: Vec<f64>,
431 pub phases: usize,
432 pub extras: Extras,
433}
434
435impl DistTransformer {
436 #[must_use]
437 pub fn new(
438 name: impl Into<String>,
439 windings: Vec<Winding>,
440 xsc_pct: Vec<f64>,
441 phases: usize,
442 ) -> Self {
443 Self {
444 name: name.into(),
445 windings,
446 xsc_pct,
447 phases,
448 extras: Extras::new(),
449 }
450 }
451}
452
453#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
454#[non_exhaustive]
455pub struct VoltageSource {
456 pub name: String,
457 pub bus: String,
458 pub terminal_map: Vec<String>,
459 pub v_magnitude: Vec<f64>,
461 pub v_angle: Vec<f64>,
463 pub extras: Extras,
464}
465
466impl VoltageSource {
467 #[must_use]
468 pub fn new(
469 name: impl Into<String>,
470 bus: impl Into<String>,
471 terminal_map: Vec<String>,
472 v_magnitude: Vec<f64>,
473 v_angle: Vec<f64>,
474 ) -> Self {
475 Self {
476 name: name.into(),
477 bus: bus.into(),
478 terminal_map,
479 v_magnitude,
480 v_angle,
481 extras: Extras::new(),
482 }
483 }
484}
485
486#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
489#[non_exhaustive]
490pub struct UntypedObject {
491 pub class: String,
492 pub name: String,
493 pub props: Vec<(Option<String>, String)>,
494}
495
496impl UntypedObject {
497 #[must_use]
498 pub fn new(
499 class: impl Into<String>,
500 name: impl Into<String>,
501 props: Vec<(Option<String>, String)>,
502 ) -> Self {
503 Self {
504 class: class.into(),
505 name: name.into(),
506 props,
507 }
508 }
509}
510
511#[derive(Clone, Debug, Serialize, Deserialize)]
517#[non_exhaustive]
518pub struct DistNetwork {
519 pub name: Option<String>,
520 pub base_frequency: f64,
522 pub buses: Vec<DistBus>,
523 pub linecodes: Vec<DistLineCode>,
524 pub lines: Vec<DistLine>,
525 pub switches: Vec<DistSwitch>,
526 pub transformers: Vec<DistTransformer>,
527 pub loads: Vec<DistLoad>,
528 pub generators: Vec<DistGenerator>,
529 pub shunts: Vec<DistShunt>,
530 pub sources: Vec<VoltageSource>,
533 pub untyped: Vec<UntypedObject>,
534 pub commands: Vec<(String, String)>,
537 pub options: Vec<(String, String)>,
538 #[serde(skip)]
545 pub defaulted: BTreeMap<String, Vec<&'static str>>,
546 pub warnings: Vec<String>,
547 #[serde(skip)]
552 pub source: Option<Arc<String>>,
553 pub source_format: Option<DistSourceFormat>,
554 pub extras: Extras,
555}
556
557pub type MulticonductorNetwork = DistNetwork;
559
560impl Default for DistNetwork {
561 fn default() -> Self {
565 DistNetwork {
566 name: None,
567 base_frequency: crate::dss::defaults::BASE_FREQUENCY,
568 buses: Vec::new(),
569 linecodes: Vec::new(),
570 lines: Vec::new(),
571 switches: Vec::new(),
572 transformers: Vec::new(),
573 loads: Vec::new(),
574 generators: Vec::new(),
575 shunts: Vec::new(),
576 sources: Vec::new(),
577 untyped: Vec::new(),
578 commands: Vec::new(),
579 options: Vec::new(),
580 defaulted: BTreeMap::new(),
581 warnings: Vec::new(),
582 source: None,
583 source_format: None,
584 extras: Extras::new(),
585 }
586 }
587}
588
589impl DistNetwork {
590 #[must_use]
591 pub fn new() -> Self {
592 Self::default()
593 }
594
595 #[must_use]
596 pub fn named(name: impl Into<String>) -> Self {
597 Self {
598 name: Some(name.into()),
599 ..Self::default()
600 }
601 }
602
603 pub fn bus(&self, id: &str) -> Option<&DistBus> {
605 self.buses.iter().find(|b| b.id.eq_ignore_ascii_case(id))
606 }
607
608 pub fn linecode(&self, name: &str) -> Option<&DistLineCode> {
610 self.linecodes
611 .iter()
612 .find(|c| c.name.eq_ignore_ascii_case(name))
613 }
614}
615
616fn zero_mat(n: usize) -> Mat {
617 vec![vec![0.0; n]; n]
618}
619
620fn matrix_extent(m: &Mat) -> usize {
621 m.iter().map(Vec::len).fold(m.len(), usize::max)
622}
623
624pub(crate) fn n_winding_phase_count(conn: WindingConn, terminal_map: &[String]) -> usize {
628 match conn {
629 WindingConn::Wye => terminal_map.len().saturating_sub(1).max(1),
630 WindingConn::Delta => {
631 if terminal_map.len() == 2 {
632 1
633 } else {
634 terminal_map.len().max(1)
635 }
636 }
637 }
638}
639
640pub(crate) fn n_winding_impedance_base(phases: usize, v_nom: f64, s: f64) -> Option<f64> {
643 let phases = phases as f64;
644 (phases > 0.0 && v_nom.is_finite() && v_nom > 0.0 && s.is_finite() && s > 0.0)
645 .then_some(phases * v_nom * v_nom / s)
646}
647
648pub(crate) fn pair_keys(n: usize) -> Vec<(usize, usize)> {
651 let mut pairs = Vec::new();
652 for i in 0..n {
653 for j in i + 1..n {
654 pairs.push((i, j));
655 }
656 }
657 pairs
658}
659
660pub(crate) fn square_from_rows(rows: &[Vec<f64>], n: usize) -> Option<Mat> {
663 let mut m = vec![vec![0.0; n]; n];
664 if rows.len() != n {
665 return None;
666 }
667 let lower = rows.iter().enumerate().all(|(i, r)| r.len() == i + 1);
668 let full = rows.iter().all(|r| r.len() == n);
669 if lower {
670 for (i, row) in rows.iter().enumerate() {
671 for (j, &v) in row.iter().enumerate() {
672 m[i][j] = v;
673 m[j][i] = v;
674 }
675 }
676 } else if full {
677 for (i, row) in rows.iter().enumerate() {
678 m[i].clone_from_slice(&row[..n]);
679 }
680 } else {
681 return None;
682 }
683 Some(m)
684}
685
686#[cfg(test)]
687mod tests {
688 use super::*;
689
690 #[test]
691 #[allow(clippy::float_cmp)]
692 fn lower_triangle_completes_symmetrically() {
693 let rows = vec![vec![1.0], vec![0.5, 2.0], vec![0.3, 0.4, 3.0]];
694 let m = square_from_rows(&rows, 3).unwrap();
695 assert_eq!(m[0][1], 0.5);
696 assert_eq!(m[1][0], 0.5);
697 assert_eq!(m[2][2], 3.0);
698 assert_eq!(m[0][2], 0.3);
699 }
700
701 #[test]
702 #[allow(clippy::float_cmp)]
703 fn full_rows_pass_through() {
704 let rows = vec![vec![1.0, 9.0], vec![8.0, 2.0]];
705 let m = square_from_rows(&rows, 2).unwrap();
706 assert_eq!(m[0][1], 9.0);
707 assert_eq!(m[1][0], 8.0);
708 }
709
710 #[test]
711 fn wrong_shape_is_rejected() {
712 assert!(square_from_rows(&[vec![1.0], vec![2.0]], 2).is_none());
713 assert!(square_from_rows(&[vec![1.0, 2.0]], 2).is_none());
714 }
715}