Entropyk/plans/boundary-condition-refactoring-architecture.md

582 lines
18 KiB
Markdown

# Architecture: Refactoring des Conditions aux Limites (FlowSource/FlowSink)
**Date:** 2026-02-22
**Author:** Architect Agent
**Status:** Draft
**Related:** Story 9-4, Epic 7 (Validation & Persistence)
---
## 1. Contexte et Problématique
### 1.1 État Actuel
Le fichier [`flow_boundary.rs`](crates/components/src/flow_boundary.rs) contient deux composants:
- **`FlowSource`**: Source de débit avec pression et enthalpie fixées
- **`FlowSink`**: Puits de débit avec contre-pression fixée
**Limitations identifiées:**
1. Distinction binaire `FluidKind::Incompressible` vs `FluidKind::Compressible` trop simpliste
2. Pas de support pour les propriétés spécifiques des fluides caloporteurs (concentration glycol)
3. Pas de support pour les propriétés de l'air (humidité relative, température bulbe humide)
4. Méthodes `energy_transfers()` et `port_enthalpies()` manquantes (Story 9-4)
### 1.2 Besoin Utilisateur
L'utilisateur a identifié le besoin de **3 types distincts** de conditions aux limites:
| Type | Fluides | Propriétés Spécifiques |
|------|---------|------------------------|
| **Compressible** | Réfrigérants (R410A, R134a, CO2, etc.) | Titre (vapor quality), pression, température, débit massique |
| **Caloporteur Liquide** | Eau, PEG, MEG, saumures | Concentration (% massique glycol), température, débit volumique ou massique |
| **Air** | Air humide | Température sèche, température bulbe humide, humidité relative, débit |
---
## 2. Architecture Proposée
### 2.1 Vue d'Ensemble
```mermaid
classDiagram
class Component {
<<trait>>
+compute_residuals()
+jacobian_entries()
+n_equations()
+get_ports()
+port_mass_flows()
+port_enthalpies()
+energy_transfers()
}
class BoundaryCondition {
<<trait>>
+fluid_type() FluidType
+validate() Result
}
class RefrigerantSource {
-fluid_id: String
-pressure: Pressure
-enthalpy: Enthalpy
-vapor_quality: Option~f64~
-mass_flow: Option~MassFlow~
+outlet: ConnectedPort
}
class RefrigerantSink {
-fluid_id: String
-pressure: Pressure
-enthalpy: Option~Enthalpy~
+inlet: ConnectedPort
}
class BrineSource {
-fluid_id: String
-concentration: Concentration
-temperature: Temperature
-mass_flow: Option~MassFlow~
-volume_flow: Option~VolumeFlow~
+outlet: ConnectedPort
}
class BrineSink {
-fluid_id: String
-concentration: Concentration
-pressure: Pressure
-temperature: Option~Temperature~
+inlet: ConnectedPort
}
class AirSource {
-temperature_dry: Temperature
-humidity_relative: Option~f64~
-wet_bulb_temp: Option~Temperature~
-mass_flow: Option~MassFlow~
+outlet: ConnectedPort
}
class AirSink {
-pressure: Pressure
-temperature: Option~Temperature~
+inlet: ConnectedPort
}
Component <|-- BoundaryCondition
BoundaryCondition <|-- RefrigerantSource
BoundaryCondition <|-- RefrigerantSink
BoundaryCondition <|-- BrineSource
BoundaryCondition <|-- BrineSink
BoundaryCondition <|-- AirSource
BoundaryCondition <|-- AirSink
```
### 2.2 Nouveaux Types Physiques Requis
```rust
// crates/core/src/types.rs
/// Concentration massique en % (0-100)
/// Utilisé pour les mélanges eau-glycol (PEG, MEG)
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Concentration(pub f64);
impl Concentration {
/// Crée une concentration depuis un pourcentage (0-100)
pub fn from_percent(value: f64) -> Self {
debug_assert!(value >= 0.0 && value <= 100.0);
Concentration(value.clamp(0.0, 100.0))
}
/// Retourne la concentration en pourcentage
pub fn to_percent(&self) -> f64 {
self.0
}
/// Retourne la fraction massique (0-1)
pub fn to_mass_fraction(&self) -> f64 {
self.0 / 100.0
}
}
/// Débit volumique en m³/s
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct VolumeFlow(pub f64);
impl VolumeFlow {
pub fn from_m3_per_s(value: f64) -> Self {
VolumeFlow(value)
}
pub fn from_l_per_min(value: f64) -> Self {
VolumeFlow(value / 1000.0 / 60.0)
}
pub fn to_m3_per_s(&self) -> f64 {
self.0
}
pub fn to_l_per_min(&self) -> f64 {
self.0 * 1000.0 * 60.0
}
}
/// Humidité relative en % (0-100)
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct RelativeHumidity(pub f64);
impl RelativeHumidity {
pub fn from_percent(value: f64) -> Self {
debug_assert!(value >= 0.0 && value <= 100.0);
RelativeHumidity(value.clamp(0.0, 100.0))
}
pub fn to_percent(&self) -> f64 {
self.0
}
pub fn to_fraction(&self) -> f64 {
self.0 / 100.0
}
}
/// Titre (vapor quality) pour fluides frigorigènes (0-1)
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct VaporQuality(pub f64);
impl VaporQuality {
pub fn from_fraction(value: f64) -> Self {
debug_assert!(value >= 0.0 && value <= 1.0);
VaporQuality(value.clamp(0.0, 1.0))
}
pub fn to_fraction(&self) -> f64 {
self.0
}
pub fn to_percent(&self) -> f64 {
self.0 * 100.0
}
}
```
### 2.3 Énumération des Types de Fluide
```rust
// crates/components/src/flow_boundary.rs
/// Types de fluide supportés par les conditions aux limites
#[derive(Debug, Clone, PartialEq)]
pub enum FluidType {
/// Fluide frigorigène compressible (R410A, R134a, CO2, etc.)
Refrigerant {
fluid_id: String,
},
/// Fluide caloporteur liquide (eau, PEG, MEG, saumure)
Brine {
fluid_id: String,
concentration: Option<Concentration>,
},
/// Air humide
Air,
}
impl FluidType {
/// Retourne true si le fluide est un réfrigérant
pub fn is_refrigerant(&self) -> bool {
matches!(self, FluidType::Refrigerant { .. })
}
/// Retourne true si le fluide est un caloporteur liquide
pub fn is_brine(&self) -> bool {
matches!(self, FluidType::Brine { .. })
}
/// Retourne true si le fluide est de l'air
pub fn is_air(&self) -> bool {
matches!(self, FluidType::Air)
}
}
```
---
## 3. Conception Détaillée
### 3.1 RefrigerantSource (Source Réfrigérant)
```rust
/// Source pour fluides frigorigènes compressibles.
///
/// Impose une pression et une enthalpie (ou titre) fixées sur le port de sortie.
/// Optionnellement, un débit massique peut être imposé.
///
/// # Équations
/// - r₀ = P_edge - P_set = 0 (condition de pression)
/// - r₁ = h_edge - h_set = 0 (condition d'enthalpie)
///
/// # Propriétés spécifiques
/// - `vapor_quality`: Titre optionnel (0 = liquide saturé, 1 = vapeur saturée)
/// - `mass_flow`: Débit massique optionnel (kg/s)
#[derive(Debug, Clone)]
pub struct RefrigerantSource {
/// Identifiant du fluide frigorigène (ex: "R410A", "R134a", "CO2")
fluid_id: String,
/// Pression de set-point [Pa]
p_set: Pressure,
/// Enthalpie de set-point [J/kg]
h_set: Enthalpy,
/// Titre optionnel (vapor quality, 0-1)
vapor_quality: Option<VaporQuality>,
/// Débit massique optionnel [kg/s]
mass_flow: Option<MassFlow>,
/// Port de sortie connecté
outlet: ConnectedPort,
}
impl RefrigerantSource {
/// Crée une source réfrigérant avec pression et enthalpie fixées.
pub fn new(
fluid_id: impl Into<String>,
pressure: Pressure,
enthalpy: Enthalpy,
outlet: ConnectedPort,
) -> Result<Self, ComponentError> {
// Validation...
}
/// Crée une source réfrigérant avec pression et titre fixés.
/// L'enthalpie est calculée automatiquement via CoolProp.
pub fn with_vapor_quality(
fluid_id: impl Into<String>,
pressure: Pressure,
vapor_quality: VaporQuality,
outlet: ConnectedPort,
) -> Result<Self, ComponentError> {
// Calcul h = h_sat_liquid + x * h_lv
}
/// Définit le débit massique imposé.
pub fn set_mass_flow(&mut self, mass_flow: MassFlow) {
self.mass_flow = Some(mass_flow);
}
}
impl Component for RefrigerantSource {
fn energy_transfers(&self, _state: &SystemState) -> Option<(Power, Power)> {
// Source = pas de transfert actif (Q=0, W=0)
Some((Power::from_watts(0.0), Power::from_watts(0.0)))
}
fn port_enthalpies(&self, _state: &SystemState) -> Result<Vec<Enthalpy>, ComponentError> {
Ok(vec![self.h_set])
}
fn port_mass_flows(&self, _state: &SystemState) -> Result<Vec<MassFlow>, ComponentError> {
// Pour une source, le débit est sortant (négatif par convention)
match self.mass_flow {
Some(mdot) => Ok(vec![MassFlow::from_kg_per_s(-mdot.to_kg_per_s())]),
None => Ok(vec![]), // Débit déterminé par les composants connectés
}
}
}
```
### 3.2 BrineSource (Source Caloporteur Liquide)
```rust
/// Source pour fluides caloporteurs liquides (eau, PEG, MEG, saumures).
///
/// Impose une température et une pression fixées sur le port de sortie.
/// La concentration en glycol est prise en compte pour les propriétés thermophysiques.
///
/// # Équations
/// - r₀ = P_edge - P_set = 0 (condition de pression)
/// - r₁ = h_edge - h_set = 0 (condition d'enthalpie, calculée depuis T et concentration)
///
/// # Propriétés spécifiques
/// - `concentration`: Concentration massique en glycol (%)
/// - `temperature`: Température du fluide [K]
/// - `mass_flow` ou `volume_flow`: Débit imposé
#[derive(Debug, Clone)]
pub struct BrineSource {
/// Identifiant du fluide (ex: "Water", "MEG", "PEG")
fluid_id: String,
/// Concentration en glycol (% massique, 0 = eau pure)
concentration: Concentration,
/// Température de set-point [K]
t_set: Temperature,
/// Pression de set-point [Pa]
p_set: Pressure,
/// Enthalpie calculée depuis T et concentration [J/kg]
h_set: Enthalpy,
/// Débit massique optionnel [kg/s]
mass_flow: Option<MassFlow>,
/// Débit volumique optionnel [m³/s]
volume_flow: Option<VolumeFlow>,
/// Port de sortie connecté
outlet: ConnectedPort,
}
impl BrineSource {
/// Crée une source d'eau pure.
pub fn water(
temperature: Temperature,
pressure: Pressure,
outlet: ConnectedPort,
) -> Result<Self, ComponentError> {
Self::new("Water", Concentration::from_percent(0.0), temperature, pressure, outlet)
}
/// Crée une source de mélange eau-glycol.
pub fn glycol_mixture(
fluid_id: impl Into<String>,
concentration: Concentration,
temperature: Temperature,
pressure: Pressure,
outlet: ConnectedPort,
) -> Result<Self, ComponentError> {
Self::new(fluid_id, concentration, temperature, pressure, outlet)
}
/// Définit le débit volumique imposé.
/// Le débit massique est calculé avec la masse volumique du mélange.
pub fn set_volume_flow(&mut self, volume_flow: VolumeFlow, density: f64) {
self.volume_flow = Some(volume_flow);
self.mass_flow = Some(MassFlow::from_kg_per_s(volume_flow.to_m3_per_s() * density));
}
}
```
### 3.3 AirSource (Source Air Humide)
```rust
/// Source pour air humide (côté air des échangeurs).
///
/// Impose les conditions de l'air entrant: température sèche, humidité, débit.
///
/// # Propriétés spécifiques
/// - `temperature_dry`: Température sèche [K]
/// - `humidity_relative`: Humidité relative [%]
/// - `wet_bulb_temp`: Température bulbe humide [K] (alternative à HR)
/// - `mass_flow`: Débit massique d'air [kg/s]
#[derive(Debug, Clone)]
pub struct AirSource {
/// Température sèche [K]
t_dry: Temperature,
/// Humidité relative [%]
rh: RelativeHumidity,
/// Température bulbe humide optionnelle [K]
t_wet_bulb: Option<Temperature>,
/// Débit massique d'air sec [kg/s]
mass_flow: Option<MassFlow>,
/// Port de sortie connecté
outlet: ConnectedPort,
}
impl AirSource {
/// Crée une source d'air avec température sèche et humidité relative.
pub fn from_dry_bulb_rh(
temperature_dry: Temperature,
relative_humidity: RelativeHumidity,
outlet: ConnectedPort,
) -> Result<Self, ComponentError> {
// ...
}
/// Crée une source d'air avec températures sèche et bulbe humide.
pub fn from_dry_and_wet_bulb(
temperature_dry: Temperature,
temperature_wet_bulb: Temperature,
outlet: ConnectedPort,
) -> Result<Self, ComponentError> {
// Calcul HR depuis températures sèche et bulbe humide
}
}
```
---
## 4. Migration et Rétrocompatibilité
### 4.1 Stratégie de Migration
1. **Phase 1**: Ajouter les nouveaux types sans supprimer les anciens
- `FlowSource` et `FlowSink` restent disponibles
- Ajouter `#[deprecated]` avec message de migration
2. **Phase 2**: Mapper les anciens constructeurs vers les nouveaux
```rust
impl FlowSource {
#[deprecated(since = "0.2.0", note = "Use RefrigerantSource or BrineSource instead")]
pub fn incompressible(...) -> Result<Self, ComponentError> {
// Délègue vers BrineSource
}
}
```
3. **Phase 3**: Supprimer les anciens types après validation
### 4.2 Mapping des Types Existants
| Ancien Type | Nouveau Type |
|-------------|--------------|
| `FlowSource::incompressible("Water", ...)` | `BrineSource::water(...)` |
| `FlowSource::incompressible("MEG", ...)` | `BrineSource::glycol_mixture("MEG", concentration, ...)` |
| `FlowSource::compressible("R410A", ...)` | `RefrigerantSource::new("R410A", ...)` |
| `FlowSink::incompressible(...)` | `BrineSink::new(...)` |
| `FlowSink::compressible(...)` | `RefrigerantSink::new(...)` |
---
## 5. Impacts sur l'Existant
### 5.1 Fichiers à Modifier
| Fichier | Changements |
|---------|-------------|
| `crates/core/src/types.rs` | Ajouter `Concentration`, `VolumeFlow`, `RelativeHumidity`, `VaporQuality` |
| `crates/core/src/lib.rs` | Re-exporter les nouveaux types |
| `crates/components/src/flow_boundary.rs` | Refactoring complet avec nouveaux types |
| `crates/components/src/lib.rs` | Exporter les nouveaux types |
| `crates/components/src/python_components.rs` | Mise à jour des bindings Python |
### 5.2 Nouveaux Fichiers
| Fichier | Description |
|---------|-------------|
| `crates/components/src/flow_boundary/refrigerant.rs` | `RefrigerantSource`, `RefrigerantSink` |
| `crates/components/src/flow_boundary/brine.rs` | `BrineSource`, `BrineSink` |
| `crates/components/src/flow_boundary/air.rs` | `AirSource`, `AirSink` |
| `crates/components/src/flow_boundary/mod.rs` | Module principal avec ré-exports |
### 5.3 Impacts sur PRD et Epics
**Nouvel Epic Suggéré:** "Epic 10: Enhanced Boundary Conditions"
**Stories Proposées:**
1. **10-1**: Ajouter les nouveaux types physiques (`Concentration`, `VolumeFlow`, `RelativeHumidity`, `VaporQuality`)
2. **10-2**: Implémenter `RefrigerantSource` et `RefrigerantSink`
3. **10-3**: Implémenter `BrineSource` et `BrineSink` avec support glycol
4. **10-4**: Implémenter `AirSource` et `AirSink` avec propriétés psychrométriques
5. **10-5**: Migration et dépréciation des anciens types
6. **10-6**: Mise à jour des bindings Python
---
## 6. Tests Requis
### 6.1 Tests Unitaires
```rust
#[cfg(test)]
mod tests {
// Tests RefrigerantSource
#[test]
fn test_refrigerant_source_energy_transfers_zero() { /* ... */ }
#[test]
fn test_refrigerant_source_port_enthalpies() { /* ... */ }
#[test]
fn test_refrigerant_source_with_vapor_quality() { /* ... */ }
// Tests BrineSource
#[test]
fn test_brine_source_water() { /* ... */ }
#[test]
fn test_brine_source_glycol_mixture() { /* ... */ }
#[test]
fn test_brine_source_concentration_validation() { /* ... */ }
// Tests AirSource
#[test]
fn test_air_source_from_dry_bulb_rh() { /* ... */ }
#[test]
fn test_air_source_from_wet_bulb() { /* ... */ }
#[test]
fn test_air_source_psychrometric_calculations() { /* ... */ }
}
```
### 6.2 Tests d'Intégration
- Vérifier que `check_energy_balance()` inclut correctement les nouvelles sources/puits
- Tester la compatibilité avec les composants existants (évaporateur, condenseur)
- Valider les calculs de propriétés avec CoolProp
---
## 7. Questions Ouvertes
1. **Calcul des propriétés psychrométriques**: Faut-il intégrer une librairie dédiée (ex: `psychrolib`) ou utiliser CoolProp?
2. **Support des mélanges eau-glycol**: CoolProp supporte-il correctement les propriétés des mélanges à différentes concentrations?
3. **Validation des concentrations**: Quelle plage de concentration est valide pour chaque type de glycol (MEG, PEG)?
4. **Performance**: Les calculs de propriétés pour les mélanges sont-ils compatibles avec les exigences de performance (< 1s)?
---
## 8. Recommandations
1. **Procéder par étapes**: D'abord compléter la Story 9-4 avec les méthodes manquantes, puis planifier l'Epic 10 pour la refonte complète.
2. **Valider avec CoolProp**: Vérifier le support des mélanges eau-glycol et des calculs psychrométriques avant implémentation.
3. **Consulter l'utilisateur**: Confirmer les besoins spécifiques pour chaque type de fluide (plages de valeurs, unités préférées).
4. **Documenter la migration**: Fournir un guide de migration clair pour les utilisateurs existants.
---
## 9. Prochaines Étapes
1. **Valider cette architecture** avec l'utilisateur
2. **Créer l'Epic 10** et les stories associées
3. **Implémenter les nouveaux types physiques** (Story 10-1)
4. **Implémenter les nouveaux composants** (Stories 10-2 à 10-4)
5. **Migrer et déprécier** les anciens types (Story 10-5)