6.5 KiB

Story 10.4: AirSource et AirSink avec Propriétés Psychrométriques

Epic: 10 - Enhanced Boundary Conditions
Priorité: P1-HIGH
Estimation: 4h
Statut: backlog
Dépendances: Story 10-1 (Nouveaux types physiques)


Story

En tant que moteur de simulation thermodynamique,
Je veux que AirSource et AirSink supportent les propriétés psychrométriques,
Afin de pouvoir simuler les côtés air des échangeurs de chaleur (évaporateurs, condenseurs).


Contexte

Les composants côté air (évaporateur air/air, condenseur air/réfrigérant) nécessitent des conditions aux limites avec:

  • Température sèche (dry bulb temperature)
  • Humidité relative ou température bulbe humide
  • Débit massique d'air

Ces propriétés sont essentielles pour:

  • Calcul des échanges thermiques et massiques (condensation sur évaporateur)
  • Dimensionnement des batteries froides/chaudes
  • Simulation des pompes à chaleur air/air et air/eau

Spécifications Techniques

AirSource

/// Source pour air humide (côté air des échangeurs).
///
/// Impose les conditions de l'air entrant avec propriétés psychrométriques.
#[derive(Debug, Clone)]
pub struct AirSource {
    /// Température sèche [K]
    t_dry: Temperature,
    /// Humidité relative [%]
    rh: RelativeHumidity,
    /// Température bulbe humide optionnelle [K]
    t_wet_bulb: Option<Temperature>,
    /// Pression atmosphérique [Pa]
    pressure: Pressure,
    /// Débit massique d'air sec optionnel [kg/s]
    mass_flow: Option<MassFlow>,
    /// Port de sortie connecté
    outlet: ConnectedPort,
}

impl AirSource {
    /// Crée une source d'air avec température sèche et humidité relative.
    pub fn from_dry_bulb_rh(
        temperature_dry: Temperature,
        relative_humidity: RelativeHumidity,
        pressure: Pressure,
        outlet: ConnectedPort,
    ) -> Result<Self, ComponentError>;
    
    /// Crée une source d'air avec températures sèche et bulbe humide.
    /// L'humidité relative est calculée automatiquement.
    pub fn from_dry_and_wet_bulb(
        temperature_dry: Temperature,
        temperature_wet_bulb: Temperature,
        pressure: Pressure,
        outlet: ConnectedPort,
    ) -> Result<Self, ComponentError>;
    
    /// Définit le débit massique d'air sec.
    pub fn set_mass_flow(&mut self, mass_flow: MassFlow);
    
    /// Retourne l'enthalpie spécifique de l'air humide [J/kg_air_sec].
    pub fn specific_enthalpy(&self) -> Result<Enthalpy, ComponentError>;
    
    /// Retourne le rapport d'humidité (kg_vapeur / kg_air_sec).
    pub fn humidity_ratio(&self) -> Result<f64, ComponentError>;
}

AirSink

/// Puits pour air humide.
#[derive(Debug, Clone)]
pub struct AirSink {
    /// Pression atmosphérique [Pa]
    pressure: Pressure,
    /// Température de retour optionnelle [K]
    t_back: Option<Temperature>,
    /// Port d'entrée connecté
    inlet: ConnectedPort,
}

impl AirSink {
    /// Crée un puits d'air à pression atmosphérique.
    pub fn new(pressure: Pressure, inlet: ConnectedPort) -> Result<Self, ComponentError>;
    
    /// Définit une température de retour fixe.
    pub fn set_return_temperature(&mut self, temperature: Temperature);
}

Calculs Psychrométriques

Formules Utilisées

/// Pression de saturation de vapeur d'eau (formule de Magnus-Tetens)
fn saturation_vapor_pressure(t: Temperature) -> Pressure {
    // P_sat = 610.78 * exp(17.27 * T_celsius / (T_celsius + 237.3))
    let t_c = t.to_celsius();
    Pressure::from_pascals(610.78 * (17.27 * t_c / (t_c + 237.3)).exp())
}

/// Rapport d'humidité depuis humidité relative
fn humidity_ratio_from_rh(
    rh: RelativeHumidity,
    t_dry: Temperature,
    p_atm: Pressure,
) -> f64 {
    // W = 0.622 * (P_v / (P_atm - P_v))
    // où P_v = RH * P_sat
    let p_sat = saturation_vapor_pressure(t_dry);
    let p_v = p_sat * rh.to_fraction();
    0.622 * p_v.to_pascals() / (p_atm.to_pascals() - p_v.to_pascals())
}

/// Enthalpie spécifique de l'air humide
fn specific_enthalpy(t_dry: Temperature, w: f64) -> Enthalpy {
    // h = 1.006 * T_celsius + W * (2501 + 1.86 * T_celsius) [kJ/kg]
    let t_c = t_dry.to_celsius();
    Enthalpy::from_joules_per_kg((1.006 * t_c + w * (2501.0 + 1.86 * t_c)) * 1000.0)
}

Fichiers à Créer/Modifier

Fichier Action
crates/components/src/flow_boundary/air.rs Créer AirSource, AirSink
crates/components/src/flow_boundary/mod.rs Ajouter ré-exports

Critères d'Acceptation

  • AirSource::from_dry_bulb_rh() crée une source avec T sèche et HR
  • AirSource::from_dry_and_wet_bulb() calcule HR depuis T bulbe humide
  • specific_enthalpy() retourne l'enthalpie de l'air humide
  • humidity_ratio() retourne le rapport d'humidité
  • AirSink::new() crée un puits à pression atmosphérique
  • energy_transfers() retourne (Power(0), Power(0))
  • Validation de l'humidité relative (0-100%)
  • Tests unitaires avec valeurs de référence ASHRAE

Tests Requis

#[cfg(test)]
mod tests {
    #[test]
    fn test_air_source_from_dry_bulb_rh() { /* ... */ }
    
    #[test]
    fn test_air_source_from_wet_bulb() { /* ... */ }
    
    #[test]
    fn test_saturation_vapor_pressure() { /* ... */ }
    
    #[test]
    fn test_humidity_ratio_calculation() { /* ... */ }
    
    #[test]
    fn test_specific_enthalpy_calculation() { /* ... */ }
    
    #[test]
    fn test_air_source_psychrometric_consistency() {
        // Vérifier que les calculs sont cohérents avec les tables ASHRAE
    }
}

Notes d'Implémentation

Alternative: Utiliser CoolProp

CoolProp supporte l'air humide via:

// Air humide avec rapport d'humidité W
let fluid = format!("Air-W-{}", w);
PropsSI("H", "T", T, "P", P, &fluid)

Cependant, les formules analytiques (Magnus-Tetens) sont plus rapides et suffisantes pour la plupart des applications.

Performance

Les calculs psychrométriques doivent être optimisés car ils sont appelés fréquemment dans les boucles de résolution. Éviter les allocations et utiliser des formules approchées si nécessaire.


Références