# Plan d'implémentation - API Diagramme PH ## Vue d'ensemble Ce document décrit le plan détaillé pour implémenter l'API REST de génération de diagrammes PH et calculs frigorifiques, avec déploiement sur AWS Elastic Beanstalk. --- ## Phase 1: Configuration du projet (Durée: 1-2 jours) ### Tâche 1.1: Structure initiale ```bash # Créer la structure du projet mkdir -p diagram-ph-api/{app,libs,tests,docker,deployment,docs} cd diagram-ph-api # Initialiser Git git init git add . git commit -m "Initial project structure" # Créer environnement virtuel python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate # Installer dépendances de base pip install fastapi uvicorn pydantic pytest ``` ### Tâche 1.2: Fichiers de configuration **requirements.txt** ```txt fastapi==0.109.0 uvicorn[standard]==0.27.0 pydantic==2.5.0 pydantic-settings==2.1.0 numpy==1.26.3 pandas==2.2.0 matplotlib==3.8.2 plotly==5.18.0 altair==5.2.0 python-multipart==0.0.6 aiofiles==23.2.1 cachetools==5.3.2 slowapi==0.1.9 python-json-logger==2.0.7 boto3==1.34.0 pytest==7.4.3 httpx==0.26.0 ``` **pyproject.toml** ```toml [project] name = "diagram-ph-api" version = "1.0.0" description = "API REST pour diagrammes PH et calculs frigorifiques" requires-python = ">=3.12" dependencies = [ "fastapi>=0.109.0", "uvicorn[standard]>=0.27.0", # ... autres dépendances ] [project.optional-dependencies] dev = [ "pytest>=7.4.3", "pytest-cov>=4.1.0", "black>=23.12.0", "ruff>=0.1.8", ] [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] ``` **.env.example** ```env # Application ENV=development LOG_LEVEL=DEBUG API_VERSION=v1 # Server HOST=0.0.0.0 PORT=8000 WORKERS=4 # AWS (pour production) AWS_REGION=eu-west-1 AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= # Rate limiting RATE_LIMIT_PER_MINUTE=100 # Cache CACHE_TTL_SECONDS=3600 CACHE_MAX_SIZE=10000 ``` --- ## Phase 2: Modules Core (Durée: 3-4 jours) ### Tâche 2.1: RefrigerantEngine **app/core/refrigerant_engine.py** ```python from pathlib import Path import ctypes import platform from functools import lru_cache from typing import Dict, Any import sys class RefrigerantEngine: """ Moteur de calcul des propriétés thermodynamiques Gère le chargement des DLL/SO et le cache """ def __init__(self, refrigerant: str): self.refrigerant = refrigerant self.lib = self._load_library() self._validate_library() def _load_library(self): """Charge la bibliothèque selon l'OS""" base_path = Path(__file__).parent.parent.parent / "libs" if platform.system() == "Windows": lib_path = base_path / "dll" / f"{self.refrigerant}.dll" elif platform.system() == "Linux": lib_path = base_path / "so" / f"lib{self.refrigerant}.so" else: raise OSError(f"Unsupported OS: {platform.system()}") if not lib_path.exists(): raise FileNotFoundError(f"Library not found: {lib_path}") try: return ctypes.CDLL(str(lib_path)) except OSError as e: raise RuntimeError(f"Failed to load {lib_path}: {e}") def _validate_library(self): """Vérifie que la bibliothèque est fonctionnelle""" try: # Test basique: récupérer les limites de pression p_min = self.p_begin() p_max = self.p_end() if p_min >= p_max: raise ValueError("Invalid pressure range") except Exception as e: raise RuntimeError(f"Library validation failed: {e}") @lru_cache(maxsize=1000) def get_properties_PT(self, pressure: float, temperature: float) -> Dict[str, Any]: """Calcul propriétés à partir de P et T (avec cache)""" # TODO: Implémenter appels DLL pass @lru_cache(maxsize=1000) def get_properties_PX(self, pressure: float, quality: float) -> Dict[str, Any]: """Calcul propriétés à partir de P et X (avec cache)""" # TODO: Implémenter appels DLL pass def get_saturation_curve(self) -> Dict[str, list]: """Génère la courbe de saturation""" # TODO: Implémenter pass def p_begin(self) -> float: """Pression minimale du fluide""" # TODO: Appel DLL pass def p_end(self) -> float: """Pression maximale du fluide""" # TODO: Appel DLL pass ``` ### Tâche 2.2: DiagramGenerator **app/core/diagram_generator.py** ```python import matplotlib.pyplot as plt import plotly.graph_objects as go import base64 from io import BytesIO from typing import List, Dict from app.models.requests import Point, DiagramOptions from app.models.enums import OutputFormat class DiagramGenerator: """Générateur de diagrammes PH multi-format""" def __init__(self, refrigerant_engine): self.engine = refrigerant_engine def generate( self, points: List[Point], output_format: OutputFormat, options: DiagramOptions ): """Point d'entrée principal pour génération""" # Récupérer données de base saturation = self.engine.get_saturation_curve() isotherms = self._calculate_isotherms(options) # Calculer propriétés des points calculated_points = self._calculate_points(points) # Générer selon format if output_format == OutputFormat.MATPLOTLIB_PNG: return self._generate_matplotlib( saturation, isotherms, calculated_points, options ) elif output_format == OutputFormat.PLOTLY_JSON: return self._generate_plotly_json( saturation, isotherms, calculated_points, options ) elif output_format == OutputFormat.PLOTLY_HTML: return self._generate_plotly_html( saturation, isotherms, calculated_points, options ) def _generate_matplotlib(self, saturation, isotherms, points, options): """Génère image PNG avec Matplotlib""" fig, ax = plt.subplots(figsize=(options.width/100, options.height/100)) # Tracer saturation ax.plot(saturation['liquid']['enthalpy'], saturation['liquid']['pressure'], 'k-', label='Saturation liquide') ax.plot(saturation['vapor']['enthalpy'], saturation['vapor']['pressure'], 'k-', label='Saturation vapeur') # Échelle log pour pression ax.set_yscale('log') # Labels ax.set_xlabel('Enthalpie [kJ/kg]') ax.set_ylabel('Pression [bar]') ax.set_title(options.title or f'Diagramme PH - {self.engine.refrigerant}') ax.grid(True) # Convertir en base64 buffer = BytesIO() plt.savefig(buffer, format='png', dpi=100, bbox_inches='tight') buffer.seek(0) image_base64 = base64.b64encode(buffer.read()).decode() plt.close() return { 'image_base64': image_base64, 'mime_type': 'image/png' } def _generate_plotly_json(self, saturation, isotherms, points, options): """Génère figure Plotly en JSON""" fig = go.Figure() # Ajouter traces... # TODO: Implémenter return fig.to_dict() ``` ### Tâche 2.3: CycleCalculator **app/core/cycle_calculator.py** ```python from typing import Dict, Any from dataclasses import dataclass @dataclass class CycleResults: cop_cooling: float cop_heating: float cooling_capacity: float heating_capacity: float compressor_power: float efficiencies: Dict[str, float] cycle_points: list class CycleCalculator: """Calculs de cycles frigorifiques""" def __init__(self, refrigerant_engine): self.engine = refrigerant_engine def calculate_standard_cycle( self, evap_outlet, comp_outlet, cond_outlet, exp_outlet, mass_flow: float, efficiencies: Dict ) -> CycleResults: """Calcul cycle standard 4 points""" # Récupérer propriétés complètes p1 = self.engine.get_properties_PT( evap_outlet['pressure'], evap_outlet['temperature'] ) p2 = self.engine.get_properties_PT( comp_outlet['pressure'], comp_outlet['temperature'] ) p3 = self.engine.get_properties_PT( cond_outlet['pressure'], cond_outlet['temperature'] ) p4 = self.engine.get_properties_PX( exp_outlet['pressure'], exp_outlet['quality'] ) # Calculs énergétiques h1, h2, h3, h4 = p1['enthalpy'], p2['enthalpy'], p3['enthalpy'], p4['enthalpy'] q_evap = mass_flow * (h1 - h4) # Puissance frigorifique [W] q_cond = mass_flow * (h2 - h3) # Puissance calorifique [W] w_comp = mass_flow * (h2 - h1) # Puissance compresseur [W] # COP cop_cooling = q_evap / w_comp cop_heating = q_cond / w_comp # Rendement isentropique h2s = self._calc_isentropic_compression(p1, p2['pressure']) eta_isentropic = (h2s - h1) / (h2 - h1) return CycleResults( cop_cooling=cop_cooling, cop_heating=cop_heating, cooling_capacity=q_evap, heating_capacity=q_cond, compressor_power=w_comp, efficiencies={ 'isentropic': eta_isentropic, 'volumetric': efficiencies.get('volumetric', 0.85), 'mechanical': efficiencies.get('mechanical', 0.95) }, cycle_points=[p1, p2, p3, p4] ) def _calc_isentropic_compression(self, point1, p2): """Calcule enthalpie après compression isentropique""" # Compression à entropie constante s1 = point1['entropy'] # TODO: Appel DLL h_ps(p2, s1) pass ``` --- ## Phase 3: API REST (Durée: 3-4 jours) ### Tâche 3.1: Structure FastAPI **app/main.py** ```python from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware from app.api.v1.router import api_router from app.core.config import settings from app.utils.logger import setup_logging # Configuration logging setup_logging() # Création application app = FastAPI( title="Diagram PH API", description="API pour diagrammes PH et calculs frigorifiques", version="1.0.0", docs_url="/docs", redoc_url="/redoc" ) # Middlewares app.add_middleware( CORSMiddleware, allow_origins=settings.CORS_ORIGINS, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.add_middleware(GZipMiddleware, minimum_size=1000) # Routes app.include_router(api_router, prefix="/api/v1") @app.on_event("startup") async def startup_event(): """Actions au démarrage""" logger.info("Starting Diagram PH API...") # Vérifier DLL/SO # Pré-charger données communes pass @app.on_event("shutdown") async def shutdown_event(): """Actions à l'arrêt""" logger.info("Shutting down Diagram PH API...") pass ``` ### Tâche 3.2: Endpoints **app/api/v1/endpoints/diagram.py** ```python from fastapi import APIRouter, HTTPException from app.models.requests import DiagramRequest from app.models.responses import DiagramResponse from app.services.diagram_service import DiagramService router = APIRouter() diagram_service = DiagramService() @router.post("/generate", response_model=DiagramResponse) async def generate_diagram(request: DiagramRequest): """Génère un diagramme PH""" try: result = await diagram_service.generate_diagram( refrigerant=request.refrigerant, points=request.points, output_format=request.output_format, options=request.diagram_options ) return result except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: raise HTTPException(status_code=500, detail="Internal server error") ``` ### Tâche 3.3: Modèles Pydantic **app/models/requests.py** ```python from pydantic import BaseModel, Field, validator from typing import List, Optional from app.models.enums import PointType, OutputFormat class Point(BaseModel): type: PointType pressure: Optional[float] = Field(None, gt=0, description="Pression [Pa]") temperature: Optional[float] = Field(None, description="Température [°C]") quality: Optional[float] = Field(None, ge=0, le=1, description="Titre [0-1]") enthalpy: Optional[float] = Field(None, description="Enthalpie [J/kg]") label: Optional[str] = None order: Optional[int] = None @validator('pressure', 'temperature', 'quality', 'enthalpy') def check_required_fields(cls, v, values): point_type = values.get('type') # Validation selon le type de point # TODO: Implémenter validation complète return v class DiagramOptions(BaseModel): show_isotherms: bool = True isotherm_step: int = Field(10, gt=0, le=50) show_saturation_lines: bool = True title: Optional[str] = None width: int = Field(1000, ge=400, le=2000) height: int = Field(800, ge=300, le=1500) class DiagramRequest(BaseModel): refrigerant: str = Field(..., description="Nom du réfrigérant") output_format: OutputFormat points: List[Point] = Field(default_factory=list) diagram_options: DiagramOptions = Field(default_factory=DiagramOptions) ``` --- ## Phase 4: Tests (Durée: 2-3 jours) ### Tâche 4.1: Tests unitaires **tests/test_core/test_refrigerant_engine.py** ```python import pytest from app.core.refrigerant_engine import RefrigerantEngine def test_load_r134a(): engine = RefrigerantEngine("R134a") assert engine.refrigerant == "R134a" assert engine.lib is not None def test_properties_pt(): engine = RefrigerantEngine("R134a") props = engine.get_properties_PT(500000, 278.15) assert 'enthalpy' in props assert 'entropy' in props assert props['pressure'] == 500000 def test_invalid_refrigerant(): with pytest.raises(FileNotFoundError): RefrigerantEngine("R999") ``` ### Tâche 4.2: Tests d'intégration **tests/test_api/test_diagram.py** ```python from fastapi.testclient import TestClient from app.main import app client = TestClient(app) def test_generate_diagram(): response = client.post( "/api/v1/diagram/generate", json={ "refrigerant": "R134a", "output_format": "plotly_json", "points": [ { "type": "PT", "pressure": 500000, "temperature": 5, "order": 1 } ] } ) assert response.status_code == 200 data = response.json() assert data['success'] is True assert 'data' in data ``` --- ## Phase 5: Docker & AWS (Durée: 2-3 jours) ### Tâche 5.1: Containerisation ```bash # Build image docker build -f docker/Dockerfile -t diagram-ph-api:latest . # Test local docker run -p 8000:8000 diagram-ph-api:latest # Test avec docker-compose docker-compose -f docker/docker-compose.yml up ``` ### Tâche 5.2: Déploiement AWS ```bash # Créer repository ECR aws ecr create-repository --repository-name diagram-ph-api # Push image docker push # Initialiser EB eb init -p docker diagram-ph-api # Créer environnement eb create diagram-ph-api-prod # Déployer ./deployment/scripts/deploy.sh ``` --- ## Phase 6: Documentation & Finalisation (Durée: 1-2 jours) ### Tâche 6.1: Documentation utilisateur **docs/EXAMPLES.md** - Créer exemples pour chaque endpoint ### Tâche 6.2: Tests finaux - [ ] Tests de charge (load testing) - [ ] Tests de sécurité - [ ] Validation sur environnement staging --- ## Métriques de succès | Métrique | Cible | Status | |----------|-------|--------| | Code coverage | > 80% | ⏳ | | Latence P95 | < 500ms | ⏳ | | Disponibilité | > 99% | ⏳ | | Taux d'erreur | < 0.1% | ⏳ | --- ## Risques & Mitigation | Risque | Impact | Probabilité | Mitigation | |--------|--------|-------------|------------| | DLL/SO incompatibles Linux | Élevé | Moyen | Tester .so tôt, avoir plan B | | Performance insuffisante | Moyen | Faible | Cache agressif, optimisation | | Coûts AWS élevés | Moyen | Moyen | Monitoring, auto-scaling | | Problèmes calculs thermodynamiques | Élevé | Faible | Tests exhaustifs, validation | --- ## Timeline estimé | Phase | Durée | Dates suggérées | |-------|-------|-----------------| | Phase 1: Configuration | 1-2 jours | Jour 1-2 | | Phase 2: Modules Core | 3-4 jours | Jour 3-6 | | Phase 3: API REST | 3-4 jours | Jour 7-10 | | Phase 4: Tests | 2-3 jours | Jour 11-13 | | Phase 5: Docker & AWS | 2-3 jours | Jour 14-16 | | Phase 6: Documentation | 1-2 jours | Jour 17-18 | | **TOTAL** | **12-18 jours** | **~3 semaines** | --- ## Prochaines étapes immédiates 1. ✅ Valider le plan avec l'équipe 2. 🔄 Préparer fichiers .so Linux 3. ⏳ Initialiser repository Git 4. ⏳ Créer structure projet 5. ⏳ Commencer Phase 1