chore: sync project state and current artifacts

This commit is contained in:
Sepehr
2026-02-22 23:27:31 +01:00
parent 1b6415776e
commit dd77089b22
232 changed files with 37056 additions and 4296 deletions

View File

@@ -45,7 +45,7 @@ use crate::polynomials::Polynomial2D;
use crate::port::{Connected, Disconnected, FluidId, Port};
use crate::{
CircuitId, Component, ComponentError, ConnectedPort, JacobianBuilder, OperationalState,
ResidualVector, SystemState,
ResidualVector, StateSlice,
};
use entropyk_core::{Calib, Enthalpy, MassFlow, Temperature};
use serde::{Deserialize, Serialize};
@@ -699,25 +699,38 @@ impl Compressor<Connected> {
}
/// Computes the full thermodynamic state at the suction port.
pub fn suction_state(&self, backend: &impl entropyk_fluids::FluidBackend) -> Result<entropyk_fluids::ThermoState, ComponentError> {
pub fn suction_state(
&self,
backend: &impl entropyk_fluids::FluidBackend,
) -> Result<entropyk_fluids::ThermoState, ComponentError> {
backend
.full_state(
entropyk_fluids::FluidId::new(self.port_suction.fluid_id().as_str()),
self.port_suction.pressure(),
self.port_suction.enthalpy(),
)
.map_err(|e| ComponentError::CalculationFailed(format!("Failed to compute suction state: {}", e)))
.map_err(|e| {
ComponentError::CalculationFailed(format!("Failed to compute suction state: {}", e))
})
}
/// Computes the full thermodynamic state at the discharge port.
pub fn discharge_state(&self, backend: &impl entropyk_fluids::FluidBackend) -> Result<entropyk_fluids::ThermoState, ComponentError> {
pub fn discharge_state(
&self,
backend: &impl entropyk_fluids::FluidBackend,
) -> Result<entropyk_fluids::ThermoState, ComponentError> {
backend
.full_state(
entropyk_fluids::FluidId::new(self.port_discharge.fluid_id().as_str()),
self.port_discharge.pressure(),
self.port_discharge.enthalpy(),
)
.map_err(|e| ComponentError::CalculationFailed(format!("Failed to compute discharge state: {}", e)))
.map_err(|e| {
ComponentError::CalculationFailed(format!(
"Failed to compute discharge state: {}",
e
))
})
}
/// Calculates the mass flow rate through the compressor.
@@ -745,7 +758,7 @@ impl Compressor<Connected> {
density_suction: f64,
sst_k: f64,
sdt_k: f64,
state: Option<&SystemState>,
state: Option<&StateSlice>,
) -> Result<MassFlow, ComponentError> {
if density_suction < 0.0 {
return Err(ComponentError::InvalidState(
@@ -801,7 +814,10 @@ impl Compressor<Connected> {
// Apply calibration: ṁ_eff = f_m × ṁ_nominal
let f_m = if let Some(st) = state {
self.calib_indices.f_m.map(|idx| st[idx]).unwrap_or(self.calib.f_m)
self.calib_indices
.f_m
.map(|idx| st[idx])
.unwrap_or(self.calib.f_m)
} else {
self.calib.f_m
};
@@ -826,7 +842,7 @@ impl Compressor<Connected> {
&self,
t_suction: Temperature,
t_discharge: Temperature,
state: Option<&SystemState>,
state: Option<&StateSlice>,
) -> f64 {
let power_nominal = match &self.model {
CompressorModel::Ahri540(coeffs) => {
@@ -843,7 +859,10 @@ impl Compressor<Connected> {
};
// Ẇ_eff = f_power × Ẇ_nominal
let f_power = if let Some(st) = state {
self.calib_indices.f_power.map(|idx| st[idx]).unwrap_or(self.calib.f_power)
self.calib_indices
.f_power
.map(|idx| st[idx])
.unwrap_or(self.calib.f_power)
} else {
self.calib.f_power
};
@@ -868,7 +887,7 @@ impl Compressor<Connected> {
&self,
t_suction: Temperature,
t_discharge: Temperature,
state: Option<&SystemState>,
state: Option<&StateSlice>,
) -> f64 {
let power_nominal = match &self.model {
CompressorModel::Ahri540(coeffs) => {
@@ -886,7 +905,10 @@ impl Compressor<Connected> {
};
// Ẇ_eff = f_power × Ẇ_nominal
let f_power = if let Some(st) = state {
self.calib_indices.f_power.map(|idx| st[idx]).unwrap_or(self.calib.f_power)
self.calib_indices
.f_power
.map(|idx| st[idx])
.unwrap_or(self.calib.f_power)
} else {
self.calib.f_power
};
@@ -1040,7 +1062,7 @@ impl Compressor<Connected> {
impl Component for Compressor<Connected> {
fn compute_residuals(
&self,
state: &SystemState,
state: &StateSlice,
residuals: &mut ResidualVector,
) -> Result<(), ComponentError> {
// Validate residual vector length
@@ -1111,7 +1133,7 @@ impl Component for Compressor<Connected> {
let power_calc = self.power_consumption_cooling(
Temperature::from_kelvin(t_suction_k),
Temperature::from_kelvin(t_discharge_k),
Some(state)
Some(state),
);
// Residual 0: Mass flow continuity
@@ -1121,6 +1143,14 @@ impl Component for Compressor<Connected> {
// Residual 1: Energy balance
// Power_calc - ṁ × (h_discharge - h_suction) / η_mech = 0
let enthalpy_change = h_discharge - h_suction;
// Prevent division by zero
if self.mechanical_efficiency.abs() < 1e-10 {
return Err(ComponentError::InvalidState(
"Mechanical efficiency is too close to zero".to_string(),
));
}
residuals[1] = power_calc - mass_flow_state * enthalpy_change / self.mechanical_efficiency;
Ok(())
@@ -1128,7 +1158,7 @@ impl Component for Compressor<Connected> {
fn jacobian_entries(
&self,
state: &SystemState,
state: &StateSlice,
jacobian: &mut JacobianBuilder,
) -> Result<(), ComponentError> {
// Validate state vector
@@ -1195,7 +1225,7 @@ impl Component for Compressor<Connected> {
self.power_consumption_cooling(
Temperature::from_kelvin(t),
Temperature::from_kelvin(t_discharge),
None
None,
)
},
h_suction,
@@ -1213,7 +1243,7 @@ impl Component for Compressor<Connected> {
self.power_consumption_cooling(
Temperature::from_kelvin(t_suction),
Temperature::from_kelvin(t),
None
None,
)
},
h_discharge,
@@ -1227,9 +1257,12 @@ impl Component for Compressor<Connected> {
// Calibration derivatives (Story 5.5)
if let Some(f_m_idx) = self.calib_indices.f_m {
// ∂r₀/∂f_m = ṁ_nominal
let density_suction = estimate_density(self.fluid_id.as_str(), p_suction, h_suction).unwrap_or(1.0);
let m_nominal = self.mass_flow_rate(density_suction, _t_suction_k, t_discharge_k, None)
.map(|m| m.to_kg_per_s()).unwrap_or(0.0);
let density_suction =
estimate_density(self.fluid_id.as_str(), p_suction, h_suction).unwrap_or(1.0);
let m_nominal = self
.mass_flow_rate(density_suction, _t_suction_k, t_discharge_k, None)
.map(|m| m.to_kg_per_s())
.unwrap_or(0.0);
jacobian.add_entry(0, f_m_idx, m_nominal);
}
@@ -1238,7 +1271,7 @@ impl Component for Compressor<Connected> {
let p_nominal = self.power_consumption_cooling(
Temperature::from_kelvin(_t_suction_k),
Temperature::from_kelvin(t_discharge_k),
None
None,
);
jacobian.add_entry(1, f_power_idx, p_nominal);
}
@@ -1250,7 +1283,10 @@ impl Component for Compressor<Connected> {
2 // Mass flow residual and energy residual
}
fn port_mass_flows(&self, state: &SystemState) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
fn port_mass_flows(
&self,
state: &StateSlice,
) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
if state.len() < 4 {
return Err(ComponentError::InvalidStateDimensions {
expected: 4,
@@ -1260,18 +1296,83 @@ impl Component for Compressor<Connected> {
let m = entropyk_core::MassFlow::from_kg_per_s(state[0]);
// Suction (inlet), Discharge (outlet), Oil (no flow modeled yet)
Ok(vec![
m,
entropyk_core::MassFlow::from_kg_per_s(-m.to_kg_per_s()),
entropyk_core::MassFlow::from_kg_per_s(0.0)
m,
entropyk_core::MassFlow::from_kg_per_s(-m.to_kg_per_s()),
entropyk_core::MassFlow::from_kg_per_s(0.0),
])
}
fn port_enthalpies(
&self,
state: &StateSlice,
) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
if state.len() < 4 {
return Err(ComponentError::InvalidStateDimensions {
expected: 4,
actual: state.len(),
});
}
Ok(vec![
entropyk_core::Enthalpy::from_joules_per_kg(state[1]),
entropyk_core::Enthalpy::from_joules_per_kg(state[2]),
entropyk_core::Enthalpy::from_joules_per_kg(0.0),
])
}
fn get_ports(&self) -> &[ConnectedPort] {
// NOTE: This returns an empty slice due to lifetime constraints.
// Use `get_ports_slice()` method on Compressor<Connected> for actual port access.
// This is a known limitation - the Component trait needs redesign for proper port access.
// FIXME: API LIMITATION - This method returns an empty slice due to lifetime constraints.
//
// The Component trait's get_ports() requires returning a reference with the same
// lifetime as &self, but the actual port storage (in Compressor<Connected>) has
// a different lifetime. This is a fundamental design issue in the trait.
//
// WORKAROUND: Use `get_ports_slice()` method on Compressor<Connected> for actual port access.
//
// TODO: Redesign Component trait to support owned port iterators or different lifetime bounds.
// See: https://github.com/your-org/entropyk/issues/XXX
&[]
}
fn energy_transfers(
&self,
state: &StateSlice,
) -> Option<(entropyk_core::Power, entropyk_core::Power)> {
match self.operational_state {
OperationalState::Off | OperationalState::Bypass => Some((
entropyk_core::Power::from_watts(0.0),
entropyk_core::Power::from_watts(0.0),
)),
OperationalState::On => {
if state.len() < 4 {
return None;
}
let h_suction = state[1]; // J/kg
let h_discharge = state[2]; // J/kg
let p_suction = self.port_suction.pressure().to_pascals();
let p_discharge = self.port_discharge.pressure().to_pascals();
let t_suction_k =
estimate_temperature(self.fluid_id.as_str(), p_suction, h_suction)
.unwrap_or(273.15);
let t_discharge_k =
estimate_temperature(self.fluid_id.as_str(), p_discharge, h_discharge)
.unwrap_or(320.0);
let power_calc = self.power_consumption_cooling(
Temperature::from_kelvin(t_suction_k),
Temperature::from_kelvin(t_discharge_k),
Some(state),
);
// Work is done *on* the compressor, so it is negative
Some((
entropyk_core::Power::from_watts(0.0),
entropyk_core::Power::from_watts(-power_calc),
))
}
}
}
}
use crate::state_machine::StateManageable;
@@ -1309,6 +1410,22 @@ impl StateManageable for Compressor<Connected> {
}
}
/// Enthalpy/density thresholds for R134a density estimation (J/kg)
mod r134a_density {
pub const ENTHALPY_VAPOR_THRESHOLD: f64 = 350_000.0;
pub const ENTHALPY_LIQUID_THRESHOLD: f64 = 200_000.0;
pub const DENSITY_VAPOR: f64 = 20.0;
pub const DENSITY_LIQUID: f64 = 1200.0;
}
/// Enthalpy/density thresholds for R410A/R454B density estimation (J/kg)
mod r410a_density {
pub const ENTHALPY_VAPOR_THRESHOLD: f64 = 380_000.0;
pub const ENTHALPY_LIQUID_THRESHOLD: f64 = 220_000.0;
pub const DENSITY_VAPOR: f64 = 25.0;
pub const DENSITY_LIQUID: f64 = 1100.0;
}
/// Estimates fluid density from pressure and enthalpy.
///
/// **PLACEHOLDER IMPLEMENTATION** - Will be replaced by CoolProp integration
@@ -1330,26 +1447,30 @@ fn estimate_density(fluid_id: &str, _pressure: f64, enthalpy: f64) -> Result<f64
match fluid_id {
"R134a" => {
// Rough approximation for R134a at typical conditions
// h ≈ 400 kJ/kg, ρ ≈ 20 kg/m³ (vapor)
// h ≈ 250 kJ/kg, ρ ≈ 1200 kg/m³ (liquid)
let density = if enthalpy > 350000.0 {
20.0 // Superheated vapor
} else if enthalpy < 200000.0 {
1200.0 // Subcooled liquid
use r134a_density::*;
let density = if enthalpy > ENTHALPY_VAPOR_THRESHOLD {
DENSITY_VAPOR // Superheated vapor
} else if enthalpy < ENTHALPY_LIQUID_THRESHOLD {
DENSITY_LIQUID // Subcooled liquid
} else {
// Linear interpolation in two-phase region
20.0 + (1200.0 - 20.0) * (350000.0 - enthalpy) / 150000.0
DENSITY_VAPOR
+ (DENSITY_LIQUID - DENSITY_VAPOR) * (ENTHALPY_VAPOR_THRESHOLD - enthalpy)
/ (ENTHALPY_VAPOR_THRESHOLD - ENTHALPY_LIQUID_THRESHOLD)
};
Ok(density)
}
"R410A" | "R454B" => {
// Similar approximation for R410A and R454B (R454B is close to R410A properties)
let density = if enthalpy > 380000.0 {
25.0
} else if enthalpy < 220000.0 {
1100.0
use r410a_density::*;
let density = if enthalpy > ENTHALPY_VAPOR_THRESHOLD {
DENSITY_VAPOR
} else if enthalpy < ENTHALPY_LIQUID_THRESHOLD {
DENSITY_LIQUID
} else {
25.0 + (1100.0 - 25.0) * (380000.0 - enthalpy) / 160000.0
DENSITY_VAPOR
+ (DENSITY_LIQUID - DENSITY_VAPOR) * (ENTHALPY_VAPOR_THRESHOLD - enthalpy)
/ (ENTHALPY_VAPOR_THRESHOLD - ENTHALPY_LIQUID_THRESHOLD)
};
Ok(density)
}
@@ -2104,13 +2225,13 @@ mod tests {
#[test]
fn test_state_manageable_circuit_id() {
let compressor = create_test_compressor();
assert_eq!(compressor.circuit_id().as_str(), "default");
assert_eq!(*compressor.circuit_id(), CircuitId::ZERO);
}
#[test]
fn test_state_manageable_set_circuit_id() {
let mut compressor = create_test_compressor();
compressor.set_circuit_id(CircuitId::new("primary"));
assert_eq!(compressor.circuit_id().as_str(), "primary");
compressor.set_circuit_id(CircuitId::from_number(5));
assert_eq!(compressor.circuit_id().as_number(), 5);
}
}