2 Commits

Author SHA1 Message Date
Repo Bot
d2a9a4197c snapshot: add simple_refrig_api and native libs (app/ipm/lib) 2025-10-19 17:20:23 +02:00
Repo Bot
6ee68aeaed fix: restore original simple_refrig_api and platform path fixes 2025-10-19 17:16:24 +02:00
50 changed files with 319 additions and 345 deletions

View File

@@ -30,15 +30,14 @@ COPY requirements.txt /app/requirements.txt
RUN python -m pip install --upgrade pip setuptools wheel && \
python -m pip install -r /app/requirements.txt
# Copy only app directory and libs/so (native .so libraries for Linux)
COPY app /app/app
COPY libs/so /app/libs/so
# Copy project
COPY . /app
# Set environment variables to use libs/so for native libraries
ENV PYTHONPATH="/app"
ENV LD_LIBRARY_PATH="/app/libs/so"
# Ensure Python and dynamic linker will find the native libs if mounted
ENV PYTHONPATH="/app:/app/IPM_SO:/app/IPM_DLL"
ENV LD_LIBRARY_PATH="/app/IPM_SO:/app/IPM_DLL"
EXPOSE 8001
# Default command runs uvicorn
# Default command runs uvicorn (use docker-compose override for development)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001"]

View File

@@ -1,21 +0,0 @@
# Frontend .dockerignore
node_modules
.next
out
build
.turbo
dist
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.git
.gitignore
README.md
.vscode
.idea
*.swp
*.swo

View File

@@ -1,50 +0,0 @@
# Frontend Dockerfile
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Copy package files
COPY package.json package-lock.json* ./
RUN npm ci
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Build the Next.js app
ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
CMD ["node", "server.js"]

View File

@@ -1,179 +0,0 @@
# DiagramPH - Docker Deployment Guide
## 🐳 Docker Compose Setup
Ce projet utilise Docker Compose pour déployer le backend (FastAPI) et le frontend (Next.js) ensemble.
### Architecture
```
┌─────────────────┐ ┌──────────────────┐
│ Frontend │─────▶│ Backend │
│ (Next.js) │ │ (FastAPI) │
│ Port: 3000 │ │ Port: 8001 │
└─────────────────┘ └──────────────────┘
│ │
└────────────────────────┘
Docker Network
```
## 🚀 Démarrage Rapide
### Production
```bash
# Build et démarrer tous les services
docker-compose up --build
# En mode détaché (background)
docker-compose up -d --build
# Arrêter les services
docker-compose down
# Arrêter et supprimer les volumes
docker-compose down -v
```
Accès:
- **Frontend**: http://localhost:3000
- **Backend API**: http://localhost:8001
- **API Docs**: http://localhost:8001/docs
### Développement
Pour le développement avec hot-reload:
```bash
# Démarrer en mode développement
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up
# Rebuild si nécessaire
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up --build
```
## 📦 Services
### Backend (Python FastAPI)
- **Port**: 8001
- **Base Image**: python:3.12-slim
- **Hot Reload**: Activé avec `--reload` en dev
- **Healthcheck**: GET /api/v1/refrigerants/
### Frontend (Next.js)
- **Port**: 3000
- **Base Image**: node:20-alpine
- **Build**: Standalone output optimisé
- **Healthcheck**: HTTP GET sur port 3000
## 🔧 Configuration
### Variables d'Environnement
#### Backend
```env
PYTHONUNBUFFERED=1
PYTHONPATH=/app:/app/IPM_SO:/app/IPM_DLL
LD_LIBRARY_PATH=/app/IPM_SO:/app/IPM_DLL
```
#### Frontend
```env
NODE_ENV=production
NEXT_PUBLIC_API_URL=http://backend:8001/api/v1
```
### Volumes
Les volumes sont montés pour permettre le hot-reload en développement:
- `./app` → Backend code
- `./Frontend` → Frontend code
- `./IPM_SO` et `./IPM_DLL` → Native libraries
## 📝 Commandes Utiles
```bash
# Voir les logs
docker-compose logs -f
# Logs d'un service spécifique
docker-compose logs -f backend
docker-compose logs -f frontend
# Reconstruire un service
docker-compose build backend
docker-compose build frontend
# Exécuter une commande dans un conteneur
docker-compose exec backend python -m pytest
docker-compose exec frontend npm test
# Voir l'état des services
docker-compose ps
# Redémarrer un service
docker-compose restart backend
docker-compose restart frontend
```
## 🐛 Troubleshooting
### Le frontend ne peut pas se connecter au backend
Vérifiez que:
1. Le backend est en cours d'exécution: `docker-compose ps`
2. Le healthcheck du backend est OK: `docker-compose logs backend`
3. Les deux services sont sur le même réseau Docker
### Erreur de build frontend
```bash
# Nettoyer le cache et rebuild
docker-compose build --no-cache frontend
```
### Erreur de permissions
```bash
# Sur Linux/Mac, ajuster les permissions
sudo chown -R $USER:$USER Frontend/.next
```
## 📚 Structure des Fichiers
```
.
├── docker-compose.yml # Configuration production
├── docker-compose.dev.yml # Override pour développement
├── Dockerfile # Backend Dockerfile
├── Frontend/
│ ├── Dockerfile # Frontend Dockerfile
│ ├── .dockerignore
│ └── next.config.js
├── app/ # Code backend
└── README-DOCKER.md # Ce fichier
```
## 🔒 Production Deployment
Pour un déploiement en production:
1. **Désactiver le mode debug**:
- Retirer `--reload` du backend
- Utiliser `NODE_ENV=production`
2. **Utiliser des secrets**:
```yaml
environment:
- DATABASE_URL=${DATABASE_URL}
```
3. **Configurer un reverse proxy** (Nginx/Traefik)
4. **Ajouter SSL/TLS** avec Let's Encrypt
## 📄 License
MIT

View File

@@ -7,18 +7,13 @@ import os
from pathlib import Path
from typing import Dict, Optional, List
# Prefer the packaged app.ipm module, but fall back to the legacy IPM_DLL
# folder only if the packaged module cannot be imported. Import the module
# object and use getattr to avoid ImportError when optional symbols like
# `MockRefifc` are missing.
# Prefer the packaged app.ipm module, but keep a fallback to the legacy
# IPM_DLL folder for development compatibility.
try:
import importlib
_sr = importlib.import_module('app.ipm.simple_refrig_api')
Refifc = getattr(_sr, 'Refifc')
MockRefifc = getattr(_sr, 'MockRefifc', None)
from app.ipm.simple_refrig_api import Refifc, MockRefifc # type: ignore
except Exception:
# Fall back to loading from IPM_DLL directory as before. Import the
# legacy module and pick attributes if present; older wrappers may not
# module and pick attributes if present; older legacy wrappers may not
# define MockRefifc.
_current_dir = Path(__file__).parent.parent.parent
_ipm_dll_dir = _current_dir / "IPM_DLL"

BIN
app/ipm/lib/dll/R12.dll Normal file

Binary file not shown.

BIN
app/ipm/lib/dll/R1233zd.dll Normal file

Binary file not shown.

BIN
app/ipm/lib/dll/R1234ze.dll Normal file

Binary file not shown.

BIN
app/ipm/lib/dll/R134a.dll Normal file

Binary file not shown.

BIN
app/ipm/lib/dll/R22.dll Normal file

Binary file not shown.

BIN
app/ipm/lib/dll/R290.dll Normal file

Binary file not shown.

BIN
app/ipm/lib/dll/R32.dll Normal file

Binary file not shown.

BIN
app/ipm/lib/dll/R404A.dll Normal file

Binary file not shown.

BIN
app/ipm/lib/dll/R410A.dll Normal file

Binary file not shown.

BIN
app/ipm/lib/dll/R452A.dll Normal file

Binary file not shown.

BIN
app/ipm/lib/dll/R454A.dll Normal file

Binary file not shown.

BIN
app/ipm/lib/dll/R454B.dll Normal file

Binary file not shown.

BIN
app/ipm/lib/dll/R502.dll Normal file

Binary file not shown.

BIN
app/ipm/lib/dll/R507A.dll Normal file

Binary file not shown.

BIN
app/ipm/lib/dll/R513A.dll Normal file

Binary file not shown.

BIN
app/ipm/lib/dll/R515B.dll Normal file

Binary file not shown.

BIN
app/ipm/lib/dll/R744.dll Normal file

Binary file not shown.

Binary file not shown.

BIN
app/ipm/lib/dll/refifc.dll Normal file

Binary file not shown.

Binary file not shown.

BIN
app/ipm/lib/so/libR12.so Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
app/ipm/lib/so/libR134a.so Normal file

Binary file not shown.

BIN
app/ipm/lib/so/libR22.so Normal file

Binary file not shown.

BIN
app/ipm/lib/so/libR290.so Normal file

Binary file not shown.

BIN
app/ipm/lib/so/libR32.so Normal file

Binary file not shown.

BIN
app/ipm/lib/so/libR404A.so Normal file

Binary file not shown.

BIN
app/ipm/lib/so/libR410A.so Normal file

Binary file not shown.

BIN
app/ipm/lib/so/libR502.so Normal file

Binary file not shown.

BIN
app/ipm/lib/so/libR507A.so Normal file

Binary file not shown.

BIN
app/ipm/lib/so/libR717.so Normal file

Binary file not shown.

BIN
app/ipm/lib/so/libR744.so Normal file

Binary file not shown.

BIN
app/ipm/lib/so/librefifc.so Normal file

Binary file not shown.

View File

@@ -101,7 +101,7 @@ class GenRefProperties(Structure):
if os.name == 'nt':
REFIFC_LIB_NAME = "refifc"
REFIFC_LIB_NAME = "refifc.dll"
else: # 'posix'
REFIFC_LIB_NAME = "librefifc.so"
@@ -112,55 +112,37 @@ class Refifc(object):
# Sauvegardez le répertoire courant pour pouvoir y revenir plus tard
self.original_directory = os.getcwd()
# Determine candidate directories for the native library.
# Prefer central repo-level folders:
# <repo_root>/libs/dll (Windows)
# <repo_root>/libs/so (Linux)
# Fall back to package-local `app/ipm/lib/<platform>` or the package dir.
# Determine candidate directories for the native library. Prefer
# app/ipm/lib/dll (Windows) or app/ipm/lib/so (POSIX) if present,
# otherwise fall back to the package directory (for compatibility).
package_dir = os.path.dirname(os.path.abspath(__file__))
# repo root is two levels above app/ipm
repo_root = os.path.abspath(os.path.join(package_dir, '..', '..'))
preferred_windows = os.path.join(repo_root, 'libs', 'dll')
preferred_linux = os.path.join(repo_root, 'libs', 'so')
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
if os.name == 'nt':
candidate_dirs = [preferred_windows]
else:
candidate_dirs = [preferred_linux]
# Change working directory to the chosen directory while loading
os.chdir(dll_directory)
# also consider the package-local lib/<platform> layout as fallback
candidate_dirs.append(os.path.join(package_dir, 'lib', 'windows' if os.name == 'nt' else 'linux'))
candidate_dirs.append(package_dir)
# Try each candidate directory in order until we can load the library.
load_exc = None
for dll_directory in candidate_dirs:
if not dll_directory:
continue
# Try to load the native library from the chosen directory; if that
# fails, attempt to load by name (for system-installed libs) and
# otherwise raise the original exception. Use RTLD_GLOBAL on POSIX
# to make symbols available for dependent shared objects.
try:
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:
try:
if os.path.isdir(dll_directory):
# temporarily change cwd to help some LoadLibrary behaviors
os.chdir(dll_directory)
self.lib = ctypes.cdll.LoadLibrary(os.path.join(dll_directory, REFIFC_LIB_NAME))
if os.name == 'nt':
self.lib = ctypes.cdll.LoadLibrary(REFIFC_LIB_NAME)
else:
# attempt to load directly by path anyway
self.lib = ctypes.cdll.LoadLibrary(os.path.join(dll_directory, REFIFC_LIB_NAME))
load_exc = None
break
except OSError as e:
load_exc = e
# try next candidate
continue
# if we failed to load from candidates, try loading by name (system path)
if load_exc is not None:
try:
self.lib = ctypes.cdll.LoadLibrary(REFIFC_LIB_NAME)
load_exc = None
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)
raise load_exc
raise
ctypes_refrig_name = refrig_name
if os.name == 'posix':
@@ -172,13 +154,24 @@ class Refifc(object):
try:
ctypes.CDLL(os.path.join(dll_directory, REFIFC_LIB_NAME))
except OSError:
# don't raise here; letting the subsequent load/check handle missing refrigerant libs
print(f"Refrig {refrig_name} not found in {dll_directory}, please check!")
# best-effort warning; not fatal here (the main loader already succeeded)
print(f"Refrig {refrig_name} not found, please check!")
func = self.lib.refdll_load
func.restype = POINTER(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):

View File

@@ -1,30 +0,0 @@
version: '3.8'
# Development override for docker-compose.yml
# Usage: docker-compose -f docker-compose.yml -f docker-compose.dev.yml up
services:
backend:
volumes:
- ./app:/app/app:cached
- ./IPM_SO:/app/IPM_SO:cached
- ./IPM_DLL:/app/IPM_DLL:cached
- ./tests:/app/tests:cached
command: uvicorn app.main:app --host 0.0.0.0 --port 8001 --reload
environment:
- DEBUG=1
frontend:
build:
context: ./Frontend
target: deps
volumes:
- ./Frontend:/app:cached
- /app/node_modules
- /app/.next
command: npm run dev
environment:
- NODE_ENV=development
- NEXT_PUBLIC_API_URL=http://localhost:8001/api/v1
ports:
- "3000:3000"

View File

@@ -1,3 +1,5 @@
version: '3.8'
services:
# Backend API Service
backend:
@@ -9,11 +11,12 @@ services:
- "8001:8001"
volumes:
- ./app:/app/app:cached
- ./libs/so:/app/libs/so:cached
- ./IPM_SO:/app/IPM_SO:cached
- ./IPM_DLL:/app/IPM_DLL:cached
environment:
- PYTHONUNBUFFERED=1
- PYTHONPATH=/app
- LD_LIBRARY_PATH=/app/libs/so
- PYTHONPATH=/app:/app/IPM_SO:/app/IPM_DLL
- LD_LIBRARY_PATH=/app/IPM_SO:/app/IPM_DLL
command: uvicorn app.main:app --host 0.0.0.0 --port 8001 --reload
networks:
- diagramph-network

View 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
View 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])

View 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])

View 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])

View 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

44
tests/test_diagram_api.py Normal file
View 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

View 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