Entropyk/docs/chiller-example-detailed.md

21 KiB
Raw Blame History

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 (2560 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 2560 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 +3060% par m²
Charge réfrigérant Base 2540%
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)

  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