335 lines
12 KiB
Python
335 lines
12 KiB
Python
"""
|
|
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]
|
|
}
|
|
]
|
|
} |