21 KiB
Exemple Détaillé : Chiller Air-Glycol 2 Circuits avec Screw Économisé + MCHX
Vue d'ensemble
Ce document détaille la conception et l'implémentation d'un chiller air-glycol complet dans Entropyk, incluant:
- 2 circuits réfrigérants indépendants
- Compresseurs Screw économisés avec contrôle VFD (25–60 Hz)
- Condenseurs MCHX (Microchannel Heat Exchanger) à air ambiant (35°C)
- Évaporateurs flooded avec eau glycolée MEG 35% (entrée 12°C, sortie 7°C)
1. Architecture du Système
1.1 Topologie par Circuit
┌─────────────────────────────────────────────────────────────────────┐
│ CIRCUIT N (×2) │
│ │
│ BrineSource(MEG35%, 12°C) │
│ ↓ │
│ ┌─────────────────┐ │
│ │ FloodedEvaporator│ ←── Drum ←── Economizer(flash) │
│ └────────┬────────┘ ↑ │
│ │ │ │
│ ↓ │ │
│ ┌─────────────────────────────┐ │ │
│ │ ScrewEconomizerCompressor │────────┘ │
│ │ (suction, discharge, eco) │ │
│ └────────────┬────────────────┘ │
│ │ │
│ ↓ │
│ ┌────────────────────┐ │
│ │ FlowSplitter (1→2) │ │
│ └────────┬───────────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ ↓ ↓ │
│ ┌─────────┐ ┌─────────┐ │
│ │MchxCoil │ │MchxCoil │ ← 2 coils par circuit │
│ │ +Fan │ │ +Fan │ (4 coils total pour 2 circuits) │
│ └────┬────┘ └────┬────┘ │
│ │ │ │
│ └─────┬─────┘ │
│ ↓ │
│ ┌────────────────────┐ │
│ │ FlowMerger (2→1) │ │
│ └────────┬───────────┘ │
│ │ │
│ ↓ │
│ ┌────────────────────┐ │
│ │ ExpansionValve │ │
│ └────────┬───────────┘ │
│ │ │
│ ↓ │
│ BrineSink(MEG35%, 7°C) │
│ │
└─────────────────────────────────────────────────────────────────────┘
1.2 Spécifications Techniques
| Paramètre | Valeur | Unité |
|---|---|---|
| Réfrigérant | R134a | - |
| Nombre de circuits | 2 | - |
| Capacité nominale | 400 | kW |
| Air ambiant | 35 | °C |
| Entrée glycol | 12 | °C |
| Sortie glycol | 7 | °C |
| Type glycol | MEG 35% | - |
| Condenseurs | 4 × MCHX | 15 kW/K chacun |
| Compresseurs | 2 × Screw économisé | ~200 kW/circuit |
| VFD | 25–60 | Hz |
2. Composants Principaux
2.1 ScrewEconomizerCompressor
2.1.1 Description Physique
Un compresseur à vis avec port d'injection économiseur opère en deux étages de compression internes:
Stage 1:
Suction (P_evap, h_suc) → compression vers P_intermediate
Injection Intermédiaire:
Flash-gas depuis l'économiseur à (P_eco, h_eco) s'injecte dans les lobes
du rotor à la pression intermédiaire. Ceci refroidit le gaz comprimé et
augmente le débit total délivré au stage 2.
Stage 2:
Gaz mélangé (P_intermediate, h_mix) → compression vers P_discharge
Résultat net:
- Capacité supérieure vs. simple mono-étage (~10-20%)
- Meilleur COP (~8-15%) pour mêmes températures condensation/évaporation
- Gamme de fonctionnement étendue (ratios compression plus élevés)
2.1.2 Ports (3 total)
| Port | Type | Description |
|---|---|---|
port_suction |
Entrée | Fluide basse pression depuis évaporateur/drum |
port_discharge |
Sortie | Fluide haute pression vers condenseur |
port_economizer |
Entrée | Injection flash-gas à pression intermédiaire |
2.1.3 Variables d'État (5 total)
| Index | Variable | Unité | Description |
|---|---|---|---|
| 0 | ṁ_suction |
kg/s | Débit massique aspiration |
| 1 | ṁ_eco |
kg/s | Débit massique économiseur |
| 2 | h_suction |
J/kg | Enthalpie aspiration |
| 3 | h_discharge |
J/kg | Enthalpie refoulement |
| 4 | W_shaft |
W | Puissance arbre |
2.1.4 Équations (5 total)
// Équation 1: Débit aspiration (courbe fabricant)
r[0] = ṁ_suc_calc(SST, SDT) × (freq/50) - ṁ_suction_state
// Équation 2: Débit économiseur
r[1] = x_eco × ṁ_suction - ṁ_eco_state
// Équation 3: Bilan énergétique (adiabatique)
// ṁ_suc × h_suc + ṁ_eco × h_eco + W/η = ṁ_total × h_dis
let ṁ_total = ṁ_suc + ṁ_eco;
r[2] = ṁ_suc × h_suc + ṁ_eco × h_eco + W/η_mech - ṁ_total × h_dis
// Équation 4: Pression économiseur (moyenne géométrique)
r[3] = P_eco - sqrt(P_suc × P_dis)
// Équation 5: Puissance (courbe fabricant)
r[4] = W_calc(SST, SDT) × (freq/50) - W_state
2.1.5 Courbes de Performance
// Exemple: ~200 kW screw R134a à 50 Hz
// SST reference: +3°C = 276.15 K
// SDT reference: +50°C = 323.15 K
fn make_screw_curves() -> ScrewPerformanceCurves {
ScrewPerformanceCurves::with_fixed_eco_fraction(
// ṁ_suc [kg/s] = 1.20 + 0.003×(SST-276) - 0.002×(SDT-323) + 1e-5×(SST-276)×(SDT-323)
Polynomial2D::bilinear(1.20, 0.003, -0.002, 0.000_01),
// W [W] = 55000 + 200×(SST-276) - 300×(SDT-323) + 0.5×...
Polynomial2D::bilinear(55_000.0, 200.0, -300.0, 0.5),
0.12, // 12% fraction économiseur
)
}
2.1.6 Contrôle VFD
// Le ratio de fréquence affecte linéairement le débit
let frequency_ratio = frequency_hz / nominal_frequency_hz; // ex: 40/50 = 0.8
// Scaling:
// ṁ_suc ∝ frequency_ratio
// W ∝ frequency_ratio
// x_eco = constant (géométrie fixe)
comp.set_frequency_hz(40.0).unwrap();
assert!((comp.frequency_ratio() - 0.8).abs() < 1e-10);
2.1.7 Création du Composant
use entropyk_components::{ScrewEconomizerCompressor, ScrewPerformanceCurves, Polynomial2D};
use entropyk_components::port::{Port, FluidId};
use entropyk_core::{Pressure, Enthalpy};
// Créer les 3 ports connectés
let suc = make_port("R134a", 3.2, 400.0); // P=3.2 bar, h=400 kJ/kg
let dis = make_port("R134a", 12.8, 440.0); // P=12.8 bar, h=440 kJ/kg
let eco = make_port("R134a", 6.4, 260.0); // P=6.4 bar (intermédiaire)
let comp = ScrewEconomizerCompressor::new(
make_screw_curves(),
"R134a",
50.0, // fréquence nominale
0.92, // rendement mécanique
suc,
dis,
eco,
).expect("compressor creation ok");
assert_eq!(comp.n_equations(), 5);
2.2 MchxCondenserCoil
2.2.1 Description Physique
Un MCHX (Microchannel Heat Exchanger) utilise des tubes plats en aluminium extrudé multi-port avec une structure d'ailettes louvrées. Comparé aux condenseurs conventionnels (RTPF):
| Propriété | RTPF | MCHX |
|---|---|---|
| UA côté air | Base | +30–60% par m² |
| Charge réfrigérant | Base | −25–40% |
| Perte de charge air | Base | Similaire |
| Poids | Base | −30% |
| Sensibilité distribution air | Moins | Plus |
2.2.2 Modèle UA Variable
UA_eff = UA_nominal × (ρ_air / ρ_ref)^0.5 × (fan_speed)^n_air
où:
ρ_air = densité air à T_amb [kg/m³]
ρ_ref = densité air de référence (1.12 kg/m³ à 35°C)
n_air = 0.5 (ASHRAE louvered fins)
2.2.3 Effet de la Vitesse Ventilateur
// À 100% vitesse ventilateur
coil.set_fan_speed_ratio(1.0);
let ua_100 = coil.ua_effective(); // = UA_nominal
// À 70% vitesse
coil.set_fan_speed_ratio(0.70);
let ua_70 = coil.ua_effective();
// UA_70 ≈ UA_nom × √0.70 ≈ UA_nom × 0.837
// À 60% vitesse
coil.set_fan_speed_ratio(0.60);
let ua_60 = coil.ua_effective();
// UA_60 ≈ UA_nom × √0.60 ≈ UA_nom × 0.775
2.2.4 Effet de la Température Ambiante
// À 35°C (design)
let coil_35 = MchxCondenserCoil::for_35c_ambient(15_000.0, 0);
let ua_35 = coil_35.ua_effective();
// À 45°C (ambiante élevée)
let mut coil_45 = MchxCondenserCoil::for_35c_ambient(15_000.0, 0);
coil_45.set_air_temperature_celsius(45.0);
let ua_45 = coil_45.ua_effective();
// UA diminue avec la température (densité air diminue)
// Ratio ≈ ρ(45°C)/ρ(35°C) ≈ 1.109/1.12 ≈ 0.99
assert!(ua_45 < ua_35);
2.2.5 Création d'une Banque de 4 Coils
// 4 coils, 15 kW/K chacun
let coils: Vec<MchxCondenserCoil> = (0..4)
.map(|i| MchxCondenserCoil::for_35c_ambient(15_000.0, i))
.collect();
let total_ua: f64 = coils.iter().map(|c| c.ua_effective()).sum();
// ≈ 60 kW/K total
// Simulation anti-override: réduire coil 0 à 70%
coils[0].set_fan_speed_ratio(0.70);
2.3 FloodedEvaporator
2.3.1 Description
L'évaporateur noyé (flooded) maintient un niveau de liquide constant dans la calandre. Le réfrigérant bout à la surface des tubes où circule le fluide secondaire (eau glycolée).
2.3.2 Configuration JSON
{
"type": "FloodedEvaporator",
"name": "evap_0",
"ua": 20000.0,
"refrigerant": "R134a",
"secondary_fluid": "MEG",
"target_quality": 0.7
}
2.3.3 Bilan Énergétique
Q_evap = ṁ_ref × (h_out - h_in) (côté réfrigérant)
Q_evap = ṁ_brine × Cp_brine × ΔT_brine (côté secondaire)
où:
ṁ_brine = débit glycol MEG 35% [kg/s]
Cp_brine ≈ 3.6 kJ/(kg·K) à 10°C
ΔT_brine = T_in - T_out = 12 - 7 = 5 K
3. Configuration JSON Complète
3.1 Structure du Fichier
{
"name": "Chiller Air-Glycol 2 Circuits",
"description": "Machine frigorifique 2 circuits indépendants",
"fluid": "R134a",
"circuits": [
{
"id": 0,
"components": [ ... ],
"edges": [ ... ]
},
{
"id": 1,
"components": [ ... ],
"edges": [ ... ]
}
],
"solver": {
"strategy": "fallback",
"max_iterations": 150,
"tolerance": 1e-6
},
"metadata": { ... }
}
3.2 Circuit 0 Détaillé
{
"id": 0,
"components": [
{
"type": "ScrewEconomizerCompressor",
"name": "screw_0",
"fluid": "R134a",
"nominal_frequency_hz": 50.0,
"mechanical_efficiency": 0.92,
"economizer_fraction": 0.12,
"mf_a00": 1.20, "mf_a10": 0.003, "mf_a01": -0.002, "mf_a11": 0.00001,
"pw_b00": 55000.0, "pw_b10": 200.0, "pw_b01": -300.0, "pw_b11": 0.5,
"p_suction_bar": 3.2, "h_suction_kj_kg": 400.0,
"p_discharge_bar": 12.8, "h_discharge_kj_kg": 440.0,
"p_eco_bar": 6.4, "h_eco_kj_kg": 260.0
},
{
"type": "MchxCondenserCoil",
"name": "mchx_0a",
"ua": 15000.0,
"coil_index": 0,
"n_air": 0.5,
"t_air_celsius": 35.0,
"fan_speed_ratio": 1.0
},
{
"type": "MchxCondenserCoil",
"name": "mchx_0b",
"ua": 15000.0,
"coil_index": 1,
"n_air": 0.5,
"t_air_celsius": 35.0,
"fan_speed_ratio": 1.0
},
{
"type": "Placeholder",
"name": "exv_0",
"n_equations": 2
},
{
"type": "FloodedEvaporator",
"name": "evap_0",
"ua": 20000.0,
"refrigerant": "R134a",
"secondary_fluid": "MEG",
"target_quality": 0.7
}
],
"edges": [
{ "from": "screw_0:outlet", "to": "mchx_0a:inlet" },
{ "from": "mchx_0a:outlet", "to": "mchx_0b:inlet" },
{ "from": "mchx_0b:outlet", "to": "exv_0:inlet" },
{ "from": "exv_0:outlet", "to": "evap_0:inlet" },
{ "from": "evap_0:outlet", "to": "screw_0:inlet" }
]
}
4. Tests d'Intégration
4.1 Test: Création Screw + Residuals
#[test]
fn test_screw_compressor_creation_and_residuals() {
let suc = make_port("R134a", 3.2, 400.0);
let dis = make_port("R134a", 12.8, 440.0);
let eco = make_port("R134a", 6.4, 260.0);
let comp = ScrewEconomizerCompressor::new(
make_screw_curves(), "R134a", 50.0, 0.92, suc, dis, eco
).expect("compressor creation ok");
assert_eq!(comp.n_equations(), 5);
// État plausible
let state = vec![1.2, 0.144, 400_000.0, 440_000.0, 55_000.0];
let mut residuals = vec![0.0; 5];
comp.compute_residuals(&state, &mut residuals).expect("ok");
// Tous résiduals finis
for (i, r) in residuals.iter().enumerate() {
assert!(r.is_finite(), "residual[{}] not finite", i);
}
}
4.2 Test: VFD Scaling
#[test]
fn test_screw_vfd_scaling() {
let mut comp = /* ... */;
// Pleine vitesse (50 Hz)
let state_full = vec![1.2, 0.144, 400_000.0, 440_000.0, 55_000.0];
comp.compute_residuals(&state_full, &mut r_full).unwrap();
// 80% vitesse (40 Hz)
comp.set_frequency_hz(40.0).unwrap();
assert!((comp.frequency_ratio() - 0.8).abs() < 1e-10);
let state_reduced = vec![0.96, 0.115, 400_000.0, 440_000.0, 44_000.0];
comp.compute_residuals(&state_reduced, &mut r_reduced).unwrap();
}
4.3 Test: MCHX UA Correction
#[test]
fn test_mchx_ua_correction_with_fan_speed() {
let mut coil = MchxCondenserCoil::for_35c_ambient(15_000.0, 0);
// 100% → UA nominal
let ua_100 = coil.ua_effective();
// 70% → UA × √0.7
coil.set_fan_speed_ratio(0.70);
let ua_70 = coil.ua_effective();
let expected_ratio = 0.70_f64.sqrt();
let actual_ratio = ua_70 / ua_100;
assert!((actual_ratio - expected_ratio).abs() < 0.02);
}
4.4 Test: Topologie 2 Circuits
#[test]
fn test_two_circuit_chiller_topology() {
let mut sys = System::new();
// Circuit 0
let comp0 = /* screw compressor */;
let comp0_node = sys.add_component_to_circuit(
Box::new(comp0), CircuitId::ZERO
).expect("add comp0");
// 2 coils pour circuit 0
for i in 0..2 {
let coil = MchxCondenserCoil::for_35c_ambient(15_000.0, i);
let coil_node = sys.add_component_to_circuit(
Box::new(coil), CircuitId::ZERO
).expect("add coil");
sys.add_edge(comp0_node, coil_node).expect("edge");
}
// Circuit 1 (similaire)
// ...
assert_eq!(sys.circuit_count(), 2);
sys.finalize().expect("finalize should succeed");
}
4.5 Test: Anti-Override Ventilateur
#[test]
fn test_fan_anti_override_speed_reduction() {
let mut coil = MchxCondenserCoil::for_35c_ambient(15_000.0, 0);
let ua_100 = coil.ua_effective();
coil.set_fan_speed_ratio(0.80);
let ua_80 = coil.ua_effective();
coil.set_fan_speed_ratio(0.60);
let ua_60 = coil.ua_effective();
// UA décroît avec la vitesse ventilateur
assert!(ua_100 > ua_80);
assert!(ua_80 > ua_60);
// Suit loi puissance: UA ∝ speed^0.5
assert!((ua_80/ua_100 - 0.80_f64.sqrt()).abs() < 0.03);
assert!((ua_60/ua_100 - 0.60_f64.sqrt()).abs() < 0.03);
}
4.6 Test: Bilan Énergétique Screw
#[test]
fn test_screw_energy_balance() {
let comp = /* screw avec ports P_suc=3.2, P_dis=12.8, P_eco=6.4 */;
let m_suc = 1.2;
let m_eco = 0.144;
let h_suc = 400_000.0;
let h_dis = 440_000.0;
let h_eco = 260_000.0;
let eta_mech = 0.92;
// W ferme le bilan énergétique:
// m_suc × h_suc + m_eco × h_eco + W/η = (m_suc + m_eco) × h_dis
let w_expected = ((m_suc + m_eco) * h_dis - m_suc * h_suc - m_eco * h_eco) * eta_mech;
let state = vec![m_suc, m_eco, h_suc, h_dis, w_expected];
let mut residuals = vec![0.0; 5];
comp.compute_residuals(&state, &mut residuals).unwrap();
// residual[2] = bilan énergétique ≈ 0
assert!(residuals[2].abs() < 1.0);
}
5. Fichiers Sources
| Fichier | Description |
|---|---|
crates/components/src/screw_economizer_compressor.rs |
Implémentation ScrewEconomizerCompressor |
crates/components/src/heat_exchanger/mchx_condenser_coil.rs |
Implémentation MchxCondenserCoil |
crates/solver/tests/chiller_air_glycol_integration.rs |
Tests d'intégration (10 tests) |
crates/cli/examples/chiller_screw_mchx_2circuits.json |
Config JSON complète 2 circuits |
crates/cli/examples/chiller_screw_mchx_validate.json |
Config validation 1 circuit |
6. Commandes CLI
Validation de Configuration
# Valider le JSON sans lancer la simulation
entropyk-cli validate --config chiller_screw_mchx_2circuits.json
Lancement de Simulation
# Avec backend test (développement)
entropyk-cli run --config chiller_screw_mchx_2circuits.json --backend test
# Avec backend tabular (R134a built-in)
entropyk-cli run --config chiller_screw_mchx_2circuits.json --backend tabular
# Avec CoolProp (si disponible)
entropyk-cli run --config chiller_screw_mchx_2circuits.json --backend coolprop
Output JSON
entropyk-cli run --config chiller_screw_mchx_2circuits.json --output results.json
7. Références
7.1 Standards et Corrélations
- ASHRAE Handbook — Chapitre 4: Heat Transfer (corrélation louvered fins, n=0.5)
- AHRI Standard 540 — Performance Rating of Positive Displacement Refrigerant Compressors
- Bitzer Technical Documentation — Screw compressor curves (HSK/CSH series)
7.2 Stories Connexes
| Story | Description |
|---|---|
| 11-3 | FloodedEvaporator implémentation |
| 12-1 | CLI internal state variables |
| 12-2 | CLI CoolProp backend |
| 12-3 | CLI Screw compressor config |
| 12-4 | CLI MCHX config |
| 12-5 | CLI FloodedEvaporator + Brine |
| 12-6 | CLI Controls (SH, VFD, fan) |
8. Points d'Attention
8.1 État Actuel (Limitations)
-
Backend TestBackend — La CLI utilise
TestBackendqui retourne des zéros. Nécessite CoolProp ou TabularBackend pour des simulations réelles (Story 12.2). -
Variables d'État Internes — Le solveur peut retourner "State dimension mismatch" si les composants complexes ne déclarent pas correctement
internal_state_len()(Story 12.1). -
Port Économiseur — Dans la config CLI actuelle, le port économiseur du Screw n'est pas connecté à un composant économiseur séparé. Le modèle utilise une fraction fixe (Story 12.3).
8.2 Prochaines Étapes
- Implémenter Story 12.1 (variables internes) pour résoudre le mismatch state/equations
- Implémenter Story 12.2 (CoolProp backend) pour des propriétés thermodynamiques réelles
- Ajouter les contrôles (Story 12.6) pour surchauffe cible et VFD
- Valider la convergence sur un point de fonctionnement nominal