//! Two-phase frictional pressure-drop correlations. //! //! Provides Friedel (1979) and Müller-Steinhagen-Heck (1986) two-phase friction //! models — the latter is the pragmatic default in NIST EVAP-COND — together //! with the Zivi void fraction and a lumped quadratic model for system-level //! solvers that do not carry detailed tube geometry. //! //! All functions are analytic and side-effect free so they can be embedded in a //! Newton residual/Jacobian without breaking the zero-panic policy. use super::correlation_registry::{ assess_candidate, correlation_metadata, CandidateAssessment, CorrelationId, CorrelationMetadata, CorrelationPurpose, DomainInputError, ExchangerGeometryType, FlowRegime, OperatingPoint, SelectionContext, }; /// Standard gravitational acceleration [m/s²]. const G_ACCEL: f64 = 9.80665; /// Inputs describing the local two-phase state and channel for a Friedel /// pressure-gradient evaluation. All quantities are SI. #[derive(Debug, Clone, Copy)] pub struct FriedelInput { /// Mass flux G = ṁ / A_cross [kg/(m²·s)]. pub mass_flux: f64, /// Hydraulic diameter [m]. pub diameter: f64, /// Vapor quality x ∈ [0, 1] [-]. pub quality: f64, /// Saturated-liquid density [kg/m³]. pub rho_liquid: f64, /// Saturated-vapor density [kg/m³]. pub rho_vapor: f64, /// Liquid dynamic viscosity [Pa·s]. pub mu_liquid: f64, /// Vapor dynamic viscosity [Pa·s]. pub mu_vapor: f64, /// Surface tension [N/m]. pub sigma: f64, } /// Returns the registered Friedel applicability metadata. pub fn friedel_metadata() -> CorrelationMetadata { correlation_metadata(CorrelationId::Friedel1979) } /// Assesses Friedel applicability without evaluating the analytic formula. pub fn assess_friedel_domain( input: &FriedelInput, geometry: ExchangerGeometryType, regime: FlowRegime, refrigerant: Option<&str>, ) -> Result { for (field, value) in [ ("mass_flux", input.mass_flux), ("hydraulic_diameter", input.diameter), ("liquid_density", input.rho_liquid), ("vapor_density", input.rho_vapor), ("liquid_viscosity", input.mu_liquid), ("vapor_viscosity", input.mu_vapor), ("surface_tension", input.sigma), ] { if !value.is_finite() || value <= 0.0 { return Err(DomainInputError::InvalidPositive { field, value }); } } if !input.quality.is_finite() || !(0.0..=1.0).contains(&input.quality) { return Err(DomainInputError::InvalidQuality { value: input.quality, }); } let context = SelectionContext { purpose: CorrelationPurpose::PressureDrop, geometry, regime, refrigerant: refrigerant.map(str::to_owned), operating_point: OperatingPoint { reynolds: Some(input.mass_flux * input.diameter / input.mu_liquid), mass_flux: Some(input.mass_flux), quality: Some(input.quality), ..OperatingPoint::default() }, }; assess_candidate(&friedel_metadata(), &context) } /// Fanning friction factor for single-phase flow (Blasius/laminar blend). /// /// Uses `16/Re` in the laminar regime (Re < 1187, the Blasius crossover) and /// the Blasius smooth-tube correlation `0.079·Re^-0.25` in the turbulent /// regime. Guards against non-positive Reynolds numbers. #[inline] pub fn fanning_friction_factor(reynolds: f64) -> f64 { if reynolds <= 0.0 { return 0.0; } if reynolds < 1187.0 { 16.0 / reynolds } else { 0.079 * reynolds.powf(-0.25) } } /// Homogeneous two-phase density 1/(x/ρ_g + (1-x)/ρ_l) [kg/m³]. #[inline] pub fn homogeneous_density(quality: f64, rho_liquid: f64, rho_vapor: f64) -> f64 { let x = quality.clamp(0.0, 1.0); let inv = x / rho_vapor.max(1e-9) + (1.0 - x) / rho_liquid.max(1e-9); if inv <= 0.0 { rho_liquid } else { 1.0 / inv } } /// Zivi (1964) void fraction based on minimum-entropy-production slip. /// /// `α = 1 / (1 + ((1-x)/x)·(ρ_g/ρ_l)^(2/3))`, clamped to [0, 1]. Returns 0 for /// x ≤ 0 and 1 for x ≥ 1. #[inline] pub fn zivi_void_fraction(quality: f64, rho_liquid: f64, rho_vapor: f64) -> f64 { let x = quality; if x <= 0.0 { return 0.0; } if x >= 1.0 { return 1.0; } let ratio = (rho_vapor.max(1e-9) / rho_liquid.max(1e-9)).powf(2.0 / 3.0); let denom = 1.0 + ((1.0 - x) / x) * ratio; (1.0 / denom).clamp(0.0, 1.0) } /// Liquid-only frictional pressure gradient `(dP/dz)_LO` [Pa/m]. /// /// The gradient the whole mixture mass flux would produce if flowing as /// saturated liquid: `2·f_LO·G²/(D·ρ_l)` (Fanning convention). #[inline] fn liquid_only_gradient(g: f64, d: f64, rho_l: f64, mu_l: f64) -> f64 { let re_lo = g * d / mu_l.max(1e-12); let f_lo = fanning_friction_factor(re_lo); 2.0 * f_lo * g * g / (d.max(1e-9) * rho_l.max(1e-9)) } /// Friedel (1979) two-phase friction multiplier `φ_LO²` [-]. /// /// Multiplies the liquid-only gradient to obtain the two-phase frictional /// gradient. Returns 1.0 at x = 0 (single-phase liquid) and is always ≥ 0. pub fn friedel_multiplier(input: &FriedelInput) -> f64 { let x = input.quality.clamp(0.0, 1.0); if x <= 0.0 { return 1.0; } let g = input.mass_flux.abs(); let d = input.diameter.max(1e-9); let rho_l = input.rho_liquid.max(1e-9); let rho_g = input.rho_vapor.max(1e-9); let mu_l = input.mu_liquid.max(1e-12); let mu_g = input.mu_vapor.max(1e-12); let re_lo = g * d / mu_l; let re_go = g * d / mu_g; let f_lo = fanning_friction_factor(re_lo); let f_go = fanning_friction_factor(re_go); // E = (1-x)² + x²·(ρ_l·f_go)/(ρ_g·f_lo) let e = (1.0 - x).powi(2) + x * x * (rho_l * f_go) / (rho_g * f_lo.max(1e-12)); // F = x^0.78·(1-x)^0.224 let f = x.powf(0.78) * (1.0 - x).powf(0.224); // H = (ρ_l/ρ_g)^0.91·(μ_g/μ_l)^0.19·(1-μ_g/μ_l)^0.7 let mu_ratio = mu_g / mu_l; let h = (rho_l / rho_g).powf(0.91) * mu_ratio.powf(0.19) * (1.0 - mu_ratio).max(0.0).powf(0.7); // Homogeneous Froude and Weber numbers. let rho_h = homogeneous_density(x, rho_l, rho_g); let fr = g * g / (G_ACCEL * d * rho_h * rho_h); let we = g * g * d / (rho_h * input.sigma.max(1e-9)); e + 3.24 * f * h / (fr.powf(0.045) * we.powf(0.035)) } /// Two-phase frictional pressure gradient `(dP/dz)` [Pa/m] via Friedel. /// /// `φ_LO² · (dP/dz)_LO`. Always ≥ 0 (a magnitude); the caller applies the sign /// according to flow direction (pressure decreases downstream). pub fn friedel_gradient(input: &FriedelInput) -> f64 { let g = input.mass_flux.abs(); let dpdz_lo = liquid_only_gradient(g, input.diameter, input.rho_liquid, input.mu_liquid); friedel_multiplier(input) * dpdz_lo } /// Total Friedel frictional pressure drop `ΔP` [Pa] over a channel `length`. /// /// Evaluated at a representative (e.g. mean) quality. Returns a positive /// magnitude. pub fn friedel_pressure_drop(input: &FriedelInput, length: f64) -> f64 { friedel_gradient(input) * length.max(0.0) } /// Selectable two-phase frictional ΔP correlation. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum TwoPhaseDpCorrelation { /// Friedel (1979) — general-purpose, surface-tension dependent. #[default] Friedel1979, /// Müller-Steinhagen-Heck (1986) — NIST EVAP-COND default, no pattern map. MullerSteinhagenHeck1986, } impl TwoPhaseDpCorrelation { /// Stable registry identifier. pub const fn id(self) -> CorrelationId { match self { Self::Friedel1979 => CorrelationId::Friedel1979, Self::MullerSteinhagenHeck1986 => CorrelationId::MullerSteinhagenHeck1986, } } /// Frictional pressure gradient [Pa/m] (positive magnitude). pub fn gradient(self, input: &FriedelInput) -> f64 { match self { Self::Friedel1979 => friedel_gradient(input), Self::MullerSteinhagenHeck1986 => msh_gradient(input), } } /// Frictional pressure drop [Pa] over `length` (positive magnitude). pub fn pressure_drop(self, input: &FriedelInput, length: f64) -> f64 { self.gradient(input) * length.max(0.0) } } /// Liquid-only and vapor-only frictional gradients for MSH [Pa/m]. #[inline] fn vapor_only_gradient(g: f64, d: f64, rho_g: f64, mu_g: f64) -> f64 { let re_go = g * d / mu_g.max(1e-12); let f_go = fanning_friction_factor(re_go); 2.0 * f_go * g * g / (d.max(1e-9) * rho_g.max(1e-9)) } /// Müller-Steinhagen-Heck (1986) two-phase frictional gradient [Pa/m]. /// /// Linear blend between liquid-only and vapor-only gradients with a cubic /// quality correction: /// `(dP/dz) = A + 2(B−A)x` at low x, then smooth to vapor-only at x→1 via /// `(dP/dz) = (A + 2(B−A)x)·(1−x)^(1/3) + B·x³` where A=(dP/dz)_LO, B=(dP/dz)_GO. pub fn msh_gradient(input: &FriedelInput) -> f64 { let x = input.quality.clamp(0.0, 1.0); let g = input.mass_flux.abs(); let a = liquid_only_gradient(g, input.diameter, input.rho_liquid, input.mu_liquid); let b = vapor_only_gradient(g, input.diameter, input.rho_vapor, input.mu_vapor); if x <= 0.0 { return a; } if x >= 1.0 { return b; } let linear = a + 2.0 * (b - a) * x; linear * (1.0 - x).powf(1.0 / 3.0) + b * x.powi(3) } /// MSH frictional pressure drop [Pa] over `length`. pub fn msh_pressure_drop(input: &FriedelInput, length: f64) -> f64 { msh_gradient(input) * length.max(0.0) } /// Returns MSH registry metadata. pub fn msh_metadata() -> CorrelationMetadata { correlation_metadata(CorrelationId::MullerSteinhagenHeck1986) } /// Assesses MSH applicability without evaluating the formula. pub fn assess_msh_domain( input: &FriedelInput, geometry: ExchangerGeometryType, regime: FlowRegime, refrigerant: Option<&str>, ) -> Result { for (field, value) in [ ("mass_flux", input.mass_flux), ("hydraulic_diameter", input.diameter), ("liquid_density", input.rho_liquid), ("vapor_density", input.rho_vapor), ("liquid_viscosity", input.mu_liquid), ("vapor_viscosity", input.mu_vapor), ] { if !value.is_finite() || value <= 0.0 { return Err(DomainInputError::InvalidPositive { field, value }); } } if !input.quality.is_finite() || !(0.0..=1.0).contains(&input.quality) { return Err(DomainInputError::InvalidQuality { value: input.quality, }); } let context = SelectionContext { purpose: CorrelationPurpose::PressureDrop, geometry, regime, refrigerant: refrigerant.map(str::to_owned), operating_point: OperatingPoint { reynolds: Some(input.mass_flux * input.diameter / input.mu_liquid), mass_flux: Some(input.mass_flux), quality: Some(input.quality), ..OperatingPoint::default() }, }; assess_candidate(&msh_metadata(), &context) } /// Lumped quadratic pressure drop `ΔP = k · ṁ·|ṁ|` [Pa]. /// /// The standard system-level representation when detailed geometry is /// unavailable: `k` [Pa·s²/kg²] is a single coefficient calibrated from one /// rated (ṁ, ΔP) point. Signed by `ṁ` so it is antisymmetric and its /// derivative `∂ΔP/∂ṁ = 2·k·|ṁ|` is continuous through ṁ = 0. #[inline] pub fn quadratic_drop(k: f64, mass_flow: f64) -> f64 { k * mass_flow * mass_flow.abs() } /// Analytic derivative of [`quadratic_drop`] w.r.t. mass flow: `2·k·|ṁ|`. #[inline] pub fn quadratic_drop_dm(k: f64, mass_flow: f64) -> f64 { 2.0 * k * mass_flow.abs() } /// Calibrates the lumped coefficient `k` from a rated point (ṁ, ΔP). /// /// `k = ΔP_rated / ṁ_rated²`. Returns 0 for a non-positive rated flow. #[inline] pub fn calibrate_quadratic_k(rated_dp: f64, rated_mass_flow: f64) -> f64 { if rated_mass_flow.abs() < 1e-12 { 0.0 } else { rated_dp / (rated_mass_flow * rated_mass_flow) } } /// Reference design pressure drop [Pa] for *quadratic* rating calibration /// (Modelica Buildings `dp_nominal` style). Not applied silently: use /// [`calibrate_quadratic_k`] or tube correlations ([`tube_two_phase_delta_p`]). pub const DEFAULT_REFRIGERANT_DP_NOMINAL_PA: f64 = 15_000.0; /// Nominal refrigerant mass flow [kg/s] paired with /// [`DEFAULT_REFRIGERANT_DP_NOMINAL_PA`] for quadratic-`k` calibration. pub const DEFAULT_REFRIGERANT_M_NOMINAL_KG_S: f64 = 0.05; /// Default tube length [m] when `dp_model=msh|friedel` omits geometry. pub const DEFAULT_TUBE_LENGTH_M: f64 = 6.0; /// Default hydraulic diameter [m] (~3/8″ DX tube). pub const DEFAULT_TUBE_DIAMETER_M: f64 = 0.0095; /// Default number of parallel refrigerant channels (keeps G in a DX-like band /// for small chillers ~0.05 kg/s). pub const DEFAULT_N_PARALLEL_TUBES: f64 = 2.0; /// Lumped `k` [Pa·s²/kg²] from the reference design point /// (`15 kPa` @ `0.05 kg/s` → `6×10⁶`). Prefer tube MSH/Friedel when geometry /// is known. #[inline] pub fn default_refrigerant_pressure_drop_coeff() -> f64 { calibrate_quadratic_k( DEFAULT_REFRIGERANT_DP_NOMINAL_PA, DEFAULT_REFRIGERANT_M_NOMINAL_KG_S, ) } /// Minimal tube-bundle geometry for DX frictional ΔP. #[derive(Debug, Clone, Copy, PartialEq)] pub struct TubeChannelGeometry { /// Refrigerant path length [m]. pub length_m: f64, /// Hydraulic diameter [m]. pub diameter_m: f64, /// Number of parallel tubes / channels [-] (≥ 1). pub n_parallel: f64, } impl TubeChannelGeometry { /// DX defaults: 6 m × 9.5 mm × 2 parallel. pub fn dx_default() -> Self { Self { length_m: DEFAULT_TUBE_LENGTH_M, diameter_m: DEFAULT_TUBE_DIAMETER_M, n_parallel: DEFAULT_N_PARALLEL_TUBES, } } /// Total cross-sectional flow area [m²]. #[inline] pub fn flow_area_m2(self) -> f64 { let d = self.diameter_m.max(1e-9); self.n_parallel.max(1.0) * std::f64::consts::PI * d * d / 4.0 } } /// Saturated-phase transport properties at the local pressure. #[derive(Debug, Clone, Copy)] pub struct SatTransportProps { /// Saturated-liquid density [kg/m³]. pub rho_liquid: f64, /// Saturated-vapor density [kg/m³]. pub rho_vapor: f64, /// Saturated-liquid dynamic viscosity [Pa·s]. pub mu_liquid: f64, /// Saturated-vapor dynamic viscosity [Pa·s]. pub mu_vapor: f64, /// Surface tension [N/m] (Friedel); unused by MSH but kept for a shared input. pub sigma: f64, } /// Acceleration pressure change [Pa]: `G² (v(x_out) − v(x_in))` with homogeneous /// specific volume `v = x/ρ_v + (1−x)/ρ_l`. Positive when quality rises (evaporator). #[inline] pub fn acceleration_drop( mass_flux: f64, x_in: f64, x_out: f64, rho_liquid: f64, rho_vapor: f64, ) -> f64 { let g = mass_flux; let v = |x: f64| { let xc = x.clamp(0.0, 1.0); xc / rho_vapor.max(1e-9) + (1.0 - xc) / rho_liquid.max(1e-9) }; g * g * (v(x_out) - v(x_in)) } /// Tube DX pressure drop [Pa] in the flow direction: /// `ΔP = ΔP_friction(x̄) + ΔP_acceleration`. /// /// Friction uses MSH (NIST EVAP-COND default) or Friedel at the mean quality /// `x̄ = ½(clamp(x_in)+clamp(x_out))` over [`TubeChannelGeometry::length_m`]. /// Signed so `P_out = P_in − ΔP` for `ṁ ≥ 0`. pub fn tube_two_phase_delta_p( correlation: TwoPhaseDpCorrelation, geom: &TubeChannelGeometry, mass_flow: f64, x_in: f64, x_out: f64, props: &SatTransportProps, ) -> f64 { let area = geom.flow_area_m2().max(1e-12); let g = mass_flow.abs() / area; let x_mean = 0.5 * (x_in.clamp(0.0, 1.0) + x_out.clamp(0.0, 1.0)); let input = FriedelInput { mass_flux: g, diameter: geom.diameter_m.max(1e-9), quality: x_mean, rho_liquid: props.rho_liquid, rho_vapor: props.rho_vapor, mu_liquid: props.mu_liquid, mu_vapor: props.mu_vapor, sigma: props.sigma.max(1e-9), }; let dp_fric = correlation.pressure_drop(&input, geom.length_m.max(0.0)); let dp_acc = acceleration_drop(g, x_in, x_out, props.rho_liquid, props.rho_vapor); let drop = dp_fric + dp_acc; if mass_flow >= 0.0 { drop } else { -drop } } /// Parse `dp_model` string: `none`/`isobaric`, `quadratic`, `msh`, `friedel`. pub fn parse_dp_model_name(name: &str) -> Option<&'static str> { match name.trim().to_ascii_lowercase().as_str() { "none" | "isobaric" | "zero" => Some("isobaric"), "quadratic" | "rated" | "lumped" | "dp_nominal" => Some("quadratic"), "msh" | "muller" | "müller" | "muller-steinhagen-heck" | "muller_steinhagen_heck" => { Some("msh") } "friedel" => Some("friedel"), _ => None, } } #[cfg(test)] mod tests { use super::*; fn r134a_like() -> FriedelInput { // Representative R134a evaporating near 5 °C. FriedelInput { mass_flux: 200.0, diameter: 0.008, quality: 0.5, rho_liquid: 1260.0, rho_vapor: 17.0, mu_liquid: 250e-6, mu_vapor: 11e-6, sigma: 0.011, } } #[test] fn multiplier_is_one_at_zero_quality() { let mut inp = r134a_like(); inp.quality = 0.0; assert!((friedel_multiplier(&inp) - 1.0).abs() < 1e-12); } #[test] fn multiplier_exceeds_one_in_two_phase() { // Two-phase acceleration of the vapor makes φ_LO² > 1. let inp = r134a_like(); assert!(friedel_multiplier(&inp) > 1.0); } #[test] fn gradient_increases_with_mass_flux() { let low = FriedelInput { mass_flux: 100.0, ..r134a_like() }; let high = FriedelInput { mass_flux: 400.0, ..r134a_like() }; assert!(friedel_gradient(&high) > friedel_gradient(&low)); } #[test] fn gradient_is_finite_and_positive_across_quality() { for q in [0.05, 0.2, 0.4, 0.6, 0.8, 0.95] { let inp = FriedelInput { quality: q, ..r134a_like() }; let g = friedel_gradient(&inp); assert!(g.is_finite() && g > 0.0, "q={q} gradient={g}"); } } #[test] fn friedel_pressure_drop_reference_magnitude() { // Sanity band: a 2 m, 8 mm R134a tube at G=200, x=0.5 gives a two-phase // drop of order a few kPa (physically plausible for this duty). let dp = friedel_pressure_drop(&r134a_like(), 2.0); assert!( dp > 500.0 && dp < 50_000.0, "dp={dp} Pa out of expected band" ); } #[test] fn zivi_void_fraction_bounds_and_monotonic() { assert_eq!(zivi_void_fraction(0.0, 1260.0, 17.0), 0.0); assert_eq!(zivi_void_fraction(1.0, 1260.0, 17.0), 1.0); let a_low = zivi_void_fraction(0.1, 1260.0, 17.0); let a_high = zivi_void_fraction(0.9, 1260.0, 17.0); assert!(a_low > 0.0 && a_low < 1.0); assert!(a_high > a_low); // Even at low quality the void fraction is high (vapor occupies most area). assert!(a_low > 0.5, "Zivi void fraction unexpectedly low: {a_low}"); } #[test] fn quadratic_drop_and_derivative() { let k_default = default_refrigerant_pressure_drop_coeff(); assert!((k_default - 6.0e6).abs() < 1.0, "15 kPa @ 0.05 kg/s → k=6e6"); let props = SatTransportProps { rho_liquid: 1260.0, rho_vapor: 17.0, mu_liquid: 250e-6, mu_vapor: 11e-6, sigma: 0.011, }; let geom = TubeChannelGeometry::dx_default(); let dp_evap = tube_two_phase_delta_p( TwoPhaseDpCorrelation::MullerSteinhagenHeck1986, &geom, 0.05, 0.2, 0.95, &props, ); assert!(dp_evap > 1000.0, "DX evaporating ΔP should be kPa-scale, got {dp_evap}"); let dp_cond = tube_two_phase_delta_p( TwoPhaseDpCorrelation::MullerSteinhagenHeck1986, &geom, 0.05, 0.95, 0.05, &props, ); // Condensation: acceleration recovers some pressure → ΔP_cond < ΔP_evap typically. assert!(dp_cond > 0.0 && dp_cond < dp_evap); let k = calibrate_quadratic_k(20_000.0, 0.1); // 20 kPa at 0.1 kg/s assert!((quadratic_drop(k, 0.1) - 20_000.0).abs() < 1e-6); // Antisymmetric. assert!((quadratic_drop(k, -0.1) + 20_000.0).abs() < 1e-6); // Derivative matches central finite difference. let m = 0.07; let d_fd = (quadratic_drop(k, m + 1e-6) - quadratic_drop(k, m - 1e-6)) / 2e-6; assert!((quadratic_drop_dm(k, m) - d_fd).abs() < 1e-3); } #[test] fn calibrate_handles_zero_flow() { assert_eq!(calibrate_quadratic_k(1000.0, 0.0), 0.0); } #[test] fn friedel_domain_assessment_is_separate_from_formula() { let assessment = assess_friedel_domain( &r134a_like(), ExchangerGeometryType::SmoothTube, FlowRegime::Evaporation, Some("R134a"), ) .unwrap(); assert!(assessment.accepted); assert_eq!(assessment.id, CorrelationId::Friedel1979); assert_eq!( assessment.domain_status, Some(super::super::DomainStatus::InDomain) ); } #[test] fn friedel_rejects_wrong_geometry_structurally() { let assessment = assess_friedel_domain( &r134a_like(), ExchangerGeometryType::BrazedPlate, FlowRegime::Evaporation, Some("R134a"), ) .unwrap(); assert!(!assessment.accepted); assert!(matches!( assessment.rejections.as_slice(), [super::super::CandidateRejection::WrongGeometry { .. }] )); assert!(assessment.domain_status.is_none()); } #[test] fn friedel_assessment_rejects_invalid_physical_input() { let mut input = r134a_like(); input.sigma = 0.0; let error = assess_friedel_domain( &input, ExchangerGeometryType::SmoothTube, FlowRegime::Evaporation, Some("R134a"), ) .unwrap_err(); assert!(matches!( error, DomainInputError::InvalidPositive { field: "surface_tension", .. } )); } #[test] fn msh_matches_liquid_only_at_x0() { let mut inp = r134a_like(); inp.quality = 0.0; let a = liquid_only_gradient( inp.mass_flux, inp.diameter, inp.rho_liquid, inp.mu_liquid, ); assert!((msh_gradient(&inp) - a).abs() < 1e-9); } #[test] fn msh_matches_vapor_only_at_x1() { let mut inp = r134a_like(); inp.quality = 1.0; let b = vapor_only_gradient(inp.mass_flux, inp.diameter, inp.rho_vapor, inp.mu_vapor); assert!((msh_gradient(&inp) - b).abs() < 1e-9); } #[test] fn msh_positive_across_quality() { for q in [0.05, 0.3, 0.5, 0.7, 0.95] { let inp = FriedelInput { quality: q, ..r134a_like() }; let g = msh_gradient(&inp); assert!(g.is_finite() && g > 0.0, "q={q} g={g}"); } } #[test] fn two_phase_dp_enum_dispatches() { let inp = r134a_like(); let friedel = TwoPhaseDpCorrelation::Friedel1979.gradient(&inp); let msh = TwoPhaseDpCorrelation::MullerSteinhagenHeck1986.gradient(&inp); assert!(friedel.is_finite() && msh.is_finite()); assert!(friedel > 0.0 && msh > 0.0); } #[test] fn msh_domain_accepted_for_smooth_tube() { let assessment = assess_msh_domain( &r134a_like(), ExchangerGeometryType::SmoothTube, FlowRegime::Condensation, Some("R134a"), ) .unwrap(); assert!(assessment.accepted); assert_eq!(assessment.id, CorrelationId::MullerSteinhagenHeck1986); } }