pub mod avr;
pub mod gov;
pub mod load;
pub mod machine;
pub mod pss;
pub mod unit;
use num_complex::Complex;
pub use avr::{Sexs, VrProportional};
pub use gov::{GoverProportional, Tgov1};
pub use load::ZipLoad;
pub use machine::{
GenCls, GenRound, GenSalient, GenTransient, Machine, MachineInit, MachineJacobian,
};
pub use pss::Stab1;
pub use unit::GeneratingUnit;
#[derive(Clone, Debug)]
pub struct ModelJacobian {
pub dfdx: Vec<f64>,
pub dfdv: Vec<f64>,
pub didx: Vec<f64>,
pub didv: [f64; 4],
}
impl ModelJacobian {
pub fn zeros(n_states: usize) -> Self {
Self {
dfdx: vec![0.0; n_states * n_states],
dfdv: vec![0.0; n_states * 2],
didx: vec![0.0; 2 * n_states],
didv: [0.0; 4],
}
}
pub fn clear(&mut self) {
self.dfdx.fill(0.0);
self.dfdv.fill(0.0);
self.didx.fill(0.0);
self.didv = [0.0; 4];
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum InitError {
ZeroTerminalVoltage,
NonPositiveParameter { name: &'static str, value: f64 },
OutsideLimits { name: &'static str, value: f64 },
}
impl std::fmt::Display for InitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InitError::ZeroTerminalVoltage => {
write!(
f,
"terminal voltage is zero, so no terminal current is defined"
)
}
InitError::NonPositiveParameter { name, value } => {
write!(f, "parameter {name} must be positive, got {value}")
}
InitError::OutsideLimits { name, value } => {
write!(
f,
"equilibrium value of {name} ({value}) lies outside its limits"
)
}
}
}
}
impl std::error::Error for InitError {}
pub trait DynamicModel: std::fmt::Debug {
fn n_states(&self) -> usize;
fn state_names(&self) -> &[&'static str];
fn norton_admittance(&self) -> Option<Complex<f64>>;
fn derivatives(&self, x: &[f64], v: Complex<f64>, out: &mut [f64]);
fn injection(&self, x: &[f64], v: Complex<f64>) -> Complex<f64>;
fn jacobian(&self, x: &[f64], v: Complex<f64>, out: &mut ModelJacobian);
fn angle_index(&self) -> Option<usize> {
None
}
fn speed_index(&self) -> Option<usize> {
None
}
fn tunable(&self) -> &'static [&'static str] {
&[]
}
fn parameter(&self, _name: &str) -> Option<f64> {
None
}
fn set_parameter(&mut self, _name: &str, _value: f64) -> Option<f64> {
None
}
fn latch(&self, _x: &[f64], _v: Complex<f64>) {}
fn project(&self, _x: &mut [f64]) {}
fn set_connected(&mut self, _connected: bool) {}
fn is_connected(&self) -> bool {
true
}
fn initialize(&mut self, v: Complex<f64>, s: Complex<f64>) -> Result<Vec<f64>, InitError>;
}
#[derive(Clone, Copy, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Limits {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum LimitState {
#[default]
Free,
AtMax,
AtMin,
}
impl Limits {
pub const NONE: Limits = Limits {
min: None,
max: None,
};
pub fn new(min: f64, max: f64) -> Self {
Self {
min: Some(min),
max: Some(max),
}
}
pub fn lower(&self) -> f64 {
self.min.unwrap_or(f64::NEG_INFINITY)
}
pub fn upper(&self) -> f64 {
self.max.unwrap_or(f64::INFINITY)
}
pub fn is_bounded(&self) -> bool {
self.min.is_some() || self.max.is_some()
}
pub fn latch(&self, x: f64, dx: f64) -> LimitState {
if x >= self.upper() && dx > 0.0 {
LimitState::AtMax
} else if x <= self.lower() && dx < 0.0 {
LimitState::AtMin
} else {
LimitState::Free
}
}
pub fn held(&self, state: LimitState, dx: f64) -> f64 {
match state {
LimitState::Free => dx,
_ => 0.0,
}
}
pub fn under(&self, state: LimitState, y: f64) -> (f64, f64) {
match state {
LimitState::Free => (y, 1.0),
LimitState::AtMax => (self.upper(), 0.0),
LimitState::AtMin => (self.lower(), 0.0),
}
}
pub fn clamp(&self, y: f64) -> (f64, f64) {
if y > self.upper() {
(self.upper(), 0.0)
} else if y < self.lower() {
(self.lower(), 0.0)
} else {
(y, 1.0)
}
}
pub fn require(&self, name: &'static str, y: f64) -> Result<(), InitError> {
if y > self.upper() || y < self.lower() {
return Err(InitError::OutsideLimits { name, value: y });
}
Ok(())
}
}
pub trait Control: std::fmt::Debug {
fn n_states(&self) -> usize;
fn state_names(&self) -> &[&'static str];
fn derivatives(&self, x: &[f64], u: f64, out: &mut [f64]);
fn jacobian(&self, x: &[f64], u: f64, dfdx: &mut [f64], dfdu: &mut [f64]);
fn output(&self, x: &[f64], u: f64) -> f64;
fn output_jacobian(&self, x: &[f64], u: f64, dydx: &mut [f64]) -> f64;
fn latch(&self, _x: &[f64], _u: f64) {}
fn project(&self, _x: &mut [f64]) {}
fn initialize(&mut self, y: f64, u: f64) -> Result<Vec<f64>, InitError>;
}
pub fn finite_difference(
model: &dyn DynamicModel,
x: &[f64],
v: Complex<f64>,
h: f64,
) -> ModelJacobian {
let n = model.n_states();
let mut out = ModelJacobian::zeros(n);
let mut f_plus = vec![0.0; n];
let mut f_minus = vec![0.0; n];
let mut probe = x.to_vec();
for j in 0..n {
let step = h * x[j].abs().max(1.0);
probe.copy_from_slice(x);
probe[j] = x[j] + step;
model.derivatives(&probe, v, &mut f_plus);
let i_plus = model.injection(&probe, v);
probe[j] = x[j] - step;
model.derivatives(&probe, v, &mut f_minus);
let i_minus = model.injection(&probe, v);
for i in 0..n {
out.dfdx[i * n + j] = (f_plus[i] - f_minus[i]) / (2.0 * step);
}
out.didx[j] = (i_plus.re - i_minus.re) / (2.0 * step);
out.didx[n + j] = (i_plus.im - i_minus.im) / (2.0 * step);
}
for (col, unit) in [Complex::new(1.0, 0.0), Complex::new(0.0, 1.0)]
.iter()
.enumerate()
{
let step = h * v.norm().max(1.0);
let v_plus = v + unit * step;
let v_minus = v - unit * step;
model.derivatives(x, v_plus, &mut f_plus);
let i_plus = model.injection(x, v_plus);
model.derivatives(x, v_minus, &mut f_minus);
let i_minus = model.injection(x, v_minus);
for i in 0..n {
out.dfdv[i * 2 + col] = (f_plus[i] - f_minus[i]) / (2.0 * step);
}
out.didv[col] = (i_plus.re - i_minus.re) / (2.0 * step);
out.didv[2 + col] = (i_plus.im - i_minus.im) / (2.0 * step);
}
out
}