11"""Wrappers for the interface between RATapi and MATLAB custom files."""
2-
2+ import atexit
33import os
44import pathlib
55import platform
66import shutil
77import subprocess
8- from contextlib import contextmanager
9- from typing import Callable
8+ from contextlib import contextmanager , suppress
9+ from typing import Callable , Tuple
1010
1111from numpy .typing import ArrayLike
1212
1313import RATapi .rat_core
1414
1515MATLAB_PATH_FILE = os .path .join (os .path .dirname (os .path .realpath (__file__ )), "matlab.txt" )
16+ __MATLAB_ENGINE = None
17+
18+
19+ def get_matlab_engine ():
20+ return __MATLAB_ENGINE
1621
1722
1823@contextmanager
19- def cd (new_dir ):
24+ def cd (new_dir : str ):
2025 """Context manager to change to a given directory and return to current directory on exit.
2126
2227 Parameters
2328 ----------
24- new_dir : string
29+ new_dir : str
2530 The path to change to
2631 """
2732 prev_dir = os .getcwd ()
@@ -32,38 +37,17 @@ def cd(new_dir):
3237 os .chdir (prev_dir )
3338
3439
35- def set_matlab_path (matlab_exe_path ):
36- """Set the path of MATLAB to use for custom functions.
37-
38- This will also register the MATLAB COM server on Windows OS which could be slow.
39-
40- Parameters
41- ----------
42- matlab_path : string
43- The full path of the MATLAB executable
44- """
45- if not matlab_exe_path :
46- return
47-
48- if platform .system () == "Windows" :
49- process = subprocess .Popen (f'"{ matlab_exe_path } " -batch "comserver(\' register\' )"' )
50- process .wait ()
51-
52- with open (MATLAB_PATH_FILE , "w" ) as path_file :
53- path_file .write (matlab_exe_path )
54-
55-
56- def get_matlab_paths (matlab_exe_path ):
40+ def get_matlab_paths (matlab_exe_path : str ) -> Tuple [str , str ]:
5741 """Return paths for loading MATLAB engine C interface dynamic libraries.
5842
5943 Parameters
6044 ----------
61- matlab_exe_path : string
45+ matlab_exe_path : str
6246 The full path of the MATLAB executable
6347
6448 Returns
6549 -------
66- paths : Tuple[string, string ]
50+ paths : Tuple[str, str ]
6751 The path of the MATLAB bin and the DLL location
6852 """
6953 if not matlab_exe_path :
@@ -87,64 +71,82 @@ def get_matlab_paths(matlab_exe_path):
8771 return f"{ bin_path .as_posix ()} /" , f"{ dll_path .as_posix ()} /"
8872
8973
90- def start_matlab ():
74+ def start_matlab (matlab_exe_path : str = "" ):
9175 """Load MATLAB engine dynamic libraries and creates wrapper object.
92-
76+
77+ Parameters
78+ ----------
79+ matlab_exe_path : str, default ""
80+ The full path of the MATLAB executable
81+
9382 Returns
9483 -------
9584 engine : Optional[RATapi.rat_core.MatlabEngine]
9685 A matlab engine object
97-
9886 """
99- try :
100- with open (MATLAB_PATH_FILE ) as path_file :
101- matlab_path = path_file .read ()
102- except FileNotFoundError :
103- matlab_path = ""
104-
105- if not matlab_path :
106- matlab_path = shutil .which ("matlab" )
107- if matlab_path is None :
108- matlab_path = ""
109- else :
110- print (matlab_path )
111- temp = pathlib .Path (matlab_path )
112- if temp .is_symlink ():
113- matlab_path = temp .readlink ().as_posix ()
114- set_matlab_path (matlab_path )
87+ matlab_exe_path = find_existing_matlab ()
11588
116- if matlab_path :
117- bin_path , dll_path = get_matlab_paths (matlab_path )
89+ if matlab_exe_path :
90+ bin_path , dll_path = get_matlab_paths (matlab_exe_path )
11891 os .environ ["MATLAB_DLL_PATH" ] = dll_path
11992 if platform .system () == "Windows" :
12093 os .environ ["PATH" ] = dll_path + os .pathsep + os .environ ["PATH" ]
12194
12295 with cd (bin_path ):
12396 engine = RATapi .rat_core .MatlabEngine ()
124-
97+ # Ensure MATLAB is closed when Python shuts down.
98+ atexit .register (engine .close )
99+
125100 return engine
126101
127102
103+ def set_matlab_path (matlab_exe_path : str ) -> None :
104+ """Set the path of MATLAB to use for custom functions.
105+
106+ This will also register the MATLAB COM server on Windows OS which could be slow.
107+
108+ Parameters
109+ ----------
110+ matlab_exe_path : str
111+ The full path of the MATLAB executable
112+ """
113+ if not matlab_exe_path :
114+ return
115+
116+ global __MATLAB_ENGINE
117+ if __MATLAB_ENGINE is not None :
118+ __MATLAB_ENGINE .close ()
119+ atexit .unregister (__MATLAB_ENGINE .close )
120+ __MATLAB_ENGINE = start_matlab (matlab_exe_path )
121+
122+
123+ if platform .system () == "Windows" :
124+ process = subprocess .Popen (f'"{ matlab_exe_path } " -batch "comserver(\' register\' )"' )
125+ process .wait ()
126+
127+ with open (MATLAB_PATH_FILE , "w" ) as path_file :
128+ path_file .write (matlab_exe_path )
129+
130+
128131class MatlabWrapper :
129132 """Creates a python callback for a MATLAB function.
130133
131134 Parameters
132135 ----------
133- filename : string
136+ filename : str
134137 The path of the file containing MATLAB function
135138 """
136139
137- engine = start_matlab ()
138-
139140 def __init__ (self , filename ) -> None :
140- if self .engine is None :
141+ engine = get_matlab_engine ()
142+ if engine is None :
141143 raise ValueError (
142144 "MATLAB is not found. Please use `set_matlab_path` to set the location of your MATLAB installation"
143145 ) from None
144146
145147 path = pathlib .Path (filename )
146- self . engine .cd (str (path .parent ))
147- self . engine .setFunction (path .stem )
148+ engine .cd (str (path .parent ))
149+ engine .setFunction (path .stem )
148150
149151 def getHandle (self ) -> Callable [[ArrayLike , ArrayLike , ArrayLike , int , int ], tuple [ArrayLike , float ]]:
150152 """Return a wrapper for the custom dynamic library function.
@@ -157,7 +159,7 @@ def getHandle(self) -> Callable[[ArrayLike, ArrayLike, ArrayLike, int, int], tup
157159 """
158160
159161 def handle (* args ):
160- return self . engine .invoke (* args )
162+ return get_matlab_engine () .invoke (* args )
161163
162164 return handle
163165
@@ -191,3 +193,24 @@ def handle(*args):
191193 return self .engine .invoke (* args )
192194
193195 return handle
196+
197+
198+ def find_existing_matlab () -> str :
199+ matlab_exe_path = ""
200+
201+ with suppress (FileNotFoundError ), open (MATLAB_PATH_FILE ) as path_file :
202+ matlab_exe_path = path_file .read ()
203+
204+ if not matlab_exe_path :
205+ matlab_exe_path = shutil .which ("matlab" )
206+ if matlab_exe_path is None :
207+ matlab_exe_path = ""
208+ else :
209+ temp = pathlib .Path (matlab_exe_path )
210+ if temp .is_symlink ():
211+ matlab_exe_path = temp .readlink ().as_posix ()
212+ set_matlab_path (matlab_exe_path )
213+
214+ return matlab_exe_path
215+
216+ __MATLAB_ENGINE = start_matlab ()
0 commit comments