diagram_ph/ARCHITECTURE.md

606 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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