18 KiB
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 contient deux composants:
FlowSource: Source de débit avec pression et enthalpie fixéesFlowSink: Puits de débit avec contre-pression fixée
Limitations identifiées:
- Distinction binaire
FluidKind::IncompressiblevsFluidKind::Compressibletrop simpliste - Pas de support pour les propriétés spécifiques des fluides caloporteurs (concentration glycol)
- Pas de support pour les propriétés de l'air (humidité relative, température bulbe humide)
- Méthodes
energy_transfers()etport_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
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
// 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
// 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)
/// 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)
/// 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)
/// 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
-
Phase 1: Ajouter les nouveaux types sans supprimer les anciens
FlowSourceetFlowSinkrestent disponibles- Ajouter
#[deprecated]avec message de migration
-
Phase 2: Mapper les anciens constructeurs vers les nouveaux
impl FlowSource { #[deprecated(since = "0.2.0", note = "Use RefrigerantSource or BrineSource instead")] pub fn incompressible(...) -> Result<Self, ComponentError> { // Délègue vers BrineSource } } -
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:
- 10-1: Ajouter les nouveaux types physiques (
Concentration,VolumeFlow,RelativeHumidity,VaporQuality) - 10-2: Implémenter
RefrigerantSourceetRefrigerantSink - 10-3: Implémenter
BrineSourceetBrineSinkavec support glycol - 10-4: Implémenter
AirSourceetAirSinkavec propriétés psychrométriques - 10-5: Migration et dépréciation des anciens types
- 10-6: Mise à jour des bindings Python
6. Tests Requis
6.1 Tests Unitaires
#[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
-
Calcul des propriétés psychrométriques: Faut-il intégrer une librairie dédiée (ex:
psychrolib) ou utiliser CoolProp? -
Support des mélanges eau-glycol: CoolProp supporte-il correctement les propriétés des mélanges à différentes concentrations?
-
Validation des concentrations: Quelle plage de concentration est valide pour chaque type de glycol (MEG, PEG)?
-
Performance: Les calculs de propriétés pour les mélanges sont-ils compatibles avec les exigences de performance (< 1s)?
8. Recommandations
-
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.
-
Valider avec CoolProp: Vérifier le support des mélanges eau-glycol et des calculs psychrométriques avant implémentation.
-
Consulter l'utilisateur: Confirmer les besoins spécifiques pour chaque type de fluide (plages de valeurs, unités préférées).
-
Documenter la migration: Fournir un guide de migration clair pour les utilisateurs existants.
9. Prochaines Étapes
- Valider cette architecture avec l'utilisateur
- Créer l'Epic 10 et les stories associées
- Implémenter les nouveaux types physiques (Story 10-1)
- Implémenter les nouveaux composants (Stories 10-2 à 10-4)
- Migrer et déprécier les anciens types (Story 10-5)