Entropyk/crates/solver/tests/mass_balance_integration.rs

272 lines
8.8 KiB
Rust

//! Integration test for mass balance validation with multiple components.
//!
//! This test verifies that the mass balance validation works correctly
//! across a multi-component system simulating a refrigeration cycle.
use entropyk_components::port::{FluidId, Port};
use entropyk_components::{
Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
};
use entropyk_core::{Enthalpy, MassFlow, Pressure};
use entropyk_solver::system::System;
// ─────────────────────────────────────────────────────────────────────────────
// Mock components for testing
// ─────────────────────────────────────────────────────────────────────────────
/// A mock component that simulates balanced mass flow (like a pipe or heat exchanger).
struct BalancedComponent {
ports: Vec<ConnectedPort>,
mass_flow_in: f64,
}
impl BalancedComponent {
fn new(ports: Vec<ConnectedPort>, mass_flow: f64) -> Self {
Self {
ports,
mass_flow_in: mass_flow,
}
}
}
impl Component for BalancedComponent {
fn compute_residuals(
&self,
_state: &StateSlice,
residuals: &mut ResidualVector,
) -> Result<(), ComponentError> {
for r in residuals.iter_mut() {
*r = 0.0;
}
Ok(())
}
fn jacobian_entries(
&self,
_state: &StateSlice,
jacobian: &mut JacobianBuilder,
) -> Result<(), ComponentError> {
for i in 0..self.n_equations() {
jacobian.add_entry(i, i, 1.0);
}
Ok(())
}
fn n_equations(&self) -> usize {
2
}
fn get_ports(&self) -> &[ConnectedPort] {
&self.ports
}
fn port_mass_flows(&self, _state: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
// Balanced: inlet positive, outlet negative
Ok(vec![
MassFlow::from_kg_per_s(self.mass_flow_in),
MassFlow::from_kg_per_s(-self.mass_flow_in),
])
}
}
/// A mock component with imbalanced mass flow (for testing violation detection).
struct ImbalancedComponent {
ports: Vec<ConnectedPort>,
}
impl ImbalancedComponent {
fn new(ports: Vec<ConnectedPort>) -> Self {
Self { ports }
}
}
impl Component for ImbalancedComponent {
fn compute_residuals(
&self,
_state: &StateSlice,
residuals: &mut ResidualVector,
) -> Result<(), ComponentError> {
for r in residuals.iter_mut() {
*r = 0.0;
}
Ok(())
}
fn jacobian_entries(
&self,
_state: &StateSlice,
jacobian: &mut JacobianBuilder,
) -> Result<(), ComponentError> {
for i in 0..self.n_equations() {
jacobian.add_entry(i, i, 1.0);
}
Ok(())
}
fn n_equations(&self) -> usize {
2
}
fn get_ports(&self) -> &[ConnectedPort] {
&self.ports
}
fn port_mass_flows(&self, _state: &StateSlice) -> Result<Vec<MassFlow>, ComponentError> {
// Imbalanced: inlet 1.0 kg/s, outlet -0.5 kg/s (sum = 0.5 kg/s violation)
Ok(vec![
MassFlow::from_kg_per_s(1.0),
MassFlow::from_kg_per_s(-0.5),
])
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Test helpers
// ─────────────────────────────────────────────────────────────────────────────
fn make_connected_port_pair(
fluid: &str,
p_bar: f64,
h_j_kg: f64,
) -> (ConnectedPort, ConnectedPort) {
let p1 = Port::new(
FluidId::new(fluid),
Pressure::from_bar(p_bar),
Enthalpy::from_joules_per_kg(h_j_kg),
);
let p2 = Port::new(
FluidId::new(fluid),
Pressure::from_bar(p_bar),
Enthalpy::from_joules_per_kg(h_j_kg),
);
let (c1, c2) = p1.connect(p2).unwrap();
(c1, c2)
}
// ─────────────────────────────────────────────────────────────────────────────
// Tests
// ─────────────────────────────────────────────────────────────────────────────
#[test]
fn test_mass_balance_4_component_cycle() {
// Simulate a 4-component refrigeration cycle: Compressor → Condenser → Valve → Evaporator
let mut system = System::new();
// Create 4 pairs of connected ports for 4 components
let (p1a, p1b) = make_connected_port_pair("R134a", 5.0, 400_000.0);
let (p2a, p2b) = make_connected_port_pair("R134a", 5.0, 400_000.0);
let (p3a, p3b) = make_connected_port_pair("R134a", 5.0, 400_000.0);
let (p4a, p4b) = make_connected_port_pair("R134a", 5.0, 400_000.0);
// Create 4 balanced components (simulating compressor, condenser, valve, evaporator)
let mass_flow = 0.1; // kg/s
let comp1 = Box::new(BalancedComponent::new(vec![p1a, p1b], mass_flow));
let comp2 = Box::new(BalancedComponent::new(vec![p2a, p2b], mass_flow));
let comp3 = Box::new(BalancedComponent::new(vec![p3a, p3b], mass_flow));
let comp4 = Box::new(BalancedComponent::new(vec![p4a, p4b], mass_flow));
// Add components to system
let n1 = system.add_component(comp1);
let n2 = system.add_component(comp2);
let n3 = system.add_component(comp3);
let n4 = system.add_component(comp4);
// Connect in a cycle
system.add_edge(n1, n2).unwrap();
system.add_edge(n2, n3).unwrap();
system.add_edge(n3, n4).unwrap();
system.add_edge(n4, n1).unwrap();
system.finalize().unwrap();
// Test with zero state vector
let state = vec![0.0; system.full_state_vector_len()];
let result = system.check_mass_balance(&state);
assert!(
result.is_ok(),
"Mass balance should pass for balanced 4-component cycle"
);
}
#[test]
fn test_mass_balance_detects_imbalance_in_cycle() {
// Create a cycle with one imbalanced component
let mut system = System::new();
let (p1a, p1b) = make_connected_port_pair("R134a", 5.0, 400_000.0);
let (p2a, p2b) = make_connected_port_pair("R134a", 5.0, 400_000.0);
let (p3a, p3b) = make_connected_port_pair("R134a", 5.0, 400_000.0);
// Two balanced components
let comp1 = Box::new(BalancedComponent::new(vec![p1a, p1b], 0.1));
let comp3 = Box::new(BalancedComponent::new(vec![p3a, p3b], 0.1));
// One imbalanced component
let comp2 = Box::new(ImbalancedComponent::new(vec![p2a, p2b]));
let n1 = system.add_component(comp1);
let n2 = system.add_component(comp2);
let n3 = system.add_component(comp3);
system.add_edge(n1, n2).unwrap();
system.add_edge(n2, n3).unwrap();
system.add_edge(n3, n1).unwrap();
system.finalize().unwrap();
let state = vec![0.0; system.full_state_vector_len()];
let result = system.check_mass_balance(&state);
assert!(
result.is_err(),
"Mass balance should fail when one component is imbalanced"
);
}
#[test]
fn test_mass_balance_multiple_components_same_flow() {
// Test that multiple components with the same mass flow pass validation
let mut system = System::new();
// Create 6 components in a chain
let mut ports = Vec::new();
for _ in 0..6 {
let (pa, pb) = make_connected_port_pair("R134a", 5.0, 400_000.0);
ports.push((pa, pb));
}
let mass_flow = 0.5; // kg/s
let components: Vec<_> = ports
.into_iter()
.map(|(pa, pb)| Box::new(BalancedComponent::new(vec![pa, pb], mass_flow)))
.collect();
let nodes: Vec<_> = components
.into_iter()
.map(|c| system.add_component(c))
.collect();
// Connect in a cycle
for i in 0..nodes.len() {
let next = (i + 1) % nodes.len();
system.add_edge(nodes[i], nodes[next]).unwrap();
}
system.finalize().unwrap();
let state = vec![0.0; system.full_state_vector_len()];
let result = system.check_mass_balance(&state);
assert!(
result.is_ok(),
"Mass balance should pass for multiple balanced components"
);
}
#[test]
fn test_mass_balance_tolerance_constant_accessible() {
// Verify the tolerance constant is accessible
assert_eq!(System::MASS_BALANCE_TOLERANCE_KG_S, 1e-9);
}