use crate::core::traits::Plant;
pub struct ThermalPlant {
temperature: f64,
tau: f64,
gain: f64,
ambient: f64,
}
impl ThermalPlant {
pub fn new(initial_temp: f64, tau: f64, gain: f64, ambient: f64) -> Self {
Self {
temperature: initial_temp,
tau,
gain,
ambient,
}
}
pub fn temperature(&self) -> f64 {
self.temperature
}
pub fn steady_state(&self, u: f64) -> f64 {
self.ambient + self.gain * u
}
pub fn add_disturbance(&mut self, delta_t: f64) {
self.temperature += delta_t;
}
}
impl Plant<f64> for ThermalPlant {
fn step(&mut self, u: f64, dt: f64) {
let dt_dt = (self.gain * u - (self.temperature - self.ambient)) / self.tau;
self.temperature += dt_dt * dt;
}
fn output(&self) -> f64 {
self.temperature
}
fn state(&self) -> &[f64] {
core::slice::from_ref(&self.temperature)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn initial_conditions() {
let plant = ThermalPlant::new(25.0, 10.0, 100.0, 25.0);
assert_eq!(plant.output(), 25.0);
}
#[test]
fn step_response_direction() {
let mut plant = ThermalPlant::new(25.0, 10.0, 100.0, 25.0);
plant.step(1.0, 0.01);
assert!(plant.output() > 25.0);
}
#[test]
fn steady_state_analytical() {
let plant = ThermalPlant::new(25.0, 10.0, 100.0, 25.0);
assert_eq!(plant.steady_state(1.0), 125.0);
}
#[test]
fn converges_to_steady_state() {
let mut plant = ThermalPlant::new(25.0, 10.0, 100.0, 25.0);
let u = 0.5;
let expected = plant.steady_state(u);
let dt = 0.01;
for _ in 0..5000 {
plant.step(u, dt);
}
assert!(
(plant.output() - expected).abs() < 1.0,
"Should converge to {}, got {}",
expected,
plant.output()
);
}
#[test]
fn time_constant_accuracy() {
let mut plant = ThermalPlant::new(0.0, 1.0, 100.0, 0.0);
let dt = 0.001;
let steps = 1000;
for _ in 0..steps {
plant.step(1.0, dt);
}
let expected_at_tau = 100.0 * (1.0 - (-1.0_f64).exp()); assert!(
(plant.output() - expected_at_tau).abs() < 1.0,
"At t=tau, expected ~{:.1}, got {:.1}",
expected_at_tau,
plant.output()
);
}
#[test]
fn disturbance_injection() {
let mut plant = ThermalPlant::new(25.0, 10.0, 100.0, 25.0);
plant.add_disturbance(-5.0);
assert_eq!(plant.output(), 20.0);
}
#[test]
fn cooling_when_above_equilibrium() {
let mut plant = ThermalPlant::new(100.0, 10.0, 50.0, 25.0);
plant.step(0.0, 0.01);
assert!(plant.output() < 100.0);
}
}