use std::collections::HashMap;
use std::path::Path;
use num_complex::Complex;
use quick_xml::Reader;
use quick_xml::events::Event;
use crate::network::ShuntAdm;
use crate::ratings::{BranchLimits, TemporaryLimit};
use crate::topology::bus_view::{BusView, RetentionPolicy, bus_view};
use crate::topology::model::{NodeBreakerTopology, NodeIdx, Switch, SwitchKind};
use crate::types::{Bus, BusType, Line, TapChanger, Transformer};
#[cfg(feature = "dynamics")]
pub mod dynamics;
pub const DEFAULT_BASE_MVA: f64 = 100.0;
#[derive(Debug)]
pub enum IidmError {
Io(std::io::Error),
Xml(String),
UnknownReference {
element: String,
reference: String,
},
NoVoltageLevels,
}
impl std::fmt::Display for IidmError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IidmError::Io(e) => write!(f, "reading IIDM file: {e}"),
IidmError::Xml(e) => write!(f, "malformed IIDM XML: {e}"),
IidmError::UnknownReference { element, reference } => {
write!(f, "`{element}` refers to undeclared `{reference}`")
}
IidmError::NoVoltageLevels => write!(f, "document declares no voltage levels"),
}
}
}
impl std::error::Error for IidmError {}
impl From<std::io::Error> for IidmError {
fn from(e: std::io::Error) -> Self {
IidmError::Io(e)
}
}
impl From<quick_xml::Error> for IidmError {
fn from(e: quick_xml::Error) -> Self {
IidmError::Xml(e.to_string())
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub enum SlackPolicy {
#[default]
LargestGeneration,
Bus(String),
}
#[derive(Clone, Debug)]
pub struct IidmOptions {
pub base_mva: f64,
pub slack: SlackPolicy,
pub retention: RetentionPolicy,
}
impl Default for IidmOptions {
fn default() -> Self {
Self {
base_mva: DEFAULT_BASE_MVA,
slack: SlackPolicy::default(),
retention: RetentionPolicy::MergeAll,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct IidmAreas {
pub of_bus: Vec<Option<usize>>,
pub targets: Vec<f64>,
pub ids: Vec<(String, String)>,
pub report: IidmAreaReport,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct IidmAreaReport {
pub areas: usize,
pub other_types: usize,
pub without_target: Vec<usize>,
pub unknown_voltage_levels: usize,
pub boundaries_ignored: usize,
pub contested_buses: usize,
pub unassigned_buses: usize,
}
#[derive(Debug)]
pub struct IidmImport {
pub buses: Vec<Bus>,
pub lines: Vec<Line>,
pub transformers: Vec<Transformer>,
pub shunts: Vec<ShuntAdm>,
pub bus_labels: Vec<String>,
pub branch_ids: Vec<String>,
pub limits: Vec<[BranchLimits; 2]>,
pub tap_changers: Vec<Option<TapChanger>>,
pub areas: IidmAreas,
pub regulation: Vec<crate::outerloop::TapRegulation>,
pub topology: NodeBreakerTopology,
pub view: BusView,
pub switch_ids: Vec<String>,
pub slack: usize,
pub base_mva: f64,
pub version: Option<String>,
pub notes: Vec<String>,
pub disconnected: Vec<String>,
pub injections: Vec<IidmInjection>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct IidmInjection {
pub id: String,
pub bus: usize,
pub generator: bool,
pub p: f64,
pub q: f64,
}
impl IidmImport {
pub fn s_base_va(&self) -> f64 {
self.base_mva * 1e6
}
pub fn n_branches(&self) -> usize {
self.lines.len() + self.transformers.len()
}
pub fn transformer_branch(&self, i: usize) -> usize {
self.lines.len() + i
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
struct NodeKey {
voltage_level: String,
key: String,
}
#[derive(Clone, Debug)]
struct RawVoltageLevel {
id: String,
nominal_v: f64,
}
#[derive(Clone, Debug)]
struct RawSwitch {
id: String,
a: NodeKey,
b: NodeKey,
kind: SwitchKind,
open: bool,
}
#[derive(Clone, Debug, Default)]
struct RawLimits {
permanent: Option<f64>,
temporary: Vec<TemporaryLimit>,
}
#[derive(Clone, Debug)]
struct RawBranch {
id: String,
end1: Option<NodeKey>,
end2: Option<NodeKey>,
r: f64,
x: f64,
g1: f64,
b1: f64,
g2: f64,
b2: f64,
limits: [RawLimits; 2],
transformer: Option<RawTransformerFields>,
}
#[derive(Clone, Debug, Default)]
struct RawTransformerFields {
rated_u1: f64,
rated_u2: f64,
ratio: Option<RawTapChanger>,
phase: Option<RawTapChanger>,
}
#[derive(Clone, Debug, Default)]
struct RawTapChanger {
low: i32,
position: i32,
steps: Vec<(f64, f64)>,
regulating: bool,
mode: Option<String>,
target: Option<f64>,
deadband: Option<f64>,
}
#[derive(Clone, Debug)]
struct RawInjection {
at: NodeKey,
id: String,
generator: bool,
p: f64,
q: f64,
regulates: Option<f64>,
generation: f64,
q_min: f64,
q_max: f64,
}
#[derive(Clone, Debug)]
struct RawShunt {
at: NodeKey,
g: f64,
b: f64,
sections: f64,
}
struct Attrs(HashMap<String, String>);
impl Attrs {
fn of(e: &quick_xml::events::BytesStart<'_>) -> Result<Self, IidmError> {
let mut map = HashMap::new();
for a in e.attributes() {
let a = a.map_err(|e| IidmError::Xml(e.to_string()))?;
let key = String::from_utf8_lossy(a.key.local_name().as_ref()).into_owned();
let raw = String::from_utf8_lossy(&a.value);
let value = quick_xml::escape::unescape(&raw)
.map_err(|e| IidmError::Xml(e.to_string()))?
.into_owned();
map.insert(key, value);
}
Ok(Self(map))
}
fn text(&self, key: &str) -> Option<&str> {
self.0.get(key).map(|s| s.as_str())
}
fn string(&self, key: &str) -> Option<String> {
self.0.get(key).cloned()
}
fn number(&self, key: &str) -> Option<f64> {
self.0.get(key).and_then(|s| s.parse::<f64>().ok())
}
fn or_zero(&self, key: &str) -> f64 {
self.number(key).unwrap_or(0.0)
}
fn integer(&self, key: &str) -> Option<i32> {
self.0.get(key).and_then(|s| s.parse::<i32>().ok())
}
fn flag(&self, key: &str) -> Option<bool> {
self.0.get(key).and_then(|s| s.parse::<bool>().ok())
}
fn terminal(&self, suffix: &str, fallback_level: Option<&str>) -> Option<NodeKey> {
let level = self
.string(&format!("voltageLevelId{suffix}"))
.or_else(|| fallback_level.map(|s| s.to_string()))?;
if let Some(node) = self.text(&format!("node{suffix}")) {
return Some(NodeKey {
voltage_level: level,
key: format!("#{node}"),
});
}
let bus = self.text(&format!("bus{suffix}"))?;
Some(NodeKey {
voltage_level: level,
key: bus.to_string(),
})
}
}
fn boundary_key(attrs: &Attrs, id: &str) -> NodeKey {
let key = attrs
.string("pairingKey")
.or_else(|| attrs.string("ucteXnodeCode"))
.unwrap_or_else(|| id.to_string());
NodeKey {
voltage_level: BOUNDARY_LEVEL.to_string(),
key,
}
}
const BOUNDARY_LEVEL: &str = "\u{0}boundary";
fn switch_kind(name: Option<&str>) -> SwitchKind {
match name.unwrap_or("") {
"BREAKER" => SwitchKind::Breaker,
"DISCONNECTOR" => SwitchKind::Disconnector,
"LOAD_BREAK_SWITCH" => SwitchKind::LoadBreakSwitch,
_ => SwitchKind::Generic,
}
}
pub fn read(path: impl AsRef<Path>) -> Result<IidmImport, IidmError> {
read_with(path, &IidmOptions::default())
}
pub fn read_with(path: impl AsRef<Path>, options: &IidmOptions) -> Result<IidmImport, IidmError> {
let text = std::fs::read_to_string(path)?;
parse_with(&text, options)
}
pub fn parse(text: &str) -> Result<IidmImport, IidmError> {
parse_with(text, &IidmOptions::default())
}
#[derive(Clone, Debug, Default)]
struct RawArea {
id: String,
name: String,
area_type: String,
target_mw: Option<f64>,
voltage_levels: Vec<String>,
boundaries: usize,
}
#[derive(Default)]
struct Collected {
version: Option<String>,
levels: Vec<RawVoltageLevel>,
areas: Vec<RawArea>,
open_area: Option<RawArea>,
in_curve: bool,
curve_envelopes: usize,
nodes: Vec<NodeKey>,
switches: Vec<RawSwitch>,
branches: Vec<RawBranch>,
injections: Vec<RawInjection>,
shunts: Vec<RawShunt>,
unknown: HashMap<String, usize>,
}
impl Collected {
fn declare(&mut self, key: &NodeKey) {
if !self.nodes.contains(key) {
self.nodes.push(key.clone());
}
}
}
pub fn parse_with(text: &str, options: &IidmOptions) -> Result<IidmImport, IidmError> {
let mut collected = collect(text)?;
if collected.levels.is_empty() {
return Err(IidmError::NoVoltageLevels);
}
convert(&mut collected, options)
}
fn collect(text: &str) -> Result<Collected, IidmError> {
let mut reader = Reader::from_str(text);
reader.config_mut().trim_text(true);
let mut c = Collected::default();
let mut buf = Vec::new();
let mut level: Option<String> = None;
let mut branch: Option<RawBranch> = None;
let mut limit_side: Option<usize> = None;
let mut tap_target: Option<bool> = None;
loop {
let event = reader.read_event_into(&mut buf);
let (start, empty) = match &event {
Ok(Event::Start(e)) => (Some(e.clone()), false),
Ok(Event::Empty(e)) => (Some(e.clone()), true),
Ok(Event::End(e)) => {
let name = String::from_utf8_lossy(e.local_name().as_ref()).into_owned();
close(
&name,
&mut level,
&mut branch,
&mut limit_side,
&mut tap_target,
&mut c,
);
buf.clear();
continue;
}
Ok(Event::Eof) => break,
Ok(_) => {
buf.clear();
continue;
}
Err(e) => return Err(IidmError::Xml(e.to_string())),
};
let e = start.expect("start or empty");
let name = String::from_utf8_lossy(e.local_name().as_ref()).into_owned();
let attrs = Attrs::of(&e)?;
open(
&name,
&attrs,
empty,
&mut level,
&mut branch,
&mut limit_side,
&mut tap_target,
&mut c,
);
if empty {
close(
&name,
&mut level,
&mut branch,
&mut limit_side,
&mut tap_target,
&mut c,
);
}
buf.clear();
}
Ok(c)
}
fn suffix_side(name: &str) -> Option<usize> {
match name.chars().last() {
Some('1') => Some(0),
Some('2') => Some(1),
_ => None,
}
}
#[allow(clippy::too_many_arguments)]
fn open(
name: &str,
attrs: &Attrs,
_empty: bool,
level: &mut Option<String>,
branch: &mut Option<RawBranch>,
limit_side: &mut Option<usize>,
tap_target: &mut Option<bool>,
c: &mut Collected,
) {
match name {
"network" => {
let mut versions: Vec<String> = attrs
.0
.values()
.filter(|v| !v.contains("/ext/"))
.filter_map(|v| v.rsplit_once("/schema/iidm/").map(|(_, x)| x.to_string()))
.filter(|v| v.starts_with('1'))
.collect();
versions.sort();
c.version = versions.into_iter().next();
}
"area" => {
c.open_area = Some(RawArea {
id: attrs.string("id").unwrap_or_default(),
name: attrs.string("name").unwrap_or_default(),
area_type: attrs.string("areaType").unwrap_or_default(),
target_mw: attrs.number("interchangeTarget"),
..RawArea::default()
});
}
"voltageLevelRef" => {
if let (Some(a), Some(id)) = (c.open_area.as_mut(), attrs.string("id")) {
a.voltage_levels.push(id);
}
}
"areaBoundary" => {
if let Some(a) = c.open_area.as_mut() {
a.boundaries += 1;
}
}
"voltageLevel" => {
let id = attrs.string("id").unwrap_or_default();
c.levels.push(RawVoltageLevel {
id: id.clone(),
nominal_v: attrs.number("nominalV").unwrap_or(1.0) * 1000.0,
});
*level = Some(id);
}
"bus" | "busbarSection" => {
let Some(vl) = level.clone() else { return };
if attrs.text("nodes").is_some() {
return;
}
let key = match attrs.text("node") {
Some(n) => format!("#{n}"),
None => match attrs.string("id") {
Some(id) => id,
None => return,
},
};
c.declare(&NodeKey {
voltage_level: vl,
key,
});
}
"switch" => {
let Some(vl) = level.clone() else { return };
let (Some(a), Some(b)) = (
attrs.terminal("1", Some(&vl)),
attrs.terminal("2", Some(&vl)),
) else {
return;
};
c.declare(&a);
c.declare(&b);
c.switches.push(RawSwitch {
id: attrs.string("id").unwrap_or_default(),
a,
b,
kind: switch_kind(attrs.text("kind")),
open: attrs.flag("open").unwrap_or(false),
});
}
"line" | "twoWindingsTransformer" => {
let end1 = attrs.terminal("1", None);
let end2 = attrs.terminal("2", None);
if let Some(k) = &end1 {
c.declare(k);
}
if let Some(k) = &end2 {
c.declare(k);
}
let transformer = (name == "twoWindingsTransformer").then(|| RawTransformerFields {
rated_u1: attrs.or_zero("ratedU1") * 1000.0,
rated_u2: attrs.or_zero("ratedU2") * 1000.0,
..Default::default()
});
let (g1, b1, g2, b2) = if transformer.is_some() {
(0.0, 0.0, attrs.or_zero("g"), attrs.or_zero("b"))
} else {
(
attrs.or_zero("g1"),
attrs.or_zero("b1"),
attrs.or_zero("g2"),
attrs.or_zero("b2"),
)
};
*branch = Some(RawBranch {
id: attrs.string("id").unwrap_or_default(),
end1,
end2,
r: attrs.or_zero("r"),
x: attrs.or_zero("x"),
g1,
b1,
g2,
b2,
limits: [RawLimits::default(), RawLimits::default()],
transformer,
});
}
"vscConverterStation" | "lccConverterStation" | "hvdcLine" => {
*c.unknown.entry(name.to_string()).or_insert(0) += 1;
}
"generator" => {
let Some(at) = attrs.terminal("", level.as_deref()) else {
return;
};
c.declare(&at);
let p = attrs.number("targetP").unwrap_or(0.0);
let regulating = attrs.flag("voltageRegulatorOn").unwrap_or(false);
c.injections.push(RawInjection {
at,
id: attrs.string("id").unwrap_or_default(),
generator: true,
p,
q: attrs
.number("targetQ")
.or(attrs.number("reactivePowerSetpoint"))
.unwrap_or(0.0),
regulates: regulating
.then(|| attrs.number("targetV").or(attrs.number("voltageSetpoint")))
.flatten(),
generation: p,
q_min: f64::NEG_INFINITY,
q_max: f64::INFINITY,
});
}
"load" => {
let Some(at) = attrs.terminal("", level.as_deref()) else {
return;
};
c.declare(&at);
c.injections.push(RawInjection {
at,
id: attrs.string("id").unwrap_or_default(),
generator: false,
p: -attrs.or_zero("p0"),
q: -attrs.or_zero("q0"),
regulates: None,
generation: 0.0,
q_min: f64::NEG_INFINITY,
q_max: f64::INFINITY,
});
}
"danglingLine" | "boundaryLine" => {
let Some(at) = attrs.terminal("", level.as_deref()) else {
return;
};
let id = attrs.string("id").unwrap_or_default();
let boundary = boundary_key(attrs, &id);
c.declare(&at);
c.declare(&boundary);
*branch = Some(RawBranch {
id: id.clone(),
end1: Some(at),
end2: Some(boundary.clone()),
r: attrs.or_zero("r"),
x: attrs.or_zero("x"),
g1: attrs.or_zero("g"),
b1: attrs.or_zero("b"),
g2: 0.0,
b2: 0.0,
limits: [RawLimits::default(), RawLimits::default()],
transformer: None,
});
c.injections.push(RawInjection {
at: boundary,
id: id.clone(),
generator: false,
p: attrs.number("generationTargetP").unwrap_or(0.0) - attrs.or_zero("p0"),
q: attrs.number("generationTargetQ").unwrap_or(0.0) - attrs.or_zero("q0"),
regulates: None,
generation: attrs.number("generationTargetP").unwrap_or(0.0),
q_min: f64::NEG_INFINITY,
q_max: f64::INFINITY,
});
}
"tieLine" if attrs.text("danglingLineId1").is_none() => {
let id = attrs.string("id").unwrap_or_default();
let boundary = boundary_key(attrs, &id);
for half in ["1", "2"] {
let Some(at) = attrs.terminal(half, None) else {
continue;
};
c.declare(&at);
c.declare(&boundary);
c.branches.push(RawBranch {
id: attrs
.string(&format!("id_{half}"))
.unwrap_or_else(|| format!("{id}_{half}")),
end1: Some(at),
end2: Some(boundary.clone()),
r: attrs.or_zero(&format!("r_{half}")),
x: attrs.or_zero(&format!("x_{half}")),
g1: attrs.or_zero(&format!("g1_{half}")),
b1: attrs.or_zero(&format!("b1_{half}")),
g2: attrs.or_zero(&format!("g2_{half}")),
b2: attrs.or_zero(&format!("b2_{half}")),
limits: [RawLimits::default(), RawLimits::default()],
transformer: None,
});
}
}
"tieLine" => {}
"shunt" => {
let Some(at) = attrs.terminal("", level.as_deref()) else {
return;
};
c.declare(&at);
let sections = attrs.number("sectionCount").unwrap_or(1.0);
c.shunts.push(RawShunt {
at,
g: 0.0,
b: 0.0,
sections,
});
}
"shuntLinearModel" => {
if let Some(last) = c.shunts.last_mut() {
last.g += attrs.or_zero("gPerSection") * last.sections;
last.b += attrs.or_zero("bPerSection") * last.sections;
}
}
"ratioTapChanger" | "phaseTapChanger" => {
let phase = name == "phaseTapChanger";
*tap_target = Some(phase);
if let Some(b) = branch.as_mut() {
if let Some(t) = b.transformer.as_mut() {
let changer = RawTapChanger {
low: attrs.integer("lowTapPosition").unwrap_or(0),
position: attrs.integer("tapPosition").unwrap_or(0),
steps: Vec::new(),
regulating: attrs.flag("regulating").unwrap_or(false),
mode: attrs.text("regulationMode").map(str::to_string),
target: attrs.number("targetV").or(attrs.number("regulationValue")),
deadband: attrs.number("targetDeadband"),
};
if phase {
t.phase = Some(changer);
} else {
t.ratio = Some(changer);
}
}
}
}
"step" => {
let Some(phase) = *tap_target else { return };
let Some(b) = branch.as_mut() else { return };
let Some(t) = b.transformer.as_mut() else {
return;
};
let target = if phase {
t.phase.as_mut()
} else {
t.ratio.as_mut()
};
if let Some(changer) = target {
changer.steps.push((
attrs.number("rho").unwrap_or(1.0),
attrs.number("alpha").unwrap_or(0.0),
));
}
}
_ if name.starts_with("operationalLimitsGroup") => {
*limit_side = suffix_side(name).or(Some(0));
}
_ if name.starts_with("currentLimits") => {
if let Some(side) = suffix_side(name) {
*limit_side = Some(side);
}
let Some(side) = *limit_side else { return };
if let Some(b) = branch.as_mut() {
b.limits[side].permanent = attrs.number("permanentLimit");
}
}
"temporaryLimit" => {
let Some(side) = *limit_side else { return };
let Some(value) = attrs.number("value") else {
return;
};
if let Some(b) = branch.as_mut() {
b.limits[side].temporary.push(TemporaryLimit {
acceptable_duration_s: attrs.number("acceptableDuration"),
value_a: value,
});
}
}
"minMaxReactiveLimits" => {
if let Some(last) = c.injections.last_mut() {
if let Some(q) = attrs.number("minQ") {
last.q_min = q;
}
if let Some(q) = attrs.number("maxQ") {
last.q_max = q;
}
}
}
"reactiveCapabilityCurve" => {
c.in_curve = true;
c.curve_envelopes += 1;
if let Some(last) = c.injections.last_mut() {
last.q_min = f64::INFINITY;
last.q_max = f64::NEG_INFINITY;
}
}
"point" if c.in_curve => {
if let Some(last) = c.injections.last_mut() {
if let Some(q) = attrs.number("minQ") {
last.q_min = last.q_min.min(q);
}
if let Some(q) = attrs.number("maxQ") {
last.q_max = last.q_max.max(q);
}
}
}
"substation"
| "busBreakerTopology"
| "nodeBreakerTopology"
| "point"
| "property"
| "extension"
| "terminalRef" => {}
other => {
*c.unknown.entry(other.to_string()).or_insert(0) += 1;
}
}
}
fn close(
name: &str,
level: &mut Option<String>,
branch: &mut Option<RawBranch>,
limit_side: &mut Option<usize>,
tap_target: &mut Option<bool>,
c: &mut Collected,
) {
match name {
"area" => {
if let Some(a) = c.open_area.take() {
c.areas.push(a);
}
}
"voltageLevel" => *level = None,
"reactiveCapabilityCurve" => c.in_curve = false,
"line" | "twoWindingsTransformer" | "danglingLine" | "boundaryLine" => {
if let Some(b) = branch.take() {
c.branches.push(b);
}
*limit_side = None;
*tap_target = None;
}
"ratioTapChanger" | "phaseTapChanger" => *tap_target = None,
_ if name.starts_with("operationalLimitsGroup") => *limit_side = None,
_ => {}
}
}
fn convert(c: &mut Collected, options: &IidmOptions) -> Result<IidmImport, IidmError> {
let s_base_va = options.base_mva * 1e6;
let nominal: HashMap<&str, f64> = c
.levels
.iter()
.map(|l| (l.id.as_str(), l.nominal_v))
.collect();
let node_of: HashMap<&NodeKey, usize> =
c.nodes.iter().enumerate().map(|(i, k)| (k, i)).collect();
let mut topology = NodeBreakerTopology::new(c.nodes.len());
let mut switch_ids = Vec::new();
for s in &c.switches {
let (Some(&a), Some(&b)) = (node_of.get(&s.a), node_of.get(&s.b)) else {
continue;
};
topology.add_switch(Switch {
kind: s.kind,
nodes: [NodeIdx(a), NodeIdx(b)],
open: s.open,
in_service: true,
});
switch_ids.push(s.id.clone());
}
let view = bus_view(&topology, &options.retention);
let mut boundary_nominal: HashMap<&str, f64> = HashMap::new();
for raw in &c.branches {
let (Some(k1), Some(k2)) = (&raw.end1, &raw.end2) else {
continue;
};
for (boundary, real) in [(k1, k2), (k2, k1)] {
if boundary.voltage_level != BOUNDARY_LEVEL {
continue;
}
if let Some(&v) = nominal.get(real.voltage_level.as_str()) {
boundary_nominal.entry(boundary.key.as_str()).or_insert(v);
}
}
}
let mut areas = IidmAreas::default();
let mut area_of_level: HashMap<&str, usize> = HashMap::new();
let known_levels: std::collections::HashSet<&str> =
c.levels.iter().map(|l| l.id.as_str()).collect();
for raw in &c.areas {
areas.report.boundaries_ignored += raw.boundaries;
if raw.area_type != "ControlArea" {
areas.report.other_types += 1;
continue;
}
let a = areas.ids.len();
areas.ids.push((raw.id.clone(), raw.name.clone()));
match raw.target_mw {
Some(mw) => areas.targets.push(-mw * 1e6 / s_base_va),
None => {
areas.report.without_target.push(a);
areas.targets.push(0.0);
}
}
for vl in &raw.voltage_levels {
if !known_levels.contains(vl.as_str()) {
areas.report.unknown_voltage_levels += 1;
continue;
}
area_of_level.entry(vl.as_str()).or_insert(a);
}
}
areas.report.areas = areas.ids.len();
let n_buses = view.n_buses();
let mut buses: Vec<Bus> = Vec::with_capacity(n_buses);
let mut bus_labels = Vec::with_capacity(n_buses);
for b in 0..n_buses {
let nodes = view.nodes_of(crate::topology::model::BusIdx(b));
let first = nodes.first().map(|n| &c.nodes[n.0]);
let u_rated = first
.and_then(|k| {
if k.voltage_level == BOUNDARY_LEVEL {
boundary_nominal.get(k.key.as_str()).copied()
} else {
nominal.get(k.voltage_level.as_str()).copied()
}
})
.unwrap_or(1.0);
let label = match first {
Some(k) if k.voltage_level == BOUNDARY_LEVEL => k.key.clone(),
Some(k) if nodes.len() == 1 => k.key.clone(),
Some(k) => format!("{}#{}", k.voltage_level, b),
None => format!("bus{b}"),
};
let mut claim: Option<usize> = None;
for n in nodes {
let Some(&a) = area_of_level.get(c.nodes[n.0].voltage_level.as_str()) else {
continue;
};
match claim {
None => claim = Some(a),
Some(existing) if existing != a => areas.report.contested_buses += 1,
Some(_) => {}
}
}
areas.of_bus.push(claim);
buses.push(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::NEG_INFINITY,
q_max: f64::INFINITY,
u_rated,
zip_terms: Vec::new(),
});
bus_labels.push(label);
}
let bus_of =
|key: &NodeKey| -> Option<usize> { node_of.get(key).map(|&n| view.bus_of(NodeIdx(n)).0) };
let mut generation = vec![0.0f64; n_buses];
let mut regulating = vec![false; n_buses];
let mut injections = Vec::with_capacity(c.injections.len());
for inj in &c.injections {
let Some(b) = bus_of(&inj.at) else { continue };
let (p_pu, q_pu) = (inj.p * 1e6 / s_base_va, inj.q * 1e6 / s_base_va);
injections.push(IidmInjection {
id: inj.id.clone(),
bus: b,
generator: inj.generator,
p: p_pu,
q: q_pu,
});
buses[b].p_spec += p_pu;
buses[b].q_spec += q_pu;
generation[b] += inj.generation;
if let Some(target_kv) = inj.regulates {
if target_kv > 0.0 && buses[b].u_rated > 0.0 {
buses[b].bus_type = BusType::PV;
buses[b].voltage_mag = target_kv * 1000.0 / buses[b].u_rated;
regulating[b] = true;
}
}
if inj.q_min.is_finite() {
buses[b].q_min = inj.q_min * 1e6 / s_base_va;
}
if inj.q_max.is_finite() {
buses[b].q_max = inj.q_max * 1e6 / s_base_va;
}
}
let mut shunts = Vec::new();
for sh in &c.shunts {
let Some(b) = bus_of(&sh.at) else { continue };
let z_base = buses[b].u_rated * buses[b].u_rated / s_base_va;
shunts.push(ShuntAdm {
at: b,
y: Complex::new(sh.g * z_base, sh.b * z_base),
});
}
let mut lines = Vec::new();
let mut line_ids = Vec::new();
let mut line_limits = Vec::new();
let mut transformers = Vec::new();
let mut transformer_ids = Vec::new();
let mut transformer_limits = Vec::new();
let mut tap_changers = Vec::new();
let mut tap_regulation: Vec<crate::outerloop::TapRegulation> = Vec::new();
let mut pending_power_regulation: Vec<(usize, usize, f64, f64, String)> = Vec::new();
let mut unsupported_phase_mode = 0usize;
let mut disconnected = Vec::new();
let mut asymmetric = 0usize;
for raw in &c.branches {
let (Some(k1), Some(k2)) = (&raw.end1, &raw.end2) else {
disconnected.push(raw.id.clone());
continue;
};
let (Some(from), Some(to)) = (bus_of(k1), bus_of(k2)) else {
disconnected.push(raw.id.clone());
continue;
};
let limits = [to_limits(&raw.limits[0]), to_limits(&raw.limits[1])];
match &raw.transformer {
None => {
let z_base = buses[to].u_rated * buses[to].u_rated / s_base_va;
let (r, x) = crate::topology::reduction::clamp_branch_impedance(
raw.r / z_base,
raw.x / z_base,
);
if (raw.g1 - raw.g2).abs() > 1e-12 || (raw.b1 - raw.b2).abs() > 1e-12 {
asymmetric += 1;
}
lines.push(Line {
from,
to,
r,
x,
b_shunt: (raw.b1 + raw.b2) * z_base,
g_shunt: (raw.g1 + raw.g2) * z_base,
});
line_ids.push(raw.id.clone());
line_limits.push(limits);
}
Some(t) => {
let z_base = buses[to].u_rated * buses[to].u_rated / s_base_va;
let (r, x) = crate::topology::reduction::clamp_branch_impedance(
raw.r / z_base,
raw.x / z_base,
);
let y_series = Complex::new(1.0, 0.0) / Complex::new(r, x);
let y_shunt = Complex::new(raw.g2 * z_base, raw.b2 * z_base);
let (tap, changer) = build_tap(t, buses[from].u_rated, buses[to].u_rated);
if let Some(reg) = t.ratio.as_ref().filter(|r| r.regulating) {
if let Some(kv) = reg.target {
tap_regulation.push(crate::outerloop::TapRegulation {
transformer: transformers.len(),
controlled_bus: to,
mode: crate::outerloop::RegulationMode::Voltage,
target: kv * 1000.0 / buses[to].u_rated,
deadband: reg.deadband.unwrap_or(0.0) * 1000.0 / buses[to].u_rated,
enabled: true,
id: raw.id.clone(),
});
}
}
if let Some(reg) = t.phase.as_ref().filter(|r| r.regulating) {
let active = reg.mode.as_deref() == Some("ACTIVE_POWER_CONTROL");
if let (true, Some(mw)) = (active, reg.target) {
pending_power_regulation.push((
transformers.len(),
to,
mw,
reg.deadband.unwrap_or(0.0),
raw.id.clone(),
));
} else if !active {
unsupported_phase_mode += 1;
}
}
transformers.push(Transformer {
from,
to,
from_status: 1,
to_status: 1,
y_series,
y_shunt,
tap,
});
transformer_ids.push(raw.id.clone());
transformer_limits.push(limits);
tap_changers.push(changer);
}
}
}
for (transformer, to, mw, deadband, id) in pending_power_regulation {
tap_regulation.push(crate::outerloop::TapRegulation {
transformer,
controlled_bus: to,
mode: crate::outerloop::RegulationMode::ActivePower {
branch: lines.len() + transformer,
terminal: crate::branch_flow::Terminal::To,
},
target: mw / options.base_mva,
deadband: deadband / options.base_mva,
enabled: true,
id,
});
}
areas.report.unassigned_buses = areas.of_bus.iter().filter(|a| a.is_none()).count();
let mut branch_ids = line_ids;
branch_ids.extend(transformer_ids);
let mut limits = line_limits;
limits.extend(transformer_limits);
let mut notes = Vec::new();
if !disconnected.is_empty() {
notes.push(format!(
"{} branch(es) omitted as disconnected",
disconnected.len()
));
}
if unsupported_phase_mode > 0 {
notes.push(format!(
"{unsupported_phase_mode} regulating phase tap changer(s) hold a current rather than \
an active power; no outer loop models that, so their controls are dropped"
));
}
if asymmetric > 0 {
notes.push(format!(
"{asymmetric} line(s) declare asymmetric shunts; summed into one pi-model term"
));
}
if c.curve_envelopes > 0 {
notes.push(format!(
"{} generator(s) state reactive limits as a capability curve; the envelope was taken, \
which is an outer approximation of a P-dependent limit",
c.curve_envelopes
));
}
let mut unknown: Vec<(&String, &usize)> = c.unknown.iter().collect();
unknown.sort();
for (name, count) in unknown {
notes.push(format!("skipped {count} `{name}` element(s)"));
}
let slack = choose_slack(
&buses,
&bus_labels,
&generation,
®ulating,
&options.slack,
&mut notes,
)?;
buses[slack].bus_type = BusType::Slack;
Ok(IidmImport {
buses,
lines,
transformers,
shunts,
bus_labels,
branch_ids,
limits,
tap_changers,
areas,
regulation: tap_regulation,
topology,
view,
switch_ids,
slack,
base_mva: options.base_mva,
version: c.version.clone(),
notes,
disconnected,
injections,
})
}
fn to_limits(raw: &RawLimits) -> BranchLimits {
BranchLimits {
patl_a: raw.permanent,
tatl: raw.temporary.clone(),
}
}
fn build_tap(
t: &RawTransformerFields,
u_from: f64,
u_to: f64,
) -> (Complex<f64>, Option<TapChanger>) {
let nominal = if t.rated_u1 > 0.0 && t.rated_u2 > 0.0 && u_from > 0.0 && u_to > 0.0 {
(t.rated_u1 / u_from) / (t.rated_u2 / u_to)
} else {
1.0
};
let base = Complex::new(nominal, 0.0);
let step_of = |changer: &Option<RawTapChanger>, at: Option<i32>| -> Complex<f64> {
let Some(ch) = changer else {
return Complex::new(1.0, 0.0);
};
let position = at.unwrap_or(ch.position);
let index = position - ch.low;
if index < 0 {
return Complex::new(1.0, 0.0);
}
match ch.steps.get(index as usize) {
Some(&(rho, alpha)) => Complex::from_polar(rho, -alpha.to_radians()),
None => Complex::new(1.0, 0.0),
}
};
let held_ratio = step_of(&t.ratio, None);
let held_phase = step_of(&t.phase, None);
let (source, phase) = match (&t.phase, &t.ratio) {
(Some(p), _) => (p, true),
(None, Some(r)) => (r, false),
(None, None) => return (base * held_ratio * held_phase, None),
};
if source.steps.is_empty() {
return (base * held_ratio * held_phase, None);
}
let steps: Vec<Complex<f64>> = (0..source.steps.len())
.map(|i| {
let at = Some(source.low + i as i32);
if phase {
base * held_ratio * step_of(&t.phase, at)
} else {
base * step_of(&t.ratio, at) * held_phase
}
})
.collect();
let changer = TapChanger {
low: source.low,
position: source
.position
.clamp(source.low, source.low + steps.len() as i32 - 1),
neutral: source.low + steps.len() as i32 / 2,
steps,
series: None,
};
let tap = changer.current().unwrap_or(base);
(tap, Some(changer))
}
fn choose_slack(
buses: &[Bus],
labels: &[String],
generation: &[f64],
regulating: &[bool],
policy: &SlackPolicy,
notes: &mut Vec<String>,
) -> Result<usize, IidmError> {
if let SlackPolicy::Bus(id) = policy {
return labels
.iter()
.position(|l| l == id)
.ok_or_else(|| IidmError::UnknownReference {
element: "slack policy".to_string(),
reference: id.clone(),
});
}
let pick = |want_regulating: bool| -> Option<usize> {
(0..buses.len())
.filter(|&i| regulating[i] == want_regulating && generation[i] > 0.0)
.max_by(|&a, &b| {
generation[a]
.total_cmp(&generation[b])
.then_with(|| labels[b].cmp(&labels[a]))
})
};
let chosen = pick(true).or_else(|| pick(false)).unwrap_or(0);
notes.push(format!(
"slack chosen as `{}` (largest generation)",
labels[chosen]
));
Ok(chosen)
}