from __future__ import print_function """ Simple system for interfacing with a MATLAB process using stdin/stdout pipes. """ from .process import Process from io import StringIO import scipy.io import numpy as np import tempfile import os, sys, glob, signal import weakref import atexit class MatlabProcess(object): """ This class starts a new matlab process, allowing remote control of the interpreter and transfer of data between python and matlab. """ # Implements a MATLAB CLI that is a bit easier for us to parse _bootstrap = r""" while true fprintf('\n::ready\n'); % Accumulate command lines until we see ::cmd_done cmd = ''; empty_count = 0; while true line = input('', 's'); if length(line) == 0 % If we encounter many empty lines, assume the host process % has ended. Is there a better way to detect this? empty_count = empty_count + 1; if empty_count > 100 fprintf('Shutting down!\n'); exit; end end if strcmp(line, '::cmd_done') break end cmd = [cmd, line, sprintf('\n')]; end % Evaluate command try %fprintf('EVAL: %s\n', cmd); eval(cmd); fprintf('\n::ok\n'); catch err fprintf('\n::err\n'); fprintf(['::message:', err.message, '\n']); fprintf(['::identifier:', err.identifier, '\n']); for i = 1:length(err.stack) frame = err.stack(i,1); fprintf(['::stack:', frame.name, ' in ', frame.file, ' line ', frame.line]); end end end """ def __init__(self, executable=None, **kwds): self.__closed = False self.__refs = weakref.WeakValueDictionary() # Decide which executables to try if executable is not None: execs = [executable] else: execs = ["matlab"] # always pick the matlab in the path, if available if sys.platform == "darwin": installed = glob.glob("/Applications/MATLAB_R*") installed.sort(reverse=True) execs.extend([os.path.join(p, "bin", "matlab") for p in installed]) # try starting each in order until one works self.__proc = None for exe in execs: try: self.__proc = Process([exe, "-nodesktop", "-nosplash"], **kwds) break except Exception as e: last_exception = e pass # bail out if we couldn't start any if self.__proc is None: raise RuntimeError( "Could not start MATLAB process.\nPaths attempted: %s " "\nLast error: %s" % (str(execs), str(e)) ) # Wait a moment for MATLAB to start up, # read the version string while True: line = self.__proc.stdout.readline() if "Copyright" in line: # next line is version info self.__version_str = self.__proc.stdout.readline().strip() break # start input loop self.__proc.stdin.write(self._bootstrap) # wait for input loop to be ready while True: line = self.__proc.stdout.readline() if line == "::ready\n": break atexit.register(self.close) def __call__(self, cmd, parse_result=True): """ Execute the specified statement(s) on the MATLAB interpreter and return the output string or raise an exception if there was an error. """ assert not self.__closed, "MATLAB process has already closed." if cmd[-1] != "\n": cmd += "\n" cmd += "::cmd_done\n" self.__proc.stdout.read() self.__proc.stdin.write(cmd) if parse_result: return self._parse_result() def _parse_result(self): assert not self.__closed, "MATLAB process has already closed." output = [] while True: line = self.__proc.stdout.readline() if line == "::ready\n": break output.append(line) for i in reversed(range(len(output))): line = output[i] if line == "::ok\n": return "".join(output[:i]) elif line == "::err\n": raise MatlabError(output[i + 1 :], output[:i]) raise RuntimeError("No success/failure code found in output (printed above).") def _get(self, name): """ Transfer an object from MATLAB to Python. """ assert isinstance(name, str) tmp = tempfile.mktemp(suffix=".mat") out = self("save('%s', '%s', '-v7')" % (tmp, name)) objs = scipy.io.loadmat(tmp) os.remove(tmp) return objs[name] def _set(self, **kwds): """ Transfer an object from Python to MATLAB and assign it to the given variable name. """ tmp = tempfile.mktemp(suffix=".mat") scipy.io.savemat(tmp, kwds) self("load('%s')" % tmp) os.remove(tmp) def _get_via_pipe(self, name): """ Transfer an object from MATLAB to Python. This method sends data over the pipe, but is less reliable than get(). """ assert isinstance(name, str) out = self("save('stdio', '%s', '-v7')" % name) start = stop = None for i, line in enumerate(out): if line.startswith("start_binary"): start = i elif line.startswith("::ok"): stop = i data = "".join(out[start + 1 : stop]) io = StringIO(data[:-1]) objs = scipy.io.loadmat(io) return objs[name] def _set_via_pipe(self, **kwds): """ Transfer an object from Python to MATLAB and assign it to the given variable name. This method sends data over the pipe, but is less reliable than set(). """ assert not self.__closed, "MATLAB process has already closed." io = StringIO() scipy.io.savemat(io, kwds) io.seek(0) strn = io.read() self.__proc.stdout.read() self.__proc.stdin.write("load('stdio')\n::cmd_done\n") while True: line = self.__proc.stdout.readline() if line == "ack load stdio\n": # now it is safe to send data break self.__proc.stdin.write(strn) self.__proc.stdin.write("\n") while True: line = self.__proc.stdout.readline() if line == "ack load finished\n": break self._parse_result() def exist(self, name): if name == "exist": return 5 else: for line in self("exist %s" % name).split("\n"): try: return int(line.strip()) except ValueError: pass def __getattr__(self, name): if name not in self.__refs: ex = self.exist(name) if ex == 0: raise AttributeError("No object named '%s' in matlab workspace." % name) elif ex in (2, 3, 5): r = MatlabFunction(self, name) elif ex == 1: r = self._mkref(name) else: typ = {4: "library file", 6: "P-file", 7: "folder", 8: "class"}[ex] raise TypeError("Variable '%s' has unsupported type '%s'" % (name, typ)) self.__refs[name] = r return self.__refs[name] def _mkref(self, name): assert name not in self.__refs ref = MatlabReference(self, name) self.__refs[name] = ref return ref def __setattr__(self, name, value): if name.startswith("_MatlabProcess__"): object.__setattr__(self, name, value) else: self._set(**{name: value}) def close(self): if self.__closed: return self("exit;\n", parse_result=False) self.__closed = True class MatlabReference(object): """ Reference to a variable in the matlab workspace. """ def __init__(self, proc, name): self._proc = proc self._name = name @property def name(self): return self._name def get(self): return self._proc._get(self._name) def clear(self): self._proc("clear %s;" % self._name) def __del__(self): self.clear() class MatlabFunction(object): """ Proxy to a MATLAB function. Calling this object transfers the arguments to MATLAB, invokes the function remotely, and then transfers the return value back to Python. """ def __init__(self, proc, name): self._proc = proc self._name = name self._nargout = None @property def nargout(self): """ Number of output arguments for this function. For some functions, requesting nargout() will fail. In these cases, the nargout property must be set manually before calling the function. """ if self._nargout is None: cmd = "fprintf('%%d\\n', nargout('%s'));" % (self._name) ret = self._proc(cmd) self._nargout = int(ret.strip()) return self._nargout @nargout.setter def nargout(self, n): self._nargout = n def __call__(self, *args, **kwds): """ Call this function with the given arguments. If _transfer is False, then the return values are left in MATLAB and references to these values are returned instead. """ import pyqtgraph as pg _transfer = kwds.pop("_transfer", True) assert len(kwds) == 0 # for generating unique variable names rand = np.random.randint(1e12) # store args to temporary variables, excluding those already present # in the workspace argnames = [] upload = {} for i, arg in enumerate(args): if isinstance(arg, MatlabReference): argnames.append(arg.name) elif np.isscalar(arg): argnames.append(repr(arg)) else: argname = "%s_%d_%d" % (self._name, i, rand) argnames.append(argname) upload[argname] = arg if len(upload) > 0: self._proc._set(**upload) try: # get number of output args nargs = self.nargout # invoke function, fetch return value(s) retvars = ["%s_rval_%d_%d" % (self._name, i, rand) for i in range(nargs)] cmd = "[%s] = %s(%s);" % (",".join(retvars), self._name, ",".join(argnames)) self._proc(cmd) if _transfer: ret = [self._proc._get(var) for var in retvars] self._proc("clear %s;" % (" ".join(retvars))) else: ret = [self._proc._mkref(name) for name in retvars] if len(ret) == 1: ret = ret[0] else: ret = tuple(ret) return ret finally: # clear all temp variables clear = list(upload.keys()) # if _transfer: # clear += retvars if len(clear) > 0: cmd = "clear %s;" % (" ".join(clear)) self._proc(cmd) return ret class MatlabError(Exception): def __init__(self, error, output): self.output = "".join(output) for line in error: self.stack = [] if line.startswith("::message:"): self.message = line[10:].strip() elif line.startswith("::identifier:"): self.identifier = line[13:].strip() elif line.startswith("::stack:"): self.stack.append(line[8:].strip()) def __repr__(self): return "MatlabError(message=%s, identifier=%s)" % ( repr(self.message), repr(self.identifier), ) def __str__(self): if len(self.stack) > 0: stack = "\nMATLAB Stack:\n%s\nMATLAB Error: " % "\n".join(self.stack) return stack + self.message else: return self.message if __name__ == "__main__": p = MatlabProcess() io = StringIO() scipy.io.savemat(io, {"x": 1}) io.seek(0) strn = io.read()