use std::collections::BTreeMap;
use powerio_dist::MulticonductorNetwork;
use powerio_matrix::BalancedNetwork;
const ELECTRICAL_TOL: f64 = 1e-8;
const MODEL_TOL: f64 = 1e-9;
pub const MAX_MODEL_DIFFS: usize = 24;
#[derive(Debug, Clone, PartialEq)]
pub struct YbusChange {
pub from_bus: usize,
pub to_bus: usize,
pub before: (f64, f64),
pub after: (f64, f64),
}
impl std::fmt::Display for YbusChange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Y[{}, {}] was {}+j{}, now {}+j{}",
self.from_bus, self.to_bus, self.before.0, self.before.1, self.after.0, self.after.1
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Injection {
LoadP,
LoadQ,
GenP,
GenQ,
DcRectifierP,
DcInverterP,
DcRectifierQ,
DcInverterQ,
}
impl Injection {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::LoadP => "load p",
Self::LoadQ => "load q",
Self::GenP => "gen p",
Self::GenQ => "gen q",
Self::DcRectifierP => "dc rectifier p",
Self::DcInverterP => "dc inverter p",
Self::DcRectifierQ => "dc rectifier q",
Self::DcInverterQ => "dc inverter q",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct InjectionChange {
pub bus: usize,
pub quantity: Injection,
pub before: f64,
pub after: f64,
pub status_only: bool,
}
impl std::fmt::Display for InjectionChange {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"bus {} {}: {} -> {}",
self.bus,
self.quantity.label(),
self.before,
self.after
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum YbusUnavailable {
Before,
After,
}
pub fn ybus_change(
before: &BalancedNetwork,
after: &BalancedNetwork,
) -> Result<Option<YbusChange>, YbusUnavailable> {
let entries = |net: &BalancedNetwork| -> Option<BTreeMap<(usize, usize), (f64, f64)>> {
let view = powerio_matrix::IndexedNetwork::new(net);
let parts =
powerio_matrix::build_ybus(&view, &powerio_matrix::BuildOptions::default()).ok()?;
let mut map = BTreeMap::new();
for (value, (i, j)) in &parts.g {
map.entry((view.bus_id(i).0, view.bus_id(j).0))
.or_insert((0.0, 0.0))
.0 = *value;
}
for (value, (i, j)) in &parts.b {
map.entry((view.bus_id(i).0, view.bus_id(j).0))
.or_insert((0.0, 0.0))
.1 = *value;
}
Some(map)
};
let a = entries(before).ok_or(YbusUnavailable::Before)?;
let b = entries(after).ok_or(YbusUnavailable::After)?;
for key in a.keys().chain(b.keys().filter(|k| !a.contains_key(*k))) {
let (ga, ba) = a.get(key).copied().unwrap_or((0.0, 0.0));
let (gb, bb) = b.get(key).copied().unwrap_or((0.0, 0.0));
if beyond_tol(ga, gb, ELECTRICAL_TOL) || beyond_tol(ba, bb, ELECTRICAL_TOL) {
return Ok(Some(YbusChange {
from_bus: key.0,
to_bus: key.1,
before: (ga, ba),
after: (gb, bb),
}));
}
}
Ok(None)
}
#[must_use]
pub fn injection_change(
before: &BalancedNetwork,
after: &BalancedNetwork,
) -> Option<InjectionChange> {
let change = first_moved(&per_bus(before, true), &per_bus(after, true), AC_INJECTIONS)?;
let status_only = first_moved(
&per_bus(before, false),
&per_bus(after, false),
AC_INJECTIONS,
)
.is_none();
Some(InjectionChange {
status_only,
..change
})
}
const AC_INJECTIONS: [Injection; 4] = [
Injection::LoadP,
Injection::LoadQ,
Injection::GenP,
Injection::GenQ,
];
const DC_TERMINALS: [Injection; 4] = [
Injection::DcRectifierP,
Injection::DcInverterP,
Injection::DcRectifierQ,
Injection::DcInverterQ,
];
fn per_bus(net: &BalancedNetwork, in_service_only: bool) -> BTreeMap<usize, [f64; 4]> {
let mut map: BTreeMap<usize, [f64; 4]> = BTreeMap::new();
for l in net
.loads
.iter()
.filter(|l| l.in_service || !in_service_only)
{
let e = map.entry(l.bus.0).or_default();
e[0] += l.p;
e[1] += l.q;
}
for g in net
.generators
.iter()
.filter(|g| g.in_service || !in_service_only)
{
let e = map.entry(g.bus.0).or_default();
e[2] += g.pg;
e[3] += g.qg;
}
map
}
fn per_bus_dc(net: &BalancedNetwork, in_service_only: bool) -> BTreeMap<usize, [f64; 4]> {
let mut map: BTreeMap<usize, [f64; 4]> = BTreeMap::new();
for d in net.hvdc.iter().filter(|d| d.in_service || !in_service_only) {
let from = map.entry(d.from.0).or_default();
from[0] += d.pf;
from[2] += d.qf;
let to = map.entry(d.to.0).or_default();
to[1] += d.pt;
to[3] += d.qt;
}
map
}
#[must_use]
pub fn dc_terminal_change(
before: &BalancedNetwork,
after: &BalancedNetwork,
) -> Option<InjectionChange> {
let change = first_moved(
&per_bus_dc(before, true),
&per_bus_dc(after, true),
DC_TERMINALS,
)?;
let status_only = first_moved(
&per_bus_dc(before, false),
&per_bus_dc(after, false),
DC_TERMINALS,
)
.is_none();
Some(InjectionChange {
status_only,
..change
})
}
fn first_moved<const N: usize>(
a: &BTreeMap<usize, [f64; N]>,
b: &BTreeMap<usize, [f64; N]>,
quantities: [Injection; N],
) -> Option<InjectionChange> {
for key in a.keys().chain(b.keys().filter(|k| !a.contains_key(*k))) {
let x = a.get(key).copied().unwrap_or([0.0; N]);
let y = b.get(key).copied().unwrap_or([0.0; N]);
for (i, quantity) in quantities.into_iter().enumerate() {
if beyond_tol(x[i], y[i], ELECTRICAL_TOL) {
return Some(InjectionChange {
bus: *key,
quantity,
before: x[i],
after: y[i],
status_only: false,
});
}
}
}
None
}
fn beyond_tol(x: f64, y: f64, tol: f64) -> bool {
if !x.is_finite() || !y.is_finite() {
#[allow(clippy::float_cmp)]
return !(x.is_nan() && y.is_nan()) && x != y;
}
(x - y).abs() > tol * x.abs().max(y.abs()).max(1.0)
}
#[must_use]
pub fn status_disagreements(a: &BalancedNetwork, b: &BalancedNetwork) -> usize {
let off = |net: &BalancedNetwork| {
[
net.generators.iter().filter(|g| !g.in_service).count(),
net.branches.iter().filter(|x| !x.in_service).count(),
net.loads.iter().filter(|l| !l.in_service).count(),
net.shunts.iter().filter(|s| !s.in_service).count(),
net.switches.iter().filter(|x| !x.closed).count(),
net.storage.iter().filter(|x| !x.in_service).count(),
net.hvdc.iter().filter(|d| !d.in_service).count(),
net.transformers_3w.iter().filter(|t| !t.in_service).count(),
]
};
off(a)
.iter()
.zip(off(b).iter())
.map(|(x, y)| x.abs_diff(*y))
.sum()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TransmissionCore {
pub buses: usize,
pub branches: usize,
pub generators: usize,
pub loads: usize,
pub shunts: usize,
pub load_p: i64,
pub load_q: i64,
pub gen_p: i64,
pub base_mva: i64,
}
fn milli(x: f64) -> i64 {
(x * 1e3).round() as i64
}
#[must_use]
pub fn transmission_core(net: &BalancedNetwork) -> TransmissionCore {
TransmissionCore {
buses: net.buses.len(),
branches: net.branches.len(),
generators: net.generators.len(),
loads: net.loads.len(),
shunts: net.shunts.len(),
load_p: milli(net.loads.iter().map(|load| load.p).sum()),
load_q: milli(net.loads.iter().map(|load| load.q).sum()),
gen_p: milli(net.generators.iter().map(|generator| generator.pg).sum()),
base_mva: milli(net.base_mva),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DistributionCore {
pub buses: usize,
pub loads: usize,
pub generators: usize,
pub shunts: usize,
pub load_p: i64,
pub load_q: i64,
}
#[must_use]
pub fn distribution_core(net: &MulticonductorNetwork) -> DistributionCore {
DistributionCore {
buses: net.buses.len(),
loads: net.loads.len(),
generators: net.generators.len(),
shunts: net.shunts.len(),
load_p: milli(net.loads.iter().flat_map(|load| load.p_nom.iter()).sum()),
load_q: milli(net.loads.iter().flat_map(|load| load.q_nom.iter()).sum()),
}
}
#[must_use]
pub fn transmission_value(net: &BalancedNetwork) -> serde_json::Value {
let mut net = net.clone();
net.name = String::new();
net.source = None;
net.source_format = powerio_matrix::SourceFormat::Matpower;
for br in &mut net.branches {
br.charging = Some(br.terminal_charging());
}
net.buses.sort_by_key(|b| b.id);
net.branches.sort_by_key(|a| (a.from, a.to));
net.loads.sort_by_key(|l| l.bus);
net.shunts.sort_by_key(|s| s.bus);
net.generators.sort_by_key(|g| g.bus);
net.storage.sort_by_key(|s| s.bus);
net.hvdc.sort_by_key(|d| (d.from, d.to));
serde_json::to_value(&net).unwrap()
}
#[must_use]
pub fn distribution_value(net: &MulticonductorNetwork) -> serde_json::Value {
let mut net = net.clone();
net.name = None;
net.source = None;
net.source_format = None;
net.warnings = Vec::new();
serde_json::to_value(&net).unwrap()
}
#[derive(Debug, Clone, PartialEq)]
pub struct ModelDiff {
pub path: String,
pub before: DiffSide,
pub after: DiffSide,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DiffSide {
Number(f64),
Value(String),
Len(usize),
Absent,
}
impl std::fmt::Display for DiffSide {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Number(x) => write!(f, "{x}"),
Self::Value(v) => write!(f, "{v}"),
Self::Len(n) => write!(f, "{n} entries"),
Self::Absent => write!(f, "absent"),
}
}
}
impl std::fmt::Display for ModelDiff {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match (&self.before, &self.after) {
(DiffSide::Len(x), DiffSide::Len(y)) => {
write!(f, "{}: {x} entries -> {y}", self.path)
}
(before, after) => write!(f, "{}: {before} -> {after}", self.path),
}
}
}
#[must_use]
pub fn model_diffs(before: &serde_json::Value, after: &serde_json::Value) -> Vec<ModelDiff> {
let mut out = Vec::new();
diff_values(before, after, "", &mut out);
out
}
fn diff_values(a: &serde_json::Value, b: &serde_json::Value, path: &str, out: &mut Vec<ModelDiff>) {
use serde_json::Value;
if out.len() >= MAX_MODEL_DIFFS {
return;
}
match (a, b) {
(Value::Number(x), Value::Number(y)) => {
let (x, y) = (
x.as_f64().unwrap_or(f64::NAN),
y.as_f64().unwrap_or(f64::NAN),
);
let within = (x - y).abs() <= MODEL_TOL * x.abs().max(y.abs()).max(1.0);
if !(within || (x.is_nan() && y.is_nan())) {
out.push(ModelDiff {
path: path.to_string(),
before: DiffSide::Number(x),
after: DiffSide::Number(y),
});
}
}
(Value::Array(xs), Value::Array(ys)) => {
if xs.len() != ys.len() {
out.push(ModelDiff {
path: path.to_string(),
before: DiffSide::Len(xs.len()),
after: DiffSide::Len(ys.len()),
});
return;
}
for (i, (x, y)) in xs.iter().zip(ys).enumerate() {
diff_values(x, y, &format!("{path}[{i}]"), out);
}
}
(Value::Object(xs), Value::Object(ys)) => {
for key in xs.keys().chain(ys.keys().filter(|k| !xs.contains_key(*k))) {
if out.len() >= MAX_MODEL_DIFFS {
return;
}
let sub = format!("{path}.{key}");
match (xs.get(key), ys.get(key)) {
(Some(x), Some(y)) => diff_values(x, y, &sub, out),
(Some(x), None) => out.push(ModelDiff {
path: sub,
before: DiffSide::Value(x.to_string()),
after: DiffSide::Absent,
}),
(None, Some(y)) => out.push(ModelDiff {
path: sub,
before: DiffSide::Absent,
after: DiffSide::Value(y.to_string()),
}),
(None, None) => unreachable!(),
}
}
}
(x, y) if x == y => {}
(x, y) => out.push(ModelDiff {
path: path.to_string(),
before: DiffSide::Value(x.to_string()),
after: DiffSide::Value(y.to_string()),
}),
}
}