19 KiB
19 KiB
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:
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:
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:
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
# 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
# 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
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
# 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é
# 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
- Cache agressif: Propriétés thermodynamiques rarement changent
- Calculs parallèles:
asynciopour I/O,multiprocessingpour calculs lourds - Pré-calcul: Courbes de saturation pré-calculées au démarrage
- Compression: Gzip pour réponses JSON volumineuses
Sécurité
Mesures de sécurité
- HTTPS obligatoire: Certificat SSL/TLS via AWS
- CORS configuré: Liste blanche de domaines autorisés
- Rate limiting: 100 req/min par IP
- Validation stricte: Pydantic pour tous les inputs
- Sanitization: Pas d'eval() ou exec() sur inputs utilisateur
- Logs d'audit: Traçabilité de toutes les requêtes
# 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
- ✅ Spécifications API complètes
- ✅ Architecture système définie
- 🔄 Implémentation des modules core
- 🔄 Configuration Docker
- ⏳ Déploiement AWS Elastic Beanstalk
- ⏳ Tests de charge et optimisation