ci: commit workspace changes from notebook and backend fixes (excludes test_env, Frontend)

This commit is contained in:
Repo Bot
2025-10-19 09:25:12 +02:00
commit 92e9b05393
80 changed files with 11653 additions and 0 deletions

1
app/services/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Business logic services"""

View 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

View 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
]
}
}

View 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
]
}
}

View 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
]
}
}

View 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

View 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