643 lines
17 KiB
Markdown
643 lines
17 KiB
Markdown
# 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 <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
|
|
|
|
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 |