606 lines
19 KiB
Markdown
606 lines
19 KiB
Markdown
# Architecture Technique - API Diagramme PH
|
||
|
||
## Vue d'ensemble du système
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Client Applications │
|
||
│ (Jupyter Notebook, React App, Mobile App, CLI Tools) │
|
||
└────────────────────┬────────────────────────────────────────┘
|
||
│ HTTPS/REST
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ AWS Elastic Beanstalk (Load Balancer) │
|
||
└────────────────────┬────────────────────────────────────────┘
|
||
│
|
||
┌────────────┼────────────┐
|
||
▼ ▼ ▼
|
||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||
│ API Server │ │ API Server │ │ API Server │
|
||
│ Instance 1 │ │ Instance 2 │ │ Instance N │
|
||
└──────────────┘ └──────────────┘ └──────────────┘
|
||
│ │ │
|
||
└────────────────┴────────────────┘
|
||
│
|
||
┌───────────────┴───────────────┐
|
||
│ │
|
||
▼ ▼
|
||
┌─────────────┐ ┌──────────────┐
|
||
│ DLL/SO Libs │ │ CloudWatch │
|
||
│ (Refrigerant│ │ (Monitoring) │
|
||
│ Properties)│ └──────────────┘
|
||
└─────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Structure du projet
|
||
|
||
```
|
||
diagram-ph-api/
|
||
├── app/
|
||
│ ├── __init__.py
|
||
│ ├── main.py # Point d'entrée FastAPI
|
||
│ ├── config.py # Configuration (env vars)
|
||
│ │
|
||
│ ├── api/
|
||
│ │ ├── __init__.py
|
||
│ │ ├── v1/
|
||
│ │ │ ├── __init__.py
|
||
│ │ │ ├── endpoints/
|
||
│ │ │ │ ├── __init__.py
|
||
│ │ │ │ ├── health.py # Health check endpoint
|
||
│ │ │ │ ├── refrigerants.py # Liste réfrigérants
|
||
│ │ │ │ ├── diagram.py # Génération diagrammes
|
||
│ │ │ │ ├── calculations.py # Calculs thermodynamiques
|
||
│ │ │ │ ├── cycle.py # Calculs cycle frigorifique
|
||
│ │ │ │ └── properties.py # Propriétés à un point
|
||
│ │ │ │
|
||
│ │ │ └── router.py # Router principal v1
|
||
│ │ │
|
||
│ │ └── dependencies.py # Dépendances FastAPI
|
||
│ │
|
||
│ ├── core/
|
||
│ │ ├── __init__.py
|
||
│ │ ├── refrigerant_engine.py # Wrapper DLL/SO + cache
|
||
│ │ ├── diagram_generator.py # Génération diagrammes
|
||
│ │ ├── cycle_calculator.py # Calculs COP, puissance
|
||
│ │ ├── economizer.py # Logique économiseur
|
||
│ │ └── cache.py # Système de cache
|
||
│ │
|
||
│ ├── models/
|
||
│ │ ├── __init__.py
|
||
│ │ ├── requests.py # Pydantic request models
|
||
│ │ ├── responses.py # Pydantic response models
|
||
│ │ └── enums.py # Enums (types, formats)
|
||
│ │
|
||
│ ├── services/
|
||
│ │ ├── __init__.py
|
||
│ │ ├── diagram_service.py # Business logic diagrammes
|
||
│ │ ├── calculation_service.py # Business logic calculs
|
||
│ │ └── validation_service.py # Validation thermodynamique
|
||
│ │
|
||
│ └── utils/
|
||
│ ├── __init__.py
|
||
│ ├── logger.py # Configuration logging
|
||
│ ├── exceptions.py # Custom exceptions
|
||
│ └── helpers.py # Fonctions utilitaires
|
||
│
|
||
├── libs/
|
||
│ ├── __init__.py
|
||
│ ├── dll/ # DLL Windows
|
||
│ │ ├── R134a.dll
|
||
│ │ ├── R410A.dll
|
||
│ │ ├── refifc.dll
|
||
│ │ └── ...
|
||
│ │
|
||
│ ├── so/ # Shared Objects Linux
|
||
│ │ ├── libR134a.so
|
||
│ │ ├── libR410A.so
|
||
│ │ ├── librefifc.so
|
||
│ │ └── ...
|
||
│ │
|
||
│ └── simple_refrig_api.py # Interface DLL/SO
|
||
│
|
||
├── tests/
|
||
│ ├── __init__.py
|
||
│ ├── conftest.py # Pytest config
|
||
│ ├── test_api/
|
||
│ │ ├── test_health.py
|
||
│ │ ├── test_diagram.py
|
||
│ │ └── test_calculations.py
|
||
│ │
|
||
│ ├── test_core/
|
||
│ │ ├── test_refrigerant_engine.py
|
||
│ │ └── test_cycle_calculator.py
|
||
│ │
|
||
│ └── test_services/
|
||
│ └── test_diagram_service.py
|
||
│
|
||
├── docker/
|
||
│ ├── Dockerfile # Image Docker production
|
||
│ ├── Dockerfile.dev # Image Docker développement
|
||
│ └── docker-compose.yml # Composition locale
|
||
│
|
||
├── deployment/
|
||
│ ├── aws/
|
||
│ │ ├── Dockerrun.aws.json # Config Elastic Beanstalk
|
||
│ │ ├── .ebextensions/ # Extensions EB
|
||
│ │ │ ├── 01_packages.config # Packages système
|
||
│ │ │ └── 02_python.config # Config Python
|
||
│ │ │
|
||
│ │ └── cloudwatch-config.json # Métriques CloudWatch
|
||
│ │
|
||
│ └── scripts/
|
||
│ ├── deploy.sh # Script déploiement
|
||
│ └── health_check.sh # Vérification santé
|
||
│
|
||
├── docs/
|
||
│ ├── API_SPECIFICATION.md # Spécifications API (✓)
|
||
│ ├── ARCHITECTURE.md # Ce document (en cours)
|
||
│ ├── DEPLOYMENT.md # Guide déploiement
|
||
│ └── EXAMPLES.md # Exemples d'utilisation
|
||
│
|
||
├── .env.example # Variables d'environnement exemple
|
||
├── .gitignore
|
||
├── requirements.txt # Dépendances Python
|
||
├── requirements-dev.txt # Dépendances développement
|
||
├── pyproject.toml # Config projet Python
|
||
├── pytest.ini # Config pytest
|
||
└── README.md # Documentation principale
|
||
```
|
||
|
||
---
|
||
|
||
## Modules principaux
|
||
|
||
### 1. RefrigerantEngine (`core/refrigerant_engine.py`)
|
||
|
||
**Responsabilités**:
|
||
- Chargement dynamique des DLL/SO selon l'OS
|
||
- Gestion du cache des propriétés calculées
|
||
- Interface unifiée pour tous les réfrigérants
|
||
- Gestion des erreurs DLL
|
||
|
||
**Pseudocode**:
|
||
```python
|
||
class RefrigerantEngine:
|
||
def __init__(self, refrigerant_name: str):
|
||
self.refrigerant = refrigerant_name
|
||
self.lib = self._load_library()
|
||
self.cache = LRUCache(maxsize=1000)
|
||
|
||
def _load_library(self):
|
||
"""Charge DLL (Windows) ou SO (Linux)"""
|
||
if os.name == 'nt':
|
||
return ctypes.CDLL(f"libs/dll/{self.refrigerant}.dll")
|
||
else:
|
||
return ctypes.CDLL(f"libs/so/lib{self.refrigerant}.so")
|
||
|
||
@lru_cache(maxsize=1000)
|
||
def get_properties_PT(self, pressure: float, temperature: float):
|
||
"""Propriétés à partir de P et T (avec cache)"""
|
||
# Appels DLL + validation
|
||
return properties_dict
|
||
|
||
def get_saturation_curve(self):
|
||
"""Courbe de saturation pour le diagramme"""
|
||
return {
|
||
"liquid_line": [...],
|
||
"vapor_line": [...]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2. DiagramGenerator (`core/diagram_generator.py`)
|
||
|
||
**Responsabilités**:
|
||
- Génération de diagrammes PH en différents formats
|
||
- Styling et configuration des graphiques
|
||
- Conversion format (Matplotlib → PNG, Plotly → JSON/HTML)
|
||
|
||
**Formats supportés**:
|
||
- `matplotlib_png`: Image PNG base64 (pour Jupyter, rapports PDF)
|
||
- `plotly_json`: JSON Plotly (pour React, applications web)
|
||
- `plotly_html`: HTML standalone (pour emails, visualisation rapide)
|
||
|
||
**Pseudocode**:
|
||
```python
|
||
class DiagramGenerator:
|
||
def __init__(self, refrigerant_engine: RefrigerantEngine):
|
||
self.engine = refrigerant_engine
|
||
|
||
def generate_diagram(
|
||
self,
|
||
points: List[Point],
|
||
format: OutputFormat,
|
||
options: DiagramOptions
|
||
) -> DiagramOutput:
|
||
"""Génère le diagramme selon le format demandé"""
|
||
|
||
# 1. Récupérer courbe de saturation
|
||
saturation = self.engine.get_saturation_curve()
|
||
|
||
# 2. Calculer isothermes
|
||
isotherms = self._calculate_isotherms(options)
|
||
|
||
# 3. Calculer propriétés des points utilisateur
|
||
calculated_points = [
|
||
self.engine.get_properties(**point.dict())
|
||
for point in points
|
||
]
|
||
|
||
# 4. Générer selon format
|
||
if format == OutputFormat.MATPLOTLIB_PNG:
|
||
return self._generate_matplotlib(
|
||
saturation, isotherms, calculated_points
|
||
)
|
||
elif format == OutputFormat.PLOTLY_JSON:
|
||
return self._generate_plotly_json(
|
||
saturation, isotherms, calculated_points
|
||
)
|
||
```
|
||
|
||
---
|
||
|
||
### 3. CycleCalculator (`core/cycle_calculator.py`)
|
||
|
||
**Responsabilités**:
|
||
- Calculs de COP (Coefficient Of Performance)
|
||
- Puissances frigorifiques et calorifiques
|
||
- Rendements isentropique, volumétrique
|
||
- Support cycles économiseur
|
||
|
||
**Formules principales**:
|
||
|
||
```
|
||
COP_froid = Q_evap / W_comp = (h1 - h4) / (h2 - h1)
|
||
COP_chaud = Q_cond / W_comp = (h2 - h3) / (h2 - h1)
|
||
|
||
où:
|
||
- Point 1: Sortie évaporateur (aspiration compresseur)
|
||
- Point 2: Sortie compresseur
|
||
- Point 3: Sortie condenseur (entrée détendeur)
|
||
- Point 4: Sortie détendeur (entrée évaporateur)
|
||
|
||
Puissance frigorifique: Q_evap = ṁ × (h1 - h4)
|
||
Puissance compresseur: W_comp = ṁ × (h2 - h1)
|
||
|
||
Rendement isentropique: η_is = (h2s - h1) / (h2 - h1)
|
||
```
|
||
|
||
**Pseudocode**:
|
||
```python
|
||
class CycleCalculator:
|
||
def calculate_standard_cycle(
|
||
self,
|
||
points: CyclePoints,
|
||
mass_flow: float,
|
||
efficiencies: Efficiencies
|
||
) -> CycleResults:
|
||
"""Calcul cycle frigorifique standard"""
|
||
|
||
# Calcul des différences d'enthalpie
|
||
h1, h2, h3, h4 = [p.enthalpy for p in points]
|
||
|
||
# Puissances
|
||
q_evap = mass_flow * (h1 - h4)
|
||
q_cond = mass_flow * (h2 - h3)
|
||
w_comp = mass_flow * (h2 - h1)
|
||
|
||
# COP
|
||
cop_cooling = q_evap / w_comp
|
||
cop_heating = q_cond / w_comp
|
||
|
||
# Rendements
|
||
h2s = self._calc_isentropic_enthalpy(points[0], points[1].pressure)
|
||
eta_is = (h2s - h1) / (h2 - h1)
|
||
|
||
return CycleResults(
|
||
cop_cooling=cop_cooling,
|
||
cop_heating=cop_heating,
|
||
cooling_capacity=q_evap,
|
||
compressor_power=w_comp,
|
||
efficiencies={...}
|
||
)
|
||
```
|
||
|
||
---
|
||
|
||
### 4. EconomizerCalculator (`core/economizer.py`)
|
||
|
||
**Responsabilités**:
|
||
- Calculs pour cycles avec économiseur
|
||
- Optimisation pression intermédiaire
|
||
- Calcul des gains de performance
|
||
|
||
**Principe économiseur**:
|
||
```
|
||
L'économiseur améliore le COP en:
|
||
1. Sous-refroidissant le liquide avant détente principale
|
||
2. Injectant vapeur flash au compresseur (pression intermédiaire)
|
||
3. Réduisant la quantité de liquide à évaporer
|
||
|
||
Gain COP typique: 5-15%
|
||
```
|
||
|
||
---
|
||
|
||
## Gestion DLL/SO multi-plateforme
|
||
|
||
### Stratégie de chargement
|
||
|
||
```python
|
||
# libs/simple_refrig_api.py (amélioré)
|
||
|
||
import os
|
||
import platform
|
||
import ctypes
|
||
from pathlib import Path
|
||
|
||
class RefrigLibraryLoader:
|
||
"""Gestionnaire de chargement DLL/SO multi-plateforme"""
|
||
|
||
BASE_DIR = Path(__file__).parent
|
||
|
||
@classmethod
|
||
def get_library_path(cls, refrigerant: str) -> Path:
|
||
"""Retourne le chemin de la bibliothèque selon l'OS"""
|
||
|
||
system = platform.system()
|
||
|
||
if system == "Windows":
|
||
return cls.BASE_DIR / "dll" / f"{refrigerant}.dll"
|
||
elif system == "Linux":
|
||
return cls.BASE_DIR / "so" / f"lib{refrigerant}.so"
|
||
elif system == "Darwin": # macOS
|
||
return cls.BASE_DIR / "dylib" / f"lib{refrigerant}.dylib"
|
||
else:
|
||
raise OSError(f"Unsupported OS: {system}")
|
||
|
||
@classmethod
|
||
def load_refrigerant(cls, refrigerant: str):
|
||
"""Charge la bibliothèque du réfrigérant"""
|
||
lib_path = cls.get_library_path(refrigerant)
|
||
|
||
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}"
|
||
)
|
||
```
|
||
|
||
### Vérification au démarrage
|
||
|
||
```python
|
||
# app/main.py
|
||
|
||
@app.on_event("startup")
|
||
async def verify_refrigerant_libraries():
|
||
"""Vérifie que toutes les DLL/SO sont disponibles"""
|
||
|
||
logger.info("Checking refrigerant libraries...")
|
||
|
||
available = []
|
||
missing = []
|
||
|
||
for refrigerant in SUPPORTED_REFRIGERANTS:
|
||
try:
|
||
lib_path = RefrigLibraryLoader.get_library_path(refrigerant)
|
||
if lib_path.exists():
|
||
# Test de chargement
|
||
RefrigLibraryLoader.load_refrigerant(refrigerant)
|
||
available.append(refrigerant)
|
||
else:
|
||
missing.append(refrigerant)
|
||
except Exception as e:
|
||
logger.error(f"Error loading {refrigerant}: {e}")
|
||
missing.append(refrigerant)
|
||
|
||
logger.info(f"Available refrigerants: {available}")
|
||
if missing:
|
||
logger.warning(f"Missing refrigerants: {missing}")
|
||
|
||
# Stocker dans app state
|
||
app.state.available_refrigerants = available
|
||
```
|
||
|
||
---
|
||
|
||
## Système de cache
|
||
|
||
### Cache multi-niveaux
|
||
|
||
```python
|
||
from functools import lru_cache
|
||
from cachetools import TTLCache
|
||
import hashlib
|
||
import json
|
||
|
||
class PropertyCache:
|
||
"""Cache à 3 niveaux pour optimiser les calculs"""
|
||
|
||
def __init__(self):
|
||
# Niveau 1: Cache mémoire LRU (rapide)
|
||
self.memory_cache = LRUCache(maxsize=10000)
|
||
|
||
# Niveau 2: Cache TTL (expire après temps)
|
||
self.ttl_cache = TTLCache(maxsize=50000, ttl=3600)
|
||
|
||
# Niveau 3: Redis (optionnel, pour multi-instance)
|
||
self.redis_client = None # À configurer si besoin
|
||
|
||
def _generate_key(self, refrigerant, method, **params):
|
||
"""Génère clé de cache unique"""
|
||
data = {
|
||
"refrigerant": refrigerant,
|
||
"method": method,
|
||
**params
|
||
}
|
||
json_str = json.dumps(data, sort_keys=True)
|
||
return hashlib.md5(json_str.encode()).hexdigest()
|
||
|
||
def get(self, key):
|
||
"""Récupère depuis cache (multi-niveaux)"""
|
||
# Essayer niveau 1
|
||
if key in self.memory_cache:
|
||
return self.memory_cache[key]
|
||
|
||
# Essayer niveau 2
|
||
if key in self.ttl_cache:
|
||
value = self.ttl_cache[key]
|
||
self.memory_cache[key] = value # Promouvoir
|
||
return value
|
||
|
||
return None
|
||
|
||
def set(self, key, value):
|
||
"""Stocke dans cache"""
|
||
self.memory_cache[key] = value
|
||
self.ttl_cache[key] = value
|
||
```
|
||
|
||
---
|
||
|
||
## Monitoring et logging
|
||
|
||
### Métriques CloudWatch
|
||
|
||
```python
|
||
# app/utils/metrics.py
|
||
|
||
import boto3
|
||
from datetime import datetime
|
||
|
||
class CloudWatchMetrics:
|
||
"""Envoi métriques vers CloudWatch"""
|
||
|
||
def __init__(self):
|
||
self.cloudwatch = boto3.client('cloudwatch')
|
||
self.namespace = 'DiagramPH/API'
|
||
|
||
def record_api_call(self, endpoint: str, duration_ms: float, status_code: int):
|
||
"""Enregistre métrique appel API"""
|
||
self.cloudwatch.put_metric_data(
|
||
Namespace=self.namespace,
|
||
MetricData=[
|
||
{
|
||
'MetricName': 'APICallDuration',
|
||
'Value': duration_ms,
|
||
'Unit': 'Milliseconds',
|
||
'Timestamp': datetime.utcnow(),
|
||
'Dimensions': [
|
||
{'Name': 'Endpoint', 'Value': endpoint},
|
||
{'Name': 'StatusCode', 'Value': str(status_code)}
|
||
]
|
||
}
|
||
]
|
||
)
|
||
|
||
def record_calculation_error(self, refrigerant: str, error_type: str):
|
||
"""Enregistre erreur de calcul"""
|
||
# Similar pattern...
|
||
```
|
||
|
||
### Logging structuré
|
||
|
||
```python
|
||
# app/utils/logger.py
|
||
|
||
import logging
|
||
import json
|
||
from pythonjsonlogger import jsonlogger
|
||
|
||
def setup_logging():
|
||
"""Configure logging JSON structuré"""
|
||
|
||
logger = logging.getLogger()
|
||
logger.setLevel(logging.INFO)
|
||
|
||
handler = logging.StreamHandler()
|
||
formatter = jsonlogger.JsonFormatter(
|
||
'%(timestamp)s %(level)s %(name)s %(message)s'
|
||
)
|
||
handler.setFormatter(formatter)
|
||
logger.addHandler(handler)
|
||
|
||
return logger
|
||
|
||
# Usage dans l'API
|
||
logger.info(
|
||
"Diagram generated",
|
||
extra={
|
||
"refrigerant": "R134a",
|
||
"points_count": 4,
|
||
"format": "plotly_json",
|
||
"duration_ms": 245
|
||
}
|
||
)
|
||
```
|
||
|
||
---
|
||
|
||
## Performance et scalabilité
|
||
|
||
### Objectifs de performance
|
||
|
||
| Métrique | Cible | Justification |
|
||
|----------|-------|---------------|
|
||
| Latence P50 | < 200ms | Expérience utilisateur fluide |
|
||
| Latence P95 | < 500ms | Acceptable pour calculs complexes |
|
||
| Latence P99 | < 1000ms | Timeout raisonnable |
|
||
| Throughput | 100 req/s/instance | Suffisant pour démarrage |
|
||
| Taux d'erreur | < 0.1% | Haute fiabilité |
|
||
|
||
### Optimisations
|
||
|
||
1. **Cache agressif**: Propriétés thermodynamiques rarement changent
|
||
2. **Calculs parallèles**: `asyncio` pour I/O, `multiprocessing` pour calculs lourds
|
||
3. **Pré-calcul**: Courbes de saturation pré-calculées au démarrage
|
||
4. **Compression**: Gzip pour réponses JSON volumineuses
|
||
|
||
---
|
||
|
||
## Sécurité
|
||
|
||
### Mesures de sécurité
|
||
|
||
1. **HTTPS obligatoire**: Certificat SSL/TLS via AWS
|
||
2. **CORS configuré**: Liste blanche de domaines autorisés
|
||
3. **Rate limiting**: 100 req/min par IP
|
||
4. **Validation stricte**: Pydantic pour tous les inputs
|
||
5. **Sanitization**: Pas d'eval() ou exec() sur inputs utilisateur
|
||
6. **Logs d'audit**: Traçabilité de toutes les requêtes
|
||
|
||
```python
|
||
# app/api/dependencies.py
|
||
|
||
from fastapi import Request, HTTPException
|
||
from slowapi import Limiter
|
||
from slowapi.util import get_remote_address
|
||
|
||
limiter = Limiter(key_func=get_remote_address)
|
||
|
||
@limiter.limit("100/minute")
|
||
async def rate_limit_check(request: Request):
|
||
"""Rate limiting par IP"""
|
||
pass
|
||
```
|
||
|
||
---
|
||
|
||
## Prochaines étapes
|
||
|
||
1. ✅ Spécifications API complètes
|
||
2. ✅ Architecture système définie
|
||
3. 🔄 Implémentation des modules core
|
||
4. 🔄 Configuration Docker
|
||
5. ⏳ Déploiement AWS Elastic Beanstalk
|
||
6. ⏳ Tests de charge et optimisation |