365 lines
12 KiB
Python
365 lines
12 KiB
Python
"""
|
||
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
|