use std::collections::HashMap;
use std::path::Path;
use num_complex::Complex;
pub use cimmodel::CimDataset;
use cimmodel::Element;
use crate::dc::{DcBus, DcBusRole, DcLine, DcSolveStatus, injected_currents, solve_dc_network};
use crate::network::ShuntAdm;
use crate::types::{Bus, BusType, Line, RegulatingMachine, 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>(ds: &'a CimDataset, mrid: &str, class: &str) -> Option<&'a Element> {
ds.entries.get(mrid).filter(|e| e.type_name() == class)
}
fn require<'a>(
ds: &'a CimDataset,
mrid: &str,
class: &str,
from_type: &'static str,
from_mrid: &str,
field: &'static str,
) -> Result<&'a Element, CgmesError> {
get(ds, mrid, class).ok_or_else(|| CgmesError::UnresolvedReference {
from_type,
from_mrid: from_mrid.to_string(),
field,
})
}
fn own<'a>(ds: &'a CimDataset, mrid: &str, class: &'static str) -> Result<&'a Element, CgmesError> {
require(ds, mrid, class, class, mrid, "(self)")
}
trait GetI64 {
fn get_i64(&self, attr: &str) -> Option<i64>;
}
impl GetI64 for Element {
fn get_i64(&self, attr: &str) -> Option<i64> {
self.get_str(attr)?.parse().ok()
}
}
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 sorted_mrids(ds: &CimDataset, type_name: &str) -> Vec<String> {
let mut v = by_type(ds, type_name).to_vec();
v.sort();
v
}
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 = own(ds, cn_mrid, "ConnectivityNode")?;
if let Some(tn) = cn.get_ref("ConnectivityNode.TopologicalNode") {
cn_to_tn.insert(cn_mrid.clone(), tn.to_string());
}
}
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 = own(ds, t_mrid, "Terminal")?;
connected_of.insert(
t_mrid.clone(),
t.get_bool("ACDCTerminal.connected").unwrap_or(true),
);
if let Some(ce) = t.get_ref("Terminal.ConductingEquipment") {
let seq = t.get_i64("ACDCTerminal.sequenceNumber").unwrap_or(1);
raw.entry(ce.to_string())
.or_default()
.push((seq, t_mrid.clone()));
}
let tn_mrid = t
.get_ref("Terminal.TopologicalNode")
.map(str::to_string)
.or_else(|| {
t.get_ref("Terminal.ConnectivityNode")
.and_then(|cn| cn_to_tn.get(cn).cloned())
});
match (tn_mrid, t.get_ref("Terminal.ConnectivityNode")) {
(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.to_string(),
t.get_ref("Terminal.ConductingEquipment")
.map(str::to_string),
));
}
(None, None) => {}
}
}
let mut orphans_by_cn: std::collections::BTreeMap<String, Vec<(String, Option<String>)>> =
std::collections::BTreeMap::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(ds, ce_mrid.as_deref()?, "EquivalentInjection")?;
let bv_ref = ei.get_ref("ConductingEquipment.BaseVoltage")?;
get(ds, bv_ref, "BaseVoltage")?.get_f64("BaseVoltage.nominalVoltage")
})
.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)
}
}
const INJECTING_CLASSES: &[&str] = &[
"EnergyConsumer",
"ConformLoad",
"NonConformLoad",
"StationSupply",
"EnergySource",
"SynchronousMachine",
"AsynchronousMachine",
"RotatingMachine",
"EquivalentInjection",
"ExternalNetworkInjection",
"PowerElectronicsConnection",
"StaticVarCompensator",
"LinearShuntCompensator",
"NonlinearShuntCompensator",
"ShuntCompensator",
"GroundingImpedance",
"PetersenCoil",
"EarthFaultCompensator",
"Ground",
"VsConverter",
"CsConverter",
"ACDCConverter",
];
fn zero_injection_flags(ds: &CimDataset, terms: &TerminalIndex, n_buses: usize) -> Vec<bool> {
let mut zero = vec![true; n_buses];
for class in INJECTING_CLASSES {
for mrid in by_type(ds, class) {
let Some(terminals) = terms.by_equipment.get(mrid) else {
continue;
};
for t in terminals {
if let Some(&bus) = terms.bus_of.get(t) {
if bus < n_buses {
zero[bus] = false;
}
}
}
}
}
zero
}
fn merge_closed_switches(
ds: &CimDataset,
buses: Vec<Bus>,
terms: &mut TerminalIndex,
) -> Result<(Vec<Bus>, Vec<usize>), CgmesError> {
let topo = switch_topology(ds, buses.len(), terms)?;
let view = crate::topology::bus_view(&topo, &crate::topology::RetentionPolicy::MergeAll);
let remap: Vec<usize> = view.bus_of_slice().iter().map(|b| b.0).collect();
let mut merged: Vec<Bus> = Vec::with_capacity(view.n_buses());
for bus in 0..view.n_buses() {
let mut b = buses[view.representative(crate::topology::model::BusIdx(bus)).0].clone();
b.idx = bus;
merged.push(b);
}
for v in terms.bus_of.values_mut() {
*v = remap[*v];
}
Ok((merged, remap))
}
fn switch_topology(
ds: &CimDataset,
n_nodes: usize,
terms: &TerminalIndex,
) -> Result<crate::topology::NodeBreakerTopology, CgmesError> {
use crate::topology::model::{NodeIdx, Switch as TopoSwitch, SwitchKind};
let mut topo = crate::topology::NodeBreakerTopology::new(n_nodes);
let add = |topo: &mut crate::topology::NodeBreakerTopology,
mrid: &str,
kind: SwitchKind,
in_service: bool,
open: bool| {
if let (Some(a), Some(b)) = (terms.bus(mrid, 0), terms.bus(mrid, 1)) {
topo.add_switch(TopoSwitch {
kind,
nodes: [NodeIdx(a), NodeIdx(b)],
open,
in_service,
});
}
};
for mrid in by_type(ds, "Switch") {
let sw = own(ds, mrid, "Switch")?;
add(
&mut topo,
mrid,
SwitchKind::Generic,
equipment_in_service(
sw.get_bool("Equipment.inService"),
sw.get_bool("Equipment.normallyInService"),
),
sw.get_bool("Switch.open").unwrap_or(false),
);
}
for mrid in by_type(ds, "Breaker") {
let br = own(ds, mrid, "Breaker")?;
add(
&mut topo,
mrid,
SwitchKind::Breaker,
equipment_in_service(
br.get_bool("Equipment.inService"),
br.get_bool("Equipment.normallyInService"),
),
br.get_bool("Switch.open").unwrap_or(false),
);
}
for mrid in by_type(ds, "LoadBreakSwitch") {
let lbs = own(ds, mrid, "LoadBreakSwitch")?;
add(
&mut topo,
mrid,
SwitchKind::LoadBreakSwitch,
equipment_in_service(
lbs.get_bool("Equipment.inService"),
lbs.get_bool("Equipment.normallyInService"),
),
lbs.get_bool("Switch.open").unwrap_or(false),
);
}
for mrid in by_type(ds, "DisconnectingCircuitBreaker") {
let dcb = own(ds, mrid, "DisconnectingCircuitBreaker")?;
add(
&mut topo,
mrid,
SwitchKind::DisconnectingCircuitBreaker,
equipment_in_service(
dcb.get_bool("Equipment.inService"),
dcb.get_bool("Equipment.normallyInService"),
),
dcb.get_bool("Switch.open").unwrap_or(false),
);
}
for mrid in by_type(ds, "Disconnector") {
let d = own(ds, mrid, "Disconnector")?;
add(
&mut topo,
mrid,
SwitchKind::Disconnector,
equipment_in_service(
d.get_bool("Equipment.inService"),
d.get_bool("Equipment.normallyInService"),
),
d.get_bool("Switch.open").unwrap_or(false),
);
}
for mrid in by_type(ds, "GroundDisconnector") {
let g = own(ds, mrid, "GroundDisconnector")?;
add(
&mut topo,
mrid,
SwitchKind::GroundDisconnector,
equipment_in_service(
g.get_bool("Equipment.inService"),
g.get_bool("Equipment.normallyInService"),
),
g.get_bool("Switch.open").unwrap_or(false),
);
}
for mrid in by_type(ds, "Jumper") {
let j = own(ds, mrid, "Jumper")?;
add(
&mut topo,
mrid,
SwitchKind::Jumper,
equipment_in_service(
j.get_bool("Equipment.inService"),
j.get_bool("Equipment.normallyInService"),
),
j.get_bool("Switch.open").unwrap_or(false),
);
}
for mrid in by_type(ds, "Cut") {
let c = own(ds, mrid, "Cut")?;
add(
&mut topo,
mrid,
SwitchKind::Cut,
equipment_in_service(
c.get_bool("Equipment.inService"),
c.get_bool("Equipment.normallyInService"),
),
c.get_bool("Switch.open").unwrap_or(false),
);
}
for mrid in by_type(ds, "Fuse") {
let f = own(ds, mrid, "Fuse")?;
add(
&mut topo,
mrid,
SwitchKind::Fuse,
equipment_in_service(
f.get_bool("Equipment.inService"),
f.get_bool("Equipment.normallyInService"),
),
f.get_bool("Switch.open").unwrap_or(false),
);
}
for mrid in by_type(ds, "Junction") {
let j = own(ds, mrid, "Junction")?;
add(
&mut topo,
mrid,
SwitchKind::Junction,
equipment_in_service(
j.get_bool("Equipment.inService"),
j.get_bool("Equipment.normallyInService"),
),
false,
);
}
Ok(topo)
}
struct TapEffect {
tap: Complex<f64>,
x_override: Option<f64>,
}
struct RawTapTable {
low: i32,
position: i32,
neutral: i32,
effects: Vec<TapEffect>,
}
impl RawTapTable {
fn compose(
&self,
compose: impl Fn(Complex<f64>) -> Complex<f64>,
series: impl Fn(Option<f64>) -> Complex<f64>,
) -> crate::types::TapChanger {
let admittances: Vec<Complex<f64>> =
self.effects.iter().map(|e| series(e.x_override)).collect();
let varies = admittances.windows(2).any(|w| w[0] != w[1]);
crate::types::TapChanger {
low: self.low,
position: self.position,
neutral: self.neutral,
steps: self.effects.iter().map(|e| compose(e.tap)).collect(),
series: varies.then_some(admittances),
}
}
}
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(ds, mrid, "RatioTapChanger") {
if let Some(end) = rtc.get_ref("RatioTapChanger.TransformerEnd") {
ratio.insert(end.to_string(), mrid.clone());
}
}
}
let mut phase_asym = HashMap::new();
for mrid in by_type(ds, "PhaseTapChangerAsymmetrical") {
if let Some(ptc) = get(ds, mrid, "PhaseTapChangerAsymmetrical") {
if let Some(end) = ptc.get_ref("PhaseTapChanger.TransformerEnd") {
phase_asym.insert(end.to_string(), mrid.clone());
}
}
}
let mut phase_sym = HashMap::new();
for mrid in by_type(ds, "PhaseTapChangerSymmetrical") {
if let Some(ptc) = get(ds, mrid, "PhaseTapChangerSymmetrical") {
if let Some(end) = ptc.get_ref("PhaseTapChanger.TransformerEnd") {
phase_sym.insert(end.to_string(), mrid.clone());
}
}
}
let mut phase_linear = HashMap::new();
for mrid in by_type(ds, "PhaseTapChangerLinear") {
if let Some(ptc) = get(ds, mrid, "PhaseTapChangerLinear") {
if let Some(end) = ptc.get_ref("PhaseTapChanger.TransformerEnd") {
phase_linear.insert(end.to_string(), mrid.clone());
}
}
}
let mut phase_tabular = HashMap::new();
for mrid in by_type(ds, "PhaseTapChangerTabular") {
if let Some(ptc) = get(ds, mrid, "PhaseTapChangerTabular") {
if let (Some(end), Some(table)) = (
ptc.get_ref("PhaseTapChanger.TransformerEnd"),
ptc.get_ref("PhaseTapChangerTabular.PhaseTapChangerTable"),
) {
phase_tabular.insert(end.to_string(), (mrid.clone(), table.to_string()));
}
}
}
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 = own(ds, mrid, "RatioTapChanger")?;
let step = rtc
.get_f64("TapChanger.step")
.ok_or_else(|| missing("RatioTapChanger", mrid, "step"))?;
if let Some(table_ref) = rtc.get_ref("RatioTapChanger.RatioTapChangerTable") {
if let Some(effect) = ratio_tap_table(ds, table_ref, step.round() as i64, xtx) {
return Ok(Some(effect));
}
}
let neutral = rtc.get_i64("TapChanger.neutralStep").unwrap_or(0) as f64;
let inc = rtc
.get_f64("RatioTapChanger.stepVoltageIncrement")
.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 = own(ds, mrid, "PhaseTapChangerAsymmetrical")?;
let theta_deg = ptc
.get_f64("PhaseTapChangerAsymmetrical.windingConnectionAngle")
.ok_or_else(|| {
missing(
"PhaseTapChangerAsymmetrical",
mrid,
"windingConnectionAngle",
)
})?;
return Ok(Some(phase_tap_asymmetrical(
ptc, mrid, theta_deg, xtx, None,
)?));
}
if let Some(mrid) = self.phase_sym.get(end_mrid) {
let ptc = own(ds, mrid, "PhaseTapChangerSymmetrical")?;
return Ok(Some(phase_tap_symmetrical(ptc, mrid, xtx, None)?));
}
if let Some(mrid) = self.phase_linear.get(end_mrid) {
let ptc = own(ds, mrid, "PhaseTapChangerLinear")?;
return Ok(Some(phase_tap_linear(ptc, mrid, xtx, None)?));
}
if let Some((ptc_mrid, table_mrid)) = self.phase_tabular.get(end_mrid) {
let ptc = own(ds, ptc_mrid, "PhaseTapChangerTabular")?;
let step = ptc
.get_f64("TapChanger.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 has_changer(&self, end_mrid: &str) -> bool {
self.ratio.contains_key(end_mrid)
|| self.phase_asym.contains_key(end_mrid)
|| self.phase_sym.contains_key(end_mrid)
|| self.phase_linear.contains_key(end_mrid)
|| self.phase_tabular.contains_key(end_mrid)
}
fn control_of(&self, ds: &CimDataset, end_mrid: &str) -> Option<(Option<String>, bool)> {
let read = |tc: &Element| {
(
tc.get_ref("TapChanger.TapChangerControl")
.map(str::to_string),
tc.get_bool("TapChanger.controlEnabled") == Some(true),
)
};
if let Some(m) = self.ratio.get(end_mrid) {
return get(ds, m, "RatioTapChanger").map(|t| read(t));
}
if let Some(m) = self.phase_asym.get(end_mrid) {
return get(ds, m, "PhaseTapChangerAsymmetrical").map(|t| read(t));
}
if let Some(m) = self.phase_sym.get(end_mrid) {
return get(ds, m, "PhaseTapChangerSymmetrical").map(|t| read(t));
}
if let Some(m) = self.phase_linear.get(end_mrid) {
return get(ds, m, "PhaseTapChangerLinear").map(|t| read(t));
}
if let Some((m, _)) = self.phase_tabular.get(end_mrid) {
return get(ds, m, "PhaseTapChangerTabular").map(|t| read(t));
}
None
}
fn steps_for_end(
&self,
ds: &CimDataset,
end_mrid: &str,
xtx: f64,
) -> Result<Option<RawTapTable>, CgmesError> {
let build =
|tc: &Element, eval: &dyn Fn(f64) -> Option<TapEffect>| -> Option<RawTapTable> {
let low = tc.get_i64("TapChanger.lowStep")? as i32;
let high = tc.get_i64("TapChanger.highStep")? as i32;
let neutral = tc.get_i64("TapChanger.neutralStep").unwrap_or(0) as i32;
let position = tc
.get_f64("TapChanger.step")
.map(|s| s.round() as i32)
.unwrap_or(neutral);
if high < low {
return None;
}
let mut effects = Vec::with_capacity((high - low + 1) as usize);
for s in low..=high {
effects.push(eval(s as f64)?);
}
Some(RawTapTable {
low,
position,
neutral,
effects,
})
};
if let Some(mrid) = self.ratio.get(end_mrid) {
let rtc = own(ds, mrid, "RatioTapChanger")?;
let table = rtc
.get_ref("RatioTapChanger.RatioTapChangerTable")
.map(str::to_string);
let neutral = rtc.get_i64("TapChanger.neutralStep").unwrap_or(0) as f64;
let inc = rtc
.get_f64("RatioTapChanger.stepVoltageIncrement")
.unwrap_or(0.0);
let eval = |s: f64| -> Option<TapEffect> {
if let Some(t) = table.as_deref() {
if let Some(effect) = ratio_tap_table(ds, t, s.round() as i64, xtx) {
return Some(effect);
}
}
Some(TapEffect {
tap: Complex::new(1.0 + (s - neutral) * inc / 100.0, 0.0),
x_override: None,
})
};
return Ok(build(rtc, &eval));
}
if let Some(mrid) = self.phase_asym.get(end_mrid) {
let ptc = own(ds, mrid, "PhaseTapChangerAsymmetrical")?;
let Some(theta_deg) = ptc.get_f64("PhaseTapChangerAsymmetrical.windingConnectionAngle")
else {
return Ok(None);
};
let eval = |s: f64| phase_tap_asymmetrical(ptc, mrid, theta_deg, xtx, Some(s)).ok();
return Ok(build(ptc, &eval));
}
if let Some(mrid) = self.phase_sym.get(end_mrid) {
let ptc = own(ds, mrid, "PhaseTapChangerSymmetrical")?;
let eval = |s: f64| phase_tap_symmetrical(ptc, mrid, xtx, Some(s)).ok();
return Ok(build(ptc, &eval));
}
if let Some(mrid) = self.phase_linear.get(end_mrid) {
let ptc = own(ds, mrid, "PhaseTapChangerLinear")?;
let eval = |s: f64| phase_tap_linear(ptc, mrid, xtx, Some(s)).ok();
return Ok(build(ptc, &eval));
}
if let Some((ptc_mrid, table_mrid)) = self.phase_tabular.get(end_mrid) {
let ptc = own(ds, ptc_mrid, "PhaseTapChangerTabular")?;
let eval =
|s: f64| phase_tap_tabular(ds, ptc_mrid, table_mrid, s.round() as i64, xtx).ok();
return Ok(build(ptc, &eval));
}
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 = own(ds, pt_mrid, "PhaseTapChangerTablePoint")?;
let Some(owner) = pt.get_ref("PhaseTapChangerTablePoint.PhaseTapChangerTable") else {
continue;
};
if owner != table_mrid || pt.get_i64("TapChangerTablePoint.step") != Some(step) {
continue;
}
let ratio = pt.get_f64("TapChangerTablePoint.ratio").unwrap_or(1.0);
let angle_rad = pt
.get_f64("PhaseTapChangerTablePoint.angle")
.unwrap_or(0.0)
.to_radians();
let tap = Complex::from_polar(ratio, angle_rad);
let x_pct = pt.get_f64("TapChangerTablePoint.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 = get(ds, pt_mrid, "RatioTapChangerTablePoint")?;
let Some(owner) = pt.get_ref("RatioTapChangerTablePoint.RatioTapChangerTable") else {
continue;
};
if owner != table_mrid || pt.get_i64("TapChangerTablePoint.step") != Some(step) {
continue;
}
let ratio = pt.get_f64("TapChangerTablePoint.ratio").unwrap_or(1.0);
let x_pct = pt.get_f64("TapChangerTablePoint.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(
ptc: &Element,
mrid: &str,
winding_connection_angle_deg: f64,
xtx: f64,
at: Option<f64>,
) -> Result<TapEffect, CgmesError> {
let step = match at {
Some(s) => s,
None => ptc
.get_f64("TapChanger.step")
.ok_or_else(|| missing("PhaseTapChanger", mrid, "step"))?,
};
let neutral = ptc.get_i64("TapChanger.neutralStep").unwrap_or(0) as f64;
let low = ptc.get_i64("TapChanger.lowStep").unwrap_or(0);
let high = ptc.get_i64("TapChanger.highStep").unwrap_or(0);
let inc = ptc
.get_f64("PhaseTapChangerNonLinear.voltageStepIncrement")
.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(
ptc.get_f64("PhaseTapChangerNonLinear.xMin"),
ptc.get_f64("PhaseTapChangerNonLinear.xMax"),
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(
ptc: &Element,
mrid: &str,
xtx: f64,
at: Option<f64>,
) -> Result<TapEffect, CgmesError> {
let step = match at {
Some(s) => s,
None => ptc
.get_f64("TapChanger.step")
.ok_or_else(|| missing("PhaseTapChanger", mrid, "step"))?,
};
let neutral = ptc.get_i64("TapChanger.neutralStep").unwrap_or(0) as f64;
let low = ptc.get_i64("TapChanger.lowStep").unwrap_or(0);
let high = ptc.get_i64("TapChanger.highStep").unwrap_or(0);
let inc = ptc
.get_f64("PhaseTapChangerNonLinear.voltageStepIncrement")
.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(
ptc.get_f64("PhaseTapChangerNonLinear.xMin"),
ptc.get_f64("PhaseTapChangerNonLinear.xMax"),
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: &Element,
mrid: &str,
xtx: f64,
at: Option<f64>,
) -> Result<TapEffect, CgmesError> {
let step = match at {
Some(s) => s,
None => ptc
.get_f64("TapChanger.step")
.ok_or_else(|| missing("PhaseTapChangerLinear", mrid, "step"))?,
};
let neutral = ptc.get_i64("TapChanger.neutralStep").unwrap_or(0) as f64;
let low = ptc.get_i64("TapChanger.lowStep").unwrap_or(0);
let high = ptc.get_i64("TapChanger.highStep").unwrap_or(0);
let inc_deg = ptc
.get_f64("PhaseTapChangerLinear.stepPhaseShiftIncrement")
.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.get_f64("PhaseTapChangerLinear.xMin"),
ptc.get_f64("PhaseTapChangerLinear.xMax"),
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 = sorted_mrids(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 = own(ds, mrid, "TopologicalNode")?;
let u_rated = match tn.get_ref("TopologicalNode.BaseVoltage") {
Some(bv_ref) => {
let bv = require(
ds,
bv_ref,
"BaseVoltage",
"TopologicalNode",
mrid,
"BaseVoltage",
)?;
bv.get_f64("BaseVoltage.nominalVoltage")
.ok_or_else(|| missing("BaseVoltage", bv_ref, "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 net = cgmes_to_network(ds, s_base_va)?;
Ok((net.buses, net.lines, net.transformers, net.shunts))
}
#[derive(Clone, Debug)]
pub struct CgmesNetwork {
pub buses: Vec<Bus>,
pub lines: Vec<Line>,
pub transformers: Vec<Transformer>,
pub shunts: Vec<ShuntAdm>,
pub generation: Vec<f64>,
pub tap_changers: Vec<Option<crate::types::TapChanger>>,
pub bus_ids: Vec<String>,
pub bus_aliases: Vec<(String, usize)>,
pub branch_ids: Vec<String>,
pub regulation: Vec<crate::outerloop::TapRegulation>,
pub tap_report: TapRegulationReport,
pub voltage_control: VoltageControlReport,
pub base_harmonization: BaseHarmonizationReport,
pub terminal_branch: HashMap<String, (usize, crate::branch_flow::Terminal)>,
}
pub fn cgmes_to_network(ds: &CimDataset, s_base_va: f64) -> Result<CgmesNetwork, CgmesError> {
let skeleton = build_ac_bus_skeleton(ds)?;
convert_equipment(ds, s_base_va, skeleton)
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct BaseHarmonizationReport {
pub groups: usize,
pub buses: usize,
pub merged: Vec<(f64, f64, usize)>,
}
fn harmonize_voltage_bases(
ds: &CimDataset,
terms: &TerminalIndex,
buses: &mut [Bus],
) -> BaseHarmonizationReport {
let n = buses.len();
let mut parent: Vec<usize> = (0..n).collect();
fn find(parent: &mut Vec<usize>, mut i: usize) -> usize {
while parent[i] != i {
parent[i] = parent[parent[i]];
i = parent[i];
}
i
}
for kind in ["ACLineSegment", "SeriesCompensator"] {
for mrid in by_type(ds, kind) {
let (Some(a), Some(b)) = (terms.bus(mrid, 0), terms.bus(mrid, 1)) else {
continue;
};
if a >= n || b >= n {
continue;
}
let (ra, rb) = (find(&mut parent, a), find(&mut parent, b));
if ra != rb {
parent[ra] = rb;
}
}
}
let mut members: HashMap<usize, Vec<usize>> = HashMap::new();
for i in 0..n {
let root = find(&mut parent, i);
members.entry(root).or_default().push(i);
}
let mut report = BaseHarmonizationReport::default();
let mut merged: HashMap<(u64, u64), usize> = HashMap::new();
for group in members.values() {
let mut tally: HashMap<u64, usize> = HashMap::new();
for &i in group {
if buses[i].u_rated > 0.0 {
*tally
.entry((buses[i].u_rated * 1e3).round() as u64)
.or_default() += 1;
}
}
if tally.len() < 2 {
continue;
}
let &keep_key = tally
.iter()
.max_by(|a, b| a.1.cmp(b.1).then(a.0.cmp(b.0)))
.map(|(k, _)| k)
.expect("a group with two nominals has at least one");
let keep = keep_key as f64 / 1e3;
report.groups += 1;
for &i in group {
if buses[i].u_rated > 0.0 && (buses[i].u_rated - keep).abs() > 1e-9 {
*merged
.entry((
(keep / 1e3).round() as u64,
(buses[i].u_rated / 1e3).round() as u64,
))
.or_default() += 1;
buses[i].u_rated = keep;
report.buses += 1;
}
}
}
report.merged = merged
.into_iter()
.map(|((k, r), c)| (k as f64, r as f64, c))
.collect();
report
.merged
.sort_by(|a, b| b.2.cmp(&a.2).then(b.0.total_cmp(&a.0)));
report
}
fn convert_equipment(
ds: &CimDataset,
s_base_va: f64,
skeleton: (Vec<Bus>, HashMap<String, usize>, TerminalIndex),
) -> Result<CgmesNetwork, CgmesError> {
let (mut buses, idx_of, terms) = skeleton;
let base_harmonization = harmonize_voltage_bases(ds, &terms, &mut buses);
let mut voltage_control = VoltageControl::new(buses.len());
for mrid in by_type(ds, "EnergyConsumer") {
let ec = own(ds, mrid, "EnergyConsumer")?;
let Some(bus) = terms.bus(mrid, 0) else {
continue;
};
if !terms.connected(mrid, 0) {
continue;
}
buses[bus].p_spec += -ec.get_f64("EnergyConsumer.p").unwrap_or(0.0) * 1e6 / s_base_va;
buses[bus].q_spec += -ec.get_f64("EnergyConsumer.q").unwrap_or(0.0) * 1e6 / s_base_va;
}
for mrid in by_type(ds, "ConformLoad") {
let cl = own(ds, mrid, "ConformLoad")?;
let Some(bus) = terms.bus(mrid, 0) else {
continue;
};
if !terms.connected(mrid, 0) {
continue;
}
buses[bus].p_spec += -cl.get_f64("EnergyConsumer.p").unwrap_or(0.0) * 1e6 / s_base_va;
buses[bus].q_spec += -cl.get_f64("EnergyConsumer.q").unwrap_or(0.0) * 1e6 / s_base_va;
}
for mrid in by_type(ds, "NonConformLoad") {
let ncl = own(ds, mrid, "NonConformLoad")?;
let Some(bus) = terms.bus(mrid, 0) else {
continue;
};
if !terms.connected(mrid, 0) {
continue;
}
buses[bus].p_spec += -ncl.get_f64("EnergyConsumer.p").unwrap_or(0.0) * 1e6 / s_base_va;
buses[bus].q_spec += -ncl.get_f64("EnergyConsumer.q").unwrap_or(0.0) * 1e6 / s_base_va;
}
for mrid in by_type(ds, "AsynchronousMachine") {
let am = own(ds, mrid, "AsynchronousMachine")?;
let Some(bus) = terms.bus(mrid, 0) else {
continue;
};
if !terms.connected(mrid, 0) {
continue;
}
buses[bus].p_spec += -am.get_f64("RotatingMachine.p").unwrap_or(0.0) * 1e6 / s_base_va;
buses[bus].q_spec += -am.get_f64("RotatingMachine.q").unwrap_or(0.0) * 1e6 / s_base_va;
}
for mrid in by_type(ds, "EquivalentInjection") {
let ei = own(ds, mrid, "EquivalentInjection")?;
let Some(bus) = terms.bus(mrid, 0) else {
continue;
};
if !terms.connected(mrid, 0) {
continue;
}
buses[bus].p_spec += -ei.get_f64("EquivalentInjection.p").unwrap_or(0.0) * 1e6 / s_base_va;
buses[bus].q_spec += -ei.get_f64("EquivalentInjection.q").unwrap_or(0.0) * 1e6 / s_base_va;
}
for mrid in by_type(ds, "PowerElectronicsConnection") {
let pec = own(ds, mrid, "PowerElectronicsConnection")?;
let pec_connected = terms.connected(mrid, 0);
if let Some(bus) = terms.bus(mrid, 0) {
if pec_connected {
buses[bus].p_spec +=
-pec.get_f64("PowerElectronicsConnection.p").unwrap_or(0.0) * 1e6 / s_base_va;
buses[bus].q_spec +=
-pec.get_f64("PowerElectronicsConnection.q").unwrap_or(0.0) * 1e6 / s_base_va;
}
}
let Some(rc_ref) = pec.get_ref("RegulatingCondEq.RegulatingControl") else {
continue;
};
if !pec_connected || pec.get_bool("RegulatingCondEq.controlEnabled") != Some(true) {
continue;
}
let rc = require(
ds,
rc_ref,
"RegulatingControl",
"PowerElectronicsConnection",
mrid,
"RegulatingControl",
)?;
if rc.get_bool("RegulatingControl.enabled") != Some(true) {
continue;
}
let is_voltage_mode = rc
.get_ref("RegulatingControl.mode")
.is_some_and(|m| m.ends_with(".voltage"));
if !is_voltage_mode {
continue;
}
let Some(term_ref) = rc.get_ref("RegulatingControl.Terminal") else {
continue;
};
let Some(controlled_bus) = terms.bus_via_terminal_mrid(term_ref) else {
continue;
};
let target = rc
.get_f64("RegulatingControl.targetValue")
.ok_or_else(|| missing("RegulatingControl", rc_ref, "targetValue"))?;
let mult = unit_multiplier(
rc.get_ref("RegulatingControl.targetValueUnitMultiplier")
.map(|u| u),
);
let q_min = pec
.get_f64("PowerElectronicsConnection.minQ")
.unwrap_or(-f64::INFINITY);
let q_max = pec
.get_f64("PowerElectronicsConnection.maxQ")
.unwrap_or(f64::INFINITY);
let target_pu = target * mult / buses[controlled_bus].u_rated;
voltage_control.regulate(
&mut buses,
Regulation {
controlled_bus,
target_pu,
q_min: if q_min.is_finite() {
q_min * 1e6 / s_base_va
} else {
q_min
},
q_max: if q_max.is_finite() {
q_max * 1e6 / s_base_va
} else {
q_max
},
id: mrid,
at_bus: terms.bus(mrid, 0),
q_scheduled: -pec.get_f64("PowerElectronicsConnection.q").unwrap_or(0.0) * 1e6
/ s_base_va,
},
);
}
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,
) -> Option<(usize, bool)> {
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,
});
Some((lines.len() - 1, true))
}
(true, false) => {
lines.push(Line {
from,
to: from,
r: 0.0,
x: 0.0,
b_shunt,
g_shunt,
});
Some((lines.len() - 1, false))
}
(false, true) => {
lines.push(Line {
from: to,
to,
r: 0.0,
x: 0.0,
b_shunt,
g_shunt,
});
Some((lines.len() - 1, false))
}
(false, false) => None,
}
}
let mut lines: Vec<Line> = Vec::new();
let mut line_ids: Vec<String> = Vec::new();
let mut line_terminal: HashMap<String, (usize, crate::branch_flow::Terminal)> = HashMap::new();
let record_line =
|terms: &TerminalIndex,
line_terminal: &mut HashMap<String, (usize, crate::branch_flow::Terminal)>,
line_ids: &mut Vec<String>,
mrid: &str,
pushed: Option<(usize, bool)>| {
let Some((idx, through)) = pushed else { return };
if line_ids.len() <= idx {
line_ids.resize(idx + 1, String::new());
}
line_ids[idx] = mrid.to_string();
if !through {
return;
}
let Some(ts) = terms.by_equipment.get(mrid) else {
return;
};
for (which, side) in [
(0, crate::branch_flow::Terminal::From),
(1, crate::branch_flow::Terminal::To),
] {
if let Some(t) = ts.get(which) {
line_terminal.insert(t.clone(), (idx, side));
}
}
};
for mrid in sorted_mrids(ds, "ACLineSegment") {
let mrid = mrid.as_str();
let ln = own(ds, mrid, "ACLineSegment")?;
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;
let pushed = push_status_aware_line(
&mut lines,
from,
to,
terms.connected(mrid, 0),
terms.connected(mrid, 1),
ln.get_f64("ACLineSegment.r").unwrap_or(0.0) / z_base,
ln.get_f64("ACLineSegment.x").unwrap_or(0.0) / z_base,
ln.get_f64("ACLineSegment.bch").unwrap_or(0.0) / y_base,
ln.get_f64("ACLineSegment.gch").unwrap_or(0.0) / y_base,
);
record_line(&terms, &mut line_terminal, &mut line_ids, mrid, pushed);
}
for mrid in sorted_mrids(ds, "SeriesCompensator") {
let mrid = mrid.as_str();
let sc = own(ds, mrid, "SeriesCompensator")?;
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 pushed = push_status_aware_line(
&mut lines,
from,
to,
terms.connected(mrid, 0),
terms.connected(mrid, 1),
sc.get_f64("SeriesCompensator.r").unwrap_or(0.0) / z_base,
sc.get_f64("SeriesCompensator.x").unwrap_or(0.0) / z_base,
0.0,
0.0,
);
record_line(&terms, &mut line_terminal, &mut line_ids, mrid, pushed);
}
for mrid in sorted_mrids(ds, "EquivalentBranch") {
let mrid = mrid.as_str();
let eb = own(ds, mrid, "EquivalentBranch")?;
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 pushed = push_status_aware_line(
&mut lines,
from,
to,
terms.connected(mrid, 0),
terms.connected(mrid, 1),
eb.get_f64("EquivalentBranch.r").unwrap_or(0.0) / z_base,
eb.get_f64("EquivalentBranch.x").unwrap_or(0.0) / z_base,
0.0,
0.0,
);
record_line(&terms, &mut line_terminal, &mut line_ids, mrid, pushed);
}
let tap_index = TapChangerIndex::build(ds);
let mut ends_by_pt: HashMap<String, Vec<&Element>> = HashMap::new();
for mrid in by_type(ds, "PowerTransformerEnd") {
let end = own(ds, mrid, "PowerTransformerEnd")?;
let Some(pt) = end.get_ref("PowerTransformerEnd.PowerTransformer") else {
return Err(missing("PowerTransformerEnd", mrid, "PowerTransformer"));
};
ends_by_pt.entry(pt.to_string()).or_default().push(end);
}
let mut star_bus_ids: Vec<(usize, String)> = Vec::new();
let mut transformers: Vec<Transformer> = Vec::new();
let mut transformer_ids: Vec<String> = Vec::new();
let mut tap_changers: Vec<Option<crate::types::TapChanger>> = Vec::new();
let mut changer_end: Vec<Option<String>> = Vec::new();
let mut terminal_sides: Vec<Vec<(String, crate::branch_flow::Terminal)>> = Vec::new();
let mut ends_by_pt: Vec<(String, Vec<&Element>)> = ends_by_pt.into_iter().collect();
ends_by_pt.sort_by(|a, b| a.0.cmp(&b.0));
for (pt_mrid, mut ends) in ends_by_pt {
ends.sort_by_key(|e| e.get_i64("TransformerEnd.endNumber").unwrap_or(0));
match ends.len() {
2 => {
let (t, c) = build_two_winding(
ds, &tap_index, &terms, &buses, &pt_mrid, ends[0], ends[1], s_base_va,
)?;
transformers.push(t);
transformer_ids.push(pt_mrid.clone());
tap_changers.push(c);
changer_end.push(
[ends[0], ends[1]]
.iter()
.find(|e| tap_index.has_changer(e.mrid()))
.map(|e| e.mrid().to_string()),
);
terminal_sides.push(
[
(ends[0], crate::branch_flow::Terminal::To),
(ends[1], crate::branch_flow::Terminal::From),
]
.iter()
.filter_map(|(e, side)| {
e.get_ref("TransformerEnd.Terminal")
.map(|t| (t.to_string(), *side))
})
.collect(),
);
}
3 => {
let star_idx = buses.len();
star_bus_ids.push((star_idx, format!("{pt_mrid}_star")));
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]
.get_f64("PowerTransformerEnd.ratedU")
.unwrap_or(1e-3)
* 1e3,
zip_terms: Vec::new(),
});
for end in &ends {
let (t, c) =
build_star_leg(ds, &tap_index, &terms, &buses, end, star_idx, s_base_va)?;
transformers.push(t);
transformer_ids.push(end.mrid().to_string());
tap_changers.push(c);
changer_end.push(
tap_index
.has_changer(end.mrid())
.then(|| end.mrid().to_string()),
);
terminal_sides.push(
end.get_ref("TransformerEnd.Terminal")
.map(|t| vec![(t.to_string(), crate::branch_flow::Terminal::To)])
.unwrap_or_default(),
);
}
}
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 = own(ds, mrid, "LinearShuntCompensator")?;
let Some(at) = terms.bus(mrid, 0) else {
continue;
};
if !terms.connected(mrid, 0) {
continue;
}
let sections = sc.get_f64("ShuntCompensator.sections").unwrap_or(0.0);
let g = sc
.get_f64("LinearShuntCompensator.gPerSection")
.unwrap_or(0.0)
* sections;
let b = sc
.get_f64("LinearShuntCompensator.bPerSection")
.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 = own(ds, mrid, "NonlinearShuntCompensator")?;
let Some(at) = terms.bus(mrid, 0) else {
continue;
};
if !terms.connected(mrid, 0) {
continue;
}
let target_section = sc
.get_f64("ShuntCompensator.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 = own(ds, pt_mrid, "NonlinearShuntCompensatorPoint")?;
let Some(owner) =
pt.get_ref("NonlinearShuntCompensatorPoint.NonlinearShuntCompensator")
else {
continue;
};
if owner != *mrid {
continue;
}
if pt
.get_i64("NonlinearShuntCompensatorPoint.sectionNumber")
.unwrap_or(-1)
== target_section
{
y = Complex::new(
pt.get_f64("NonlinearShuntCompensatorPoint.g")
.unwrap_or(0.0),
pt.get_f64("NonlinearShuntCompensatorPoint.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 = own(ds, mrid, "TopologicalIsland")?;
for tn in ti.get_refs("TopologicalIsland.TopologicalNodes") {
if let Some(&idx) = idx_of.get(tn) {
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;
}
}
let mut generation: Vec<f64> = vec![0.0; buses.len()];
for mrid in by_type(ds, "SynchronousMachine") {
let sm = own(ds, mrid, "SynchronousMachine")?;
let machine_in_service = equipment_in_service(
sm.get_bool("Equipment.inService"),
sm.get_bool("Equipment.normallyInService"),
);
let machine_connected = terms.connected(mrid, 0) && machine_in_service;
if let Some(bus) = terms.bus(mrid, 0) {
if machine_connected {
let p = sm.get_f64("RotatingMachine.p").unwrap_or(0.0);
let q = sm.get_f64("RotatingMachine.q").unwrap_or(0.0);
buses[bus].p_spec += -p * 1e6 / s_base_va;
buses[bus].q_spec += q * 1e6 / s_base_va; if let Some(g) = generation.get_mut(bus) {
*g += -p * 1e6 / s_base_va;
}
}
}
let Some(rc_ref) = sm.get_ref("RegulatingCondEq.RegulatingControl") else {
continue;
};
if !machine_connected || sm.get_bool("RegulatingCondEq.controlEnabled") != Some(true) {
continue;
}
let rc = require(
ds,
rc_ref,
"RegulatingControl",
"SynchronousMachine",
mrid,
"RegulatingControl",
)?;
if rc.get_bool("RegulatingControl.enabled") != Some(true) {
continue;
}
let is_voltage_mode = rc
.get_ref("RegulatingControl.mode")
.is_some_and(|m| m.ends_with(".voltage"));
if !is_voltage_mode {
continue;
}
let Some(term_ref) = rc.get_ref("RegulatingControl.Terminal") else {
continue;
};
let Some(controlled_bus) = terms.bus_via_terminal_mrid(term_ref) else {
continue;
};
let target = rc
.get_f64("RegulatingControl.targetValue")
.ok_or_else(|| missing("RegulatingControl", rc_ref, "targetValue"))?;
let mult = unit_multiplier(
rc.get_ref("RegulatingControl.targetValueUnitMultiplier")
.map(|u| u),
);
let q_min = sm
.get_f64("SynchronousMachine.minQ")
.unwrap_or(-f64::INFINITY);
let q_max = sm
.get_f64("SynchronousMachine.maxQ")
.unwrap_or(f64::INFINITY);
let target_pu = target * mult / buses[controlled_bus].u_rated;
voltage_control.regulate(
&mut buses,
Regulation {
controlled_bus,
target_pu,
q_min: if q_min.is_finite() {
q_min * 1e6 / s_base_va
} else {
q_min
},
q_max: if q_max.is_finite() {
q_max * 1e6 / s_base_va
} else {
q_max
},
id: mrid,
at_bus: terms.bus(mrid, 0),
q_scheduled: sm.get_f64("RotatingMachine.q").unwrap_or(0.0) * 1e6 / s_base_va,
},
);
}
for mrid in by_type(ds, "StaticVarCompensator") {
let sc = own(ds, mrid, "StaticVarCompensator")?;
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.get_f64("StaticVarCompensator.q").unwrap_or(0.0) * 1e6 / s_base_va;
}
}
let Some(rc_ref) = sc.get_ref("RegulatingCondEq.RegulatingControl") else {
continue;
};
if !svc_connected || sc.get_bool("RegulatingCondEq.controlEnabled") != Some(true) {
continue;
}
let rc = require(
ds,
rc_ref,
"RegulatingControl",
"StaticVarCompensator",
mrid,
"RegulatingControl",
)?;
if rc.get_bool("RegulatingControl.enabled") != Some(true) {
continue;
}
let is_voltage_mode = rc
.get_ref("RegulatingControl.mode")
.is_some_and(|m| m.ends_with(".voltage"));
if !is_voltage_mode {
continue;
}
let Some(term_ref) = rc.get_ref("RegulatingControl.Terminal") else {
continue;
};
let Some(controlled_bus) = terms.bus_via_terminal_mrid(term_ref) else {
continue;
};
let target = rc
.get_f64("RegulatingControl.targetValue")
.ok_or_else(|| missing("RegulatingControl", rc_ref, "targetValue"))?;
let mult = unit_multiplier(
rc.get_ref("RegulatingControl.targetValueUnitMultiplier")
.map(|u| u),
);
let z_base = own_bus.map(|b| buses[b].u_rated * buses[b].u_rated / s_base_va);
let q_min = match (sc.get_f64("StaticVarCompensator.inductiveRating"), z_base) {
(Some(x), Some(zb)) if x != 0.0 => zb / x,
_ => -f64::INFINITY,
};
let q_max = match (sc.get_f64("StaticVarCompensator.capacitiveRating"), z_base) {
(Some(x), Some(zb)) if x != 0.0 => zb / x,
_ => f64::INFINITY,
};
let target_pu = target * mult / buses[controlled_bus].u_rated;
voltage_control.regulate(
&mut buses,
Regulation {
controlled_bus,
target_pu,
q_min,
q_max,
id: mrid,
at_bus: own_bus,
q_scheduled: sc.get_f64("StaticVarCompensator.q").unwrap_or(0.0) * 1e6 / s_base_va,
},
);
}
for mrid in by_type(ds, "ExternalNetworkInjection") {
let eni = own(ds, mrid, "ExternalNetworkInjection")?;
let eni_connected = terms.connected(mrid, 0);
if let Some(bus) = terms.bus(mrid, 0) {
if eni_connected {
buses[bus].p_spec +=
-eni.get_f64("ExternalNetworkInjection.p").unwrap_or(0.0) * 1e6 / s_base_va;
buses[bus].q_spec +=
-eni.get_f64("ExternalNetworkInjection.q").unwrap_or(0.0) * 1e6 / s_base_va;
}
}
let Some(rc_ref) = eni.get_ref("RegulatingCondEq.RegulatingControl") else {
continue;
};
if !eni_connected || eni.get_bool("RegulatingCondEq.controlEnabled") != Some(true) {
continue;
}
let rc = require(
ds,
rc_ref,
"RegulatingControl",
"ExternalNetworkInjection",
mrid,
"RegulatingControl",
)?;
if rc.get_bool("RegulatingControl.enabled") != Some(true) {
continue;
}
let is_voltage_mode = rc
.get_ref("RegulatingControl.mode")
.is_some_and(|m| m.ends_with(".voltage"));
if !is_voltage_mode {
continue;
}
let Some(term_ref) = rc.get_ref("RegulatingControl.Terminal") else {
continue;
};
let Some(controlled_bus) = terms.bus_via_terminal_mrid(term_ref) else {
continue;
};
let target = rc
.get_f64("RegulatingControl.targetValue")
.ok_or_else(|| missing("RegulatingControl", rc_ref, "targetValue"))?;
let mult = unit_multiplier(
rc.get_ref("RegulatingControl.targetValueUnitMultiplier")
.map(|u| u),
);
let q_min = eni
.get_f64("ExternalNetworkInjection.minQ")
.unwrap_or(-f64::INFINITY);
let q_max = eni
.get_f64("ExternalNetworkInjection.maxQ")
.unwrap_or(f64::INFINITY);
let target_pu = target * mult / buses[controlled_bus].u_rated;
voltage_control.regulate(
&mut buses,
Regulation {
controlled_bus,
target_pu,
q_min: if q_min.is_finite() {
q_min * 1e6 / s_base_va
} else {
q_min
},
q_max: if q_max.is_finite() {
q_max * 1e6 / s_base_va
} else {
q_max
},
id: mrid,
at_bus: terms.bus(mrid, 0),
q_scheduled: -eni.get_f64("ExternalNetworkInjection.q").unwrap_or(0.0) * 1e6
/ s_base_va,
},
);
}
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 = own(ds, mrid, "TopologicalIsland")?;
if let Some(tn_ref) = ti.get_ref("TopologicalIsland.AngleRefTopologicalNode") {
if let Some(&idx) = idx_of.get(tn_ref) {
buses[idx].bus_type = BusType::Slack;
slack_indices.push(idx);
slack_angle_ref_mrid.push(tn_ref.to_string());
}
}
}
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 = own(ds, mrid, "SvVoltage")?;
if sv
.get_ref("SvVoltage.TopologicalNode")
.is_some_and(|tn| tn == slack_mrid)
{
if let Some(angle_deg) = sv.get_f64("SvVoltage.angle") {
buses[slack_idx].voltage_ang = angle_deg.to_radians();
}
if let Some(v) = sv.get_f64("SvVoltage.v") {
buses[slack_idx].voltage_mag = (v * 1e3) / buses[slack_idx].u_rated;
}
break;
}
}
}
let terminal_branch = {
let mut map = line_terminal;
for (i, sides) in terminal_sides.iter().enumerate() {
for (terminal, side) in sides {
map.insert(terminal.clone(), (lines.len() + i, *side));
}
}
map
};
let (regulation, tap_report) = read_tap_regulation(
ds,
&tap_index,
&terms,
&buses,
&lines,
&tap_changers,
&changer_end,
&terminal_sides,
s_base_va,
);
generation.resize(buses.len(), 0.0);
for g in &mut generation {
*g = g.max(0.0);
}
let voltage_control = voltage_control.finish(&buses);
let mut named: Vec<(&String, usize)> = idx_of.iter().map(|(m, i)| (m, *i)).collect();
named.sort();
let bus_aliases: Vec<(String, usize)> = named.iter().map(|(m, i)| ((*m).clone(), *i)).collect();
let mut bus_ids: Vec<String> = vec![String::new(); buses.len()];
for (mrid, i) in named {
if let Some(slot) = bus_ids.get_mut(i)
&& slot.is_empty()
{
*slot = mrid.clone();
}
}
for (i, id) in star_bus_ids {
if let Some(slot) = bus_ids.get_mut(i) {
*slot = id;
}
}
line_ids.resize(lines.len(), String::new());
let branch_ids: Vec<String> = line_ids.into_iter().chain(transformer_ids).collect();
Ok(CgmesNetwork {
bus_ids,
bus_aliases,
branch_ids,
generation,
base_harmonization,
buses,
lines,
transformers,
shunts,
tap_changers,
regulation,
tap_report,
voltage_control,
terminal_branch,
})
}
#[derive(Clone, Debug)]
pub struct ControlAreaImport {
pub of_bus: Vec<Option<usize>>,
pub targets: Vec<f64>,
pub tolerances: Vec<Option<f64>>,
pub ids: Vec<(String, String)>,
pub report: ControlAreaReport,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ControlAreaReport {
pub areas: usize,
pub tie_flows: usize,
pub unresolved_tie_flows: usize,
pub without_boundary: Vec<usize>,
pub without_target: Vec<usize>,
pub contested_buses: usize,
pub unassigned_buses: usize,
}
pub fn cgmes_control_areas(
ds: &CimDataset,
net: &CgmesNetwork,
s_base_va: f64,
) -> Result<ControlAreaImport, CgmesError> {
let n = net.buses.len();
let mut report = ControlAreaReport::default();
let mut area_mrids: Vec<String> = by_type(ds, "ControlArea").to_vec();
area_mrids.sort();
let mut ids = Vec::new();
let mut targets = Vec::new();
let mut tolerances = Vec::new();
for mrid in &area_mrids {
let ca = own(ds, mrid, "ControlArea")?;
ids.push((
mrid.clone(),
ca.get_str("IdentifiedObject.name")
.unwrap_or("")
.to_string(),
));
match ca.get_f64("ControlArea.netInterchange") {
Some(mw) => targets.push(-mw * 1e6 / s_base_va),
None => {
report.without_target.push(targets.len());
targets.push(0.0);
}
}
tolerances.push(
ca.get_f64("ControlArea.pTolerance")
.map(|mw| mw * 1e6 / s_base_va),
);
}
report.areas = ids.len();
let index_of = |mrid: &str| area_mrids.iter().position(|m| m == mrid);
let params = crate::branch_flow::branch_params(&net.lines, &net.transformers);
let mut cut = vec![false; params.len()];
let mut seeds: Vec<Vec<usize>> = vec![Vec::new(); ids.len()];
for mrid in by_type(ds, "TieFlow") {
let tf = own(ds, mrid, "TieFlow")?;
report.tie_flows += 1;
let (Some(area_ref), Some(term_ref)) = (
tf.get_ref("TieFlow.ControlArea"),
tf.get_ref("TieFlow.Terminal"),
) else {
report.unresolved_tie_flows += 1;
continue;
};
let Some(a) = index_of(area_ref) else {
report.unresolved_tie_flows += 1;
continue;
};
let Some(&(branch, side)) = net.terminal_branch.get(term_ref) else {
report.unresolved_tie_flows += 1;
continue;
};
cut[branch] = true;
seeds[a].push(match side {
crate::branch_flow::Terminal::From => params[branch].to,
crate::branch_flow::Terminal::To => params[branch].from,
});
}
let mut adjacency: Vec<Vec<usize>> = vec![Vec::new(); n];
for (i, bp) in params.iter().enumerate() {
if cut[i] || bp.from == bp.to {
continue;
}
adjacency[bp.from].push(bp.to);
adjacency[bp.to].push(bp.from);
}
let mut component = vec![usize::MAX; n];
let mut n_components = 0;
for start in 0..n {
if component[start] != usize::MAX {
continue;
}
let mut stack = vec![start];
component[start] = n_components;
while let Some(b) = stack.pop() {
for &next in &adjacency[b] {
if component[next] == usize::MAX {
component[next] = n_components;
stack.push(next);
}
}
}
n_components += 1;
}
let mut area_of_component: Vec<Option<usize>> = vec![None; n_components];
for (a, buses) in seeds.iter().enumerate() {
if buses.is_empty() {
report.without_boundary.push(a);
continue;
}
for &b in buses {
let c = component[b];
match area_of_component[c] {
None => area_of_component[c] = Some(a),
Some(existing) if existing != a => report.contested_buses += 1,
Some(_) => {}
}
}
}
let of_bus: Vec<Option<usize>> = (0..n).map(|b| area_of_component[component[b]]).collect();
report.unassigned_buses = of_bus.iter().filter(|a| a.is_none()).count();
Ok(ControlAreaImport {
of_bus,
targets,
tolerances,
ids,
report,
})
}
#[derive(Clone, Debug, PartialEq)]
pub struct TargetConflict {
pub bus: usize,
pub existing: f64,
pub proposed: f64,
pub id: String,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct VoltageControlReport {
pub regulated_buses: usize,
pub shared_buses: usize,
pub machines: Vec<RegulatingMachine>,
pub nonregulating_q: Vec<f64>,
pub target_conflicts: Vec<TargetConflict>,
}
struct Regulation<'a> {
controlled_bus: usize,
target_pu: f64,
q_min: f64,
q_max: f64,
id: &'a str,
at_bus: Option<usize>,
q_scheduled: f64,
}
struct VoltageControl {
seen: Vec<bool>,
target: Vec<Option<(f64, String)>>,
controllers: Vec<usize>,
conflicts: Vec<TargetConflict>,
machines: Vec<RegulatingMachine>,
scheduled_q: Vec<f64>,
}
impl VoltageControl {
fn new(n: usize) -> Self {
Self {
seen: vec![false; n],
target: vec![None; n],
controllers: vec![0; n],
conflicts: Vec::new(),
machines: Vec::new(),
scheduled_q: vec![0.0; n],
}
}
fn regulate(&mut self, buses: &mut [Bus], reg: Regulation<'_>) {
let Regulation {
controlled_bus: bus,
target_pu,
q_min,
q_max,
id,
at_bus,
q_scheduled,
} = reg;
if buses[bus].bus_type == BusType::PQ {
buses[bus].bus_type = BusType::PV;
}
if let Some((existing, _)) = &self.target[bus] {
if (*existing - target_pu).abs() > 1e-9 {
self.conflicts.push(TargetConflict {
bus,
existing: *existing,
proposed: target_pu,
id: id.to_string(),
});
}
}
self.target[bus] = Some((target_pu, id.to_string()));
buses[bus].voltage_mag = target_pu;
if self.seen[bus] {
buses[bus].q_min += q_min;
buses[bus].q_max += q_max;
} else {
buses[bus].q_min = q_min;
buses[bus].q_max = q_max;
self.seen[bus] = true;
}
self.controllers[bus] += 1;
if let Some(at) = at_bus {
self.scheduled_q[at] += q_scheduled;
self.machines.push(RegulatingMachine {
id: id.to_string(),
at_bus: at,
controls_bus: bus,
target_pu,
q_min,
q_max,
q_scheduled,
key: None,
});
}
}
fn finish(self, buses: &[Bus]) -> VoltageControlReport {
VoltageControlReport {
regulated_buses: self.controllers.iter().filter(|c| **c > 0).count(),
shared_buses: self.controllers.iter().filter(|c| **c > 1).count(),
target_conflicts: self.conflicts,
nonregulating_q: buses
.iter()
.map(|b| b.q_spec - self.scheduled_q.get(b.idx).copied().unwrap_or(0.0))
.collect(),
machines: self.machines,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TapRegulationReport {
pub converted: usize,
pub disabled: usize,
pub unattached: usize,
pub without_target: usize,
pub unsupported_mode: usize,
pub unresolved_flow: usize,
}
#[allow(clippy::too_many_arguments)]
fn read_tap_regulation(
ds: &CimDataset,
tap_index: &TapChangerIndex,
terms: &TerminalIndex,
buses: &[Bus],
lines: &[Line],
tap_changers: &[Option<crate::types::TapChanger>],
changer_end: &[Option<String>],
terminal_sides: &[Vec<(String, crate::branch_flow::Terminal)>],
s_base_va: f64,
) -> (Vec<crate::outerloop::TapRegulation>, TapRegulationReport) {
use crate::outerloop::{RegulationMode, TapRegulation};
let mut out = Vec::new();
let mut report = TapRegulationReport::default();
let mut flow_of: HashMap<&str, (usize, crate::branch_flow::Terminal)> = HashMap::new();
for (i, sides) in terminal_sides.iter().enumerate() {
for (terminal, side) in sides {
flow_of.insert(terminal.as_str(), (lines.len() + i, *side));
}
}
for (i, changer) in tap_changers.iter().enumerate() {
if changer.is_none() {
continue;
}
let Some(end_mrid) = changer_end[i].as_deref() else {
continue;
};
let Some((control_mrid, control_enabled)) = tap_index.control_of(ds, end_mrid) else {
continue;
};
let Some(control_mrid) = control_mrid else {
continue;
};
let Some(rc) = get(ds, &control_mrid, "TapChangerControl") else {
report.unattached += 1;
continue;
};
let enabled = control_enabled && rc.get_bool("RegulatingControl.enabled") == Some(true);
if !enabled {
report.disabled += 1;
continue;
}
let Some(term_ref) = rc.get_ref("RegulatingControl.Terminal") else {
report.unattached += 1;
continue;
};
let Some(controlled_bus) = terms.bus_via_terminal_mrid(term_ref) else {
report.unattached += 1;
continue;
};
let Some(target) = rc.get_f64("RegulatingControl.targetValue") else {
report.without_target += 1;
continue;
};
let mult = unit_multiplier(rc.get_ref("RegulatingControl.targetValueUnitMultiplier"));
let deadband = rc
.get_f64("RegulatingControl.targetDeadband")
.unwrap_or(0.0);
let uri = rc.get_ref("RegulatingControl.mode").unwrap_or("");
let (mode, target, deadband) = if uri.ends_with(".voltage") {
let base = buses[controlled_bus].u_rated;
(
RegulationMode::Voltage,
target * mult / base,
deadband * mult / base,
)
} else if uri.ends_with(".activePower") {
let Some(&(branch, terminal)) = flow_of.get(term_ref) else {
report.unresolved_flow += 1;
continue;
};
(
RegulationMode::ActivePower { branch, terminal },
target * mult / s_base_va,
deadband * mult / s_base_va,
)
} else {
report.unsupported_mode += 1;
continue;
};
report.converted += 1;
out.push(TapRegulation {
transformer: i,
controlled_bus,
mode,
target,
deadband,
enabled: true,
id: control_mrid,
});
}
(out, report)
}
fn build_two_winding(
ds: &CimDataset,
tap_index: &TapChangerIndex,
terms: &TerminalIndex,
buses: &[Bus],
pt_mrid: &str,
end1: &Element,
end2: &Element,
s_base_va: f64,
) -> Result<(Transformer, Option<crate::types::TapChanger>), CgmesError> {
let term1 = end1
.get_ref("TransformerEnd.Terminal")
.ok_or_else(|| missing("PowerTransformerEnd", end1.mrid(), "Terminal"))?;
let term2 = end2
.get_ref("TransformerEnd.Terminal")
.ok_or_else(|| missing("PowerTransformerEnd", end2.mrid(), "Terminal"))?;
let bus1 =
terms
.bus_via_terminal_mrid(term1)
.ok_or_else(|| CgmesError::UnresolvedReference {
from_type: "PowerTransformerEnd",
from_mrid: end1.mrid().to_string(),
field: "Terminal.TopologicalNode",
})?;
let bus2 =
terms
.bus_via_terminal_mrid(term2)
.ok_or_else(|| CgmesError::UnresolvedReference {
from_type: "PowerTransformerEnd",
from_mrid: end2.mrid().to_string(),
field: "Terminal.TopologicalNode",
})?;
let tap1 = tap_index.effect_for_end(
ds,
end1.mrid(),
end1.get_f64("PowerTransformerEnd.x").unwrap_or(0.0),
)?;
let tap2 = tap_index.effect_for_end(
ds,
end2.mrid(),
end2.get_f64("PowerTransformerEnd.x").unwrap_or(0.0),
)?;
let on_end1 = tap1.is_some();
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.get_f64("PowerTransformerEnd.r").unwrap_or(0.0);
let x1 = x_override.unwrap_or_else(|| end1.get_f64("PowerTransformerEnd.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.get_f64("PowerTransformerEnd.g").unwrap_or(0.0);
let b1 = end1.get_f64("PowerTransformerEnd.b").unwrap_or(0.0);
let u1 = end1
.get_f64("PowerTransformerEnd.ratedU")
.ok_or_else(|| missing("PowerTransformerEnd", end1.mrid(), "ratedU"))?
* 1e3;
let z_base = u1 * u1 / s_base_va;
let structural_1 = u1 / buses[bus1].u_rated;
let structural_2 = end2
.get_f64("PowerTransformerEnd.ratedU")
.map_or(1.0, |u2| (u2 * 1e3) / buses[bus2].u_rated);
let structural = structural_1 / structural_2;
let tap = tap / structural;
let end_mrid = if on_end1 { end1.mrid() } else { end2.mrid() };
let xtx = if on_end1 {
end1.get_f64("PowerTransformerEnd.x").unwrap_or(0.0)
} else {
end2.get_f64("PowerTransformerEnd.x").unwrap_or(0.0)
};
let changer = tap_index.steps_for_end(ds, end_mrid, xtx)?.map(|raw| {
raw.compose(
|t| {
if on_end1 {
Complex::new(1.0, 0.0) / t / structural
} else {
t / structural
}
},
|x| {
let x = if on_end1 {
x.unwrap_or_else(|| end1.get_f64("PowerTransformerEnd.x").unwrap_or(0.0))
} else {
x1
};
Complex::new(z_base, 0.0) / Complex::new(r1, x)
},
)
});
Ok((
Transformer {
from: bus2,
to: bus1,
from_status: terms.connected_via_terminal_mrid(term2) as u8,
to_status: terms.connected_via_terminal_mrid(term1) as u8,
y_series: Complex::new(z_base, 0.0) / Complex::new(r1, x1),
y_shunt: Complex::new(g1, b1) * z_base,
tap,
},
changer,
))
}
fn build_star_leg(
ds: &CimDataset,
tap_index: &TapChangerIndex,
terms: &TerminalIndex,
buses: &[Bus],
end: &Element,
star_idx: usize,
s_base_va: f64,
) -> Result<(Transformer, Option<crate::types::TapChanger>), CgmesError> {
let term = end
.get_ref("TransformerEnd.Terminal")
.ok_or_else(|| missing("PowerTransformerEnd", end.mrid(), "Terminal"))?;
let bus = terms
.bus_via_terminal_mrid(term)
.ok_or_else(|| CgmesError::UnresolvedReference {
from_type: "PowerTransformerEnd",
from_mrid: end.mrid().to_string(),
field: "Terminal.TopologicalNode",
})?;
let effect = tap_index.effect_for_end(
ds,
end.mrid(),
end.get_f64("PowerTransformerEnd.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.get_f64("PowerTransformerEnd.r").unwrap_or(0.0);
let x = x_override.unwrap_or_else(|| end.get_f64("PowerTransformerEnd.x").unwrap_or(0.0));
let g = end.get_f64("PowerTransformerEnd.g").unwrap_or(0.0);
let b = end.get_f64("PowerTransformerEnd.b").unwrap_or(0.0);
let u = end
.get_f64("PowerTransformerEnd.ratedU")
.ok_or_else(|| missing("PowerTransformerEnd", end.mrid(), "ratedU"))?
* 1e3;
let z_base = u * u / s_base_va;
let structural = u / buses[bus].u_rated;
let tap = tap / structural;
let changer = tap_index
.steps_for_end(
ds,
end.mrid(),
end.get_f64("PowerTransformerEnd.x").unwrap_or(0.0),
)?
.map(|raw| {
raw.compose(
|t| Complex::new(1.0, 0.0) / t / structural,
|xo| {
let x =
xo.unwrap_or_else(|| end.get_f64("PowerTransformerEnd.x").unwrap_or(0.0));
Complex::new(z_base, 0.0) / Complex::new(r, x)
},
)
});
Ok((
Transformer {
from: star_idx,
to: bus,
from_status: 1, to_status: terms.connected_via_terminal_mrid(term) as u8,
y_series: Complex::new(z_base, 0.0) / Complex::new(r, x),
y_shunt: Complex::new(g, b) * z_base,
tap,
},
changer,
))
}
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: &Element, mrid: &str) -> Result<ConverterRole, CgmesError> {
let suffix = vc
.get_ref("VsConverter.pPccControl")
.and_then(|u| u.rsplit('.').next());
match suffix {
Some("udc") => Ok(ConverterRole::UdcSlack(
vc.get_f64("ACDCConverter.targetUdc")
.ok_or_else(|| missing("VsConverter", mrid, "targetUdc"))?,
)),
Some("pPcc") => Ok(ConverterRole::FixedAc(
vc.get_f64("ACDCConverter.targetPpcc")
.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: &Element, mrid: &str) -> Result<ConverterRole, CgmesError> {
let suffix = cc
.get_ref("CsConverter.pPccControl")
.and_then(|u| u.rsplit('.').next());
match suffix {
Some("dcVoltage") => Ok(ConverterRole::UdcSlack(
cc.get_f64("ACDCConverter.targetUdc")
.ok_or_else(|| missing("CsConverter", mrid, "targetUdc"))?,
)),
Some("dcCurrent") => {
let idc = cc
.get_f64("CsConverter.targetIdc")
.ok_or_else(|| missing("CsConverter", mrid, "targetIdc"))?;
let is_rectifier = cc
.get_ref("CsConverter.operatingMode")
.is_some_and(|m| m.ends_with(".rectifier"));
Ok(ConverterRole::FixedIdc(if is_rectifier {
idc
} else {
-idc
}))
}
Some("activePower") => Ok(ConverterRole::FixedAc(
cc.get_f64("ACDCConverter.targetPpcc")
.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(ds, t_mrid, "DCTerminal") else {
continue;
};
if let Some(ce) = t.get_ref("DCTerminal.DCConductingEquipment") {
let seq = t.get_i64("ACDCTerminal.sequenceNumber").unwrap_or(1);
raw.entry(ce.to_string())
.or_default()
.push((seq, t_mrid.clone()));
}
if let Some(tn) = t.get_ref("DCBaseTerminal.DCTopologicalNode") {
if let Some(&idx) = dc_idx_of.get(tn) {
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 = own(ds, mrid, "DCLineSegment")?;
if !equipment_in_service(
seg.get_bool("Equipment.inService"),
seg.get_bool("Equipment.normallyInService"),
) {
continue;
}
let Some((a, b)) = terms.line(mrid) else {
continue;
};
let r = seg
.get_f64("DCLineSegment.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 = own(ds, mrid, "DCSeriesDevice")?;
if !equipment_in_service(
dev.get_bool("Equipment.inService"),
dev.get_bool("Equipment.normallyInService"),
) {
continue;
}
let Some((a, b)) = terms.line(mrid) else {
continue;
};
let r = dev
.get_f64("DCSeriesDevice.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 = own(ds, mrid, "DCBreaker")?;
if !equipment_in_service(
br.get_bool("Equipment.inService"),
br.get_bool("Equipment.normallyInService"),
) {
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 = own(ds, mrid, "DCDisconnector")?;
if !equipment_in_service(
dc.get_bool("Equipment.inService"),
dc.get_bool("Equipment.normallyInService"),
) {
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 = own(ds, mrid, "DCSwitch")?;
if !equipment_in_service(
sw.get_bool("Equipment.inService"),
sw.get_bool("Equipment.normallyInService"),
) {
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 = own(ds, mrid, "DCGround")?;
if !equipment_in_service(
g.get_bool("Equipment.inService"),
g.get_bool("Equipment.normallyInService"),
) {
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 = own(ds, mrid, "DCShunt")?;
if !equipment_in_service(
sh.get_bool("Equipment.inService"),
sh.get_bool("Equipment.normallyInService"),
) {
continue;
}
let Some(bus) = terms.single_bus(mrid) else {
continue;
};
if let Some(r) = sh.get_f64("DCShunt.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(ds, t_mrid, "ACDCConverterDCTerminal") else {
continue;
};
let is_positive = t
.get_ref("ACDCConverterDCTerminal.polarity")
.is_some_and(|p| p.ends_with(".positive"));
if !is_positive {
continue;
}
let (Some(ce), Some(tn)) = (
t.get_ref("ACDCConverterDCTerminal.DCConductingEquipment"),
t.get_ref("DCBaseTerminal.DCTopologicalNode"),
) else {
continue;
};
if let Some(&bus) = dc_idx_of.get(tn) {
positive_pole_of.insert(ce.to_string(), bus);
}
}
let mut converters: Vec<ConverterInfo> = Vec::new();
for mrid in by_type(ds, "VsConverter") {
let vc = own(ds, mrid, "VsConverter")?;
let Some(&dc_bus) = positive_pole_of.get(mrid) else {
continue;
};
let Some(ac_bus) = vc
.get_ref("ACDCConverter.PccTerminal")
.and_then(|r| ac_terms.bus_via_terminal_mrid(r))
.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.get_f64("ACDCConverter.idleLoss").unwrap_or(0.0),
switching_loss: vc.get_f64("ACDCConverter.switchingLoss").unwrap_or(0.0),
resistive_loss: vc.get_f64("ACDCConverter.resistiveLoss").unwrap_or(0.0),
base_s: vc
.get_f64("ACDCConverter.baseS")
.ok_or_else(|| missing("VsConverter", mrid, "baseS"))?,
rated_udc: vc
.get_f64("ACDCConverter.ratedUdc")
.ok_or_else(|| missing("VsConverter", mrid, "ratedUdc"))?,
q_mw: vc.get_f64("ACDCConverter.q").unwrap_or(0.0),
});
}
for mrid in by_type(ds, "CsConverter") {
let cc = own(ds, mrid, "CsConverter")?;
let Some(&dc_bus) = positive_pole_of.get(mrid) else {
continue;
};
let Some(ac_bus) = cc
.get_ref("ACDCConverter.PccTerminal")
.and_then(|r| ac_terms.bus_via_terminal_mrid(r))
.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.get_f64("ACDCConverter.idleLoss").unwrap_or(0.0),
switching_loss: cc.get_f64("ACDCConverter.switchingLoss").unwrap_or(0.0),
resistive_loss: cc.get_f64("ACDCConverter.resistiveLoss").unwrap_or(0.0),
base_s: cc
.get_f64("ACDCConverter.baseS")
.ok_or_else(|| missing("CsConverter", mrid, "baseS"))?,
rated_udc: cc
.get_f64("ACDCConverter.ratedUdc")
.ok_or_else(|| missing("CsConverter", mrid, "ratedUdc"))?,
q_mw: cc.get_f64("ACDCConverter.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,
}))
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum CgmesTopologyMode {
Auto,
#[default]
BusBranchFromTp,
NodeBreakerFromEq,
}
#[derive(Clone, Debug)]
pub struct CgmesNodeBreaker {
pub topology: crate::topology::NodeBreakerTopology,
pub node_mrids: Vec<String>,
pub switch_mrids: Vec<String>,
pub tn_of_node: Vec<Option<String>>,
}
pub fn cgmes_node_breaker_topology(ds: &CimDataset) -> Result<CgmesNodeBreaker, CgmesError> {
use crate::topology::model::{NodeIdx, Switch as TopoSwitch, SwitchKind};
let cn_mrids = by_type(ds, "ConnectivityNode");
let mut node_of_cn: HashMap<&str, usize> = HashMap::with_capacity(cn_mrids.len());
let mut node_mrids: Vec<String> = Vec::with_capacity(cn_mrids.len());
let mut tn_of_node: Vec<Option<String>> = Vec::with_capacity(cn_mrids.len());
for mrid in cn_mrids {
let cn = own(ds, mrid, "ConnectivityNode")?;
node_of_cn.insert(mrid.as_str(), node_mrids.len());
node_mrids.push(mrid.clone());
tn_of_node.push(
cn.get_ref("ConnectivityNode.TopologicalNode")
.map(str::to_string),
);
}
let mut node_of_terminal: HashMap<&str, usize> = HashMap::new();
let mut terminals_of_equipment: HashMap<&str, Vec<(i64, &str)>> = HashMap::new();
for t_mrid in by_type(ds, "Terminal") {
let t = own(ds, t_mrid, "Terminal")?;
if let Some(cn) = t.get_ref("Terminal.ConnectivityNode") {
if let Some(&node) = node_of_cn.get(cn) {
node_of_terminal.insert(t_mrid.as_str(), node);
}
}
if let Some(ce) = t.get_ref("Terminal.ConductingEquipment") {
terminals_of_equipment.entry(ce).or_default().push((
t.get_i64("ACDCTerminal.sequenceNumber").unwrap_or(1),
t_mrid.as_str(),
));
}
}
for list in terminals_of_equipment.values_mut() {
list.sort();
}
let mut topology = crate::topology::NodeBreakerTopology::new(node_mrids.len());
let mut switch_mrids: Vec<String> = Vec::new();
let add = |topology: &mut crate::topology::NodeBreakerTopology,
switch_mrids: &mut Vec<String>,
mrid: &String,
kind: SwitchKind,
in_service: bool,
open: bool| {
let Some(terms) = terminals_of_equipment.get(mrid.as_str()) else {
return;
};
if terms.len() < 2 {
return;
}
let (Some(&a), Some(&b)) = (
node_of_terminal.get(terms[0].1),
node_of_terminal.get(terms[1].1),
) else {
return;
};
topology.add_switch(TopoSwitch {
kind,
nodes: [NodeIdx(a), NodeIdx(b)],
open,
in_service,
});
switch_mrids.push(mrid.clone());
};
for mrid in by_type(ds, "Switch") {
let sw = own(ds, mrid, "Switch")?;
add(
&mut topology,
&mut switch_mrids,
mrid,
SwitchKind::Generic,
equipment_in_service(
sw.get_bool("Equipment.inService"),
sw.get_bool("Equipment.normallyInService"),
),
sw.get_bool("Switch.open").unwrap_or(false),
);
}
for mrid in by_type(ds, "Breaker") {
let br = own(ds, mrid, "Breaker")?;
add(
&mut topology,
&mut switch_mrids,
mrid,
SwitchKind::Breaker,
equipment_in_service(
br.get_bool("Equipment.inService"),
br.get_bool("Equipment.normallyInService"),
),
br.get_bool("Switch.open").unwrap_or(false),
);
}
for mrid in by_type(ds, "LoadBreakSwitch") {
let lbs = own(ds, mrid, "LoadBreakSwitch")?;
add(
&mut topology,
&mut switch_mrids,
mrid,
SwitchKind::LoadBreakSwitch,
equipment_in_service(
lbs.get_bool("Equipment.inService"),
lbs.get_bool("Equipment.normallyInService"),
),
lbs.get_bool("Switch.open").unwrap_or(false),
);
}
for mrid in by_type(ds, "DisconnectingCircuitBreaker") {
let dcb = own(ds, mrid, "DisconnectingCircuitBreaker")?;
add(
&mut topology,
&mut switch_mrids,
mrid,
SwitchKind::DisconnectingCircuitBreaker,
equipment_in_service(
dcb.get_bool("Equipment.inService"),
dcb.get_bool("Equipment.normallyInService"),
),
dcb.get_bool("Switch.open").unwrap_or(false),
);
}
for mrid in by_type(ds, "Disconnector") {
let d = own(ds, mrid, "Disconnector")?;
add(
&mut topology,
&mut switch_mrids,
mrid,
SwitchKind::Disconnector,
equipment_in_service(
d.get_bool("Equipment.inService"),
d.get_bool("Equipment.normallyInService"),
),
d.get_bool("Switch.open").unwrap_or(false),
);
}
for mrid in by_type(ds, "GroundDisconnector") {
let g = own(ds, mrid, "GroundDisconnector")?;
add(
&mut topology,
&mut switch_mrids,
mrid,
SwitchKind::GroundDisconnector,
equipment_in_service(
g.get_bool("Equipment.inService"),
g.get_bool("Equipment.normallyInService"),
),
g.get_bool("Switch.open").unwrap_or(false),
);
}
for mrid in by_type(ds, "Jumper") {
let j = own(ds, mrid, "Jumper")?;
add(
&mut topology,
&mut switch_mrids,
mrid,
SwitchKind::Jumper,
equipment_in_service(
j.get_bool("Equipment.inService"),
j.get_bool("Equipment.normallyInService"),
),
j.get_bool("Switch.open").unwrap_or(false),
);
}
for mrid in by_type(ds, "Cut") {
let c = own(ds, mrid, "Cut")?;
add(
&mut topology,
&mut switch_mrids,
mrid,
SwitchKind::Cut,
equipment_in_service(
c.get_bool("Equipment.inService"),
c.get_bool("Equipment.normallyInService"),
),
c.get_bool("Switch.open").unwrap_or(false),
);
}
for mrid in by_type(ds, "Fuse") {
let f = own(ds, mrid, "Fuse")?;
add(
&mut topology,
&mut switch_mrids,
mrid,
SwitchKind::Fuse,
equipment_in_service(
f.get_bool("Equipment.inService"),
f.get_bool("Equipment.normallyInService"),
),
f.get_bool("Switch.open").unwrap_or(false),
);
}
for mrid in by_type(ds, "Junction") {
let j = own(ds, mrid, "Junction")?;
add(
&mut topology,
&mut switch_mrids,
mrid,
SwitchKind::Junction,
equipment_in_service(
j.get_bool("Equipment.inService"),
j.get_bool("Equipment.normallyInService"),
),
false,
);
}
for mrid in by_type(ds, "BusbarSection") {
let Some(terms) = terminals_of_equipment.get(mrid.as_str()) else {
continue;
};
for &(_, t) in terms {
if let Some(&node) = node_of_terminal.get(t) {
topology.busbars.push(NodeIdx(node));
}
}
}
topology.busbars.sort_unstable();
topology.busbars.dedup();
Ok(CgmesNodeBreaker {
topology,
node_mrids,
switch_mrids,
tn_of_node,
})
}
fn connectivity_node_voltage(ds: &CimDataset, cn_mrid: &str) -> Option<f64> {
let cn = get(ds, cn_mrid, "ConnectivityNode")?;
let container = cn.get_ref("ConnectivityNode.ConnectivityNodeContainer")?;
let vl_mrid = match get(ds, container, "VoltageLevel") {
Some(_) => container.to_string(),
None => {
let bay = get(ds, container, "Bay")?;
bay.get_ref("Bay.VoltageLevel")?.to_string()
}
};
let vl = get(ds, &vl_mrid, "VoltageLevel")?;
let bv = get(ds, vl.get_ref("VoltageLevel.BaseVoltage")?, "BaseVoltage")?;
Some(bv.get_f64("BaseVoltage.nominalVoltage")? * 1e3)
}
fn build_node_breaker_skeleton(
ds: &CimDataset,
policy: &crate::topology::RetentionPolicy,
) -> Result<
(
Vec<Bus>,
HashMap<String, usize>,
TerminalIndex,
CgmesNodeBreaker,
crate::topology::BusView,
),
CgmesError,
> {
use crate::topology::model::{BusIdx, NodeIdx};
let nb = cgmes_node_breaker_topology(ds)?;
if nb.topology.n_nodes == 0 {
return Err(CgmesError::NoTopologicalNodes);
}
let view = crate::topology::bus_view(&nb.topology, policy);
let mut buses: Vec<Bus> = (0..view.n_buses())
.map(|b| {
let u_rated = view
.nodes_of(BusIdx(b))
.iter()
.find_map(|n| connectivity_node_voltage(ds, &nb.node_mrids[n.0]))
.unwrap_or(0.0);
Bus {
idx: b,
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(),
}
})
.collect();
let mut node_of_cn: HashMap<&str, usize> = HashMap::with_capacity(nb.node_mrids.len());
for (i, mrid) in nb.node_mrids.iter().enumerate() {
node_of_cn.insert(mrid.as_str(), i);
}
let mut raw: HashMap<String, Vec<(i64, String)>> = HashMap::new();
let mut bus_of: HashMap<String, usize> = HashMap::new();
let mut connected_of: HashMap<String, bool> = HashMap::new();
let mut unresolved: Vec<String> = Vec::new();
for t_mrid in by_type(ds, "Terminal") {
let t = own(ds, t_mrid, "Terminal")?;
connected_of.insert(
t_mrid.clone(),
t.get_bool("ACDCTerminal.connected").unwrap_or(true),
);
if let Some(ce) = t.get_ref("Terminal.ConductingEquipment") {
let seq = t.get_i64("ACDCTerminal.sequenceNumber").unwrap_or(1);
raw.entry(ce.to_string())
.or_default()
.push((seq, t_mrid.clone()));
}
match t
.get_ref("Terminal.ConnectivityNode")
.and_then(|cn| node_of_cn.get(cn))
{
Some(&node) => {
bus_of.insert(t_mrid.clone(), view.bus_of(NodeIdx(node)).0);
}
None => unresolved.push(t_mrid.clone()),
}
}
for t_mrid in unresolved {
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: 0.0,
zip_terms: Vec::new(),
});
bus_of.insert(t_mrid, 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();
let mut idx_of: HashMap<String, usize> = HashMap::new();
for (node, tn) in nb.tn_of_node.iter().enumerate() {
if let Some(tn) = tn {
idx_of.insert(tn.clone(), view.bus_of(NodeIdx(node)).0);
}
}
Ok((
buses,
idx_of,
TerminalIndex {
by_equipment,
bus_of,
connected_of,
},
nb,
view,
))
}
pub fn cgmes_node_breaker_to_buses_and_branches(
ds: &CimDataset,
s_base_va: f64,
policy: &crate::topology::RetentionPolicy,
treatment: crate::switches::SwitchTreatment,
) -> Result<crate::switches::NodeBreakerNetwork, CgmesError> {
let (buses, idx_of, terms, nb, view) = build_node_breaker_skeleton(ds, policy)?;
let mut zero_injection = zero_injection_flags(ds, &terms, buses.len());
let converted = convert_equipment(ds, s_base_va, (buses, idx_of, terms))?;
let CgmesNetwork {
buses,
lines,
transformers,
shunts,
..
} = converted;
zero_injection.resize(buses.len(), true);
if treatment == crate::switches::SwitchTreatment::Merge && !view.retained().is_empty() {
return Err(CgmesError::UnsupportedTransformer {
mrid: "(retention policy)".to_string(),
reason: "SwitchTreatment::Merge cannot represent a retained switch; use \
RetentionPolicy::MergeAll or SwitchTreatment::Regularize"
.to_string(),
});
}
Ok(crate::switches::NodeBreakerNetwork::new(
buses,
lines,
transformers,
shunts,
view,
nb.topology,
nb.switch_mrids,
zero_injection,
treatment,
))
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct EquipmentLimits {
pub terminals: Vec<crate::ratings::BranchLimits>,
}
impl EquipmentLimits {
pub fn terminal(&self, which: usize) -> crate::ratings::BranchLimits {
self.terminals.get(which).cloned().unwrap_or_default()
}
pub fn tightest_patl(&self) -> Option<f64> {
self.terminals
.iter()
.filter_map(|t| t.patl_a)
.fold(None, |acc, v| {
Some(match acc {
Some(a) => f64::min(a, v),
None => v,
})
})
}
}
pub fn cgmes_operational_limits(
ds: &CimDataset,
) -> Result<(HashMap<String, EquipmentLimits>, LimitImportReport), CgmesError> {
use crate::ratings::{BranchLimits, TemporaryLimit};
let mut position_of: HashMap<String, (String, usize)> = HashMap::new();
let mut by_equipment: HashMap<String, Vec<(i64, String)>> = HashMap::new();
for mrid in by_type(ds, "Terminal") {
let Some(t) = get(ds, mrid, "Terminal") else {
continue;
};
let Some(ce) = t.get_ref("Terminal.ConductingEquipment") else {
continue;
};
by_equipment.entry(ce.to_string()).or_default().push((
t.get_i64("ACDCTerminal.sequenceNumber").unwrap_or(1),
mrid.clone(),
));
}
let mut terminal_count: HashMap<String, usize> = HashMap::new();
for (equipment, mut terms) in by_equipment {
terms.sort_by_key(|(seq, _)| *seq);
terminal_count.insert(equipment.clone(), terms.len());
for (i, (_, t_mrid)) in terms.into_iter().enumerate() {
position_of.insert(t_mrid, (equipment.clone(), i));
}
}
let mut set_targets: HashMap<String, (String, Vec<usize>)> = HashMap::new();
for mrid in by_type(ds, "OperationalLimitSet") {
let Some(set) = get(ds, mrid, "OperationalLimitSet") else {
continue;
};
if let Some(t) = set.get_ref("OperationalLimitSet.Terminal") {
if let Some((equipment, which)) = position_of.get(t) {
set_targets.insert(mrid.clone(), (equipment.clone(), vec![*which]));
continue;
}
}
if let Some(e) = set.get_ref("OperationalLimitSet.Equipment") {
let n = terminal_count.get(e).copied().unwrap_or(0);
if n > 0 {
set_targets.insert(mrid.clone(), (e.to_string(), (0..n).collect()));
}
}
}
let mut limits: HashMap<String, EquipmentLimits> = HashMap::new();
let mut report = LimitImportReport::default();
for mrid in by_type(ds, "CurrentLimit") {
let Some(limit) = get(ds, mrid, "CurrentLimit") else {
continue;
};
let Some(value) = limit
.get_f64("CurrentLimit.value")
.or(limit.get_f64("CurrentLimit.normalValue"))
else {
report.without_value += 1;
continue;
};
let Some(set_ref) = limit.get_ref("OperationalLimit.OperationalLimitSet") else {
report.unattached += 1;
continue;
};
let Some((equipment, which)) = set_targets.get(set_ref) else {
report.unattached += 1;
continue;
};
let (duration, permanent) = match limit.get_ref("OperationalLimit.OperationalLimitType") {
Some(t) => match get(ds, t, "OperationalLimitType") {
Some(kind) => {
let patl = kind
.get_bool("OperationalLimitType.isInfiniteDuration")
.unwrap_or(false)
|| kind
.get_ref("OperationalLimitType.kind")
.is_some_and(|k| k.rsplit('.').next() == Some("patl"));
(
kind.get_f64("OperationalLimitType.acceptableDuration"),
patl || kind
.get_f64("OperationalLimitType.acceptableDuration")
.is_none(),
)
}
None => (None, true),
},
None => (None, true),
};
let entry = limits.entry(equipment.clone()).or_default();
let needed = which.iter().copied().max().map(|m| m + 1).unwrap_or(0);
if entry.terminals.len() < needed {
entry.terminals.resize(needed, BranchLimits::default());
}
for &i in which {
let slot = &mut entry.terminals[i];
if permanent {
slot.patl_a = Some(match slot.patl_a {
Some(existing) => existing.min(value),
None => value,
});
} else {
slot.tatl.push(TemporaryLimit {
acceptable_duration_s: duration,
value_a: value,
});
}
}
report.current_limits += 1;
}
for name in ["ActivePowerLimit", "ApparentPowerLimit", "VoltageLimit"] {
report.skipped += by_type(ds, name).len();
}
Ok((limits, report))
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct LimitImportReport {
pub current_limits: usize,
pub unattached: usize,
pub without_value: usize,
pub skipped: usize,
}
#[cfg(test)]
mod voltage_control_tests {
use super::*;
fn reg(bus: usize, target: f64, q_min: f64, q_max: f64, id: &str) -> Regulation<'_> {
Regulation {
controlled_bus: bus,
target_pu: target,
q_min,
q_max,
id,
at_bus: Some(bus),
q_scheduled: 0.0,
}
}
fn bus(idx: usize) -> Bus {
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: 400e3,
zip_terms: Vec::new(),
}
}
#[test]
fn two_machines_on_one_bus_sum_their_capability() {
let mut buses = vec![bus(0), bus(1)];
let mut vc = VoltageControl::new(2);
vc.regulate(&mut buses, reg(1, 1.02, -2.0, 3.0, "a"));
assert_eq!(buses[1].bus_type, BusType::PV);
assert_eq!(
(buses[1].q_min, buses[1].q_max),
(-2.0, 3.0),
"the first assigns"
);
vc.regulate(&mut buses, reg(1, 1.02, -1.5, 4.0, "b"));
assert_eq!(
(buses[1].q_min, buses[1].q_max),
(-3.5, 7.0),
"the second adds"
);
let report = vc.finish(&buses);
assert_eq!(report.regulated_buses, 1);
assert_eq!(report.shared_buses, 1);
assert!(report.target_conflicts.is_empty());
}
#[test]
fn an_unlimited_machine_makes_the_joint_capability_unlimited() {
let mut buses = vec![bus(0)];
let mut vc = VoltageControl::new(1);
vc.regulate(&mut buses, reg(0, 1.0, -2.0, 3.0, "a"));
vc.regulate(&mut buses, reg(0, 1.0, -f64::INFINITY, f64::INFINITY, "b"));
assert!(buses[0].q_min.is_infinite() && buses[0].q_min < 0.0);
assert!(buses[0].q_max.is_infinite() && buses[0].q_max > 0.0);
assert!(!buses[0].q_min.is_nan() && !buses[0].q_max.is_nan());
}
#[test]
fn one_machine_is_left_exactly_as_it_was() {
let mut buses = vec![bus(0)];
let mut vc = VoltageControl::new(1);
vc.regulate(&mut buses, reg(0, 1.05, -1.0, 1.0, "only"));
assert_eq!((buses[0].q_min, buses[0].q_max), (-1.0, 1.0));
assert_eq!(buses[0].voltage_mag, 1.05);
let report = vc.finish(&buses);
assert_eq!(report.shared_buses, 0);
}
#[test]
fn a_disagreeing_target_is_reported_and_the_last_still_wins() {
let mut buses = vec![bus(0)];
let mut vc = VoltageControl::new(1);
vc.regulate(&mut buses, reg(0, 1.02, -1.0, 1.0, "first"));
vc.regulate(&mut buses, reg(0, 1.06, -1.0, 1.0, "second"));
assert_eq!(buses[0].voltage_mag, 1.06, "last writer still wins");
let report = vc.finish(&buses);
assert_eq!(report.target_conflicts.len(), 1);
let c = &report.target_conflicts[0];
assert_eq!(
(c.bus, c.existing, c.proposed, c.id.as_str()),
(0, 1.02, 1.06, "second")
);
assert_eq!((buses[0].q_min, buses[0].q_max), (-2.0, 2.0));
}
#[test]
fn a_negligible_difference_is_not_a_conflict() {
let mut buses = vec![bus(0)];
let mut vc = VoltageControl::new(1);
vc.regulate(&mut buses, reg(0, 1.02, -1.0, 1.0, "first"));
vc.regulate(&mut buses, reg(0, 1.02 + 1e-12, -1.0, 1.0, "second"));
assert!(vc.finish(&buses).target_conflicts.is_empty());
}
}