Files
Entropyk/crates/solver/tests/refrigeration_cycle_integration.rs
sepehr 3358b74342 Add diagram workbench UI with Modelica DoF coaching and ISO glyphs.
Ship the Next.js cycle editor with CAD chrome, technical HX symbols, Fixed/Free boundary guidance, and secondary water/air pressure drop support in the solver stack.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 22:46:46 +02:00

508 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Connected> ≡ ConnectedPort
type CP = Port<Connected>;
// ─── 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<Vec<MassFlow>, 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<Vec<MassFlow>, 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<Vec<MassFlow>, 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<Vec<MassFlow>, 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<usize> = [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
);
}