# 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

```rust
/// 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

```rust
/// 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

```rust
/// 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

```rust
#[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:
```rust
// 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

- [Architecture Document](../../plans/boundary-condition-refactoring-architecture.md)
- [Story 10-1: Nouveaux types physiques](./10-1-new-physical-types.md)
- [ASHRAE Fundamentals - Psychrometrics](https://www.ashrae.org/)
- [CoolProp Humid Air](http://www.coolprop.org/fluid_properties/HumidAir.html)
