Add diagram workbench UI with Modelica DoF coaching and ISO glyphs.

Ship the Next.js cycle editor with CAD chrome, technical HX symbols, Fixed/Free boundary guidance, and secondary water/air pressure drop support in the solver stack.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-17 22:46:46 +02:00
parent 62efea0646
commit 3358b74342
275 changed files with 70187 additions and 5230 deletions

View File

@@ -0,0 +1,718 @@
//! 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<CandidateAssessment, DomainInputError> {
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(BA)x` at low x, then smooth to vapor-only at x→1 via
/// `(dP/dz) = (A + 2(BA)x)·(1x)^(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<CandidateAssessment, DomainInputError> {
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 + (1x)/ρ_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);
}
}