use std::collections::HashMap;
use std::path::Path;
use num_complex::Complex;
pub use cimdecoder::CimDataset;
use cimstructs::{
ACDCConverterDCTerminal, ACLineSegment, BaseVoltage, CsConverter, DCBreaker, DCDisconnector,
DCGround, DCLineSegment, DCSeriesDevice, DCShunt, DCSwitch, DCTerminal, EnergyConsumer,
EquivalentInjection, LinearShuntCompensator, NonlinearShuntCompensator,
NonlinearShuntCompensatorPoint, PhaseTapChangerAsymmetrical, PhaseTapChangerNonLinear,
PhaseTapChangerSymmetrical, PowerElectronicsConnection, PowerTransformerEnd, RatioTapChanger,
RegulatingControl, StaticVarCompensator, SynchronousMachine, Terminal, TopologicalIsland,
TopologicalNode, VsConverter,
};
use crate::dc::{injected_currents, solve_dc_network, DcBus, DcBusRole, DcLine, DcSolveStatus};
use crate::network::ShuntAdm;
use crate::topology::UnionFind;
use crate::types::{Bus, BusType, Line, Transformer};
pub fn load_profiles(paths: &[&Path]) -> Result<CimDataset, Box<dyn std::error::Error>> {
CimDataset::decode_files(paths)
}
#[derive(Debug)]
pub enum CgmesError {
UnresolvedReference { from_type: &'static str, from_mrid: String, field: &'static str },
MissingField { type_name: &'static str, mrid: String, field: &'static str },
NoTopologicalNodes,
NoAngleReference,
UnsupportedTransformer { mrid: String, reason: String },
UnsupportedConverterControl { mrid: String, mode: String },
DcNetworkDidNotConverge,
}
impl std::fmt::Display for CgmesError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CgmesError::UnresolvedReference { from_type, from_mrid, field } => {
write!(f, "{from_type} {from_mrid}: {field} does not resolve to a decoded element")
}
CgmesError::MissingField { type_name, mrid, field } => {
write!(f, "{type_name} {mrid}: required field {field} is missing")
}
CgmesError::NoTopologicalNodes => {
write!(f, "dataset has no TopologicalNode entries (is the TP profile loaded?)")
}
CgmesError::NoAngleReference => {
write!(f, "no TopologicalIsland.AngleRefTopologicalNode found (is the SV profile loaded?)")
}
CgmesError::UnsupportedTransformer { mrid, reason } => {
write!(f, "PowerTransformer {mrid}: {reason}")
}
CgmesError::UnsupportedConverterControl { mrid, mode } => {
write!(f, "ACDCConverter {mrid}: unsupported pPccControl mode {mode:?}")
}
CgmesError::DcNetworkDidNotConverge => {
write!(f, "DC network solve did not converge")
}
}
}
}
impl std::error::Error for CgmesError {}
fn get<'a, T: 'static>(ds: &'a CimDataset, mrid: &str) -> Option<&'a T> {
ds.entries.get(mrid)?.element.as_any().downcast_ref::<T>()
}
fn require<'a, T: 'static>(
ds: &'a CimDataset, mrid: &str, from_type: &'static str, from_mrid: &str, field: &'static str,
) -> Result<&'a T, CgmesError> {
get(ds, mrid).ok_or_else(|| CgmesError::UnresolvedReference {
from_type, from_mrid: from_mrid.to_string(), field,
})
}
fn missing(type_name: &'static str, mrid: &str, field: &'static str) -> CgmesError {
CgmesError::MissingField { type_name, mrid: mrid.to_string(), field }
}
fn by_type<'a>(ds: &'a CimDataset, type_name: &str) -> &'a [String] {
ds.by_type.get(type_name).map(|v| v.as_slice()).unwrap_or(&[])
}
fn unit_multiplier(uri: Option<&str>) -> f64 {
match uri.and_then(|u| u.rsplit('.').next()) {
Some("Y") => 1e24, Some("Z") => 1e21, Some("E") => 1e18, Some("P") => 1e15,
Some("T") => 1e12, Some("G") => 1e9, Some("M") => 1e6, Some("k") => 1e3,
Some("h") => 1e2, Some("da") => 1e1, Some("d") => 1e-1, Some("c") => 1e-2,
Some("m") => 1e-3, Some("micro") => 1e-6, Some("n") => 1e-9, Some("p") => 1e-12,
_ => 1.0,
}
}
struct TerminalIndex {
by_equipment: HashMap<String, Vec<String>>,
bus_of: HashMap<String, usize>,
connected_of: HashMap<String, bool>,
}
impl TerminalIndex {
fn build(ds: &CimDataset, idx_of: &HashMap<String, usize>, buses: &mut Vec<Bus>) -> Result<Self, CgmesError> {
let mut cn_to_tn: HashMap<String, String> = HashMap::new();
for cn_mrid in by_type(ds, "ConnectivityNode") {
let cn: &cimstructs::ConnectivityNode = require(ds, cn_mrid, "ConnectivityNode", cn_mrid, "(self)")?;
if let Some(tn) = &cn.topological_node {
cn_to_tn.insert(cn_mrid.clone(), tn.mrid.clone());
}
}
let mut raw: HashMap<String, Vec<(i64, String)>> = HashMap::new();
let mut bus_of = HashMap::new();
let mut connected_of = HashMap::new();
let mut orphans: Vec<(String, String, Option<String>)> = Vec::new(); for t_mrid in by_type(ds, "Terminal") {
let t: &Terminal = require(ds, t_mrid, "Terminal", t_mrid, "(self)")?;
connected_of.insert(t_mrid.clone(), t.base.connected.unwrap_or(true));
if let Some(ce) = &t.conducting_equipment {
let seq = t.base.sequence_number.unwrap_or(1);
raw.entry(ce.mrid.clone()).or_default().push((seq, t_mrid.clone()));
}
let tn_mrid = t.topological_node.as_ref().map(|tn| tn.mrid.clone()).or_else(|| {
t.connectivity_node.as_ref().and_then(|cn| cn_to_tn.get(&cn.mrid).cloned())
});
match (tn_mrid, &t.connectivity_node) {
(Some(tn_mrid), _) => {
if let Some(&idx) = idx_of.get(&tn_mrid) {
bus_of.insert(t_mrid.clone(), idx);
}
}
(None, Some(cn)) => {
orphans.push((t_mrid.clone(), cn.mrid.clone(), t.conducting_equipment.as_ref().map(|ce| ce.mrid.clone())));
}
(None, None) => {}
}
}
let mut orphans_by_cn: HashMap<String, Vec<(String, Option<String>)>> = HashMap::new();
for (t_mrid, cn_mrid, ce_mrid) in orphans {
orphans_by_cn.entry(cn_mrid).or_default().push((t_mrid, ce_mrid));
}
for (cn_mrid, terms_here) in &orphans_by_cn {
let u_rated = terms_here
.iter()
.find_map(|(_, ce_mrid)| {
let ei = get::<EquivalentInjection>(ds, ce_mrid.as_deref()?)?;
let bv_ref = ei.base.base.base_voltage.as_ref()?;
get::<BaseVoltage>(ds, &bv_ref.mrid)?.nominal_voltage
})
.ok_or_else(|| missing("ConnectivityNode", cn_mrid, "(no EquivalentInjection with a resolvable BaseVoltage found)"))?
* 1e3;
let idx = buses.len();
buses.push(Bus {
idx, bus_type: BusType::PQ, voltage_mag: 1.0, voltage_ang: 0.0,
p_spec: 0.0, q_spec: 0.0, q_min: -f64::INFINITY, q_max: f64::INFINITY,
u_rated, zip_terms: Vec::new(),
});
for (t_mrid, _) in terms_here {
bus_of.insert(t_mrid.clone(), idx);
}
}
let by_equipment = raw
.into_iter()
.map(|(eq, mut v)| {
v.sort_by_key(|(seq, _)| *seq);
(eq, v.into_iter().map(|(_, m)| m).collect())
})
.collect();
Ok(TerminalIndex { by_equipment, bus_of, connected_of })
}
fn bus(&self, equipment_mrid: &str, which: usize) -> Option<usize> {
self.by_equipment.get(equipment_mrid)?.get(which).and_then(|t| self.bus_of.get(t)).copied()
}
fn bus_via_terminal_mrid(&self, terminal_mrid: &str) -> Option<usize> {
self.bus_of.get(terminal_mrid).copied()
}
fn connected(&self, equipment_mrid: &str, which: usize) -> bool {
self.by_equipment
.get(equipment_mrid)
.and_then(|ts| ts.get(which))
.and_then(|t| self.connected_of.get(t))
.copied()
.unwrap_or(true)
}
fn connected_via_terminal_mrid(&self, terminal_mrid: &str) -> bool {
self.connected_of.get(terminal_mrid).copied().unwrap_or(true)
}
}
fn merge_closed_switches(ds: &CimDataset, buses: Vec<Bus>, terms: &mut TerminalIndex) -> Result<(Vec<Bus>, Vec<usize>), CgmesError> {
let mut uf = UnionFind::new(buses.len());
fn union_pair(uf: &mut UnionFind, terms: &TerminalIndex, mrid: &str, in_service: bool, open: bool) {
if !in_service || open {
return;
}
if let (Some(a), Some(b)) = (terms.bus(mrid, 0), terms.bus(mrid, 1)) {
uf.union(a, b);
}
}
for mrid in by_type(ds, "Switch") {
let sw: &cimstructs::Switch = require(ds, mrid, "Switch", mrid, "(self)")?;
union_pair(&mut uf, terms, mrid, equipment_in_service(sw.base.base.in_service, sw.base.base.normally_in_service), sw.open.unwrap_or(false));
}
for mrid in by_type(ds, "Breaker") {
let br: &cimstructs::Breaker = require(ds, mrid, "Breaker", mrid, "(self)")?;
union_pair(&mut uf, terms, mrid, equipment_in_service(br.base.base.base.base.in_service, br.base.base.base.base.normally_in_service), br.base.base.open.unwrap_or(false));
}
for mrid in by_type(ds, "LoadBreakSwitch") {
let lbs: &cimstructs::LoadBreakSwitch = require(ds, mrid, "LoadBreakSwitch", mrid, "(self)")?;
union_pair(&mut uf, terms, mrid, equipment_in_service(lbs.base.base.base.base.in_service, lbs.base.base.base.base.normally_in_service), lbs.base.base.open.unwrap_or(false));
}
for mrid in by_type(ds, "DisconnectingCircuitBreaker") {
let dcb: &cimstructs::DisconnectingCircuitBreaker = require(ds, mrid, "DisconnectingCircuitBreaker", mrid, "(self)")?;
union_pair(&mut uf, terms, mrid, equipment_in_service(dcb.base.base.base.base.base.in_service, dcb.base.base.base.base.base.normally_in_service), dcb.base.base.base.open.unwrap_or(false));
}
for mrid in by_type(ds, "Disconnector") {
let d: &cimstructs::Disconnector = require(ds, mrid, "Disconnector", mrid, "(self)")?;
union_pair(&mut uf, terms, mrid, equipment_in_service(d.base.base.base.in_service, d.base.base.base.normally_in_service), d.base.open.unwrap_or(false));
}
for mrid in by_type(ds, "GroundDisconnector") {
let g: &cimstructs::GroundDisconnector = require(ds, mrid, "GroundDisconnector", mrid, "(self)")?;
union_pair(&mut uf, terms, mrid, equipment_in_service(g.base.base.base.in_service, g.base.base.base.normally_in_service), g.base.open.unwrap_or(false));
}
for mrid in by_type(ds, "Jumper") {
let j: &cimstructs::Jumper = require(ds, mrid, "Jumper", mrid, "(self)")?;
union_pair(&mut uf, terms, mrid, equipment_in_service(j.base.base.base.in_service, j.base.base.base.normally_in_service), j.base.open.unwrap_or(false));
}
for mrid in by_type(ds, "Cut") {
let c: &cimstructs::Cut = require(ds, mrid, "Cut", mrid, "(self)")?;
union_pair(&mut uf, terms, mrid, equipment_in_service(c.base.base.base.in_service, c.base.base.base.normally_in_service), c.base.open.unwrap_or(false));
}
for mrid in by_type(ds, "Fuse") {
let f: &cimstructs::Fuse = require(ds, mrid, "Fuse", mrid, "(self)")?;
union_pair(&mut uf, terms, mrid, equipment_in_service(f.base.base.base.in_service, f.base.base.base.normally_in_service), f.base.open.unwrap_or(false));
}
for mrid in by_type(ds, "Junction") {
let j: &cimstructs::Junction = require(ds, mrid, "Junction", mrid, "(self)")?;
union_pair(&mut uf, terms, mrid, equipment_in_service(j.base.base.base.in_service, j.base.base.base.normally_in_service), false);
}
let (remap, n_merged) = crate::topology::merge_groups(buses.len(), &mut uf);
let mut merged: Vec<Bus> = Vec::with_capacity(n_merged);
for i in 0..buses.len() {
if remap[i] == merged.len() {
let root = uf.find(i);
let mut b = buses[root].clone();
b.idx = remap[i];
merged.push(b);
}
}
for v in terms.bus_of.values_mut() {
*v = remap[*v];
}
Ok((merged, remap))
}
struct TapEffect {
tap: Complex<f64>,
x_override: Option<f64>,
}
struct TapChangerIndex {
ratio: HashMap<String, String>,
phase_asym: HashMap<String, String>,
phase_sym: HashMap<String, String>,
phase_linear: HashMap<String, String>,
phase_tabular: HashMap<String, (String, String)>,
}
impl TapChangerIndex {
fn build(ds: &CimDataset) -> Self {
let mut ratio = HashMap::new();
for mrid in by_type(ds, "RatioTapChanger") {
if let Some(rtc) = get::<RatioTapChanger>(ds, mrid) {
if let Some(end) = &rtc.transformer_end {
ratio.insert(end.mrid.clone(), mrid.clone());
}
}
}
let mut phase_asym = HashMap::new();
for mrid in by_type(ds, "PhaseTapChangerAsymmetrical") {
if let Some(ptc) = get::<PhaseTapChangerAsymmetrical>(ds, mrid) {
if let Some(end) = &ptc.base.base.transformer_end {
phase_asym.insert(end.mrid.clone(), mrid.clone());
}
}
}
let mut phase_sym = HashMap::new();
for mrid in by_type(ds, "PhaseTapChangerSymmetrical") {
if let Some(ptc) = get::<PhaseTapChangerSymmetrical>(ds, mrid) {
if let Some(end) = &ptc.base.base.transformer_end {
phase_sym.insert(end.mrid.clone(), mrid.clone());
}
}
}
let mut phase_linear = HashMap::new();
for mrid in by_type(ds, "PhaseTapChangerLinear") {
if let Some(ptc) = get::<cimstructs::PhaseTapChangerLinear>(ds, mrid) {
if let Some(end) = &ptc.base.transformer_end {
phase_linear.insert(end.mrid.clone(), mrid.clone());
}
}
}
let mut phase_tabular = HashMap::new();
for mrid in by_type(ds, "PhaseTapChangerTabular") {
if let Some(ptc) = get::<cimstructs::PhaseTapChangerTabular>(ds, mrid) {
if let (Some(end), Some(table)) = (&ptc.base.transformer_end, &ptc.phase_tap_changer_table) {
phase_tabular.insert(end.mrid.clone(), (mrid.clone(), table.mrid.clone()));
}
}
}
TapChangerIndex { ratio, phase_asym, phase_sym, phase_linear, phase_tabular }
}
fn effect_for_end(&self, ds: &CimDataset, end_mrid: &str, xtx: f64) -> Result<Option<TapEffect>, CgmesError> {
if let Some(mrid) = self.ratio.get(end_mrid) {
let rtc: &RatioTapChanger = require(ds, mrid, "RatioTapChanger", mrid, "(self)")?;
let step = rtc.base.step.ok_or_else(|| missing("RatioTapChanger", mrid, "step"))?;
if let Some(table_ref) = &rtc.ratio_tap_changer_table {
if let Some(effect) = ratio_tap_table(ds, &table_ref.mrid, step.round() as i64, xtx) {
return Ok(Some(effect));
}
}
let neutral = rtc.base.neutral_step.unwrap_or(0) as f64;
let inc = rtc.step_voltage_increment.unwrap_or(0.0);
let ratio = 1.0 + (step - neutral) * inc / 100.0;
return Ok(Some(TapEffect { tap: Complex::new(ratio, 0.0), x_override: None }));
}
if let Some(mrid) = self.phase_asym.get(end_mrid) {
let ptc: &PhaseTapChangerAsymmetrical = require(ds, mrid, "PhaseTapChangerAsymmetrical", mrid, "(self)")?;
let theta_deg = ptc.winding_connection_angle.ok_or_else(|| {
missing("PhaseTapChangerAsymmetrical", mrid, "windingConnectionAngle")
})?;
return Ok(Some(phase_tap_asymmetrical(&ptc.base, mrid, theta_deg, xtx)?));
}
if let Some(mrid) = self.phase_sym.get(end_mrid) {
let ptc: &PhaseTapChangerSymmetrical = require(ds, mrid, "PhaseTapChangerSymmetrical", mrid, "(self)")?;
return Ok(Some(phase_tap_symmetrical(&ptc.base, mrid, xtx)?));
}
if let Some(mrid) = self.phase_linear.get(end_mrid) {
let ptc: &cimstructs::PhaseTapChangerLinear = require(ds, mrid, "PhaseTapChangerLinear", mrid, "(self)")?;
return Ok(Some(phase_tap_linear(ptc, mrid, xtx)?));
}
if let Some((ptc_mrid, table_mrid)) = self.phase_tabular.get(end_mrid) {
let ptc: &cimstructs::PhaseTapChangerTabular = require(ds, ptc_mrid, "PhaseTapChangerTabular", ptc_mrid, "(self)")?;
let step = ptc.base.base.step.ok_or_else(|| missing("PhaseTapChangerTabular", ptc_mrid, "step"))?;
return Ok(Some(phase_tap_tabular(ds, ptc_mrid, table_mrid, step.round() as i64, xtx)?));
}
Ok(None)
}
}
fn phase_tap_tabular(
ds: &CimDataset, ptc_mrid: &str, table_mrid: &str, step: i64, xtx: f64,
) -> Result<TapEffect, CgmesError> {
for pt_mrid in by_type(ds, "PhaseTapChangerTablePoint") {
let pt: &cimstructs::PhaseTapChangerTablePoint =
require(ds, pt_mrid, "PhaseTapChangerTablePoint", pt_mrid, "(self)")?;
let Some(owner) = &pt.phase_tap_changer_table else { continue };
if owner.mrid != *table_mrid || pt.base.step != Some(step) {
continue;
}
let ratio = pt.base.ratio.unwrap_or(1.0);
let angle_rad = pt.angle.unwrap_or(0.0).to_radians();
let tap = Complex::from_polar(ratio, angle_rad);
let x_pct = pt.base.x.unwrap_or(0.0);
return Ok(TapEffect { tap, x_override: Some(xtx * (1.0 + x_pct / 100.0)) });
}
Err(CgmesError::UnresolvedReference {
from_type: "PhaseTapChangerTabular",
from_mrid: ptc_mrid.to_string(),
field: "(no PhaseTapChangerTablePoint matching the current step)",
})
}
fn ratio_tap_table(ds: &CimDataset, table_mrid: &str, step: i64, xtx: f64) -> Option<TapEffect> {
for pt_mrid in by_type(ds, "RatioTapChangerTablePoint") {
let pt: &cimstructs::RatioTapChangerTablePoint = get(ds, pt_mrid)?;
let Some(owner) = &pt.ratio_tap_changer_table else { continue };
if owner.mrid != *table_mrid || pt.base.step != Some(step) {
continue;
}
let ratio = pt.base.ratio.unwrap_or(1.0);
let x_pct = pt.base.x.unwrap_or(0.0);
return Some(TapEffect { tap: Complex::new(ratio, 0.0), x_override: Some(xtx * (1.0 + x_pct / 100.0)) });
}
None
}
fn x_min_max(x_min: Option<f64>, x_max: Option<f64>, xtx: f64) -> Option<(f64, f64)> {
let x_min_raw = x_min.unwrap_or(0.0);
let x_min = if x_min_raw <= 0.0 { xtx } else { x_min_raw };
let x_max = x_max?;
if !(x_min.is_finite() && x_max.is_finite()) || x_min < 0.0 || x_max <= 0.0 || x_min > x_max {
return None;
}
Some((x_min, x_max))
}
fn phase_tap_asymmetrical(
base: &PhaseTapChangerNonLinear, mrid: &str, winding_connection_angle_deg: f64, xtx: f64,
) -> Result<TapEffect, CgmesError> {
let tc = &base.base.base;
let step = tc.step.ok_or_else(|| missing("PhaseTapChanger", mrid, "step"))?;
let neutral = tc.neutral_step.unwrap_or(0) as f64;
let low = tc.low_step.unwrap_or(0);
let high = tc.high_step.unwrap_or(0);
let inc = base.voltage_step_increment.unwrap_or(0.0);
let theta = winding_connection_angle_deg.to_radians();
let angle_rad_at = |s: f64| -> f64 {
let d = (s - neutral) * inc / 100.0;
let dx = 1.0 + d * theta.cos();
let dy = d * theta.sin();
dy.atan2(dx)
};
let ratio_at = |s: f64| -> f64 {
let d = (s - neutral) * inc / 100.0;
let dx = 1.0 + d * theta.cos();
let dy = d * theta.sin();
dx.hypot(dy)
};
let alpha = angle_rad_at(step);
let tap = Complex::from_polar(ratio_at(step), alpha);
let alpha_max = (low..=high).map(|s| angle_rad_at(s as f64)).fold(f64::MIN, f64::max);
let x_override = match (x_min_max(base.x_min, base.x_max, xtx), alpha_max != 0.0) {
(Some((x_min, x_max)), true) => {
let numer = theta.sin() - alpha_max.tan() * theta.cos();
let denom = theta.sin() - alpha.tan() * theta.cos();
let t = alpha.tan() / alpha_max.tan() * numer / denom;
Some(x_min + (x_max - x_min) * t * t)
}
(Some(_), false) => Some(0.0),
(None, _) => None,
};
Ok(TapEffect { tap, x_override })
}
fn phase_tap_symmetrical(base: &PhaseTapChangerNonLinear, mrid: &str, xtx: f64) -> Result<TapEffect, CgmesError> {
let tc = &base.base.base;
let step = tc.step.ok_or_else(|| missing("PhaseTapChanger", mrid, "step"))?;
let neutral = tc.neutral_step.unwrap_or(0) as f64;
let low = tc.low_step.unwrap_or(0);
let high = tc.high_step.unwrap_or(0);
let inc = base.voltage_step_increment.unwrap_or(0.0);
let angle_rad_at = |s: f64| -> f64 {
let du = (s - neutral) * inc / 100.0;
2.0 * (du / 2.0).atan()
};
let alpha = angle_rad_at(step);
let tap = Complex::from_polar(1.0, alpha);
let alpha_max = (low..=high).map(|s| angle_rad_at(s as f64)).fold(f64::MIN, f64::max);
let x_override = match (x_min_max(base.x_min, base.x_max, xtx), alpha_max != 0.0) {
(Some((x_min, x_max)), true) => {
let ratio = (alpha / 2.0).sin() / (alpha_max / 2.0).sin();
Some(x_min + (x_max - x_min) * ratio * ratio)
}
(Some(_), false) => Some(0.0),
(None, _) => None,
};
Ok(TapEffect { tap, x_override })
}
fn phase_tap_linear(ptc: &cimstructs::PhaseTapChangerLinear, mrid: &str, xtx: f64) -> Result<TapEffect, CgmesError> {
let tc = &ptc.base.base;
let step = tc.step.ok_or_else(|| missing("PhaseTapChangerLinear", mrid, "step"))?;
let neutral = tc.neutral_step.unwrap_or(0) as f64;
let low = tc.low_step.unwrap_or(0);
let high = tc.high_step.unwrap_or(0);
let inc_deg = ptc.step_phase_shift_increment.unwrap_or(0.0);
let angle_rad_at = |s: f64| -> f64 { ((s - neutral) * inc_deg).to_radians() };
let alpha = angle_rad_at(step);
let tap = Complex::from_polar(1.0, alpha);
let alpha_max = (low..=high).map(|s| angle_rad_at(s as f64)).fold(f64::MIN, f64::max);
let x_override = match (x_min_max(ptc.x_min, ptc.x_max, xtx), alpha_max != 0.0) {
(Some((x_min, x_max)), true) => {
let ratio = (alpha / 2.0).sin() / (alpha_max / 2.0).sin();
Some(x_min + (x_max - x_min) * ratio * ratio)
}
(Some(_), false) => Some(0.0),
(None, _) => None,
};
Ok(TapEffect { tap, x_override })
}
fn build_ac_bus_skeleton(ds: &CimDataset) -> Result<(Vec<Bus>, HashMap<String, usize>, TerminalIndex), CgmesError> {
let tn_mrids = by_type(ds, "TopologicalNode");
if tn_mrids.is_empty() {
return Err(CgmesError::NoTopologicalNodes);
}
let mut idx_of: HashMap<String, usize> = HashMap::new();
let mut buses: Vec<Bus> = Vec::with_capacity(tn_mrids.len());
for (i, mrid) in tn_mrids.iter().enumerate() {
idx_of.insert(mrid.clone(), i);
let tn: &TopologicalNode = require(ds, mrid, "TopologicalNode", mrid, "(self)")?;
let u_rated = match &tn.base_voltage {
Some(bv_ref) => {
let bv: &BaseVoltage = require(ds, &bv_ref.mrid, "TopologicalNode", mrid, "BaseVoltage")?;
bv.nominal_voltage.ok_or_else(|| missing("BaseVoltage", &bv_ref.mrid, "nominalVoltage"))? * 1e3
}
None => return Err(missing("TopologicalNode", mrid, "BaseVoltage")),
};
buses.push(Bus {
idx: i,
bus_type: BusType::PQ,
voltage_mag: 1.0,
voltage_ang: 0.0,
p_spec: 0.0,
q_spec: 0.0,
q_min: -f64::INFINITY,
q_max: f64::INFINITY,
u_rated,
zip_terms: Vec::new(),
});
}
let mut terms = TerminalIndex::build(ds, &idx_of, &mut buses)?;
let (buses, switch_merge_remap) = merge_closed_switches(ds, buses, &mut terms)?;
for v in idx_of.values_mut() {
*v = switch_merge_remap[*v];
}
Ok((buses, idx_of, terms))
}
pub fn cgmes_topological_node_bus_index(ds: &CimDataset) -> Result<HashMap<String, usize>, CgmesError> {
let (_, idx_of, _) = build_ac_bus_skeleton(ds)?;
Ok(idx_of)
}
pub fn cgmes_to_buses_and_branches(
ds: &CimDataset, s_base_va: f64,
) -> Result<(Vec<Bus>, Vec<Line>, Vec<Transformer>, Vec<ShuntAdm>), CgmesError> {
let (mut buses, idx_of, terms) = build_ac_bus_skeleton(ds)?;
for mrid in by_type(ds, "EnergyConsumer") {
let ec: &EnergyConsumer = require(ds, mrid, "EnergyConsumer", mrid, "(self)")?;
let Some(bus) = terms.bus(mrid, 0) else { continue };
if !terms.connected(mrid, 0) { continue }
buses[bus].p_spec += -ec.p.unwrap_or(0.0) * 1e6 / s_base_va;
buses[bus].q_spec += -ec.q.unwrap_or(0.0) * 1e6 / s_base_va;
}
for mrid in by_type(ds, "ConformLoad") {
let cl: &cimstructs::ConformLoad = require(ds, mrid, "ConformLoad", mrid, "(self)")?;
let Some(bus) = terms.bus(mrid, 0) else { continue };
if !terms.connected(mrid, 0) { continue }
buses[bus].p_spec += -cl.base.p.unwrap_or(0.0) * 1e6 / s_base_va;
buses[bus].q_spec += -cl.base.q.unwrap_or(0.0) * 1e6 / s_base_va;
}
for mrid in by_type(ds, "NonConformLoad") {
let ncl: &cimstructs::NonConformLoad = require(ds, mrid, "NonConformLoad", mrid, "(self)")?;
let Some(bus) = terms.bus(mrid, 0) else { continue };
if !terms.connected(mrid, 0) { continue }
buses[bus].p_spec += -ncl.base.p.unwrap_or(0.0) * 1e6 / s_base_va;
buses[bus].q_spec += -ncl.base.q.unwrap_or(0.0) * 1e6 / s_base_va;
}
for mrid in by_type(ds, "AsynchronousMachine") {
let am: &cimstructs::AsynchronousMachine = require(ds, mrid, "AsynchronousMachine", mrid, "(self)")?;
let Some(bus) = terms.bus(mrid, 0) else { continue };
if !terms.connected(mrid, 0) { continue }
buses[bus].p_spec += -am.base.p.unwrap_or(0.0) * 1e6 / s_base_va;
buses[bus].q_spec += -am.base.q.unwrap_or(0.0) * 1e6 / s_base_va;
}
for mrid in by_type(ds, "EquivalentInjection") {
let ei: &EquivalentInjection = require(ds, mrid, "EquivalentInjection", mrid, "(self)")?;
let Some(bus) = terms.bus(mrid, 0) else { continue };
if !terms.connected(mrid, 0) { continue }
buses[bus].p_spec += -ei.p.unwrap_or(0.0) * 1e6 / s_base_va;
buses[bus].q_spec += -ei.q.unwrap_or(0.0) * 1e6 / s_base_va;
}
for mrid in by_type(ds, "PowerElectronicsConnection") {
let pec: &PowerElectronicsConnection = require(ds, mrid, "PowerElectronicsConnection", mrid, "(self)")?;
let pec_connected = terms.connected(mrid, 0);
if let Some(bus) = terms.bus(mrid, 0) {
if pec_connected {
buses[bus].p_spec += -pec.p.unwrap_or(0.0) * 1e6 / s_base_va;
buses[bus].q_spec += -pec.q.unwrap_or(0.0) * 1e6 / s_base_va;
}
}
let Some(rc_ref) = &pec.base.regulating_control else { continue };
if !pec_connected || pec.base.control_enabled != Some(true) {
continue;
}
let rc: &RegulatingControl = require(ds, &rc_ref.mrid, "PowerElectronicsConnection", mrid, "RegulatingControl")?;
if rc.enabled != Some(true) {
continue;
}
let is_voltage_mode = rc.mode.as_ref().is_some_and(|m| m.uri.ends_with(".voltage"));
if !is_voltage_mode {
continue;
}
let Some(term_ref) = &rc.terminal else { continue };
let Some(controlled_bus) = terms.bus_via_terminal_mrid(&term_ref.mrid) else { continue };
let target = rc.target_value.ok_or_else(|| missing("RegulatingControl", &rc_ref.mrid, "targetValue"))?;
let mult = unit_multiplier(rc.target_value_unit_multiplier.as_ref().map(|u| u.uri.as_str()));
if buses[controlled_bus].bus_type == BusType::PQ {
buses[controlled_bus].bus_type = BusType::PV;
}
buses[controlled_bus].voltage_mag = target * mult / buses[controlled_bus].u_rated;
let q_min = pec.min_q.unwrap_or(-f64::INFINITY);
let q_max = pec.max_q.unwrap_or(f64::INFINITY);
buses[controlled_bus].q_min = if q_min.is_finite() { q_min * 1e6 / s_base_va } else { q_min };
buses[controlled_bus].q_max = if q_max.is_finite() { q_max * 1e6 / s_base_va } else { q_max };
}
fn push_status_aware_line(lines: &mut Vec<Line>, from: usize, to: usize, from_conn: bool, to_conn: bool, r: f64, x: f64, b_shunt: f64, g_shunt: f64) {
match (from_conn, to_conn) {
(true, true) => {
let (r, x) = crate::topology::clamp_branch_impedance(r, x);
lines.push(Line { from, to, r, x, b_shunt, g_shunt })
}
(true, false) => lines.push(Line { from, to: from, r: 0.0, x: 0.0, b_shunt, g_shunt }),
(false, true) => lines.push(Line { from: to, to, r: 0.0, x: 0.0, b_shunt, g_shunt }),
(false, false) => {}
}
}
let mut lines: Vec<Line> = Vec::new();
for mrid in by_type(ds, "ACLineSegment") {
let ln: &ACLineSegment = require(ds, mrid, "ACLineSegment", mrid, "(self)")?;
let (Some(from), Some(to)) = (terms.bus(mrid, 0), terms.bus(mrid, 1)) else { continue };
let u_rated = buses[from].u_rated;
let z_base = u_rated * u_rated / s_base_va;
let y_base = 1.0 / z_base;
push_status_aware_line(
&mut lines, from, to, terms.connected(mrid, 0), terms.connected(mrid, 1),
ln.r.unwrap_or(0.0) / z_base, ln.x.unwrap_or(0.0) / z_base,
ln.bch.unwrap_or(0.0) / y_base, ln.gch.unwrap_or(0.0) / y_base,
);
}
for mrid in by_type(ds, "SeriesCompensator") {
let sc: &cimstructs::SeriesCompensator = require(ds, mrid, "SeriesCompensator", mrid, "(self)")?;
let (Some(from), Some(to)) = (terms.bus(mrid, 0), terms.bus(mrid, 1)) else { continue };
let u_rated = buses[from].u_rated;
let z_base = u_rated * u_rated / s_base_va;
push_status_aware_line(
&mut lines, from, to, terms.connected(mrid, 0), terms.connected(mrid, 1),
sc.r.unwrap_or(0.0) / z_base, sc.x.unwrap_or(0.0) / z_base, 0.0, 0.0,
);
}
for mrid in by_type(ds, "EquivalentBranch") {
let eb: &cimstructs::EquivalentBranch = require(ds, mrid, "EquivalentBranch", mrid, "(self)")?;
let (Some(from), Some(to)) = (terms.bus(mrid, 0), terms.bus(mrid, 1)) else { continue };
let u_rated = buses[from].u_rated;
let z_base = u_rated * u_rated / s_base_va;
push_status_aware_line(
&mut lines, from, to, terms.connected(mrid, 0), terms.connected(mrid, 1),
eb.r.unwrap_or(0.0) / z_base, eb.x.unwrap_or(0.0) / z_base, 0.0, 0.0,
);
}
let tap_index = TapChangerIndex::build(ds);
let mut ends_by_pt: HashMap<String, Vec<&PowerTransformerEnd>> = HashMap::new();
for mrid in by_type(ds, "PowerTransformerEnd") {
let end: &PowerTransformerEnd = require(ds, mrid, "PowerTransformerEnd", mrid, "(self)")?;
let Some(pt) = &end.power_transformer else {
return Err(missing("PowerTransformerEnd", mrid, "PowerTransformer"));
};
ends_by_pt.entry(pt.mrid.clone()).or_default().push(end);
}
let mut transformers: Vec<Transformer> = Vec::new();
for (pt_mrid, mut ends) in ends_by_pt {
ends.sort_by_key(|e| e.base.end_number.unwrap_or(0));
match ends.len() {
2 => transformers.push(build_two_winding(ds, &tap_index, &terms, &buses, &pt_mrid, ends[0], ends[1], s_base_va)?),
3 => {
let star_idx = buses.len();
buses.push(Bus {
idx: star_idx, bus_type: BusType::PQ, voltage_mag: 1.0, voltage_ang: 0.0,
p_spec: 0.0, q_spec: 0.0, q_min: -f64::INFINITY, q_max: f64::INFINITY,
u_rated: ends[0].rated_u.unwrap_or(1e-3) * 1e3, zip_terms: Vec::new(),
});
for end in &ends {
transformers.push(build_star_leg(ds, &tap_index, &terms, &buses, end, star_idx, s_base_va)?);
}
}
n => return Err(CgmesError::UnsupportedTransformer {
mrid: pt_mrid, reason: format!("{n} PowerTransformerEnds (only 2 or 3 supported)"),
}),
}
}
let mut shunts: Vec<ShuntAdm> = Vec::new();
for mrid in by_type(ds, "LinearShuntCompensator") {
let sc: &LinearShuntCompensator = require(ds, mrid, "LinearShuntCompensator", mrid, "(self)")?;
let Some(at) = terms.bus(mrid, 0) else { continue };
if !terms.connected(mrid, 0) { continue }
let sections = sc.base.sections.unwrap_or(0.0);
let g = sc.g_per_section.unwrap_or(0.0) * sections;
let b = sc.b_per_section.unwrap_or(0.0) * sections;
let z_base = buses[at].u_rated * buses[at].u_rated / s_base_va;
shunts.push(ShuntAdm { at, y: Complex::new(g, b) * z_base });
}
for mrid in by_type(ds, "NonlinearShuntCompensator") {
let sc: &NonlinearShuntCompensator = require(ds, mrid, "NonlinearShuntCompensator", mrid, "(self)")?;
let Some(at) = terms.bus(mrid, 0) else { continue };
if !terms.connected(mrid, 0) { continue }
let target_section = sc.base.sections.unwrap_or(0.0).round() as i64;
let mut y = Complex::new(0.0, 0.0);
for pt_mrid in by_type(ds, "NonlinearShuntCompensatorPoint") {
let pt: &NonlinearShuntCompensatorPoint =
require(ds, pt_mrid, "NonlinearShuntCompensatorPoint", pt_mrid, "(self)")?;
let Some(owner) = &pt.nonlinear_shunt_compensator else { continue };
if owner.mrid != *mrid {
continue;
}
if pt.section_number.unwrap_or(-1) == target_section {
y = Complex::new(pt.g.unwrap_or(0.0), pt.b.unwrap_or(0.0));
break;
}
}
let z_base = buses[at].u_rated * buses[at].u_rated / s_base_va;
shunts.push(ShuntAdm { at, y: y * z_base });
}
let mut energized = vec![false; buses.len()];
for mrid in by_type(ds, "TopologicalIsland") {
let ti: &TopologicalIsland = require(ds, mrid, "TopologicalIsland", mrid, "(self)")?;
for tn in &ti.topological_nodes {
if let Some(&idx) = idx_of.get(&tn.mrid) {
energized[idx] = true;
}
}
}
let tn_backed_indices: std::collections::HashSet<usize> = idx_of.values().copied().collect();
for (i, energized) in energized.iter_mut().enumerate() {
if !tn_backed_indices.contains(&i) {
*energized = true;
}
}
for (i, bus) in buses.iter_mut().enumerate() {
if !energized[i] {
bus.bus_type = BusType::Slack;
bus.voltage_mag = 0.0;
bus.p_spec = 0.0;
bus.q_spec = 0.0;
}
}
for mrid in by_type(ds, "SynchronousMachine") {
let sm: &SynchronousMachine = require(ds, mrid, "SynchronousMachine", mrid, "(self)")?;
let machine_in_service = equipment_in_service(
sm.base.base.base.base.base.in_service, sm.base.base.base.base.base.normally_in_service);
let machine_connected = terms.connected(mrid, 0) && machine_in_service;
if let Some(bus) = terms.bus(mrid, 0) {
if machine_connected {
let p = sm.base.p.unwrap_or(0.0);
let q = sm.base.q.unwrap_or(0.0);
buses[bus].p_spec += -p * 1e6 / s_base_va;
buses[bus].q_spec += q * 1e6 / s_base_va; }
}
let Some(rc_ref) = &sm.base.base.regulating_control else { continue };
if !machine_connected || sm.base.base.control_enabled != Some(true) {
continue;
}
let rc: &RegulatingControl = require(ds, &rc_ref.mrid, "SynchronousMachine", mrid, "RegulatingControl")?;
if rc.enabled != Some(true) {
continue;
}
let is_voltage_mode = rc.mode.as_ref().is_some_and(|m| m.uri.ends_with(".voltage"));
if !is_voltage_mode {
continue;
}
let Some(term_ref) = &rc.terminal else { continue };
let Some(controlled_bus) = terms.bus_via_terminal_mrid(&term_ref.mrid) else { continue };
let target = rc.target_value.ok_or_else(|| missing("RegulatingControl", &rc_ref.mrid, "targetValue"))?;
let mult = unit_multiplier(rc.target_value_unit_multiplier.as_ref().map(|u| u.uri.as_str()));
if buses[controlled_bus].bus_type == BusType::PQ {
buses[controlled_bus].bus_type = BusType::PV;
}
buses[controlled_bus].voltage_mag = target * mult / buses[controlled_bus].u_rated;
let q_min = sm.min_q.unwrap_or(-f64::INFINITY);
let q_max = sm.max_q.unwrap_or(f64::INFINITY);
buses[controlled_bus].q_min = if q_min.is_finite() { q_min * 1e6 / s_base_va } else { q_min };
buses[controlled_bus].q_max = if q_max.is_finite() { q_max * 1e6 / s_base_va } else { q_max };
}
for mrid in by_type(ds, "StaticVarCompensator") {
let sc: &StaticVarCompensator = require(ds, mrid, "StaticVarCompensator", mrid, "(self)")?;
let svc_connected = terms.connected(mrid, 0);
let own_bus = terms.bus(mrid, 0);
if let Some(bus) = own_bus {
if svc_connected {
buses[bus].q_spec += sc.q.unwrap_or(0.0) * 1e6 / s_base_va;
}
}
let Some(rc_ref) = &sc.base.regulating_control else { continue };
if !svc_connected || sc.base.control_enabled != Some(true) {
continue;
}
let rc: &RegulatingControl = require(ds, &rc_ref.mrid, "StaticVarCompensator", mrid, "RegulatingControl")?;
if rc.enabled != Some(true) {
continue;
}
let is_voltage_mode = rc.mode.as_ref().is_some_and(|m| m.uri.ends_with(".voltage"));
if !is_voltage_mode {
continue;
}
let Some(term_ref) = &rc.terminal else { continue };
let Some(controlled_bus) = terms.bus_via_terminal_mrid(&term_ref.mrid) else { continue };
let target = rc.target_value.ok_or_else(|| missing("RegulatingControl", &rc_ref.mrid, "targetValue"))?;
let mult = unit_multiplier(rc.target_value_unit_multiplier.as_ref().map(|u| u.uri.as_str()));
if buses[controlled_bus].bus_type == BusType::PQ {
buses[controlled_bus].bus_type = BusType::PV;
}
buses[controlled_bus].voltage_mag = target * mult / buses[controlled_bus].u_rated;
let z_base = own_bus.map(|b| buses[b].u_rated * buses[b].u_rated / s_base_va);
buses[controlled_bus].q_min = match (sc.inductive_rating, z_base) {
(Some(x), Some(zb)) if x != 0.0 => zb / x,
_ => -f64::INFINITY,
};
buses[controlled_bus].q_max = match (sc.capacitive_rating, z_base) {
(Some(x), Some(zb)) if x != 0.0 => zb / x,
_ => f64::INFINITY,
};
}
for mrid in by_type(ds, "ExternalNetworkInjection") {
let eni: &cimstructs::ExternalNetworkInjection = require(ds, mrid, "ExternalNetworkInjection", mrid, "(self)")?;
let eni_connected = terms.connected(mrid, 0);
if let Some(bus) = terms.bus(mrid, 0) {
if eni_connected {
buses[bus].p_spec += -eni.p.unwrap_or(0.0) * 1e6 / s_base_va;
buses[bus].q_spec += -eni.q.unwrap_or(0.0) * 1e6 / s_base_va;
}
}
let Some(rc_ref) = &eni.base.regulating_control else { continue };
if !eni_connected || eni.base.control_enabled != Some(true) {
continue;
}
let rc: &RegulatingControl = require(ds, &rc_ref.mrid, "ExternalNetworkInjection", mrid, "RegulatingControl")?;
if rc.enabled != Some(true) {
continue;
}
let is_voltage_mode = rc.mode.as_ref().is_some_and(|m| m.uri.ends_with(".voltage"));
if !is_voltage_mode {
continue;
}
let Some(term_ref) = &rc.terminal else { continue };
let Some(controlled_bus) = terms.bus_via_terminal_mrid(&term_ref.mrid) else { continue };
let target = rc.target_value.ok_or_else(|| missing("RegulatingControl", &rc_ref.mrid, "targetValue"))?;
let mult = unit_multiplier(rc.target_value_unit_multiplier.as_ref().map(|u| u.uri.as_str()));
if buses[controlled_bus].bus_type == BusType::PQ {
buses[controlled_bus].bus_type = BusType::PV;
}
buses[controlled_bus].voltage_mag = target * mult / buses[controlled_bus].u_rated;
let q_min = eni.min_q.unwrap_or(-f64::INFINITY);
let q_max = eni.max_q.unwrap_or(f64::INFINITY);
buses[controlled_bus].q_min = if q_min.is_finite() { q_min * 1e6 / s_base_va } else { q_min };
buses[controlled_bus].q_max = if q_max.is_finite() { q_max * 1e6 / s_base_va } else { q_max };
}
let mut slack_indices: Vec<usize> = Vec::new();
let mut slack_angle_ref_mrid: Vec<String> = Vec::new();
for mrid in by_type(ds, "TopologicalIsland") {
let ti: &TopologicalIsland = require(ds, mrid, "TopologicalIsland", mrid, "(self)")?;
if let Some(tn_ref) = &ti.angle_ref_topological_node {
if let Some(&idx) = idx_of.get(&tn_ref.mrid) {
buses[idx].bus_type = BusType::Slack;
slack_indices.push(idx);
slack_angle_ref_mrid.push(tn_ref.mrid.clone());
}
}
}
if slack_indices.is_empty() {
return Err(CgmesError::NoAngleReference);
}
for (&slack_idx, slack_mrid) in slack_indices.iter().zip(slack_angle_ref_mrid.iter()) {
for mrid in by_type(ds, "SvVoltage") {
let sv: &cimstructs::SvVoltage = require(ds, mrid, "SvVoltage", mrid, "(self)")?;
if sv.topological_node.as_ref().is_some_and(|tn| &tn.mrid == slack_mrid) {
if let Some(angle_deg) = sv.angle {
buses[slack_idx].voltage_ang = angle_deg.to_radians();
}
if let Some(v) = sv.v {
buses[slack_idx].voltage_mag = (v * 1e3) / buses[slack_idx].u_rated;
}
break;
}
}
}
Ok((buses, lines, transformers, shunts))
}
fn build_two_winding(
ds: &CimDataset, tap_index: &TapChangerIndex, terms: &TerminalIndex, buses: &[Bus],
pt_mrid: &str, end1: &PowerTransformerEnd, end2: &PowerTransformerEnd, s_base_va: f64,
) -> Result<Transformer, CgmesError> {
let term1 = end1.base.terminal.as_ref().ok_or_else(|| missing("PowerTransformerEnd", end1.mrid_str(), "Terminal"))?;
let term2 = end2.base.terminal.as_ref().ok_or_else(|| missing("PowerTransformerEnd", end2.mrid_str(), "Terminal"))?;
let bus1 = terms.bus_via_terminal_mrid(&term1.mrid)
.ok_or_else(|| CgmesError::UnresolvedReference { from_type: "PowerTransformerEnd", from_mrid: end1.mrid_str().to_string(), field: "Terminal.TopologicalNode" })?;
let bus2 = terms.bus_via_terminal_mrid(&term2.mrid)
.ok_or_else(|| CgmesError::UnresolvedReference { from_type: "PowerTransformerEnd", from_mrid: end2.mrid_str().to_string(), field: "Terminal.TopologicalNode" })?;
let tap1 = tap_index.effect_for_end(ds, end1.mrid_str(), end1.x.unwrap_or(0.0))?;
let tap2 = tap_index.effect_for_end(ds, end2.mrid_str(), end2.x.unwrap_or(0.0))?;
let (tap, x_override) = match (tap1, tap2) {
(Some(_), Some(_)) => {
return Err(CgmesError::UnsupportedTransformer {
mrid: pt_mrid.to_string(),
reason: "tap changers on both ends of a 2-winding transformer aren't supported".into(),
})
}
(Some(t1), None) => (Complex::new(1.0, 0.0) / t1.tap, t1.x_override),
(None, Some(t2)) => (t2.tap, None),
(None, None) => (Complex::new(1.0, 0.0), None),
};
let r1 = end1.r.unwrap_or(0.0);
let x1 = x_override.unwrap_or_else(|| end1.x.unwrap_or(0.0));
if r1 == 0.0 && x1 == 0.0 {
return Err(CgmesError::UnsupportedTransformer {
mrid: pt_mrid.to_string(),
reason: "end 1 has zero series impedance (expected all series impedance on end 1)".into(),
});
}
let g1 = end1.g.unwrap_or(0.0);
let b1 = end1.b.unwrap_or(0.0);
let u1 = end1.rated_u.ok_or_else(|| missing("PowerTransformerEnd", end1.mrid_str(), "ratedU"))? * 1e3;
let z_base = u1 * u1 / s_base_va;
let structural_1 = u1 / buses[bus1].u_rated;
let structural_2 = end2.rated_u.map_or(1.0, |u2| (u2 * 1e3) / buses[bus2].u_rated);
let structural = structural_1 / structural_2;
let tap = tap / structural;
Ok(Transformer {
from: bus2,
to: bus1,
from_status: terms.connected_via_terminal_mrid(&term2.mrid) as u8,
to_status: terms.connected_via_terminal_mrid(&term1.mrid) as u8,
y_series: Complex::new(z_base, 0.0) / Complex::new(r1, x1),
y_shunt: Complex::new(g1, b1) * z_base,
tap,
})
}
fn build_star_leg(
ds: &CimDataset, tap_index: &TapChangerIndex, terms: &TerminalIndex, buses: &[Bus],
end: &PowerTransformerEnd, star_idx: usize, s_base_va: f64,
) -> Result<Transformer, CgmesError> {
let term = end.base.terminal.as_ref().ok_or_else(|| missing("PowerTransformerEnd", end.mrid_str(), "Terminal"))?;
let bus = terms.bus_via_terminal_mrid(&term.mrid)
.ok_or_else(|| CgmesError::UnresolvedReference { from_type: "PowerTransformerEnd", from_mrid: end.mrid_str().to_string(), field: "Terminal.TopologicalNode" })?;
let effect = tap_index.effect_for_end(ds, end.mrid_str(), end.x.unwrap_or(0.0))?;
let (tap, x_override) = match effect {
Some(t) => (Complex::new(1.0, 0.0) / t.tap, t.x_override),
None => (Complex::new(1.0, 0.0), None),
};
let r = end.r.unwrap_or(0.0);
let x = x_override.unwrap_or_else(|| end.x.unwrap_or(0.0));
let g = end.g.unwrap_or(0.0);
let b = end.b.unwrap_or(0.0);
let u = end.rated_u.ok_or_else(|| missing("PowerTransformerEnd", end.mrid_str(), "ratedU"))? * 1e3;
let z_base = u * u / s_base_va;
let structural = u / buses[bus].u_rated;
let tap = tap / structural;
Ok(Transformer {
from: star_idx,
to: bus,
from_status: 1, to_status: terms.connected_via_terminal_mrid(&term.mrid) as u8,
y_series: Complex::new(z_base, 0.0) / Complex::new(r, x),
y_shunt: Complex::new(g, b) * z_base,
tap,
})
}
trait MridStr {
fn mrid_str(&self) -> &str;
}
impl MridStr for PowerTransformerEnd {
fn mrid_str(&self) -> &str {
cimstructs::base::CimElement::mrid(self)
}
}
const DC_SWITCH_R: f64 = 1e-4;
const DC_SOLVE_TOL: f64 = 1e-6;
fn equipment_in_service(in_service: Option<bool>, normally_in_service: Option<bool>) -> bool {
in_service.or(normally_in_service).unwrap_or(true)
}
fn converter_loss_mw(idle: f64, switching: f64, resistive: f64, base_s: f64, rated_udc: f64, idc_amps: f64) -> f64 {
let i_base = base_s * 1000.0 / rated_udc;
let idc_pu = if i_base > 0.0 { idc_amps / i_base } else { 0.0 };
idle + switching * idc_pu.abs() + resistive * idc_pu * idc_pu
}
#[derive(Clone, Copy)]
enum ConverterRole {
UdcSlack(f64),
FixedIdc(f64),
FixedAc(f64),
}
fn classify_vs_converter(vc: &VsConverter, mrid: &str) -> Result<ConverterRole, CgmesError> {
let suffix = vc.p_pcc_control.as_ref().and_then(|u| u.uri.rsplit('.').next());
match suffix {
Some("udc") => Ok(ConverterRole::UdcSlack(vc.base.target_udc.ok_or_else(|| missing("VsConverter", mrid, "targetUdc"))?)),
Some("pPcc") => Ok(ConverterRole::FixedAc(vc.base.target_ppcc.ok_or_else(|| missing("VsConverter", mrid, "targetPpcc"))?)),
other => Err(CgmesError::UnsupportedConverterControl { mrid: mrid.to_string(), mode: other.unwrap_or("(none)").to_string() }),
}
}
fn classify_cs_converter(cc: &CsConverter, mrid: &str) -> Result<ConverterRole, CgmesError> {
let suffix = cc.p_pcc_control.as_ref().and_then(|u| u.uri.rsplit('.').next());
match suffix {
Some("dcVoltage") => Ok(ConverterRole::UdcSlack(cc.base.target_udc.ok_or_else(|| missing("CsConverter", mrid, "targetUdc"))?)),
Some("dcCurrent") => {
let idc = cc.target_idc.ok_or_else(|| missing("CsConverter", mrid, "targetIdc"))?;
let is_rectifier = cc.operating_mode.as_ref().is_some_and(|m| m.uri.ends_with(".rectifier"));
Ok(ConverterRole::FixedIdc(if is_rectifier { idc } else { -idc }))
}
Some("activePower") => Ok(ConverterRole::FixedAc(cc.base.target_ppcc.ok_or_else(|| missing("CsConverter", mrid, "targetPpcc"))?)),
other => Err(CgmesError::UnsupportedConverterControl { mrid: mrid.to_string(), mode: other.unwrap_or("(none)").to_string() }),
}
}
struct ConverterInfo {
ac_bus: usize,
dc_bus: usize,
role: ConverterRole,
idle_loss: f64,
switching_loss: f64,
resistive_loss: f64,
base_s: f64,
rated_udc: f64,
q_mw: f64,
}
struct DcTerminalIndex {
by_equipment: HashMap<String, Vec<String>>,
bus_of: HashMap<String, usize>,
}
impl DcTerminalIndex {
fn build(ds: &CimDataset, dc_idx_of: &HashMap<String, usize>) -> Self {
let mut raw: HashMap<String, Vec<(i64, String)>> = HashMap::new();
let mut bus_of = HashMap::new();
for t_mrid in by_type(ds, "DCTerminal") {
let Some(t) = get::<DCTerminal>(ds, t_mrid) else { continue };
if let Some(ce) = &t.dc_conducting_equipment {
let seq = t.base.base.sequence_number.unwrap_or(1);
raw.entry(ce.mrid.clone()).or_default().push((seq, t_mrid.clone()));
}
if let Some(tn) = &t.base.dc_topological_node {
if let Some(&idx) = dc_idx_of.get(&tn.mrid) {
bus_of.insert(t_mrid.clone(), idx);
}
}
}
let by_equipment = raw.into_iter().map(|(eq, mut v)| {
v.sort_by_key(|(seq, _)| *seq);
(eq, v.into_iter().map(|(_, m)| m).collect())
}).collect();
DcTerminalIndex { by_equipment, bus_of }
}
fn line(&self, equipment_mrid: &str) -> Option<(usize, usize)> {
let ts = self.by_equipment.get(equipment_mrid)?;
if ts.len() < 2 {
return None;
}
Some((*self.bus_of.get(&ts[0])?, *self.bus_of.get(&ts[1])?))
}
fn single_bus(&self, equipment_mrid: &str) -> Option<usize> {
let ts = self.by_equipment.get(equipment_mrid)?;
self.bus_of.get(ts.first()?).copied()
}
}
pub struct DcResolution {
pub dc_bus_mrids: Vec<String>,
pub voltages_kv: Vec<f64>,
pub status: DcSolveStatus,
}
pub fn cgmes_resolve_dc_converters(
ds: &CimDataset, buses: &mut [Bus], s_base_va: f64,
) -> Result<Option<DcResolution>, CgmesError> {
let dctn_mrids = by_type(ds, "DCTopologicalNode");
if dctn_mrids.is_empty() {
return Ok(None);
}
let (_, _, ac_terms) = build_ac_bus_skeleton(ds)?;
let dc_idx_of: HashMap<String, usize> = dctn_mrids.iter().enumerate()
.map(|(i, mrid)| (mrid.clone(), i)).collect();
let mut dc_buses: Vec<DcBus> = (0..dctn_mrids.len()).map(|idx| DcBus {
idx, role: DcBusRole::Passive, udc_fixed: 0.0, voltage: 0.0, shunt_g: 0.0,
}).collect();
let terms = DcTerminalIndex::build(ds, &dc_idx_of);
let mut dc_lines: Vec<DcLine> = Vec::new();
for mrid in by_type(ds, "DCLineSegment") {
let seg: &DCLineSegment = require(ds, mrid, "DCLineSegment", mrid, "(self)")?;
if !equipment_in_service(seg.base.base.in_service, seg.base.base.normally_in_service) { continue }
let Some((a, b)) = terms.line(mrid) else { continue };
let r = seg.resistance.ok_or_else(|| missing("DCLineSegment", mrid, "resistance"))?;
dc_lines.push(DcLine { from: a, to: b, r });
}
for mrid in by_type(ds, "DCSeriesDevice") {
let dev: &DCSeriesDevice = require(ds, mrid, "DCSeriesDevice", mrid, "(self)")?;
if !equipment_in_service(dev.base.base.in_service, dev.base.base.normally_in_service) { continue }
let Some((a, b)) = terms.line(mrid) else { continue };
let r = dev.resistance.ok_or_else(|| missing("DCSeriesDevice", mrid, "resistance"))?;
dc_lines.push(DcLine { from: a, to: b, r });
}
for mrid in by_type(ds, "DCBreaker") {
let br: &DCBreaker = require(ds, mrid, "DCBreaker", mrid, "(self)")?;
if !equipment_in_service(br.base.base.base.in_service, br.base.base.base.normally_in_service) { continue }
let Some((a, b)) = terms.line(mrid) else { continue };
dc_lines.push(DcLine { from: a, to: b, r: DC_SWITCH_R });
}
for mrid in by_type(ds, "DCDisconnector") {
let dc: &DCDisconnector = require(ds, mrid, "DCDisconnector", mrid, "(self)")?;
if !equipment_in_service(dc.base.base.base.in_service, dc.base.base.base.normally_in_service) { continue }
let Some((a, b)) = terms.line(mrid) else { continue };
dc_lines.push(DcLine { from: a, to: b, r: DC_SWITCH_R });
}
for mrid in by_type(ds, "DCSwitch") {
let sw: &DCSwitch = require(ds, mrid, "DCSwitch", mrid, "(self)")?;
if !equipment_in_service(sw.base.base.in_service, sw.base.base.normally_in_service) { continue }
let Some((a, b)) = terms.line(mrid) else { continue };
dc_lines.push(DcLine { from: a, to: b, r: DC_SWITCH_R });
}
for mrid in by_type(ds, "DCGround") {
let g: &DCGround = require(ds, mrid, "DCGround", mrid, "(self)")?;
if !equipment_in_service(g.base.base.in_service, g.base.base.normally_in_service) { continue }
let Some(bus) = terms.single_bus(mrid) else { continue };
dc_buses[bus].role = DcBusRole::Ground;
dc_buses[bus].udc_fixed = 0.0;
}
for mrid in by_type(ds, "DCShunt") {
let sh: &DCShunt = require(ds, mrid, "DCShunt", mrid, "(self)")?;
if !equipment_in_service(sh.base.base.in_service, sh.base.base.normally_in_service) { continue }
let Some(bus) = terms.single_bus(mrid) else { continue };
if let Some(r) = sh.resistance {
if r != 0.0 {
dc_buses[bus].shunt_g += 1.0 / r;
}
}
}
let mut positive_pole_of: HashMap<String, usize> = HashMap::new();
for t_mrid in by_type(ds, "ACDCConverterDCTerminal") {
let Some(t) = get::<ACDCConverterDCTerminal>(ds, t_mrid) else { continue };
let is_positive = t.polarity.as_ref().is_some_and(|p| p.uri.ends_with(".positive"));
if !is_positive { continue }
let (Some(ce), Some(tn)) = (&t.dc_conducting_equipment, &t.base.dc_topological_node) else { continue };
if let Some(&bus) = dc_idx_of.get(&tn.mrid) {
positive_pole_of.insert(ce.mrid.clone(), bus);
}
}
let mut converters: Vec<ConverterInfo> = Vec::new();
for mrid in by_type(ds, "VsConverter") {
let vc: &VsConverter = require(ds, mrid, "VsConverter", mrid, "(self)")?;
let Some(&dc_bus) = positive_pole_of.get(mrid) else { continue };
let Some(ac_bus) = vc.base.pcc_terminal.as_ref()
.and_then(|r| ac_terms.bus_via_terminal_mrid(&r.mrid))
.or_else(|| ac_terms.bus(mrid, 0)) else { continue };
converters.push(ConverterInfo {
ac_bus, dc_bus,
role: classify_vs_converter(vc, mrid)?,
idle_loss: vc.base.idle_loss.unwrap_or(0.0),
switching_loss: vc.base.switching_loss.unwrap_or(0.0),
resistive_loss: vc.base.resistive_loss.unwrap_or(0.0),
base_s: vc.base.base_s.ok_or_else(|| missing("VsConverter", mrid, "baseS"))?,
rated_udc: vc.base.rated_udc.ok_or_else(|| missing("VsConverter", mrid, "ratedUdc"))?,
q_mw: vc.base.q.unwrap_or(0.0),
});
}
for mrid in by_type(ds, "CsConverter") {
let cc: &CsConverter = require(ds, mrid, "CsConverter", mrid, "(self)")?;
let Some(&dc_bus) = positive_pole_of.get(mrid) else { continue };
let Some(ac_bus) = cc.base.pcc_terminal.as_ref()
.and_then(|r| ac_terms.bus_via_terminal_mrid(&r.mrid))
.or_else(|| ac_terms.bus(mrid, 0)) else { continue };
converters.push(ConverterInfo {
ac_bus, dc_bus,
role: classify_cs_converter(cc, mrid)?,
idle_loss: cc.base.idle_loss.unwrap_or(0.0),
switching_loss: cc.base.switching_loss.unwrap_or(0.0),
resistive_loss: cc.base.resistive_loss.unwrap_or(0.0),
base_s: cc.base.base_s.ok_or_else(|| missing("CsConverter", mrid, "baseS"))?,
rated_udc: cc.base.rated_udc.ok_or_else(|| missing("CsConverter", mrid, "ratedUdc"))?,
q_mw: cc.base.q.unwrap_or(0.0),
});
}
for c in &converters {
match c.role {
ConverterRole::UdcSlack(udc_kv) => {
dc_buses[c.dc_bus].role = DcBusRole::UdcSlack;
dc_buses[c.dc_bus].udc_fixed = udc_kv;
}
ConverterRole::FixedIdc(idc_amps) => {
dc_buses[c.dc_bus].role = DcBusRole::FixedIdc { idc_spec: idc_amps / 1000.0 };
}
ConverterRole::FixedAc(_) => {} }
}
for c in &converters {
let ConverterRole::FixedAc(pac_absorbed_target) = c.role else { continue };
let mut idc_amps = 0.0;
for _ in 0..20 {
let loss = converter_loss_mw(c.idle_loss, c.switching_loss, c.resistive_loss, c.base_s, c.rated_udc, idc_amps);
dc_buses[c.dc_bus].role = DcBusRole::FixedP { p_spec: pac_absorbed_target - loss };
let status = solve_dc_network(&mut dc_buses, &dc_lines, DC_SOLVE_TOL, 100);
if !status.converged {
return Err(CgmesError::DcNetworkDidNotConverge);
}
let currents = injected_currents(&dc_buses, &dc_lines);
let idc_new = currents[c.dc_bus].abs() * 1000.0; let converged = (idc_new - idc_amps).abs() < 1e-6;
idc_amps = idc_new;
if converged {
break;
}
}
}
let status = solve_dc_network(&mut dc_buses, &dc_lines, DC_SOLVE_TOL, 100);
let currents = injected_currents(&dc_buses, &dc_lines);
for c in &converters {
let p_dc_injected = dc_buses[c.dc_bus].voltage * currents[c.dc_bus];
let idc_amps = currents[c.dc_bus].abs() * 1000.0;
let loss = converter_loss_mw(c.idle_loss, c.switching_loss, c.resistive_loss, c.base_s, c.rated_udc, idc_amps);
let pac_absorbed = p_dc_injected + loss;
buses[c.ac_bus].p_spec += -pac_absorbed * 1e6 / s_base_va;
buses[c.ac_bus].q_spec += -c.q_mw * 1e6 / s_base_va;
}
Ok(Some(DcResolution {
dc_bus_mrids: dctn_mrids.to_vec(),
voltages_kv: dc_buses.iter().map(|b| b.voltage).collect(),
status,
}))
}