Skip to main content

twine_models/support/hx/arrangement/
shell_and_tube.rs

1//! Shell-and-tube effectiveness-NTU relationships.
2
3use std::marker::PhantomData;
4
5use thiserror::Error;
6
7use crate::support::hx::{
8    CapacitanceRate, Effectiveness, Ntu,
9    effectiveness_ntu::{EffectivenessRelation, NtuRelation, effectiveness_via, ntu_via},
10};
11
12/// Shell-and-tube heat exchanger arrangement.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct ShellAndTube<const S: u16, const T: u16> {
15    _marker: PhantomData<()>,
16}
17
18impl<const S: u16, const T: u16> ShellAndTube<S, T> {
19    const fn validate() -> Result<(), ShellAndTubeConfigError> {
20        if S == 0 {
21            return Err(ShellAndTubeConfigError::ZeroShellPasses);
22        }
23        if S > u16::MAX / 2 {
24            return Err(ShellAndTubeConfigError::ShellPassOverflow);
25        }
26        if T < 2 * S {
27            return Err(ShellAndTubeConfigError::InsufficientTubePasses);
28        }
29        if !T.is_multiple_of(2 * S) {
30            return Err(ShellAndTubeConfigError::TubePassesNotMultiple);
31        }
32        Ok(())
33    }
34
35    /// Construct a validated shell-and-tube arrangement configuration.
36    ///
37    /// # Errors
38    ///
39    /// Returns [`ShellAndTubeConfigError`] when the pass counts violate the supported
40    /// shell-and-tube families (one shell pass with any even number of tube passes, or
41    /// `N` shell passes with a tube pass count that is an even multiple of `N`).
42    pub const fn new() -> Result<Self, ShellAndTubeConfigError> {
43        if let Err(err) = Self::validate() {
44            return Err(err);
45        }
46
47        Ok(Self {
48            _marker: PhantomData,
49        })
50    }
51}
52
53impl<const S: u16, const T: u16> EffectivenessRelation for ShellAndTube<S, T> {
54    fn effectiveness(&self, ntu: Ntu, capacitance_rates: [CapacitanceRate; 2]) -> Effectiveness {
55        let eff_1: fn(f64, f64) -> f64 = |ntu_1, cr| {
56            2. / (1.
57                + cr
58                + (1. + cr.powi(2)).sqrt() * (1. + (-ntu_1 * (1. + cr.powi(2)).sqrt()).exp())
59                    / (1. - (-ntu_1 * (1. + cr.powi(2)).sqrt()).exp()))
60        };
61
62        if S == 1 {
63            effectiveness_via(ntu, capacitance_rates, eff_1)
64        } else {
65            effectiveness_via(ntu, capacitance_rates, |ntu_1, cr| {
66                let eff_1 = eff_1(ntu_1, cr);
67
68                if cr < 1. {
69                    (((1. - eff_1 * cr) / (1. - eff_1)).powi(S.into()) - 1.)
70                        / (((1. - eff_1 * cr) / (1. - eff_1)).powi(S.into()) - cr)
71                } else {
72                    // cr == 1
73                    (f64::from(S) * eff_1) / (1. + eff_1 * (f64::from(S) - 1.))
74                }
75            })
76        }
77    }
78}
79
80impl<const S: u16, const T: u16> NtuRelation for ShellAndTube<S, T> {
81    fn ntu(&self, effectiveness: Effectiveness, capacitance_rates: [CapacitanceRate; 2]) -> Ntu {
82        let ntu_1: fn(f64, f64) -> f64 = |eff_1, cr| {
83            let e = (2. - eff_1 * (1. + cr)) / (eff_1 * (1. + cr.powi(2)).sqrt());
84            ((e + 1.) / (e - 1.)).ln() / (1. + cr.powi(2)).sqrt()
85        };
86
87        if S == 1 {
88            ntu_via(effectiveness, capacitance_rates, ntu_1)
89        } else {
90            ntu_via(effectiveness, capacitance_rates, |eff, cr| {
91                let eff_1 = if cr < 1. {
92                    let f = ((eff * cr - 1.) / (eff - 1.)).powf(1.0 / f64::from(S));
93                    (f - 1.) / (f - cr)
94                } else {
95                    eff / (f64::from(S) - eff * (f64::from(S) - 1.))
96                };
97                ntu_1(eff_1, cr)
98            })
99        }
100    }
101}
102
103/// Errors returned when constructing a [`ShellAndTube`] arrangement.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
105pub enum ShellAndTubeConfigError {
106    /// No shell passes were configured.
107    #[error("shell pass count must be at least 1")]
108    ZeroShellPasses,
109    /// The requested shell pass count is too large to validate.
110    #[error("shell pass count is too large")]
111    ShellPassOverflow,
112    /// Tube passes are fewer than twice the shell passes.
113    #[error("tube passes must be at least twice the shell passes")]
114    InsufficientTubePasses,
115    /// Tube passes are not an even multiple of shell passes.
116    #[error("tube passes must be an even multiple of shell passes")]
117    TubePassesNotMultiple,
118}
119
120#[cfg(test)]
121mod tests {
122    use crate::support::constraint::ConstraintResult;
123    use approx::assert_relative_eq;
124    use uom::si::{ratio::ratio, thermal_conductance::watt_per_kelvin};
125
126    use super::*;
127
128    fn roundtrip_for<const N: u16, const T: u16>() -> ConstraintResult<()> {
129        let arrangement =
130            ShellAndTube::<N, T>::new().expect("shell-and-tube configuration should be valid");
131
132        let ntus = [0.1, 0.5, 1., 5.];
133        let capacitance_rates = [[1., 1.], [1., 2.], [2., 1.], [1., 4.]];
134
135        for ntu in ntus {
136            for pair in capacitance_rates {
137                let rates = [
138                    CapacitanceRate::new::<watt_per_kelvin>(pair[0])?,
139                    CapacitanceRate::new::<watt_per_kelvin>(pair[1])?,
140                ];
141
142                let eff = arrangement.effectiveness(Ntu::new(ntu)?, rates);
143                let back = arrangement.ntu(eff, rates);
144
145                assert_relative_eq!(back.get::<ratio>(), ntu, max_relative = 1e-12);
146            }
147        }
148
149        Ok(())
150    }
151
152    #[test]
153    fn validation_outcomes() {
154        const MAX: u16 = u16::MAX;
155
156        assert_eq!(
157            ShellAndTube::<0, 2>::new(),
158            Err(ShellAndTubeConfigError::ZeroShellPasses)
159        );
160
161        assert_eq!(
162            ShellAndTube::<MAX, 2>::new(),
163            Err(ShellAndTubeConfigError::ShellPassOverflow)
164        );
165
166        assert_eq!(
167            ShellAndTube::<3, 4>::new(),
168            Err(ShellAndTubeConfigError::InsufficientTubePasses)
169        );
170
171        assert_eq!(
172            ShellAndTube::<3, 8>::new(),
173            Err(ShellAndTubeConfigError::TubePassesNotMultiple)
174        );
175
176        assert!(ShellAndTube::<1, 2>::new().is_ok());
177    }
178
179    #[test]
180    fn roundtrip() -> ConstraintResult<()> {
181        roundtrip_for::<1, 2>()?;
182        roundtrip_for::<1, 4>()?;
183        roundtrip_for::<2, 4>()?;
184        roundtrip_for::<3, 12>()?;
185
186        Ok(())
187    }
188}