# Story 10.4: AirSource et AirSink avec Propriétés Psychrométriques **Epic:** 10 - Enhanced Boundary Conditions **Priorité:** P1-HIGH **Estimation:** 4h **Statut:** done **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). --- ## Acceptance Criteria - [x] `AirSource::from_dry_bulb_rh()` crée une source avec T sèche et HR - [x] `AirSource::from_dry_and_wet_bulb()` calcule HR depuis T bulbe humide - [x] `specific_enthalpy()` retourne l'enthalpie de l'air humide - [x] `humidity_ratio()` retourne le rapport d'humidité - [x] `AirSink::new()` crée un puits à pression atmosphérique - [x] `energy_transfers()` retourne `(Power(0), Power(0))` - [x] Validation de l'humidité relative (0-100%) - [x] Tests unitaires avec valeurs de référence ASHRAE --- ## Tasks / Subtasks - [x] Task 1: Implémenter AirSource (AC: #1, #2, #3, #4, #7) - [x] 1.1 Créer struct avec champs : `t_dry_k`, `rh`, `p_set_pa`, `w` (calculé), `h_set_jkg` (calculé), `outlet` - [x] 1.2 Implémenter `from_dry_bulb_rh()` avec calculs psychrométriques (W, h) - [x] 1.3 Implémenter `from_dry_and_wet_bulb()` via formule de Sprung - [x] 1.4 Implémenter `Component::compute_residuals()` (2 équations) - [x] 1.5 Implémenter `Component::jacobian_entries()` (diagonal 1.0) - [x] 1.6 Implémenter `Component::get_ports()`, `port_mass_flows()`, `port_enthalpies()`, `energy_transfers()` - [x] 1.7 Ajouter accesseurs : `t_dry()`, `rh()`, `p_set()`, `humidity_ratio()`, `h_set()` - [x] 1.8 Ajouter setters : `set_temperature()`, `set_rh()` (recalcul automatique) - [x] Task 2: Implémenter AirSink (AC: #5, #6) - [x] 2.1 Créer struct avec champs : `p_back_pa`, `t_back_k` (optional), `rh_back` (optional), `h_back_jkg` (optional), `inlet` - [x] 2.2 Implémenter `new()` constructor (1-équation mode par défaut) - [x] 2.3 Implémenter count dynamique d'équations (1 ou 2) - [x] 2.4 Implémenter méthodes `Component` trait - [x] 2.5 Ajouter `set_return_temperature()`, `clear_return_temperature()` pour toggle dynamique - [x] Task 3: Fonctions psychrométriques (AC: #3, #4, #8) - [x] 3.1 Implémenter `saturation_vapor_pressure()` (Magnus-Tetens) - [x] 3.2 Implémenter `humidity_ratio_from_rh()` - [x] 3.3 Implémenter `specific_enthalpy_from_w()` - [x] 3.4 Implémenter `rh_from_wet_bulb()` (formule de Sprung) - [x] Task 4: Intégration du module (AC: #5, #6) - [x] 4.1 Ajouter `pub mod air_boundary` dans `crates/components/src/lib.rs` - [x] 4.2 Ajouter `pub use air_boundary::{AirSink, AirSource}` - [x] Task 5: Tests (AC: #1-8) - [x] 5.1 Tests AirSource : `from_dry_bulb_rh`, `from_dry_and_wet_bulb`, wet > dry retourne erreur - [x] 5.2 Tests psychrométriques : `saturation_vapor_pressure` (ASHRAE ref), `humidity_ratio`, `specific_enthalpy` - [x] 5.3 Tests AirSink : création, pression invalide, toggle dynamique - [x] 5.4 Tests résiduels zéro au set-point (AirSource et AirSink 1-eq et 2-eq) - [x] 5.5 Tests trait object (`Box`) - [x] 5.6 Tests `energy_transfers()` = (0, 0) - [x] 5.7 Tests signatures - [x] Task 6: Validation - [x] 6.1 `cargo test --package entropyk-components --lib -- air_boundary` → 23 passed, 0 failed - [x] 6.2 `cargo test --package entropyk-components --lib` → 469 passed, 0 failed (aucune régression) - [x] 6.3 Aucun avertissement clippy dans `air_boundary.rs` - [x] Task 7: Code Review Fixes (AI-Review) - [x] 7.1 Fixed `set_temperature()` and `set_rh()` to return `Result<(), ComponentError>` - [x] 7.2 Fixed `humidity_ratio_from_rh()` to return `Result` instead of silent 0.0 - [x] 7.3 Added validation for P_v >= P_atm error case - [x] 7.4 Updated Sprung formula documentation for unventilated psychrometers - [x] 7.5 Tightened ASHRAE test tolerances (0.5% for P_sat, 1% for h and W) - [x] 7.6 Tightened specific_enthalpy test range (45-56 kJ/kg for 25°C/50%RH) - [x] 7.7 Updated File List with missing files from Epic 10 --- ## Dev Notes ### Architecture Patterns (MUST follow) 1. **NewType Pattern**: Utiliser `RelativeHumidity` de `entropyk_core`, jamais `f64` nu pour l'humidité 2. **Zero-Panic Policy**: Toutes les méthodes retournent `Result` 3. **Component Trait**: Implémenter toutes les méthodes du trait de façon identique aux composants existants 4. **Pas de dépendance backend**: Contrairement à BrineSource/RefrigerantSource, AirSource utilise des formules analytiques (Magnus-Tetens) — pas besoin de `FluidBackend` ### Pattern suivi Ce composant suit le pattern exact de `brine_boundary.rs` et `refrigerant_boundary.rs`, avec les différences : | Aspect | RefrigerantSource | BrineSource | AirSource | |--------|-------------------|-------------|-----------| | État spec | (P, VaporQuality) | (P, T, Concentration) | (T_dry, RH, P_atm) | | Validation fluide | `!is_incompressible()` | `is_incompressible()` | aucune (air) | | Backend requis | Oui | Oui | Non (analytique) | | Calcul enthalpie | FluidBackend::PQ | FluidBackend::PT | Magnus-Tetens | ### Formules Psychrométriques ```rust // Pression de saturation (Magnus-Tetens) P_sat = 610.78 * exp(17.27 * T_c / (T_c + 237.3)) [Pa] // Rapport d'humidité W = 0.622 * P_v / (P_atm - P_v) où P_v = RH * P_sat // Enthalpie spécifique [J/kg_da] h = 1006 * T_c + W * (2_501_000 + 1860 * T_c) // Humidité relative depuis bulbe humide (Sprung) e = e_sat(T_wet) - 6.6e-4 * (T_dry - T_wet) * P_atm RH = e / e_sat(T_dry) ``` ### Fichier créé - `crates/components/src/air_boundary.rs` — AirSource, AirSink, helpers psychrométriques ### Fix préexistant Corrigé `flooded_evaporator.rs:171-172` qui utilisait une méthode inexistante `enthalpy_px()`. Remplacé par l'appel correct via `FluidBackend::property()` avec `FluidState::from_px()`. --- ## Dev Agent Record ### Agent Model Used openrouter/anthropic/claude-sonnet-4.6 ### Debug Log References Aucun blocage. Fix d'une erreur de compilation préexistante dans `flooded_evaporator.rs` (méthode `enthalpy_px` inexistante remplacée par `backend.property(...)` avec `FluidState::from_px(...)`). ### Completion Notes List - Créé `crates/components/src/air_boundary.rs` avec `AirSource` et `AirSink` - Implémenté 4 helpers psychrométriques : `saturation_vapor_pressure`, `humidity_ratio_from_rh`, `specific_enthalpy_from_w`, `rh_from_wet_bulb` - Utilisé `RelativeHumidity` de `entropyk_core` pour la sécurité des types - Aucune dépendance au `FluidBackend` — formules analytiques Magnus-Tetens - `AirSink` dynamique : toggle entre 1-équation (pression seule) et 2-équations (P + h) - 23 tests unitaires passent dont 3 validations ASHRAE de référence - 469 tests au total dans le package, 0 régression - Module exporté dans `lib.rs` avec `AirSource` et `AirSink` - Fix secondaire : `flooded_evaporator.rs` erreur de compilation préexistante corrigée ### File List - `crates/components/src/air_boundary.rs` (créé) - `crates/components/src/lib.rs` (modifié — ajout module + re-exports) - `crates/components/src/heat_exchanger/flooded_evaporator.rs` (modifié — fix erreur de compilation préexistante) ### Files Created in Epic 10 (Related Context) - `crates/components/src/brine_boundary.rs` (créé — Story 10-3) - `crates/components/src/refrigerant_boundary.rs` (créé — Story 10-2) - `crates/components/src/drum.rs` (créé — Story 11-2) ### Change Log - 2026-02-23: Implémentation AirSource et AirSink avec propriétés psychrométriques complètes (Story 10-4) - 2026-02-23: **Code Review (AI)** — Fixed 8 issues: - Fixed `set_temperature()` and `set_rh()` to return `Result` with proper error handling - Fixed `humidity_ratio_from_rh()` to return `Result` instead of silent 0.0 on invalid P_v - Added validation for P_v >= P_atm (now returns descriptive error) - Updated Sprung formula documentation to clarify unventilated psychrometer assumption - Tightened ASHRAE test tolerances: P_sat (0.5%), enthalpy (1%), humidity ratio (1%) - Tightened specific_enthalpy test range from (40-80) to (45-56) kJ/kg - Updated File List with related files from Epic 10 - 23 tests pass, 0 regressions, 0 air_boundary clippy warnings --- ## Senior Developer Review (AI) **Reviewer:** Claude-4 (Sonnet) **Date:** 2026-02-23 **Outcome:** ✅ **APPROVED with Fixes Applied** ### Issues Found and Fixed #### 🔴 Critical (1) 1. **Missing Result type on setters** — `set_temperature()` and `set_rh()` did not return `Result` despite potential failure modes. **FIXED:** Both now return `Result<(), ComponentError>` with proper validation. #### 🟡 High (2) 2. **Sprung formula assumptions undocumented** — The psychrometric constant A_psy = 6.6e-4 is specific to unventilated psychrometers. **FIXED:** Added explicit documentation about this assumption. 3. **ASHRAE test tolerances too loose** — Original tolerances (1.6% for P_sat, 2.6% for h) were too permissive. **FIXED:** Tightened to 0.5% for P_sat and 1% for h and W. #### 🟡 Medium (2) 4. **File List incomplete** — Story documented only 3 files but Epic 10 created 6+ files. **FIXED:** Added "Files Created in Epic 10 (Related Context)" section. 5. **Silent error handling** — `humidity_ratio_from_rh()` returned 0.0 when P_v >= P_atm instead of error. **FIXED:** Now returns descriptive `ComponentError::InvalidState`. #### 🟢 Low (3) 6. **RH clamping without warning** — Documented behavior, acceptable for production use. 7. **Test enthalpy range too wide** — Was 40-80 kJ/kg, now 45-56 kJ/kg (ASHRAE standard). 8. **Documentation mismatch** — Setter docs claimed Result return type but didn't implement it. **FIXED:** Implementation now matches documentation. ### Verification - ✅ All 23 air_boundary tests pass - ✅ All 469 component tests pass (0 regressions) - ✅ 0 clippy warnings specific to air_boundary.rs - ✅ All Acceptance Criteria validated - ✅ All Tasks marked [x] verified complete ### Recommendation Story is **READY FOR PRODUCTION**. All critical and high issues resolved. Test coverage excellent (23 tests, including 3 ASHRAE reference validations).