/// 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, }; use entropyk_components::port::{Connected, FluidId, Port}; // 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 let initial_state = vec![ p_hp, 485_000.0, // edge0 comp→cond p_hp, 260_000.0, // edge1 cond→valve p_lp, 260_000.0, // edge2 valve→evap p_lp, 410_000.0, // edge3 evap→comp ]; 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; println!(" comp→cond : P={:.2} bar, h={:.1} kJ/kg", sv[0]/1e5, sv[1]/1e3); println!(" cond→valve : P={:.2} bar, h={:.1} kJ/kg", sv[2]/1e5, sv[3]/1e3); println!(" valve→evap : P={:.2} bar, h={:.1} kJ/kg", sv[4]/1e5, sv[5]/1e3); println!(" evap→comp : P={:.2} bar, h={:.1} kJ/kg", sv[6]/1e5, sv[7]/1e3); } Err(e) => { panic!("❌ Solveur échoué : {:?}", e); } } assert!(elapsed.as_millis() < 5000, "Doit converger en < 5 secondes"); assert!(result.is_ok(), "Solveur doit converger"); }