Only call figure.show if it exits.
[pypiezo.git] / pypiezo / base.py
index 3e5a07918aef74047ab480790e5e7bf7e2030f0e..3220e21833a060eca3eb76d2a03e0dd695ed6ba8 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright (C) 2011-2012 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2011-2012 W. Trevor King <wking@tremily.us>
 #
 # This file is part of pypiezo.
 #
 # You should have received a copy of the GNU General Public License along with
 # pypiezo.  If not, see <http://www.gnu.org/licenses/>.
 
-"Basic piezo control."
+"""Basic piezo control.
+
+Several of the classes defined in this module are simple wrappers for
+combining a `pycomedi` class instance (e.g. `Channel`) with the
+appropriate config data (e.g. `InputChannelConfig`).  The idea is that
+the `h5config`-based config data will make it easy for you to save
+your hardware configuration to disk (so that you have a record of what
+you did).  It should also make it easy to load your configuration from
+the disk (so that you can do the same thing again).  Because this
+`h5config` <-> `pycomedi` communication works both ways, you have two
+options when you're initializing a class:
+
+1) On your first run, it's probably easiest to setup your channels and
+   such using the usual `pycomedi` interface
+   (`device.find_subdevice_by_type`, etc.)  After you have setup your
+   channels, you can initialize them with a stock config instance, and
+   call the `setup_config` method to copy the channel configuration
+   into the config file.  Now the config instance is ready to be saved
+   to disk.
+2) On later runs, you have the option of loading the `pycomedi`
+   objects from your old configuration.  After loading the config data
+   from disk, initialize your class by passing in a `devices` list,
+   but without passing in the `Channel` instances.  The class will
+   take care of setting up the channel instances internally,
+   recreating your earlier setup.
+
+For examples of how to apply either approach to a particular class,
+see that class' docstring.
+"""
 
 import math as _math
 from time import sleep as _sleep
@@ -30,7 +58,10 @@ except (ImportError, RuntimeError), e:
     _matplotlib = None
     _matplotlib_import_error = e
 
-from pycomedi.constant import AREF, TRIG_SRC, SDF
+from pycomedi.device import Device
+from pycomedi.subdevice import StreamingSubdevice
+from pycomedi.channel import AnalogChannel
+from pycomedi.constant import AREF, TRIG_SRC, SDF, SUBDEVICE_TYPE, UNIT
 from pycomedi.utility import inttrig_insn, Reader, Writer
 
 from . import LOG as _LOG
@@ -170,6 +201,95 @@ def get_axis_name(axis_config):
     channel_config = axis_config['channel']
     return channel_config['name']
 
+def load_device(filename, devices):
+    """Return an open device from `devices` which has a given `filename`.
+
+    Sometimes a caller will already have the required `Device`, in
+    which case we just pull that instance out of `devices`, check that
+    it's open, and return it.  Other times, the caller may want us to
+    open the device ourselves, so if we can't find an appropriate
+    device in `devices`, we create a new one, append it to `devices`
+    (so the caller can close it later), and return it.
+
+    You will have to open the `Device` yourself, though, because the
+    open device instance should not be held by a particular
+    `PiezoAxis` instance.  If you don't want to open devices yourself,
+    you can pass in a blank list of devices, and the initialization
+    routine will append any necessary-but-missing devices to it.
+
+    >>> from pycomedi.device import Device
+
+    >>> devices = [Device('/dev/comedi0')]
+    >>> device = load_device(filename='/dev/comedi0', devices=devices)
+    >>> device.filename
+    '/dev/comedi0'
+    >>> device.file is not None
+    True
+    >>> device.close()
+
+    >>> devices = []
+    >>> device = load_device(filename='/dev/comedi0', devices=devices)
+    >>> devices == [device]
+    True
+    >>> device.filename
+    '/dev/comedi0'
+    >>> device.file is not None
+    True
+    >>> device.close()
+
+    We try and return helpful errors when things go wrong:
+
+    >>> device = load_device(filename='/dev/comedi0', devices=None)
+    Traceback (most recent call last):
+      ...
+    TypeError: 'NoneType' object is not iterable
+    >>> device = load_device(filename='/dev/comedi0', devices=tuple())
+    Traceback (most recent call last):
+      ...
+    ValueError: none of the available devices ([]) match /dev/comedi0, and we cannot append to ()
+    >>> device = load_device(filename='/dev/comediX', devices=[])
+    Traceback (most recent call last):
+      ...
+    PyComediError: comedi_open (/dev/comediX): No such file or directory (None)
+    """
+    try:
+        matching_devices = [d for d in devices if d.filename == filename]
+    except TypeError:
+        _LOG.error('non-iterable devices? ({})'.format(devices))
+        raise
+    if matching_devices:
+        device = matching_devices[0]
+        if device.file is None:
+            device.open()
+    else:
+        device = Device(filename)
+        device.open()
+        try:
+            devices.append(device)  # pass new device back to caller
+        except AttributeError:
+            device.close()
+            raise ValueError(
+                ('none of the available devices ({}) match {}, and we '
+                 'cannot append to {}').format(
+                    [d.filename for d in devices], filename, devices))
+    return device
+
+def _load_channel_from_config(channel, devices, subdevice_type):
+    c = channel.config  # reduce verbosity
+    if not channel.channel:
+        device = load_device(filename=c['device'], devices=devices)
+        if c['subdevice'] < 0:
+            subdevice = device.find_subdevice_by_type(
+                subdevice_type, factory=StreamingSubdevice)
+        else:
+            subdevice = device.subdevice(
+                index=c['subdevice'], factory=StreamingSubdevice)
+        channel.channel = subdevice.channel(
+            index=c['channel'], factory=AnalogChannel,
+            aref=c['analog-reference'])
+        channel.channel.range = channel.channel.get_range(index=c['range'])
+    channel.name = c['name']
+
 def _setup_channel_config(config, channel):
     """Initialize the `ChannelConfig` `config` using the
     `AnalogChannel` `channel`.
@@ -264,28 +384,39 @@ class PiezoAxis (object):
     ... # doctest: +ELLIPSIS
     -1.6...e-06
 
+    Opening from the config alone:
+
+    >>> p = PiezoAxis(config=config)
+    >>> p.load_from_config(devices=[d])
+    >>> p.axis_channel  # doctest: +ELLIPSIS
+    <pycomedi.channel.AnalogChannel object at 0x...>
+    >>> p.monitor_channel  # doctest: +ELLIPSIS
+    <pycomedi.channel.AnalogChannel object at 0x...>
+
     >>> d.close()
     """
     def __init__(self, config, axis_channel=None, monitor_channel=None):
         self.config = config
-        if (config['monitor'] and
-            config['channel']['device'] != config['monitor']['device']):
-            raise NotImplementedError(
-                ('piezo axis control and monitor on different devices '
-                 '(%s and %s)') % (
-                    config['channel']['device'],
-                    config['monitor']['device']))
-        if not axis_channel:
-            raise NotImplementedError(
-                'pypiezo not yet capable of opening its own axis channel')
-            #axis_channel = pycomedi...
         self.axis_channel = axis_channel
-        if config['monitor'] and not monitor_channel:
-            raise NotImplementedError(
-                'pypiezo not yet capable of opening its own monitor channel')
-            #monitor_channel = pycomedi...
         self.monitor_channel = monitor_channel
-        self.name = config['channel']['name']
+
+    def load_from_config(self, devices):
+        c = self.config  # reduce verbosity
+        if (c['monitor'] and
+            c['channel']['device'] != c['monitor']['device']):
+            raise NotImplementedError(
+                ('piezo axis control and monitor on different devices '
+                 '({} and {})').format(
+                    c['channel']['device'], c['monitor']['device']))
+        if not self.axis_channel:
+            output = OutputChannel(config=c['channel'])
+            output.load_from_config(devices=devices)
+            self.axis_channel = output.channel
+        if c['monitor'] and not self.monitor_channel:
+            monitor = InputChannel(config=c['monitor'])
+            monitor.load_from_config(devices=devices)
+            self.monitor_channel = monitor.channel
+        self.name = c['channel']['name']
 
     def setup_config(self):
         "Initialize the axis (and monitor) configs."
@@ -301,6 +432,65 @@ class PiezoAxis (object):
                 self.config['channel'], self.axis_channel.get_maxdata())
 
 
+
+class OutputChannel(object):
+    """An input channel monitoring some interesting parameter.
+
+    >>> from pycomedi.device import Device
+    >>> from pycomedi.subdevice import StreamingSubdevice
+    >>> from pycomedi.channel import AnalogChannel
+    >>> from pycomedi.constant import AREF, SUBDEVICE_TYPE, UNIT
+
+    >>> d = Device('/dev/comedi0')
+    >>> d.open()
+
+    >>> s = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
+    ...     factory=StreamingSubdevice)
+
+    >>> channel = s.channel(0, factory=AnalogChannel, aref=AREF.diff)
+    >>> channel.range = channel.find_range(unit=UNIT.volt, min=-10, max=10)
+
+    >>> channel_config = _config.OutputChannelConfig()
+
+    >>> c = OutputChannel(config=channel_config, channel=channel)
+    >>> c.setup_config()
+    >>> print(channel_config.dump())
+    name: 
+    device: /dev/comedi0
+    subdevice: 1
+    channel: 0
+    maxdata: 65535
+    range: 0
+    analog-reference: diff
+    conversion-coefficients: -10.0, 0.000305180437934
+    conversion-origin: 0.0
+    inverse-conversion-coefficients: 0.0, 3276.75
+    inverse-conversion-origin: -10.0
+
+    >>> convert_volts_to_bits(c.config, -10)
+    0.0
+
+    Opening from the config alone:
+
+    >>> c = OutputChannel(config=channel_config)
+    >>> c.load_from_config(devices=[d])
+    >>> c.channel  # doctest: +ELLIPSIS
+    <pycomedi.channel.AnalogChannel object at 0x...>
+
+    >>> d.close()
+    """
+    def __init__(self, config, channel=None):
+        self.config = config
+        self.channel = channel
+
+    def load_from_config(self, devices):
+        _load_channel_from_config(
+            channel=self, devices=devices, subdevice_type=SUBDEVICE_TYPE.ao)
+
+    def setup_config(self):
+        _setup_channel_config(self.config, self.channel)
+
+
 class InputChannel(object):
     """An input channel monitoring some interesting parameter.
 
@@ -338,16 +528,22 @@ class InputChannel(object):
     >>> convert_bits_to_volts(c.config, 0)
     -10.0
 
+    Opening from the config alone:
+
+    >>> c = InputChannel(config=channel_config)
+    >>> c.load_from_config(devices=[d])
+    >>> c.channel  # doctest: +ELLIPSIS
+    <pycomedi.channel.AnalogChannel object at 0x...>
+
     >>> d.close()
     """
     def __init__(self, config, channel=None):
         self.config = config
-        if not channel:
-            raise NotImplementedError(
-                'pypiezo not yet capable of opening its own channel')
-            #channel = pycomedi...
         self.channel = channel
-        self.name = config['name']
+
+    def load_from_config(self, devices):
+        _load_channel_from_config(
+            channel=self, devices=devices, subdevice_type=SUBDEVICE_TYPE.ai)
 
     def setup_config(self):
         _setup_channel_config(self.config, self.channel)
@@ -365,35 +561,30 @@ class Piezo (object):
     >>> d = Device('/dev/comedi0')
     >>> d.open()
 
-    >>> s_in = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
-    ...     factory=StreamingSubdevice)
-    >>> s_out = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
-    ...     factory=StreamingSubdevice)
-
-    >>> axis_channel = s_out.channel(
-    ...     0, factory=AnalogChannel, aref=AREF.ground)
-    >>> monitor_channel = s_in.channel(
-    ...     0, factory=AnalogChannel, aref=AREF.diff)
-    >>> input_channel = s_in.channel(1, factory=AnalogChannel, aref=AREF.diff)
-    >>> for chan in [axis_channel, monitor_channel, input_channel]:
-    ...     chan.range = chan.find_range(unit=UNIT.volt, min=-10, max=10)
-
     >>> axis_config = _config.AxisConfig()
-    >>> axis_config.update({'gain':20, 'sensitivity':8e-9})
+    >>> axis_config['gain'] = 20.0
+    >>> axis_config['sensitivity'] = 8e-9
     >>> axis_config['channel'] = _config.OutputChannelConfig()
+    >>> axis_config['channel']['analog-reference'] = AREF.ground
     >>> axis_config['channel']['name'] = 'z'
     >>> axis_config['monitor'] = _config.InputChannelConfig()
-    >>> input_config = _config.InputChannelConfig()
-    >>> input_config['name'] = 'some-input'
-
-    >>> a = PiezoAxis(config=axis_config, axis_channel=axis_channel,
-    ...     monitor_channel=monitor_channel)
+    >>> axis_config['monitor']['analog-reference'] = AREF.diff
+    >>> a = PiezoAxis(config=axis_config)
+    >>> a.load_from_config(devices=[d])
     >>> a.setup_config()
 
-    >>> c = InputChannel(config=input_config, channel=input_channel)
+    >>> input_config = _config.InputChannelConfig()
+    >>> input_config['analog-reference'] = AREF.diff
+    >>> input_config['name'] = 'some-input'
+    >>> c = InputChannel(config=input_config)
+    >>> c.load_from_config(devices=[d])
     >>> c.setup_config()
 
-    >>> p = Piezo(axes=[a], inputs=[c], name='Charlie')
+    >>> config = _config.PiezoConfig()
+    >>> config['name'] = 'Charlie'
+
+    >>> p = Piezo(config=config, axes=[a], inputs=[c])
+    >>> p.setup_config()
     >>> inputs = p.read_inputs()
     >>> pprint(inputs)  # doctest: +SKIP
     {'some-input': 34494L, 'z-monitor': 32669L}
@@ -443,18 +634,52 @@ class Piezo (object):
      'z': array([    0,  3276,  ..., 32760], dtype=uint16),
      'z-monitor': array([ 3102,  6384,  ..., 32647], dtype=uint16)}
 
+    Opening from the config alone:
+
+    >>> p = Piezo(config=config)
+    >>> p.load_from_config(devices=[d])
+    >>> for axis in p.axes:
+    ...     print(axis.axis_channel)
+    ...     print(axis.monitor_channel)
+    ... # doctest: +ELLIPSIS
+    <pycomedi.channel.AnalogChannel object at 0x...>
+    <pycomedi.channel.AnalogChannel object at 0x...>
+    >>> for input in p.inputs:
+    ...     print(input.channel)
+    ... # doctest: +ELLIPSIS
+    <pycomedi.channel.AnalogChannel object at 0x...>
+
     >>> d.close()
     """
-    def __init__(self, axes, inputs, name=None):
+    def __init__(self, config, axes=None, inputs=None):
+        self.config=config
         self.axes = axes
         self.inputs = inputs
-        self.config = _config.PiezoConfig()
-        self.name = name
-        self.config['name'] = name
-        self.config['axes'] = [x.config for x in axes]
-        self.config['inputs'] = [x.config for x in inputs]
         self.last_output = {}
 
+    def load_from_config(self, devices):
+        if not self.axes:
+            self.axes = []
+            for config in self.config['axes']:
+                axis = PiezoAxis(config=config)
+                axis.load_from_config(devices=devices)
+                self.axes.append(axis)
+            self.last_output.clear()
+        if not self.inputs:
+            self.inputs = []
+            for config in self.config['inputs']:
+                input = InputChannel(config=config)
+                input.load_from_config(devices=devices)
+                self.inputs.append(input)
+        self.name = self.config['name']
+
+    def setup_config(self):
+        "Initialize the axis and input configs."
+        for x in self.axes + self.inputs:
+            x.setup_config()
+        self.config['axes'] = [x.config for x in self.axes]
+        self.config['inputs'] = [x.config for x in self.inputs]
+
     def axis_by_name(self, name):
         "Get an axis by its name."
         for axis in self.axes:
@@ -522,7 +747,8 @@ class Piezo (object):
 
     def jump(self, axis_name, position, steps=1, sleep=None):
         "Move the output named `axis_name` to `position`."
-        _LOG.debug('jump %s to %s in %d steps' % (axis_name, position, steps))
+        _LOG.debug('jump {} to {} in {} steps'.format(
+                axis_name, position, steps))
         if steps > 1:
             try:
                 orig_pos = self.last_output[axis_name]
@@ -531,9 +757,12 @@ class Piezo (object):
                     ("cannot make a soft jump to {} because we don't have a "
                      'last-output position for {}').format(
                         position, axis_name))
-                steps = 1
+                return self.jump(axis_name=axis_name, position=position)
             else:
-                for pos in _numpy.linspace(orig_pos, position, steps+1)[1:]:
+                for i,pos in enumerate(_numpy.linspace(
+                        orig_pos, position, steps+1)[1:]):
+                    _LOG.debug('jump {} to {} ({} of {} steps)'.format(
+                            axis_name, pos, i, steps))
                     self.jump(axis_name=axis_name, position=pos)
                     if sleep:
                         _sleep(sleep)
@@ -670,7 +899,11 @@ class Piezo (object):
                             (input_data, input_names)]:
                 for i,name in enumerate(names):
                     axes.plot(d[:,i], label=name)
-            figure.show()
+            figure.canvas.draw()
+            if hasattr(figure, 'show'):
+                figure.show()
+            if not _matplotlib.is_interactive():
+                _matplotlib_pyplot.show()
         return input_data
 
     def named_ramp(self, data, frequency, output_names, input_names=()):
@@ -683,3 +916,16 @@ class Piezo (object):
         for i,name in enumerate(input_names):
             ret[name] = input_data[:,i]
         return ret
+
+    def zero(self, axis_names=None, **kwargs):
+        zeros = []
+        if axis_names is None:
+            axis_names = [axis.name for axis in self.axes]
+        for axis_name in axis_names:
+            axis = self.axis_by_name(axis_name)
+            config = self.config.select_config(
+                'axes', axis_name, get_attribute=get_axis_name)['channel']
+            zero = convert_volts_to_bits(config, 0)
+            zeros.append(zero)
+            self.jump(axis_name, zero)
+        return zeros