use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tgov1Params {
pub r: f64,
pub t1: f64,
pub t2: f64,
pub t3: f64,
pub v_max: f64,
pub v_min: f64,
pub dt: f64,
}
impl Tgov1Params {
pub fn steam_typical() -> Self {
Self {
r: 0.05, t1: 0.50, t2: 3.00, t3: 10.0, v_max: 1.0,
v_min: 0.0,
dt: 0.02,
}
}
pub fn gas_turbine() -> Self {
Self {
r: 0.04,
t1: 0.20,
t2: 0.30,
t3: 3.00,
v_max: 1.0,
v_min: 0.0,
dt: 0.01,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Tgov1State {
pub x1: f64,
pub x2: f64,
pub p_ref: f64,
pub p_m: f64,
}
pub struct Tgov1 {
pub params: Tgov1Params,
pub state: Tgov1State,
}
impl Tgov1 {
pub fn new(params: Tgov1Params, p_m_init: f64) -> Self {
let x1 = p_m_init; let x2 = p_m_init;
Self {
state: Tgov1State {
x1,
x2,
p_ref: p_m_init,
p_m: p_m_init,
},
params,
}
}
pub fn step(&mut self, omega_pu: f64, dt: f64) -> f64 {
let p = &self.params;
let delta_omega = omega_pu - 1.0;
let u_gov = self.state.p_ref - delta_omega / p.r;
let dx1 = (u_gov.clamp(p.v_min, p.v_max) - self.state.x1) / p.t1;
self.state.x1 = (self.state.x1 + dt * dx1).clamp(p.v_min, p.v_max);
let dx2 = (self.state.x1 - self.state.x2) / p.t2.max(1e-6);
self.state.x2 += dt * dx2;
let p_m_raw = self.state.x2 - p.dt * delta_omega;
self.state.p_m = p_m_raw.clamp(p.v_min, p.v_max);
self.state.p_m
}
pub fn set_reference(&mut self, p_ref: f64) {
self.state.p_ref = p_ref.clamp(self.params.v_min, self.params.v_max);
}
pub fn reset(&mut self, p_m_init: f64) {
self.state.x1 = p_m_init;
self.state.x2 = p_m_init;
self.state.p_ref = p_m_init;
self.state.p_m = p_m_init;
}
}
pub struct DroopGovernor {
pub r: f64,
pub p_ref: f64,
pub omega_ref: f64,
pub p_max: f64,
pub p_min: f64,
}
impl DroopGovernor {
pub fn new(r: f64, p_ref: f64) -> Self {
Self {
r,
p_ref,
omega_ref: 1.0,
p_max: 1.0,
p_min: 0.0,
}
}
pub fn mechanical_power(&self, omega_pu: f64) -> f64 {
let delta_omega = omega_pu - self.omega_ref;
(self.p_ref - delta_omega / self.r).clamp(self.p_min, self.p_max)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tgov1_steady_state_unchanged() {
let params = Tgov1Params::steam_typical();
let mut gov = Tgov1::new(params, 0.8);
for _ in 0..1000 {
gov.step(1.0, 0.01);
}
assert!(
(gov.state.p_m - 0.8).abs() < 0.05,
"Pm should remain near setpoint: {:.4}",
gov.state.p_m
);
}
#[test]
fn test_tgov1_response_to_underfrequency() {
let params = Tgov1Params::steam_typical();
let mut gov = Tgov1::new(params, 0.8);
let p_init = gov.state.p_m;
for _ in 0..200 {
gov.step(0.99, 0.01); }
assert!(
gov.state.p_m > p_init,
"Governor should increase Pm for under-speed"
);
}
#[test]
fn test_tgov1_clamping() {
let params = Tgov1Params::steam_typical();
let mut gov = Tgov1::new(params, 0.95);
for _ in 0..500 {
let pm = gov.step(0.90, 0.01);
assert!(
(0.0 - 1e-10..=1.0 + 1e-10).contains(&pm),
"Pm must be within limits: {:.4}",
pm
);
}
}
#[test]
fn test_droop_governor_response() {
let gov = DroopGovernor::new(0.05, 0.8);
let p_sync = gov.mechanical_power(1.0);
let p_slow = gov.mechanical_power(0.99); assert!((p_sync - 0.8).abs() < 1e-10);
assert!(
p_slow > p_sync,
"Under-speed should increase mechanical power"
);
}
#[test]
fn test_droop_governor_limits() {
let gov = DroopGovernor::new(0.05, 0.5);
let p = gov.mechanical_power(0.50); assert!(p <= gov.p_max);
}
#[test]
fn test_tgov1_gas_turbine_faster() {
let steam = Tgov1Params::steam_typical();
let gas = Tgov1Params::gas_turbine();
assert!(gas.t1 < steam.t1);
assert!(gas.t3 < steam.t3);
}
}