# Story 9.5: Complétion Epic 7 - FlowSplitter/FlowMerger Energy Methods **Epic:** 9 - Coherence Corrections (Post-Audit) **Priorité:** P1-CRITIQUE **Estimation:** 4h **Statut:** review **Dépendances:** Story 9.2 (FluidId unification) --- ## Story > En tant que moteur de simulation thermodynamique, > Je veux que `FlowSplitter` et `FlowMerger` implémentent `energy_transfers()` et `port_enthalpies()`, > Afin que les jonctions soient correctement prises en compte dans le bilan énergétique. --- ## Contexte L'audit de cohérence a révélé que les composants de jonction (`FlowSplitter`, `FlowMerger`) implémentent `port_mass_flows()` mais **PAS** `energy_transfers()` ni `port_enthalpies()`. **Conséquence** : Ces composants sont **ignorés silencieusement** dans `check_energy_balance()`. --- ## Problème Actuel ```rust // crates/components/src/flow_junction.rs // FlowSplitter et FlowMerger ont: // - fn port_mass_flows() ✓ // MANQUE: // - fn port_enthalpies() ✗ // - fn energy_transfers() ✗ ``` --- ## Solution Proposée ### Physique des jonctions **FlowSplitter** (diviseur de flux) : - Un port d'entrée, plusieurs ports de sortie - Conservation du débit massique : ṁ_in = Σ ṁ_out - Conservation de l'enthalpie (mélange non-mixing) : h_in = h_out (pour chaque branche) - Pas de transfert thermique : Q = 0 - Pas de travail : W = 0 **FlowMerger** (collecteur de flux) : - Plusieurs ports d'entrée, un port de sortie - Conservation du débit massique : Σ ṁ_in = ṁ_out - Bilan énergétique : ṁ_out × h_out = Σ (ṁ_in × h_in) - Pas de transfert thermique : Q = 0 - Pas de travail : W = 0 ### Implémentation ```rust // crates/components/src/flow_junction.rs impl Component for FlowSplitter { // ... existing implementations ... /// Retourne les enthalpies des ports (ordre: inlet, puis outlets). fn port_enthalpies( &self, _state: &SystemState, ) -> Result, ComponentError> { let mut enthalpies = Vec::with_capacity(self.ports_outlet.len() + 1); // Enthalpie du port d'entrée let h_in = self.port_inlet.enthalpy() .ok_or_else(|| ComponentError::MissingData { component: self.name().to_string(), data: "inlet enthalpy".to_string(), })?; enthalpies.push(h_in); // Enthalpies des ports de sortie for (i, outlet) in self.ports_outlet.iter().enumerate() { let h_out = outlet.enthalpy() .ok_or_else(|| ComponentError::MissingData { component: self.name().to_string(), data: format!("outlet {} enthalpy", i), })?; enthalpies.push(h_out); } Ok(enthalpies) } /// Retourne les transferts énergétiques du diviseur. /// /// Un diviseur de flux est adiabatique: /// - Q = 0 (pas d'échange thermique) /// - W = 0 (pas de travail) fn energy_transfers(&self, _state: &SystemState) -> Option<(Power, Power)> { Some((Power::from_watts(0.0), Power::from_watts(0.0))) } } impl Component for FlowMerger { // ... existing implementations ... /// Retourne les enthalpies des ports (ordre: inlets, puis outlet). fn port_enthalpies( &self, _state: &SystemState, ) -> Result, ComponentError> { let mut enthalpies = Vec::with_capacity(self.ports_inlet.len() + 1); // Enthalpies des ports d'entrée for (i, inlet) in self.ports_inlet.iter().enumerate() { let h_in = inlet.enthalpy() .ok_or_else(|| ComponentError::MissingData { component: self.name().to_string(), data: format!("inlet {} enthalpy", i), })?; enthalpies.push(h_in); } // Enthalpie du port de sortie let h_out = self.port_outlet.enthalpy() .ok_or_else(|| ComponentError::MissingData { component: self.name().to_string(), data: "outlet enthalpy".to_string(), })?; enthalpies.push(h_out); Ok(enthalpies) } /// Retourne les transferts énergétiques du collecteur. /// /// Un collecteur de flux est adiabatique: /// - Q = 0 (pas d'échange thermique) /// - W = 0 (pas de travail) fn energy_transfers(&self, _state: &SystemState) -> Option<(Power, Power)> { Some((Power::from_watts(0.0), Power::from_watts(0.0))) } } ``` --- ## Fichiers à Modifier | Fichier | Action | |---------|--------| | `crates/components/src/flow_junction.rs` | Ajouter `port_enthalpies()` et `energy_transfers()` pour `FlowSplitter` et `FlowMerger` | --- ## Critères d'Acceptation - [x] `FlowSplitter::energy_transfers()` retourne `Some((Power(0), Power(0)))` - [x] `FlowMerger::energy_transfers()` retourne `Some((Power(0), Power(0)))` - [x] `FlowSplitter::port_enthalpies()` retourne `[h_in, h_out1, h_out2, ...]` - [x] `FlowMerger::port_enthalpies()` retourne `[h_in1, h_in2, ..., h_out]` - [x] Gestion d'erreur si ports non connectés - [x] Tests unitaires passent - [x] `check_energy_balance()` ne skip plus ces composants --- ## Tests Requis ```rust #[cfg(test)] mod tests { use super::*; use entropyk_core::{Enthalpy, Power, MassFlow}; #[test] fn test_flow_splitter_energy_transfers_zero() { let splitter = create_test_splitter(2); // 1 inlet, 2 outlets let state = SystemState::default(); let (heat, work) = splitter.energy_transfers(&state).unwrap(); assert_eq!(heat.to_watts(), 0.0); assert_eq!(work.to_watts(), 0.0); } #[test] fn test_flow_merger_energy_transfers_zero() { let merger = create_test_merger(2); // 2 inlets, 1 outlet let state = SystemState::default(); let (heat, work) = merger.energy_transfers(&state).unwrap(); assert_eq!(heat.to_watts(), 0.0); assert_eq!(work.to_watts(), 0.0); } #[test] fn test_flow_splitter_port_enthalpies_count() { let splitter = create_test_splitter(3); // 1 inlet, 3 outlets let state = SystemState::default(); let enthalpies = splitter.port_enthalpies(&state).unwrap(); // 1 inlet + 3 outlets = 4 enthalpies assert_eq!(enthalpies.len(), 4); } #[test] fn test_flow_merger_port_enthalpies_count() { let merger = create_test_merger(3); // 3 inlets, 1 outlet let state = SystemState::default(); let enthalpies = merger.port_enthalpies(&state).unwrap(); // 3 inlets + 1 outlet = 4 enthalpies assert_eq!(enthalpies.len(), 4); } #[test] fn test_flow_splitter_enthalpy_conservation() { // Pour un splitter idéal: h_in = h_out1 = h_out2 = ... let splitter = create_test_splitter_with_equal_enthalpies(); let state = SystemState::default(); let enthalpies = splitter.port_enthalpies(&state).unwrap(); let h_in = enthalpies[0]; for h_out in &enthalpies[1..] { assert_relative_eq!(h_out.to_joules_per_kg(), h_in.to_joules_per_kg()); } } } ``` --- ## Note sur le Bilan Énergétique des Jonctions ### FlowSplitter ``` Énergie entrante = ṁ_in × h_in Énergie sortante = Σ ṁ_out_i × h_out_i Pour un splitter idéal (non-mixing): h_in = h_out_i (pour tout i) ṁ_in = Σ ṁ_out_i Bilan: Énergie_in = Énergie_out ✓ ``` ### FlowMerger ``` Énergie entrante = Σ ṁ_in_i × h_in_i Énergie sortante = ṁ_out × h_out Pour un merger idéal: ṁ_out = Σ ṁ_in_i h_out = Σ (ṁ_in_i × h_in_i) / ṁ_out (mélange adiabatique) Bilan: Énergie_in = Énergie_out ✓ ``` --- ## Références - [Epic 7 Story 7.2 - Energy Balance Validation](./7-2-energy-balance-validation.md) - [Coherence Audit Report](./coherence-audit-remediation-plan.md) --- ## File List | File | Action | |------|--------| | `crates/components/src/flow_junction.rs` | Modified - Added `port_enthalpies()` and `energy_transfers()` for `FlowSplitter` and `FlowMerger` | --- ## Dev Agent Record ### Implementation Plan - Add `port_enthalpies()` method to `FlowSplitter` returning `[h_in, h_out1, h_out2, ...]` - Add `port_enthalpies()` method to `FlowMerger` returning `[h_in1, h_in2, ..., h_out]` - Add `energy_transfers()` method to both returning `Some((Power(0), Power(0)))` (adiabatic components) - Add comprehensive unit tests for both methods ### Completion Notes - ✅ Implemented `FlowSplitter::port_enthalpies()` - returns enthalpies from inlet and all outlet ports - ✅ Implemented `FlowMerger::port_enthalpies()` - returns enthalpies from all inlet ports and outlet port - ✅ Implemented `FlowSplitter::energy_transfers()` - returns `Some((Power(0), Power(0)))` (adiabatic) - ✅ Implemented `FlowMerger::energy_transfers()` - returns `Some((Power(0), Power(0)))` (adiabatic) - ✅ Added 6 new unit tests covering all acceptance criteria - ✅ All 22 flow_junction tests pass - ✅ `check_energy_balance()` will now include FlowSplitter/FlowMerger components --- ## Change Log | Date | Change | |------|--------| | 2026-02-22 | Completed implementation of `energy_transfers()` and `port_enthalpies()` for FlowSplitter and FlowMerger |