use entropyk_components::port::{Connected, FluidId, Port}; /// Test d'intégration : boucle réfrigération simple R134a en Rust natif. /// /// Ce test valide que le solveur Newton converge sur un cycle 4 composants /// en utilisant des mock components algébriques linéaires dont les équations /// sont mathématiquement cohérentes (ferment la boucle). use entropyk_components::{ Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice, }; use entropyk_core::{Enthalpy, MassFlow, Pressure}; use entropyk_solver::{ solver::{NewtonConfig, Solver}, system::{System, DEFAULT_MASS_FLOW_SEED_KG_S}, }; // Type alias: Port ≡ ConnectedPort type CP = Port; // ─── Mock compresseur ───────────────────────────────────────────────────────── // r[0] = p_disc - (p_suc + 1 MPa) // r[1] = h_disc - (h_suc + 75 kJ/kg) struct MockCompressor { port_suc: CP, port_disc: CP, } impl Component for MockCompressor { fn compute_residuals( &self, _s: &StateSlice, r: &mut ResidualVector, ) -> Result<(), ComponentError> { r[0] = self.port_disc.pressure().to_pascals() - (self.port_suc.pressure().to_pascals() + 1_000_000.0); r[1] = self.port_disc.enthalpy().to_joules_per_kg() - (self.port_suc.enthalpy().to_joules_per_kg() + 75_000.0); Ok(()) } fn jacobian_entries( &self, _s: &StateSlice, _j: &mut JacobianBuilder, ) -> Result<(), ComponentError> { Ok(()) } fn n_equations(&self) -> usize { 2 } fn get_ports(&self) -> &[ConnectedPort] { &[] } fn port_mass_flows(&self, _: &StateSlice) -> Result, ComponentError> { Ok(vec![ MassFlow::from_kg_per_s(0.05), MassFlow::from_kg_per_s(-0.05), ]) } } // ─── Mock condenseur ────────────────────────────────────────────────────────── // r[0] = p_out - p_in // r[1] = h_out - (h_in - 225 kJ/kg) struct MockCondenser { port_in: CP, port_out: CP, } impl Component for MockCondenser { fn compute_residuals( &self, _s: &StateSlice, r: &mut ResidualVector, ) -> Result<(), ComponentError> { r[0] = self.port_out.pressure().to_pascals() - self.port_in.pressure().to_pascals(); r[1] = self.port_out.enthalpy().to_joules_per_kg() - (self.port_in.enthalpy().to_joules_per_kg() - 225_000.0); Ok(()) } fn jacobian_entries( &self, _s: &StateSlice, _j: &mut JacobianBuilder, ) -> Result<(), ComponentError> { Ok(()) } fn n_equations(&self) -> usize { 2 } fn get_ports(&self) -> &[ConnectedPort] { &[] } fn port_mass_flows(&self, _: &StateSlice) -> Result, ComponentError> { Ok(vec![ MassFlow::from_kg_per_s(0.05), MassFlow::from_kg_per_s(-0.05), ]) } } // ─── Mock détendeur ─────────────────────────────────────────────────────────── // r[0] = p_out - (p_in - 1 MPa) // r[1] = h_out - h_in struct MockValve { port_in: CP, port_out: CP, } impl Component for MockValve { fn compute_residuals( &self, _s: &StateSlice, r: &mut ResidualVector, ) -> Result<(), ComponentError> { r[0] = self.port_out.pressure().to_pascals() - (self.port_in.pressure().to_pascals() - 1_000_000.0); r[1] = self.port_out.enthalpy().to_joules_per_kg() - self.port_in.enthalpy().to_joules_per_kg(); Ok(()) } fn jacobian_entries( &self, _s: &StateSlice, _j: &mut JacobianBuilder, ) -> Result<(), ComponentError> { Ok(()) } fn n_equations(&self) -> usize { 2 } fn get_ports(&self) -> &[ConnectedPort] { &[] } fn port_mass_flows(&self, _: &StateSlice) -> Result, ComponentError> { Ok(vec![ MassFlow::from_kg_per_s(0.05), MassFlow::from_kg_per_s(-0.05), ]) } } // ─── Mock évaporateur ───────────────────────────────────────────────────────── // r[0] = p_out - p_in // r[1] = h_out - (h_in + 150 kJ/kg) struct MockEvaporator { port_in: CP, port_out: CP, } impl Component for MockEvaporator { fn compute_residuals( &self, _s: &StateSlice, r: &mut ResidualVector, ) -> Result<(), ComponentError> { r[0] = self.port_out.pressure().to_pascals() - self.port_in.pressure().to_pascals(); r[1] = self.port_out.enthalpy().to_joules_per_kg() - (self.port_in.enthalpy().to_joules_per_kg() + 150_000.0); Ok(()) } fn jacobian_entries( &self, _s: &StateSlice, _j: &mut JacobianBuilder, ) -> Result<(), ComponentError> { Ok(()) } fn n_equations(&self) -> usize { 2 } fn get_ports(&self) -> &[ConnectedPort] { &[] } fn port_mass_flows(&self, _: &StateSlice) -> Result, ComponentError> { Ok(vec![ MassFlow::from_kg_per_s(0.05), MassFlow::from_kg_per_s(-0.05), ]) } } // ─── Helpers ────────────────────────────────────────────────────────────────── fn port(p_pa: f64, h_j_kg: f64) -> CP { let (connected, _) = Port::new( FluidId::new("R134a"), Pressure::from_pascals(p_pa), Enthalpy::from_joules_per_kg(h_j_kg), ) .connect(Port::new( FluidId::new("R134a"), Pressure::from_pascals(p_pa), Enthalpy::from_joules_per_kg(h_j_kg), )) .unwrap(); connected } // ─── Test ───────────────────────────────────────────────────────────────────── #[test] fn test_simple_refrigeration_loop_rust() { // Les équations : // Comp : p0 = p3 + 1 MPa ; h0 = h3 + 75 kJ/kg // Cond : p1 = p0 ; h1 = h0 - 225 kJ/kg // Valve : p2 = p1 - 1 MPa ; h2 = h1 // Evap : p3 = p2 ; h3 = h2 + 150 kJ/kg // // Bilan enthalpique en boucle : 75 - 225 + 150 = 0 → fermé ✓ // Bilan pressionnel en boucle : +1 - 0 - 1 - 0 = 0 → fermé ✓ // // Solution analytique (8 inconnues, 8 équations → infinité de solutions // dépendant du point de référence, mais le solveur en trouve une) : // En posant h3 = 410 kJ/kg, p3 = 350 kPa : // h0 = 485, p0 = 1.35 MPa // h1 = 260, p1 = 1.35 MPa // h2 = 260, p2 = 350 kPa // h3 = 410, p3 = 350 kPa let p_lp = 350_000.0_f64; // Pa let p_hp = 1_350_000.0_f64; // Pa = p_lp + 1 MPa // Les 4 bords (edge) du cycle : // edge0 : comp → cond // edge1 : cond → valve // edge2 : valve → evap // edge3 : evap → comp let comp = Box::new(MockCompressor { port_suc: port(p_lp, 410_000.0), port_disc: port(p_hp, 485_000.0), }); let cond = Box::new(MockCondenser { port_in: port(p_hp, 485_000.0), port_out: port(p_hp, 260_000.0), }); let valv = Box::new(MockValve { port_in: port(p_hp, 260_000.0), port_out: port(p_lp, 260_000.0), }); let evap = Box::new(MockEvaporator { port_in: port(p_lp, 260_000.0), port_out: port(p_lp, 410_000.0), }); let mut system = System::new(); let n_comp = system.add_component(comp); let n_cond = system.add_component(cond); let n_valv = system.add_component(valv); let n_evap = system.add_component(evap); system.add_edge(n_comp, n_cond).unwrap(); system.add_edge(n_cond, n_valv).unwrap(); system.add_edge(n_valv, n_evap).unwrap(); system.add_edge(n_evap, n_comp).unwrap(); system.finalize().unwrap(); let n_vars = system.full_state_vector_len(); println!("Variables d'état : {}", n_vars); // État initial = solution analytique exacte → résidus = 0 → converge 1 itération. // CM1.4 layout: 1 ṁ partagé (branche série unique) + (P, h) par arête. // state = [ṁ, P₀, h₀, P₁, h₁, P₂, h₂, P₃, h₃] (9 éléments) let m = DEFAULT_MASS_FLOW_SEED_KG_S; let initial_state = vec![ m, // ṁ partagé (branche 0) p_hp, 485_000.0, // edge0 comp→cond : P, h p_hp, 260_000.0, // edge1 cond→valve : P, h p_lp, 260_000.0, // edge2 valve→evap : P, h p_lp, 410_000.0, // edge3 evap→comp : P, h ]; let mut config = NewtonConfig { max_iterations: 50, tolerance: 1e-6, line_search: false, use_numerical_jacobian: true, // analytique vide → numérique initial_state: Some(initial_state), ..NewtonConfig::default() }; let t0 = std::time::Instant::now(); let result = config.solve(&mut system); let elapsed = t0.elapsed(); println!("Durée : {:?}", elapsed); match &result { Ok(converged) => { println!( "✅ Convergé en {} itérations ({:?})", converged.iterations, elapsed ); let sv = &converged.state; // CM1.4 layout: sv[0]=ṁ, then (P,h) per edge at stride 2. println!( " comp→cond : P={:.2} bar, h={:.1} kJ/kg", sv[1] / 1e5, sv[2] / 1e3 ); println!( " cond→valve : P={:.2} bar, h={:.1} kJ/kg", sv[3] / 1e5, sv[4] / 1e3 ); println!( " valve→evap : P={:.2} bar, h={:.1} kJ/kg", sv[5] / 1e5, sv[6] / 1e3 ); println!( " evap→comp : P={:.2} bar, h={:.1} kJ/kg", sv[7] / 1e5, sv[8] / 1e3 ); } Err(e) => { panic!("❌ Solveur échoué : {:?}", e); } } assert!(elapsed.as_millis() < 5000, "Doit converger en < 5 secondes"); assert!(result.is_ok(), "Solveur doit converger"); } // ─── T6 — Topology presolve assertions ─────────────────────────────────────── /// AC #3, #5: For a pure 4-edge series cycle, the topology presolve must: /// - Produce state_vector_len = 9 (1 ṁ branch + 4×2 P,h) instead of 12 (old 4×3). /// - Assign the same ṁ state index to all 4 edges (shared branch). /// - Keep the system square: n_branches inferred as state_len - 2×edge_count = 1. #[test] fn test_topology_presolve_state_layout() { let p_lp = 350_000.0_f64; let p_hp = 1_350_000.0_f64; let comp = Box::new(MockCompressor { port_suc: port(p_lp, 410_000.0), port_disc: port(p_hp, 485_000.0), }); let cond = Box::new(MockCondenser { port_in: port(p_hp, 485_000.0), port_out: port(p_hp, 260_000.0), }); let valv = Box::new(MockValve { port_in: port(p_hp, 260_000.0), port_out: port(p_lp, 260_000.0), }); let evap = Box::new(MockEvaporator { port_in: port(p_lp, 260_000.0), port_out: port(p_lp, 410_000.0), }); let mut system = System::new(); let n_comp = system.add_component(comp); let n_cond = system.add_component(cond); let n_valv = system.add_component(valv); let n_evap = system.add_component(evap); let e0 = system.add_edge(n_comp, n_cond).unwrap(); let e1 = system.add_edge(n_cond, n_valv).unwrap(); let e2 = system.add_edge(n_valv, n_evap).unwrap(); let e3 = system.add_edge(n_evap, n_comp).unwrap(); system.finalize().unwrap(); // AC #3: CM1.4 state layout must be |B| + 2|E| = 1 + 8 = 9 (not 12). let state_len = system.state_vector_len(); assert_eq!( state_len, 9, "CM1.4 state must be 1 branch + 4×2 P,h = 9, got {}", state_len ); // AC #3: Branch count inference — all branches used exactly 1 ṁ slot. let edge_count = 4; let n_branches_inferred = state_len - 2 * edge_count; assert_eq!( n_branches_inferred, 1, "pure series cycle must have exactly 1 branch, inferred {}", n_branches_inferred ); // AC #3: All 4 edges share the same ṁ state index. let m_idx: Vec = [e0, e1, e2, e3] .iter() .map(|&e| system.edge_state_indices_full(e).0) .collect(); let first_m = m_idx[0]; assert!( m_idx.iter().all(|&m| m == first_m), "all edges in a series branch must share the same ṁ index; got {:?}", m_idx ); assert_eq!(first_m, 0, "shared ṁ index must be 0 (first slot)"); } /// AC #5: A two-circuit system (2 independent series cycles) must have /// 2 independent branch ṁ unknowns and state_vector_len = 2×(1 + 2×4) = 18. #[test] fn test_topology_presolve_two_independent_circuits() { use entropyk_solver::CircuitId; let p_lp = 350_000.0_f64; let p_hp = 1_350_000.0_f64; let mut system = System::new(); // ── Circuit 0 ── let c0_comp = system .add_component_to_circuit( Box::new(MockCompressor { port_suc: port(p_lp, 410_000.0), port_disc: port(p_hp, 485_000.0), }), CircuitId::ZERO, ) .unwrap(); let c0_cond = system .add_component_to_circuit( Box::new(MockCondenser { port_in: port(p_hp, 485_000.0), port_out: port(p_hp, 260_000.0), }), CircuitId::ZERO, ) .unwrap(); let c0_valv = system .add_component_to_circuit( Box::new(MockValve { port_in: port(p_hp, 260_000.0), port_out: port(p_lp, 260_000.0), }), CircuitId::ZERO, ) .unwrap(); let c0_evap = system .add_component_to_circuit( Box::new(MockEvaporator { port_in: port(p_lp, 260_000.0), port_out: port(p_lp, 410_000.0), }), CircuitId::ZERO, ) .unwrap(); system.add_edge(c0_comp, c0_cond).unwrap(); system.add_edge(c0_cond, c0_valv).unwrap(); system.add_edge(c0_valv, c0_evap).unwrap(); system.add_edge(c0_evap, c0_comp).unwrap(); // ── Circuit 1 ── let c1 = CircuitId::from_number(1); let c1_comp = system .add_component_to_circuit( Box::new(MockCompressor { port_suc: port(p_lp, 410_000.0), port_disc: port(p_hp, 485_000.0), }), c1, ) .unwrap(); let c1_cond = system .add_component_to_circuit( Box::new(MockCondenser { port_in: port(p_hp, 485_000.0), port_out: port(p_hp, 260_000.0), }), c1, ) .unwrap(); let c1_valv = system .add_component_to_circuit( Box::new(MockValve { port_in: port(p_hp, 260_000.0), port_out: port(p_lp, 260_000.0), }), c1, ) .unwrap(); let c1_evap = system .add_component_to_circuit( Box::new(MockEvaporator { port_in: port(p_lp, 260_000.0), port_out: port(p_lp, 410_000.0), }), c1, ) .unwrap(); system.add_edge(c1_comp, c1_cond).unwrap(); system.add_edge(c1_cond, c1_valv).unwrap(); system.add_edge(c1_valv, c1_evap).unwrap(); system.add_edge(c1_evap, c1_comp).unwrap(); system.finalize().unwrap(); // 2 circuits × (1 branch + 4×2 P,h) = 2 × 9 = 18 state variables. let state_len = system.state_vector_len(); assert_eq!( state_len, 18, "two independent 4-edge cycles = 2 branches + 8×2 P,h = 18, got {}", state_len ); // Inferred branch count = 18 - 2*8 = 2. let n_branches_inferred = state_len - 2 * 8; assert_eq!( n_branches_inferred, 2, "two independent cycles must have 2 branches, inferred {}", n_branches_inferred ); }