"""Heat-exchanger performance correlations used by area targeting routines."""
import math
import numpy as np
from ..lib.enums import HeatExchangerTypes as HX
__all__ = [
"CalcAreaUE",
"Coth",
"CrossflowUnmixedEff1",
"CrossflowUnmixedEff2",
"HX_Eff",
"HX_NTU",
"HX_NTU_Numerical",
"MultiPassEff",
"MultiPassNTU",
"compute_LMTD_from_dts",
"compute_LMTD_from_ts",
"eNTU_slope_Numerical",
]
[docs]
def compute_LMTD_from_dts(
delta_T1: float | list | np.ndarray,
delta_T2: float | list | np.ndarray,
) -> np.ndarray:
"""Return the LMTD for a counterflow heat exchanger from end-point deltas."""
# Check temperature directions for counter-current assumption
delta_T1 = np.array(delta_T1)
delta_T2 = np.array(delta_T2)
if delta_T1.round(6).min() <= 0 or delta_T2.round(6).min() <= 0:
raise ValueError(
f"Invalid temperature differences: ΔT1={delta_T1}, ΔT2={delta_T2}"
)
mask_equal = np.isclose(delta_T1, delta_T2, atol=1e-6)
lmtd = np.empty_like(delta_T1, dtype=float)
arithmetic = (delta_T1 + delta_T2) / 2
np.copyto(lmtd, arithmetic, where=mask_equal)
np.divide(
delta_T1 - delta_T2,
np.log(delta_T1 / delta_T2),
out=lmtd,
where=~mask_equal,
)
return lmtd
[docs]
def compute_LMTD_from_ts(
T_hot_in: float | list | np.ndarray,
T_hot_out: float | list | np.ndarray,
T_cold_in: float | list | np.ndarray,
T_cold_out: float | list | np.ndarray,
) -> float:
"""Return the LMTD for a counterflow heat exchanger from temperatures."""
T_hot_in = np.array(T_hot_in)
T_hot_out = np.array(T_hot_out)
T_cold_in = np.array(T_cold_in)
T_cold_out = np.array(T_cold_out)
# Check temperature directions for counter-current assumption
if T_hot_in < T_hot_out:
raise ValueError("Hot fluid must cool down (T_hot_in > T_hot_out)")
if T_cold_out < T_cold_in:
raise ValueError("Cold fluid must heat up (T_cold_out > T_cold_in)")
return compute_LMTD_from_dts(
T_hot_in - T_cold_out, # Inlet diff (hottest hot - hottest cold)
T_hot_out - T_cold_in, # Outlet diff (coldest hot - coldest cold)
)
[docs]
def HX_Eff(Arrangement, Ntu, c, Passes=None, Rows=None, Cmin_Phase=None):
"""Return heat exchanger effectiveness for the specified arrangement/NTU/c ratio."""
if Passes is None:
Passes = 1
Ntu = Ntu / Passes
if Ntu > 0 and c >= 0:
# Counter Flow - Single Pass Effectiveness
if Arrangement == HX.CF.value:
# test = c * math.exp(-Ntu * (1 - c))
if c != 1 and c * math.exp(-Ntu * (1 - c)) != 1:
eff = (1 - math.exp(-Ntu * (1 - c))) / (
1 - c * math.exp(-Ntu * (1 - c))
)
else:
eff = Ntu / (1 + Ntu)
# Parallel Flow - Single Pass Effectiveness
elif Arrangement == HX.PF.value:
eff = (1 - math.exp(-Ntu * (1 + c))) / (1 + c)
# Cross Flow - Both Streams Unmixed Effectiveness
elif Arrangement == HX.CrFUU:
if Rows is None or Cmin_Phase is None:
eff = CrossflowUnmixedEff1(Ntu, c)
else:
eff = CrossflowUnmixedEff2(Ntu, c, Rows, Cmin_Phase)
# Cross Flow - Both Streams Mixed Effectiveness
elif Arrangement == HX.CrFMM:
eff = (
1 / (1 - math.exp(-Ntu)) + c / (1 - math.exp(-Ntu * c)) - 1 / Ntu
) ** -1
# Cross Flow - Stream Cmax Unmixed Effectiveness
elif Arrangement == HX.CrFMUmax:
eff = 1 - math.exp(-1 / c * (1 - math.exp(-Ntu * c)))
# Cross Flow - Stream Cmin Unmixed Effectiveness
elif Arrangement == HX.CrFMUmin:
eff = 1 / c * (1 - math.exp(-c * (1 - math.exp(-Ntu))))
# Shell and Tube - One Shell Pass; 2,4,6, etc., Tube Passes Effectiveness
elif Arrangement == HX.ShellTube.value:
d = (1 + c**2) ** 0.5
eff = 2 / ((1 + c) + d**0.5 * Coth(Ntu * d / 2))
# Condensing or Evaporating of One Fluid
elif Arrangement == HX.CondEvap:
eff = 1 - math.exp(-Ntu)
else:
eff = HX_Eff(HX.CF.value, Ntu, c, 1)
else:
eff = 0
if Passes > 1:
Eff_p = eff
return MultiPassEff(Eff_p, c, Passes)
else:
return eff
[docs]
def HX_NTU(Arrangement, eff, c, Passes=None):
"""Compute NTU for a target effectiveness and exchanger arrangement."""
if Passes is None:
Passes = 1
if Passes > 1:
Eff_p = MultiPassNTU(eff, c, Passes)
eff = Eff_p
if eff > 0 and eff < 1:
# Counter Flow - Single Pass Effectiveness
if Arrangement == HX.CF.value:
if c != 1:
Ntu = 1 / (1 - c) * math.log((1 - eff * c) / (1 - eff))
else:
Ntu = eff / (1 - eff)
# Parallel Flow - Single Pass Effectiveness
elif Arrangement == HX.PF.value:
Ntu = -math.log(1 - eff * (1 + c)) / (1 + c)
# Cross Flow - Both Streams Unmixed NTU
elif Arrangement == HX.CrFUU:
Ntu = HX_NTU_Numerical(Arrangement, eff, c)
# Cross Flow - Both Streams Mixed NTU
elif Arrangement == HX.CrFMM:
Ntu = HX_NTU_Numerical(Arrangement, eff, c)
# Cross Flow - Stream Cmax Unmixed NTU
elif Arrangement == HX.CrFMUmax:
Ntu = -1 / c * math.log(1 + c * math.log(1 - eff))
# Cross Flow - Stream Cmin Unmixed NTU
elif Arrangement == HX.CrFMUmin:
Ntu = -math.log(1 + 1 / c * math.log(1 - eff * c))
# Shell and Tube - One Shell Pass; 2,4,6, etc., Tube Passes NTU
elif Arrangement == HX.ShellTube.value:
D1 = 1 + c - (1 + c**2) ** (1 / 4)
D2 = 1 + c + (1 + c**2) ** (1 / 4)
Ntu = (1 + c**2) ** -0.5 * math.log((2 - eff * D1) / (2 - eff * D2))
# Condensing or Evaporating of One Fluid
elif Arrangement == HX.CondEvap:
Ntu = -math.log(1 - eff)
else:
Ntu = -1
else:
Ntu = 0
return Ntu * Passes
[docs]
def CalcAreaUE(Arrangement, U, C_p, T_p1, T_p2, T_u1, T_u2, Passes):
"""Estimate the exchanger ``area * U`` product from duty and temperatures."""
Q = C_p * abs(T_p1 - T_p2)
C_u = Q / abs(T_u1 - T_u2)
if C_p < C_u:
eff = Q / C_p / abs(T_p1 - T_u1)
c = C_p / C_u
Ntu = HX_NTU(Arrangement, eff, c, Passes)
return Ntu * C_p / U
else:
eff = Q / C_u / abs(T_p1 - T_u1)
c = C_u / C_p
Ntu = HX_NTU(Arrangement, eff, c, Passes)
return Ntu * C_u / U
[docs]
def eNTU_slope_Numerical(Arrangement, Ntu, c, Passes):
"""Compute a finite-difference effectiveness slope with respect to NTU."""
dx = 1e-6
if Ntu > 0:
return (
HX_Eff(Arrangement, Ntu + dx, c, Passes)
- HX_Eff(Arrangement, Ntu, c, Passes)
) / dx
[docs]
def Coth(R):
"""Convenience wrapper for the hyperbolic cotangent function."""
return (math.exp(2 * R) + 1) / (math.exp(2 * R) - 1)
[docs]
def MultiPassEff(eff, c, Passes):
"""Convert single-pass effectiveness into equivalent multi-pass effectiveness."""
if c != 1:
return (((1 - eff * c) / (1 - eff)) ** Passes - 1) / (
((1 - eff * c) / (1 - eff)) ** Passes - c
)
else:
return Passes * eff / (1 + eff * (Passes - 1))
[docs]
def MultiPassNTU(Eff_p, c, Passes):
"""Convert multi-pass effectiveness back to an equivalent single-pass value."""
if c != 1:
return (((1 - Eff_p * c) / (1 - Eff_p)) ** (1 / Passes) - 1) / (
((1 - Eff_p * c) / (1 - Eff_p)) ** (1 / Passes) - c
)
else:
return Eff_p / (Passes - Eff_p * (Passes - 1))
[docs]
def CrossflowUnmixedEff1(Ntu, c):
"""Series approximation for cross-flow effectiveness with unmixed streams."""
Sum_Pn = 0
for i in range(1, 21):
Pn = 0
for j in range(1, i):
Pn = Pn + c**i / math.factorial(i + 1) * (i - j + 1) / math.factorial(
j
) * Ntu ** (i + j)
Sum_Pn = Sum_Pn + Pn
return 1 - math.exp(-Ntu) - math.exp(-(1 + c) * Ntu) * Sum_Pn
[docs]
def CrossflowUnmixedEff2(Ntu, c, Rows, Cmin_fluid):
"""Lookup-derived correlations for cross-flow exchangers with finite rows."""
# ESDU 86018
if Cmin_fluid == "Air":
if Rows == 1:
eff = 1 / c * (1 - math.exp(-c * (1 - math.exp(-Ntu))))
elif Rows == 2:
k = 1 - math.exp(-Ntu / 2)
eff = 1 / c * (1 - math.exp(-2 * k * c) * (1 + c * k**2))
elif Rows == 3:
k = 1 - math.exp(-Ntu / 3)
eff = (
1
/ c
* (
1
- math.exp(-3 * k * c)
* (1 + c * k**2 * (3 - k) + (3 * c**2 * k**4) / 2)
)
)
elif Rows == 4:
k = 1 - math.exp(-Ntu / 4)
eff = (
1
/ c
* (
1
- math.exp(-4 * k * c)
* (
1
+ c * k**2 * (6 - 4 * k + k**2)
+ 4 * c**2 * k**4 * (2 - k)
+ (8 * c**3 * k**6) / 3
)
)
)
elif Rows > 4:
eff = CrossflowUnmixedEff1(Ntu, c)
else:
if Rows == 1:
eff = 1 - math.exp(-1 / c * (1 - math.exp(-Ntu * c)))
elif Rows == 2:
k = 1 - math.exp(-Ntu * c / 2)
eff = 1 - math.exp(-2 * k / c) * (1 + (k**2) / c)
elif Rows == 3:
k = 1 - math.exp(-Ntu * c / 3)
eff = 1 - math.exp(-3 * k / c) * (
1 + k**2 * (3 - k) / c + (3 * k**4) / (2 * c**2)
)
elif Rows == 4:
k = 1 - math.exp(-Ntu * c / 4)
eff = 1 - math.exp(-4 * k / c) * (
1
+ k**2 * (6 - 4 * k + k**2) / c
+ 4 * k**4 * (2 - k) / c**2
+ (8 * k**6) / (3 * c**3)
)
elif Rows > 4:
eff = CrossflowUnmixedEff1(Ntu, c)
return eff
[docs]
def HX_NTU_Numerical(Arrangement, eff, c):
"""Solve for NTU numerically when closed-form expressions are unavailable."""
NTU1 = 0.001
NTU2 = 0.1
eps = 1e-5
f = 100.0
count = 1
F1 = eff - HX_Eff(Arrangement, NTU1, c)
F2 = eff - HX_Eff(Arrangement, NTU2, c)
while f > eps:
a = (F1 - F2) / (NTU1 - NTU2)
b = F1 - a * NTU1
NTU3 = -b / a
F3 = eff - HX_Eff(Arrangement, NTU3, c)
f = abs(F3)
NTU1 = NTU2
F1 = F2
NTU2 = NTU3
F2 = F3
count += 1
if count > 50:
raise ValueError("Solution does not converge")
return NTU3