# 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) ```rust // É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 ```rust // 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 ```rust // 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 ```rust 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 ```text 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 ```rust // À 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 ```rust // À 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 ```rust // 4 coils, 15 kW/K chacun let coils: Vec = (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 ```json { "type": "FloodedEvaporator", "name": "evap_0", "ua": 20000.0, "refrigerant": "R134a", "secondary_fluid": "MEG", "target_quality": 0.7 } ``` #### 2.3.3 Bilan Énergétique ```text 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 ```json { "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é ```json { "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 ```rust #[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 ```rust #[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 ```rust #[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 ```rust #[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 ```rust #[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 ```rust #[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 ```bash # Valider le JSON sans lancer la simulation entropyk-cli validate --config chiller_screw_mchx_2circuits.json ``` ### Lancement de Simulation ```bash # 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 ```bash 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) 1. **Backend TestBackend** — La CLI utilise `TestBackend` qui retourne des zéros. Nécessite CoolProp ou TabularBackend pour des simulations réelles (Story 12.2). 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). 3. **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 1. Implémenter Story 12.1 (variables internes) pour résoudre le mismatch state/equations 2. Implémenter Story 12.2 (CoolProp backend) pour des propriétés thermodynamiques réelles 3. Ajouter les contrôles (Story 12.6) pour surchauffe cible et VFD 4. Valider la convergence sur un point de fonctionnement nominal