223 lines
6.5 KiB
Markdown

# 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)