//! Epic 1 Integration Tests - Extensible Component Framework //! //! Tests for User Stories: //! - 1-1: Component Trait Definition //! - 1-2: Physical Types (NewType Pattern) //! - 1-3: Port and Connection System //! - 1-4: Compressor Component (AHRI 540) //! - 1-5: Generic Heat Exchanger Framework //! - 1-6: Expansion Valve Component //! - 1-7: Component State Machine (ON/OFF/BYPASS) //! - 1-8: Auxiliary and Transport Components //! - 1-11: Flow Junctions (Splitter/Merger) //! - 1-12: Boundary Conditions (Source/Sink) use approx::assert_relative_eq; use entropyk_components::{ Ahri540Coefficients, Compressor, CompressorModel, ConnectedPort, EpsNtuModel, ExchangerType, ExpansionValve, FluidId, OperationalState, Port, Pump, SstSdtCoefficients, StateManageable, }; use entropyk_components::heat_exchanger::HeatTransferModel; use entropyk_core::{Enthalpy, MassFlow, Pressure, Temperature}; // ============================================================================= // Story 1-2: Physical Types (NewType Pattern) // ============================================================================= mod story_1_2_types { use super::*; #[test] fn test_pressure_conversions() { let p_bar = Pressure::from_bar(1.0); assert_relative_eq!(p_bar.to_pascals(), 100_000.0, epsilon = 1e-6); assert_relative_eq!(p_bar.to_bar(), 1.0, epsilon = 1e-10); } #[test] fn test_temperature_conversions() { let t_c = Temperature::from_celsius(0.0); assert_relative_eq!(t_c.to_kelvin(), 273.15, epsilon = 1e-10); assert_relative_eq!(t_c.to_celsius(), 0.0, epsilon = 1e-10); } #[test] fn test_enthalpy_conversions() { let h_kj = Enthalpy::from_kilojoules_per_kg(100.0); assert_relative_eq!(h_kj.to_joules_per_kg(), 100_000.0, epsilon = 1e-6); } #[test] fn test_mass_flow_regularization() { let zero = MassFlow::from_kg_per_s(0.0); let regularized = zero.regularized(); assert!(regularized.to_kg_per_s() > 0.0); } } // ============================================================================= // Story 1-3: Port and Connection System // ============================================================================= mod story_1_3_ports { use super::*; #[test] fn test_port_creation() { let port = Port::new( FluidId::new("R134a"), Pressure::from_bar(10.0), Enthalpy::from_joules_per_kg(400_000.0), ); assert_eq!(port.fluid_id().as_str(), "R134a"); } #[test] fn test_port_connection_success() { let p1 = Port::new( FluidId::new("R134a"), Pressure::from_bar(10.0), Enthalpy::from_joules_per_kg(400_000.0), ); let p2 = Port::new( FluidId::new("R134a"), Pressure::from_bar(10.0), Enthalpy::from_joules_per_kg(400_000.0), ); let result = p1.connect(p2); assert!(result.is_ok()); } #[test] fn test_port_connection_fluid_mismatch() { let p1 = Port::new( FluidId::new("R134a"), Pressure::from_bar(10.0), Enthalpy::from_joules_per_kg(400_000.0), ); let p2 = Port::new( FluidId::new("R410A"), Pressure::from_bar(10.0), Enthalpy::from_joules_per_kg(400_000.0), ); let result = p1.connect(p2); assert!(result.is_err()); } } // ============================================================================= // Story 1-4: Compressor Component (AHRI 540) // ============================================================================= mod story_1_4_compressor { use super::*; #[test] fn test_ahri540_coefficients_creation() { let coeffs = Ahri540Coefficients::new( 0.85, 2.5, // M1, M2 (flow) 500.0, 1500.0, -2.5, 1.8, // M3-M6 (cooling) 600.0, 1600.0, -3.0, 2.0, // M7-M10 (heating) ); assert!(coeffs.validate().is_ok()); } #[test] fn test_ahri540_invalid_m2() { let coeffs = Ahri540Coefficients::new( 0.85, -1.0, // M2 must be positive 500.0, 1500.0, -2.5, 1.8, 600.0, 1600.0, -3.0, 2.0, ); assert!(coeffs.validate().is_err()); } #[test] fn test_sst_sdt_coefficients() { // Use bilinear constructor instead of removed ::default() let coeffs = SstSdtCoefficients::bilinear( 0.05, 0.001, 0.0005, 0.00001, // mass flow coefficients 1000.0, 50.0, 30.0, 0.5, // power coefficients ); // Verify evaluation works (bilinear model) let mass_flow = coeffs.mass_flow_at(263.15, 313.15); // -10°C SST, 40°C SDT assert!(mass_flow > 0.0); } } // ============================================================================= // Story 1-5: Generic Heat Exchanger Framework // ============================================================================= mod story_1_5_heat_exchanger { use super::*; #[test] fn test_eps_ntu_counter_flow() { let model = EpsNtuModel::counter_flow(5000.0); // ua() is accessed through the HeatTransferModel trait assert_eq!(model.ua(), 5000.0); } #[test] fn test_eps_ntu_effectiveness_counter_flow() { let model = EpsNtuModel::counter_flow(5000.0); // Test effectiveness calculation let ntu = 1.0; let c_r = 0.5; let eps = model.effectiveness(ntu, c_r); // For counter-flow with NTU=1, C_r=0.5: ε ≈ 0.63 assert!(eps > 0.0 && eps < 1.0); } #[test] fn test_eps_ntu_phase_change() { let model = EpsNtuModel::counter_flow(5000.0); // Phase change: C_r → 0 let ntu = 2.0; let c_r = 1e-12; // Effectively zero let eps = model.effectiveness(ntu, c_r); // For phase change: ε = 1 - exp(-NTU) let expected = 1.0 - (-ntu).exp(); assert_relative_eq!(eps, expected, epsilon = 1e-6); } } // ============================================================================= // Story 1-6: Expansion Valve Component // ============================================================================= mod story_1_6_expansion_valve { use super::*; #[test] fn test_valve_creation() { let inlet = Port::new( FluidId::new("R410A"), Pressure::from_bar(25.0), Enthalpy::from_joules_per_kg(250_000.0), ); let outlet = Port::new( FluidId::new("R410A"), Pressure::from_bar(25.0), Enthalpy::from_joules_per_kg(250_000.0), ); let valve = ExpansionValve::new(inlet, outlet, Some(1.0)); assert!(valve.is_ok()); } #[test] fn test_valve_invalid_opening() { let inlet = Port::new( FluidId::new("R410A"), Pressure::from_bar(25.0), Enthalpy::from_joules_per_kg(250_000.0), ); let outlet = Port::new( FluidId::new("R410A"), Pressure::from_bar(25.0), Enthalpy::from_joules_per_kg(250_000.0), ); // Opening > 1.0 should fail let valve = ExpansionValve::new(inlet, outlet, Some(1.5)); assert!(valve.is_err()); } } // ============================================================================= // Story 1-7: Component State Machine // ============================================================================= mod story_1_7_state_machine { use super::*; #[test] fn test_operational_state_transitions() { let on = OperationalState::On; let off = OperationalState::Off; let bypass = OperationalState::Bypass; // On can transition to any state assert!(on.can_transition_to(OperationalState::Off)); assert!(on.can_transition_to(OperationalState::Bypass)); // Off can transition to On assert!(off.can_transition_to(OperationalState::On)); } #[test] fn test_circuit_id_creation() { let circuit = entropyk_components::CircuitId::from_number(5); assert_eq!(circuit.as_number(), 5); } } // ============================================================================= // Story 1-8: Auxiliary Components (Pipe, Pump) // ============================================================================= mod story_1_8_auxiliary { use super::*; #[test] fn test_pipe_creation() { use entropyk_components::PipeGeometry; let geometry = PipeGeometry { length_m: 10.0, diameter_m: 0.022, roughness_m: 1.5e-6, }; assert_relative_eq!(geometry.length_m, 10.0); assert_relative_eq!(geometry.diameter_m, 0.022); } #[test] fn test_pump_curves() { use entropyk_components::PumpCurves; // Use PumpCurves::quadratic constructor (fields are no longer public) let curves = PumpCurves::quadratic( 30.0, -10.0, -50.0, // head: H = 30 - 10Q - 50Q² 0.5, 0.3, -0.5, // efficiency: η = 0.5 + 0.3Q - 0.5Q² ) .unwrap(); // At Q=0, H should be H0 = 30 let h_at_zero = curves.head_at_flow(0.0); assert_relative_eq!(h_at_zero, 30.0, epsilon = 1e-6); } } // ============================================================================= // Story 1-11: Flow Junctions // ============================================================================= mod story_1_11_junctions { use super::*; use entropyk_components::FlowSplitter; fn make_connected_port(fluid: &str, p_pa: f64, h_jkg: f64) -> ConnectedPort { let a = Port::new( FluidId::new(fluid), Pressure::from_pascals(p_pa), Enthalpy::from_joules_per_kg(h_jkg), ); let b = Port::new( FluidId::new(fluid), Pressure::from_pascals(p_pa), Enthalpy::from_joules_per_kg(h_jkg), ); a.connect(b).unwrap().0 } #[test] fn test_flow_splitter_creation() { // FlowSplitter::new() is removed; use ::incompressible() let inlet = make_connected_port("Water", 100_000.0, 42_000.0); let outlet1 = make_connected_port("Water", 100_000.0, 42_000.0); let outlet2 = make_connected_port("Water", 100_000.0, 42_000.0); let splitter = FlowSplitter::incompressible("Water", inlet, vec![outlet1, outlet2]); assert!(splitter.is_ok()); } #[test] fn test_flow_source_creation() { // FlowSource::new() is removed; use ::incompressible() (deprecated but still functional) #[allow(deprecated)] { use entropyk_components::FlowSource; let port = make_connected_port("Water", 100_000.0, 42_000.0); let source = FlowSource::incompressible( "Water", 100_000.0, 42_000.0, port, ); assert!(source.is_ok()); let s = source.unwrap(); assert_eq!(s.fluid_id(), "Water"); } } #[test] fn test_flow_sink_creation() { // FlowSink::new() is removed; use ::incompressible() (deprecated but still functional) #[allow(deprecated)] { use entropyk_components::FlowSink; let port = make_connected_port("Water", 100_000.0, 42_000.0); let sink = FlowSink::incompressible("Water", 100_000.0, None, port); assert!(sink.is_ok()); let s = sink.unwrap(); assert_eq!(s.fluid_id(), "Water"); } } }