""" 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")