ci: commit workspace changes from notebook and backend fixes (excludes test_env, Frontend)
This commit is contained in:
1
app/api/v1/__init__.py
Normal file
1
app/api/v1/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""API v1 routes"""
|
||||
1
app/api/v1/endpoints/__init__.py
Normal file
1
app/api/v1/endpoints/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""API v1 endpoints"""
|
||||
335
app/api/v1/endpoints/cycles.py
Normal file
335
app/api/v1/endpoints/cycles.py
Normal file
@@ -0,0 +1,335 @@
|
||||
"""
|
||||
Endpoints API pour les calculs de cycles frigorifiques.
|
||||
|
||||
Ce module fournit les endpoints pour:
|
||||
- Calcul de cycles simples
|
||||
- Calcul de cycles avec économiseur
|
||||
- Liste des types de cycles disponibles
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from typing import List
|
||||
import logging
|
||||
|
||||
from app.models.cycle import (
|
||||
SimpleCycleRequest,
|
||||
SimpleCycleResponse,
|
||||
CyclePerformance,
|
||||
CyclePoint,
|
||||
CycleError
|
||||
)
|
||||
from app.core.refrigerant_loader import RefrigerantLibrary
|
||||
from app.services.cycle_calculator import CycleCalculator
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/cycles/types",
|
||||
response_model=List[str],
|
||||
summary="Liste des types de cycles",
|
||||
description="Retourne la liste des types de cycles frigorifiques disponibles"
|
||||
)
|
||||
async def get_cycle_types():
|
||||
"""
|
||||
Retourne les types de cycles disponibles.
|
||||
|
||||
Returns:
|
||||
Liste des types de cycles
|
||||
"""
|
||||
return [
|
||||
"simple",
|
||||
"economizer"
|
||||
]
|
||||
|
||||
|
||||
@router.post(
|
||||
"/cycles/simple",
|
||||
response_model=SimpleCycleResponse,
|
||||
summary="Calcul d'un cycle frigorifique simple",
|
||||
description="Calcule les performances d'un cycle frigorifique simple à 4 points",
|
||||
status_code=status.HTTP_200_OK
|
||||
)
|
||||
async def calculate_simple_cycle(request: SimpleCycleRequest):
|
||||
"""
|
||||
Calcule un cycle frigorifique simple.
|
||||
|
||||
Le cycle simple est composé de 4 points:
|
||||
- Point 1: Sortie évaporateur (aspiration compresseur)
|
||||
- Point 2: Refoulement compresseur
|
||||
- Point 3: Sortie condenseur
|
||||
- Point 4: Sortie détendeur
|
||||
|
||||
Notes:
|
||||
- L'API accepte des pressions en bar. En interne nous utilisons Pa.
|
||||
- Cette fonction convertit les pressions entrantes en Pa avant le calcul
|
||||
et reconvertit les pressions renvoyées en bar pour la réponse.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Calcul cycle simple pour {request.refrigerant}")
|
||||
|
||||
# Charger le réfrigérant
|
||||
try:
|
||||
refrigerant = RefrigerantLibrary(request.refrigerant)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Refrigerant '{request.refrigerant}' not found: {str(e)}"
|
||||
)
|
||||
|
||||
# Créer le calculateur
|
||||
calculator = CycleCalculator(refrigerant)
|
||||
|
||||
# Déterminer les pressions (soit fournies en bar, soit calculées depuis les températures)
|
||||
if request.evap_pressure is not None:
|
||||
evap_pressure_pa = request.evap_pressure * 1e5 # bar -> Pa
|
||||
logger.info(f"Evap pressure provided: {request.evap_pressure:.3f} bar -> {evap_pressure_pa:.0f} Pa")
|
||||
else:
|
||||
# Calculer la pression depuis la température (retourne Pa)
|
||||
evap_pressure_pa = calculator.get_pressure_from_saturation_temperature(
|
||||
request.evap_temperature, quality=1.0 # Vapeur saturée
|
||||
)
|
||||
logger.info(f"Pression d'évaporation calculée: {evap_pressure_pa/1e5:.3f} bar pour T={request.evap_temperature}°C")
|
||||
|
||||
if request.cond_pressure is not None:
|
||||
cond_pressure_pa = request.cond_pressure * 1e5 # bar -> Pa
|
||||
logger.info(f"Cond pressure provided: {request.cond_pressure:.3f} bar -> {cond_pressure_pa:.0f} Pa")
|
||||
else:
|
||||
# Calculer la pression depuis la température (retourne Pa)
|
||||
cond_pressure_pa = calculator.get_pressure_from_saturation_temperature(
|
||||
request.cond_temperature, quality=0.0 # Liquide saturé
|
||||
)
|
||||
logger.info(f"Pression de condensation calculée: {cond_pressure_pa/1e5:.3f} bar pour T={request.cond_temperature}°C")
|
||||
|
||||
# Calculer le rapport de pression (unité indépendante)
|
||||
pressure_ratio = cond_pressure_pa / evap_pressure_pa
|
||||
|
||||
# Déterminer le rendement du compresseur (soit fourni, soit calculé)
|
||||
if request.compressor_efficiency is not None:
|
||||
compressor_efficiency = request.compressor_efficiency
|
||||
logger.info(f"Rendement compresseur fourni: {compressor_efficiency:.3f}")
|
||||
else:
|
||||
# Calculer automatiquement depuis le rapport de pression
|
||||
compressor_efficiency = calculator.calculate_compressor_efficiency(pressure_ratio)
|
||||
logger.info(f"Rendement compresseur calculé: {compressor_efficiency:.3f} (PR={pressure_ratio:.2f})")
|
||||
|
||||
# Calculer le cycle (toutes les pressions passées en Pa)
|
||||
result = calculator.calculate_simple_cycle(
|
||||
evap_pressure=evap_pressure_pa,
|
||||
cond_pressure=cond_pressure_pa,
|
||||
superheat=request.superheat,
|
||||
subcool=request.subcool,
|
||||
compressor_efficiency=compressor_efficiency,
|
||||
mass_flow=request.mass_flow
|
||||
)
|
||||
|
||||
# Construire la réponse : convertir les pressions internes (Pa) en bar pour l'API
|
||||
cycle_points = [
|
||||
CyclePoint(
|
||||
point_id=pt["point_id"],
|
||||
description=pt["description"],
|
||||
pressure=(pt["pressure"] / 1e5) if pt.get("pressure") is not None else None,
|
||||
temperature=pt.get("temperature"),
|
||||
enthalpy=pt.get("enthalpy"),
|
||||
entropy=pt.get("entropy"),
|
||||
quality=pt.get("quality")
|
||||
)
|
||||
for pt in result["points"]
|
||||
]
|
||||
|
||||
# Convertir pressures dans diagram_data si présent
|
||||
diagram_data = result.get("diagram_data")
|
||||
if diagram_data and "cycle_points" in diagram_data:
|
||||
diagram_data["cycle_points"] = [
|
||||
{"enthalpy": cp["enthalpy"], "pressure": (cp["pressure"] / 1e5)}
|
||||
for cp in diagram_data["cycle_points"]
|
||||
]
|
||||
|
||||
performance = CyclePerformance(
|
||||
cop=result["performance"]["cop"],
|
||||
cooling_capacity=result["performance"]["cooling_capacity"],
|
||||
heating_capacity=result["performance"]["heating_capacity"],
|
||||
compressor_power=result["performance"]["compressor_power"],
|
||||
compressor_efficiency=result["performance"]["compressor_efficiency"],
|
||||
mass_flow=result["performance"]["mass_flow"],
|
||||
volumetric_flow=result["performance"]["volumetric_flow"],
|
||||
compression_ratio=result["performance"]["compression_ratio"],
|
||||
discharge_temperature=result["performance"]["discharge_temperature"]
|
||||
)
|
||||
|
||||
return SimpleCycleResponse(
|
||||
refrigerant=request.refrigerant,
|
||||
cycle_type="simple",
|
||||
points=cycle_points,
|
||||
performance=performance,
|
||||
diagram_data=diagram_data
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Invalid calculation parameters: {str(e)}"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Cycle calculation error: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/cycles/simple/validate",
|
||||
response_model=dict,
|
||||
summary="Validation des paramètres de cycle",
|
||||
description="Valide les paramètres d'un cycle sans effectuer le calcul complet"
|
||||
)
|
||||
async def validate_cycle_parameters(request: SimpleCycleRequest):
|
||||
"""
|
||||
Valide les paramètres d'un cycle frigorifique.
|
||||
|
||||
Vérifie:
|
||||
- Existence du réfrigérant
|
||||
- Cohérence des pressions (P_cond > P_evap)
|
||||
- Plages de valeurs acceptables
|
||||
|
||||
Args:
|
||||
request: Paramètres à valider
|
||||
|
||||
Returns:
|
||||
État de validation et messages
|
||||
"""
|
||||
issues = []
|
||||
|
||||
# Vérifier le réfrigérant
|
||||
try:
|
||||
refrigerant = RefrigerantLibrary(request.refrigerant)
|
||||
calculator = CycleCalculator(refrigerant)
|
||||
except Exception:
|
||||
issues.append(f"Refrigerant '{request.refrigerant}' not found")
|
||||
calculator = None
|
||||
|
||||
# Déterminer les pressions
|
||||
if request.evap_pressure is not None:
|
||||
evap_pressure = request.evap_pressure
|
||||
else:
|
||||
if calculator:
|
||||
evap_pressure = calculator.get_pressure_from_saturation_temperature(
|
||||
request.evap_temperature, quality=1.0
|
||||
)
|
||||
else:
|
||||
evap_pressure = None
|
||||
|
||||
if request.cond_pressure is not None:
|
||||
cond_pressure = request.cond_pressure
|
||||
else:
|
||||
if calculator:
|
||||
cond_pressure = calculator.get_pressure_from_saturation_temperature(
|
||||
request.cond_temperature, quality=0.0
|
||||
)
|
||||
else:
|
||||
cond_pressure = None
|
||||
|
||||
# Vérifier les pressions
|
||||
if evap_pressure and cond_pressure and cond_pressure <= evap_pressure:
|
||||
issues.append(
|
||||
f"Condensing pressure ({cond_pressure:.2f} bar) must be "
|
||||
f"greater than evaporating pressure ({evap_pressure:.2f} bar)"
|
||||
)
|
||||
|
||||
# Vérifier le rendement (seulement s'il est fourni)
|
||||
if request.compressor_efficiency is not None:
|
||||
if not 0.4 <= request.compressor_efficiency <= 1.0:
|
||||
issues.append(
|
||||
f"Compressor efficiency ({request.compressor_efficiency}) should be "
|
||||
f"between 0.4 and 1.0"
|
||||
)
|
||||
|
||||
# Vérifier le débit
|
||||
if request.mass_flow <= 0:
|
||||
issues.append(
|
||||
f"Mass flow rate ({request.mass_flow}) must be positive"
|
||||
)
|
||||
|
||||
# Vérifier surchauffe et sous-refroidissement
|
||||
if request.superheat < 0:
|
||||
issues.append(f"Superheat ({request.superheat}) cannot be negative")
|
||||
|
||||
if request.subcool < 0:
|
||||
issues.append(f"Subcooling ({request.subcool}) cannot be negative")
|
||||
|
||||
is_valid = len(issues) == 0
|
||||
|
||||
return {
|
||||
"valid": is_valid,
|
||||
"issues": issues if not is_valid else [],
|
||||
"message": "Parameters are valid" if is_valid else "Parameters validation failed"
|
||||
}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/cycles/info",
|
||||
summary="Informations sur les cycles",
|
||||
description="Retourne des informations détaillées sur les différents types de cycles"
|
||||
)
|
||||
async def get_cycles_info():
|
||||
"""
|
||||
Retourne des informations sur les cycles disponibles.
|
||||
|
||||
Returns:
|
||||
Descriptions des types de cycles
|
||||
"""
|
||||
return {
|
||||
"cycles": [
|
||||
{
|
||||
"type": "simple",
|
||||
"name": "Cycle frigorifique simple",
|
||||
"description": "Cycle de base à compression simple avec 4 points",
|
||||
"components": [
|
||||
"Evaporateur",
|
||||
"Compresseur",
|
||||
"Condenseur",
|
||||
"Détendeur"
|
||||
],
|
||||
"points": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "Sortie évaporateur (aspiration)",
|
||||
"state": "Vapeur surchauffée"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "Refoulement compresseur",
|
||||
"state": "Vapeur haute pression"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "Sortie condenseur",
|
||||
"state": "Liquide sous-refroidi"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"name": "Sortie détendeur",
|
||||
"state": "Mélange liquide-vapeur"
|
||||
}
|
||||
],
|
||||
"typical_cop_range": [2.5, 4.5]
|
||||
},
|
||||
{
|
||||
"type": "economizer",
|
||||
"name": "Cycle avec économiseur",
|
||||
"description": "Cycle à double étage avec séparateur intermédiaire",
|
||||
"components": [
|
||||
"Evaporateur",
|
||||
"Compresseur BP",
|
||||
"Economiseur",
|
||||
"Compresseur HP",
|
||||
"Condenseur",
|
||||
"Détendeur principal",
|
||||
"Détendeur secondaire"
|
||||
],
|
||||
"status": "À implémenter",
|
||||
"typical_cop_range": [3.0, 5.5]
|
||||
}
|
||||
]
|
||||
}
|
||||
187
app/api/v1/endpoints/diagrams.py
Normal file
187
app/api/v1/endpoints/diagrams.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
Endpoints API pour la génération de diagrammes PH.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from typing import Dict, Any
|
||||
|
||||
from app.models.diagram import (
|
||||
DiagramRequest,
|
||||
DiagramResponse,
|
||||
DiagramError
|
||||
)
|
||||
from app.services.diagram_generator import DiagramGenerator
|
||||
from app.core.refrigerant_loader import get_refrigerant
|
||||
from collections import OrderedDict
|
||||
import json
|
||||
|
||||
|
||||
router = APIRouter(prefix="/diagrams", tags=["diagrams"])
|
||||
|
||||
|
||||
@router.post(
|
||||
"/ph",
|
||||
response_model=DiagramResponse,
|
||||
summary="Générer un diagramme Pression-Enthalpie",
|
||||
description="Génère un diagramme PH avec courbe de saturation et isothermes",
|
||||
status_code=status.HTTP_200_OK
|
||||
)
|
||||
async def generate_ph_diagram(request: DiagramRequest) -> DiagramResponse:
|
||||
"""
|
||||
Génère un diagramme Pression-Enthalpie.
|
||||
|
||||
Args:
|
||||
request: Paramètres du diagramme
|
||||
|
||||
Returns:
|
||||
DiagramResponse avec image et/ou données
|
||||
|
||||
Raises:
|
||||
HTTPException: Si erreur lors de la génération
|
||||
"""
|
||||
try:
|
||||
# Charger le réfrigérant
|
||||
refrigerant = get_refrigerant(request.refrigerant)
|
||||
if refrigerant is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Réfrigérant '{request.refrigerant}' non disponible"
|
||||
)
|
||||
|
||||
# Créer le générateur
|
||||
generator = DiagramGenerator(refrigerant)
|
||||
|
||||
# Configurer dimensions si PNG
|
||||
if request.format in ["png", "both"]:
|
||||
generator.fig_width = request.width / 100
|
||||
generator.fig_height = request.height / 100
|
||||
generator.dpi = request.dpi
|
||||
|
||||
# Convertir cycle_points en tuples
|
||||
cycle_points_tuples = None
|
||||
if request.cycle_points:
|
||||
cycle_points_tuples = [
|
||||
(pt["enthalpy"], pt["pressure"])
|
||||
for pt in request.cycle_points
|
||||
]
|
||||
|
||||
# Simple in-memory LRU cache for diagram JSON results (keyed by request body)
|
||||
# Cache only JSON responses to avoid storing large PNG blobs in memory
|
||||
if not hasattr(generate_ph_diagram, "_diagram_cache"):
|
||||
generate_ph_diagram._diagram_cache = OrderedDict()
|
||||
generate_ph_diagram._cache_size = 50
|
||||
|
||||
def _make_cache_key(req: DiagramRequest) -> str:
|
||||
# Use a stable JSON string of important fields as cache key
|
||||
def _serialize(v):
|
||||
# Pydantic BaseModel expose model_dump in v2; fall back to dict
|
||||
try:
|
||||
if hasattr(v, 'model_dump'):
|
||||
return v.model_dump()
|
||||
elif hasattr(v, 'dict'):
|
||||
return v.dict()
|
||||
except Exception:
|
||||
pass
|
||||
return v
|
||||
|
||||
key_obj = {
|
||||
"refrigerant": req.refrigerant,
|
||||
"pressure_range": _serialize(req.pressure_range),
|
||||
"include_isotherms": req.include_isotherms,
|
||||
"cycle_points": _serialize(req.cycle_points),
|
||||
"format": req.format,
|
||||
}
|
||||
return json.dumps(key_obj, sort_keys=True, separators=(",", ":"))
|
||||
|
||||
cache_key = _make_cache_key(request)
|
||||
# If client requested JSON or both, try cache
|
||||
cached_result = None
|
||||
if request.format in ["json", "both"]:
|
||||
cached_result = generate_ph_diagram._diagram_cache.get(cache_key)
|
||||
|
||||
if cached_result is not None:
|
||||
# move to end (most recently used)
|
||||
generate_ph_diagram._diagram_cache.move_to_end(cache_key)
|
||||
result = cached_result
|
||||
generation_time = 0.0
|
||||
else:
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
result = generator.generate_complete_diagram(
|
||||
cycle_points=cycle_points_tuples,
|
||||
title=request.title,
|
||||
export_format=request.format
|
||||
)
|
||||
|
||||
generation_time = (time.time() - start_time) * 1000 # ms
|
||||
# store JSON part in cache if present and format includes json
|
||||
if request.format in ["json", "both"] and "data" in result:
|
||||
# store only the data dictionary to keep cache small
|
||||
cache_entry = {"data": result.get("data")}
|
||||
generate_ph_diagram._diagram_cache[cache_key] = cache_entry
|
||||
# enforce cache size
|
||||
if len(generate_ph_diagram._diagram_cache) > generate_ph_diagram._cache_size:
|
||||
generate_ph_diagram._diagram_cache.popitem(last=False)
|
||||
|
||||
# Construire la réponse
|
||||
response_data = {
|
||||
"success": True,
|
||||
"metadata": {
|
||||
"generation_time_ms": round(generation_time, 2),
|
||||
"refrigerant": request.refrigerant,
|
||||
"export_format": request.format
|
||||
},
|
||||
"message": f"Diagramme PH généré pour {request.refrigerant}"
|
||||
}
|
||||
|
||||
# Ajouter l'image si générée
|
||||
if "image_base64" in result:
|
||||
response_data["image"] = result["image_base64"]
|
||||
response_data["metadata"]["image_format"] = "png"
|
||||
response_data["metadata"]["image_width"] = request.width
|
||||
response_data["metadata"]["image_height"] = request.height
|
||||
|
||||
# Ajouter les données si générées
|
||||
if "data" in result:
|
||||
response_data["data"] = result["data"]
|
||||
|
||||
return DiagramResponse(**response_data)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Paramètres invalides: {str(e)}"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erreur génération diagramme: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
summary="Types de diagrammes disponibles",
|
||||
response_model=Dict[str, Any]
|
||||
)
|
||||
async def list_diagram_types():
|
||||
"""Liste les types de diagrammes disponibles."""
|
||||
return {
|
||||
"diagram_types": [
|
||||
{
|
||||
"type": "ph",
|
||||
"name": "Pression-Enthalpie",
|
||||
"description": "Diagramme log(P) vs h avec courbe de saturation et isothermes",
|
||||
"endpoint": "/api/v1/diagrams/ph"
|
||||
}
|
||||
],
|
||||
"export_formats": ["png", "json", "both"],
|
||||
"supported_refrigerants": [
|
||||
"R12", "R22", "R32", "R134a", "R290", "R404A", "R410A",
|
||||
"R452A", "R454A", "R454B", "R502", "R507A", "R513A",
|
||||
"R515B", "R744", "R1233zd", "R1234ze"
|
||||
]
|
||||
}
|
||||
225
app/api/v1/endpoints/properties.py
Normal file
225
app/api/v1/endpoints/properties.py
Normal file
@@ -0,0 +1,225 @@
|
||||
"""
|
||||
Endpoints pour les calculs de proprietes thermodynamiques
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
|
||||
from app.models.properties import (
|
||||
PropertyCalculationRequest,
|
||||
PropertyCalculationResponse,
|
||||
SaturationRequest,
|
||||
SaturationResponse
|
||||
)
|
||||
from app.services.thermodynamics import get_thermodynamics_service
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/calculate",
|
||||
response_model=PropertyCalculationResponse,
|
||||
summary="Calculer les proprietes thermodynamiques",
|
||||
description="Calcule les proprietes thermodynamiques d'un refrigerant selon differents parametres d'entree"
|
||||
)
|
||||
async def calculate_properties(request: PropertyCalculationRequest):
|
||||
"""
|
||||
Calcule les proprietes thermodynamiques d'un refrigerant.
|
||||
|
||||
Types de calcul disponibles:
|
||||
- **px**: A partir de pression et qualite
|
||||
- **pT**: A partir de pression et temperature
|
||||
- **ph**: A partir de pression et enthalpie
|
||||
- **Tx**: A partir de temperature et qualite
|
||||
|
||||
Args:
|
||||
request: Parametres du calcul
|
||||
|
||||
Returns:
|
||||
PropertyCalculationResponse: Proprietes calculees
|
||||
|
||||
Raises:
|
||||
400: Parametres invalides
|
||||
404: Refrigerant non trouve
|
||||
500: Erreur de calcul
|
||||
"""
|
||||
try:
|
||||
service = get_thermodynamics_service()
|
||||
|
||||
if request.calculation_type == "px":
|
||||
if request.pressure is None or request.quality is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Pression et qualite requises pour le calcul px"
|
||||
)
|
||||
result = service.calculate_from_px(
|
||||
request.refrigerant,
|
||||
request.pressure,
|
||||
request.quality
|
||||
)
|
||||
|
||||
elif request.calculation_type == "pT":
|
||||
if request.pressure is None or request.temperature is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Pression et temperature requises pour le calcul pT"
|
||||
)
|
||||
result = service.calculate_from_pT(
|
||||
request.refrigerant,
|
||||
request.pressure,
|
||||
request.temperature
|
||||
)
|
||||
|
||||
elif request.calculation_type == "ph":
|
||||
if request.pressure is None or request.enthalpy is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Pression et enthalpie requises pour le calcul ph"
|
||||
)
|
||||
result = service.calculate_from_ph(
|
||||
request.refrigerant,
|
||||
request.pressure,
|
||||
request.enthalpy
|
||||
)
|
||||
|
||||
elif request.calculation_type == "Tx":
|
||||
if request.temperature is None or request.quality is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Temperature et qualite requises pour le calcul Tx"
|
||||
)
|
||||
result = service.calculate_from_Tx(
|
||||
request.refrigerant,
|
||||
request.temperature,
|
||||
request.quality
|
||||
)
|
||||
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Type de calcul invalide: {request.calculation_type}"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erreur lors du calcul des proprietes: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/saturation",
|
||||
response_model=SaturationResponse,
|
||||
summary="Proprietes de saturation",
|
||||
description="Obtient les proprietes de saturation (liquide et vapeur) a une pression donnee"
|
||||
)
|
||||
async def get_saturation_properties(request: SaturationRequest):
|
||||
"""
|
||||
Obtient les proprietes de saturation d'un refrigerant.
|
||||
|
||||
Retourne les proprietes du liquide sature et de la vapeur saturee
|
||||
a la pression specifiee, ainsi que la temperature de saturation
|
||||
et la chaleur latente de vaporisation.
|
||||
|
||||
Args:
|
||||
request: Refrigerant et pression
|
||||
|
||||
Returns:
|
||||
SaturationResponse: Proprietes de saturation
|
||||
|
||||
Raises:
|
||||
404: Refrigerant non trouve
|
||||
500: Erreur de calcul
|
||||
"""
|
||||
try:
|
||||
service = get_thermodynamics_service()
|
||||
result = service.get_saturation_properties(
|
||||
request.refrigerant,
|
||||
request.pressure
|
||||
)
|
||||
return result
|
||||
|
||||
except ValueError as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erreur lors du calcul des proprietes de saturation: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
summary="Information sur les endpoints de proprietes",
|
||||
description="Retourne la liste des endpoints disponibles pour les calculs de proprietes"
|
||||
)
|
||||
async def properties_info():
|
||||
"""
|
||||
Retourne les informations sur les endpoints disponibles.
|
||||
|
||||
Returns:
|
||||
dict: Liste des endpoints et leurs descriptions
|
||||
"""
|
||||
return {
|
||||
"endpoints": [
|
||||
{
|
||||
"path": "/api/v1/properties/calculate",
|
||||
"method": "POST",
|
||||
"description": "Calcule les proprietes thermodynamiques",
|
||||
"calculation_types": [
|
||||
{
|
||||
"type": "px",
|
||||
"description": "A partir de pression et qualite",
|
||||
"required": ["pressure", "quality"]
|
||||
},
|
||||
{
|
||||
"type": "pT",
|
||||
"description": "A partir de pression et temperature",
|
||||
"required": ["pressure", "temperature"]
|
||||
},
|
||||
{
|
||||
"type": "ph",
|
||||
"description": "A partir de pression et enthalpie",
|
||||
"required": ["pressure", "enthalpy"]
|
||||
},
|
||||
{
|
||||
"type": "Tx",
|
||||
"description": "A partir de temperature et qualite",
|
||||
"required": ["temperature", "quality"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/api/v1/properties/saturation",
|
||||
"method": "POST",
|
||||
"description": "Obtient les proprietes de saturation",
|
||||
"required": ["refrigerant", "pressure"]
|
||||
}
|
||||
],
|
||||
"units": {
|
||||
"pressure": "Pa (Pascal)",
|
||||
"temperature": "K (Kelvin)",
|
||||
"enthalpy": "J/kg",
|
||||
"entropy": "J/kg.K",
|
||||
"density": "kg/m3",
|
||||
"quality": "0-1 (sans dimension)"
|
||||
},
|
||||
"example": {
|
||||
"refrigerant": "R134a",
|
||||
"calculation_type": "px",
|
||||
"pressure": 500000,
|
||||
"quality": 0.5
|
||||
}
|
||||
}
|
||||
150
app/api/v1/endpoints/refrigerants.py
Normal file
150
app/api/v1/endpoints/refrigerants.py
Normal file
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
Endpoints pour les refrigerants
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from typing import List
|
||||
|
||||
from app.models.refrigerant import (
|
||||
RefrigerantInfo,
|
||||
RefrigerantsListResponse
|
||||
)
|
||||
from app.core.refrigerant_loader import get_manager
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=RefrigerantsListResponse,
|
||||
summary="Liste des refrigerants disponibles",
|
||||
description="Retourne la liste de tous les refrigerants supportes avec leur disponibilite"
|
||||
)
|
||||
async def list_refrigerants():
|
||||
"""
|
||||
Liste tous les refrigerants disponibles dans le systeme.
|
||||
|
||||
Retourne pour chaque refrigerant:
|
||||
- name: Nom du refrigerant (ex: R134a, R410A)
|
||||
- available: Si la bibliotheque est disponible
|
||||
- loaded: Si le refrigerant est actuellement charge en memoire
|
||||
- error: Message d'erreur si indisponible
|
||||
|
||||
Returns:
|
||||
RefrigerantsListResponse: Liste des refrigerants avec statistiques
|
||||
"""
|
||||
try:
|
||||
manager = get_manager()
|
||||
refrigerants = manager.get_available_refrigerants()
|
||||
|
||||
available_count = sum(1 for r in refrigerants if r.get("available", False))
|
||||
|
||||
return RefrigerantsListResponse(
|
||||
refrigerants=refrigerants,
|
||||
total=len(refrigerants),
|
||||
available_count=available_count
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erreur lors de la recuperation des refrigerants: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{refrigerant_name}",
|
||||
response_model=RefrigerantInfo,
|
||||
summary="Informations sur un refrigerant specifique",
|
||||
description="Retourne les informations detaillees d'un refrigerant"
|
||||
)
|
||||
async def get_refrigerant_info(refrigerant_name: str):
|
||||
"""
|
||||
Obtient les informations d'un refrigerant specifique.
|
||||
|
||||
Args:
|
||||
refrigerant_name: Nom du refrigerant (ex: R134a, R410A)
|
||||
|
||||
Returns:
|
||||
RefrigerantInfo: Informations du refrigerant
|
||||
|
||||
Raises:
|
||||
404: Si le refrigerant n'existe pas
|
||||
500: En cas d'erreur interne
|
||||
"""
|
||||
try:
|
||||
manager = get_manager()
|
||||
refrigerants = manager.get_available_refrigerants()
|
||||
|
||||
# Rechercher le refrigerant
|
||||
refrig_info = next(
|
||||
(r for r in refrigerants if r["name"] == refrigerant_name),
|
||||
None
|
||||
)
|
||||
|
||||
if refrig_info is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Refrigerant '{refrigerant_name}' non trouve. "
|
||||
f"Refrigerants disponibles: {', '.join(r['name'] for r in refrigerants)}"
|
||||
)
|
||||
|
||||
return RefrigerantInfo(**refrig_info)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erreur lors de la recuperation du refrigerant: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{refrigerant_name}/load",
|
||||
response_model=RefrigerantInfo,
|
||||
summary="Charger un refrigerant en memoire",
|
||||
description="Charge explicitement un refrigerant en memoire pour accelerer les calculs"
|
||||
)
|
||||
async def load_refrigerant(refrigerant_name: str):
|
||||
"""
|
||||
Charge un refrigerant en memoire.
|
||||
|
||||
Utile pour precharger les refrigerants frequemment utilises
|
||||
et ameliorer les performances.
|
||||
|
||||
Args:
|
||||
refrigerant_name: Nom du refrigerant a charger
|
||||
|
||||
Returns:
|
||||
RefrigerantInfo: Informations du refrigerant charge
|
||||
|
||||
Raises:
|
||||
404: Si le refrigerant n'existe pas
|
||||
500: Si le chargement echoue
|
||||
"""
|
||||
try:
|
||||
manager = get_manager()
|
||||
|
||||
# Tenter de charger le refrigerant
|
||||
lib = manager.load_refrigerant(refrigerant_name)
|
||||
|
||||
return RefrigerantInfo(
|
||||
name=refrigerant_name,
|
||||
available=True,
|
||||
loaded=True
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
# Refrigerant non supporte
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=str(e)
|
||||
)
|
||||
except Exception as e:
|
||||
# Erreur de chargement
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Erreur lors du chargement de {refrigerant_name}: {str(e)}"
|
||||
)
|
||||
Reference in New Issue
Block a user