twine_components/thermal/hx/
stream.rs

1use twine_thermo::{HeatFlow, units::TemperatureDifference};
2use uom::si::f64::ThermodynamicTemperature;
3
4use crate::thermal::hx::capacitance_rate::CapacitanceRate;
5
6/// Inlet state for a stream entering the heat exchanger.
7///
8/// Assumes the fluid's specific heat remains constant through the exchanger.
9#[derive(Debug, Clone, Copy)]
10pub struct StreamInlet {
11    pub(crate) capacitance_rate: CapacitanceRate,
12    pub(crate) temperature: ThermodynamicTemperature,
13}
14
15impl StreamInlet {
16    /// Capture the inlet capacitance rate and temperature.
17    #[must_use]
18    pub fn new(capacitance_rate: CapacitanceRate, temperature: ThermodynamicTemperature) -> Self {
19        Self {
20            capacitance_rate,
21            temperature,
22        }
23    }
24
25    pub(crate) fn with_heat_flow(self, heat_flow: HeatFlow) -> Stream {
26        Stream {
27            capacitance_rate: self.capacitance_rate,
28            inlet_temperature: self.temperature,
29            heat_flow,
30            outlet_temperature: {
31                if self.capacitance_rate.is_infinite() {
32                    self.temperature
33                } else {
34                    match heat_flow {
35                        HeatFlow::In(heat_flow) => {
36                            self.temperature + (heat_flow.into_inner() / *self.capacitance_rate)
37                        }
38                        HeatFlow::Out(heat_flow) => {
39                            self.temperature - (heat_flow.into_inner() / *self.capacitance_rate)
40                        }
41                        HeatFlow::None => self.temperature,
42                    }
43                }
44            },
45        }
46    }
47}
48
49/// A fully-resolved heat exchanger stream.
50#[derive(Debug, Clone, Copy, PartialEq)]
51pub struct Stream {
52    /// Effective capacitance rate for the stream.
53    pub capacitance_rate: CapacitanceRate,
54    /// Temperature at the exchanger inlet.
55    pub inlet_temperature: ThermodynamicTemperature,
56    /// Temperature after the stream leaves the exchanger.
57    ///
58    /// When the capacitance rate tends to infinity this matches the inlet
59    /// temperature.
60    pub outlet_temperature: ThermodynamicTemperature,
61    /// Net heat flow direction and magnitude for the stream.
62    pub heat_flow: HeatFlow,
63}
64
65impl Stream {
66    /// Construct a fully-resolved stream from a known heat flow.
67    ///
68    /// Given the stream's inlet temperature, capacitance rate, and heat flow,
69    /// this calculates the outlet temperature using the energy balance:
70    /// `Q = C * (T_out - T_in)`.
71    ///
72    /// This constructor is useful when you know the heat transfer rate for a stream
73    /// (for example, from measurements or system specifications) and need to determine
74    /// the resulting outlet temperature.
75    ///
76    /// # Example
77    ///
78    /// ```rust
79    /// # use twine_core::constraint::ConstraintResult;
80    /// use uom::si::{
81    ///     f64::{Power, ThermodynamicTemperature},
82    ///     power::kilowatt,
83    ///     thermal_conductance::kilowatt_per_kelvin,
84    ///     thermodynamic_temperature::degree_celsius,
85    /// };
86    /// use twine_components::thermal::hx::{CapacitanceRate, Stream};
87    /// use twine_thermo::HeatFlow;
88    ///
89    /// # fn main() -> ConstraintResult<()> {
90    /// let stream = Stream::new_from_heat_flow(
91    ///     CapacitanceRate::new::<kilowatt_per_kelvin>(3.)?,
92    ///     ThermodynamicTemperature::new::<degree_celsius>(80.),
93    ///     HeatFlow::outgoing(Power::new::<kilowatt>(60.))?,
94    /// );
95    ///
96    /// // Outlet temperature is calculated automatically
97    /// assert_eq!(
98    ///     stream.outlet_temperature.get::<degree_celsius>(),
99    ///     60.0
100    /// );
101    /// # Ok(())
102    /// # }
103    /// ```
104    #[must_use]
105    pub fn new_from_heat_flow(
106        capacitance_rate: CapacitanceRate,
107        inlet_temperature: ThermodynamicTemperature,
108        heat_flow: HeatFlow,
109    ) -> Self {
110        Self {
111            capacitance_rate,
112            inlet_temperature,
113            outlet_temperature: match heat_flow {
114                HeatFlow::In(heat_rate) => {
115                    inlet_temperature + heat_rate.into_inner() / *capacitance_rate
116                }
117                HeatFlow::Out(heat_rate) => {
118                    inlet_temperature - heat_rate.into_inner() / *capacitance_rate
119                }
120                HeatFlow::None => inlet_temperature,
121            },
122            heat_flow,
123        }
124    }
125
126    /// Construct a fully-resolved stream from known inlet and outlet temperatures.
127    ///
128    /// Given the stream's inlet and outlet temperatures along with its capacitance rate,
129    /// this calculates the heat flow using the energy balance: `Q = C * (T_out - T_in)`.
130    ///
131    /// The heat flow direction is automatically determined from the temperature change:
132    /// - If outlet > inlet, the heat flow is [`HeatFlow::In`]
133    /// - If outlet < inlet, the heat flow is [`HeatFlow::Out`]
134    /// - If outlet = inlet, the heat flow is [`HeatFlow::None`]
135    ///
136    /// This constructor is useful when you know both inlet and outlet temperatures for
137    /// a stream (for example, from measurements) and need to determine the heat transfer
138    /// rate.
139    ///
140    /// # Example
141    ///
142    /// ```rust
143    /// # use twine_core::constraint::ConstraintResult;
144    /// use uom::si::{
145    ///     f64::ThermodynamicTemperature,
146    ///     power::kilowatt,
147    ///     thermal_conductance::kilowatt_per_kelvin,
148    ///     thermodynamic_temperature::degree_celsius,
149    /// };
150    /// use twine_components::thermal::hx::{CapacitanceRate, Stream};
151    /// use twine_thermo::HeatFlow;
152    ///
153    /// # fn main() -> ConstraintResult<()> {
154    /// let stream = Stream::new_from_outlet_temperature(
155    ///     CapacitanceRate::new::<kilowatt_per_kelvin>(3.)?,
156    ///     ThermodynamicTemperature::new::<degree_celsius>(80.),
157    ///     ThermodynamicTemperature::new::<degree_celsius>(60.),
158    /// );
159    ///
160    /// // Heat flow magnitude is calculated automatically (80°C → 60°C means heat is leaving)
161    /// assert!(matches!(stream.heat_flow, HeatFlow::Out(_)));
162    /// assert_eq!(
163    ///     stream.heat_flow.signed().abs().get::<kilowatt>(),
164    ///     60.0
165    /// );
166    /// # Ok(())
167    /// # }
168    /// ```
169    ///
170    /// # Panics
171    ///
172    /// Panics if the temperatures cannot be compared (e.g., contain NaN values) or if
173    /// the calculated heat rate magnitude is invalid (which should not occur in normal use).
174    #[must_use]
175    pub fn new_from_outlet_temperature(
176        capacitance_rate: CapacitanceRate,
177        inlet_temperature: ThermodynamicTemperature,
178        outlet_temperature: ThermodynamicTemperature,
179    ) -> Self {
180        let heat_rate_magnitude =
181            *capacitance_rate * inlet_temperature.minus(outlet_temperature).abs();
182
183        Self {
184            capacitance_rate,
185            inlet_temperature,
186            outlet_temperature,
187            heat_flow: match inlet_temperature
188                .partial_cmp(&outlet_temperature)
189                .expect("temperatures to be comparable")
190            {
191                std::cmp::Ordering::Less => HeatFlow::incoming(heat_rate_magnitude)
192                    .expect("heat rate magnitude should always be positive"),
193                std::cmp::Ordering::Equal => HeatFlow::None,
194                std::cmp::Ordering::Greater => HeatFlow::outgoing(heat_rate_magnitude)
195                    .expect("heat rate magnitude should always be positive"),
196            },
197        }
198    }
199}
200
201impl From<Stream> for StreamInlet {
202    fn from(stream: Stream) -> Self {
203        Self {
204            capacitance_rate: stream.capacitance_rate,
205            temperature: stream.inlet_temperature,
206        }
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use twine_core::constraint::ConstraintResult;
213    use uom::si::{
214        f64::Power, power::watt, thermal_conductance::watt_per_kelvin,
215        thermodynamic_temperature::kelvin,
216    };
217
218    use super::*;
219
220    #[test]
221    fn stream_inlet_with_heat_flow() -> ConstraintResult<()> {
222        let capacitance_rate = CapacitanceRate::new::<watt_per_kelvin>(10.)?;
223        let inlet_temperature = ThermodynamicTemperature::new::<kelvin>(300.);
224        let heat_rate = Power::new::<watt>(20.);
225
226        let inlet = StreamInlet::new(capacitance_rate, inlet_temperature);
227
228        let no_heat = inlet.with_heat_flow(HeatFlow::None);
229        let incoming = inlet.with_heat_flow(HeatFlow::incoming(heat_rate)?);
230        let outgoing = inlet.with_heat_flow(HeatFlow::outgoing(heat_rate)?);
231
232        assert_eq!(
233            no_heat,
234            Stream {
235                capacitance_rate,
236                inlet_temperature,
237                outlet_temperature: inlet_temperature,
238                heat_flow: HeatFlow::None
239            }
240        );
241        assert_eq!(
242            incoming,
243            Stream {
244                capacitance_rate,
245                inlet_temperature,
246                outlet_temperature: ThermodynamicTemperature::new::<kelvin>(302.),
247                heat_flow: HeatFlow::incoming(heat_rate)?
248            }
249        );
250        assert_eq!(
251            outgoing,
252            Stream {
253                capacitance_rate,
254                inlet_temperature,
255                outlet_temperature: ThermodynamicTemperature::new::<kelvin>(298.),
256                heat_flow: HeatFlow::outgoing(heat_rate)?
257            }
258        );
259
260        Ok(())
261    }
262
263    #[test]
264    fn stream_new_from_heat_rate() -> ConstraintResult<()> {
265        let capacitance_rate = CapacitanceRate::new::<watt_per_kelvin>(10.)?;
266        let inlet_temperature = ThermodynamicTemperature::new::<kelvin>(300.);
267        let heat_flow = HeatFlow::incoming(Power::new::<watt>(20.))?;
268
269        let stream = Stream::new_from_heat_flow(capacitance_rate, inlet_temperature, heat_flow);
270
271        assert_eq!(
272            stream,
273            Stream {
274                capacitance_rate,
275                inlet_temperature,
276                outlet_temperature: ThermodynamicTemperature::new::<kelvin>(302.),
277                heat_flow
278            }
279        );
280
281        Ok(())
282    }
283
284    #[test]
285    fn stream_new_from_outlet_temperature() -> ConstraintResult<()> {
286        let capacitance_rate = CapacitanceRate::new::<watt_per_kelvin>(10.)?;
287        let inlet_temperature = ThermodynamicTemperature::new::<kelvin>(300.);
288        let outlet_temperature = ThermodynamicTemperature::new::<kelvin>(302.);
289
290        let stream = Stream::new_from_outlet_temperature(
291            capacitance_rate,
292            inlet_temperature,
293            outlet_temperature,
294        );
295
296        assert_eq!(
297            stream,
298            Stream {
299                capacitance_rate,
300                inlet_temperature,
301                outlet_temperature,
302                heat_flow: HeatFlow::incoming(Power::new::<watt>(20.))?
303            }
304        );
305
306        Ok(())
307    }
308}