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}