Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d751861c8 | |||
|
|
d2a9a4197c | ||
|
|
6ee68aeaed |
@ -7,21 +7,82 @@ 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.
|
||||
try:
|
||||
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
|
||||
# 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"
|
||||
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)
|
||||
# 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.
|
||||
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.
|
||||
_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:
|
||||
@ -134,16 +195,23 @@ class RefrigerantManager:
|
||||
Liste de dictionnaires avec nom et disponibilite
|
||||
"""
|
||||
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:
|
||||
# Tenter de charger pour verifier disponibilite
|
||||
if refrig not in self._loaded_refrigerants:
|
||||
self.load_refrigerant(refrig)
|
||||
|
||||
lib_file = libs_dir / f"lib{refrig}.so"
|
||||
exists = lib_file.exists()
|
||||
available.append({
|
||||
"name": refrig,
|
||||
"available": True,
|
||||
"available": bool(exists or (refrig in self._loaded_refrigerants)),
|
||||
"loaded": refrig in self._loaded_refrigerants
|
||||
})
|
||||
except Exception as e:
|
||||
@ -152,7 +220,7 @@ class RefrigerantManager:
|
||||
"available": False,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
|
||||
return available
|
||||
|
||||
def load_refrigerant(self, refrig_name: str) -> RefrigerantLibrary:
|
||||
|
||||
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.
BIN
app/ipm/lib/so/librefifc.so
Normal file
BIN
app/ipm/lib/so/librefifc.so
Normal file
Binary file not shown.
@ -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"
|
||||
|
||||
@ -113,10 +113,10 @@ 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/<platform> if present, otherwise fall back to the
|
||||
# package directory (for compatibility with older layouts).
|
||||
# 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__))
|
||||
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
|
||||
|
||||
# Change working directory to the chosen directory while loading
|
||||
@ -124,33 +124,56 @@ class Refifc(object):
|
||||
|
||||
# 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.
|
||||
# otherwise raise the original exception. Use RTLD_GLOBAL on POSIX
|
||||
# to make symbols available for dependent shared objects.
|
||||
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:
|
||||
try:
|
||||
self.lib = ctypes.cdll.LoadLibrary(REFIFC_LIB_NAME)
|
||||
except Exception as e:
|
||||
if os.name == 'nt':
|
||||
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
|
||||
os.chdir(self.original_directory)
|
||||
raise
|
||||
|
||||
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!")
|
||||
|
||||
func = self.lib.refdll_load
|
||||
func.restype = POINTER(c_void_p)
|
||||
# expect a void* handle from the loader
|
||||
func.restype = 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())
|
||||
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
|
||||
|
||||
# def __del__(self):
|
||||
|
||||
|
||||
@ -360,5 +360,48 @@ 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,7 +152,12 @@ class DiagramGenerator:
|
||||
def plot_diagram(
|
||||
self,
|
||||
cycle_points: Optional[List[Tuple[float, float]]] = None,
|
||||
title: Optional[str] = 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,
|
||||
) -> Figure:
|
||||
"""
|
||||
Génère le diagramme PH complet.
|
||||
@ -187,14 +192,19 @@ 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
|
||||
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)
|
||||
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")
|
||||
# Lignes 196-202 du code original: Plot isotherms (optional)
|
||||
if include_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')
|
||||
@ -220,6 +230,19 @@ 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:
|
||||
|
||||
@ -8,7 +8,10 @@ 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",
|
||||
]
|
||||
|
||||
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)
|
||||
5
start_api.sh
Executable file
5
start_api.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/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
|
||||
BIN
test_complete.png
Normal file
BIN
test_complete.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 232 KiB |
BIN
test_matplotlib.png
Normal file
BIN
test_matplotlib.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
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 |
BIN
test_saturation.png
Normal file
BIN
test_saturation.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 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
|
||||
@ -201,8 +201,13 @@ def test_api_direct():
|
||||
try:
|
||||
import requests
|
||||
|
||||
# Charger la requête
|
||||
with open('request_r290.json', 'r') as f:
|
||||
# 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:
|
||||
request_data = json.load(f)
|
||||
|
||||
# Appeler l'API
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user