219 lines
10 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:** 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<dyn Component>`)
- [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<f64, ComponentError>` 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<T, ComponentError>`
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) 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).