Skip to content

Commit 0f4c042

Browse files
committed
matlab http client
1 parent 86d8963 commit 0f4c042

File tree

6 files changed

+407
-99
lines changed

6 files changed

+407
-99
lines changed

Dockerfile

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,6 @@
11
# syntax=docker/dockerfile:1
22

3-
# Stage 1: Install MATLAB Engine API in MATLAB container
4-
# The mathworks/matlab:r2024b image uses Python 3.12 (Ubuntu Noble)
5-
FROM mathworks/matlab:r2024b AS matlab-builder
6-
USER root
7-
RUN apt-get update && apt-get install -y python3 python3-pip python3-dev && rm -rf /var/lib/apt/lists/*
8-
WORKDIR /opt/matlab/R2024b/extern/engines/python
9-
# Build with system Python 3.12; the compiled extension uses stable ABI and works with 3.11+
10-
# System pip/setuptools are sufficient; no need to upgrade
11-
RUN python3 setup.py install --prefix=/tmp/matlabengine
12-
RUN find /tmp/matlabengine -maxdepth 5 -type d -print
13-
# The installer creates lib/python3.12/site-packages but we'll copy to 3.11 runtime
14-
# Create a symlink so both 3.11 and 3.12 paths work
15-
RUN if [ -d /tmp/matlabengine/lib/python3.12 ]; then \
16-
cd /tmp/matlabengine/lib && \
17-
ln -s python3.12 python3.11; \
18-
fi
19-
20-
# Stage 2: Web application
21-
# We'll use Python 3.11 for the web image so manylinux wheels for your requirements
3+
# Web application - no MATLAB Engine needed
224
FROM python:3.11-slim AS base
235

246
ENV PYTHONDONTWRITEBYTECODE=1 \
@@ -41,20 +23,6 @@ COPY requirements.txt ./
4123
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
4224
pip install --no-cache-dir -r requirements.txt
4325

44-
# Copy MATLAB Engine API from builder
45-
# Structure: /tmp/matlabengine/local/lib/python3.12/dist-packages/
46-
COPY --from=matlab-builder /tmp/matlabengine/local/lib/python3.12/dist-packages/ /usr/local/lib/python3.11/site-packages/
47-
48-
# Copy MATLAB runtime libraries (compiled .so files) into site-packages so Python can find them
49-
COPY --from=matlab-builder /opt/matlab/R2024b/extern/bin/glnxa64/*.so /usr/local/lib/python3.11/site-packages/
50-
51-
# Extract the .egg file so Python can import the matlab module
52-
RUN cd /usr/local/lib/python3.11/site-packages && \
53-
if [ -d matlabengine-24.2-py3.12.egg ]; then \
54-
cp -r matlabengine-24.2-py3.12.egg/matlab . && \
55-
cp -r matlabengine-24.2-py3.12.egg/EGG-INFO matlabengine-24.2.egg-info; \
56-
fi
57-
5826
# Copy application code
5927
COPY . .
6028

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
"""
2+
MATLAB HTTP Client
3+
4+
This module provides a client to communicate with the MATLAB HTTP API server
5+
running in a separate Docker container.
6+
"""
7+
8+
import os
9+
import requests
10+
import logging
11+
from typing import Dict, Any, List, Optional
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
class MatlabHTTPClient:
17+
"""
18+
Client to communicate with MATLAB HTTP API server.
19+
Replaces direct MATLAB Engine API usage with HTTP requests.
20+
"""
21+
_instance: Optional['MatlabHTTPClient'] = None
22+
23+
def __new__(cls):
24+
if cls._instance is None:
25+
cls._instance = super(MatlabHTTPClient, cls).__new__(cls)
26+
cls._instance._initialized = False
27+
return cls._instance
28+
29+
def __init__(self):
30+
"""Initialize the HTTP client."""
31+
if self._initialized:
32+
return
33+
34+
# Get MATLAB server configuration from environment
35+
self.matlab_host = os.getenv('MATLAB_HOST', 'matlab')
36+
self.matlab_port = int(os.getenv('MATLAB_HTTP_PORT', '9090'))
37+
self.base_url = f"http://{self.matlab_host}:{self.matlab_port}"
38+
self.timeout = 300 # 5 minutes timeout for MATLAB operations
39+
40+
logger.info(f"MATLAB HTTP Client initialized: {self.base_url}")
41+
self._initialized = True
42+
43+
def health_check(self) -> bool:
44+
"""
45+
Check if the MATLAB server is healthy and responsive.
46+
47+
Returns:
48+
bool: True if server is healthy, False otherwise
49+
"""
50+
try:
51+
response = requests.get(
52+
f"{self.base_url}/health",
53+
timeout=5
54+
)
55+
return response.status_code == 200
56+
except Exception as e:
57+
logger.error(f"MATLAB health check failed: {e}")
58+
return False
59+
60+
def execute(
61+
self,
62+
function: str,
63+
*args,
64+
nargout: int = 1,
65+
**kwargs
66+
) -> Dict[str, Any]:
67+
"""
68+
Execute a MATLAB function.
69+
70+
Args:
71+
function: Name of the MATLAB function to execute
72+
*args: Positional arguments to pass to the function
73+
nargout: Number of output arguments (default 1)
74+
**kwargs: Keyword arguments to pass to the function
75+
76+
Returns:
77+
Dictionary with 'status' and either 'result' or 'message':
78+
- {'status': 'success', 'result': <return_value>}
79+
- {'status': 'error', 'message': <error_message>}
80+
"""
81+
try:
82+
payload = {
83+
'function': function,
84+
'args': list(args),
85+
'kwargs': kwargs,
86+
'nargout': nargout
87+
}
88+
89+
logger.debug(f"Executing MATLAB function: {function}")
90+
91+
response = requests.post(
92+
f"{self.base_url}/execute",
93+
json=payload,
94+
timeout=self.timeout
95+
)
96+
97+
result = response.json()
98+
99+
if response.status_code == 200:
100+
logger.debug(f"Function executed successfully: {function}")
101+
return result
102+
else:
103+
logger.error(f"Function execution failed: {result.get('message', 'Unknown error')}")
104+
return result
105+
106+
except requests.exceptions.Timeout:
107+
error_msg = f"MATLAB function {function} timed out after {self.timeout} seconds"
108+
logger.error(error_msg)
109+
return {'status': 'error', 'message': error_msg}
110+
except requests.exceptions.ConnectionError as e:
111+
error_msg = f"Cannot connect to MATLAB server at {self.base_url}: {str(e)}"
112+
logger.error(error_msg)
113+
return {'status': 'error', 'message': error_msg}
114+
except Exception as e:
115+
error_msg = f"Unexpected error executing {function}: {str(e)}"
116+
logger.error(error_msg)
117+
return {'status': 'error', 'message': error_msg}
118+
119+
def eval(self, code: str, nargout: int = 0) -> Dict[str, Any]:
120+
"""
121+
Evaluate MATLAB code.
122+
123+
Args:
124+
code: MATLAB code string to evaluate
125+
nargout: Number of output arguments (default 0)
126+
127+
Returns:
128+
Dictionary with 'status' and either 'result' or 'message':
129+
- {'status': 'success', 'result': <return_value>} (if nargout > 0)
130+
- {'status': 'success'} (if nargout == 0)
131+
- {'status': 'error', 'message': <error_message>}
132+
"""
133+
try:
134+
payload = {
135+
'code': code,
136+
'nargout': nargout
137+
}
138+
139+
logger.debug(f"Evaluating MATLAB code: {code[:100]}...")
140+
141+
response = requests.post(
142+
f"{self.base_url}/eval",
143+
json=payload,
144+
timeout=self.timeout
145+
)
146+
147+
result = response.json()
148+
149+
if response.status_code == 200:
150+
logger.debug("Code evaluated successfully")
151+
return result
152+
else:
153+
logger.error(f"Code evaluation failed: {result.get('message', 'Unknown error')}")
154+
return result
155+
156+
except requests.exceptions.Timeout:
157+
error_msg = f"MATLAB code evaluation timed out after {self.timeout} seconds"
158+
logger.error(error_msg)
159+
return {'status': 'error', 'message': error_msg}
160+
except requests.exceptions.ConnectionError as e:
161+
error_msg = f"Cannot connect to MATLAB server at {self.base_url}: {str(e)}"
162+
logger.error(error_msg)
163+
return {'status': 'error', 'message': error_msg}
164+
except Exception as e:
165+
error_msg = f"Unexpected error evaluating code: {str(e)}"
166+
logger.error(error_msg)
167+
return {'status': 'error', 'message': error_msg}
168+
169+
@property
170+
def is_connected(self) -> bool:
171+
"""Check if the MATLAB server is connected and responsive."""
172+
return self.health_check()
173+
174+
175+
# Alias for backwards compatibility
176+
MatlabSessionManager = MatlabHTTPClient

curationTool/reactions/utils/gen_vmh_abbrs.py

Lines changed: 2 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -6,61 +6,8 @@
66
import traceback
77
from django.conf import settings
88

9-
# AGGRESSIVE DEBUG LOGGING
10-
print(f"=" * 80, flush=True)
11-
print(f"[DEBUG gen_vmh_abbrs] MODULE LOADING STARTED", flush=True)
12-
print(f"[DEBUG gen_vmh_abbrs] Python executable: {sys.executable}", flush=True)
13-
print(f"[DEBUG gen_vmh_abbrs] Python version: {sys.version}", flush=True)
14-
print(f"[DEBUG gen_vmh_abbrs] sys.path: {sys.path}", flush=True)
15-
16-
# Check environment variables
17-
matlab_remote_enabled = os.getenv('MATLAB_REMOTE_ENABLED', 'NOT_SET')
18-
print(f"[DEBUG gen_vmh_abbrs] MATLAB_REMOTE_ENABLED = '{matlab_remote_enabled}'", flush=True)
19-
20-
# Check if matlabengine package exists
21-
try:
22-
import matlab
23-
print(f"[DEBUG gen_vmh_abbrs] ✓ 'matlab' package found at: {matlab.__file__}", flush=True)
24-
try:
25-
import matlab.engine
26-
print(f"[DEBUG gen_vmh_abbrs] ✓ 'matlab.engine' module found at: {matlab.engine.__file__}", flush=True)
27-
except ImportError as e:
28-
print(f"[DEBUG gen_vmh_abbrs] ✗ 'matlab.engine' NOT found: {e}", flush=True)
29-
except ImportError as e:
30-
print(f"[DEBUG gen_vmh_abbrs] ✗ 'matlab' package NOT found: {e}", flush=True)
31-
32-
# Check if MatlabSessionManagerRemote exists
33-
import os.path
34-
remote_manager_path = os.path.join(os.path.dirname(__file__), 'MatlabSessionManagerRemote.py')
35-
print(f"[DEBUG gen_vmh_abbrs] Looking for MatlabSessionManagerRemote at: {remote_manager_path}", flush=True)
36-
print(f"[DEBUG gen_vmh_abbrs] File exists? {os.path.exists(remote_manager_path)}", flush=True)
37-
38-
# Use remote MATLAB session manager if enabled, otherwise use local
39-
skip = False
40-
MatlabSessionManager = None
41-
42-
try:
43-
if matlab_remote_enabled.lower() == 'true':
44-
print(f"[DEBUG gen_vmh_abbrs] >>> Importing MatlabSessionManagerRemote...", flush=True)
45-
from reactions.utils.MatlabSessionManagerRemote import MatlabSessionManager
46-
print(f"[DEBUG gen_vmh_abbrs] >>> SUCCESS! MatlabSessionManager = {MatlabSessionManager}", flush=True)
47-
else:
48-
print(f"[DEBUG gen_vmh_abbrs] >>> Importing MatlabSessionManager (local)...", flush=True)
49-
from reactions.utils.MatlabSessionManager import MatlabSessionManager
50-
print(f"[DEBUG gen_vmh_abbrs] >>> SUCCESS! MatlabSessionManager = {MatlabSessionManager}", flush=True)
51-
except Exception as e:
52-
print(f"[ERROR gen_vmh_abbrs] ✗✗✗ IMPORT FAILED ✗✗✗", flush=True)
53-
print(f"[ERROR gen_vmh_abbrs] Exception: {e}", flush=True)
54-
print(f"[ERROR gen_vmh_abbrs] Exception type: {type(e).__name__}", flush=True)
55-
print(f"[ERROR gen_vmh_abbrs] Full traceback:", flush=True)
56-
traceback.print_exc(file=sys.stdout)
57-
sys.stdout.flush()
58-
skip = True
59-
60-
print(f"[DEBUG gen_vmh_abbrs] Final state: MatlabSessionManager = {MatlabSessionManager}, skip = {skip}", flush=True)
61-
print(f"[DEBUG gen_vmh_abbrs] MODULE LOADING COMPLETED", flush=True)
62-
print(f"=" * 80, flush=True)
63-
sys.stdout.flush()
9+
# Use HTTP client to communicate with MATLAB container
10+
from reactions.utils.MatlabHTTPClient import MatlabSessionManager
6411

6512
def check_reaction_abbr_exists(abbr):
6613
BASE_URL = settings.OLD_VMH_BASE_URL
@@ -115,9 +62,6 @@ def gen_metabolite_abbr(
11562
if found:
11663
return abbr
11764
else:
118-
if MatlabSessionManager is None:
119-
raise RuntimeError("MatlabSessionManager is not available. MATLAB integration is not configured.")
120-
12165
matlab_session = MatlabSessionManager()
12266
result = matlab_session.execute('generateVMHMetAbbr', metabolite_name)
12367
abbr = result['result'] if result['status'] == 'success' else metabolite_name

docker-compose.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ services:
1616
matlab:
1717
condition: service_started
1818
environment:
19-
- MATLAB_REMOTE_ENABLED=true
20-
- MATLAB_SESSION_NAME=matlab_shared_session
19+
- MATLAB_HOST=matlab
20+
- MATLAB_HTTP_PORT=9090
2121

2222
db:
2323
image: postgres:15-alpine
@@ -40,7 +40,7 @@ services:
4040
- .env
4141
environment:
4242
- MLM_LICENSE_FILE=${MLM_LICENSE_FILE}
43-
- MATLAB_SESSION_NAME=matlab_shared_session
43+
- MATLAB_HTTP_PORT=9090
4444
# Override paths for MATLAB container (COBRA and VMH installed in image)
4545
- COBRA_PATH=/matlab/toolboxes/cobratoolbox
4646
- SCRIPT_DIRECTORIES=/matlab/scripts,/matlab/toolboxes/vmh_revamped/vmh_db_update/src,/matlab/toolboxes/cobratoolbox/src/dataIntegration/metaboAnnotator/buildMetStruct,/matlab/toolboxes/cobratoolbox
@@ -56,7 +56,7 @@ services:
5656
# Restart on failure but not on manual stop or successful exit
5757
restart: on-failure
5858
healthcheck:
59-
test: ["CMD-SHELL", "python3 -c 'import matlab.engine; print(len(matlab.engine.find_matlab()))' || exit 1"]
59+
test: ["CMD-SHELL", "curl -f http://localhost:9090/health || exit 1"]
6060
interval: 30s
6161
timeout: 10s
6262
retries: 3

docker/matlab/Dockerfile

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ RUN apt-get update && apt-get install -y \
1717
WORKDIR /opt/matlab/R2024b/extern/engines/python
1818
RUN python3 setup.py install
1919

20+
# Install Flask for HTTP API server
21+
RUN python3 -m pip install --break-system-packages flask
22+
2023
# Create directories for MATLAB scripts and toolboxes
2124
RUN mkdir -p /matlab/scripts
2225
RUN mkdir -p /matlab/toolboxes/vmh_revamped
@@ -40,8 +43,8 @@ COPY --chown=root:root vmh_revamped/ /matlab/toolboxes/vmh_revamped/
4043
# COPY ./scripts /matlab/scripts
4144

4245
# Create a startup script that runs MATLAB in server mode
43-
COPY docker/matlab/matlab_engine_server.py /matlab/
44-
RUN chmod +x /matlab/matlab_engine_server.py
46+
COPY docker/matlab/matlab_http_server.py /matlab/
47+
RUN chmod +x /matlab/matlab_http_server.py
4548

4649
# Create directory for license files
4750
RUN mkdir -p /licenses
@@ -55,5 +58,5 @@ USER root
5558
# Override the default MATLAB entrypoint to run our Python script instead
5659
ENTRYPOINT []
5760

58-
# Run MATLAB Engine API server
59-
CMD ["python3", "/matlab/matlab_engine_server.py"]
61+
# Run MATLAB HTTP API server
62+
CMD ["python3", "/matlab/matlab_http_server.py"]

0 commit comments

Comments
 (0)