17 KiB
17 KiB
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
# 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
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
[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
# 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
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
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
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
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
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
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
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
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
# 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
# Créer repository ECR
aws ecr create-repository --repository-name diagram-ph-api
# Push image
docker push <ecr-repo-url>
# 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
- ✅ Valider le plan avec l'équipe
- 🔄 Préparer fichiers .so Linux
- ⏳ Initialiser repository Git
- ⏳ Créer structure projet
- ⏳ Commencer Phase 1