use std::collections::BTreeMap;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::Error;
pub type Extras = BTreeMap<String, Value>;
pub const DEFAULT_BASE_FREQUENCY: f64 = 60.0;
fn default_base_frequency() -> f64 {
DEFAULT_BASE_FREQUENCY
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct BusId(pub usize);
impl std::fmt::Display for BusId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
#[repr(u8)]
#[non_exhaustive]
pub enum BusType {
Pq = 1,
Pv = 2,
Ref = 3,
Isolated = 4,
}
impl BusType {
pub(crate) fn from_f64(v: f64) -> Self {
match v as i32 {
2 => Self::Pv,
3 => Self::Ref,
4 => Self::Isolated,
_ => Self::Pq,
}
}
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Pq => "PQ",
Self::Pv => "PV",
Self::Ref => "REF",
Self::Isolated => "ISOLATED",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenCost {
pub model: u8,
pub startup: f64,
pub shutdown: f64,
pub ncost: usize,
pub coeffs: Vec<f64>,
}
impl GenCost {
pub fn quadratic(&self) -> Option<(f64, f64)> {
if self.model != 2 {
return None;
}
if self.coeffs.len() < self.ncost {
return None;
}
match self.ncost {
3 => Some((2.0 * self.coeffs[0], self.coeffs[1])),
2 => Some((0.0, self.coeffs[0])),
1 => Some((0.0, 0.0)),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum SourceFormat {
Matpower,
PowerModelsJson,
EgretJson,
Psse,
PowerWorld,
PandapowerJson,
Pslf,
PowerWorldBinary,
InMemory,
Normalized,
Gridfm,
PypsaCsv,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Network {
pub name: String,
pub base_mva: f64,
#[serde(default = "default_base_frequency")]
pub base_frequency: f64,
pub buses: Vec<Bus>,
pub loads: Vec<Load>,
pub shunts: Vec<Shunt>,
pub branches: Vec<Branch>,
pub generators: Vec<Generator>,
pub storage: Vec<Storage>,
pub hvdc: Vec<Hvdc>,
#[serde(default)]
pub transformers_3w: Vec<Transformer3W>,
#[serde(default)]
pub areas: Vec<Area>,
#[serde(default)]
pub solver: Option<SolverParams>,
pub source_format: SourceFormat,
#[serde(skip)]
pub source: Option<Arc<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bus {
pub id: BusId,
pub kind: BusType,
pub vm: f64,
pub va: f64,
pub base_kv: f64,
pub vmax: f64,
pub vmin: f64,
#[serde(default)]
pub evhi: Option<f64>,
#[serde(default)]
pub evlo: Option<f64>,
pub area: usize,
pub zone: usize,
pub name: Option<String>,
pub extras: Extras,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Load {
pub bus: BusId,
pub p: f64,
pub q: f64,
pub in_service: bool,
pub extras: Extras,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Shunt {
pub bus: BusId,
pub g: f64,
pub b: f64,
pub in_service: bool,
#[serde(default)]
pub control: Option<SwitchedShuntControl>,
pub extras: Extras,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum SwitchedShuntMode {
Locked,
Continuous,
Discrete,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShuntBlock {
pub steps: u32,
pub b: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SwitchedShuntControl {
pub mode: SwitchedShuntMode,
pub vhigh: f64,
pub vlow: f64,
pub control_bus: Option<BusId>,
pub rmpct: f64,
pub blocks: Vec<ShuntBlock>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Branch {
pub from: BusId,
pub to: BusId,
pub r: f64,
pub x: f64,
pub b: f64,
pub rate_a: f64,
pub rate_b: f64,
pub rate_c: f64,
pub tap: f64,
pub shift: f64,
pub in_service: bool,
pub angmin: f64,
pub angmax: f64,
#[serde(default)]
pub control: Option<TransformerControl>,
pub extras: Extras,
}
impl Branch {
#[must_use]
pub fn effective_tap(&self) -> f64 {
if self.tap == 0.0 { 1.0 } else { self.tap }
}
#[must_use]
pub fn is_transformer(&self) -> bool {
self.tap != 0.0 || self.shift != 0.0
}
#[must_use]
pub fn has_angle_limits(&self) -> bool {
self.angmin > -360.0 || self.angmax < 360.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum TransformerControlMode {
Fixed,
Voltage,
ReactiveFlow,
ActiveFlow,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct TransformerControl {
pub mode: TransformerControlMode,
pub controlled_bus: Option<BusId>,
pub tap_min: f64,
pub tap_max: f64,
pub band_min: f64,
pub band_max: f64,
pub ntp: u32,
pub mva_base: f64,
}
impl Default for TransformerControl {
fn default() -> Self {
TransformerControl {
mode: TransformerControlMode::Fixed,
controlled_bus: None,
tap_min: 0.9,
tap_max: 1.1,
band_min: 0.9,
band_max: 1.1,
ntp: 33,
mva_base: 0.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Generator {
pub bus: BusId,
pub pg: f64,
pub qg: f64,
pub pmax: f64,
pub pmin: f64,
pub qmax: f64,
pub qmin: f64,
pub vg: f64,
pub mbase: f64,
pub in_service: bool,
pub cost: Option<GenCost>,
#[serde(default = "default_caps", with = "caps_serde")]
pub caps: GenCaps,
#[serde(default)]
pub regulated_bus: Option<BusId>,
}
impl Generator {
#[must_use]
pub fn has_caps(&self) -> bool {
self.caps.iter().any(Option::is_some)
}
}
pub type GenCaps = [Option<f64>; GEN_EXTRA_KEYS.len()];
fn default_caps() -> GenCaps {
[None; GEN_EXTRA_KEYS.len()]
}
mod caps_serde {
use super::{GEN_EXTRA_KEYS, GenCaps};
use serde::de::{Deserialize, Deserializer};
use serde::ser::{SerializeMap, Serializer};
use std::collections::BTreeMap;
pub(super) fn serialize<S: Serializer>(caps: &GenCaps, s: S) -> Result<S::Ok, S::Error> {
let present = caps.iter().filter(|v| v.is_some()).count();
let mut map = s.serialize_map(Some(present))?;
for (key, slot) in GEN_EXTRA_KEYS.iter().zip(caps.iter()) {
if let Some(value) = slot {
map.serialize_entry(key, value)?;
}
}
map.end()
}
pub(super) fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<GenCaps, D::Error> {
let named = Option::<BTreeMap<String, f64>>::deserialize(d)?.unwrap_or_default();
let mut caps: GenCaps = [None; GEN_EXTRA_KEYS.len()];
for (slot, key) in caps.iter_mut().zip(GEN_EXTRA_KEYS.iter()) {
*slot = named.get(*key).copied();
}
Ok(caps)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Storage {
pub bus: BusId,
pub ps: f64,
pub qs: f64,
pub energy: f64,
pub energy_rating: f64,
pub charge_rating: f64,
pub discharge_rating: f64,
pub charge_efficiency: f64,
pub discharge_efficiency: f64,
pub thermal_rating: f64,
pub qmin: f64,
pub qmax: f64,
pub r: f64,
pub x: f64,
pub p_loss: f64,
pub q_loss: f64,
pub in_service: bool,
pub extras: Extras,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Hvdc {
pub from: BusId,
pub to: BusId,
pub in_service: bool,
pub pf: f64,
pub pt: f64,
pub qf: f64,
pub qt: f64,
pub vf: f64,
pub vt: f64,
pub pmin: f64,
pub pmax: f64,
pub qminf: f64,
pub qmaxf: f64,
pub qmint: f64,
pub qmaxt: f64,
pub loss0: f64,
pub loss1: f64,
pub extras: Extras,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Area {
pub number: usize,
pub slack_bus: Option<BusId>,
pub net_interchange: f64,
pub tolerance: f64,
pub name: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SolverParams {
pub newton_tolerance: Option<f64>,
pub max_iterations: Option<u32>,
pub zero_impedance_threshold: Option<f64>,
pub adjust_taps: Option<bool>,
pub adjust_area_interchange: Option<bool>,
pub adjust_phase_shift: Option<bool>,
pub adjust_dc_taps: Option<bool>,
pub adjust_switched_shunt: Option<bool>,
}
impl SolverParams {
#[must_use]
pub fn is_empty(&self) -> bool {
*self == SolverParams::default()
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Impedance {
pub r: f64,
pub x: f64,
pub base_mva: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Winding {
pub bus: BusId,
pub tap: f64,
pub shift: f64,
pub nominal_kv: f64,
pub rate_a: f64,
pub rate_b: f64,
pub rate_c: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Transformer3W {
pub windings: [Winding; 3],
pub z: [Impedance; 3],
pub star_vm: f64,
pub star_va: f64,
pub mag_g: f64,
pub mag_b: f64,
pub in_service: bool,
pub name: Option<String>,
pub extras: Extras,
}
impl Transformer3W {
#[must_use]
pub fn star_impedances(&self) -> [(f64, f64); 3] {
let [z12, z23, z31] = self.z;
let half = |a: f64, b: f64, c: f64| (a + b - c) / 2.0;
[
(half(z12.r, z31.r, z23.r), half(z12.x, z31.x, z23.x)),
(half(z12.r, z23.r, z31.r), half(z12.x, z23.x, z31.x)),
(half(z23.r, z31.r, z12.r), half(z23.x, z31.x, z12.x)),
]
}
#[must_use]
pub fn star_expansion(&self, star_id: BusId) -> (Bus, [Branch; 3]) {
let star = Bus {
id: star_id,
kind: BusType::Pq,
vm: self.star_vm,
va: self.star_va,
base_kv: self.windings[0].nominal_kv,
vmax: 1.1,
vmin: 0.9,
evhi: None,
evlo: None,
area: 0,
zone: 0,
name: self.name.clone(),
extras: Extras::new(),
};
let zs = self.star_impedances();
let branch = |w: &Winding, (r, x): (f64, f64)| Branch {
from: w.bus,
to: star_id,
r,
x,
b: 0.0,
rate_a: w.rate_a,
rate_b: w.rate_b,
rate_c: w.rate_c,
tap: w.tap,
shift: w.shift,
in_service: self.in_service,
angmin: -360.0,
angmax: 360.0,
control: None,
extras: Extras::new(),
};
let branches = [
branch(&self.windings[0], zs[0]),
branch(&self.windings[1], zs[1]),
branch(&self.windings[2], zs[2]),
];
(star, branches)
}
}
pub(crate) const GEN_EXTRA_KEYS: [&str; 11] = [
"pc1", "pc2", "qc1min", "qc1max", "qc2min", "qc2max", "ramp_agc", "ramp_10", "ramp_30",
"ramp_q", "apf",
];
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct Diagnostic {
pub element: String,
pub field: &'static str,
pub old: f64,
pub new: f64,
pub reason: &'static str,
}
fn repair_vm(vm: f64) -> Option<f64> {
(!vm.is_finite() || vm <= 0.0 || vm > 2.0).then_some(1.0)
}
fn repair_va(va: f64) -> Option<f64> {
(!va.is_finite() || va.abs() > 2000.0).then_some(0.0)
}
fn repair_mbase(mbase: f64, sbase: f64) -> Option<f64> {
(!mbase.is_finite() || mbase <= 0.0).then_some(sbase)
}
fn repair_vg(vg: f64) -> Option<f64> {
(!vg.is_finite() || vg <= 0.0).then_some(1.0)
}
impl Network {
#[must_use]
pub fn in_memory(
name: impl Into<String>,
base_mva: f64,
buses: Vec<Bus>,
branches: Vec<Branch>,
) -> Network {
Network {
name: name.into(),
base_mva,
base_frequency: DEFAULT_BASE_FREQUENCY,
buses,
loads: Vec::new(),
shunts: Vec::new(),
branches,
generators: Vec::new(),
storage: Vec::new(),
hvdc: Vec::new(),
transformers_3w: Vec::new(),
areas: Vec::new(),
solver: None,
source_format: SourceFormat::InMemory,
source: None,
}
}
pub fn to_json(&self) -> crate::Result<String> {
serde_json::to_string(self).map_err(|e| Error::FormatRead {
format: "JSON",
message: e.to_string(),
})
}
#[allow(clippy::too_many_lines)]
pub(crate) fn non_finite_fields(&self) -> Vec<String> {
fn bad<'a>(
fields: impl IntoIterator<Item = (&'a str, f64)>,
) -> impl Iterator<Item = &'a str> {
fields
.into_iter()
.filter_map(|(name, v)| (!v.is_finite()).then_some(name))
}
let mut out = Vec::new();
if !self.base_mva.is_finite() {
out.push("base_mva".into());
}
for (i, b) in self.buses.iter().enumerate() {
#[rustfmt::skip]
let Bus { id: _, kind: _, vm, va, base_kv, vmax, vmin, evhi: _, evlo: _, area: _, zone: _, name: _, extras: _ } = b;
let fields = [
("vm", *vm),
("va", *va),
("base_kv", *base_kv),
("vmax", *vmax),
("vmin", *vmin),
];
out.extend(bad(fields).map(|f| format!("buses[{i}].{f}")));
}
for (i, l) in self.loads.iter().enumerate() {
let Load {
bus: _,
p,
q,
in_service: _,
extras: _,
} = l;
out.extend(bad([("p", *p), ("q", *q)]).map(|f| format!("loads[{i}].{f}")));
}
for (i, s) in self.shunts.iter().enumerate() {
let Shunt {
bus: _,
g,
b,
in_service: _,
control: _,
extras: _,
} = s;
out.extend(bad([("g", *g), ("b", *b)]).map(|f| format!("shunts[{i}].{f}")));
}
for (i, br) in self.branches.iter().enumerate() {
#[rustfmt::skip]
let Branch { from: _, to: _, r, x, b, rate_a, rate_b, rate_c, tap, shift, in_service: _, angmin, angmax, control: _, extras: _ } = br;
let fields = [
("r", *r),
("x", *x),
("b", *b),
("rate_a", *rate_a),
("rate_b", *rate_b),
("rate_c", *rate_c),
("tap", *tap),
("shift", *shift),
("angmin", *angmin),
("angmax", *angmax),
];
out.extend(bad(fields).map(|f| format!("branches[{i}].{f}")));
}
for (i, g) in self.generators.iter().enumerate() {
#[rustfmt::skip]
let Generator { bus: _, pg, qg, pmax, pmin, qmax, qmin, vg, mbase, in_service: _, cost, caps, regulated_bus: _ } = g;
let fields = [
("pg", *pg),
("qg", *qg),
("pmax", *pmax),
("pmin", *pmin),
("qmax", *qmax),
("qmin", *qmin),
("vg", *vg),
("mbase", *mbase),
];
out.extend(bad(fields).map(|f| format!("generators[{i}].{f}")));
if let Some(GenCost {
model: _,
startup,
shutdown,
ncost: _,
coeffs,
}) = cost
{
out.extend(
bad([("startup", *startup), ("shutdown", *shutdown)])
.map(|f| format!("generators[{i}].cost.{f}")),
);
if coeffs.iter().any(|c| !c.is_finite()) {
out.push(format!("generators[{i}].cost.coeffs"));
}
}
for (key, slot) in GEN_EXTRA_KEYS.iter().zip(caps.iter()) {
if matches!(slot, Some(v) if !v.is_finite()) {
out.push(format!("generators[{i}].caps.{key}"));
}
}
}
for (i, s) in self.storage.iter().enumerate() {
#[rustfmt::skip]
let Storage { bus: _, ps, qs, energy, energy_rating, charge_rating, discharge_rating, charge_efficiency, discharge_efficiency, thermal_rating, qmin, qmax, r, x, p_loss, q_loss, in_service: _, extras: _ } = s;
let fields = [
("ps", *ps),
("qs", *qs),
("energy", *energy),
("energy_rating", *energy_rating),
("charge_rating", *charge_rating),
("discharge_rating", *discharge_rating),
("charge_efficiency", *charge_efficiency),
("discharge_efficiency", *discharge_efficiency),
("thermal_rating", *thermal_rating),
("qmin", *qmin),
("qmax", *qmax),
("r", *r),
("x", *x),
("p_loss", *p_loss),
("q_loss", *q_loss),
];
out.extend(bad(fields).map(|f| format!("storage[{i}].{f}")));
}
for (i, h) in self.hvdc.iter().enumerate() {
#[rustfmt::skip]
let Hvdc { from: _, to: _, in_service: _, pf, pt, qf, qt, vf, vt, pmin, pmax, qminf, qmaxf, qmint, qmaxt, loss0, loss1, extras: _ } = h;
let fields = [
("pf", *pf),
("pt", *pt),
("qf", *qf),
("qt", *qt),
("vf", *vf),
("vt", *vt),
("pmin", *pmin),
("pmax", *pmax),
("qminf", *qminf),
("qmaxf", *qmaxf),
("qmint", *qmint),
("qmaxt", *qmaxt),
("loss0", *loss0),
("loss1", *loss1),
];
out.extend(bad(fields).map(|f| format!("hvdc[{i}].{f}")));
}
out
}
pub fn to_format(&self, format: crate::TargetFormat) -> crate::Result<crate::Conversion> {
crate::write_as(self, format)
}
#[must_use]
pub fn to_matpower(&self) -> String {
crate::write_matpower(self)
}
pub fn from_json(text: &str) -> crate::Result<Network> {
let net: Network = serde_json::from_str(text).map_err(|e| Error::FormatRead {
format: "JSON",
message: e.to_string(),
})?;
net.check_references("JSON")?;
if net.buses.is_empty() {
return Err(Error::FormatRead {
format: "JSON",
message: "case has no buses".into(),
});
}
Ok(net)
}
#[must_use]
pub fn is_normalized(&self) -> bool {
self.source_format == SourceFormat::Normalized
}
pub fn check_base_mva(&self) -> crate::Result<()> {
if self.base_mva.is_finite() && self.base_mva > 0.0 {
Ok(())
} else {
Err(crate::Error::InvalidBaseMva {
base: self.base_mva,
})
}
}
#[must_use]
pub fn validate_values(&self) -> Vec<Diagnostic> {
let mut out = Vec::new();
for b in &self.buses {
if let Some(new) = repair_vm(b.vm) {
out.push(Diagnostic {
element: format!("bus {}", b.id),
field: "vm",
old: b.vm,
new,
reason: "voltage magnitude outside [0, 2] p.u.",
});
}
if let Some(new) = repair_va(b.va) {
out.push(Diagnostic {
element: format!("bus {}", b.id),
field: "va",
old: b.va,
new,
reason: "voltage angle outside ±2000°",
});
}
}
for g in &self.generators {
if let Some(new) = repair_mbase(g.mbase, self.base_mva) {
out.push(Diagnostic {
element: format!("generator at bus {}", g.bus),
field: "mbase",
old: g.mbase,
new,
reason: "non-positive generator MVA base",
});
}
if let Some(new) = repair_vg(g.vg) {
out.push(Diagnostic {
element: format!("generator at bus {}", g.bus),
field: "vg",
old: g.vg,
new,
reason: "non-positive voltage setpoint",
});
}
}
out
}
pub(crate) fn invalidate_source(&mut self) {
self.source = None;
}
pub fn repair(&mut self) -> Vec<Diagnostic> {
let findings = self.validate_values();
let sbase = self.base_mva;
for b in &mut self.buses {
if let Some(new) = repair_vm(b.vm) {
b.vm = new;
}
if let Some(new) = repair_va(b.va) {
b.va = new;
}
}
for g in &mut self.generators {
if let Some(new) = repair_mbase(g.mbase, sbase) {
g.mbase = new;
}
if let Some(new) = repair_vg(g.vg) {
g.vg = new;
}
}
if !findings.is_empty() {
self.invalidate_source();
}
findings
}
pub(crate) fn expand_transformers_3w(&self) -> std::borrow::Cow<'_, Network> {
if self.transformers_3w.is_empty() {
return std::borrow::Cow::Borrowed(self);
}
let mut net = self.clone();
let scale = if net.is_normalized() {
1.0
} else {
net.base_mva
};
let base_id = net.buses.iter().map(|b| b.id.0).max().unwrap_or(0) + 1;
for (k, t) in self
.transformers_3w
.iter()
.filter(|t| t.in_service)
.enumerate()
{
let star_id = BusId(base_id + k);
let (star, branches) = t.star_expansion(star_id);
net.buses.push(star);
net.branches.extend(branches);
if t.mag_g != 0.0 || t.mag_b != 0.0 {
net.shunts.push(Shunt {
bus: star_id,
g: t.mag_g * scale,
b: t.mag_b * scale,
in_service: true,
control: None,
extras: Extras::new(),
});
}
}
net.transformers_3w.clear();
std::borrow::Cow::Owned(net)
}
pub fn validate(&self) -> crate::Result<()> {
self.check_references("network")
}
pub(crate) fn check_references(&self, format: &'static str) -> crate::Result<()> {
let mut ids = std::collections::HashSet::with_capacity(self.buses.len());
for b in &self.buses {
if !ids.insert(b.id) {
return Err(Error::FormatRead {
format,
message: format!("duplicate bus id {}", b.id),
});
}
}
let check = |bus: BusId, what: &str| -> crate::Result<()> {
if ids.contains(&bus) {
Ok(())
} else {
Err(Error::FormatRead {
format,
message: format!("{what} references unknown bus {bus}"),
})
}
};
for (i, br) in self.branches.iter().enumerate() {
for bus in [br.from, br.to] {
if !ids.contains(&bus) {
return Err(Error::FormatRead {
format,
message: format!("branch {i} references unknown bus {bus}"),
});
}
}
if let Some(bus) = br.control.as_ref().and_then(|c| c.controlled_bus) {
check(bus, "transformer control")?;
}
}
for l in &self.loads {
check(l.bus, "load")?;
}
for s in &self.shunts {
check(s.bus, "shunt")?;
if let Some(bus) = s.control.as_ref().and_then(|c| c.control_bus) {
check(bus, "switched-shunt control")?;
}
}
for g in &self.generators {
check(g.bus, "generator")?;
if let Some(bus) = g.regulated_bus {
check(bus, "generator voltage control")?;
}
}
for d in &self.hvdc {
check(d.from, "dcline")?;
check(d.to, "dcline")?;
}
for s in &self.storage {
check(s.bus, "storage")?;
}
for a in &self.areas {
if let Some(slack) = a.slack_bus {
check(slack, "area swing")?;
}
}
for t in &self.transformers_3w {
for w in &t.windings {
check(w.bus, "3-winding transformer")?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn close(actual: f64, expected: f64) {
assert!((actual - expected).abs() < 1e-12, "{actual} != {expected}");
}
fn bus(id: usize) -> Bus {
Bus {
id: BusId(id),
kind: BusType::Pq,
vm: 1.0,
va: 0.0,
base_kv: 230.0,
vmax: 1.1,
vmin: 0.9,
evhi: None,
evlo: None,
area: 1,
zone: 1,
name: None,
extras: Extras::new(),
}
}
fn winding(b: usize) -> Winding {
Winding {
bus: BusId(b),
tap: 1.0,
shift: 0.0,
nominal_kv: 230.0,
rate_a: 100.0,
rate_b: 0.0,
rate_c: 0.0,
}
}
fn transformer_3w() -> Transformer3W {
let z = |r, x| Impedance {
r,
x,
base_mva: 100.0,
};
Transformer3W {
windings: [winding(1), winding(2), winding(3)],
z: [z(0.01, 0.10), z(0.02, 0.20), z(0.03, 0.30)],
star_vm: 0.98,
star_va: -1.5,
mag_g: 0.0,
mag_b: 0.0,
in_service: true,
name: Some("T1".into()),
extras: Extras::new(),
}
}
#[test]
fn star_impedances_split_the_pairwise_values() {
let [(r1, x1), (r2, x2), (r3, x3)] = transformer_3w().star_impedances();
close(r1, 0.01);
close(x1, 0.10);
close(r2, 0.0);
close(x2, 0.0);
close(r3, 0.02);
close(x3, 0.20);
}
#[test]
fn star_expansion_builds_a_star_bus_and_three_branches() {
let t = transformer_3w();
let (star, branches) = t.star_expansion(BusId(99));
assert_eq!(star.id, BusId(99));
close(star.vm, 0.98);
close(star.va, -1.5);
for (i, br) in branches.iter().enumerate() {
assert_eq!(br.from, t.windings[i].bus);
assert_eq!(br.to, BusId(99));
close(br.tap, 1.0);
close(br.rate_a, 100.0);
}
close(branches[2].r, 0.02);
close(branches[2].x, 0.20);
}
#[test]
fn three_winding_transformer_survives_json_transport() {
let mut net = Network::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
net.transformers_3w.push(transformer_3w());
net.validate().unwrap();
let back = Network::from_json(&net.to_json().unwrap()).unwrap();
assert_eq!(back.transformers_3w.len(), 1);
close(back.transformers_3w[0].z[1].x, 0.20);
assert_eq!(back.transformers_3w[0].windings[2].bus, BusId(3));
}
#[test]
fn check_references_rejects_a_dangling_winding_bus() {
let mut net = Network::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
net.transformers_3w.push(transformer_3w()); let err = net.validate().unwrap_err().to_string();
assert!(
err.contains("3-winding transformer references unknown bus 3"),
"got {err}"
);
}
fn regulating_branch(reg: usize) -> Branch {
Branch {
from: BusId(1),
to: BusId(2),
r: 0.0,
x: 0.1,
b: 0.0,
rate_a: 0.0,
rate_b: 0.0,
rate_c: 0.0,
tap: 1.0,
shift: 0.0,
in_service: true,
angmin: -360.0,
angmax: 360.0,
control: Some(TransformerControl {
mode: TransformerControlMode::Voltage,
controlled_bus: Some(BusId(reg)),
tap_min: 0.95,
tap_max: 1.05,
band_min: 1.0,
band_max: 1.02,
ntp: 17,
mva_base: 100.0,
}),
extras: Extras::new(),
}
}
#[test]
fn transformer_control_survives_json_transport() {
let mut net = Network::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
net.branches.push(regulating_branch(3));
net.validate().unwrap();
let back = Network::from_json(&net.to_json().unwrap()).unwrap();
let c = back.branches[0].control.as_ref().unwrap();
assert_eq!(c.mode, TransformerControlMode::Voltage);
assert_eq!(c.controlled_bus, Some(BusId(3)));
close(c.tap_max, 1.05);
assert_eq!(c.ntp, 17);
}
#[test]
fn gen_caps_serialize_as_a_named_map_that_grows_additively() {
let mut caps: GenCaps = [None; GEN_EXTRA_KEYS.len()];
caps[8] = Some(1.5); caps[10] = Some(0.5); let g = Generator {
bus: BusId(1),
pg: 10.0,
qg: 0.0,
pmax: 100.0,
pmin: 0.0,
qmax: 50.0,
qmin: -50.0,
vg: 1.0,
mbase: 100.0,
in_service: true,
cost: None,
caps,
regulated_bus: None,
};
let json = serde_json::to_string(&g).unwrap();
assert!(json.contains(r#""caps":{"#), "caps is an object: {json}");
assert!(json.contains(r#""ramp_30":1.5"#) && json.contains(r#""apf":0.5"#));
let back: Generator = serde_json::from_str(&json).unwrap();
assert_eq!(back.caps, g.caps);
let with_future = r#"{"bus":1,"pg":10,"qg":0,"pmax":100,"pmin":0,"qmax":50,"qmin":-50,
"vg":1,"mbase":100,"in_service":true,"cost":null,
"caps":{"ramp_30":1.5,"future_ramp":9.9}}"#;
let g2: Generator = serde_json::from_str(with_future).unwrap();
assert_eq!(g2.caps[8], Some(1.5));
assert_eq!(g2.caps.iter().filter(|v| v.is_some()).count(), 1);
let no_caps = r#"{"bus":1,"pg":10,"qg":0,"pmax":100,"pmin":0,"qmax":50,"qmin":-50,
"vg":1,"mbase":100,"in_service":true,"cost":null}"#;
let g3: Generator = serde_json::from_str(no_caps).unwrap();
assert!(!g3.has_caps());
let null_caps = r#"{"bus":1,"pg":10,"qg":0,"pmax":100,"pmin":0,"qmax":50,"qmin":-50,
"vg":1,"mbase":100,"in_service":true,"cost":null,"caps":null}"#;
let g4: Generator = serde_json::from_str(null_caps).unwrap();
assert!(!g4.has_caps());
}
#[test]
fn non_finite_fields_lists_every_offender_not_just_the_first() {
let bus = |id, vm| Bus {
id: BusId(id),
kind: BusType::Pq,
vm,
va: 0.0,
base_kv: 230.0,
vmax: 1.1,
vmin: 0.9,
evhi: None,
evlo: None,
area: 1,
zone: 1,
name: None,
extras: Extras::new(),
};
let branch = Branch {
from: BusId(1),
to: BusId(2),
r: 0.0,
x: f64::INFINITY,
b: 0.0,
rate_a: 0.0,
rate_b: 0.0,
rate_c: 0.0,
tap: 0.0,
shift: 0.0,
in_service: true,
angmin: -360.0,
angmax: 360.0,
control: None,
extras: Extras::new(),
};
let mut g = Generator {
bus: BusId(1),
pg: 0.0,
qg: 0.0,
pmax: 0.0,
pmin: 0.0,
qmax: 0.0,
qmin: 0.0,
vg: 1.0,
mbase: 100.0,
in_service: true,
cost: None,
caps: GenCaps::default(),
regulated_bus: None,
};
g.caps[8] = Some(f64::INFINITY); let mut net = Network::in_memory(
"nf",
100.0,
vec![bus(1, f64::NAN), bus(2, 1.0)],
vec![branch],
);
net.generators.push(g);
let fields = net.non_finite_fields();
assert!(fields.contains(&"buses[0].vm".to_string()), "{fields:?}");
assert!(fields.contains(&"branches[0].x".to_string()), "{fields:?}");
assert!(
fields.contains(&"generators[0].caps.ramp_30".to_string()),
"caps reported at key precision: {fields:?}"
);
assert_eq!(
fields.len(),
3,
"exactly the three offenders, no more: {fields:?}"
);
}
#[test]
fn check_references_rejects_a_dangling_controlled_bus() {
let mut net = Network::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
net.branches.push(regulating_branch(9)); let err = net.validate().unwrap_err().to_string();
assert!(
err.contains("transformer control references unknown bus 9"),
"got {err}"
);
}
fn switched_shunt(reg: usize) -> Shunt {
Shunt {
bus: BusId(1),
g: 0.0,
b: 19.0,
in_service: true,
control: Some(SwitchedShuntControl {
mode: SwitchedShuntMode::Discrete,
vhigh: 1.05,
vlow: 0.95,
control_bus: Some(BusId(reg)),
rmpct: 100.0,
blocks: vec![
ShuntBlock { steps: 2, b: 25.0 },
ShuntBlock { steps: 1, b: 50.0 },
],
}),
extras: Extras::new(),
}
}
#[test]
fn switched_shunt_control_survives_json_transport() {
let mut net = Network::in_memory("t", 100.0, vec![bus(1), bus(2), bus(3)], Vec::new());
net.shunts.push(switched_shunt(3));
net.validate().unwrap();
let back = Network::from_json(&net.to_json().unwrap()).unwrap();
let c = back.shunts[0].control.as_ref().unwrap();
assert_eq!(c.mode, SwitchedShuntMode::Discrete);
assert_eq!(c.control_bus, Some(BusId(3)));
assert_eq!(c.blocks.len(), 2);
close(c.blocks[1].b, 50.0);
}
#[test]
fn check_references_rejects_a_dangling_switched_shunt_control_bus() {
let mut net = Network::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
net.shunts.push(switched_shunt(9)); let err = net.validate().unwrap_err().to_string();
assert!(
err.contains("switched-shunt control references unknown bus 9"),
"got {err}"
);
}
#[test]
fn validate_values_flags_and_repair_clamps_out_of_domain_values() {
let mut net = Network::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
net.buses[0].vm = 0.0; net.buses[1].va = 9000.0; net.generators.push(Generator {
bus: BusId(1),
pg: 10.0,
qg: 0.0,
pmax: 100.0,
pmin: 0.0,
qmax: 50.0,
qmin: -50.0,
vg: 0.0, mbase: 0.0, in_service: true,
cost: None,
caps: Default::default(),
regulated_bus: None,
});
let diags = net.validate_values();
let fields: std::collections::BTreeSet<_> = diags.iter().map(|d| d.field).collect();
assert_eq!(
fields,
["mbase", "va", "vg", "vm"].into_iter().collect(),
"all four out-of-domain fields reported"
);
close(net.buses[0].vm, 0.0);
let applied = net.repair();
assert_eq!(applied.len(), diags.len());
close(net.buses[0].vm, 1.0);
close(net.buses[1].va, 0.0);
close(net.generators[0].mbase, 100.0); close(net.generators[0].vg, 1.0);
assert!(net.validate_values().is_empty());
}
#[test]
fn validate_values_is_empty_for_a_clean_network() {
let net = Network::in_memory("t", 100.0, vec![bus(1), bus(2)], Vec::new());
assert!(net.validate_values().is_empty());
}
}