Skip to main content

twine_models/support/thermo/
state.rs

1use std::ops::Div;
2
3use twine_core::StepIntegrable;
4use uom::si::f64::{MassDensity, TemperatureInterval, ThermodynamicTemperature, Time};
5
6/// The thermodynamic state of a fluid.
7///
8/// A `State<Fluid>` captures the thermodynamic state of a specific fluid,
9/// including its temperature, density, and any fluid-specific data.
10///
11/// The `Fluid` type parameter can be a simple marker type,
12/// such as [`Air`](crate::fluid::Air) or [`Water`](crate::fluid::Water),
13/// or a structured type containing additional data, such as mixture composition
14/// or particle concentration.
15///
16/// `State` is the primary input to capability-based thermodynamic models for
17/// calculating pressure, enthalpy, entropy, and related quantities.
18///
19/// # Example
20///
21/// ```
22/// use twine_models::support::thermo::{State, fluid::Air};
23/// use uom::si::{
24///     f64::{ThermodynamicTemperature, MassDensity},
25///     thermodynamic_temperature::kelvin,
26///     mass_density::kilogram_per_cubic_meter,
27/// };
28///
29/// let state = State {
30///     temperature: ThermodynamicTemperature::new::<kelvin>(300.0),
31///     density: MassDensity::new::<kilogram_per_cubic_meter>(1.0),
32///     fluid: Air,
33/// };
34/// ```
35#[derive(Debug, Clone, Copy, PartialEq)]
36pub struct State<Fluid> {
37    pub temperature: ThermodynamicTemperature,
38    pub density: MassDensity,
39    pub fluid: Fluid,
40}
41
42impl<Fluid> State<Fluid> {
43    /// Creates a new state with the given temperature, density, and fluid.
44    #[must_use]
45    pub fn new(temperature: ThermodynamicTemperature, density: MassDensity, fluid: Fluid) -> Self {
46        Self {
47            temperature,
48            density,
49            fluid,
50        }
51    }
52
53    /// Returns a new state with the given temperature, keeping other fields unchanged.
54    #[must_use]
55    pub fn with_temperature(self, temperature: ThermodynamicTemperature) -> Self {
56        Self {
57            temperature,
58            ..self
59        }
60    }
61
62    /// Returns a new state with the given density, keeping other fields unchanged.
63    #[must_use]
64    pub fn with_density(self, density: MassDensity) -> Self {
65        Self { density, ..self }
66    }
67
68    /// Returns a new state with the given fluid, keeping other fields unchanged.
69    #[must_use]
70    pub fn with_fluid(self, fluid: Fluid) -> Self {
71        Self { fluid, ..self }
72    }
73}
74
75/// The time derivative of a thermodynamic [`State`].
76///
77/// Parameterized over the fluid derivative type directly, keeping the struct
78/// independent of the [`StepIntegrable`] trait.
79/// The connection to `StepIntegrable` happens at the impl site on `State<Fluid>`,
80/// where `StateDerivative<Fluid::Derivative>` becomes the associated type.
81///
82/// # Example
83///
84/// ```
85/// use twine_models::support::thermo::{State, StateDerivative, fluid::Air};
86/// use uom::si::{
87///     f64::{MassDensity, TemperatureInterval, ThermodynamicTemperature, Time},
88///     mass_density::kilogram_per_cubic_meter,
89///     thermodynamic_temperature::kelvin,
90///     temperature_interval::kelvin as interval_kelvin,
91///     time::second,
92/// };
93///
94/// let state = State {
95///     temperature: ThermodynamicTemperature::new::<kelvin>(300.0),
96///     density: MassDensity::new::<kilogram_per_cubic_meter>(1.2),
97///     fluid: Air,
98/// };
99///
100/// let dt = Time::new::<second>(1.0);
101/// let deriv: StateDerivative<()> = StateDerivative {
102///     temperature: TemperatureInterval::new::<interval_kelvin>(1.0) / dt,
103///     density: MassDensity::new::<kilogram_per_cubic_meter>(0.1) / dt,
104///     fluid: (),
105/// };
106/// ```
107#[derive(Debug, Clone, Copy, PartialEq)]
108pub struct StateDerivative<FluidDerivative> {
109    /// Rate of change of temperature (K/s).
110    pub temperature: <TemperatureInterval as Div<Time>>::Output,
111
112    /// Rate of change of density (kg/m³/s).
113    pub density: <MassDensity as Div<Time>>::Output,
114
115    /// Fluid-specific derivative.
116    pub fluid: FluidDerivative,
117}
118
119impl<Fluid> StepIntegrable<Time> for State<Fluid>
120where
121    Fluid: StepIntegrable<Time>,
122{
123    type Derivative = StateDerivative<Fluid::Derivative>;
124
125    fn step(&self, derivative: Self::Derivative, delta: Time) -> Self {
126        Self {
127            temperature: self.temperature + derivative.temperature * delta,
128            density: self.density + derivative.density * delta,
129            fluid: self.fluid.step(derivative.fluid, delta),
130        }
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    use twine_core::StepIntegrable;
139    use uom::si::{
140        f64::{MassDensity, TemperatureInterval, ThermodynamicTemperature, Time},
141        mass_density::kilogram_per_cubic_meter,
142        temperature_interval::kelvin as interval_kelvin,
143        thermodynamic_temperature::kelvin,
144        time::second,
145    };
146
147    use crate::support::thermo::fluid::Air;
148
149    #[test]
150    fn step_advances_temperature_and_density() {
151        let state = State {
152            temperature: ThermodynamicTemperature::new::<kelvin>(300.0),
153            density: MassDensity::new::<kilogram_per_cubic_meter>(1.2),
154            fluid: Air,
155        };
156
157        let dt = Time::new::<second>(2.0);
158        let deriv = StateDerivative {
159            temperature: TemperatureInterval::new::<interval_kelvin>(3.0) / dt,
160            density: MassDensity::new::<kilogram_per_cubic_meter>(0.4) / dt,
161            fluid: (),
162        };
163
164        let next = state.step(deriv, dt);
165
166        approx::assert_relative_eq!(
167            next.temperature.get::<kelvin>(),
168            303.0,
169            max_relative = 1e-10,
170        );
171        approx::assert_relative_eq!(
172            next.density.get::<kilogram_per_cubic_meter>(),
173            1.6,
174            max_relative = 1e-10,
175        );
176    }
177
178    #[test]
179    fn step_zero_derivative_is_identity() {
180        let state = State {
181            temperature: ThermodynamicTemperature::new::<kelvin>(290.0),
182            density: MassDensity::new::<kilogram_per_cubic_meter>(1.0),
183            fluid: Air,
184        };
185
186        let dt = Time::new::<second>(1.0);
187        let deriv = StateDerivative {
188            temperature: TemperatureInterval::new::<interval_kelvin>(0.0) / dt,
189            density: MassDensity::new::<kilogram_per_cubic_meter>(0.0) / dt,
190            fluid: (),
191        };
192
193        let next = state.step(deriv, dt);
194
195        assert_eq!(next, state);
196    }
197
198    #[test]
199    fn zst_fluid_markers_step_to_themselves() {
200        use crate::support::thermo::fluid::{CarbonDioxide, Water};
201
202        let dt = Time::new::<second>(1.0);
203
204        assert_eq!(Air.step((), dt), Air);
205        assert_eq!(Water.step((), dt), Water);
206        assert_eq!(CarbonDioxide.step((), dt), CarbonDioxide);
207    }
208}