diagram_ph/ARCHITECTURE.md

19 KiB
Raw Permalink Blame History

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

  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
# 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