use num_complex::Complex;
use crate::network::{ShuntAdm, build_ybus, power_injections, stamp_shunts};
use crate::types::{Bus, Line, Transformer};
use super::DynamicSystem;
use super::dae::{DaeLayout, DaePattern, residual};
use super::models::{DynamicModel, InitError};
pub struct DeviceSpec {
pub id: String,
pub bus: usize,
pub s: Complex<f64>,
pub model: Box<dyn DynamicModel>,
}
impl std::fmt::Debug for DeviceSpec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DeviceSpec")
.field("id", &self.id)
.field("bus", &self.bus)
.field("s", &self.s)
.field("model", &self.model)
.finish()
}
}
#[derive(Debug)]
pub struct SystemSpec<'a> {
pub buses: &'a [Bus],
pub lines: &'a [Line],
pub transformers: &'a [Transformer],
pub shunts: &'a [ShuntAdm],
pub devices: Vec<DeviceSpec>,
pub fixed_buses: Vec<usize>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum BuildError {
BusOutOfRange {
id: String,
bus: usize,
n_bus: usize,
},
DeviceAtFixedBus {
id: String,
bus: usize,
},
ModelInit {
id: String,
source: InitError,
},
BusWithoutAdmittance {
bus: usize,
},
NotAnEquilibrium {
max_derivative: f64,
},
NetworkResidual {
norm: f64,
},
}
impl std::fmt::Display for BuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BuildError::BusOutOfRange { id, bus, n_bus } => {
write!(
f,
"device {id} is at bus {bus}, but the network has {n_bus} buses"
)
}
BuildError::DeviceAtFixedBus { id, bus } => {
write!(
f,
"device {id} is at bus {bus}, whose voltage is held constant"
)
}
BuildError::ModelInit { id, source } => write!(f, "device {id}: {source}"),
BuildError::BusWithoutAdmittance { bus } => {
write!(
f,
"bus {bus} has no admittance to anything, so the network block is singular"
)
}
BuildError::NotAnEquilibrium { max_derivative } => write!(
f,
"initial point is not an equilibrium: largest state derivative is {max_derivative:.3e}"
),
BuildError::NetworkResidual { norm } => write!(
f,
"network constraint is not satisfied at the initial point: residual {norm:.3e}"
),
}
}
}
impl std::error::Error for BuildError {}
const EQUILIBRIUM_TOL: f64 = 1e-8;
pub fn build(spec: SystemSpec<'_>) -> Result<DynamicSystem, BuildError> {
let n_bus = spec.buses.len();
let mut is_fixed = vec![false; n_bus];
for &b in &spec.fixed_buses {
if b < n_bus {
is_fixed[b] = true;
}
}
for dev in &spec.devices {
if dev.bus >= n_bus {
return Err(BuildError::BusOutOfRange {
id: dev.id.clone(),
bus: dev.bus,
n_bus,
});
}
if is_fixed[dev.bus] {
return Err(BuildError::DeviceAtFixedBus {
id: dev.id.clone(),
bus: dev.bus,
});
}
}
let v: Vec<Complex<f64>> = spec
.buses
.iter()
.map(|b| Complex::from_polar(b.voltage_mag, b.voltage_ang))
.collect();
let mut base = build_ybus(n_bus, spec.lines, spec.transformers);
stamp_shunts(&mut base, spec.shunts);
let (p_calc, q_calc) = power_injections(spec.buses, &base.finish());
let mut ybus = build_ybus(n_bus, spec.lines, spec.transformers);
stamp_shunts(&mut ybus, spec.shunts);
let mut remaining: Vec<Complex<f64>> = (0..n_bus)
.map(|i| Complex::new(p_calc[i], q_calc[i]))
.collect();
for dev in &spec.devices {
remaining[dev.bus] -= dev.s;
}
let mut load_y = vec![Complex::new(0.0, 0.0); n_bus];
for i in 0..n_bus {
let v_sq = v[i].norm_sqr();
if !is_fixed[i] && v_sq != 0.0 {
load_y[i] = -remaining[i].conj() / v_sq;
}
ybus.add(i, i, load_y[i]);
}
for dev in &spec.devices {
if let Some(y) = dev.model.norton_admittance() {
ybus.add(dev.bus, dev.bus, y);
}
}
let ybus = ybus.finish();
let mut ids = Vec::with_capacity(spec.devices.len());
let mut models: Vec<Box<dyn DynamicModel>> = Vec::with_capacity(spec.devices.len());
let mut dev_bus = Vec::with_capacity(spec.devices.len());
let mut dev_len = Vec::with_capacity(spec.devices.len());
let mut x0: Vec<f64> = Vec::new();
for dev in spec.devices {
let DeviceSpec {
id,
bus,
s,
mut model,
} = dev;
let states = model
.initialize(v[bus], s)
.map_err(|source| BuildError::ModelInit {
id: id.clone(),
source,
})?;
dev_len.push(states.len());
dev_bus.push(bus);
x0.extend_from_slice(&states);
ids.push(id);
models.push(model);
}
let layout = DaeLayout::new(&dev_len, &dev_bus, n_bus);
let pattern = DaePattern::analyze(layout, &ybus, &spec.fixed_buses);
if let Some(bus) = pattern.bus_without_diagonal() {
return Err(BuildError::BusWithoutAdmittance { bus });
}
let system = DynamicSystem {
models,
ids,
ybus,
pattern,
v_fixed: v.clone(),
lines: spec.lines.to_vec(),
transformers: spec.transformers.to_vec(),
shunts: spec.shunts.to_vec(),
outaged: vec![false; spec.lines.len() + spec.transformers.len()],
load_y,
fault_y: vec![Complex::new(0.0, 0.0); n_bus],
x0,
v0: v,
};
let drift = system.max_derivative();
if drift > EQUILIBRIUM_TOL {
return Err(BuildError::NotAnEquilibrium {
max_derivative: drift,
});
}
let norm = system.network_residual_norm();
if norm > EQUILIBRIUM_TOL {
return Err(BuildError::NetworkResidual { norm });
}
Ok(system)
}
impl DynamicSystem {
pub fn network_residual_norm(&self) -> f64 {
let layout = self.pattern.layout();
let n = layout.n();
let f0 = vec![0.0; layout.n_diff];
let mut f_scratch = vec![0.0; layout.n_diff];
let mut out = vec![0.0; n];
residual(
&self.pattern,
&self.models,
&self.x0,
&f0,
&self.x0,
&self.v0,
&self.v_fixed,
0.0,
0.0,
&self.ybus,
&mut f_scratch,
&mut out,
);
out[layout.n_diff..]
.iter()
.fold(0.0f64, |m, r| m.max(r.abs()))
}
}