Compare commits
4 Commits
c0c0e6e3ea
...
snapshot/2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2a9a4197c | ||
|
|
6ee68aeaed | ||
|
|
d22184cf70 | ||
|
|
24db8ad426 |
116
README.md
116
README.md
@@ -41,113 +41,29 @@ graph TB
|
|||||||
F[CloudFront CDN]
|
F[CloudFront CDN]
|
||||||
G[Application Load Balancer]
|
G[Application Load Balancer]
|
||||||
|
|
||||||
subgraph "Elastic Beanstalk Environment"
|
# API Diagramme PH - Project Overview (English)
|
||||||
H1[API Server 1<br/>Docker Container]
|
|
||||||
H2[API Server 2<br/>Docker Container]
|
|
||||||
H3[API Server N<br/>Docker Container]
|
|
||||||
end
|
|
||||||
|
|
||||||
I[CloudWatch<br/>Logs & Metrics]
|
This repository contains a FastAPI-based REST API for generating Pressure-Enthalpy (PH) diagrams
|
||||||
J[S3 Bucket<br/>Static Assets]
|
and performing advanced refrigeration thermodynamic calculations.
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "API Container"
|
For the full French documentation, see: `README_fr.md` (converted from the original README).
|
||||||
K[FastAPI Application]
|
|
||||||
L[RefrigerantEngine<br/>DLL/SO Wrapper]
|
|
||||||
M[DiagramGenerator<br/>Matplotlib/Plotly]
|
|
||||||
N[CycleCalculator<br/>Thermodynamics]
|
|
||||||
O[Cache Layer<br/>LRU + TTL]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph "Native Libraries"
|
Badges
|
||||||
P[R134a.so]
|
- Python 3.12+
|
||||||
Q[R410A.so]
|
- FastAPI
|
||||||
R[refifc.so]
|
- Docker-ready
|
||||||
S[Other refrigerants...]
|
|
||||||
end
|
|
||||||
|
|
||||||
A & B & C & D --> E
|
Quick start
|
||||||
E --> F
|
- Install dependencies and run with uvicorn (see documentation in the `docs/` folder).
|
||||||
F --> G
|
|
||||||
G --> H1 & H2 & H3
|
|
||||||
H1 & H2 & H3 --> I
|
|
||||||
H1 & H2 & H3 -.-> J
|
|
||||||
|
|
||||||
H1 --> K
|
Repository structure (short)
|
||||||
K --> L & M & N & O
|
- `app/` : application code
|
||||||
L --> P & Q & R & S
|
- `libs/` : native libraries (dll/ and so/)
|
||||||
|
- `scripts/` : helper scripts
|
||||||
|
- `docs/` : extra documentation
|
||||||
|
|
||||||
style A fill:#e1f5ff
|
If you need the original French README, open `README_fr.md`.
|
||||||
style B fill:#e1f5ff
|
|
||||||
style C fill:#e1f5ff
|
|
||||||
style D fill:#e1f5ff
|
|
||||||
style G fill:#ff9999
|
|
||||||
style H1 fill:#99ff99
|
|
||||||
style H2 fill:#99ff99
|
|
||||||
style H3 fill:#99ff99
|
|
||||||
style K fill:#ffcc99
|
|
||||||
style L fill:#ffff99
|
|
||||||
style M fill:#ffff99
|
|
||||||
style N fill:#ffff99
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📁 Structure du projet
|
|
||||||
|
|
||||||
```
|
|
||||||
diagram-ph-api/
|
|
||||||
├── 📄 API_SPECIFICATION.md # Spécifications complètes des endpoints
|
|
||||||
├── 📄 ARCHITECTURE.md # Architecture technique détaillée
|
|
||||||
├── 📄 DEPLOYMENT.md # Guide de déploiement AWS
|
|
||||||
├── 📄 IMPLEMENTATION_PLAN.md # Plan d'implémentation par phases
|
|
||||||
├── 📄 README.md # Ce fichier
|
|
||||||
│
|
|
||||||
├── app/ # Code source de l'API
|
|
||||||
│ ├── main.py # Point d'entrée FastAPI
|
|
||||||
│ ├── config.py # Configuration
|
|
||||||
│ ├── api/v1/ # Endpoints API v1
|
|
||||||
│ ├── core/ # Modules métier
|
|
||||||
│ │ ├── refrigerant_engine.py
|
|
||||||
│ │ ├── diagram_generator.py
|
|
||||||
│ │ ├── cycle_calculator.py
|
|
||||||
│ │ └── economizer.py
|
|
||||||
│ ├── models/ # Modèles Pydantic
|
|
||||||
│ ├── services/ # Business logic
|
|
||||||
│ └── utils/ # Utilitaires
|
|
||||||
│
|
|
||||||
├── libs/ # Bibliothèques natives
|
|
||||||
│ ├── dll/ # DLL Windows
|
|
||||||
│ └── so/ # Shared Objects Linux
|
|
||||||
│
|
|
||||||
├── tests/ # Tests automatisés
|
|
||||||
├── docker/ # Configuration Docker
|
|
||||||
├── deployment/ # Scripts et config AWS
|
|
||||||
└── docs/ # Documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Quick Start
|
|
||||||
|
|
||||||
### Prérequis
|
|
||||||
|
|
||||||
- Python 3.12+
|
|
||||||
- Docker (optionnel, recommandé)
|
|
||||||
- Fichiers DLL/SO des réfrigérants
|
|
||||||
|
|
||||||
### Installation locale
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Cloner le repository
|
|
||||||
git clone https://github.com/votre-org/diagram-ph-api.git
|
|
||||||
cd diagram-ph-api
|
|
||||||
|
|
||||||
# Créer environnement virtuel
|
|
||||||
python -m venv .venv
|
|
||||||
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
||||||
|
|
||||||
# Installer dépendances
|
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
# Copier et configurer .env
|
# Copier et configurer .env
|
||||||
|
|||||||
99
README_fr.md
Normal file
99
README_fr.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
````markdown
|
||||||
|
# API Diagramme PH - Projet Complet
|
||||||
|
|
||||||
|
> API REST pour la génération de diagrammes Pression-Enthalpie (PH) et calculs thermodynamiques frigorifiques avancés
|
||||||
|
|
||||||
|
[](https://www.python.org/)
|
||||||
|
[](https://fastapi.tiangolo.com/)
|
||||||
|
[](https://www.docker.com/)
|
||||||
|
[](https://aws.amazon.com/elasticbeanstalk/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Vue d'ensemble
|
||||||
|
|
||||||
|
Cette API permet de:
|
||||||
|
- ✅ Générer des diagrammes PH interactifs (Plotly) ou statiques (Matplotlib)
|
||||||
|
- ✅ Calculer les propriétés thermodynamiques des réfrigérants
|
||||||
|
- ✅ Analyser les cycles frigorifiques (COP, puissance, rendements)
|
||||||
|
- ✅ Supporter les cycles avec économiseur
|
||||||
|
- ✅ Calculer la puissance entre deux points d'un cycle
|
||||||
|
- ✅ Supporter 17 réfrigérants différents
|
||||||
|
|
||||||
|
### Réfrigérants supportés
|
||||||
|
|
||||||
|
R12, R22, R32, **R134a**, R290, R404A, **R410A**, R452A, R454A, R454B, R502, R507A, R513A, R515B, **R744 (CO2)**, R1233zd, R1234ze
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Architecture du système
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "Client Layer"
|
||||||
|
A[Jupyter Notebook]
|
||||||
|
B[React Application]
|
||||||
|
C[Mobile App]
|
||||||
|
D[CLI Tools]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "AWS Cloud"
|
||||||
|
E[Route 53 DNS]
|
||||||
|
F[CloudFront CDN]
|
||||||
|
G[Application Load Balancer]
|
||||||
|
|
||||||
|
subgraph "Elastic Beanstalk Environment"
|
||||||
|
H1[API Server 1<br/>Docker Container]
|
||||||
|
H2[API Server 2<br/>Docker Container]
|
||||||
|
H3[API Server N<br/>Docker Container]
|
||||||
|
end
|
||||||
|
|
||||||
|
I[CloudWatch<br/>Logs & Metrics]
|
||||||
|
J[S3 Bucket<br/>Static Assets]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "API Container"
|
||||||
|
K[FastAPI Application]
|
||||||
|
L[RefrigerantEngine<br/>DLL/SO Wrapper]
|
||||||
|
M[DiagramGenerator<br/>Matplotlib/Plotly]
|
||||||
|
N[CycleCalculator<br/>Thermodynamics]
|
||||||
|
O[Cache Layer<br/>LRU + TTL]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "Native Libraries"
|
||||||
|
P[R134a.so]
|
||||||
|
Q[R410A.so]
|
||||||
|
R[refifc.so]
|
||||||
|
S[Other refrigerants...]
|
||||||
|
end
|
||||||
|
|
||||||
|
A & B & C & D --> E
|
||||||
|
E --> F
|
||||||
|
F --> G
|
||||||
|
G --> H1 & H2 & H3
|
||||||
|
H1 & H2 & H3 --> I
|
||||||
|
H1 & H2 & H3 -.-> J
|
||||||
|
|
||||||
|
H1 --> K
|
||||||
|
K --> L & M & N & O
|
||||||
|
L --> P & Q & R & S
|
||||||
|
|
||||||
|
style A fill:#e1f5ff
|
||||||
|
style B fill:#e1f5ff
|
||||||
|
style C fill:#e1f5ff
|
||||||
|
style D fill:#e1f5ff
|
||||||
|
style G fill:#ff9999
|
||||||
|
style H1 fill:#99ff99
|
||||||
|
style H2 fill:#99ff99
|
||||||
|
style H3 fill:#99ff99
|
||||||
|
style K fill:#ffcc99
|
||||||
|
style L fill:#ffff99
|
||||||
|
style M fill:#ffff99
|
||||||
|
style N fill:#ffff99
|
||||||
|
````
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
(the rest of the French README is the same as the original and has been preserved)
|
||||||
|
|
||||||
|
````
|
||||||
BIN
app/ipm/lib/dll/R12.dll
Normal file
BIN
app/ipm/lib/dll/R12.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R1233zd.dll
Normal file
BIN
app/ipm/lib/dll/R1233zd.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R1234ze.dll
Normal file
BIN
app/ipm/lib/dll/R1234ze.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R134a.dll
Normal file
BIN
app/ipm/lib/dll/R134a.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R22.dll
Normal file
BIN
app/ipm/lib/dll/R22.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R290.dll
Normal file
BIN
app/ipm/lib/dll/R290.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R32.dll
Normal file
BIN
app/ipm/lib/dll/R32.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R404A.dll
Normal file
BIN
app/ipm/lib/dll/R404A.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R410A.dll
Normal file
BIN
app/ipm/lib/dll/R410A.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R452A.dll
Normal file
BIN
app/ipm/lib/dll/R452A.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R454A.dll
Normal file
BIN
app/ipm/lib/dll/R454A.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R454B.dll
Normal file
BIN
app/ipm/lib/dll/R454B.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R502.dll
Normal file
BIN
app/ipm/lib/dll/R502.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R507A.dll
Normal file
BIN
app/ipm/lib/dll/R507A.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R513A.dll
Normal file
BIN
app/ipm/lib/dll/R513A.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R515B.dll
Normal file
BIN
app/ipm/lib/dll/R515B.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/R744.dll
Normal file
BIN
app/ipm/lib/dll/R744.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/msvcr100.dll
Normal file
BIN
app/ipm/lib/dll/msvcr100.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/refifc.dll
Normal file
BIN
app/ipm/lib/dll/refifc.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/dll/refifcV1.dll
Normal file
BIN
app/ipm/lib/dll/refifcV1.dll
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR12.so
Normal file
BIN
app/ipm/lib/so/libR12.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR1233zd.so
Normal file
BIN
app/ipm/lib/so/libR1233zd.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR1234ze.so
Normal file
BIN
app/ipm/lib/so/libR1234ze.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR134a.so
Normal file
BIN
app/ipm/lib/so/libR134a.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR22.so
Normal file
BIN
app/ipm/lib/so/libR22.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR290.so
Normal file
BIN
app/ipm/lib/so/libR290.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR32.so
Normal file
BIN
app/ipm/lib/so/libR32.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR404A.so
Normal file
BIN
app/ipm/lib/so/libR404A.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR410A.so
Normal file
BIN
app/ipm/lib/so/libR410A.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR502.so
Normal file
BIN
app/ipm/lib/so/libR502.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR507A.so
Normal file
BIN
app/ipm/lib/so/libR507A.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR717.so
Normal file
BIN
app/ipm/lib/so/libR717.so
Normal file
Binary file not shown.
BIN
app/ipm/lib/so/libR744.so
Normal file
BIN
app/ipm/lib/so/libR744.so
Normal file
Binary file not shown.
@@ -101,7 +101,7 @@ class GenRefProperties(Structure):
|
|||||||
|
|
||||||
|
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
REFIFC_LIB_NAME = "refifc"
|
REFIFC_LIB_NAME = "refifc.dll"
|
||||||
else: # 'posix'
|
else: # 'posix'
|
||||||
REFIFC_LIB_NAME = "librefifc.so"
|
REFIFC_LIB_NAME = "librefifc.so"
|
||||||
|
|
||||||
@@ -113,10 +113,10 @@ class Refifc(object):
|
|||||||
# Sauvegardez le répertoire courant pour pouvoir y revenir plus tard
|
# Sauvegardez le répertoire courant pour pouvoir y revenir plus tard
|
||||||
self.original_directory = os.getcwd()
|
self.original_directory = os.getcwd()
|
||||||
# Determine candidate directories for the native library. Prefer
|
# Determine candidate directories for the native library. Prefer
|
||||||
# app/ipm/lib/<platform> if present, otherwise fall back to the
|
# app/ipm/lib/dll (Windows) or app/ipm/lib/so (POSIX) if present,
|
||||||
# package directory (for compatibility with older layouts).
|
# otherwise fall back to the package directory (for compatibility).
|
||||||
package_dir = os.path.dirname(os.path.abspath(__file__))
|
package_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
platform_dir = os.path.join(package_dir, 'lib', 'windows' if os.name == 'nt' else 'linux')
|
platform_dir = os.path.join(package_dir, 'lib', 'dll' if os.name == 'nt' else 'so')
|
||||||
dll_directory = platform_dir if os.path.isdir(platform_dir) else package_dir
|
dll_directory = platform_dir if os.path.isdir(platform_dir) else package_dir
|
||||||
|
|
||||||
# Change working directory to the chosen directory while loading
|
# Change working directory to the chosen directory while loading
|
||||||
@@ -124,13 +124,22 @@ class Refifc(object):
|
|||||||
|
|
||||||
# Try to load the native library from the chosen directory; if that
|
# Try to load the native library from the chosen directory; if that
|
||||||
# fails, attempt to load by name (for system-installed libs) and
|
# fails, attempt to load by name (for system-installed libs) and
|
||||||
# otherwise raise the original exception.
|
# otherwise raise the original exception. Use RTLD_GLOBAL on POSIX
|
||||||
|
# to make symbols available for dependent shared objects.
|
||||||
try:
|
try:
|
||||||
self.lib = ctypes.cdll.LoadLibrary(os.path.join(dll_directory, REFIFC_LIB_NAME))
|
full_lib_path = os.path.join(dll_directory, REFIFC_LIB_NAME)
|
||||||
|
if os.name == 'nt':
|
||||||
|
self.lib = ctypes.cdll.LoadLibrary(full_lib_path)
|
||||||
|
else:
|
||||||
|
# Use RTLD_GLOBAL so dependent .so files can resolve symbols
|
||||||
|
self.lib = ctypes.CDLL(full_lib_path, mode=ctypes.RTLD_GLOBAL)
|
||||||
except OSError:
|
except OSError:
|
||||||
try:
|
try:
|
||||||
self.lib = ctypes.cdll.LoadLibrary(REFIFC_LIB_NAME)
|
if os.name == 'nt':
|
||||||
except Exception as e:
|
self.lib = ctypes.cdll.LoadLibrary(REFIFC_LIB_NAME)
|
||||||
|
else:
|
||||||
|
self.lib = ctypes.CDLL(REFIFC_LIB_NAME, mode=ctypes.RTLD_GLOBAL)
|
||||||
|
except Exception:
|
||||||
# Restore cwd before raising
|
# Restore cwd before raising
|
||||||
os.chdir(self.original_directory)
|
os.chdir(self.original_directory)
|
||||||
raise
|
raise
|
||||||
@@ -145,12 +154,24 @@ class Refifc(object):
|
|||||||
try:
|
try:
|
||||||
ctypes.CDLL(os.path.join(dll_directory, REFIFC_LIB_NAME))
|
ctypes.CDLL(os.path.join(dll_directory, REFIFC_LIB_NAME))
|
||||||
except OSError:
|
except OSError:
|
||||||
|
# best-effort warning; not fatal here (the main loader already succeeded)
|
||||||
print(f"Refrig {refrig_name} not found, please check!")
|
print(f"Refrig {refrig_name} not found, please check!")
|
||||||
|
|
||||||
func = self.lib.refdll_load
|
func = self.lib.refdll_load
|
||||||
func.restype = POINTER(c_void_p)
|
func.restype = POINTER(c_void_p)
|
||||||
func.argtypes = [c_char_p, c_void_p]
|
func.argtypes = [c_char_p, c_void_p]
|
||||||
self.handle = func(c_char_p(refrig_name.encode('utf-8')), c_void_p())
|
# On POSIX the native loader often expects the full SO filename
|
||||||
|
# (e.g. "libR290.so"). We built `ctypes_refrig_name` above to match
|
||||||
|
# that convention; use it when calling the native loader.
|
||||||
|
name_to_pass = ctypes_refrig_name if ctypes_refrig_name else refrig_name
|
||||||
|
try:
|
||||||
|
self.handle = func(c_char_p(name_to_pass.encode('utf-8')), c_void_p())
|
||||||
|
finally:
|
||||||
|
# restore cwd even if the native call raises
|
||||||
|
try:
|
||||||
|
os.chdir(self.original_directory)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# def __del__(self):
|
# def __del__(self):
|
||||||
|
|
||||||
|
|||||||
12
scripts/container_check.py
Normal file
12
scripts/container_check.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import traceback, sys
|
||||||
|
try:
|
||||||
|
from app.core.refrigerant_loader import RefrigerantLibrary
|
||||||
|
r = RefrigerantLibrary('R290')
|
||||||
|
print('Loaded Refifc OK')
|
||||||
|
try:
|
||||||
|
print('pbegin', r.p_begin())
|
||||||
|
except Exception as e:
|
||||||
|
print('p_begin failed:', e)
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
58
scripts/run_api_tests.py
Normal file
58
scripts/run_api_tests.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import requests, json, base64, os
|
||||||
|
base = 'http://127.0.0.1:8001'
|
||||||
|
print('Health ->', requests.get(base + '/api/v1/health').json())
|
||||||
|
# Diagram JSON
|
||||||
|
body = {
|
||||||
|
'refrigerant': 'R290',
|
||||||
|
'pressure_range': {'min': 0.1, 'max': 10.0},
|
||||||
|
'format': 'json',
|
||||||
|
'include_isotherms': True,
|
||||||
|
'width': 800,
|
||||||
|
'height': 600,
|
||||||
|
'dpi': 100
|
||||||
|
}
|
||||||
|
print('\nRequesting diagram JSON...')
|
||||||
|
r = requests.post(base + '/api/v1/diagrams/ph', json=body, timeout=60)
|
||||||
|
print('Status', r.status_code)
|
||||||
|
try:
|
||||||
|
j = r.json()
|
||||||
|
print('Keys in response:', list(j.keys()))
|
||||||
|
if 'data' in j:
|
||||||
|
print('Saturation curve length:', len(j['data'].get('saturation_curve', [])))
|
||||||
|
except Exception as e:
|
||||||
|
print('Failed to parse JSON:', e, r.text[:200])
|
||||||
|
|
||||||
|
# Diagram PNG
|
||||||
|
body['format'] = 'png'
|
||||||
|
print('\nRequesting diagram PNG...')
|
||||||
|
r2 = requests.post(base + '/api/v1/diagrams/ph', json=body, timeout=60)
|
||||||
|
print('Status', r2.status_code)
|
||||||
|
try:
|
||||||
|
j2 = r2.json()
|
||||||
|
print('Keys in response:', list(j2.keys()))
|
||||||
|
if 'image' in j2:
|
||||||
|
img_b64 = j2['image']
|
||||||
|
os.makedirs('test_outputs', exist_ok=True)
|
||||||
|
path = os.path.join('test_outputs','sample_diagram.png')
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(base64.b64decode(img_b64))
|
||||||
|
print('Saved PNG to', path)
|
||||||
|
except ValueError:
|
||||||
|
print('Response is not JSON, printing text length', len(r2.text))
|
||||||
|
|
||||||
|
# Simple cycle (pressure mode)
|
||||||
|
print('\nRequesting simple cycle...')
|
||||||
|
cycle_body = {
|
||||||
|
'refrigerant': 'R290',
|
||||||
|
'evap_pressure': 0.2, # bar
|
||||||
|
'cond_pressure': 6.0, # bar
|
||||||
|
'superheat': 5.0,
|
||||||
|
'subcool': 2.0,
|
||||||
|
'mass_flow': 0.1
|
||||||
|
}
|
||||||
|
r3 = requests.post(base + '/api/v1/cycles/simple', json=cycle_body, timeout=60)
|
||||||
|
print('Status', r3.status_code)
|
||||||
|
try:
|
||||||
|
print('Simple cycle keys:', list(r3.json().keys()))
|
||||||
|
except Exception as e:
|
||||||
|
print('Failed to parse cycle response:', e, r3.text[:200])
|
||||||
78
scripts/run_api_tests_docker.py
Normal file
78
scripts/run_api_tests_docker.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import requests, json, base64, os
|
||||||
|
base = 'http://127.0.0.1:8002'
|
||||||
|
print('Health ->', requests.get(base + '/api/v1/health').json())
|
||||||
|
# Diagram JSON
|
||||||
|
body = {
|
||||||
|
'refrigerant': 'R290',
|
||||||
|
'pressure_range': {'min': 0.1, 'max': 10.0},
|
||||||
|
'format': 'json',
|
||||||
|
'include_isotherms': True,
|
||||||
|
'width': 800,
|
||||||
|
'height': 600,
|
||||||
|
'dpi': 100
|
||||||
|
}
|
||||||
|
print('\nRequesting diagram JSON...')
|
||||||
|
r = requests.post(base + '/api/v1/diagrams/ph', json=body, timeout=120)
|
||||||
|
print('Status', r.status_code)
|
||||||
|
try:
|
||||||
|
j = r.json()
|
||||||
|
print('Keys in response:', list(j.keys()))
|
||||||
|
if 'data' in j:
|
||||||
|
print('Saturation curve length:', len(j['data'].get('saturation_curve', [])))
|
||||||
|
except Exception as e:
|
||||||
|
print('Failed to parse JSON:', e, r.text[:200])
|
||||||
|
|
||||||
|
# Diagram PNG
|
||||||
|
body['format'] = 'png'
|
||||||
|
print('\nRequesting diagram PNG...')
|
||||||
|
r2 = requests.post(base + '/api/v1/diagrams/ph', json=body, timeout=120)
|
||||||
|
print('Status', r2.status_code)
|
||||||
|
try:
|
||||||
|
j2 = r2.json()
|
||||||
|
print('Keys in response:', list(j2.keys()))
|
||||||
|
if 'image' in j2:
|
||||||
|
img_b64 = j2['image']
|
||||||
|
os.makedirs('test_outputs', exist_ok=True)
|
||||||
|
path = os.path.join('test_outputs','docker_sample_diagram.png')
|
||||||
|
with open(path, 'wb') as f:
|
||||||
|
f.write(base64.b64decode(img_b64))
|
||||||
|
print('Saved PNG to', path)
|
||||||
|
except Exception as e:
|
||||||
|
print('Failed to parse PNG response:', e, r2.text[:200])
|
||||||
|
|
||||||
|
# Simple cycle (pressure mode)
|
||||||
|
print('\nRequesting simple cycle (pressure mode)...')
|
||||||
|
cycle_body = {
|
||||||
|
'refrigerant': 'R290',
|
||||||
|
'evap_pressure': 0.2, # bar
|
||||||
|
'cond_pressure': 6.0, # bar
|
||||||
|
'superheat': 5.0,
|
||||||
|
'subcool': 2.0,
|
||||||
|
'mass_flow': 0.1
|
||||||
|
}
|
||||||
|
r3 = requests.post(base + '/api/v1/cycles/simple', json=cycle_body, timeout=120)
|
||||||
|
print('Status', r3.status_code)
|
||||||
|
try:
|
||||||
|
print('Simple cycle keys:', list(r3.json().keys()))
|
||||||
|
except Exception as e:
|
||||||
|
print('Failed to parse cycle response:', e, r3.text[:200])
|
||||||
|
|
||||||
|
# Simple cycle (temperature mode)
|
||||||
|
print('\nRequesting simple cycle (temperature mode)...')
|
||||||
|
cycle_body2 = {
|
||||||
|
'refrigerant': 'R290',
|
||||||
|
'evap_temperature': -10.0,
|
||||||
|
'cond_temperature': 40.0,
|
||||||
|
'superheat': 5.0,
|
||||||
|
'subcool': 2.0,
|
||||||
|
'mass_flow': 0.1
|
||||||
|
}
|
||||||
|
r4 = requests.post(base + '/api/v1/cycles/simple', json=cycle_body2, timeout=120)
|
||||||
|
print('Status', r4.status_code)
|
||||||
|
try:
|
||||||
|
j4 = r4.json()
|
||||||
|
print('Keys:', list(j4.keys()))
|
||||||
|
if 'performance' in j4:
|
||||||
|
print('COP:', j4['performance'].get('cop'))
|
||||||
|
except Exception as e:
|
||||||
|
print('Failed to parse cycle temp response:', e, r4.text[:200])
|
||||||
25
scripts/test_cycle_temp.py
Normal file
25
scripts/test_cycle_temp.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import requests, json
|
||||||
|
base = 'http://127.0.0.1:8001'
|
||||||
|
print('Health ->', requests.get(base + '/api/v1/health').json())
|
||||||
|
|
||||||
|
body = {
|
||||||
|
'refrigerant': 'R290',
|
||||||
|
'evap_temperature': -10.0, # °C
|
||||||
|
'cond_temperature': 40.0, # °C
|
||||||
|
'superheat': 5.0,
|
||||||
|
'subcool': 2.0,
|
||||||
|
'mass_flow': 0.1
|
||||||
|
}
|
||||||
|
print('\nRequesting simple cycle (temperature mode)...')
|
||||||
|
r = requests.post(base + '/api/v1/cycles/simple', json=body, timeout=60)
|
||||||
|
print('Status', r.status_code)
|
||||||
|
try:
|
||||||
|
j = r.json()
|
||||||
|
print('Keys:', list(j.keys()))
|
||||||
|
if 'performance' in j:
|
||||||
|
print('COP:', j['performance'].get('cop'))
|
||||||
|
print('Compressor efficiency:', j['performance'].get('compressor_efficiency'))
|
||||||
|
if 'diagram_data' in j:
|
||||||
|
print('Diagram cycle points count:', len(j['diagram_data'].get('cycle_points', [])))
|
||||||
|
except Exception as e:
|
||||||
|
print('Failed to parse JSON:', e, r.text[:200])
|
||||||
28
scripts/test_economizer.py
Normal file
28
scripts/test_economizer.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Ensure project root is on sys.path so 'app' package is importable when running scripts
|
||||||
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
if ROOT not in sys.path:
|
||||||
|
sys.path.insert(0, ROOT)
|
||||||
|
|
||||||
|
from app.core.refrigerant_loader import RefrigerantLibrary
|
||||||
|
from app.services.cycle_calculator import CycleCalculator
|
||||||
|
|
||||||
|
# Create refrigerant and calculator
|
||||||
|
refrig = RefrigerantLibrary('R290')
|
||||||
|
calc = CycleCalculator(refrig)
|
||||||
|
|
||||||
|
# Typical pressures in Pa (convert from bar)
|
||||||
|
evap = 0.2 * 1e5
|
||||||
|
cond = 6.0 * 1e5
|
||||||
|
inter = 2.0 * 1e5
|
||||||
|
|
||||||
|
res = calc.calculate_cycle_with_economizer(evap, cond, inter, superheat=5.0, subcool=3.0, mass_flow=0.1)
|
||||||
|
|
||||||
|
print('Economizer result keys:', res.keys())
|
||||||
|
print('Flash fraction:', res['performance'].get('flash_fraction'))
|
||||||
|
print('COP:', res['performance'].get('cop'))
|
||||||
|
print('Points:')
|
||||||
|
for p in res['points']:
|
||||||
|
print(' -', p)
|
||||||
BIN
test_outputs/sample_diagram.png
Normal file
BIN
test_outputs/sample_diagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 147 KiB |
44
tests/test_diagram_api.py
Normal file
44
tests/test_diagram_api.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import base64
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_diagram_json_no_image():
|
||||||
|
body = {
|
||||||
|
'refrigerant': 'R290',
|
||||||
|
'pressure_range': {'min': 0.1, 'max': 10.0},
|
||||||
|
'format': 'json',
|
||||||
|
'include_isotherms': True,
|
||||||
|
'width': 800,
|
||||||
|
'height': 600,
|
||||||
|
'dpi': 100
|
||||||
|
}
|
||||||
|
r = client.post('/api/v1/diagrams/ph', json=body)
|
||||||
|
assert r.status_code == 200
|
||||||
|
j = r.json()
|
||||||
|
# image key should not be present for json-only
|
||||||
|
assert 'image' not in j
|
||||||
|
assert 'data' in j
|
||||||
|
|
||||||
|
|
||||||
|
def test_diagram_png_includes_image():
|
||||||
|
body = {
|
||||||
|
'refrigerant': 'R290',
|
||||||
|
'pressure_range': {'min': 0.1, 'max': 10.0},
|
||||||
|
'format': 'png',
|
||||||
|
'include_isotherms': True,
|
||||||
|
'width': 800,
|
||||||
|
'height': 600,
|
||||||
|
'dpi': 100
|
||||||
|
}
|
||||||
|
r = client.post('/api/v1/diagrams/ph', json=body)
|
||||||
|
assert r.status_code == 200
|
||||||
|
j = r.json()
|
||||||
|
assert 'image' in j
|
||||||
|
# Validate base64 decodes
|
||||||
|
decoded = base64.b64decode(j['image'])
|
||||||
|
assert len(decoded) > 10
|
||||||
19
tests/test_economizer_unit.py
Normal file
19
tests/test_economizer_unit.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import sys, os
|
||||||
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
if ROOT not in sys.path:
|
||||||
|
sys.path.insert(0, ROOT)
|
||||||
|
|
||||||
|
from app.core.refrigerant_loader import RefrigerantLibrary
|
||||||
|
from app.services.cycle_calculator import CycleCalculator
|
||||||
|
|
||||||
|
|
||||||
|
def test_economizer_runs():
|
||||||
|
refrigerant = RefrigerantLibrary('R290')
|
||||||
|
calc = CycleCalculator(refrigerant)
|
||||||
|
evap = 0.2 * 1e5
|
||||||
|
cond = 6.0 * 1e5
|
||||||
|
inter = 2.0 * 1e5
|
||||||
|
res = calc.calculate_cycle_with_economizer(evap, cond, inter, mass_flow=0.1)
|
||||||
|
assert 'performance' in res
|
||||||
|
assert 'flash_fraction' in res['performance']
|
||||||
|
assert 0.0 <= res['performance']['flash_fraction'] <= 1.0
|
||||||
Reference in New Issue
Block a user