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

@@ -74,7 +74,7 @@
//! ```
use crate::{
Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, SystemState,
Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};
// ─────────────────────────────────────────────────────────────────────────────
@@ -195,7 +195,12 @@ impl FlowSplitter {
"FlowSplitter with 1 outlet is just a pipe — use a Pipe component instead".into(),
));
}
Ok(Self { kind, fluid_id: fluid, inlet, outlets })
Ok(Self {
kind,
fluid_id: fluid,
inlet,
outlets,
})
}
// ── Accessors ─────────────────────────────────────────────────────────────
@@ -238,7 +243,7 @@ impl Component for FlowSplitter {
fn compute_residuals(
&self,
_state: &SystemState,
_state: &StateSlice,
residuals: &mut ResidualVector,
) -> Result<(), ComponentError> {
let n_eqs = self.n_equations();
@@ -286,7 +291,7 @@ impl Component for FlowSplitter {
fn jacobian_entries(
&self,
_state: &SystemState,
_state: &StateSlice,
jacobian: &mut JacobianBuilder,
) -> Result<(), ComponentError> {
// All residuals are linear differences → constant Jacobian.
@@ -312,6 +317,65 @@ impl Component for FlowSplitter {
// the actual solver coupling is via the System graph edges.
&[]
}
fn port_mass_flows(
&self,
state: &StateSlice,
) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
// FlowSplitter: 1 inlet → N outlets
// Mass balance: inlet = sum of outlets
// State layout: [m_in, m_out_1, m_out_2, ...]
let n_outlets = self.n_outlets();
if state.len() < 1 + n_outlets {
return Err(ComponentError::InvalidStateDimensions {
expected: 1 + n_outlets,
actual: state.len(),
});
}
let mut flows = Vec::with_capacity(1 + n_outlets);
// Inlet (positive = entering)
flows.push(entropyk_core::MassFlow::from_kg_per_s(state[0]));
// Outlets (negative = leaving)
for i in 0..n_outlets {
flows.push(entropyk_core::MassFlow::from_kg_per_s(-state[1 + i]));
}
Ok(flows)
}
/// Returns the enthalpies of all ports (inlet first, then outlets).
///
/// For a flow splitter, the enthalpy is conserved across branches:
/// `h_in = h_out_1 = h_out_2 = ...` (isenthalpic split).
fn port_enthalpies(
&self,
_state: &StateSlice,
) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
let mut enthalpies = Vec::with_capacity(1 + self.outlets.len());
enthalpies.push(self.inlet.enthalpy());
for outlet in &self.outlets {
enthalpies.push(outlet.enthalpy());
}
Ok(enthalpies)
}
/// Returns the energy transfers for the flow splitter.
///
/// A flow splitter is adiabatic:
/// - **Heat (Q)**: 0 W (no heat exchange with environment)
/// - **Work (W)**: 0 W (no mechanical work)
fn energy_transfers(
&self,
_state: &StateSlice,
) -> Option<(entropyk_core::Power, entropyk_core::Power)> {
Some((
entropyk_core::Power::from_watts(0.0),
entropyk_core::Power::from_watts(0.0),
))
}
}
// ─────────────────────────────────────────────────────────────────────────────
@@ -462,7 +526,10 @@ impl FlowMerger {
let total_flow: f64 = weights.iter().sum();
if total_flow <= 0.0 {
// Fall back to equal weighting
self.inlets.iter().map(|p| p.enthalpy().to_joules_per_kg()).sum::<f64>()
self.inlets
.iter()
.map(|p| p.enthalpy().to_joules_per_kg())
.sum::<f64>()
/ n as f64
} else {
self.inlets
@@ -475,7 +542,10 @@ impl FlowMerger {
}
None => {
// Equal weighting
self.inlets.iter().map(|p| p.enthalpy().to_joules_per_kg()).sum::<f64>()
self.inlets
.iter()
.map(|p| p.enthalpy().to_joules_per_kg())
.sum::<f64>()
/ n as f64
}
}
@@ -493,7 +563,7 @@ impl Component for FlowMerger {
fn compute_residuals(
&self,
_state: &SystemState,
_state: &StateSlice,
residuals: &mut ResidualVector,
) -> Result<(), ComponentError> {
let n_eqs = self.n_equations();
@@ -529,7 +599,7 @@ impl Component for FlowMerger {
fn jacobian_entries(
&self,
_state: &SystemState,
_state: &StateSlice,
jacobian: &mut JacobianBuilder,
) -> Result<(), ComponentError> {
// Diagonal approximation — the full coupling is resolved by the System
@@ -544,6 +614,65 @@ impl Component for FlowMerger {
fn get_ports(&self) -> &[ConnectedPort] {
&[]
}
fn port_mass_flows(
&self,
state: &StateSlice,
) -> Result<Vec<entropyk_core::MassFlow>, ComponentError> {
// FlowMerger: N inlets → 1 outlet
// Mass balance: sum of inlets = outlet
// State layout: [m_in_1, m_in_2, ..., m_out]
let n_inlets = self.n_inlets();
if state.len() < n_inlets + 1 {
return Err(ComponentError::InvalidStateDimensions {
expected: n_inlets + 1,
actual: state.len(),
});
}
let mut flows = Vec::with_capacity(n_inlets + 1);
// Inlets (positive = entering)
for i in 0..n_inlets {
flows.push(entropyk_core::MassFlow::from_kg_per_s(state[i]));
}
// Outlet (negative = leaving)
flows.push(entropyk_core::MassFlow::from_kg_per_s(-state[n_inlets]));
Ok(flows)
}
/// Returns the enthalpies of all ports (inlets first, then outlet).
///
/// For a flow merger, the outlet enthalpy is determined by
/// the mixing of inlet streams (mass-weighted average).
fn port_enthalpies(
&self,
_state: &StateSlice,
) -> Result<Vec<entropyk_core::Enthalpy>, ComponentError> {
let mut enthalpies = Vec::with_capacity(self.inlets.len() + 1);
for inlet in &self.inlets {
enthalpies.push(inlet.enthalpy());
}
enthalpies.push(self.outlet.enthalpy());
Ok(enthalpies)
}
/// Returns the energy transfers for the flow merger.
///
/// A flow merger is adiabatic:
/// - **Heat (Q)**: 0 W (no heat exchange with environment)
/// - **Work (W)**: 0 W (no mechanical work)
fn energy_transfers(
&self,
_state: &StateSlice,
) -> Option<(entropyk_core::Power, entropyk_core::Power)> {
Some((
entropyk_core::Power::from_watts(0.0),
entropyk_core::Power::from_watts(0.0),
))
}
}
// ─────────────────────────────────────────────────────────────────────────────
@@ -599,8 +728,8 @@ mod tests {
#[test]
fn test_splitter_incompressible_creation() {
let inlet = make_port("Water", 3.0e5, 2.0e5);
let out_a = make_port("Water", 3.0e5, 2.0e5);
let out_b = make_port("Water", 3.0e5, 2.0e5);
let out_a = make_port("Water", 3.0e5, 2.0e5);
let out_b = make_port("Water", 3.0e5, 2.0e5);
let s = FlowSplitter::incompressible("Water", inlet, vec![out_a, out_b]).unwrap();
assert_eq!(s.n_outlets(), 2);
@@ -612,9 +741,9 @@ mod tests {
#[test]
fn test_splitter_compressible_creation() {
let inlet = make_port("R410A", 24.0e5, 4.65e5);
let out_a = make_port("R410A", 24.0e5, 4.65e5);
let out_b = make_port("R410A", 24.0e5, 4.65e5);
let out_c = make_port("R410A", 24.0e5, 4.65e5);
let out_a = make_port("R410A", 24.0e5, 4.65e5);
let out_b = make_port("R410A", 24.0e5, 4.65e5);
let out_c = make_port("R410A", 24.0e5, 4.65e5);
let s = FlowSplitter::compressible("R410A", inlet, vec![out_a, out_b, out_c]).unwrap();
assert_eq!(s.n_outlets(), 3);
@@ -626,16 +755,19 @@ mod tests {
#[test]
fn test_splitter_rejects_refrigerant_as_incompressible() {
let inlet = make_port("R410A", 24.0e5, 4.65e5);
let out_a = make_port("R410A", 24.0e5, 4.65e5);
let out_b = make_port("R410A", 24.0e5, 4.65e5);
let out_a = make_port("R410A", 24.0e5, 4.65e5);
let out_b = make_port("R410A", 24.0e5, 4.65e5);
let result = FlowSplitter::incompressible("R410A", inlet, vec![out_a, out_b]);
assert!(result.is_err(), "R410A should not be accepted as incompressible");
assert!(
result.is_err(),
"R410A should not be accepted as incompressible"
);
}
#[test]
fn test_splitter_rejects_single_outlet() {
let inlet = make_port("Water", 3.0e5, 2.0e5);
let out = make_port("Water", 3.0e5, 2.0e5);
let out = make_port("Water", 3.0e5, 2.0e5);
let result = FlowSplitter::incompressible("Water", inlet, vec![out]);
assert!(result.is_err());
}
@@ -644,8 +776,8 @@ mod tests {
fn test_splitter_residuals_zero_at_consistent_state() {
// Consistent state: all pressures and enthalpies equal
let inlet = make_port("Water", 3.0e5, 2.0e5);
let out_a = make_port("Water", 3.0e5, 2.0e5);
let out_b = make_port("Water", 3.0e5, 2.0e5);
let out_a = make_port("Water", 3.0e5, 2.0e5);
let out_b = make_port("Water", 3.0e5, 2.0e5);
let s = FlowSplitter::incompressible("Water", inlet, vec![out_a, out_b]).unwrap();
let state = vec![0.0; 6]; // dummy, not used by current impl
@@ -656,7 +788,8 @@ mod tests {
assert!(
r.abs() < 1.0,
"residual[{}] = {} should be ≈ 0 for consistent state",
i, r
i,
r
);
}
}
@@ -664,8 +797,8 @@ mod tests {
#[test]
fn test_splitter_residuals_nonzero_on_pressure_mismatch() {
let inlet = make_port("Water", 3.0e5, 2.0e5);
let out_a = make_port("Water", 2.5e5, 2.0e5); // lower pressure!
let out_b = make_port("Water", 3.0e5, 2.0e5);
let out_a = make_port("Water", 2.5e5, 2.0e5); // lower pressure!
let out_b = make_port("Water", 3.0e5, 2.0e5);
let s = FlowSplitter::incompressible("Water", inlet, vec![out_a, out_b]).unwrap();
let state = vec![0.0; 6];
@@ -673,7 +806,11 @@ mod tests {
s.compute_residuals(&state, &mut res).unwrap();
// r[0] = P_out_a - P_in = 2.5e5 - 3.0e5 = -0.5e5
assert!((res[0] - (-0.5e5)).abs() < 1.0, "expected -0.5e5, got {}", res[0]);
assert!(
(res[0] - (-0.5e5)).abs() < 1.0,
"expected -0.5e5, got {}",
res[0]
);
}
#[test]
@@ -688,8 +825,8 @@ mod tests {
#[test]
fn test_splitter_water_type_aliases() {
let inlet = make_port("Water", 3.0e5, 2.0e5);
let out_a = make_port("Water", 3.0e5, 2.0e5);
let out_b = make_port("Water", 3.0e5, 2.0e5);
let out_a = make_port("Water", 3.0e5, 2.0e5);
let out_b = make_port("Water", 3.0e5, 2.0e5);
// IncompressibleSplitter is a type alias for FlowSplitter
let _s: IncompressibleSplitter =
@@ -700,8 +837,8 @@ mod tests {
#[test]
fn test_merger_incompressible_creation() {
let in_a = make_port("Water", 3.0e5, 2.0e5);
let in_b = make_port("Water", 3.0e5, 2.4e5);
let in_a = make_port("Water", 3.0e5, 2.0e5);
let in_b = make_port("Water", 3.0e5, 2.4e5);
let outlet = make_port("Water", 3.0e5, 2.2e5);
let m = FlowMerger::incompressible("Water", vec![in_a, in_b], outlet).unwrap();
@@ -713,9 +850,9 @@ mod tests {
#[test]
fn test_merger_compressible_creation() {
let in_a = make_port("R134a", 8.0e5, 4.0e5);
let in_b = make_port("R134a", 8.0e5, 4.2e5);
let in_c = make_port("R134a", 8.0e5, 3.8e5);
let in_a = make_port("R134a", 8.0e5, 4.0e5);
let in_b = make_port("R134a", 8.0e5, 4.2e5);
let in_c = make_port("R134a", 8.0e5, 3.8e5);
let outlet = make_port("R134a", 8.0e5, 4.0e5);
let m = FlowMerger::compressible("R134a", vec![in_a, in_b, in_c], outlet).unwrap();
@@ -727,7 +864,7 @@ mod tests {
#[test]
fn test_merger_rejects_single_inlet() {
let in_a = make_port("Water", 3.0e5, 2.0e5);
let in_a = make_port("Water", 3.0e5, 2.0e5);
let outlet = make_port("Water", 3.0e5, 2.0e5);
let result = FlowMerger::incompressible("Water", vec![in_a], outlet);
assert!(result.is_err());
@@ -738,8 +875,8 @@ mod tests {
// Equal branches → mixed enthalpy = inlet enthalpy
let h = 2.0e5_f64;
let p = 3.0e5_f64;
let in_a = make_port("Water", p, h);
let in_b = make_port("Water", p, h);
let in_a = make_port("Water", p, h);
let in_b = make_port("Water", p, h);
let outlet = make_port("Water", p, h); // h_mixed = (h+h)/2 = h
let m = FlowMerger::incompressible("Water", vec![in_a, in_b], outlet).unwrap();
@@ -759,8 +896,8 @@ mod tests {
let h_expected = (h_a + h_b) / 2.0; // equal-weight average
let p = 3.0e5_f64;
let in_a = make_port("Water", p, h_a);
let in_b = make_port("Water", p, h_b);
let in_a = make_port("Water", p, h_a);
let in_b = make_port("Water", p, h_b);
let outlet = make_port("Water", p, h_expected);
let m = FlowMerger::incompressible("Water", vec![in_a, in_b], outlet).unwrap();
@@ -779,8 +916,8 @@ mod tests {
// ṁ_b = 0.7 kg/s, h_b = 3e5 J/kg
// h_mix = (0.3*2e5 + 0.7*3e5) / 1.0 = (6e4 + 21e4) = 2.7e5 J/kg
let p = 3.0e5_f64;
let in_a = make_port("Water", p, 2.0e5);
let in_b = make_port("Water", p, 3.0e5);
let in_a = make_port("Water", p, 2.0e5);
let in_b = make_port("Water", p, 3.0e5);
let outlet = make_port("Water", p, 2.7e5);
let m = FlowMerger::incompressible("Water", vec![in_a, in_b], outlet)
@@ -802,25 +939,130 @@ mod tests {
#[test]
fn test_merger_as_trait_object() {
let in_a = make_port("Water", 3.0e5, 2.0e5);
let in_b = make_port("Water", 3.0e5, 2.0e5);
let in_a = make_port("Water", 3.0e5, 2.0e5);
let in_b = make_port("Water", 3.0e5, 2.0e5);
let outlet = make_port("Water", 3.0e5, 2.0e5);
let merger: Box<dyn Component> = Box::new(
FlowMerger::incompressible("Water", vec![in_a, in_b], outlet).unwrap()
);
let merger: Box<dyn Component> =
Box::new(FlowMerger::incompressible("Water", vec![in_a, in_b], outlet).unwrap());
assert_eq!(merger.n_equations(), 3);
}
#[test]
fn test_splitter_as_trait_object() {
let inlet = make_port("R410A", 24.0e5, 4.65e5);
let out_a = make_port("R410A", 24.0e5, 4.65e5);
let out_b = make_port("R410A", 24.0e5, 4.65e5);
let out_a = make_port("R410A", 24.0e5, 4.65e5);
let out_b = make_port("R410A", 24.0e5, 4.65e5);
let splitter: Box<dyn Component> = Box::new(
FlowSplitter::compressible("R410A", inlet, vec![out_a, out_b]).unwrap()
);
let splitter: Box<dyn Component> =
Box::new(FlowSplitter::compressible("R410A", inlet, vec![out_a, out_b]).unwrap());
assert_eq!(splitter.n_equations(), 3);
}
// ── energy_transfers tests ─────────────────────────────────────────────────
#[test]
fn test_splitter_energy_transfers_zero() {
let inlet = make_port("Water", 3.0e5, 2.0e5);
let out_a = make_port("Water", 3.0e5, 2.0e5);
let out_b = make_port("Water", 3.0e5, 2.0e5);
let splitter = FlowSplitter::incompressible("Water", inlet, vec![out_a, out_b]).unwrap();
let state = vec![0.0; 6];
let (heat, work) = splitter.energy_transfers(&state).unwrap();
assert_eq!(heat.to_watts(), 0.0);
assert_eq!(work.to_watts(), 0.0);
}
#[test]
fn test_merger_energy_transfers_zero() {
let in_a = make_port("Water", 3.0e5, 2.0e5);
let in_b = make_port("Water", 3.0e5, 2.4e5);
let outlet = make_port("Water", 3.0e5, 2.2e5);
let merger = FlowMerger::incompressible("Water", vec![in_a, in_b], outlet).unwrap();
let state = vec![0.0; 6];
let (heat, work) = merger.energy_transfers(&state).unwrap();
assert_eq!(heat.to_watts(), 0.0);
assert_eq!(work.to_watts(), 0.0);
}
// ── port_enthalpies tests ──────────────────────────────────────────────────
#[test]
fn test_splitter_port_enthalpies_count() {
let inlet = make_port("Water", 3.0e5, 2.0e5);
let out_a = make_port("Water", 3.0e5, 2.0e5);
let out_b = make_port("Water", 3.0e5, 2.0e5);
let out_c = make_port("Water", 3.0e5, 2.0e5);
let splitter =
FlowSplitter::incompressible("Water", inlet, vec![out_a, out_b, out_c]).unwrap();
let state = vec![0.0; 8];
let enthalpies = splitter.port_enthalpies(&state).unwrap();
// 1 inlet + 3 outlets = 4 enthalpies
assert_eq!(enthalpies.len(), 4);
}
#[test]
fn test_merger_port_enthalpies_count() {
let in_a = make_port("Water", 3.0e5, 2.0e5);
let in_b = make_port("Water", 3.0e5, 2.4e5);
let in_c = make_port("Water", 3.0e5, 2.2e5);
let outlet = make_port("Water", 3.0e5, 2.2e5);
let merger = FlowMerger::incompressible("Water", vec![in_a, in_b, in_c], outlet).unwrap();
let state = vec![0.0; 8];
let enthalpies = merger.port_enthalpies(&state).unwrap();
// 3 inlets + 1 outlet = 4 enthalpies
assert_eq!(enthalpies.len(), 4);
}
#[test]
fn test_splitter_port_enthalpies_values() {
let h_in = 2.5e5_f64;
let h_out_a = 2.5e5_f64;
let h_out_b = 2.5e5_f64;
let inlet = make_port("Water", 3.0e5, h_in);
let out_a = make_port("Water", 3.0e5, h_out_a);
let out_b = make_port("Water", 3.0e5, h_out_b);
let splitter = FlowSplitter::incompressible("Water", inlet, vec![out_a, out_b]).unwrap();
let state = vec![0.0; 6];
let enthalpies = splitter.port_enthalpies(&state).unwrap();
assert_eq!(enthalpies[0].to_joules_per_kg(), h_in);
assert_eq!(enthalpies[1].to_joules_per_kg(), h_out_a);
assert_eq!(enthalpies[2].to_joules_per_kg(), h_out_b);
}
#[test]
fn test_merger_port_enthalpies_values() {
let h_in_a = 2.0e5_f64;
let h_in_b = 3.0e5_f64;
let h_out = 2.5e5_f64;
let in_a = make_port("Water", 3.0e5, h_in_a);
let in_b = make_port("Water", 3.0e5, h_in_b);
let outlet = make_port("Water", 3.0e5, h_out);
let merger = FlowMerger::incompressible("Water", vec![in_a, in_b], outlet).unwrap();
let state = vec![0.0; 6];
let enthalpies = merger.port_enthalpies(&state).unwrap();
assert_eq!(enthalpies[0].to_joules_per_kg(), h_in_a);
assert_eq!(enthalpies[1].to_joules_per_kg(), h_in_b);
assert_eq!(enthalpies[2].to_joules_per_kg(), h_out);
}
}