355 lines
12 KiB
Python
355 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 # bar
|
||
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 __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 (bar)
|
||
\"\"\"
|
||
temperature_kelvin = temperature_celsius + 273.15
|
||
pressure_pa = self.refrigerant.p_Tx(temperature_kelvin, quality)
|
||
# p_Tx retourne des Pascals, convertir en bar
|
||
pressure_bar = pressure_pa / 1e5
|
||
return pressure_bar
|
||
|
||
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 (bar)
|
||
quality: Titre vapeur (0-1)
|
||
|
||
Returns:
|
||
État thermodynamique complet
|
||
\"\"\"
|
||
# RefrigerantLibrary prend bar directement
|
||
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, # bar
|
||
temperature=T_K - 273.15, # °C
|
||
enthalpy=h_J / 1000, # kJ/kg
|
||
entropy=s_J / 1000, # 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 (bar)
|
||
enthalpy: Enthalpie (kJ/kg)
|
||
|
||
Returns:
|
||
État thermodynamique complet
|
||
\"\"\"
|
||
# RefrigerantLibrary prend bar et 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, # bar
|
||
temperature=T_K - 273.15, # °C
|
||
enthalpy=enthalpy, # kJ/kg
|
||
entropy=s_J / 1000, # 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 (bar)
|
||
superheat: Surchauffe (°C)
|
||
|
||
Returns:
|
||
État thermodynamique
|
||
\"\"\"
|
||
# RefrigerantLibrary prend bar directement
|
||
# Température de saturation
|
||
T_sat_K = self.refrigerant.T_px(pressure, 1.0)
|
||
T_K = T_sat_K + 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)
|
||
|
||
def calculate_subcool_point(
|
||
self,
|
||
pressure: float,
|
||
subcool: float
|
||
) -> ThermodynamicState:
|
||
\"\"\"
|
||
Calcule un point avec sous-refroidissement.
|
||
|
||
Args:
|
||
pressure: Pression (bar)
|
||
subcool: Sous-refroidissement (°C)
|
||
|
||
Returns:
|
||
État thermodynamique
|
||
\"\"\"
|
||
# RefrigerantLibrary prend bar directement
|
||
# Température de saturation
|
||
T_sat_K = self.refrigerant.T_px(pressure, 0.0)
|
||
T_K = T_sat_K - 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)
|
||
|
||
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 (bar)
|
||
h_in: Enthalpie entrée (kJ/kg)
|
||
p_out: Pression sortie (bar)
|
||
|
||
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
|
||
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 (bar)
|
||
cond_pressure: Pression condensation (bar)
|
||
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 = point1.enthalpy + (point2s.enthalpy - 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 = point1.enthalpy - point4.enthalpy # kJ/kg
|
||
w_comp = point2.enthalpy - point1.enthalpy # kJ/kg
|
||
q_cond = point2.enthalpy - 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
|
||
]
|
||
}
|
||
}
|