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/models/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""Pydantic models for requests and responses"""

223
app/models/cycle.py Normal file
View File

@@ -0,0 +1,223 @@
"""
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")

188
app/models/diagram.py Normal file
View File

@@ -0,0 +1,188 @@
"""
Modèles Pydantic pour les diagrammes PH.
"""
from typing import Optional, List, Dict, Any
from pydantic import BaseModel, Field, field_validator
class DiagramPointRequest(BaseModel):
"""Point personnalisé à tracer sur le diagramme."""
pressure: float = Field(
...,
description="Pression (bar)",
gt=0
)
enthalpy: float = Field(
...,
description="Enthalpie (kJ/kg)"
)
temperature: Optional[float] = Field(
None,
description="Température (Celsius) - optionnel"
)
entropy: Optional[float] = Field(
None,
description="Entropie (kJ/kg.K) - optionnel"
)
quality: Optional[float] = Field(
None,
description="Titre vapeur (0-1) - optionnel",
ge=0,
le=1
)
label: Optional[str] = Field(
None,
description="Label du point - optionnel"
)
class PressureRange(BaseModel):
"""Plage de pression."""
min: float = Field(..., gt=0, description="Pression minimale (bar)")
max: float = Field(..., gt=0, description="Pression maximale (bar)")
class EnthalpyRange(BaseModel):
"""Plage d'enthalpie."""
min: float = Field(..., description="Enthalpie minimale (kJ/kg)")
max: float = Field(..., description="Enthalpie maximale (kJ/kg)")
class DiagramRequest(BaseModel):
"""Requête pour générer un diagramme PH."""
refrigerant: str = Field(
...,
description="Code du réfrigérant (ex: R134a, R410A)",
examples=["R134a", "R410A", "R744"]
)
pressure_range: PressureRange = Field(
...,
description="Plage de pression du diagramme"
)
enthalpy_range: Optional[EnthalpyRange] = Field(
None,
description="Plage d'enthalpie - auto si non fourni"
)
include_isotherms: bool = Field(
True,
description="Inclure les isothermes"
)
isotherm_values: Optional[List[float]] = Field(
None,
description="Températures isothermes spécifiques (Celsius)"
)
cycle_points: Optional[List[Dict[str, float]]] = Field(
None,
description="Points du cycle [(enthalpy, pressure), ...]"
)
title: Optional[str] = Field(
None,
description="Titre personnalisé du diagramme"
)
format: str = Field(
"both",
description="Format: 'png', 'json', ou 'both'",
pattern="^(png|json|both)$"
)
width: int = Field(1400, gt=0, description="Largeur image (pixels)")
height: int = Field(900, gt=0, description="Hauteur image (pixels)")
dpi: int = Field(100, gt=0, description="DPI de l'image")
class SaturationPoint(BaseModel):
"""Point sur la courbe de saturation."""
enthalpy: float = Field(..., description="Enthalpie (kJ/kg)")
pressure: float = Field(..., description="Pression (bar)")
class IsothermCurve(BaseModel):
"""Courbe isotherme."""
temperature: float = Field(..., description="Température (Celsius)")
unit: str = Field("°C", description="Unité de température")
points: List[SaturationPoint] = Field(
...,
description="Points de la courbe"
)
class DiagramPointResponse(BaseModel):
"""Point personnalisé dans la réponse."""
enthalpy: float = Field(..., description="Enthalpie (kJ/kg)")
pressure: float = Field(..., description="Pression (bar)")
temperature: Optional[float] = Field(None, description="Température (Celsius)")
entropy: Optional[float] = Field(None, description="Entropie (kJ/kg.K)")
quality: Optional[float] = Field(None, description="Titre vapeur (0-1)")
class DiagramDataResponse(BaseModel):
"""Données JSON du diagramme."""
refrigerant: str = Field(..., description="Code du réfrigérant")
ranges: Dict[str, Optional[float]] = Field(
...,
description="Plages de valeurs du diagramme"
)
saturation_curve: Dict[str, List[SaturationPoint]] = Field(
...,
description="Courbe de saturation (liquide et vapeur)"
)
isotherms: Optional[List[IsothermCurve]] = Field(
None,
description="Courbes isothermes"
)
custom_points: Optional[List[DiagramPointResponse]] = Field(
None,
description="Points personnalisés"
)
class DiagramResponse(BaseModel):
"""Réponse complète de génération de diagramme."""
success: bool = Field(True, description="Succès")
image: Optional[str] = Field(
None,
description="Image PNG base64"
)
data: Optional[Dict[str, Any]] = Field(
None,
description="Données JSON"
)
metadata: Dict[str, Any] = Field(
default_factory=dict,
description="Métadonnées"
)
message: Optional[str] = Field(
None,
description="Message"
)
class DiagramError(BaseModel):
"""Erreur lors de la génération de diagramme."""
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")

201
app/models/properties.py Normal file
View File

@@ -0,0 +1,201 @@
"""
Modeles pour les calculs de proprietes thermodynamiques
"""
from typing import Optional, Literal
from pydantic import BaseModel, Field, field_validator
class PropertyCalculationRequest(BaseModel):
"""Requete pour calculer des proprietes thermodynamiques"""
refrigerant: str = Field(
...,
description="Nom du refrigerant",
example="R134a"
)
calculation_type: Literal["px", "pT", "ph", "Tx"] = Field(
...,
description="Type de calcul: px (pression-qualite), pT (pression-temperature), ph (pression-enthalpie), Tx (temperature-qualite)",
example="px"
)
# Parametres d'entree selon le type
pressure: Optional[float] = Field(
None,
description="Pression en Pa",
gt=0,
example=500000
)
temperature: Optional[float] = Field(
None,
description="Temperature en K",
gt=0,
example=280.0
)
quality: Optional[float] = Field(
None,
description="Qualite (titre) entre 0 et 1",
ge=0,
le=1,
example=0.5
)
enthalpy: Optional[float] = Field(
None,
description="Enthalpie en J/kg",
example=250000
)
@field_validator('pressure')
@classmethod
def validate_pressure(cls, v, info):
calc_type = info.data.get('calculation_type')
if calc_type in ['px', 'pT', 'ph'] and v is None:
raise ValueError(f"Pression requise pour le calcul {calc_type}")
return v
@field_validator('temperature')
@classmethod
def validate_temperature(cls, v, info):
calc_type = info.data.get('calculation_type')
if calc_type in ['pT', 'Tx'] and v is None:
raise ValueError(f"Temperature requise pour le calcul {calc_type}")
return v
@field_validator('quality')
@classmethod
def validate_quality(cls, v, info):
calc_type = info.data.get('calculation_type')
if calc_type in ['px', 'Tx'] and v is None:
raise ValueError(f"Qualite requise pour le calcul {calc_type}")
return v
@field_validator('enthalpy')
@classmethod
def validate_enthalpy(cls, v, info):
calc_type = info.data.get('calculation_type')
if calc_type == 'ph' and v is None:
raise ValueError("Enthalpie requise pour le calcul ph")
return v
class Config:
json_schema_extra = {
"examples": [
{
"refrigerant": "R134a",
"calculation_type": "px",
"pressure": 500000,
"quality": 0.5
},
{
"refrigerant": "R410A",
"calculation_type": "pT",
"pressure": 800000,
"temperature": 280.0
},
{
"refrigerant": "R744",
"calculation_type": "ph",
"pressure": 3000000,
"enthalpy": 400000
}
]
}
class SaturationRequest(BaseModel):
"""Requete pour obtenir les proprietes de saturation"""
refrigerant: str = Field(
...,
description="Nom du refrigerant",
example="R134a"
)
pressure: float = Field(
...,
description="Pression en Pa",
gt=0,
example=500000
)
class Config:
json_schema_extra = {
"example": {
"refrigerant": "R134a",
"pressure": 500000
}
}
class PropertyInputs(BaseModel):
"""Parametres d'entree du calcul"""
pressure: Optional[float] = Field(None, description="Pression (Pa)")
pressure_bar: Optional[float] = Field(None, description="Pression (bar)")
temperature: Optional[float] = Field(None, description="Temperature (K)")
temperature_celsius: Optional[float] = Field(None, description="Temperature (°C)")
quality: Optional[float] = Field(None, description="Qualite (0-1)")
enthalpy: Optional[float] = Field(None, description="Enthalpie (J/kg)")
class ThermodynamicPropertiesDetailed(BaseModel):
"""Proprietes thermodynamiques detaillees"""
temperature: float = Field(..., description="Temperature (K)")
temperature_celsius: float = Field(..., description="Temperature (°C)")
enthalpy: float = Field(..., description="Enthalpie (J/kg)")
enthalpy_kj_kg: float = Field(..., description="Enthalpie (kJ/kg)")
entropy: float = Field(..., description="Entropie (J/kg.K)")
entropy_kj_kgK: float = Field(..., description="Entropie (kJ/kg.K)")
density: float = Field(..., description="Masse volumique (kg/m³)")
specific_volume: Optional[float] = Field(None, description="Volume specifique (m³/kg)")
quality: Optional[float] = Field(None, description="Qualite (0-1)")
class SaturationPropertiesDetailed(BaseModel):
"""Proprietes de saturation detaillees"""
temperature: float = Field(..., description="Temperature de saturation (K)")
temperature_celsius: float = Field(..., description="Temperature de saturation (°C)")
enthalpy_liquid: float = Field(..., description="Enthalpie liquide (J/kg)")
enthalpy_liquid_kj_kg: float = Field(..., description="Enthalpie liquide (kJ/kg)")
enthalpy_vapor: float = Field(..., description="Enthalpie vapeur (J/kg)")
enthalpy_vapor_kj_kg: float = Field(..., description="Enthalpie vapeur (kJ/kg)")
density_liquid: float = Field(..., description="Masse volumique liquide (kg/m³)")
density_vapor: float = Field(..., description="Masse volumique vapeur (kg/m³)")
latent_heat: float = Field(..., description="Chaleur latente (J/kg)")
latent_heat_kj_kg: float = Field(..., description="Chaleur latente (kJ/kg)")
class PropertyCalculationResponse(BaseModel):
"""Reponse complete d'un calcul de proprietes"""
refrigerant: str = Field(..., description="Nom du refrigerant")
inputs: PropertyInputs = Field(..., description="Parametres d'entree")
properties: ThermodynamicPropertiesDetailed = Field(..., description="Proprietes calculees")
saturation: SaturationPropertiesDetailed = Field(..., description="Proprietes de saturation")
note: Optional[str] = Field(None, description="Note informative")
class PhaseProperties(BaseModel):
"""Proprietes d'une phase (liquide ou vapeur)"""
enthalpy: float = Field(..., description="Enthalpie (J/kg)")
enthalpy_kj_kg: float = Field(..., description="Enthalpie (kJ/kg)")
density: float = Field(..., description="Masse volumique (kg/m³)")
specific_volume: Optional[float] = Field(None, description="Volume specifique (m³/kg)")
entropy: float = Field(..., description="Entropie (J/kg.K)")
entropy_kj_kgK: float = Field(..., description="Entropie (kJ/kg.K)")
class SaturationResponse(BaseModel):
"""Reponse pour les proprietes de saturation"""
refrigerant: str = Field(..., description="Nom du refrigerant")
pressure: float = Field(..., description="Pression (Pa)")
pressure_bar: float = Field(..., description="Pression (bar)")
temperature_saturation: float = Field(..., description="Temperature de saturation (K)")
temperature_saturation_celsius: float = Field(..., description="Temperature de saturation (°C)")
liquid: PhaseProperties = Field(..., description="Proprietes du liquide sature")
vapor: PhaseProperties = Field(..., description="Proprietes de la vapeur saturee")
latent_heat: float = Field(..., description="Chaleur latente (J/kg)")
latent_heat_kj_kg: float = Field(..., description="Chaleur latente (kJ/kg)")

59
app/models/refrigerant.py Normal file
View File

@@ -0,0 +1,59 @@
"""
Modeles Pydantic pour les refrigerants
"""
from typing import Optional, List
from pydantic import BaseModel, Field
class RefrigerantInfo(BaseModel):
"""Informations sur un refrigerant"""
name: str = Field(..., description="Nom du refrigerant (ex: R134a)")
available: bool = Field(..., description="Disponibilite de la bibliotheque")
loaded: bool = Field(default=False, description="Si charge en memoire")
error: Optional[str] = Field(None, description="Message d'erreur si indisponible")
class RefrigerantsListResponse(BaseModel):
"""Reponse pour la liste des refrigerants"""
refrigerants: List[RefrigerantInfo] = Field(..., description="Liste des refrigerants")
total: int = Field(..., description="Nombre total de refrigerants")
available_count: int = Field(..., description="Nombre de refrigerants disponibles")
class ThermodynamicProperties(BaseModel):
"""Proprietes thermodynamiques d'un point"""
temperature: float = Field(..., description="Temperature (K)")
pressure: float = Field(..., description="Pression (Pa)")
enthalpy: float = Field(..., description="Enthalpie (J/kg)")
entropy: float = Field(..., description="Entropie (J/kg.K)")
density: float = Field(..., description="Densite (kg/m3)")
quality: Optional[float] = Field(None, description="Qualite (0-1)", ge=0, le=1)
class SaturationProperties(BaseModel):
"""Proprietes de saturation"""
temperature_sat: float = Field(..., description="Temperature de saturation (K)")
pressure: float = Field(..., description="Pression (Pa)")
enthalpy_liquid: float = Field(..., description="Enthalpie liquide saturee (J/kg)")
enthalpy_vapor: float = Field(..., description="Enthalpie vapeur saturee (J/kg)")
density_liquid: float = Field(..., description="Densite liquide (kg/m3)")
density_vapor: float = Field(..., description="Densite vapeur (kg/m3)")
class PropertyRequest(BaseModel):
"""Requete pour calculer des proprietes"""
refrigerant: str = Field(..., description="Nom du refrigerant", example="R134a")
pressure: float = Field(..., description="Pression (Pa)", gt=0)
quality: Optional[float] = Field(None, description="Qualite (0-1)", ge=0, le=1)
temperature: Optional[float] = Field(None, description="Temperature (K)", gt=0)
enthalpy: Optional[float] = Field(None, description="Enthalpie (J/kg)")
class Config:
json_schema_extra = {
"example": {
"refrigerant": "R134a",
"pressure": 500000,
"quality": 0.5
}
}