1use std::collections::BTreeMap;
15use std::path::Path;
16use std::sync::Arc;
17
18use super::defaults as dd;
19use super::lex::{BusSpec, Value, VarMap};
20use super::raw::{RawDss, RawObject, parse_raw_with};
21use crate::error::{Error, Result};
22use crate::model::{
23 Configuration, DistBus, DistGenerator, DistLine, DistLineCode, DistLoad, DistLoadVoltageModel,
24 DistNetwork, DistShunt, DistSourceFormat, DistSwitch, DistTransformer, Extras, Mat,
25 UntypedObject, VoltageSource, Winding, WindingConn, pair_keys, square_from_rows,
26};
27
28pub fn parse_dss_file(path: impl AsRef<Path>) -> Result<DistNetwork> {
30 let path = path.as_ref();
31 let text = std::fs::read_to_string(path).map_err(|source| Error::Io {
32 path: path.display().to_string(),
33 source,
34 })?;
35 let raw = parse_raw_with(&text, &path.display().to_string(), &mut |p: &Path| {
36 std::fs::read_to_string(p)
37 });
38 Ok(network_from_raw(&raw, Arc::new(text)))
39}
40
41pub fn parse_dss_str(text: &str) -> DistNetwork {
44 let raw = parse_raw_with(text, "<string>", &mut |p: &Path| std::fs::read_to_string(p));
45 network_from_raw(&raw, Arc::new(text.to_string()))
46}
47
48pub fn network_from_raw(raw: &RawDss, source: Arc<String>) -> DistNetwork {
50 let mut rd = Reader {
51 net: DistNetwork {
52 name: raw.circuit_name.clone(),
53 base_frequency: dd::BASE_FREQUENCY,
54 source: Some(source),
55 source_format: Some(DistSourceFormat::Dss),
56 warnings: raw.warnings.clone(),
57 ..DistNetwork::default()
58 },
59 buses: BTreeMap::new(),
60 bus_order: Vec::new(),
61 linecode_units: BTreeMap::new(),
62 vars: &raw.vars,
63 };
64
65 for (name, value) in &raw.options {
66 if name.len() >= "defaultb".len() && "defaultbasefrequency".starts_with(name.as_str()) {
72 if let Ok(f) = value.to_f64(Some(rd.vars)) {
73 rd.net.base_frequency = f;
74 }
75 }
76 rd.net.options.push((name.clone(), value.text.clone()));
77 }
78 for cmd in &raw.commands {
79 rd.net.commands.push((cmd.verb.clone(), cmd.args.clone()));
80 }
81
82 for obj in raw.of_class("linecode") {
85 let lc = rd.linecode(obj);
86 rd.net.linecodes.push(lc);
87 }
88 for obj in raw.of_class("vsource") {
89 let vs = rd.vsource(obj);
90 rd.net.sources.push(vs);
91 }
92 for obj in raw.of_class("line") {
93 rd.line(obj);
94 }
95 for obj in raw.of_class("transformer") {
96 let t = rd.transformer(obj);
97 rd.net.transformers.push(t);
98 }
99 for obj in raw.of_class("load") {
100 let l = rd.load(obj);
101 rd.net.loads.push(l);
102 }
103 for obj in raw.of_class("capacitor") {
104 rd.capacitor(obj);
105 }
106 for obj in raw.of_class("reactor") {
107 rd.reactor(obj);
108 }
109 for obj in raw.of_class("generator") {
110 let g = rd.generator(obj);
111 rd.net.generators.push(g);
112 }
113 for obj in raw.of_class("swtcontrol") {
114 rd.swtcontrol(obj);
115 }
116 for obj in raw.of_class("regcontrol") {
117 rd.regcontrol(obj);
118 }
119 for obj in &raw.objects {
120 if !matches!(
121 obj.class.as_str(),
122 "linecode"
123 | "vsource"
124 | "line"
125 | "transformer"
126 | "load"
127 | "capacitor"
128 | "reactor"
129 | "generator"
130 | "swtcontrol"
131 | "regcontrol"
132 ) {
133 rd.net.untyped.push(UntypedObject::from(obj));
134 }
135 }
136
137 let known: std::collections::BTreeSet<String> = rd
140 .net
141 .linecodes
142 .iter()
143 .map(|c| c.name.to_ascii_lowercase())
144 .collect();
145 let missing: Vec<String> = rd
146 .net
147 .lines
148 .iter()
149 .filter(|l| !known.contains(&l.linecode.to_ascii_lowercase()))
150 .map(|l| {
151 format!(
152 "line {} references unknown linecode `{}`",
153 l.name, l.linecode
154 )
155 })
156 .collect();
157 rd.net.warnings.extend(missing);
158
159 finish_buses(rd, raw)
160}
161
162fn finish_buses(mut rd: Reader, raw: &RawDss) -> DistNetwork {
170 let mut coords: BTreeMap<String, (f64, f64)> = BTreeMap::new();
171 for c in &raw.buscoords {
172 coords.insert(c.bus.to_ascii_lowercase(), (c.x, c.y));
173 }
174 let buses = std::mem::take(&mut rd.bus_order);
175 let states = std::mem::take(&mut rd.buses);
176 let mut net = rd.net;
177 let mut neutral_names: BTreeMap<String, String> = BTreeMap::new();
178 for id in buses {
179 let st = &states[&id];
180 let mut terminals: Vec<i32> = st.nodes.iter().copied().filter(|&n| n != 0).collect();
181 terminals.sort_unstable();
182 let mut bus = DistBus {
183 id: st.display.clone(),
184 terminals: terminals.iter().map(ToString::to_string).collect(),
185 ..DistBus::default()
186 };
187 if st.nodes.contains(&0) {
188 let neutral = terminals.last().map_or(4, |&n| n.max(3) + 1);
189 bus.terminals.push(neutral.to_string());
190 bus.grounded.push(neutral.to_string());
191 neutral_names.insert(id.clone(), neutral.to_string());
192 }
193 if let Some((x, y)) = coords.get(&id) {
194 bus.extras.insert("x".into(), (*x).into());
195 bus.extras.insert("y".into(), (*y).into());
196 }
197 net.buses.push(bus);
198 }
199
200 let rewrite = |bus: &str, map: &mut [String]| {
201 if let Some(neutral) = neutral_names.get(&bus.to_ascii_lowercase()) {
202 for t in map.iter_mut().filter(|t| *t == "0") {
203 t.clone_from(neutral);
204 }
205 }
206 };
207 for l in &mut net.lines {
208 rewrite(&l.bus_from, &mut l.terminal_map_from);
209 rewrite(&l.bus_to, &mut l.terminal_map_to);
210 }
211 for s in &mut net.switches {
212 rewrite(&s.bus_from, &mut s.terminal_map_from);
213 rewrite(&s.bus_to, &mut s.terminal_map_to);
214 }
215 for l in &mut net.loads {
216 rewrite(&l.bus, &mut l.terminal_map);
217 }
218 for g in &mut net.generators {
219 rewrite(&g.bus, &mut g.terminal_map);
220 }
221 for s in &mut net.shunts {
222 rewrite(&s.bus, &mut s.terminal_map);
223 }
224 for v in &mut net.sources {
225 rewrite(&v.bus, &mut v.terminal_map);
226 }
227 for t in &mut net.transformers {
228 for w in &mut t.windings {
229 rewrite(&w.bus, &mut w.terminal_map);
230 }
231 }
232 net
233}
234
235impl From<&RawObject> for UntypedObject {
236 fn from(obj: &RawObject) -> Self {
237 UntypedObject {
238 class: obj.class.clone(),
239 name: obj.name.clone(),
240 props: obj
241 .props
242 .iter()
243 .map(|p| (p.name.clone(), p.value.text.clone()))
244 .collect(),
245 }
246 }
247}
248
249struct BusState {
250 display: String,
251 nodes: std::collections::BTreeSet<i32>,
252}
253
254struct Reader<'a> {
255 net: DistNetwork,
256 buses: BTreeMap<String, BusState>,
257 bus_order: Vec<String>,
258 linecode_units: BTreeMap<String, Option<f64>>,
262 vars: &'a VarMap,
263}
264
265struct Props<'a> {
268 by_name: BTreeMap<&'a str, &'a Value>,
269 consumed: std::cell::RefCell<Vec<&'a str>>,
270}
271
272impl<'a> Props<'a> {
273 fn new(obj: &'a RawObject) -> Self {
274 let mut by_name = BTreeMap::new();
275 for p in &obj.props {
276 if let Some(n) = &p.name {
277 by_name.insert(n.as_str(), &p.value);
278 }
279 }
280 Props {
281 by_name,
282 consumed: std::cell::RefCell::new(Vec::new()),
283 }
284 }
285
286 fn get(&self, name: &'a str) -> Option<&'a Value> {
287 self.consumed.borrow_mut().push(name);
288 self.by_name.get(name).copied()
289 }
290
291 fn leftovers(&self) -> Vec<(&str, &Value)> {
293 let consumed = self.consumed.borrow();
294 self.by_name
295 .iter()
296 .filter(|(k, _)| !consumed.contains(*k) && **k != "like")
297 .map(|(k, v)| (*k, *v))
298 .collect()
299 }
300}
301
302const REACTOR_IMPEDANCE_FORMS: &[&str] = &[
309 "rmatrix", "xmatrix", "r", "x", "z1", "z2", "z0", "z", "rcurve", "lcurve", "lmh",
310];
311
312#[derive(Clone, Copy)]
313struct KvarShuntSpec {
314 class: &'static str,
315 series_name: &'static str,
316 default_phases: usize,
317 default_kvar: f64,
318 default_kv: f64,
319 b_sign: f64,
320}
321
322const CAPACITOR_KVAR_SHUNT: KvarShuntSpec = KvarShuntSpec {
323 class: "capacitor",
324 series_name: "capacitors",
325 default_phases: dd::capacitor::PHASES,
326 default_kvar: dd::capacitor::KVAR,
327 default_kv: dd::capacitor::KV,
328 b_sign: 1.0,
329};
330
331const REACTOR_KVAR_SHUNT: KvarShuntSpec = KvarShuntSpec {
332 class: "reactor",
333 series_name: "reactors",
334 default_phases: dd::reactor::PHASES,
335 default_kvar: dd::reactor::KVAR,
336 default_kv: dd::reactor::KV,
337 b_sign: -1.0,
338};
339
340impl Reader<'_> {
341 fn warn(&mut self, msg: impl Into<String>) {
342 self.net.warnings.push(msg.into());
343 }
344
345 fn defaulted(&mut self, class: &str, name: &str, field: &'static str) {
346 let fields = self
347 .net
348 .defaulted
349 .entry(format!("{class}.{name}"))
350 .or_default();
351 if !fields.contains(&field) {
352 fields.push(field);
353 }
354 }
355
356 fn f64_prop(&mut self, p: Option<&Value>) -> Option<f64> {
357 p.and_then(|v| v.to_f64(Some(self.vars)).ok())
358 }
359
360 fn usize_prop(&mut self, p: Option<&Value>) -> Option<usize> {
361 p.and_then(|v| v.to_i64(Some(self.vars)).ok())
362 .map(|i| usize::try_from(i).unwrap_or(0))
363 }
364
365 fn units_code(&mut self, units: Option<&str>, class: &str, name: &str) -> Option<f64> {
370 let u = units?;
371 if let Some(f) = dd::unit_to_meters(u) {
372 return Some(f);
373 }
374 if !u.to_ascii_lowercase().starts_with("no") {
375 self.net.warnings.push(format!(
376 "{class} {name}: unknown units `{u}`; treated as none"
377 ));
378 }
379 None
380 }
381
382 fn stash_numeric(&self, v: &Value) -> serde_json::Value {
387 if v.text.parse::<f64>().is_ok() {
388 v.text.clone().into()
389 } else {
390 match v.to_f64(Some(self.vars)) {
391 Ok(n) => n.into(),
392 Err(_) => v.text.clone().into(),
393 }
394 }
395 }
396
397 fn stash_kv_and_phases(&self, props: &Props, extras: &mut Extras, kv: f64, phases: usize) {
400 let kv_value = match props.by_name.get("kv") {
401 Some(written) => self.stash_numeric(written),
402 None => kv.into(),
403 };
404 extras.insert("kv".into(), kv_value);
405 let phases_value = match props.by_name.get("phases") {
406 Some(written) => self.stash_numeric(written),
407 None => (phases as u64).into(),
408 };
409 extras.insert("phases".into(), phases_value);
410 if let Some(written) = props.by_name.get("conn") {
414 extras.insert("conn".into(), written.text.clone().into());
415 }
416 }
417
418 fn f64_or(
420 &mut self,
421 props: &Props,
422 key: &'static str,
423 class: &str,
424 name: &str,
425 default: f64,
426 ) -> f64 {
427 if let Some(v) = self.f64_prop(props.get(key)) {
428 v
429 } else {
430 self.defaulted(class, name, key);
431 default
432 }
433 }
434
435 fn usize_or(
436 &mut self,
437 props: &Props,
438 key: &'static str,
439 class: &str,
440 name: &str,
441 default: usize,
442 ) -> usize {
443 if let Some(v) = self.usize_prop(props.get(key)) {
444 v
445 } else {
446 self.defaulted(class, name, key);
447 default
448 }
449 }
450
451 fn terminals(
456 &mut self,
457 spec: &BusSpec,
458 phases: usize,
459 nconds: usize,
460 keep: usize,
461 ) -> Vec<String> {
462 let mut nodes: Vec<i32> = (1..=i32::try_from(nconds).unwrap_or(i32::MAX)).collect();
463 for n in nodes.iter_mut().skip(phases) {
464 *n = 0;
465 }
466 for (i, &n) in spec.nodes.iter().enumerate().take(nconds) {
467 nodes[i] = n.max(0); }
469 let key = spec.name.to_ascii_lowercase();
470 let state = self.buses.entry(key.clone()).or_insert_with(|| {
471 self.bus_order.push(key.clone());
472 BusState {
473 display: spec.name.clone(),
474 nodes: std::collections::BTreeSet::new(),
475 }
476 });
477 for &n in nodes.iter().take(keep) {
478 state.nodes.insert(n);
479 }
480 nodes.truncate(keep);
481 nodes.iter().map(ToString::to_string).collect()
482 }
483
484 fn linecode(&mut self, obj: &RawObject) -> DistLineCode {
487 let props = Props::new(obj);
488 let n = self.usize_or(
489 &props,
490 "nphases",
491 "linecode",
492 &obj.name,
493 dd::linecode::NPHASES,
494 );
495 let units = props.get("units").map(|v| v.text.clone());
496 let units_m = self.units_code(units.as_deref(), "linecode", &obj.name);
497 let per_meter = units_m.unwrap_or(1.0);
498 self.linecode_units
499 .insert(obj.name.to_ascii_lowercase(), units_m);
500
501 let freq = self
502 .f64_prop(props.get("basefreq"))
503 .unwrap_or(self.net.base_frequency);
504
505 let z = self.impedance_matrices(
506 &props,
507 n,
508 "linecode",
509 &obj.name,
510 dd::line::R1,
511 dd::line::X1,
512 dd::line::R0,
513 dd::line::X0,
514 dd::line::C1_NF,
515 dd::line::C0_NF,
516 );
517 if z.all_default {
518 self.defaulted("linecode", &obj.name, "rmatrix");
519 }
520
521 let b_half = scale_mat(
524 &z.c_nf,
525 std::f64::consts::TAU * freq * 1e-9 / per_meter / 2.0,
526 );
527 let zero = vec![vec![0.0; n]; n];
528
529 let amps = self.f64_or(
532 &props,
533 "emergamps",
534 "linecode",
535 &obj.name,
536 dd::line::EMERGAMPS,
537 );
538 let i_max = Some(vec![amps; n]);
539
540 let mut extras = extras_from_leftovers(&props);
541 if let Some(u) = units {
542 extras.insert("units".into(), u.into());
543 }
544 for (key, text) in z.malformed {
545 extras.insert(key.to_string(), text.into());
546 }
547 DistLineCode {
548 name: obj.name.clone(),
549 n_conductors: n,
550 r_series: scale_mat(&z.r, 1.0 / per_meter),
551 x_series: scale_mat(&z.x, 1.0 / per_meter),
552 g_from: zero.clone(),
553 b_from: b_half.clone(),
554 g_to: zero,
555 b_to: b_half,
556 i_max,
557 s_max: None,
558 extras,
559 }
560 }
561
562 #[allow(clippy::too_many_arguments)]
565 fn impedance_matrices(
566 &mut self,
567 props: &Props,
568 n: usize,
569 class: &str,
570 name: &str,
571 r1d: f64,
572 x1d: f64,
573 r0d: f64,
574 x0d: f64,
575 c1d: f64,
576 c0d: f64,
577 ) -> SeriesImpedance {
578 let mut malformed: Vec<(&'static str, String)> = Vec::new();
579 let mut rows = |key: &'static str| -> Option<Mat> {
580 let v = props.get(key)?;
581 let parsed = v
582 .to_rows(Some(self.vars))
583 .ok()
584 .and_then(|rows| square_from_rows(&rows, n));
585 if parsed.is_none() {
586 malformed.push((key, v.text.clone()));
587 }
588 parsed
589 };
590 let rm = rows("rmatrix");
591 let xm = rows("xmatrix");
592 let cm = rows("cmatrix");
593 for (key, _) in &malformed {
597 self.warn(format!(
598 "{class} {name}: `{key}` does not parse as a {n}x{n} matrix; \
599 sequence values apply and the text is kept in extras"
600 ));
601 }
602 let any_written = [
603 "rmatrix", "xmatrix", "cmatrix", "r1", "x1", "r0", "x0", "c1", "c0", "b1", "b0",
604 ]
605 .iter()
606 .any(|k| props.by_name.contains_key(*k));
607
608 let seq = |props: &Props, k1: &'static str, k0: &'static str, d1: f64, d0: f64| {
609 let v1 = props
610 .get(k1)
611 .and_then(|v| v.to_f64(Some(self.vars)).ok())
612 .unwrap_or(d1);
613 let v0 = props
614 .get(k0)
615 .and_then(|v| v.to_f64(Some(self.vars)).ok())
616 .unwrap_or(d0);
617 let s = (2.0 * v1 + v0) / 3.0;
620 let m = (v0 - v1) / 3.0;
621 let mut mat = vec![vec![m; n]; n];
622 for (i, row) in mat.iter_mut().enumerate() {
623 row[i] = s;
624 }
625 mat
626 };
627
628 SeriesImpedance {
629 r: rm.unwrap_or_else(|| seq(props, "r1", "r0", r1d, r0d)),
630 x: xm.unwrap_or_else(|| seq(props, "x1", "x0", x1d, x0d)),
631 c_nf: cm.unwrap_or_else(|| seq(props, "c1", "c0", c1d, c0d)),
632 all_default: !any_written,
633 malformed,
634 }
635 }
636
637 fn vsource(&mut self, obj: &RawObject) -> VoltageSource {
640 let props = Props::new(obj);
641 let phases = self.usize_or(&props, "phases", "vsource", &obj.name, dd::vsource::PHASES);
642 let basekv = self.f64_or(&props, "basekv", "vsource", &obj.name, dd::vsource::BASEKV);
643 let pu = self.f64_or(&props, "pu", "vsource", &obj.name, dd::vsource::PU);
644 let angle_deg = self.f64_or(
645 &props,
646 "angle",
647 "vsource",
648 &obj.name,
649 dd::vsource::ANGLE_DEG,
650 );
651 let spec = if let Some(v) = props.get("bus1") {
652 v.to_bus_spec()
653 } else {
654 self.defaulted("vsource", &obj.name, "bus1");
655 Value::new(dd::vsource::BUS1).to_bus_spec()
656 };
657 let map = self.terminals(&spec, phases, phases + 1, phases + 1);
658
659 let v_ln = if phases == 1 {
665 basekv * 1e3 * pu
666 } else {
667 basekv * 1e3 * pu / (2.0 * (std::f64::consts::PI / phases as f64).sin())
668 };
669 let mut v_magnitude = vec![v_ln; phases];
670 let mut v_angle: Vec<f64> = (0..phases)
671 .map(|k| {
672 let deg = angle_deg - 360.0 / phases as f64 * k as f64;
673 let a = deg.to_radians();
674 let shifted = (a + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU);
677 if shifted <= 0.0 {
678 std::f64::consts::PI
679 } else {
680 shifted - std::f64::consts::PI
681 }
682 })
683 .collect();
684 v_magnitude.push(0.0);
686 v_angle.push(0.0);
687
688 let mut extras = extras_from_leftovers(&props);
691 extras.insert("basekv".into(), basekv.into());
692 extras.insert("angle".into(), angle_deg.into());
693 if (pu - 1.0).abs() > 0.0 {
694 extras.insert("pu".into(), pu.into());
695 }
696 VoltageSource {
697 name: obj.name.clone(),
698 bus: spec.name,
699 terminal_map: map,
700 v_magnitude,
701 v_angle,
702 extras,
703 }
704 }
705
706 fn line(&mut self, obj: &RawObject) {
709 let props = Props::new(obj);
710 let phases = self
711 .usize_prop(props.get("phases"))
712 .unwrap_or(dd::line::PHASES);
713 let spec1 = bus_spec(props.get("bus1"), "");
714 let spec2 = bus_spec(props.get("bus2"), "");
715 let map_from = self.terminals(&spec1, phases, phases, phases);
717 let map_to = self.terminals(&spec2, phases, phases, phases);
718
719 let is_switch = props.get("switch").is_some_and(super::lex::Value::to_bool);
720 if is_switch {
721 let amps = self.f64_or(&props, "emergamps", "line", &obj.name, dd::line::EMERGAMPS);
722 let i_max = Some(vec![amps; phases]);
723 let mut extras = extras_from_leftovers(&props);
724 for k in ["linecode", "length", "r1", "x1", "rmatrix", "xmatrix"] {
727 if let Some(v) = props.by_name.get(k) {
728 extras.insert(k.to_string(), v.text.clone().into());
729 self.warn(format!(
730 "line {}: `{k}` is ignored by OpenDSS on switch=yes; kept in extras",
731 obj.name
732 ));
733 }
734 }
735 self.net.switches.push(DistSwitch {
736 name: obj.name.clone(),
737 bus_from: spec1.name,
738 bus_to: spec2.name,
739 terminal_map_from: map_from,
740 terminal_map_to: map_to,
741 open: false,
742 i_max,
743 extras,
744 });
745 return;
746 }
747
748 let length_units = props.get("units").map(|v| v.text.clone());
749 let line_units_m = self.units_code(length_units.as_deref(), "line", &obj.name);
750 let length = self.f64_or(&props, "length", "line", &obj.name, dd::line::LENGTH);
751
752 let mut malformed: Vec<(&'static str, String)> = Vec::new();
759 let (linecode, length_factor) = if let Some(code) = props.get("linecode") {
760 let lc_units_m = self
761 .linecode_units
762 .get(&code.text.to_ascii_lowercase())
763 .copied()
764 .flatten();
765 let factor = match (lc_units_m, line_units_m) {
766 (Some(_), Some(lf)) => lf,
767 (Some(lcf), None) => lcf,
768 (None, _) => 1.0,
769 };
770 (code.text.clone(), factor)
771 } else {
772 let factor = line_units_m.unwrap_or(1.0);
773 let (code, bad) = self.synthesize_linecode(&props, phases, factor, &obj.name);
774 malformed = bad;
775 (code, factor)
776 };
777
778 let mut extras = extras_from_leftovers(&props);
779 if let Some(u) = length_units {
780 extras.insert("units".into(), u.into());
781 }
782 for (key, text) in malformed {
783 extras.insert(key.to_string(), text.into());
784 }
785 self.net.lines.push(DistLine {
786 name: obj.name.clone(),
787 bus_from: spec1.name,
788 bus_to: spec2.name,
789 terminal_map_from: map_from,
790 terminal_map_to: map_to,
791 linecode,
792 length: length * length_factor,
793 extras,
794 });
795 }
796
797 fn synthesize_linecode(
801 &mut self,
802 props: &Props,
803 phases: usize,
804 length_factor: f64,
805 line_name: &str,
806 ) -> (String, Vec<(&'static str, String)>) {
807 let z = self.impedance_matrices(
808 props,
809 phases,
810 "line",
811 line_name,
812 dd::line::R1,
813 dd::line::X1,
814 dd::line::R0,
815 dd::line::X0,
816 dd::line::C1_NF,
817 dd::line::C0_NF,
818 );
819 if z.all_default {
820 self.defaulted("line", line_name, "r1");
821 self.defaulted("line", line_name, "x1");
822 }
823 let b_half = scale_mat(
824 &z.c_nf,
825 std::f64::consts::TAU * self.net.base_frequency * 1e-9 / length_factor / 2.0,
826 );
827 let zero = vec![vec![0.0; phases]; phases];
828 let amps = self.f64_or(props, "emergamps", "line", line_name, dd::line::EMERGAMPS);
829 let i_max = Some(vec![amps; phases]);
830 let name = format!("_line_{line_name}");
831 self.net.linecodes.push(DistLineCode {
832 name: name.clone(),
833 n_conductors: phases,
834 r_series: scale_mat(&z.r, 1.0 / length_factor),
835 x_series: scale_mat(&z.x, 1.0 / length_factor),
836 g_from: zero.clone(),
837 b_from: b_half.clone(),
838 g_to: zero,
839 b_to: b_half,
840 i_max,
841 s_max: None,
842 extras: Extras::new(),
843 });
844 (name, z.malformed)
845 }
846
847 fn load_power(&mut self, obj: &RawObject) -> LoadPower {
861 let mut s = LoadPower {
862 kw: dd::load::KW,
863 kvar: 0.0,
867 pf: dd::load::PF,
868 spec_kvar: false, kw_written: false,
870 pf_written: false,
871 };
872 let mut start = 0;
873 for end in obj.edit_bounds() {
874 for p in &obj.props[start..end] {
875 let Some(key @ ("kw" | "kvar" | "pf")) = p.name.as_deref() else {
876 continue;
877 };
878 let Some(v) = self.f64_prop(Some(&p.value)) else {
879 continue;
880 };
881 match key {
882 "kw" => {
883 s.kw = v;
884 s.spec_kvar = false;
885 s.kw_written = true;
886 }
887 "kvar" => {
888 s.kvar = v;
889 s.spec_kvar = true;
890 }
891 _ => {
892 s.pf = v;
893 s.pf_written = true;
894 }
895 }
896 }
897 start = end;
898 if s.spec_kvar {
900 let kva = s.kw.hypot(s.kvar);
901 if kva > 0.0 {
902 s.pf = s.kw / kva;
903 if s.kw * s.kvar < 0.0 {
905 s.pf = -s.pf;
906 }
907 }
908 } else {
909 s.kvar = s.kw * (1.0 / (s.pf * s.pf) - 1.0).sqrt();
910 if s.pf < 0.0 {
911 s.kvar = -s.kvar;
912 }
913 }
914 }
915 s
916 }
917
918 fn load(&mut self, obj: &RawObject) -> DistLoad {
919 let props = Props::new(obj);
920 let phases = self.usize_or(&props, "phases", "load", &obj.name, dd::load::PHASES);
921 let conn_delta = props.get("conn").is_some_and(|v| {
922 v.text.to_ascii_lowercase().starts_with('d') || v.text.eq_ignore_ascii_case("ll")
923 });
924 let kv = self.f64_or(&props, "kv", "load", &obj.name, dd::load::KV);
925 let LoadPower {
926 kw,
927 kvar: q_total,
928 pf,
929 spec_kvar,
930 kw_written,
931 pf_written,
932 } = self.load_power(obj);
933 if !kw_written {
934 self.defaulted("load", &obj.name, "kw");
935 }
936 let _ = (props.get("kw"), props.get("kvar"), props.get("pf"));
938 let mut pf_source: Option<f64> = None;
944 if !spec_kvar {
945 if !pf_written {
946 self.defaulted("load", &obj.name, "pf");
947 }
948 pf_source = Some(pf);
949 }
950 let model = self
951 .usize_prop(props.get("model"))
952 .map_or(dd::load::MODEL, |m| i64::try_from(m).unwrap_or(i64::MAX));
953
954 let spec = bus_spec(props.get("bus1"), "");
955 let nconds = if conn_delta && phases == 3 {
956 phases
957 } else {
958 phases + 1
959 };
960 let map = self.terminals(&spec, phases, nconds, nconds);
961
962 let configuration = if phases == 1 {
963 Configuration::SinglePhase
964 } else if conn_delta {
965 Configuration::Delta
966 } else {
967 Configuration::Wye
968 };
969
970 let mut extras = extras_from_leftovers(&props);
977 self.stash_kv_and_phases(&props, &mut extras, kv, phases);
978 if let Some(pf) = pf_source {
979 extras.insert("pf".into(), pf.into());
980 }
981 if model != 1 {
982 extras.insert("model".into(), model.into());
983 }
984 let v_phase = if phases >= 2 && configuration == Configuration::Wye {
985 kv * 1e3 / 3f64.sqrt()
986 } else {
987 kv * 1e3
988 };
989 let v_nom = vec![v_phase; phases];
990 let zipv = props
991 .get("zipv")
992 .and_then(|v| v.to_vector(Some(self.vars)).ok())
993 .unwrap_or_default();
994 let voltage_model = match model {
995 2 => DistLoadVoltageModel::ConstantImpedance { v_nom },
996 5 => DistLoadVoltageModel::ConstantCurrent { v_nom },
997 8 if zipv.len() >= 6 => DistLoadVoltageModel::Zip {
998 v_nom,
999 alpha_z: vec![zipv[0]; phases],
1000 alpha_i: vec![zipv[1]; phases],
1001 alpha_p: vec![zipv[2]; phases],
1002 beta_z: vec![zipv[3]; phases],
1003 beta_i: vec![zipv[4]; phases],
1004 beta_p: vec![zipv[5]; phases],
1005 },
1006 8 => DistLoadVoltageModel::Zip {
1007 v_nom,
1008 alpha_z: Vec::new(),
1009 alpha_i: Vec::new(),
1010 alpha_p: Vec::new(),
1011 beta_z: Vec::new(),
1012 beta_i: Vec::new(),
1013 beta_p: Vec::new(),
1014 },
1015 _ => DistLoadVoltageModel::ConstantPower { v_nom },
1016 };
1017 DistLoad {
1018 name: obj.name.clone(),
1019 bus: spec.name,
1020 terminal_map: map,
1021 configuration,
1022 p_nom: vec![kw * 1e3 / phases as f64; phases],
1023 q_nom: vec![q_total * 1e3 / phases as f64; phases],
1024 voltage_model,
1025 extras,
1026 }
1027 }
1028
1029 #[allow(clippy::too_many_lines)] fn transformer(&mut self, obj: &RawObject) -> DistTransformer {
1033 let mut phases = dd::transformer::PHASES;
1036 let mut n_windings = dd::transformer::WINDINGS;
1037 let mut windings = vec![WindingRaw::default(); n_windings];
1038 let mut active = 0usize;
1039 let mut xhl = dd::transformer::XHL;
1040 let mut xht = dd::transformer::XHT;
1041 let mut xlt = dd::transformer::XLT;
1042 let mut xhl_specified = false;
1043 let mut x_pairs: BTreeMap<(usize, usize), f64> = BTreeMap::new();
1044 let mut extras = Extras::new();
1045 let conn_is_delta =
1046 |t: &str| t.to_ascii_lowercase().starts_with('d') || t.eq_ignore_ascii_case("ll");
1047 for p in &obj.props {
1048 let Some(name) = &p.name else { continue };
1049 let v = &p.value;
1050 match name.as_str() {
1051 "phases" => {
1052 phases = self.usize_prop(Some(v)).unwrap_or(phases);
1053 }
1054 "windings" => {
1055 n_windings = self.usize_prop(Some(v)).unwrap_or(n_windings).max(1);
1056 windings = vec![WindingRaw::default(); n_windings];
1057 active = 0;
1058 }
1059 "wdg" => {
1060 let k = self.usize_prop(Some(v)).unwrap_or(1).max(1);
1061 grow(&mut windings, k, &mut n_windings);
1062 active = k - 1;
1063 }
1064 "bus" => windings[active].bus = Some(v.to_bus_spec()),
1065 "conn" => windings[active].conn_delta = conn_is_delta(&v.text),
1066 "kv" | "kva" | "tap" | "%r" => {
1067 let parsed = self.f64_prop(Some(v));
1068 let w = &mut windings[active];
1069 match name.as_str() {
1070 "kv" => {
1071 w.kv = parsed.unwrap_or(w.kv);
1072 w.kv_specified = true;
1073 }
1074 "kva" => {
1075 w.kva = parsed.unwrap_or(w.kva);
1076 w.kva_specified = true;
1077 }
1078 "tap" => w.tap = parsed.unwrap_or(w.tap),
1079 _ => w.r_pct = parsed.unwrap_or(w.r_pct),
1080 }
1081 }
1082 "buses" | "conns" => {
1083 let items = v.to_string_list(Some(self.vars));
1084 grow(&mut windings, items.len(), &mut n_windings);
1085 apply_winding_strings(&mut windings, name, &items);
1086 }
1087 "kvs" | "kvas" | "taps" | "%rs" => match v.to_vector(Some(self.vars)) {
1088 Ok(items) => {
1089 grow(&mut windings, items.len(), &mut n_windings);
1090 apply_winding_numbers(&mut windings, name, &items);
1091 }
1092 Err(e) => self.warn(format!("transformer {}: {name}: {e}", obj.name)),
1093 },
1094 "%loadloss" => {
1095 if let Some(ll) = self.f64_prop(Some(v)) {
1100 for w in windings.iter_mut().take(2) {
1101 w.r_pct = ll / 2.0;
1102 }
1103 }
1104 extras.insert("%loadloss".to_string(), v.text.clone().into());
1105 }
1106 "xhl" | "x12" => {
1107 xhl = self.f64_prop(Some(v)).unwrap_or(xhl);
1108 xhl_specified = true;
1109 x_pairs.insert((0, 1), xhl);
1110 }
1111 "xht" | "x13" => {
1112 xht = self.f64_prop(Some(v)).unwrap_or(xht);
1113 x_pairs.insert((0, 2), xht);
1114 }
1115 "xlt" | "x23" => {
1116 xlt = self.f64_prop(Some(v)).unwrap_or(xlt);
1117 x_pairs.insert((1, 2), xlt);
1118 }
1119 other if x_pair_key(other).is_some() => {
1120 if let Some((i, j)) = x_pair_key(other) {
1121 let x = self.f64_prop(Some(v)).unwrap_or(0.0);
1122 x_pairs.insert((i, j), x);
1123 }
1124 }
1125 other => {
1126 extras.insert(other.to_string(), v.text.clone().into());
1127 }
1128 }
1129 }
1130
1131 if !xhl_specified {
1132 self.defaulted("transformer", &obj.name, "xhl");
1133 }
1134 let out = self.finish_windings(&windings, phases, &obj.name);
1135
1136 let xsc_pct = if n_windings >= 3 {
1137 pair_keys(n_windings)
1138 .into_iter()
1139 .map(|pair| {
1140 x_pairs.get(&pair).copied().unwrap_or(match pair {
1141 (0, 1) => xhl,
1142 (0, 2) => xht,
1143 (1, 2) => xlt,
1144 _ => 0.0,
1145 })
1146 })
1147 .collect()
1148 } else {
1149 vec![xhl]
1150 };
1151 DistTransformer {
1152 name: obj.name.clone(),
1153 windings: out,
1154 xsc_pct,
1155 phases,
1156 extras,
1157 }
1158 }
1159
1160 fn finish_windings(
1163 &mut self,
1164 windings: &[WindingRaw],
1165 phases: usize,
1166 name: &str,
1167 ) -> Vec<Winding> {
1168 let mut out = Vec::with_capacity(windings.len());
1169 for (i, w) in windings.iter().enumerate() {
1170 if !w.kv_specified {
1171 self.defaulted("transformer", name, "kv");
1172 }
1173 if !w.kva_specified {
1174 self.defaulted("transformer", name, "kva");
1175 }
1176 let spec = w
1177 .bus
1178 .clone()
1179 .unwrap_or_else(|| Value::new(format!("{name}_w{}", i + 1)).to_bus_spec());
1180 let keep = if w.conn_delta {
1186 phases.max(2)
1187 } else {
1188 phases + 1
1189 };
1190 let map = self.terminals(&spec, phases, phases + 1, keep);
1191 out.push(Winding {
1192 bus: spec.name,
1193 terminal_map: map,
1194 conn: if w.conn_delta {
1195 WindingConn::Delta
1196 } else {
1197 WindingConn::Wye
1198 },
1199 v_ref: w.kv * 1e3,
1200 s_rating: w.kva * 1e3,
1201 r_pct: w.r_pct,
1202 tap: w.tap,
1203 });
1204 }
1205 out
1206 }
1207
1208 fn capacitor(&mut self, obj: &RawObject) {
1211 self.kvar_shunt(obj, CAPACITOR_KVAR_SHUNT);
1212 }
1213
1214 fn reactor(&mut self, obj: &RawObject) {
1222 let props = Props::new(obj);
1223 let phases = self.usize_or(&props, "phases", "reactor", &obj.name, dd::reactor::PHASES);
1224 if phases == 0 {
1225 self.warn(format!(
1226 "reactor {}: nonpositive `phases` value is not a typed shunt; kept untyped",
1227 obj.name
1228 ));
1229 self.net.untyped.push(UntypedObject::from(obj));
1230 return;
1231 }
1232 let bus = bus_spec(props.get("bus1"), "");
1233 let bus2 = props.get("bus2").map(super::lex::Value::to_bus_spec);
1234 let grounding_return = bus2
1235 .as_ref()
1236 .is_some_and(|return_bus| same_bus_ground_return(&bus, return_bus, phases));
1237
1238 if bus2.is_some() && !grounding_return {
1239 self.warn(format!(
1240 "reactor {}: series reactors (bus2) are not typed yet; kept untyped",
1241 obj.name
1242 ));
1243 self.net.untyped.push(UntypedObject::from(obj));
1244 return;
1245 }
1246
1247 if let Some(form) = REACTOR_IMPEDANCE_FORMS
1248 .iter()
1249 .find(|k| !matches!(**k, "r" | "x") && props.by_name.contains_key(**k))
1250 {
1251 self.warn(format!(
1252 "reactor {}: impedance form (`{form}`) is not typed yet; kept untyped",
1253 obj.name
1254 ));
1255 self.net.untyped.push(UntypedObject::from(obj));
1256 return;
1257 }
1258 let has_rx = props.by_name.contains_key("r") || props.by_name.contains_key("x");
1259 if has_rx {
1260 if grounding_return {
1261 self.grounding_impedance_reactor(obj, &props, &bus, phases);
1262 } else {
1263 let form = if props.by_name.contains_key("r") {
1264 "r"
1265 } else {
1266 "x"
1267 };
1268 self.warn(format!(
1269 "reactor {}: impedance form (`{form}`) is not typed yet; kept untyped",
1270 obj.name
1271 ));
1272 self.net.untyped.push(UntypedObject::from(obj));
1273 }
1274 return;
1275 }
1276
1277 self.kvar_shunt_with_props(obj, &props, REACTOR_KVAR_SHUNT);
1278 }
1279
1280 fn grounding_impedance_reactor(
1281 &mut self,
1282 obj: &RawObject,
1283 props: &Props<'_>,
1284 bus: &BusSpec,
1285 phases: usize,
1286 ) {
1287 let term = |v: Option<&Value>| v.map_or(Ok(0.0), |val| val.to_f64(Some(self.vars)));
1292 let (Ok(resistance), Ok(reactance)) = (term(props.get("r")), term(props.get("x"))) else {
1293 self.warn(format!(
1294 "reactor {}: `r`/`x` does not evaluate to a number; kept untyped",
1295 obj.name
1296 ));
1297 self.net.untyped.push(UntypedObject::from(obj));
1298 return;
1299 };
1300 let denom = resistance * resistance + reactance * reactance;
1301 if !denom.is_finite() || denom <= 0.0 {
1302 self.warn(format!(
1303 "reactor {}: zero impedance grounding reactor is not a typed shunt; kept untyped",
1304 obj.name
1305 ));
1306 self.net.untyped.push(UntypedObject::from(obj));
1307 return;
1308 }
1309 let map = self.terminals(bus, phases, phases + 1, phases);
1310 let dim = map.len();
1311 let mut conductance = vec![vec![0.0; dim]; dim];
1312 let mut susceptance = vec![vec![0.0; dim]; dim];
1313 let y_g = resistance / denom;
1314 let y_b = -reactance / denom;
1315 for idx in 0..dim {
1316 conductance[idx][idx] = y_g;
1317 susceptance[idx][idx] = y_b;
1318 }
1319 self.net.shunts.push(DistShunt {
1320 name: obj.name.clone(),
1321 bus: bus.name.clone(),
1322 terminal_map: map,
1323 g: conductance,
1324 b: susceptance,
1325 extras: extras_from_leftovers(props),
1326 });
1327 }
1328
1329 fn kvar_shunt(&mut self, obj: &RawObject, spec: KvarShuntSpec) {
1330 let props = Props::new(obj);
1331 self.kvar_shunt_with_props(obj, &props, spec);
1332 }
1333
1334 fn kvar_shunt_with_props(&mut self, obj: &RawObject, props: &Props<'_>, spec: KvarShuntSpec) {
1335 let phases = self.usize_or(props, "phases", spec.class, &obj.name, spec.default_phases);
1336 if phases == 0 {
1337 self.warn(format!(
1338 "{} {}: nonpositive `phases` value is not a typed shunt; kept untyped",
1339 spec.class, obj.name
1340 ));
1341 self.net.untyped.push(UntypedObject::from(obj));
1342 return;
1343 }
1344 let conn_delta = props.get("conn").is_some_and(|v| {
1348 v.text.to_ascii_lowercase().starts_with('d') || v.text.eq_ignore_ascii_case("ll")
1349 });
1350 let bus = bus_spec(props.get("bus1"), "");
1351 if let Some(return_bus) = props.get("bus2").map(super::lex::Value::to_bus_spec) {
1352 if !same_bus_ground_return(&bus, &return_bus, phases) {
1353 self.warn(format!(
1354 "{} {}: series {} (bus2) are not typed yet; kept untyped",
1355 spec.class, obj.name, spec.series_name
1356 ));
1357 self.net.untyped.push(UntypedObject::from(obj));
1358 return;
1359 }
1360 }
1361
1362 if conn_delta && phases == 1 && bus.nodes.len() < 2 {
1363 self.warn(format!(
1364 "{} {}: single phase delta shunt needs two bus nodes; kept untyped",
1365 spec.class, obj.name
1366 ));
1367 self.net.untyped.push(UntypedObject::from(obj));
1368 return;
1369 }
1370 let kvar = props
1373 .get("kvar")
1374 .and_then(|v| v.to_vector(Some(self.vars)).ok())
1375 .and_then(|v| v.first().copied())
1376 .unwrap_or_else(|| {
1377 self.defaulted(spec.class, &obj.name, "kvar");
1378 spec.default_kvar
1379 });
1380 let kv = self.f64_or(props, "kv", spec.class, &obj.name, spec.default_kv);
1381 let v_ref = if conn_delta {
1384 kv * 1e3
1385 } else if phases == 2 || phases == 3 {
1386 kv * 1e3 / 3f64.sqrt()
1387 } else {
1388 kv * 1e3
1389 };
1390 let v_sq = v_ref * v_ref;
1394 if !v_ref.is_finite() || v_ref <= 0.0 || !v_sq.is_finite() || v_sq == 0.0 {
1395 self.warn(format!(
1396 "{} {}: invalid `kv` value is not a typed shunt; kept untyped",
1397 spec.class, obj.name
1398 ));
1399 self.net.untyped.push(UntypedObject::from(obj));
1400 return;
1401 }
1402
1403 let (nconds, keep) = if conn_delta {
1404 let keep = match phases {
1405 1 => 2,
1406 2 => 3,
1407 _ => phases,
1408 };
1409 (keep, keep)
1410 } else {
1411 (phases + 1, phases)
1415 };
1416 let map = self.terminals(&bus, phases, nconds, keep);
1417 let Some(susceptance) =
1418 kvar_shunt_matrix(&map, phases, conn_delta, kvar, v_ref, spec.b_sign)
1419 else {
1420 self.warn(format!(
1421 "{} {}: delta shunt terminal map is not typed; kept untyped",
1422 spec.class, obj.name
1423 ));
1424 self.net.untyped.push(UntypedObject::from(obj));
1425 return;
1426 };
1427 let mut extras = extras_from_leftovers(props);
1428 self.stash_kv_and_phases(props, &mut extras, kv, phases);
1429 extras.insert("kvar".into(), kvar.into());
1430 if conn_delta {
1431 extras.insert("conn".into(), "delta".into());
1432 }
1433 self.net.shunts.push(DistShunt {
1434 name: obj.name.clone(),
1435 bus: bus.name,
1436 terminal_map: map,
1437 g: vec![vec![0.0; susceptance.len()]; susceptance.len()],
1438 b: susceptance,
1439 extras,
1440 });
1441 }
1442
1443 fn generator(&mut self, obj: &RawObject) -> DistGenerator {
1446 let props = Props::new(obj);
1447 let phases = self.usize_or(
1448 &props,
1449 "phases",
1450 "generator",
1451 &obj.name,
1452 dd::generator::PHASES,
1453 );
1454 let conn_delta = props.get("conn").is_some_and(|v| {
1456 v.text.to_ascii_lowercase().starts_with('d') || v.text.eq_ignore_ascii_case("ll")
1457 });
1458 let mut kw = dd::generator::KW;
1467 let mut kvar = dd::generator::KVAR;
1468 let mut pf = dd::generator::PF;
1469 let (mut kw_written, mut q_written) = (false, false);
1470 for p in &obj.props {
1471 let Some(key @ ("kw" | "kvar" | "pf")) = p.name.as_deref() else {
1472 continue;
1473 };
1474 let Some(v) = self.f64_prop(Some(&p.value)) else {
1475 continue;
1476 };
1477 match key {
1478 "kw" | "pf" => {
1479 if key == "kw" {
1480 kw = v;
1481 kw_written = true;
1482 } else {
1483 pf = v;
1484 q_written = true;
1485 }
1486 if pf != 0.0 {
1487 kvar = kw * (pf.acos().tan()).copysign(pf);
1488 }
1489 }
1490 _ => {
1491 kvar = v;
1492 q_written = true;
1493 let kva = kw.hypot(kvar);
1494 pf = if kva == 0.0 { 1.0 } else { kw / kva };
1495 if kw * kvar < 0.0 {
1496 pf = -pf;
1497 }
1498 }
1499 }
1500 }
1501 if !kw_written {
1502 self.defaulted("generator", &obj.name, "kw");
1503 }
1504 if !q_written {
1505 self.defaulted("generator", &obj.name, "kvar");
1506 }
1507 let _ = (props.get("kw"), props.get("kvar"), props.get("pf"));
1509 let kv = self.f64_or(&props, "kv", "generator", &obj.name, dd::generator::KV);
1510 let maxkvar = self.f64_prop(props.get("maxkvar"));
1511 let minkvar = self.f64_prop(props.get("minkvar"));
1512
1513 let spec = bus_spec(props.get("bus1"), "");
1514 let nconds = if conn_delta && phases == 3 {
1515 phases
1516 } else {
1517 phases + 1
1518 };
1519 let map = self.terminals(&spec, phases, nconds, nconds);
1520
1521 let per_phase = |total_kw: f64| vec![total_kw * 1e3 / phases as f64; phases];
1522 let mut extras = extras_from_leftovers(&props);
1523 self.stash_kv_and_phases(&props, &mut extras, kv, phases);
1524 DistGenerator {
1525 name: obj.name.clone(),
1526 bus: spec.name,
1527 terminal_map: map,
1528 configuration: if phases == 1 {
1529 Configuration::SinglePhase
1530 } else if conn_delta {
1531 Configuration::Delta
1532 } else {
1533 Configuration::Wye
1534 },
1535 p_nom: per_phase(kw),
1536 q_nom: per_phase(kvar),
1537 p_min: None,
1538 p_max: None,
1539 q_min: minkvar.map(per_phase),
1540 q_max: maxkvar.map(per_phase),
1541 cost: None,
1542 extras,
1543 }
1544 }
1545
1546 fn swtcontrol(&mut self, obj: &RawObject) {
1549 let props = Props::new(obj);
1550 let Some(target) = props.get("switchedobj").map(|v| v.text.clone()) else {
1551 self.warn(format!("swtcontrol {}: no SwitchedObj; ignored", obj.name));
1552 return;
1553 };
1554 let line_name = match target.split_once('.') {
1557 Some((class, rest)) if class.eq_ignore_ascii_case("line") => rest,
1558 _ => target.as_str(),
1559 };
1560 let mut open = None;
1563 for p in &obj.props {
1564 match p.name.as_deref() {
1565 Some("action" | "state") => {
1566 open = Some(p.value.text.to_ascii_lowercase().starts_with('o'));
1567 }
1568 Some("normal") if open.is_none() => {
1569 open = Some(p.value.text.to_ascii_lowercase().starts_with('o'));
1570 }
1571 _ => {}
1572 }
1573 }
1574 let open = open.unwrap_or(false);
1575 match self
1576 .net
1577 .switches
1578 .iter_mut()
1579 .find(|s| s.name.eq_ignore_ascii_case(line_name))
1580 {
1581 Some(sw) => sw.open = open,
1582 None => self.warn(format!(
1583 "swtcontrol {}: switched object `{target}` is not a switch line",
1584 obj.name
1585 )),
1586 }
1587 }
1588
1589 fn regcontrol(&mut self, obj: &RawObject) {
1590 let props = Props::new(obj);
1591 let target = props
1592 .get("transformer")
1593 .map_or_else(String::new, |v| v.text.clone());
1594 self.warn(format!(
1595 "regcontrol {}: voltage regulation is ignored; transformer `{target}` keeps its written taps",
1596 obj.name
1597 ));
1598 self.net.untyped.push(UntypedObject::from(obj));
1599 }
1600}
1601
1602fn scale_mat(m: &Mat, k: f64) -> Mat {
1604 m.iter()
1605 .map(|row| row.iter().map(|v| v * k).collect())
1606 .collect()
1607}
1608
1609fn filled_phase_nodes(spec: &BusSpec, phases: usize) -> Vec<i32> {
1610 let mut nodes: Vec<i32> = (1..=i32::try_from(phases).unwrap_or(i32::MAX)).collect();
1611 for (idx, &node) in spec.nodes.iter().enumerate().take(phases) {
1612 nodes[idx] = node.max(0);
1613 }
1614 nodes
1615}
1616
1617fn same_bus_ground_return(bus: &BusSpec, return_bus: &BusSpec, phases: usize) -> bool {
1618 bus.name.eq_ignore_ascii_case(&return_bus.name)
1619 && !return_bus.nodes.is_empty()
1620 && filled_phase_nodes(return_bus, phases)
1621 .iter()
1622 .all(|&n| n <= 0)
1623}
1624
1625pub(super) fn delta_edges(n: usize, phases: usize) -> Vec<(usize, usize)> {
1629 if n < 2 {
1630 Vec::new()
1631 } else if phases >= 3 && n >= 3 {
1632 (0..n).map(|i| (i, (i + 1) % n)).collect()
1633 } else {
1634 let branches = phases.max(1).min(n - 1);
1635 (0..branches).map(|i| (i, i + 1)).collect()
1636 }
1637}
1638
1639fn kvar_shunt_matrix(
1640 map: &[String],
1641 phases: usize,
1642 conn_delta: bool,
1643 kvar: f64,
1644 v_ref: f64,
1645 b_sign: f64,
1646) -> Option<Mat> {
1647 let dim = map.len();
1648 let mut susceptance = vec![vec![0.0; dim]; dim];
1649 if conn_delta {
1650 let edges = delta_edges(dim, phases);
1651 if edges.is_empty() || map.iter().any(|t| t == "0") {
1652 return None;
1653 }
1654 let b_branch = b_sign * kvar * 1e3 / edges.len() as f64 / (v_ref * v_ref);
1655 for (from, to) in edges {
1656 susceptance[from][from] += b_branch;
1657 susceptance[to][to] += b_branch;
1658 susceptance[from][to] -= b_branch;
1659 susceptance[to][from] -= b_branch;
1660 }
1661 } else {
1662 let b_phase = b_sign * kvar * 1e3 / phases as f64 / (v_ref * v_ref);
1663 for (idx, row) in susceptance.iter_mut().enumerate().take(phases) {
1664 row[idx] = b_phase;
1665 }
1666 }
1667 Some(susceptance)
1668}
1669
1670fn bus_spec(v: Option<&Value>, fallback: &str) -> BusSpec {
1671 v.map_or_else(
1672 || Value::new(fallback).to_bus_spec(),
1673 super::lex::Value::to_bus_spec,
1674 )
1675}
1676
1677fn extras_from_leftovers(props: &Props) -> Extras {
1678 let mut extras = Extras::new();
1679 for (k, v) in props.leftovers() {
1680 extras.insert(k.to_string(), v.text.clone().into());
1681 }
1682 extras
1683}
1684
1685fn apply_winding_strings(windings: &mut [WindingRaw], name: &str, items: &[String]) {
1687 let conn_is_delta =
1688 |t: &str| t.to_ascii_lowercase().starts_with('d') || t.eq_ignore_ascii_case("ll");
1689 for (i, item) in items.iter().enumerate() {
1690 let w = &mut windings[i];
1691 if name == "buses" {
1692 w.bus = Some(Value::new(item.clone()).to_bus_spec());
1693 } else {
1694 w.conn_delta = conn_is_delta(item);
1695 }
1696 }
1697}
1698
1699fn apply_winding_numbers(windings: &mut [WindingRaw], name: &str, items: &[f64]) {
1702 for (i, &item) in items.iter().enumerate() {
1703 let w = &mut windings[i];
1704 match name {
1705 "kvs" => {
1706 w.kv = item;
1707 w.kv_specified = true;
1708 }
1709 "kvas" => {
1710 w.kva = item;
1711 w.kva_specified = true;
1712 }
1713 "taps" => w.tap = item,
1714 _ => w.r_pct = item,
1715 }
1716 }
1717}
1718
1719fn x_pair_key(name: &str) -> Option<(usize, usize)> {
1720 let rest = name.strip_prefix('x')?;
1721 if rest.len() != 2 || !rest.chars().all(|c| c.is_ascii_digit()) {
1722 return None;
1723 }
1724 let mut chars = rest.chars();
1725 let i = chars.next()?.to_digit(10)? as usize;
1726 let j = chars.next()?.to_digit(10)? as usize;
1727 if i == 0 || j == 0 || i == j {
1728 return None;
1729 }
1730 Some((i.min(j) - 1, i.max(j) - 1))
1731}
1732
1733struct LoadPower {
1737 kw: f64,
1738 kvar: f64,
1739 pf: f64,
1740 spec_kvar: bool,
1742 kw_written: bool,
1743 pf_written: bool,
1744}
1745
1746struct SeriesImpedance {
1748 r: Mat,
1749 x: Mat,
1750 c_nf: Mat,
1751 all_default: bool,
1753 malformed: Vec<(&'static str, String)>,
1757}
1758
1759#[derive(Clone)]
1760struct WindingRaw {
1761 bus: Option<BusSpec>,
1762 conn_delta: bool,
1763 kv: f64,
1764 kva: f64,
1765 tap: f64,
1766 r_pct: f64,
1767 kv_specified: bool,
1768 kva_specified: bool,
1769}
1770
1771impl Default for WindingRaw {
1772 fn default() -> Self {
1773 WindingRaw {
1774 bus: None,
1775 conn_delta: false,
1776 kv: dd::transformer::KV,
1777 kva: dd::transformer::KVA,
1778 tap: dd::transformer::TAP,
1779 r_pct: dd::transformer::PCT_R,
1780 kv_specified: false,
1781 kva_specified: false,
1782 }
1783 }
1784}
1785
1786fn grow(windings: &mut Vec<WindingRaw>, n: usize, count: &mut usize) {
1788 if n > windings.len() {
1789 windings.resize(n, WindingRaw::default());
1790 *count = n;
1791 }
1792}
1793
1794#[cfg(test)]
1795mod tests {
1796 use super::*;
1797
1798 fn has_warning(net: &DistNetwork, needle: &str) -> bool {
1799 net.warnings.iter().any(|w| w.contains(needle))
1800 }
1801
1802 #[test]
1803 fn vsource_magnitude_is_the_polygon_chord() {
1804 let net = parse_dss_str(
1807 "New Circuit.c basekv=12.47 pu=1.05 phases=2 bus1=src.1.2\n\
1808 New Vsource.aux basekv=12.47 phases=4 bus1=b2\n\
1809 New Vsource.solo basekv=2.4 phases=1 bus1=b3.1",
1810 );
1811 let two = &net.sources[0];
1812 assert!((two.v_magnitude[0] - 12.47e3 * 1.05 / 2.0).abs() < 1e-9);
1813 assert!((two.v_angle[1] - std::f64::consts::PI).abs() < 1e-12);
1816 let four = &net.sources[1];
1817 let chord = 2.0 * (std::f64::consts::PI / 4.0).sin();
1818 assert!((four.v_magnitude[0] - 12.47e3 / chord).abs() < 1e-9);
1819 let solo = &net.sources[2];
1820 assert!((solo.v_magnitude[0] - 2.4e3).abs() < 1e-9);
1821 }
1822
1823 #[test]
1824 fn vsource_defaults_are_recorded() {
1825 let net = parse_dss_str("New Circuit.c1");
1826 let fields = net.defaulted.get("vsource.source").expect("entry");
1827 for key in ["phases", "pu", "angle", "basekv", "bus1"] {
1828 assert!(fields.contains(&key), "missing {key}");
1829 }
1830 }
1831
1832 fn r_and_length(lc_tail: &str, line_tail: &str) -> (f64, f64) {
1834 let net = parse_dss_str(&format!(
1835 "New Circuit.c\n\
1836 New Linecode.lc nphases=1 rmatrix=(0.5){lc_tail}\n\
1837 New Line.l1 bus1=a.1 bus2=b.1 phases=1 linecode=lc{line_tail}"
1838 ));
1839 let line = net.lines.iter().find(|l| l.name == "l1").unwrap();
1840 let code = net.linecode(&line.linecode).unwrap();
1841 (code.r_series[0][0], line.length)
1842 }
1843
1844 #[test]
1845 fn unitless_line_length_is_in_linecode_units() {
1846 let (r, len) = r_and_length(" units=km", " length=2");
1850 assert!((len - 2000.0).abs() < 1e-9);
1851 assert!((r * len - 1.0).abs() < 1e-12);
1852 }
1853
1854 #[test]
1855 fn unitless_linecode_is_per_line_unit() {
1856 let (r, len) = r_and_length("", " length=2 units=km");
1859 assert!((len - 2.0).abs() < 1e-12);
1860 assert!((r * len - 1.0).abs() < 1e-12);
1861 }
1862
1863 #[test]
1864 fn written_units_on_both_sides_convert() {
1865 let (r, len) = r_and_length(" units=km", " length=500 units=m");
1867 assert!((len - 500.0).abs() < 1e-9);
1868 assert!((r * len - 0.25).abs() < 1e-12);
1869 }
1870
1871 #[test]
1872 fn no_units_anywhere_takes_the_raw_product() {
1873 let (r, len) = r_and_length("", " length=2");
1874 assert!((len - 2.0).abs() < 1e-12);
1875 assert!((r * len - 1.0).abs() < 1e-12);
1876 }
1877
1878 #[test]
1879 fn two_phase_wye_capacitor_kv_is_line_to_line() {
1880 let net = parse_dss_str(
1883 "New Circuit.c\n\
1884 New Capacitor.c2 bus1=b.1.2 phases=2 kv=12.47 kvar=600\n\
1885 New Capacitor.c1 bus1=b.3 phases=1 kv=7.2 kvar=300",
1886 );
1887 let c2 = net.shunts.iter().find(|s| s.name == "c2").unwrap();
1888 let v2 = 12.47e3 / 3f64.sqrt();
1889 assert!((c2.b[0][0] * v2 * v2 / 300e3 - 1.0).abs() < 1e-12);
1890 let c1 = net.shunts.iter().find(|s| s.name == "c1").unwrap();
1891 let v1 = 7.2e3;
1892 assert!((c1.b[0][0] * v1 * v1 / 300e3 - 1.0).abs() < 1e-12);
1893 }
1894
1895 #[test]
1896 fn capacitor_and_reactor_kvar_shunts_share_magnitude_with_opposite_sign() {
1897 let net = parse_dss_str(
1898 "New Circuit.c\n\
1899 New Capacitor.cap bus1=b.1 phases=1 kv=7.2 kvar=300\n\
1900 New Reactor.rea bus1=b.2 phases=1 kv=7.2 kvar=300",
1901 );
1902 let cap = net.shunts.iter().find(|s| s.name == "cap").unwrap();
1903 let rea = net.shunts.iter().find(|s| s.name == "rea").unwrap();
1904 assert!(cap.b[0][0] > 0.0);
1905 assert!(rea.b[0][0] < 0.0);
1906 assert!((cap.b[0][0] + rea.b[0][0]).abs() < 1e-18);
1907 }
1908
1909 #[test]
1910 fn kvar_shunts_with_nonpositive_phases_stay_untyped() {
1911 let net = parse_dss_str(
1912 "New Circuit.c\n\
1913 New Capacitor.cap bus1=b.1 phases=0 kv=7.2 kvar=300\n\
1914 New Reactor.rea bus1=b.2 phases=0 kv=7.2 kvar=300",
1915 );
1916 assert!(net.shunts.is_empty());
1917 assert!(
1918 net.untyped
1919 .iter()
1920 .any(|u| u.class.eq_ignore_ascii_case("capacitor") && u.name == "cap")
1921 );
1922 assert!(
1923 net.untyped
1924 .iter()
1925 .any(|u| u.class.eq_ignore_ascii_case("reactor") && u.name == "rea")
1926 );
1927 assert!(
1928 net.warnings
1929 .iter()
1930 .any(|w| w.contains("capacitor cap: nonpositive `phases`"))
1931 );
1932 assert!(
1933 net.warnings
1934 .iter()
1935 .any(|w| w.contains("reactor rea: nonpositive `phases`"))
1936 );
1937 }
1938
1939 #[test]
1940 fn ll_connection_means_delta() {
1941 let net = parse_dss_str(
1943 "New Circuit.c\n\
1944 New Generator.g bus1=b.1.2.3 phases=3 conn=ll kw=90 kvar=30 kv=4.16\n\
1945 New Capacitor.cap bus1=b.1.2.3 phases=3 conn=ll kvar=600 kv=4.16",
1946 );
1947 assert_eq!(net.generators[0].configuration, Configuration::Delta);
1948 assert_eq!(net.shunts.len(), 1);
1950 let sh = &net.shunts[0];
1951 assert!(sh.b[0][1] < 0.0, "{:?}", sh.b);
1952 assert_eq!(sh.terminal_map, vec!["1", "2", "3"]);
1953 assert!(
1954 net.untyped
1955 .iter()
1956 .all(|u| !(u.class.eq_ignore_ascii_case("capacitor") && u.name == "cap"))
1957 );
1958 }
1959
1960 #[test]
1961 fn load_kw_after_kvar_reverts_to_pf() {
1962 let net =
1965 parse_dss_str("New Circuit.c\nNew Load.l bus1=b.1 phases=1 kv=2.4 kvar=20 kw=100");
1966 let l = &net.loads[0];
1967 let q: f64 = l.q_nom.iter().sum();
1968 assert!((q - 100e3 * 0.88f64.acos().tan()).abs() < 1e-6);
1969 assert_eq!(
1970 l.extras.get("pf").and_then(serde_json::Value::as_f64),
1971 Some(0.88)
1972 );
1973 assert!(
1974 net.defaulted
1975 .get("load.l")
1976 .is_some_and(|f| f.contains(&"pf"))
1977 );
1978 }
1979
1980 #[test]
1981 fn load_like_replays_the_sources_recalced_pf() {
1982 let net = parse_dss_str(
1989 "New Circuit.c\n\
1990 New Load.a bus1=b.1 phases=1 kv=2.4 kvar=20\n\
1991 New Load.b like=a kw=100",
1992 );
1993 let b = net.loads.iter().find(|l| l.name == "b").unwrap();
1994 let q: f64 = b.q_nom.iter().sum();
1995 assert!((q - 200e3).abs() < 1e-6);
1996 let pf = b.extras.get("pf").and_then(serde_json::Value::as_f64);
1998 assert!((pf.unwrap() - 0.447_213_595_499_957_9).abs() < 1e-12);
1999 let a = net.loads.iter().find(|l| l.name == "a").unwrap();
2001 let qa: f64 = a.q_nom.iter().sum();
2002 assert!((qa - 20e3).abs() < 1e-9);
2003 }
2004
2005 #[test]
2006 fn load_tilde_continuation_recalcs_at_each_edit() {
2007 let net = parse_dss_str(
2011 "New Circuit.c\n\
2012 New Load.l bus1=b.1 phases=1 kv=2.4 kvar=20\n\
2013 ~ kw=100",
2014 );
2015 let q: f64 = net.loads[0].q_nom.iter().sum();
2016 assert!((q - 200e3).abs() < 1e-6);
2017 }
2018
2019 #[test]
2020 fn load_pf_between_kvar_and_kw_applies() {
2021 let net = parse_dss_str(
2026 "New Circuit.c\nNew Load.l bus1=b.1 phases=1 kv=2.4 kvar=20 pf=0.95 kw=100",
2027 );
2028 let l = &net.loads[0];
2029 let q: f64 = l.q_nom.iter().sum();
2030 assert!((q - 100e3 * 0.95f64.acos().tan()).abs() < 1e-6);
2031 assert_eq!(
2032 l.extras.get("pf").and_then(serde_json::Value::as_f64),
2033 Some(0.95)
2034 );
2035 assert!(
2036 !net.defaulted
2037 .get("load.l")
2038 .is_some_and(|f| f.contains(&"pf"))
2039 );
2040 }
2041
2042 #[test]
2043 fn load_kvar_after_kw_stays() {
2044 let net =
2045 parse_dss_str("New Circuit.c\nNew Load.l bus1=b.1 phases=1 kv=2.4 kw=100 kvar=20");
2046 let l = &net.loads[0];
2047 let q: f64 = l.q_nom.iter().sum();
2048 assert!((q - 20e3).abs() < 1e-9);
2049 assert!(!l.extras.contains_key("pf"));
2051 }
2052
2053 #[test]
2054 fn generator_kw_after_kvar_resyncs_q() {
2055 let net =
2059 parse_dss_str("New Circuit.c\nNew Generator.g bus1=b.1 phases=1 kv=2.4 kvar=20 kw=100");
2060 let q: f64 = net.generators[0].q_nom.iter().sum();
2061 assert!((q - 2e3).abs() < 1e-6);
2062 }
2063
2064 #[test]
2065 fn generator_kvar_after_kw_stays() {
2066 let net =
2067 parse_dss_str("New Circuit.c\nNew Generator.g bus1=b.1 phases=1 kv=2.4 kw=100 kvar=20");
2068 let q: f64 = net.generators[0].q_nom.iter().sum();
2069 assert!((q - 20e3).abs() < 1e-9);
2070 }
2071
2072 #[test]
2073 fn generator_pf_after_kvar_wins() {
2074 let net = parse_dss_str(
2077 "New Circuit.c\nNew Generator.g bus1=b.1.2.3 phases=3 kv=4.16 kvar=20 pf=0.9",
2078 );
2079 let q: f64 = net.generators[0].q_nom.iter().sum();
2080 assert!((q - 1000e3 * 0.9f64.acos().tan()).abs() < 1e-3);
2081 }
2082
2083 #[test]
2084 fn malformed_matrix_warns_and_keeps_text() {
2085 let net = parse_dss_str(
2089 "New Circuit.c\n\
2090 New Linecode.bad nphases=2 rmatrix=(1 2 3) units=m\n\
2091 New Line.l2 bus1=a.1.2 bus2=b.1.2 phases=2 rmatrix=(bogus) length=10",
2092 );
2093 assert!(has_warning(&net, "linecode bad") && has_warning(&net, "rmatrix"));
2094 assert!(
2095 !net.defaulted
2096 .get("linecode.bad")
2097 .is_some_and(|f| f.contains(&"rmatrix"))
2098 );
2099 let code = net.linecode("bad").unwrap();
2100 assert!(
2101 code.extras
2102 .get("rmatrix")
2103 .and_then(serde_json::Value::as_str)
2104 .is_some_and(|s| s.contains("1 2 3"))
2105 );
2106 let diag = (2.0 * dd::line::R1 + dd::line::R0) / 3.0;
2108 assert!((code.r_series[0][0] - diag).abs() < 1e-12);
2109 assert!(has_warning(&net, "line l2"));
2111 let l2 = net.lines.iter().find(|l| l.name == "l2").unwrap();
2112 assert!(
2113 l2.extras
2114 .get("rmatrix")
2115 .and_then(serde_json::Value::as_str)
2116 .is_some_and(|s| s.contains("bogus"))
2117 );
2118 }
2119
2120 #[test]
2121 fn switchedobj_class_prefix_is_case_insensitive() {
2122 let net = parse_dss_str(
2123 "New Circuit.c\n\
2124 New Line.sw1 bus1=a.1 bus2=b.1 phases=1 switch=y\n\
2125 New SwtControl.s1 SwitchedObj=LINE.sw1 Action=open",
2126 );
2127 assert!(net.switches[0].open);
2128 }
2129
2130 #[test]
2131 fn phases_token_rides_in_extras() {
2132 let net = parse_dss_str(
2135 "New Circuit.c\n\
2136 New Load.l bus1=b.1.2 phases=2 conn=delta kw=50 kvar=10 kv=4.8\n\
2137 New Generator.g bus1=b.1.2.3 kw=10 kvar=2 kv=4.16\n\
2138 New Capacitor.cap bus1=b.1.2.3 phases=3 kvar=600 kv=4.16",
2139 );
2140 let l = &net.loads[0];
2141 assert_eq!(l.terminal_map.len(), 3);
2142 assert_eq!(
2143 l.extras.get("phases").and_then(serde_json::Value::as_str),
2144 Some("2")
2145 );
2146 assert_eq!(
2148 net.generators[0]
2149 .extras
2150 .get("phases")
2151 .and_then(serde_json::Value::as_u64),
2152 Some(3)
2153 );
2154 assert_eq!(
2155 net.shunts[0]
2156 .extras
2157 .get("phases")
2158 .and_then(serde_json::Value::as_str),
2159 Some("3")
2160 );
2161 }
2162
2163 #[test]
2164 fn rpn_kv_token_stashes_the_evaluated_value() {
2165 let net = parse_dss_str("New Circuit.c\nNew Load.l bus1=b.1 phases=1 kw=10 kv={4.8 2 /}");
2167 assert_eq!(
2168 net.loads[0]
2169 .extras
2170 .get("kv")
2171 .and_then(serde_json::Value::as_f64),
2172 Some(2.4)
2173 );
2174 }
2175}