# 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<Vec<entropyk_core::Enthalpy>, 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<Vec<entropyk_core::Enthalpy>, 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 |
