diff --git a/app/ipm/lib/linux/libR12.so b/app/ipm/lib/linux/libR12.so new file mode 100644 index 0000000..808f462 Binary files /dev/null and b/app/ipm/lib/linux/libR12.so differ diff --git a/app/ipm/lib/linux/libR1233zd.so b/app/ipm/lib/linux/libR1233zd.so new file mode 100644 index 0000000..a631106 Binary files /dev/null and b/app/ipm/lib/linux/libR1233zd.so differ diff --git a/app/ipm/lib/linux/libR1234ze.so b/app/ipm/lib/linux/libR1234ze.so new file mode 100644 index 0000000..4c21d0b Binary files /dev/null and b/app/ipm/lib/linux/libR1234ze.so differ diff --git a/app/ipm/lib/linux/libR134a.so b/app/ipm/lib/linux/libR134a.so new file mode 100644 index 0000000..3fcd62f Binary files /dev/null and b/app/ipm/lib/linux/libR134a.so differ diff --git a/app/ipm/lib/linux/libR22.so b/app/ipm/lib/linux/libR22.so new file mode 100644 index 0000000..9300f76 Binary files /dev/null and b/app/ipm/lib/linux/libR22.so differ diff --git a/app/ipm/lib/linux/libR290.so b/app/ipm/lib/linux/libR290.so new file mode 100644 index 0000000..a5e09a0 Binary files /dev/null and b/app/ipm/lib/linux/libR290.so differ diff --git a/app/ipm/lib/linux/libR32.so b/app/ipm/lib/linux/libR32.so new file mode 100644 index 0000000..902a685 Binary files /dev/null and b/app/ipm/lib/linux/libR32.so differ diff --git a/app/ipm/lib/linux/libR404A.so b/app/ipm/lib/linux/libR404A.so new file mode 100644 index 0000000..8fbbc41 Binary files /dev/null and b/app/ipm/lib/linux/libR404A.so differ diff --git a/app/ipm/lib/linux/libR410A.so b/app/ipm/lib/linux/libR410A.so new file mode 100644 index 0000000..acc91bd Binary files /dev/null and b/app/ipm/lib/linux/libR410A.so differ diff --git a/app/ipm/lib/linux/libR502.so b/app/ipm/lib/linux/libR502.so new file mode 100644 index 0000000..5be1062 Binary files /dev/null and b/app/ipm/lib/linux/libR502.so differ diff --git a/app/ipm/lib/linux/libR507A.so b/app/ipm/lib/linux/libR507A.so new file mode 100644 index 0000000..8b7a0d5 Binary files /dev/null and b/app/ipm/lib/linux/libR507A.so differ diff --git a/app/ipm/lib/linux/libR717.so b/app/ipm/lib/linux/libR717.so new file mode 100644 index 0000000..3739193 Binary files /dev/null and b/app/ipm/lib/linux/libR717.so differ diff --git a/app/ipm/lib/linux/libR744.so b/app/ipm/lib/linux/libR744.so new file mode 100644 index 0000000..1c1775c Binary files /dev/null and b/app/ipm/lib/linux/libR744.so differ diff --git a/app/ipm/lib/linux/librefifc.so b/app/ipm/lib/linux/librefifc.so new file mode 100644 index 0000000..4f5fb53 Binary files /dev/null and b/app/ipm/lib/linux/librefifc.so differ diff --git a/app/ipm/simple_refrig_api.py b/app/ipm/simple_refrig_api.py index 3c0a9dd..385b316 100644 --- a/app/ipm/simple_refrig_api.py +++ b/app/ipm/simple_refrig_api.py @@ -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/ 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,13 +124,22 @@ 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 @@ -145,12 +154,24 @@ 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!") 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): diff --git a/scripts/container_check.py b/scripts/container_check.py new file mode 100644 index 0000000..4768c61 --- /dev/null +++ b/scripts/container_check.py @@ -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) diff --git a/scripts/run_api_tests.py b/scripts/run_api_tests.py new file mode 100644 index 0000000..e1f33cf --- /dev/null +++ b/scripts/run_api_tests.py @@ -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]) diff --git a/scripts/run_api_tests_docker.py b/scripts/run_api_tests_docker.py new file mode 100644 index 0000000..2a351a9 --- /dev/null +++ b/scripts/run_api_tests_docker.py @@ -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]) diff --git a/scripts/test_cycle_temp.py b/scripts/test_cycle_temp.py new file mode 100644 index 0000000..d323e11 --- /dev/null +++ b/scripts/test_cycle_temp.py @@ -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]) diff --git a/scripts/test_economizer.py b/scripts/test_economizer.py new file mode 100644 index 0000000..289f1d2 --- /dev/null +++ b/scripts/test_economizer.py @@ -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) diff --git a/test_outputs/sample_diagram.png b/test_outputs/sample_diagram.png new file mode 100644 index 0000000..4b2ad91 Binary files /dev/null and b/test_outputs/sample_diagram.png differ diff --git a/tests/test_diagram_api.py b/tests/test_diagram_api.py new file mode 100644 index 0000000..39ac869 --- /dev/null +++ b/tests/test_diagram_api.py @@ -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 diff --git a/tests/test_economizer_unit.py b/tests/test_economizer_unit.py new file mode 100644 index 0000000..3f61264 --- /dev/null +++ b/tests/test_economizer_unit.py @@ -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