mod core;
use std::convert::Infallible;
use twine_core::{DerivativeOf, Model, OdeProblem, StepIntegrable};
use uom::si::{
f64::{ThermodynamicTemperature, Time},
temperature_interval::kelvin as delta_kelvin,
thermodynamic_temperature::kelvin,
};
pub use core::{
AuxHeatFlow, Environment, Fluid, Geometry, Insulation, Location, PortFlow, PortLocation,
StratifiedTank, StratifiedTankError, StratifiedTankInput, StratifiedTankOutput,
TemperatureRate, ValidatedPower,
};
impl<const N: usize, const P: usize, const Q: usize> Model for StratifiedTank<N, P, Q> {
type Input = StratifiedTankInput<N, P, Q>;
type Output = StratifiedTankOutput<N>;
type Error = Infallible;
fn call(&self, input: &Self::Input) -> Result<Self::Output, Self::Error> {
Ok(self.evaluate(input))
}
}
#[derive(Debug, Clone, Copy)]
pub struct TankState<const N: usize> {
pub temperatures: [ThermodynamicTemperature; N],
}
#[derive(Debug, Clone, Copy)]
pub struct TankDerivative<const N: usize> {
pub rates: [TemperatureRate; N],
}
impl<const N: usize> StepIntegrable<Time> for TankState<N> {
type Derivative = TankDerivative<N>;
fn step(&self, derivative: TankDerivative<N>, delta: Time) -> Self {
TankState {
temperatures: std::array::from_fn(|i| {
let delta_t = (derivative.rates[i] * delta).get::<delta_kelvin>();
let t_k = self.temperatures[i].get::<kelvin>();
ThermodynamicTemperature::new::<kelvin>(t_k + delta_t)
}),
}
}
}
pub struct TankOdeProblem<const N: usize, const P: usize, const Q: usize>;
impl<const N: usize, const P: usize, const Q: usize> OdeProblem for TankOdeProblem<N, P, Q> {
type Input = StratifiedTankInput<N, P, Q>;
type Output = StratifiedTankOutput<N>;
type Delta = Time;
type State = TankState<N>;
type Error = Infallible;
fn state(&self, input: &Self::Input) -> Result<TankState<N>, Infallible> {
Ok(TankState {
temperatures: input.temperatures,
})
}
fn derivative(
&self,
_input: &Self::Input,
output: &Self::Output,
) -> Result<DerivativeOf<TankState<N>, Time>, Infallible> {
Ok(TankDerivative {
rates: output.derivatives,
})
}
fn build_input(
&self,
base: &Self::Input,
state: &Self::State,
_delta: &Time,
) -> Result<Self::Input, Infallible> {
Ok(StratifiedTankInput {
temperatures: state.temperatures,
port_flows: base.port_flows,
aux_heat_flows: base.aux_heat_flows,
environment: base.environment,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use twine_core::Model;
use twine_solvers::transient::euler;
use uom::si::{
f64::{Length, MassDensity, Power, SpecificHeatCapacity, ThermalConductivity, VolumeRate},
length::meter,
mass_density::kilogram_per_cubic_meter,
power::kilowatt,
specific_heat_capacity::kilojoule_per_kilogram_kelvin,
thermal_conductivity::watt_per_meter_kelvin,
thermodynamic_temperature::degree_celsius,
time::second,
volume_rate::gallon_per_minute,
};
use std::f64::consts::PI;
use uom::ConstZero;
fn k_per_s(rate: TemperatureRate) -> f64 {
use uom::si::temperature_interval::kelvin as delta_kelvin;
(rate * Time::new::<second>(1.0)).get::<delta_kelvin>()
}
fn test_tank() -> StratifiedTank<3, 1, 1> {
let fluid = Fluid {
density: MassDensity::new::<kilogram_per_cubic_meter>(1000.0),
specific_heat: SpecificHeatCapacity::new::<kilojoule_per_kilogram_kelvin>(4.0),
thermal_conductivity: ThermalConductivity::ZERO,
};
let geometry = Geometry::VerticalCylinder {
diameter: Length::new::<meter>((4.0 / PI).sqrt()),
height: Length::new::<meter>(3.0),
};
StratifiedTank::new(
fluid,
geometry,
Insulation::Adiabatic,
[Location::tank_top()],
[PortLocation {
inlet: Location::tank_bottom(),
outlet: Location::tank_top(),
}],
)
.unwrap()
}
fn port_flow(gpm: f64, celsius: f64) -> PortFlow {
PortFlow::new(
VolumeRate::new::<gallon_per_minute>(gpm),
ThermodynamicTemperature::new::<degree_celsius>(celsius),
)
.unwrap()
}
fn ambient(celsius: f64) -> Environment {
let t = ThermodynamicTemperature::new::<degree_celsius>(celsius);
Environment {
bottom: t,
side: t,
top: t,
}
}
#[test]
fn model_call_delegates_to_evaluate() {
let tank = test_tank();
let t = ThermodynamicTemperature::new::<degree_celsius>(30.0);
let input = StratifiedTankInput {
temperatures: [t; 3],
port_flows: [port_flow(0.0, 30.0)],
aux_heat_flows: [AuxHeatFlow::None],
environment: ambient(30.0),
};
let via_model = Model::call(&tank, &input).unwrap();
let via_evaluate = tank.evaluate(&input);
assert_eq!(via_model.temperatures, via_evaluate.temperatures);
for i in 0..3 {
assert_relative_eq!(
k_per_s(via_model.derivatives[i]),
k_per_s(via_evaluate.derivatives[i])
);
}
}
#[test]
fn aux_heating_raises_temperature_over_time() {
let tank = test_tank();
let t0 = ThermodynamicTemperature::new::<degree_celsius>(20.0);
let initial = StratifiedTankInput {
temperatures: [t0; 3],
port_flows: [port_flow(0.0, 20.0)],
aux_heat_flows: [AuxHeatFlow::heating(Power::new::<kilowatt>(20.0)).unwrap()],
environment: ambient(20.0),
};
let dt = Time::new::<second>(1.0);
let solution =
euler::solve_unobserved(&tank, &TankOdeProblem::<3, 1, 1>, initial, dt, 100).unwrap();
let final_input = &solution.history.last().unwrap().input;
let t_top = final_input.temperatures[2].get::<degree_celsius>();
let t_bot = final_input.temperatures[0].get::<degree_celsius>();
assert_relative_eq!(t_top, 20.5, epsilon = 1e-10);
assert_relative_eq!(t_bot, 20.0, epsilon = 1e-10);
}
#[test]
fn at_equilibrium_all_derivatives_zero() {
let tank = test_tank();
let t = ThermodynamicTemperature::new::<degree_celsius>(20.0);
let input = StratifiedTankInput {
temperatures: [t; 3],
port_flows: [port_flow(0.0, 20.0)],
aux_heat_flows: [AuxHeatFlow::None],
environment: ambient(20.0),
};
let out = tank.evaluate(&input);
assert!(out.temperatures.iter().all(|&nt| nt == t));
for deriv in out.derivatives {
assert_relative_eq!(k_per_s(deriv), 0.0, epsilon = 1e-15);
}
}
#[test]
fn aux_cooling_lowers_temperature_over_time() {
let tank = test_tank();
let t0 = ThermodynamicTemperature::new::<degree_celsius>(50.0);
let initial = StratifiedTankInput {
temperatures: [t0; 3],
port_flows: [port_flow(0.0, 50.0)],
aux_heat_flows: [AuxHeatFlow::cooling(Power::new::<kilowatt>(20.0)).unwrap()],
environment: ambient(50.0),
};
let dt = Time::new::<second>(1.0);
let solution =
euler::solve_unobserved(&tank, &TankOdeProblem::<3, 1, 1>, initial, dt, 100).unwrap();
let final_input = &solution.history.last().unwrap().input;
let t_top = final_input.temperatures[2].get::<degree_celsius>();
let t_bot = final_input.temperatures[0].get::<degree_celsius>();
assert_relative_eq!(t_top, 49.5, epsilon = 1e-10);
assert_relative_eq!(t_bot, 50.0, epsilon = 1e-10);
}
#[test]
fn instantaneous_aux_cooling_derivative_is_negative() {
let tank = test_tank();
let t = ThermodynamicTemperature::new::<degree_celsius>(50.0);
let input = StratifiedTankInput {
temperatures: [t; 3],
port_flows: [port_flow(0.0, 50.0)],
aux_heat_flows: [AuxHeatFlow::cooling(Power::new::<kilowatt>(20.0)).unwrap()],
environment: ambient(50.0),
};
let out = tank.evaluate(&input);
assert_relative_eq!(k_per_s(out.derivatives[0]), 0.0);
assert_relative_eq!(k_per_s(out.derivatives[1]), 0.0);
assert_relative_eq!(k_per_s(out.derivatives[2]), -0.005);
}
#[test]
fn tank_with_conduction_reaches_equilibrium() {
let fluid = Fluid {
density: MassDensity::new::<kilogram_per_cubic_meter>(1000.0),
specific_heat: SpecificHeatCapacity::new::<kilojoule_per_kilogram_kelvin>(4.0),
thermal_conductivity: ThermalConductivity::new::<watt_per_meter_kelvin>(10_000.0),
};
let geometry = Geometry::VerticalCylinder {
diameter: Length::new::<meter>((4.0 / PI).sqrt()),
height: Length::new::<meter>(3.0),
};
let tank: StratifiedTank<3, 0, 0> =
StratifiedTank::new(fluid, geometry, Insulation::Adiabatic, [], []).unwrap();
let t_hot = ThermodynamicTemperature::new::<degree_celsius>(80.0);
let t_cold = ThermodynamicTemperature::new::<degree_celsius>(20.0);
let t_avg = ThermodynamicTemperature::new::<degree_celsius>(50.0);
let initial = StratifiedTankInput {
temperatures: [t_cold, t_avg, t_hot],
port_flows: [],
aux_heat_flows: [],
environment: Environment {
bottom: t_avg,
side: t_avg,
top: t_avg,
},
};
let dt = Time::new::<second>(10.0);
let solution =
euler::solve_unobserved(&tank, &TankOdeProblem::<3, 0, 0>, initial, dt, 5000).unwrap();
let final_input = &solution.history.last().unwrap().input;
for temp in final_input.temperatures {
assert_relative_eq!(temp.get::<degree_celsius>(), 50.0, epsilon = 1.0);
}
}
}