307 lines
9.3 KiB
Markdown
307 lines
9.3 KiB
Markdown
# 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 |
|