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