1use std::collections::{BTreeMap, BTreeSet};
2
3use num_complex::Complex64;
4use powerio_core::{Diagnostic, Error, FormatId, PioModule, Source};
5
6use super::{
7 GlmObject, GlmProject, GlmProperty, GlmPropertyValue, GlmQuantityKind, ObjectIndex, ObjectKey,
8 PreprocessOptions, Statement, TokenKind, parse_project,
9};
10use crate::diagnostics::codes;
11use crate::{
12 Configuration, DistBus, DistLine, DistLineCode, DistLoad, DistLoadVoltageModel,
13 MulticonductorNetwork, VoltageSource,
14};
15
16const METERS_PER_MILE: f64 = 1609.344;
17
18mod batteries;
19mod cables;
20mod capacitors;
21mod delta_delta;
22mod delta_grounded;
23mod fbs_lines;
24mod generators;
25mod geometry;
26mod inverters;
27mod loads;
28mod reactors;
29mod regulators;
30mod reliability;
31mod solar;
32mod sources;
33mod split_phase;
34mod substations;
35mod switches;
36mod templates;
37mod transformers;
38mod triplex_lines;
39mod triplex_zip;
40
41pub fn parse(
70 source: Source,
71 options: PreprocessOptions,
72) -> Result<PioModule<MulticonductorNetwork>, Error> {
73 let preprocessing =
74 serde_json::json!({"defines": options.defines, "reinclude": options.reinclude});
75 let source_format = match options.dialect {
76 super::GlmDialect::Pnnl5_3 => crate::DistTargetFormat::Gridlabd,
77 super::GlmDialect::Arras4_3 => crate::DistTargetFormat::GridlabdArras,
78 };
79 let project = parse_project(source, options)?;
80 let index = project.index_objects()?;
81 let mut reader = Reader {
82 project: &project,
83 index,
84 net: MulticonductorNetwork::new(),
85 diagnostics: Vec::new(),
86 roots: BTreeMap::new(),
87 names: BTreeMap::new(),
88 identities: BTreeSet::new(),
89 quantities: BTreeMap::new(),
90 phases: BTreeMap::new(),
91 nominal: BTreeMap::new(),
92 triplex: BTreeMap::new(),
93 attachments: BTreeMap::new(),
94 line_capacitance: false,
95 newton_raphson: false,
96 switch_default_resistance: 1e-4,
97 };
98 reader.top_level()?;
102 reader.inventory()?;
103 reader.buses()?;
104 reader.loads_and_sources()?;
105 reader.lines()?;
106 reader.reactors()?;
107 reader.triplex_lines()?;
108 reader.switches()?;
109 reader.transformers()?;
110 reader.regulators()?;
111 reader.capacitors()?;
112 reader.generators()?;
113 reader.fixed_inverters()?;
114 reader.inactive_reliability()?;
115 reader.coordinates()?;
116 crate::require_electrical_readiness(&reader.net)
117 .map_err(|e| e.with_source(project.source().clone()))?;
118 let mut module = PioModule::parsed(
119 reader.net,
120 project
121 .source()
122 .clone()
123 .with_format(FormatId::new(source_format.name())?),
124 reader.diagnostics,
125 )?;
126 module.insert_extension("org.powerio.gridlabd", serde_json::json!({
127 "version": 1,
128 "dialect": source_format.name(),
129 "dependencies": project.dependencies().iter().map(powerio_core::SourceId::as_str).collect::<Vec<_>>(),
130 "absolute_include": project.has_absolute_include(),
131 "preprocessing": preprocessing,
132 }))?;
133 Ok(module)
134}
135
136struct Reader<'a> {
137 project: &'a GlmProject,
138 index: ObjectIndex<'a>,
139 net: MulticonductorNetwork,
140 diagnostics: Vec<Diagnostic>,
141 roots: BTreeMap<ObjectKey, ObjectKey>,
142 names: BTreeMap<ObjectKey, String>,
143 identities: BTreeSet<String>,
144 quantities: BTreeMap<(ObjectKey, String), Complex64>,
145 phases: BTreeMap<ObjectKey, Vec<usize>>,
146 nominal: BTreeMap<ObjectKey, f64>,
147 triplex: BTreeMap<ObjectKey, usize>,
148 attachments: BTreeMap<ObjectKey, Vec<(crate::DistLocationTarget, String)>>,
149 line_capacitance: bool,
150 newton_raphson: bool,
151 switch_default_resistance: f64,
152}
153
154impl Reader<'_> {
155 fn error(&self, key: ObjectKey, message: impl Into<String>) -> Error {
156 self.project
157 .electrical_error(self.index.objects()[key.ordinal()].span(), message)
158 }
159
160 fn quantity(&self, key: ObjectKey, name: &str) -> Option<Complex64> {
161 self.quantities.get(&(key, name.to_owned())).copied()
162 }
163
164 fn real(&self, key: ObjectKey, name: &str) -> Option<f64> {
165 self.quantity(key, name).map(|v| v.re)
166 }
167
168 fn text(&self, key: ObjectKey, name: &str) -> Result<Option<String>, Error> {
169 property(&self.index.objects()[key.ordinal()], name)
170 .map(|p| literal(p).map_err(|m| self.error(key, m)))
171 .transpose()
172 }
173
174 fn top_level(&mut self) -> Result<(), Error> {
175 let mut powerflow = false;
176 let mut generators = false;
177 let mut climate = false;
178 let mut reliability = false;
179 for statement in self.project.statements() {
180 match statement {
181 Statement::Block { header, body, span }
182 if header.first().is_some_and(|t| t.text() == "module") =>
183 {
184 if header.len() == 2 && header[1].text() == "reliability" {
185 self.reliability_module(body, span)?;
186 reliability = true;
187 continue;
188 }
189 if header.len() != 2 || header[1].text() != "powerflow" {
190 return Err(self
191 .project
192 .electrical_error(span, "unsupported module configuration"));
193 }
194 powerflow = true;
195 for entry in body {
196 let Statement::Simple { tokens, span } = entry else {
197 return Err(self
198 .project
199 .electrical_error(span, "unsupported powerflow module syntax"));
200 };
201 if tokens.len() != 2 {
202 return Err(self
203 .project
204 .electrical_error(span, "powerflow setting requires one value"));
205 }
206 self.powerflow_setting(tokens[0].text(), tokens[1].text(), span)?;
207 }
208 }
209 Statement::Simple { tokens, span }
210 if tokens.first().is_some_and(|t| t.text() == "module") =>
211 {
212 if tokens.len() != 2
213 || !matches!(
214 tokens[1].text(),
215 "powerflow"
216 | "tape"
217 | "assert"
218 | "generators"
219 | "climate"
220 | "reliability"
221 )
222 {
223 return Err(self.project.electrical_error(
224 span,
225 "module may introduce unmodeled electrical behavior",
226 ));
227 }
228 powerflow |= tokens[1].text() == "powerflow";
229 generators |= tokens[1].text() == "generators";
230 climate |= tokens[1].text() == "climate";
231 reliability |= tokens[1].text() == "reliability";
232 }
233 Statement::Block { header, .. }
234 if header
235 .first()
236 .is_some_and(|t| matches!(t.text(), "object" | "clock")) => {}
237 Statement::Directive(token) => self.global_directive(token)?,
238 Statement::Simple { span, .. } | Statement::Block { span, .. } => {
239 return Err(self
240 .project
241 .electrical_error(span, "unsupported top-level electrical declaration"));
242 }
243 }
244 }
245 if !generators
246 && let Some(object) = self
247 .index
248 .objects()
249 .iter()
250 .find(|o| matches!(class(o), "diesel_dg" | "solar" | "inverter" | "battery"))
251 {
252 return Err(self.error(
253 object.key(),
254 "generator objects require the generators module",
255 ));
256 }
257 if !climate
258 && let Some(object) = self.index.objects().iter().find(|o| class(o) == "climate")
259 {
260 return Err(self.error(object.key(), "climate object requires the climate module"));
261 }
262 if !reliability
263 && let Some(object) = self.index.objects().iter().find(|o| class(o) == "eventgen")
264 {
265 return Err(self.error(object.key(), "eventgen requires the reliability module"));
266 }
267 if !powerflow {
268 return Err(Error::new(
269 &codes::READ_GLM_ELECTRICAL_UNSUPPORTED,
270 "GLM static network requires the powerflow module",
271 )
272 .with_source(self.project.source().clone()));
273 }
274 Ok(())
275 }
276
277 fn global_directive(&mut self, token: &super::Token) -> Result<(), Error> {
278 let text = token.text().trim();
279 let setting = text.strip_prefix("#set ").and_then(|s| s.split_once('='));
280 if let Some(("iteration_limit", value)) =
281 setting.map(|(k, v)| (k.trim(), v.trim().trim_end_matches(';')))
282 {
283 if value.parse::<u32>().ok().is_none_or(|v| v == 0) {
284 return Err(self.project.electrical_error(
285 &token.span(),
286 "iteration_limit requires a positive integer",
287 ));
288 }
289 self.diagnostics.push(Diagnostic::of(&codes::READ_GLM_SOURCE_ONLY,
290 "iteration_limit is retained in source; canonical output uses its own solver settings"));
291 return Ok(());
292 }
293 if let Some((name @ ("profiler" | "relax_naming_rules"), value)) =
294 setting.map(|(k, v)| (k.trim(), v.trim().trim_end_matches(';')))
295 {
296 if !matches!(value, "0" | "1" | "TRUE" | "FALSE" | "true" | "false") {
297 return Err(self
298 .project
299 .electrical_error(&token.span(), format!("{name} requires a boolean")));
300 }
301 self.diagnostics.push(Diagnostic::of(
302 &codes::READ_GLM_SOURCE_ONLY,
303 format!("{name} is a non-electrical engine setting retained in source; object identities remain literal"),
304 ));
305 return Ok(());
306 }
307 if !setting.is_some_and(|(name, _)| {
308 matches!(
309 name.trim(),
310 "complex_format"
311 | "complex_output_format"
312 | "double_format"
313 | "savefile"
314 | "reinclude"
315 | "literal_if"
316 | "relax_undefined_if"
317 )
318 }) {
319 return Err(self.project.electrical_error(
320 &token.span(),
321 "unqualified global directive in static GLM input",
322 ));
323 }
324 Ok(())
325 }
326
327 fn powerflow_setting(
328 &mut self,
329 name: &str,
330 value: &str,
331 span: &powerio_core::SourceSpan,
332 ) -> Result<(), Error> {
333 match (name, value) {
334 ("solver_method", "NR") => self.newton_raphson = true,
335 ("solver_method", "FBS") => self.newton_raphson = false,
336 ("line_limits", value)
337 if matches!(
338 value.to_ascii_lowercase().as_str(),
339 "true" | "false" | "0" | "1"
340 ) =>
341 {
342 self.diagnostics.push(Diagnostic::of(
343 &codes::READ_GLM_SOURCE_ONLY,
344 "line_limits controls native limit reporting; the setting remains in source",
345 ));
346 }
347 ("default_maximum_voltage_error" | "maximum_voltage_error", value) => {
348 if !value.parse::<f64>().is_ok_and(|v| v.is_finite() && v > 0.0) {
349 return Err(self.project.electrical_error(
350 span,
351 "default voltage convergence tolerance requires a finite positive scalar",
352 ));
353 }
354 self.diagnostics.push(Diagnostic::of(&codes::READ_GLM_SOURCE_ONLY,
355 "native voltage convergence tolerance remains in source; canonical output uses its own solver settings"));
356 }
357 ("default_resistance", value) => {
358 self.switch_default_resistance = value
359 .parse::<f64>()
360 .ok()
361 .filter(|v| v.is_finite() && *v > 0.0)
362 .ok_or_else(|| {
363 self.project.electrical_error(
364 span,
365 "default_resistance requires a finite positive scalar in ohms",
366 )
367 })?;
368 }
369 ("nominal_frequency", value) => {
370 *self.net.base_frequency_mut() = value
371 .parse::<f64>()
372 .ok()
373 .filter(|v| [50.0, 60.0].contains(v))
374 .ok_or_else(|| {
375 self.project.electrical_error(
376 span,
377 "nominal_frequency must be 50 or 60 Hz in this profile",
378 )
379 })?;
380 }
381 ("line_capacitance", v) if v.eq_ignore_ascii_case("true") => {
382 self.line_capacitance = true;
383 }
384 ("line_capacitance", v) if v.eq_ignore_ascii_case("false") => {
385 self.line_capacitance = false;
386 }
387 _ => {
388 return Err(self
389 .project
390 .electrical_error(span, "unqualified powerflow global setting"));
391 }
392 }
393 Ok(())
394 }
395
396 fn reserve_explicit_names(&mut self) -> Result<(), Error> {
397 let mut components = BTreeSet::new();
398 for object in self.index.objects() {
399 if let Some(name) = object.name() {
400 let folded = name.to_ascii_lowercase();
401 self.identities.insert(folded.clone());
405 if !templates::is_template(class(object)) && !components.insert(folded) {
406 return Err(self.error(
407 object.key(),
408 "object identities collide under current canonical case-insensitive lookup",
409 ));
410 }
411 }
412 }
413 Ok(())
414 }
415
416 fn inventory(&mut self) -> Result<(), Error> {
417 let used_templates = self.used_templates()?;
418 self.reserve_explicit_names()?;
419 for object in self.index.objects() {
420 let name = object.name().map_or_else(
421 || {
422 allocate_name(
423 &mut self.identities,
424 &format!("__glm_object_{}", object.key().ordinal()),
425 )
426 },
427 str::to_owned,
428 );
429 self.names.insert(object.key(), name);
430 let class = class(object);
431 if templates::is_template(class) && !used_templates.contains(&object.key()) {
432 self.diagnostics.push(Diagnostic::of(&codes::READ_GLM_SOURCE_ONLY,
433 format!("unused {class} {} is retained in source; it contributes no network equipment", self.names[&object.key()])));
434 continue;
435 }
436 if matches!(
437 class,
438 "recorder"
439 | "multi_recorder"
440 | "impedance_dump"
441 | "voltdump"
442 | "currdump"
443 | "complex_assert"
444 | "assert"
445 ) {
446 self.diagnostics.push(Diagnostic::of(
447 &codes::READ_GLM_SOURCE_ONLY,
448 format!(
449 "{class} {} remains in source; no recording/assertion behavior is executed",
450 self.names[&object.key()]
451 ),
452 ));
453 continue;
454 }
455 if !supported_class(class) {
456 return Err(self.error(
457 object.key(),
458 format!("class {class} has no verified static mapping yet"),
459 ));
460 }
461 if !object.other_statements().is_empty() {
462 return Err(self.error(object.key(), "uninterpreted object-body syntax"));
463 }
464 for property in object.properties() {
465 let name = property.name;
466 if !allowed_property(class, name) {
467 return Err(self.project.electrical_error(
468 property.span,
469 format!("unmapped property {class}.{name}"),
470 ));
471 }
472 if let Some((kind, unit)) = quantity_spec(class, name, self.project.dialect()) {
473 let result = self.project.quantity(property, kind, unit)?;
476 self.diagnostics.extend(result.diagnostics);
477 Self::assign_quantity(
478 &mut self.quantities,
479 object.key(),
480 class,
481 name,
482 result.value,
483 );
484 }
485 }
486 if let Some(diagnostic) = self.rating_metadata(object.key())? {
487 self.diagnostics.push(diagnostic);
488 }
489 }
490 let keys: Vec<_> = self
491 .index
492 .objects()
493 .iter()
494 .filter(|o| bus_class(class(o)))
495 .map(GlmObject::key)
496 .collect();
497 for key in keys {
498 let mut root = key;
499 let mut path = Vec::new();
500 loop {
501 if let Some(&known) = self.roots.get(&root) {
502 root = known;
503 break;
504 }
505 path.push(root);
506 let Some(parent) = self.index.parent(root) else {
507 break;
508 };
509 if !bus_class(class(&self.index.objects()[parent.ordinal()])) {
510 return Err(self.error(key, "electrical parent must be a node, meter or load"));
511 }
512 root = parent;
513 }
514 for descendant in path {
515 self.roots.insert(descendant, root);
516 }
517 }
518 self.validate_parent_profile()
519 }
520
521 fn validate_parent_profile(&self) -> Result<(), Error> {
522 if self.newton_raphson && self.project.dialect() == super::GlmDialect::Arras4_3 {
523 for (&key, &root) in &self.roots {
524 if key != root && self.index.parent(key) != Some(root) {
525 return Err(self.error(key, format!(
526 "Arras 4.3 NR nested electrical parents are not qualified: {} has a parent below root {}; the pinned engine can omit descendant loads or fail; flattening would change source behavior",
527 self.names[&key], self.names[&root]
528 )));
529 }
530 }
531 }
532 Ok(())
533 }
534
535 fn buses(&mut self) -> Result<(), Error> {
536 let roots: BTreeSet<_> = self.roots.values().copied().collect();
537 for key in roots {
538 let flags = self
539 .text(key, "phases")?
540 .ok_or_else(|| self.error(key, "explicit phases are required"))?;
541 let phases = if split_phase::bus_class(class(&self.index.objects()[key.ordinal()])) {
542 let anchor = split_phase::anchor(&flags).map_err(|m| self.error(key, m))?;
543 self.triplex.insert(key, anchor);
544 vec![0, 1]
545 } else {
546 parse_bus_phases(&flags).map_err(|m| self.error(key, m))?
547 };
548 let nominal = self
549 .real(key, "nominal_voltage")
550 .ok_or_else(|| self.error(key, "nominal_voltage is required"))?;
551 if nominal <= 0.0 {
552 return Err(self.error(key, "nominal_voltage must be positive"));
553 }
554 let mut terminals: Vec<_> = phases.iter().map(|p| (p + 1).to_string()).collect();
555 terminals.push("n".into());
556 let mut bus = DistBus::new(&self.names[&key], terminals);
557 bus.grounded.push("n".into());
558 self.net.buses_mut().push(bus);
559 self.attachments
560 .entry(key)
561 .or_default()
562 .push((crate::DistLocationTarget::Bus, self.names[&key].clone()));
563 self.phases.insert(key, phases);
564 self.nominal.insert(key, nominal);
565 }
566 Ok(())
567 }
568
569 fn assign_quantity(
570 quantities: &mut BTreeMap<(ObjectKey, String), Complex64>,
571 key: ObjectKey,
572 class: &str,
573 name: &str,
574 value: Complex64,
575 ) {
576 if let Some((canonical, imaginary)) = reactors::alias(class, name) {
577 let previous = quantities.entry((key, canonical)).or_default();
578 if imaginary {
579 previous.im = value.re;
580 } else {
581 previous.re = value.re;
582 }
583 } else if let Some((canonical, imaginary, default)) = (class == "transformer_configuration")
584 .then(|| transformers::alias(name))
585 .flatten()
586 {
587 let previous = quantities.entry((key, canonical.into())).or_insert(default);
588 if imaginary {
589 previous.im = value.re;
590 } else {
591 previous.re = value.re;
592 }
593 } else if let Some((canonical, Some(imaginary), _, _)) = if class == "load" {
594 loads::property(name)
595 } else if split_phase::bus_class(class) {
596 split_phase::property(class, name)
597 } else {
598 None
599 } {
600 let previous = quantities.entry((key, canonical.to_owned())).or_default();
601 if imaginary {
602 previous.im = value.re;
603 } else {
604 previous.re = value.re;
605 }
606 } else {
607 quantities.insert((key, name.to_owned()), value);
608 }
609 }
610
611 fn loads_and_sources(&mut self) -> Result<(), Error> {
612 let implicit_source = self.implicit_fbs_source()?;
613 let entries: Vec<_> = self.roots.iter().map(|(&k, &r)| (k, r)).collect();
614 for (key, root) in entries {
615 if self.triplex.contains_key(&root) {
616 if implicit_source == Some(key) {
617 return Err(self.error(key, "implicit triplex voltage source is not qualified"));
618 }
619 self.triplex_loads(key, root)?;
620 continue;
621 }
622 if split_phase::bus_class(class(&self.index.objects()[key.ordinal()])) {
623 return Err(self.error(key, "triplex child requires a triplex parent"));
624 }
625 let phases = if key == root {
626 self.phases[&root].clone()
627 } else {
628 let phases = parse_bus_phases(
629 self.text(key, "phases")?
630 .as_deref()
631 .ok_or_else(|| self.error(key, "child requires explicit phases"))?,
632 )
633 .map_err(|m| self.error(key, m))?;
634 if phases.iter().any(|p| !self.phases[&root].contains(p)) {
635 return Err(self.error(key, "child phases are outside its parent bus"));
636 }
637 phases
638 };
639 let nominal = self.nominal[&root];
640 if key != root
643 && class(&self.index.objects()[key.ordinal()]) != "capacitor"
644 && self
645 .real(key, "nominal_voltage")
646 .is_some_and(|v| v.total_cmp(&nominal).is_ne())
647 {
648 return Err(self.error(key, "child nominal voltage conflicts with its parent"));
649 }
650 let sequence = self.substation_voltages(key)?;
651 let bustype = self.text(key, "bustype")?.unwrap_or_else(|| "PQ".into());
652 if bustype == "SWING" || (bustype == "PQ" && implicit_source == Some(key)) {
653 if key != root {
654 return Err(self.error(key, "parented SWING source is not qualified"));
655 }
656 let mut magnitudes = Vec::new();
657 let mut angles = Vec::new();
658 for &phase in &phases {
659 let value = sequence.as_ref().map_or_else(
660 || {
661 self.quantity(key, &format!("voltage_{}", phase_name(phase)))
662 .filter(|v| *v != Complex64::default())
663 .unwrap_or_else(|| nominal_phasor(nominal, phase))
664 },
665 |v| v[phase],
666 );
667 magnitudes.push(value.norm());
668 angles.push(value.arg());
669 }
670 self.net.sources_mut().push(VoltageSource::new(
671 &self.names[&key],
672 &self.names[&root],
673 phases.iter().map(|p| (p + 1).to_string()).collect(),
674 magnitudes,
675 angles,
676 ));
677 } else if bustype == "SWING_PQ" {
678 self.connected_swing_pq(key, root)?;
679 } else if bustype != "PQ" {
680 return Err(self.error(
681 key,
682 format!("bustype {bustype} needs an additional static model"),
683 ));
684 }
685 if class(&self.index.objects()[key.ordinal()]) != "load" {
686 continue;
687 }
688 let delta = self
689 .text(key, "phases")?
690 .is_some_and(|flags| flags.contains('D'));
691 if !delta {
692 self.load_terms(key, root, &phases, nominal)?;
693 }
694 self.connected_load_terms(key, root, &phases, nominal, delta)?;
695 }
696 Ok(())
697 }
698
699 fn load_terms(
700 &mut self,
701 key: ObjectKey,
702 root: ObjectKey,
703 phases: &[usize],
704 nominal: f64,
705 ) -> Result<(), Error> {
706 let laws = ["power", "current", "impedance"];
707 let law_count = laws
708 .iter()
709 .filter(|law| {
710 phases.iter().any(|&phase| {
711 self.quantity(key, &format!("constant_{law}_{}", phase_name(phase)))
712 .is_some()
713 })
714 })
715 .count()
716 + self.explicit_load_term_count(key);
717 for law in laws {
718 let mut values = Vec::new();
719 let mut stated = false;
720 for &phase in phases {
721 let value = self.quantity(key, &format!("constant_{law}_{}", phase_name(phase)));
722 stated |= value.is_some();
723 let value = value.unwrap_or_default();
724 let power = match law {
725 "power" => value,
726 "current" => nominal_phasor(nominal, phase) * value.conj(),
727 _ if value == Complex64::default() => Complex64::default(),
728 _ => nominal * nominal / value.conj(),
729 };
730 if !power.re.is_finite() || !power.im.is_finite() {
731 return Err(self.error(key, "load power normalization overflows"));
732 }
733 if value != Complex64::default() && power == Complex64::default() {
734 return Err(self.error(key, "load power normalization underflows to zero"));
735 }
736 values.push(power);
737 }
738 if !stated {
739 continue;
740 }
741 let mut terminals: Vec<_> = phases.iter().map(|p| (p + 1).to_string()).collect();
742 terminals.push("n".into());
743 let name = if key != root && law_count == 1 {
744 self.names[&key].clone()
745 } else {
746 allocate_name(
747 &mut self.identities,
748 &format!("{}__glm_{law}", self.names[&key]),
749 )
750 };
751 let mut load = DistLoad::new(
752 name,
753 &self.names[&root],
754 terminals,
755 Configuration::Wye,
756 values.iter().map(|v| v.re).collect(),
757 values.iter().map(|v| v.im).collect(),
758 );
759 let v_nom = vec![nominal; phases.len()];
760 load.voltage_model = match law {
761 "power" => DistLoadVoltageModel::ConstantPower { v_nom },
762 "current" => DistLoadVoltageModel::ConstantCurrent { v_nom },
763 _ => DistLoadVoltageModel::ConstantImpedance { v_nom },
764 };
765 if key != root {
766 self.attachments
767 .entry(key)
768 .or_default()
769 .push((crate::DistLocationTarget::Load, load.name.clone()));
770 }
771 self.net.loads_mut().push(load);
772 }
773 Ok(())
774 }
775
776 fn has_explicit_matrix(&self, config: ObjectKey) -> bool {
777 (1..=3).any(|i| {
778 self.quantity(config, &format!("z{i}{i}"))
779 .is_some_and(|v| v != Complex64::default())
780 })
781 }
782
783 fn lines(&mut self) -> Result<(), Error> {
784 let keys: Vec<_> = self
785 .index
786 .objects()
787 .iter()
788 .filter(|o| matches!(class(o), "overhead_line" | "underground_line"))
789 .map(GlmObject::key)
790 .collect();
791 for key in keys {
792 let phases = parse_phases(
793 self.text(key, "phases")?
794 .as_deref()
795 .ok_or_else(|| self.error(key, "line requires explicit phases"))?,
796 )
797 .map_err(|m| self.error(key, m))?;
798 let reference = |name| {
799 self.index
800 .resolve_property(key, name)?
801 .ok_or_else(|| self.error(key, format!("missing {name} reference")))
802 };
803 let from = reference("from")?;
804 let to = reference("to")?;
805 let config = reference("configuration")?;
806 let from = *self
807 .roots
808 .get(&from)
809 .ok_or_else(|| self.error(key, "line from is not an electrical bus"))?;
810 let to = *self
811 .roots
812 .get(&to)
813 .ok_or_else(|| self.error(key, "line to is not an electrical bus"))?;
814 if self.triplex.contains_key(&from) || self.triplex.contains_key(&to) {
815 return Err(self.error(key, "ordinary line cannot connect triplex buses"));
816 }
817 if phases
818 .iter()
819 .any(|p| !self.phases[&from].contains(p) || !self.phases[&to].contains(p))
820 {
821 return Err(self.error(key, "line phases are absent at an endpoint"));
822 }
823 if class(&self.index.objects()[config.ordinal()]) != "line_configuration" {
824 return Err(self.error(key, "line configuration has the wrong class"));
825 }
826 let length = self
827 .real(key, "length")
828 .ok_or_else(|| self.error(key, "line length is required"))?
829 * 0.3048;
830 if length <= 0.0 || !length.is_finite() {
831 return Err(self.error(key, "line length must be finite and positive"));
832 }
833 let code_name = allocate_name(
834 &mut self.identities,
835 &format!("{}__glm_linecode", self.names[&key]),
836 );
837 let code = if self.has_explicit_matrix(config) {
838 self.line_code(config, &phases, &code_name)?
839 } else {
840 let neutral = self
841 .text(key, "phases")?
842 .is_some_and(|flags| flags.contains('N'));
843 if class(&self.index.objects()[key.ordinal()]) == "underground_line" {
844 self.cable_line_code(config, &phases, neutral, &code_name)?
845 } else {
846 self.geometry_line_code(config, &phases, neutral, &code_name)?
847 }
848 };
849 if !super::line_physics::line_shunts_commute(&code) {
850 if !self.newton_raphson {
851 self.fbs_line(key, &phases, &code, length)?;
852 self.net.line_codes_mut().push(code);
853 self.attach(config, crate::DistLocationTarget::LineCode, code_name);
854 continue;
855 }
856 self.warn_native_line_reporting(key);
857 }
858 self.net.line_codes_mut().push(code);
859 self.attach(
860 config,
861 crate::DistLocationTarget::LineCode,
862 code_name.clone(),
863 );
864 self.attach(
865 key,
866 crate::DistLocationTarget::Line,
867 self.names[&key].clone(),
868 );
869 let terminals = phases
870 .iter()
871 .map(|p| (p + 1).to_string())
872 .collect::<Vec<_>>();
873 self.net.lines_mut().push(DistLine::new(
874 &self.names[&key],
875 &self.names[&from],
876 &self.names[&to],
877 terminals.clone(),
878 terminals,
879 code_name,
880 length,
881 ));
882 }
883 Ok(())
884 }
885
886 fn line_code(
887 &self,
888 config: ObjectKey,
889 phases: &[usize],
890 name: &str,
891 ) -> Result<DistLineCode, Error> {
892 let n = phases.len();
893 let mut resistance = vec![vec![0.0; n]; n];
894 let mut reactance = resistance.clone();
895 let mut susceptance = resistance.clone();
896 for (row, &a) in phases.iter().enumerate() {
897 for (col, &c) in phases.iter().enumerate() {
898 let z = self
899 .quantity(config, &format!("z{}{}", a + 1, c + 1))
900 .unwrap_or_default()
901 / METERS_PER_MILE;
902 let capacitance = self
903 .real(config, &format!("c{}{}", a + 1, c + 1))
904 .unwrap_or_default();
905 if capacitance != 0.0 && !self.line_capacitance {
906 return Err(self.error(config, "stated line capacitance with the native switch disabled needs explicit ignored-capacitance handling"));
907 }
908 resistance[row][col] = z.re;
909 reactance[row][col] = z.im;
910 susceptance[row][col] =
911 std::f64::consts::PI * self.net.base_frequency() * capacitance * 1e-9
912 / METERS_PER_MILE;
913 }
914 }
915 let mut code = DistLineCode::new(name, resistance, reactance);
916 code.b_from.clone_from(&susceptance);
917 code.b_to = susceptance;
918 Ok(code)
919 }
920
921 fn coordinates(&mut self) -> Result<(), Error> {
922 if !self.index.objects().iter().any(|o| {
923 o.properties()
924 .iter()
925 .any(|p| matches!(p.name, "latitude" | "longitude"))
926 }) {
927 return Ok(());
928 }
929 let points = self.index.declared_coordinates()?;
930 for object in self.index.objects() {
931 let point = points[object.key().ordinal()];
932 if point.latitude.is_none() && point.longitude.is_none() {
933 continue;
934 }
935 if class(object) == "climate" {
936 self.diagnostics.push(Diagnostic::of(&codes::READ_GLM_SOURCE_ONLY, format!("climate {}: weather-station coordinates remain in source; no electrical component is created", self.names[&object.key()])));
937 continue;
938 }
939 let Some(targets) = self.attachments.get(&object.key()) else {
940 return Err(self.error(
941 object.key(),
942 "coordinate-bearing object has no typed component attachment yet",
943 ));
944 };
945 for (target, name) in targets {
946 let mut location = crate::DistComponentLocation::new(
947 *target,
948 name,
949 crate::DistGeographicPoint::new(point.latitude, point.longitude),
950 );
951 location.kind = Some(crate::DistCoordsKind::Source);
952 self.net.component_locations_mut().push(location);
953 }
954 }
955 self.net.validate_component_locations()
956 }
957
958 fn attach(&mut self, key: ObjectKey, target: crate::DistLocationTarget, name: String) {
959 self.attachments
960 .entry(key)
961 .or_default()
962 .push((target, name));
963 }
964}
965
966fn allocate_name(identities: &mut BTreeSet<String>, base: &str) -> String {
969 if identities.insert(base.to_ascii_lowercase()) {
970 return base.to_owned();
971 }
972 for suffix in 1usize.. {
973 let name = format!("{base}_{suffix}");
974 if identities.insert(name.to_ascii_lowercase()) {
975 return name;
976 }
977 }
978 unreachable!("bounded object inventory cannot exhaust usize names")
979}
980
981fn property<'a>(object: &'a GlmObject<'_>, name: &str) -> Option<&'a GlmProperty<'a>> {
982 object.properties().iter().rev().find(|p| p.name == name)
983}
984
985fn literal(property: &GlmProperty<'_>) -> Result<String, &'static str> {
986 let GlmPropertyValue::Tokens(tokens) = &property.value else {
987 return Err("expected a literal property value");
988 };
989 let [token] = *tokens else {
990 return Err("expected one literal token");
991 };
992 if token.kind() == TokenKind::Quoted {
993 token
994 .text()
995 .strip_prefix('"')
996 .and_then(|s| s.strip_suffix('"'))
997 .map(str::to_owned)
998 .ok_or("expected a double-quoted literal")
999 } else {
1000 Ok(token.text().to_owned())
1001 }
1002}
1003
1004fn class<'a>(object: &GlmObject<'a>) -> &'a str {
1005 object
1006 .class_name()
1007 .strip_prefix("powerflow.")
1008 .unwrap_or(object.class_name())
1009}
1010fn supported_class(name: &str) -> bool {
1011 bus_class(name)
1012 || matches!(
1013 name,
1014 "diesel_dg"
1015 | "inverter"
1016 | "solar"
1017 | "battery"
1018 | "climate"
1019 | "series_reactor"
1020 | "fault_check"
1021 | "eventgen"
1022 | "overhead_line"
1023 | "underground_line"
1024 | "underground_line_conductor"
1025 | "triplex_line"
1026 | "triplex_line_configuration"
1027 | "triplex_line_conductor"
1028 | "line_configuration"
1029 | "switch"
1030 | "overhead_line_conductor"
1031 | "line_spacing"
1032 | "transformer"
1033 | "transformer_configuration"
1034 | "regulator"
1035 | "regulator_configuration"
1036 )
1037}
1038fn bus_class(name: &str) -> bool {
1039 matches!(name, "node" | "load" | "meter" | "capacitor" | "substation")
1040 || split_phase::bus_class(name)
1041}
1042fn matrix_property(name: &str) -> bool {
1043 let bytes = name.as_bytes();
1044 bytes.len() == 3
1045 && matches!(bytes[0], b'z' | b'c')
1046 && (b'1'..=b'3').contains(&bytes[1])
1047 && (b'1'..=b'3').contains(&bytes[2])
1048}
1049fn allowed_property(class: &str, name: &str) -> bool {
1050 matches!(
1051 name,
1052 "name" | "parent" | "groupid" | "latitude" | "longitude"
1053 ) || (bus_class(class)
1054 && matches!(
1055 name,
1056 "phases"
1057 | "bustype"
1058 | "nominal_voltage"
1059 | "maximum_voltage_error"
1060 | "voltage_A"
1061 | "voltage_B"
1062 | "voltage_C"
1063 ))
1064 || (class == "load" && loads::property(name).is_some())
1065 || (matches!(class, "overhead_line" | "underground_line")
1066 && matches!(name, "phases" | "from" | "to" | "length" | "configuration"))
1067 || (class == "line_configuration" && matrix_property(name))
1068 || triplex_zip::quantity_spec(class, name).is_some()
1069 || split_phase::allowed(class, name)
1070 || triplex_lines::allowed(class, name)
1071 || geometry::rating_property(class, name)
1072 || reliability::allowed(class, name)
1073 || batteries::allowed(class, name)
1074 || solar::allowed(class, name)
1075 || inverters::allowed(class, name)
1076 || reactors::allowed(class, name)
1077 || substations::allowed(class, name)
1078 || generators::allowed(class, name)
1079 || capacitors::allowed(class, name)
1080 || regulators::allowed(class, name)
1081 || transformers::allowed(class, name)
1082 || geometry::allowed(class, name)
1083 || cables::unit(class, name).is_some()
1084 || (class == "switch"
1085 && matches!(
1086 name,
1087 "phases"
1088 | "from"
1089 | "to"
1090 | "status"
1091 | "operating_mode"
1092 | "phase_A_state"
1093 | "phase_B_state"
1094 | "phase_C_state"
1095 | "switch_impedance"
1096 | "switch_resistance"
1097 | "switch_reactance"
1098 ))
1099}
1100fn quantity_spec(
1101 class: &str,
1102 name: &str,
1103 dialect: super::GlmDialect,
1104) -> Option<(GlmQuantityKind, Option<&'static str>)> {
1105 use GlmQuantityKind::{Complex, Real};
1106 if let Some(spec) = solar::quantity_spec(class, name)
1107 .or_else(|| batteries::quantity_spec(class, name))
1108 .or_else(|| inverters::quantity_spec(class, name))
1109 {
1110 return Some(spec);
1111 }
1112 if let Some(spec) = triplex_zip::quantity_spec(class, name)
1113 .or_else(|| split_phase::quantity_spec(class, name))
1114 .or_else(|| triplex_lines::quantity_spec(class, name))
1115 .or_else(|| generators::quantity_spec(class, name))
1116 .or_else(|| reactors::quantity_spec(class, name))
1117 .or_else(|| substations::quantity_spec(class, name))
1118 {
1119 return Some((spec.0, Some(spec.1)));
1120 }
1121 if let Some(spec) = transformers::quantity_spec(class, name)
1122 .map(|(kind, unit)| (kind, Some(unit)))
1123 .or_else(|| regulators::quantity_spec(class, name))
1124 {
1125 return Some(
1127 if class == "regulator_configuration"
1128 && name == "regulation"
1129 && dialect == super::GlmDialect::Arras4_3
1130 {
1131 (spec.0, Some("%"))
1132 } else {
1133 spec
1134 },
1135 );
1136 }
1137 if let Some(unit) = capacitors::unit(class, name)
1138 .or_else(|| geometry::unit(class, name))
1139 .or_else(|| cables::unit(class, name))
1140 {
1141 return Some((Real, Some(unit)));
1142 }
1143 if class == "switch" {
1144 match name {
1145 "switch_impedance" => return Some((Complex, Some("Ohm"))),
1146 "switch_resistance" | "switch_reactance" => return Some((Real, Some("Ohm"))),
1147 _ => {}
1148 }
1149 }
1150 if bus_class(class) {
1151 match name {
1152 "nominal_voltage" | "maximum_voltage_error" => return Some((Real, Some("V"))),
1153 "voltage_A" | "voltage_B" | "voltage_C" => return Some((Complex, Some("V"))),
1154 _ => {}
1155 }
1156 }
1157 if class == "load" {
1158 return loads::property(name).map(|(_, _, kind, unit)| (kind, Some(unit)));
1159 }
1160 if matches!(class, "overhead_line" | "underground_line") && name == "length" {
1161 return Some((Real, Some("ft")));
1162 }
1163 if class == "line_configuration" && matrix_property(name) {
1164 return Some(if name.starts_with('z') {
1165 (Complex, Some("Ohm/mile"))
1166 } else {
1167 (Real, Some("nF/mile"))
1168 });
1169 }
1170 None
1171}
1172pub(super) fn phase_name(phase: usize) -> char {
1173 ['A', 'B', 'C'][phase]
1174}
1175pub(super) fn nominal_phasor(magnitude: f64, phase: usize) -> Complex64 {
1176 Complex64::from_polar(
1177 magnitude,
1178 [
1179 0.0,
1180 -2.0 * std::f64::consts::PI / 3.0,
1181 2.0 * std::f64::consts::PI / 3.0,
1182 ][phase],
1183 )
1184}
1185fn parse_bus_phases(text: &str) -> Result<Vec<usize>, &'static str> {
1186 if text.bytes().filter(|&c| c == b'D').count() > 1 {
1187 return Err("duplicate delta phase flag");
1188 }
1189 parse_phases(&text.replace('D', ""))
1190}
1191
1192fn parse_phases(text: &str) -> Result<Vec<usize>, &'static str> {
1193 let mut phases = BTreeSet::new();
1194 let mut neutral = false;
1195 for phase in text.bytes() {
1196 match phase {
1197 b'A'..=b'C' => {
1198 if !phases.insert(usize::from(phase - b'A')) {
1199 return Err("duplicate phase flag");
1200 }
1201 }
1202 b'N' if !neutral => neutral = true,
1203 _ => {
1204 return Err(
1205 "phase flags require additional delta, split-phase or conductor interpretation",
1206 );
1207 }
1208 }
1209 }
1210 if phases.is_empty() {
1211 return Err("no active phase declared");
1212 }
1213 Ok(phases.into_iter().collect())
1214}