# 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, /// Pression atmosphérique [Pa] pressure: Pressure, /// Débit massique d'air sec optionnel [kg/s] mass_flow: Option, /// 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; /// 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; /// 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; /// Retourne le rapport d'humidité (kg_vapeur / kg_air_sec). pub fn humidity_ratio(&self) -> Result; } ``` ### 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, /// 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; /// 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)