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 BusId {
#[must_use]
pub const fn new(id: usize) -> Self {
Self(id)
}
}
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)]
#[non_exhaustive]
pub struct GenCost {
pub model: u8,
pub startup: f64,
pub shutdown: f64,
pub ncost: usize,
pub coeffs: Vec<f64>,
}
impl GenCost {
#[must_use]
pub fn new(model: u8, startup: f64, shutdown: f64, coeffs: Vec<f64>) -> Self {
let ncost = if model == 1 {
coeffs.len() / 2
} else {
coeffs.len()
};
Self {
model,
startup,
shutdown,
ncost,
coeffs,
}
}
#[must_use]
pub fn with_ncost(
model: u8,
startup: f64,
shutdown: f64,
ncost: usize,
coeffs: Vec<f64>,
) -> Self {
Self {
model,
startup,
shutdown,
ncost,
coeffs,
}
}
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,
Goc3Json,
SurgeJson,
}
impl SourceFormat {
#[must_use]
pub fn name(self) -> &'static str {
match self {
SourceFormat::Matpower => "matpower",
SourceFormat::PowerModelsJson => "powermodels-json",
SourceFormat::EgretJson => "egret-json",
SourceFormat::Psse => "psse",
SourceFormat::PowerWorld => "powerworld",
SourceFormat::PandapowerJson => "pandapower-json",
SourceFormat::Pslf => "pslf",
SourceFormat::PowerWorldBinary => "powerworld-pwb",
SourceFormat::InMemory => "in-memory",
SourceFormat::Normalized => "normalized",
SourceFormat::Gridfm => "gridfm",
SourceFormat::PypsaCsv => "pypsa-csv",
SourceFormat::Goc3Json => "goc3-json",
SourceFormat::SurgeJson => "surge-json",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
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>,
#[serde(default)]
pub switches: Vec<Switch>,
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>>,
}
pub type BalancedNetwork = Network;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
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,
}
impl Bus {
#[must_use]
pub fn new(id: BusId, kind: BusType, base_kv: f64) -> Self {
Self {
id,
kind,
vm: 1.0,
va: 0.0,
base_kv,
vmax: 1.1,
vmin: 0.9,
evhi: None,
evlo: None,
area: 1,
zone: 1,
name: None,
extras: Extras::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Load {
pub bus: BusId,
pub p: f64,
pub q: f64,
#[serde(default)]
pub voltage_model: Option<LoadVoltageModel>,
pub in_service: bool,
pub extras: Extras,
}
impl Load {
#[must_use]
pub fn new(bus: BusId, p: f64, q: f64) -> Self {
Self {
bus,
p,
q,
voltage_model: None,
in_service: true,
extras: Extras::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
#[non_exhaustive]
pub enum LoadVoltageModel {
ConstantPower,
Zip {
p_constant_power: f64,
q_constant_power: f64,
p_constant_current: f64,
q_constant_current: f64,
p_constant_impedance: f64,
q_constant_impedance: f64,
#[serde(default)]
v_nom: Option<f64>,
#[serde(default)]
load_type: Option<i32>,
#[serde(default)]
scaling: Option<f64>,
},
Exponential {
p: f64,
q: f64,
#[serde(default)]
v_nom: Option<f64>,
gamma_p: f64,
gamma_q: f64,
},
}
impl LoadVoltageModel {
#[must_use]
pub fn has_non_matpower_fields(&self) -> bool {
match self {
Self::ConstantPower => false,
Self::Zip {
p_constant_current,
q_constant_current,
p_constant_impedance,
q_constant_impedance,
v_nom,
load_type,
scaling,
..
} => {
*p_constant_current != 0.0
|| *q_constant_current != 0.0
|| *p_constant_impedance != 0.0
|| *q_constant_impedance != 0.0
|| v_nom.is_some()
|| load_type.is_some()
|| scaling.is_some()
}
Self::Exponential { .. } => true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
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,
}
impl Shunt {
#[must_use]
pub fn new(bus: BusId, g: f64, b: f64) -> Self {
Self {
bus,
g,
b,
in_service: true,
control: None,
extras: Extras::new(),
}
}
}
#[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)]
#[non_exhaustive]
pub struct ShuntBlock {
pub steps: u32,
pub b: f64,
}
impl ShuntBlock {
#[must_use]
pub const fn new(steps: u32, b: f64) -> Self {
Self { steps, b }
}
}
#[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>,
}
impl SwitchedShuntControl {
#[must_use]
pub fn new(mode: SwitchedShuntMode, vhigh: f64, vlow: f64, blocks: Vec<ShuntBlock>) -> Self {
Self {
mode,
vhigh,
vlow,
control_bus: None,
rmpct: 100.0,
blocks,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Branch {
pub from: BusId,
pub to: BusId,
pub r: f64,
pub x: f64,
pub b: f64,
#[serde(default)]
pub charging: Option<BranchCharging>,
pub rate_a: f64,
pub rate_b: f64,
pub rate_c: f64,
#[serde(default)]
pub rating_sets: Vec<BranchRatingSet>,
#[serde(default)]
pub current_ratings: Option<BranchCurrentRatings>,
pub tap: f64,
pub shift: f64,
pub in_service: bool,
pub angmin: f64,
pub angmax: f64,
#[serde(default)]
pub control: Option<TransformerControl>,
#[serde(default)]
pub solution: Option<BranchSolution>,
pub extras: Extras,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct BranchRatingSet {
pub name: String,
pub rate_mva: f64,
}
impl BranchRatingSet {
#[must_use]
pub fn new(name: impl Into<String>, rate_mva: f64) -> Self {
Self {
name: name.into(),
rate_mva,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct BranchCharging {
pub g_fr: f64,
pub b_fr: f64,
pub g_to: f64,
pub b_to: f64,
}
impl BranchCharging {
#[must_use]
pub const fn new(g_fr: f64, b_fr: f64, g_to: f64, b_to: f64) -> Self {
Self {
g_fr,
b_fr,
g_to,
b_to,
}
}
#[must_use]
pub fn from_total_b(b: f64) -> Self {
Self {
g_fr: 0.0,
b_fr: b / 2.0,
g_to: 0.0,
b_to: b / 2.0,
}
}
#[must_use]
pub fn total_b(self) -> f64 {
self.b_fr + self.b_to
}
#[must_use]
pub fn total_g(self) -> f64 {
self.g_fr + self.g_to
}
#[must_use]
pub fn is_matpower_symmetric(self) -> bool {
self.g_fr.abs() <= f64::EPSILON
&& self.g_to.abs() <= f64::EPSILON
&& (self.b_fr - self.b_to).abs() <= f64::EPSILON
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct BranchCurrentRatings {
pub c_rating_a: f64,
pub c_rating_b: f64,
pub c_rating_c: f64,
}
impl BranchCurrentRatings {
#[must_use]
pub const fn new(c_rating_a: f64, c_rating_b: f64, c_rating_c: f64) -> Self {
Self {
c_rating_a,
c_rating_b,
c_rating_c,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct BranchSolution {
pub pf: f64,
pub qf: f64,
pub pt: f64,
pub qt: f64,
}
impl BranchSolution {
#[must_use]
pub const fn new(pf: f64, qf: f64, pt: f64, qt: f64) -> Self {
Self { pf, qf, pt, qt }
}
}
impl Branch {
#[must_use]
pub fn new(from: BusId, to: BusId, r: f64, x: f64) -> Self {
Self {
from,
to,
r,
x,
b: 0.0,
charging: None,
rate_a: 0.0,
rate_b: 0.0,
rate_c: 0.0,
rating_sets: Vec::new(),
current_ratings: None,
tap: 0.0,
shift: 0.0,
in_service: true,
angmin: -360.0,
angmax: 360.0,
control: None,
solution: None,
extras: Extras::new(),
}
}
#[must_use]
pub fn effective_tap(&self) -> f64 {
if self.tap == 0.0 { 1.0 } else { self.tap }
}
#[must_use]
pub fn terminal_charging(&self) -> BranchCharging {
self.charging
.unwrap_or_else(|| BranchCharging::from_total_b(self.b))
}
#[must_use]
pub fn legacy_total_charging_b(&self) -> f64 {
self.terminal_charging().total_b()
}
#[must_use]
pub fn has_non_matpower_charging(&self) -> bool {
self.charging
.is_some_and(|charging| !charging.is_matpower_symmetric())
}
#[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, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Switch {
pub from: BusId,
pub to: BusId,
pub closed: bool,
#[serde(default)]
pub thermal_rating: Option<f64>,
#[serde(default)]
pub current_rating: Option<f64>,
#[serde(default)]
pub pf: Option<f64>,
#[serde(default)]
pub qf: Option<f64>,
#[serde(default)]
pub pt: Option<f64>,
#[serde(default)]
pub qt: Option<f64>,
pub extras: Extras,
}
impl Switch {
#[must_use]
pub fn new(from: BusId, to: BusId, closed: bool) -> Self {
Self {
from,
to,
closed,
thermal_rating: None,
current_rating: None,
pf: None,
qf: None,
pt: None,
qt: None,
extras: Extras::new(),
}
}
}
#[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,
}
}
}
impl TransformerControl {
#[must_use]
pub fn new(mode: TransformerControlMode) -> Self {
Self {
mode,
..Self::default()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
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 new(bus: BusId) -> Self {
Self {
bus,
pg: 0.0,
qg: 0.0,
pmax: 0.0,
pmin: 0.0,
qmax: 0.0,
qmin: 0.0,
vg: 1.0,
mbase: 0.0,
in_service: true,
cost: None,
caps: default_caps(),
regulated_bus: None,
}
}
#[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)]
#[non_exhaustive]
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,
#[serde(default)]
pub current_rating: Option<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,
}
impl Storage {
#[must_use]
pub fn new(bus: BusId) -> Self {
Self {
bus,
ps: 0.0,
qs: 0.0,
energy: 0.0,
energy_rating: 0.0,
charge_rating: 0.0,
discharge_rating: 0.0,
charge_efficiency: 1.0,
discharge_efficiency: 1.0,
thermal_rating: 0.0,
current_rating: None,
qmin: 0.0,
qmax: 0.0,
r: 0.0,
x: 0.0,
p_loss: 0.0,
q_loss: 0.0,
in_service: true,
extras: Extras::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
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,
#[serde(default)]
pub cost: Option<GenCost>,
pub extras: Extras,
}
impl Hvdc {
#[must_use]
pub fn new(from: BusId, to: BusId) -> Self {
Self {
from,
to,
in_service: true,
pf: 0.0,
pt: 0.0,
qf: 0.0,
qt: 0.0,
vf: 1.0,
vt: 1.0,
pmin: 0.0,
pmax: 0.0,
qminf: 0.0,
qmaxf: 0.0,
qmint: 0.0,
qmaxt: 0.0,
loss0: 0.0,
loss1: 0.0,
cost: None,
extras: Extras::new(),
}
}
}
#[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>,
}
impl Area {
#[must_use]
pub fn new(number: usize) -> Self {
Self {
number,
slack_bus: None,
net_interchange: 0.0,
tolerance: 0.0,
name: None,
}
}
}
#[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 new() -> Self {
Self::default()
}
#[must_use]
pub fn is_empty(&self) -> bool {
*self == SolverParams::default()
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Impedance {
pub r: f64,
pub x: f64,
pub base_mva: f64,
}
impl Impedance {
#[must_use]
pub const fn new(r: f64, x: f64, base_mva: f64) -> Self {
Self { r, x, base_mva }
}
}
#[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,
}
impl Winding {
#[must_use]
pub fn new(bus: BusId) -> Self {
Self {
bus,
tap: 1.0,
shift: 0.0,
nominal_kv: 0.0,
rate_a: 0.0,
rate_b: 0.0,
rate_c: 0.0,
}
}
}
#[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 new(windings: [Winding; 3], z: [Impedance; 3]) -> Self {
Self {
windings,
z,
star_vm: 1.0,
star_va: 0.0,
mag_g: 0.0,
mag_b: 0.0,
in_service: true,
name: None,
extras: Extras::new(),
}
}
#[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,
charging: None,
rate_a: w.rate_a,
rate_b: w.rate_b,
rate_c: w.rate_c,
rating_sets: Vec::new(),
current_ratings: None,
tap: w.tap,
shift: w.shift,
in_service: self.in_service,
angmin: -360.0,
angmax: 360.0,
control: None,
solution: 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 new(name: impl Into<String>, base_mva: f64) -> Network {
Network {
name: name.into(),
base_mva,
base_frequency: DEFAULT_BASE_FREQUENCY,
buses: Vec::new(),
loads: Vec::new(),
shunts: Vec::new(),
branches: Vec::new(),
switches: Vec::new(),
generators: Vec::new(),
storage: Vec::new(),
hvdc: Vec::new(),
transformers_3w: Vec::new(),
areas: Vec::new(),
solver: None,
source_format: SourceFormat::InMemory,
source: None,
}
}
#[must_use]
pub fn in_memory(
name: impl Into<String>,
base_mva: f64,
buses: Vec<Bus>,
branches: Vec<Branch>,
) -> Network {
let mut net = Self::new(name, base_mva);
net.buses = buses;
net.branches = branches;
net
}
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());
}
if !self.base_frequency.is_finite() {
out.push("base_frequency".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,
voltage_model,
in_service: _,
extras: _,
} = l;
out.extend(bad([("p", *p), ("q", *q)]).map(|f| format!("loads[{i}].{f}")));
if let Some(model) = voltage_model {
match model {
LoadVoltageModel::ConstantPower => {}
LoadVoltageModel::Zip {
p_constant_power,
q_constant_power,
p_constant_current,
q_constant_current,
p_constant_impedance,
q_constant_impedance,
v_nom,
load_type: _,
scaling,
} => {
let fields = [
("p_constant_power", *p_constant_power),
("q_constant_power", *q_constant_power),
("p_constant_current", *p_constant_current),
("q_constant_current", *q_constant_current),
("p_constant_impedance", *p_constant_impedance),
("q_constant_impedance", *q_constant_impedance),
];
out.extend(bad(fields).map(|f| format!("loads[{i}].voltage_model.{f}")));
if matches!(v_nom, Some(v) if !v.is_finite()) {
out.push(format!("loads[{i}].voltage_model.v_nom"));
}
if matches!(scaling, Some(v) if !v.is_finite()) {
out.push(format!("loads[{i}].voltage_model.scaling"));
}
}
LoadVoltageModel::Exponential {
p,
q,
v_nom,
gamma_p,
gamma_q,
} => {
out.extend(
bad([
("p", *p),
("q", *q),
("gamma_p", *gamma_p),
("gamma_q", *gamma_q),
])
.map(|f| format!("loads[{i}].voltage_model.{f}")),
);
if matches!(v_nom, Some(v) if !v.is_finite()) {
out.push(format!("loads[{i}].voltage_model.v_nom"));
}
}
}
}
}
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, charging, rate_a, rate_b, rate_c, rating_sets, current_ratings, tap, shift, in_service: _, angmin, angmax, control: _, solution, 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}")));
out.extend(
rating_sets
.iter()
.enumerate()
.filter(|(_, r)| !r.rate_mva.is_finite())
.map(|(j, _)| format!("branches[{i}].rating_sets[{j}].rate_mva")),
);
if let Some(charging) = charging {
let BranchCharging {
g_fr,
b_fr,
g_to,
b_to,
} = charging;
let fields = [
("g_fr", *g_fr),
("b_fr", *b_fr),
("g_to", *g_to),
("b_to", *b_to),
];
out.extend(bad(fields).map(|f| format!("branches[{i}].charging.{f}")));
}
if let Some(current) = current_ratings {
let BranchCurrentRatings {
c_rating_a,
c_rating_b,
c_rating_c,
} = current;
let fields = [
("c_rating_a", *c_rating_a),
("c_rating_b", *c_rating_b),
("c_rating_c", *c_rating_c),
];
out.extend(bad(fields).map(|f| format!("branches[{i}].current_ratings.{f}")));
}
if let Some(solution) = solution {
let BranchSolution { pf, qf, pt, qt } = solution;
out.extend(
bad([("pf", *pf), ("qf", *qf), ("pt", *pt), ("qt", *qt)])
.map(|f| format!("branches[{i}].solution.{f}")),
);
}
}
for (i, sw) in self.switches.iter().enumerate() {
let Switch {
from: _,
to: _,
closed: _,
thermal_rating,
current_rating,
pf,
qf,
pt,
qt,
extras: _,
} = sw;
for (field, value) in [
("thermal_rating", *thermal_rating),
("current_rating", *current_rating),
("pf", *pf),
("qf", *qf),
("pt", *pt),
("qt", *qt),
] {
if matches!(value, Some(v) if !v.is_finite()) {
out.push(format!("switches[{i}].{field}"));
}
}
}
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, current_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}")));
if matches!(current_rating, Some(v) if !v.is_finite()) {
out.push(format!("storage[{i}].current_rating"));
}
}
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, cost, 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}")));
if let Some(GenCost {
model: _,
startup,
shutdown,
ncost: _,
coeffs,
}) = cost
{
out.extend(
bad([("startup", *startup), ("shutdown", *shutdown)])
.map(|f| format!("hvdc[{i}].cost.{f}")),
);
if coeffs.iter().any(|c| !c.is_finite()) {
out.push(format!("hvdc[{i}].cost.coeffs"));
}
}
}
out
}
pub fn to_format(&self, format: crate::TargetFormat) -> crate::Result<crate::Conversion> {
crate::write_as(self, format)
}
pub fn to_format_with_options(
&self,
format: crate::TargetFormat,
options: &crate::WriteOptions,
) -> crate::Result<crate::Conversion> {
crate::write_as_with_options(self, format, options)
}
#[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 (i, sw) in self.switches.iter().enumerate() {
for bus in [sw.from, sw.to] {
if !ids.contains(&bus) {
return Err(Error::FormatRead {
format,
message: format!("switch {i} references unknown bus {bus}"),
});
}
}
}
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,
charging: None,
rate_a: 0.0,
rate_b: 0.0,
rate_c: 0.0,
rating_sets: Vec::new(),
current_ratings: None,
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,
}),
solution: None,
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,
charging: None,
rate_a: 0.0,
rate_b: 0.0,
rate_c: 0.0,
rating_sets: Vec::new(),
current_ratings: None,
tap: 0.0,
shift: 0.0,
in_service: true,
angmin: -360.0,
angmax: 360.0,
control: None,
solution: 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());
}
}