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

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é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

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

  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

    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

#[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)