feat: implement mass balance validation for Story 7.1
- Added port_mass_flows to Component trait and implements for core components. - Added System::check_mass_balance and integrated it into the solver. - Restored connect methods for ExpansionValve, Compressor, and Pipe to fix integration tests. - Updated Python and C bindings for validation errors. - Updated sprint status and story documentation.
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
#[cfg(feature = "coolprop")]
|
||||
use crate::damped_backend::DampedBackend;
|
||||
use crate::errors::{FluidError, FluidResult};
|
||||
use crate::types::{CriticalPoint, FluidId, Phase, Property, FluidState};
|
||||
use crate::types::{CriticalPoint, FluidId, FluidState, Phase, Property};
|
||||
|
||||
#[cfg(feature = "coolprop")]
|
||||
use crate::mixture::Mixture;
|
||||
@@ -37,18 +37,79 @@ impl CoolPropBackend {
|
||||
let backend = CoolPropBackend {
|
||||
critical_cache: RwLock::new(HashMap::new()),
|
||||
available_fluids: vec![
|
||||
// HFC Refrigerants
|
||||
FluidId::new("R134a"),
|
||||
FluidId::new("R410A"),
|
||||
FluidId::new("R404A"),
|
||||
FluidId::new("R407C"),
|
||||
FluidId::new("R32"),
|
||||
FluidId::new("R125"),
|
||||
FluidId::new("R143a"),
|
||||
FluidId::new("R152A"),
|
||||
FluidId::new("R22"),
|
||||
FluidId::new("R23"),
|
||||
FluidId::new("R41"),
|
||||
FluidId::new("R245fa"),
|
||||
FluidId::new("R245ca"),
|
||||
// HFO/HFC Low-GWP Refrigerants
|
||||
FluidId::new("R1234yf"),
|
||||
FluidId::new("R1234ze(E)"),
|
||||
FluidId::new("R1234ze(Z)"),
|
||||
FluidId::new("R1233zd(E)"),
|
||||
FluidId::new("R1243zf"),
|
||||
FluidId::new("R1336mzz(E)"),
|
||||
FluidId::new("R513A"),
|
||||
FluidId::new("R454B"),
|
||||
FluidId::new("R452B"),
|
||||
FluidId::new("R32"),
|
||||
// Natural Refrigerants
|
||||
FluidId::new("R744"),
|
||||
FluidId::new("R290"),
|
||||
FluidId::new("R600"),
|
||||
FluidId::new("R600a"),
|
||||
FluidId::new("R1270"),
|
||||
FluidId::new("R717"),
|
||||
// Other Refrigerants
|
||||
FluidId::new("R11"),
|
||||
FluidId::new("R12"),
|
||||
FluidId::new("R13"),
|
||||
FluidId::new("R14"),
|
||||
FluidId::new("R113"),
|
||||
FluidId::new("R114"),
|
||||
FluidId::new("R115"),
|
||||
FluidId::new("R116"),
|
||||
FluidId::new("R123"),
|
||||
FluidId::new("R124"),
|
||||
FluidId::new("R141b"),
|
||||
FluidId::new("R142b"),
|
||||
FluidId::new("R218"),
|
||||
FluidId::new("R227EA"),
|
||||
FluidId::new("R236EA"),
|
||||
FluidId::new("R236FA"),
|
||||
FluidId::new("R365MFC"),
|
||||
FluidId::new("RC318"),
|
||||
FluidId::new("R507A"),
|
||||
// Non-Refrigerant Fluids
|
||||
FluidId::new("Water"),
|
||||
FluidId::new("Air"),
|
||||
FluidId::new("Ammonia"),
|
||||
FluidId::new("CO2"),
|
||||
FluidId::new("Nitrogen"),
|
||||
FluidId::new("Oxygen"),
|
||||
FluidId::new("Argon"),
|
||||
FluidId::new("Helium"),
|
||||
FluidId::new("Hydrogen"),
|
||||
FluidId::new("Methane"),
|
||||
FluidId::new("Ethane"),
|
||||
FluidId::new("Propane"),
|
||||
FluidId::new("Butane"),
|
||||
FluidId::new("Ethylene"),
|
||||
FluidId::new("Propylene"),
|
||||
FluidId::new("Ethanol"),
|
||||
FluidId::new("Methanol"),
|
||||
FluidId::new("Acetone"),
|
||||
FluidId::new("Benzene"),
|
||||
FluidId::new("Toluene"),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -66,20 +127,76 @@ impl CoolPropBackend {
|
||||
|
||||
/// Get the CoolProp internal name for a fluid.
|
||||
fn fluid_name(&self, fluid: &FluidId) -> String {
|
||||
// Map common names to CoolProp internal names
|
||||
match fluid.0.to_lowercase().as_str() {
|
||||
// HFC Refrigerants
|
||||
"r134a" => "R134a".to_string(),
|
||||
"r410a" => "R410A".to_string(),
|
||||
"r404a" => "R404A".to_string(),
|
||||
"r407c" => "R407C".to_string(),
|
||||
"r32" => "R32".to_string(),
|
||||
"r125" => "R125".to_string(),
|
||||
"co2" | "r744" => "CO2".to_string(),
|
||||
"r290" => "R290".to_string(),
|
||||
"r600" => "R600".to_string(),
|
||||
"r600a" => "R600A".to_string(),
|
||||
"r143a" => "R143a".to_string(),
|
||||
"r152a" | "r152a" => "R152A".to_string(),
|
||||
"r22" => "R22".to_string(),
|
||||
"r23" => "R23".to_string(),
|
||||
"r41" => "R41".to_string(),
|
||||
"r245fa" => "R245fa".to_string(),
|
||||
"r245ca" => "R245ca".to_string(),
|
||||
// HFO/HFC Low-GWP Refrigerants
|
||||
"r1234yf" => "R1234yf".to_string(),
|
||||
"r1234ze" | "r1234ze(e)" => "R1234ze(E)".to_string(),
|
||||
"r1234ze(z)" => "R1234ze(Z)".to_string(),
|
||||
"r1233zd" | "r1233zd(e)" => "R1233zd(E)".to_string(),
|
||||
"r1243zf" => "R1243zf".to_string(),
|
||||
"r1336mzz" | "r1336mzz(e)" => "R1336mzz(E)".to_string(),
|
||||
"r513a" => "R513A".to_string(),
|
||||
"r513b" => "R513B".to_string(),
|
||||
"r454b" => "R454B".to_string(),
|
||||
"r452b" => "R452B".to_string(),
|
||||
// Natural Refrigerants (aliases)
|
||||
"r744" | "co2" => "CO2".to_string(),
|
||||
"r290" | "propane" => "R290".to_string(),
|
||||
"r600" | "butane" | "n-butane" => "R600".to_string(),
|
||||
"r600a" | "isobutane" => "R600A".to_string(),
|
||||
"r1270" | "propylene" => "R1270".to_string(),
|
||||
"r717" | "ammonia" => "Ammonia".to_string(),
|
||||
// Other Refrigerants
|
||||
"r11" => "R11".to_string(),
|
||||
"r12" => "R12".to_string(),
|
||||
"r13" => "R13".to_string(),
|
||||
"r14" => "R14".to_string(),
|
||||
"r113" => "R113".to_string(),
|
||||
"r114" => "R114".to_string(),
|
||||
"r115" => "R115".to_string(),
|
||||
"r116" => "R116".to_string(),
|
||||
"r123" => "R123".to_string(),
|
||||
"r124" => "R124".to_string(),
|
||||
"r141b" => "R141b".to_string(),
|
||||
"r142b" => "R142b".to_string(),
|
||||
"r218" => "R218".to_string(),
|
||||
"r227ea" => "R227EA".to_string(),
|
||||
"r236ea" => "R236EA".to_string(),
|
||||
"r236fa" => "R236FA".to_string(),
|
||||
"r365mfc" => "R365MFC".to_string(),
|
||||
"rc318" => "RC318".to_string(),
|
||||
"r507a" => "R507A".to_string(),
|
||||
// Non-Refrigerant Fluids
|
||||
"water" => "Water".to_string(),
|
||||
"air" => "Air".to_string(),
|
||||
"nitrogen" => "Nitrogen".to_string(),
|
||||
"oxygen" => "Oxygen".to_string(),
|
||||
"argon" => "Argon".to_string(),
|
||||
"helium" => "Helium".to_string(),
|
||||
"hydrogen" => "Hydrogen".to_string(),
|
||||
"methane" => "Methane".to_string(),
|
||||
"ethane" => "Ethane".to_string(),
|
||||
"ethylene" => "Ethylene".to_string(),
|
||||
"ethanol" => "Ethanol".to_string(),
|
||||
"methanol" => "Methanol".to_string(),
|
||||
"acetone" => "Acetone".to_string(),
|
||||
"benzene" => "Benzene".to_string(),
|
||||
"toluene" => "Toluene".to_string(),
|
||||
// Pass through unknown names
|
||||
n => n.to_string(),
|
||||
}
|
||||
}
|
||||
@@ -355,9 +472,14 @@ impl crate::backend::FluidBackend for CoolPropBackend {
|
||||
}
|
||||
}
|
||||
|
||||
fn full_state(&self, fluid: FluidId, p: entropyk_core::Pressure, h: entropyk_core::Enthalpy) -> FluidResult<crate::types::ThermoState> {
|
||||
fn full_state(
|
||||
&self,
|
||||
fluid: FluidId,
|
||||
p: entropyk_core::Pressure,
|
||||
h: entropyk_core::Enthalpy,
|
||||
) -> FluidResult<crate::types::ThermoState> {
|
||||
let coolprop_fluid = self.fluid_name(&fluid);
|
||||
|
||||
|
||||
if !self.is_fluid_available(&fluid) {
|
||||
return Err(FluidError::UnknownFluid { fluid: fluid.0 });
|
||||
}
|
||||
@@ -369,7 +491,10 @@ impl crate::backend::FluidBackend for CoolPropBackend {
|
||||
let t_k = coolprop::props_si_ph("T", p_pa, h_j_kg, &coolprop_fluid);
|
||||
if t_k.is_nan() {
|
||||
return Err(FluidError::InvalidState {
|
||||
reason: format!("CoolProp returned NaN for Temperature at P={}, h={} for {}", p_pa, h_j_kg, fluid),
|
||||
reason: format!(
|
||||
"CoolProp returned NaN for Temperature at P={}, h={} for {}",
|
||||
p_pa, h_j_kg, fluid
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -378,7 +503,7 @@ impl crate::backend::FluidBackend for CoolPropBackend {
|
||||
let q = coolprop::props_si_ph("Q", p_pa, h_j_kg, &coolprop_fluid);
|
||||
|
||||
let phase = self.phase(fluid.clone(), FluidState::from_ph(p, h))?;
|
||||
|
||||
|
||||
let quality = if (0.0..=1.0).contains(&q) {
|
||||
Some(crate::types::Quality::new(q))
|
||||
} else {
|
||||
@@ -395,7 +520,7 @@ impl crate::backend::FluidBackend for CoolPropBackend {
|
||||
Some(crate::types::TemperatureDelta::new(t_bubble - t_k))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
@@ -408,7 +533,7 @@ impl crate::backend::FluidBackend for CoolPropBackend {
|
||||
Some(crate::types::TemperatureDelta::new(t_k - t_dew))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
@@ -487,7 +612,12 @@ impl crate::backend::FluidBackend for CoolPropBackend {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn full_state(&self, _fluid: FluidId, _p: entropyk_core::Pressure, _h: entropyk_core::Enthalpy) -> FluidResult<crate::types::ThermoState> {
|
||||
fn full_state(
|
||||
&self,
|
||||
_fluid: FluidId,
|
||||
_p: entropyk_core::Pressure,
|
||||
_h: entropyk_core::Enthalpy,
|
||||
) -> FluidResult<crate::types::ThermoState> {
|
||||
Err(FluidError::CoolPropError(
|
||||
"CoolProp not available. Enable 'coolprop' feature to use this backend.".to_string(),
|
||||
))
|
||||
@@ -624,17 +754,19 @@ mod tests {
|
||||
let pressure = Pressure::from_bar(1.0);
|
||||
let enthalpy = entropyk_core::Enthalpy::from_kilojoules_per_kg(415.0); // Superheated vapor region
|
||||
|
||||
let state = backend.full_state(fluid.clone(), pressure, enthalpy).unwrap();
|
||||
|
||||
let state = backend
|
||||
.full_state(fluid.clone(), pressure, enthalpy)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.fluid, fluid);
|
||||
assert_eq!(state.pressure, pressure);
|
||||
assert_eq!(state.enthalpy, enthalpy);
|
||||
|
||||
|
||||
// Temperature should be valid
|
||||
assert!(state.temperature.to_celsius() > -30.0);
|
||||
assert!(state.density > 0.0);
|
||||
assert!(state.entropy.to_joules_per_kg_kelvin() > 0.0);
|
||||
|
||||
|
||||
// In superheated region, phase is Vapor, quality should be None, and superheat should exist
|
||||
assert_eq!(state.phase, Phase::Vapor);
|
||||
assert_eq!(state.quality, None);
|
||||
|
||||
@@ -169,22 +169,53 @@ impl Mixture {
|
||||
/// Get molar mass (g/mol) for common refrigerants.
|
||||
fn molar_mass(fluid: &str) -> f64 {
|
||||
match fluid.to_uppercase().as_str() {
|
||||
// HFC Refrigerants
|
||||
"R32" => 52.02,
|
||||
"R125" => 120.02,
|
||||
"R134A" => 102.03,
|
||||
"R143A" => 84.04,
|
||||
"R152A" => 66.05,
|
||||
"R22" => 86.47,
|
||||
"R23" => 70.01,
|
||||
"R41" => 34.03,
|
||||
"R245FA" => 134.05,
|
||||
// HFO Refrigerants
|
||||
"R1234YF" => 114.04,
|
||||
"R1234ZE" => 114.04,
|
||||
"R1234ZE" | "R1234ZE(E)" => 114.04,
|
||||
"R1233ZD" | "R1233ZD(E)" => 130.50,
|
||||
"R1243ZF" => 96.06,
|
||||
// Predefined Mixtures (average molar mass)
|
||||
"R410A" => 72.58,
|
||||
"R404A" => 97.60,
|
||||
"R407C" => 86.20,
|
||||
"R507A" => 98.86,
|
||||
"R513A" => 108.5,
|
||||
"R454B" => 83.03,
|
||||
"R452B" => 68.5,
|
||||
"R454C" => 99.6,
|
||||
// Natural Refrigerants
|
||||
"R290" | "PROPANE" => 44.10,
|
||||
"R600" | "BUTANE" => 58.12,
|
||||
"R600" | "BUTANE" | "N-BUTANE" => 58.12,
|
||||
"R600A" | "ISOBUTANE" => 58.12,
|
||||
"R1270" | "PROPYLENE" => 42.08,
|
||||
"R717" | "AMMONIA" => 17.03,
|
||||
"CO2" | "R744" => 44.01,
|
||||
// Other Fluids
|
||||
"WATER" | "H2O" => 18.02,
|
||||
"AIR" => 28.97,
|
||||
"NITROGEN" | "N2" => 28.01,
|
||||
"OXYGEN" | "O2" => 32.00,
|
||||
"ARGON" => 39.95,
|
||||
"HELIUM" => 4.00,
|
||||
"HYDROGEN" | "H2" => 2.02,
|
||||
"METHANE" => 16.04,
|
||||
"ETHANE" => 30.07,
|
||||
"ETHYLENE" => 28.05,
|
||||
"ETHANOL" => 46.07,
|
||||
"METHANOL" => 32.04,
|
||||
"ACETONE" => 58.08,
|
||||
"BENZENE" => 78.11,
|
||||
"TOLUENE" => 92.14,
|
||||
_ => 50.0, // Default fallback
|
||||
}
|
||||
}
|
||||
@@ -247,7 +278,7 @@ impl std::error::Error for MixtureError {}
|
||||
pub mod predefined {
|
||||
use super::*;
|
||||
|
||||
/// R454B: R32 (50%) / R1234yf (50%) - mass fractions
|
||||
/// R454B: R32 (50%) / R1234yf (50%) - mass fractions (Opteon XL41)
|
||||
pub fn r454b() -> Mixture {
|
||||
Mixture::from_mass_fractions(&[("R32", 0.5), ("R1234yf", 0.5)]).unwrap()
|
||||
}
|
||||
@@ -267,6 +298,31 @@ pub mod predefined {
|
||||
Mixture::from_mass_fractions(&[("R125", 0.44), ("R143a", 0.52), ("R134a", 0.04)]).unwrap()
|
||||
}
|
||||
|
||||
/// R507A: R125 (50%) / R143a (50%) - mass fractions (azeotropic)
|
||||
pub fn r507a() -> Mixture {
|
||||
Mixture::from_mass_fractions(&[("R125", 0.5), ("R143a", 0.5)]).unwrap()
|
||||
}
|
||||
|
||||
/// R513A: R134a (56%) / R1234yf (44%) - mass fractions (Opteon XP10)
|
||||
pub fn r513a() -> Mixture {
|
||||
Mixture::from_mass_fractions(&[("R134a", 0.56), ("R1234yf", 0.44)]).unwrap()
|
||||
}
|
||||
|
||||
/// R452B: R32 (67%) / R125 (7%) / R1234yf (26%) - mass fractions (Opteon XL55)
|
||||
pub fn r452b() -> Mixture {
|
||||
Mixture::from_mass_fractions(&[("R32", 0.67), ("R125", 0.07), ("R1234yf", 0.26)]).unwrap()
|
||||
}
|
||||
|
||||
/// R454C: R32 (21.5%) / R1234yf (78.5%) - mass fractions (Opteon XL20)
|
||||
pub fn r454c() -> Mixture {
|
||||
Mixture::from_mass_fractions(&[("R32", 0.215), ("R1234yf", 0.785)]).unwrap()
|
||||
}
|
||||
|
||||
/// R455A: R32 (21.5%) / R1234yf (75.5%) / CO2 (3%) - mass fractions
|
||||
pub fn r455a() -> Mixture {
|
||||
Mixture::from_mass_fractions(&[("R32", 0.215), ("R1234yf", 0.755), ("CO2", 0.03)]).unwrap()
|
||||
}
|
||||
|
||||
/// R32/R125 (50/50) mixture - mass fractions
|
||||
pub fn r32_r125_5050() -> Mixture {
|
||||
Mixture::from_mass_fractions(&[("R32", 0.5), ("R125", 0.5)]).unwrap()
|
||||
|
||||
Reference in New Issue
Block a user