1use powerio_tx::format::routing::TransmissionFormat;
9
10#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
12#[serde(rename_all = "lowercase")]
13#[non_exhaustive]
14pub enum NetworkFamily {
15 Transmission,
16 Distribution,
17}
18
19#[derive(Clone, Copy, Debug, serde::Serialize)]
21#[non_exhaustive]
22pub struct GridFormatInfo {
23 pub format: FormatInfo,
24 pub label: &'static str,
25 pub family: NetworkFamily,
28 pub can_read: bool,
29}
30
31impl GridFormatInfo {
32 #[must_use]
37 pub fn supports_family(&self, family: NetworkFamily) -> bool {
38 self.family == family
39 || (self.format.token == "ravens-json"
40 && matches!(
41 family,
42 NetworkFamily::Transmission | NetworkFamily::Distribution
43 ))
44 }
45}
46
47pub fn grid_formats() -> impl Iterator<Item = GridFormatInfo> {
57 use NetworkFamily::{Distribution, Transmission};
58 const FORMATS: &[(&str, &str, NetworkFamily, bool)] = &[
59 ("matpower", "MATPOWER", Transmission, true),
60 ("psse", "PSS/E RAW 33", Transmission, true),
61 ("psse34", "PSS/E RAW 34", Transmission, true),
62 ("psse35", "PSS/E RAW 35", Transmission, true),
63 ("psse-rawx", "PSS/E RAWX", Transmission, true),
64 ("powermodels-json", "PowerModels JSON", Transmission, true),
65 ("pandapower-json", "pandapower JSON", Transmission, true),
66 ("pypsa-csv", "PyPSA CSV folder", Transmission, true),
67 ("egret-json", "Egret JSON", Transmission, true),
68 ("powerworld", "PowerWorld AUX", Transmission, true),
69 ("pslf", "PSLF EPC", Transmission, true),
70 ("xiidm", "PowSybl XIIDM", Transmission, true),
71 ("jiidm", "PowSybl JIIDM", Transmission, true),
72 ("cgmes", "CIM CGMES", Transmission, true),
73 ("ucte", "UCTE-DEF", Transmission, true),
74 ("surge-json", "Surge JSON", Transmission, true),
75 ("ieee-cdf", "IEEE CDF (read only)", Transmission, true),
76 ("pwb", "PowerWorld PWB (read only)", Transmission, true),
77 (
78 "opfdata-json",
79 "DeepMind OPFData (read only)",
80 Transmission,
81 true,
82 ),
83 ("goc3-json", "GO Challenge 3", Transmission, true),
84 #[cfg(feature = "sincal")]
85 ("sincal-balanced", "SINCAL balanced", Transmission, true),
86 #[cfg(feature = "sincal")]
87 (
88 "sincal-multiconductor",
89 "SINCAL multiconductor",
90 Distribution,
91 true,
92 ),
93 ("dss", "OpenDSS", Distribution, true),
94 ("gridlabd", "GridLAB-D (PNNL GLM)", Distribution, true),
95 (
96 "gridlabd-arras",
97 "GridLAB-D (Arras GLM)",
98 Distribution,
99 true,
100 ),
101 (
104 "cim-dist",
105 "CIM distribution (CIM100 cim17v40, GridAPPS-D profile)",
106 Distribution,
107 true,
108 ),
109 ("pmd-json", "PMD JSON", Distribution, true),
110 ("pmd-math-json", "PMD MATHEMATICAL JSON", Distribution, true),
111 (
115 "ravens-json",
116 "MG-RAVENS JSON (balanced and multiconductor profiles)",
117 Distribution,
118 true,
119 ),
120 (
121 "bmopf-json",
122 "BMOPF JSON (detect version)",
123 Distribution,
124 true,
125 ),
126 ("bmopf-json@0.1.0", "BMOPF 0.1.0", Distribution, false),
127 (
128 "bmopf-json@0.2.0",
129 "BMOPF 0.2.0 proposal",
130 Distribution,
131 false,
132 ),
133 #[cfg(feature = "gridfm")]
134 ("gridfm", "GridFM Parquet", Transmission, true),
135 ];
136 FORMATS
137 .iter()
138 .map(|&(token, label, family, can_read)| GridFormatInfo {
139 format: resolve_format(token).expect("catalog token has format metadata"),
140 label,
141 family,
142 can_read,
143 })
144}
145
146#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
155#[non_exhaustive]
156pub struct FormatInfo {
157 pub token: &'static str,
160 pub extension: Option<&'static str>,
162 pub is_directory: bool,
165 pub can_emit: bool,
167}
168
169const fn info(
170 token: &'static str,
171 extension: Option<&'static str>,
172 is_directory: bool,
173 can_emit: bool,
174) -> FormatInfo {
175 FormatInfo {
176 token,
177 extension,
178 is_directory,
179 can_emit,
180 }
181}
182
183#[must_use]
191pub fn resolve_format(name: &str) -> Option<FormatInfo> {
192 if name.eq_ignore_ascii_case("sincal") {
193 return Some(info("sincal", Some("sinx"), false, false));
194 }
195 if powerio_tx::format::routing::parse_distribution_format(name)
196 == Some(powerio_tx::format::routing::DistributionFormat::SincalMulticonductor)
197 {
198 return Some(info("sincal-multiconductor", Some("sinx"), false, false));
199 }
200 match name {
201 "bmopf-json@0.1.0" => return Some(info("bmopf-json@0.1.0", Some("json"), false, true)),
202 "bmopf-json@0.2.0" => return Some(info("bmopf-json@0.2.0", Some("json"), false, true)),
203 _ => {}
204 }
205
206 if crate::is_geo_layer_token(name) {
207 return Some(info(
208 "geo-json",
209 Some(powerio_tx::geo::GEO_LAYER_EXTENSION),
210 false,
211 true,
212 ));
213 }
214 if crate::is_pwd_display_token(name) {
215 return Some(info("powerworld-pwd", Some("pwd"), false, false));
216 }
217 if let Some(kind) = crate::contingency_file_of_token(name) {
218 return Some(info(kind.token(), Some(kind.extension()), false, true));
219 }
220 if let Some(format) = powerio_tx::format::parse_target_format(name) {
221 let is_cgmes = format == powerio_tx::TargetFormat::Cgmes;
222 return Some(info(
223 format.token(),
224 (!is_cgmes).then_some(format.extension()),
225 is_cgmes,
226 !matches!(format, powerio_tx::TargetFormat::DeepMindOpfDataJson),
227 ));
228 }
229
230 if let Some(format) = powerio_dist::parse_dist_target_format(name) {
231 return Some(match format.name() {
232 "dss" => info("dss", Some("dss"), true, true),
233 "gridlabd" => info("gridlabd", Some("glm"), true, true),
234 "gridlabd-arras" => info("gridlabd-arras", Some("glm"), true, true),
235 "pmd-json" => info("pmd-json", Some("json"), false, true),
236 "pmd-math-json" => info("pmd-math-json", Some("json"), false, true),
237 "bmopf-json" => info("bmopf-json", Some("json"), false, true),
238 "cim-dist" => info("cim-dist", Some("xml"), false, true),
239 _ => return None,
240 });
241 }
242
243 match powerio_tx::format::routing::parse_transmission_format(name) {
244 Some(TransmissionFormat::SincalBalanced) => {
245 Some(info("sincal-balanced", Some("sinx"), false, false))
246 }
247 Some(TransmissionFormat::PypsaCsv) => Some(info("pypsa-csv", None, true, true)),
248 Some(TransmissionFormat::Pwb) => Some(info("pwb", Some("pwb"), false, false)),
249 Some(TransmissionFormat::Gridfm) => Some(info("gridfm", None, true, true)),
250 Some(TransmissionFormat::IeeeCdf) => Some(info("ieee-cdf", Some("txt"), false, false)),
253 _ => None,
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn aliases_resolve_to_one_canonical_descriptor() {
263 assert_eq!(resolve_format("m"), resolve_format("MATPOWER"));
264 assert_eq!(resolve_format("pm").unwrap().token, "powermodels-json");
265 assert_eq!(resolve_format("engineering").unwrap().token, "pmd-json");
266 assert_eq!(resolve_format("xiidm").unwrap().token, "xiidm");
267 assert_eq!(resolve_format("jiidm").unwrap().token, "jiidm");
268 assert_eq!(resolve_format("jiidm").unwrap().extension, Some("jiidm"));
269 assert!(resolve_format("cgmes").unwrap().is_directory);
270 assert_eq!(resolve_format("iidm"), None);
271 assert_eq!(resolve_format("rawx"), None);
272 assert_eq!(resolve_format("psse-rawx").unwrap().token, "psse-rawx");
273 assert_eq!(resolve_format("con"), resolve_format("PSSE_CON"));
274 assert_eq!(resolve_format("subsystem").unwrap().token, "psse-sub");
275 assert_eq!(resolve_format("mon").unwrap().extension, Some("mon"));
276 }
277
278 #[test]
279 fn destination_and_read_only_shapes_are_explicit() {
280 let dss = resolve_format("opendss").unwrap();
281 assert!(dss.is_directory);
282 assert!(dss.can_emit);
283 assert_eq!(dss.extension, Some("dss"));
284
285 let pypsa = resolve_format("pypsa").unwrap();
286 assert!(pypsa.is_directory);
287 assert_eq!(pypsa.extension, None);
288
289 let pwb = resolve_format("pwb").unwrap();
290 assert!(!pwb.is_directory);
291 assert!(!pwb.can_emit);
292 assert_eq!(pwb.extension, Some("pwb"));
293
294 let cdf = resolve_format("cdf").unwrap();
295 assert_eq!(cdf.token, "ieee-cdf");
296 assert!(!cdf.is_directory);
297 assert!(!cdf.can_emit);
298 assert_eq!(cdf.extension, Some("txt"));
299 }
300
301 #[test]
302 fn catalog_lists_every_distribution_token_the_resolver_accepts() {
303 for token in [
306 "dss",
307 "gridlabd",
308 "gridlabd-arras",
309 "pmd-json",
310 "bmopf-json",
311 "cim-dist",
312 ] {
313 let resolved = resolve_format(token).expect(token);
314 let entry = grid_formats()
315 .find(|entry| entry.format.token == token)
316 .unwrap_or_else(|| panic!("{token} resolves but is not in the catalog"));
317 assert_eq!(entry.format, resolved, "{token}");
318 assert_eq!(entry.family, NetworkFamily::Distribution, "{token}");
319 assert!(entry.can_read, "{token}");
320 }
321 assert_eq!(resolve_format("glm").unwrap().token, "gridlabd");
323 assert_eq!(resolve_format("gridlabd-pnnl").unwrap().token, "gridlabd");
324 }
325
326 #[test]
327 fn distribution_cim_names_its_cim_release_and_profile() {
328 let entry = grid_formats()
329 .find(|entry| entry.format.token == "cim-dist")
330 .unwrap();
331 assert_eq!(
332 entry.label,
333 "CIM distribution (CIM100 cim17v40, GridAPPS-D profile)"
334 );
335 }
336
337 #[test]
338 fn catalog_family_capability_includes_both_ravens_profiles() {
339 for entry in grid_formats() {
340 assert!(
341 entry.supports_family(entry.family),
342 "{}",
343 entry.format.token
344 );
345 let other = match entry.family {
346 NetworkFamily::Transmission => NetworkFamily::Distribution,
347 NetworkFamily::Distribution => NetworkFamily::Transmission,
348 };
349 assert_eq!(
350 entry.supports_family(other),
351 entry.format.token == "ravens-json",
352 "{}",
353 entry.format.token
354 );
355 }
356 let balanced: Vec<_> = grid_formats()
357 .filter(|entry| entry.supports_family(NetworkFamily::Transmission))
358 .map(|entry| entry.format.token)
359 .collect();
360 assert!(balanced.contains(&"ravens-json"));
361 assert!(!balanced.contains(&"pmd-math-json"));
362 }
363
364 #[test]
365 fn nonformats_do_not_resolve() {
366 assert_eq!(resolve_format("not-a-format"), None);
367 assert_eq!(resolve_format("json"), None);
368 assert_eq!(resolve_format("pio-json"), None);
369 }
370}