1use std::path::Path;
12use std::sync::Arc;
13
14use serde_json::{Map, Value};
15
16use crate::error::{Error, Result};
17use crate::model::{
18 Configuration, DistBus, DistGenerator, DistLine, DistLineCode, DistLoad, DistLoadVoltageModel,
19 DistShunt, DistSourceFormat, DistSwitch, DistTransformer, Extras, Mat, MulticonductorNetwork,
20 UntypedObject, VoltageSource, Winding, WindingConn,
21};
22
23pub fn parse_pmd_file(path: impl AsRef<Path>) -> Result<MulticonductorNetwork> {
24 let path = path.as_ref();
25 let text = std::fs::read_to_string(path).map_err(|source| Error::Io {
26 path: path.display().to_string(),
27 source,
28 })?;
29 parse_pmd_str(&text)
30}
31
32pub fn parse_pmd_str(text: &str) -> Result<MulticonductorNetwork> {
33 let doc: Value = serde_json::from_str(text).map_err(|e| Error::Json {
34 format: "PMD",
35 message: e.to_string(),
36 })?;
37 let Value::Object(doc) = doc else {
38 return Err(Error::Json {
39 format: "PMD",
40 message: "top level is not an object".into(),
41 });
42 };
43 if let Some(dm) = doc.get("data_model").and_then(Value::as_str) {
48 if !dm.eq_ignore_ascii_case("ENGINEERING") {
49 return Err(Error::Json {
50 format: "PMD",
51 message: format!(
52 "`data_model` is `{dm}`; this reader supports the ENGINEERING model \
53 (convert MATHEMATICAL output back with \
54 PowerModelsDistribution.transform_solution or export the ENGINEERING \
55 model)"
56 ),
57 });
58 }
59 }
60 let mut net = MulticonductorNetwork {
61 source: Some(Arc::new(text.to_string())),
62 source_format: Some(DistSourceFormat::PmdJson),
63 base_frequency: 60.0,
64 ..MulticonductorNetwork::default()
65 };
66 let mut rd = Reader { net: &mut net };
67 rd.document(&doc);
68 crate::model::warn_unresolved_references(&mut net);
69 Ok(net)
70}
71
72struct Reader<'a> {
73 net: &'a mut MulticonductorNetwork,
74}
75
76fn restore(key: &str, v: &Value) -> f64 {
78 if v.is_null() {
79 if key.ends_with("_ub") || key.ends_with("max") {
80 f64::INFINITY
81 } else if key.ends_with("_lb") || key.ends_with("min") {
82 f64::NEG_INFINITY
83 } else {
84 f64::NAN
85 }
86 } else {
87 v.as_f64().unwrap_or(f64::NAN)
88 }
89}
90
91fn floats(key: &str, v: Option<&Value>) -> Option<Vec<f64>> {
92 v?.as_array()
93 .map(|a| a.iter().map(|x| restore(key, x)).collect())
94}
95
96const MAX_MATRIX_DIM: usize = 64;
102
103fn matrix(key: &str, v: Option<&Value>, what: &str, warnings: &mut Vec<String>) -> Option<Mat> {
109 let v = v?;
110 let Some(cols) = v.as_array() else {
111 warnings.push(format!(
112 "{what}: `{key}` is not an array of columns; dropped"
113 ));
114 return None;
115 };
116 let n = cols.len();
117 if n > MAX_MATRIX_DIM {
118 warnings.push(format!(
119 "{key}: matrix dimension {n} exceeds the supported maximum of \
120 {MAX_MATRIX_DIM}; dropped"
121 ));
122 return None;
123 }
124 let mut m = vec![vec![0.0; n]; n];
125 for (j, col) in cols.iter().enumerate() {
126 let Some(col) = col.as_array() else {
127 warnings.push(format!(
128 "{what}: `{key}` column {j} is not an array; kept as zeros"
129 ));
130 continue;
131 };
132 for (i, x) in col.iter().enumerate().take(n) {
133 m[i][j] = restore(key, x);
134 }
135 }
136 Some(m)
137}
138
139fn ints_as_strings(v: Option<&Value>) -> Vec<String> {
140 v.and_then(Value::as_array)
141 .map(|a| {
142 a.iter()
143 .map(|x| {
144 x.as_i64().map_or_else(
145 || x.as_str().unwrap_or_default().to_string(),
146 |i| i.to_string(),
147 )
148 })
149 .collect()
150 })
151 .unwrap_or_default()
152}
153
154fn string(v: Option<&Value>) -> String {
155 v.and_then(Value::as_str).unwrap_or_default().to_string()
156}
157
158fn pad_to(m: Mat, n: usize) -> Mat {
160 if m.len() >= n {
161 return m;
162 }
163 let mut out = vec![vec![0.0; n]; n];
164 for (i, row) in m.into_iter().enumerate() {
165 for (j, v) in row.into_iter().enumerate() {
166 out[i][j] = v;
167 }
168 }
169 out
170}
171
172fn take_extras(o: &Map<String, Value>, known: &[&str]) -> Extras {
176 o.iter()
177 .filter(|(k, _)| !known.contains(&k.as_str()) && k.as_str() != "name")
179 .map(|(k, v)| (k.clone(), v.clone()))
180 .collect()
181}
182
183fn stash_status(
186 o: &Map<String, Value>,
187 extras: &mut Extras,
188 what: &str,
189 warnings: &mut Vec<String>,
190) {
191 if let Some(s) = o.get("status").and_then(Value::as_str)
192 && s != "ENABLED"
193 {
194 extras.insert("pmd_status".into(), Value::String(s.to_string()));
195 warnings.push(format!(
196 "{what}: status {s} kept in extras; other formats emit the element enabled"
197 ));
198 }
199}
200
201fn linecode_from(
206 name: &str,
207 o: &Map<String, Value>,
208 base_frequency: f64,
209 warnings: &mut Vec<String>,
210) -> DistLineCode {
211 let what = format!("linecode {name}");
212 let mut mat = |key: &str| matrix(key, o.get(key), &what, warnings);
213 let mats = [
214 mat("rs"),
215 mat("xs"),
216 mat("g_fr"),
217 mat("g_to"),
218 mat("b_fr"),
219 mat("b_to"),
220 ];
221 let n = mats.iter().flatten().map(Vec::len).max().unwrap_or(0);
224 if mats.iter().flatten().any(|m| m.len() < n) {
225 warnings.push(format!(
226 "linecode {name}: matrix sizes disagree; smaller ones padded \
227 with zeros to {n}x{n}"
228 ));
229 }
230 let [r, x, gf, gt, bf, bt] = mats.map(|m| pad_to(m.unwrap_or_default(), n));
231 let omega = std::f64::consts::TAU * base_frequency * 1e-9;
234 let to_b = |m: Mat| -> Mat {
235 m.into_iter()
236 .map(|row| row.into_iter().map(|v| v * omega).collect())
237 .collect()
238 };
239 DistLineCode {
240 name: name.to_string(),
241 n_conductors: n,
242 x_series: x,
243 g_from: gf,
244 g_to: gt,
245 b_from: to_b(bf),
246 b_to: to_b(bt),
247 r_series: r,
248 i_max: floats("cm_ub", o.get("cm_ub")),
252 s_max: floats("sm_ub", o.get("sm_ub")),
253 source: None,
254 extras: {
255 let mut extras = Extras::new();
258 if let Some(b) = o.get("b_fr") {
259 extras.insert("pmd_b_fr".into(), b.clone());
260 }
261 if let Some(b) = o.get("b_to") {
262 extras.insert("pmd_b_to".into(), b.clone());
263 }
264 extras
265 },
266 }
267}
268
269#[allow(clippy::float_cmp)]
274fn representative_taps(tm_set: Option<&Value>) -> (Vec<f64>, bool) {
275 let mut firsts = Vec::new();
276 let mut differ = false;
277 for w in tm_set
278 .and_then(Value::as_array)
279 .map(Vec::as_slice)
280 .unwrap_or_default()
281 {
282 let taps: Vec<f64> = w
283 .as_array()
284 .map(|p| p.iter().map(|v| restore("tm_set", v)).collect())
285 .unwrap_or_default();
286 let first = taps.first().copied().unwrap_or(1.0);
287 differ |= taps.iter().any(|&t| t != first);
288 firsts.push(first);
289 }
290 (firsts, differ)
291}
292
293struct WindingNums<'a> {
294 rw: &'a [f64],
295 xsc: &'a [f64],
296 sm_nom: &'a [f64],
297 vm_nom: &'a [f64],
298 tm_set: &'a [f64],
299}
300
301fn build_windings(
306 buses: &[String],
307 configs: &[WindingConn],
308 polarity: &[i64],
309 o: &Map<String, Value>,
310 nums: &WindingNums,
311) -> (Vec<Winding>, usize, bool) {
312 let _ = nums.xsc;
313 let mut windings = Vec::with_capacity(buses.len());
314 let mut phases = 1;
315 let mut unrolled = false;
316 for (w, bus) in buses.iter().enumerate() {
317 let mut map = ints_as_strings(
318 o.get("connections")
319 .and_then(Value::as_array)
320 .and_then(|a| a.get(w)),
321 );
322 let conn = configs.get(w).copied().unwrap_or(WindingConn::Wye);
323 if polarity.get(w) == Some(&-1)
324 && conn == WindingConn::Wye
325 && configs.first() == Some(&WindingConn::Delta)
326 && map.len() > 1
327 {
328 let phases_part = map.len() - 1;
329 map[..phases_part].rotate_right(1);
330 unrolled = true;
331 }
332 if conn == WindingConn::Wye {
333 phases = phases.max(map.len().saturating_sub(1));
334 } else {
335 phases = phases.max(map.len());
336 }
337 windings.push(Winding {
338 bus: bus.clone(),
339 terminal_map: map,
340 conn,
341 v_ref: nums.vm_nom.get(w).copied().unwrap_or(f64::NAN) * 1e3,
342 s_rating: nums.sm_nom.get(w).copied().unwrap_or(f64::NAN) * 1e3,
343 r_pct: nums.rw.get(w).copied().unwrap_or(0.0) * 100.0,
344 tap: nums.tm_set.get(w).copied().unwrap_or(1.0),
345 r_neutral: None,
346 x_neutral: None,
347 });
348 }
349 (windings, phases, unrolled)
350}
351
352const SECTIONS: &[&str] = &[
359 "bus",
360 "linecode",
361 "line",
362 "switch",
363 "load",
364 "generator",
365 "shunt",
366 "voltage_source",
367 "transformer",
368];
369
370impl Reader<'_> {
371 fn document(&mut self, doc: &Map<String, Value>) {
372 if let Some(name) = doc.get("name").and_then(Value::as_str) {
373 self.net.name = Some(name.to_string());
374 }
375 let settings = doc.get("settings").and_then(Value::as_object);
376 let stated = settings
377 .and_then(|s| s.get("base_frequency"))
378 .and_then(Value::as_f64);
379 if let Some(f) = stated {
380 self.net.base_frequency = f;
381 }
382 if let Some(settings) = settings {
383 self.net
384 .extras
385 .insert("pmd_settings".into(), Value::Object(settings.clone()));
386 }
387 for key in ["data_model", "files", "conductor_ids", "per_unit"] {
388 if let Some(v) = doc.get(key) {
389 self.net.extras.insert(format!("pmd_{key}"), v.clone());
390 }
391 }
392
393 for &key in SECTIONS {
394 let Some(Value::Object(items)) = doc.get(key) else {
395 continue;
396 };
397 match key {
398 "bus" => self.buses(items),
399 "linecode" => self.linecodes(items),
400 "line" => self.lines(items),
401 "switch" => self.switches(items),
402 "load" => self.loads(items),
403 "generator" => self.generators(items),
404 "shunt" => self.shunts(items),
405 "voltage_source" => self.sources(items),
406 "transformer" => self.transformers(items),
407 _ => unreachable!(),
408 }
409 }
410 for (key, value) in doc {
411 if SECTIONS.contains(&key.as_str()) || key == "settings" || key == "name" {
412 continue;
413 }
414 let Value::Object(items) = value else {
415 continue;
416 };
417 self.net.warnings.push(format!(
418 "ENGINEERING `{key}` components are not typed; kept untyped"
419 ));
420 for (name, v) in items {
421 self.net.untyped.push(UntypedObject {
422 class: key.clone(),
423 name: name.clone(),
424 props: vec![(None, v.to_string())],
425 });
426 }
427 }
428 if stated.is_none() {
429 crate::model::warn_defaulted_frequency(self.net, "settings.base_frequency");
430 }
431 }
432
433 fn buses(&mut self, items: &Map<String, Value>) {
434 for (id, v) in items {
435 let Value::Object(o) = v else { continue };
436 let mut extras = take_extras(
437 o,
438 &["terminals", "grounded", "rg", "xg", "status", "lat", "lon"],
439 );
440 if let Some(x) = o.get("lon") {
441 extras.insert("x".into(), x.clone());
442 }
443 if let Some(y) = o.get("lat") {
444 extras.insert("y".into(), y.clone());
445 }
446 let rg = floats("rg", o.get("rg")).unwrap_or_default();
447 let xg = floats("xg", o.get("xg")).unwrap_or_default();
448 if rg.iter().any(|&r| r != 0.0) || xg.iter().any(|&x| x != 0.0) {
449 self.net.warnings.push(format!(
450 "bus {id}: nonzero grounding impedance is not typed; kept in extras"
451 ));
452 extras.insert("rg".into(), o.get("rg").cloned().unwrap_or(Value::Null));
453 extras.insert("xg".into(), o.get("xg").cloned().unwrap_or(Value::Null));
454 }
455 stash_status(o, &mut extras, &format!("bus {id}"), &mut self.net.warnings);
456 self.net.buses.push(DistBus {
457 id: id.clone(),
458 terminals: ints_as_strings(o.get("terminals")),
459 grounded: ints_as_strings(o.get("grounded")),
460 extras,
461 ..DistBus::default()
462 });
463 }
464 }
465
466 fn linecodes(&mut self, items: &Map<String, Value>) {
467 for (name, v) in items {
468 let Value::Object(o) = v else { continue };
469 let mut lc = linecode_from(name, o, self.net.base_frequency, &mut self.net.warnings);
470 let mut extras = take_extras(
471 o,
472 &["rs", "xs", "g_fr", "g_to", "b_fr", "b_to", "cm_ub", "sm_ub"],
473 );
474 extras.append(&mut lc.extras);
475 lc.extras = extras;
476 self.net.linecodes.push(lc);
477 }
478 }
479
480 fn lines(&mut self, items: &Map<String, Value>) {
481 for (name, v) in items {
482 let Value::Object(o) = v else { continue };
483 let mut known = vec![
484 "f_bus",
485 "t_bus",
486 "f_connections",
487 "t_connections",
488 "linecode",
489 "length",
490 "status",
491 "source_id",
492 ];
493 let mut linecode = string(o.get("linecode"));
494 let mut extras;
495 let mut i_max = None;
496 let mut s_max = None;
497 if linecode.is_empty() && o.get("rs").is_some() {
502 known.extend(["rs", "xs", "g_fr", "g_to", "b_fr", "b_to", "cm_ub", "sm_ub"]);
503 extras = take_extras(o, &known);
504 let mut lc_name = format!("{name}_z");
505 let mut k = 2;
506 while self.net.linecode(&lc_name).is_some() {
507 lc_name = format!("{name}_z{k}");
508 k += 1;
509 }
510 let lc =
511 linecode_from(&lc_name, o, self.net.base_frequency, &mut self.net.warnings);
512 self.net.linecodes.push(lc);
513 self.net.warnings.push(format!(
514 "line {name}: inline impedance materialized as linecode {lc_name}; the PMD writer re-inlines it"
515 ));
516 extras.insert("pmd_inline".into(), Value::Bool(true));
517 linecode = lc_name;
518 } else {
519 known.extend(["cm_ub", "sm_ub"]);
520 extras = take_extras(o, &known);
521 i_max = floats("cm_ub", o.get("cm_ub"));
522 s_max = floats("sm_ub", o.get("sm_ub"));
523 }
524 stash_status(
525 o,
526 &mut extras,
527 &format!("line {name}"),
528 &mut self.net.warnings,
529 );
530 self.net.lines.push(DistLine {
531 name: name.clone(),
532 bus_from: string(o.get("f_bus")),
533 bus_to: string(o.get("t_bus")),
534 terminal_map_from: ints_as_strings(o.get("f_connections")),
535 terminal_map_to: ints_as_strings(o.get("t_connections")),
536 linecode,
537 length: o.get("length").map_or(f64::NAN, |v| restore("length", v)),
538 route: None,
539 i_max,
540 s_max,
541 extras,
542 });
543 }
544 }
545
546 fn switches(&mut self, items: &Map<String, Value>) {
547 for (name, v) in items {
548 let Value::Object(o) = v else { continue };
549 let mut extras = take_extras(
550 o,
551 &[
552 "f_bus",
553 "t_bus",
554 "f_connections",
555 "t_connections",
556 "state",
557 "cm_ub",
558 "status",
559 "source_id",
560 "dispatchable",
561 "rs",
562 "xs",
563 "g_fr",
564 "g_to",
565 "b_fr",
566 "b_to",
567 ],
568 );
569 for key in ["rs", "xs"] {
573 if let Some(m) = o.get(key) {
574 extras.insert(format!("pmd_{key}"), m.clone());
575 }
576 }
577 stash_status(
578 o,
579 &mut extras,
580 &format!("switch {name}"),
581 &mut self.net.warnings,
582 );
583 self.net.switches.push(DistSwitch {
584 name: name.clone(),
585 bus_from: string(o.get("f_bus")),
586 bus_to: string(o.get("t_bus")),
587 terminal_map_from: ints_as_strings(o.get("f_connections")),
588 terminal_map_to: ints_as_strings(o.get("t_connections")),
589 open: o.get("state").and_then(Value::as_str) == Some("OPEN"),
590 i_max: floats("cm_ub", o.get("cm_ub")),
591 extras,
592 });
593 }
594 }
595
596 fn loads(&mut self, items: &Map<String, Value>) {
597 for (name, v) in items {
598 let Value::Object(o) = v else { continue };
599 let connections = ints_as_strings(o.get("connections"));
600 let configuration = match o.get("configuration").and_then(Value::as_str) {
601 Some("DELTA") if connections.len() > 2 => Configuration::Delta,
602 _ if connections.len() <= 2 => Configuration::SinglePhase,
603 Some("DELTA") => Configuration::Delta,
604 _ => Configuration::Wye,
605 };
606 let scale = |key: &str| {
607 floats(key, o.get(key))
608 .unwrap_or_default()
609 .iter()
610 .map(|v| v * 1e3)
611 .collect::<Vec<_>>()
612 };
613 let mut extras = take_extras(
614 o,
615 &[
616 "bus",
617 "connections",
618 "configuration",
619 "pd_nom",
620 "qd_nom",
621 "status",
622 "source_id",
623 "dispatchable",
624 "vm_nom",
625 "model",
626 ],
627 );
628 if let Some(kv) = o.get("vm_nom") {
629 extras.insert("kv".into(), kv.clone());
630 }
631 if let Some(model) = o.get("model").and_then(Value::as_str) {
632 let dss_model = match model {
633 "IMPEDANCE" => 2,
634 "CURRENT" => 5,
635 "ZIPV" => 8,
636 _ => 1,
637 };
638 if dss_model != 1 {
639 extras.insert("model".into(), dss_model.into());
640 }
641 }
642 let v_nom: Vec<f64> = floats("vm_nom", o.get("vm_nom"))
643 .or_else(|| o.get("vm_nom").map(|v| vec![restore("vm_nom", v)]))
644 .unwrap_or_default()
645 .iter()
646 .map(|v| v * 1e3)
647 .collect();
648 let voltage_model = match o.get("model").and_then(Value::as_str) {
649 Some("IMPEDANCE") => DistLoadVoltageModel::ConstantImpedance { v_nom },
650 Some("CURRENT") => DistLoadVoltageModel::ConstantCurrent { v_nom },
651 Some("ZIPV") => DistLoadVoltageModel::Zip {
652 v_nom,
653 alpha_z: Vec::new(),
654 alpha_i: Vec::new(),
655 alpha_p: Vec::new(),
656 beta_z: Vec::new(),
657 beta_i: Vec::new(),
658 beta_p: Vec::new(),
659 },
660 _ => DistLoadVoltageModel::ConstantPower { v_nom },
661 };
662 stash_status(
663 o,
664 &mut extras,
665 &format!("load {name}"),
666 &mut self.net.warnings,
667 );
668 self.net.loads.push(DistLoad {
669 name: name.clone(),
670 bus: string(o.get("bus")),
671 terminal_map: connections,
672 configuration,
673 p_nom: scale("pd_nom"),
674 q_nom: scale("qd_nom"),
675 voltage_model,
676 extras,
677 });
678 }
679 }
680
681 fn generators(&mut self, items: &Map<String, Value>) {
682 for (name, v) in items {
683 let Value::Object(o) = v else { continue };
684 let scale = |key: &str| {
685 floats(key, o.get(key)).map(|v| v.iter().map(|x| x * 1e3).collect::<Vec<f64>>())
686 };
687 let mut extras = take_extras(
688 o,
689 &[
690 "bus",
691 "connections",
692 "configuration",
693 "pg",
694 "qg",
695 "pg_lb",
696 "pg_ub",
697 "qg_lb",
698 "qg_ub",
699 "status",
700 "source_id",
701 ],
702 );
703 stash_status(
704 o,
705 &mut extras,
706 &format!("generator {name}"),
707 &mut self.net.warnings,
708 );
709 self.net.generators.push(DistGenerator {
710 name: name.clone(),
711 bus: string(o.get("bus")),
712 terminal_map: ints_as_strings(o.get("connections")),
713 configuration: match o.get("configuration").and_then(Value::as_str) {
714 Some("DELTA") => Configuration::Delta,
715 _ => Configuration::Wye,
716 },
717 p_nom: scale("pg").unwrap_or_default(),
718 q_nom: scale("qg").unwrap_or_default(),
719 p_min: scale("pg_lb"),
723 p_max: scale("pg_ub"),
724 q_min: scale("qg_lb"),
725 q_max: scale("qg_ub"),
726 cost: None,
727 s_max: None,
728 i_max: None,
729 extras,
730 });
731 }
732 }
733
734 fn shunts(&mut self, items: &Map<String, Value>) {
735 for (name, v) in items {
736 let Value::Object(o) = v else { continue };
737 let what = format!("shunt {name}");
738 let g = matrix("gs", o.get("gs"), &what, &mut self.net.warnings).unwrap_or_default();
739 let b = matrix("bs", o.get("bs"), &what, &mut self.net.warnings).unwrap_or_default();
740 let mut extras = take_extras(
741 o,
742 &["bus", "connections", "gs", "bs", "status", "source_id"],
743 );
744 stash_status(
745 o,
746 &mut extras,
747 &format!("shunt {name}"),
748 &mut self.net.warnings,
749 );
750 self.net.shunts.push(DistShunt {
751 name: name.clone(),
752 bus: string(o.get("bus")),
753 terminal_map: ints_as_strings(o.get("connections")),
754 g,
755 b,
756 extras,
757 });
758 }
759 }
760
761 fn sources(&mut self, items: &Map<String, Value>) {
762 for (name, v) in items {
763 let Value::Object(o) = v else { continue };
764 let mut extras = take_extras(
765 o,
766 &["bus", "connections", "vm", "va", "status", "source_id"],
767 );
768 stash_status(
769 o,
770 &mut extras,
771 &format!("voltage source {name}"),
772 &mut self.net.warnings,
773 );
774 self.net.sources.push(VoltageSource {
775 name: name.clone(),
776 bus: string(o.get("bus")),
777 terminal_map: ints_as_strings(o.get("connections")),
778 v_magnitude: floats("vm", o.get("vm"))
779 .unwrap_or_default()
780 .iter()
781 .map(|v| v * 1e3)
782 .collect(),
783 v_angle: floats("va", o.get("va"))
784 .unwrap_or_default()
785 .iter()
786 .map(|a| a.to_radians())
787 .collect(),
788 extras,
789 });
790 }
791 }
792
793 fn transformers(&mut self, items: &Map<String, Value>) {
794 for (name, v) in items {
795 let Value::Object(o) = v else { continue };
796 let t = self.transformer(name, o);
797 self.net.transformers.push(t);
798 }
799 }
800
801 fn stash_polarity(
805 &mut self,
806 name: &str,
807 o: &Map<String, Value>,
808 windings: &[Winding],
809 polarity: &[i64],
810 unrolled: bool,
811 extras: &mut Extras,
812 ) {
813 let file_polarity: Vec<i64> = (0..windings.len())
814 .map(|w| polarity.get(w).copied().unwrap_or(1))
815 .collect();
816 if file_polarity == super::write::lag_polarity(windings) {
817 return;
818 }
819 extras.insert(
820 "pmd_polarity".into(),
821 o.get("polarity")
822 .cloned()
823 .unwrap_or_else(|| file_polarity.clone().into()),
824 );
825 if unrolled && let Some(c) = o.get("connections") {
826 extras.insert("pmd_connections".into(), c.clone());
827 }
828 self.net.warnings.push(format!(
829 "transformer {name}: polarity {file_polarity:?} is not the lag convention; kept in extras (other formats assume lag)"
830 ));
831 }
832
833 fn transformer(&mut self, name: &str, o: &Map<String, Value>) -> DistTransformer {
834 let mut buses = ints_as_strings(o.get("bus"));
838 if buses.len() > MAX_MATRIX_DIM {
839 self.net.warnings.push(format!(
840 "transformer {name}: winding count {} exceeds the supported maximum of \
841 {MAX_MATRIX_DIM}; extra windings dropped",
842 buses.len()
843 ));
844 buses.truncate(MAX_MATRIX_DIM);
845 }
846 let configs: Vec<WindingConn> = o
847 .get("configuration")
848 .and_then(Value::as_array)
849 .map(|a| {
850 a.iter()
851 .map(|c| {
852 if c.as_str() == Some("DELTA") {
853 WindingConn::Delta
854 } else {
855 WindingConn::Wye
856 }
857 })
858 .collect()
859 })
860 .unwrap_or_default();
861 let polarity: Vec<i64> = o
862 .get("polarity")
863 .and_then(Value::as_array)
864 .map(|a| a.iter().map(|p| p.as_i64().unwrap_or(1)).collect())
865 .unwrap_or_default();
866 let rw = floats("rw", o.get("rw")).unwrap_or_default();
867 let xsc = floats("xsc", o.get("xsc")).unwrap_or_default();
868 let sm_nom = floats("sm_nom", o.get("sm_nom")).unwrap_or_default();
869 let vm_nom = floats("vm_nom", o.get("vm_nom")).unwrap_or_default();
870 let (tm_set, taps_differ) = representative_taps(o.get("tm_set"));
871 if taps_differ {
872 self.net.warnings.push(format!(
873 "transformer {name}: per phase taps differ; the winding tap keeps the first phase (full arrays in extras)"
874 ));
875 }
876
877 let (windings, phases, unrolled) = build_windings(
878 &buses,
879 &configs,
880 &polarity,
881 o,
882 &WindingNums {
883 rw: &rw,
884 xsc: &xsc,
885 sm_nom: &sm_nom,
886 vm_nom: &vm_nom,
887 tm_set: &tm_set,
888 },
889 );
890
891 if o.get("controls").is_some() {
892 self.net.warnings.push(format!(
893 "transformer {name}: regulator controls are not typed; kept in extras"
894 ));
895 }
896 let mut extras = take_extras(
897 o,
898 &[
899 "bus",
900 "connections",
901 "configuration",
902 "polarity",
903 "rw",
904 "xsc",
905 "sm_nom",
906 "vm_nom",
907 "tm_set",
908 "tm_fix",
909 "tm_lb",
910 "tm_ub",
911 "tm_step",
912 "status",
913 "source_id",
914 "noloadloss",
915 "cmag",
916 "sm_ub",
917 ],
918 );
919 for key in ["tm_set", "tm_lb", "tm_ub", "tm_fix", "tm_step"] {
920 if let Some(v) = o.get(key) {
921 extras.insert(format!("pmd_{key}"), v.clone());
922 }
923 }
924 self.stash_polarity(name, o, &windings, &polarity, unrolled, &mut extras);
925 stash_status(
926 o,
927 &mut extras,
928 &format!("transformer {name}"),
929 &mut self.net.warnings,
930 );
931 DistTransformer {
932 name: name.to_string(),
933 windings,
934 xsc_pct: xsc.iter().map(|x| x * 100.0).collect(),
935 phases,
936 extras,
937 }
938 }
939}