Update project structure and configurations
This commit is contained in:
@@ -1,11 +1,18 @@
|
||||
{
|
||||
"name": "Water/water BPHX example",
|
||||
"name": "BPHX Evaporator + Condenser — standalone runnable examples",
|
||||
"fluid": "R410A",
|
||||
"circuits": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Refrigerant",
|
||||
"name": "Evaporator circuit (R410A / Water)",
|
||||
"components": [
|
||||
{
|
||||
"type": "RefrigerantSource",
|
||||
"name": "ref_src_evap",
|
||||
"fluid": "R410A",
|
||||
"p_set_bar": 4.0,
|
||||
"quality": 0.2
|
||||
},
|
||||
{
|
||||
"type": "BphxEvaporator",
|
||||
"name": "evap",
|
||||
@@ -21,10 +28,33 @@
|
||||
"hot_pressure_bar": 2.0,
|
||||
"hot_mass_flow_kg_s": 0.5,
|
||||
"cold_fluid": "R410A",
|
||||
"cold_t_inlet_c": 5.0,
|
||||
"cold_t_inlet_c": 2.0,
|
||||
"cold_pressure_bar": 4.0,
|
||||
"cold_mass_flow_kg_s": 0.1
|
||||
},
|
||||
{
|
||||
"type": "RefrigerantSink",
|
||||
"name": "ref_snk_evap",
|
||||
"fluid": "R410A",
|
||||
"p_back_bar": 4.0
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{ "from": "ref_src_evap:outlet", "to": "evap:inlet" },
|
||||
{ "from": "evap:outlet", "to": "ref_snk_evap:inlet" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Condenser circuit (R410A / Water)",
|
||||
"components": [
|
||||
{
|
||||
"type": "RefrigerantSource",
|
||||
"name": "ref_src_cond",
|
||||
"fluid": "R410A",
|
||||
"p_set_bar": 18.0,
|
||||
"quality": 1.0
|
||||
},
|
||||
{
|
||||
"type": "BphxCondenser",
|
||||
"name": "cond",
|
||||
@@ -42,13 +72,22 @@
|
||||
"cold_t_inlet_c": 25.0,
|
||||
"cold_pressure_bar": 2.0,
|
||||
"cold_mass_flow_kg_s": 0.5
|
||||
},
|
||||
{
|
||||
"type": "RefrigerantSink",
|
||||
"name": "ref_snk_cond",
|
||||
"fluid": "R410A",
|
||||
"p_back_bar": 18.0
|
||||
}
|
||||
],
|
||||
"edges": []
|
||||
"edges": [
|
||||
{ "from": "ref_src_cond:outlet", "to": "cond:inlet" },
|
||||
{ "from": "cond:outlet", "to": "ref_snk_cond:inlet" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"solver": {
|
||||
"strategy": "newton",
|
||||
"strategy": "fallback",
|
||||
"max_iterations": 50,
|
||||
"tolerance": 1e-6
|
||||
}
|
||||
|
||||
@@ -526,6 +526,26 @@ fn resolve_port_index(component_type: &str, port_name: &str, is_source: bool) ->
|
||||
}
|
||||
}
|
||||
},
|
||||
// BphxEvaporator and BphxCondenser: 2-port refrigerant circuit (inlet=0, outlet=1).
|
||||
// Secondary-fluid conditions are set via JSON params, not graph edges.
|
||||
"BphxEvaporator" | "BphxCondenser" => match port_lower.as_str() {
|
||||
"inlet" | "in" | "refrigerant_in" => 0,
|
||||
"outlet" | "out" | "refrigerant_out" => 1,
|
||||
_ => {
|
||||
tracing::warn!(
|
||||
port_name,
|
||||
component_type,
|
||||
"Unknown port name for {}, defaulting to {}",
|
||||
component_type,
|
||||
if is_source { 1 } else { 0 }
|
||||
);
|
||||
if is_source {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
// Default: inlet=0, outlet=1 for all 2-port components
|
||||
match port_lower.as_str() {
|
||||
@@ -598,21 +618,109 @@ fn parse_side_conditions(
|
||||
}
|
||||
|
||||
/// Build BphxGeometry from JSON params: dh (m), area (m²), n_plates. Defaults: 0.003, 0.5, 20.
|
||||
///
|
||||
/// Returns an error if any parameter is physically invalid (≤ 0).
|
||||
fn bphx_geometry_from_params(
|
||||
params: &std::collections::HashMap<String, serde_json::Value>,
|
||||
exchanger_type: entropyk_components::heat_exchanger::BphxType,
|
||||
) -> entropyk_components::heat_exchanger::BphxGeometry {
|
||||
) -> CliResult<entropyk_components::heat_exchanger::BphxGeometry> {
|
||||
use entropyk_components::heat_exchanger::BphxGeometry;
|
||||
let dh = params
|
||||
.get("dh_m")
|
||||
let dh = params.get("dh_m").and_then(|v| v.as_f64()).unwrap_or(0.003);
|
||||
if dh <= 0.0 {
|
||||
return Err(CliError::Config(format!(
|
||||
"BphxGeometry: dh_m must be > 0 (got {:.6} m)",
|
||||
dh
|
||||
)));
|
||||
}
|
||||
let area = params
|
||||
.get("area_m2")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.003);
|
||||
let area = params.get("area_m2").and_then(|v| v.as_f64()).unwrap_or(0.5);
|
||||
let n_plates = params
|
||||
.unwrap_or(0.5);
|
||||
if area <= 0.0 {
|
||||
return Err(CliError::Config(format!(
|
||||
"BphxGeometry: area_m2 must be > 0 (got {:.4} m²)",
|
||||
area
|
||||
)));
|
||||
}
|
||||
let n_plates_raw = params
|
||||
.get("n_plates")
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(20) as u32;
|
||||
BphxGeometry::from_dh_area(dh, area, n_plates).with_exchanger_type(exchanger_type)
|
||||
.unwrap_or(20);
|
||||
if n_plates_raw > u32::MAX as u64 {
|
||||
return Err(CliError::Config(format!(
|
||||
"BphxGeometry: n_plates too large (got {}, max {})",
|
||||
n_plates_raw, u32::MAX
|
||||
)));
|
||||
}
|
||||
let n_plates = n_plates_raw as u32;
|
||||
if n_plates == 0 {
|
||||
return Err(CliError::Config(
|
||||
"BphxGeometry: n_plates must be > 0".into(),
|
||||
));
|
||||
}
|
||||
Ok(BphxGeometry::from_dh_area(dh, area, n_plates).with_exchanger_type(exchanger_type))
|
||||
}
|
||||
|
||||
/// Extract calibration factors for BphxEvaporator/BphxCondenser from JSON params.
|
||||
///
|
||||
/// Errors if `ua_nominal == 0` and an explicit `ua` override is provided (geometry is
|
||||
/// likely invalid). Warns if both `ua` and `f_ua` are provided simultaneously.
|
||||
fn bphx_calib_from_params(
|
||||
params: &std::collections::HashMap<String, serde_json::Value>,
|
||||
ua_nominal: f64,
|
||||
) -> CliResult<entropyk_core::Calib> {
|
||||
use entropyk_core::Calib;
|
||||
let config_ua = params.get("ua").and_then(|v| v.as_f64());
|
||||
let explicit_f_ua = params.get("f_ua").and_then(|v| v.as_f64());
|
||||
|
||||
if config_ua.is_some() && explicit_f_ua.is_some() {
|
||||
tracing::warn!(
|
||||
"BphxExchanger: both 'ua' and 'f_ua' provided — 'ua' takes precedence, 'f_ua' ignored"
|
||||
);
|
||||
}
|
||||
|
||||
let f_ua = match config_ua {
|
||||
Some(u) => {
|
||||
if u < 0.0 {
|
||||
return Err(CliError::Config(format!(
|
||||
"BphxExchanger: ua must be >= 0 (got {:.2} W/K)",
|
||||
u
|
||||
)));
|
||||
}
|
||||
if ua_nominal > 0.0 {
|
||||
u / ua_nominal
|
||||
} else {
|
||||
return Err(CliError::Config(
|
||||
"BphxExchanger: ua_nominal is zero — cannot compute f_ua from explicit 'ua' override. Check geometry parameters.".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
None => explicit_f_ua.unwrap_or(1.0),
|
||||
};
|
||||
|
||||
if f_ua <= 0.0 {
|
||||
return Err(CliError::Config(format!(
|
||||
"BphxExchanger: f_ua must be > 0 (got {:.4})",
|
||||
f_ua
|
||||
)));
|
||||
}
|
||||
|
||||
let f_dp = params.get("f_dp").and_then(|v| v.as_f64()).unwrap_or(1.0);
|
||||
if f_dp <= 0.0 {
|
||||
return Err(CliError::Config(format!(
|
||||
"BphxExchanger: f_dp must be > 0 (got {:.4})",
|
||||
f_dp
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(Calib {
|
||||
f_m: 1.0,
|
||||
f_dp,
|
||||
f_ua,
|
||||
f_power: 1.0,
|
||||
f_etav: 1.0,
|
||||
calibration_source: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a pair of connected ports for components that need them (screw, MCHX, fan...).
|
||||
@@ -1230,9 +1338,8 @@ fn create_component(
|
||||
use entropyk_components::heat_exchanger::{
|
||||
BphxCorrelation, BphxEvaporator, BphxEvaporatorMode, BphxType,
|
||||
};
|
||||
use entropyk_core::Calib;
|
||||
|
||||
let geo = bphx_geometry_from_params(params, BphxType::Evaporator);
|
||||
let geo = bphx_geometry_from_params(params, BphxType::Evaporator)?;
|
||||
let refrigerant = params
|
||||
.get("refrigerant")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -1242,29 +1349,64 @@ fn create_component(
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Water");
|
||||
|
||||
let mode = match params.get("mode").and_then(|v| v.as_str()).unwrap_or("dx") {
|
||||
let mode_str = params
|
||||
.get("mode")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("dx")
|
||||
.to_lowercase();
|
||||
let mode = match mode_str.as_str() {
|
||||
"flooded" => {
|
||||
let target_quality = params
|
||||
.get("target_quality")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.7);
|
||||
if !(0.0..=1.0).contains(&target_quality) {
|
||||
return Err(CliError::Config(format!(
|
||||
"BphxEvaporator: target_quality must be in [0, 1] (got {:.4})",
|
||||
target_quality
|
||||
)));
|
||||
}
|
||||
BphxEvaporatorMode::Flooded { target_quality }
|
||||
}
|
||||
_ => {
|
||||
other => {
|
||||
if other != "dx" {
|
||||
tracing::warn!(
|
||||
mode = other,
|
||||
"Unknown BphxEvaporator mode '{}', falling back to 'dx'",
|
||||
other
|
||||
);
|
||||
}
|
||||
let target_superheat = params
|
||||
.get("target_superheat_k")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(5.0);
|
||||
BphxEvaporatorMode::Dx {
|
||||
target_superheat,
|
||||
if target_superheat < 0.0 {
|
||||
return Err(CliError::Config(format!(
|
||||
"BphxEvaporator: target_superheat_k must be >= 0 (got {:.2} K)",
|
||||
target_superheat
|
||||
)));
|
||||
}
|
||||
BphxEvaporatorMode::Dx { target_superheat }
|
||||
}
|
||||
};
|
||||
|
||||
let correlation = match params.get("correlation").and_then(|v| v.as_str()) {
|
||||
Some("Shah1979") => BphxCorrelation::Shah1979,
|
||||
Some("Shah2021") => BphxCorrelation::Shah2021,
|
||||
_ => BphxCorrelation::Longo2004,
|
||||
let correlation_str = params
|
||||
.get("correlation")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Longo2004")
|
||||
.to_lowercase();
|
||||
let correlation = match correlation_str.as_str() {
|
||||
"shah1979" => BphxCorrelation::Shah1979,
|
||||
"shah2021" => BphxCorrelation::Shah2021,
|
||||
"longo2004" => BphxCorrelation::Longo2004,
|
||||
other => {
|
||||
tracing::warn!(
|
||||
correlation = other,
|
||||
"Unknown BphxEvaporator correlation '{}', falling back to Longo2004",
|
||||
other
|
||||
);
|
||||
BphxCorrelation::Longo2004
|
||||
}
|
||||
};
|
||||
|
||||
let mut evap = BphxEvaporator::new(geo)
|
||||
@@ -1274,6 +1416,9 @@ fn create_component(
|
||||
.with_fluid_backend(Arc::clone(&backend))
|
||||
.with_correlation(correlation);
|
||||
|
||||
// Convention (Evaporator): hot_fluid = secondary (brine/water), cold_fluid = refrigerant.
|
||||
// The refrigerant evaporates (absorbs heat from the secondary).
|
||||
// Note: this is opposite to the Condenser convention — see BphxCondenser.
|
||||
if params.contains_key("hot_fluid") {
|
||||
let hot = parse_side_conditions(params, "hot")?;
|
||||
evap.set_secondary_conditions(hot);
|
||||
@@ -1283,24 +1428,7 @@ fn create_component(
|
||||
evap.set_refrigerant_conditions(cold);
|
||||
}
|
||||
|
||||
let ua_nominal = evap.ua();
|
||||
let config_ua = params.get("ua").and_then(|v| v.as_f64());
|
||||
let f_ua = config_ua
|
||||
.map(|u| if ua_nominal > 0.0 { u / ua_nominal } else { 1.0 })
|
||||
.unwrap_or_else(|| {
|
||||
params
|
||||
.get("f_ua")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(1.0)
|
||||
});
|
||||
let f_dp = params.get("f_dp").and_then(|v| v.as_f64()).unwrap_or(1.0);
|
||||
evap.set_calib(Calib {
|
||||
f_m: 1.0,
|
||||
f_dp,
|
||||
f_ua,
|
||||
f_power: 1.0,
|
||||
f_etav: 1.0,
|
||||
});
|
||||
evap.set_calib(bphx_calib_from_params(params, evap.ua())?);
|
||||
|
||||
Ok(Box::new(evap))
|
||||
}
|
||||
@@ -1310,9 +1438,8 @@ fn create_component(
|
||||
use entropyk_components::heat_exchanger::{
|
||||
BphxCondenser, BphxCorrelation, BphxType,
|
||||
};
|
||||
use entropyk_core::Calib;
|
||||
|
||||
let geo = bphx_geometry_from_params(params, BphxType::Condenser);
|
||||
let geo = bphx_geometry_from_params(params, BphxType::Condenser)?;
|
||||
let refrigerant = params
|
||||
.get("refrigerant")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -1326,10 +1453,23 @@ fn create_component(
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(3.0);
|
||||
|
||||
let correlation = match params.get("correlation").and_then(|v| v.as_str()) {
|
||||
Some("Shah1979") => BphxCorrelation::Shah1979,
|
||||
Some("Shah2021") => BphxCorrelation::Shah2021,
|
||||
_ => BphxCorrelation::Longo2004,
|
||||
let correlation_str = params
|
||||
.get("correlation")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Longo2004")
|
||||
.to_lowercase();
|
||||
let correlation = match correlation_str.as_str() {
|
||||
"shah1979" => BphxCorrelation::Shah1979,
|
||||
"shah2021" => BphxCorrelation::Shah2021,
|
||||
"longo2004" => BphxCorrelation::Longo2004,
|
||||
other => {
|
||||
tracing::warn!(
|
||||
correlation = other,
|
||||
"Unknown BphxCondenser correlation '{}', falling back to Longo2004",
|
||||
other
|
||||
);
|
||||
BphxCorrelation::Longo2004
|
||||
}
|
||||
};
|
||||
|
||||
let mut cond = BphxCondenser::new(geo)
|
||||
@@ -1339,6 +1479,9 @@ fn create_component(
|
||||
.with_target_subcooling(target_subcooling)
|
||||
.with_correlation(correlation);
|
||||
|
||||
// Convention (Condenser): hot_fluid = refrigerant, cold_fluid = secondary (brine/water).
|
||||
// The refrigerant condenses (releases heat to the secondary).
|
||||
// Note: this is opposite to the Evaporator convention — see BphxEvaporator.
|
||||
if params.contains_key("hot_fluid") {
|
||||
let hot = parse_side_conditions(params, "hot")?;
|
||||
cond.set_refrigerant_conditions(hot);
|
||||
@@ -1348,24 +1491,7 @@ fn create_component(
|
||||
cond.set_secondary_conditions(cold);
|
||||
}
|
||||
|
||||
let ua_nominal = cond.ua();
|
||||
let config_ua = params.get("ua").and_then(|v| v.as_f64());
|
||||
let f_ua = config_ua
|
||||
.map(|u| if ua_nominal > 0.0 { u / ua_nominal } else { 1.0 })
|
||||
.unwrap_or_else(|| {
|
||||
params
|
||||
.get("f_ua")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(1.0)
|
||||
});
|
||||
let f_dp = params.get("f_dp").and_then(|v| v.as_f64()).unwrap_or(1.0);
|
||||
cond.set_calib(Calib {
|
||||
f_m: 1.0,
|
||||
f_dp,
|
||||
f_ua,
|
||||
f_power: 1.0,
|
||||
f_etav: 1.0,
|
||||
});
|
||||
cond.set_calib(bphx_calib_from_params(params, cond.ua())?);
|
||||
|
||||
Ok(Box::new(cond))
|
||||
}
|
||||
@@ -1375,8 +1501,48 @@ fn create_component(
|
||||
Ok(Box::new(SimpleComponent::new("", n_eqs)))
|
||||
}
|
||||
|
||||
"FreeCoolingExchanger" | "FreeCooling" => {
|
||||
use entropyk::{FreeCoolingConfig, FreeCoolingControlMode, FreeCoolingExchanger, FreeCoolingMode};
|
||||
use entropyk_components::port::{FluidId, Port};
|
||||
use entropyk_core::{CircuitId, Enthalpy, Pressure};
|
||||
|
||||
let effectiveness = params.get("effectiveness").and_then(|v| v.as_f64()).unwrap_or(0.85);
|
||||
let ua = params.get("ua").and_then(|v| v.as_f64()).unwrap_or(10_000.0);
|
||||
let cold_mass_flow = params.get("coldMassFlow").and_then(|v| v.as_f64()).unwrap_or(0.5);
|
||||
let hot_mass_flow = params.get("hotMassFlow").and_then(|v| v.as_f64()).unwrap_or(0.5);
|
||||
let cold_cp = params.get("coldCp").and_then(|v| v.as_f64()).unwrap_or(4186.0);
|
||||
let hot_cp = params.get("hotCp").and_then(|v| v.as_f64()).unwrap_or(4186.0);
|
||||
|
||||
let config = FreeCoolingConfig {
|
||||
effectiveness,
|
||||
ua,
|
||||
cold_mass_flow,
|
||||
hot_mass_flow,
|
||||
cold_cp,
|
||||
hot_cp,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let circuit_id = CircuitId(0);
|
||||
let fluid = FluidId::new("Water");
|
||||
let p = Pressure::from_pascals(3e5);
|
||||
let h = Enthalpy::from_joules_per_kg(63_000.0);
|
||||
|
||||
let (ci, co) = Port::new(FluidId::new("Water"), p, h)
|
||||
.connect(Port::new(FluidId::new("Water"), p, h))
|
||||
.map_err(|e| CliError::Config(format!("Port connect error: {}", e)))?;
|
||||
let (hi, ho) = Port::new(FluidId::new("Water"), p, h)
|
||||
.connect(Port::new(FluidId::new("Water"), p, h))
|
||||
.map_err(|e| CliError::Config(format!("Port connect error: {}", e)))?;
|
||||
|
||||
let fc = FreeCoolingExchanger::new("freecooling", circuit_id, config, ci, co, hi, ho)
|
||||
.map_err(|e| CliError::Config(format!("FreeCoolingExchanger error: {}", e)))?;
|
||||
|
||||
Ok(Box::new(fc))
|
||||
}
|
||||
|
||||
_ => Err(CliError::Config(format!(
|
||||
"Unknown component type: '{}'. Supported: ScrewEconomizerCompressor, MchxCondenserCoil, FloodedEvaporator, BphxEvaporator, BphxCondenser, Condenser, CondenserCoil, Evaporator, EvaporatorCoil, HeatExchanger, Compressor, ExpansionValve, Pump, Placeholder",
|
||||
"Unknown component type: '{}'. Supported: ScrewEconomizerCompressor, MchxCondenserCoil, FloodedEvaporator, BphxEvaporator, BphxCondenser, FreeCoolingExchanger, Condenser, CondenserCoil, Evaporator, EvaporatorCoil, HeatExchanger, Compressor, ExpansionValve, Pump, Placeholder",
|
||||
component_type
|
||||
))),
|
||||
}
|
||||
|
||||
@@ -311,7 +311,10 @@ fn test_screw_compressor_preset_config() {
|
||||
std::fs::write(&config_path, json).unwrap();
|
||||
|
||||
let config = ScenarioConfig::from_file(&config_path);
|
||||
assert!(config.is_ok(), "Config with preset should parse successfully");
|
||||
assert!(
|
||||
config.is_ok(),
|
||||
"Config with preset should parse successfully"
|
||||
);
|
||||
|
||||
let config = config.unwrap();
|
||||
let params = &config.circuits[0].components[0].params;
|
||||
@@ -391,8 +394,8 @@ fn test_screw_compressor_grasso_preset_config() {
|
||||
fn test_ac2_frequency_ratio_set_correctly_by_cli() {
|
||||
use entropyk_components::{
|
||||
polynomials::Polynomial2D,
|
||||
screw_economizer_compressor::{ScrewEconomizerCompressor, ScrewPerformanceCurves},
|
||||
port::{FluidId, Port},
|
||||
screw_economizer_compressor::{ScrewEconomizerCompressor, ScrewPerformanceCurves},
|
||||
};
|
||||
use entropyk_core::{Enthalpy, Pressure};
|
||||
|
||||
@@ -464,7 +467,11 @@ fn test_ac1_mchx_ua_nominal_parsed_from_config() {
|
||||
let comp = &config.circuits[0].components[0];
|
||||
|
||||
// AC1: ua_nominal_kw_k field parsed correctly
|
||||
assert_eq!(comp.ua_nominal_kw_k, Some(8.5), "ua_nominal_kw_k should be 8.5 kW/K");
|
||||
assert_eq!(
|
||||
comp.ua_nominal_kw_k,
|
||||
Some(8.5),
|
||||
"ua_nominal_kw_k should be 8.5 kW/K"
|
||||
);
|
||||
assert_eq!(comp.fan_speed, Some(1.0));
|
||||
assert_eq!(comp.air_inlet_temp_c, Some(35.0));
|
||||
}
|
||||
@@ -472,8 +479,8 @@ fn test_ac1_mchx_ua_nominal_parsed_from_config() {
|
||||
/// AC2: Given fan_speed=0.64, n_air_exponent=0.5, UA_eff ≈ UA_nom × √0.64 = UA_nom × 0.8.
|
||||
#[test]
|
||||
fn test_ac2_fan_speed_064_yields_ua_eff_08() {
|
||||
use entropyk_components::heat_exchanger::MchxCondenserCoil;
|
||||
use approx::assert_relative_eq;
|
||||
use entropyk_components::heat_exchanger::MchxCondenserCoil;
|
||||
|
||||
let ua_nominal = 8_500.0; // W/K (8.5 kW/K)
|
||||
let n_air = 0.5;
|
||||
@@ -485,7 +492,7 @@ fn test_ac2_fan_speed_064_yields_ua_eff_08() {
|
||||
|
||||
// AC2: UA_eff ≈ UA_nom × 0.64^0.5 = UA_nom × 0.8
|
||||
let expected_ua = ua_nominal * 0.8; // 0.64^0.5 = 0.8
|
||||
// Allow 5% tolerance for density correction at 35°C
|
||||
// Allow 5% tolerance for density correction at 35°C
|
||||
let ua_eff = coil.ua_effective();
|
||||
assert_relative_eq!(ua_eff, expected_ua, epsilon = expected_ua * 0.05);
|
||||
}
|
||||
@@ -519,7 +526,10 @@ fn test_ac3_condenser_bank_2x2_generates_4_components() {
|
||||
let bank_comp = &config.circuits[0].components[0];
|
||||
|
||||
// Verify bank config parsed
|
||||
let bank = bank_comp.condenser_bank.as_ref().expect("condenser_bank must be present");
|
||||
let bank = bank_comp
|
||||
.condenser_bank
|
||||
.as_ref()
|
||||
.expect("condenser_bank must be present");
|
||||
assert_eq!(bank.circuits, 2);
|
||||
assert_eq!(bank.coils_per_circuit, 2);
|
||||
|
||||
@@ -742,11 +752,18 @@ fn test_bphx_evaporator_and_condenser_config_parsing() {
|
||||
|
||||
let result = run_simulation(&config_path, None, false).unwrap();
|
||||
|
||||
// create_component must accept both types (no "Unknown component type").
|
||||
// create_component must accept both types. Two distinct assertions:
|
||||
// (a) no "Unknown component type" — both Bphx types must be registered.
|
||||
// (b) no "Failed to create component" — construction must succeed, not just be recognised.
|
||||
if let Some(ref err) = result.error {
|
||||
assert!(
|
||||
!err.contains("Unknown component type"),
|
||||
"BphxEvaporator and BphxCondenser must be supported: {}",
|
||||
"BphxEvaporator and BphxCondenser must be registered in create_component: {}",
|
||||
err
|
||||
);
|
||||
assert!(
|
||||
!err.contains("Failed to create component"),
|
||||
"BphxEvaporator/BphxCondenser construction must not fail: {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
@@ -754,10 +771,52 @@ fn test_bphx_evaporator_and_condenser_config_parsing() {
|
||||
// We expect Error or NonConverged (edges empty -> topology/finalization failure), not config parse failure.
|
||||
match result.status {
|
||||
SimulationStatus::Error => {
|
||||
// Failure is expected (e.g. isolated nodes); config parsing succeeded.
|
||||
// Failure is expected (e.g. isolated nodes); config parsing and construction succeeded.
|
||||
}
|
||||
SimulationStatus::NonConverged | SimulationStatus::Converged | SimulationStatus::Timeout => {
|
||||
SimulationStatus::NonConverged
|
||||
| SimulationStatus::Converged
|
||||
| SimulationStatus::Timeout => {
|
||||
// Also acceptable if we get to solver stage.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Story 15-4 — Integration: BphxEvaporator and BphxCondenser in bounded circuits
|
||||
/// (RefrigerantSource → Bphx → RefrigerantSink) must reach the solver stage.
|
||||
/// Validates that config parsing, component construction, AND edge routing all succeed.
|
||||
#[test]
|
||||
fn test_bphx_bounded_circuit_reaches_solver_stage() {
|
||||
use entropyk_cli::run::run_simulation;
|
||||
|
||||
let example = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("examples/bphx_evaporator_condenser.json");
|
||||
|
||||
if !example.exists() {
|
||||
panic!(
|
||||
"Test fixture missing: {} — this test requires the example file to exist",
|
||||
example.display()
|
||||
);
|
||||
}
|
||||
|
||||
let result = run_simulation(&example, None, false).unwrap();
|
||||
|
||||
// Three-gate assertion: config → construction → edge routing must all succeed.
|
||||
if let Some(ref err) = result.error {
|
||||
assert!(
|
||||
!err.contains("Unknown component type"),
|
||||
"[Gate 1] Bphx type not registered: {}",
|
||||
err
|
||||
);
|
||||
assert!(
|
||||
!err.contains("Failed to create component"),
|
||||
"[Gate 2] Bphx construction failed: {}",
|
||||
err
|
||||
);
|
||||
assert!(
|
||||
!err.contains("Failed to add edge") && !err.contains("Edge references unknown"),
|
||||
"[Gate 3] Edge routing failed: {}",
|
||||
err
|
||||
);
|
||||
// Any remaining error (e.g. solver non-convergence) is acceptable.
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user