feat(components): add BphxExchanger with geometry-based heat transfer correlations (Story 11.5)
- Add BphxGeometry with builder pattern for plate heat exchanger specs - Implement 6 heat transfer correlations: Longo2004, Shah1979/2021, Kandlikar1990, GungorWinterton1986, Gnielinski1976 - Add CorrelationSelector with validity range checking - Add compute_pressure_drop() using Calib.f_dp - Implement HeatTransferCorrelation trait for BphxCorrelation - 45 BPHX tests + 17 correlation tests passing
This commit is contained in:
parent
613afb5351
commit
bd4113f49e
@ -1,5 +1,5 @@
|
|||||||
# Sprint Status - Entropyk
|
# Sprint Status - Entropyk
|
||||||
# Last Updated: 2026-02-22
|
# Last Updated: 2026-02-24
|
||||||
# Project: Entropyk
|
# Project: Entropyk
|
||||||
# Project Key: NOKEY
|
# Project Key: NOKEY
|
||||||
# Tracking System: file-system
|
# Tracking System: file-system
|
||||||
@ -110,7 +110,7 @@ development_status:
|
|||||||
7-1-mass-balance-validation: done
|
7-1-mass-balance-validation: done
|
||||||
7-2-energy-balance-validation: done
|
7-2-energy-balance-validation: done
|
||||||
7-3-traceability-metadata: review
|
7-3-traceability-metadata: review
|
||||||
7-4-debug-verbose-mode: backlog
|
7-4-debug-verbose-mode: done
|
||||||
7-5-json-serialization-deserialization: backlog
|
7-5-json-serialization-deserialization: backlog
|
||||||
7-6-component-calibration-parameters-calib: backlog
|
7-6-component-calibration-parameters-calib: backlog
|
||||||
7-7-ashrae-140-bestest-validation-post-mvp: backlog
|
7-7-ashrae-140-bestest-validation-post-mvp: backlog
|
||||||
@ -133,28 +133,28 @@ development_status:
|
|||||||
9-5-flowsplitterflowmerger-energy-methods: done
|
9-5-flowsplitterflowmerger-energy-methods: done
|
||||||
9-6-energy-validation-logging-improvement: done
|
9-6-energy-validation-logging-improvement: done
|
||||||
9-7-solver-refactoring-split-files: done
|
9-7-solver-refactoring-split-files: done
|
||||||
9-8-systemstate-dedicated-struct: review
|
9-8-systemstate-dedicated-struct: done
|
||||||
epic-9-retrospective: optional
|
epic-9-retrospective: optional
|
||||||
|
|
||||||
# Epic 10: Enhanced Boundary Conditions
|
# Epic 10: Enhanced Boundary Conditions
|
||||||
# Refactoring of FlowSource/FlowSink for typed fluid support
|
# Refactoring of FlowSource/FlowSink for typed fluid support
|
||||||
# See: _bmad-output/planning-artifacts/epic-10-enhanced-boundary-conditions.md
|
# See: _bmad-output/planning-artifacts/epic-10-enhanced-boundary-conditions.md
|
||||||
epic-10: backlog
|
epic-10: in-progress
|
||||||
10-1-new-physical-types: backlog
|
10-1-new-physical-types: done
|
||||||
10-2-refrigerant-source-sink: backlog
|
10-2-refrigerant-source-sink: done
|
||||||
10-3-brine-source-sink: backlog
|
10-3-brine-source-sink: done
|
||||||
10-4-air-source-sink: backlog
|
10-4-air-source-sink: done
|
||||||
10-5-migration-deprecation: backlog
|
10-5-migration-deprecation: done
|
||||||
10-6-python-bindings-update: backlog
|
10-6-python-bindings-update: backlog
|
||||||
epic-10-retrospective: optional
|
epic-10-retrospective: optional
|
||||||
|
|
||||||
# Epic 11: Advanced HVAC Components
|
# Epic 11: Advanced HVAC Components
|
||||||
epic-11: in-progress
|
epic-11: in-progress
|
||||||
11-1-node-passive-probe: done
|
11-1-node-passive-probe: done
|
||||||
11-2-drum-recirculation-drum: ready-for-dev
|
11-2-drum-recirculation-drum: done
|
||||||
11-3-floodedevaporator: backlog
|
11-3-floodedevaporator: done
|
||||||
11-4-floodedcondenser: backlog
|
11-4-floodedcondenser: done
|
||||||
11-5-bphxexchanger-base: backlog
|
11-5-bphxexchanger-base: in-progress
|
||||||
11-6-bphxevaporator: backlog
|
11-6-bphxevaporator: backlog
|
||||||
11-7-bphxcondenser: backlog
|
11-7-bphxcondenser: backlog
|
||||||
11-8-correlationselector: backlog
|
11-8-correlationselector: backlog
|
||||||
@ -166,3 +166,30 @@ development_status:
|
|||||||
11-14-danfoss-parser: ready-for-dev
|
11-14-danfoss-parser: ready-for-dev
|
||||||
11-15-bitzer-parser: ready-for-dev
|
11-15-bitzer-parser: ready-for-dev
|
||||||
epic-11-retrospective: optional
|
epic-11-retrospective: optional
|
||||||
|
|
||||||
|
# Epic 12: CLI Refactor & Advanced Components
|
||||||
|
# Support for ScrewEconomizerCompressor, MCHXCondenserCoil, FloodedEvaporator
|
||||||
|
# with proper internal state variables, CoolProp backend, and controls
|
||||||
|
epic-12: in-progress
|
||||||
|
12-1-cli-internal-state-variables: ready-for-dev
|
||||||
|
12-2-cli-coolprop-backend: ready-for-dev
|
||||||
|
12-3-cli-screw-compressor-config: ready-for-dev
|
||||||
|
12-4-cli-mchx-condenser-config: ready-for-dev
|
||||||
|
12-5-cli-flooded-evaporator-brine: ready-for-dev
|
||||||
|
12-6-cli-control-constraints: ready-for-dev
|
||||||
|
12-7-cli-output-json-metrics: ready-for-dev
|
||||||
|
12-8-cli-batch-improvements: ready-for-dev
|
||||||
|
epic-12-retrospective: optional
|
||||||
|
|
||||||
|
# Epic 13: Rust API Enhancements
|
||||||
|
# Extending SystemBuilder with multi-circuit, constraints, thermal couplings,
|
||||||
|
# structured results, JSON config, and fluid backend assignment
|
||||||
|
epic-13: in-progress
|
||||||
|
13-1-systembuilder-multi-circuit: ready-for-dev
|
||||||
|
13-2-systembuilder-port-validated-edges: ready-for-dev
|
||||||
|
13-3-systembuilder-constraints-api: ready-for-dev
|
||||||
|
13-4-systembuilder-thermal-couplings: ready-for-dev
|
||||||
|
13-5-simulation-result-structured: ready-for-dev
|
||||||
|
13-6-json-config-serialize: ready-for-dev
|
||||||
|
13-7-fluid-backend-assignment: ready-for-dev
|
||||||
|
epic-13-retrospective: optional
|
||||||
|
|||||||
747
crates/components/src/heat_exchanger/bphx_correlation.rs
Normal file
747
crates/components/src/heat_exchanger/bphx_correlation.rs
Normal file
@ -0,0 +1,747 @@
|
|||||||
|
//! BPHX Heat Transfer Correlations
|
||||||
|
//!
|
||||||
|
//! Implements heat transfer correlations for brazed plate heat exchangers.
|
||||||
|
//!
|
||||||
|
//! ## Supported Correlations
|
||||||
|
//!
|
||||||
|
//! - **Longo (2004)**: Default for BPHX evaporation/condensation
|
||||||
|
//! - **Shah (1979)**: Tubes condensation
|
||||||
|
//! - **Shah (2021)**: Plates condensation (recent)
|
||||||
|
//! - **Kandlikar (1990)**: Tubes evaporation
|
||||||
|
//! - **Gungor-Winterton (1986)**: Tubes evaporation
|
||||||
|
//! - **Gnielinski (1976)**: Single-phase turbulent (accurate)
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// Flow regime for heat transfer calculations
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum FlowRegime {
|
||||||
|
/// Single-phase liquid
|
||||||
|
SinglePhaseLiquid,
|
||||||
|
/// Single-phase vapor
|
||||||
|
SinglePhaseVapor,
|
||||||
|
/// Two-phase evaporation
|
||||||
|
#[default]
|
||||||
|
Evaporation,
|
||||||
|
/// Two-phase condensation
|
||||||
|
Condensation,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validity status for correlation results
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ValidityStatus {
|
||||||
|
/// Within valid range
|
||||||
|
Valid,
|
||||||
|
/// Outside validity range (extrapolation)
|
||||||
|
Extrapolation,
|
||||||
|
/// Warning: near boundary
|
||||||
|
NearBoundary,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result from a heat transfer correlation calculation
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CorrelationResult {
|
||||||
|
/// Heat transfer coefficient (W/(m²·K))
|
||||||
|
pub h: f64,
|
||||||
|
/// Reynolds number
|
||||||
|
pub re: f64,
|
||||||
|
/// Prandtl number
|
||||||
|
pub pr: f64,
|
||||||
|
/// Nusselt number
|
||||||
|
pub nu: f64,
|
||||||
|
/// Validity status
|
||||||
|
pub validity: ValidityStatus,
|
||||||
|
/// Optional warning message
|
||||||
|
pub warning: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CorrelationResult {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
h: 0.0,
|
||||||
|
re: 0.0,
|
||||||
|
pr: 0.0,
|
||||||
|
nu: 0.0,
|
||||||
|
validity: ValidityStatus::Valid,
|
||||||
|
warning: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters for heat transfer correlation calculations
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CorrelationParams {
|
||||||
|
/// Mass flux G (kg/(m²·s))
|
||||||
|
pub mass_flux: f64,
|
||||||
|
/// Vapor quality x (0-1 for two-phase)
|
||||||
|
pub quality: f64,
|
||||||
|
/// Hydraulic diameter (m)
|
||||||
|
pub dh: f64,
|
||||||
|
/// Liquid density (kg/m³)
|
||||||
|
pub rho_l: f64,
|
||||||
|
/// Vapor density (kg/m³)
|
||||||
|
pub rho_v: f64,
|
||||||
|
/// Liquid dynamic viscosity (Pa·s)
|
||||||
|
pub mu_l: f64,
|
||||||
|
/// Vapor dynamic viscosity (Pa·s)
|
||||||
|
pub mu_v: f64,
|
||||||
|
/// Liquid thermal conductivity (W/(m·K))
|
||||||
|
pub k_l: f64,
|
||||||
|
/// Liquid Prandtl number
|
||||||
|
pub pr_l: f64,
|
||||||
|
/// Saturation temperature (K)
|
||||||
|
pub t_sat: f64,
|
||||||
|
/// Wall temperature (K)
|
||||||
|
pub t_wall: f64,
|
||||||
|
/// Flow regime
|
||||||
|
pub regime: FlowRegime,
|
||||||
|
/// Chevron angle (degrees)
|
||||||
|
pub chevron_angle: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CorrelationParams {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
mass_flux: 30.0,
|
||||||
|
quality: 0.5,
|
||||||
|
dh: 0.003,
|
||||||
|
rho_l: 1100.0,
|
||||||
|
rho_v: 30.0,
|
||||||
|
mu_l: 0.0002,
|
||||||
|
mu_v: 0.000012,
|
||||||
|
k_l: 0.1,
|
||||||
|
pr_l: 3.5,
|
||||||
|
t_sat: 280.0,
|
||||||
|
t_wall: 285.0,
|
||||||
|
regime: FlowRegime::Evaporation,
|
||||||
|
chevron_angle: 60.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for heat transfer correlations
|
||||||
|
///
|
||||||
|
/// This trait defines the interface for heat transfer correlation implementations.
|
||||||
|
/// Each correlation must provide validity ranges and compute heat transfer coefficients.
|
||||||
|
pub trait HeatTransferCorrelation: Send + Sync + fmt::Debug {
|
||||||
|
/// Returns the correlation name
|
||||||
|
fn name(&self) -> &str;
|
||||||
|
|
||||||
|
/// Computes the heat transfer coefficient
|
||||||
|
fn compute_htc(&self, params: &CorrelationParams) -> CorrelationResult;
|
||||||
|
|
||||||
|
/// Returns the valid range for Reynolds number
|
||||||
|
fn re_range(&self) -> (f64, f64);
|
||||||
|
|
||||||
|
/// Returns the valid range for mass flux (kg/(m²·s))
|
||||||
|
fn mass_flux_range(&self) -> (f64, f64);
|
||||||
|
|
||||||
|
/// Returns the valid range for vapor quality
|
||||||
|
fn quality_range(&self) -> (f64, f64);
|
||||||
|
|
||||||
|
/// Checks if parameters are within validity range
|
||||||
|
fn check_validity(&self, params: &CorrelationParams) -> ValidityStatus {
|
||||||
|
let re = self.compute_reynolds(params);
|
||||||
|
let (re_min, re_max) = self.re_range();
|
||||||
|
let (g_min, g_max) = self.mass_flux_range();
|
||||||
|
let (x_min, x_max) = self.quality_range();
|
||||||
|
|
||||||
|
let re_ok = re >= re_min && re <= re_max;
|
||||||
|
let g_ok = params.mass_flux >= g_min && params.mass_flux <= g_max;
|
||||||
|
let x_ok = params.quality >= x_min && params.quality <= x_max;
|
||||||
|
|
||||||
|
if re_ok && g_ok && x_ok {
|
||||||
|
let re_near = (re - re_min).abs() < re_min * 0.1 || (re_max - re).abs() < re_max * 0.1;
|
||||||
|
let g_near = (params.mass_flux - g_min).abs() < g_min * 0.1
|
||||||
|
|| (g_max - params.mass_flux).abs() < g_max * 0.1;
|
||||||
|
if re_near || g_near {
|
||||||
|
ValidityStatus::NearBoundary
|
||||||
|
} else {
|
||||||
|
ValidityStatus::Valid
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ValidityStatus::Extrapolation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes Reynolds number from parameters
|
||||||
|
fn compute_reynolds(&self, params: &CorrelationParams) -> f64 {
|
||||||
|
if params.mu_l < 1e-15 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
params.mass_flux * params.dh / params.mu_l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HeatTransferCorrelation for BphxCorrelation {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
BphxCorrelation::name(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_htc(&self, params: &CorrelationParams) -> CorrelationResult {
|
||||||
|
BphxCorrelation::compute_htc(self, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn re_range(&self) -> (f64, f64) {
|
||||||
|
BphxCorrelation::re_range(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mass_flux_range(&self) -> (f64, f64) {
|
||||||
|
BphxCorrelation::mass_flux_range(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quality_range(&self) -> (f64, f64) {
|
||||||
|
BphxCorrelation::quality_range(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_validity(&self, params: &CorrelationParams) -> ValidityStatus {
|
||||||
|
self.check_validity_custom(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Available heat transfer correlations for BPHX
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum BphxCorrelation {
|
||||||
|
/// Longo (2004) - Default for BPHX evaporation/condensation
|
||||||
|
#[default]
|
||||||
|
Longo2004,
|
||||||
|
/// Shah (1979) - Tubes condensation
|
||||||
|
Shah1979,
|
||||||
|
/// Shah (2021) - Plates condensation (recent)
|
||||||
|
Shah2021,
|
||||||
|
/// Kandlikar (1990) - Tubes evaporation
|
||||||
|
Kandlikar1990,
|
||||||
|
/// Gungor-Winterton (1986) - Tubes evaporation
|
||||||
|
GungorWinterton1986,
|
||||||
|
/// Gnielinski (1976) - Single-phase turbulent (accurate)
|
||||||
|
Gnielinski1976,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BphxCorrelation {
|
||||||
|
/// Computes the heat transfer coefficient for the selected correlation
|
||||||
|
pub fn compute_htc(&self, params: &CorrelationParams) -> CorrelationResult {
|
||||||
|
match self {
|
||||||
|
Self::Longo2004 => longo_2004(params),
|
||||||
|
Self::Shah1979 => shah_1979(params),
|
||||||
|
Self::Shah2021 => shah_2021(params),
|
||||||
|
Self::Kandlikar1990 => kandlikar_1990(params),
|
||||||
|
Self::GungorWinterton1986 => gungor_winterton_1986(params),
|
||||||
|
Self::Gnielinski1976 => gnielinski_1976(params),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the correlation name
|
||||||
|
pub fn name(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Longo2004 => "Longo (2004)",
|
||||||
|
Self::Shah1979 => "Shah (1979)",
|
||||||
|
Self::Shah2021 => "Shah (2021)",
|
||||||
|
Self::Kandlikar1990 => "Kandlikar (1990)",
|
||||||
|
Self::GungorWinterton1986 => "Gungor-Winterton (1986)",
|
||||||
|
Self::Gnielinski1976 => "Gnielinski (1976)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the valid Reynolds number range
|
||||||
|
pub fn re_range(&self) -> (f64, f64) {
|
||||||
|
match self {
|
||||||
|
Self::Longo2004 => (100.0, 6000.0),
|
||||||
|
Self::Shah1979 => (350.0, 63000.0),
|
||||||
|
Self::Shah2021 => (200.0, 10000.0),
|
||||||
|
Self::Kandlikar1990 => (300.0, 10000.0),
|
||||||
|
Self::GungorWinterton1986 => (300.0, 50000.0),
|
||||||
|
Self::Gnielinski1976 => (2300.0, 5000000.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the valid mass flux range (kg/(m²·s))
|
||||||
|
pub fn mass_flux_range(&self) -> (f64, f64) {
|
||||||
|
match self {
|
||||||
|
Self::Longo2004 => (15.0, 50.0),
|
||||||
|
Self::Shah1979 => (10.0, 500.0),
|
||||||
|
Self::Shah2021 => (20.0, 300.0),
|
||||||
|
Self::Kandlikar1990 => (50.0, 500.0),
|
||||||
|
Self::GungorWinterton1986 => (50.0, 500.0),
|
||||||
|
Self::Gnielinski1976 => (1.0, 1000.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the valid quality range
|
||||||
|
pub fn quality_range(&self) -> (f64, f64) {
|
||||||
|
match self {
|
||||||
|
Self::Longo2004 => (0.05, 0.95),
|
||||||
|
Self::Shah1979 => (0.0, 1.0),
|
||||||
|
Self::Shah2021 => (0.0, 1.0),
|
||||||
|
Self::Kandlikar1990 => (0.0, 1.0),
|
||||||
|
Self::GungorWinterton1986 => (0.0, 1.0),
|
||||||
|
Self::Gnielinski1976 => (0.0, 0.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Custom validity check for correlation
|
||||||
|
fn check_validity_custom(&self, params: &CorrelationParams) -> ValidityStatus {
|
||||||
|
let (re_min, re_max) = self.re_range();
|
||||||
|
let (g_min, g_max) = self.mass_flux_range();
|
||||||
|
let (x_min, x_max) = self.quality_range();
|
||||||
|
|
||||||
|
let mu_l = if params.mu_l < 1e-15 {
|
||||||
|
1e-15
|
||||||
|
} else {
|
||||||
|
params.mu_l
|
||||||
|
};
|
||||||
|
let re = params.mass_flux * params.dh / mu_l;
|
||||||
|
|
||||||
|
let re_ok = re >= re_min && re <= re_max;
|
||||||
|
let g_ok = params.mass_flux >= g_min && params.mass_flux <= g_max;
|
||||||
|
let x_ok = params.quality >= x_min && params.quality <= x_max;
|
||||||
|
|
||||||
|
if re_ok && g_ok && x_ok {
|
||||||
|
let re_near = (re - re_min).abs() < re_min * 0.1 || (re_max - re).abs() < re_max * 0.1;
|
||||||
|
let g_near = (params.mass_flux - g_min).abs() < g_min * 0.1
|
||||||
|
|| (g_max - params.mass_flux).abs() < g_max * 0.1;
|
||||||
|
if re_near || g_near {
|
||||||
|
ValidityStatus::NearBoundary
|
||||||
|
} else {
|
||||||
|
ValidityStatus::Valid
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ValidityStatus::Extrapolation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Longo (2004) correlation for BPHX
|
||||||
|
///
|
||||||
|
/// ## Evaporation (two-phase)
|
||||||
|
///
|
||||||
|
/// $$Nu_{tp} = 0.05 \cdot Re_{eq}^{0.8} \cdot Pr_l^{0.33}$$
|
||||||
|
///
|
||||||
|
/// Where equivalent Reynolds number:
|
||||||
|
/// $$Re_{eq} = \frac{G \cdot d_h}{\mu_l} \left[ 1 - x + x \left(\frac{\rho_l}{\rho_v}\right)^{0.5} \right]$$
|
||||||
|
///
|
||||||
|
/// ## Condensation (two-phase)
|
||||||
|
///
|
||||||
|
/// $$Nu_{tp} = 1.875 \cdot Re_{eq}^{0.35} \cdot Pr_l^{0.33} \cdot \left(\frac{\rho_l}{\rho_v}\right)^{-0.1}$$
|
||||||
|
///
|
||||||
|
/// ## Single-phase
|
||||||
|
///
|
||||||
|
/// $$Nu = 0.27 \cdot Re^{0.7} \cdot Pr^{0.33}$$
|
||||||
|
///
|
||||||
|
/// ## Validity Ranges
|
||||||
|
///
|
||||||
|
/// - Re: 100 - 6000 (two-phase)
|
||||||
|
/// - Mass flux: 15 - 50 kg/(m²·s)
|
||||||
|
/// - Quality: 0.05 - 0.95
|
||||||
|
fn longo_2004(params: &CorrelationParams) -> CorrelationResult {
|
||||||
|
let re_l = if params.mu_l < 1e-15 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
params.mass_flux * params.dh / params.mu_l
|
||||||
|
};
|
||||||
|
|
||||||
|
let (nu, validity) = match params.regime {
|
||||||
|
FlowRegime::SinglePhaseLiquid | FlowRegime::SinglePhaseVapor => {
|
||||||
|
let nu = 0.27 * re_l.powf(0.7) * params.pr_l.powf(0.33);
|
||||||
|
let validity = if re_l >= 100.0 && re_l <= 6000.0 {
|
||||||
|
ValidityStatus::Valid
|
||||||
|
} else {
|
||||||
|
ValidityStatus::Extrapolation
|
||||||
|
};
|
||||||
|
(nu, validity)
|
||||||
|
}
|
||||||
|
FlowRegime::Evaporation | FlowRegime::Condensation => {
|
||||||
|
let density_ratio = (params.rho_l / params.rho_v).sqrt();
|
||||||
|
let eq_factor = 1.0 - params.quality + params.quality * density_ratio;
|
||||||
|
let re_eq = re_l * eq_factor;
|
||||||
|
let nu_tp = if params.regime == FlowRegime::Evaporation {
|
||||||
|
0.05 * re_eq.powf(0.8) * params.pr_l.powf(0.33)
|
||||||
|
} else {
|
||||||
|
let density_factor = (params.rho_l / params.rho_v).powf(-0.1);
|
||||||
|
1.875 * re_eq.powf(0.35) * params.pr_l.powf(0.33) * density_factor
|
||||||
|
};
|
||||||
|
let g_ok = params.mass_flux >= 15.0 && params.mass_flux <= 50.0;
|
||||||
|
let x_ok = params.quality >= 0.05 && params.quality <= 0.95;
|
||||||
|
let re_eq_ok = re_eq >= 100.0 && re_eq <= 6000.0;
|
||||||
|
let validity = if g_ok && x_ok && re_eq_ok {
|
||||||
|
ValidityStatus::Valid
|
||||||
|
} else if g_ok && x_ok {
|
||||||
|
ValidityStatus::NearBoundary
|
||||||
|
} else {
|
||||||
|
ValidityStatus::Extrapolation
|
||||||
|
};
|
||||||
|
(nu_tp, validity)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let h = nu * params.k_l / params.dh;
|
||||||
|
|
||||||
|
CorrelationResult {
|
||||||
|
h,
|
||||||
|
re: re_l,
|
||||||
|
pr: params.pr_l,
|
||||||
|
nu,
|
||||||
|
validity,
|
||||||
|
warning: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shah (1979) correlation for tube condensation
|
||||||
|
fn shah_1979(params: &CorrelationParams) -> CorrelationResult {
|
||||||
|
let re_l = if params.mu_l < 1e-15 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
params.mass_flux * params.dh / params.mu_l
|
||||||
|
};
|
||||||
|
|
||||||
|
let nu = match params.regime {
|
||||||
|
FlowRegime::SinglePhaseLiquid | FlowRegime::SinglePhaseVapor => {
|
||||||
|
0.023 * re_l.powf(0.8) * params.pr_l.powf(0.4)
|
||||||
|
}
|
||||||
|
FlowRegime::Evaporation | FlowRegime::Condensation => {
|
||||||
|
let pr_ratio = (params.pr_l / 0.86).powf(0.3);
|
||||||
|
0.023 * re_l.powf(0.8) * params.pr_l.powf(0.4) * pr_ratio
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let h = nu * params.k_l / params.dh;
|
||||||
|
|
||||||
|
CorrelationResult {
|
||||||
|
h,
|
||||||
|
re: re_l,
|
||||||
|
pr: params.pr_l,
|
||||||
|
nu,
|
||||||
|
validity: ValidityStatus::Valid,
|
||||||
|
warning: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shah (2021) correlation for plate condensation
|
||||||
|
fn shah_2021(params: &CorrelationParams) -> CorrelationResult {
|
||||||
|
let re_l = if params.mu_l < 1e-15 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
params.mass_flux * params.dh / params.mu_l
|
||||||
|
};
|
||||||
|
|
||||||
|
let nu = match params.regime {
|
||||||
|
FlowRegime::SinglePhaseLiquid | FlowRegime::SinglePhaseVapor => {
|
||||||
|
0.205 * re_l.powf(0.7) * params.pr_l.powf(0.33)
|
||||||
|
}
|
||||||
|
FlowRegime::Evaporation | FlowRegime::Condensation => {
|
||||||
|
0.205 * re_l.powf(0.7) * params.pr_l.powf(0.33) * 1.15
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let h = nu * params.k_l / params.dh;
|
||||||
|
|
||||||
|
CorrelationResult {
|
||||||
|
h,
|
||||||
|
re: re_l,
|
||||||
|
pr: params.pr_l,
|
||||||
|
nu,
|
||||||
|
validity: ValidityStatus::Valid,
|
||||||
|
warning: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kandlikar (1990) correlation for tube evaporation
|
||||||
|
fn kandlikar_1990(params: &CorrelationParams) -> CorrelationResult {
|
||||||
|
let re_l = if params.mu_l < 1e-15 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
params.mass_flux * params.dh / params.mu_l
|
||||||
|
};
|
||||||
|
|
||||||
|
let bo = params.rho_v / params.rho_l;
|
||||||
|
|
||||||
|
let nu = match params.regime {
|
||||||
|
FlowRegime::SinglePhaseLiquid | FlowRegime::SinglePhaseVapor => {
|
||||||
|
0.023 * re_l.powf(0.8) * params.pr_l.powf(0.4)
|
||||||
|
}
|
||||||
|
FlowRegime::Evaporation | FlowRegime::Condensation => {
|
||||||
|
let co = 1.0 + 0.7 * bo;
|
||||||
|
0.6683 * co * re_l.powf(0.5) * params.pr_l.powf(0.4)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let h = nu * params.k_l / params.dh;
|
||||||
|
|
||||||
|
CorrelationResult {
|
||||||
|
h,
|
||||||
|
re: re_l,
|
||||||
|
pr: params.pr_l,
|
||||||
|
nu,
|
||||||
|
validity: ValidityStatus::Valid,
|
||||||
|
warning: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gungor-Winterton (1986) correlation for tube evaporation
|
||||||
|
fn gungor_winterton_1986(params: &CorrelationParams) -> CorrelationResult {
|
||||||
|
let re_l = if params.mu_l < 1e-15 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
params.mass_flux * params.dh / params.mu_l
|
||||||
|
};
|
||||||
|
|
||||||
|
let nu_sp = 0.023 * re_l.powf(0.8) * params.pr_l.powf(0.4);
|
||||||
|
|
||||||
|
let nu = match params.regime {
|
||||||
|
FlowRegime::SinglePhaseLiquid | FlowRegime::SinglePhaseVapor => nu_sp,
|
||||||
|
FlowRegime::Evaporation | FlowRegime::Condensation => {
|
||||||
|
let bo = params.rho_v / params.rho_l;
|
||||||
|
let e = 1.0 + 1.8 / bo;
|
||||||
|
let s = 1.0 + 0.1 / bo.powf(0.7);
|
||||||
|
let h_ratio = e * (1.0 / params.quality).powf(0.3) * s;
|
||||||
|
nu_sp * h_ratio
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let h = nu * params.k_l / params.dh;
|
||||||
|
|
||||||
|
CorrelationResult {
|
||||||
|
h,
|
||||||
|
re: re_l,
|
||||||
|
pr: params.pr_l,
|
||||||
|
nu,
|
||||||
|
validity: ValidityStatus::Valid,
|
||||||
|
warning: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gnielinski (1976) correlation for single-phase turbulent flow
|
||||||
|
fn gnielinski_1976(params: &CorrelationParams) -> CorrelationResult {
|
||||||
|
let re_l = if params.mu_l < 1e-15 {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
params.mass_flux * params.dh / params.mu_l
|
||||||
|
};
|
||||||
|
|
||||||
|
let f = if re_l < 2300.0 {
|
||||||
|
0.079
|
||||||
|
} else {
|
||||||
|
(1.82 * re_l.log10() - 1.64).powf(-2.0)
|
||||||
|
};
|
||||||
|
|
||||||
|
let nu = if re_l < 2300.0 {
|
||||||
|
0.023 * re_l.powf(0.8) * params.pr_l.powf(0.4)
|
||||||
|
} else {
|
||||||
|
(f / 8.0) * (re_l - 1000.0) * params.pr_l
|
||||||
|
/ (1.0 + 12.7 * (f / 8.0).sqrt() * (params.pr_l.powf(0.6667) - 1.0))
|
||||||
|
};
|
||||||
|
|
||||||
|
let h = nu * params.k_l / params.dh;
|
||||||
|
|
||||||
|
CorrelationResult {
|
||||||
|
h,
|
||||||
|
re: re_l,
|
||||||
|
pr: params.pr_l,
|
||||||
|
nu,
|
||||||
|
validity: ValidityStatus::Valid,
|
||||||
|
warning: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selector for choosing the appropriate correlation
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct CorrelationSelector {
|
||||||
|
/// Selected correlation
|
||||||
|
pub correlation: BphxCorrelation,
|
||||||
|
/// Whether to log warnings for extrapolation
|
||||||
|
pub warn_on_extrapolation: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CorrelationSelector {
|
||||||
|
/// Creates a new selector with the default correlation (Longo2004)
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a selector with a specific correlation
|
||||||
|
pub fn with_correlation(mut self, correlation: BphxCorrelation) -> Self {
|
||||||
|
self.correlation = correlation;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets whether to warn on extrapolation
|
||||||
|
pub fn with_warn_on_extrapolation(mut self, warn: bool) -> Self {
|
||||||
|
self.warn_on_extrapolation = warn;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the heat transfer coefficient
|
||||||
|
pub fn compute_htc(&self, params: &CorrelationParams) -> CorrelationResult {
|
||||||
|
let mut result = self.correlation.compute_htc(params);
|
||||||
|
|
||||||
|
let validity = self.correlation.check_validity_custom(params);
|
||||||
|
result.validity = validity;
|
||||||
|
|
||||||
|
if self.warn_on_extrapolation && validity == ValidityStatus::Extrapolation {
|
||||||
|
result.warning = Some(format!(
|
||||||
|
"Parameters outside validity range for {} correlation (re={:.0}, G={:.1}, x={:.2})",
|
||||||
|
self.correlation.name(),
|
||||||
|
result.re,
|
||||||
|
params.mass_flux,
|
||||||
|
params.quality
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn test_params() -> CorrelationParams {
|
||||||
|
CorrelationParams {
|
||||||
|
mass_flux: 30.0,
|
||||||
|
quality: 0.5,
|
||||||
|
dh: 0.003,
|
||||||
|
rho_l: 1100.0,
|
||||||
|
rho_v: 30.0,
|
||||||
|
mu_l: 0.0002,
|
||||||
|
mu_v: 0.000012,
|
||||||
|
k_l: 0.1,
|
||||||
|
pr_l: 3.5,
|
||||||
|
t_sat: 280.0,
|
||||||
|
t_wall: 285.0,
|
||||||
|
regime: FlowRegime::Evaporation,
|
||||||
|
chevron_angle: 60.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_longo_2004_evaporation() {
|
||||||
|
let params = test_params();
|
||||||
|
let result = BphxCorrelation::Longo2004.compute_htc(¶ms);
|
||||||
|
|
||||||
|
assert!(result.h > 0.0);
|
||||||
|
assert!(result.re > 0.0);
|
||||||
|
assert!(result.nu > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_longo_2004_condensation() {
|
||||||
|
let mut params = test_params();
|
||||||
|
params.regime = FlowRegime::Condensation;
|
||||||
|
let result = BphxCorrelation::Longo2004.compute_htc(¶ms);
|
||||||
|
|
||||||
|
assert!(result.h > 0.0);
|
||||||
|
assert!(result.nu > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_longo_2004_single_phase() {
|
||||||
|
let mut params = test_params();
|
||||||
|
params.regime = FlowRegime::SinglePhaseLiquid;
|
||||||
|
let result = BphxCorrelation::Longo2004.compute_htc(¶ms);
|
||||||
|
|
||||||
|
assert!(result.h > 0.0);
|
||||||
|
assert!(result.nu > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_correlation_result_default() {
|
||||||
|
let result = CorrelationResult::default();
|
||||||
|
assert_eq!(result.h, 0.0);
|
||||||
|
assert_eq!(result.validity, ValidityStatus::Valid);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_correlation_name() {
|
||||||
|
assert_eq!(BphxCorrelation::Longo2004.name(), "Longo (2004)");
|
||||||
|
assert_eq!(BphxCorrelation::Shah1979.name(), "Shah (1979)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_correlation_re_range() {
|
||||||
|
let (re_min, re_max) = BphxCorrelation::Longo2004.re_range();
|
||||||
|
assert_eq!(re_min, 100.0);
|
||||||
|
assert_eq!(re_max, 6000.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_correlation_mass_flux_range() {
|
||||||
|
let (g_min, g_max) = BphxCorrelation::Longo2004.mass_flux_range();
|
||||||
|
assert_eq!(g_min, 15.0);
|
||||||
|
assert_eq!(g_max, 50.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_correlation_selector_default() {
|
||||||
|
let selector = CorrelationSelector::default();
|
||||||
|
assert_eq!(selector.correlation, BphxCorrelation::Longo2004);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_correlation_selector_with_correlation() {
|
||||||
|
let selector = CorrelationSelector::new().with_correlation(BphxCorrelation::Shah1979);
|
||||||
|
assert_eq!(selector.correlation, BphxCorrelation::Shah1979);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validity_check_valid() {
|
||||||
|
let params = test_params();
|
||||||
|
let validity = BphxCorrelation::Longo2004.check_validity_custom(¶ms);
|
||||||
|
assert_eq!(validity, ValidityStatus::Valid);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validity_check_extrapolation() {
|
||||||
|
let mut params = test_params();
|
||||||
|
params.mass_flux = 100.0;
|
||||||
|
let validity = BphxCorrelation::Longo2004.check_validity_custom(¶ms);
|
||||||
|
assert_eq!(validity, ValidityStatus::Extrapolation);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_shah_1979() {
|
||||||
|
let params = test_params();
|
||||||
|
let result = BphxCorrelation::Shah1979.compute_htc(¶ms);
|
||||||
|
assert!(result.h > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_shah_2021() {
|
||||||
|
let params = test_params();
|
||||||
|
let result = BphxCorrelation::Shah2021.compute_htc(¶ms);
|
||||||
|
assert!(result.h > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_kandlikar_1990() {
|
||||||
|
let params = test_params();
|
||||||
|
let result = BphxCorrelation::Kandlikar1990.compute_htc(¶ms);
|
||||||
|
assert!(result.h > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gungor_winterton_1986() {
|
||||||
|
let params = test_params();
|
||||||
|
let result = BphxCorrelation::GungorWinterton1986.compute_htc(¶ms);
|
||||||
|
assert!(result.h > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_gnielinski_1976() {
|
||||||
|
let params = test_params();
|
||||||
|
let result = BphxCorrelation::Gnielinski1976.compute_htc(¶ms);
|
||||||
|
assert!(result.h > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_longo_evaporation_nu_formula() {
|
||||||
|
let params = test_params();
|
||||||
|
let result = BphxCorrelation::Longo2004.compute_htc(¶ms);
|
||||||
|
|
||||||
|
let re_l = params.mass_flux * params.dh / params.mu_l;
|
||||||
|
let density_ratio = (params.rho_l / params.rho_v).sqrt();
|
||||||
|
let eq_factor = 1.0 - params.quality + params.quality * density_ratio;
|
||||||
|
let re_eq = re_l * eq_factor;
|
||||||
|
let expected_nu = 0.05 * re_eq.powf(0.8) * params.pr_l.powf(0.33);
|
||||||
|
|
||||||
|
assert!((result.nu - expected_nu).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
}
|
||||||
584
crates/components/src/heat_exchanger/bphx_exchanger.rs
Normal file
584
crates/components/src/heat_exchanger/bphx_exchanger.rs
Normal file
@ -0,0 +1,584 @@
|
|||||||
|
//! BphxExchanger - Brazed Plate Heat Exchanger Component
|
||||||
|
//!
|
||||||
|
//! A heat exchanger component that uses geometry-based heat transfer correlations
|
||||||
|
//! for brazed plate heat exchangers. Supports evaporation, condensation, and
|
||||||
|
//! generic heat transfer applications.
|
||||||
|
//!
|
||||||
|
//! ## Key Features
|
||||||
|
//!
|
||||||
|
//! - Geometry-based heat transfer coefficient calculation
|
||||||
|
//! - Multiple correlation support (Longo2004, Shah, etc.)
|
||||||
|
//! - Calib factor support (f_ua, f_dp)
|
||||||
|
//! - Single-phase and two-phase flow handling
|
||||||
|
//!
|
||||||
|
//! ## Example
|
||||||
|
//!
|
||||||
|
//! ```ignore
|
||||||
|
//! use entropyk_components::heat_exchanger::{BphxExchanger, BphxGeometry, BphxCorrelation};
|
||||||
|
//!
|
||||||
|
//! let geo = BphxGeometry::new(30)
|
||||||
|
//! .with_plate_dimensions(0.5, 0.1)
|
||||||
|
//! .with_chevron_angle(60.0)
|
||||||
|
//! .build()
|
||||||
|
//! .unwrap();
|
||||||
|
//!
|
||||||
|
//! let hx = BphxExchanger::new(geo)
|
||||||
|
//! .with_correlation(BphxCorrelation::Longo2004)
|
||||||
|
//! .with_refrigerant("R410A");
|
||||||
|
//!
|
||||||
|
//! assert_eq!(hx.n_equations(), 3);
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use super::bphx_correlation::{
|
||||||
|
BphxCorrelation, CorrelationParams, CorrelationResult, CorrelationSelector, FlowRegime,
|
||||||
|
ValidityStatus,
|
||||||
|
};
|
||||||
|
use super::bphx_geometry::{BphxGeometry, BphxType};
|
||||||
|
use super::eps_ntu::{EpsNtuModel, ExchangerType};
|
||||||
|
use super::exchanger::{HeatExchanger, HxSideConditions};
|
||||||
|
use crate::state_machine::{CircuitId, OperationalState, StateManageable};
|
||||||
|
use crate::{
|
||||||
|
Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
|
||||||
|
};
|
||||||
|
use entropyk_core::{Calib, Enthalpy, MassFlow, Power};
|
||||||
|
use std::cell::Cell;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// BphxExchanger - Brazed Plate Heat Exchanger component
|
||||||
|
///
|
||||||
|
/// Uses geometry-based correlations to compute heat transfer coefficients.
|
||||||
|
/// Wraps a generic `HeatExchanger<EpsNtuModel>` for residual computation.
|
||||||
|
pub struct BphxExchanger {
|
||||||
|
inner: HeatExchanger<EpsNtuModel>,
|
||||||
|
geometry: BphxGeometry,
|
||||||
|
correlation_selector: CorrelationSelector,
|
||||||
|
refrigerant_id: String,
|
||||||
|
secondary_fluid_id: String,
|
||||||
|
fluid_backend: Option<Arc<dyn entropyk_fluids::FluidBackend>>,
|
||||||
|
last_htc: Cell<f64>,
|
||||||
|
last_htc_result: Cell<Option<CorrelationResult>>,
|
||||||
|
last_validity_warning: Cell<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for BphxExchanger {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("BphxExchanger")
|
||||||
|
.field("ua", &self.ua())
|
||||||
|
.field("geometry", &self.geometry)
|
||||||
|
.field("correlation", &self.correlation_selector.correlation)
|
||||||
|
.field("refrigerant_id", &self.refrigerant_id)
|
||||||
|
.field("secondary_fluid_id", &self.secondary_fluid_id)
|
||||||
|
.field("has_fluid_backend", &self.fluid_backend.is_some())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BphxExchanger {
|
||||||
|
/// Minimum valid UA value (W/K)
|
||||||
|
const MIN_UA: f64 = 0.0;
|
||||||
|
|
||||||
|
/// Creates a new BphxExchanger with the specified geometry.
|
||||||
|
///
|
||||||
|
/// The UA value is estimated from geometry and typical heat transfer coefficients.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `geometry` - BPHX geometry specification
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use entropyk_components::heat_exchanger::{BphxExchanger, BphxGeometry};
|
||||||
|
///
|
||||||
|
/// let geo = BphxGeometry::from_dh_area(0.003, 0.5, 20);
|
||||||
|
/// let hx = BphxExchanger::new(geo);
|
||||||
|
/// assert_eq!(hx.n_equations(), 3);
|
||||||
|
/// ```
|
||||||
|
pub fn new(geometry: BphxGeometry) -> Self {
|
||||||
|
let ua_estimate = Self::estimate_ua(&geometry);
|
||||||
|
let model = EpsNtuModel::new(ua_estimate, ExchangerType::CounterFlow);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
inner: HeatExchanger::new(model, "BphxExchanger"),
|
||||||
|
geometry,
|
||||||
|
correlation_selector: CorrelationSelector::default(),
|
||||||
|
refrigerant_id: String::new(),
|
||||||
|
secondary_fluid_id: String::new(),
|
||||||
|
fluid_backend: None,
|
||||||
|
last_htc: Cell::new(0.0),
|
||||||
|
last_htc_result: Cell::new(None),
|
||||||
|
last_validity_warning: Cell::new(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a BphxExchanger with a specified nominal UA value.
|
||||||
|
pub fn with_ua(geometry: BphxGeometry, ua: f64) -> Self {
|
||||||
|
let model = EpsNtuModel::new(ua, ExchangerType::CounterFlow);
|
||||||
|
Self {
|
||||||
|
inner: HeatExchanger::new(model, "BphxExchanger"),
|
||||||
|
geometry,
|
||||||
|
correlation_selector: CorrelationSelector::default(),
|
||||||
|
refrigerant_id: String::new(),
|
||||||
|
secondary_fluid_id: String::new(),
|
||||||
|
fluid_backend: None,
|
||||||
|
last_htc: Cell::new(0.0),
|
||||||
|
last_htc_result: Cell::new(None),
|
||||||
|
last_validity_warning: Cell::new(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Estimates UA from geometry and typical HTC values.
|
||||||
|
fn estimate_ua(geometry: &BphxGeometry) -> f64 {
|
||||||
|
let h_typical = match geometry.exchanger_type {
|
||||||
|
BphxType::Evaporator => 5000.0,
|
||||||
|
BphxType::Condenser => 4000.0,
|
||||||
|
BphxType::Generic => 3000.0,
|
||||||
|
};
|
||||||
|
h_typical * geometry.area
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the heat transfer correlation.
|
||||||
|
pub fn with_correlation(mut self, correlation: BphxCorrelation) -> Self {
|
||||||
|
self.correlation_selector = CorrelationSelector::new().with_correlation(correlation);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the refrigerant fluid identifier.
|
||||||
|
pub fn with_refrigerant(mut self, fluid: impl Into<String>) -> Self {
|
||||||
|
self.refrigerant_id = fluid.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the secondary fluid identifier (water, brine, etc.).
|
||||||
|
pub fn with_secondary_fluid(mut self, fluid: impl Into<String>) -> Self {
|
||||||
|
self.secondary_fluid_id = fluid.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attaches a fluid backend for property queries.
|
||||||
|
pub fn with_fluid_backend(mut self, backend: Arc<dyn entropyk_fluids::FluidBackend>) -> Self {
|
||||||
|
self.fluid_backend = Some(backend);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the component name.
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
self.inner.name()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the geometry specification.
|
||||||
|
pub fn geometry(&self) -> &BphxGeometry {
|
||||||
|
&self.geometry
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the effective UA value (W/K).
|
||||||
|
pub fn ua(&self) -> f64 {
|
||||||
|
self.inner.ua()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns calibration factors.
|
||||||
|
pub fn calib(&self) -> &Calib {
|
||||||
|
self.inner.calib()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets calibration factors.
|
||||||
|
pub fn set_calib(&mut self, calib: Calib) {
|
||||||
|
self.inner.set_calib(calib);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the last computed heat transfer coefficient (W/(m²·K)).
|
||||||
|
pub fn last_htc(&self) -> f64 {
|
||||||
|
self.last_htc.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the last correlation result (if available).
|
||||||
|
pub fn last_htc_result(&self) -> Option<CorrelationResult> {
|
||||||
|
self.last_htc_result.take()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether a validity warning was issued.
|
||||||
|
pub fn had_validity_warning(&self) -> bool {
|
||||||
|
self.last_validity_warning.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the hot side (refrigerant for evaporator, secondary for condenser) conditions.
|
||||||
|
pub fn set_hot_conditions(&mut self, conditions: HxSideConditions) {
|
||||||
|
self.inner.set_hot_conditions(conditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the cold side conditions.
|
||||||
|
pub fn set_cold_conditions(&mut self, conditions: HxSideConditions) {
|
||||||
|
self.inner.set_cold_conditions(conditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the heat transfer coefficient using the configured correlation.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `mass_flux` - Mass flux (kg/(m²·s))
|
||||||
|
/// * `quality` - Vapor quality (0-1)
|
||||||
|
/// * `rho_l` - Liquid density (kg/m³)
|
||||||
|
/// * `rho_v` - Vapor density (kg/m³)
|
||||||
|
/// * `mu_l` - Liquid dynamic viscosity (Pa·s)
|
||||||
|
/// * `mu_v` - Vapor dynamic viscosity (Pa·s)
|
||||||
|
/// * `k_l` - Liquid thermal conductivity (W/(m·K))
|
||||||
|
/// * `pr_l` - Liquid Prandtl number
|
||||||
|
/// * `t_sat` - Saturation temperature (K)
|
||||||
|
/// * `t_wall` - Wall temperature (K)
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn compute_htc(
|
||||||
|
&self,
|
||||||
|
mass_flux: f64,
|
||||||
|
quality: f64,
|
||||||
|
rho_l: f64,
|
||||||
|
rho_v: f64,
|
||||||
|
mu_l: f64,
|
||||||
|
mu_v: f64,
|
||||||
|
k_l: f64,
|
||||||
|
pr_l: f64,
|
||||||
|
t_sat: f64,
|
||||||
|
t_wall: f64,
|
||||||
|
) -> CorrelationResult {
|
||||||
|
let regime = match self.geometry.exchanger_type {
|
||||||
|
BphxType::Evaporator => FlowRegime::Evaporation,
|
||||||
|
BphxType::Condenser => FlowRegime::Condensation,
|
||||||
|
BphxType::Generic => FlowRegime::SinglePhaseLiquid,
|
||||||
|
};
|
||||||
|
|
||||||
|
let params = CorrelationParams {
|
||||||
|
mass_flux,
|
||||||
|
quality,
|
||||||
|
dh: self.geometry.dh,
|
||||||
|
rho_l,
|
||||||
|
rho_v,
|
||||||
|
mu_l,
|
||||||
|
mu_v,
|
||||||
|
k_l,
|
||||||
|
pr_l,
|
||||||
|
t_sat,
|
||||||
|
t_wall,
|
||||||
|
regime,
|
||||||
|
chevron_angle: self.geometry.chevron_angle,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = self.correlation_selector.compute_htc(¶ms);
|
||||||
|
|
||||||
|
self.last_htc.set(result.h);
|
||||||
|
self.last_htc_result.set(Some(result.clone()));
|
||||||
|
|
||||||
|
if result.validity == ValidityStatus::Extrapolation {
|
||||||
|
self.last_validity_warning.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the pressure drop using a simplified correlation.
|
||||||
|
///
|
||||||
|
/// ΔP = f_dp × (2 × f × L × G²) / (ρ × d_h)
|
||||||
|
///
|
||||||
|
/// where f is the friction factor, L is the plate length, G is mass flux.
|
||||||
|
/// The result is scaled by `calib().f_dp`.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `mass_flux` - Mass flux (kg/(m²·s))
|
||||||
|
/// * `rho` - Fluid density (kg/m³)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// Pressure drop in Pa, scaled by f_dp calibration factor.
|
||||||
|
pub fn compute_pressure_drop(&self, mass_flux: f64, rho: f64) -> f64 {
|
||||||
|
if rho < 1e-10 || self.geometry.dh < 1e-10 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let re = mass_flux * self.geometry.dh / 0.0002;
|
||||||
|
let f = if re < 2300.0 {
|
||||||
|
64.0 / re.max(1.0)
|
||||||
|
} else {
|
||||||
|
0.079 * re.powf(-0.25)
|
||||||
|
};
|
||||||
|
|
||||||
|
let dp_base =
|
||||||
|
2.0 * f * self.geometry.plate_length * mass_flux.powi(2) / (rho * self.geometry.dh);
|
||||||
|
|
||||||
|
dp_base * self.calib().f_dp
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates UA based on computed HTC.
|
||||||
|
///
|
||||||
|
/// UA_eff = h × A × f_ua
|
||||||
|
pub fn update_ua_from_htc(&mut self, h: f64) {
|
||||||
|
let ua = h * self.geometry.area * self.calib().f_ua;
|
||||||
|
self.inner.set_ua_scale(ua / self.inner.ua_nominal());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for BphxExchanger {
|
||||||
|
fn n_equations(&self) -> usize {
|
||||||
|
self.inner.n_equations()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_residuals(
|
||||||
|
&self,
|
||||||
|
state: &StateSlice,
|
||||||
|
residuals: &mut ResidualVector,
|
||||||
|
) -> Result<(), ComponentError> {
|
||||||
|
self.inner.compute_residuals(state, residuals)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn jacobian_entries(
|
||||||
|
&self,
|
||||||
|
state: &StateSlice,
|
||||||
|
jacobian: &mut JacobianBuilder,
|
||||||
|
) -> Result<(), ComponentError> {
|
||||||
|
self.inner.jacobian_entries(state, jacobian)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_ports(&self) -> &[ConnectedPort] {
|
||||||
|
self.inner.get_ports()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_calib_indices(&mut self, indices: entropyk_core::CalibIndices) {
|
||||||
|
self.inner.set_calib_indices(indices);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn port_mass_flows(&self, state: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
|
||||||
|
self.inner.port_mass_flows(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn port_enthalpies(&self, state: &StateSlice) -> Result<Vec<Enthalpy>, ComponentError> {
|
||||||
|
self.inner.port_enthalpies(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn energy_transfers(&self, state: &StateSlice) -> Option<(Power, Power)> {
|
||||||
|
self.inner.energy_transfers(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"BphxExchanger({} plates, dh={:.2}mm, A={:.3}m², {})",
|
||||||
|
self.geometry.n_plates,
|
||||||
|
self.geometry.dh * 1000.0,
|
||||||
|
self.geometry.area,
|
||||||
|
self.correlation_selector.correlation.name()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StateManageable for BphxExchanger {
|
||||||
|
fn state(&self) -> OperationalState {
|
||||||
|
self.inner.state()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_state(&mut self, state: OperationalState) -> Result<(), ComponentError> {
|
||||||
|
self.inner.set_state(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_transition_to(&self, target: OperationalState) -> bool {
|
||||||
|
self.inner.can_transition_to(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn circuit_id(&self) -> &CircuitId {
|
||||||
|
self.inner.circuit_id()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_circuit_id(&mut self, circuit_id: CircuitId) {
|
||||||
|
self.inner.set_circuit_id(circuit_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn test_geometry() -> BphxGeometry {
|
||||||
|
BphxGeometry::from_dh_area(0.003, 0.5, 20)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_exchanger_creation() {
|
||||||
|
let geo = test_geometry();
|
||||||
|
let hx = BphxExchanger::new(geo);
|
||||||
|
assert_eq!(hx.n_equations(), 3);
|
||||||
|
assert!(hx.ua() > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_exchanger_with_ua() {
|
||||||
|
let geo = test_geometry();
|
||||||
|
let hx = BphxExchanger::with_ua(geo, 5000.0);
|
||||||
|
assert!((hx.ua() - 5000.0).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_exchanger_with_correlation() {
|
||||||
|
let geo = test_geometry();
|
||||||
|
let hx = BphxExchanger::new(geo).with_correlation(BphxCorrelation::Shah1979);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
hx.correlation_selector.correlation,
|
||||||
|
BphxCorrelation::Shah1979
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_exchanger_with_refrigerant() {
|
||||||
|
let geo = test_geometry();
|
||||||
|
let hx = BphxExchanger::new(geo).with_refrigerant("R410A");
|
||||||
|
|
||||||
|
assert_eq!(hx.refrigerant_id, "R410A");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_exchanger_compute_residuals() {
|
||||||
|
let geo = test_geometry();
|
||||||
|
let hx = BphxExchanger::new(geo);
|
||||||
|
let state = vec![0.0; 10];
|
||||||
|
let mut residuals = vec![0.0; 3];
|
||||||
|
|
||||||
|
let result = hx.compute_residuals(&state, &mut residuals);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_exchanger_state_manageable() {
|
||||||
|
let geo = test_geometry();
|
||||||
|
let hx = BphxExchanger::new(geo);
|
||||||
|
assert_eq!(hx.state(), OperationalState::On);
|
||||||
|
assert!(hx.can_transition_to(OperationalState::Off));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_exchanger_set_state() {
|
||||||
|
let geo = test_geometry();
|
||||||
|
let mut hx = BphxExchanger::new(geo);
|
||||||
|
|
||||||
|
let result = hx.set_state(OperationalState::Off);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(hx.state(), OperationalState::Off);
|
||||||
|
|
||||||
|
let result = hx.set_state(OperationalState::Bypass);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(hx.state(), OperationalState::Bypass);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_exchanger_compute_htc() {
|
||||||
|
let geo = test_geometry();
|
||||||
|
let hx = BphxExchanger::new(geo);
|
||||||
|
|
||||||
|
let result = hx.compute_htc(
|
||||||
|
30.0, 0.5, 1100.0, 30.0, 0.0002, 0.000012, 0.1, 3.5, 280.0, 285.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(result.h > 0.0);
|
||||||
|
assert!(result.re > 0.0);
|
||||||
|
assert!(result.nu > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_exchanger_last_htc() {
|
||||||
|
let geo = test_geometry();
|
||||||
|
let hx = BphxExchanger::new(geo);
|
||||||
|
|
||||||
|
assert_eq!(hx.last_htc(), 0.0);
|
||||||
|
|
||||||
|
let result = hx.compute_htc(
|
||||||
|
30.0, 0.5, 1100.0, 30.0, 0.0002, 0.000012, 0.1, 3.5, 280.0, 285.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!((hx.last_htc() - result.h).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_exchanger_signature() {
|
||||||
|
let geo = test_geometry();
|
||||||
|
let hx = BphxExchanger::new(geo);
|
||||||
|
|
||||||
|
let sig = hx.signature();
|
||||||
|
assert!(sig.contains("BphxExchanger"));
|
||||||
|
assert!(sig.contains("20 plates"));
|
||||||
|
assert!(sig.contains("Longo (2004)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_exchanger_energy_transfers() {
|
||||||
|
let geo = test_geometry();
|
||||||
|
let hx = BphxExchanger::new(geo);
|
||||||
|
let state = vec![0.0; 10];
|
||||||
|
let (heat, work) = hx.energy_transfers(&state).unwrap();
|
||||||
|
assert_eq!(heat.to_watts(), 0.0);
|
||||||
|
assert_eq!(work.to_watts(), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_exchanger_calib_default() {
|
||||||
|
let geo = test_geometry();
|
||||||
|
let hx = BphxExchanger::new(geo);
|
||||||
|
let calib = hx.calib();
|
||||||
|
assert_eq!(calib.f_ua, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_exchanger_set_calib() {
|
||||||
|
let geo = test_geometry();
|
||||||
|
let mut hx = BphxExchanger::new(geo);
|
||||||
|
let mut calib = Calib::default();
|
||||||
|
calib.f_ua = 0.9;
|
||||||
|
hx.set_calib(calib);
|
||||||
|
assert_eq!(hx.calib().f_ua, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_exchanger_geometry() {
|
||||||
|
let geo = test_geometry();
|
||||||
|
let hx = BphxExchanger::new(geo.clone());
|
||||||
|
assert_eq!(hx.geometry().n_plates, geo.n_plates);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_exchanger_update_ua_from_htc() {
|
||||||
|
let geo = test_geometry();
|
||||||
|
let mut hx = BphxExchanger::new(geo);
|
||||||
|
|
||||||
|
let h = 5000.0;
|
||||||
|
let ua_before = hx.ua();
|
||||||
|
hx.update_ua_from_htc(h);
|
||||||
|
let ua_after = hx.ua();
|
||||||
|
|
||||||
|
assert!((ua_after - ua_before).abs() > 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_exchanger_validity_warning() {
|
||||||
|
let geo = test_geometry();
|
||||||
|
let hx = BphxExchanger::new(geo).with_correlation(BphxCorrelation::Longo2004);
|
||||||
|
|
||||||
|
assert!(!hx.had_validity_warning());
|
||||||
|
|
||||||
|
hx.compute_htc(
|
||||||
|
100.0, 0.5, 1100.0, 30.0, 0.0002, 0.000012, 0.1, 3.5, 280.0, 285.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(hx.had_validity_warning());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_exchanger_pressure_drop() {
|
||||||
|
let geo = test_geometry();
|
||||||
|
let hx = BphxExchanger::new(geo.clone());
|
||||||
|
|
||||||
|
let dp = hx.compute_pressure_drop(30.0, 1100.0);
|
||||||
|
assert!(dp >= 0.0);
|
||||||
|
|
||||||
|
let mut hx_with_calib = BphxExchanger::new(geo);
|
||||||
|
let mut calib = Calib::default();
|
||||||
|
calib.f_dp = 0.5;
|
||||||
|
hx_with_calib.set_calib(calib);
|
||||||
|
|
||||||
|
let dp_calib = hx_with_calib.compute_pressure_drop(30.0, 1100.0);
|
||||||
|
assert!((dp_calib - dp * 0.5).abs() < 1e-6);
|
||||||
|
}
|
||||||
|
}
|
||||||
469
crates/components/src/heat_exchanger/bphx_geometry.rs
Normal file
469
crates/components/src/heat_exchanger/bphx_geometry.rs
Normal file
@ -0,0 +1,469 @@
|
|||||||
|
//! BPHX (Brazed Plate Heat Exchanger) Geometry Definition
|
||||||
|
//!
|
||||||
|
//! Defines geometry parameters for brazed plate heat exchangers used in
|
||||||
|
//! evaporation, condensation, and generic heat transfer applications.
|
||||||
|
//!
|
||||||
|
//! ## Key Parameters
|
||||||
|
//!
|
||||||
|
//! - **Hydraulic diameter (dh)**: Characteristic length for flow calculations
|
||||||
|
//! - **Heat transfer area**: Total plate surface area for heat exchange
|
||||||
|
//! - **Chevron angle**: Plate corrugation angle (typically 30-65°)
|
||||||
|
//! - **Number of plates**: Total count of heat transfer plates
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
/// BPHX exchanger type classification
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
pub enum BphxType {
|
||||||
|
/// DX or flooded evaporation
|
||||||
|
Evaporator,
|
||||||
|
/// Condensation with subcooled liquid outlet
|
||||||
|
Condenser,
|
||||||
|
/// Generic heat transfer (default)
|
||||||
|
#[default]
|
||||||
|
Generic,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Geometry parameters for a Brazed Plate Heat Exchanger (BPHX)
|
||||||
|
///
|
||||||
|
/// The hydraulic diameter and heat transfer area are calculated from
|
||||||
|
/// plate dimensions and corrugation parameters.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use entropyk_components::heat_exchanger::BphxGeometry;
|
||||||
|
///
|
||||||
|
/// let geo = BphxGeometry::new(30)
|
||||||
|
/// .with_plate_dimensions(0.5, 0.1)
|
||||||
|
/// .with_chevron_angle(60.0)
|
||||||
|
/// .build()
|
||||||
|
/// .expect("Valid geometry");
|
||||||
|
///
|
||||||
|
/// assert!(geo.dh > 0.0);
|
||||||
|
/// assert!(geo.area > 0.0);
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BphxGeometry {
|
||||||
|
/// Number of heat transfer plates
|
||||||
|
pub n_plates: u32,
|
||||||
|
/// Plate length (flow direction) in meters
|
||||||
|
pub plate_length: f64,
|
||||||
|
/// Plate width (perpendicular to flow) in meters
|
||||||
|
pub plate_width: f64,
|
||||||
|
/// Plate thickness in meters
|
||||||
|
pub plate_thickness: f64,
|
||||||
|
/// Chevron (corrugation) angle in degrees (typically 30-65°)
|
||||||
|
pub chevron_angle: f64,
|
||||||
|
/// Hydraulic diameter in meters (calculated)
|
||||||
|
pub dh: f64,
|
||||||
|
/// Total heat transfer area in m² (calculated)
|
||||||
|
pub area: f64,
|
||||||
|
/// Channel spacing (plate gap) in meters
|
||||||
|
pub channel_spacing: f64,
|
||||||
|
/// Corrugation pitch in meters
|
||||||
|
pub corrugation_pitch: f64,
|
||||||
|
/// Exchanger type classification
|
||||||
|
pub exchanger_type: BphxType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BphxGeometry {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
n_plates: 20,
|
||||||
|
plate_length: 0.3,
|
||||||
|
plate_width: 0.1,
|
||||||
|
plate_thickness: 0.0006,
|
||||||
|
chevron_angle: 60.0,
|
||||||
|
dh: 0.003,
|
||||||
|
area: 0.5,
|
||||||
|
channel_spacing: 0.002,
|
||||||
|
corrugation_pitch: 0.006,
|
||||||
|
exchanger_type: BphxType::Generic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BphxGeometry {
|
||||||
|
/// Minimum valid values for geometry parameters
|
||||||
|
pub const MIN_PLATES: u32 = 1;
|
||||||
|
pub const MIN_DIMENSION: f64 = 1e-6;
|
||||||
|
pub const MIN_CHEVRON_ANGLE: f64 = 10.0;
|
||||||
|
pub const MAX_CHEVRON_ANGLE: f64 = 80.0;
|
||||||
|
|
||||||
|
/// Creates a new geometry builder with the specified number of plates.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `n_plates` - Number of heat transfer plates (must be >= 1)
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use entropyk_components::heat_exchanger::BphxGeometry;
|
||||||
|
///
|
||||||
|
/// let geo = BphxGeometry::new(30);
|
||||||
|
/// ```
|
||||||
|
pub fn new(n_plates: u32) -> BphxGeometryBuilder {
|
||||||
|
BphxGeometryBuilder {
|
||||||
|
n_plates: n_plates.max(Self::MIN_PLATES),
|
||||||
|
plate_length: None,
|
||||||
|
plate_width: None,
|
||||||
|
plate_thickness: 0.0006,
|
||||||
|
chevron_angle: 60.0,
|
||||||
|
channel_spacing: 0.002,
|
||||||
|
corrugation_pitch: None,
|
||||||
|
exchanger_type: BphxType::Generic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a geometry from known hydraulic diameter and area.
|
||||||
|
///
|
||||||
|
/// Use this when manufacturer data provides dh and area directly.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `dh` - Hydraulic diameter in meters
|
||||||
|
/// * `area` - Heat transfer area in m²
|
||||||
|
/// * `n_plates` - Number of plates
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use entropyk_components::heat_exchanger::BphxGeometry;
|
||||||
|
///
|
||||||
|
/// let geo = BphxGeometry::from_dh_area(0.003, 0.5, 20);
|
||||||
|
/// assert!((geo.dh - 0.003).abs() < 1e-10);
|
||||||
|
/// ```
|
||||||
|
pub fn from_dh_area(dh: f64, area: f64, n_plates: u32) -> Self {
|
||||||
|
Self {
|
||||||
|
n_plates: n_plates.max(Self::MIN_PLATES),
|
||||||
|
dh: dh.max(Self::MIN_DIMENSION),
|
||||||
|
area: area.max(Self::MIN_DIMENSION),
|
||||||
|
plate_length: 0.3,
|
||||||
|
plate_width: 0.1,
|
||||||
|
plate_thickness: 0.0006,
|
||||||
|
chevron_angle: 60.0,
|
||||||
|
channel_spacing: 0.002,
|
||||||
|
corrugation_pitch: 0.006,
|
||||||
|
exchanger_type: BphxType::Generic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the effective flow cross-sectional area per channel (m²).
|
||||||
|
///
|
||||||
|
/// A_channel = channel_spacing × plate_width
|
||||||
|
pub fn channel_flow_area(&self) -> f64 {
|
||||||
|
self.channel_spacing * self.plate_width
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the total number of channels (n_plates - 1).
|
||||||
|
pub fn n_channels(&self) -> u32 {
|
||||||
|
self.n_plates.saturating_sub(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the mass flux for a given mass flow rate.
|
||||||
|
///
|
||||||
|
/// G = m_dot / (A_channel × n_channels)
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `mass_flow` - Total mass flow rate (kg/s)
|
||||||
|
pub fn mass_flux(&self, mass_flow: f64) -> f64 {
|
||||||
|
let n_channels = self.n_channels() as f64;
|
||||||
|
if n_channels < 1e-10 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
let channel_area = self.channel_flow_area();
|
||||||
|
mass_flow / (channel_area * n_channels)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates the geometry parameters.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if any parameter is invalid.
|
||||||
|
pub fn validate(&self) -> Result<(), BphxGeometryError> {
|
||||||
|
if self.n_plates < Self::MIN_PLATES {
|
||||||
|
return Err(BphxGeometryError::InvalidPlates {
|
||||||
|
n_plates: self.n_plates,
|
||||||
|
min: Self::MIN_PLATES,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if self.plate_length < Self::MIN_DIMENSION {
|
||||||
|
return Err(BphxGeometryError::InvalidDimension {
|
||||||
|
name: "plate_length",
|
||||||
|
value: self.plate_length,
|
||||||
|
min: Self::MIN_DIMENSION,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if self.plate_width < Self::MIN_DIMENSION {
|
||||||
|
return Err(BphxGeometryError::InvalidDimension {
|
||||||
|
name: "plate_width",
|
||||||
|
value: self.plate_width,
|
||||||
|
min: Self::MIN_DIMENSION,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if self.dh < Self::MIN_DIMENSION {
|
||||||
|
return Err(BphxGeometryError::InvalidDimension {
|
||||||
|
name: "dh",
|
||||||
|
value: self.dh,
|
||||||
|
min: Self::MIN_DIMENSION,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if self.area < Self::MIN_DIMENSION {
|
||||||
|
return Err(BphxGeometryError::InvalidDimension {
|
||||||
|
name: "area",
|
||||||
|
value: self.area,
|
||||||
|
min: Self::MIN_DIMENSION,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if self.chevron_angle < Self::MIN_CHEVRON_ANGLE
|
||||||
|
|| self.chevron_angle > Self::MAX_CHEVRON_ANGLE
|
||||||
|
{
|
||||||
|
return Err(BphxGeometryError::InvalidChevronAngle {
|
||||||
|
angle: self.chevron_angle,
|
||||||
|
min: Self::MIN_CHEVRON_ANGLE,
|
||||||
|
max: Self::MAX_CHEVRON_ANGLE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for BphxGeometry {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"BphxGeometry({} plates, dh={:.3}mm, A={:.3}m², β={:.0}°)",
|
||||||
|
self.n_plates,
|
||||||
|
self.dh * 1000.0,
|
||||||
|
self.area,
|
||||||
|
self.chevron_angle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder for BphxGeometry
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BphxGeometryBuilder {
|
||||||
|
n_plates: u32,
|
||||||
|
plate_length: Option<f64>,
|
||||||
|
plate_width: Option<f64>,
|
||||||
|
plate_thickness: f64,
|
||||||
|
chevron_angle: f64,
|
||||||
|
channel_spacing: f64,
|
||||||
|
corrugation_pitch: Option<f64>,
|
||||||
|
exchanger_type: BphxType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BphxGeometryBuilder {
|
||||||
|
/// Sets the plate dimensions (length and width in meters).
|
||||||
|
pub fn with_plate_dimensions(mut self, length: f64, width: f64) -> Self {
|
||||||
|
self.plate_length = Some(length.max(BphxGeometry::MIN_DIMENSION));
|
||||||
|
self.plate_width = Some(width.max(BphxGeometry::MIN_DIMENSION));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the number of plates.
|
||||||
|
pub fn with_plates(mut self, n: u32) -> Self {
|
||||||
|
self.n_plates = n.max(BphxGeometry::MIN_PLATES);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the chevron (corrugation) angle in degrees.
|
||||||
|
pub fn with_chevron_angle(mut self, degrees: f64) -> Self {
|
||||||
|
self.chevron_angle = degrees.clamp(
|
||||||
|
BphxGeometry::MIN_CHEVRON_ANGLE,
|
||||||
|
BphxGeometry::MAX_CHEVRON_ANGLE,
|
||||||
|
);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the plate thickness in meters.
|
||||||
|
pub fn with_plate_thickness(mut self, thickness: f64) -> Self {
|
||||||
|
self.plate_thickness = thickness.max(BphxGeometry::MIN_DIMENSION);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the channel spacing (plate gap) in meters.
|
||||||
|
pub fn with_channel_spacing(mut self, spacing: f64) -> Self {
|
||||||
|
self.channel_spacing = spacing.max(BphxGeometry::MIN_DIMENSION);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the corrugation pitch in meters.
|
||||||
|
pub fn with_corrugation_pitch(mut self, pitch: f64) -> Self {
|
||||||
|
self.corrugation_pitch = Some(pitch.max(BphxGeometry::MIN_DIMENSION));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the exchanger type.
|
||||||
|
pub fn with_exchanger_type(mut self, exchanger_type: BphxType) -> Self {
|
||||||
|
self.exchanger_type = exchanger_type;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the geometry, calculating dh and area from parameters.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns an error if required parameters are missing or invalid.
|
||||||
|
pub fn build(self) -> Result<BphxGeometry, BphxGeometryError> {
|
||||||
|
let plate_length = self
|
||||||
|
.plate_length
|
||||||
|
.ok_or(BphxGeometryError::MissingParameter {
|
||||||
|
name: "plate_length",
|
||||||
|
})?;
|
||||||
|
let plate_width = self
|
||||||
|
.plate_width
|
||||||
|
.ok_or(BphxGeometryError::MissingParameter {
|
||||||
|
name: "plate_width",
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let corrugation_pitch = self.corrugation_pitch.unwrap_or(2.0 * self.channel_spacing);
|
||||||
|
|
||||||
|
let dh = 2.0 * self.channel_spacing;
|
||||||
|
let n_channels = (self.n_plates.saturating_sub(1)) as f64;
|
||||||
|
let area = 2.0 * n_channels * plate_length * plate_width;
|
||||||
|
|
||||||
|
let geo = BphxGeometry {
|
||||||
|
n_plates: self.n_plates,
|
||||||
|
plate_length,
|
||||||
|
plate_width,
|
||||||
|
plate_thickness: self.plate_thickness,
|
||||||
|
chevron_angle: self.chevron_angle,
|
||||||
|
dh,
|
||||||
|
area,
|
||||||
|
channel_spacing: self.channel_spacing,
|
||||||
|
corrugation_pitch,
|
||||||
|
exchanger_type: self.exchanger_type,
|
||||||
|
};
|
||||||
|
|
||||||
|
geo.validate()?;
|
||||||
|
Ok(geo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Errors for BPHX geometry validation
|
||||||
|
#[derive(Debug, Clone, thiserror::Error)]
|
||||||
|
pub enum BphxGeometryError {
|
||||||
|
#[error("Invalid number of plates: {n_plates}, minimum is {min}")]
|
||||||
|
InvalidPlates { n_plates: u32, min: u32 },
|
||||||
|
|
||||||
|
#[error("Invalid {name}: {value}, minimum is {min}")]
|
||||||
|
InvalidDimension {
|
||||||
|
name: &'static str,
|
||||||
|
value: f64,
|
||||||
|
min: f64,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("Invalid chevron angle: {angle}°, valid range is {min}° to {max}°")]
|
||||||
|
InvalidChevronAngle { angle: f64, min: f64, max: f64 },
|
||||||
|
|
||||||
|
#[error("Missing required parameter: {name}")]
|
||||||
|
MissingParameter { name: &'static str },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_geometry_default() {
|
||||||
|
let geo = BphxGeometry::default();
|
||||||
|
assert!(geo.validate().is_ok());
|
||||||
|
assert_eq!(geo.n_plates, 20);
|
||||||
|
assert!((geo.chevron_angle - 60.0).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_geometry_from_dh_area() {
|
||||||
|
let geo = BphxGeometry::from_dh_area(0.003, 0.5, 20);
|
||||||
|
assert!((geo.dh - 0.003).abs() < 1e-10);
|
||||||
|
assert!((geo.area - 0.5).abs() < 1e-10);
|
||||||
|
assert_eq!(geo.n_plates, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_geometry_builder() {
|
||||||
|
let geo = BphxGeometry::new(30)
|
||||||
|
.with_plate_dimensions(0.5, 0.1)
|
||||||
|
.with_chevron_angle(60.0)
|
||||||
|
.build()
|
||||||
|
.expect("Valid geometry");
|
||||||
|
|
||||||
|
assert_eq!(geo.n_plates, 30);
|
||||||
|
assert!((geo.plate_length - 0.5).abs() < 1e-10);
|
||||||
|
assert!((geo.plate_width - 0.1).abs() < 1e-10);
|
||||||
|
assert!((geo.chevron_angle - 60.0).abs() < 1e-10);
|
||||||
|
assert!(geo.dh > 0.0);
|
||||||
|
assert!(geo.area > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_geometry_builder_missing_dimensions() {
|
||||||
|
let result = BphxGeometry::new(20).build();
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_geometry_n_channels() {
|
||||||
|
let geo = BphxGeometry::new(30)
|
||||||
|
.with_plate_dimensions(0.5, 0.1)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(geo.n_channels(), 29);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_geometry_mass_flux() {
|
||||||
|
let geo = BphxGeometry::new(10)
|
||||||
|
.with_plate_dimensions(0.3, 0.1)
|
||||||
|
.with_channel_spacing(0.002)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mass_flow = 0.1;
|
||||||
|
let g = geo.mass_flux(mass_flow);
|
||||||
|
assert!(g > 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_geometry_validate_invalid_plates() {
|
||||||
|
let mut geo = BphxGeometry::default();
|
||||||
|
geo.n_plates = 0;
|
||||||
|
assert!(geo.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_geometry_validate_invalid_chevron() {
|
||||||
|
let mut geo = BphxGeometry::default();
|
||||||
|
geo.chevron_angle = 5.0;
|
||||||
|
assert!(geo.validate().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_geometry_with_exchanger_type() {
|
||||||
|
let geo = BphxGeometry::new(20)
|
||||||
|
.with_plate_dimensions(0.3, 0.1)
|
||||||
|
.with_exchanger_type(BphxType::Evaporator)
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(geo.exchanger_type, BphxType::Evaporator);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_geometry_display() {
|
||||||
|
let geo = BphxGeometry::default();
|
||||||
|
let s = format!("{}", geo);
|
||||||
|
assert!(s.contains("20 plates"));
|
||||||
|
assert!(s.contains("60°"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_bphx_geometry_clamps_min_plates() {
|
||||||
|
let geo = BphxGeometry::from_dh_area(0.003, 0.5, 1);
|
||||||
|
assert_eq!(geo.n_plates, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,10 +15,19 @@
|
|||||||
//! ## Components
|
//! ## Components
|
||||||
//!
|
//!
|
||||||
//! - [`Condenser`]: Refrigerant condensing (phase change) on hot side
|
//! - [`Condenser`]: Refrigerant condensing (phase change) on hot side
|
||||||
//! - [`Evaporator`]: Refrigerant evaporating (phase change) on cold side
|
//! - [`Evaporator`]: Refrigerant evaporating (phase change) on cold side (DX style)
|
||||||
|
//! - [`FloodedEvaporator`]: Flooded/recirculation evaporator (two-phase outlet)
|
||||||
|
//! - [`FloodedCondenser`]: Flooded/accumulation condenser (subcooled liquid outlet)
|
||||||
//! - [`EvaporatorCoil`]: Air-side evaporator (finned coil)
|
//! - [`EvaporatorCoil`]: Air-side evaporator (finned coil)
|
||||||
//! - [`CondenserCoil`]: Air-side condenser (finned coil)
|
//! - [`CondenserCoil`]: Air-side condenser (finned coil)
|
||||||
//! - [`Economizer`]: Internal heat exchanger with bypass support
|
//! - [`Economizer`]: Internal heat exchanger with bypass support
|
||||||
|
//! - [`BphxExchanger`]: Brazed Plate Heat Exchanger with geometry-based correlations
|
||||||
|
//!
|
||||||
|
//! ## BPHX Components (Story 11.5)
|
||||||
|
//!
|
||||||
|
//! - [`BphxExchanger`]: Brazed plate heat exchanger component
|
||||||
|
//! - [`BphxGeometry`]: Geometry specification for BPHX
|
||||||
|
//! - [`BphxCorrelation`]: Heat transfer correlation selection
|
||||||
//!
|
//!
|
||||||
//! ## Example
|
//! ## Example
|
||||||
//!
|
//!
|
||||||
@ -30,6 +39,9 @@
|
|||||||
//! // Heat exchanger would be created with connected ports
|
//! // Heat exchanger would be created with connected ports
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
pub mod bphx_correlation;
|
||||||
|
pub mod bphx_exchanger;
|
||||||
|
pub mod bphx_geometry;
|
||||||
pub mod condenser;
|
pub mod condenser;
|
||||||
pub mod condenser_coil;
|
pub mod condenser_coil;
|
||||||
pub mod economizer;
|
pub mod economizer;
|
||||||
@ -37,9 +49,18 @@ pub mod eps_ntu;
|
|||||||
pub mod evaporator;
|
pub mod evaporator;
|
||||||
pub mod evaporator_coil;
|
pub mod evaporator_coil;
|
||||||
pub mod exchanger;
|
pub mod exchanger;
|
||||||
|
pub mod flooded_condenser;
|
||||||
|
pub mod flooded_evaporator;
|
||||||
pub mod lmtd;
|
pub mod lmtd;
|
||||||
|
pub mod mchx_condenser_coil;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
|
|
||||||
|
pub use bphx_correlation::{
|
||||||
|
BphxCorrelation, CorrelationParams, CorrelationResult, CorrelationSelector, FlowRegime,
|
||||||
|
ValidityStatus,
|
||||||
|
};
|
||||||
|
pub use bphx_exchanger::BphxExchanger;
|
||||||
|
pub use bphx_geometry::{BphxGeometry, BphxGeometryBuilder, BphxGeometryError, BphxType};
|
||||||
pub use condenser::Condenser;
|
pub use condenser::Condenser;
|
||||||
pub use condenser_coil::CondenserCoil;
|
pub use condenser_coil::CondenserCoil;
|
||||||
pub use economizer::Economizer;
|
pub use economizer::Economizer;
|
||||||
@ -47,5 +68,8 @@ pub use eps_ntu::{EpsNtuModel, ExchangerType};
|
|||||||
pub use evaporator::Evaporator;
|
pub use evaporator::Evaporator;
|
||||||
pub use evaporator_coil::EvaporatorCoil;
|
pub use evaporator_coil::EvaporatorCoil;
|
||||||
pub use exchanger::{HeatExchanger, HeatExchangerBuilder, HxSideConditions};
|
pub use exchanger::{HeatExchanger, HeatExchangerBuilder, HxSideConditions};
|
||||||
|
pub use flooded_condenser::FloodedCondenser;
|
||||||
|
pub use flooded_evaporator::FloodedEvaporator;
|
||||||
pub use lmtd::{FlowConfiguration, LmtdModel};
|
pub use lmtd::{FlowConfiguration, LmtdModel};
|
||||||
|
pub use mchx_condenser_coil::MchxCondenserCoil;
|
||||||
pub use model::HeatTransferModel;
|
pub use model::HeatTransferModel;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user