PageRenderTime 69ms CodeModel.GetById 33ms RepoModel.GetById 1ms app.codeStats 0ms

/mlabwrap_dev/mlabwrap_core.py

https://bitbucket.org/joshayers/mlabwrap
Python | 567 lines | 465 code | 27 blank | 75 comment | 23 complexity | a9e360665fb50fa9fb6cc56d8469cb34 MD5 | raw file
  1. ##############################################################################
  2. ################## mlabwrap: transparently wraps matlab(tm) ##################
  3. ##############################################################################
  4. ##
  5. ## o author: Alexander Schmolck <a.schmolck@gmx.net>
  6. ## o created: 2002-05-29 21:51:59+00:40
  7. ## o version: see `__version__`
  8. ## o keywords: matlab wrapper
  9. ## o license: MIT
  10. ## o FIXME:
  11. ## - it seems proxies can somehow still 'disappear', maybe in connection
  12. ## with exceptions in the matlab workspace?
  13. ## - add test that defunct proxy-values are culled from matlab workspace
  14. ## (for some reason ipython seems to keep them alive somehwere, even after
  15. ## a zaphist, should find out what causes that!)
  16. ## - add tests for exception handling!
  17. ## - the proxy getitem/setitem only works quite properly for 1D arrays
  18. ## (matlab's moronic syntax also means that 'foo(bar)(watz)' is not the
  19. ## same as 'tmp = foo(bar); tmp(watz)' -- indeed chances are the former
  20. ## will fail (not, apparently however with 'foo{bar}{watz}' (blech)). This
  21. ## would make it quite hard to the proxy thing 'properly' for nested
  22. ## proxies, so things may break down for complicated cases but can be
  23. ## easily fixed manually e.g.: ``mlab._set('tmp', foo(bar));
  24. ## mlab._get('tmp',remove=True)[watz]``
  25. ## - Guess there should be some in principle avoidable problems with
  26. ## assignments to sub-proxies (in addition to the more fundamental,
  27. ## unavoidable problem that ``proxy[index].part = foo`` can't work as
  28. ## expected if ``proxy[index]`` is a marshallable value that doesn't need
  29. ## to be proxied itself; see below for workaround).
  30. ## o XXX:
  31. ## - better support of string 'arrays'
  32. ## - multi-dimensional arrays are unsupported
  33. ## - treatment of lists, tuples and arrays with non-numerical values (these
  34. ## should presumably be wrapped into wrapper classes MlabCell etc.)
  35. ## - should test classes and further improve struct support?
  36. ## - should we transform 1D vectors into row vectors when handing them to
  37. ## matlab?
  38. ## - what should be flattend? Should there be a scalarization opition?
  39. ## - ``autosync_dirs`` is a bit of a hack (and maybe``handle_out``, too)...
  40. ## - is ``global mlab`` in unpickling of proxies OK?
  41. ## - hasattr fun for proxies (__deepcopy__ etc.)
  42. ## - check pickling
  43. ## o TODO:
  44. ## - delattr
  45. ## - better error reporting: test for number of input args etc.
  46. ## - add cloning of proxies.
  47. ## - pickling for nested proxies (and session management for pickling)
  48. ## - more tests
  49. ## o !!!:
  50. ## - matlab complex arrays are intelligently of type 'double'
  51. ## - ``class('func')`` but ``class(var)``
  52. __version__ = '1.1'
  53. __author__ = "Alexander Schmolck <a.schmolck@gmx.net>"
  54. __all__ = ['matlab', 'MlabError']
  55. import warnings
  56. import os, sys, re
  57. import weakref
  58. import logging
  59. import numpy as np
  60. from mlabwrap_dev import mlabraw
  61. MODULE_LOGGER = logging.getLogger(__name__)
  62. MODULE_LOGGER.addHandler(logging.NullHandler())
  63. #XXX: nested access
  64. def _flush_write_stdout(s):
  65. """Writes `s` to stdout and flushes. Default value for ``handle_out``."""
  66. sys.stdout.write(s); sys.stdout.flush()
  67. # XXX I changed this to no longer use weakrefs because it didn't seem 100%
  68. # reliable on second thought; need to check if we need to do something to
  69. # speed up proxy reclamation on the matlab side.
  70. class CurlyIndexer(object):
  71. """A helper class to mimick ``foo{bar}``-style indexing in python."""
  72. def __init__(self, proxy):
  73. self.proxy = proxy
  74. def __getitem__(self, index):
  75. return self.proxy.__getitem__(index, '{}')
  76. def __setitem__(self, index, value):
  77. self.proxy.__setitem__(index, value, '{}')
  78. class MlabObjectProxy(object):
  79. """A proxy class for matlab objects that can't be converted to python
  80. types.
  81. WARNING: There are impedance-mismatch issues between python and matlab
  82. that make designing such a class difficult (e.g. dimensionality, indexing
  83. and ``length`` work fundamentally different in matlab than in python), so
  84. although this class currently tries to transparently support some stuff
  85. (notably (1D) indexing, slicing and attribute access), other operations
  86. (e.g. math operators and in particular __len__ and __iter__) are not yet
  87. supported. Don't depend on the indexing semantics not to change.
  88. Note:
  89. Assigning to parts of proxy objects (e.g. ``proxy[index].part =
  90. [[1,2,3]]``) should *largely* work as expected, the only exception
  91. would be if ``proxy.foo[index] = 3`` where ``proxy.foo[index]`` is some
  92. type that can be converted to python (i.e. an array or string, (or
  93. cell, if cell conversion has been enabled)), because then ``proxy.foo``
  94. returns a new python object. For these cases it's necessary to do::
  95. some_array[index] = 3; proxy.foo = some_array
  96. """
  97. def __init__(self, mlabwrap, name, parent=None):
  98. self.__dict__['_mlabwrap'] = mlabwrap
  99. self.__dict__['_name'] = name
  100. """The name is the name of the proxies representation in matlab."""
  101. self.__dict__['_parent'] = parent
  102. """To fake matlab's ``obj{foo}`` style indexing."""
  103. def __repr__(self):
  104. output = []
  105. mlab._do('disp(%s)' % self._name, nout=0, handle_out=output.append)
  106. rep = "".join(output)
  107. klass = self._mlabwrap._do("class(%s)" % self._name)
  108. ## #XXX what about classes?
  109. ## if klass == "struct":
  110. ## rep = "\n" + self._mlabwrap._format_struct(self._name)
  111. ## else:
  112. ## rep = ""
  113. return "<%s of matlab-class: %r; internal name: %r; has parent: %s>\n%s" % (
  114. type(self).__name__, klass,
  115. self._name, ['yes', 'no'][self._parent is None],
  116. rep)
  117. def __del__(self):
  118. if self._parent is None:
  119. mlabraw.eval(self._mlabwrap._session, 'clear %s;' % self._name)
  120. def _get_part(self, to_get):
  121. if self._mlabwrap._var_type(to_get) in self._mlabwrap._mlabraw_can_convert:
  122. #!!! need assignment to TMP_VAL__ because `mlabraw.get` only works
  123. # with 'atomic' values like ``foo`` and not e.g. ``foo.bar``.
  124. mlabraw.eval(self._mlabwrap._session, "TMP_VAL__=%s" % to_get)
  125. return self._mlabwrap._get('TMP_VAL__', remove=True)
  126. return type(self)(self._mlabwrap, to_get, self)
  127. def _set_part(self, to_set, value):
  128. #FIXME s.a.
  129. if isinstance(value, MlabObjectProxy):
  130. mlabraw.eval(self._mlabwrap._session, "%s = %s;" % (to_set, value._name))
  131. else:
  132. self._mlabwrap._set("TMP_VAL__", value)
  133. mlabraw.eval(self._mlabwrap._session, "%s = TMP_VAL__;" % to_set)
  134. mlabraw.eval(self._mlabwrap._session, 'clear TMP_VAL__;')
  135. def __getattr__(self, attr):
  136. if attr == "_":
  137. return self.__dict__.setdefault('_', CurlyIndexer(self))
  138. else:
  139. return self._get_part("%s.%s" % (self._name, attr))
  140. def __setattr__(self, attr, value):
  141. self._set_part("%s.%s" % (self._name, attr), value)
  142. # FIXME still have to think properly about how to best translate Matlab semantics here...
  143. def __nonzero__(self):
  144. raise TypeError("%s does not yet implement truth testing" % type(self).__name__)
  145. def __len__(self):
  146. raise TypeError("%s does not yet implement __len__" % type(self).__name__)
  147. def __iter__(self):
  148. raise TypeError("%s does not yet implement iteration" % type(self).__name__)
  149. def _matlab_str_repr(s):
  150. if '\n' not in s:
  151. return "'%s'" % s.replace("'","''")
  152. else:
  153. # Matlab's string literals suck. They can't represent all
  154. # strings, so we need to use sprintf
  155. return "sprintf('%s')" % escape(s).replace("'","''").replace("%", "%%")
  156. _matlab_str_repr = staticmethod(_matlab_str_repr)
  157. #FIXME: those two only work ok for 1D indexing
  158. def _convert_index(self, index):
  159. if isinstance(index, int):
  160. return str(index + 1) # -> matlab 1-based indexing
  161. elif isinstance(index, basestring):
  162. return self._matlab_str_repr(index)
  163. elif isinstance(index, slice):
  164. if index == slice(None,None,None):
  165. return ":"
  166. elif index.step not in (None,1):
  167. raise ValueError("Illegal index for a proxy %r" % index)
  168. else:
  169. start = (index.start or 0) + 1
  170. if start == 0: start_s = 'end'
  171. elif start < 0: start_s = 'end%d' % start
  172. else: start_s = '%d' % start
  173. if index.stop is None: stop_s = 'end'
  174. elif index.stop < 0: stop_s = 'end%d' % index.stop
  175. else: stop_s = '%d' % index.stop
  176. return '%s:%s' % (start_s, stop_s)
  177. else:
  178. raise TypeError("Unsupported index type: %r." % type(index))
  179. def __getitem__(self, index, parens='()'):
  180. """WARNING: Semi-finished, semantics might change because it's not yet
  181. clear how to best bridge the matlab/python impedence match.
  182. HACK: Matlab decadently allows overloading *2* different indexing parens,
  183. ``()`` and ``{}``, hence the ``parens`` option."""
  184. index = self._convert_index(index)
  185. return self._get_part("".join([self._name,parens[0],index,parens[1]]))
  186. def __setitem__(self, index, value, parens='()'):
  187. """WARNING: see ``__getitem__``."""
  188. index = self._convert_index(index)
  189. return self._set_part("".join([self._name,parens[0],index,parens[1]]),
  190. value)
  191. class MlabConversionError(Exception):
  192. """Raised when a Matlab type can't be converted to a python primitive."""
  193. pass
  194. class MlabWrap(object):
  195. """An instance of this class manages a Matlab session.
  196. All attribute lookups on the class instance are translated into Matlab
  197. commands. Python and Matlab objects are automatically converted as
  198. necessary. The details of this handling can be controlled with a number of
  199. instance attributes, which are documented below.
  200. By creating multiple instances of this class, it is possible to have several
  201. independent Matlab sessions open simultaneously.
  202. """
  203. def __init__(self):
  204. """Create a new matlab(tm) wrapper object.
  205. """
  206. self._array_cast = None
  207. """specifies a cast for arrays. If the result of an
  208. operation is a numpy array, ``return_type(res)`` will be returned
  209. instead."""
  210. self._autosync_dirs=True
  211. """`autosync_dirs` specifies whether the working directory of the
  212. matlab session should be kept in sync with that of python."""
  213. self._flatten_row_vecs = False
  214. """Automatically return 1xn matrices as flat numeric arrays."""
  215. self._flatten_col_vecs = False
  216. """Automatically return nx1 matrices as flat numeric arrays."""
  217. self._clear_call_args = True
  218. """Remove the function args from matlab workspace after each function
  219. call. Otherwise they are left to be (partly) overwritten by the next
  220. function call. This saves a function call in matlab but means that the
  221. memory used up by the arguments will remain unreclaimed till
  222. overwritten."""
  223. self._session = mlabraw.open(os.getenv("MLABRAW_CMD_STR", ""))
  224. # atexit.register(lambda handle=self._session: mlabraw.close(handle))
  225. self._proxies = weakref.WeakValueDictionary()
  226. """Use ``mlab._proxies.values()`` for a list of matlab object's that
  227. are currently proxied."""
  228. self._proxy_count = 0
  229. self._mlabraw_can_convert = ('double', 'char')
  230. """The matlab(tm) types that mlabraw will automatically convert for us."""
  231. self._dont_proxy = {'cell' : False}
  232. """The matlab(tm) types we can handle ourselves with a bit of
  233. effort. To turn on autoconversion for e.g. cell arrays do:
  234. ``mlab._dont_proxy["cell"] = True``."""
  235. self._logger = logging.getLogger(__name__ + '.MlabWrap')
  236. def sync_dirs(self):
  237. """Synchronize the Matlab current directory with the Python
  238. current directory.
  239. """
  240. # can't use self.cd, because calling it from self._do in order to
  241. # autosync directories will cause infinite recursion
  242. command = "cd('{0}')"
  243. self._mlabraw_eval(command.format(os.getcwd()))
  244. def close(self):
  245. """Close Matlab.
  246. """
  247. try:
  248. mlabraw.close(self._session)
  249. except AttributeError:
  250. pass
  251. def __del__(self):
  252. self.close()
  253. def __enter__(self):
  254. return self
  255. def __exit__(self, *exc_info):
  256. self.close()
  257. return False
  258. def _format_struct(self, varname):
  259. res = []
  260. fieldnames = self._do("fieldnames(%s)" % varname)
  261. size = np.ravel(self._do("size(%s)" % varname))
  262. return "%dx%d struct array with fields:\n%s" % (
  263. size[0], size[1], "\n ".join([""] + fieldnames))
  264. ## fieldnames
  265. ## fieldvalues = self._do(",".join(["%s.%s" % (varname, fn)
  266. ## for fn in fieldnames]), nout=len(fieldnames))
  267. ## maxlen = max(map(len, fieldnames))
  268. ## return "\n".join(["%*s: %s" % (maxlen, (`fv`,`fv`[:20] + '...')[len(`fv`) > 23])
  269. ## for fv in fieldvalues])
  270. def _var_type(self, varname):
  271. mlabraw.eval(self._session,
  272. "TMP_CLS__ = class(%(x)s); if issparse(%(x)s),"
  273. "TMP_CLS__ = [TMP_CLS__,'-sparse']; end;" % dict(x=varname))
  274. res_type = mlabraw.get(self._session, "TMP_CLS__")
  275. mlabraw.eval(self._session, "clear TMP_CLS__;") # unlikely to need try/finally to ensure clear
  276. return res_type
  277. def _make_proxy(self, varname, parent=None, constructor=MlabObjectProxy):
  278. """Creates a proxy for a variable.
  279. XXX create and cache nested proxies also here.
  280. """
  281. # FIXME why not just use gensym here?
  282. proxy_val_name = "PROXY_VAL%d__" % self._proxy_count
  283. self._proxy_count += 1
  284. mlabraw.eval(self._session, "%s = %s;" % (proxy_val_name, varname))
  285. res = constructor(self, proxy_val_name, parent)
  286. self._proxies[proxy_val_name] = res
  287. return res
  288. def _get_cell(self, varname):
  289. # XXX can currently only handle ``{}`` and 1D cells
  290. mlabraw.eval(self._session,
  291. "TMP_SIZE_INFO__ = \
  292. [all(size(%(vn)s) == 0), \
  293. min(size(%(vn)s)) == 1 & ndims(%(vn)s) == 2, \
  294. max(size(%(vn)s))];" % {'vn':varname})
  295. is_empty, is_rank1, cell_len = map(int,
  296. self._get("TMP_SIZE_INFO__", remove=True).flat)
  297. if is_empty:
  298. return []
  299. elif is_rank1:
  300. cell_bits = (["TMP%i%s__" % (i, gensym('_'))
  301. for i in range(cell_len)])
  302. mlabraw.eval(self._session, '[%s] = deal(%s{:});' %
  303. (",".join(cell_bits), varname))
  304. # !!! this recursive call means we have to take care with
  305. # overwriting temps!!!
  306. return self._get_values(cell_bits)
  307. else:
  308. raise MlabConversionError("Not a 1D cell array")
  309. def _manually_convert(self, varname, vartype):
  310. if vartype == 'cell':
  311. return self._get_cell(varname)
  312. def _get_values(self, varnames):
  313. """Retrieve the variable names from Matlab and then clear them.
  314. """
  315. if not varnames:
  316. raise ValueError("No varnames") #to prevent clear('')
  317. result = []
  318. for varname in varnames:
  319. result.append(self._get(varname))
  320. #FIXME wrap try/finally?
  321. command = "clear('{0}');".format("','".join(varnames))
  322. self._mlabraw_eval(command)
  323. return result
  324. def _do(self, command, *args, **kwargs):
  325. """Semi-raw execution of a matlab command.
  326. Smartly handle calls to matlab, figure out what to do with `args`,
  327. and when to use function call syntax and not.
  328. If no `args` are specified, the ``cmd`` not ``result = cmd()`` form is
  329. used in Matlab -- this also makes literal Matlab commands legal
  330. (eg. cmd=``get(gca, 'Children')``).
  331. If ``nout=0`` is specified, the Matlab command is executed as
  332. procedure, otherwise it is executed as function (default), nout
  333. specifying how many values should be returned (default 1).
  334. **Beware that if you use don't specify ``nout=0`` for a `cmd` that
  335. never returns a value will raise an error** (because assigning a
  336. variable to a call that doesn't return a value is illegal in matlab).
  337. ``cast`` specifies which typecast should be applied to the result
  338. (e.g. `int`), it defaults to none.
  339. XXX: should we add ``parens`` parameter?
  340. """
  341. handle_out = kwargs.get('handle_out', _flush_write_stdout)
  342. if self._autosync_dirs:
  343. self.sync_dirs()
  344. nout = kwargs.get('nout', 1)
  345. #XXX what to do with matlab screen output
  346. arg_names = []
  347. try:
  348. for count, arg in enumerate(args):
  349. if isinstance(arg, MlabObjectProxy):
  350. arg_names.append(arg._name)
  351. else:
  352. next_name = 'ARGUMENT{0}__'.format(count)
  353. arg_names.append(next_name)
  354. self._mlabraw_put(arg_names[-1], arg)
  355. if args:
  356. command = "{0}({1})".format(command, ','.join(arg_names))
  357. # three cases for nout: 0, 1, more than 1
  358. # 0 -> None, 1 -> val, >1 -> [val1, val2, ...]
  359. if nout == 0:
  360. handle_out(self._mlabraw_eval(command + ';'))
  361. return
  362. # deal with matlab-style multiple value return
  363. result_vars = ['RESULT{0}__'.format(i) for i in xrange(nout)]
  364. command = '[{0}] = {1};'.format(','.join(result_vars), command)
  365. handle_out(self._mlabraw_eval(command))
  366. result = self._get_values(result_vars)
  367. if nout == 1:
  368. result = result[0]
  369. else:
  370. result = tuple(result)
  371. if kwargs.has_key('cast'):
  372. if nout == 0:
  373. raise TypeError("Can't cast: 0 nout")
  374. return kwargs['cast'](result)
  375. else:
  376. return result
  377. finally:
  378. if len(arg_names) and self._clear_call_args:
  379. command = "clear('{0}');".format("','".join(arg_names))
  380. self._mlabraw_eval(command)
  381. def _mlabraw_eval(self, command):
  382. """Log the Matlab command and then pass it to mlabraw.eval.
  383. """
  384. self._logger.info('Sending command: ' + command)
  385. return mlabraw.eval(self._session, command)
  386. def _mlabraw_put(self, var_name, value):
  387. """Log the Matlab variable transmittal and then pass it to
  388. mlabraw.put.
  389. """
  390. self._logger.info('Putting variable: ' + var_name)
  391. return mlabraw.put(self._session, var_name, value)
  392. def _mlabraw_get(self, var_name):
  393. """Log the Matlab variable retrieval and then pass it to
  394. mlabraw.get.
  395. """
  396. self._logger.info('Getting variable: ' + var_name)
  397. return mlabraw.get(self._session, var_name)
  398. # this is really raw, no conversion of [[]] -> [], whatever
  399. def _get(self, name, remove=False):
  400. """Directly access a variable in matlab space.
  401. This should normally not be used by user code."""
  402. # FIXME should this really be needed in normal operation?
  403. if name in self._proxies:
  404. return self._proxies[name]
  405. varname = name
  406. vartype = self._var_type(varname)
  407. if vartype in self._mlabraw_can_convert:
  408. var = self._mlabraw_get(varname)
  409. if isinstance(var, np.ndarray):
  410. if self._flatten_row_vecs and np.shape(var)[0] == 1:
  411. var.shape = var.shape[1:2]
  412. elif self._flatten_col_vecs and np.shape(var)[1] == 1:
  413. var.shape = var.shape[0:1]
  414. if self._array_cast:
  415. var = self._array_cast(var)
  416. else:
  417. var = None
  418. if self._dont_proxy.get(vartype):
  419. # manual conversions may fail (e.g. for multidimensional
  420. # cell arrays), in that case just fall back on proxying.
  421. try:
  422. var = self._manually_convert(varname, vartype)
  423. except MlabConversionError: pass
  424. if var is None:
  425. # we can't convert this to a python object, so we just
  426. # create a proxy, and don't delete the real matlab
  427. # reference until the proxy is garbage collected
  428. var = self._make_proxy(varname)
  429. if remove:
  430. command = "clear('{0}');".format(varname)
  431. self._mlabraw_eval(command)
  432. return var
  433. def _set(self, var_name, value):
  434. """Directly set a variable `name` in matlab space to `value`.
  435. This should normally not be used in user code."""
  436. if isinstance(value, MlabObjectProxy):
  437. mlabraw.eval(self._session, "%s = %s;" % (var_name, value._name))
  438. else:
  439. self._mlabraw_put(var_name, value)
  440. def _make_mlab_command(self, name, nout, doc=None):
  441. def mlab_command(*args, **kwargs):
  442. if 'nout' not in kwargs:
  443. kwargs['nout'] = nout
  444. return self._do(name, *args, **kwargs)
  445. mlab_command.__doc__ = "\n" + doc
  446. return mlab_command
  447. # XXX this method needs some refactoring, but only after it is clear how
  448. # things should be done (e.g. what should be extracted from docstrings and
  449. # how)
  450. def __getattr__(self, attr):
  451. """Magically creates a wapper to a matlab function, procedure or
  452. object on-the-fly."""
  453. if re.search(r'\W', attr): # work around ipython <= 0.7.3 bug
  454. raise ValueError("Attributes don't look like this: %r" % attr)
  455. if attr.startswith('__'):
  456. raise AttributeError, attr
  457. assert not attr.startswith('_') # XXX
  458. # print_ -> print
  459. if attr[-1] == "_":
  460. name = attr[:-1]
  461. else:
  462. name = attr
  463. try:
  464. nout = int(self._do("nargout('{0}')".format(name), nout=1))
  465. except mlabraw.error, msg:
  466. # determine if "name" is a Matlab script and must be called
  467. # with nout=0
  468. msg = str(msg)
  469. if name in msg and 'is a script' in msg:
  470. nout = 0
  471. else:
  472. typ = np.ravel(self._do("exist('%s')" % name))[0]
  473. if typ == 0: # doesn't exist
  474. raise AttributeError("No such matlab object: %s" % name)
  475. else:
  476. msg = ("Couldn't determine number of output args "
  477. "for {0}, assuming 1.")
  478. warnings.warn(msg.format(name))
  479. nout = 1
  480. # nargout returns -1 for functions with a variable number of output
  481. # arguments, if nout not specified, assume 1
  482. if nout == -1:
  483. nout = 1
  484. doc = self._do("help('%s')" % name)
  485. mlab_command = self._make_mlab_command(name, nout, doc)
  486. #!!! attr, *not* name, because we might have python keyword name!
  487. setattr(self, attr, mlab_command)
  488. return mlab_command
  489. matlab = MlabWrap
  490. MlabError = mlabraw.error