use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Avr1Params {
pub ka: f64,
pub ta: f64,
pub ke: f64,
pub te: f64,
pub kf: f64,
pub tf: f64,
pub vr_max: f64,
pub vr_min: f64,
pub a_e: f64,
pub b_e: f64,
}
impl Avr1Params {
pub fn steam_typical() -> Self {
Self {
ka: 46.0,
ta: 0.06,
ke: -0.047,
te: 0.46,
kf: 0.1,
tf: 1.0,
vr_max: 1.0,
vr_min: -0.95,
a_e: 0.0039,
b_e: 1.555,
}
}
pub fn hydro_slow() -> Self {
Self {
ka: 20.0,
ta: 0.20,
ke: 1.0,
te: 0.95,
kf: 0.06,
tf: 1.0,
vr_max: 3.5,
vr_min: 0.0,
a_e: 0.0,
b_e: 0.0,
}
}
pub fn saturation(&self, efd: f64) -> f64 {
if self.a_e < 1e-12 || efd <= 0.0 {
return 0.0;
}
self.a_e * (self.b_e * efd).exp()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Avr1State {
pub efd: f64,
pub vr: f64,
pub rf: f64,
pub vref: f64,
pub time_s: f64,
}
impl Avr1State {
pub fn from_steady_state(efd0: f64, vt: f64, params: &Avr1Params) -> Self {
let rf0 = (params.kf / params.tf) * efd0;
let se0 = params.saturation(efd0);
let vr0 = (params.ke + se0) * efd0;
let vref0 = vt + vr0 / params.ka;
Self {
efd: efd0,
vr: vr0,
rf: rf0,
vref: vref0,
time_s: 0.0,
}
}
fn derivatives(&self, vt: f64, params: &Avr1Params) -> (f64, f64, f64) {
let vf = self.rf - (params.kf / params.tf) * self.efd;
let ve = self.vref - vt - vf;
let se = params.saturation(self.efd);
let defd_dt = (self.vr - (params.ke + se) * self.efd) / params.te;
let dvr_dt_unclamped = (-self.vr + params.ka * ve) / params.ta;
let at_limit = (self.vr >= params.vr_max && dvr_dt_unclamped > 0.0)
|| (self.vr <= params.vr_min && dvr_dt_unclamped < 0.0);
let dvr_dt = if at_limit { 0.0 } else { dvr_dt_unclamped };
let drf_dt = (-self.rf + (params.kf / params.tf) * self.efd) / params.tf;
(defd_dt, dvr_dt, drf_dt)
}
pub fn step_rk4(&mut self, vt: f64, dt: f64, params: &Avr1Params) {
let (k1e, k1r, k1f) = self.derivatives(vt, params);
let s2 = Avr1State {
efd: self.efd + 0.5 * dt * k1e,
vr: (self.vr + 0.5 * dt * k1r).clamp(params.vr_min, params.vr_max),
rf: self.rf + 0.5 * dt * k1f,
..*self
};
let (k2e, k2r, k2f) = s2.derivatives(vt, params);
let s3 = Avr1State {
efd: self.efd + 0.5 * dt * k2e,
vr: (self.vr + 0.5 * dt * k2r).clamp(params.vr_min, params.vr_max),
rf: self.rf + 0.5 * dt * k2f,
..*self
};
let (k3e, k3r, k3f) = s3.derivatives(vt, params);
let s4 = Avr1State {
efd: self.efd + dt * k3e,
vr: (self.vr + dt * k3r).clamp(params.vr_min, params.vr_max),
rf: self.rf + dt * k3f,
..*self
};
let (k4e, k4r, k4f) = s4.derivatives(vt, params);
self.efd += dt / 6.0 * (k1e + 2.0 * k2e + 2.0 * k3e + k4e);
self.vr = (self.vr + dt / 6.0 * (k1r + 2.0 * k2r + 2.0 * k3r + k4r))
.clamp(params.vr_min, params.vr_max);
self.rf += dt / 6.0 * (k1f + 2.0 * k2f + 2.0 * k3f + k4f);
self.time_s += dt;
}
pub fn simulate_voltage_step(
efd0: f64,
vt_initial: f64,
vt_final: f64,
t_step: f64,
t_end: f64,
dt: f64,
params: &Avr1Params,
) -> (Vec<f64>, Vec<f64>) {
let mut state = Avr1State::from_steady_state(efd0, vt_initial, params);
let mut times = Vec::new();
let mut efds = Vec::new();
while state.time_s <= t_end {
let vt = if state.time_s >= t_step {
vt_final
} else {
vt_initial
};
times.push(state.time_s);
efds.push(state.efd);
state.step_rk4(vt, dt, params);
}
(times, efds)
}
}
pub struct AvrGenerator {
pub params: Avr1Params,
pub state: Avr1State,
}
impl AvrGenerator {
pub fn new(params: Avr1Params, efd0: f64, vt0: f64) -> Self {
let state = Avr1State::from_steady_state(efd0, vt0, ¶ms);
Self { params, state }
}
pub fn step(&mut self, vt: f64, dt: f64) {
self.state.step_rk4(vt, dt, &self.params);
}
pub fn efd(&self) -> f64 {
self.state.efd
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_steady_state_initialisation() {
let params = Avr1Params::steam_typical();
let efd0 = 1.2;
let vt0 = 1.0;
let state = Avr1State::from_steady_state(efd0, vt0, ¶ms);
let (de, dv, dr) = state.derivatives(vt0, ¶ms);
assert!(de.abs() < 1e-6, "dEfd/dt = {de:.2e} at steady state");
assert!(dv.abs() < 1e-6, "dVr/dt = {dv:.2e} at steady state");
assert!(dr.abs() < 1e-9, "dRf/dt = {dr:.2e} at steady state");
}
#[test]
fn test_voltage_step_response() {
let params = Avr1Params::steam_typical();
let (times, efds) =
Avr1State::simulate_voltage_step(1.2, 1.0, 1.05, 0.5, 5.0, 0.01, ¶ms);
assert!(!times.is_empty());
let efd_initial = efds[0];
let efd_final = *efds.last().unwrap();
assert!(
efd_final < efd_initial,
"Efd should decrease for voltage increase: {efd_initial:.4} → {efd_final:.4}"
);
}
#[test]
fn test_avr_generator_step() {
let params = Avr1Params::hydro_slow();
let mut avr = AvrGenerator::new(params, 1.1, 1.0);
let efd_init = avr.efd();
for _ in 0..100 {
avr.step(1.0, 0.01);
}
assert!(
(avr.efd() - efd_init).abs() < 0.01,
"Efd drifted from SS: {efd_init:.4} → {:.4}",
avr.efd()
);
}
#[test]
fn test_saturation_function() {
let params = Avr1Params::steam_typical();
assert!(params.saturation(0.0).abs() < 0.01);
assert!(params.saturation(2.0) > params.saturation(1.0));
}
#[test]
fn test_vr_limiter() {
let params = Avr1Params::steam_typical();
let state = Avr1State {
efd: 1.2,
vr: params.vr_max,
rf: 0.12,
vref: 1.0,
time_s: 0.0,
};
let (_, dvr, _) = state.derivatives(0.5, ¶ms);
assert!(
dvr <= 0.0,
"Vr limiter should block positive dVr when at max: dvr = {dvr:.4}"
);
}
#[test]
fn test_hydro_slow_preset_parameters() {
let p = Avr1Params::hydro_slow();
assert!(
(p.ke - 1.0).abs() < 1e-12,
"hydro_slow ke should be 1.0, got {:.6}",
p.ke
);
assert!(
p.a_e.abs() < 1e-12,
"hydro_slow a_e should be 0.0 (no saturation), got {:.6}",
p.a_e
);
assert!(
p.vr_min.abs() < 1e-12,
"hydro_slow vr_min should be 0.0, got {:.6}",
p.vr_min
);
assert!(
(p.vr_max - 3.5).abs() < 1e-12,
"hydro_slow vr_max should be 3.5, got {:.6}",
p.vr_max
);
}
#[test]
fn test_saturation_zero_for_nonpositive_efd() {
let p = Avr1Params::steam_typical();
assert_eq!(
p.saturation(-1.0),
0.0,
"saturation(-1.0) must be 0.0 for steam_typical"
);
assert_eq!(
p.saturation(0.0),
0.0,
"saturation(0.0) must be 0.0 for steam_typical"
);
}
#[test]
fn test_saturation_monotonically_increasing() {
let p = Avr1Params::steam_typical();
let se1 = p.saturation(1.0);
let se15 = p.saturation(1.5);
let se2 = p.saturation(2.0);
assert!(
se2 > se15,
"saturation(2.0)={se2:.6} must exceed saturation(1.5)={se15:.6}"
);
assert!(
se15 > se1,
"saturation(1.5)={se15:.6} must exceed saturation(1.0)={se1:.6}"
);
}
#[test]
fn test_hydro_slow_steady_state_derivatives() {
let params = Avr1Params::hydro_slow();
let efd0 = 1.0;
let vt = 1.0;
let state = Avr1State::from_steady_state(efd0, vt, ¶ms);
let (de, dv, dr) = state.derivatives(vt, ¶ms);
assert!(
de.abs() < 1e-6,
"dEfd/dt = {de:.2e} at hydro_slow steady state"
);
assert!(
dv.abs() < 1e-6,
"dVr/dt = {dv:.2e} at hydro_slow steady state"
);
assert!(
dr.abs() < 1e-9,
"dRf/dt = {dr:.2e} at hydro_slow steady state"
);
}
#[test]
fn test_simulate_voltage_step_trace_length() {
let params = Avr1Params::steam_typical();
let dt = 0.01;
let t_end = 2.0;
let (times, efds) =
Avr1State::simulate_voltage_step(1.2, 1.0, 1.05, 0.5, t_end, dt, ¶ms);
let expected = (t_end / dt) as usize + 1;
assert!(
times.len().abs_diff(expected) <= 1,
"expected ~{expected} samples, got {}",
times.len()
);
assert_eq!(
efds.len(),
times.len(),
"times and efds must have the same length"
);
assert!(
times[0].abs() < 1e-12,
"first time sample must be 0.0, got {}",
times[0]
);
}
#[test]
fn test_avr_generator_responds_to_voltage_dip() {
let params = Avr1Params::steam_typical();
let mut avr = AvrGenerator::new(params, 1.2, 1.0);
let efd_before = avr.efd();
for _ in 0..50 {
avr.step(0.9, 0.01);
}
let efd_after = avr.efd();
assert!(
efd_after > efd_before,
"Efd should increase after voltage dip: {efd_before:.4} → {efd_after:.4}"
);
}
#[test]
fn test_time_advances_correctly_after_rk4_steps() {
let params = Avr1Params::steam_typical();
let mut state = Avr1State::from_steady_state(1.2, 1.0, ¶ms);
let dt = 0.01_f64;
let n_steps = 37_usize;
for _ in 0..n_steps {
state.step_rk4(1.0, dt, ¶ms);
}
let expected_time = dt * n_steps as f64;
assert!(
(state.time_s - expected_time).abs() < 1e-9,
"time_s should be {expected_time:.6} after {n_steps} steps, got {:.6}",
state.time_s
);
}
}