use surge_network::Network;
use surge_network::network::boundary::{
BoundaryData, BoundaryPoint, EquivalentBranchData, EquivalentNetworkData, EquivalentShuntData,
ModelAuthoritySet,
};
use super::indices::CgmesIndices;
use super::types::ObjMap;
pub(crate) fn build_boundary_data(objects: &ObjMap, idx: &CgmesIndices, network: &mut Network) {
let mut data = BoundaryData::default();
for (id, obj) in objects.iter().filter(|(_, o)| o.class == "BoundaryPoint") {
let cn_mrid = obj.get_ref("ConnectivityNode").map(|s| s.to_string());
let bus = cn_mrid.as_deref().and_then(|cn_id| {
let tn = objects
.get(cn_id)
.and_then(|cn_obj| cn_obj.get_ref("TopologicalNode"))
.or_else(|| {
idx.tn_ids.iter().find_map(|tn_id| {
let tn_obj = objects.get(tn_id.as_str())?;
if tn_obj.get_ref("ConnectivityNodes") == Some(cn_id)
|| tn_obj.get_ref("ConnectivityNodeContainer") == Some(cn_id)
{
Some(tn_id.as_str())
} else {
None
}
})
});
tn.and_then(|tn_id| idx.tn_bus(tn_id))
});
let parse_bool = |key: &str| -> bool {
obj.get_text(key)
.map(|s| s == "true" || s == "1")
.unwrap_or(false)
};
data.boundary_points.push(BoundaryPoint {
mrid: id.clone(),
connectivity_node_mrid: cn_mrid,
from_end_iso_code: obj.get_text("fromEndIsoCode").map(|s| s.to_string()),
to_end_iso_code: obj.get_text("toEndIsoCode").map(|s| s.to_string()),
from_end_name: obj.get_text("fromEndName").map(|s| s.to_string()),
to_end_name: obj.get_text("toEndName").map(|s| s.to_string()),
from_end_name_tso: obj.get_text("fromEndNameTso").map(|s| s.to_string()),
to_end_name_tso: obj.get_text("toEndNameTso").map(|s| s.to_string()),
is_direct_current: parse_bool("isDirectCurrent"),
is_excluded_from_area_interchange: parse_bool("isExcludedFromAreaInterchange"),
bus,
});
}
let mas_ids: Vec<String> = objects
.iter()
.filter(|(_, o)| o.class == "ModelAuthoritySet")
.map(|(id, _)| id.clone())
.collect();
for mas_id in &mas_ids {
let obj = &objects[mas_id];
let members: Vec<String> = objects
.iter()
.filter(|(_, o)| {
o.get_ref("ModelAuthoritySet")
.map(|r| r == mas_id)
.unwrap_or(false)
})
.map(|(eq_id, _)| eq_id.clone())
.collect();
data.model_authority_sets.push(ModelAuthoritySet {
mrid: mas_id.clone(),
name: obj.get_text("name").unwrap_or("unknown").to_string(),
description: obj.get_text("description").map(|s| s.to_string()),
members,
});
}
for (id, obj) in objects
.iter()
.filter(|(_, o)| o.class == "EquivalentNetwork")
{
data.equivalent_networks.push(EquivalentNetworkData {
mrid: id.clone(),
name: obj.get_text("name").unwrap_or("unknown").to_string(),
description: obj.get_text("description").map(|s| s.to_string()),
region_mrid: obj.get_ref("Region").map(|s| s.to_string()),
});
}
for (id, obj) in objects
.iter()
.filter(|(_, o)| o.class == "EquivalentBranch")
{
let terminals = idx.terminals(id);
let (from_bus, to_bus) = match terminals.len() {
0 => (None, None),
1 => {
let b = idx
.terminal_tn(objects, &terminals[0])
.and_then(|tn| idx.tn_bus(tn));
(b, None)
}
_ => {
let b1 = idx
.terminal_tn(objects, &terminals[0])
.and_then(|tn| idx.tn_bus(tn));
let b2 = idx
.terminal_tn(objects, &terminals[1])
.and_then(|tn| idx.tn_bus(tn));
(b1, b2)
}
};
data.equivalent_branches.push(EquivalentBranchData {
mrid: id.clone(),
network_mrid: obj.get_ref("EquivalentNetwork").map(|s| s.to_string()),
r_ohm: obj.parse_f64("r").unwrap_or(0.0),
x_ohm: obj.parse_f64("x").unwrap_or(0.0),
r0_ohm: obj.parse_f64("r0"),
x0_ohm: obj.parse_f64("x0"),
r2_ohm: obj
.parse_f64("r21")
.or_else(|| obj.parse_f64("negativeR21")),
x2_ohm: obj
.parse_f64("x21")
.or_else(|| obj.parse_f64("negativeX21")),
from_bus,
to_bus,
});
}
for (id, obj) in objects.iter().filter(|(_, o)| o.class == "EquivalentShunt") {
let terminals = idx.terminals(id);
let bus = terminals
.first()
.and_then(|tid| idx.terminal_tn(objects, tid))
.and_then(|tn| idx.tn_bus(tn));
data.equivalent_shunts.push(EquivalentShuntData {
mrid: id.clone(),
network_mrid: obj.get_ref("EquivalentNetwork").map(|s| s.to_string()),
g_s: obj.parse_f64("g").unwrap_or(0.0),
b_s: obj.parse_f64("b").unwrap_or(0.0),
bus,
});
}
if !data.is_empty() {
tracing::info!(
boundary_points = data.boundary_points.len(),
model_authority_sets = data.model_authority_sets.len(),
equivalent_networks = data.equivalent_networks.len(),
equivalent_branches = data.equivalent_branches.len(),
equivalent_shunts = data.equivalent_shunts.len(),
"CGMES boundary data parsed"
);
network.cim.boundary_data = data;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cgmes::types::{CimObj, CimVal};
use std::collections::HashMap;
fn make_obj(class: &str, attrs: &[(&str, CimVal)]) -> CimObj {
let mut obj = CimObj::new(class);
for (k, v) in attrs {
obj.attrs.insert(k.to_string(), v.clone());
}
obj
}
fn text(s: &str) -> CimVal {
CimVal::Text(s.to_string())
}
fn refv(s: &str) -> CimVal {
CimVal::Ref(s.to_string())
}
#[test]
fn test_boundary_point_parsing() {
let mut objects: ObjMap = HashMap::new();
objects.insert(
"bp1".to_string(),
make_obj(
"BoundaryPoint",
&[
("fromEndIsoCode", text("DE")),
("toEndIsoCode", text("FR")),
("fromEndName", text("TenneT")),
("toEndName", text("RTE")),
("isDirectCurrent", text("false")),
("isExcludedFromAreaInterchange", text("true")),
("ConnectivityNode", refv("cn1")),
],
),
);
let idx = CgmesIndices::build(&objects);
let mut network = Network::default();
build_boundary_data(&objects, &idx, &mut network);
assert_eq!(network.cim.boundary_data.boundary_points.len(), 1);
let bp = &network.cim.boundary_data.boundary_points[0];
assert_eq!(bp.mrid, "bp1");
assert_eq!(bp.from_end_iso_code.as_deref(), Some("DE"));
assert_eq!(bp.to_end_iso_code.as_deref(), Some("FR"));
assert_eq!(bp.from_end_name.as_deref(), Some("TenneT"));
assert_eq!(bp.to_end_name.as_deref(), Some("RTE"));
assert!(!bp.is_direct_current);
assert!(bp.is_excluded_from_area_interchange);
assert_eq!(bp.connectivity_node_mrid.as_deref(), Some("cn1"));
assert!(bp.bus.is_none());
}
#[test]
fn test_model_authority_set_with_members() {
let mut objects: ObjMap = HashMap::new();
objects.insert(
"mas1".to_string(),
make_obj(
"ModelAuthoritySet",
&[
("name", text("TenneT_TSO")),
("description", text("TenneT TSO BV")),
],
),
);
objects.insert(
"gen1".to_string(),
make_obj("SynchronousMachine", &[("ModelAuthoritySet", refv("mas1"))]),
);
objects.insert(
"line1".to_string(),
make_obj("ACLineSegment", &[("ModelAuthoritySet", refv("mas1"))]),
);
objects.insert(
"gen2".to_string(),
make_obj(
"SynchronousMachine",
&[("ModelAuthoritySet", refv("mas_other"))],
),
);
let idx = CgmesIndices::build(&objects);
let mut network = Network::default();
build_boundary_data(&objects, &idx, &mut network);
assert_eq!(network.cim.boundary_data.model_authority_sets.len(), 1);
let mas = &network.cim.boundary_data.model_authority_sets[0];
assert_eq!(mas.name, "TenneT_TSO");
assert_eq!(mas.description.as_deref(), Some("TenneT TSO BV"));
assert_eq!(mas.members.len(), 2);
assert!(mas.members.contains(&"gen1".to_string()));
assert!(mas.members.contains(&"line1".to_string()));
}
#[test]
fn test_equivalent_network_parsing() {
let mut objects: ObjMap = HashMap::new();
objects.insert(
"eqnet1".to_string(),
make_obj(
"EquivalentNetwork",
&[
("name", text("External_FR")),
("description", text("French external equivalent")),
("Region", refv("region_fr")),
],
),
);
let idx = CgmesIndices::build(&objects);
let mut network = Network::default();
build_boundary_data(&objects, &idx, &mut network);
assert_eq!(network.cim.boundary_data.equivalent_networks.len(), 1);
let en = &network.cim.boundary_data.equivalent_networks[0];
assert_eq!(en.mrid, "eqnet1");
assert_eq!(en.name, "External_FR");
assert_eq!(
en.description.as_deref(),
Some("French external equivalent")
);
assert_eq!(en.region_mrid.as_deref(), Some("region_fr"));
}
#[test]
fn test_equivalent_branch_parsing() {
let mut objects: ObjMap = HashMap::new();
objects.insert(
"eqbr1".to_string(),
make_obj(
"EquivalentBranch",
&[
("r", text("1.5")),
("x", text("10.0")),
("r0", text("3.0")),
("x0", text("20.0")),
("r21", text("1.5")),
("x21", text("10.0")),
("EquivalentNetwork", refv("eqnet1")),
],
),
);
let idx = CgmesIndices::build(&objects);
let mut network = Network::default();
build_boundary_data(&objects, &idx, &mut network);
assert_eq!(network.cim.boundary_data.equivalent_branches.len(), 1);
let eb = &network.cim.boundary_data.equivalent_branches[0];
assert_eq!(eb.mrid, "eqbr1");
assert_eq!(eb.r_ohm, 1.5);
assert_eq!(eb.x_ohm, 10.0);
assert_eq!(eb.r0_ohm, Some(3.0));
assert_eq!(eb.x0_ohm, Some(20.0));
assert_eq!(eb.r2_ohm, Some(1.5));
assert_eq!(eb.x2_ohm, Some(10.0));
assert_eq!(eb.network_mrid.as_deref(), Some("eqnet1"));
assert!(eb.from_bus.is_none());
assert!(eb.to_bus.is_none());
}
#[test]
fn test_equivalent_shunt_parsing() {
let mut objects: ObjMap = HashMap::new();
objects.insert(
"eqsh1".to_string(),
make_obj(
"EquivalentShunt",
&[
("g", text("0.001")),
("b", text("0.05")),
("EquivalentNetwork", refv("eqnet1")),
],
),
);
let idx = CgmesIndices::build(&objects);
let mut network = Network::default();
build_boundary_data(&objects, &idx, &mut network);
assert_eq!(network.cim.boundary_data.equivalent_shunts.len(), 1);
let es = &network.cim.boundary_data.equivalent_shunts[0];
assert_eq!(es.mrid, "eqsh1");
assert_eq!(es.g_s, 0.001);
assert_eq!(es.b_s, 0.05);
assert_eq!(es.network_mrid.as_deref(), Some("eqnet1"));
assert!(es.bus.is_none());
}
#[test]
fn test_empty_boundary_data_not_set() {
let objects: ObjMap = HashMap::new();
let idx = CgmesIndices::build(&objects);
let mut network = Network::default();
build_boundary_data(&objects, &idx, &mut network);
assert!(network.cim.boundary_data.is_empty());
}
}