use crate::error::{Result, UshmaError};
pub const STEFAN_BOLTZMANN: f64 = 5.670_374_419e-8;
pub const BOLTZMANN_K: f64 = 1.380_649e-23;
#[tracing::instrument(level = "debug")]
pub fn conduction(
conductivity: f64,
area: f64,
t_hot: f64,
t_cold: f64,
thickness: f64,
) -> Result<f64> {
if conductivity < 0.0 {
return Err(UshmaError::InvalidConductivity {
value: conductivity,
});
}
if thickness.abs() < 1e-30 {
return Err(UshmaError::DivisionByZero {
context: "thickness cannot be zero for conduction".into(),
});
}
Ok(conductivity * area * (t_hot - t_cold) / thickness)
}
#[inline]
#[must_use]
pub fn convection(h: f64, area: f64, t_surface: f64, t_fluid: f64) -> f64 {
h * area * (t_surface - t_fluid)
}
#[tracing::instrument(level = "debug")]
pub fn radiation(emissivity: f64, area: f64, t_surface: f64, t_surrounding: f64) -> Result<f64> {
if !(0.0..=1.0).contains(&emissivity) {
return Err(UshmaError::InvalidParameter {
reason: format!("emissivity {emissivity} must be in [0, 1]"),
});
}
if t_surface < 0.0 {
return Err(UshmaError::InvalidTemperature { kelvin: t_surface });
}
if t_surrounding < 0.0 {
return Err(UshmaError::InvalidTemperature {
kelvin: t_surrounding,
});
}
let t_s4 = t_surface.powi(4);
let t_r4 = t_surrounding.powi(4);
Ok(emissivity * STEFAN_BOLTZMANN * area * (t_s4 - t_r4))
}
pub fn thermal_resistance_conduction(conductivity: f64, area: f64, thickness: f64) -> Result<f64> {
let denom = conductivity * area;
if denom.abs() < 1e-30 {
return Err(UshmaError::DivisionByZero {
context: "k⋅A cannot be zero for thermal resistance".into(),
});
}
Ok(thickness / denom)
}
pub fn thermal_resistance_convection(h: f64, area: f64) -> Result<f64> {
let denom = h * area;
if denom.abs() < 1e-30 {
return Err(UshmaError::DivisionByZero {
context: "h⋅A cannot be zero for thermal resistance".into(),
});
}
Ok(1.0 / denom)
}
#[inline]
#[must_use]
pub fn thermal_resistance_series(resistances: &[f64]) -> f64 {
resistances.iter().sum()
}
pub fn thermal_resistance_parallel(resistances: &[f64]) -> Result<f64> {
let sum: f64 = resistances.iter().map(|r| 1.0 / r).sum();
if sum.abs() < 1e-30 {
return Err(UshmaError::DivisionByZero {
context: "parallel resistance sum is zero".into(),
});
}
Ok(1.0 / sum)
}
#[inline]
#[must_use]
pub fn heat_stored(mass: f64, specific_heat: f64, delta_t: f64) -> f64 {
mass * specific_heat * delta_t
}
pub fn thermal_diffusivity(conductivity: f64, density: f64, specific_heat: f64) -> Result<f64> {
let denom = density * specific_heat;
if denom.abs() < 1e-30 {
return Err(UshmaError::DivisionByZero {
context: "ρ⋅c_p cannot be zero for thermal diffusivity".into(),
});
}
Ok(conductivity / denom)
}
pub fn biot_number(h: f64, characteristic_length: f64, conductivity: f64) -> Result<f64> {
if conductivity.abs() < 1e-30 {
return Err(UshmaError::DivisionByZero {
context: "conductivity cannot be zero for Biot number".into(),
});
}
Ok(h * characteristic_length / conductivity)
}
#[inline]
#[must_use]
pub fn lumped_capacitance(
t_initial: f64,
t_environment: f64,
time: f64,
time_constant: f64,
) -> f64 {
t_environment + (t_initial - t_environment) * (-time / time_constant).exp()
}
pub fn reynolds_number(velocity: f64, length: f64, kinematic_viscosity: f64) -> Result<f64> {
if kinematic_viscosity <= 0.0 {
return Err(UshmaError::InvalidParameter {
reason: format!("kinematic viscosity {kinematic_viscosity} must be positive"),
});
}
Ok(velocity * length / kinematic_viscosity)
}
pub fn prandtl_number(kinematic_viscosity: f64, thermal_diffusivity: f64) -> Result<f64> {
if thermal_diffusivity <= 0.0 {
return Err(UshmaError::DivisionByZero {
context: "thermal diffusivity must be positive for Prandtl number".into(),
});
}
Ok(kinematic_viscosity / thermal_diffusivity)
}
pub fn nusselt_number(h: f64, length: f64, conductivity: f64) -> Result<f64> {
if conductivity.abs() < 1e-30 {
return Err(UshmaError::DivisionByZero {
context: "conductivity cannot be zero for Nusselt number".into(),
});
}
Ok(h * length / conductivity)
}
pub fn nusselt_dittus_boelter(re: f64, pr: f64) -> Result<f64> {
if re <= 0.0 {
return Err(UshmaError::InvalidParameter {
reason: format!("Reynolds number {re} must be positive"),
});
}
if pr <= 0.0 {
return Err(UshmaError::InvalidParameter {
reason: format!("Prandtl number {pr} must be positive"),
});
}
Ok(0.023 * re.powf(0.8) * pr.powf(0.4))
}
pub fn nusselt_natural_vertical(ra: f64, pr: f64) -> Result<f64> {
if ra < 0.0 {
return Err(UshmaError::InvalidParameter {
reason: format!("Rayleigh number {ra} must be non-negative"),
});
}
if pr <= 0.0 {
return Err(UshmaError::InvalidParameter {
reason: format!("Prandtl number {pr} must be positive"),
});
}
let psi = (1.0 + (0.492 / pr).powf(9.0 / 16.0)).powf(-8.0 / 27.0);
let nu = (0.825 + 0.387 * ra.powf(1.0 / 6.0) * psi).powi(2);
Ok(nu)
}
pub fn boundary_layer_thickness(x: f64, re_x: f64) -> Result<f64> {
if x <= 0.0 {
return Err(UshmaError::InvalidParameter {
reason: format!("distance x={x} m must be positive"),
});
}
if re_x <= 0.0 {
return Err(UshmaError::InvalidParameter {
reason: format!("local Reynolds number {re_x} must be positive"),
});
}
Ok(5.0 * x / re_x.sqrt())
}
pub fn thermal_boundary_layer(x: f64, re_x: f64, pr: f64) -> Result<f64> {
if pr <= 0.0 {
return Err(UshmaError::InvalidParameter {
reason: format!("Prandtl number {pr} must be positive"),
});
}
let delta = boundary_layer_thickness(x, re_x)?;
Ok(delta / pr.powf(1.0 / 3.0))
}
pub fn fin_parameter(h: f64, perimeter: f64, k: f64, cross_area: f64) -> Result<f64> {
let denom = k * cross_area;
if denom <= 0.0 {
return Err(UshmaError::DivisionByZero {
context: "k·A_c must be positive for fin parameter".into(),
});
}
if h < 0.0 || perimeter <= 0.0 {
return Err(UshmaError::InvalidParameter {
reason: format!("h={h}, perimeter={perimeter} must be positive"),
});
}
Ok((h * perimeter / denom).sqrt())
}
#[tracing::instrument(level = "debug")]
pub fn fin_rectangular_heat(
h: f64,
perimeter: f64,
k: f64,
cross_area: f64,
length: f64,
t_base: f64,
t_fluid: f64,
) -> Result<f64> {
let m = fin_parameter(h, perimeter, k, cross_area)?;
if length <= 0.0 {
return Err(UshmaError::InvalidParameter {
reason: format!("fin length {length} m must be positive"),
});
}
let coeff = (h * perimeter * k * cross_area).sqrt();
Ok(coeff * (t_base - t_fluid) * (m * length).tanh())
}
pub fn fin_efficiency_rectangular(m: f64, length: f64) -> Result<f64> {
let ml = m * length;
if ml.abs() < 1e-30 {
return Ok(1.0); }
Ok(ml.tanh() / ml)
}
pub fn fin_effectiveness(
q_fin: f64,
h: f64,
cross_area: f64,
t_base: f64,
t_fluid: f64,
) -> Result<f64> {
let q_no_fin = h * cross_area * (t_base - t_fluid);
if q_no_fin.abs() < 1e-30 {
return Err(UshmaError::DivisionByZero {
context: "no temperature difference for fin effectiveness".into(),
});
}
Ok(q_fin / q_no_fin)
}
#[tracing::instrument(level = "debug")]
pub fn lmtd_parallel(t_h_in: f64, t_h_out: f64, t_c_in: f64, t_c_out: f64) -> Result<f64> {
let dt1 = t_h_in - t_c_in;
let dt2 = t_h_out - t_c_out;
lmtd_from_deltas(dt1, dt2)
}
#[tracing::instrument(level = "debug")]
pub fn lmtd_counter(t_h_in: f64, t_h_out: f64, t_c_in: f64, t_c_out: f64) -> Result<f64> {
let dt1 = t_h_in - t_c_out;
let dt2 = t_h_out - t_c_in;
lmtd_from_deltas(dt1, dt2)
}
fn lmtd_from_deltas(dt1: f64, dt2: f64) -> Result<f64> {
if dt1 <= 0.0 || dt2 <= 0.0 {
return Err(UshmaError::InvalidParameter {
reason: format!("temperature differences must be positive: ΔT₁={dt1}, ΔT₂={dt2}"),
});
}
let ratio = dt1 / dt2;
if (ratio - 1.0).abs() < 1e-10 {
return Ok(0.5 * (dt1 + dt2));
}
Ok((dt1 - dt2) / ratio.ln())
}
#[inline]
#[must_use]
pub fn heat_exchanger_lmtd(u: f64, area: f64, lmtd: f64) -> f64 {
u * area * lmtd
}
pub fn ntu(u: f64, area: f64, c_min: f64) -> Result<f64> {
if c_min <= 0.0 {
return Err(UshmaError::DivisionByZero {
context: "C_min must be positive for NTU".into(),
});
}
Ok(u * area / c_min)
}
pub fn effectiveness_parallel(ntu_val: f64, c_ratio: f64) -> Result<f64> {
if ntu_val < 0.0 {
return Err(UshmaError::InvalidParameter {
reason: format!("NTU {ntu_val} must be non-negative"),
});
}
if !(0.0..=1.0).contains(&c_ratio) {
return Err(UshmaError::InvalidParameter {
reason: format!("capacity ratio Cr={c_ratio} must be in [0, 1]"),
});
}
if c_ratio < 1e-30 {
return Ok(1.0 - (-ntu_val).exp());
}
Ok((1.0 - (-(1.0 + c_ratio) * ntu_val).exp()) / (1.0 + c_ratio))
}
pub fn effectiveness_counter(ntu_val: f64, c_ratio: f64) -> Result<f64> {
if ntu_val < 0.0 {
return Err(UshmaError::InvalidParameter {
reason: format!("NTU {ntu_val} must be non-negative"),
});
}
if !(0.0..=1.0).contains(&c_ratio) {
return Err(UshmaError::InvalidParameter {
reason: format!("capacity ratio Cr={c_ratio} must be in [0, 1]"),
});
}
if c_ratio < 1e-30 {
return Ok(1.0 - (-ntu_val).exp());
}
if (c_ratio - 1.0).abs() < 1e-10 {
return Ok(ntu_val / (1.0 + ntu_val));
}
let exp_term = (-(1.0 - c_ratio) * ntu_val).exp();
Ok((1.0 - exp_term) / (1.0 - c_ratio * exp_term))
}
#[inline]
#[must_use]
pub fn heat_exchanger_ntu(effectiveness: f64, c_min: f64, t_h_in: f64, t_c_in: f64) -> f64 {
effectiveness * c_min * (t_h_in - t_c_in)
}
#[tracing::instrument(level = "debug")]
pub fn view_factor_parallel_plates(width: f64, height: f64, distance: f64) -> Result<f64> {
if width <= 0.0 || height <= 0.0 || distance <= 0.0 {
return Err(UshmaError::InvalidParameter {
reason: format!("dimensions must be positive: W={width}, H={height}, D={distance}"),
});
}
let x = width / distance;
let y = height / distance;
let x2 = x * x;
let y2 = y * y;
let term1 = ((1.0 + x2) * (1.0 + y2) / (1.0 + x2 + y2)).sqrt().ln();
let term2 = x * (1.0 + y2).sqrt() * (x / (1.0 + y2).sqrt()).atan();
let term3 = y * (1.0 + x2).sqrt() * (y / (1.0 + x2).sqrt()).atan();
let term4 = x * x.atan();
let term5 = y * y.atan();
let f12 = (2.0 / (std::f64::consts::PI * x * y)) * (term1 + term2 + term3 - term4 - term5);
Ok(f12)
}
pub fn view_factor_perpendicular_plates(width: f64, h1: f64, h2: f64) -> Result<f64> {
if width <= 0.0 || h1 <= 0.0 || h2 <= 0.0 {
return Err(UshmaError::InvalidParameter {
reason: format!("dimensions must be positive: W={width}, H1={h1}, H2={h2}"),
});
}
let h = h1 / width;
let w = h2 / width;
let h2v = h * h;
let w2 = w * w;
let a = (1.0 + h2v) * (1.0 + w2);
let b = 1.0 + h2v + w2;
let term1 = w * (1.0 + h2v).sqrt().atan() + h * (1.0 + w2).sqrt().atan();
let f12 = (1.0 / (std::f64::consts::PI * h)) * (term1 - b.sqrt().atan() + 0.25 * (a / b).ln());
Ok(f12.clamp(0.0, 1.0))
}
pub fn view_factor_coaxial_disks(r1: f64, r2: f64, distance: f64) -> Result<f64> {
if r1 <= 0.0 || r2 <= 0.0 || distance <= 0.0 {
return Err(UshmaError::InvalidParameter {
reason: format!("dimensions must be positive: r1={r1}, r2={r2}, d={distance}"),
});
}
let r1_d = r1 / distance;
let r2_d = r2 / distance;
let s = 1.0 + (1.0 + r2_d * r2_d) / (r1_d * r1_d);
let f12 = 0.5 * (s - (s * s - 4.0 * (r2_d / r1_d).powi(2)).sqrt());
Ok(f12)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_conduction_positive_flux() {
let q = conduction(401.0, 0.01, 373.15, 293.15, 0.1).unwrap();
assert!(q > 0.0);
}
#[test]
fn test_conduction_zero_gradient() {
let q = conduction(401.0, 0.01, 300.0, 300.0, 0.1).unwrap();
assert!(q.abs() < 1e-10);
}
#[test]
fn test_convection() {
let q = convection(25.0, 1.0, 373.15, 293.15);
assert!((q - 2000.0).abs() < 1.0);
}
#[test]
fn test_radiation() {
let q = radiation(1.0, 1.0, 373.15, 293.15).unwrap();
assert!(q > 0.0);
}
#[test]
fn test_radiation_negative_temp() {
assert!(radiation(1.0, 1.0, -10.0, 293.15).is_err());
}
#[test]
fn test_thermal_resistance_series() {
let r = thermal_resistance_series(&[1.0, 2.0, 3.0]);
assert!((r - 6.0).abs() < 1e-10);
}
#[test]
fn test_thermal_resistance_parallel() {
let r = thermal_resistance_parallel(&[2.0, 2.0]).unwrap();
assert!((r - 1.0).abs() < 1e-10);
}
#[test]
fn test_heat_stored() {
let q = heat_stored(1.0, 4186.0, 10.0);
assert!((q - 41860.0).abs() < 1.0);
}
#[test]
fn test_thermal_diffusivity() {
let alpha = thermal_diffusivity(401.0, 8960.0, 385.0).unwrap();
assert!((alpha - 1.16e-4).abs() / 1.16e-4 < 0.01);
}
#[test]
fn test_biot_number() {
let bi = biot_number(25.0, 0.01, 401.0).unwrap();
assert!(bi < 0.1);
}
#[test]
fn test_lumped_capacitance() {
let t0 = 373.15;
let t_env = 293.15;
let tau = 100.0;
let t = lumped_capacitance(t0, t_env, 0.0, tau);
assert!((t - t0).abs() < 1e-10);
let t_inf = lumped_capacitance(t0, t_env, 10000.0, tau);
assert!((t_inf - t_env).abs() < 1.0);
}
#[test]
fn test_invalid_conductivity() {
assert!(conduction(-1.0, 1.0, 373.0, 293.0, 0.1).is_err());
}
#[test]
fn test_radiation_invalid_emissivity() {
assert!(radiation(-0.1, 1.0, 373.0, 293.0).is_err());
assert!(radiation(1.1, 1.0, 373.0, 293.0).is_err());
assert!(radiation(0.0, 1.0, 373.0, 293.0).is_ok());
assert!(radiation(1.0, 1.0, 373.0, 293.0).is_ok());
}
#[test]
fn test_conduction_zero_thickness() {
assert!(conduction(401.0, 1.0, 373.0, 293.0, 0.0).is_err());
}
#[test]
fn test_thermal_resistance_convection_zero() {
assert!(thermal_resistance_convection(0.0, 1.0).is_err());
assert!(thermal_resistance_convection(25.0, 0.0).is_err());
}
#[test]
fn test_thermal_diffusivity_zero_density() {
assert!(thermal_diffusivity(401.0, 0.0, 385.0).is_err());
}
#[test]
fn test_biot_number_zero_conductivity() {
assert!(biot_number(25.0, 0.01, 0.0).is_err());
}
#[test]
fn test_conduction_nan_propagation() {
let result = conduction(f64::NAN, 1.0, 373.0, 293.0, 0.1);
assert!(result.unwrap().is_nan());
}
#[test]
fn test_radiation_nan_emissivity() {
assert!(radiation(f64::NAN, 1.0, 373.0, 293.0).is_err());
}
#[test]
fn test_heat_stored_negative_delta() {
let q = heat_stored(1.0, 4186.0, -10.0);
assert!(q < 0.0);
}
#[test]
fn test_thermal_resistance_parallel_single() {
let r = thermal_resistance_parallel(&[5.0]).unwrap();
assert!((r - 5.0).abs() < 1e-10);
}
#[test]
fn test_reynolds_number() {
let re = reynolds_number(1.0, 0.1, 1e-6).unwrap();
assert!((re - 100_000.0).abs() < 1.0);
}
#[test]
fn test_reynolds_invalid() {
assert!(reynolds_number(1.0, 0.1, 0.0).is_err());
assert!(reynolds_number(1.0, 0.1, -1e-6).is_err());
}
#[test]
fn test_prandtl_number() {
let pr = prandtl_number(1.6e-5, 2.2e-5).unwrap();
assert!((pr - 0.727).abs() < 0.01);
}
#[test]
fn test_nusselt_number() {
let nu = nusselt_number(25.0, 0.1, 0.6).unwrap();
assert!((nu - 25.0 * 0.1 / 0.6).abs() < 1e-10);
}
#[test]
fn test_dittus_boelter() {
let nu = nusselt_dittus_boelter(50_000.0, 0.7).unwrap();
assert!(nu > 100.0 && nu < 150.0);
}
#[test]
fn test_dittus_boelter_invalid() {
assert!(nusselt_dittus_boelter(0.0, 0.7).is_err());
assert!(nusselt_dittus_boelter(50_000.0, 0.0).is_err());
}
#[test]
fn test_nusselt_natural_vertical() {
let nu = nusselt_natural_vertical(1e9, 0.7).unwrap();
assert!(nu > 50.0); }
#[test]
fn test_nusselt_natural_vertical_zero_ra() {
let nu = nusselt_natural_vertical(0.0, 0.7).unwrap();
assert!((nu - 0.825_f64.powi(2)).abs() < 0.01);
}
#[test]
fn test_boundary_layer_thickness() {
let delta = boundary_layer_thickness(0.5, 100_000.0).unwrap();
assert!((delta - 0.00791).abs() < 0.001);
}
#[test]
fn test_thermal_boundary_layer() {
let delta = boundary_layer_thickness(0.5, 100_000.0).unwrap();
let delta_t = thermal_boundary_layer(0.5, 100_000.0, 0.7).unwrap();
assert!(delta_t > delta);
}
#[test]
fn test_boundary_layer_invalid() {
assert!(boundary_layer_thickness(0.0, 100_000.0).is_err());
assert!(boundary_layer_thickness(0.5, 0.0).is_err());
assert!(thermal_boundary_layer(0.5, 100_000.0, 0.0).is_err());
}
#[test]
fn test_fin_parameter() {
let m = fin_parameter(25.0, 0.02, 237.0, 1e-4).unwrap();
assert!((m - 4.59).abs() < 0.1);
}
#[test]
fn test_fin_rectangular_heat() {
let q = fin_rectangular_heat(25.0, 0.02, 237.0, 1e-4, 0.05, 373.15, 293.15).unwrap();
assert!(q > 0.0); }
#[test]
fn test_fin_efficiency_short_fin() {
let eta = fin_efficiency_rectangular(5.0, 0.001).unwrap();
assert!((eta - 1.0).abs() < 0.01);
}
#[test]
fn test_fin_efficiency_long_fin() {
let eta = fin_efficiency_rectangular(5.0, 0.5).unwrap();
assert!(eta < 1.0);
assert!(eta > 0.0);
}
#[test]
fn test_fin_efficiency_zero_length() {
let eta = fin_efficiency_rectangular(5.0, 0.0).unwrap();
assert!((eta - 1.0).abs() < 1e-10);
}
#[test]
fn test_fin_effectiveness_greater_than_one() {
let q_fin = fin_rectangular_heat(25.0, 0.02, 237.0, 1e-4, 0.05, 373.15, 293.15).unwrap();
let eff = fin_effectiveness(q_fin, 25.0, 1e-4, 373.15, 293.15).unwrap();
assert!(eff > 1.0, "Fin effectiveness {eff} should exceed 1.0");
}
#[test]
fn test_fin_parameter_invalid() {
assert!(fin_parameter(25.0, 0.02, 0.0, 1e-4).is_err()); assert!(fin_parameter(25.0, 0.0, 237.0, 1e-4).is_err()); }
#[test]
fn test_lmtd_parallel() {
let lmtd = lmtd_parallel(150.0, 100.0, 30.0, 80.0).unwrap();
let expected = (120.0 - 20.0) / (120.0_f64 / 20.0).ln();
assert!((lmtd - expected).abs() < 0.1);
}
#[test]
fn test_lmtd_counter() {
let lmtd = lmtd_counter(150.0, 100.0, 30.0, 80.0).unwrap();
assert!((lmtd - 70.0).abs() < 0.1);
}
#[test]
fn test_lmtd_counter_gte_parallel() {
let lmtd_p = lmtd_parallel(200.0, 120.0, 50.0, 90.0).unwrap();
let lmtd_c = lmtd_counter(200.0, 120.0, 50.0, 90.0).unwrap();
assert!(lmtd_c >= lmtd_p);
}
#[test]
fn test_lmtd_invalid() {
assert!(lmtd_parallel(50.0, 40.0, 60.0, 30.0).is_err());
}
#[test]
fn test_heat_exchanger_lmtd_basic() {
let q = heat_exchanger_lmtd(500.0, 2.0, 50.0);
assert!((q - 50_000.0).abs() < 1e-10);
}
#[test]
fn test_ntu_basic() {
let n = ntu(500.0, 2.0, 1000.0).unwrap();
assert!((n - 1.0).abs() < 1e-10);
}
#[test]
fn test_effectiveness_parallel_increases_with_ntu() {
let e1 = effectiveness_parallel(1.0, 0.5).unwrap();
let e2 = effectiveness_parallel(3.0, 0.5).unwrap();
assert!(e2 > e1);
assert!(e2 <= 1.0);
}
#[test]
fn test_effectiveness_counter_gte_parallel() {
let ep = effectiveness_parallel(2.0, 0.5).unwrap();
let ec = effectiveness_counter(2.0, 0.5).unwrap();
assert!(ec >= ep);
}
#[test]
fn test_effectiveness_counter_balanced() {
let e = effectiveness_counter(2.0, 1.0).unwrap();
assert!((e - 2.0 / 3.0).abs() < 1e-10);
}
#[test]
fn test_effectiveness_cr_zero() {
let ep = effectiveness_parallel(2.0, 0.0).unwrap();
let ec = effectiveness_counter(2.0, 0.0).unwrap();
let expected = 1.0 - (-2.0_f64).exp();
assert!((ep - expected).abs() < 1e-10);
assert!((ec - expected).abs() < 1e-10);
}
#[test]
fn test_effectiveness_invalid() {
assert!(effectiveness_parallel(-1.0, 0.5).is_err());
assert!(effectiveness_parallel(2.0, 1.5).is_err());
assert!(effectiveness_counter(2.0, -0.1).is_err());
}
#[test]
fn test_heat_exchanger_ntu_basic() {
let q = heat_exchanger_ntu(0.8, 1000.0, 200.0, 50.0);
assert!((q - 0.8 * 1000.0 * 150.0).abs() < 1e-10);
}
#[test]
fn test_view_factor_parallel_plates_close() {
let f = view_factor_parallel_plates(10.0, 10.0, 0.1).unwrap();
assert!(f > 0.9, "F={f} should be close to 1 for large close plates");
}
#[test]
fn test_view_factor_parallel_plates_far() {
let f = view_factor_parallel_plates(0.1, 0.1, 10.0).unwrap();
assert!(f < 0.01);
}
#[test]
fn test_view_factor_parallel_plates_range() {
let f = view_factor_parallel_plates(1.0, 1.0, 1.0).unwrap();
assert!(f > 0.0 && f < 1.0);
}
#[test]
fn test_view_factor_parallel_plates_invalid() {
assert!(view_factor_parallel_plates(0.0, 1.0, 1.0).is_err());
assert!(view_factor_parallel_plates(1.0, 0.0, 1.0).is_err());
assert!(view_factor_parallel_plates(1.0, 1.0, 0.0).is_err());
}
#[test]
fn test_view_factor_perpendicular_range() {
let f = view_factor_perpendicular_plates(1.0, 1.0, 1.0).unwrap();
assert!(f > 0.0 && f < 1.0);
}
#[test]
fn test_view_factor_perpendicular_invalid() {
assert!(view_factor_perpendicular_plates(0.0, 1.0, 1.0).is_err());
}
#[test]
fn test_view_factor_coaxial_disks_equal() {
let f = view_factor_coaxial_disks(1.0, 1.0, 0.1).unwrap();
assert!(f > 0.9);
}
#[test]
fn test_view_factor_coaxial_disks_far() {
let f = view_factor_coaxial_disks(0.1, 0.1, 10.0).unwrap();
assert!(f < 0.01);
}
#[test]
fn test_view_factor_coaxial_disks_range() {
let f = view_factor_coaxial_disks(0.5, 0.5, 1.0).unwrap();
assert!(f > 0.0 && f < 1.0);
}
#[test]
fn test_view_factor_invalid() {
assert!(view_factor_coaxial_disks(0.0, 0.5, 1.0).is_err());
assert!(view_factor_coaxial_disks(0.5, 0.0, 1.0).is_err());
assert!(view_factor_coaxial_disks(0.5, 0.5, 0.0).is_err());
}
}