""" Service de calculs de cycles frigorifiques. Ce module fournit les fonctionnalités pour calculer les performances d'un cycle frigorifique: - Cycle simple (compression simple) - Cycle avec économiseur (double étage) - Calculs de COP, puissance, rendement """ import math from typing import Optional, Tuple, List, Dict, Any from dataclasses import dataclass from app.core.refrigerant_loader import RefrigerantLibrary @dataclass class ThermodynamicState: """État thermodynamique complet d'un point.""" pressure: float # Pa (SI) temperature: float # °C enthalpy: float # kJ/kg entropy: float # kJ/kg.K density: Optional[float] = None # kg/m³ quality: Optional[float] = None # 0-1 class CycleCalculator: """Calculateur de cycles frigorifiques.""" def _safe_val(self, value, default=0): """Retourne value ou default si None""" return value if value is not None else default def __init__(self, refrigerant: RefrigerantLibrary): """ Initialise le calculateur. Args: refrigerant: Instance de RefrigerantLibrary """ self.refrigerant = refrigerant def get_pressure_from_saturation_temperature(self, temperature_celsius: float, quality: float = 0.5) -> float: """ Calcule la pression de saturation à partir d'une température. Args: temperature_celsius: Température de saturation (°C) quality: Qualité pour le calcul (0.0 pour liquide, 1.0 pour vapeur, 0.5 par défaut) Returns: Pression de saturation (Pa) """ if temperature_celsius is None: raise ValueError("temperature_celsius cannot be None") temperature_kelvin = temperature_celsius + 273.15 pressure_pa = self.refrigerant.p_Tx(temperature_kelvin, quality) # p_Tx retourne des Pa (Refifc utilise Pa). On travaille en Pa en interne. return pressure_pa if pressure_pa else 1.0 def calculate_compressor_efficiency(self, pressure_ratio: float) -> float: """ Calcule le rendement isentropique du compresseur basé sur le rapport de pression. Utilise une corrélation empirique typique pour compresseurs frigorifiques: η_is = 0.90 - 0.04 × ln(PR) Cette formule reflète la dégradation du rendement avec l'augmentation du rapport de pression. Args: pressure_ratio: Rapport de pression P_cond / P_evap Returns: Rendement isentropique (0-1) Note: - PR = 2.0 → η ≈ 0.87 (87%) - PR = 4.0 → η ≈ 0.84 (84%) - PR = 6.0 → η ≈ 0.83 (83%) - PR = 8.0 → η ≈ 0.82 (82%) """ if pressure_ratio < 1.0: raise ValueError(f"Le rapport de pression doit être >= 1.0, reçu: {pressure_ratio}") # Formule empirique typique efficiency = 0.90 - 0.04 * math.log(pressure_ratio) # Limiter entre des valeurs réalistes efficiency = max(0.60, min(0.90, efficiency)) return efficiency def calculate_point_px( self, pressure: float, quality: float ) -> ThermodynamicState: """ Calcule l'état thermodynamique à partir de P et x. Args: pressure: Pression (Pa) quality: Titre vapeur (0-1) Returns: État thermodynamique complet """ # RefrigerantLibrary prend des pressions en Pa T_K = self.refrigerant.T_px(pressure, quality) h_J = self.refrigerant.h_px(pressure, quality) s_J = self.refrigerant.s_px(pressure, quality) rho = self.refrigerant.rho_px(pressure, quality) return ThermodynamicState( pressure=pressure, # Pa temperature=(T_K - 273.15) if T_K else 0, # °C enthalpy=(h_J / 1000) if h_J else 0, # kJ/kg entropy=(s_J / 1000) if s_J else 0, # kJ/kg.K (CORRECTION: était J/kg.K) density=rho, # kg/m³ quality=quality ) def calculate_point_ph( self, pressure: float, enthalpy: float ) -> ThermodynamicState: """ Calcule l'état thermodynamique à partir de P et h. Args: pressure: Pression (Pa) enthalpy: Enthalpie (kJ/kg) Returns: État thermodynamique complet """ # RefrigerantLibrary prend des pressions en Pa et enthalpie en J/kg h_J = enthalpy * 1000 x = self.refrigerant.x_ph(pressure, h_J) T_K = self.refrigerant.T_px(pressure, x) s_J = self.refrigerant.s_px(pressure, x) rho = self.refrigerant.rho_px(pressure, x) return ThermodynamicState( pressure=pressure, # Pa temperature=(T_K - 273.15) if T_K else 0, # °C enthalpy=enthalpy, # kJ/kg entropy=(s_J / 1000) if s_J else 0, # kJ/kg.K (CORRECTION: était J/kg.K) density=rho, # kg/m³ quality=x if 0 <= x <= 1 else None ) def calculate_superheat_point( self, pressure: float, superheat: float ) -> ThermodynamicState: """ Calcule un point avec surchauffe. Args: pressure: Pression (Pa) superheat: Surchauffe (°C) Returns: État thermodynamique """ # RefrigerantLibrary prend des pressions en Pa # Température de saturation T_sat_K = self.refrigerant.T_px(pressure, 1.0) T_K = (T_sat_K if T_sat_K else 273.15) + superheat # Propriétés à P et T h_J = self.refrigerant.h_pT(pressure, T_K) return self.calculate_point_ph(pressure, (h_J / 1000) if h_J else 0) def calculate_subcool_point( self, pressure: float, subcool: float ) -> ThermodynamicState: """ Calcule un point avec sous-refroidissement. Args: pressure: Pression (Pa) subcool: Sous-refroidissement (°C) Returns: État thermodynamique """ # RefrigerantLibrary prend des pressions en Pa # Température de saturation T_sat_K = self.refrigerant.T_px(pressure, 0.0) T_K = (T_sat_K if T_sat_K else 273.15) - subcool # Propriétés à P et T h_J = self.refrigerant.h_pT(pressure, T_K) return self.calculate_point_ph(pressure, (h_J / 1000) if h_J else 0) def calculate_isentropic_compression( self, p_in: float, h_in: float, p_out: float ) -> ThermodynamicState: """ Calcule la compression isentropique (approximation). Args: p_in: Pression entrée (Pa) h_in: Enthalpie entrée (kJ/kg) p_out: Pression sortie (Pa) Returns: État en sortie (compression isentropique approximée) """ # État d'entrée state_in = self.calculate_point_ph(p_in, h_in) # Méthode simplifiée: utiliser relation polytropique # Pour un gaz réel: T_out/T_in = (P_out/P_in)^((k-1)/k) # Approximation pour réfrigérants: k ≈ 1.15 k = 1.15 T_in_K = (state_in.temperature + 273.15) if state_in.temperature is not None else 273.15 # p_out and p_in are in Pa; ratio is unitless T_out_K = T_in_K * ((p_out / p_in) ** ((k - 1) / k)) # Calculer enthalpie à P_out et T_out h_out_J = self.refrigerant.h_pT(p_out, T_out_K) return self.calculate_point_ph(p_out, h_out_J / 1000) def calculate_simple_cycle( self, evap_pressure: float, cond_pressure: float, superheat: float = 5.0, subcool: float = 3.0, compressor_efficiency: float = 0.70, mass_flow: float = 0.1 ) -> Dict[str, Any]: """ Calcule un cycle frigorifique simple (4 points). Args: evap_pressure: Pression évaporation (Pa) cond_pressure: Pression condensation (Pa) superheat: Surchauffe (°C) subcool: Sous-refroidissement (°C) compressor_efficiency: Rendement isentropique compresseur mass_flow: Débit massique (kg/s) Returns: Dictionnaire avec points et performances """ # Point 1: Sortie évaporateur (aspiration compresseur) point1 = self.calculate_superheat_point(evap_pressure, superheat) # Point 2s: Refoulement isentropique point2s = self.calculate_isentropic_compression( evap_pressure, point1.enthalpy, cond_pressure ) # Point 2: Refoulement réel h2 = self._safe_val(point1.enthalpy) + (self._safe_val(point2s.enthalpy) - self._safe_val(point1.enthalpy)) / compressor_efficiency point2 = self.calculate_point_ph(cond_pressure, h2) # Point 3: Sortie condenseur point3 = self.calculate_subcool_point(cond_pressure, subcool) # Point 4: Sortie détendeur (détente isenthalpique) point4 = self.calculate_point_ph(evap_pressure, point3.enthalpy) # Calculs de performances q_evap = self._safe_val(point1.enthalpy) - self._safe_val(point4.enthalpy) # kJ/kg w_comp = self._safe_val(point2.enthalpy) - self._safe_val(point1.enthalpy) # kJ/kg q_cond = self._safe_val(point2.enthalpy) - self._safe_val(point3.enthalpy) # kJ/kg cooling_capacity = mass_flow * q_evap # kW compressor_power = mass_flow * w_comp # kW heating_capacity = mass_flow * q_cond # kW cop = q_evap / w_comp if w_comp > 0 else 0 compression_ratio = cond_pressure / evap_pressure # Débit volumique à l'aspiration volumetric_flow = None if point1.density and point1.density > 0: volumetric_flow = (mass_flow / point1.density) * 3600 # m³/h return { "points": [ { "point_id": "1", "description": "Evaporator Outlet (Suction)", "pressure": point1.pressure, "temperature": point1.temperature, "enthalpy": point1.enthalpy, "entropy": point1.entropy, "quality": point1.quality }, { "point_id": "2", "description": "Compressor Discharge", "pressure": point2.pressure, "temperature": point2.temperature, "enthalpy": point2.enthalpy, "entropy": point2.entropy, "quality": point2.quality }, { "point_id": "3", "description": "Condenser Outlet", "pressure": point3.pressure, "temperature": point3.temperature, "enthalpy": point3.enthalpy, "entropy": point3.entropy, "quality": point3.quality }, { "point_id": "4", "description": "Expansion Valve Outlet", "pressure": point4.pressure, "temperature": point4.temperature, "enthalpy": point4.enthalpy, "entropy": point4.entropy, "quality": point4.quality } ], "performance": { "cop": cop, "cooling_capacity": cooling_capacity, "heating_capacity": heating_capacity, "compressor_power": compressor_power, "compressor_efficiency": compressor_efficiency, "mass_flow": mass_flow, "volumetric_flow": volumetric_flow, "compression_ratio": compression_ratio, "discharge_temperature": point2.temperature }, "diagram_data": { "cycle_points": [ {"enthalpy": point1.enthalpy, "pressure": point1.pressure}, {"enthalpy": point2.enthalpy, "pressure": point2.pressure}, {"enthalpy": point3.enthalpy, "pressure": point3.pressure}, {"enthalpy": point4.enthalpy, "pressure": point4.pressure}, {"enthalpy": point1.enthalpy, "pressure": point1.pressure} # Fermer le cycle ] } } # Force reload 2025-10-18 23:04:14