Compare commits
1 Commits
snapshot/2
...
snapshot/2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d5005ce4e |
13
Dockerfile
13
Dockerfile
@@ -30,14 +30,15 @@ COPY requirements.txt /app/requirements.txt
|
||||
RUN python -m pip install --upgrade pip setuptools wheel && \
|
||||
python -m pip install -r /app/requirements.txt
|
||||
|
||||
# Copy project
|
||||
COPY . /app
|
||||
# Copy only app directory and libs/so (native .so libraries for Linux)
|
||||
COPY app /app/app
|
||||
COPY libs/so /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"
|
||||
# Set environment variables to use libs/so for native libraries
|
||||
ENV PYTHONPATH="/app"
|
||||
ENV LD_LIBRARY_PATH="/app/libs/so"
|
||||
|
||||
EXPOSE 8001
|
||||
|
||||
# Default command runs uvicorn (use docker-compose override for development)
|
||||
# Default command runs uvicorn
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001"]
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# 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
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# 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"]
|
||||
|
||||
179
README-DOCKER.md
179
README-DOCKER.md
@@ -0,0 +1,179 @@
|
||||
# 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
|
||||
|
||||
@@ -7,13 +7,18 @@ import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, List
|
||||
|
||||
# Prefer the packaged app.ipm module, but keep a fallback to the legacy
|
||||
# IPM_DLL folder for development compatibility.
|
||||
# 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.
|
||||
try:
|
||||
from app.ipm.simple_refrig_api import Refifc, MockRefifc # type: ignore
|
||||
import importlib
|
||||
_sr = importlib.import_module('app.ipm.simple_refrig_api')
|
||||
Refifc = getattr(_sr, 'Refifc')
|
||||
MockRefifc = getattr(_sr, 'MockRefifc', None)
|
||||
except Exception:
|
||||
# Fall back to loading from IPM_DLL directory as before. Import the
|
||||
# module and pick attributes if present; older legacy wrappers may not
|
||||
# legacy module and pick attributes if present; older wrappers may not
|
||||
# define MockRefifc.
|
||||
_current_dir = Path(__file__).parent.parent.parent
|
||||
_ipm_dll_dir = _current_dir / "IPM_DLL"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -101,7 +101,7 @@ class GenRefProperties(Structure):
|
||||
|
||||
|
||||
if os.name == 'nt':
|
||||
REFIFC_LIB_NAME = "refifc.dll"
|
||||
REFIFC_LIB_NAME = "refifc"
|
||||
else: # 'posix'
|
||||
REFIFC_LIB_NAME = "librefifc.so"
|
||||
|
||||
@@ -112,37 +112,55 @@ 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
|
||||
# app/ipm/lib/dll (Windows) or app/ipm/lib/so (POSIX) if present,
|
||||
# otherwise fall back to the package directory (for compatibility).
|
||||
# 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.
|
||||
package_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
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
|
||||
# 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')
|
||||
|
||||
# Change working directory to the chosen directory while loading
|
||||
os.chdir(dll_directory)
|
||||
if os.name == 'nt':
|
||||
candidate_dirs = [preferred_windows]
|
||||
else:
|
||||
candidate_dirs = [preferred_linux]
|
||||
|
||||
# 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:
|
||||
# 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:
|
||||
if os.name == 'nt':
|
||||
self.lib = ctypes.cdll.LoadLibrary(REFIFC_LIB_NAME)
|
||||
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))
|
||||
else:
|
||||
self.lib = ctypes.CDLL(REFIFC_LIB_NAME, mode=ctypes.RTLD_GLOBAL)
|
||||
# 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
|
||||
except Exception:
|
||||
# Restore cwd before raising
|
||||
# restore cwd before raising
|
||||
os.chdir(self.original_directory)
|
||||
raise
|
||||
raise load_exc
|
||||
|
||||
ctypes_refrig_name = refrig_name
|
||||
if os.name == 'posix':
|
||||
@@ -154,24 +172,13 @@ class Refifc(object):
|
||||
try:
|
||||
ctypes.CDLL(os.path.join(dll_directory, REFIFC_LIB_NAME))
|
||||
except OSError:
|
||||
# best-effort warning; not fatal here (the main loader already succeeded)
|
||||
print(f"Refrig {refrig_name} not found, please check!")
|
||||
# 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!")
|
||||
|
||||
func = self.lib.refdll_load
|
||||
func.restype = POINTER(c_void_p)
|
||||
func.argtypes = [c_char_p, 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
|
||||
self.handle = func(c_char_p(refrig_name.encode('utf-8')), c_void_p())
|
||||
|
||||
# def __del__(self):
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
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"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Backend API Service
|
||||
backend:
|
||||
@@ -11,12 +9,11 @@ services:
|
||||
- "8001:8001"
|
||||
volumes:
|
||||
- ./app:/app/app:cached
|
||||
- ./IPM_SO:/app/IPM_SO:cached
|
||||
- ./IPM_DLL:/app/IPM_DLL:cached
|
||||
- ./libs/so:/app/libs/so:cached
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
- PYTHONPATH=/app:/app/IPM_SO:/app/IPM_DLL
|
||||
- LD_LIBRARY_PATH=/app/IPM_SO:/app/IPM_DLL
|
||||
- PYTHONPATH=/app
|
||||
- LD_LIBRARY_PATH=/app/libs/so
|
||||
command: uvicorn app.main:app --host 0.0.0.0 --port 8001 --reload
|
||||
networks:
|
||||
- diagramph-network
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
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)
|
||||
@@ -1,58 +0,0 @@
|
||||
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])
|
||||
@@ -1,78 +0,0 @@
|
||||
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])
|
||||
@@ -1,25 +0,0 @@
|
||||
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])
|
||||
@@ -1,28 +0,0 @@
|
||||
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.
|
Before Width: | Height: | Size: 147 KiB |
@@ -1,44 +0,0 @@
|
||||
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
|
||||
@@ -1,19 +0,0 @@
|
||||
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