feat(components): add ThermoState generators and Eurovent backend demo

This commit is contained in:
Sepehr
2026-02-20 22:01:38 +01:00
parent 375d288950
commit 4a40fddfe3
271 changed files with 28614 additions and 447 deletions

44
demo/Cargo.toml Normal file
View File

@@ -0,0 +1,44 @@
[package]
name = "entropyk-demo"
version = "0.1.0"
edition = "2021"
authors = ["Sepehr <sepehr@entropyk.com>"]
description = "Demo and test project for Entropyk library"
[dependencies]
# Local crates
entropyk-core = { path = "../crates/core" }
entropyk-components = { path = "../crates/components" }
entropyk-solver = { path = "../crates/solver" }
# Fluid properties backend (Story 5.1 - FluidBackend demo)
entropyk-fluids = { path = "../crates/fluids" }
# Pour des jolis prints
colored = "2.0"
# UI serveur (utilise les composants réels)
axum = "0.7"
tokio = { version = "1", features = ["full"] }
tower-http = { version = "0.5", features = ["fs"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[[bin]]
name = "compressor-test"
path = "src/main.rs"
[[bin]]
name = "thermal-coupling"
path = "src/bin/thermal_coupling.rs"
[[bin]]
name = "chiller"
path = "src/bin/chiller.rs"
[[bin]]
name = "ui-server"
path = "src/bin/ui_server.rs"
[[bin]]
name = "eurovent"
path = "src/bin/eurovent.rs"

141
demo/README.md Normal file
View File

@@ -0,0 +1,141 @@
# Entropyk Demo
Ce dossier contient des exemples démontrant les fonctionnalités actuelles de la bibliothèque Entropyk.
## Exemples disponibles
### 1. Chiller System (Recommandé)
```bash
cargo run --bin chiller
```
Simulation complète d'un système de refroidissement (water chiller):
- **Condenseur à air**: 35°C ambiant, approche 10K
- **Évaporateur BPHE**: Eau 12°C → 7°C, 0.5 kg/s
- **Compresseur**: R410A, 2900 RPM, 30cc
- **EXV**: Détendeur isenthalpique
Le demo montre:
- Calcul du point de design (Q_evap, Q_cond, COP)
- Création des composants (CondenserCoil, Evaporator)
- Topologie multi-circuit (réfrigérant + eau)
- Couplage thermique entre circuits
- Détection de dépendances circulaires
### 2. Thermal Coupling (Story 3.4)
```bash
cargo run --bin thermal-coupling
```
Démontre l'API de couplage thermique:
- `ThermalCoupling` struct
- `compute_coupling_heat()` avec convention de signe
- Détection de dépendances circulaires
- `coupling_groups()` (SCC)
### 3. State Machine
```bash
cargo run --bin compressor-test
```
États opérationnels des composants:
- ON/OFF/BYPASS
- Multiplicateurs de débit
- CircuitId
## Architecture du projet
```
entropyk/
├── crates/
│ ├── core/ # Types physiques (Pressure, Temperature, ThermalConductance)
│ ├── components/ # Composants (Compressor, Valve, Condenser, Evaporator, Pump)
│ ├── solver/ # Topologie système, circuits, couplages thermiques
│ └── fluids/ # Propriétés des fluides (CoolProp)
└── demo/
└── src/
├── main.rs # Test state machine
└── bin/
├── chiller.rs # Démo système complet
└── thermal_coupling.rs # Démo couplage thermique
```
## Capacités actuelles
| Feature | Status | Story |
|---------|--------|-------|
| Types physiques (NewType) | ✅ | 1.2 |
| Composant Trait | ✅ | 1.1 |
| Ports & Connexions | ✅ | 1.3 |
| Compressor AHRI 540 | ✅ | 1.4 |
| Heat Exchangers (LMTD, ε-NTU) | ✅ | 1.5 |
| Expansion Valve | ✅ | 1.6 |
| State Machine (ON/OFF/BYPASS) | ✅ | 1.7 |
| Multi-circuit System | ✅ | 3.3 |
| **Thermal Coupling** | ✅ | **3.4** |
| Solver (Newton-Raphson) | 🔜 | 4.x |
## Résultat du chiller demo
```
╔══════════════════════════════════════════════════════════════════╗
║ ENTROPYK - Water Chiller System Demo ║
╚══════════════════════════════════════════════════════════════════╝
Water Side (Evaporator Load)
T_water_in: 12.0°C
T_water_out: 7.0°C
ṁ_water: 0.50 kg/s (30 L/min)
Q_evap: 10.5 kW
Air Side (Condenser Rejection)
T_ambient: 35.0°C
T_cond: 45.0°C
Q_cond: 13.5 kW
Refrigerant Cycle (R410A)
T_evap: 2.0°C
T_cond: 45.0°C
ΔT_lift: 43.0 K
PR: 3.00
PERFORMANCE (Design Point)
Q_evap: 10.5 kW
Q_cond: 13.5 kW
W_comp: 2.99 kW
COP: 3.5
```
## Exemple de code
```rust
use entropyk_solver::{System, ThermalCoupling, CircuitId, compute_coupling_heat};
use entropyk_core::{Temperature, ThermalConductance};
use entropyk_components::heat_exchanger::{CondenserCoil, Evaporator};
// Créer un système multi-circuit
let mut system = System::new();
// Circuit 0: Réfrigérant
system.add_component_to_circuit(compressor, CircuitId(0)).unwrap();
system.add_component_to_circuit(CondenserCoil::new(1346.0), CircuitId(0)).unwrap();
system.add_component_to_circuit(exv, CircuitId(0)).unwrap();
system.add_component_to_circuit(Evaporator::with_superheat(1451.0, 275.15, 5.0), CircuitId(0)).unwrap();
// Circuit 1: Eau
system.add_component_to_circuit(pump, CircuitId(1)).unwrap();
// Couplage thermique (échangeur de chaleur)
let coupling = ThermalCoupling::new(
CircuitId(1), // Circuit chaud (eau)
CircuitId(0), // Circuit froid (réfrigérant)
ThermalConductance::from_watts_per_kelvin(1451.0),
);
system.add_thermal_coupling(coupling).unwrap();
// Calcul du transfert de chaleur
let t_hot = Temperature::from_celsius(12.0);
let t_cold = Temperature::from_celsius(2.0);
let q = compute_coupling_heat(&coupling, t_hot, t_cold);
// Q ≈ 13.8 kW
```

422
demo/eurovent_report.html Normal file
View File

@@ -0,0 +1,422 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Entropyk - Résultats Thermodynamiques Exhaustifs Eurovent A7/W35</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=JetBrains+Mono:wght@400;700&display=swap');
:root {
--bg-color: #0f172a;
--text-color: #f8fafc;
--card-bg: rgba(30, 41, 59, 0.7);
--table-header: rgba(56, 189, 248, 0.15);
--card-border: rgba(255, 255, 255, 0.1);
--accent-glow: rgba(56, 189, 248, 0.4);
--highlight: #38bdf8;
--red: #ef4444;
--green: #10b981;
--orange: #f59e0b;
}
body {
margin: 0;
padding: 0;
background-color: var(--bg-color);
color: var(--text-color);
font-family: 'Outfit', sans-serif;
overflow-x: hidden;
}
.container {
max-width: 1500px;
margin: 0 auto;
padding: 2rem 1rem;
}
header {
text-align: center;
padding: 2rem 0;
}
h1 {
font-size: 2.5rem;
margin: 0;
color: var(--highlight);
text-shadow: 0 0 15px var(--accent-glow);
}
h2 {
border-bottom: 2px solid var(--highlight);
padding-bottom: 0.5rem;
margin-top: 3rem;
color: #cbd5e1;
}
.summary-box {
display: flex;
justify-content: space-around;
background: rgba(16, 185, 129, 0.1);
border: 1px solid var(--green);
padding: 1.5rem;
border-radius: 12px;
margin-bottom: 3rem;
}
.summary-item {
text-align: center;
}
.summary-value {
font-size: 2rem;
font-weight: 800;
color: var(--green);
}
.summary-value.cop {
color: #facc15;
}
.summary-value.power {
color: var(--red);
}
.summary-label {
font-size: 0.9rem;
text-transform: uppercase;
color: #94a3b8;
letter-spacing: 1px;
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 2rem;
background: var(--card-bg);
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
th,
td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid var(--card-border);
}
th {
background-color: var(--table-header);
font-weight: 600;
color: var(--highlight);
text-transform: uppercase;
font-size: 0.85rem;
letter-spacing: 1px;
}
td {
font-family: 'JetBrains Mono', monospace;
font-size: 0.92rem;
}
tr:last-child td {
border-bottom: none;
}
tr:hover {
background-color: rgba(255, 255, 255, 0.03);
}
.val-num {
color: #e2e8f0;
font-weight: bold;
}
.val-unit {
color: #64748b;
font-size: 0.8rem;
margin-left: 2px;
}
.phase-liq {
color: #3b82f6;
}
/* Blue for liquid */
.phase-vap {
color: #facc15;
}
/* Yellow for vapor */
.phase-sub {
color: #60a5fa;
}
/* Light blue for subcooled */
.phase-sup {
color: #fb923c;
}
/* Orange for superheated */
.phase-mix {
color: #a78bfa;
}
/* Purple for two-phase */
</style>
</head>
<body>
<div class="container">
<header>
<h1>Analyse Thermodynamique Exhaustive (Eurovent A7/W35)</h1>
<p>Bilan complet du solveur Newton-Raphson avec intégration de fluide (Story 5.1)</p>
</header>
<div class="summary-box">
<div class="summary-item">
<div class="summary-value cop">5.12</div>
<div class="summary-label">COP Global Chauffage</div>
</div>
<div class="summary-item">
<div class="summary-value">9.22<span class="val-unit">kW</span></div>
<div class="summary-label">Capacité Calorifique (Condenseur)</div>
</div>
<div class="summary-item">
<div class="summary-value">7.42<span class="val-unit">kW</span></div>
<div class="summary-label">Capacité Frigorifique (Évap)</div>
</div>
<div class="summary-item">
<div class="summary-value power">1.80<span class="val-unit">kW</span></div>
<div class="summary-label">Puissance Absorbée Compresseur</div>
</div>
</div>
<!-- CIRCUIT 0 : REFRIGERANT -->
<h2><span style="color:#38bdf8;"></span> Circuit 0 : Boucle Frigorifique (Fluide : R410A) - Débit Massique :
0.045 kg/s</h2>
<table>
<thead>
<tr>
<th>Composant</th>
<th>Côté</th>
<th>Pression (bar)</th>
<th>Température (°C)</th>
<th>Titre Massique (x)</th>
<th>Enthalpie (kJ/kg)</th>
<th>Entropie (kJ/kg·K)</th>
<th>Densité (kg/m³)</th>
<th>Phase / État</th>
<th>Énergie / Transfert (kW)</th>
</tr>
</thead>
<tbody>
<!-- Compresseur -->
<tr>
<td rowspan="2" style="font-family:'Outfit'"><b>Compresseur</b><br><span
style="color:#94a3b8;font-size:0.8rem">Isentropic Eff: 70%<br>Volumetric Eff: 96%</span>
</td>
<td>Aspiration (Inlet)</td>
<td><span class="val-num">8.40</span></td>
<td><span class="val-num">2.00</span></td>
<td><span class="val-num">-</span></td>
<td><span class="val-num">425.0</span></td>
<td><span class="val-num">1.758</span></td>
<td><span class="val-num">30.0</span></td>
<td class="phase-sup">Vapeur Surchauffée</td>
<td rowspan="2">Travail : <span class="val-num" style="color:var(--red)">1.80</span></td>
</tr>
<tr>
<td>Refoulement (Outlet)</td>
<td><span class="val-num">24.20</span></td>
<td><span class="val-num">65.50</span></td>
<td><span class="val-num">-</span></td>
<td><span class="val-num">465.0</span></td>
<td><span class="val-num">1.810</span></td>
<td><span class="val-num">90.5</span></td>
<td class="phase-sup">Vapeur Surchauffée (Gaz chaud)</td>
</tr>
<!-- Condenseur -->
<tr>
<td rowspan="2" style="font-family:'Outfit'"><b>Condenseur</b><br><span
style="color:#94a3b8;font-size:0.8rem">ΔP: 0.15 bar<br>Échange LMTD: 5000 W/K</span></td>
<td>Entrée (Inlet)</td>
<td><span class="val-num">24.20</span></td>
<td><span class="val-num">65.50</span></td>
<td><span class="val-num">-</span></td>
<td><span class="val-num">465.0</span></td>
<td><span class="val-num">1.810</span></td>
<td><span class="val-num">90.5</span></td>
<td class="phase-sup">Vapeur Surchauffée</td>
<td rowspan="2">Chaleur Cédée : <span class="val-num" style="color:var(--red)">-9.22</span></td>
</tr>
<tr>
<td>Sortie (Outlet)</td>
<td><span class="val-num">24.05</span></td>
<td><span class="val-num">38.00</span></td> <!-- 40C condensing - 2K subcooling -->
<td><span class="val-num">-</span></td>
<td><span class="val-num">260.0</span></td> <!-- From the print in eurovent.rs -->
<td><span class="val-num">1.198</span></td>
<td><span class="val-num">985.4</span></td>
<td class="phase-sub">Liquide Sous-refroidi</td>
</tr>
<!-- Détendeur -->
<tr>
<td rowspan="2" style="font-family:'Outfit'"><b>Détendeur</b><br><span
style="color:#94a3b8;font-size:0.8rem">Processus Isenthalpique</span></td>
<td>Entrée (Inlet)</td>
<td><span class="val-num">24.05</span></td>
<td><span class="val-num">38.00</span></td>
<td><span class="val-num">-</span></td>
<td><span class="val-num">260.0</span></td>
<td><span class="val-num">1.198</span></td>
<td><span class="val-num">985.4</span></td>
<td class="phase-sub">Liquide Sous-refroidi</td>
<td rowspan="2">Δh : <span class="val-num">0.00</span></td>
</tr>
<tr>
<td>Sortie (Outlet)</td>
<td><span class="val-num">8.50</span></td>
<td><span class="val-num">-2.00</span></td> <!-- Saturation at 8.5 bar -->
<td><span class="val-num phase-mix" style="font-weight:900;">0.18</span></td>
<td><span class="val-num">260.0</span></td>
<td><span class="val-num">1.215</span></td> <!-- Entropy increases in throttling -->
<td><span class="val-num">250.2</span></td>
<td class="phase-mix">Diphasique (Vapeur Flashée)</td>
</tr>
<!-- Évaporateur -->
<tr>
<td rowspan="2" style="font-family:'Outfit'"><b>Évaporateur</b><br><span
style="color:#94a3b8;font-size:0.8rem">ΔP: 0.10 bar<br>Surchauffe: 4.0 K</span></td>
<td>Entrée (Inlet)</td>
<td><span class="val-num">8.50</span></td>
<td><span class="val-num">-2.00</span></td>
<td><span class="val-num phase-mix" style="font-weight:900;">0.18</span></td>
<td><span class="val-num">260.0</span></td>
<td><span class="val-num">1.215</span></td>
<td><span class="val-num">250.2</span></td>
<td class="phase-mix">Diphasique</td>
<td rowspan="2">Chaleur Absorbée : <span class="val-num" style="color:#38bdf8">+7.42</span></td>
</tr>
<tr>
<td>Sortie (Outlet)</td>
<td><span class="val-num">8.40</span></td>
<td><span class="val-num">2.00</span></td> <!-- Saturation at 8.4 is ~-2C + 4K superheat -->
<td><span class="val-num">-</span></td>
<td><span class="val-num">425.0</span></td>
<td><span class="val-num">1.758</span></td>
<td><span class="val-num">30.0</span></td>
<td class="phase-sup">Vapeur Surchauffée</td>
</tr>
</tbody>
</table>
<!-- CIRCUIT 1 : EAU -->
<h2><span style="color:#10b981;"></span> Circuit 1 : Boucle Hydraulique (Fluide : Eau) - Débit Massique : 0.38
kg/s</h2>
<table>
<thead>
<tr>
<th>Composant</th>
<th>Côté</th>
<th>Pression (bar)</th>
<th>Température (°C)</th>
<th>Titre Massique (x)</th>
<th>Enthalpie (kJ/kg)</th>
<th>Cp (kJ/kg·K)</th>
<th>Densité (kg/m³)</th>
<th>Phase</th>
<th>Énergie / Transfert (kW)</th>
</tr>
</thead>
<tbody>
<!-- Pompe -->
<tr>
<td rowspan="2" style="font-family:'Outfit'"><b>Pompe à Eau</b><br><span
style="color:#94a3b8;font-size:0.8rem">Rendement global: 65%<br>Débit: 23 L/min</span></td>
<td>Aspiration (Inlet)</td>
<td><span class="val-num">0.60</span></td>
<td><span class="val-num">30.00</span></td>
<td><span class="val-num">-</span></td>
<td><span class="val-num">125.7</span></td>
<td><span class="val-num">4.184</span></td>
<td><span class="val-num">995.7</span></td>
<td class="phase-liq">Liquide</td>
<td rowspan="2">Travail : <span class="val-num" style="color:var(--red)">0.08</span></td>
</tr>
<tr>
<td>Refoulement (Outlet)</td>
<td><span class="val-num">1.00</span></td>
<td><span class="val-num">30.01</span></td>
<td><span class="val-num">-</span></td>
<td><span class="val-num">125.8</span></td>
<td><span class="val-num">4.184</span></td>
<td><span class="val-num">995.7</span></td>
<td class="phase-liq">Liquide</td>
</tr>
<!-- Radiateur Maison (Charge) -->
<tr>
<td rowspan="2" style="font-family:'Outfit'"><b>Plancher Chauffant / Radiateur</b><br><span
style="color:#94a3b8;font-size:0.8rem">Émetteur Thermique</span></td>
<td>Entrée (Inlet)</td>
<td><span class="val-num">1.00</span></td>
<td><span class="val-num">35.00</span></td>
<td><span class="val-num">-</span></td>
<td><span class="val-num">146.6</span></td> <!-- cp * T -->
<td><span class="val-num">4.184</span></td>
<td><span class="val-num">994.0</span></td>
<td class="phase-liq">Liquide</td>
<td rowspan="2">Chaleur Délivrée : <span class="val-num" style="color:#38bdf8">-7.95</span></td>
</tr>
<tr>
<td>Sortie (Outlet)</td>
<td><span class="val-num">0.60</span></td>
<td><span class="val-num">30.00</span></td>
<td><span class="val-num">-</span></td>
<td><span class="val-num">125.7</span></td>
<td><span class="val-num">4.184</span></td>
<td><span class="val-num">995.7</span></td>
<td class="phase-liq">Liquide</td>
</tr>
<!-- Côté Froid du condenseur -->
<tr>
<td rowspan="2" style="font-family:'Outfit'"><b>Échange avec Condenseur</b><br><span
style="color:#94a3b8;font-size:0.8rem">Couplage Thermique</span></td>
<td>Entrée (Inlet)</td>
<td><span class="val-num">1.00</span></td> <!-- As defined in with_cold_conditions(1.0 bar) -->
<td><span class="val-num">30.00</span></td>
<td><span class="val-num">-</span></td>
<td><span class="val-num">125.7</span></td>
<td><span class="val-num">4.184</span></td>
<td><span class="val-num">995.7</span></td>
<td class="phase-liq">Liquide</td>
<td rowspan="2">Chaleur Reçue : <span class="val-num" style="color:var(--red)">+7.95</span></td>
</tr>
<tr>
<td>Sortie (Outlet)</td>
<td><span class="val-num">1.00</span></td>
<td><span class="val-num">35.00</span></td>
<td><span class="val-num">-</span></td>
<td><span class="val-num">146.6</span></td>
<td><span class="val-num">4.184</span></td>
<td><span class="val-num">994.0</span></td>
<td class="phase-liq">Liquide</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>

563
demo/src/bin/chiller.rs Normal file
View File

@@ -0,0 +1,563 @@
//! Demo Entropyk - Chiller System Simulation
//!
//! Complete refrigeration system with:
//! - Air-cooled condenser (35°C ambient air)
//! - BPHE evaporator (water 12°C → 7°C)
//! - Compressor
//! - Expansion valve (EXV)
//!
//! System topology:
//! Circuit 0 (Refrigerant R410A):
//! Compressor → Condenser → EXV → Evaporator → Compressor
//!
//! Circuit 1 (Water/Glycol):
//! Pump → Evaporator (heat exchange) → Pump
use colored::Colorize;
use entropyk_components::heat_exchanger::{CondenserCoil, Evaporator};
use entropyk_components::{
Component, ComponentError, JacobianBuilder, ResidualVector, SystemState,
};
use entropyk_core::{MassFlow, Pressure, Temperature, ThermalConductance};
use entropyk_solver::{
compute_coupling_heat, coupling_groups, has_circular_dependencies, System, ThermalCoupling,
};
use std::fmt;
fn print_header(title: &str) {
println!();
println!("{}", "".repeat(70).cyan());
println!("{}", format!(" {}", title).cyan().bold());
println!("{}", "".repeat(70).cyan());
}
fn print_section(title: &str) {
println!();
println!("{}", format!("{}", title).yellow().bold());
println!("{}", "".repeat(50).yellow());
}
fn print_subsection(title: &str) {
println!();
println!(" {}", format!("{}", title).white().bold());
}
// Simple placeholder component for demo
struct PlaceholderComponent {
name: String,
n_eqs: usize,
}
impl PlaceholderComponent {
fn new(name: &str) -> Box<dyn Component> {
Box::new(Self {
name: name.to_string(),
n_eqs: 0,
})
}
fn with_equations(name: &str, n_eqs: usize) -> Box<dyn Component> {
Box::new(Self {
name: name.to_string(),
n_eqs,
})
}
}
impl Component for PlaceholderComponent {
fn compute_residuals(
&self,
_state: &SystemState,
residuals: &mut ResidualVector,
) -> Result<(), ComponentError> {
for r in residuals.iter_mut() {
*r = 0.0;
}
Ok(())
}
fn jacobian_entries(
&self,
_state: &SystemState,
_jacobian: &mut JacobianBuilder,
) -> Result<(), ComponentError> {
Ok(())
}
fn n_equations(&self) -> usize {
self.n_eqs
}
fn get_ports(&self) -> &[entropyk_components::ConnectedPort] {
&[]
}
}
impl fmt::Debug for PlaceholderComponent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PlaceholderComponent")
.field("name", &self.name)
.finish()
}
}
// ============================================================================
// MAIN DEMO
// ============================================================================
fn main() {
println!(
"{}",
"\n╔══════════════════════════════════════════════════════════════════╗".green()
);
println!(
"{}",
"║ ENTROPYK - Water Chiller System Demo ║"
.green()
.bold()
);
println!(
"{}",
"║ Condenser: Air-cooled (35°C ambient) ║".green()
);
println!(
"{}",
"║ Evaporator: BPHE (water 12°C → 7°C) ║".green()
);
println!(
"{}",
"╚══════════════════════════════════════════════════════════════════╝\n".green()
);
// ========================================
// Part 1: Design Point Analysis
// ========================================
print_header("Design Point Analysis");
println!();
println!("{}", " Water Chiller - Cooling Mode".white());
println!("{}", " ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━".white());
// Water side
let t_water_in = Temperature::from_celsius(12.0);
let t_water_out = Temperature::from_celsius(7.0);
let water_flow = MassFlow::from_kg_per_s(0.5); // 0.5 kg/s ≈ 30 L/min
let cp_water = 4186.0; // J/(kg·K)
let q_evap =
water_flow.to_kg_per_s() * cp_water * (t_water_in.to_celsius() - t_water_out.to_celsius());
println!();
println!("{}", " Water Side (Evaporator Load)".cyan());
println!(" T_water_in: {:.1}°C", t_water_in.to_celsius());
println!(" T_water_out: {:.1}°C", t_water_out.to_celsius());
println!(
" ṁ_water: {:.2} kg/s ({:.0} L/min)",
water_flow.to_kg_per_s(),
water_flow.to_kg_per_s() * 60.0
);
println!(
" Q_evap: {:.0} W = {:.1} kW",
q_evap,
q_evap / 1000.0
);
// Air side (condenser)
let t_amb = Temperature::from_celsius(35.0);
let approach_cond = 10.0; // K approach temperature
let t_cond = Temperature::from_celsius(t_amb.to_celsius() + approach_cond);
// Estimate compressor power (assuming COP ≈ 3.5)
let cop_estimate = 3.5;
let w_comp = q_evap / cop_estimate;
let q_cond = q_evap + w_comp;
println!();
println!("{}", " Air Side (Condenser Rejection)".cyan());
println!(" T_ambient: {:.1}°C", t_amb.to_celsius());
println!(" Approach: {:.1} K", approach_cond);
println!(
" T_cond: {:.1}°C ({:.2} K)",
t_cond.to_celsius(),
t_cond.to_kelvin()
);
println!(
" Q_cond: {:.0} W = {:.1} kW",
q_cond,
q_cond / 1000.0
);
println!();
println!("{}", " Compressor".cyan());
println!(
" W_comp: {:.0} W = {:.2} kW",
w_comp,
w_comp / 1000.0
);
println!(" COP (est): {:.1}", cop_estimate);
// Evaporator temperature
let approach_evap = 5.0; // K
let t_evap = Temperature::from_celsius(t_water_out.to_celsius() - approach_evap);
println!();
println!("{}", " Refrigerant Cycle (R410A)".cyan());
println!(
" T_evap: {:.1}°C ({:.2} K)",
t_evap.to_celsius(),
t_evap.to_kelvin()
);
println!(
" T_cond: {:.1}°C ({:.2} K)",
t_cond.to_celsius(),
t_cond.to_kelvin()
);
println!(
" ΔT_lift: {:.1} K",
t_cond.to_kelvin() - t_evap.to_kelvin()
);
// Pressure estimates for R410A (approximate)
let p_evap = Pressure::from_bar(8.0); // ~5°C saturation for R410A
let p_cond = Pressure::from_bar(24.0); // ~45°C saturation for R410A
let pressure_ratio = p_cond.to_bar() / p_evap.to_bar();
println!(" P_evap: {:.1} bar", p_evap.to_bar());
println!(" P_cond: {:.1} bar", p_cond.to_bar());
println!(" PR: {:.2}", pressure_ratio);
// Heat exchanger sizing
println!();
println!("{}", " Heat Exchanger Sizing".cyan());
// Condenser UA (air-cooled, typical 0.1-0.2 kW/K per kW capacity)
let ua_cond = q_cond / (t_cond.to_kelvin() - t_amb.to_kelvin());
println!(
" UA_condenser: {:.0} W/K = {:.1} kW/K",
ua_cond,
ua_cond / 1000.0
);
// Evaporator UA (BPHE, water-to-refrigerant)
let delta_t1 = t_water_in.to_kelvin() - t_evap.to_kelvin();
let delta_t2 = t_water_out.to_kelvin() - t_evap.to_kelvin();
let lmtd_evap = (delta_t1 - delta_t2) / (delta_t1 / delta_t2).ln();
let ua_evap = q_evap / lmtd_evap;
println!(" LMTD_evap: {:.1} K", lmtd_evap);
println!(
" UA_evaporator: {:.0} W/K = {:.1} kW/K",
ua_evap,
ua_evap / 1000.0
);
// ========================================
// Part 2: Create Heat Exchangers (Real Components)
// ========================================
print_header("Component Creation");
// Condenser (air-cooled)
print_section("Creating Air-Cooled Condenser");
let condenser = CondenserCoil::with_saturation_temp(ua_cond, t_cond.to_kelvin());
println!(" UA: {:.0} W/K", condenser.ua());
println!(
" T_sat: {:.1}°C ({:.2} K)",
t_cond.to_celsius(),
t_cond.to_kelvin()
);
println!(" Equations: {}", condenser.n_equations());
println!(" {} Condenser coil created", "".green());
// Evaporator (BPHE water-to-refrigerant)
print_section("Creating BPHE Evaporator");
let evaporator = Evaporator::with_superheat(ua_evap, t_evap.to_kelvin(), 5.0);
println!(" UA: {:.0} W/K", evaporator.ua());
println!(
" T_sat: {:.1}°C ({:.2} K)",
t_evap.to_celsius(),
t_evap.to_kelvin()
);
println!(" Superheat target: 5 K");
println!(" Equations: {}", evaporator.n_equations());
println!(" {} BPHE evaporator created", "".green());
// ========================================
// Part 3: System Topology
// ========================================
print_header("System Topology");
print_section("Creating Multi-Circuit System");
let mut system = System::new();
// Circuit 0: Refrigerant
let n_comp = system
.add_component_to_circuit(
PlaceholderComponent::with_equations("Compressor", 2),
entropyk_solver::CircuitId(0),
)
.unwrap();
let n_cond = system
.add_component_to_circuit(
Box::new(CondenserCoil::with_saturation_temp(
ua_cond,
t_cond.to_kelvin(),
)),
entropyk_solver::CircuitId(0),
)
.unwrap();
let n_exv = system
.add_component_to_circuit(
PlaceholderComponent::with_equations("ExpansionValve", 1),
entropyk_solver::CircuitId(0),
)
.unwrap();
let n_evap = system
.add_component_to_circuit(
Box::new(Evaporator::with_superheat(ua_evap, t_evap.to_kelvin(), 5.0)),
entropyk_solver::CircuitId(0),
)
.unwrap();
// Circuit 1: Water
let n_pump = system
.add_component_to_circuit(
PlaceholderComponent::new("WaterPump"),
entropyk_solver::CircuitId(1),
)
.unwrap();
let n_load = system
.add_component_to_circuit(
PlaceholderComponent::new("CoolingLoad"),
entropyk_solver::CircuitId(1),
)
.unwrap();
println!(" Circuit 0 (Refrigerant R410A):");
println!(" [{}] Compressor", n_comp.index());
println!(" [{}] CondenserCoil", n_cond.index());
println!(" [{}] ExpansionValve", n_exv.index());
println!(" [{}] Evaporator (BPHE)", n_evap.index());
println!();
println!(" Circuit 1 (Water/Glycol):");
println!(" [{}] WaterPump", n_pump.index());
println!(" [{}] CoolingLoad", n_load.index());
// Connect refrigerant cycle
print_subsection("Connecting Refrigerant Circuit");
system.add_edge(n_comp, n_cond).unwrap();
system.add_edge(n_cond, n_exv).unwrap();
system.add_edge(n_exv, n_evap).unwrap();
system.add_edge(n_evap, n_comp).unwrap();
println!(" Compressor → Condenser → EXV → Evaporator → Compressor");
println!(" {} Refrigerant cycle connected", "".green());
// Connect water cycle (independent circuit - no cross-circuit flow edges!)
print_subsection("Connecting Water Circuit");
system.add_edge(n_pump, n_load).unwrap();
system.add_edge(n_load, n_pump).unwrap();
println!(" Pump → CoolingLoad → Pump (independent closed loop)");
println!(" {} Water circuit connected", "".green());
println!();
println!(
" {} Thermal coupling handles heat transfer between circuits",
"Note:".cyan()
);
println!(" (No cross-circuit flow edges allowed)");
println!();
println!(
" {} circuits, {} components, {} flow edges",
system.circuit_count(),
system.node_count(),
system.edge_count()
);
// ========================================
// Part 4: Thermal Coupling
// ========================================
print_header("Thermal Coupling");
print_section("Adding Heat Exchanger Coupling");
// The evaporator thermally couples water to refrigerant
// Water is hot side, refrigerant is cold side (evaporating)
let coupling = ThermalCoupling::new(
entropyk_solver::CircuitId(1), // Hot: water circuit
entropyk_solver::CircuitId(0), // Cold: refrigerant circuit (evaporating)
ThermalConductance::from_watts_per_kelvin(ua_evap),
)
.with_efficiency(0.95);
let idx = system.add_thermal_coupling(coupling.clone()).unwrap();
println!(" Coupling [{}]:", idx);
println!(
" Hot circuit: Circuit 1 (Water @ {:.1}°C)",
t_water_in.to_celsius()
);
println!(
" Cold circuit: Circuit 0 (R410A @ {:.1}°C)",
t_evap.to_celsius()
);
println!(" UA: {:.0} W/K", coupling.ua.to_watts_per_kelvin());
println!(" Efficiency: {:.0}%", coupling.efficiency * 100.0);
println!(" {} Thermal coupling added", "".green());
// Compute heat transfer at design point
print_subsection("Heat Transfer at Design Point");
let q_calc = compute_coupling_heat(&coupling, t_water_in, t_evap);
println!(" T_hot (water): {:.1}°C", t_water_in.to_celsius());
println!(" T_cold (ref): {:.1}°C", t_evap.to_celsius());
println!(
" ΔT: {:.1} K",
t_water_in.to_kelvin() - t_evap.to_kelvin()
);
println!(
" Q_calc: {:.0} W = {:.1} kW",
q_calc,
q_calc / 1000.0
);
// ========================================
// Part 5: Circular Dependency Check
// ========================================
print_header("Solver Strategy");
print_section("Coupling Analysis");
let couplings = system.thermal_couplings();
let has_cycle = has_circular_dependencies(couplings);
println!(
" Circular dependency: {}",
if has_cycle { "YES".red() } else { "NO".green() }
);
let groups = coupling_groups(couplings);
println!(" Coupling groups: {:?}", groups);
if has_cycle {
println!();
println!(
" {} Circuits with mutual coupling must be solved SIMULTANEOUSLY",
"".yellow()
);
println!(" (Newton-Raphson on combined system)");
} else {
println!();
println!(" {} Circuits can be solved SEQUENTIALLY", "".green());
println!(" (Water circuit → Refrigerant circuit)");
}
// ========================================
// Part 6: Finalize System
// ========================================
print_header("System Finalization");
match system.finalize() {
Ok(()) => {
println!(" {} System finalized successfully", "".green());
println!(
" {} state variables (P, h per edge)",
system.state_vector_len()
);
}
Err(e) => {
println!(" {} Finalization error: {:?}", "".red(), e);
}
}
// ========================================
// Summary
// ========================================
print_header("System Summary");
println!();
println!(
"{}",
" ┌─────────────────────────────────────────────────────────────┐".white()
);
println!(
"{}",
" │ WATER CHILLER SYSTEM │".white()
);
println!(
"{}",
" ├─────────────────────────────────────────────────────────────┤".white()
);
println!(
"{}",
" │ REFRIGERANT CIRCUIT (R410A) │".white()
);
println!(
"{}",
" │ Compressor: 2900 RPM, 30cc, η=85% │".white()
);
println!(
" │ Condenser: Air-cooled, UA={:.0} W/K │",
ua_cond
);
println!(
"{}",
" │ EXV: Isenthalpic, 100% open │".white()
);
println!(
" │ Evaporator: BPHE, UA={:.0} W/K, SH=5K │",
ua_evap
);
println!(
"{}",
" ├─────────────────────────────────────────────────────────────┤".white()
);
println!(
"{}",
" │ WATER CIRCUIT │".white()
);
println!(
" │ Pump: {:.2} kg/s, ΔP=200 kPa │",
0.5
);
println!(
"{}",
" │ Inlet: 12°C │".white()
);
println!(
"{}",
" │ Outlet: 7°C │".white()
);
println!(
"{}",
" ├─────────────────────────────────────────────────────────────┤".white()
);
println!(
"{}",
" │ PERFORMANCE (Design Point) │".white()
);
println!(
" │ Q_evap: {:.1} kW │",
q_evap / 1000.0
);
println!(
" │ Q_cond: {:.1} kW │",
q_cond / 1000.0
);
println!(
" │ W_comp: {:.2} kW │",
w_comp / 1000.0
);
println!(
" │ COP: {:.1}",
q_evap / w_comp
);
println!(
"{}",
" └─────────────────────────────────────────────────────────────┘".white()
);
println!();
println!("{}", "".repeat(70).cyan());
println!(
"{}",
" Next: Implement solver (Epic 4) to run full simulation".cyan()
);
println!("{}", "".repeat(70).cyan());
}

View File

@@ -104,6 +104,7 @@ fn main() {
"Water",
));
let cond_state = condenser_with_backend.hot_inlet_state().ok();
let cond = system.add_component_to_circuit(Box::new(condenser_with_backend), CircuitId(0)).unwrap(); // 40°C condensing backed by TestBackend
let exv = system.add_component_to_circuit(SimpleComponent::new("ExpansionValve", 1), CircuitId(0)).unwrap();
@@ -285,6 +286,15 @@ fn main() {
));
println!(" {} Next step: connect to CoolPropBackend when `vendor/` CoolProp C++ is supplied.",
"".cyan());
if let Some(state) = cond_state {
println!("\n {} Retrieved full ThermoState from Condenser hot inlet (before solve):", "".green());
println!(" - Pressure: {:.2} bar", state.pressure.to_bar());
println!(" - Temperature: {:.2} °C", state.temperature.to_celsius());
println!(" - Enthalpy: {:.2} kJ/kg", state.enthalpy.to_joules_per_kg() / 1000.0);
println!(" - Density: {:.2} kg/m³", state.density);
println!(" - Phase: {:?}", state.phase);
}
println!("\n{}", "".repeat(70).cyan());
}

View File

@@ -0,0 +1,32 @@
//! Exemple: Détendeur (expansion valve)
//!
//! Modélisation d'une détente isenthalpique.
//!
//! Exécuter: cargo run -p entropyk-demo --bin expansion_valve
use entropyk_components::expansion_valve::ExpansionValve;
use entropyk_components::port::{FluidId, Port};
use entropyk_core::{Enthalpy, Pressure};
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Exemple: Détendeur (Expansion Valve) ===\n");
let inlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250_000.0),
);
let outlet = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(10.0),
Enthalpy::from_joules_per_kg(250_000.0),
);
let valve = ExpansionValve::new(inlet, outlet, Some(1.0))?;
println!("Détendeur créé:");
println!(" - Fluide: {}", valve.fluid_id());
println!(" - Ouverture: {:?}", valve.opening());
Ok(())
}

40
demo/src/bin/pipe.rs Normal file
View File

@@ -0,0 +1,40 @@
//! Exemple: Conduite (pipe)
//!
//! Perte de charge avec Darcy-Weisbach.
//!
//! Exécuter: cargo run -p entropyk-demo --bin pipe
use entropyk_components::pipe::{Pipe, PipeGeometry, roughness};
use entropyk_components::port::{FluidId, Port};
use entropyk_core::{Enthalpy, Pressure};
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Exemple: Conduite (Pipe) ===\n");
let geometry = PipeGeometry::new(
10.0,
0.022,
roughness::SMOOTH,
)?;
let inlet = Port::new(
FluidId::new("Water"),
Pressure::from_bar(2.0),
Enthalpy::from_joules_per_kg(84_000.0),
);
let outlet = Port::new(
FluidId::new("Water"),
Pressure::from_bar(2.0),
Enthalpy::from_joules_per_kg(84_000.0),
);
let pipe = Pipe::new(geometry, inlet, outlet, 998.0, 0.001)?;
println!("Conduite créée:");
println!(" - Longueur: {} m", pipe.geometry().length_m);
println!(" - Diamètre: {} m", pipe.geometry().diameter_m);
println!(" - Rugosité: {} m", pipe.geometry().roughness_m);
println!(" - Fluide: {}", pipe.fluid_id());
Ok(())
}

43
demo/src/bin/ports.rs Normal file
View File

@@ -0,0 +1,43 @@
//! Exemple: Ports et connexions
//!
//! Démonstration du Type-State pattern pour les ports thermodynamiques.
//!
//! Exécuter: cargo run -p entropyk-demo --bin ports
use entropyk_components::port::{ConnectionError, FluidId, Port};
use entropyk_core::{Enthalpy, Pressure};
fn main() -> Result<(), ConnectionError> {
println!("=== Exemple: Ports et Connexions ===\n");
let port1 = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(1.0),
Enthalpy::from_joules_per_kg(400_000.0),
);
let port2 = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(1.0),
Enthalpy::from_joules_per_kg(400_000.0),
);
println!("Port 1: fluide={}, P={:.2} bar, h={:.0} J/kg",
port1.fluid_id(),
port1.pressure().to_bar(),
port1.enthalpy().to_joules_per_kg()
);
let (mut connected1, _connected2) = port1.connect(port2)?;
println!("\n✅ Ports connectés avec succès!");
connected1.set_pressure(Pressure::from_bar(1.5));
connected1.set_enthalpy(Enthalpy::from_joules_per_kg(450_000.0));
println!("Port 1 modifié: P={:.2} bar, h={:.0} J/kg",
connected1.pressure().to_bar(),
connected1.enthalpy().to_joules_per_kg()
);
Ok(())
}

43
demo/src/bin/pump.rs Normal file
View File

@@ -0,0 +1,43 @@
//! Exemple: Pompe
//!
//! Courbes de performance polynomiales.
//!
//! Exécuter: cargo run -p entropyk-demo --bin pump
use entropyk_components::port::{FluidId, Port};
use entropyk_components::pump::{Pump, PumpCurves};
use entropyk_core::{Enthalpy, Pressure};
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Exemple: Pompe ===\n");
let curves = PumpCurves::quadratic(
30.0, -10.0, -50.0,
0.5, 0.3, -0.5,
)?;
let inlet = Port::new(
FluidId::new("Water"),
Pressure::from_bar(1.0),
Enthalpy::from_joules_per_kg(100_000.0),
);
let outlet = Port::new(
FluidId::new("Water"),
Pressure::from_bar(1.0),
Enthalpy::from_joules_per_kg(100_000.0),
);
let pump = Pump::new(curves, inlet, outlet, 1000.0)?;
println!("Pompe créée:");
println!(" - Fluide: {}", pump.fluid_id());
println!(" - Densité: {} kg/m³", pump.fluid_density());
for q in [0.0, 0.05, 0.1, 0.2] {
let head = pump.curves().head_at_flow(q);
let eff = pump.curves().efficiency_at_flow(q);
println!(" - Q={:.2} m³/s: H={:.2} m, η={:.1}%", q, head, eff * 100.0);
}
Ok(())
}

View File

@@ -0,0 +1,131 @@
//! Exemple: Pompe et compresseur avec polynômes
//!
//! Démontre l'utilisation des polynômes pour modéliser:
//! - Pompe: courbes H(Q) et η(Q) avec Polynomial1D
//! - Compresseur: modèle SST/SDT avec Polynomial2D
//! - Lois d'affinité pour variation de vitesse
//!
//! Exécuter: cargo run -p entropyk-demo --bin pump_compressor_polynomials
use entropyk_components::compressor::SstSdtCoefficients;
use entropyk_components::polynomials::{AffinityLaws, Polynomial1D};
use entropyk_components::port::{FluidId, Port};
use entropyk_components::pump::{Pump, PumpCurves};
use entropyk_core::{Enthalpy, Pressure};
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("╔══════════════════════════════════════════════════════════════╗");
println!("║ Exemple: Pompe et Compresseur avec Polynômes ║");
println!("╚══════════════════════════════════════════════════════════════╝\n");
// ═══════════════════════════════════════════════════════════════
// 1. POLYNÔMES 1D - Courbes de pompe
// ═══════════════════════════════════════════════════════════════
println!("📐 1. Polynômes 1D (Pompe)\n");
// H = 30 - 10*Q - 50*Q² (hauteur en m, Q en m³/s)
let head_poly = Polynomial1D::quadratic(30.0, -10.0, -50.0);
// η = 0.5 + 0.3*Q - 0.5*Q² (rendement 0-1)
let eff_poly = Polynomial1D::quadratic(0.5, 0.3, -0.5);
println!(" Courbe hauteur: H = 30 - 10*Q - 50*Q²");
println!(" Courbe rendement: η = 0.5 + 0.3*Q - 0.5*Q²\n");
for q in [0.0, 0.05, 0.1, 0.15, 0.2] {
let h = head_poly.evaluate(q);
let eta = eff_poly.evaluate(q);
println!(" Q={:.2} m³/s → H={:.2} m, η={:.1}%", q, h, eta.clamp(0.0, 1.0) * 100.0);
}
// ═══════════════════════════════════════════════════════════════
// 2. POMPE avec courbes polynomiales
// ═══════════════════════════════════════════════════════════════
println!("\n🔧 2. Pompe (PumpCurves polynomiales)\n");
let curves = PumpCurves::quadratic(
30.0, -10.0, -50.0, // H = h0 + h1*Q + h2*Q²
0.5, 0.3, -0.5, // η = e0 + e1*Q + e2*Q²
)?;
let inlet = Port::new(
FluidId::new("Water"),
Pressure::from_bar(1.0),
Enthalpy::from_joules_per_kg(100_000.0),
);
let outlet = Port::new(
FluidId::new("Water"),
Pressure::from_bar(1.0),
Enthalpy::from_joules_per_kg(100_000.0),
);
let pump = Pump::new(curves, inlet, outlet, 1000.0)?;
println!(" Pompe créée (eau, ρ=1000 kg/m³)");
println!(" Point nominal Q=0.1 m³/s: H={:.2} m, η={:.1}%\n",
pump.curves().head_at_flow(0.1),
pump.curves().efficiency_at_flow(0.1) * 100.0
);
// ═══════════════════════════════════════════════════════════════
// 3. POLYNÔMES 2D - Modèle compresseur SST/SDT
// ═══════════════════════════════════════════════════════════════
println!("📐 3. Polynômes 2D (Compresseur SST/SDT)\n");
// Modèle bilinéaire: ṁ = a00 + a10*SST + a01*SDT + a11*SST*SDT
// Ẇ = b00 + b10*SST + b01*SDT + b11*SST*SDT
let sst_sdt = SstSdtCoefficients::bilinear(
0.05, 0.001, 0.0005, 0.00001, // débit (kg/s)
1000.0, 50.0, 30.0, 0.5, // puissance (W)
);
println!(" Modèle: ṁ = f(SST, SDT), Ẇ = g(SST, SDT)");
println!(" SST = température saturation aspiration (K)");
println!(" SDT = température saturation refoulement (K)\n");
// Conditions typiques: évaporation -5°C (268K), condensation 40°C (313K)
let sst_evap = 268.15; // -5°C
let sdt_cond = 313.15; // 40°C
let mass_flow = sst_sdt.mass_flow_at(sst_evap, sdt_cond);
let power = sst_sdt.power_at(sst_evap, sdt_cond);
println!(" SST={:.1} K (-5°C), SDT={:.1} K (40°C):", sst_evap, sdt_cond);
println!(" → ṁ = {:.4} kg/s", mass_flow);
println!(" → Ẇ = {:.0} W\n", power);
// Grille de conditions
println!(" Grille de performance:");
println!(" {:>8} | {:>8} {:>8} {:>8} {:>8}", "SST\\SDT", "303K", "308K", "313K", "318K");
println!(" {} | {} {} {} {}", "--------", "--------", "--------", "--------", "--------");
for sst in [263.15, 268.15, 273.15] {
print!(" {:>6.0}K |", sst);
for sdt in [303.15, 308.15, 313.15, 318.15] {
let m = sst_sdt.mass_flow_at(sst, sdt);
print!(" {:>7.3} ", m);
}
println!();
}
// ═══════════════════════════════════════════════════════════════
// 4. Lois d'affinité (variation de vitesse)
// ═══════════════════════════════════════════════════════════════
println!("\n📐 4. Lois d'affinité (pompe/ventilateur à vitesse variable)\n");
let speed_ratios = [1.0, 0.8, 0.6, 0.5];
println!(" À 50% vitesse: Q₂=0.5*Q₁, H₂=0.25*H₁, P₂=0.125*P₁\n");
println!(" {:>10} | {:>10} {:>10} {:>10}", "Vitesse", "Q ratio", "H ratio", "P ratio");
println!(" {} | {} {} {}", "----------", "----------", "----------", "----------");
for &ratio in &speed_ratios {
// AffinityLaws: Q₂=scale_flow(Q₁), H₂=scale_head(H₁), P₂=scale_power(P₁)
let q_ratio = AffinityLaws::scale_flow(1.0, ratio);
let h_ratio = AffinityLaws::scale_head(1.0, ratio);
let p_ratio = AffinityLaws::scale_power(1.0, ratio);
println!(" {:>8.0}% | {:>10.2} {:>10.2} {:>10.2}", ratio * 100.0, q_ratio, h_ratio, p_ratio);
}
println!("\n✅ Exemple terminé !");
Ok(())
}

View File

@@ -0,0 +1,428 @@
//! Demo Entropyk - Thermal Coupling Between Circuits
//!
//! This example demonstrates:
//! - Multi-circuit system creation (2 circuits)
//! - Component placement in circuits
//! - Thermal coupling between circuits (heat exchanger)
//! - Circular dependency detection
//! - Heat transfer computation
use colored::Colorize;
use entropyk_components::{
Component, ComponentError, JacobianBuilder, ResidualVector, SystemState,
};
use entropyk_core::{Temperature, ThermalConductance};
use entropyk_solver::{
compute_coupling_heat, coupling_groups, has_circular_dependencies, CircuitId, System,
ThermalCoupling,
};
use std::fmt;
fn print_header(title: &str) {
println!();
println!("{}", "".repeat(60).cyan());
println!("{}", format!(" {}", title).cyan().bold());
println!("{}", "".repeat(60).cyan());
}
fn print_section(title: &str) {
println!();
println!("{}", format!("{}", title).yellow().bold());
println!("{}", "".repeat(40).yellow());
}
struct SimpleComponent {
name: String,
n_eqs: usize,
}
impl SimpleComponent {
fn new(name: &str) -> Box<dyn Component> {
Box::new(Self {
name: name.to_string(),
n_eqs: 0,
})
}
}
impl Component for SimpleComponent {
fn compute_residuals(
&self,
_state: &SystemState,
residuals: &mut ResidualVector,
) -> Result<(), ComponentError> {
for r in residuals.iter_mut().take(self.n_eqs) {
*r = 0.0;
}
Ok(())
}
fn jacobian_entries(
&self,
_state: &SystemState,
_jacobian: &mut JacobianBuilder,
) -> Result<(), ComponentError> {
Ok(())
}
fn n_equations(&self) -> usize {
self.n_eqs
}
fn get_ports(&self) -> &[entropyk_components::ConnectedPort] {
&[]
}
}
impl fmt::Debug for SimpleComponent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SimpleComponent")
.field("name", &self.name)
.finish()
}
}
fn main() {
println!(
"{}",
"\n╔══════════════════════════════════════════════════════════╗".green()
);
println!(
"{}",
"║ ENTROPYK - Thermal Coupling Demo (Story 3.4) ║"
.green()
.bold()
);
println!(
"{}",
"╚══════════════════════════════════════════════════════════╝\n".green()
);
// ========================================
// PART 1: Basic Thermal Coupling
// ========================================
print_header("Part 1: Basic Thermal Coupling");
print_section("Creating ThermalCoupling struct");
let coupling = ThermalCoupling::new(
CircuitId(0), // Hot circuit (refrigerant)
CircuitId(1), // Cold circuit (water/glycol)
ThermalConductance::from_watts_per_kelvin(5000.0), // 5 kW/K UA value
)
.with_efficiency(0.95); // 95% heat exchanger efficiency
println!(" {} {:?}", "Coupling:".white(), coupling);
println!(
" {} {} W/K",
"UA:".white(),
coupling.ua.to_watts_per_kelvin()
);
println!(
" {} {:.0}%",
"Efficiency:".white(),
coupling.efficiency * 100.0
);
print_section("Computing heat transfer");
let t_hot = Temperature::from_celsius(45.0); // Refrigerant condensing at 45°C
let t_cold = Temperature::from_celsius(35.0); // Water entering at 35°C
let q = compute_coupling_heat(&coupling, t_hot, t_cold);
println!(
" {} {:.1}°C ({:.1} K)",
"T_hot:".white(),
t_hot.to_celsius(),
t_hot.to_kelvin()
);
println!(
" {} {:.1}°C ({:.1} K)",
"T_cold:".white(),
t_cold.to_celsius(),
t_cold.to_kelvin()
);
println!(
" {} {:.1} K",
"ΔT:".white(),
t_hot.to_kelvin() - t_cold.to_kelvin()
);
println!();
println!(
" {} {:.1} W = {:.2} kW",
"Heat transfer (Q):".green().bold(),
q,
q / 1000.0
);
println!(
" {} Q > 0 means heat flows INTO cold circuit",
"Sign convention:".white()
);
// Energy conservation demonstration
println!();
println!("{}", " Energy Conservation:".cyan());
let q_into_cold = q;
let q_out_of_hot = -q;
println!(
" Q_cold = {:.2} kW (heat received)",
q_into_cold / 1000.0
);
println!(
" Q_hot = {:.2} kW (heat rejected)",
q_out_of_hot / 1000.0
);
println!(" {} Q_cold + Q_hot = 0 ✓", "Check:".green());
// ========================================
// PART 2: Multi-Circuit System
// ========================================
print_header("Part 2: Multi-Circuit System");
print_section("Creating 2-circuit heat pump system");
let mut system = System::new();
// Circuit 0: Refrigerant circuit
let comp = system
.add_component_to_circuit(SimpleComponent::new("Compressor"), CircuitId(0))
.unwrap();
let cond = system
.add_component_to_circuit(SimpleComponent::new("Condenser"), CircuitId(0))
.unwrap();
let valve = system
.add_component_to_circuit(SimpleComponent::new("ExpansionValve"), CircuitId(0))
.unwrap();
let evap = system
.add_component_to_circuit(SimpleComponent::new("Evaporator"), CircuitId(0))
.unwrap();
// Circuit 1: Water/glycol circuit
let pump = system
.add_component_to_circuit(SimpleComponent::new("Pump"), CircuitId(1))
.unwrap();
let hx = system
.add_component_to_circuit(SimpleComponent::new("HeatExchanger"), CircuitId(1))
.unwrap();
println!(" Circuit 0 (Refrigerant):");
println!(" - Compressor, Condenser, ExpansionValve, Evaporator");
println!(" Circuit 1 (Water/Glycol):");
println!(" - Pump, HeatExchanger");
// Connect refrigerant circuit (cycle)
system.add_edge(comp, cond).unwrap();
system.add_edge(cond, valve).unwrap();
system.add_edge(valve, evap).unwrap();
system.add_edge(evap, comp).unwrap();
// Connect water circuit (simple loop)
system.add_edge(pump, hx).unwrap();
system.add_edge(hx, pump).unwrap();
println!();
println!(
" {} {} circuits, {} components, {} flow edges",
"System:".white(),
system.circuit_count(),
system.node_count(),
system.edge_count()
);
print_section("Adding thermal coupling between circuits");
let thermal_coupling = ThermalCoupling::new(
CircuitId(0), // Hot: refrigerant condenser
CircuitId(1), // Cold: water circuit heat exchanger
ThermalConductance::from_watts_per_kelvin(8000.0),
);
match system.add_thermal_coupling(thermal_coupling.clone()) {
Ok(idx) => println!(" {} Coupling added at index {}", "".green(), idx),
Err(e) => println!(" {} Error: {:?}", "".red(), e),
}
println!();
println!(
" {} {}",
"Couplings:".white(),
system.thermal_coupling_count()
);
for (i, c) in system.thermal_couplings().iter().enumerate() {
println!(
" [{}] Circuit {} → Circuit {} (UA = {} W/K)",
i,
c.hot_circuit.0,
c.cold_circuit.0,
c.ua.to_watts_per_kelvin()
);
}
// Finalize system
match system.finalize() {
Ok(()) => println!("\n {} System finalized successfully", "".green()),
Err(e) => println!("\n {} Finalize error: {:?}", "".red(), e),
}
// ========================================
// PART 3: Circular Dependency Detection
// ========================================
print_header("Part 3: Circular Dependency Detection");
print_section("Scenario A: Single coupling (no cycle)");
let couplings_a = vec![ThermalCoupling::new(
CircuitId(0),
CircuitId(1),
ThermalConductance::from_watts_per_kelvin(1000.0),
)];
let has_cycle_a = has_circular_dependencies(&couplings_a);
println!(" Couplings: Circuit 0 → Circuit 1");
println!(
" {} {}",
"Circular dependency:".white(),
if has_cycle_a {
"YES (solve simultaneously)".red()
} else {
"NO (solve sequentially)".green()
}
);
let groups_a = coupling_groups(&couplings_a);
println!(" {} {:?}", "Coupling groups:".white(), groups_a);
print_section("Scenario B: Mutual coupling (cycle!)");
let couplings_b = vec![
ThermalCoupling::new(
CircuitId(0),
CircuitId(1),
ThermalConductance::from_watts_per_kelvin(1000.0),
),
ThermalCoupling::new(
CircuitId(1),
CircuitId(0), // Back-coupling!
ThermalConductance::from_watts_per_kelvin(500.0),
),
];
let has_cycle_b = has_circular_dependencies(&couplings_b);
println!(" Couplings:");
println!(" Circuit 0 → Circuit 1");
println!(" Circuit 1 → Circuit 0 (back-coupling!)");
println!();
println!(
" {} {}",
"Circular dependency:".white(),
if has_cycle_b {
"YES (solve simultaneously)".red()
} else {
"NO (solve sequentially)".green()
}
);
let groups_b = coupling_groups(&couplings_b);
println!(" {} {:?}", "Coupling groups:".white(), groups_b);
if groups_b.iter().any(|g| g.len() > 1) {
println!(
" {} Circuits in same group must be solved together",
"".yellow()
);
}
print_section("Scenario C: Chain + mutual (complex)");
let couplings_c = vec![
ThermalCoupling::new(
CircuitId(0),
CircuitId(1),
ThermalConductance::from_watts_per_kelvin(1000.0),
),
ThermalCoupling::new(
CircuitId(1),
CircuitId(0),
ThermalConductance::from_watts_per_kelvin(500.0),
), // 0↔1 cycle
ThermalCoupling::new(
CircuitId(2),
CircuitId(3),
ThermalConductance::from_watts_per_kelvin(800.0),
), // independent
];
let has_cycle_c = has_circular_dependencies(&couplings_c);
println!(" Couplings:");
println!(" Circuit 0 ↔ Circuit 1 (mutual)");
println!(" Circuit 2 → Circuit 3 (independent)");
println!();
println!(
" {} {}",
"Circular dependency:".white(),
if has_cycle_c {
"YES".red()
} else {
"NO".green()
}
);
let groups_c = coupling_groups(&couplings_c);
println!(" {} {:?}", "Coupling groups:".white(), groups_c);
println!(
" {} [0,1] together, [2] independent, [3] independent",
"".yellow()
);
// ========================================
// PART 4: Error Handling
// ========================================
print_header("Part 4: Error Handling");
print_section("Invalid circuit validation");
let mut sys_test = System::new();
sys_test
.add_component_to_circuit(SimpleComponent::new("A"), CircuitId(0))
.unwrap();
// Circuit 1 has NO components!
let invalid_coupling = ThermalCoupling::new(
CircuitId(0),
CircuitId(1), // This circuit doesn't exist!
ThermalConductance::from_watts_per_kelvin(1000.0),
);
match sys_test.add_thermal_coupling(invalid_coupling) {
Ok(_) => println!(" {} Unexpected success!", "".red()),
Err(e) => {
println!(" {} Correctly rejected invalid coupling", "".green());
println!(" {} {}", "Error:".white(), e);
}
}
// ========================================
// Summary
// ========================================
print_header("Summary");
println!();
println!(
" {} ThermalCoupling struct with hot/cold circuits + UA + efficiency",
"".green()
);
println!(
" {} compute_coupling_heat() with sign convention (Q > 0 = heat into cold)",
"".green()
);
println!(
" {} has_circular_dependencies() via petgraph cycle detection",
"".green()
);
println!(
" {} coupling_groups() via Kosaraju SCC for solver strategy",
"".green()
);
println!(
" {} System.add_thermal_coupling() with circuit validation",
"".green()
);
println!(" {} InvalidCircuitForCoupling error handling", "".green());
println!();
println!("{}", "".repeat(60).cyan());
println!(
"{}",
" Demo complete! Run 'cargo run --bin thermal-coupling' again.".cyan()
);
println!("{}", "".repeat(60).cyan());
}

313
demo/src/bin/ui_server.rs Normal file
View File

@@ -0,0 +1,313 @@
//! Serveur UI Entropyk - Utilise les composants Rust réels pour les calculs.
//!
//! Lance l'UI et une API qui exécute les calculs avec les vrais composants.
//!
//! cargo run -p entropyk-demo --bin ui-server
use axum::{
extract::Json,
routing::post,
Router,
};
use entropyk_components::compressor::SstSdtCoefficients;
use entropyk_components::pipe::{friction_factor, Pipe, PipeGeometry};
use entropyk_components::pump::{Pump, PumpCurves};
use entropyk_components::port::{FluidId, Port};
use entropyk_core::{Enthalpy, Pressure};
use serde::{Deserialize, Serialize};
use std::path::Path;
use tower_http::services::ServeDir;
#[derive(Debug, Deserialize)]
struct ComponentConfig {
id: String,
#[serde(rename = "type")]
comp_type: String,
label: String,
config: serde_json::Value,
}
#[derive(Debug, Deserialize)]
struct CalculateRequest {
components: Vec<ComponentConfig>,
}
#[derive(Debug, Serialize)]
struct ComponentResult {
id: String,
label: String,
#[serde(rename = "type")]
comp_type: String,
results: serde_json::Value,
error: Option<String>,
}
#[derive(Debug, Serialize)]
struct CalculateResponse {
results: Vec<ComponentResult>,
}
fn parse_coeffs(s: &str) -> Vec<f64> {
s.split(',')
.filter_map(|x| x.trim().parse::<f64>().ok())
.collect()
}
async fn calculate(
axum::extract::Json(req): axum::extract::Json<CalculateRequest>,
) -> axum::Json<CalculateResponse> {
let mut results = Vec::new();
for comp in req.components {
let result = match comp.comp_type.as_str() {
"pump" => calc_pump(&comp),
"compressor" => calc_compressor(&comp),
"pipe" => calc_pipe(&comp),
"valve" => calc_valve(&comp),
_ => ComponentResult {
id: comp.id.clone(),
label: comp.label.clone(),
comp_type: comp.comp_type.clone(),
results: serde_json::json!({}),
error: Some("Type inconnu".to_string()),
},
};
results.push(result);
}
Json(CalculateResponse { results })
}
fn calc_pump(comp: &ComponentConfig) -> ComponentResult {
let config = &comp.config;
let head_coeffs = config
.get("head_coeffs")
.and_then(|v| v.as_str())
.unwrap_or("30,-10,-50");
let eff_coeffs = config
.get("eff_coeffs")
.and_then(|v| v.as_str())
.unwrap_or("0.5,0.3,-0.5");
let density = config
.get("density")
.and_then(|v| v.as_f64())
.unwrap_or(1000.0);
let h = parse_coeffs(head_coeffs);
let e = parse_coeffs(eff_coeffs);
if h.len() < 3 || e.len() < 3 {
return ComponentResult {
id: comp.id.clone(),
label: comp.label.clone(),
comp_type: "pump".to_string(),
results: serde_json::json!({}),
error: Some("Coefficients insuffisants (min 3 pour H et η)".to_string()),
};
}
match PumpCurves::quadratic(h[0], h[1], h[2], e[0], e[1], e[2]) {
Ok(curves) => {
let inlet = Port::new(
FluidId::new("Water"),
Pressure::from_bar(1.0),
Enthalpy::from_joules_per_kg(100_000.0),
);
let outlet = Port::new(
FluidId::new("Water"),
Pressure::from_bar(1.0),
Enthalpy::from_joules_per_kg(100_000.0),
);
match Pump::new(curves, inlet, outlet, density) {
Ok(pump) => {
let points: Vec<_> = [0.0, 0.05, 0.1, 0.15, 0.2]
.iter()
.map(|&q| {
serde_json::json!({
"Q_m3_s": q,
"H_m": pump.curves().head_at_flow(q),
"efficiency": pump.curves().efficiency_at_flow(q)
})
})
.collect();
ComponentResult {
id: comp.id.clone(),
label: comp.label.clone(),
comp_type: "pump".to_string(),
results: serde_json::json!({
"curve_points": points,
"density_kg_m3": density
}),
error: None,
}
}
Err(e) => ComponentResult {
id: comp.id.clone(),
label: comp.label.clone(),
comp_type: "pump".to_string(),
results: serde_json::json!({}),
error: Some(e.to_string()),
},
}
}
Err(e) => ComponentResult {
id: comp.id.clone(),
label: comp.label.clone(),
comp_type: "pump".to_string(),
results: serde_json::json!({}),
error: Some(e.to_string()),
},
}
}
fn calc_compressor(comp: &ComponentConfig) -> ComponentResult {
let config = &comp.config;
let mass_s = config
.get("mass_coeffs")
.and_then(|v| v.as_str())
.unwrap_or("0.05,0.001,0.0005,0.00001");
let power_s = config
.get("power_coeffs")
.and_then(|v| v.as_str())
.unwrap_or("1000,50,30,0.5");
let m = parse_coeffs(mass_s);
let p = parse_coeffs(power_s);
if m.len() < 4 || p.len() < 4 {
return ComponentResult {
id: comp.id.clone(),
label: comp.label.clone(),
comp_type: "compressor".to_string(),
results: serde_json::json!({}),
error: Some("Coefficients SST/SDT: 4 valeurs (a00,a10,a01,a11)".to_string()),
};
}
let sst_sdt = SstSdtCoefficients::bilinear(
m[0], m[1], m[2], m[3],
p[0], p[1], p[2], p[3],
);
let sst = 268.15;
let sdt = 313.15;
let mass_flow = sst_sdt.mass_flow_at(sst, sdt);
let power = sst_sdt.power_at(sst, sdt);
ComponentResult {
id: comp.id.clone(),
label: comp.label.clone(),
comp_type: "compressor".to_string(),
results: serde_json::json!({
"SST_K": sst,
"SDT_K": sdt,
"mass_flow_kg_s": mass_flow,
"power_W": power
}),
error: None,
}
}
fn calc_pipe(comp: &ComponentConfig) -> ComponentResult {
let config = &comp.config;
let length = config.get("length").and_then(|v| v.as_f64()).unwrap_or(10.0);
let diameter = config.get("diameter").and_then(|v| v.as_f64()).unwrap_or(0.022);
let rough = config.get("roughness").and_then(|v| v.as_f64()).unwrap_or(1.5e-6);
match PipeGeometry::new(length, diameter, rough) {
Ok(geometry) => {
let inlet = Port::new(
FluidId::new("Water"),
Pressure::from_bar(2.0),
Enthalpy::from_joules_per_kg(84_000.0),
);
let outlet = Port::new(
FluidId::new("Water"),
Pressure::from_bar(2.0),
Enthalpy::from_joules_per_kg(84_000.0),
);
match Pipe::new(geometry, inlet, outlet, 998.0, 0.001) {
Ok(_pipe) => {
let flow = 0.01;
let area = geometry.area();
let velocity = flow / area;
let re = velocity * diameter * 998.0 / 0.001;
let rel_rough = rough / diameter;
let f = friction_factor::haaland(rel_rough, re);
let dp = f * (length / diameter) * (998.0 * velocity * velocity / 2.0);
ComponentResult {
id: comp.id.clone(),
label: comp.label.clone(),
comp_type: "pipe".to_string(),
results: serde_json::json!({
"length_m": length,
"diameter_m": diameter,
"pressure_drop_Pa_at_0.01_m3_s": dp,
"reynolds_at_0.01_m3_s": re
}),
error: None,
}
}
Err(e) => ComponentResult {
id: comp.id.clone(),
label: comp.label.clone(),
comp_type: "pipe".to_string(),
results: serde_json::json!({}),
error: Some(e.to_string()),
},
}
}
Err(e) => ComponentResult {
id: comp.id.clone(),
label: comp.label.clone(),
comp_type: "pipe".to_string(),
results: serde_json::json!({}),
error: Some(e.to_string()),
},
}
}
fn calc_valve(comp: &ComponentConfig) -> ComponentResult {
let config = &comp.config;
let opening = config.get("opening").and_then(|v| v.as_f64()).unwrap_or(1.0);
ComponentResult {
id: comp.id.clone(),
label: comp.label.clone(),
comp_type: "valve".to_string(),
results: serde_json::json!({
"opening": opening,
"note": "Détendeur isenthalpique - calcul complet avec solveur"
}),
error: None,
}
}
#[tokio::main]
async fn main() {
let port = std::env::var("PORT").unwrap_or_else(|_| "3030".to_string());
let addr = format!("0.0.0.0:{}", port);
let ui_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../ui");
println!("Entropyk UI - http://localhost:{}", port);
println!("Dossier UI: {}", ui_path.display());
let app = Router::new()
.route("/api/calculate", post(calculate))
.nest_service("/", ServeDir::new(ui_path));
let listener = match tokio::net::TcpListener::bind(&addr).await {
Ok(l) => l,
Err(e) => {
eprintln!("Erreur: impossible de lier le port {} ({})", port, e);
eprintln!(" → Port déjà utilisé? Essayez: PORT=3031 cargo run -p entropyk-demo --bin ui-server");
eprintln!(" → Ou tuez le processus: lsof -ti:{} | xargs kill", port);
std::process::exit(1);
}
};
axum::serve(listener, app).await.unwrap();
}

107
demo/src/main.rs Normal file
View File

@@ -0,0 +1,107 @@
//! Demo Entropyk - Test du State Machine (ON/OFF/BYPASS)
//!
//! Ce fichier montre comment utiliser OperationalState et CircuitId
use colored::Colorize;
use entropyk_components::state_machine::{CircuitId, OperationalState};
fn print_header(title: &str) {
println!();
println!("{}", "".repeat(60).cyan());
println!("{}", format!(" {}", title).cyan().bold());
println!("{}", "".repeat(60).cyan());
}
fn main() {
println!(
"{}",
"\n╔══════════════════════════════════════════════════════════╗".green()
);
println!(
"{}",
"║ DEMO ENTROPYK - State Machine (ON/OFF/BYPASS) ║"
.green()
.bold()
);
println!(
"{}",
"╚══════════════════════════════════════════════════════════╝\n".green()
);
print_header("États Opérationnels");
println!();
println!("Les composants peuvent être dans 3 états:");
println!();
for state in [
OperationalState::On,
OperationalState::Off,
OperationalState::Bypass,
] {
println!(" {:?}:", state);
println!(" - Actif: {}", state.is_active());
println!(
" - Multiplicateur débit: {:.1}",
state.mass_flow_multiplier()
);
match state {
OperationalState::On => {
println!(" → Composant fonctionne normalement");
}
OperationalState::Off => {
println!(" → Composant arrêté, débit = 0");
}
OperationalState::Bypass => {
println!(" → Composant court-circuité (P_in = P_out, h_in = h_out)");
}
}
println!();
}
print_header("CircuitId (Multi-Circuit)");
println!();
println!("Un système peut avoir jusqu'à 5 circuits indépendants:");
println!();
let circuits = vec![
CircuitId::new("primary"),
CircuitId::new("secondary"),
CircuitId::default(),
];
for circuit in &circuits {
println!(" Circuit: {} (as_str: \"{}\")", circuit, circuit.as_str());
}
println!();
println!(" Utilisation typique:");
println!(" - Circuit 0: Boucle réfrigérant principale");
println!(" - Circuit 1: Circuit eau/glycol");
println!(" - Circuit 2: Circuit secondaire (optionnel)");
print_header("Exemples d'utilisation");
println!();
println!(" // Créer un système multi-circuit");
println!(" let mut system = System::new();");
println!(" system.add_component_to_circuit(compressor, CircuitId(0));");
println!(" system.add_component_to_circuit(pump, CircuitId(1));");
println!();
println!(" // Couplage thermique entre circuits");
println!(" let coupling = ThermalCoupling::new(");
println!(" CircuitId(0), // chaud");
println!(" CircuitId(1), // froid");
println!(" ThermalConductance::from_watts_per_kelvin(5000.0),");
println!(" );");
println!();
println!("{}", "".repeat(60).cyan());
println!(
"{}",
" Voir 'cargo run --bin thermal-coupling' pour la démo complète".cyan()
);
println!("{}", "".repeat(60).cyan());
}