//! 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, mass_flow_in: f64, } impl BalancedComponent { fn new(ports: Vec, 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, 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, } impl ImbalancedComponent { fn new(ports: Vec) -> 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, 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); }