From 63ee4e23268a580843e7679db1e3bab69967950a Mon Sep 17 00:00:00 2001 From: Ismail Badawi Date: Tue, 29 Jul 2014 19:13:26 -0400 Subject: [PATCH 1/8] Rename json functions away from +pkg syntax. --- pymatbridge/matlab/matlabserver.m | 4 ++-- .../util/json_v0.2.2/{+json => json}/java/README | 0 .../util/json_v0.2.2/{+json => json}/java/json.jar | Bin .../json_v0.2.2/{+json/dump.m => json/json_dump.m} | 6 +++--- .../json_v0.2.2/{+json/load.m => json/json_load.m} | 6 +++--- .../json_v0.2.2/{+json/read.m => json/json_read.m} | 4 ++-- .../{+json/startup.m => json/json_startup.m} | 4 ++-- .../{+json/write.m => json/json_write.m} | 4 ++-- pymatbridge/matlab/util/pymat_eval.m | 4 ++-- pymatbridge/matlab/util/pymat_feval.m | 4 ++-- pymatbridge/matlab/util/pymat_get_variable.m | 4 ++-- 11 files changed, 20 insertions(+), 20 deletions(-) rename pymatbridge/matlab/util/json_v0.2.2/{+json => json}/java/README (100%) rename pymatbridge/matlab/util/json_v0.2.2/{+json => json}/java/json.jar (100%) rename pymatbridge/matlab/util/json_v0.2.2/{+json/dump.m => json/json_dump.m} (97%) rename pymatbridge/matlab/util/json_v0.2.2/{+json/load.m => json/json_load.m} (98%) rename pymatbridge/matlab/util/json_v0.2.2/{+json/read.m => json/json_read.m} (88%) rename pymatbridge/matlab/util/json_v0.2.2/{+json/startup.m => json/json_startup.m} (97%) rename pymatbridge/matlab/util/json_v0.2.2/{+json/write.m => json/json_write.m} (87%) diff --git a/pymatbridge/matlab/matlabserver.m b/pymatbridge/matlab/matlabserver.m index 55b223b..9e20c1e 100644 --- a/pymatbridge/matlab/matlabserver.m +++ b/pymatbridge/matlab/matlabserver.m @@ -3,12 +3,12 @@ function matlabserver(socket_address) % over the socket. I then enters the listen-respond mode until it gets an % "exit" command -json.startup +json_startup messenger('init', socket_address); while(1) msg_in = messenger('listen'); - req = json.load(msg_in); + req = json_load(msg_in); switch(req.cmd) case {'connect'} diff --git a/pymatbridge/matlab/util/json_v0.2.2/+json/java/README b/pymatbridge/matlab/util/json_v0.2.2/json/java/README similarity index 100% rename from pymatbridge/matlab/util/json_v0.2.2/+json/java/README rename to pymatbridge/matlab/util/json_v0.2.2/json/java/README diff --git a/pymatbridge/matlab/util/json_v0.2.2/+json/java/json.jar b/pymatbridge/matlab/util/json_v0.2.2/json/java/json.jar similarity index 100% rename from pymatbridge/matlab/util/json_v0.2.2/+json/java/json.jar rename to pymatbridge/matlab/util/json_v0.2.2/json/java/json.jar diff --git a/pymatbridge/matlab/util/json_v0.2.2/+json/dump.m b/pymatbridge/matlab/util/json_v0.2.2/json/json_dump.m similarity index 97% rename from pymatbridge/matlab/util/json_v0.2.2/+json/dump.m rename to pymatbridge/matlab/util/json_v0.2.2/json/json_dump.m index 00690a0..b5d3970 100644 --- a/pymatbridge/matlab/util/json_v0.2.2/+json/dump.m +++ b/pymatbridge/matlab/util/json_v0.2.2/json/json_dump.m @@ -1,4 +1,4 @@ -function str = dump(value, varargin) +function str = json_dump(value, varargin) %DUMP Encode matlab value into a JSON string. % % SYNOPSIS @@ -64,7 +64,7 @@ % % See also json.load json.write - json.startup('WarnOnAddpath', true); + json_startup('WarnOnAddpath', true); options = get_options_(varargin{:}); obj = dump_data_(value, options); if isempty(options.indent) @@ -149,4 +149,4 @@ else error('json:typeError', 'Unsupported data type: %s', class(value)); end -end \ No newline at end of file +end diff --git a/pymatbridge/matlab/util/json_v0.2.2/+json/load.m b/pymatbridge/matlab/util/json_v0.2.2/json/json_load.m similarity index 98% rename from pymatbridge/matlab/util/json_v0.2.2/+json/load.m rename to pymatbridge/matlab/util/json_v0.2.2/json/json_load.m index 3829a79..3ce7b42 100644 --- a/pymatbridge/matlab/util/json_v0.2.2/+json/load.m +++ b/pymatbridge/matlab/util/json_v0.2.2/json/json_load.m @@ -1,4 +1,4 @@ -function value = load(str, varargin) +function value = json_load(str, varargin) %LOAD Load matlab value from a JSON string. % % SYNOPSIS @@ -57,7 +57,7 @@ % % See also json.dump json.read - json.startup('WarnOnAddpath', true); + json_startup('WarnOnAddpath', true); options = get_options_(varargin{:}); singleton = false; @@ -196,4 +196,4 @@ fields = fieldnames(value); vec = [vec, uint8([fields{:}])]; end -end \ No newline at end of file +end diff --git a/pymatbridge/matlab/util/json_v0.2.2/+json/read.m b/pymatbridge/matlab/util/json_v0.2.2/json/json_read.m similarity index 88% rename from pymatbridge/matlab/util/json_v0.2.2/+json/read.m rename to pymatbridge/matlab/util/json_v0.2.2/json/json_read.m index 411bc6e..213fa47 100644 --- a/pymatbridge/matlab/util/json_v0.2.2/+json/read.m +++ b/pymatbridge/matlab/util/json_v0.2.2/json/json_read.m @@ -1,4 +1,4 @@ -function value = read(filename, varargin) +function value = json_read(filename, varargin) %READ Load a matlab value from a JSON file. % % SYNOPSIS @@ -19,7 +19,7 @@ fid = 0; try fid = fopen(filename, 'r'); - value = json.load(fscanf(fid, '%c', inf)); + value = json_load(fscanf(fid, '%c', inf)); fclose(fid); catch e if fid ~= 0, fclose(fid); end diff --git a/pymatbridge/matlab/util/json_v0.2.2/+json/startup.m b/pymatbridge/matlab/util/json_v0.2.2/json/json_startup.m similarity index 97% rename from pymatbridge/matlab/util/json_v0.2.2/+json/startup.m rename to pymatbridge/matlab/util/json_v0.2.2/json/json_startup.m index f88edac..77c0853 100644 --- a/pymatbridge/matlab/util/json_v0.2.2/+json/startup.m +++ b/pymatbridge/matlab/util/json_v0.2.2/json/json_startup.m @@ -1,4 +1,4 @@ -function startup(varargin) +function json_startup(varargin) %STARTUP Initialize runtime environment. % % SYNOPSIS @@ -42,4 +42,4 @@ function startup(varargin) ]); end end -end \ No newline at end of file +end diff --git a/pymatbridge/matlab/util/json_v0.2.2/+json/write.m b/pymatbridge/matlab/util/json_v0.2.2/json/json_write.m similarity index 87% rename from pymatbridge/matlab/util/json_v0.2.2/+json/write.m rename to pymatbridge/matlab/util/json_v0.2.2/json/json_write.m index 707dd08..8d8862c 100644 --- a/pymatbridge/matlab/util/json_v0.2.2/+json/write.m +++ b/pymatbridge/matlab/util/json_v0.2.2/json/json_write.m @@ -1,4 +1,4 @@ -function write(value, filename, varargin) +function json_write(value, filename, varargin) %WRITE Write a matlab value into a JSON file. % % SYNOPSIS @@ -19,7 +19,7 @@ function write(value, filename, varargin) fid = 0; try fid = fopen(filename, 'w'); - fprintf(fid, '%s', json.dump(value, varargin{:})); + fprintf(fid, '%s', json_dump(value, varargin{:})); fclose(fid); catch e if fid ~= 0, fclose(fid); end diff --git a/pymatbridge/matlab/util/pymat_eval.m b/pymatbridge/matlab/util/pymat_eval.m index b071cb8..510d753 100644 --- a/pymatbridge/matlab/util/pymat_eval.m +++ b/pymatbridge/matlab/util/pymat_eval.m @@ -26,7 +26,7 @@ if ~code_check response.message = 'No code provided as POST parameter'; - json_response = json.dump(response); + json_response = json_dump(response); return; end @@ -71,6 +71,6 @@ response.content.code = code; -json_response = json.dump(response); +json_response = json_dump(response); end %function diff --git a/pymatbridge/matlab/util/pymat_feval.m b/pymatbridge/matlab/util/pymat_feval.m index 2fd3a48..137abcb 100644 --- a/pymatbridge/matlab/util/pymat_feval.m +++ b/pymatbridge/matlab/util/pymat_feval.m @@ -20,7 +20,7 @@ if ~func_path_check response.message = 'No function given as func_path POST parameter'; - json_response = json.dump(response); + json_response = json_dump(response); return end @@ -35,7 +35,7 @@ response.success = 'true'; response.message = 'Successfully completed request'; - json_response = json.dump(response); + json_response = json_dump(response); return diff --git a/pymatbridge/matlab/util/pymat_get_variable.m b/pymatbridge/matlab/util/pymat_get_variable.m index c37fa90..391a133 100644 --- a/pymatbridge/matlab/util/pymat_get_variable.m +++ b/pymatbridge/matlab/util/pymat_get_variable.m @@ -17,7 +17,7 @@ if ~varname_check response.message = 'No variable name provided as input argument'; - json_response = json.dump(response); + json_response = json_dump(response); return end @@ -26,7 +26,7 @@ response.var = evalin('base', varname); -json_response = json.dump(response); +json_response = json_dump(response); return end From 30d2d501b22fb744d78c0ad4f95c6d690925e262 Mon Sep 17 00:00:00 2001 From: Ismail Badawi Date: Tue, 29 Jul 2014 19:24:05 -0400 Subject: [PATCH 2/8] Use Octave-compatible syntax for calling Java. --- .../matlab/util/json_v0.2.2/json/json_dump.m | 15 ++++++++------- .../matlab/util/json_v0.2.2/json/json_load.m | 9 +++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/pymatbridge/matlab/util/json_v0.2.2/json/json_dump.m b/pymatbridge/matlab/util/json_v0.2.2/json/json_dump.m index b5d3970..e582047 100644 --- a/pymatbridge/matlab/util/json_v0.2.2/json/json_dump.m +++ b/pymatbridge/matlab/util/json_v0.2.2/json/json_dump.m @@ -95,11 +95,12 @@ function obj = dump_data_(value, options) %DUMP_DATA_ if ischar(value) && (isvector(value) || isempty(value)) - obj = java.lang.String(value); + obj = javaObject('java.lang.String', value); elseif isempty(value) && isnumeric(value) - obj = org.json.JSONObject.NULL; + json_object = javaObject('org.json.JSONObject'); + obj = json_object.NULL; elseif ~isscalar(value) - obj = org.json.JSONArray(); + obj = javaObject('org.json.JSONArray'); if ndims(value) > 2 split_value = num2cell(value, 1:ndims(value)-1); @@ -124,13 +125,13 @@ end end elseif iscell(value) - obj = org.json.JSONArray(); + obj = javaObject('org.json.JSONArray'); for i = 1:numel(value) obj.put(dump_data_(value{i}, options)); end elseif isnumeric(value) if isreal(value) - obj = java.lang.Double(value); + obj = javaObject('java.lang.Double', value); % Encode complex number as a struct else complex_struct = struct; @@ -139,9 +140,9 @@ obj = dump_data_(complex_struct, options); end elseif islogical(value) - obj = java.lang.Boolean(value); + obj = javaObject('java.lang.Boolean', value); elseif isstruct(value) - obj = org.json.JSONObject(); + obj = javaObject('org.json.JSONObject'); keys = fieldnames(value); for i = 1:length(keys) obj.put(keys{i},dump_data_(value.(keys{i}), options)); diff --git a/pymatbridge/matlab/util/json_v0.2.2/json/json_load.m b/pymatbridge/matlab/util/json_v0.2.2/json/json_load.m index 3ce7b42..39dcf6d 100644 --- a/pymatbridge/matlab/util/json_v0.2.2/json/json_load.m +++ b/pymatbridge/matlab/util/json_v0.2.2/json/json_load.m @@ -66,11 +66,11 @@ error('json:invalidString','Invalid JSON string'); end if str(1)=='{' - node = org.json.JSONObject(java.lang.String(str)); + node = javaObject('org.json.JSONObject', javaObject('java.lang.String', str)); else singleton = str(1) ~= '[' && str(end) ~= ']'; if singleton, str = ['[',str,']']; end - node = org.json.JSONArray(java.lang.String(str)); + node = javaObject('org.json.JSONArray', javaObject('java.lang.String', str)); end value = parse_data_(node, options); if singleton, value = value{:}; end @@ -119,7 +119,7 @@ warning('json:fieldNameConflict', ... 'Field %s renamed to %s', field, safe_field); end - value.(safe_field) = parse_data_(node.get(java.lang.String(key)), ... + value.(safe_field) = parse_data_(node.get(javaObject('java.lang.String', key)), ... options); end % Check if the struct just decoded represents a complex number @@ -127,7 +127,8 @@ complex_value = complex(value.real, value.imag); value = complex_value; end - elseif isa(node, 'org.json.JSONObject$Null') + % In MATLAB, nested classes end up with a $ in the name, in Octave it's a . + elseif isa(node, 'org.json.JSONObject$Null') || isa(node, 'org.json.JSONObject.Null') value = []; else error('json:typeError', 'Unknown data type: %s', class(node)); From a2f398290b1030e87754a9daeb5da130d54c0ea9 Mon Sep 17 00:00:00 2001 From: Ismail Badawi Date: Mon, 28 Jul 2014 16:20:45 -0400 Subject: [PATCH 3/8] Add support for invoking Octave instead of Matlab. * Extract most of the Matlab class into a base Session class * Matlab and Octave are subclasses that tweak the command line invocation * Eliminate some code duplication along the way --- pymatbridge/matlab_magic.py | 5 +- pymatbridge/pymatbridge.py | 264 ++++++++++++++++++++---------------- 2 files changed, 150 insertions(+), 119 deletions(-) diff --git a/pymatbridge/matlab_magic.py b/pymatbridge/matlab_magic.py index 384bc13..b7d7f92 100644 --- a/pymatbridge/matlab_magic.py +++ b/pymatbridge/matlab_magic.py @@ -114,7 +114,7 @@ def eval(self, line): """ Parse and evaluate a single line of matlab """ - run_dict = self.Matlab.run_code(line, maxtime=self.Matlab.maxtime) + run_dict = self.Matlab.run_code(line) if run_dict['success'] == 'false': raise MatlabInterperterError(line, run_dict['content']['stdout']) @@ -127,8 +127,7 @@ def set_matlab_var(self, name, value): Set up a variable in Matlab workspace """ run_dict = self.Matlab.run_func("pymat_set_variable.m", - {'name':name, 'value':value}, - maxtime=self.Matlab.maxtime) + {'name':name, 'value':value}) if run_dict['success'] == 'false': raise MatlabInterperterError(line, run_dict['content']['stdout']) diff --git a/pymatbridge/pymatbridge.py b/pymatbridge/pymatbridge.py index 8e6c95c..3225490 100644 --- a/pymatbridge/pymatbridge.py +++ b/pymatbridge/pymatbridge.py @@ -12,9 +12,7 @@ import os, time import zmq import subprocess -import platform import sys - import json # JSON encoder extension to handle complex numbers @@ -31,97 +29,61 @@ def as_complex(dct): return complex(dct['real'], dct['imag']) return dct - MATLAB_FOLDER = '%s/matlab' % os.path.realpath(os.path.dirname(__file__)) -# Start a Matlab server and bind it to a ZMQ socket(TCP/IPC) -def _run_matlab_server(matlab_bin, matlab_socket_addr, matlab_log, matlab_id, matlab_startup_options): - command = matlab_bin - command += ' %s ' % matlab_startup_options - command += ' -r "' - command += "addpath(genpath(" - command += "'%s'" % MATLAB_FOLDER - command += ')), matlabserver(\'%s\'),exit"' % matlab_socket_addr - - if matlab_log: - command += ' -logfile ./pymatbridge/logs/matlablog_%s.txt > ./pymatbridge/logs/bashlog_%s.txt' % (matlab_id, matlab_id) - - subprocess.Popen(command, shell = True, stdin=subprocess.PIPE) - return True - - -class Matlab(object): +class Session(object): """ - A class for communicating with a matlab session + A class for communicating with a MATLAB session. It provides the behavior + common across different MATLAB implementations. You shouldn't instantiate + this directly; rather, use the Matlab or Octave subclasses. """ - def __init__(self, matlab='matlab', socket_addr=None, + def __init__(self, executable, socket_addr=None, id='python-matlab-bridge', log=False, maxtime=60, platform=None, startup_options=None): - """ - Initialize this thing. - - Parameters - ---------- - - matlab : str - A string that woul start matlab at the terminal. Per default, this - is set to 'matlab', so that you can alias in your bash setup - - socket_addr : str - A string that represents a valid ZMQ socket address, such as - "ipc:///tmp/pymatbridge", "tcp://127.0.0.1:55555", etc. - - id : str - An identifier for this instance of the pymatbridge - - log : bool - Whether to save a log file in some known location. - - maxtime : float - The maximal time to wait for a response from matlab (optional, - Default is 10 sec) - - platform : string - The OS of the machine on which this is running. Per default this - will be taken from sys.platform. - """ - # Setup internal state variables self.started = False self.running = False - self.matlab = matlab + self.executable = executable self.socket_addr = socket_addr - self.id = id self.log = log self.maxtime = maxtime + self.platform = platform if platform is not None else sys.platform + self.startup_options = startup_options - if platform is None: - self.platform = sys.platform - else: - self.platform = platform - - if self.socket_addr is None: # use the default + if socket_addr is None: self.socket_addr = "tcp://127.0.0.1:55555" if self.platform == "win32" else "ipc:///tmp/pymatbridge" - if startup_options: - self.startup_options = startup_options - elif self.platform == 'win32': - self.startup_options = ' -automation -noFigureWindows' - else: - self.startup_options = ' -nodesktop -nodisplay' + if self.log: + startup_options += ' > ./pymatbridge/logs/bashlog_%s.txt' % self.id self.context = None self.socket = None + def _preamble_code(self): + return ["addpath(genpath('%s'))" % MATLAB_FOLDER] + + def _execute_flag(self): + raise NotImplemented + + def _run_server(self): + code = self._preamble_code() + code.extend([ + "matlabserver('%s')" % self.socket_addr, + 'exit' + ]) + command = '%s %s %s "%s"' % (self.executable, self.startup_options, + self._execute_flag(), ','.join(code)) + subprocess.Popen(command, shell=True, stdin=subprocess.PIPE) + # Start server/client session and make the connection def start(self): # Start the MATLAB server in a new process print "Starting MATLAB on ZMQ socket %s" % (self.socket_addr) print "Send 'exit' command to kill the server" - _run_matlab_server(self.matlab, self.socket_addr, self.log, self.id, self.startup_options) + self._run_server() # Start the client self.context = zmq.Context() @@ -131,22 +93,23 @@ def start(self): self.started = True # Test if connection is established - if (self.is_connected()): + if self.is_connected(): print "MATLAB started and connected!" return True else: print "MATLAB failed to start" return False - - # Stop the Matlab server - def stop(self): - req = json.dumps(dict(cmd="exit"), cls=ComplexEncoder) + def _response(self, **kwargs): + req = json.dumps(kwargs, cls=ComplexEncoder) self.socket.send(req) resp = self.socket.recv_string() + return resp + # Stop the Matlab server + def stop(self): # Matlab should respond with "exit" if successful - if resp == "exit": + if self._response(cmd='exit') == "exit": print "MATLAB closed" self.started = False @@ -162,69 +125,138 @@ def is_connected(self): self.socket.send(req) start_time = time.time() - while(True): + while True: try: resp = self.socket.recv_string(flags=zmq.NOBLOCK) - if resp == "connected": - return True - else: - return False + return resp == "connected" except zmq.ZMQError: sys.stdout.write('.') time.sleep(1) - if (time.time() - start_time > self.maxtime) : - print "Matlab session timed out after %d seconds" % (self.maxtime) + if time.time() - start_time > self.maxtime: + print "Matlab session timed out after %d seconds" % self.maxtime return False - def is_function_processor_working(self): result = self.run_func('%s/test_functions/test_sum.m' % MATLAB_FOLDER, {'echo': 'Matlab: Function processor is working!'}) - if result['success'] == 'true': - return True - else: - return False - + return result['success'] == 'true' - # Run a function in Matlab and return the result - def run_func(self, func_path, func_args=None, maxtime=None): + def _json_response(self, **kwargs): if self.running: time.sleep(0.05) + return json.loads(self._response(**kwargs), object_hook=as_complex) - req = dict(cmd="run_function") - req['func_path'] = func_path - req['func_args'] = func_args + # Run a function in Matlab and return the result + def run_func(self, func_path, func_args=None): + return self._json_response(cmd='run_function', + func_path=func_path, + func_args=func_args) - req = json.dumps(req, cls=ComplexEncoder) - self.socket.send(req) - resp = self.socket.recv_string() - resp = json.loads(resp, object_hook=as_complex) + # Run some code in Matlab command line provide by a string + def run_code(self, code): + return self._json_response(cmd='run_code', code=code) - return resp + def get_variable(self, varname): + return self._json_response(cmd='get_var', varname=varname)['var'] - # Run some code in Matlab command line provide by a string - def run_code(self, code, maxtime=None): - if self.running: - time.sleep(0.05) - req = dict(cmd="run_code") - req['code'] = code - req = json.dumps(req, cls=ComplexEncoder) - self.socket.send(req) - resp = self.socket.recv_string() - resp = json.loads(resp, object_hook=as_complex) +class Matlab(Session): + def __init__(self, executable='matlab', socket_addr=None, + id='python-matlab-bridge', log=False, maxtime=60, + platform=None, startup_options=None): + """ + Initialize this thing. - return resp + Parameters + ---------- - def get_variable(self, varname, maxtime=None): - if self.running: - time.sleep(0.05) + executable : str + A string that would start Matlab at the terminal. Per default, this + is set to 'matlab', so that you can alias in your bash setup - req = dict(cmd="get_var") - req['varname'] = varname - req = json.dumps(req, cls=ComplexEncoder) - self.socket.send(req) - resp = self.socket.recv_string() - resp = json.loads(resp, object_hook=as_complex) + socket_addr : str + A string that represents a valid ZMQ socket address, such as + "ipc:///tmp/pymatbridge", "tcp://127.0.0.1:55555", etc. + + id : str + An identifier for this instance of the pymatbridge. + + log : bool + Whether to save a log file in some known location. - return resp['var'] + maxtime : float + The maximal time to wait for a response from matlab (optional, + Default is 10 sec) + + platform : string + The OS of the machine on which this is running. Per default this + will be taken from sys.platform. + startup_options : string + Command line options to pass to MATLAB. Optional; sensible defaults + are used if this is not provided. + """ + if platform is None: + platform = sys.platform + if startup_options is None: + if platform == 'win32': + startup_options = ' -automation -noFigureWindows' + else: + startup_options = ' -nodesktop -nodisplay' + if log: + startup_options += ' -logfile ./pymatbridge/logs/matlablog_%s.txt' % id + super(Matlab, self).__init__(executable, socket_addr, id, log, maxtime, + platform, startup_options) + + def _execute_flag(self): + return '-r' + + +class Octave(Session): + def __init__(self, executable='octave', socket_addr=None, + id='python-matlab-bridge', log=False, maxtime=60, + platform=None, startup_options=None): + """ + Initialize this thing. + + Parameters + ---------- + + executable : str + A string that would start Octave at the terminal. Per default, this + is set to 'octave', so that you can alias in your bash setup + + socket_addr : str + A string that represents a valid ZMQ socket address, such as + "ipc:///tmp/pymatbridge", "tcp://127.0.0.1:55555", etc. + + id : str + An identifier for this instance of the pymatbridge. + + log : bool + Whether to save a log file in some known location. + + maxtime : float + The maximal time to wait for a response from matlab (optional, + Default is 10 sec) + + platform : string + The OS of the machine on which this is running. Per default this + will be taken from sys.platform. + + startup_options : string + Command line options to pass to Octave. Optional; sensible defaults + are used if this is not provided. + """ + if startup_options is None: + startup_options = '--silent --no-gui' + super(Octave, self).__init__(executable, socket_addr, id, log, maxtime, + platform, startup_options) + + def _preamble_code(self): + code = super(Octave, self)._preamble_code() + if self.log: + code.append("diary('./pymatbridge/logs/octavelog_%s.txt')" % self.id) + return code + + def _execute_flag(self): + return '--eval' From f94bb75f13c0c3daeee326e1d551ecdc22e0b362 Mon Sep 17 00:00:00 2001 From: Ismail Badawi Date: Sun, 3 Aug 2014 16:14:36 -0400 Subject: [PATCH 4/8] s/Session/_Session/, and restore its docstring --- pymatbridge/pymatbridge.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/pymatbridge/pymatbridge.py b/pymatbridge/pymatbridge.py index 3225490..35bbfc2 100644 --- a/pymatbridge/pymatbridge.py +++ b/pymatbridge/pymatbridge.py @@ -32,7 +32,7 @@ def as_complex(dct): MATLAB_FOLDER = '%s/matlab' % os.path.realpath(os.path.dirname(__file__)) -class Session(object): +class _Session(object): """ A class for communicating with a MATLAB session. It provides the behavior common across different MATLAB implementations. You shouldn't instantiate @@ -42,7 +42,37 @@ class Session(object): def __init__(self, executable, socket_addr=None, id='python-matlab-bridge', log=False, maxtime=60, platform=None, startup_options=None): + """ + Initialize this thing. + + Parameters + ---------- + + executable : str + A string that would start the session at the terminal. + + socket_addr : str + A string that represents a valid ZMQ socket address, such as + "ipc:///tmp/pymatbridge", "tcp://127.0.0.1:55555", etc. + + id : str + An identifier for this instance of the pymatbridge. + log : bool + Whether to save a log file in some known location. + + maxtime : float + The maximal time to wait for a response from the session (optional, + Default is 10 sec) + + platform : string + The OS of the machine on which this is running. Per default this + will be taken from sys.platform. + + startup_options : string + Command line options to include in the executable's invocation. + Optional; sensible defaults are used if this is not provided. + """ self.started = False self.running = False self.executable = executable @@ -159,7 +189,7 @@ def get_variable(self, varname): return self._json_response(cmd='get_var', varname=varname)['var'] -class Matlab(Session): +class Matlab(_Session): def __init__(self, executable='matlab', socket_addr=None, id='python-matlab-bridge', log=False, maxtime=60, platform=None, startup_options=None): @@ -211,7 +241,7 @@ def _execute_flag(self): return '-r' -class Octave(Session): +class Octave(_Session): def __init__(self, executable='octave', socket_addr=None, id='python-matlab-bridge', log=False, maxtime=60, platform=None, startup_options=None): @@ -236,7 +266,7 @@ def __init__(self, executable='octave', socket_addr=None, Whether to save a log file in some known location. maxtime : float - The maximal time to wait for a response from matlab (optional, + The maximal time to wait for a response from octave (optional, Default is 10 sec) platform : string From d3e706be3328e4f565b309e6974065c9c27ffd5e Mon Sep 17 00:00:00 2001 From: Ismail Badawi Date: Sun, 3 Aug 2014 16:24:00 -0400 Subject: [PATCH 5/8] Use appropriate program name in output. --- pymatbridge/pymatbridge.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/pymatbridge/pymatbridge.py b/pymatbridge/pymatbridge.py index 35bbfc2..8cd7fb4 100644 --- a/pymatbridge/pymatbridge.py +++ b/pymatbridge/pymatbridge.py @@ -92,6 +92,9 @@ def __init__(self, executable, socket_addr=None, self.context = None self.socket = None + def _program_name(self): + raise NotImplemented + def _preamble_code(self): return ["addpath(genpath('%s'))" % MATLAB_FOLDER] @@ -111,7 +114,7 @@ def _run_server(self): # Start server/client session and make the connection def start(self): # Start the MATLAB server in a new process - print "Starting MATLAB on ZMQ socket %s" % (self.socket_addr) + print "Starting %s on ZMQ socket %s" % (self._program_name(), self.socket_addr) print "Send 'exit' command to kill the server" self._run_server() @@ -124,10 +127,10 @@ def start(self): # Test if connection is established if self.is_connected(): - print "MATLAB started and connected!" + print "%s started and connected!" % self._program_name() return True else: - print "MATLAB failed to start" + print "%s failed to start" % self._program_name() return False def _response(self, **kwargs): @@ -140,7 +143,7 @@ def _response(self, **kwargs): def stop(self): # Matlab should respond with "exit" if successful if self._response(cmd='exit') == "exit": - print "MATLAB closed" + print "%s closed" % self._program_name() self.started = False return True @@ -163,11 +166,12 @@ def is_connected(self): sys.stdout.write('.') time.sleep(1) if time.time() - start_time > self.maxtime: - print "Matlab session timed out after %d seconds" % self.maxtime + print "%s session timed out after %d seconds" % (self._program_name(), self.maxtime) return False def is_function_processor_working(self): - result = self.run_func('%s/test_functions/test_sum.m' % MATLAB_FOLDER, {'echo': 'Matlab: Function processor is working!'}) + result = self.run_func('%s/test_functions/test_sum.m' % MATLAB_FOLDER, + {'echo': '%s: Function processor is working!' % self._program_name()}) return result['success'] == 'true' def _json_response(self, **kwargs): @@ -237,6 +241,9 @@ def __init__(self, executable='matlab', socket_addr=None, super(Matlab, self).__init__(executable, socket_addr, id, log, maxtime, platform, startup_options) + def _program_name(self): + return 'MATLAB' + def _execute_flag(self): return '-r' @@ -282,6 +289,9 @@ def __init__(self, executable='octave', socket_addr=None, super(Octave, self).__init__(executable, socket_addr, id, log, maxtime, platform, startup_options) + def _program_name(self): + return 'Octave' + def _preamble_code(self): code = super(Octave, self)._preamble_code() if self.log: From 8705694f6e117b70b3c04166fb83c2d25928e9c6 Mon Sep 17 00:00:00 2001 From: Ismail Badawi Date: Sun, 3 Aug 2014 16:52:02 -0400 Subject: [PATCH 6/8] Make test suite runnable with Octave. --- pymatbridge/tests/test_run_code.py | 31 +++++++++++++++++++++--------- pymatbridge/tests/test_utils.py | 7 ++++++- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/pymatbridge/tests/test_run_code.py b/pymatbridge/tests/test_run_code.py index 45b2ae8..2693ce1 100644 --- a/pymatbridge/tests/test_run_code.py +++ b/pymatbridge/tests/test_run_code.py @@ -23,7 +23,10 @@ def test_disp(self): npt.assert_equal(result1, "Hello world\n") npt.assert_equal(result2, " \n") - npt.assert_equal(result3, "") + if tu.on_octave(): + npt.assert_equal(result3, '\n') + else: + npt.assert_equal(result3, "") # Make some assignments and run basic operations def test_basic_operation(self): @@ -34,13 +37,20 @@ def test_basic_operation(self): result_product = self.mlab.run_code("a * b")['content']['stdout'] result_division = self.mlab.run_code("c = a / b")['content']['stdout'] - - npt.assert_equal(result_assignment_a, unicode("\na =\n\n 21.2345\n\n")) - npt.assert_equal(result_assignment_b, unicode("\nb =\n\n 347.7450\n\n")) - npt.assert_equal(result_sum, unicode("\nans =\n\n 368.9795\n\n")) - npt.assert_equal(result_diff, unicode("\nans =\n\n -326.5105\n\n")) - npt.assert_equal(result_product, unicode("\nans =\n\n 7.3842e+03\n\n")) - npt.assert_equal(result_division, unicode("\nc =\n\n 0.0611\n\n")) + if tu.on_octave(): + npt.assert_equal(result_assignment_a, unicode("a = 21.235\n")) + npt.assert_equal(result_assignment_b, unicode("b = 347.75\n")) + npt.assert_equal(result_sum, unicode("ans = 368.98\n")) + npt.assert_equal(result_diff, unicode("ans = -326.51\n")) + npt.assert_equal(result_product, unicode("ans = 7384.2\n")) + npt.assert_equal(result_division, unicode("c = 0.061063\n")) + else: + npt.assert_equal(result_assignment_a, unicode("\na =\n\n 21.2345\n\n")) + npt.assert_equal(result_assignment_b, unicode("\nb =\n\n 347.7450\n\n")) + npt.assert_equal(result_sum, unicode("\nans =\n\n 368.9795\n\n")) + npt.assert_equal(result_diff, unicode("\nans =\n\n -326.5105\n\n")) + npt.assert_equal(result_product, unicode("\nans =\n\n 7.3842e+03\n\n")) + npt.assert_equal(result_division, unicode("\nc =\n\n 0.0611\n\n")) # Put in some undefined code def test_undefined_code(self): @@ -48,4 +58,7 @@ def test_undefined_code(self): message = self.mlab.run_code("this_is_nonsense")['content']['stdout'] npt.assert_equal(success, "false") - npt.assert_equal(message, "Undefined function or variable 'this_is_nonsense'.") + if tu.on_octave(): + npt.assert_equal(message, "'this_is_nonsense' undefined near line 1 column 1") + else: + npt.assert_equal(message, "Undefined function or variable 'this_is_nonsense'.") diff --git a/pymatbridge/tests/test_utils.py b/pymatbridge/tests/test_utils.py index 0d8131e..ade76f3 100644 --- a/pymatbridge/tests/test_utils.py +++ b/pymatbridge/tests/test_utils.py @@ -1,8 +1,13 @@ +import os + import pymatbridge as pymat import numpy.testing as npt +def on_octave(): + return bool(os.environ.get('USE_OCTAVE', False)) + def connect_to_matlab(): - mlab = pymat.Matlab() + mlab = pymat.Octave() if on_octave() else pymat.Matlab() mlab.start() npt.assert_(mlab.is_connected(), msg = "connect_to_matlab(): Connection failed") From d36718853ee8816527712568a49bd2aa293fa3db Mon Sep 17 00:00:00 2001 From: Ismail Badawi Date: Sun, 3 Aug 2014 16:53:31 -0400 Subject: [PATCH 7/8] Fix messenger compile errors on gcc / clang. --- messenger/src/messenger.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/messenger/src/messenger.c b/messenger/src/messenger.c index beb9a03..f14cb3c 100644 --- a/messenger/src/messenger.c +++ b/messenger/src/messenger.c @@ -90,10 +90,10 @@ void mexFunction(int nlhs, mxArray *plhs[], if (!initialized) { if (!initialize(socket_addr)) { - p[0] = true; + p[0] = 1; mexPrintf("Socket created at: %s\n", socket_addr); } else { - p[0] = false; + p[0] = 0; mexErrMsgTxt("Socket creation failed."); } } else { @@ -137,9 +137,9 @@ void mexFunction(int nlhs, mxArray *plhs[], p = mxGetLogicals(plhs[0]); if (msglen == respond(msg_out, msglen)) { - p[0] = true; + p[0] = 1; } else { - p[0] = false; + p[0] = 0; mexErrMsgTxt("Failed to send message due to ZMQ error"); } From 5115a0502d78b35bdd9b888143ece367a305785f Mon Sep 17 00:00:00 2001 From: Ismail Badawi Date: Tue, 5 Aug 2014 13:28:24 -0400 Subject: [PATCH 8/8] Mention Octave support in README. --- README.md | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ae6a54c..75728d1 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ This should make the python-matlab-bridge import-able. ## Usage -To use the pymatbridge you need to connect your python interperter to a Matlab +To use the pymatbridge you need to connect your python interpreter to a Matlab session. This is done in the following manner: from pymatbridge import Matlab @@ -72,12 +72,12 @@ This creates a matlab session class instance, into which you will be able to inject code and variables, and query for results. By default, when you use `start`, this will open whatever gets called when you type `matlab` in your Terminal, but you can also specify the location of your Matlab -application when initialzing your matlab session class: +application when initializing your matlab session class: - mlab = Matlab(matlab='/Applications/MATLAB_R2011a.app/bin/matlab') + mlab = Matlab(executable='/Applications/MATLAB_R2011a.app/bin/matlab') You can then start the Matlab server, which will kick off your matlab session, -and create the connection between your Python interperter and this session: +and create the connection between your Python interpreter and this session: mlab.start() @@ -99,7 +99,7 @@ the `get_variable` method: mlab.get_variable('a') -You can run any MATLAB functions contained within a .m file of the +You can run any MATLAB functions contained within a .m file of the same name. For example, to call the function jk in jk.m: %% MATLAB @@ -123,6 +123,37 @@ You can shut down the MATLAB server by calling: Tip: you can execute MATLAB code at the beginning of each of your matlab sessions by adding code to the `~/startup.m` file. +### Octave support & caveats + +A `pymatbridge.Octave` class is provided with exactly the same interface +as `pymatbridge.Matlab`: + + from pymatbridge import Octave + octave = Octave() + +Rather than looking for `matlab` at the shell, this will look for `octave`. +As with `pymatbridge.Matlab`, you can override this by specifying the +`executable` keyword argument. + +There are a few caveats to note about Octave support: + +* `pymatbridge.Matlab` invokes MATLAB with command line options that suppress +the display of figures -- these are instead saved to image files, accessible +via `results['content']['figures']` in the results dict. No such mechanism +seems to be available for Octave, so when drawing figures you'll likely see +them briefly pop up and disappear. (If you know of a way around this, feel +free to send a pull request). +* The zmq messenger used to communicate with the Octave session is written in C +and needs to be compiled. For MATLAB various prebuilt binaries are provided +and added to MATLAB's runtime path, but as of this writing the same isn't +true for Octave. You'll need to compile the messenger (in +`messenger/src/messenger.c`) using an incantation like +`mkoctfile --mex -lzmq messenger.c` and place the resulting `messenger.mex` +file somewhere in Octave's runtime path. + +Finally, rather than `~/startup.m`, Octave looks for an `~/.octaverc` file for +commands to execute before every session. (This is a good place to manipulate +the runtime path, for example). ### Matlab magic: