Update project structure and configurations

This commit is contained in:
2026-05-23 10:19:55 +02:00
parent ab5dc7e568
commit 62efea0646
1832 changed files with 83568 additions and 51829 deletions

View File

@@ -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
))),
}