use std::collections::{HashMap, HashSet};
use crate::network::{
Branch, Bus, BusId, BusType, GEN_EXTRA_KEYS, GenCost, Generator, Hvdc, Load, Network, Shunt,
SourceFormat, Storage, Transformer3W,
};
use crate::{Error, Result};
pub(crate) const DEG_TO_RAD: f64 = std::f64::consts::PI / 180.0;
pub(crate) const RAD_TO_DEG: f64 = 180.0 / std::f64::consts::PI;
pub(crate) const GEN_PU_KEYS: [&str; 4] = ["ramp_agc", "ramp_10", "ramp_30", "ramp_q"];
pub(crate) fn cost_to_pu(cost: &GenCost, base: f64) -> Vec<f64> {
match cost.model {
2 => {
let coeffs = &cost.coeffs[..cost.ncost.min(cost.coeffs.len())];
let k = coeffs.len();
coeffs
.iter()
.enumerate()
.map(|(i, &c)| {
c * base.powi(i32::try_from(k - 1 - i).expect("cost degree fits i32"))
})
.collect()
}
1 => {
let coeffs = &cost.coeffs[..(cost.ncost * 2).min(cost.coeffs.len())];
coeffs
.iter()
.enumerate()
.map(|(i, &c)| if i % 2 == 0 { c / base } else { c })
.collect()
}
_ => cost.coeffs.clone(),
}
}
pub(crate) fn cost_from_pu(coeffs: &[f64], model: u8, base: f64) -> Vec<f64> {
let k = coeffs.len();
if model == 2 {
coeffs
.iter()
.enumerate()
.map(|(i, &c)| c / base.powi(i32::try_from(k - 1 - i).expect("cost degree fits i32")))
.collect()
} else if model == 1 {
coeffs
.iter()
.enumerate()
.map(|(i, &c)| if i % 2 == 0 { c * base } else { c })
.collect()
} else {
coeffs.to_vec()
}
}
fn remap(map: &HashMap<BusId, BusId>, id: BusId) -> Option<BusId> {
map.get(&id).copied()
}
fn norm_loads(loads: &[Load], base: f64, map: &HashMap<BusId, BusId>) -> Vec<Load> {
loads
.iter()
.filter(|l| l.in_service)
.filter_map(|l| {
Some(Load {
bus: remap(map, l.bus)?,
p: l.p / base,
q: l.q / base,
..l.clone()
})
})
.collect()
}
fn norm_shunts(shunts: &[Shunt], base: f64, map: &HashMap<BusId, BusId>) -> Vec<Shunt> {
shunts
.iter()
.filter(|s| s.in_service)
.filter_map(|s| {
Some(Shunt {
bus: remap(map, s.bus)?,
g: s.g / base,
b: s.b / base,
control: s.control.clone().map(|mut c| {
c.control_bus = c.control_bus.and_then(|b| remap(map, b));
c
}),
..s.clone()
})
})
.collect()
}
fn norm_branches(branches: &[Branch], base: f64, map: &HashMap<BusId, BusId>) -> Vec<Branch> {
branches
.iter()
.filter(|br| br.in_service)
.filter_map(|br| {
Some(Branch {
from: remap(map, br.from)?,
to: remap(map, br.to)?,
rate_a: br.rate_a / base,
rate_b: br.rate_b / base,
rate_c: br.rate_c / base,
tap: br.effective_tap(),
shift: br.shift * DEG_TO_RAD,
angmin: br.angmin * DEG_TO_RAD,
angmax: br.angmax * DEG_TO_RAD,
control: br.control.clone().map(|mut c| {
c.controlled_bus = c.controlled_bus.and_then(|b| remap(map, b));
c
}),
..br.clone()
})
})
.collect()
}
fn norm_gens(gens: &[Generator], base: f64, map: &HashMap<BusId, BusId>) -> Vec<Generator> {
gens.iter()
.filter(|g| g.in_service)
.filter_map(|g| {
let bus = remap(map, g.bus)?;
let mut caps = g.caps;
for (i, key) in GEN_EXTRA_KEYS.iter().enumerate() {
if GEN_PU_KEYS.contains(key) {
if let Some(v) = caps[i] {
caps[i] = Some(v / base);
}
}
}
Some(Generator {
bus,
pg: g.pg / base,
qg: g.qg / base,
pmax: g.pmax / base,
pmin: g.pmin / base,
qmax: g.qmax / base,
qmin: g.qmin / base,
cost: g.cost.as_ref().map(|c| GenCost {
coeffs: cost_to_pu(c, base),
..c.clone()
}),
caps,
regulated_bus: g.regulated_bus.and_then(|b| remap(map, b)),
..g.clone()
})
})
.collect()
}
fn norm_storage(storage: &[Storage], base: f64, map: &HashMap<BusId, BusId>) -> Vec<Storage> {
storage
.iter()
.filter(|s| s.in_service)
.filter_map(|s| {
Some(Storage {
bus: remap(map, s.bus)?,
energy: s.energy / base,
energy_rating: s.energy_rating / base,
charge_rating: s.charge_rating / base,
discharge_rating: s.discharge_rating / base,
thermal_rating: s.thermal_rating / base,
qmin: s.qmin / base,
qmax: s.qmax / base,
p_loss: s.p_loss / base,
q_loss: s.q_loss / base,
..s.clone()
})
})
.collect()
}
fn norm_hvdc(hvdc: &[Hvdc], base: f64, map: &HashMap<BusId, BusId>) -> Vec<Hvdc> {
hvdc.iter()
.filter(|d| d.in_service)
.filter_map(|d| {
Some(Hvdc {
from: remap(map, d.from)?,
to: remap(map, d.to)?,
pf: d.pf / base,
pt: d.pt / base,
qf: d.qf / base,
qt: d.qt / base,
qminf: d.qminf / base,
qmaxf: d.qmaxf / base,
qmint: d.qmint / base,
qmaxt: d.qmaxt / base,
loss0: d.loss0 / base,
..d.clone()
})
})
.collect()
}
fn norm_transformers_3w(
xfmrs: &[Transformer3W],
base: f64,
map: &HashMap<BusId, BusId>,
) -> Vec<Transformer3W> {
xfmrs
.iter()
.filter(|t| t.in_service)
.filter_map(|t| {
let mut windings = t.windings.clone();
for w in &mut windings {
w.bus = remap(map, w.bus)?;
w.shift *= DEG_TO_RAD;
w.rate_a /= base;
w.rate_b /= base;
w.rate_c /= base;
}
Some(Transformer3W {
windings,
star_va: t.star_va * DEG_TO_RAD,
..t.clone()
})
})
.collect()
}
impl Network {
pub fn to_normalized(&self) -> Result<Network> {
self.check_base_mva()?;
let base = self.base_mva;
let mut id_map: HashMap<BusId, BusId> = HashMap::with_capacity(self.buses.len());
let mut buses: Vec<Bus> = Vec::with_capacity(self.buses.len());
for b in &self.buses {
if b.kind == BusType::Isolated {
continue;
}
id_map.insert(b.id, b.id);
buses.push(Bus {
va: b.va * DEG_TO_RAD,
..b.clone()
});
}
let loads = norm_loads(&self.loads, base, &id_map);
let shunts = norm_shunts(&self.shunts, base, &id_map);
let branches = norm_branches(&self.branches, base, &id_map);
let generators = norm_gens(&self.generators, base, &id_map);
let storage = norm_storage(&self.storage, base, &id_map);
let hvdc = norm_hvdc(&self.hvdc, base, &id_map);
let transformers_3w = norm_transformers_3w(&self.transformers_3w, base, &id_map);
let gen_buses: HashSet<BusId> = generators.iter().map(|g| g.bus).collect();
for b in &mut buses {
b.kind = match (gen_buses.contains(&b.id), b.kind) {
(true, BusType::Ref) => BusType::Ref,
(true, _) => BusType::Pv,
(false, _) => BusType::Pq,
};
}
if !buses.iter().any(|b| b.kind == BusType::Ref) {
let slack = generators
.iter()
.max_by(|a, b| {
let key = |p: f64| if p.is_nan() { f64::NEG_INFINITY } else { p };
key(a.pmax).total_cmp(&key(b.pmax))
})
.map(|g| g.bus)
.ok_or(Error::ReferenceBusCount { found: 0 })?;
if let Some(b) = buses.iter_mut().find(|b| b.id == slack) {
b.kind = BusType::Ref;
}
}
let net = Network {
name: self.name.clone(),
base_mva: base,
base_frequency: self.base_frequency,
buses,
loads,
shunts,
branches,
generators,
storage,
hvdc,
transformers_3w,
areas: Vec::new(),
solver: None,
source_format: SourceFormat::Normalized,
source: None,
};
debug_assert!(
net.validate().is_ok(),
"to_normalized produced a dangling reference"
);
Ok(net)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-9
}
#[test]
fn to_normalized_drops_a_control_bus_whose_target_was_filtered_out() {
use crate::network::{Extras, SwitchedShuntControl, SwitchedShuntMode};
let mkbus = |id: usize, kind: BusType| Bus {
id: BusId(id),
kind,
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(),
};
let 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: 0.0,
shift: 0.0,
in_service: true,
angmin: -360.0,
angmax: 360.0,
control: None,
extras: Extras::new(),
};
let mut net = Network::in_memory(
"n",
100.0,
vec![
mkbus(1, BusType::Ref),
mkbus(2, BusType::Pq),
mkbus(3, BusType::Isolated),
],
vec![branch],
);
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: 1.0,
mbase: 100.0,
in_service: true,
cost: None,
caps: Default::default(),
regulated_bus: None,
});
net.shunts.push(Shunt {
bus: BusId(2),
g: 0.0,
b: 10.0,
in_service: true,
control: Some(SwitchedShuntControl {
mode: SwitchedShuntMode::Discrete,
vhigh: 1.05,
vlow: 0.95,
control_bus: Some(BusId(3)),
rmpct: 100.0,
blocks: Vec::new(),
}),
extras: Extras::new(),
});
let norm = net.to_normalized().unwrap();
norm.validate().unwrap();
let c = norm.shunts[0].control.as_ref().expect("control retained");
assert_eq!(
c.control_bus, None,
"a control bus pointing at a filtered-out isolated bus is dropped, not left dangling"
);
}
#[test]
fn cost_to_pu_polynomial_scales_and_trims() {
let cost = GenCost {
model: 2,
startup: 0.0,
shutdown: 0.0,
ncost: 2,
coeffs: vec![24.035, -403.5, 0.0, 0.0, 0.0, 0.0],
};
let out = cost_to_pu(&cost, 100.0);
assert_eq!(out.len(), 2, "padding dropped");
assert!(approx(out[0], 2403.5)); assert!(approx(out[1], -403.5)); }
#[test]
fn cost_to_pu_piecewise_scales_mw_only_and_trims() {
let cost = GenCost {
model: 1,
startup: 0.0,
shutdown: 0.0,
ncost: 4,
coeffs: vec![
0.0, 0.0, 100.0, 2500.0, 200.0, 5500.0, 250.0, 7250.0, 0.0, 0.0,
],
};
let out = cost_to_pu(&cost, 100.0);
assert_eq!(out.len(), 8, "trimmed to 2·ncost, padding dropped");
assert!(
approx(out[0], 0.0)
&& approx(out[2], 1.0)
&& approx(out[4], 2.0)
&& approx(out[6], 2.5)
);
assert!(
approx(out[1], 0.0)
&& approx(out[3], 2500.0)
&& approx(out[5], 5500.0)
&& approx(out[7], 7250.0)
);
}
#[test]
fn cost_rescale_round_trips() {
let cost = GenCost {
model: 2,
startup: 0.0,
shutdown: 0.0,
ncost: 3,
coeffs: vec![0.11, 5.0, 150.0],
};
let pu = cost_to_pu(&cost, 100.0);
assert!((pu[0] - 0.11 * 100.0 * 100.0).abs() < 1e-9);
assert!((pu[1] - 5.0 * 100.0).abs() < 1e-9);
assert!((pu[2] - 150.0).abs() < 1e-9);
let back = cost_from_pu(&pu, 2, 100.0);
for (a, b) in back.iter().zip(&cost.coeffs) {
assert!((a - b).abs() < 1e-9);
}
}
#[test]
fn cost_rescale_passes_through_unknown_model() {
let cost = GenCost {
model: 0,
startup: 0.0,
shutdown: 0.0,
ncost: 2,
coeffs: vec![3.0, 7.0, 9.0],
};
let pu = cost_to_pu(&cost, 100.0);
assert_eq!(pu, cost.coeffs, "to_pu must not scale an unknown model");
let back = cost_from_pu(&pu, cost.model, 100.0);
assert_eq!(back, cost.coeffs, "from_pu must not scale an unknown model");
}
#[test]
fn cost_rescale_round_trips_piecewise() {
let cost = GenCost {
model: 1,
startup: 0.0,
shutdown: 0.0,
ncost: 4,
coeffs: vec![0.0, 0.0, 100.0, 2500.0, 200.0, 5500.0, 250.0, 7250.0],
};
let pu = cost_to_pu(&cost, 100.0);
let back = cost_from_pu(&pu, 1, 100.0);
for (a, b) in back.iter().zip(&cost.coeffs) {
assert!((a - b).abs() < 1e-9, "{a} != {b}");
}
}
}