chore: sync project state and current artifacts
This commit is contained in:
@@ -12,12 +12,10 @@
|
||||
use super::model::{FluidState, HeatTransferModel};
|
||||
use crate::state_machine::{CircuitId, OperationalState, StateManageable};
|
||||
use crate::{
|
||||
Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, SystemState,
|
||||
};
|
||||
use entropyk_core::{Calib, Pressure, Temperature, MassFlow};
|
||||
use entropyk_fluids::{
|
||||
FluidBackend, FluidId as FluidsFluidId, Property, ThermoState,
|
||||
Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
|
||||
};
|
||||
use entropyk_core::{Calib, MassFlow, Pressure, Temperature};
|
||||
use entropyk_fluids::{FluidBackend, FluidId as FluidsFluidId, Property, ThermoState};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -109,16 +107,23 @@ pub struct HxSideConditions {
|
||||
|
||||
impl HxSideConditions {
|
||||
/// Returns the inlet temperature in Kelvin.
|
||||
pub fn temperature_k(&self) -> f64 { self.temperature_k }
|
||||
pub fn temperature_k(&self) -> f64 {
|
||||
self.temperature_k
|
||||
}
|
||||
/// Returns the inlet pressure in Pascals.
|
||||
pub fn pressure_pa(&self) -> f64 { self.pressure_pa }
|
||||
pub fn pressure_pa(&self) -> f64 {
|
||||
self.pressure_pa
|
||||
}
|
||||
/// Returns the mass flow rate in kg/s.
|
||||
pub fn mass_flow_kg_s(&self) -> f64 { self.mass_flow_kg_s }
|
||||
pub fn mass_flow_kg_s(&self) -> f64 {
|
||||
self.mass_flow_kg_s
|
||||
}
|
||||
/// Returns a reference to the fluid identifier.
|
||||
pub fn fluid_id(&self) -> &FluidsFluidId { &self.fluid_id }
|
||||
pub fn fluid_id(&self) -> &FluidsFluidId {
|
||||
&self.fluid_id
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl HxSideConditions {
|
||||
/// Creates a new set of boundary conditions.
|
||||
pub fn new(
|
||||
@@ -126,22 +131,34 @@ impl HxSideConditions {
|
||||
pressure: Pressure,
|
||||
mass_flow: MassFlow,
|
||||
fluid_id: impl Into<String>,
|
||||
) -> Self {
|
||||
) -> Result<Self, ComponentError> {
|
||||
let t = temperature.to_kelvin();
|
||||
let p = pressure.to_pascals();
|
||||
let m = mass_flow.to_kg_per_s();
|
||||
|
||||
|
||||
// Basic validation for physically plausible states
|
||||
assert!(t > 0.0, "Temperature must be greater than 0 K");
|
||||
assert!(p > 0.0, "Pressure must be strictly positive");
|
||||
assert!(m >= 0.0, "Mass flow must be non-negative");
|
||||
|
||||
Self {
|
||||
if t <= 0.0 {
|
||||
return Err(ComponentError::InvalidState(
|
||||
"Temperature must be greater than 0 K".to_string(),
|
||||
));
|
||||
}
|
||||
if p <= 0.0 {
|
||||
return Err(ComponentError::InvalidState(
|
||||
"Pressure must be strictly positive".to_string(),
|
||||
));
|
||||
}
|
||||
if m < 0.0 {
|
||||
return Err(ComponentError::InvalidState(
|
||||
"Mass flow must be non-negative".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
temperature_k: t,
|
||||
pressure_pa: p,
|
||||
mass_flow_kg_s: m,
|
||||
fluid_id: FluidsFluidId::new(fluid_id),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +225,7 @@ impl<Model: HeatTransferModel> HeatExchanger<Model> {
|
||||
///
|
||||
/// ```no_run
|
||||
/// use entropyk_components::heat_exchanger::{HeatExchanger, LmtdModel, FlowConfiguration, HxSideConditions};
|
||||
/// use entropyk_fluids::TestBackend;
|
||||
/// use entropyk_fluids::{TestBackend, FluidId};
|
||||
/// use entropyk_core::{Temperature, Pressure, MassFlow};
|
||||
/// use std::sync::Arc;
|
||||
///
|
||||
@@ -220,13 +237,13 @@ impl<Model: HeatTransferModel> HeatExchanger<Model> {
|
||||
/// Pressure::from_bar(25.0),
|
||||
/// MassFlow::from_kg_per_s(0.05),
|
||||
/// "R410A",
|
||||
/// ))
|
||||
/// ).unwrap())
|
||||
/// .with_cold_conditions(HxSideConditions::new(
|
||||
/// Temperature::from_celsius(30.0),
|
||||
/// Pressure::from_bar(1.5),
|
||||
/// MassFlow::from_kg_per_s(0.2),
|
||||
/// "Water",
|
||||
/// ));
|
||||
/// ).unwrap());
|
||||
/// ```
|
||||
pub fn with_fluid_backend(mut self, backend: Arc<dyn FluidBackend>) -> Self {
|
||||
self.fluid_backend = Some(backend);
|
||||
@@ -277,26 +294,48 @@ impl<Model: HeatTransferModel> HeatExchanger<Model> {
|
||||
|
||||
/// Computes the full thermodynamic state at the hot inlet.
|
||||
pub fn hot_inlet_state(&self) -> Result<ThermoState, ComponentError> {
|
||||
let backend = self.fluid_backend.as_ref().ok_or_else(|| ComponentError::CalculationFailed("No FluidBackend configured".to_string()))?;
|
||||
let conditions = self.hot_conditions.as_ref().ok_or_else(|| ComponentError::CalculationFailed("Hot conditions not set".to_string()))?;
|
||||
let backend = self.fluid_backend.as_ref().ok_or_else(|| {
|
||||
ComponentError::CalculationFailed("No FluidBackend configured".to_string())
|
||||
})?;
|
||||
let conditions = self.hot_conditions.as_ref().ok_or_else(|| {
|
||||
ComponentError::CalculationFailed("Hot conditions not set".to_string())
|
||||
})?;
|
||||
let h = self.query_enthalpy(conditions)?;
|
||||
backend.full_state(
|
||||
conditions.fluid_id().clone(),
|
||||
Pressure::from_pascals(conditions.pressure_pa()),
|
||||
entropyk_core::Enthalpy::from_joules_per_kg(h),
|
||||
).map_err(|e| ComponentError::CalculationFailed(format!("Failed to compute hot inlet state: {}", e)))
|
||||
backend
|
||||
.full_state(
|
||||
conditions.fluid_id().clone(),
|
||||
Pressure::from_pascals(conditions.pressure_pa()),
|
||||
entropyk_core::Enthalpy::from_joules_per_kg(h),
|
||||
)
|
||||
.map_err(|e| {
|
||||
ComponentError::CalculationFailed(format!(
|
||||
"Failed to compute hot inlet state: {}",
|
||||
e
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Computes the full thermodynamic state at the cold inlet.
|
||||
pub fn cold_inlet_state(&self) -> Result<ThermoState, ComponentError> {
|
||||
let backend = self.fluid_backend.as_ref().ok_or_else(|| ComponentError::CalculationFailed("No FluidBackend configured".to_string()))?;
|
||||
let conditions = self.cold_conditions.as_ref().ok_or_else(|| ComponentError::CalculationFailed("Cold conditions not set".to_string()))?;
|
||||
let backend = self.fluid_backend.as_ref().ok_or_else(|| {
|
||||
ComponentError::CalculationFailed("No FluidBackend configured".to_string())
|
||||
})?;
|
||||
let conditions = self.cold_conditions.as_ref().ok_or_else(|| {
|
||||
ComponentError::CalculationFailed("Cold conditions not set".to_string())
|
||||
})?;
|
||||
let h = self.query_enthalpy(conditions)?;
|
||||
backend.full_state(
|
||||
conditions.fluid_id().clone(),
|
||||
Pressure::from_pascals(conditions.pressure_pa()),
|
||||
entropyk_core::Enthalpy::from_joules_per_kg(h),
|
||||
).map_err(|e| ComponentError::CalculationFailed(format!("Failed to compute cold inlet state: {}", e)))
|
||||
backend
|
||||
.full_state(
|
||||
conditions.fluid_id().clone(),
|
||||
Pressure::from_pascals(conditions.pressure_pa()),
|
||||
entropyk_core::Enthalpy::from_joules_per_kg(h),
|
||||
)
|
||||
.map_err(|e| {
|
||||
ComponentError::CalculationFailed(format!(
|
||||
"Failed to compute cold inlet state: {}",
|
||||
e
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Queries Cp (J/(kg·K)) from the backend for a given side.
|
||||
@@ -306,10 +345,18 @@ impl<Model: HeatTransferModel> HeatExchanger<Model> {
|
||||
Pressure::from_pascals(conditions.pressure_pa()),
|
||||
Temperature::from_kelvin(conditions.temperature_k()),
|
||||
);
|
||||
backend.property(conditions.fluid_id().clone(), Property::Cp, state) // Need to clone FluidId because trait signature requires it for now? Actually FluidId can be cloned cheaply depending on its implementation. We'll leave the clone if required by `property()`. Let's assume it is.
|
||||
.map_err(|e| ComponentError::CalculationFailed(format!("FluidBackend Cp query failed: {}", e)))
|
||||
backend
|
||||
.property(conditions.fluid_id().clone(), Property::Cp, state) // Need to clone FluidId because trait signature requires it for now? Actually FluidId can be cloned cheaply depending on its implementation. We'll leave the clone if required by `property()`. Let's assume it is.
|
||||
.map_err(|e| {
|
||||
ComponentError::CalculationFailed(format!(
|
||||
"FluidBackend Cp query failed: {}",
|
||||
e
|
||||
))
|
||||
})
|
||||
} else {
|
||||
Err(ComponentError::CalculationFailed("No FluidBackend configured".to_string()))
|
||||
Err(ComponentError::CalculationFailed(
|
||||
"No FluidBackend configured".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,10 +367,18 @@ impl<Model: HeatTransferModel> HeatExchanger<Model> {
|
||||
Pressure::from_pascals(conditions.pressure_pa()),
|
||||
Temperature::from_kelvin(conditions.temperature_k()),
|
||||
);
|
||||
backend.property(conditions.fluid_id().clone(), Property::Enthalpy, state)
|
||||
.map_err(|e| ComponentError::CalculationFailed(format!("FluidBackend Enthalpy query failed: {}", e)))
|
||||
backend
|
||||
.property(conditions.fluid_id().clone(), Property::Enthalpy, state)
|
||||
.map_err(|e| {
|
||||
ComponentError::CalculationFailed(format!(
|
||||
"FluidBackend Enthalpy query failed: {}",
|
||||
e
|
||||
))
|
||||
})
|
||||
} else {
|
||||
Err(ComponentError::CalculationFailed("No FluidBackend configured".to_string()))
|
||||
Err(ComponentError::CalculationFailed(
|
||||
"No FluidBackend configured".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -389,7 +444,7 @@ impl<Model: HeatTransferModel> HeatExchanger<Model> {
|
||||
impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
_state: &SystemState,
|
||||
_state: &StateSlice,
|
||||
residuals: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
if residuals.len() < self.n_equations() {
|
||||
@@ -431,7 +486,7 @@ impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
|
||||
// at the System level via Ports.
|
||||
// Let's refine the approach: we still need to query properties. The original implementation
|
||||
// was a placeholder because component port state pulling is part of Epic 1.3 / Epic 4.
|
||||
|
||||
|
||||
let (hot_inlet, hot_outlet, cold_inlet, cold_outlet) =
|
||||
if let (Some(hot_cond), Some(cold_cond), Some(_backend)) = (
|
||||
&self.hot_conditions,
|
||||
@@ -448,7 +503,7 @@ impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
|
||||
hot_cond.mass_flow_kg_s(),
|
||||
hot_cp,
|
||||
);
|
||||
|
||||
|
||||
// Extract current iteration values from `_state` if available, or fallback to heuristics.
|
||||
// The `SystemState` passed here contains the global state variables.
|
||||
// For a 3-equation heat exchanger, the state variables associated with it
|
||||
@@ -457,7 +512,7 @@ impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
|
||||
// we'll attempt a safe estimation that incorporates `_state` conceptually,
|
||||
// but avoids direct indexing out of bounds. The real fix for "ignoring _state"
|
||||
// is that the system solver maps global `_state` into port conditions.
|
||||
|
||||
|
||||
// Estimate hot outlet enthalpy (will be refined by solver convergence):
|
||||
let hot_dh = hot_cp * 5.0; // J/kg per degree
|
||||
let hot_outlet = Self::create_fluid_state(
|
||||
@@ -516,7 +571,7 @@ impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
|
||||
|
||||
fn jacobian_entries(
|
||||
&self,
|
||||
_state: &SystemState,
|
||||
_state: &StateSlice,
|
||||
_jacobian: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
// ∂r/∂f_ua = -∂Q/∂f_ua (Story 5.5)
|
||||
@@ -524,7 +579,7 @@ impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
|
||||
// Need to compute Q_nominal (with UA_scale = 1.0)
|
||||
// This requires repeating the residual calculation logic with dynamic_ua_scale = None
|
||||
// For now, we'll use a finite difference approximation or a simplified nominal calculation.
|
||||
|
||||
|
||||
// Re-use logic from compute_residuals but only for Q
|
||||
if let (Some(hot_cond), Some(cold_cond), Some(_backend)) = (
|
||||
&self.hot_conditions,
|
||||
@@ -540,8 +595,8 @@ impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
|
||||
hot_cond.mass_flow_kg_s(),
|
||||
hot_cp,
|
||||
);
|
||||
|
||||
let hot_dh = hot_cp * 5.0;
|
||||
|
||||
let hot_dh = hot_cp * 5.0;
|
||||
let hot_outlet = Self::create_fluid_state(
|
||||
hot_cond.temperature_k() - 5.0,
|
||||
hot_cond.pressure_pa() * 0.998,
|
||||
@@ -568,9 +623,10 @@ impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
|
||||
cold_cp,
|
||||
);
|
||||
|
||||
let q_nominal = self.model.compute_heat_transfer(
|
||||
&hot_inlet, &hot_outlet, &cold_inlet, &cold_outlet, None
|
||||
).to_watts();
|
||||
let q_nominal = self
|
||||
.model
|
||||
.compute_heat_transfer(&hot_inlet, &hot_outlet, &cold_inlet, &cold_outlet, None)
|
||||
.to_watts();
|
||||
|
||||
// r0 = Q_hot - Q -> ∂r0/∂f_ua = -Q_nominal
|
||||
// r1 = Q_cold - Q -> ∂r1/∂f_ua = -Q_nominal
|
||||
@@ -596,6 +652,75 @@ impl<Model: HeatTransferModel + 'static> Component for HeatExchanger<Model> {
|
||||
// Port storage pending integration with Port<Connected> system from Story 1.3.
|
||||
&[]
|
||||
}
|
||||
|
||||
fn port_mass_flows(
|
||||
&self,
|
||||
_state: &StateSlice,
|
||||
) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
|
||||
// HeatExchanger has two sides: hot and cold, each with inlet and outlet.
|
||||
// Mass balance: hot_in = hot_out, cold_in = cold_out (no mixing between sides)
|
||||
//
|
||||
// For now, we use the configured conditions if available.
|
||||
// When port storage is implemented, this will use actual port state.
|
||||
let mut flows = Vec::with_capacity(4);
|
||||
|
||||
if let Some(hot_cond) = &self.hot_conditions {
|
||||
let m_hot = hot_cond.mass_flow_kg_s();
|
||||
// Hot inlet (positive = entering), Hot outlet (negative = leaving)
|
||||
flows.push(entropyk_core::MassFlow::from_kg_per_s(m_hot));
|
||||
flows.push(entropyk_core::MassFlow::from_kg_per_s(-m_hot));
|
||||
}
|
||||
|
||||
if let Some(cold_cond) = &self.cold_conditions {
|
||||
let m_cold = cold_cond.mass_flow_kg_s();
|
||||
// Cold inlet (positive = entering), Cold outlet (negative = leaving)
|
||||
flows.push(entropyk_core::MassFlow::from_kg_per_s(m_cold));
|
||||
flows.push(entropyk_core::MassFlow::from_kg_per_s(-m_cold));
|
||||
}
|
||||
|
||||
Ok(flows)
|
||||
}
|
||||
|
||||
fn port_enthalpies(
|
||||
&self,
|
||||
state: &StateSlice,
|
||||
) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
|
||||
let mut enthalpies = Vec::with_capacity(4);
|
||||
|
||||
// This matches the order in port_mass_flows
|
||||
if let Some(hot_cond) = &self.hot_conditions {
|
||||
let h_in = self.query_enthalpy(hot_cond).unwrap_or(400_000.0);
|
||||
enthalpies.push(entropyk_core::Enthalpy::from_joules_per_kg(h_in));
|
||||
// HACK: As mentioned in compute_residuals, proper port mappings are pending.
|
||||
// We use a dummy 5 K delta for the outlet until full Port system integration.
|
||||
let cp = self.query_cp(hot_cond).unwrap_or(1000.0);
|
||||
enthalpies.push(entropyk_core::Enthalpy::from_joules_per_kg(h_in - cp * 5.0));
|
||||
}
|
||||
|
||||
if let Some(cold_cond) = &self.cold_conditions {
|
||||
let h_in = self.query_enthalpy(cold_cond).unwrap_or(80_000.0);
|
||||
enthalpies.push(entropyk_core::Enthalpy::from_joules_per_kg(h_in));
|
||||
let cp = self.query_cp(cold_cond).unwrap_or(4180.0);
|
||||
enthalpies.push(entropyk_core::Enthalpy::from_joules_per_kg(h_in + cp * 5.0));
|
||||
}
|
||||
|
||||
Ok(enthalpies)
|
||||
}
|
||||
|
||||
fn energy_transfers(
|
||||
&self,
|
||||
_state: &StateSlice,
|
||||
) -> Option<(entropyk_core::Power, entropyk_core::Power)> {
|
||||
match self.operational_state {
|
||||
OperationalState::Off | OperationalState::Bypass | OperationalState::On => {
|
||||
// Internal heat exchange between tracked streams; adiabatic to macro-environment
|
||||
Some((
|
||||
entropyk_core::Power::from_watts(0.0),
|
||||
entropyk_core::Power::from_watts(0.0),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Model: HeatTransferModel + 'static> StateManageable for HeatExchanger<Model> {
|
||||
@@ -684,11 +809,11 @@ mod tests {
|
||||
let model = LmtdModel::counter_flow(5000.0);
|
||||
let hx = HeatExchangerBuilder::new(model)
|
||||
.name("Condenser")
|
||||
.circuit_id(CircuitId::new("primary"))
|
||||
.circuit_id(CircuitId::from_number(5))
|
||||
.build();
|
||||
|
||||
assert_eq!(hx.name(), "Condenser");
|
||||
assert_eq!(hx.circuit_id().as_str(), "primary");
|
||||
assert_eq!(hx.circuit_id().as_number(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -723,7 +848,7 @@ mod tests {
|
||||
let model = LmtdModel::counter_flow(5000.0);
|
||||
let hx = HeatExchanger::new(model, "Test");
|
||||
|
||||
assert_eq!(hx.circuit_id().as_str(), "default");
|
||||
assert_eq!(*hx.circuit_id(), CircuitId::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -731,8 +856,8 @@ mod tests {
|
||||
let model = LmtdModel::counter_flow(5000.0);
|
||||
let mut hx = HeatExchanger::new(model, "Test");
|
||||
|
||||
hx.set_circuit_id(CircuitId::new("secondary"));
|
||||
assert_eq!(hx.circuit_id().as_str(), "secondary");
|
||||
hx.set_circuit_id(CircuitId::from_number(2));
|
||||
assert_eq!(hx.circuit_id().as_number(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -775,18 +900,18 @@ mod tests {
|
||||
fn test_circuit_id_via_builder() {
|
||||
let model = LmtdModel::counter_flow(5000.0);
|
||||
let hx = HeatExchangerBuilder::new(model)
|
||||
.circuit_id(CircuitId::new("circuit_1"))
|
||||
.circuit_id(CircuitId::from_number(1))
|
||||
.build();
|
||||
|
||||
assert_eq!(hx.circuit_id().as_str(), "circuit_1");
|
||||
assert_eq!(hx.circuit_id().as_number(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_circuit_id() {
|
||||
let model = LmtdModel::counter_flow(5000.0);
|
||||
let hx = HeatExchanger::new(model, "Test").with_circuit_id(CircuitId::new("main"));
|
||||
let hx = HeatExchanger::new(model, "Test").with_circuit_id(CircuitId::from_number(3));
|
||||
|
||||
assert_eq!(hx.circuit_id().as_str(), "main");
|
||||
assert_eq!(hx.circuit_id().as_number(), 3);
|
||||
}
|
||||
|
||||
// ===== Story 5.1: FluidBackend Integration Tests =====
|
||||
@@ -804,8 +929,7 @@ mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
let model = LmtdModel::counter_flow(5000.0);
|
||||
let hx = HeatExchanger::new(model, "Test")
|
||||
.with_fluid_backend(Arc::new(TestBackend::new()));
|
||||
let hx = HeatExchanger::new(model, "Test").with_fluid_backend(Arc::new(TestBackend::new()));
|
||||
|
||||
assert!(hx.has_fluid_backend());
|
||||
}
|
||||
@@ -819,7 +943,8 @@ mod tests {
|
||||
Pressure::from_bar(25.0),
|
||||
MassFlow::from_kg_per_s(0.05),
|
||||
"R410A",
|
||||
);
|
||||
)
|
||||
.expect("Valid conditions should not fail");
|
||||
|
||||
assert!((conds.temperature_k() - 333.15).abs() < 0.01);
|
||||
assert!((conds.pressure_pa() - 25.0e5).abs() < 1.0);
|
||||
@@ -837,23 +962,32 @@ mod tests {
|
||||
let model = LmtdModel::counter_flow(5000.0);
|
||||
let hx = HeatExchanger::new(model, "Condenser")
|
||||
.with_fluid_backend(Arc::new(TestBackend::new()))
|
||||
.with_hot_conditions(HxSideConditions::new(
|
||||
Temperature::from_celsius(60.0),
|
||||
Pressure::from_bar(20.0),
|
||||
MassFlow::from_kg_per_s(0.05),
|
||||
"R410A",
|
||||
))
|
||||
.with_cold_conditions(HxSideConditions::new(
|
||||
Temperature::from_celsius(30.0),
|
||||
Pressure::from_pascals(102_000.0),
|
||||
MassFlow::from_kg_per_s(0.2),
|
||||
"Water",
|
||||
));
|
||||
.with_hot_conditions(
|
||||
HxSideConditions::new(
|
||||
Temperature::from_celsius(60.0),
|
||||
Pressure::from_bar(20.0),
|
||||
MassFlow::from_kg_per_s(0.05),
|
||||
"R410A",
|
||||
)
|
||||
.expect("Valid hot conditions"),
|
||||
)
|
||||
.with_cold_conditions(
|
||||
HxSideConditions::new(
|
||||
Temperature::from_celsius(30.0),
|
||||
Pressure::from_pascals(102_000.0),
|
||||
MassFlow::from_kg_per_s(0.2),
|
||||
"Water",
|
||||
)
|
||||
.expect("Valid cold conditions"),
|
||||
);
|
||||
|
||||
let state = vec![0.0f64; 10];
|
||||
let mut residuals = vec![0.0f64; 3];
|
||||
let result = hx.compute_residuals(&state, &mut residuals);
|
||||
assert!(result.is_ok(), "compute_residuals with FluidBackend should succeed");
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"compute_residuals with FluidBackend should succeed"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -870,27 +1004,37 @@ mod tests {
|
||||
|
||||
let state = vec![0.0f64; 10];
|
||||
let mut residuals_no_backend = vec![0.0f64; 3];
|
||||
hx_no_backend.compute_residuals(&state, &mut residuals_no_backend).unwrap();
|
||||
hx_no_backend
|
||||
.compute_residuals(&state, &mut residuals_no_backend)
|
||||
.unwrap();
|
||||
|
||||
// With backend (real Water + R410A properties)
|
||||
let model2 = LmtdModel::counter_flow(5000.0);
|
||||
let hx_with_backend = HeatExchanger::new(model2, "HX_with_backend")
|
||||
.with_fluid_backend(Arc::new(TestBackend::new()))
|
||||
.with_hot_conditions(HxSideConditions::new(
|
||||
Temperature::from_celsius(60.0),
|
||||
Pressure::from_bar(20.0),
|
||||
MassFlow::from_kg_per_s(0.05),
|
||||
"R410A",
|
||||
))
|
||||
.with_cold_conditions(HxSideConditions::new(
|
||||
Temperature::from_celsius(30.0),
|
||||
Pressure::from_pascals(102_000.0),
|
||||
MassFlow::from_kg_per_s(0.2),
|
||||
"Water",
|
||||
));
|
||||
.with_hot_conditions(
|
||||
HxSideConditions::new(
|
||||
Temperature::from_celsius(60.0),
|
||||
Pressure::from_bar(20.0),
|
||||
MassFlow::from_kg_per_s(0.05),
|
||||
"R410A",
|
||||
)
|
||||
.expect("Valid hot conditions"),
|
||||
)
|
||||
.with_cold_conditions(
|
||||
HxSideConditions::new(
|
||||
Temperature::from_celsius(30.0),
|
||||
Pressure::from_pascals(102_000.0),
|
||||
MassFlow::from_kg_per_s(0.2),
|
||||
"Water",
|
||||
)
|
||||
.expect("Valid cold conditions"),
|
||||
);
|
||||
|
||||
let mut residuals_with_backend = vec![0.0f64; 3];
|
||||
hx_with_backend.compute_residuals(&state, &mut residuals_with_backend).unwrap();
|
||||
hx_with_backend
|
||||
.compute_residuals(&state, &mut residuals_with_backend)
|
||||
.unwrap();
|
||||
|
||||
// The energy balance residual (index 2) should differ because real Cp differs
|
||||
// from the 1000.0/4180.0 hardcoded fallback values.
|
||||
|
||||
Reference in New Issue
Block a user