# 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