powerio_tnd/formats/sincal/balanced/
source.rs1use crate::formats::sincal::storage::{AcquiredProject, DatabaseSnapshot, TableRecords};
3use powerio_core::{Diagnostic, Error, FormatId, PioModule, Source};
4
5use powerio_tx::BalancedNetwork;
6use powerio_tx::diagnostics::codes;
7use powerio_tx::format::routing::{TransmissionFormat, parse_transmission_format};
8
9#[derive(Clone, Debug, Default, PartialEq)]
12#[non_exhaustive]
13pub struct SincalBalancedReadOptions {
14 pub variant: Option<i64>,
16 pub snapshot_hours: Option<f64>,
18 pub acquired_tables: Option<String>,
21}
22
23fn failure(error: impl std::fmt::Display, source: &Source) -> Error {
24 Error::new(&codes::PARSE_SINCAL_MALFORMED, error.to_string()).with_source(source.clone())
25}
26
27pub fn parse(source: Source) -> Result<PioModule<BalancedNetwork>, Error> {
28 parse_with_options(source, &SincalBalancedReadOptions::default())
29}
30
31pub fn parse_with_options(
37 source: Source,
38 options: &SincalBalancedReadOptions,
39) -> Result<PioModule<BalancedNetwork>, Error> {
40 let selected = source
41 .format()
42 .and_then(|f| parse_transmission_format(f.as_str()));
43 if selected != Some(TransmissionFormat::SincalBalanced) {
44 return Err(Error::new(&codes::REQUEST_SINCAL_PROFILE_REQUIRED,
45 "SINCAL can contain balanced or multiconductor networks; select 'sincal-balanced' for positive sequence or 'sincal-multiconductor' for conductor-resolved interpretation. There is no balancing fallback.")
46 .with_source(source));
47 }
48 let source = source.with_format(FormatId::new("sincal-balanced")?);
49 let (snapshot, retained) = if let Some(name) = &options.acquired_tables {
50 let primary = source.primary_buffer().map_err(|e| failure(e, &source))?;
51 let records = source
52 .referenced_buffer(&primary, name)
53 .map_err(|e| failure(e, &source))?;
54 TableRecords::decode(records.bytes())
55 .map_err(|e| failure(e, &source))?
56 .verify_source(primary.bytes())
57 .map_err(|e| failure(e, &source))?;
58 let snapshot = DatabaseSnapshot::decode_records(records.bytes(), options.variant)
59 .map_err(|e| failure(e, &source))?;
60 (snapshot, source)
61 } else {
62 if source
63 .primary_buffer()
64 .map_err(|e| failure(e, &source))?
65 .bytes()
66 .starts_with(b"\0\x01\0\0Standard Jet DB\0")
67 {
68 return Err(failure(
69 "Access MDB parsing requires an explicitly supplied acquired_tables companion from import_access.py; the library does not run external tools",
70 &source,
71 ));
72 }
73 let acquired = AcquiredProject::read(&source).map_err(|e| failure(e, &source))?;
74 let retained = acquired
75 .source
76 .with_format(FormatId::new("sincal-balanced")?);
77 let snapshot = DatabaseSnapshot::decode(acquired.database.bytes(), options.variant)
78 .map_err(|e| failure(e, &retained))?;
79 (snapshot, retained)
80 };
81 let geometry = snapshot
82 .drawing_geometry()
83 .map_err(|e| failure(e, &retained))?;
84 let mut network =
85 super::read_balanced_snapshot_at(&snapshot, retained.name(), options.snapshot_hours)
86 .map_err(|e| failure(e, &retained))?;
87 let inventory = retention_inventory(&snapshot, profile_applied(&network))
88 .map_err(|e| failure(e, &retained))?;
89 let mut diagnostics = vec![
90 Diagnostic::of(
91 &codes::READ_SINCAL_CONVERSION_BASE,
92 "The balanced profile maps native powers, voltages and impedances on a 100 MVA conversion base; this is not a native system base.",
93 ),
94 Diagnostic::of(
95 &codes::READ_SINCAL_RETAINED_SOURCE_ONLY,
96 "Fault, dynamic, protection, economic and untyped graphic data, stored calculation results, and native settings outside the selected load-flow snapshot remain only in retained source. They are not part of the typed balanced network. An acquired-table companion is caller-supplied input, not a native export or independent attestation.",
97 ),
98 ];
99 diagnostics.extend(inventory);
100 super::geometry::attach(&mut network, &geometry, &mut diagnostics)?;
101 diagnostics.extend(default_diagnostics(&network)?);
102 if !network.generators().is_empty() {
103 diagnostics.push(Diagnostic::of(&codes::READ_SINCAL_LIMITS_UNSPECIFIED,
104 "Native generator capability limits are disabled in this profile; typed P/Q limits are unbounded. This does not establish OPF capability data."));
105 }
106 let mut module = PioModule::parsed(network, retained, diagnostics)?;
107 geometry.retain_view(&mut module)?;
108 Ok(module)
109}
110
111fn default_diagnostics(
112 network: &powerio_tx::network::BalancedNetwork,
113) -> Result<Vec<Diagnostic>, Error> {
114 let defaults = network
115 .buses()
116 .iter()
117 .filter_map(|bus| {
118 bus.extras
119 .get("sincal_voltage_basis")
120 .map(|value| (&bus.uid, value))
121 })
122 .chain(network.branches().iter().filter_map(|branch| {
123 branch
124 .extras
125 .get("sincal_defaulted_temperature")
126 .map(|value| (&branch.uid, value))
127 }));
128 defaults.map(|(component, value)| {
129 let mut diagnostic = Diagnostic::of(&codes::READ_SINCAL_VALUE_DEFAULTED,
130 format!("Versioned SINCAL field default applied to {component:?}; see structured native field and value"));
131 diagnostic.insert_detail("component", serde_json::json!(component))?;
132 diagnostic.insert_detail("default", value.clone())?;
133 Ok(diagnostic)
134 }).collect()
135}
136
137pub(super) fn profile_applied(network: &BalancedNetwork) -> bool {
139 network
140 .loads()
141 .iter()
142 .any(|load| load.extras.contains_key("sincal_profile"))
143}
144
145pub(super) fn retention_inventory(
149 snapshot: &DatabaseSnapshot,
150 profile_applied: bool,
151) -> crate::formats::sincal::storage::Result<Vec<Diagnostic>> {
152 let profile_tables: &[&str] = if profile_applied {
153 &["OpSer", "OpSerVal"]
154 } else {
155 &[]
156 };
157 let interpreted = [
158 "Version",
159 "Variant",
160 "Node",
161 "Element",
162 "Terminal",
163 "VoltageLevel",
164 "CalcParameter",
165 "Line",
166 "Load",
167 "Infeeder",
168 "TwoWindingTransformer",
169 "DCInfeeder",
170 "ShuntCondensator",
171 ]
172 .iter()
173 .chain(profile_tables)
174 .copied()
175 .collect::<Vec<_>>();
176 snapshot.retention_diagnostics(
177 &codes::READ_SINCAL_RETAINED_SOURCE_ONLY,
178 &interpreted,
179 &[
180 (
181 "Infeeder",
182 &[
183 "Flag_Har",
184 "HarImp_ID",
185 "HarVolt_ID",
186 "HarCur_ID",
187 "Flag_Reliability",
188 "SupplyType_ID",
189 ],
190 ),
191 ("Line", &["Flag_Har", "Flag_Reliability"]),
192 ("TwoWindingTransformer", &["Flag_Har", "Flag_Reliability"]),
193 ],
194 )
195}