ci: commit workspace changes from notebook and backend fixes (excludes test_env, Frontend)
This commit is contained in:
1
app/services/__init__.py
Normal file
1
app/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Business logic services"""
|
||||
364
app/services/cycle_calculator.py
Normal file
364
app/services/cycle_calculator.py
Normal file
@@ -0,0 +1,364 @@
|
||||
"""
|
||||
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
|
||||
354
app/services/cycle_calculator.py.backup
Normal file
354
app/services/cycle_calculator.py.backup
Normal file
@@ -0,0 +1,354 @@
|
||||
"""
|
||||
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
|
||||
]
|
||||
}
|
||||
}
|
||||
354
app/services/cycle_calculator.py.temp
Normal file
354
app/services/cycle_calculator.py.temp
Normal file
@@ -0,0 +1,354 @@
|
||||
"""
|
||||
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
|
||||
]
|
||||
}
|
||||
}
|
||||
354
app/services/cycle_calculator_clean.py
Normal file
354
app/services/cycle_calculator_clean.py
Normal file
@@ -0,0 +1,354 @@
|
||||
"""
|
||||
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
|
||||
]
|
||||
}
|
||||
}
|
||||
319
app/services/diagram_generator.py
Normal file
319
app/services/diagram_generator.py
Normal file
@@ -0,0 +1,319 @@
|
||||
"""
|
||||
Service de génération de diagrammes Pression-Enthalpie (PH).
|
||||
|
||||
Basé sur le code original diagram_PH.py qui fonctionne correctement.
|
||||
"""
|
||||
|
||||
import io
|
||||
import base64
|
||||
import numpy as np
|
||||
import matplotlib
|
||||
# Configurer le backend Agg (non-interactif) pour éviter les problèmes sur Windows
|
||||
matplotlib.use('Agg')
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.figure import Figure
|
||||
from typing import Dict, List, Tuple, Optional, Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.core.refrigerant_loader import RefrigerantLibrary
|
||||
|
||||
# Cache pour éviter de recalculer les courbes lourdes par réfrigérant
|
||||
_diagram_cache: Dict[str, Any] = {}
|
||||
|
||||
|
||||
@dataclass
|
||||
class DiagramPoint:
|
||||
"""Point dans le diagramme PH."""
|
||||
pressure: float # bar
|
||||
enthalpy: float # kJ/kg
|
||||
temperature: Optional[float] = None # Celsius
|
||||
entropy: Optional[float] = None # kJ/kg.K
|
||||
quality: Optional[float] = None # 0-1
|
||||
|
||||
|
||||
class DiagramGenerator:
|
||||
"""Générateur de diagrammes Pression-Enthalpie."""
|
||||
|
||||
def __init__(self, refrigerant: RefrigerantLibrary):
|
||||
"""
|
||||
Initialise le générateur.
|
||||
|
||||
Args:
|
||||
refrigerant: Bibliothèque du réfrigérant
|
||||
"""
|
||||
self.refrigerant = refrigerant
|
||||
self.refrig_name = refrigerant.refrig_name
|
||||
self.fig_width = 15
|
||||
self.fig_height = 10
|
||||
self.dpi = 100
|
||||
|
||||
# Utiliser le cache partagé pour éviter des recalculs coûteux
|
||||
cached = _diagram_cache.get(self.refrig_name)
|
||||
if cached and isinstance(cached, tuple) and len(cached) == 9:
|
||||
(
|
||||
self.Hsl,
|
||||
self.Hsv,
|
||||
self.Psat,
|
||||
self.Tsat,
|
||||
self.Tmax,
|
||||
self.Tmin,
|
||||
self.T_lst,
|
||||
self.P,
|
||||
self.IsoT_lst,
|
||||
) = cached
|
||||
else:
|
||||
# Calculer et stocker dans le cache
|
||||
Hsl, Hsv, Psat, Tsat = self.get_psat_values()
|
||||
# Assign Tsat early because get_IsoT_values relies on self.Tsat
|
||||
self.Hsl, self.Hsv, self.Psat, self.Tsat = Hsl, Hsv, Psat, Tsat
|
||||
Tmax, Tmin, T_lst, P, IsoT_lst = self.get_IsoT_values()
|
||||
self.Tmax, self.Tmin, self.T_lst, self.P, self.IsoT_lst = Tmax, Tmin, T_lst, P, IsoT_lst
|
||||
|
||||
_diagram_cache[self.refrig_name] = (
|
||||
self.Hsl,
|
||||
self.Hsv,
|
||||
self.Psat,
|
||||
self.Tsat,
|
||||
self.Tmax,
|
||||
self.Tmin,
|
||||
self.T_lst,
|
||||
self.P,
|
||||
self.IsoT_lst,
|
||||
)
|
||||
|
||||
def get_psat_values(self) -> Tuple[List[float], List[float], List[float], List[float]]:
|
||||
"""
|
||||
Calcule les valeurs de saturation (courbe en cloche).
|
||||
COPIE EXACTE de diagram_PH.py lignes 39-63
|
||||
|
||||
Returns:
|
||||
Tuple (Hsl, Hsv, Psat, Tsat)
|
||||
"""
|
||||
Hsl, Hsv, Psat, Tsat = [], [], [], []
|
||||
|
||||
# COPIE EXACTE ligne 49 du code original
|
||||
for p in np.arange(self.refrigerant.p_begin(), self.refrigerant.p_end(), 0.5e5):
|
||||
# Lignes 51-57 du code original
|
||||
Hsl.append(self.refrigerant.hsl_px(p, 0) / 1e3)
|
||||
Hsv.append(self.refrigerant.hsv_px(p, 1) / 1e3)
|
||||
# Store Psat in Pa (internal SI), convert to bar only when displaying/exporting
|
||||
Psat.append(p)
|
||||
Tsat.append(self.refrigerant.T_px(p, 0.5))
|
||||
|
||||
# Lignes 60-61 du code original
|
||||
if len(Hsl) > 2 and Hsl[-1] == Hsl[-2]:
|
||||
break
|
||||
|
||||
return Hsl, Hsv, Psat, Tsat
|
||||
|
||||
def find_whole_10_numbers(self, Tmin: float, Tmax: float) -> np.ndarray:
|
||||
"""
|
||||
Trouve les températures rondes (multiples de 10) dans la plage.
|
||||
COPIE EXACTE de refDLL.py lignes 131-133
|
||||
|
||||
Args:
|
||||
Tmin: Température minimale (°C)
|
||||
Tmax: Température maximale (°C)
|
||||
|
||||
Returns:
|
||||
Array des températures
|
||||
"""
|
||||
# COPIE EXACTE lignes 131-133 de refDLL.py
|
||||
start = int(Tmin // 10 + 1)
|
||||
end = int(Tmax // 10 + 1)
|
||||
return np.arange(start * 10, end * 10, 10)
|
||||
|
||||
def get_IsoT_values(self) -> Tuple[float, float, np.ndarray, np.ndarray, List[List[float]]]:
|
||||
"""
|
||||
Calcule les valeurs isothermes.
|
||||
COPIE EXACTE de diagram_PH.py lignes 129-162
|
||||
|
||||
Returns:
|
||||
Tuple (Tmax, Tmin, T_lst, P, IsoT_lst)
|
||||
"""
|
||||
# COPIE EXACTE ligne 138 du code original
|
||||
# T = [self.callref.refrig.T_px(p, 0.5) - 273.15 for p in np.arange(self.callref.refrig.p_begin(), self.callref.refrig.p_end(), 50e5)]
|
||||
|
||||
# Lignes 141 du code original
|
||||
Tmax = max(self.Tsat) - 273.15 - 1
|
||||
Tmin = min(self.Tsat) - 273.15
|
||||
|
||||
# Ligne 144 du code original
|
||||
T_lst = self.find_whole_10_numbers(Tmin, Tmax)
|
||||
|
||||
# Ligne 147 du code original
|
||||
P = np.arange(self.refrigerant.p_begin(), self.refrigerant.p_end(), 0.05e5)
|
||||
|
||||
# Ligne 150 du code original
|
||||
IsoT_lst = [[self.refrigerant.h_pT(p, temp + 273.15) / 1e3 for p in P] for temp in T_lst]
|
||||
|
||||
return Tmax, Tmin, T_lst, P, IsoT_lst
|
||||
|
||||
def plot_diagram(
|
||||
self,
|
||||
cycle_points: Optional[List[Tuple[float, float]]] = None,
|
||||
title: Optional[str] = None
|
||||
) -> Figure:
|
||||
"""
|
||||
Génère le diagramme PH complet.
|
||||
COPIE EXACTE de diagram_PH.py lignes 183-224
|
||||
|
||||
Args:
|
||||
cycle_points: Points du cycle [(h, p), ...]
|
||||
title: Titre du diagramme
|
||||
|
||||
Returns:
|
||||
Figure matplotlib
|
||||
"""
|
||||
# Configuration des tailles de police - COPIE EXACTE lignes 184-190
|
||||
SMALL_SIZE = 10
|
||||
MEDIUM_SIZE = 22
|
||||
BIGGER_SIZE = 28
|
||||
|
||||
plt.rc('font', size=SMALL_SIZE)
|
||||
plt.rc('axes', titlesize=SMALL_SIZE)
|
||||
plt.rc('axes', labelsize=MEDIUM_SIZE)
|
||||
plt.rc('xtick', labelsize=SMALL_SIZE)
|
||||
plt.rc('ytick', labelsize=SMALL_SIZE)
|
||||
plt.rc('legend', fontsize=SMALL_SIZE)
|
||||
plt.rc('figure', titlesize=BIGGER_SIZE)
|
||||
|
||||
# Ligne 191 du code original - taille configurable
|
||||
fig = Figure(figsize=[self.fig_width, self.fig_height])
|
||||
ax = fig.add_subplot(111)
|
||||
|
||||
# Lignes 193-194 du code original: Plot saturation lines
|
||||
# Psat is stored in Pa internally; plot in bar for readability
|
||||
ax.plot(self.Hsl, [p / 1e5 for p in self.Psat], 'k-', label='Liquid Saturation')
|
||||
ax.plot(self.Hsv, [p / 1e5 for p in self.Psat], 'k-', label='Vapor Saturation')
|
||||
|
||||
# Lignes 196-202 du code original: Plot isotherms
|
||||
for Th_lst, temp in zip(self.IsoT_lst, self.T_lst):
|
||||
ax.plot(Th_lst, self.P / 1e5, 'g--', label=f'{temp}°C Isotherm', alpha=0.5)
|
||||
ax.annotate('{:.0f}°C'.format(temp),
|
||||
(self.refrigerant.h_px(self.refrigerant.p_Tx(temp + 273.15, 0.5), 0.1) / 1e3,
|
||||
self.refrigerant.p_Tx(temp + 273.15, 0.5) / 1e5),
|
||||
ha='center',
|
||||
backgroundcolor="white")
|
||||
|
||||
# Ligne 204 du code original
|
||||
ax.set_yscale('log')
|
||||
|
||||
# Tracer les points du cycle si fournis (adapté pour l'API)
|
||||
if cycle_points and len(cycle_points) > 0:
|
||||
h_cycle = [p[0] for p in cycle_points]
|
||||
p_cycle = [p[1] for p in cycle_points]
|
||||
ax.plot(h_cycle, p_cycle, 'r-o', linewidth=2, markersize=8,
|
||||
label='Cycle', zorder=10)
|
||||
|
||||
# Lignes 218-221 du code original
|
||||
ax.set_xlabel('Enthalpy [kJ/kg]')
|
||||
ax.set_ylabel('Pressure [bar]')
|
||||
if title:
|
||||
ax.set_title(title)
|
||||
else:
|
||||
ax.set_title(f'PH Diagram for {self.refrig_name}')
|
||||
ax.grid(True, which='both', linestyle='--')
|
||||
|
||||
# Ligne 223 du code original
|
||||
ax.legend(loc='best', fontsize=SMALL_SIZE)
|
||||
|
||||
# Ligne 224 du code original
|
||||
fig.tight_layout()
|
||||
return fig
|
||||
|
||||
def export_to_base64(self, fig: Figure) -> str:
|
||||
"""
|
||||
Exporte la figure en PNG base64.
|
||||
|
||||
Args:
|
||||
fig: Figure matplotlib
|
||||
|
||||
Returns:
|
||||
String base64 de l'image PNG
|
||||
"""
|
||||
buf = io.BytesIO()
|
||||
# NE PAS utiliser bbox_inches='tight' car ça peut tronquer le graphique
|
||||
# Utiliser pad_inches pour ajouter une marge
|
||||
fig.savefig(buf, format='png', dpi=self.dpi, bbox_inches='tight', pad_inches=0.2)
|
||||
buf.seek(0)
|
||||
img_base64 = base64.b64encode(buf.read()).decode('utf-8')
|
||||
buf.close()
|
||||
return img_base64
|
||||
|
||||
def generate_diagram_data(
|
||||
self,
|
||||
cycle_points: Optional[List[Tuple[float, float]]] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Génère les données du diagramme en format JSON.
|
||||
Utilise les données déjà calculées à l'initialisation.
|
||||
|
||||
Args:
|
||||
cycle_points: Points du cycle
|
||||
|
||||
Returns:
|
||||
Dictionnaire avec les données du diagramme
|
||||
"""
|
||||
data = {
|
||||
"refrigerant": self.refrig_name,
|
||||
"saturation_curve": [
|
||||
{"enthalpy": float(h), "pressure": float(p / 1e5)}
|
||||
for h, p in zip(self.Hsl + self.Hsv, self.Psat + self.Psat)
|
||||
]
|
||||
}
|
||||
|
||||
# Ajouter isothermes
|
||||
isotherms_data = []
|
||||
for Th_lst, temp in zip(self.IsoT_lst, self.T_lst):
|
||||
points = []
|
||||
for h, p in zip(Th_lst, self.P / 1e5):
|
||||
if h is not None and not np.isnan(h):
|
||||
points.append({"enthalpy": float(h), "pressure": float(p)})
|
||||
|
||||
if len(points) > 0:
|
||||
isotherms_data.append({
|
||||
"temperature": float(temp),
|
||||
"points": points
|
||||
})
|
||||
|
||||
data["isotherms"] = isotherms_data
|
||||
|
||||
# Ajouter points du cycle
|
||||
if cycle_points:
|
||||
data["cycle_points"] = [
|
||||
{"enthalpy": float(h), "pressure": float(p)}
|
||||
for h, p in cycle_points
|
||||
]
|
||||
|
||||
return data
|
||||
|
||||
def generate_complete_diagram(
|
||||
self,
|
||||
cycle_points: Optional[List[Tuple[float, float]]] = None,
|
||||
title: Optional[str] = None,
|
||||
export_format: str = "both"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Génère le diagramme complet.
|
||||
Utilise les données déjà calculées à l'initialisation.
|
||||
|
||||
Args:
|
||||
cycle_points: Points du cycle
|
||||
title: Titre
|
||||
export_format: "png", "json", "both"
|
||||
|
||||
Returns:
|
||||
Dictionnaire avec image et/ou données
|
||||
"""
|
||||
result = {}
|
||||
|
||||
if export_format in ["png", "both"]:
|
||||
fig = self.plot_diagram(cycle_points, title)
|
||||
result["image_base64"] = self.export_to_base64(fig)
|
||||
plt.close(fig)
|
||||
|
||||
if export_format in ["json", "both"]:
|
||||
result["data"] = self.generate_diagram_data(cycle_points)
|
||||
|
||||
return result
|
||||
251
app/services/thermodynamics.py
Normal file
251
app/services/thermodynamics.py
Normal file
@@ -0,0 +1,251 @@
|
||||
"""
|
||||
Service pour les calculs thermodynamiques
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional, Any
|
||||
from app.core.refrigerant_loader import get_refrigerant
|
||||
|
||||
|
||||
class ThermodynamicsService:
|
||||
"""Service pour effectuer les calculs thermodynamiques"""
|
||||
|
||||
def calculate_from_px(
|
||||
self,
|
||||
refrigerant: str,
|
||||
pressure: float,
|
||||
quality: float
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Calcule les proprietes thermodynamiques a partir de P et x
|
||||
|
||||
Args:
|
||||
refrigerant: Nom du refrigerant (ex: "R134a")
|
||||
pressure: Pression en Pa
|
||||
quality: Qualite (0-1)
|
||||
|
||||
Returns:
|
||||
Dictionnaire avec toutes les proprietes
|
||||
"""
|
||||
lib = get_refrigerant(refrigerant)
|
||||
|
||||
# Calculs des proprietes principales
|
||||
temperature = lib.T_px(pressure, quality)
|
||||
enthalpy = lib.h_px(pressure, quality)
|
||||
entropy = lib.s_px(pressure, quality)
|
||||
density = lib.rho_px(pressure, quality)
|
||||
temp_sat = lib.Ts_px(pressure, quality)
|
||||
|
||||
# Proprietes de saturation
|
||||
enthalpy_liquid = lib.hsl_px(pressure, 0)
|
||||
enthalpy_vapor = lib.hsv_px(pressure, 1)
|
||||
density_liquid = lib.rhosl_px(pressure, 0)
|
||||
density_vapor = lib.rhosv_px(pressure, 1)
|
||||
|
||||
return {
|
||||
"refrigerant": refrigerant,
|
||||
"inputs": {
|
||||
"pressure": pressure,
|
||||
"pressure_bar": pressure / 1e5,
|
||||
"quality": quality
|
||||
},
|
||||
"properties": {
|
||||
"temperature": temperature,
|
||||
"temperature_celsius": temperature - 273.15,
|
||||
"enthalpy": enthalpy,
|
||||
"enthalpy_kj_kg": enthalpy / 1000,
|
||||
"entropy": entropy,
|
||||
"entropy_kj_kgK": entropy / 1000,
|
||||
"density": density,
|
||||
"specific_volume": 1 / density if density > 0 else None,
|
||||
"quality": quality
|
||||
},
|
||||
"saturation": {
|
||||
"temperature": temp_sat,
|
||||
"temperature_celsius": temp_sat - 273.15,
|
||||
"enthalpy_liquid": enthalpy_liquid,
|
||||
"enthalpy_liquid_kj_kg": enthalpy_liquid / 1000,
|
||||
"enthalpy_vapor": enthalpy_vapor,
|
||||
"enthalpy_vapor_kj_kg": enthalpy_vapor / 1000,
|
||||
"density_liquid": density_liquid,
|
||||
"density_vapor": density_vapor,
|
||||
"latent_heat": enthalpy_vapor - enthalpy_liquid,
|
||||
"latent_heat_kj_kg": (enthalpy_vapor - enthalpy_liquid) / 1000
|
||||
}
|
||||
}
|
||||
|
||||
def calculate_from_pT(
|
||||
self,
|
||||
refrigerant: str,
|
||||
pressure: float,
|
||||
temperature: float
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Calcule les proprietes a partir de P et T
|
||||
|
||||
Args:
|
||||
refrigerant: Nom du refrigerant
|
||||
pressure: Pression en Pa
|
||||
temperature: Temperature en K
|
||||
|
||||
Returns:
|
||||
Dictionnaire avec les proprietes
|
||||
"""
|
||||
lib = get_refrigerant(refrigerant)
|
||||
|
||||
enthalpy = lib.h_pT(pressure, temperature)
|
||||
|
||||
# Determiner la qualite approximativement
|
||||
temp_sat = lib.Ts_px(pressure, 0.5)
|
||||
|
||||
# Si proche de la saturation, calculer la qualite
|
||||
quality = None
|
||||
if abs(temperature - temp_sat) < 0.1:
|
||||
# En zone diphasique
|
||||
h_liquid = lib.hsl_px(pressure, 0)
|
||||
h_vapor = lib.hsv_px(pressure, 1)
|
||||
if h_vapor > h_liquid:
|
||||
quality = (enthalpy - h_liquid) / (h_vapor - h_liquid)
|
||||
quality = max(0, min(1, quality))
|
||||
|
||||
# Si qualite determinee, utiliser px pour avoir toutes les proprietes
|
||||
if quality is not None:
|
||||
return self.calculate_from_px(refrigerant, pressure, quality)
|
||||
|
||||
# Sinon, calculer les proprietes de base (vapeur surchauffee ou liquide sous-refroidi)
|
||||
# Approximation: utiliser x=1 pour vapeur ou x=0 pour liquide
|
||||
if temperature > temp_sat:
|
||||
# Vapeur surchauffee, utiliser x=1 comme approximation
|
||||
quality_approx = 1.0
|
||||
else:
|
||||
# Liquide sous-refroidi, utiliser x=0 comme approximation
|
||||
quality_approx = 0.0
|
||||
|
||||
result = self.calculate_from_px(refrigerant, pressure, quality_approx)
|
||||
result["properties"]["temperature"] = temperature
|
||||
result["properties"]["temperature_celsius"] = temperature - 273.15
|
||||
result["properties"]["enthalpy"] = enthalpy
|
||||
result["properties"]["enthalpy_kj_kg"] = enthalpy / 1000
|
||||
result["properties"]["quality"] = quality
|
||||
result["inputs"]["temperature"] = temperature
|
||||
result["inputs"]["temperature_celsius"] = temperature - 273.15
|
||||
result["note"] = "Calcul a partir de P et T - proprietes approximatives hors saturation"
|
||||
|
||||
return result
|
||||
|
||||
def calculate_from_ph(
|
||||
self,
|
||||
refrigerant: str,
|
||||
pressure: float,
|
||||
enthalpy: float
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Calcule les proprietes a partir de P et h
|
||||
|
||||
Args:
|
||||
refrigerant: Nom du refrigerant
|
||||
pressure: Pression en Pa
|
||||
enthalpy: Enthalpie en J/kg
|
||||
|
||||
Returns:
|
||||
Dictionnaire avec les proprietes
|
||||
"""
|
||||
lib = get_refrigerant(refrigerant)
|
||||
|
||||
# Calculer la qualite
|
||||
quality = lib.x_ph(pressure, enthalpy)
|
||||
|
||||
# Utiliser la qualite pour calculer le reste
|
||||
return self.calculate_from_px(refrigerant, pressure, quality)
|
||||
|
||||
def calculate_from_Tx(
|
||||
self,
|
||||
refrigerant: str,
|
||||
temperature: float,
|
||||
quality: float
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Calcule les proprietes a partir de T et x
|
||||
|
||||
Args:
|
||||
refrigerant: Nom du refrigerant
|
||||
temperature: Temperature en K
|
||||
quality: Qualite (0-1)
|
||||
|
||||
Returns:
|
||||
Dictionnaire avec les proprietes
|
||||
"""
|
||||
lib = get_refrigerant(refrigerant)
|
||||
|
||||
# Calculer la pression
|
||||
pressure = lib.p_Tx(temperature, quality)
|
||||
|
||||
# Utiliser la pression pour calculer le reste
|
||||
return self.calculate_from_px(refrigerant, pressure, quality)
|
||||
|
||||
def get_saturation_properties(
|
||||
self,
|
||||
refrigerant: str,
|
||||
pressure: float
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Obtient les proprietes de saturation a une pression donnee
|
||||
|
||||
Args:
|
||||
refrigerant: Nom du refrigerant
|
||||
pressure: Pression en Pa
|
||||
|
||||
Returns:
|
||||
Proprietes de saturation (liquide et vapeur)
|
||||
"""
|
||||
lib = get_refrigerant(refrigerant)
|
||||
|
||||
# Temperature de saturation
|
||||
temp_sat = lib.Ts_px(pressure, 0.5)
|
||||
|
||||
# Proprietes liquide (x=0)
|
||||
h_liquid = lib.hsl_px(pressure, 0)
|
||||
rho_liquid = lib.rhosl_px(pressure, 0)
|
||||
s_liquid = lib.s_px(pressure, 0)
|
||||
|
||||
# Proprietes vapeur (x=1)
|
||||
h_vapor = lib.hsv_px(pressure, 1)
|
||||
rho_vapor = lib.rhosv_px(pressure, 1)
|
||||
s_vapor = lib.s_px(pressure, 1)
|
||||
|
||||
return {
|
||||
"refrigerant": refrigerant,
|
||||
"pressure": pressure,
|
||||
"pressure_bar": pressure / 1e5,
|
||||
"temperature_saturation": temp_sat,
|
||||
"temperature_saturation_celsius": temp_sat - 273.15,
|
||||
"liquid": {
|
||||
"enthalpy": h_liquid,
|
||||
"enthalpy_kj_kg": h_liquid / 1000,
|
||||
"density": rho_liquid,
|
||||
"specific_volume": 1 / rho_liquid if rho_liquid > 0 else None,
|
||||
"entropy": s_liquid,
|
||||
"entropy_kj_kgK": s_liquid / 1000
|
||||
},
|
||||
"vapor": {
|
||||
"enthalpy": h_vapor,
|
||||
"enthalpy_kj_kg": h_vapor / 1000,
|
||||
"density": rho_vapor,
|
||||
"specific_volume": 1 / rho_vapor if rho_vapor > 0 else None,
|
||||
"entropy": s_vapor,
|
||||
"entropy_kj_kgK": s_vapor / 1000
|
||||
},
|
||||
"latent_heat": h_vapor - h_liquid,
|
||||
"latent_heat_kj_kg": (h_vapor - h_liquid) / 1000
|
||||
}
|
||||
|
||||
|
||||
# Instance globale du service
|
||||
_service: Optional[ThermodynamicsService] = None
|
||||
|
||||
|
||||
def get_thermodynamics_service() -> ThermodynamicsService:
|
||||
"""Obtient l'instance du service thermodynamique"""
|
||||
global _service
|
||||
if _service is None:
|
||||
_service = ThermodynamicsService()
|
||||
return _service
|
||||
Reference in New Issue
Block a user