582 lines
18 KiB
Markdown
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)
|