//! Quick test: EXV OperationalState Off path. //! Validates that the EXV with `Off` produces ṁ = 0 in its residual and keeps //! the Jacobian non-singular. Run with: //! cargo test --release -p entropyk-components --test test_exv_off -- --nocapture use entropyk_components::isenthalpic_expansion_valve::IsenthalpicExpansionValve; use entropyk_components::state_machine::{OperationalState, StateManageable}; use entropyk_components::{Component, JacobianBuilder}; fn make_exv() -> IsenthalpicExpansionValve { let mut exv = IsenthalpicExpansionValve::new(278.15) .with_refrigerant("R134a") .with_emergent_pressure() .with_orifice_fixed(2.0e-6, 1.0); // Layout: [m_inlet=0, m_outlet=1, p_inlet=2, h_inlet=3, p_outlet=4, h_outlet=5] exv.set_system_context(0, &[(0, 2, 3), (1, 4, 5)]); exv } #[test] fn exv_off_produces_zero_flow_residual() { let mut exv = make_exv(); // Turn the valve OFF. exv.set_operational_state_unchecked(OperationalState::Off); assert!(exv.state().is_off()); // State where m_outlet = 0.05 (non-zero flow) — the Off residual must drive it to 0. let state: Vec = vec![0.05, 0.05, 1.5e6, 250_000.0, 0.4e6, 250_000.0]; let mut r = vec![0.0_f64; exv.n_equations()]; exv.compute_residuals(&state, &mut r).expect("residuals"); // The orifice equation is the last one (after isenthalpic + mass conservation). // r_orifice should equal m_outlet - 0 = 0.05 (non-zero → Newton will push it to 0). let orifice_residual = *r.last().unwrap(); assert!( (orifice_residual - 0.05).abs() < 1e-12, "Off residual should be ṁ = 0.05 (forcing flow to 0), got {}", orifice_residual ); // Jacobian: ∂r_orifice/∂m_outlet = 1 (keeps Newton coupled on the mass flow). let mut jb = JacobianBuilder::new(); exv.jacobian_entries(&state, &mut jb).expect("jacobian"); let entries = jb.entries(); let m_out_idx = 1; let orifice_row = exv.n_equations() - 1; let dm_out: f64 = entries .iter() .filter(|(row, col, _)| *row == orifice_row && *col == m_out_idx) .map(|(_, _, v)| *v) .sum(); assert!( (dm_out - 1.0).abs() < 1e-12, "Off Jacobian ∂r/∂m_outlet should be 1.0, got {}", dm_out ); println!( "EXV Off: residual={:.4e} (target 0.05), ∂r/∂m_out={:.3} (target 1.0) — OK", orifice_residual, dm_out ); } #[test] fn exv_on_still_uses_orifice_equation() { let mut exv = make_exv(); // Default state is On. assert!(exv.state().is_on()); // Use a backend so ρ_in can be evaluated. let backend: std::sync::Arc = std::sync::Arc::new(entropyk_fluids::TestBackend::new()); exv.set_fluid_backend_from_builder(backend); // State with a physical ΔP across the valve: P_in = 13 bar, P_out = 3.5 bar. let state: Vec = vec![0.05, 0.05, 1.3e6, 250_000.0, 0.35e6, 250_000.0]; let mut r = vec![0.0_f64; exv.n_equations()]; exv.compute_residuals(&state, &mut r).expect("residuals"); let orifice_residual = *r.last().unwrap(); // Orifice equation is active: residual is non-trivial (not just ṁ). assert!( orifice_residual.abs() > 1e-6, "On residual should be non-trivial (orifice equation active), got {}", orifice_residual ); println!( "EXV On: residual={:.4e} (orifice equation active) — OK", orifice_residual ); } #[test] fn exv_state_transitions_are_validated() { let mut exv = IsenthalpicExpansionValve::new(278.15); assert!(exv.state().is_on()); // On → Off is a legal transition. assert!(exv.can_transition_to(OperationalState::Off)); exv.set_state(OperationalState::Off).expect("On → Off"); assert!(exv.state().is_off()); // Off → On is legal. assert!(exv.can_transition_to(OperationalState::On)); exv.set_state(OperationalState::On).expect("Off → On"); assert!(exv.state().is_on()); }