chore: sync project state and current artifacts
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user