# -*- coding: utf-8 -*-
# Copyright (c) oct2py developers.
# Distributed under the terms of the MIT License.
from __future__ import print_function, absolute_import, division
import logging
import os
import tempfile
import warnings
import numpy as np
from metakernel.pexpect import EOF, TIMEOUT
from octave_kernel.kernel import OctaveEngine, STDIN_PROMPT
from .io import read_file, write_file, Cell, StructArray
from .utils import Oct2PyError, get_log
from .compat import unicode, input, string_types
from .dynamic import (
_make_function_ptr_instance, _make_variable_ptr_instance,
_make_user_class, OctavePtr)
[docs]class Oct2Py(object):
"""Manages an Octave session.
Uses MAT files to pass data between Octave and Numpy.
The function must either exist as an m-file in this directory or
on Octave's path.
The first command will take about 0.5s for Octave to load up.
The subsequent commands will be much faster.
You may provide a logger object for logging events, or the oct2py.get_log()
default will be used. When calling commands, logger.info() will be used
to stream output, unless a `stream_handler` is provided.
Parameters
----------
executable : str, optional
Name of the Octave executable, can be a system path. If this is not
given, we look for an OCTAVE_EXECUTABLE environmental variable.
The fallback is to call "octave-cli" or "octave".
logger : logging object, optional
Optional logger to use for Oct2Py session
timeout : float, optional
Timeout in seconds for commands
oned_as : {'row', 'column'}, optional
If 'column', write 1-D numpy arrays as column vectors.
If 'row', write 1-D numpy arrays as row vectors.}
temp_dir : str, optional
If specified, the session's MAT files will be created in the
directory, otherwise a default directory is used. This can be
a shared memory (tmpfs) path.
convert_to_float : bool, optional
If true, convert integer types to float when passing to Octave.
"""
def __init__(self, executable=None, logger=None, timeout=None,
oned_as='row', temp_dir=None, convert_to_float=True):
"""Start Octave and set up the session.
"""
self._oned_as = oned_as
self._executable = executable
self._engine = None
self._logger = None
self.logger = logger
self.timeout = timeout
self.temp_dir = temp_dir or tempfile.mkdtemp()
self.convert_to_float = convert_to_float
self._user_classes = dict()
self._function_ptrs = dict()
self.restart()
@property
def logger(self):
"""The logging instance used by the session."""
return self._logger
@logger.setter
def logger(self, value):
self._logger = value or get_log()
if self._engine:
self._engine.logger = self._logger
def __enter__(self):
"""Return octave object, restart session if necessary"""
if not self._engine:
self.restart()
return self
def __exit__(self, type, value, traceback):
"""Close session"""
self.exit()
[docs] def exit(self):
"""Quits this octave session and cleans up.
"""
if self._engine:
self._engine.repl.terminate()
self._engine = None
[docs] def push(self, name, var, timeout=None, verbose=True):
"""
Put a variable or variables into the Octave session.
Parameters
----------
name : str or list
Name of the variable(s).
var : object or list
The value(s) to pass.
timeout : float
Time to wait for response from Octave (per line).
**kwargs: Deprecated kwargs, ignored.
Examples
--------
>>> from oct2py import octave
>>> y = [1, 2]
>>> octave.push('y', y)
>>> octave.pull('y')
array([[ 1., 2.]])
>>> octave.push(['x', 'y'], ['spam', [1, 2, 3, 4]])
>>> octave.pull(['x', 'y']) # doctest: +SKIP
[u'spam', array([[1, 2, 3, 4]])]
Notes
-----
Integer type arguments will be converted to floating point
unless `convert_to_float=False`.
"""
if isinstance(name, (str, unicode)):
name = [name]
var = [var]
for (n, v) in zip(name, var):
self.feval('assignin', 'base', n, v, nout=0, timeout=timeout,
verbose=verbose)
[docs] def pull(self, var, timeout=None, verbose=True):
"""
Retrieve a value or values from the Octave session.
Parameters
----------
var : str or list
Name of the variable(s) to retrieve.
timeout : float, optional.
Time to wait for response from Octave (per line).
**kwargs: Deprecated kwargs, ignored.
Returns
-------
out : object
Object returned by Octave.
Raises
------
Oct2PyError
If the variable does not exist in the Octave session.
Examples
--------
>>> from oct2py import octave
>>> y = [1, 2]
>>> octave.push('y', y)
>>> octave.pull('y')
array([[ 1., 2.]])
>>> octave.push(['x', 'y'], ['spam', [1, 2, 3, 4]])
>>> octave.pull(['x', 'y']) # doctest: +SKIP
[u'spam', array([[1, 2, 3, 4]])]
"""
if isinstance(var, (str, unicode)):
var = [var]
outputs = []
for name in var:
exist = self._exist(name)
if exist == 1:
outputs.append(self.feval('evalin', 'base', name,
timeout=timeout, verbose=verbose))
else:
outputs.append(self.get_pointer(name, timeout=timeout))
if len(outputs) == 1:
return outputs[0]
return outputs
[docs] def get_pointer(self, name, timeout=None):
"""Get a pointer to a named object in the Octave workspace.
Parameters
----------
name: str
The name of the object in the Octave workspace.
timemout: float, optional.
Time to wait for response from Octave (per line).
Examples
--------
>>> from oct2py import octave
>>> octave.eval('foo = [1, 2];')
>>> ptr = octave.get_pointer('foo')
>>> ptr.value
array([[ 1., 2.]])
>>> ptr.address
'foo'
>>> # Can be passed as an argument
>>> octave.disp(ptr) # doctest: +SKIP
1 2
>>> from oct2py import octave
>>> sin = octave.get_pointer('sin') # equivalent to `octave.sin`
>>> sin.address
'@sin'
>>> x = octave.quad(sin, 0, octave.pi())
>>> x
2.0
Notes
-----
Pointers can be passed to `feval` or dynamic functions as function arguments. A pointer passed as a nested value will be passed by value instead.
Raises
------
Oct2PyError
If the variable does not exist in the Octave session or is of
unknown type.
Returns
-------
A variable, object, user class, or function pointer as appropriate.
"""
exist = self._exist(name)
isobject = self._isobject(name, exist)
if exist == 0:
raise Oct2PyError('"%s" is undefined' % name)
elif exist == 1:
return _make_variable_ptr_instance(self, name)
elif isobject:
return self._get_user_class(name)
elif exist in [2, 3, 5]:
return self._get_function_ptr(name)
raise Oct2PyError('Unknown type for object "%s"' % name)
[docs] def feval(self, func_path, *func_args, **kwargs):
"""Run a function in Octave and return the result.
Parameters
----------
func_path: str
Name of function to run or a path to an m-file.
func_args: object, optional
Args to send to the function.
nout: int, optional
Desired number of return arguments, defaults to 1.
store_as: str, optional
If given, saves the result to the given Octave variable name
instead of returning it.
verbose : bool, optional
Log Octave output at INFO level. If False, log at DEBUG level.
stream_handler: callable, optional
A function that is called for each line of output from the
evaluation.
timeout: float, optional
The timeout in seconds for the call.
plot_dir: str, optional
If specificed, save the session's plot figures to the plot
directory instead of displaying the plot window.
plot_name : str, optional
Saved plots will start with `plot_name` and
end with "_%%.xxx' where %% is the plot number and
xxx is the `plot_format`.
plot_format: str, optional
The format in which to save the plot.
plot_width: int, optional
The plot with in pixels.
plot_height: int, optional
The plot height in pixels.
Notes
-----
The function arguments passed follow Octave calling convention, not
Python. That is, all values must be passed as a comma separated list,
not using `x=foo` assignment.
Examples
--------
>>> from oct2py import octave
>>> cell = octave.feval('cell', 10, 10, 10)
>>> cell.shape
(10, 10, 10)
>>> from oct2py import octave
>>> x = octave.feval('linspace', 0, octave.pi() / 2)
>>> x.shape
(1, 100)
>>> from oct2py import octave
>>> x = octave.feval('svd', octave.hilb(3))
>>> x
array([[ 1.40831893],
[ 0.12232707],
[ 0.00268734]])
>>> # specify three return values
>>> (u, v, d) = octave.feval('svd', octave.hilb(3), nout=3)
>>> u.shape
(3, 3)
Returns
-------
The Python value(s) returned by the Octave function call.
"""
if not self._engine:
raise Oct2PyError('Session is not open')
nout = kwargs.get('nout', None)
if nout is None:
nout = 1
plot_dir = kwargs.get('plot_dir')
settings = dict(backend='inline' if plot_dir else None,
format=kwargs.get('plot_format'),
name=kwargs.get('plot_name'),
width=kwargs.get('plot_width'),
height=kwargs.get('plot_height'),
resolution=kwargs.get('plot_res'))
self._engine.plot_settings = settings
dname = os.path.dirname(func_path)
fname = os.path.basename(func_path)
func_name, ext = os.path.splitext(fname)
if ext and not ext == '.m':
raise TypeError('Need to give path to .m file')
if func_name == 'clear':
raise Oct2PyError('Cannot use `clear` command directly, use' +
' eval("clear(var1, var2)")')
stream_handler = kwargs.get('stream_handler')
verbose = kwargs.get('verbose', True)
store_as = kwargs.get('store_as', '')
timeout = kwargs.get('timeout', self.timeout)
if not stream_handler:
stream_handler = self.logger.info if verbose else self.logger.debug
return self._feval(func_name, func_args, dname=dname, nout=nout,
timeout=timeout, stream_handler=stream_handler,
store_as=store_as, plot_dir=plot_dir)
[docs] def eval(self, cmds, verbose=True, timeout=None, stream_handler=None,
temp_dir=None, plot_dir=None, plot_name='plot', plot_format='svg',
plot_width=None, plot_height=None, plot_res=None,
nout=0, **kwargs):
"""
Evaluate an Octave command or commands.
Parameters
----------
cmds : str or list
Commands(s) to pass to Octave.
verbose : bool, optional
Log Octave output at INFO level. If False, log at DEBUG level.
stream_handler: callable, optional
A function that is called for each line of output from the
evaluation.
timeout : float, optional
Time to wait for response from Octave (per line). If not given,
the instance `timeout` is used.
nout : int, optional.
The desired number of returned values, defaults to 0. If nout
is 0, the `ans` will be returned as the return value.
temp_dir: str, optional
If specified, the session's MAT files will be created in the
directory, otherwise a the instance `temp_dir` is used.
a shared memory (tmpfs) path.
plot_dir: str, optional
If specificed, save the session's plot figures to the plot
directory instead of displaying the plot window.
plot_name : str, optional
Saved plots will start with `plot_name` and
end with "_%%.xxx' where %% is the plot number and
xxx is the `plot_format`.
plot_format: str, optional
The format in which to save the plot (PNG by default).
plot_width: int, optional
The plot with in pixels.
plot_height: int, optional
The plot height in pixels.
plot_res: int, optional
The plot resolution in pixels per inch.
**kwargs Deprectated kwargs.
Examples
--------
>>> from oct2py import octave
>>> octave.eval('disp("hello")') # doctest: +SKIP
hello
>>> x = octave.eval('round(quad(@sin, 0, pi/2));')
>>> x
1.0
>>> a = octave.eval('disp("hello");1;') # doctest: +SKIP
hello
>>> a = octave.eval('disp("hello");1;', verbose=False)
>>> a
1.0
>>> from oct2py import octave
>>> lines = []
>>> octave.eval('for i = 1:3; disp(i);end', \
stream_handler=lines.append)
>>> lines # doctest: +SKIP
[' 1', ' 2', ' 3']
Returns
-------
out : object
Octave "ans" variable, or None.
Notes
-----
The deprecated `log` kwarg will temporarily set the `logger` level to
`WARN`. Using the `logger` settings directly is preferred.
The deprecated `return_both` kwarg will still work, but the preferred
method is to use the `stream_handler`. If `stream_handler` is given,
the `return_both` kwarg will be honored but will give an empty string
as the reponse.
Raises
------
Oct2PyError
If the command(s) fail.
"""
if isinstance(cmds, (str, unicode)):
cmds = [cmds]
prev_temp_dir = self.temp_dir
self.temp_dir = temp_dir or self.temp_dir
prev_log_level = self.logger.level
if kwargs.get('log') is False:
self.logger.setLevel(logging.WARN)
for name in ['log', 'return_both']:
if name not in kwargs:
continue
msg = 'Using deprecated `%s` kwarg, see docs on `Oct2Py.eval()`'
warnings.warn(msg % name, stacklevel=2)
return_both = kwargs.pop('return_both', False)
lines = []
if return_both and not stream_handler:
stream_handler = lines.append
ans = None
for cmd in cmds:
resp = self.feval('evalin', 'base', cmd,
nout=nout, timeout=timeout,
stream_handler=stream_handler,
verbose=verbose, plot_dir=plot_dir,
plot_name=plot_name, plot_format=plot_format,
plot_width=plot_width, plot_height=plot_height,
plot_res=plot_res)
if resp is not None:
ans = resp
self.temp_dir = prev_temp_dir
self.logger.setLevel(prev_log_level)
if return_both:
return '\n'.join(lines), ans
return ans
[docs] def restart(self):
"""Restart an Octave session in a clean state
"""
if self._engine:
self._engine.repl.terminate()
executable = self._executable
if executable:
os.environ['OCTAVE_EXECUTABLE'] = executable
if 'OCTAVE_EXECUTABLE' not in os.environ and 'OCTAVE' in os.environ:
os.environ['OCTAVE_EXECUTABLE'] = os.environ['OCTAVE']
self._engine = OctaveEngine(stdin_handler=self._handle_stdin,
logger=self.logger)
# Add local Octave scripts.
here = os.path.realpath(os.path.dirname(__file__))
self._engine.eval('addpath("%s");' % here.replace(os.path.sep, '/'))
def _feval(self, func_name, func_args=(), dname='', nout=0,
timeout=None, stream_handler=None, store_as='', plot_dir=None):
"""Run the given function with the given args.
"""
engine = self._engine
if engine is None:
raise Oct2PyError('Session is closed')
# Set up our mat file paths.
out_file = os.path.join(self.temp_dir, 'writer.mat')
out_file = out_file.replace(os.path.sep, '/')
in_file = os.path.join(self.temp_dir, 'reader.mat')
in_file = in_file.replace(os.path.sep, '/')
func_args = list(func_args)
ref_indices = []
for (i, value) in enumerate(func_args):
if isinstance(value, OctavePtr):
ref_indices.append(i + 1)
func_args[i] = value.address
ref_indices = np.array(ref_indices)
# Save the request data to the output file.
req = dict(func_name=func_name, func_args=tuple(func_args),
dname=dname or '', nout=nout,
store_as=store_as or '',
ref_indices=ref_indices)
write_file(req, out_file, oned_as=self._oned_as,
convert_to_float=self.convert_to_float)
# Set up the engine and evaluate the `_pyeval()` function.
engine.stream_handler = stream_handler or self.logger.info
if timeout is None:
timeout = self.timeout
try:
engine.eval('_pyeval("%s", "%s");' % (out_file, in_file),
timeout=timeout)
except KeyboardInterrupt as e:
stream_handler(engine.repl.interrupt())
raise
except TIMEOUT:
stream_handler(engine.repl.interrupt())
raise Oct2PyError('Timed out, interrupting')
except EOF:
stream_handler(engine.repl.child.before)
self.restart()
raise Oct2PyError('Session died, restarting')
# Read in the output.
resp = read_file(in_file, self)
if resp['err']:
msg = self._parse_error(resp['err'])
raise Oct2PyError(msg)
result = resp['result'].ravel().tolist()
if isinstance(result, list) and len(result) == 1:
result = result[0]
# Check for sentinel value.
if (isinstance(result, Cell) and
result.size == 1 and
isinstance(result[0], string_types) and
result[0] == '__no_value__'):
result = None
if plot_dir:
self._engine.make_figures(plot_dir)
return result
def _parse_error(self, err):
"""Create a traceback for an Octave evaluation error.
"""
self.logger.debug(err)
stack = err.get('stack', [])
if not err['message'].startswith('parse error:'):
err['message'] = 'error: ' + err['message']
errmsg = 'Octave evaluation error:\n%s' % err['message']
if not isinstance(stack, StructArray):
return errmsg
errmsg += '\nerror: called from:'
for item in stack[:-1]:
errmsg += '\n %(name)s at line %(line)d' % item
try:
errmsg += ', column %(column)d' % item
except Exception:
pass
return errmsg
def _handle_stdin(self, line):
"""Handle a stdin request from the session."""
return input(line.replace(STDIN_PROMPT, ''))
def _get_doc(self, name):
"""
Get the documentation of an Octave procedure or object.
Parameters
----------
name : str
Function name to search for.
Returns
-------
out : str
Documentation string.
Raises
------
Oct2PyError
If the procedure or object function has a syntax error.
"""
doc = 'No documentation for %s' % name
engine = self._engine
doc = engine.eval('help("%s")' % name, silent=True)
if 'syntax error:' in doc.lower():
raise Oct2PyError(doc)
if 'error:' in doc.lower():
doc = engine.eval('type("%s")' % name, silent=True)
doc = '\n'.join(doc.splitlines()[:3])
default = self.feval.__doc__
default = ' ' + default[default.find('func_args:'):]
default = '\n'.join([line[8:] for line in default.splitlines()])
doc = '\n'.join(doc.splitlines())
doc = '\n' + doc + '\n\nParameters\n----------\n' + default
doc += '\n**kwargs - Deprecated keyword arguments\n\n'
doc += 'Notes\n-----\n'
doc += 'Keyword arguments to dynamic functions are deprecated.\n'
doc += 'The `plot_*` kwargs will be ignored, but the rest will\n'
doc += 'used as key - value pairs as in version 3.x.\n'
doc += 'Use `set_plot_settings()` for plot settings, and use\n'
doc += '`func_args` directly for key - value pairs.'
return doc
def _exist(self, name):
"""Test whether a name exists and return the name code.
Raises an error when the name does not exist.
"""
cmd = 'exist("%s")' % name
resp = self._engine.eval(cmd, silent=True).strip()
exist = int(resp.split()[-1])
if exist == 0:
msg = 'Value "%s" does not exist in Octave workspace'
raise Oct2PyError(msg % name)
return exist
def _isobject(self, name, exist):
"""Test whether the name is an object."""
if exist in [2, 5]:
return False
cmd = 'isobject(%s)' % name
resp = self._engine.eval(cmd, silent=True).strip()
return resp == 'ans = 1'
def _get_function_ptr(self, name):
"""Get or create a function pointer of the given name."""
func = _make_function_ptr_instance
self._function_ptrs.setdefault(name, func(self, name))
return self._function_ptrs[name]
def _get_user_class(self, name):
"""Get or create a user class of the given type."""
self._user_classes.setdefault(name, _make_user_class(self, name))
return self._user_classes[name]
def __getattr__(self, attr):
"""Automatically creates a wapper to an Octave function or object.
Adapted from the mlabwrap project.
"""
# needed for help(Oct2Py())
if attr.startswith('__'):
return super(Oct2Py, self).__getattr__(attr)
# close_ -> close
if attr[-1] == "_":
name = attr[:-1]
else:
name = attr
if self._engine is None:
raise Oct2PyError('Session is closed')
# Make sure the name exists.
exist = self._exist(name)
if exist not in [2, 3, 5, 103]:
msg = 'Name "%s" is not a valid callable, use `pull` for variables'
raise Oct2PyError(msg % name)
if name == 'clear':
raise Oct2PyError('Cannot use `clear` command directly, use' +
' `eval("clear(var1, var2)")`')
# Check for user defined class.
if self._isobject(name, exist):
obj = self._get_user_class(name)
else:
obj = self._get_function_ptr(name)
# !!! attr, *not* name, because we might have python keyword name!
setattr(self, attr, obj)
return obj