use powerio_tx::format::routing::TransmissionFormat;
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum NetworkFamily {
Transmission,
Distribution,
}
#[derive(Clone, Copy, Debug, serde::Serialize)]
#[non_exhaustive]
pub struct GridFormatInfo {
pub format: FormatInfo,
pub label: &'static str,
pub family: NetworkFamily,
pub can_read: bool,
}
pub fn grid_formats() -> impl Iterator<Item = GridFormatInfo> {
use NetworkFamily::{Distribution, Transmission};
const FORMATS: &[(&str, &str, NetworkFamily, bool)] = &[
("matpower", "MATPOWER", Transmission, true),
("psse", "PSS/E RAW 33", Transmission, true),
("psse34", "PSS/E RAW 34", Transmission, true),
("psse35", "PSS/E RAW 35", Transmission, true),
("psse-rawx", "PSS/E RAWX", Transmission, true),
("powermodels-json", "PowerModels JSON", Transmission, true),
("pandapower-json", "pandapower JSON", Transmission, true),
("pypsa-csv", "PyPSA CSV folder", Transmission, true),
("egret-json", "Egret JSON", Transmission, true),
("powerworld", "PowerWorld AUX", Transmission, true),
("pslf", "PSLF EPC", Transmission, true),
("xiidm", "PowSybl XIIDM", Transmission, true),
("jiidm", "PowSybl JIIDM", Transmission, true),
("cgmes", "CIM CGMES", Transmission, true),
("ucte", "UCTE-DEF", Transmission, true),
("surge-json", "Surge JSON", Transmission, true),
("ieee-cdf", "IEEE CDF (read only)", Transmission, true),
("pwb", "PowerWorld PWB (read only)", Transmission, true),
(
"opfdata-json",
"DeepMind OPFData (read only)",
Transmission,
true,
),
("goc3-json", "GO Challenge 3", Transmission, true),
#[cfg(feature = "sincal")]
("sincal-balanced", "SINCAL balanced", Transmission, true),
#[cfg(feature = "sincal")]
(
"sincal-multiconductor",
"SINCAL multiconductor",
Distribution,
true,
),
("dss", "OpenDSS", Distribution, true),
("gridlabd", "GridLAB-D (PNNL GLM)", Distribution, true),
(
"gridlabd-arras",
"GridLAB-D (Arras GLM)",
Distribution,
true,
),
(
"cim-dist",
"CIM distribution (CIM100 cim17v40, GridAPPS-D profile)",
Distribution,
true,
),
("pmd-json", "PMD JSON", Distribution, true),
(
"bmopf-json",
"BMOPF JSON (detect version)",
Distribution,
true,
),
("bmopf-json@0.1.0", "BMOPF 0.1.0", Distribution, false),
(
"bmopf-json@0.2.0",
"BMOPF 0.2.0 proposal",
Distribution,
false,
),
#[cfg(feature = "gridfm")]
("gridfm", "GridFM Parquet", Transmission, true),
];
FORMATS
.iter()
.map(|&(token, label, family, can_read)| GridFormatInfo {
format: resolve_format(token).expect("catalog token has format metadata"),
label,
family,
can_read,
})
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
#[non_exhaustive]
pub struct FormatInfo {
pub token: &'static str,
pub extension: Option<&'static str>,
pub is_directory: bool,
pub can_emit: bool,
}
const fn info(
token: &'static str,
extension: Option<&'static str>,
is_directory: bool,
can_emit: bool,
) -> FormatInfo {
FormatInfo {
token,
extension,
is_directory,
can_emit,
}
}
#[must_use]
pub fn resolve_format(name: &str) -> Option<FormatInfo> {
if name.eq_ignore_ascii_case("sincal") {
return Some(info("sincal", Some("sinx"), false, false));
}
if powerio_tx::format::routing::parse_distribution_format(name)
== Some(powerio_tx::format::routing::DistributionFormat::SincalMulticonductor)
{
return Some(info("sincal-multiconductor", Some("sinx"), false, false));
}
match name {
"bmopf-json@0.1.0" => return Some(info("bmopf-json@0.1.0", Some("json"), false, true)),
"bmopf-json@0.2.0" => return Some(info("bmopf-json@0.2.0", Some("json"), false, true)),
_ => {}
}
if crate::is_geo_layer_token(name) {
return Some(info(
"geo-json",
Some(powerio_tx::geo::GEO_LAYER_EXTENSION),
false,
true,
));
}
if crate::is_pwd_display_token(name) {
return Some(info("powerworld-pwd", Some("pwd"), false, false));
}
if let Some(kind) = crate::contingency_file_of_token(name) {
return Some(info(kind.token(), Some(kind.extension()), false, true));
}
if let Some(format) = powerio_tx::format::parse_target_format(name) {
let is_cgmes = format == powerio_tx::TargetFormat::Cgmes;
return Some(info(
format.token(),
(!is_cgmes).then_some(format.extension()),
is_cgmes,
!matches!(format, powerio_tx::TargetFormat::DeepMindOpfDataJson),
));
}
if let Some(format) = powerio_dist::parse_dist_target_format(name) {
return Some(match format.name() {
"dss" => info("dss", Some("dss"), true, true),
"gridlabd" => info("gridlabd", Some("glm"), true, true),
"gridlabd-arras" => info("gridlabd-arras", Some("glm"), true, true),
"pmd-json" => info("pmd-json", Some("json"), false, true),
"bmopf-json" => info("bmopf-json", Some("json"), false, true),
"cim-dist" => info("cim-dist", Some("xml"), false, true),
_ => return None,
});
}
match powerio_tx::format::routing::parse_transmission_format(name) {
Some(TransmissionFormat::SincalBalanced) => {
Some(info("sincal-balanced", Some("sinx"), false, false))
}
Some(TransmissionFormat::PypsaCsv) => Some(info("pypsa-csv", None, true, true)),
Some(TransmissionFormat::Pwb) => Some(info("pwb", Some("pwb"), false, false)),
Some(TransmissionFormat::Gridfm) => Some(info("gridfm", None, true, true)),
Some(TransmissionFormat::IeeeCdf) => Some(info("ieee-cdf", Some("txt"), false, false)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn aliases_resolve_to_one_canonical_descriptor() {
assert_eq!(resolve_format("m"), resolve_format("MATPOWER"));
assert_eq!(resolve_format("pm").unwrap().token, "powermodels-json");
assert_eq!(resolve_format("engineering").unwrap().token, "pmd-json");
assert_eq!(resolve_format("xiidm").unwrap().token, "xiidm");
assert_eq!(resolve_format("jiidm").unwrap().token, "jiidm");
assert_eq!(resolve_format("jiidm").unwrap().extension, Some("jiidm"));
assert!(resolve_format("cgmes").unwrap().is_directory);
assert_eq!(resolve_format("iidm"), None);
assert_eq!(resolve_format("rawx"), None);
assert_eq!(resolve_format("psse-rawx").unwrap().token, "psse-rawx");
assert_eq!(resolve_format("con"), resolve_format("PSSE_CON"));
assert_eq!(resolve_format("subsystem").unwrap().token, "psse-sub");
assert_eq!(resolve_format("mon").unwrap().extension, Some("mon"));
}
#[test]
fn destination_and_read_only_shapes_are_explicit() {
let dss = resolve_format("opendss").unwrap();
assert!(dss.is_directory);
assert!(dss.can_emit);
assert_eq!(dss.extension, Some("dss"));
let pypsa = resolve_format("pypsa").unwrap();
assert!(pypsa.is_directory);
assert_eq!(pypsa.extension, None);
let pwb = resolve_format("pwb").unwrap();
assert!(!pwb.is_directory);
assert!(!pwb.can_emit);
assert_eq!(pwb.extension, Some("pwb"));
let cdf = resolve_format("cdf").unwrap();
assert_eq!(cdf.token, "ieee-cdf");
assert!(!cdf.is_directory);
assert!(!cdf.can_emit);
assert_eq!(cdf.extension, Some("txt"));
}
#[test]
fn catalog_lists_every_distribution_token_the_resolver_accepts() {
for token in [
"dss",
"gridlabd",
"gridlabd-arras",
"pmd-json",
"bmopf-json",
"cim-dist",
] {
let resolved = resolve_format(token).expect(token);
let entry = grid_formats()
.find(|entry| entry.format.token == token)
.unwrap_or_else(|| panic!("{token} resolves but is not in the catalog"));
assert_eq!(entry.format, resolved, "{token}");
assert_eq!(entry.family, NetworkFamily::Distribution, "{token}");
assert!(entry.can_read, "{token}");
}
assert_eq!(resolve_format("glm").unwrap().token, "gridlabd");
assert_eq!(resolve_format("gridlabd-pnnl").unwrap().token, "gridlabd");
}
#[test]
fn distribution_cim_names_its_cim_release_and_profile() {
let entry = grid_formats()
.find(|entry| entry.format.token == "cim-dist")
.unwrap();
assert_eq!(
entry.label,
"CIM distribution (CIM100 cim17v40, GridAPPS-D profile)"
);
}
#[test]
fn nonformats_do_not_resolve() {
assert_eq!(resolve_format("not-a-format"), None);
assert_eq!(resolve_format("json"), None);
assert_eq!(resolve_format("pio-json"), None);
}
}