Compare commits
1 Commits
linux
...
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,82 +7,26 @@ import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, List
|
||||
|
||||
# If IPM_DISABLE_NATIVE=1 is set, use a lightweight MockRefifc to avoid
|
||||
# loading the native shared libraries during tests or environments where
|
||||
# the native binaries are not available. This is a fast, deterministic
|
||||
# fallback and prevents expensive or failing native loads at import time.
|
||||
_USE_MOCK = os.environ.get("IPM_DISABLE_NATIVE", "0") in ("1", "true", "True")
|
||||
|
||||
class MockRefifc:
|
||||
"""Minimal mock of the Refifc interface used for fast tests.
|
||||
|
||||
It implements only the methods the app commonly calls (p_begin, p_end,
|
||||
hsl_px, hsv_px, T_px, h_pT, h_px, p_Tx) with simple deterministic
|
||||
formulas so tests and diagram generation can run without native libs.
|
||||
"""
|
||||
def __init__(self, refrig_name: str):
|
||||
self.refrig_name = refrig_name
|
||||
|
||||
def p_begin(self):
|
||||
return 1e4 # Pa
|
||||
|
||||
def p_end(self):
|
||||
return 4e6 # Pa
|
||||
|
||||
def hsl_px(self, p, x):
|
||||
# return a plausible enthalpy (J/kg)
|
||||
return 1e5 + 0.1 * p
|
||||
|
||||
def hsv_px(self, p, x):
|
||||
return 2e5 + 0.1 * p
|
||||
|
||||
def T_px(self, p, x):
|
||||
# return Kelvin
|
||||
return 273.15 + 20.0 + (p / 1e5) * 5.0
|
||||
|
||||
def h_pT(self, p, T):
|
||||
return 1.5e5 + (T - 273.15) * 1000.0
|
||||
|
||||
def h_px(self, p, x):
|
||||
return self.hsl_px(p, x) if x == 0 else self.hsv_px(p, x)
|
||||
|
||||
def p_Tx(self, T, x):
|
||||
# inverse of T_px approximately
|
||||
return ( (T - 273.15 - 20.0) / 5.0 ) * 1e5
|
||||
|
||||
|
||||
if _USE_MOCK:
|
||||
# Use the lightweight mock implementation defined above
|
||||
Refifc = MockRefifc
|
||||
else:
|
||||
# Prefer the packaged app.ipm module. For very old/legacy setups that still
|
||||
# ship a top-level `simple_refrig_api.py` in an `IPM_DLL` folder we keep a
|
||||
# fallback, but only if that file actually exists. This avoids attempting a
|
||||
# top-level import when the module is provided as `app.ipm.simple_refrig_api`.
|
||||
try:
|
||||
# Import the package module and read attributes to allow the wrapper to
|
||||
# work even when `MockRefifc` is not defined in the implementation.
|
||||
# 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:
|
||||
import importlib
|
||||
_sr_pkg = importlib.import_module('app.ipm.simple_refrig_api')
|
||||
Refifc = getattr(_sr_pkg, 'Refifc')
|
||||
MockRefifc = getattr(_sr_pkg, 'MockRefifc', None)
|
||||
except Exception as _first_exc:
|
||||
# If a legacy IPM_DLL/simple_refrig_api.py file exists, import it as a
|
||||
# top-level module; otherwise re-raise the original exception.
|
||||
_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
|
||||
# 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"
|
||||
legacy_module_file = _ipm_dll_dir / "simple_refrig_api.py"
|
||||
|
||||
if legacy_module_file.exists():
|
||||
if str(_ipm_dll_dir) not in sys.path:
|
||||
sys.path.insert(0, str(_ipm_dll_dir))
|
||||
import simple_refrig_api as _sr # type: ignore
|
||||
Refifc = getattr(_sr, 'Refifc')
|
||||
MockRefifc = getattr(_sr, 'MockRefifc', None)
|
||||
else:
|
||||
# No legacy file found; re-raise the original import error so the
|
||||
# caller sees the underlying cause (missing dependency, etc.).
|
||||
raise _first_exc
|
||||
|
||||
|
||||
class RefrigerantLibrary:
|
||||
@@ -196,22 +140,15 @@ class RefrigerantManager:
|
||||
"""
|
||||
available = []
|
||||
|
||||
# Instead of attempting to load every refrigerant (which triggers
|
||||
# potentially expensive native library loads), prefer a fast check by
|
||||
# detecting whether the corresponding shared object exists in the
|
||||
# repository's ipm lib/so directory. Loading is left for explicit
|
||||
# requests (POST /{refrig}/load) or when a refrigerant is already
|
||||
# present in memory.
|
||||
repo_app_dir = Path(__file__).parent.parent
|
||||
libs_dir = repo_app_dir / 'ipm' / 'lib' / 'so'
|
||||
|
||||
for refrig in self.SUPPORTED_REFRIGERANTS:
|
||||
try:
|
||||
lib_file = libs_dir / f"lib{refrig}.so"
|
||||
exists = lib_file.exists()
|
||||
# Tenter de charger pour verifier disponibilite
|
||||
if refrig not in self._loaded_refrigerants:
|
||||
self.load_refrigerant(refrig)
|
||||
|
||||
available.append({
|
||||
"name": refrig,
|
||||
"available": bool(exists or (refrig in self._loaded_refrigerants)),
|
||||
"available": True,
|
||||
"loaded": refrig in self._loaded_refrigerants
|
||||
})
|
||||
except Exception as e:
|
||||
|
||||
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,68 +112,73 @@ 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
|
||||
if os.name == 'nt':
|
||||
candidate_dirs = [preferred_windows]
|
||||
else:
|
||||
candidate_dirs = [preferred_linux]
|
||||
|
||||
# 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.path.isdir(dll_directory):
|
||||
# temporarily change cwd to help some LoadLibrary behaviors
|
||||
os.chdir(dll_directory)
|
||||
|
||||
# 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)
|
||||
self.lib = ctypes.cdll.LoadLibrary(os.path.join(dll_directory, REFIFC_LIB_NAME))
|
||||
else:
|
||||
# Use RTLD_GLOBAL so dependent .so files can resolve symbols
|
||||
self.lib = ctypes.CDLL(full_lib_path, mode=ctypes.RTLD_GLOBAL)
|
||||
except OSError:
|
||||
# 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:
|
||||
if os.name == 'nt':
|
||||
self.lib = ctypes.cdll.LoadLibrary(REFIFC_LIB_NAME)
|
||||
else:
|
||||
self.lib = ctypes.CDLL(REFIFC_LIB_NAME, mode=ctypes.RTLD_GLOBAL)
|
||||
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':
|
||||
if not ctypes_refrig_name.lower().endswith("so"):
|
||||
ctypes_refrig_name = ctypes_refrig_name + ".so"
|
||||
if not ctypes_refrig_name.lower().startswith("lib"):
|
||||
ctypes_refrig_name = "lib" + ctypes_refrig_name
|
||||
|
||||
# Use the plain refrigerant identifier when calling the native loader.
|
||||
# On POSIX the native library usually resolves the actual "libRxxx.so"
|
||||
# filename itself and expects a simple name like "R134a". Passing a
|
||||
# modified filename (e.g. "libR134a.so") can confuse the native loader
|
||||
# and lead to crashes. Also ensure the loader returns a void pointer and
|
||||
# validate it before using.
|
||||
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
|
||||
# expect a void* handle from the loader
|
||||
func.restype = c_void_p
|
||||
func.restype = POINTER(c_void_p)
|
||||
func.argtypes = [c_char_p, c_void_p]
|
||||
name_bytes = refrig_name.encode('utf-8')
|
||||
try:
|
||||
res = func(c_char_p(name_bytes), c_void_p())
|
||||
if not res:
|
||||
# loader returned NULL -> raise to surface a Python-level error
|
||||
raise OSError(f"refdll_load returned NULL for refrigerant '{refrig_name}'")
|
||||
# store handle as a c_void_p
|
||||
self.handle = c_void_p(res)
|
||||
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):
|
||||
|
||||
|
||||
@@ -360,48 +360,5 @@ class CycleCalculator:
|
||||
}
|
||||
}
|
||||
|
||||
def calculate_cycle_with_economizer(
|
||||
self,
|
||||
evap_pressure: float,
|
||||
cond_pressure: float,
|
||||
inter_pressure: float,
|
||||
superheat: float = 5.0,
|
||||
subcool: float = 3.0,
|
||||
compressor_efficiency: float = 0.70,
|
||||
mass_flow: float = 0.1
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Compatibility wrapper for tests: simple economizer approximation.
|
||||
|
||||
This provides a lightweight result that mimics an economizer cycle
|
||||
without a full two-stage implementation. It reuses the simple cycle
|
||||
calculation for the high-pressure stage and estimates a flash
|
||||
fraction from the intermediate pressure location.
|
||||
"""
|
||||
# Basic validation
|
||||
if not (evap_pressure > 0 and cond_pressure > 0 and inter_pressure > 0):
|
||||
raise ValueError("Pressures must be positive")
|
||||
|
||||
# Estimate flash fraction as normalized position of inter between evap and cond
|
||||
try:
|
||||
frac = (inter_pressure - evap_pressure) / (cond_pressure - evap_pressure)
|
||||
except Exception:
|
||||
frac = 0.0
|
||||
flash_fraction = max(0.0, min(1.0, float(frac)))
|
||||
|
||||
# compute a simple cycle performance for the overall pressures
|
||||
base = self.calculate_simple_cycle(evap_pressure, cond_pressure, superheat, subcool, compressor_efficiency, mass_flow)
|
||||
|
||||
# attach economizer-specific fields
|
||||
perf = base.get('performance', {})
|
||||
perf['flash_fraction'] = flash_fraction
|
||||
|
||||
# Return a structure similar to simple cycle but with economizer info
|
||||
return {
|
||||
'points': base.get('points', []),
|
||||
'performance': perf,
|
||||
'diagram_data': base.get('diagram_data', {})
|
||||
}
|
||||
|
||||
|
||||
# Force reload 2025-10-18 23:04:14
|
||||
|
||||
@@ -152,12 +152,7 @@ class DiagramGenerator:
|
||||
def plot_diagram(
|
||||
self,
|
||||
cycle_points: Optional[List[Tuple[float, float]]] = None,
|
||||
title: Optional[str] = None,
|
||||
p_min: Optional[float] = None,
|
||||
p_max: Optional[float] = None,
|
||||
h_min: Optional[float] = None,
|
||||
h_max: Optional[float] = None,
|
||||
include_isotherms: bool = True,
|
||||
title: Optional[str] = None
|
||||
) -> Figure:
|
||||
"""
|
||||
Génère le diagramme PH complet.
|
||||
@@ -192,19 +187,14 @@ class DiagramGenerator:
|
||||
ax.plot(self.Hsl, [p / 1e5 for p in self.Psat], 'k-', label='Liquid Saturation')
|
||||
ax.plot(self.Hsv, [p / 1e5 for p in self.Psat], 'k-', label='Vapor Saturation')
|
||||
|
||||
# Lignes 196-202 du code original: Plot isotherms (optional)
|
||||
if include_isotherms:
|
||||
# Lignes 196-202 du code original: Plot isotherms
|
||||
for Th_lst, temp in zip(self.IsoT_lst, self.T_lst):
|
||||
ax.plot(Th_lst, self.P / 1e5, 'g--', label=f'{temp}°C Isotherm', alpha=0.5)
|
||||
try:
|
||||
ax.annotate('{:.0f}°C'.format(temp),
|
||||
(self.refrigerant.h_px(self.refrigerant.p_Tx(temp + 273.15, 0.5), 0.1) / 1e3,
|
||||
self.refrigerant.p_Tx(temp + 273.15, 0.5) / 1e5),
|
||||
ha='center',
|
||||
backgroundcolor="white")
|
||||
except Exception:
|
||||
# Non-fatal: annotation failure shouldn't break plotting
|
||||
pass
|
||||
|
||||
# Ligne 204 du code original
|
||||
ax.set_yscale('log')
|
||||
@@ -230,19 +220,6 @@ class DiagramGenerator:
|
||||
|
||||
# Ligne 224 du code original
|
||||
fig.tight_layout()
|
||||
# Apply axis limits if provided (p_min/p_max are in bar, h_min/h_max in kJ/kg)
|
||||
try:
|
||||
if p_min is not None or p_max is not None:
|
||||
y_min = p_min if p_min is not None else ax.get_ylim()[0]
|
||||
y_max = p_max if p_max is not None else ax.get_ylim()[1]
|
||||
ax.set_ylim(float(y_min), float(y_max))
|
||||
if h_min is not None or h_max is not None:
|
||||
x_min = h_min if h_min is not None else ax.get_xlim()[0]
|
||||
x_max = h_max if h_max is not None else ax.get_xlim()[1]
|
||||
ax.set_xlim(float(x_min), float(x_max))
|
||||
except Exception:
|
||||
# ignore axis limit errors
|
||||
pass
|
||||
return fig
|
||||
|
||||
def export_to_base64(self, fig: Figure) -> str:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,10 +8,7 @@ dependencies = [
|
||||
"altair>=5.5.0",
|
||||
"ipykernel>=6.29.5",
|
||||
"matplotlib>=3.10.3",
|
||||
"numpy>=2.3.0",
|
||||
"openpyxl>=3.1.5",
|
||||
"pandas>=2.3.0",
|
||||
"pip>=25.2",
|
||||
"plotly>=6.1.2",
|
||||
"unicorn>=2.1.4",
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
export LD_LIBRARY_PATH="$(pwd)/app/ipm/lib/so:${LD_LIBRARY_PATH:-}"
|
||||
export PYTHONPATH="$(pwd)"
|
||||
. .venv/bin/activate
|
||||
uvicorn app.main:app --host 127.0.0.1 --port 8001
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 232 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 147 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 24 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
|
||||
@@ -201,13 +201,8 @@ def test_api_direct():
|
||||
try:
|
||||
import requests
|
||||
|
||||
# Charger la requête (fichier à côté de ce script)
|
||||
request_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'request_r290.json')
|
||||
if not os.path.exists(request_path):
|
||||
# Fallback: essayer dans le répertoire courant
|
||||
request_path = 'request_r290.json'
|
||||
|
||||
with open(request_path, 'r') as f:
|
||||
# Charger la requête
|
||||
with open('request_r290.json', 'r') as f:
|
||||
request_data = json.load(f)
|
||||
|
||||
# Appeler l'API
|
||||
|
||||
Reference in New Issue
Block a user