use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ZipModel {
pub p0: f64,
pub q0: f64,
pub alpha: [f64; 3], pub beta: [f64; 3], pub v_nom: f64,
}
impl ZipModel {
pub fn constant_power(p0: f64, q0: f64) -> Self {
Self {
p0,
q0,
alpha: [0.0, 0.0, 1.0],
beta: [0.0, 0.0, 1.0],
v_nom: 1.0,
}
}
pub fn constant_impedance(p0: f64, q0: f64) -> Self {
Self {
p0,
q0,
alpha: [1.0, 0.0, 0.0],
beta: [1.0, 0.0, 0.0],
v_nom: 1.0,
}
}
pub fn residential(p0: f64, q0: f64) -> Self {
Self {
p0,
q0,
alpha: [0.10, 0.20, 0.70],
beta: [0.10, 0.20, 0.70],
v_nom: 1.0,
}
}
pub fn industrial(p0: f64, q0: f64) -> Self {
Self {
p0,
q0,
alpha: [0.30, 0.30, 0.40],
beta: [0.30, 0.30, 0.40],
v_nom: 1.0,
}
}
pub fn active_power(&self, v: f64) -> f64 {
let vn = v / self.v_nom;
self.p0 * (self.alpha[0] * vn * vn + self.alpha[1] * vn + self.alpha[2])
}
pub fn reactive_power(&self, v: f64) -> f64 {
let vn = v / self.v_nom;
self.q0 * (self.beta[0] * vn * vn + self.beta[1] * vn + self.beta[2])
}
pub fn dp_dv_nominal(&self) -> f64 {
self.p0 * (2.0 * self.alpha[0] * self.v_nom + self.alpha[1]) / (self.v_nom * self.v_nom)
}
pub fn is_voltage_stabilising(&self) -> bool {
self.dp_dv_nominal() > 0.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InductionMotorParams {
pub p_rated: f64,
pub power_factor: f64,
pub h: f64,
pub x_prime: f64,
pub r_s: f64,
pub mech_load_exp: f64,
}
impl InductionMotorParams {
pub fn air_conditioner(p_rated: f64) -> Self {
Self {
p_rated,
power_factor: 0.85,
h: 0.3,
x_prime: 0.12,
r_s: 0.02,
mech_load_exp: 0.0,
}
}
pub fn pump_fan(p_rated: f64) -> Self {
Self {
p_rated,
power_factor: 0.88,
h: 1.5,
x_prime: 0.18,
r_s: 0.015,
mech_load_exp: 2.0,
}
}
pub fn critical_slip(&self, v: f64) -> f64 {
let _ = v;
self.r_s / self.x_prime
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct MotorState {
pub slip: f64,
pub stalled: bool,
}
impl MotorState {
pub fn at_rated_load(params: &InductionMotorParams) -> Self {
let s_rated = params.r_s / params.x_prime;
Self {
slip: s_rated,
stalled: false,
}
}
}
fn electrical_torque(params: &InductionMotorParams, slip: f64, v: f64) -> f64 {
let s = slip.abs().max(1e-6);
let r_over_s = params.r_s / s;
let denom = r_over_s * r_over_s + params.x_prime * params.x_prime;
v * v * r_over_s / denom
}
fn mechanical_torque(params: &InductionMotorParams, slip: f64) -> f64 {
let omega_pu = (1.0 - slip).max(0.0);
params.p_rated * omega_pu.powf(params.mech_load_exp)
}
pub fn motor_step(
params: &InductionMotorParams,
state: &MotorState,
v: f64,
dt: f64,
) -> (MotorState, f64) {
if state.stalled {
let p_stall = v * v / (params.x_prime + params.r_s);
return (
MotorState {
slip: 1.0,
stalled: true,
},
p_stall * params.p_rated,
);
}
let t_e = electrical_torque(params, state.slip, v);
let t_m = mechanical_torque(params, state.slip);
let ds_dt = (t_m - t_e) / (2.0 * params.h);
let new_slip = (state.slip + ds_dt * dt).clamp(0.0, 1.0);
let s_crit = params.critical_slip(v);
let stalled = new_slip >= s_crit * 5.0 || (v < 0.7 && new_slip > 0.5);
let omega = (1.0 - new_slip).max(0.0);
let p_mw = t_e * omega * params.p_rated;
(
MotorState {
slip: new_slip,
stalled,
},
p_mw.max(0.0),
)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClodModel {
pub motor_frac: f64,
pub motor: InductionMotorParams,
pub zip: ZipModel,
pub p_total: f64,
}
impl ClodModel {
pub fn wecc_standard(p_total: f64, q_total: f64) -> Self {
Self {
motor_frac: 0.4,
motor: InductionMotorParams::air_conditioner(p_total * 0.4),
zip: ZipModel::residential(p_total * 0.6, q_total * 0.6),
p_total,
}
}
pub fn active_power(&self, v: f64, motor_state: &MotorState) -> f64 {
let t_e = electrical_torque(&self.motor, motor_state.slip, v);
let omega = (1.0 - motor_state.slip).max(0.0);
let p_motor = t_e * omega * self.motor.p_rated;
let p_zip = self.zip.active_power(v);
p_motor + p_zip
}
pub fn reactive_power(&self, v: f64, _motor_state: &MotorState) -> f64 {
let q_motor = v * v / self.motor.x_prime * self.motor.p_rated;
let q_zip = self.zip.reactive_power(v);
q_motor + q_zip
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoadRecoveryModel {
pub p0: f64,
pub q0: f64,
pub alpha_s: f64,
pub alpha_t: f64,
pub beta_s: f64,
pub beta_t: f64,
pub t_p: f64,
pub t_q: f64,
pub x_p: f64, pub x_q: f64, }
impl LoadRecoveryModel {
pub fn new(p0: f64, q0: f64, t_p: f64, t_q: f64) -> Self {
Self {
p0,
q0,
alpha_s: 0.5,
alpha_t: 2.0,
beta_s: 3.5,
beta_t: 7.0,
t_p,
t_q,
x_p: 0.0,
x_q: 0.0,
}
}
pub fn power_demand(&self, v: f64) -> (f64, f64) {
let p = self.p0 * v.powf(self.alpha_t) + self.x_p;
let q = self.q0 * v.powf(self.beta_t) + self.x_q;
(p.max(0.0), q.max(0.0))
}
pub fn step(&mut self, v: f64, dt: f64) {
let p_s = self.p0 * v.powf(self.alpha_s);
let p_t = self.p0 * v.powf(self.alpha_t);
let dx_p = (p_s - p_t - self.x_p) / self.t_p;
self.x_p += dx_p * dt;
let q_s = self.q0 * v.powf(self.beta_s);
let q_t = self.q0 * v.powf(self.beta_t);
let dx_q = (q_s - q_t - self.x_q) / self.t_q;
self.x_q += dx_q * dt;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zip_constant_power() {
let z = ZipModel::constant_power(1.0, 0.3);
assert!((z.active_power(1.0) - 1.0).abs() < 1e-10);
assert!((z.active_power(0.9) - 1.0).abs() < 1e-10); assert!((z.reactive_power(1.0) - 0.3).abs() < 1e-10);
}
#[test]
fn test_zip_constant_impedance_voltage_sensitive() {
let z = ZipModel::constant_impedance(1.0, 0.3);
let p09 = z.active_power(0.9);
let p10 = z.active_power(1.0);
assert!(
p09 < p10,
"Z load decreases with voltage: {:.4} < {:.4}",
p09,
p10
);
assert!(
(p09 / p10 - 0.81).abs() < 0.01,
"P ~ V²: ratio={:.4}",
p09 / p10
);
}
#[test]
fn test_zip_residential_fractions_sum_to_one() {
let z = ZipModel::residential(1.0, 1.0);
let sum_alpha: f64 = z.alpha.iter().sum();
assert!(
(sum_alpha - 1.0).abs() < 1e-10,
"α fractions: {:.6}",
sum_alpha
);
}
#[test]
fn test_zip_dp_dv_positive_for_z_load() {
let z = ZipModel::constant_impedance(1.0, 0.3);
assert!(z.dp_dv_nominal() > 0.0);
assert!(z.is_voltage_stabilising());
}
#[test]
fn test_zip_dp_dv_zero_for_p_load() {
let z = ZipModel::constant_power(1.0, 0.3);
assert!(z.dp_dv_nominal().abs() < 1e-10);
}
#[test]
fn test_motor_at_rated_load_small_slip() {
let params = InductionMotorParams::air_conditioner(0.5);
let state = MotorState::at_rated_load(¶ms);
assert!(
state.slip > 0.0 && state.slip < 0.5,
"Rated slip out of range: {:.4}",
state.slip
);
}
#[test]
fn test_motor_step_nominal_voltage() {
let params = InductionMotorParams::pump_fan(1.0);
let state = MotorState::at_rated_load(¶ms);
let (new_state, p) = motor_step(¶ms, &state, 1.0, 0.01);
assert!(!new_state.stalled, "Motor should not stall at V=1.0");
assert!(p > 0.0, "Active power should be positive: {:.4}", p);
}
#[test]
fn test_motor_step_low_voltage_may_stall() {
let params = InductionMotorParams::air_conditioner(1.0);
let mut state = MotorState {
slip: 0.5,
stalled: false,
};
for _ in 0..100 {
let (ns, _) = motor_step(¶ms, &state, 0.5, 0.01);
state = ns;
}
let _ = state.stalled;
}
#[test]
fn test_motor_stalled_consumes_power() {
let params = InductionMotorParams::air_conditioner(1.0);
let state = MotorState {
slip: 1.0,
stalled: true,
};
let (new_state, p) = motor_step(¶ms, &state, 1.0, 0.01);
assert!(new_state.stalled);
assert!(p > 0.0, "Stalled motor should consume power: {:.4}", p);
}
#[test]
fn test_clod_active_power_at_nominal() {
let clod = ClodModel::wecc_standard(1.0, 0.3);
let state = MotorState::at_rated_load(&clod.motor);
let p = clod.active_power(1.0, &state);
assert!(p > 0.0, "CLOD active power: {:.4}", p);
}
#[test]
fn test_clod_reactive_power_positive() {
let clod = ClodModel::wecc_standard(1.0, 0.3);
let state = MotorState::at_rated_load(&clod.motor);
let q = clod.reactive_power(1.0, &state);
assert!(q > 0.0, "CLOD reactive power: {:.4}", q);
}
#[test]
fn test_load_recovery_nominal_steady_state() {
let model = LoadRecoveryModel::new(1.0, 0.3, 60.0, 80.0);
let (p, q) = model.power_demand(1.0);
assert!((p - 1.0).abs() < 1e-10, "P at V=1: {:.6}", p);
assert!((q - 0.3).abs() < 1e-10, "Q at V=1: {:.6}", q);
}
#[test]
fn test_load_recovery_step_after_voltage_dip() {
let mut model = LoadRecoveryModel::new(1.0, 0.3, 30.0, 40.0);
let v_dip = 0.8;
for _ in 0..100 {
model.step(v_dip, 0.1);
}
let _ = model.x_p;
let (p_dip, _) = model.power_demand(v_dip);
assert!(
p_dip < 1.0,
"Power at dip should be < nominal: {:.4}",
p_dip
);
}
#[test]
fn test_electrical_torque_increases_with_voltage() {
let params = InductionMotorParams::air_conditioner(1.0);
let t1 = electrical_torque(¶ms, 0.05, 0.8);
let t2 = electrical_torque(¶ms, 0.05, 1.0);
assert!(
t2 > t1,
"Torque increases with voltage: {:.4} > {:.4}",
t2,
t1
);
}
}