twine_models/support/hx/arrangement/
shell_and_tube.rs1use 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#[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 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 (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#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
105pub enum ShellAndTubeConfigError {
106 #[error("shell pass count must be at least 1")]
108 ZeroShellPasses,
109 #[error("shell pass count is too large")]
111 ShellPassOverflow,
112 #[error("tube passes must be at least twice the shell passes")]
114 InsufficientTubePasses,
115 #[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}