""" 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] } ] }