1use std::collections::{BTreeMap, BTreeSet, HashMap};
4
5use serde::{Deserialize, Serialize};
6
7use powerio::{
8 BalancedNetwork, BusId, NORMALIZED_SOLVER_TABLES_PASS, NormalizedSolverTables,
9 SolverTableUnits, SourceFormat,
10};
11use powerio_dist::{DistSourceFormat, MulticonductorNetwork};
12
13use crate::diagnostics::{DiagnosticSeverity, DiagnosticStage, StructuredDiagnostic};
14use crate::lowering::{
15 LoweringRecord, MulticonductorToBalancedError, MulticonductorToBalancedOptions,
16 MulticonductorToBalancedReadiness, check_multiconductor_to_balanced_lowering,
17 lower_multiconductor_to_balanced,
18};
19use crate::model::{ModelKind, ModelPayload};
20use crate::provenance::{
21 Confidence, MappingKind, Origin, Producer, SourceDescriptor, SourceMapEntry, SourceRef,
22};
23use crate::summary::{ObjectSummary, ObjectTopology, ObjectUnits};
24use crate::validation::{ValidationPass, ValidationStatus, ValidationSummary};
25
26pub const PIO_PACKAGE_SCHEMA_URL: &str = "https://powerio.dev/schema/pio-package/0.1";
28
29pub const PIO_PACKAGE_SCHEMA_VERSION: &str = "0.1.0";
32
33fn default_schema_url() -> String {
34 PIO_PACKAGE_SCHEMA_URL.to_owned()
35}
36
37fn default_schema_version() -> String {
38 PIO_PACKAGE_SCHEMA_VERSION.to_owned()
39}
40
41#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
45pub struct DerivedMetadata {
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub matrix_stats: Option<serde_json::Value>,
48 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub normalized_solver_tables: Option<NormalizedSolverTableMetadata>,
50 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
51 pub cache_keys: BTreeMap<String, String>,
52}
53
54impl DerivedMetadata {
55 fn is_empty(&self) -> bool {
56 self.matrix_stats.is_none()
57 && self.normalized_solver_tables.is_none()
58 && self.cache_keys.is_empty()
59 }
60}
61
62#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
64#[non_exhaustive]
65pub struct NormalizedSolverTableMetadata {
66 pub pass: String,
67 pub units: SolverTableUnits,
68 pub row_counts: NormalizedSolverTableRowCounts,
69 pub bus_ids: Vec<BusId>,
70 pub reference_bus_indices: Vec<usize>,
71 pub component_labels: Vec<usize>,
72 pub branch_from_arc_indices: Vec<usize>,
73 pub branch_to_arc_indices: Vec<usize>,
74 pub source_rows: NormalizedSolverTableSourceRows,
75}
76
77#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
79#[non_exhaustive]
80pub struct NormalizedSolverTableRowCounts {
81 pub buses: usize,
82 pub loads: usize,
83 pub shunts: usize,
84 pub branches: usize,
85 pub switches: usize,
86 pub arcs: usize,
87 pub generators: usize,
88 pub storage: usize,
89 pub hvdc: usize,
90}
91
92#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
94#[non_exhaustive]
95pub struct NormalizedSolverTableSourceRows {
96 pub buses: Vec<Option<usize>>,
97 pub loads: Vec<Option<usize>>,
98 pub shunts: Vec<Option<usize>>,
99 pub branches: Vec<Option<usize>>,
100 pub switches: Vec<Option<usize>>,
101 pub generators: Vec<Option<usize>>,
102 pub storage: Vec<Option<usize>>,
103 pub hvdc: Vec<Option<usize>>,
104}
105
106impl From<&NormalizedSolverTables> for NormalizedSolverTableMetadata {
107 fn from(tables: &NormalizedSolverTables) -> Self {
108 Self {
109 pass: NORMALIZED_SOLVER_TABLES_PASS.to_owned(),
110 units: tables.units.clone(),
111 row_counts: NormalizedSolverTableRowCounts {
112 buses: tables.buses.len(),
113 loads: tables.loads.len(),
114 shunts: tables.shunts.len(),
115 branches: tables.branches.len(),
116 switches: tables.switches.len(),
117 arcs: tables.arcs.len(),
118 generators: tables.generators.len(),
119 storage: tables.storage.len(),
120 hvdc: tables.hvdc.len(),
121 },
122 bus_ids: tables.index.bus_ids.clone(),
123 reference_bus_indices: tables.index.reference_bus_indices.clone(),
124 component_labels: tables.index.component_labels.clone(),
125 branch_from_arc_indices: tables.index.branch_from_arc_indices.clone(),
126 branch_to_arc_indices: tables.index.branch_to_arc_indices.clone(),
127 source_rows: NormalizedSolverTableSourceRows {
128 buses: tables.index.bus_source_rows.clone(),
129 loads: tables.index.load_source_rows.clone(),
130 shunts: tables.index.shunt_source_rows.clone(),
131 branches: tables.index.branch_source_rows.clone(),
132 switches: tables.index.switch_source_rows.clone(),
133 generators: tables.index.generator_source_rows.clone(),
134 storage: tables.index.storage_source_rows.clone(),
135 hvdc: tables.index.hvdc_source_rows.clone(),
136 },
137 }
138 }
139}
140
141#[derive(Clone, Debug, Serialize, Deserialize)]
150pub struct CompilerPackage {
151 #[serde(default = "default_schema_url")]
153 pub schema: String,
154 #[serde(default = "default_schema_version")]
156 pub schema_version: String,
157 pub producer: Producer,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub package_id: Option<String>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub created_at: Option<String>,
165 pub model_kind: ModelKind,
167 pub model: ModelPayload,
168 pub origin: Origin,
169 #[serde(default, skip_serializing_if = "Vec::is_empty")]
170 pub sources: Vec<SourceDescriptor>,
171 #[serde(default, skip_serializing_if = "Vec::is_empty")]
172 pub source_maps: Vec<SourceMapEntry>,
173 #[serde(default, skip_serializing_if = "Vec::is_empty")]
174 pub diagnostics: Vec<StructuredDiagnostic>,
175 pub validation: ValidationSummary,
176 #[serde(default)]
177 pub summary: ObjectSummary,
178 #[serde(default, skip_serializing_if = "Vec::is_empty")]
179 pub lowering_history: Vec<LoweringRecord>,
180 #[serde(default, skip_serializing_if = "DerivedMetadata::is_empty")]
181 pub derived: DerivedMetadata,
182}
183
184impl CompilerPackage {
185 pub fn from_balanced(net: BalancedNetwork) -> Self {
189 let origin = balanced_origin(&net);
190 let summary = balanced_summary(&net);
191 let sources = balanced_sources(&net);
192 let source_id = sources.first().map(|s| s.id.clone());
193 let source_maps = balanced_source_maps(&net, source_id.as_deref());
194 Self {
195 schema: default_schema_url(),
196 schema_version: default_schema_version(),
197 producer: Producer::powerio(),
198 package_id: None,
199 created_at: None,
200 model_kind: ModelKind::Balanced,
201 model: ModelPayload::balanced(net),
202 origin,
203 sources,
204 source_maps,
205 diagnostics: Vec::new(),
206 validation: ValidationSummary::ok(),
207 summary,
208 lowering_history: Vec::new(),
209 derived: DerivedMetadata::default(),
210 }
211 }
212
213 pub fn from_multiconductor(net: MulticonductorNetwork) -> Self {
218 let summary = multiconductor_summary(&net);
219 let sources = multiconductor_sources(&net);
220 let source_id = sources.first().map(|s| s.id.clone());
221 let source_maps = multiconductor_source_maps(&net, source_id.as_deref());
222 let origin = multiconductor_origin(&net);
223
224 let diagnostics: Vec<StructuredDiagnostic> = net
225 .warnings
226 .iter()
227 .map(|w| {
228 StructuredDiagnostic::new(
229 "READ.DIST.PARSE_WARNING",
230 DiagnosticSeverity::Warning,
231 DiagnosticStage::Read,
232 w.clone(),
233 )
234 })
235 .collect();
236 let validation = ValidationSummary::from_diagnostics(&diagnostics);
237
238 Self {
239 schema: default_schema_url(),
240 schema_version: default_schema_version(),
241 producer: Producer::powerio(),
242 package_id: None,
243 created_at: None,
244 model_kind: ModelKind::Multiconductor,
245 model: ModelPayload::multiconductor(net),
246 origin,
247 sources,
248 source_maps,
249 diagnostics,
250 validation,
251 summary,
252 lowering_history: Vec::new(),
253 derived: DerivedMetadata::default(),
254 }
255 }
256
257 pub fn model_kind(&self) -> ModelKind {
259 self.model_kind
260 }
261
262 pub fn kind_is_consistent(&self) -> bool {
265 self.model_kind == self.model.kind()
266 }
267
268 pub fn as_balanced(&self) -> Option<&BalancedNetwork> {
270 self.model.as_balanced()
271 }
272
273 pub fn as_multiconductor(&self) -> Option<&MulticonductorNetwork> {
275 self.model.as_multiconductor()
276 }
277
278 pub fn to_json(&self) -> serde_json::Result<String> {
280 serde_json::to_string(self)
281 }
282
283 pub fn to_json_pretty(&self) -> serde_json::Result<String> {
285 serde_json::to_string_pretty(self)
286 }
287
288 pub fn from_json(text: &str) -> serde_json::Result<Self> {
290 let pkg: Self = serde_json::from_str(text)?;
291 if !Self::supports_schema_version(&pkg.schema_version) {
292 return Err(<serde_json::Error as serde::de::Error>::custom(format!(
293 "unsupported .pio.json schema_version {}; this reader supports major version {}",
294 pkg.schema_version,
295 supported_schema_major()
296 )));
297 }
298 if !pkg.kind_is_consistent() {
299 return Err(<serde_json::Error as serde::de::Error>::custom(
300 "model_kind does not match model.kind",
301 ));
302 }
303 Ok(pkg)
304 }
305
306 pub fn supports_schema_version(version: &str) -> bool {
312 schema_major(version).is_some_and(|major| major == supported_schema_major())
313 }
314
315 #[must_use]
316 pub fn with_origin(mut self, origin: Origin) -> Self {
317 self.origin = origin;
318 self
319 }
320
321 #[must_use]
322 pub fn with_package_id(mut self, id: impl Into<String>) -> Self {
323 self.package_id = Some(id.into());
324 self
325 }
326
327 #[must_use]
328 pub fn with_created_at(mut self, created_at: impl Into<String>) -> Self {
329 self.created_at = Some(created_at.into());
330 self
331 }
332
333 #[must_use]
334 pub fn with_sources(mut self, sources: Vec<SourceDescriptor>) -> Self {
335 self.sources = sources;
336 self
337 }
338
339 #[must_use]
340 pub fn with_source_maps(mut self, source_maps: Vec<SourceMapEntry>) -> Self {
341 self.source_maps = source_maps;
342 self
343 }
344
345 pub fn push_lowering(&mut self, record: LoweringRecord) {
347 self.lowering_history.push(record);
348 }
349
350 pub fn attach_normalized_solver_table_metadata(
357 &mut self,
358 ) -> std::result::Result<bool, powerio::Error> {
359 let Some(net) = self.as_balanced() else {
360 return Ok(false);
361 };
362 let tables = net.to_normalized_solver_tables()?;
363 self.derived.normalized_solver_tables = Some(NormalizedSolverTableMetadata::from(&tables));
364 Ok(true)
365 }
366
367 pub fn with_normalized_solver_table_metadata(
369 mut self,
370 ) -> std::result::Result<Self, powerio::Error> {
371 self.attach_normalized_solver_table_metadata()?;
372 Ok(self)
373 }
374
375 #[must_use]
378 pub fn check_multiconductor_to_balanced_lowering(
379 &self,
380 ) -> Option<MulticonductorToBalancedReadiness> {
381 self.as_multiconductor().map(|net| {
382 check_multiconductor_to_balanced_lowering(
383 net,
384 MulticonductorToBalancedOptions::default(),
385 )
386 })
387 }
388
389 pub fn lower_multiconductor_to_balanced(
394 &self,
395 options: MulticonductorToBalancedOptions,
396 ) -> Result<Self, MulticonductorToBalancedError> {
397 let Some(net) = self.as_multiconductor() else {
398 let diagnostic = StructuredDiagnostic::new(
399 "LOWER.MULTI_TO_BALANCED.WRONG_MODEL_KIND",
400 DiagnosticSeverity::Error,
401 DiagnosticStage::Lower,
402 format!(
403 "multiconductor to balanced lowering requires a multiconductor package, got {:?}",
404 self.model_kind
405 ),
406 );
407 return Err(MulticonductorToBalancedError::new(
408 options,
409 vec![diagnostic],
410 ));
411 };
412
413 let lowered = lower_multiconductor_to_balanced(net, options)?;
414 let mut record = lowered.record;
415 let mut output = CompilerPackage::from_balanced(lowered.network);
416 output.origin = Origin::Derived {
417 parent_package_id: self.package_id.clone(),
418 pass: "multiconductor-to-balanced".to_owned(),
419 options: record.options.clone(),
420 };
421 output.sources = derived_sources(self);
422 let source_id = output.sources.first().map(|source| source.id.as_str());
423 output.source_maps = match output.as_balanced() {
424 Some(balanced) => lowered_balanced_source_maps(net, balanced, source_id),
425 None => Vec::new(),
426 };
427 output.diagnostics.clone_from(&record.diagnostics);
428 output.lowering_history.clone_from(&self.lowering_history);
429 output.run_sane_validation();
430 record.validation_status = output.validation.status;
431 output.push_lowering(record);
432 Ok(output)
433 }
434
435 pub fn run_sane_validation(&mut self) {
441 self.diagnostics
442 .retain(|d| !is_sane_validation_code(d.code.as_str()));
443
444 let (mut diagnostics, passes) = match &self.model {
445 ModelPayload::Balanced { balanced_network } => sane_validate_balanced(balanced_network),
446 ModelPayload::Multiconductor {
447 multiconductor_network,
448 } => sane_validate_multiconductor(multiconductor_network),
449 };
450
451 attach_source_refs(&mut diagnostics, &self.source_maps);
452 self.diagnostics.extend(diagnostics);
453 self.validation =
454 ValidationSummary::from_diagnostics(&self.diagnostics).with_passes(passes);
455 }
456}
457
458fn schema_major(version: &str) -> Option<u64> {
459 let (core, suffix) = match version.split_once('-') {
463 Some((core, rest)) => match rest.split_once('+') {
464 Some((pre, build)) => (core, Some((Some(pre), Some(build)))),
465 None => (core, Some((Some(rest), None))),
466 },
467 None => match version.split_once('+') {
468 Some((core, build)) => (core, Some((None, Some(build)))),
469 None => (version, None),
470 },
471 };
472 if let Some((pre, build)) = suffix {
473 if pre.is_some_and(|s| !valid_semver_suffix(s))
474 || build.is_some_and(|s| !valid_semver_suffix(s))
475 {
476 return None;
477 }
478 }
479 let mut parts = core.split('.');
480 let major = parts.next()?;
481 let minor = parts.next()?;
482 let patch = parts.next()?;
483 if parts.next().is_some() {
484 return None;
485 }
486 let major = parse_semver_number(major)?;
487 parse_semver_number(minor)?;
488 parse_semver_number(patch)?;
489 Some(major)
490}
491
492fn parse_semver_number(s: &str) -> Option<u64> {
493 if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) || (s.len() > 1 && s.starts_with('0'))
494 {
495 return None;
496 }
497 s.parse().ok()
498}
499
500fn valid_semver_suffix(s: &str) -> bool {
501 !s.is_empty()
502 && s.split('.').all(|part| {
503 !part.is_empty() && part.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-')
504 })
505}
506
507fn supported_schema_major() -> u64 {
508 schema_major(PIO_PACKAGE_SCHEMA_VERSION).expect("package schema version has a major number")
509}
510
511const SANE_VALIDATION_CODES: [&str; 6] = [
512 "VALIDATE.BALANCED.STRUCTURE",
513 "VALIDATE.BALANCED.VALUE_DOMAIN",
514 "VALIDATE.MULTI.STRUCTURE",
515 "VALIDATE.MULTI.TERMINAL_MAP",
516 "VALIDATE.MULTI.UNTYPED_OBJECT",
517 "VALIDATE.MULTI.NO_VOLTAGE_SOURCE",
518];
519
520fn is_sane_validation_code(code: &str) -> bool {
521 SANE_VALIDATION_CODES.contains(&code)
522}
523
524fn validation_status(diagnostics: &[StructuredDiagnostic]) -> ValidationStatus {
525 diagnostics
526 .iter()
527 .map(|d| match d.severity {
528 DiagnosticSeverity::Debug => ValidationStatus::Ok,
529 DiagnosticSeverity::Info => ValidationStatus::Info,
530 DiagnosticSeverity::Warning => ValidationStatus::Warning,
531 DiagnosticSeverity::Error => ValidationStatus::Error,
532 DiagnosticSeverity::Fatal => ValidationStatus::Fatal,
533 })
534 .max()
535 .unwrap_or(ValidationStatus::Ok)
536}
537
538fn sane_validate_balanced(
539 net: &BalancedNetwork,
540) -> (Vec<StructuredDiagnostic>, Vec<ValidationPass>) {
541 let mut structure = Vec::new();
542 if let Err(err) = net.validate() {
543 structure.push(StructuredDiagnostic::new(
544 "VALIDATE.BALANCED.STRUCTURE",
545 DiagnosticSeverity::Error,
546 DiagnosticStage::Validate,
547 err.to_string(),
548 ));
549 }
550
551 let bus_index: HashMap<usize, usize> = net
552 .buses
553 .iter()
554 .enumerate()
555 .map(|(idx, b)| (b.id.0, idx))
556 .collect();
557 let mut value_domain = Vec::new();
558 for finding in net.validate_values() {
559 let element_path =
560 balanced_value_finding_path(net, &bus_index, &finding).unwrap_or_else(|| {
561 format!(
562 "/model/balanced_network/{}#{}",
563 finding.element.replace(' ', "_"),
564 finding.field
565 )
566 });
567 let mut d = StructuredDiagnostic::new(
568 "VALIDATE.BALANCED.VALUE_DOMAIN",
569 DiagnosticSeverity::Warning,
570 DiagnosticStage::Validate,
571 format!(
572 "{} field `{}` is outside its value domain; suggested value is {}",
573 finding.element, finding.field, finding.new
574 ),
575 )
576 .with_element_path(element_path)
577 .with_suggested_action("Run the explicit repair pass if these defaults are desired.");
578 d.details
579 .insert("element".to_owned(), serde_json::json!(finding.element));
580 d.details
581 .insert("field".to_owned(), serde_json::json!(finding.field));
582 d.details
583 .insert("old".to_owned(), serde_json::json!(finding.old));
584 d.details
585 .insert("new".to_owned(), serde_json::json!(finding.new));
586 d.details
587 .insert("reason".to_owned(), serde_json::json!(finding.reason));
588 value_domain.push(d);
589 }
590
591 let passes = vec![
592 ValidationPass::new("balanced.structure", validation_status(&structure)),
593 ValidationPass::new("balanced.value_domain", validation_status(&value_domain)),
594 ];
595 structure.extend(value_domain);
596 (structure, passes)
597}
598
599fn attach_source_refs(diagnostics: &mut [StructuredDiagnostic], source_maps: &[SourceMapEntry]) {
600 let mut by_path: HashMap<&str, &SourceRef> = HashMap::with_capacity(source_maps.len());
604 for map in source_maps {
605 by_path
606 .entry(map.element_path.as_str())
607 .or_insert(&map.source_ref);
608 }
609 for diagnostic in diagnostics {
610 if diagnostic.source_ref.is_some() {
611 continue;
612 }
613 let Some(path) = diagnostic.element_path.as_deref() else {
614 continue;
615 };
616 if let Some(source_ref) = by_path.get(path) {
617 diagnostic.source_ref = Some((*source_ref).clone());
618 }
619 }
620}
621
622fn balanced_value_finding_path(
623 net: &BalancedNetwork,
624 bus_index: &HashMap<usize, usize>,
625 finding: &powerio::Diagnostic,
626) -> Option<String> {
627 if let Some(id) = finding
628 .element
629 .strip_prefix("bus ")
630 .and_then(|s| s.parse::<usize>().ok())
631 {
632 let idx = *bus_index.get(&id)?;
633 return Some(format!(
634 "/model/balanced_network/buses/{idx}/{}",
635 finding.field
636 ));
637 }
638
639 if let Some(id) = finding
640 .element
641 .strip_prefix("generator at bus ")
642 .and_then(|s| s.parse::<usize>().ok())
643 {
644 let mut matches = net
648 .generators
649 .iter()
650 .enumerate()
651 .filter(|(_, g)| {
652 g.bus.0 == id
653 && generator_field(g, finding.field)
654 .is_some_and(|v| v.to_bits() == finding.old.to_bits())
655 })
656 .map(|(idx, _)| idx);
657 let idx = matches.next()?;
658 if matches.next().is_some() {
659 return None;
660 }
661 return Some(format!(
662 "/model/balanced_network/generators/{idx}/{}",
663 finding.field
664 ));
665 }
666
667 None
668}
669
670fn generator_field(generator: &powerio::Generator, field: &str) -> Option<f64> {
671 Some(match field {
672 "mbase" => generator.mbase,
673 "vg" => generator.vg,
674 _ => return None,
675 })
676}
677
678fn sane_validate_multiconductor(
679 net: &MulticonductorNetwork,
680) -> (Vec<StructuredDiagnostic>, Vec<ValidationPass>) {
681 let mut structure = Vec::new();
682 let mut terminal_maps = Vec::new();
683 let mut untyped = Vec::new();
684 let mut sources = Vec::new();
685
686 let (bus_ids, bus_terminals) = multiconductor_bus_index(net, &mut structure);
687
688 validate_multiconductor_lines(
689 net,
690 &bus_ids,
691 &bus_terminals,
692 &mut structure,
693 &mut terminal_maps,
694 );
695 validate_multiconductor_switches(
696 net,
697 &bus_ids,
698 &bus_terminals,
699 &mut structure,
700 &mut terminal_maps,
701 );
702 validate_multiconductor_transformers(
703 net,
704 &bus_ids,
705 &bus_terminals,
706 &mut structure,
707 &mut terminal_maps,
708 );
709 validate_multiconductor_injections(
710 net,
711 &bus_ids,
712 &bus_terminals,
713 &mut structure,
714 &mut terminal_maps,
715 );
716
717 for (i, obj) in net.untyped.iter().enumerate() {
718 untyped.push(
719 StructuredDiagnostic::new(
720 "VALIDATE.MULTI.UNTYPED_OBJECT",
721 DiagnosticSeverity::Warning,
722 DiagnosticStage::Validate,
723 format!(
724 "{} {} is preserved as an untyped object",
725 obj.class, obj.name
726 ),
727 )
728 .with_element_path(format!("/model/multiconductor_network/untyped/{i}")),
729 );
730 }
731
732 if net.sources.is_empty() {
733 sources.push(StructuredDiagnostic::new(
734 "VALIDATE.MULTI.NO_VOLTAGE_SOURCE",
735 DiagnosticSeverity::Warning,
736 DiagnosticStage::Validate,
737 "multiconductor package has no voltage source",
738 ));
739 }
740
741 let passes = vec![
742 ValidationPass::new("multiconductor.structure", validation_status(&structure)),
743 ValidationPass::new(
744 "multiconductor.terminal_map",
745 validation_status(&terminal_maps),
746 ),
747 ValidationPass::new("multiconductor.untyped_object", validation_status(&untyped)),
748 ValidationPass::new("multiconductor.voltage_source", validation_status(&sources)),
749 ];
750
751 let mut diagnostics = structure;
752 diagnostics.extend(terminal_maps);
753 diagnostics.extend(untyped);
754 diagnostics.extend(sources);
755 (diagnostics, passes)
756}
757
758fn validate_multiconductor_lines(
759 net: &MulticonductorNetwork,
760 bus_ids: &BTreeSet<String>,
761 bus_terminals: &BTreeMap<String, BTreeSet<String>>,
762 structure: &mut Vec<StructuredDiagnostic>,
763 terminal_maps: &mut Vec<StructuredDiagnostic>,
764) {
765 for (i, line) in net.lines.iter().enumerate() {
766 check_bus_ref(
767 &line.bus_from,
768 &format!("line {} from bus", line.name),
769 &format!("/model/multiconductor_network/lines/{i}/bus_from"),
770 bus_ids,
771 structure,
772 );
773 check_bus_ref(
774 &line.bus_to,
775 &format!("line {} to bus", line.name),
776 &format!("/model/multiconductor_network/lines/{i}/bus_to"),
777 bus_ids,
778 structure,
779 );
780 if !net
781 .linecodes
782 .iter()
783 .any(|c| c.name.eq_ignore_ascii_case(&line.linecode))
784 {
785 structure.push(
786 StructuredDiagnostic::new(
787 "VALIDATE.MULTI.STRUCTURE",
788 DiagnosticSeverity::Error,
789 DiagnosticStage::Validate,
790 format!(
791 "line {} references unknown linecode `{}`",
792 line.name, line.linecode
793 ),
794 )
795 .with_element_path(format!("/model/multiconductor_network/lines/{i}/linecode")),
796 );
797 }
798 check_terminal_map(
799 &line.bus_from,
800 &line.terminal_map_from,
801 &format!("line {} from terminals", line.name),
802 &format!("/model/multiconductor_network/lines/{i}/terminal_map_from"),
803 bus_terminals,
804 terminal_maps,
805 );
806 check_terminal_map(
807 &line.bus_to,
808 &line.terminal_map_to,
809 &format!("line {} to terminals", line.name),
810 &format!("/model/multiconductor_network/lines/{i}/terminal_map_to"),
811 bus_terminals,
812 terminal_maps,
813 );
814 }
815}
816
817fn validate_multiconductor_switches(
818 net: &MulticonductorNetwork,
819 bus_ids: &BTreeSet<String>,
820 bus_terminals: &BTreeMap<String, BTreeSet<String>>,
821 structure: &mut Vec<StructuredDiagnostic>,
822 terminal_maps: &mut Vec<StructuredDiagnostic>,
823) {
824 for (i, sw) in net.switches.iter().enumerate() {
825 check_bus_ref(
826 &sw.bus_from,
827 &format!("switch {} from bus", sw.name),
828 &format!("/model/multiconductor_network/switches/{i}/bus_from"),
829 bus_ids,
830 structure,
831 );
832 check_bus_ref(
833 &sw.bus_to,
834 &format!("switch {} to bus", sw.name),
835 &format!("/model/multiconductor_network/switches/{i}/bus_to"),
836 bus_ids,
837 structure,
838 );
839 check_terminal_map(
840 &sw.bus_from,
841 &sw.terminal_map_from,
842 &format!("switch {} from terminals", sw.name),
843 &format!("/model/multiconductor_network/switches/{i}/terminal_map_from"),
844 bus_terminals,
845 terminal_maps,
846 );
847 check_terminal_map(
848 &sw.bus_to,
849 &sw.terminal_map_to,
850 &format!("switch {} to terminals", sw.name),
851 &format!("/model/multiconductor_network/switches/{i}/terminal_map_to"),
852 bus_terminals,
853 terminal_maps,
854 );
855 }
856}
857
858fn validate_multiconductor_transformers(
859 net: &MulticonductorNetwork,
860 bus_ids: &BTreeSet<String>,
861 bus_terminals: &BTreeMap<String, BTreeSet<String>>,
862 structure: &mut Vec<StructuredDiagnostic>,
863 terminal_maps: &mut Vec<StructuredDiagnostic>,
864) {
865 for (i, tx) in net.transformers.iter().enumerate() {
866 for (j, winding) in tx.windings.iter().enumerate() {
867 check_bus_ref(
868 &winding.bus,
869 &format!("transformer {} winding {j} bus", tx.name),
870 &format!("/model/multiconductor_network/transformers/{i}/windings/{j}/bus"),
871 bus_ids,
872 structure,
873 );
874 check_terminal_map(
875 &winding.bus,
876 &winding.terminal_map,
877 &format!("transformer {} winding {j} terminals", tx.name),
878 &format!(
879 "/model/multiconductor_network/transformers/{i}/windings/{j}/terminal_map"
880 ),
881 bus_terminals,
882 terminal_maps,
883 );
884 }
885 }
886}
887
888fn validate_multiconductor_injections(
889 net: &MulticonductorNetwork,
890 bus_ids: &BTreeSet<String>,
891 bus_terminals: &BTreeMap<String, BTreeSet<String>>,
892 structure: &mut Vec<StructuredDiagnostic>,
893 terminal_maps: &mut Vec<StructuredDiagnostic>,
894) {
895 let mut ctx = MultiValidationContext {
896 bus_ids,
897 bus_terminals,
898 structure,
899 terminal_maps,
900 };
901 for (i, load) in net.loads.iter().enumerate() {
902 check_one_bus_element(
903 &load.bus,
904 &load.terminal_map,
905 &format!("load {}", load.name),
906 &format!("/model/multiconductor_network/loads/{i}"),
907 &mut ctx,
908 );
909 }
910 for (i, generator) in net.generators.iter().enumerate() {
911 check_one_bus_element(
912 &generator.bus,
913 &generator.terminal_map,
914 &format!("generator {}", generator.name),
915 &format!("/model/multiconductor_network/generators/{i}"),
916 &mut ctx,
917 );
918 }
919 for (i, shunt) in net.shunts.iter().enumerate() {
920 check_one_bus_element(
921 &shunt.bus,
922 &shunt.terminal_map,
923 &format!("shunt {}", shunt.name),
924 &format!("/model/multiconductor_network/shunts/{i}"),
925 &mut ctx,
926 );
927 }
928 for (i, source) in net.sources.iter().enumerate() {
929 check_one_bus_element(
930 &source.bus,
931 &source.terminal_map,
932 &format!("voltage source {}", source.name),
933 &format!("/model/multiconductor_network/sources/{i}"),
934 &mut ctx,
935 );
936 }
937}
938
939struct MultiValidationContext<'a> {
940 bus_ids: &'a BTreeSet<String>,
941 bus_terminals: &'a BTreeMap<String, BTreeSet<String>>,
942 structure: &'a mut Vec<StructuredDiagnostic>,
943 terminal_maps: &'a mut Vec<StructuredDiagnostic>,
944}
945
946fn check_one_bus_element(
947 bus: &str,
948 terminal_map: &[String],
949 label: &str,
950 path: &str,
951 ctx: &mut MultiValidationContext<'_>,
952) {
953 check_bus_ref(
954 bus,
955 &format!("{label} bus"),
956 &format!("{path}/bus"),
957 ctx.bus_ids,
958 ctx.structure,
959 );
960 check_terminal_map(
961 bus,
962 terminal_map,
963 &format!("{label} terminals"),
964 &format!("{path}/terminal_map"),
965 ctx.bus_terminals,
966 ctx.terminal_maps,
967 );
968}
969
970fn multiconductor_bus_index(
971 net: &MulticonductorNetwork,
972 diagnostics: &mut Vec<StructuredDiagnostic>,
973) -> (BTreeSet<String>, BTreeMap<String, BTreeSet<String>>) {
974 let mut ids = BTreeSet::new();
975 let mut terminals = BTreeMap::new();
976 let mut first_seen = BTreeMap::<String, String>::new();
977 for (i, bus) in net.buses.iter().enumerate() {
978 let key = bus.id.to_ascii_lowercase();
979 if let Some(first) = first_seen.insert(key.clone(), bus.id.clone()) {
980 diagnostics.push(
981 StructuredDiagnostic::new(
982 "VALIDATE.MULTI.STRUCTURE",
983 DiagnosticSeverity::Error,
984 DiagnosticStage::Validate,
985 format!("duplicate bus id `{}` conflicts with `{first}`", bus.id),
986 )
987 .with_element_path(format!("/model/multiconductor_network/buses/{i}/id")),
988 );
989 }
990 ids.insert(key.clone());
991 terminals.insert(key, bus.terminals.iter().cloned().collect());
992 }
993 (ids, terminals)
994}
995
996fn check_bus_ref(
997 bus: &str,
998 what: &str,
999 path: &str,
1000 bus_ids: &BTreeSet<String>,
1001 diagnostics: &mut Vec<StructuredDiagnostic>,
1002) {
1003 if !bus_ids.contains(&bus.to_ascii_lowercase()) {
1004 diagnostics.push(
1005 StructuredDiagnostic::new(
1006 "VALIDATE.MULTI.STRUCTURE",
1007 DiagnosticSeverity::Error,
1008 DiagnosticStage::Validate,
1009 format!("{what} references unknown bus `{bus}`"),
1010 )
1011 .with_element_path(path),
1012 );
1013 }
1014}
1015
1016fn check_terminal_map(
1017 bus: &str,
1018 terminal_map: &[String],
1019 what: &str,
1020 path: &str,
1021 bus_terminals: &BTreeMap<String, BTreeSet<String>>,
1022 diagnostics: &mut Vec<StructuredDiagnostic>,
1023) {
1024 if terminal_map.is_empty() {
1025 diagnostics.push(
1026 StructuredDiagnostic::new(
1027 "VALIDATE.MULTI.TERMINAL_MAP",
1028 DiagnosticSeverity::Error,
1029 DiagnosticStage::Validate,
1030 format!("{what} has an empty terminal map"),
1031 )
1032 .with_element_path(path),
1033 );
1034 return;
1035 }
1036
1037 let Some(known) = bus_terminals.get(&bus.to_ascii_lowercase()) else {
1038 return;
1039 };
1040 for terminal in terminal_map {
1041 if !known.contains(terminal) {
1042 diagnostics.push(
1043 StructuredDiagnostic::new(
1044 "VALIDATE.MULTI.TERMINAL_MAP",
1045 DiagnosticSeverity::Error,
1046 DiagnosticStage::Validate,
1047 format!("{what} references unknown terminal `{terminal}` on bus `{bus}`"),
1048 )
1049 .with_element_path(path),
1050 );
1051 }
1052 }
1053}
1054
1055fn balanced_format_name(f: SourceFormat) -> &'static str {
1057 match f {
1058 SourceFormat::Matpower => "matpower",
1059 SourceFormat::PowerModelsJson => "powermodels-json",
1060 SourceFormat::EgretJson => "egret-json",
1061 SourceFormat::Psse => "psse",
1062 SourceFormat::PowerWorld => "powerworld",
1063 SourceFormat::PandapowerJson => "pandapower-json",
1064 SourceFormat::Pslf => "pslf",
1065 SourceFormat::PowerWorldBinary => "powerworld-pwb",
1066 SourceFormat::InMemory => "in-memory",
1067 SourceFormat::Normalized => "normalized",
1068 SourceFormat::Gridfm => "gridfm",
1069 SourceFormat::PypsaCsv => "pypsa-csv",
1070 _ => "unknown",
1071 }
1072}
1073
1074fn balanced_origin(net: &BalancedNetwork) -> Origin {
1075 match net.source_format {
1076 SourceFormat::InMemory => Origin::InMemory,
1077 SourceFormat::Normalized => Origin::Derived {
1078 parent_package_id: None,
1079 pass: "normalize-balanced".to_owned(),
1080 options: serde_json::Map::new(),
1081 },
1082 SourceFormat::Gridfm | SourceFormat::PypsaCsv => Origin::Folder {
1083 path: String::new(),
1084 format: balanced_format_name(net.source_format).to_owned(),
1085 file_hashes: BTreeMap::new(),
1086 },
1087 SourceFormat::PowerWorldBinary => Origin::BinaryFile {
1088 path: String::new(),
1089 format: balanced_format_name(net.source_format).to_owned(),
1090 hash: None,
1091 decoded_sections: Vec::new(),
1092 },
1093 other => Origin::File {
1094 path: String::new(),
1095 format: balanced_format_name(other).to_owned(),
1096 hash: None,
1097 retained_source: net.source.is_some(),
1098 },
1099 }
1100}
1101
1102fn balanced_sources(net: &BalancedNetwork) -> Vec<SourceDescriptor> {
1103 let Some(kind) = balanced_source_kind(net.source_format) else {
1104 return Vec::new();
1105 };
1106 vec![SourceDescriptor {
1107 id: "src0".to_owned(),
1108 kind: kind.to_owned(),
1109 path: None,
1110 format: Some(balanced_format_name(net.source_format).to_owned()),
1111 hash: None,
1112 }]
1113}
1114
1115fn balanced_source_kind(f: SourceFormat) -> Option<&'static str> {
1116 match f {
1117 SourceFormat::InMemory | SourceFormat::Normalized => None,
1118 SourceFormat::Gridfm | SourceFormat::PypsaCsv => Some("folder"),
1119 SourceFormat::PowerWorldBinary => Some("binary_file"),
1120 _ => Some("file"),
1121 }
1122}
1123
1124fn balanced_summary(net: &BalancedNetwork) -> ObjectSummary {
1125 let mut elements = BTreeMap::new();
1126 elements.insert("buses".to_owned(), net.buses.len() as u64);
1127 elements.insert("loads".to_owned(), net.loads.len() as u64);
1128 elements.insert("shunts".to_owned(), net.shunts.len() as u64);
1129 elements.insert("branches".to_owned(), net.branches.len() as u64);
1130 elements.insert("generators".to_owned(), net.generators.len() as u64);
1131 elements.insert("storage".to_owned(), net.storage.len() as u64);
1132 elements.insert("hvdc".to_owned(), net.hvdc.len() as u64);
1133 elements.insert(
1134 "transformers_3w".to_owned(),
1135 net.transformers_3w.len() as u64,
1136 );
1137
1138 let reference_buses: Vec<String> = net
1139 .buses
1140 .iter()
1141 .filter(|b| b.kind == powerio::BusType::Ref)
1142 .map(|b| b.id.0.to_string())
1143 .collect();
1144
1145 ObjectSummary {
1146 elements,
1147 topology: Some(ObjectTopology {
1148 connected_components: None,
1149 reference_buses,
1150 }),
1151 units: Some(ObjectUnits {
1152 power: Some("MW/MVAr".to_owned()),
1153 angle: Some("degrees".to_owned()),
1154 base_mva: Some(net.base_mva),
1155 }),
1156 }
1157}
1158
1159fn balanced_source_maps(net: &BalancedNetwork, source_id: Option<&str>) -> Vec<SourceMapEntry> {
1160 let Some(source_id) = source_id else {
1161 return Vec::new();
1162 };
1163 let mut entries = Vec::new();
1164 push_balanced_network_maps(&mut entries, source_id, net.source_format);
1165 push_balanced_bus_maps(&mut entries, source_id, net.buses.len());
1166 push_balanced_injection_maps(&mut entries, source_id, net);
1167 push_balanced_branch_maps(&mut entries, source_id, net);
1168 push_balanced_generator_maps(&mut entries, source_id, net.generators.len());
1169 entries
1170}
1171
1172fn push_balanced_network_maps(
1173 entries: &mut Vec<SourceMapEntry>,
1174 source_id: &str,
1175 source_format: SourceFormat,
1176) {
1177 push_balanced_map(
1178 entries,
1179 source_id,
1180 "/model/balanced_network/base_mva",
1181 "case",
1182 "base_mva",
1183 MappingKind::Exact,
1184 );
1185 if balanced_has_frequency_source(source_format) {
1186 push_balanced_map(
1187 entries,
1188 source_id,
1189 "/model/balanced_network/base_frequency",
1190 "case",
1191 "base_frequency",
1192 MappingKind::Exact,
1193 );
1194 }
1195}
1196
1197fn push_balanced_bus_maps(entries: &mut Vec<SourceMapEntry>, source_id: &str, len: usize) {
1198 push_balanced_record_maps(
1199 entries,
1200 source_id,
1201 "buses",
1202 len,
1203 "bus",
1204 &[
1205 "id", "kind", "vm", "va", "base_kv", "vmax", "vmin", "area", "zone",
1206 ],
1207 MappingKind::Exact,
1208 );
1209}
1210
1211fn push_balanced_injection_maps(
1212 entries: &mut Vec<SourceMapEntry>,
1213 source_id: &str,
1214 net: &BalancedNetwork,
1215) {
1216 if net.source_format == SourceFormat::Matpower {
1217 push_matpower_injection_maps(entries, source_id, net);
1218 } else {
1219 push_balanced_record_maps(
1220 entries,
1221 source_id,
1222 "loads",
1223 net.loads.len(),
1224 "load",
1225 &["bus", "p", "q", "in_service"],
1226 MappingKind::Exact,
1227 );
1228 push_balanced_record_maps(
1229 entries,
1230 source_id,
1231 "shunts",
1232 net.shunts.len(),
1233 "shunt",
1234 &["bus", "g", "b", "in_service"],
1235 MappingKind::Exact,
1236 );
1237 }
1238}
1239
1240fn push_balanced_branch_maps(
1241 entries: &mut Vec<SourceMapEntry>,
1242 source_id: &str,
1243 net: &BalancedNetwork,
1244) {
1245 for (i, branch) in net.branches.iter().enumerate() {
1246 push_balanced_record_map(
1247 entries,
1248 source_id,
1249 "branches",
1250 i,
1251 "branch",
1252 &[
1253 "from",
1254 "to",
1255 "r",
1256 "x",
1257 "b",
1258 "rate_a",
1259 "rate_b",
1260 "rate_c",
1261 "tap",
1262 "shift",
1263 "in_service",
1264 "angmin",
1265 "angmax",
1266 ],
1267 MappingKind::Exact,
1268 );
1269 if branch.charging.is_some() {
1270 for field in ["g_fr", "b_fr", "g_to", "b_to"] {
1271 push_balanced_map(
1272 entries,
1273 source_id,
1274 &format!("/model/balanced_network/branches/{i}/charging/{field}"),
1275 "branch",
1276 field,
1277 MappingKind::Exact,
1278 );
1279 }
1280 }
1281 }
1282}
1283
1284fn push_balanced_generator_maps(entries: &mut Vec<SourceMapEntry>, source_id: &str, len: usize) {
1285 push_balanced_record_maps(
1286 entries,
1287 source_id,
1288 "generators",
1289 len,
1290 "generator",
1291 &[
1292 "bus",
1293 "pg",
1294 "qg",
1295 "pmax",
1296 "pmin",
1297 "qmax",
1298 "qmin",
1299 "vg",
1300 "mbase",
1301 "in_service",
1302 ],
1303 MappingKind::Exact,
1304 );
1305}
1306
1307fn balanced_has_frequency_source(source_format: SourceFormat) -> bool {
1308 matches!(
1309 source_format,
1310 SourceFormat::Psse | SourceFormat::PandapowerJson
1311 )
1312}
1313
1314fn push_matpower_injection_maps(
1315 entries: &mut Vec<SourceMapEntry>,
1316 source_id: &str,
1317 net: &BalancedNetwork,
1318) {
1319 push_balanced_record_maps(
1323 entries,
1324 source_id,
1325 "loads",
1326 net.loads.len(),
1327 "bus",
1328 &["bus", "p", "q", "in_service"],
1329 MappingKind::Split,
1330 );
1331 push_balanced_record_maps(
1332 entries,
1333 source_id,
1334 "shunts",
1335 net.shunts.len(),
1336 "bus",
1337 &["bus", "g", "b", "in_service"],
1338 MappingKind::Split,
1339 );
1340}
1341
1342fn push_balanced_record_maps(
1343 entries: &mut Vec<SourceMapEntry>,
1344 source_id: &str,
1345 collection: &str,
1346 len: usize,
1347 record: &str,
1348 fields: &[&str],
1349 mapping_kind: MappingKind,
1350) {
1351 for i in 0..len {
1352 push_balanced_record_map(
1353 entries,
1354 source_id,
1355 collection,
1356 i,
1357 record,
1358 fields,
1359 mapping_kind,
1360 );
1361 }
1362}
1363
1364fn push_balanced_record_map(
1365 entries: &mut Vec<SourceMapEntry>,
1366 source_id: &str,
1367 collection: &str,
1368 i: usize,
1369 record: &str,
1370 fields: &[&str],
1371 mapping_kind: MappingKind,
1372) {
1373 for &field in fields {
1374 push_balanced_map(
1375 entries,
1376 source_id,
1377 &format!("/model/balanced_network/{collection}/{i}/{field}"),
1378 record,
1379 field,
1380 mapping_kind,
1381 );
1382 }
1383}
1384
1385fn push_balanced_map(
1386 entries: &mut Vec<SourceMapEntry>,
1387 source_id: &str,
1388 element_path: &str,
1389 record: &str,
1390 field: &str,
1391 mapping_kind: MappingKind,
1392) {
1393 entries.push(SourceMapEntry {
1394 element_path: element_path.to_owned(),
1395 source_ref: SourceRef::new(source_id)
1396 .with_record(record)
1397 .with_field(field),
1398 mapping_kind,
1399 confidence: Confidence::High,
1400 });
1401}
1402
1403fn multiconductor_summary(net: &MulticonductorNetwork) -> ObjectSummary {
1404 let mut elements = BTreeMap::new();
1405 elements.insert("buses".to_owned(), net.buses.len() as u64);
1406 elements.insert("linecodes".to_owned(), net.linecodes.len() as u64);
1407 elements.insert("lines".to_owned(), net.lines.len() as u64);
1408 elements.insert("switches".to_owned(), net.switches.len() as u64);
1409 elements.insert("transformers".to_owned(), net.transformers.len() as u64);
1410 elements.insert("loads".to_owned(), net.loads.len() as u64);
1411 elements.insert("generators".to_owned(), net.generators.len() as u64);
1412 elements.insert("shunts".to_owned(), net.shunts.len() as u64);
1413 elements.insert("voltage_sources".to_owned(), net.sources.len() as u64);
1414
1415 ObjectSummary {
1416 elements,
1417 topology: None,
1418 units: Some(ObjectUnits {
1419 power: Some("W/var".to_owned()),
1420 angle: Some("radians".to_owned()),
1421 base_mva: None,
1422 }),
1423 }
1424}
1425
1426fn multiconductor_sources(net: &MulticonductorNetwork) -> Vec<SourceDescriptor> {
1427 match net.source_format {
1428 Some(sf) => vec![SourceDescriptor {
1429 id: "src0".to_owned(),
1430 kind: "file".to_owned(),
1431 path: None,
1432 format: Some(dist_format_name(sf).to_owned()),
1433 hash: None,
1434 }],
1435 None => Vec::new(),
1436 }
1437}
1438
1439fn dist_format_name(f: DistSourceFormat) -> &'static str {
1440 f.name()
1441}
1442
1443fn multiconductor_origin(net: &MulticonductorNetwork) -> Origin {
1444 match net.source_format {
1445 Some(sf) => Origin::File {
1446 path: String::new(),
1447 format: dist_format_name(sf).to_owned(),
1448 hash: None,
1449 retained_source: net.source.is_some(),
1450 },
1451 None => Origin::InMemory,
1452 }
1453}
1454
1455fn derived_sources(parent: &CompilerPackage) -> Vec<SourceDescriptor> {
1456 if !parent.sources.is_empty() {
1457 return parent.sources.clone();
1458 }
1459 vec![SourceDescriptor {
1460 id: "parent".to_owned(),
1461 kind: "package".to_owned(),
1462 path: None,
1463 format: Some("pio-json".to_owned()),
1464 hash: parent.package_id.clone(),
1465 }]
1466}
1467
1468fn lowered_balanced_source_maps(
1469 input: &MulticonductorNetwork,
1470 balanced: &BalancedNetwork,
1471 source_id: Option<&str>,
1472) -> Vec<SourceMapEntry> {
1473 let Some(source_id) = source_id else {
1474 return Vec::new();
1475 };
1476 let mut entries = Vec::new();
1477 push_lowered_bus_maps(&mut entries, source_id, input);
1478 push_lowered_branch_maps(&mut entries, source_id, input, balanced);
1479 push_lowered_load_maps(&mut entries, source_id, input, balanced);
1480 push_lowered_shunt_maps(&mut entries, source_id, input, balanced);
1481 push_lowered_generator_maps(&mut entries, source_id, input, balanced);
1482 entries
1483}
1484
1485fn push_lowered_bus_maps(
1486 entries: &mut Vec<SourceMapEntry>,
1487 source_id: &str,
1488 input: &MulticonductorNetwork,
1489) {
1490 for (idx, bus) in input.buses.iter().enumerate() {
1491 for (field, mapping_kind) in [
1492 ("id", MappingKind::Synthetic),
1493 ("kind", MappingKind::Lowered),
1494 ("vm", MappingKind::ConvertedUnits),
1495 ("va", MappingKind::ConvertedUnits),
1496 ("base_kv", MappingKind::ConvertedUnits),
1497 ("area", MappingKind::Defaulted),
1498 ("zone", MappingKind::Defaulted),
1499 ("name", MappingKind::Lowered),
1500 ] {
1501 push_lowered_map(
1502 entries,
1503 source_id,
1504 &format!("/model/balanced_network/buses/{idx}/{field}"),
1505 "multiconductor_bus",
1506 field,
1507 mapping_kind,
1508 );
1509 }
1510 for field in ["vmin", "vmax"] {
1511 let mapping_kind = if bus.v_min.is_some() && bus.v_max.is_some() {
1512 MappingKind::ConvertedUnits
1513 } else {
1514 MappingKind::Defaulted
1515 };
1516 push_lowered_map(
1517 entries,
1518 source_id,
1519 &format!("/model/balanced_network/buses/{idx}/{field}"),
1520 "multiconductor_bus",
1521 field,
1522 mapping_kind,
1523 );
1524 }
1525 }
1526}
1527
1528fn push_lowered_branch_maps(
1529 entries: &mut Vec<SourceMapEntry>,
1530 source_id: &str,
1531 input: &MulticonductorNetwork,
1532 balanced: &BalancedNetwork,
1533) {
1534 for (idx, branch) in balanced.branches.iter().enumerate() {
1535 let record = "multiconductor_line";
1536 for (field, mapping_kind) in [
1537 ("from", MappingKind::Lowered),
1538 ("to", MappingKind::Lowered),
1539 ("r", MappingKind::ConvertedUnits),
1540 ("x", MappingKind::ConvertedUnits),
1541 ("b", MappingKind::ConvertedUnits),
1542 ("in_service", MappingKind::Lowered),
1543 ("tap", MappingKind::Defaulted),
1544 ("shift", MappingKind::Defaulted),
1545 ("angmin", MappingKind::Defaulted),
1546 ("angmax", MappingKind::Defaulted),
1547 ] {
1548 push_lowered_map(
1549 entries,
1550 source_id,
1551 &format!("/model/balanced_network/branches/{idx}/{field}"),
1552 record,
1553 field,
1554 mapping_kind,
1555 );
1556 }
1557 let has_rating = input
1558 .lines
1559 .get(idx)
1560 .and_then(|line| input.linecode(&line.linecode))
1561 .is_some_and(|code| code.i_max.is_some() || code.s_max.is_some());
1562 let rate_kind = if has_rating {
1563 MappingKind::ConvertedUnits
1564 } else {
1565 MappingKind::Defaulted
1566 };
1567 for field in ["rate_a", "rate_b", "rate_c"] {
1568 push_lowered_map(
1569 entries,
1570 source_id,
1571 &format!("/model/balanced_network/branches/{idx}/{field}"),
1572 record,
1573 field,
1574 rate_kind,
1575 );
1576 }
1577 if branch.charging.is_some() {
1578 for field in ["g_fr", "b_fr", "g_to", "b_to"] {
1579 push_lowered_map(
1580 entries,
1581 source_id,
1582 &format!("/model/balanced_network/branches/{idx}/charging/{field}"),
1583 record,
1584 field,
1585 MappingKind::ConvertedUnits,
1586 );
1587 }
1588 }
1589 }
1590}
1591
1592fn push_lowered_load_maps(
1593 entries: &mut Vec<SourceMapEntry>,
1594 source_id: &str,
1595 input: &MulticonductorNetwork,
1596 balanced: &BalancedNetwork,
1597) {
1598 for idx in 0..balanced.loads.len().min(input.loads.len()) {
1599 for (field, mapping_kind) in [
1600 ("bus", MappingKind::Lowered),
1601 ("p", MappingKind::Aggregated),
1602 ("q", MappingKind::Aggregated),
1603 ("in_service", MappingKind::Lowered),
1604 ] {
1605 push_lowered_map(
1606 entries,
1607 source_id,
1608 &format!("/model/balanced_network/loads/{idx}/{field}"),
1609 "multiconductor_load",
1610 field,
1611 mapping_kind,
1612 );
1613 }
1614 }
1615}
1616
1617fn push_lowered_shunt_maps(
1618 entries: &mut Vec<SourceMapEntry>,
1619 source_id: &str,
1620 input: &MulticonductorNetwork,
1621 balanced: &BalancedNetwork,
1622) {
1623 for idx in 0..balanced.shunts.len().min(input.shunts.len()) {
1624 for (field, mapping_kind) in [
1625 ("bus", MappingKind::Lowered),
1626 ("g", MappingKind::Aggregated),
1627 ("b", MappingKind::Aggregated),
1628 ("in_service", MappingKind::Lowered),
1629 ] {
1630 push_lowered_map(
1631 entries,
1632 source_id,
1633 &format!("/model/balanced_network/shunts/{idx}/{field}"),
1634 "multiconductor_shunt",
1635 field,
1636 mapping_kind,
1637 );
1638 }
1639 }
1640}
1641
1642fn push_lowered_generator_maps(
1643 entries: &mut Vec<SourceMapEntry>,
1644 source_id: &str,
1645 input: &MulticonductorNetwork,
1646 balanced: &BalancedNetwork,
1647) {
1648 for idx in 0..balanced.generators.len().min(input.generators.len()) {
1649 let generator = &input.generators[idx];
1650 for (field, mapping_kind) in [
1651 ("bus", MappingKind::Lowered),
1652 ("pg", MappingKind::Aggregated),
1653 ("qg", MappingKind::Aggregated),
1654 ("vg", MappingKind::Defaulted),
1655 ("mbase", MappingKind::Synthetic),
1656 ("in_service", MappingKind::Lowered),
1657 ] {
1658 push_lowered_map(
1659 entries,
1660 source_id,
1661 &format!("/model/balanced_network/generators/{idx}/{field}"),
1662 "multiconductor_generator",
1663 field,
1664 mapping_kind,
1665 );
1666 }
1667 for (field, present) in [
1668 ("pmin", generator.p_min.is_some()),
1669 ("pmax", generator.p_max.is_some()),
1670 ("qmin", generator.q_min.is_some()),
1671 ("qmax", generator.q_max.is_some()),
1672 ] {
1673 push_lowered_map(
1674 entries,
1675 source_id,
1676 &format!("/model/balanced_network/generators/{idx}/{field}"),
1677 "multiconductor_generator",
1678 field,
1679 if present {
1680 MappingKind::Aggregated
1681 } else {
1682 MappingKind::Defaulted
1683 },
1684 );
1685 }
1686 }
1687}
1688
1689fn push_lowered_map(
1690 entries: &mut Vec<SourceMapEntry>,
1691 source_id: &str,
1692 element_path: &str,
1693 record: &str,
1694 field: &str,
1695 mapping_kind: MappingKind,
1696) {
1697 entries.push(SourceMapEntry {
1698 element_path: element_path.to_owned(),
1699 source_ref: SourceRef::new(source_id)
1700 .with_record(record)
1701 .with_field(field),
1702 mapping_kind,
1703 confidence: Confidence::High,
1704 });
1705}
1706
1707fn multiconductor_source_maps(
1712 net: &MulticonductorNetwork,
1713 source_id: Option<&str>,
1714) -> Vec<SourceMapEntry> {
1715 let Some(source_id) = source_id else {
1716 return Vec::new();
1717 };
1718 let mut entries = Vec::new();
1719 for (element, fields) in &net.defaulted {
1720 for field in fields {
1721 entries.push(SourceMapEntry {
1722 element_path: format!("/model/multiconductor_network/{element}#{field}"),
1723 source_ref: SourceRef::new(source_id).with_field((*field).to_owned()),
1724 mapping_kind: MappingKind::Defaulted,
1725 confidence: Confidence::High,
1726 });
1727 }
1728 }
1729 entries
1730}