Skip to main content

powerio_dist/glm/
read.rs

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
41/// Read the initial static GLM electrical profile. Unknown physical
42/// classes, properties and globals fail explicitly; they are not silently
43/// omitted from an apparently usable network. The source project is retained.
44///
45/// This initial profile covers ordinary phase-to-earth node/load/meter objects,
46/// fixed source phasors, P/I/Z loads, overhead and underground line matrices
47/// or conductor geometry, and fixed ordinary-phase switch contacts. Finite
48/// contacts use exact line equivalents; ideal and open contacts remain typed switches.
49/// Ordinary single-phase and three-phase grounded-wye transformers use winding
50/// records; FBS additionally preserves its finite default winding-2 core shunt.
51/// A connected directed FBS tree may use its implicit upstream voltage source.
52/// Wye regulators preserve fixed phase taps, NR resistance and FBS ideal
53/// ratios in winding equivalents.
54/// Wye capacitors use fixed per-phase shunt admittances. Active controls require
55/// explicit individual switch states and produce a frozen-control diagnostic.
56/// Delta–grounded-wye transformers use phase-pair primary coils with grounded
57/// secondaries, preserving native step-up/down polarity in both solvers.
58/// Center-tapped services with grounded neutrals use typed windings and an
59/// internal core impedance load; triplex P/I/Z loads retain leg polarity.
60/// Triplex geometry uses full conductor matrices with both endpoint neutrals
61/// grounded, preserving native hot-conductor and neutral-current behavior.
62/// Direct triplex matrices preserve their signed hot-port equations. Canonical
63/// grounded service output derives native coefficients from current typed values;
64/// floating neutrals and additional service profiles remain unfinished.
65/// Standalone substations preserve sequence-derived source phasors. Static
66/// reactors use uncoupled lines or ideal contacts, including native NR zero
67/// impedance substitution. PNNL constant-PQ generators preserve initialized
68/// per-phase injections and ratings. Other device profiles remain unfinished.
69pub 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    // Profile identity belongs to the module's source descriptors and context.
99    // Extending the legacy network source-format enum would change a frozen
100    // IR 2 record solely to carry non-electrical provenance.
101    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                // Passive templates resolve by native case-sensitive identity.
402                // Their names never become typed component/linecode identities.
403                // Still reserve them against generated names for canonical output.
404                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                    // Native setters run in source order, even if a later
474                    // assignment overwrites the value or its phase is absent.
475                    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            // Native capacitor initialization copies the parent's node
641            // voltage; an explicit cap_nominal_voltage sets its device rating.
642            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
966// Reserve explicit source identities first so generated components can never
967// shadow a source name (including an object declared later in the document).
968fn 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        // Arras publishes regulation in percent; PNNL has no unit.
1126        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}