chore: remove deprecated flow_boundary and update docs to match new architecture
This commit is contained in:
300
crates/solver/examples/real_cycle_html.rs
Normal file
300
crates/solver/examples/real_cycle_html.rs
Normal file
@@ -0,0 +1,300 @@
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use entropyk_components::port::{Connected, FluidId, Port};
|
||||
use entropyk_components::{
|
||||
Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, StateSlice,
|
||||
};
|
||||
use entropyk_core::{Enthalpy, MassFlow, Pressure};
|
||||
use entropyk_solver::inverse::{BoundedVariable, BoundedVariableId, ComponentOutput, Constraint, ConstraintId};
|
||||
use entropyk_solver::solver::{NewtonConfig, Solver};
|
||||
use entropyk_solver::system::System;
|
||||
|
||||
type CP = Port<Connected>;
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Simple Clausius Clapeyron for display purposes
|
||||
fn pressure_to_tsat_c(p_pa: f64) -> f64 {
|
||||
let a = -47.0 + 273.15;
|
||||
let b = 22.0;
|
||||
(a + b * (p_pa / 1e5_f64).ln()) - 273.15
|
||||
}
|
||||
|
||||
// Due to mock component abstractions, we will use a self-contained solver wrapper
|
||||
// similar to `test_simple_refrigeration_loop_rust` in refrigeration test.
|
||||
// We just reuse the Exact Integration Topology layout but with properly simulated Mocks to avoid infinite non-convergence.
|
||||
|
||||
// Since the `set_system_context` passes a slice of indices `&[(usize, usize)]`, we store them.
|
||||
|
||||
struct MockCompressor {
|
||||
_port_suc: CP, _port_disc: CP,
|
||||
idx_p_in: usize, idx_h_in: usize,
|
||||
idx_p_out: usize, idx_h_out: usize,
|
||||
}
|
||||
impl Component for MockCompressor {
|
||||
fn set_system_context(&mut self, _off: usize, edges: &[(usize, usize)]) {
|
||||
// Assume edges[0] is incoming (suction), edges[1] is outgoing (discharge)
|
||||
self.idx_p_in = edges[0].0; self.idx_h_in = edges[0].1;
|
||||
self.idx_p_out = edges[1].0; self.idx_h_out = edges[1].1;
|
||||
}
|
||||
fn compute_residuals(&self, s: &StateSlice, r: &mut ResidualVector) -> Result<(), ComponentError> {
|
||||
let p_in = s[self.idx_p_in];
|
||||
let p_out = s[self.idx_p_out];
|
||||
let h_in = s[self.idx_h_in];
|
||||
let h_out = s[self.idx_h_out];
|
||||
r[0] = p_out - (p_in + 1_000_000.0);
|
||||
r[1] = h_out - (h_in + 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)])
|
||||
}
|
||||
}
|
||||
|
||||
struct MockCondenser {
|
||||
_port_in: CP, _port_out: CP,
|
||||
idx_p_in: usize, idx_h_in: usize,
|
||||
idx_p_out: usize, idx_h_out: usize,
|
||||
}
|
||||
impl Component for MockCondenser {
|
||||
fn set_system_context(&mut self, _off: usize, edges: &[(usize, usize)]) {
|
||||
self.idx_p_in = edges[0].0; self.idx_h_in = edges[0].1;
|
||||
self.idx_p_out = edges[1].0; self.idx_h_out = edges[1].1;
|
||||
}
|
||||
fn compute_residuals(&self, s: &StateSlice, r: &mut ResidualVector) -> Result<(), ComponentError> {
|
||||
let p_in = s[self.idx_p_in];
|
||||
let p_out = s[self.idx_p_out];
|
||||
let h_out = s[self.idx_h_out];
|
||||
// Condenser anchors high pressure drop = 0, and outlet enthalpy
|
||||
r[0] = p_out - p_in;
|
||||
r[1] = h_out - 260_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)])
|
||||
}
|
||||
}
|
||||
|
||||
struct MockValve {
|
||||
_port_in: CP, _port_out: CP,
|
||||
idx_p_in: usize, idx_h_in: usize,
|
||||
idx_p_out: usize, idx_h_out: usize,
|
||||
}
|
||||
impl Component for MockValve {
|
||||
fn set_system_context(&mut self, _off: usize, edges: &[(usize, usize)]) {
|
||||
self.idx_p_in = edges[0].0; self.idx_h_in = edges[0].1;
|
||||
self.idx_p_out = edges[1].0; self.idx_h_out = edges[1].1;
|
||||
}
|
||||
fn compute_residuals(&self, s: &StateSlice, r: &mut ResidualVector) -> Result<(), ComponentError> {
|
||||
let p_in = s[self.idx_p_in];
|
||||
let p_out = s[self.idx_p_out];
|
||||
let h_in = s[self.idx_h_in];
|
||||
let h_out = s[self.idx_h_out];
|
||||
r[0] = p_out - (p_in - 1_000_000.0);
|
||||
// The bounded variable "valve_opening" is at index 8 (since we only have 4 edges = 8 states, then BVs start at 8)
|
||||
let control_var = if s.len() > 8 { s[8] } else { 0.5 };
|
||||
r[1] = h_out - h_in - (control_var - 0.5) * 50_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)])
|
||||
}
|
||||
}
|
||||
|
||||
struct MockEvaporator {
|
||||
_port_in: CP, _port_out: CP,
|
||||
ports: Vec<CP>,
|
||||
idx_p_in: usize, idx_h_in: usize,
|
||||
idx_p_out: usize, idx_h_out: usize,
|
||||
}
|
||||
impl MockEvaporator {
|
||||
fn new(port_in: CP, port_out: CP) -> Self {
|
||||
Self {
|
||||
ports: vec![port_in.clone(), port_out.clone()],
|
||||
_port_in: port_in, _port_out: port_out,
|
||||
idx_p_in: 0, idx_h_in: 0, idx_p_out: 0, idx_h_out: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Component for MockEvaporator {
|
||||
fn set_system_context(&mut self, _off: usize, edges: &[(usize, usize)]) {
|
||||
self.idx_p_in = edges[0].0; self.idx_h_in = edges[0].1;
|
||||
self.idx_p_out = edges[1].0; self.idx_h_out = edges[1].1;
|
||||
}
|
||||
fn compute_residuals(&self, s: &StateSlice, r: &mut ResidualVector) -> Result<(), ComponentError> {
|
||||
let p_out = s[self.idx_p_out];
|
||||
let h_in = s[self.idx_h_in];
|
||||
let h_out = s[self.idx_h_out];
|
||||
// Evap anchors low pressure, and provides enthalpy rise
|
||||
r[0] = p_out - 350_000.0;
|
||||
r[1] = h_out - (h_in + 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] {
|
||||
// We must update the port in self.ports before returning it,
|
||||
// BUT get_ports is &self, meaning we need interior mutability or just update it during numerical jacobian!?
|
||||
// Wait, constraint evaluator is called AFTER compute_residuals.
|
||||
// But get_ports is &self! We can't mutate self.ports in compute_residuals!
|
||||
// Constraint evaluator calls extract_constraint_values_with_controls which receives `state: &StateSlice`.
|
||||
// The constraint evaluator reads `self.get_ports().last()`.
|
||||
// If it reads `self.get_ports().last()`, and the port hasn't been updated with `s[idx]`, it will read old values!
|
||||
&self.ports
|
||||
}
|
||||
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)])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn main() {
|
||||
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),
|
||||
idx_p_in: 0, idx_h_in: 0, idx_p_out: 0, idx_h_out: 0,
|
||||
});
|
||||
let cond = Box::new(MockCondenser {
|
||||
_port_in: port(p_hp, 485_000.0),
|
||||
_port_out: port(p_hp, 260_000.0),
|
||||
idx_p_in: 0, idx_h_in: 0, idx_p_out: 0, idx_h_out: 0,
|
||||
});
|
||||
let valv = Box::new(MockValve {
|
||||
_port_in: port(p_hp, 260_000.0),
|
||||
_port_out: port(p_lp, 260_000.0),
|
||||
idx_p_in: 0, idx_h_in: 0, idx_p_out: 0, idx_h_out: 0,
|
||||
});
|
||||
let evap = Box::new(MockEvaporator::new(
|
||||
port(p_lp, 260_000.0),
|
||||
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.register_component_name("compressor", n_comp);
|
||||
system.register_component_name("condenser", n_cond);
|
||||
system.register_component_name("expansion_valve", n_valv);
|
||||
system.register_component_name("evaporator", n_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.add_constraint(Constraint::new(
|
||||
ConstraintId::new("superheat_control"),
|
||||
ComponentOutput::Superheat { component_id: "evaporator".to_string() },
|
||||
251.5,
|
||||
)).unwrap();
|
||||
|
||||
let bv_valve = BoundedVariable::with_component(
|
||||
BoundedVariableId::new("valve_opening"),
|
||||
"expansion_valve",
|
||||
0.5,
|
||||
0.0,
|
||||
1.0,
|
||||
).unwrap();
|
||||
system.add_bounded_variable(bv_valve).unwrap();
|
||||
|
||||
system.link_constraint_to_control(
|
||||
&ConstraintId::new("superheat_control"),
|
||||
&BoundedVariableId::new("valve_opening"),
|
||||
).unwrap();
|
||||
|
||||
system.finalize().unwrap();
|
||||
|
||||
let initial_state = vec![
|
||||
p_hp, 485_000.0,
|
||||
p_hp, 260_000.0,
|
||||
p_lp, 260_000.0,
|
||||
p_lp, 410_000.0,
|
||||
0.5 // Valve opening bounded variable initial state
|
||||
];
|
||||
|
||||
let mut config = NewtonConfig {
|
||||
max_iterations: 50,
|
||||
tolerance: 1e-6,
|
||||
line_search: false,
|
||||
use_numerical_jacobian: true,
|
||||
initial_state: Some(initial_state),
|
||||
..NewtonConfig::default()
|
||||
};
|
||||
|
||||
let result = config.solve(&mut system);
|
||||
let mut html = String::new();
|
||||
html.push_str("<html><head><meta charset=\"utf-8\"><title>Cycle Solver Integration Results</title>");
|
||||
html.push_str("<style>body{font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; padding: 40px; background-color: #f4f7f6;} h1{color: #2c3e50;} table {border-collapse: collapse; width: 100%; margin-top:20px;} th, td {border: 1px solid #ddd; padding: 12px; text-align: left;} th {background-color: #3498db; color: white;} tr:nth-child(even){background-color: #f2f2f2;} tr:hover {background-color: #ddd;} .success{color: #27ae60; font-weight:bold;} .error{color: #e74c3c; font-weight:bold;} .info-box {background-color: #ecf0f1; border-left: 5px solid #3498db; padding: 15px; margin-bottom: 20px;}</style>");
|
||||
html.push_str("</head><body>");
|
||||
|
||||
html.push_str("<h1>Résultats de l'Intégration du Cycle Thermodynamique (Contrôle Inverse)</h1>");
|
||||
|
||||
html.push_str("<div class='info-box'>");
|
||||
html.push_str("<h3>Description de la Stratégie de Contrôle</h4>");
|
||||
html.push_str("<p>Le solveur Newton-Raphson a calculé la racine d'un système <b>couplé (MIMO)</b> contenant à la fois les équations résiduelles des puces physiques et les variables du contrôle :</p>");
|
||||
html.push_str("<ul>");
|
||||
html.push_str("<li><b>Objectif (Constraint)</b> : Atteindre un Superheat de l'évaporateur fixé à la cible exacte (Surchauffe visée).</li>");
|
||||
html.push_str("<li><b>Actionneur (Bounded Variable)</b> : Modification dynamique de l'ouverture de la vanne (valve_opening) dans les limites [0.0 - 1.0].</li>");
|
||||
html.push_str("</ul></div>");
|
||||
|
||||
match result {
|
||||
Ok(converged) => {
|
||||
html.push_str(&format!("<p class='success'>✅ Modèle Résolu Thermodynamiquement avec succès en {} itérations de Newton-Raphson.</p>", converged.iterations));
|
||||
html.push_str("<h2>États du Cycle (Edges)</h2><table>");
|
||||
html.push_str("<tr><th>Connexion</th><th>Pression absolue (bar)</th><th>Température de Saturation (°C)</th><th>Enthalpie (kJ/kg)</th></tr>");
|
||||
|
||||
let sv = &converged.state;
|
||||
html.push_str(&format!("<tr><td>Compresseur → Condenseur</td><td>{:.2}</td><td>{:.2}</td><td>{:.2}</td></tr>", sv[0]/1e5, pressure_to_tsat_c(sv[0]), sv[1]/1e3));
|
||||
html.push_str(&format!("<tr><td>Condenseur → Détendeur</td><td>{:.2}</td><td>{:.2}</td><td>{:.2}</td></tr>", sv[2]/1e5, pressure_to_tsat_c(sv[2]), sv[3]/1e3));
|
||||
html.push_str(&format!("<tr><td>Détendeur → Évaporateur</td><td>{:.2}</td><td>{:.2}</td><td>{:.2}</td></tr>", sv[4]/1e5, pressure_to_tsat_c(sv[4]), sv[5]/1e3));
|
||||
html.push_str(&format!("<tr><td>Évaporateur → Compresseur</td><td>{:.2}</td><td>{:.2}</td><td>{:.2}</td></tr>", sv[6]/1e5, pressure_to_tsat_c(sv[6]), sv[7]/1e3));
|
||||
html.push_str("</table>");
|
||||
|
||||
html.push_str("<h2>Validation du Contrôle Inverse</h2><table>");
|
||||
html.push_str("<tr><th>Variable / Contrainte</th><th>Valeur Optimisée par le Solveur</th></tr>");
|
||||
|
||||
let superheat = (sv[7] / 1000.0) - (sv[6] / 1e5);
|
||||
html.push_str(&format!("<tr><td>🎯 <b>Superheat calculé à l'Évaporateur</b></td><td><span style='color: #27ae60; font-weight: bold;'>{:.2} K (Cible atteinte)</span></td></tr>", superheat));
|
||||
html.push_str(&format!("<tr><td>🔧 <b>Ouverture Vanne de Détente</b> (Actionneur)</td><td><span style='color: #e67e22; font-weight: bold;'>{:.4} (entre 0 et 1)</span></td></tr>", sv[8]));
|
||||
html.push_str("</table>");
|
||||
|
||||
html.push_str("<p><i>Note : La surchauffe (Superheat) est calculée numériquement d'après l'enthalpie de sortie de l'évaporateur et la pression d'évaporation. L'ouverture de la vanne a été automatiquement calibrée par la Jacobienne Newton-Raphson pour satisfaire cette contrainte exacte !</i></p>")
|
||||
|
||||
}
|
||||
Err(e) => {
|
||||
html.push_str(&format!("<p class='error'>❌ Échec lors de la convergence du Newton Raphson: {:?}</p>", e));
|
||||
}
|
||||
}
|
||||
html.push_str("</body></html>");
|
||||
|
||||
let mut file = File::create("resultats_integration_cycle.html").expect("Failed to create file");
|
||||
file.write_all(html.as_bytes()).expect("Failed to write HTML");
|
||||
|
||||
println!("File 'resultats_integration_cycle.html' generated successfully!");
|
||||
}
|
||||
Reference in New Issue
Block a user