diagram_ph/app/models/cycle.py

223 lines
7.4 KiB
Python

"""
Modèles Pydantic pour les calculs de cycles frigorifiques.
"""
from typing import Optional, List, Dict, Any
from pydantic import BaseModel, Field, field_validator
class CyclePoint(BaseModel):
"""Point d'un cycle frigorifique."""
point_id: str = Field(
...,
description="Identifiant du point (1, 2, 3, 4, etc.)",
examples=["1", "2", "3", "4"]
)
pressure: float = Field(
...,
description="Pression (bar)",
gt=0
)
temperature: Optional[float] = Field(
None,
description="Température (°C)"
)
enthalpy: Optional[float] = Field(
None,
description="Enthalpie (kJ/kg)"
)
entropy: Optional[float] = Field(
None,
description="Entropie (kJ/kg.K)"
)
quality: Optional[float] = Field(
None,
description="Titre vapeur (0-1)",
ge=0,
le=1
)
description: Optional[str] = Field(
None,
description="Description du point"
)
class SimpleCycleRequest(BaseModel):
"""
Requête pour calcul de cycle frigorifique simple (4 points).
Vous pouvez spécifier soit les pressions, soit les températures de saturation :
- evap_pressure OU evap_temperature (température d'évaporation)
- cond_pressure OU cond_temperature (température de condensation)
"""
refrigerant: str = Field(
...,
description="Code du réfrigérant",
examples=["R134a", "R410A"]
)
# Évaporation : Pression OU Température
evap_pressure: Optional[float] = Field(
None,
description="Pression d'évaporation (bar). Utiliser soit evap_pressure, soit evap_temperature",
gt=0,
examples=[2.9]
)
evap_temperature: Optional[float] = Field(
None,
description="Température d'évaporation (°C). Utiliser soit evap_pressure, soit evap_temperature",
examples=[-10.0]
)
# Condensation : Pression OU Température
cond_pressure: Optional[float] = Field(
None,
description="Pression de condensation (bar). Utiliser soit cond_pressure, soit cond_temperature",
gt=0,
examples=[12.0]
)
cond_temperature: Optional[float] = Field(
None,
description="Température de condensation (°C). Utiliser soit cond_pressure, soit cond_temperature",
examples=[40.0]
)
superheat: float = Field(
5.0,
description="Surchauffe à l'évaporateur (°C)",
ge=0,
examples=[5.0]
)
subcool: float = Field(
3.0,
description="Sous-refroidissement au condenseur (°C)",
ge=0,
examples=[3.0]
)
compressor_efficiency: Optional[float] = Field(
None,
description="Rendement isentropique du compresseur (0-1). Si non fourni, calculé automatiquement depuis le rapport de pression",
gt=0,
le=1,
examples=[0.70, 0.85]
)
mass_flow: float = Field(
0.1,
description="Débit massique (kg/s)",
gt=0,
examples=[0.1]
)
@field_validator('evap_temperature')
@classmethod
def validate_evap_input(cls, v, info):
"""Valide qu'on a soit evap_pressure, soit evap_temperature (mais pas les deux)."""
evap_pressure = info.data.get('evap_pressure')
if evap_pressure is None and v is None:
raise ValueError('Vous devez fournir soit evap_pressure, soit evap_temperature')
if evap_pressure is not None and v is not None:
raise ValueError('Fournissez soit evap_pressure, soit evap_temperature (pas les deux)')
return v
@field_validator('cond_temperature')
@classmethod
def validate_cond_input(cls, v, info):
"""Valide qu'on a soit cond_pressure, soit cond_temperature (mais pas les deux)."""
cond_pressure = info.data.get('cond_pressure')
if cond_pressure is None and v is None:
raise ValueError('Vous devez fournir soit cond_pressure, soit cond_temperature')
if cond_pressure is not None and v is not None:
raise ValueError('Fournissez soit cond_pressure, soit cond_temperature (pas les deux)')
return v
class CyclePerformance(BaseModel):
"""Performances calculées du cycle."""
cop: float = Field(..., description="Coefficient de performance")
cooling_capacity: float = Field(..., description="Puissance frigorifique (kW)")
heating_capacity: float = Field(..., description="Puissance calorifique (kW)")
compressor_power: float = Field(..., description="Puissance compresseur (kW)")
compressor_efficiency: float = Field(..., description="Rendement isentropique du compresseur")
mass_flow: float = Field(..., description="Débit massique (kg/s)")
volumetric_flow: Optional[float] = Field(None, description="Débit volumique (m³/h)")
compression_ratio: float = Field(..., description="Taux de compression")
discharge_temperature: float = Field(..., description="Température refoulement (°C)")
class SimpleCycleResponse(BaseModel):
"""Réponse complète pour un cycle frigorifique simple."""
success: bool = Field(True, description="Succès de l'opération")
refrigerant: str = Field(..., description="Réfrigérant utilisé")
points: List[CyclePoint] = Field(
...,
description="Points du cycle (1: sortie évap, 2: refoulement, 3: sortie cond, 4: aspiration)"
)
performance: CyclePerformance = Field(
...,
description="Performances du cycle"
)
diagram_data: Optional[Dict[str, Any]] = Field(
None,
description="Données pour tracer le cycle sur diagramme PH"
)
message: Optional[str] = Field(
None,
description="Message informatif"
)
class EconomizerCycleRequest(BaseModel):
"""Requête pour cycle avec économiseur."""
refrigerant: str = Field(..., description="Code du réfrigérant")
evap_pressure: float = Field(..., description="Pression évaporation (bar)", gt=0)
intermediate_pressure: float = Field(..., description="Pression intermédiaire (bar)", gt=0)
cond_pressure: float = Field(..., description="Pression condensation (bar)", gt=0)
superheat: float = Field(5.0, description="Surchauffe (°C)", ge=0)
subcool: float = Field(3.0, description="Sous-refroidissement (°C)", ge=0)
compressor_efficiency: float = Field(0.70, description="Rendement compresseur", gt=0, le=1)
mass_flow: float = Field(0.1, description="Débit massique total (kg/s)", gt=0)
@field_validator('intermediate_pressure')
@classmethod
def validate_intermediate_pressure(cls, v, info):
"""Valide P_evap < P_inter < P_cond."""
if 'evap_pressure' in info.data and v <= info.data['evap_pressure']:
raise ValueError('intermediate_pressure doit être > evap_pressure')
if 'cond_pressure' in info.data and v >= info.data['cond_pressure']:
raise ValueError('intermediate_pressure doit être < cond_pressure')
return v
class CycleError(BaseModel):
"""Erreur lors du calcul de cycle."""
success: bool = Field(False, description="Échec de l'opération")
error: str = Field(..., description="Message d'erreur")
details: Optional[str] = Field(None, description="Détails supplémentaires")