X-Git-Url: http://git.tremily.us/?a=blobdiff_plain;ds=sidebyside;f=pypiezo%2Fbase.py;h=3220e21833a060eca3eb76d2a03e0dd695ed6ba8;hb=29f0344f89483af840175ba4049ed8e467a3d567;hp=3e5a07918aef74047ab480790e5e7bf7e2030f0e;hpb=3e0d22c9b07c55f2d2e6f73a94c752e24ca0e10a;p=pypiezo.git diff --git a/pypiezo/base.py b/pypiezo/base.py index 3e5a079..3220e21 100644 --- a/pypiezo/base.py +++ b/pypiezo/base.py @@ -1,4 +1,4 @@ -# Copyright (C) 2011-2012 W. Trevor King +# Copyright (C) 2011-2012 W. Trevor King # # This file is part of pypiezo. # @@ -14,7 +14,35 @@ # You should have received a copy of the GNU General Public License along with # pypiezo. If not, see . -"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 + + >>> p.monitor_channel # doctest: +ELLIPSIS + + >>> 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 + + + >>> 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 + + >>> 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 + + + >>> for input in p.inputs: + ... print(input.channel) + ... # doctest: +ELLIPSIS + + >>> 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