1use std::collections::BTreeSet;
15
16use serde_json::{Map, Value, json};
17
18use crate::convert::Conversion;
19use crate::model::{
20 Configuration, DistLineCode, DistLoadVoltageModel, DistNetwork, DistTransformer, Extras, Mat,
21 VoltageSource, Winding, WindingConn,
22};
23
24pub fn write_pmd_json(net: &DistNetwork) -> Conversion {
31 let mut w = Writer {
32 warnings: Vec::new(),
33 };
34 let doc = w.document(net);
35 Conversion {
36 text: serde_json::to_string_pretty(&doc).expect("maps and finite numbers") + "\n",
37 warnings: w.warnings,
38 }
39}
40
41struct Writer {
42 warnings: Vec<String>,
43}
44
45fn conns(map: &[String], warnings: &mut Vec<String>, what: &str) -> Vec<i64> {
48 map.iter()
49 .enumerate()
50 .map(|(k, t)| {
51 t.parse::<i64>().unwrap_or_else(|_| {
52 let fallback = 90 + i64::try_from(k).unwrap_or(0);
53 warnings.push(format!(
54 "{what}: terminal `{t}` is not numeric; emitted as {fallback}"
55 ));
56 fallback
57 })
58 })
59 .collect()
60}
61
62fn matrix(m: &Mat) -> Value {
64 let n = m.len();
65 let cols: Vec<Value> = (0..n)
66 .map(|j| Value::Array((0..n).map(|i| json!(m[i][j])).collect()))
67 .collect();
68 Value::Array(cols)
69}
70
71fn zero_matrix(n: usize) -> Mat {
72 vec![vec![0.0; n]; n]
73}
74
75fn shunt_is_delta(extras: &Extras) -> bool {
77 extras
78 .get("conn")
79 .and_then(|v| v.as_str())
80 .is_some_and(|t| t.to_ascii_lowercase().starts_with('d') || t.eq_ignore_ascii_case("ll"))
81}
82
83fn scale(m: &Mat, k: f64) -> Mat {
84 m.iter()
85 .map(|row| row.iter().map(|v| v * k).collect())
86 .collect()
87}
88
89impl Writer {
90 fn warn(&mut self, msg: impl Into<String>) {
91 self.warnings.push(msg.into());
92 }
93
94 fn extras_dropped(&mut self, extras: &crate::model::Extras, consumed: &[&str], what: &str) {
98 for key in extras.keys() {
99 if consumed.contains(&key.as_str()) || key.starts_with("pmd_") || key == "bmopf_subtype"
100 {
101 continue;
102 }
103 self.warn(format!(
104 "{what}: `{key}` has no ENGINEERING field; dropped from the output"
105 ));
106 }
107 }
108
109 fn extras_f64(extras: &Extras, key: &str) -> Option<f64> {
110 extras.get(key).and_then(|v| {
111 v.as_f64()
112 .or_else(|| v.as_str().and_then(|s| s.parse().ok()))
113 })
114 }
115
116 fn status(extras: &Extras) -> Value {
119 extras
120 .get("pmd_status")
121 .cloned()
122 .unwrap_or_else(|| json!("ENABLED"))
123 }
124
125 fn document(&mut self, net: &DistNetwork) -> Value {
126 let mut doc = Map::new();
127 doc.insert("data_model".into(), json!("ENGINEERING"));
128 doc.insert(
129 "name".into(),
130 json!(net.name.clone().unwrap_or_default().to_lowercase()),
131 );
132 doc.insert(
133 "files".into(),
134 net.extras
135 .get("pmd_files")
136 .cloned()
137 .unwrap_or_else(|| json!([])),
138 );
139
140 let settings = net
143 .extras
144 .get("pmd_settings")
145 .cloned()
146 .unwrap_or_else(|| synthesized_settings(net));
147 doc.insert("settings".into(), settings);
148
149 let max_conductor = net
150 .buses
151 .iter()
152 .flat_map(|b| &b.terminals)
153 .filter_map(|t| t.parse::<i64>().ok())
154 .max()
155 .unwrap_or(4)
156 .max(4);
157 doc.insert(
158 "conductor_ids".into(),
159 Value::Array((1..=max_conductor).map(|i| json!(i)).collect()),
160 );
161
162 let mut buses = Map::new();
163 for b in &net.buses {
164 let mut o = Map::new();
165 o.insert(
166 "terminals".into(),
167 json!(conns(
168 &b.terminals,
169 &mut self.warnings,
170 &format!("bus {}", b.id)
171 )),
172 );
173 let grounded = conns(&b.grounded, &mut self.warnings, &format!("bus {}", b.id));
174 for key in ["rg", "xg"] {
177 let v = b
178 .extras
179 .get(key)
180 .cloned()
181 .unwrap_or_else(|| json!(vec![0.0; grounded.len()]));
182 o.insert(key.into(), v);
183 }
184 o.insert("grounded".into(), json!(grounded));
185 o.insert("status".into(), Self::status(&b.extras));
186 if let Some(x) = Self::extras_f64(&b.extras, "x") {
187 o.insert("lon".into(), json!(x));
188 }
189 if let Some(y) = Self::extras_f64(&b.extras, "y") {
190 o.insert("lat".into(), json!(y));
191 }
192 for (key, present) in [
195 ("v_min", b.v_min.is_some()),
196 ("v_max", b.v_max.is_some()),
197 ("vpn_min", b.vpn_min.is_some()),
198 ("vpn_max", b.vpn_max.is_some()),
199 ("vpp_min", b.vpp_min.is_some()),
200 ("vpp_max", b.vpp_max.is_some()),
201 ("vsym_min", b.vsym_min.is_some()),
202 ("vsym_max", b.vsym_max.is_some()),
203 ] {
204 if present {
205 self.warn(format!(
206 "bus {}: `{key}` volt bounds have no ENGINEERING field; dropped",
207 b.id
208 ));
209 }
210 }
211 buses.insert(b.id.to_lowercase(), Value::Object(o));
212 }
213 doc.insert("bus".into(), Value::Object(buses));
214
215 Self::linecodes(net, &mut doc);
216 self.branches(net, &mut doc);
217 self.injections(net, &mut doc);
218 self.transformers(net, &mut doc);
219
220 for u in &net.untyped {
221 self.warn(format!(
222 "{} {}: class is not converted to ENGINEERING; dropped from the output",
223 u.class, u.name
224 ));
225 }
226 Value::Object(doc)
227 }
228
229 fn linecodes(net: &DistNetwork, doc: &mut Map<String, Value>) {
230 let inlined = inlined_codes(net);
234 let mut codes = Map::new();
235 for c in &net.linecodes {
236 if inlined.contains(&c.name.to_lowercase()) {
237 continue;
238 }
239 let mut o = Map::new();
240 insert_impedance_matrices(&mut o, c, net.base_frequency);
241 if let Some(i_max) = &c.i_max {
242 o.insert("cm_ub".into(), json!(i_max));
243 }
244 if let Some(s_max) = &c.s_max {
245 o.insert("sm_ub".into(), json!(s_max));
246 }
247 codes.insert(c.name.to_lowercase(), Value::Object(o));
248 }
249 if !codes.is_empty() {
250 doc.insert("linecode".into(), Value::Object(codes));
251 }
252 }
253
254 fn branches(&mut self, net: &DistNetwork, doc: &mut Map<String, Value>) {
255 if !net.lines.is_empty() {
256 let mut lines = Map::new();
257 for l in &net.lines {
258 let mut o = Map::new();
259 o.insert("f_bus".into(), json!(l.bus_from.to_lowercase()));
260 o.insert("t_bus".into(), json!(l.bus_to.to_lowercase()));
261 let what = format!("line {}", l.name);
262 o.insert(
263 "f_connections".into(),
264 json!(conns(&l.terminal_map_from, &mut self.warnings, &what)),
265 );
266 o.insert(
267 "t_connections".into(),
268 json!(conns(&l.terminal_map_to, &mut self.warnings, &what)),
269 );
270 o.insert("length".into(), json!(l.length));
271 let inline = l.extras.get("pmd_inline").and_then(Value::as_bool) == Some(true);
275 match net.linecode(&l.linecode) {
276 Some(c) if inline => {
277 insert_impedance_matrices(&mut o, c, net.base_frequency);
278 if let Some(i_max) = &c.i_max {
279 o.insert("cm_ub".into(), json!(i_max));
280 }
281 }
282 _ => {
283 if inline {
284 self.warn(format!(
285 "{what}: linecode `{}` is missing; emitted the reference instead of inline impedance",
286 l.linecode
287 ));
288 }
289 o.insert("linecode".into(), json!(l.linecode.to_lowercase()));
290 }
291 }
292 o.insert("status".into(), Self::status(&l.extras));
293 o.insert(
294 "source_id".into(),
295 json!(format!("line.{}", l.name.to_lowercase())),
296 );
297 self.extras_dropped(&l.extras, &["units"], &what);
298 lines.insert(l.name.to_lowercase(), Value::Object(o));
299 }
300 doc.insert("line".into(), Value::Object(lines));
301 }
302
303 if !net.switches.is_empty() {
304 let mut switches = Map::new();
305 for s in &net.switches {
306 let mut o = Map::new();
307 let n = s.terminal_map_from.len();
308 let what = format!("switch {}", s.name);
309 o.insert("f_bus".into(), json!(s.bus_from.to_lowercase()));
310 o.insert("t_bus".into(), json!(s.bus_to.to_lowercase()));
311 o.insert(
312 "f_connections".into(),
313 json!(conns(&s.terminal_map_from, &mut self.warnings, &what)),
314 );
315 o.insert(
316 "t_connections".into(),
317 json!(conns(&s.terminal_map_to, &mut self.warnings, &what)),
318 );
319 let rs = s.extras.get("pmd_rs").cloned().unwrap_or_else(|| {
324 let mut rs = zero_matrix(n);
325 for (i, row) in rs.iter_mut().enumerate() {
326 row[i] = 1e-4 * 0.001;
327 }
328 matrix(&rs)
329 });
330 o.insert("rs".into(), rs);
331 let xs = s
332 .extras
333 .get("pmd_xs")
334 .cloned()
335 .unwrap_or_else(|| matrix(&zero_matrix(n)));
336 o.insert("xs".into(), xs);
337 o.insert("g_fr".into(), matrix(&zero_matrix(n)));
338 o.insert("g_to".into(), matrix(&zero_matrix(n)));
339 o.insert("b_fr".into(), matrix(&zero_matrix(n)));
340 o.insert("b_to".into(), matrix(&zero_matrix(n)));
341 if let Some(i_max) = &s.i_max {
342 o.insert("cm_ub".into(), json!(i_max));
343 }
344 o.insert(
345 "state".into(),
346 json!(if s.open { "OPEN" } else { "CLOSED" }),
347 );
348 o.insert("dispatchable".into(), json!("YES"));
349 o.insert("status".into(), Self::status(&s.extras));
350 o.insert(
351 "source_id".into(),
352 json!(format!("line.{}", s.name.to_lowercase())),
353 );
354 self.extras_dropped(&s.extras, &[], &what);
355 switches.insert(s.name.to_lowercase(), Value::Object(o));
356 }
357 doc.insert("switch".into(), Value::Object(switches));
358 }
359 }
360
361 fn loads(&mut self, net: &DistNetwork, doc: &mut Map<String, Value>) {
362 if !net.loads.is_empty() {
363 let mut loads = Map::new();
364 for l in &net.loads {
365 let mut o = Map::new();
366 let what = format!("load {}", l.name);
367 let connections = conns(&l.terminal_map, &mut self.warnings, &what);
368 let configuration = match l.configuration {
371 Configuration::Delta => "DELTA",
372 Configuration::Wye => "WYE",
373 Configuration::SinglePhase => {
374 let grounded_return = l
375 .terminal_map
376 .last()
377 .zip(net.bus(&l.bus))
378 .is_some_and(|(t, b)| b.grounded.contains(t));
379 if grounded_return { "WYE" } else { "DELTA" }
380 }
381 };
382 o.insert("configuration".into(), json!(configuration));
383 o.insert("connections".into(), json!(connections));
384 o.insert(
385 "pd_nom".into(),
386 json!(l.p_nom.iter().map(|p| p / 1e3).collect::<Vec<_>>()),
387 );
388 o.insert(
389 "qd_nom".into(),
390 json!(l.q_nom.iter().map(|q| q / 1e3).collect::<Vec<_>>()),
391 );
392 o.insert("bus".into(), json!(l.bus.to_lowercase()));
393 let mut insert_vm_nom = |v_nom: &[f64]| {
394 if let Some(value) = source_vm_nom(&l.extras, v_nom) {
395 o.insert("vm_nom".into(), value);
396 } else if !v_nom.is_empty() {
397 let value = if v_nom.len() == 1 {
398 json!(v_nom[0] / 1e3)
399 } else {
400 json!(v_nom.iter().map(|v| v / 1e3).collect::<Vec<_>>())
401 };
402 o.insert("vm_nom".into(), value);
403 } else if let Some(kv) = Self::extras_f64(&l.extras, "kv") {
404 o.insert("vm_nom".into(), json!(kv));
405 }
406 };
407 let model = match &l.voltage_model {
408 DistLoadVoltageModel::ConstantImpedance { v_nom } => {
409 insert_vm_nom(v_nom);
410 "IMPEDANCE"
411 }
412 DistLoadVoltageModel::ConstantCurrent { v_nom } => {
413 insert_vm_nom(v_nom);
414 "CURRENT"
415 }
416 DistLoadVoltageModel::Zip { v_nom, .. } => {
417 insert_vm_nom(v_nom);
418 "ZIPV"
419 }
420 DistLoadVoltageModel::Exponential { v_nom, .. } => {
421 insert_vm_nom(v_nom);
422 self.warn(format!(
423 "{what}: exponential load model has no ENGINEERING field; emitted POWER"
424 ));
425 "POWER"
426 }
427 DistLoadVoltageModel::ConstantPower { v_nom } => {
428 insert_vm_nom(v_nom);
429 "POWER"
430 }
431 };
432 o.insert("model".into(), json!(model));
433 o.insert("dispatchable".into(), json!("NO"));
434 o.insert("status".into(), Self::status(&l.extras));
435 o.insert(
436 "source_id".into(),
437 json!(format!("load.{}", l.name.to_lowercase())),
438 );
439 self.extras_dropped(&l.extras, &["kv", "model", "pf"], &what);
440 loads.insert(l.name.to_lowercase(), Value::Object(o));
441 }
442 doc.insert("load".into(), Value::Object(loads));
443 }
444 }
445
446 fn generators(&mut self, net: &DistNetwork, doc: &mut Map<String, Value>) {
447 if !net.generators.is_empty() {
448 let mut gens = Map::new();
449 for g in &net.generators {
450 let mut o = Map::new();
451 let what = format!("generator {}", g.name);
452 o.insert("bus".into(), json!(g.bus.to_lowercase()));
453 o.insert(
454 "connections".into(),
455 json!(conns(&g.terminal_map, &mut self.warnings, &what)),
456 );
457 o.insert(
458 "configuration".into(),
459 json!(match g.configuration {
460 Configuration::Delta => "DELTA",
461 _ => "WYE",
462 }),
463 );
464 let kw = |w: &[f64]| w.iter().map(|v| v / 1e3).collect::<Vec<_>>();
465 o.insert("pg".into(), json!(kw(&g.p_nom)));
466 o.insert("qg".into(), json!(kw(&g.q_nom)));
467 if let Some(b) = &g.q_min {
468 o.insert("qg_lb".into(), json!(kw(b)));
469 }
470 if let Some(b) = &g.q_max {
471 o.insert("qg_ub".into(), json!(kw(b)));
472 }
473 if let Some(b) = &g.p_min {
474 o.insert("pg_lb".into(), json!(kw(b)));
475 }
476 if let Some(b) = &g.p_max {
477 o.insert("pg_ub".into(), json!(kw(b)));
478 }
479 if g.cost.is_some() {
480 self.warn(format!(
481 "{what}: generation cost has no ENGINEERING field; dropped"
482 ));
483 }
484 o.insert("control_mode".into(), json!("FREQUENCYDROOP"));
485 o.insert("status".into(), Self::status(&g.extras));
486 o.insert(
487 "source_id".into(),
488 json!(format!("generator.{}", g.name.to_lowercase())),
489 );
490 self.extras_dropped(&g.extras, &["kv"], &what);
491 gens.insert(g.name.to_lowercase(), Value::Object(o));
492 }
493 doc.insert("generator".into(), Value::Object(gens));
494 }
495 }
496
497 fn injections(&mut self, net: &DistNetwork, doc: &mut Map<String, Value>) {
498 self.loads(net, doc);
499 self.generators(net, doc);
500 if !net.shunts.is_empty() {
501 let mut shunts = Map::new();
502 for s in &net.shunts {
503 let mut o = Map::new();
504 let what = format!("shunt {}", s.name);
505 o.insert("bus".into(), json!(s.bus.to_lowercase()));
506 o.insert(
507 "connections".into(),
508 json!(conns(&s.terminal_map, &mut self.warnings, &what)),
509 );
510 o.insert("gs".into(), matrix(&s.g));
511 o.insert("bs".into(), matrix(&s.b));
512 let configuration = if shunt_is_delta(&s.extras) {
516 "DELTA"
517 } else {
518 "WYE"
519 };
520 o.insert("configuration".into(), json!(configuration));
521 o.insert("model".into(), json!("CAPACITOR"));
522 o.insert("dispatchable".into(), json!("NO"));
523 o.insert("status".into(), Self::status(&s.extras));
524 o.insert(
525 "source_id".into(),
526 json!(format!("capacitor.{}", s.name.to_lowercase())),
527 );
528 self.extras_dropped(&s.extras, &["kv", "kvar", "conn"], &what);
529 shunts.insert(s.name.to_lowercase(), Value::Object(o));
530 }
531 doc.insert("shunt".into(), Value::Object(shunts));
532 }
533
534 let mut sources = Map::new();
535 for vs in &net.sources {
536 sources.insert(vs.name.to_lowercase(), self.voltage_source(vs));
537 }
538 doc.insert("voltage_source".into(), Value::Object(sources));
539 }
540
541 fn voltage_source(&mut self, vs: &VoltageSource) -> Value {
542 let mut o = Map::new();
543 let what = format!("voltage source {}", vs.name);
544 let connections = conns(&vs.terminal_map, &mut self.warnings, &what);
545 let n = connections.len();
546 o.insert("bus".into(), json!(vs.bus.to_lowercase()));
547 o.insert("connections".into(), json!(connections));
548 o.insert("configuration".into(), json!("WYE"));
549 o.insert(
550 "vm".into(),
551 json!(vs.v_magnitude.iter().map(|v| v / 1e3).collect::<Vec<_>>()),
552 );
553 o.insert(
554 "va".into(),
555 json!(
556 vs.v_angle
557 .iter()
558 .map(|a| a.to_degrees())
559 .collect::<Vec<_>>()
560 ),
561 );
562 if let (Some(rs), Some(xs)) = (vs.extras.get("rs"), vs.extras.get("xs")) {
566 o.insert("rs".into(), rs.clone());
567 o.insert("xs".into(), xs.clone());
568 } else {
569 let (rs, xs) = thevenin(vs, n);
570 if rs.iter().flatten().all(|&v| v == 0.0) {
571 self.warn(format!(
572 "{what}: no short circuit data; emitted an ideal source (zero rs/xs)"
573 ));
574 }
575 o.insert("rs".into(), matrix(&rs));
576 o.insert("xs".into(), matrix(&xs));
577 }
578 o.insert("status".into(), Self::status(&vs.extras));
579 o.insert(
580 "source_id".into(),
581 json!(format!("vsource.{}", vs.name.to_lowercase())),
582 );
583 self.extras_dropped(
586 &vs.extras,
587 &[
588 "basekv",
589 "basemva",
590 "pu",
591 "angle",
592 "mvasc1",
593 "mvasc3",
594 "x1r1",
595 "x0r0",
596 "rs",
597 "xs",
598 "isc1",
599 "isc3",
600 "configuration",
601 ],
602 &what,
603 );
604 Value::Object(o)
605 }
606
607 fn transformers(&mut self, net: &DistNetwork, doc: &mut Map<String, Value>) {
608 if net.transformers.is_empty() {
609 return;
610 }
611 let mut out = Map::new();
612 for t in &net.transformers {
613 out.insert(t.name.to_lowercase(), self.transformer(t));
614 }
615 doc.insert("transformer".into(), Value::Object(out));
616 }
617
618 fn transformer(&mut self, t: &DistTransformer) -> Value {
619 let mut o = Map::new();
620 let what = format!("transformer {}", t.name);
621 let phases = t.phases;
622
623 let stashed = t.extras.contains_key("pmd_polarity");
629 let mut buses = Vec::new();
630 let mut connections: Vec<Value> = Vec::new();
631 for (w_idx, w) in t.windings.iter().enumerate() {
632 buses.push(json!(w.bus.to_lowercase()));
633 let mut c = conns(&w.terminal_map, &mut self.warnings, &what);
634 if !stashed
635 && w_idx > 0
636 && t.windings[0].conn == WindingConn::Delta
637 && w.conn == WindingConn::Wye
638 && c.len() > 1
639 {
640 let phases_part = c.len() - 1;
641 c[..phases_part].rotate_left(1);
642 }
643 connections.push(json!(c));
644 }
645 o.insert("bus".into(), Value::Array(buses));
646 o.insert(
647 "connections".into(),
648 t.extras
649 .get("pmd_connections")
650 .cloned()
651 .unwrap_or(Value::Array(connections)),
652 );
653 o.insert(
654 "polarity".into(),
655 t.extras
656 .get("pmd_polarity")
657 .cloned()
658 .unwrap_or_else(|| json!(lag_polarity(&t.windings))),
659 );
660 o.insert(
661 "configuration".into(),
662 Value::Array(
663 t.windings
664 .iter()
665 .map(|w| {
666 json!(match w.conn {
667 WindingConn::Wye => "WYE",
668 WindingConn::Delta => "DELTA",
669 })
670 })
671 .collect(),
672 ),
673 );
674 o.insert(
675 "rw".into(),
676 json!(
677 t.windings
678 .iter()
679 .map(|w| w.r_pct / 100.0)
680 .collect::<Vec<_>>()
681 ),
682 );
683 o.insert(
684 "xsc".into(),
685 json!(t.xsc_pct.iter().map(|x| x / 100.0).collect::<Vec<_>>()),
686 );
687 o.insert(
688 "sm_nom".into(),
689 json!(
690 t.windings
691 .iter()
692 .map(|w| w.s_rating / 1e3)
693 .collect::<Vec<_>>()
694 ),
695 );
696 o.insert(
697 "vm_nom".into(),
698 json!(t.windings.iter().map(|w| w.v_ref / 1e3).collect::<Vec<_>>()),
699 );
700 let sm_ub =
701 Self::extras_f64(&t.extras, "emerghkva").unwrap_or(t.windings[0].s_rating / 1e3 * 1.5);
702 o.insert("sm_ub".into(), json!(sm_ub));
703 insert_tap_fields(&mut o, t, phases);
704 if let Some(controls) = t.extras.get("controls") {
705 o.insert("controls".into(), controls.clone());
706 }
707 let noloadloss = Self::extras_f64(&t.extras, "%noloadloss").unwrap_or(0.0) / 100.0;
708 let cmag = Self::extras_f64(&t.extras, "%imag").unwrap_or(0.0) / 100.0;
709 o.insert("noloadloss".into(), json!(noloadloss));
710 o.insert("cmag".into(), json!(cmag));
711 o.insert("status".into(), Self::status(&t.extras));
712 o.insert(
713 "source_id".into(),
714 json!(format!("transformer.{}", t.name.to_lowercase())),
715 );
716 self.extras_dropped(
717 &t.extras,
718 &["controls", "%loadloss", "%noloadloss", "%imag", "emerghkva"],
719 &what,
720 );
721 Value::Object(o)
722 }
723}
724
725fn insert_tap_fields(o: &mut Map<String, Value>, t: &DistTransformer, phases: usize) {
729 let nw = t.windings.len();
730 let mut insert = |key: &str, default: fn(&DistTransformer, usize, usize) -> Value| {
731 let v = t
732 .extras
733 .get(&format!("pmd_{key}"))
734 .cloned()
735 .unwrap_or_else(|| default(t, nw, phases));
736 o.insert(key.into(), v);
737 };
738 insert("tm_set", |t, _, phases| {
739 Value::Array(
740 t.windings
741 .iter()
742 .map(|w| json!(vec![w.tap; phases]))
743 .collect(),
744 )
745 });
746 insert("tm_fix", |_, nw, phases| {
747 Value::Array((0..nw).map(|_| json!(vec![true; phases])).collect())
748 });
749 insert("tm_lb", |_, nw, phases| {
750 Value::Array((0..nw).map(|_| json!(vec![0.9; phases])).collect())
751 });
752 insert("tm_ub", |_, nw, phases| {
753 Value::Array((0..nw).map(|_| json!(vec![1.1; phases])).collect())
754 });
755 insert("tm_step", |_, nw, phases| {
756 Value::Array((0..nw).map(|_| json!(vec![1.0 / 32.0; phases])).collect())
757 });
758}
759
760fn synthesized_settings(net: &DistNetwork) -> Value {
765 let mut settings = Map::new();
766 settings.insert("base_frequency".into(), json!(net.base_frequency));
767 settings.insert("power_scale_factor".into(), json!(1000.0));
768 settings.insert("voltage_scale_factor".into(), json!(1000.0));
769 let sbase = net
770 .sources
771 .first()
772 .and_then(|vs| Writer::extras_f64(&vs.extras, "basemva"))
773 .map_or(100_000.0, |mva| mva * 1e3);
774 settings.insert("sbase_default".into(), json!(sbase));
775 let mut vbases = Map::new();
776 for vs in &net.sources {
777 let phases = count_phases(vs).max(1) as f64;
778 let vln_kv = Writer::extras_f64(&vs.extras, "basekv").map_or_else(
779 || {
780 let pu = Writer::extras_f64(&vs.extras, "pu").unwrap_or(1.0);
781 vs.v_magnitude.first().copied().unwrap_or(0.0) / 1e3 / pu
782 },
783 |kv| kv / phases.sqrt(),
784 );
785 vbases.insert(vs.bus.to_lowercase(), json!(vln_kv));
786 }
787 settings.insert("vbases_default".into(), Value::Object(vbases));
788 Value::Object(settings)
789}
790
791pub(super) fn lag_polarity(windings: &[Winding]) -> Vec<i64> {
797 let nw = windings.len();
798 let mut polarity = vec![1i64; nw];
799 for (w_idx, w) in windings.iter().enumerate().skip(1) {
800 if windings[0].conn == WindingConn::Delta
801 && w.conn == WindingConn::Wye
802 && w.terminal_map.len() > 1
803 {
804 polarity[w_idx] = -1;
805 }
806 if w_idx == 2 && nw == 3 && windings[1].terminal_map.last() == w.terminal_map.first() {
808 polarity[w_idx] = -1;
809 }
810 }
811 polarity
812}
813
814fn inlined_codes(net: &DistNetwork) -> BTreeSet<String> {
817 let mut inlined = BTreeSet::new();
818 for c in &net.linecodes {
819 let mut refs = net
820 .lines
821 .iter()
822 .filter(|l| l.linecode.eq_ignore_ascii_case(&c.name))
823 .peekable();
824 if refs.peek().is_some()
825 && refs.all(|l| l.extras.get("pmd_inline").and_then(Value::as_bool) == Some(true))
826 {
827 inlined.insert(c.name.to_lowercase());
828 }
829 }
830 inlined
831}
832
833fn insert_impedance_matrices(o: &mut Map<String, Value>, c: &DistLineCode, base_frequency: f64) {
839 o.insert("rs".into(), matrix(&c.r_series));
840 o.insert("xs".into(), matrix(&c.x_series));
841 o.insert("g_fr".into(), matrix(&c.g_from));
842 o.insert("g_to".into(), matrix(&c.g_to));
843 if let (Some(fr), Some(to)) = (c.extras.get("pmd_b_fr"), c.extras.get("pmd_b_to")) {
844 o.insert("b_fr".into(), fr.clone());
845 o.insert("b_to".into(), to.clone());
846 } else {
847 let to_nf = 1.0 / (std::f64::consts::TAU * base_frequency * 1e-9);
848 o.insert("b_fr".into(), matrix(&scale(&c.b_from, to_nf)));
849 o.insert("b_to".into(), matrix(&scale(&c.b_to, to_nf)));
850 }
851}
852
853fn thevenin(vs: &VoltageSource, n_cond: usize) -> (Mat, Mat) {
858 let get = |key: &str| Writer::extras_f64(&vs.extras, key);
859 let basekv = get("basekv").unwrap_or_else(|| {
860 vs.v_magnitude.first().copied().unwrap_or(0.0) / 1e3 * (count_phases(vs) as f64).sqrt()
862 });
863 let phases = count_phases(vs);
864 if basekv <= 0.0 || phases == 0 {
865 return (zero_matrix(n_cond), zero_matrix(n_cond));
866 }
867 let mvasc3 = get("mvasc3").unwrap_or(2000.0);
868 let mvasc1 = get("mvasc1").unwrap_or(2100.0);
869 let x1r1 = get("x1r1").unwrap_or(4.0);
870 let x0r0 = get("x0r0").unwrap_or(3.0);
871 let factor = if phases == 1 { 1.0 } else { 3f64.sqrt() };
872
873 let isc1 = mvasc1 * 1e3 / (basekv * factor);
874 let x1 = basekv * basekv / mvasc3 / (1.0 + 1.0 / (x1r1 * x1r1)).sqrt();
875 let r1 = x1 / x1r1;
876 let a = 1.0 + x0r0 * x0r0;
877 let b = 4.0 * (r1 + x1 * x0r0);
878 let c = 4.0 * (r1 * r1 + x1 * x1) - (3.0 * basekv * 1000.0 / factor / isc1).powi(2);
879 let disc = (b * b - 4.0 * a * c).max(0.0).sqrt();
880 let r0 = ((-b + disc) / (2.0 * a)).max((-b - disc) / (2.0 * a));
881 let x0 = r0 * x0r0;
882
883 let r_self = (2.0 * r1 + r0) / 3.0;
884 let x_self = (2.0 * x1 + x0) / 3.0;
885 let r_mutual = (r0 - r1) / 3.0;
886 let x_mutual = (x0 - x1) / 3.0;
887
888 let mut r_mat = vec![vec![r_mutual; n_cond]; n_cond];
889 let mut x_mat = vec![vec![x_mutual; n_cond]; n_cond];
890 for i in 0..n_cond {
891 r_mat[i][i] = r_self;
892 x_mat[i][i] = x_self;
893 }
894 (r_mat, x_mat)
895}
896
897fn count_phases(vs: &VoltageSource) -> usize {
898 vs.v_magnitude.iter().filter(|&&v| v > 0.0).count()
899}
900
901fn source_vm_nom(extras: &Extras, v_nom: &[f64]) -> Option<Value> {
902 let raw = extras.get("kv")?;
903 if v_nom.is_empty() {
904 return Some(raw.clone());
905 }
906 if let Some(kv) = raw
907 .as_f64()
908 .or_else(|| raw.as_str().and_then(|s| s.parse().ok()))
909 {
910 if v_nom.iter().all(|v| same_voltage(*v, kv * 1e3)) {
911 return Some(json!(kv));
912 }
913 }
914 let vals: Vec<f64> = raw
915 .as_array()?
916 .iter()
917 .filter_map(serde_json::Value::as_f64)
918 .collect();
919 if vals.len() == 1 && v_nom.iter().all(|v| same_voltage(*v, vals[0] * 1e3)) {
920 return Some(raw.clone());
921 }
922 if vals.len() == v_nom.len()
923 && vals
924 .iter()
925 .zip(v_nom)
926 .all(|(a, b)| same_voltage(*b, *a * 1e3))
927 {
928 return Some(raw.clone());
929 }
930 None
931}
932
933fn same_voltage(a: f64, b: f64) -> bool {
934 (a - b).abs() <= 1e-9 * a.abs().max(b.abs()).max(1.0)
935}