1use std::collections::{BTreeMap, BTreeSet};
15
16use serde_json::{Map, Value, json};
17
18use crate::convert::Conversion;
19use crate::geo::CoordinateSpace;
20use crate::model::{
21 Configuration, DistBus, DistLine, DistLineCode, DistLoadVoltageModel, DistTransformer, Extras,
22 Mat, MulticonductorNetwork, VoltageSource, Winding, WindingConn,
23};
24
25pub fn write_pmd_json(net: &MulticonductorNetwork) -> Conversion {
32 let mut w = Writer {
33 warnings: Vec::new(),
34 renamed_terminals: renamed_terminals(net),
35 };
36 let doc = w.document(net);
37 Conversion {
38 text: serde_json::to_string_pretty(&doc).expect("maps and finite numbers") + "\n",
39 sidecars: Vec::new(),
40 warnings: w.warnings,
41 diagnostics: Vec::new(),
42 }
43}
44
45const MAX_DIM: usize = 64;
53
54struct Writer {
55 warnings: Vec<String>,
56 renamed_terminals: BTreeMap<String, i64>,
58}
59
60fn renamed_terminals(net: &MulticonductorNetwork) -> BTreeMap<String, i64> {
76 let mut used = BTreeSet::new();
77 let mut names = BTreeSet::new();
78 for terminal in all_terminal_names(net) {
79 match terminal.parse::<i64>() {
80 Ok(value) => {
81 used.insert(value);
82 }
83 Err(_) => {
84 names.insert(terminal.to_string());
85 }
86 }
87 }
88 let mut out = BTreeMap::new();
89 let mut next = 1i64;
90 for name in names {
91 while used.contains(&next) {
92 next += 1;
93 }
94 used.insert(next);
95 out.insert(name, next);
96 next += 1;
97 }
98 out
99}
100
101impl Writer {
102 fn conns(&mut self, map: &[String], what: &str) -> Vec<i64> {
106 map.iter()
107 .map(|t| {
108 if let Ok(value) = t.parse::<i64>() {
109 return value;
110 }
111 let id = self.renamed_terminals.get(t).copied().unwrap_or(0);
112 self.warnings.push(format!(
113 "{what}: terminal `{t}` is not numeric; emitted as {id}"
114 ));
115 id
116 })
117 .collect()
118 }
119}
120
121fn all_terminal_names(net: &MulticonductorNetwork) -> impl Iterator<Item = &str> {
122 let maps = net
123 .lines
124 .iter()
125 .flat_map(|l| [&l.terminal_map_from, &l.terminal_map_to])
126 .chain(
127 net.switches
128 .iter()
129 .flat_map(|s| [&s.terminal_map_from, &s.terminal_map_to]),
130 )
131 .chain(net.loads.iter().map(|l| &l.terminal_map))
132 .chain(net.generators.iter().map(|g| &g.terminal_map))
133 .chain(net.capacitors.iter().map(|c| &c.terminal_map))
134 .chain(net.shunts.iter().map(|s| &s.terminal_map))
135 .chain(net.sources.iter().map(|s| &s.terminal_map))
136 .chain(net.ibrs.iter().map(|i| &i.terminal_map))
137 .chain(
138 net.transformers
139 .iter()
140 .flat_map(|t| t.windings.iter().map(|w| &w.terminal_map)),
141 );
142 net.buses
143 .iter()
144 .flat_map(|b| [&b.terminals, &b.grounded])
145 .chain(maps)
146 .flat_map(|m| m.iter().map(String::as_str))
147}
148
149fn matrix(m: &Mat) -> Value {
153 let n = m.len();
154 let cols: Vec<Value> = (0..n)
155 .map(|j| {
156 Value::Array(
157 (0..n)
158 .map(|i| json!(m[i].get(j).copied().unwrap_or(0.0)))
159 .collect(),
160 )
161 })
162 .collect();
163 Value::Array(cols)
164}
165
166fn zero_matrix(n: usize) -> Mat {
167 vec![vec![0.0; n]; n]
168}
169
170fn shunt_is_delta(extras: &Extras) -> bool {
172 extras
173 .get("conn")
174 .and_then(|v| v.as_str())
175 .is_some_and(|t| t.to_ascii_lowercase().starts_with('d') || t.eq_ignore_ascii_case("ll"))
176}
177
178fn scale(m: &Mat, k: f64) -> Mat {
179 m.iter()
180 .map(|row| row.iter().map(|v| v * k).collect())
181 .collect()
182}
183
184impl Writer {
185 fn warn(&mut self, msg: impl Into<String>) {
186 self.warnings.push(msg.into());
187 }
188
189 fn bounded_dim(&mut self, n: usize, what: &str) -> usize {
192 if n > MAX_DIM {
193 self.warn(format!(
194 "{what}: {n} conductors exceed the supported maximum of \
195 {MAX_DIM}; matrices emitted at {MAX_DIM}"
196 ));
197 MAX_DIM
198 } else {
199 n
200 }
201 }
202
203 fn extras_dropped(&mut self, extras: &crate::model::Extras, consumed: &[&str], what: &str) {
207 for key in extras.keys() {
208 if consumed.contains(&key.as_str()) || key.starts_with("pmd_") || key == "bmopf_subtype"
209 {
210 continue;
211 }
212 self.warn(format!(
213 "{what}: `{key}` has no ENGINEERING field; dropped from the output"
214 ));
215 }
216 }
217
218 fn extras_f64(extras: &Extras, key: &str) -> Option<f64> {
219 extras.get(key).and_then(|v| {
220 v.as_f64()
221 .or_else(|| v.as_str().and_then(|s| s.parse().ok()))
222 })
223 }
224
225 fn status(extras: &Extras) -> Value {
228 extras
229 .get("pmd_status")
230 .cloned()
231 .unwrap_or_else(|| json!("ENABLED"))
232 }
233
234 fn bus_coordinates(
235 &mut self,
236 o: &mut Map<String, Value>,
237 b: &DistBus,
238 net: &MulticonductorNetwork,
239 ) {
240 if let Some(location) = b.location {
241 if !matches!(
242 net.geo.as_ref().map(|geo| &geo.space),
243 Some(CoordinateSpace::Geographic { .. })
244 ) {
245 self.warnings.push(format!(
246 "bus {}: non-geographic or undeclared location is not emitted to PMD lon/lat",
247 b.id
248 ));
249 return;
250 }
251 if location.x.is_finite() && location.y.is_finite() {
252 o.insert("lon".into(), json!(location.x));
253 o.insert("lat".into(), json!(location.y));
254 } else {
255 self.warnings.push(format!(
256 "bus {}: nonfinite location is not emitted to PMD JSON",
257 b.id
258 ));
259 }
260 return;
261 }
262 if let Some(x) = Self::extras_f64(&b.extras, "x") {
263 o.insert("lon".into(), json!(x));
264 }
265 if let Some(y) = Self::extras_f64(&b.extras, "y") {
266 o.insert("lat".into(), json!(y));
267 }
268 }
269
270 fn document(&mut self, net: &MulticonductorNetwork) -> Value {
271 let mut doc = Map::new();
272 doc.insert("data_model".into(), json!("ENGINEERING"));
273 doc.insert(
274 "name".into(),
275 json!(net.name.clone().unwrap_or_default().to_lowercase()),
276 );
277 doc.insert(
278 "files".into(),
279 net.extras
280 .get("pmd_files")
281 .cloned()
282 .unwrap_or_else(|| json!([])),
283 );
284
285 let settings = net
288 .extras
289 .get("pmd_settings")
290 .cloned()
291 .unwrap_or_else(|| synthesized_settings(net));
292 doc.insert("settings".into(), settings);
293
294 let max_conductor = net
305 .buses
306 .iter()
307 .flat_map(|b| &b.terminals)
308 .filter_map(|t| t.parse::<i64>().ok())
309 .chain(self.renamed_terminals.values().copied())
310 .max()
311 .unwrap_or(4);
312 let max_dim = i64::try_from(MAX_DIM).expect("MAX_DIM is small");
313 if max_conductor > max_dim {
314 self.warn(format!(
315 "terminal {max_conductor} exceeds the supported maximum conductor id \
316 of {MAX_DIM}; conductor_ids enumerated to {MAX_DIM}"
317 ));
318 }
319 let max_conductor = max_conductor.clamp(4, max_dim);
320 doc.insert(
321 "conductor_ids".into(),
322 Value::Array((1..=max_conductor).map(|i| json!(i)).collect()),
323 );
324
325 let mut buses = Map::new();
326 for b in &net.buses {
327 let mut o = Map::new();
328 o.insert(
329 "terminals".into(),
330 json!(self.conns(&b.terminals, &format!("bus {}", b.id))),
331 );
332 let grounded = self.conns(&b.grounded, &format!("bus {}", b.id));
333 for key in ["rg", "xg"] {
336 let v = b
337 .extras
338 .get(key)
339 .cloned()
340 .unwrap_or_else(|| json!(vec![0.0; grounded.len()]));
341 o.insert(key.into(), v);
342 }
343 o.insert("grounded".into(), json!(grounded));
344 o.insert("status".into(), Self::status(&b.extras));
345 self.bus_coordinates(&mut o, b, net);
346 for (key, present) in [
349 ("v_min", b.v_min.is_some()),
350 ("v_max", b.v_max.is_some()),
351 ("vpn_min", b.vpn_min.is_some()),
352 ("vpn_max", b.vpn_max.is_some()),
353 ("vpp_min", b.vpp_min.is_some()),
354 ("vpp_max", b.vpp_max.is_some()),
355 ("vpos_min", b.vpos_min.is_some()),
356 ("vpos_max", b.vpos_max.is_some()),
357 ("vneg_max", b.vneg_max.is_some()),
358 ("vzero_max", b.vzero_max.is_some()),
359 ("vn_max", b.vn_max.is_some()),
360 ] {
361 if present {
362 self.warn(format!(
363 "bus {}: `{key}` volt bounds have no ENGINEERING field; dropped",
364 b.id
365 ));
366 }
367 }
368 buses.insert(b.id.to_lowercase(), Value::Object(o));
369 }
370 doc.insert("bus".into(), Value::Object(buses));
371
372 self.linecodes(net, &mut doc);
373 self.branches(net, &mut doc);
374 self.injections(net, &mut doc);
375 self.transformers(net, &mut doc);
376
377 for u in &net.untyped {
378 self.warn(format!(
379 "{} {}: class is not converted to ENGINEERING; dropped from the output",
380 u.class, u.name
381 ));
382 }
383 Value::Object(doc)
384 }
385
386 fn linecodes(&mut self, net: &MulticonductorNetwork, doc: &mut Map<String, Value>) {
387 let inlined = inlined_codes(net);
391 let mut codes = Map::new();
392 for c in &net.linecodes {
393 if inlined.contains(&c.name.to_lowercase()) {
394 continue;
395 }
396 let mut o = Map::new();
397 insert_impedance_matrices(&mut o, c, net.base_frequency);
398 if let Some(i_max) = &c.i_max {
399 o.insert("cm_ub".into(), json!(i_max));
400 }
401 if let Some(s_max) = &c.s_max {
402 o.insert("sm_ub".into(), json!(s_max));
403 }
404 if c.source.is_some() {
405 self.warn(format!(
406 "linecode {}: matrix provenance `source` has no ENGINEERING field; dropped",
407 c.name
408 ));
409 }
410 codes.insert(c.name.to_lowercase(), Value::Object(o));
411 }
412 if !codes.is_empty() {
413 doc.insert("linecode".into(), Value::Object(codes));
414 }
415 }
416
417 fn line_ratings(o: &mut Map<String, Value>, l: &DistLine) {
420 if let Some(i_max) = &l.i_max {
421 o.insert("cm_ub".into(), json!(i_max));
422 }
423 if let Some(s_max) = &l.s_max {
424 o.insert("sm_ub".into(), json!(s_max));
425 }
426 }
427
428 fn branches(&mut self, net: &MulticonductorNetwork, doc: &mut Map<String, Value>) {
429 if !net.lines.is_empty() {
430 let mut lines = Map::new();
431 for l in &net.lines {
432 let mut o = Map::new();
433 o.insert("f_bus".into(), json!(l.bus_from.to_lowercase()));
434 o.insert("t_bus".into(), json!(l.bus_to.to_lowercase()));
435 let what = format!("line {}", l.name);
436 o.insert(
437 "f_connections".into(),
438 json!(self.conns(&l.terminal_map_from, &what)),
439 );
440 o.insert(
441 "t_connections".into(),
442 json!(self.conns(&l.terminal_map_to, &what)),
443 );
444 o.insert("length".into(), json!(l.length));
445 let inline = l.extras.get("pmd_inline").and_then(Value::as_bool) == Some(true);
449 match net.linecode(&l.linecode) {
450 Some(c) if inline => {
451 insert_impedance_matrices(&mut o, c, net.base_frequency);
452 if let Some(i_max) = &c.i_max {
453 o.insert("cm_ub".into(), json!(i_max));
454 }
455 if let Some(s_max) = &c.s_max {
456 o.insert("sm_ub".into(), json!(s_max));
457 }
458 }
459 _ => {
460 if inline {
461 self.warn(format!(
462 "{what}: linecode `{}` is missing; emitted the reference instead of inline impedance",
463 l.linecode
464 ));
465 }
466 o.insert("linecode".into(), json!(l.linecode.to_lowercase()));
467 }
468 }
469 Self::line_ratings(&mut o, l);
470 o.insert("status".into(), Self::status(&l.extras));
471 o.insert(
472 "source_id".into(),
473 json!(format!("line.{}", l.name.to_lowercase())),
474 );
475 self.extras_dropped(&l.extras, &["units"], &what);
476 lines.insert(l.name.to_lowercase(), Value::Object(o));
477 }
478 doc.insert("line".into(), Value::Object(lines));
479 }
480
481 if !net.switches.is_empty() {
482 let mut switches = Map::new();
483 for s in &net.switches {
484 let mut o = Map::new();
485 let what = format!("switch {}", s.name);
486 let n = self.bounded_dim(s.terminal_map_from.len(), &what);
487 o.insert("f_bus".into(), json!(s.bus_from.to_lowercase()));
488 o.insert("t_bus".into(), json!(s.bus_to.to_lowercase()));
489 o.insert(
490 "f_connections".into(),
491 json!(self.conns(&s.terminal_map_from, &what)),
492 );
493 o.insert(
494 "t_connections".into(),
495 json!(self.conns(&s.terminal_map_to, &what)),
496 );
497 let rs = s.extras.get("pmd_rs").cloned().unwrap_or_else(|| {
502 let mut rs = zero_matrix(n);
503 for (i, row) in rs.iter_mut().enumerate() {
504 row[i] = 1e-4 * 0.001;
505 }
506 matrix(&rs)
507 });
508 o.insert("rs".into(), rs);
509 let xs = s
510 .extras
511 .get("pmd_xs")
512 .cloned()
513 .unwrap_or_else(|| matrix(&zero_matrix(n)));
514 o.insert("xs".into(), xs);
515 o.insert("g_fr".into(), matrix(&zero_matrix(n)));
516 o.insert("g_to".into(), matrix(&zero_matrix(n)));
517 o.insert("b_fr".into(), matrix(&zero_matrix(n)));
518 o.insert("b_to".into(), matrix(&zero_matrix(n)));
519 if let Some(i_max) = &s.i_max {
520 o.insert("cm_ub".into(), json!(i_max));
521 }
522 o.insert(
523 "state".into(),
524 json!(if s.open { "OPEN" } else { "CLOSED" }),
525 );
526 o.insert("dispatchable".into(), json!("YES"));
527 o.insert("status".into(), Self::status(&s.extras));
528 o.insert(
529 "source_id".into(),
530 json!(format!("line.{}", s.name.to_lowercase())),
531 );
532 self.extras_dropped(&s.extras, &[], &what);
533 switches.insert(s.name.to_lowercase(), Value::Object(o));
534 }
535 doc.insert("switch".into(), Value::Object(switches));
536 }
537 }
538
539 fn loads(&mut self, net: &MulticonductorNetwork, doc: &mut Map<String, Value>) {
540 if !net.loads.is_empty() {
541 let mut loads = Map::new();
542 for l in &net.loads {
543 let mut o = Map::new();
544 let what = format!("load {}", l.name);
545 let connections = self.conns(&l.terminal_map, &what);
546 let configuration = match l.configuration {
549 Configuration::Delta => "DELTA",
550 Configuration::Wye => "WYE",
551 Configuration::SinglePhase => {
552 let grounded_return = l
553 .terminal_map
554 .last()
555 .zip(net.bus(&l.bus))
556 .is_some_and(|(t, b)| b.grounded.contains(t));
557 if grounded_return { "WYE" } else { "DELTA" }
558 }
559 };
560 o.insert("configuration".into(), json!(configuration));
561 o.insert("connections".into(), json!(connections));
562 o.insert(
563 "pd_nom".into(),
564 json!(l.p_nom.iter().map(|p| p / 1e3).collect::<Vec<_>>()),
565 );
566 o.insert(
567 "qd_nom".into(),
568 json!(l.q_nom.iter().map(|q| q / 1e3).collect::<Vec<_>>()),
569 );
570 o.insert("bus".into(), json!(l.bus.to_lowercase()));
571 let mut insert_vm_nom = |v_nom: &[f64]| {
572 if let Some(value) = source_vm_nom(&l.extras, v_nom) {
573 o.insert("vm_nom".into(), value);
574 } else if !v_nom.is_empty() {
575 let value = if v_nom.len() == 1 {
576 json!(v_nom[0] / 1e3)
577 } else {
578 json!(v_nom.iter().map(|v| v / 1e3).collect::<Vec<_>>())
579 };
580 o.insert("vm_nom".into(), value);
581 } else if let Some(kv) = Self::extras_f64(&l.extras, "kv") {
582 o.insert("vm_nom".into(), json!(kv));
583 }
584 };
585 let model = match &l.voltage_model {
586 DistLoadVoltageModel::ConstantImpedance { v_nom } => {
587 insert_vm_nom(v_nom);
588 "IMPEDANCE"
589 }
590 DistLoadVoltageModel::ConstantCurrent { v_nom } => {
591 insert_vm_nom(v_nom);
592 "CURRENT"
593 }
594 DistLoadVoltageModel::Zip { v_nom, .. } => {
595 insert_vm_nom(v_nom);
596 "ZIPV"
597 }
598 DistLoadVoltageModel::Exponential { v_nom, .. } => {
599 insert_vm_nom(v_nom);
600 self.warn(format!(
601 "{what}: exponential load model has no ENGINEERING field; emitted POWER"
602 ));
603 "POWER"
604 }
605 DistLoadVoltageModel::ConstantPower { v_nom } => {
606 insert_vm_nom(v_nom);
607 "POWER"
608 }
609 };
610 o.insert("model".into(), json!(model));
611 o.insert("dispatchable".into(), json!("NO"));
612 o.insert("status".into(), Self::status(&l.extras));
613 o.insert(
614 "source_id".into(),
615 json!(format!("load.{}", l.name.to_lowercase())),
616 );
617 self.extras_dropped(&l.extras, &["kv", "model", "pf"], &what);
618 loads.insert(l.name.to_lowercase(), Value::Object(o));
619 }
620 doc.insert("load".into(), Value::Object(loads));
621 }
622 }
623
624 fn generators(&mut self, net: &MulticonductorNetwork, doc: &mut Map<String, Value>) {
625 if !net.generators.is_empty() {
626 let mut gens = Map::new();
627 for g in &net.generators {
628 let mut o = Map::new();
629 let what = format!("generator {}", g.name);
630 o.insert("bus".into(), json!(g.bus.to_lowercase()));
631 o.insert(
632 "connections".into(),
633 json!(self.conns(&g.terminal_map, &what)),
634 );
635 o.insert(
636 "configuration".into(),
637 json!(match g.configuration {
638 Configuration::Delta => "DELTA",
639 _ => "WYE",
640 }),
641 );
642 let kw = |w: &[f64]| w.iter().map(|v| v / 1e3).collect::<Vec<_>>();
643 o.insert("pg".into(), json!(kw(&g.p_nom)));
644 o.insert("qg".into(), json!(kw(&g.q_nom)));
645 if let Some(b) = &g.q_min {
646 o.insert("qg_lb".into(), json!(kw(b)));
647 }
648 if let Some(b) = &g.q_max {
649 o.insert("qg_ub".into(), json!(kw(b)));
650 }
651 if let Some(b) = &g.p_min {
652 o.insert("pg_lb".into(), json!(kw(b)));
653 }
654 if let Some(b) = &g.p_max {
655 o.insert("pg_ub".into(), json!(kw(b)));
656 }
657 if g.cost.is_some() {
658 self.warn(format!(
659 "{what}: generation cost has no ENGINEERING field; dropped"
660 ));
661 }
662 for (key, present) in [("s_max", g.s_max.is_some()), ("i_max", g.i_max.is_some())] {
665 if present {
666 self.warn(format!(
667 "{what}: `{key}` has no ENGINEERING generator mapping yet; dropped"
668 ));
669 }
670 }
671 o.insert("control_mode".into(), json!("FREQUENCYDROOP"));
672 o.insert("status".into(), Self::status(&g.extras));
673 o.insert(
674 "source_id".into(),
675 json!(format!("generator.{}", g.name.to_lowercase())),
676 );
677 self.extras_dropped(&g.extras, &["kv"], &what);
678 gens.insert(g.name.to_lowercase(), Value::Object(o));
679 }
680 doc.insert("generator".into(), Value::Object(gens));
681 }
682 }
683
684 fn injections(&mut self, net: &MulticonductorNetwork, doc: &mut Map<String, Value>) {
685 self.loads(net, doc);
686 self.generators(net, doc);
687 for c in &net.capacitors {
689 self.warn(format!(
690 "capacitor {}: rated capacitor banks are not converted to ENGINEERING JSON; dropped",
691 c.name
692 ));
693 }
694 if !net.shunts.is_empty() {
695 let mut shunts = Map::new();
696 for s in &net.shunts {
697 let mut o = Map::new();
698 let what = format!("shunt {}", s.name);
699 o.insert("bus".into(), json!(s.bus.to_lowercase()));
700 o.insert(
701 "connections".into(),
702 json!(self.conns(&s.terminal_map, &what)),
703 );
704 o.insert("gs".into(), matrix(&s.g));
705 o.insert("bs".into(), matrix(&s.b));
706 let configuration = if shunt_is_delta(&s.extras) {
710 "DELTA"
711 } else {
712 "WYE"
713 };
714 o.insert("configuration".into(), json!(configuration));
715 o.insert("model".into(), json!("CAPACITOR"));
716 o.insert("dispatchable".into(), json!("NO"));
717 o.insert("status".into(), Self::status(&s.extras));
718 o.insert(
719 "source_id".into(),
720 json!(format!("capacitor.{}", s.name.to_lowercase())),
721 );
722 self.extras_dropped(&s.extras, &["kv", "kvar", "conn"], &what);
723 shunts.insert(s.name.to_lowercase(), Value::Object(o));
724 }
725 doc.insert("shunt".into(), Value::Object(shunts));
726 }
727
728 let mut sources = Map::new();
729 for vs in &net.sources {
730 sources.insert(vs.name.to_lowercase(), self.voltage_source(vs));
731 }
732 doc.insert("voltage_source".into(), Value::Object(sources));
733 }
734
735 fn voltage_source(&mut self, vs: &VoltageSource) -> Value {
736 let mut o = Map::new();
737 let what = format!("voltage source {}", vs.name);
738 let connections = self.conns(&vs.terminal_map, &what);
739 let n = self.bounded_dim(connections.len(), &what);
740 o.insert("bus".into(), json!(vs.bus.to_lowercase()));
741 o.insert("connections".into(), json!(connections));
742 o.insert("configuration".into(), json!("WYE"));
743 o.insert(
744 "vm".into(),
745 json!(vs.v_magnitude.iter().map(|v| v / 1e3).collect::<Vec<_>>()),
746 );
747 o.insert(
748 "va".into(),
749 json!(
750 vs.v_angle
751 .iter()
752 .map(|a| a.to_degrees())
753 .collect::<Vec<_>>()
754 ),
755 );
756 if let (Some(rs), Some(xs)) = (vs.extras.get("rs"), vs.extras.get("xs")) {
760 o.insert("rs".into(), rs.clone());
761 o.insert("xs".into(), xs.clone());
762 } else {
763 let (rs, xs) = thevenin(vs, n);
764 if rs.iter().flatten().all(|&v| v == 0.0) {
765 self.warn(format!(
766 "{what}: no short circuit data; emitted an ideal source (zero rs/xs)"
767 ));
768 }
769 o.insert("rs".into(), matrix(&rs));
770 o.insert("xs".into(), matrix(&xs));
771 }
772 o.insert("status".into(), Self::status(&vs.extras));
773 o.insert(
774 "source_id".into(),
775 json!(format!("vsource.{}", vs.name.to_lowercase())),
776 );
777 self.extras_dropped(
780 &vs.extras,
781 &[
782 "basekv",
783 "basemva",
784 "pu",
785 "angle",
786 "mvasc1",
787 "mvasc3",
788 "x1r1",
789 "x0r0",
790 "rs",
791 "xs",
792 "isc1",
793 "isc3",
794 "configuration",
795 ],
796 &what,
797 );
798 Value::Object(o)
799 }
800
801 fn transformers(&mut self, net: &MulticonductorNetwork, doc: &mut Map<String, Value>) {
802 if net.transformers.is_empty() {
803 return;
804 }
805 let mut out = Map::new();
806 for t in &net.transformers {
807 out.insert(t.name.to_lowercase(), self.transformer(t));
808 }
809 doc.insert("transformer".into(), Value::Object(out));
810 }
811
812 fn transformer(&mut self, t: &DistTransformer) -> Value {
813 let mut o = Map::new();
814 let what = format!("transformer {}", t.name);
815 let phases = t.phases;
816
817 let stashed = t.extras.contains_key("pmd_polarity");
823 let mut buses = Vec::new();
824 let mut connections: Vec<Value> = Vec::new();
825 for (w_idx, w) in t.windings.iter().enumerate() {
826 buses.push(json!(w.bus.to_lowercase()));
827 let mut c = self.conns(&w.terminal_map, &what);
828 if !stashed
829 && w_idx > 0
830 && t.windings[0].conn == WindingConn::Delta
831 && w.conn == WindingConn::Wye
832 && c.len() > 1
833 {
834 let phases_part = c.len() - 1;
835 c[..phases_part].rotate_left(1);
836 }
837 connections.push(json!(c));
838 }
839 o.insert("bus".into(), Value::Array(buses));
840 o.insert(
841 "connections".into(),
842 t.extras
843 .get("pmd_connections")
844 .cloned()
845 .unwrap_or(Value::Array(connections)),
846 );
847 o.insert(
848 "polarity".into(),
849 t.extras
850 .get("pmd_polarity")
851 .cloned()
852 .unwrap_or_else(|| json!(lag_polarity(&t.windings))),
853 );
854 o.insert(
855 "configuration".into(),
856 Value::Array(
857 t.windings
858 .iter()
859 .map(|w| {
860 json!(match w.conn {
861 WindingConn::Wye => "WYE",
862 WindingConn::Delta => "DELTA",
863 })
864 })
865 .collect(),
866 ),
867 );
868 o.insert(
869 "rw".into(),
870 json!(
871 t.windings
872 .iter()
873 .map(|w| w.r_pct / 100.0)
874 .collect::<Vec<_>>()
875 ),
876 );
877 o.insert(
878 "xsc".into(),
879 json!(t.xsc_pct.iter().map(|x| x / 100.0).collect::<Vec<_>>()),
880 );
881 o.insert(
882 "sm_nom".into(),
883 json!(
884 t.windings
885 .iter()
886 .map(|w| w.s_rating / 1e3)
887 .collect::<Vec<_>>()
888 ),
889 );
890 o.insert(
891 "vm_nom".into(),
892 json!(t.windings.iter().map(|w| w.v_ref / 1e3).collect::<Vec<_>>()),
893 );
894 let sm_ub = Self::extras_f64(&t.extras, "emerghkva")
899 .unwrap_or_else(|| t.windings.first().map_or(0.0, |w| w.s_rating / 1e3 * 1.5));
900 o.insert("sm_ub".into(), json!(sm_ub));
901 insert_tap_fields(&mut o, t, phases);
902 if let Some(controls) = t.extras.get("controls") {
903 o.insert("controls".into(), controls.clone());
904 }
905 let noloadloss = Self::extras_f64(&t.extras, "%noloadloss").unwrap_or(0.0) / 100.0;
906 let cmag = Self::extras_f64(&t.extras, "%imag").unwrap_or(0.0) / 100.0;
907 o.insert("noloadloss".into(), json!(noloadloss));
908 o.insert("cmag".into(), json!(cmag));
909 o.insert("status".into(), Self::status(&t.extras));
910 o.insert(
911 "source_id".into(),
912 json!(format!("transformer.{}", t.name.to_lowercase())),
913 );
914 self.extras_dropped(
915 &t.extras,
916 &["controls", "%loadloss", "%noloadloss", "%imag", "emerghkva"],
917 &what,
918 );
919 Value::Object(o)
920 }
921}
922
923fn insert_tap_fields(o: &mut Map<String, Value>, t: &DistTransformer, phases: usize) {
927 let nw = t.windings.len();
928 let mut insert = |key: &str, default: fn(&DistTransformer, usize, usize) -> Value| {
929 let v = t
930 .extras
931 .get(&format!("pmd_{key}"))
932 .cloned()
933 .unwrap_or_else(|| default(t, nw, phases));
934 o.insert(key.into(), v);
935 };
936 insert("tm_set", |t, _, phases| {
937 Value::Array(
938 t.windings
939 .iter()
940 .map(|w| json!(vec![w.tap; phases]))
941 .collect(),
942 )
943 });
944 insert("tm_fix", |_, nw, phases| {
945 Value::Array((0..nw).map(|_| json!(vec![true; phases])).collect())
946 });
947 insert("tm_lb", |_, nw, phases| {
948 Value::Array((0..nw).map(|_| json!(vec![0.9; phases])).collect())
949 });
950 insert("tm_ub", |_, nw, phases| {
951 Value::Array((0..nw).map(|_| json!(vec![1.1; phases])).collect())
952 });
953 insert("tm_step", |_, nw, phases| {
954 Value::Array((0..nw).map(|_| json!(vec![1.0 / 32.0; phases])).collect())
955 });
956}
957
958fn synthesized_settings(net: &MulticonductorNetwork) -> Value {
963 let mut settings = Map::new();
964 settings.insert("base_frequency".into(), json!(net.base_frequency));
965 settings.insert("power_scale_factor".into(), json!(1000.0));
966 settings.insert("voltage_scale_factor".into(), json!(1000.0));
967 let sbase = net
968 .sources
969 .first()
970 .and_then(|vs| Writer::extras_f64(&vs.extras, "basemva"))
971 .map_or(100_000.0, |mva| mva * 1e3);
972 settings.insert("sbase_default".into(), json!(sbase));
973 let mut vbases = Map::new();
974 for vs in &net.sources {
975 let phases = count_phases(vs).max(1) as f64;
976 let vln_kv = Writer::extras_f64(&vs.extras, "basekv").map_or_else(
977 || {
978 let pu = Writer::extras_f64(&vs.extras, "pu").unwrap_or(1.0);
979 vs.v_magnitude.first().copied().unwrap_or(0.0) / 1e3 / pu
980 },
981 |kv| kv / phases.sqrt(),
982 );
983 vbases.insert(vs.bus.to_lowercase(), json!(vln_kv));
984 }
985 settings.insert("vbases_default".into(), Value::Object(vbases));
986 Value::Object(settings)
987}
988
989pub(super) fn lag_polarity(windings: &[Winding]) -> Vec<i64> {
995 let nw = windings.len();
996 let mut polarity = vec![1i64; nw];
997 for (w_idx, w) in windings.iter().enumerate().skip(1) {
998 if windings[0].conn == WindingConn::Delta
999 && w.conn == WindingConn::Wye
1000 && w.terminal_map.len() > 1
1001 {
1002 polarity[w_idx] = -1;
1003 }
1004 if w_idx == 2 && nw == 3 && windings[1].terminal_map.last() == w.terminal_map.first() {
1006 polarity[w_idx] = -1;
1007 }
1008 }
1009 polarity
1010}
1011
1012fn inlined_codes(net: &MulticonductorNetwork) -> BTreeSet<String> {
1015 let mut inlined = BTreeSet::new();
1016 for c in &net.linecodes {
1017 let mut refs = net
1018 .lines
1019 .iter()
1020 .filter(|l| l.linecode.eq_ignore_ascii_case(&c.name))
1021 .peekable();
1022 if refs.peek().is_some()
1023 && refs.all(|l| l.extras.get("pmd_inline").and_then(Value::as_bool) == Some(true))
1024 {
1025 inlined.insert(c.name.to_lowercase());
1026 }
1027 }
1028 inlined
1029}
1030
1031fn insert_impedance_matrices(o: &mut Map<String, Value>, c: &DistLineCode, base_frequency: f64) {
1037 o.insert("rs".into(), matrix(&c.r_series));
1038 o.insert("xs".into(), matrix(&c.x_series));
1039 o.insert("g_fr".into(), matrix(&c.g_from));
1040 o.insert("g_to".into(), matrix(&c.g_to));
1041 if let (Some(fr), Some(to)) = (c.extras.get("pmd_b_fr"), c.extras.get("pmd_b_to")) {
1042 o.insert("b_fr".into(), fr.clone());
1043 o.insert("b_to".into(), to.clone());
1044 } else {
1045 let to_nf = 1.0 / (std::f64::consts::TAU * base_frequency * 1e-9);
1046 o.insert("b_fr".into(), matrix(&scale(&c.b_from, to_nf)));
1047 o.insert("b_to".into(), matrix(&scale(&c.b_to, to_nf)));
1048 }
1049}
1050
1051fn thevenin(vs: &VoltageSource, n_cond: usize) -> (Mat, Mat) {
1056 let get = |key: &str| Writer::extras_f64(&vs.extras, key);
1057 let basekv = get("basekv").unwrap_or_else(|| {
1058 vs.v_magnitude.first().copied().unwrap_or(0.0) / 1e3 * (count_phases(vs) as f64).sqrt()
1060 });
1061 let phases = count_phases(vs);
1062 if basekv <= 0.0 || phases == 0 {
1063 return (zero_matrix(n_cond), zero_matrix(n_cond));
1064 }
1065 let mvasc3 = get("mvasc3").unwrap_or(2000.0);
1066 let mvasc1 = get("mvasc1").unwrap_or(2100.0);
1067 let x1r1 = get("x1r1").unwrap_or(4.0);
1068 let x0r0 = get("x0r0").unwrap_or(3.0);
1069 let factor = if phases == 1 { 1.0 } else { 3f64.sqrt() };
1070
1071 let isc1 = mvasc1 * 1e3 / (basekv * factor);
1072 let x1 = basekv * basekv / mvasc3 / (1.0 + 1.0 / (x1r1 * x1r1)).sqrt();
1073 let r1 = x1 / x1r1;
1074 let a = 1.0 + x0r0 * x0r0;
1075 let b = 4.0 * (r1 + x1 * x0r0);
1076 let c = 4.0 * (r1 * r1 + x1 * x1) - (3.0 * basekv * 1000.0 / factor / isc1).powi(2);
1077 let disc = (b * b - 4.0 * a * c).max(0.0).sqrt();
1078 let r0 = ((-b + disc) / (2.0 * a)).max((-b - disc) / (2.0 * a));
1079 let x0 = r0 * x0r0;
1080
1081 let r_self = (2.0 * r1 + r0) / 3.0;
1082 let x_self = (2.0 * x1 + x0) / 3.0;
1083 let r_mutual = (r0 - r1) / 3.0;
1084 let x_mutual = (x0 - x1) / 3.0;
1085
1086 let mut r_mat = vec![vec![r_mutual; n_cond]; n_cond];
1087 let mut x_mat = vec![vec![x_mutual; n_cond]; n_cond];
1088 for i in 0..n_cond {
1089 r_mat[i][i] = r_self;
1090 x_mat[i][i] = x_self;
1091 }
1092 (r_mat, x_mat)
1093}
1094
1095fn count_phases(vs: &VoltageSource) -> usize {
1096 vs.v_magnitude.iter().filter(|&&v| v > 0.0).count()
1097}
1098
1099fn source_vm_nom(extras: &Extras, v_nom: &[f64]) -> Option<Value> {
1100 let raw = extras.get("kv")?;
1101 if v_nom.is_empty() {
1102 return Some(raw.clone());
1103 }
1104 if let Some(kv) = raw
1105 .as_f64()
1106 .or_else(|| raw.as_str().and_then(|s| s.parse().ok()))
1107 {
1108 if v_nom.iter().all(|v| same_voltage(*v, kv * 1e3)) {
1109 return Some(json!(kv));
1110 }
1111 }
1112 let vals: Vec<f64> = raw
1113 .as_array()?
1114 .iter()
1115 .filter_map(serde_json::Value::as_f64)
1116 .collect();
1117 if vals.len() == 1 && v_nom.iter().all(|v| same_voltage(*v, vals[0] * 1e3)) {
1118 return Some(raw.clone());
1119 }
1120 if vals.len() == v_nom.len()
1121 && vals
1122 .iter()
1123 .zip(v_nom)
1124 .all(|(a, b)| same_voltage(*b, *a * 1e3))
1125 {
1126 return Some(raw.clone());
1127 }
1128 None
1129}
1130
1131fn same_voltage(a: f64, b: f64) -> bool {
1132 (a - b).abs() <= 1e-9 * a.abs().max(b.abs()).max(1.0)
1133}