Fix doctest config-dump output in base.py.
[pypiezo.git] / pypiezo / base.py
1 # Copyright (C) 2008-2011 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of pypiezo.
4 #
5 # pypiezo is free software; you can redistribute it and/or modify it
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation, either version 3 of the License, or (at your
8 # option) any later version.
9 #
10 # pypiezo is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 # General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with pypiezo.  If not, see <http://www.gnu.org/licenses/>.
17
18 "Basic piezo control."
19
20 import math as _math
21 from time import sleep as _sleep
22
23 import numpy as _numpy
24 from scipy.stats import linregress as _linregress
25
26 try:
27     import matplotlib as _matplotlib
28     import matplotlib.pyplot as _matplotlib_pyplot
29     import time as _time  # for timestamping lines on plots
30 except (ImportError, RuntimeError), e:
31     _matplotlib = None
32     _matplotlib_import_error = e
33
34 from pycomedi.constant import TRIG_SRC, SDF
35 from pycomedi.utility import inttrig_insn, Reader, Writer
36
37 from . import LOG as _LOG
38 from . import config as _config
39 from . import package_config as _package_config
40
41
42 def convert_bits_to_volts(config, data):
43     """Convert bit-valued data to volts.
44
45     >>> config = _config.ChannelConfig()
46     >>> config['conversion-coefficients'] = [1, 2, 3]
47     >>> config['conversion-origin'] = -1
48     >>> convert_bits_to_volts(config, -1)
49     1
50     >>> convert_bits_to_volts(
51     ...     config, _numpy.array([-1, 0, 1, 2], dtype=_numpy.float))
52     array([  1.,   6.,  17.,  34.])
53     """
54     coefficients = config['conversion-coefficients']
55     origin = config['conversion-origin']
56     return _numpy.polyval(list(reversed(coefficients)), data-origin)
57
58 def convert_volts_to_bits(config, data):
59     """Convert bit-valued data to volts.
60
61     >>> config = _config.ChannelConfig()
62     >>> config['inverse-conversion-coefficients'] = [1, 2, 3]
63     >>> config['inverse-conversion-origin'] = -1
64     >>> convert_volts_to_bits(config, -1)
65     1
66     >>> convert_volts_to_bits(
67     ...     config, _numpy.array([-1, 0, 1, 2], dtype=_numpy.float))
68     array([  1.,   6.,  17.,  34.])
69
70     Note that the inverse coeffiecient and offset are difficult to
71     derive from the forward coefficient and offset.  The current
72     Comedilib conversion functions, `comedi_to_physical()` and
73     `comedi_from_physical()` take `comedi_polynomial_t` conversion
74     arguments.  `comedi_polynomial_t` is defined in `comedilib.h`_,
75     and holds a polynomial of length
76     `COMEDI_MAX_NUM_POLYNOMIAL_COEFFICIENTS`, currently set to 4.  The
77     inverse of this cubic polynomial might be another polynomial, but
78     it might also have a more complicated form.  A general inversion
79     solution is considered too complicated, so when you're setting up
80     your configuration, you should use Comedilib to save both the
81     forward and inverse coefficients and offsets.
82
83     .. _comedilib.h: http://www.comedi.org/git?p=comedi/comedilib.git;a=blob;f=include/comedilib.h;hb=HEAD
84     """
85     origin = config['inverse-conversion-origin']
86     inverse_coefficients = config['inverse-conversion-coefficients']
87     if len(inverse_coefficients) == 0:
88         raise NotImplementedError('cubic polynomial inversion')
89     return _numpy.polyval(list(reversed(inverse_coefficients)), data-origin)
90
91 def convert_volts_to_meters(config, data):
92     """Convert volt-valued data to meters.
93
94     >>> config = _config.AxisConfig()
95     >>> config['gain'] = 20.0
96     >>> config['sensitivity'] = 8e-9
97     >>> convert_volts_to_meters(config, 1)
98     1.6e-07
99     >>> convert_volts_to_meters(
100     ...     config, _numpy.array([1, 6, 17, 34], dtype=_numpy.float))
101     ... # doctest: +ELLIPSIS
102     array([  1.6...e-07,   9.6...e-07,   2.7...e-06,
103              5.4...e-06])
104     """
105     return data * config['gain'] * config['sensitivity']
106
107 def convert_meters_to_volts(config, data):
108     """Convert bit-valued data to volts.
109
110     >>> config = _config.AxisConfig()
111     >>> config['gain'] = 20.0
112     >>> config['sensitivity'] = 8e-9
113     >>> convert_meters_to_volts(config, 1.6e-7)
114     1.0
115     >>> convert_meters_to_volts(
116     ...     config, _numpy.array([1.6e-7, 9.6e-7, 2.72e-6, 5.44e-6],
117     ...                          dtype=_numpy.float))
118     array([  1.,   6.,  17.,  34.])
119     """
120     return data / (config['gain'] * config['sensitivity'])
121
122 def convert_bits_to_meters(axis_config, data):
123     """Convert bit-valued data to meters.
124
125     >>> channel_config = _config.ChannelConfig()
126     >>> channel_config['conversion-coefficients'] = [1, 2, 3]
127     >>> channel_config['conversion-origin'] = -1
128     >>> axis_config = _config.AxisConfig()
129     >>> axis_config['gain'] = 20.0
130     >>> axis_config['sensitivity'] = 8e-9
131     >>> axis_config['channel'] = channel_config
132     >>> convert_bits_to_meters(axis_config, 1)
133     ... # doctest: +ELLIPSIS
134     2.7...e-06
135     >>> convert_bits_to_meters(
136     ...     axis_config, _numpy.array([-1, 0, 1, 2], dtype=_numpy.float))
137     ... # doctest: +ELLIPSIS
138     array([  1.6...e-07,   9.6...e-07,   2.7...e-06,
139              5.4...e-06])
140     """
141     data = convert_bits_to_volts(axis_config['channel'], data)
142     return convert_volts_to_meters(axis_config, data)
143
144 def convert_meters_to_bits(axis_config, data):
145     """Convert meter-valued data to volts.
146
147     >>> channel_config = _config.ChannelConfig()
148     >>> channel_config['inverse-conversion-coefficients'] = [1, 2, 3]
149     >>> channel_config['inverse-conversion-origin'] = -1
150     >>> axis_config = _config.AxisConfig()
151     >>> axis_config['gain'] = 20.0
152     >>> axis_config['sensitivity'] = 8e-9
153     >>> axis_config['channel'] = channel_config
154     >>> convert_meters_to_bits(axis_config, 1.6e-7)
155     17.0
156     >>> convert_meters_to_bits(
157     ...     axis_config,
158     ...     _numpy.array([1.6e-7, 9.6e-7, 2.72e-6, 5.44e-6],
159     ...                  dtype=_numpy.float))
160     array([   17.,   162.,  1009.,  3746.])
161     """
162     data = convert_meters_to_volts(axis_config, data)
163     return convert_volts_to_bits(axis_config['channel'], data)
164
165 def _setup_channel_config(config, channel):
166     """Initialize the `ChannelConfig` `config` using the
167     `AnalogChannel` `channel`.
168     """
169     config['device'] = channel.subdevice.device.filename
170     config['subdevice'] = channel.subdevice.index
171     config['channel'] = channel.index
172     config['maxdata'] = channel.get_maxdata()
173     config['range'] = channel.range.value
174     converter = channel.get_converter()
175     config['conversion-origin'
176            ] = converter.get_to_physical_expansion_origin()
177     config['conversion-coefficients'
178            ] = converter.get_to_physical_coefficients()
179     config['inverse-conversion-origin'
180            ] = converter.get_from_physical_expansion_origin()
181     config['inverse-conversion-coefficients'
182            ] = converter.get_from_physical_coefficients()
183
184
185 class PiezoAxis (object):
186     """A one-dimensional piezoelectric actuator.
187
188     If used, the montoring channel must (as of now) be on the same
189     device as the controlling channel.
190
191     >>> from pycomedi.device import Device
192     >>> from pycomedi.subdevice import StreamingSubdevice
193     >>> from pycomedi.channel import AnalogChannel
194     >>> from pycomedi.constant import (AREF, SUBDEVICE_TYPE, UNIT)
195
196     >>> d = Device('/dev/comedi0')
197     >>> d.open()
198
199     >>> s_in = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
200     ...     factory=StreamingSubdevice)
201     >>> s_out = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
202     ...     factory=StreamingSubdevice)
203
204     >>> axis_channel = s_out.channel(
205     ...     0, factory=AnalogChannel, aref=AREF.ground)
206     >>> monitor_channel = s_in.channel(
207     ...     0, factory=AnalogChannel, aref=AREF.diff)
208     >>> for chan in [axis_channel, monitor_channel]:
209     ...     chan.range = chan.find_range(unit=UNIT.volt, min=-10, max=10)
210
211     >>> config = _config.AxisConfig()
212     >>> config.update({'gain':20, 'sensitivity':8e-9})
213     >>> config['channel'] = _config.OutputChannelConfig()
214     >>> config['monitor'] = _config.InputChannelConfig()
215     >>> config['monitor']['device'] = '/dev/comediX'
216
217     >>> p = PiezoAxis(config=config)
218     ... # doctest: +NORMALIZE_WHITESPACE
219     Traceback (most recent call last):
220       ...
221     NotImplementedError: piezo axis control and monitor on different devices
222      (/dev/comedi0 and /dev/comediX)
223
224     >>> config['monitor']['device'] = config['channel']['device']
225     >>> p = PiezoAxis(config=config,
226     ...     axis_channel=axis_channel, monitor_channel=monitor_channel)
227
228     >>> p.setup_config()
229     >>> print(config['channel'].dump())
230     name: 
231     device: /dev/comedi0
232     subdevice: 1
233     channel: 0
234     maxdata: 65535
235     range: 0
236     conversion-coefficients: -10.0, 0.000305180437934
237     conversion-origin: 0.0
238     inverse-conversion-coefficients: 0.0, 3276.75
239     inverse-conversion-origin: -10.0
240     >>> print(config['monitor'].dump())
241     name: 
242     device: /dev/comedi0
243     subdevice: 0
244     channel: 0
245     maxdata: 65535
246     range: 0
247     conversion-coefficients: -10.0, 0.000305180437934
248     conversion-origin: 0.0
249     inverse-conversion-coefficients: 0.0, 3276.75
250     inverse-conversion-origin: -10.0
251
252     >>> convert_bits_to_meters(p.config, 0)
253     ... # doctest: +ELLIPSIS
254     -1.6...e-06
255
256     >>> d.close()
257     """
258     def __init__(self, config, axis_channel=None, monitor_channel=None):
259         self.config = config
260         if (config['monitor'] and
261             config['channel']['device'] != config['monitor']['device']):
262             raise NotImplementedError(
263                 ('piezo axis control and monitor on different devices '
264                  '(%s and %s)') % (
265                     config['channel']['device'],
266                     config['monitor']['device']))
267         if not axis_channel:
268             raise NotImplementedError(
269                 'pypiezo not yet capable of opening its own axis channel')
270             #axis_channel = pycomedi...
271         self.axis_channel = axis_channel
272         if config['monitor'] and not monitor_channel:
273             raise NotImplementedError(
274                 'pypiezo not yet capable of opening its own monitor channel')
275             #monitor_channel = pycomedi...
276         self.monitor_channel = monitor_channel
277         self.name = config['channel']['name']
278
279     def setup_config(self):
280         "Initialize the axis (and monitor) configs."
281         _setup_channel_config(self.config['channel'], self.axis_channel)
282         if self.monitor_channel:
283             _setup_channel_config(
284                 self.config['monitor'], self.monitor_channel)
285         if self.config['minimum'] is None:
286             self.config['minimum'] = convert_bits_to_volts(
287                 self.config['channel'], 0)
288         if self.config['maximum'] is None:
289             self.config['maximum'] = convert_bits_to_volts(
290                 self.config['channel'], self.axis_channel.get_maxdata())
291
292
293 class InputChannel(object):
294     """An input channel monitoring some interesting parameter.
295
296     >>> from pycomedi.device import Device
297     >>> from pycomedi.subdevice import StreamingSubdevice
298     >>> from pycomedi.channel import AnalogChannel
299     >>> from pycomedi.constant import AREF, SUBDEVICE_TYPE, UNIT
300
301     >>> d = Device('/dev/comedi0')
302     >>> d.open()
303
304     >>> s = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
305     ...     factory=StreamingSubdevice)
306
307     >>> channel = s.channel(0, factory=AnalogChannel, aref=AREF.diff)
308     >>> channel.range = channel.find_range(unit=UNIT.volt, min=-10, max=10)
309
310     >>> channel_config = _config.InputChannelConfig()
311
312     >>> c = InputChannel(config=channel_config, channel=channel)
313     >>> c.setup_config()
314     >>> print(channel_config.dump())
315     name: 
316     device: /dev/comedi0
317     subdevice: 0
318     channel: 0
319     maxdata: 65535
320     range: 0
321     conversion-coefficients: -10.0, 0.000305180437934
322     conversion-origin: 0.0
323     inverse-conversion-coefficients: 0.0, 3276.75
324     inverse-conversion-origin: -10.0
325
326     >>> convert_bits_to_volts(c.config, 0)
327     -10.0
328
329     >>> d.close()
330     """
331     def __init__(self, config, channel=None):
332         self.config = config
333         if not channel:
334             raise NotImplementedError(
335                 'pypiezo not yet capable of opening its own channel')
336             #channel = pycomedi...
337         self.channel = channel
338         self.name = config['name']
339
340     def setup_config(self):
341         _setup_channel_config(self.config, self.channel)
342
343
344 class Piezo (object):
345     """A piezo actuator-controlled experiment.
346
347     >>> from pprint import pprint
348     >>> from pycomedi.device import Device
349     >>> from pycomedi.subdevice import StreamingSubdevice
350     >>> from pycomedi.channel import AnalogChannel
351     >>> from pycomedi.constant import AREF, SUBDEVICE_TYPE, UNIT
352
353     >>> d = Device('/dev/comedi0')
354     >>> d.open()
355
356     >>> s_in = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
357     ...     factory=StreamingSubdevice)
358     >>> s_out = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
359     ...     factory=StreamingSubdevice)
360
361     >>> axis_channel = s_out.channel(
362     ...     0, factory=AnalogChannel, aref=AREF.ground)
363     >>> monitor_channel = s_in.channel(
364     ...     0, factory=AnalogChannel, aref=AREF.diff)
365     >>> input_channel = s_in.channel(1, factory=AnalogChannel, aref=AREF.diff)
366     >>> for chan in [axis_channel, monitor_channel, input_channel]:
367     ...     chan.range = chan.find_range(unit=UNIT.volt, min=-10, max=10)
368
369     >>> axis_config = _config.AxisConfig()
370     >>> axis_config.update({'gain':20, 'sensitivity':8e-9})
371     >>> axis_config['channel'] = _config.OutputChannelConfig()
372     >>> axis_config['channel']['name'] = 'z'
373     >>> axis_config['monitor'] = _config.InputChannelConfig()
374     >>> input_config = _config.InputChannelConfig()
375     >>> input_config['name'] = 'some-input'
376
377     >>> a = PiezoAxis(config=axis_config, axis_channel=axis_channel,
378     ...     monitor_channel=monitor_channel)
379     >>> a.setup_config()
380
381     >>> c = InputChannel(config=input_config, channel=input_channel)
382     >>> c.setup_config()
383
384     >>> p = Piezo(axes=[a], inputs=[c], name='Charlie')
385     >>> inputs = p.read_inputs()
386     >>> pprint(inputs)  # doctest: +SKIP
387     {'some-input': 34494L, 'z-monitor': 32669L}
388
389     >>> pos = convert_volts_to_bits(p.config['axes'][0]['channel'], 0)
390     >>> pos
391     32767.5
392     >>> p.jump('z', pos)
393     >>> p.last_output == {'z': int(pos)}
394     True
395
396     :meth:`ramp` raises an error if passed an invalid `data` `dtype`.
397
398     >>> output_data = _numpy.arange(0, int(pos), step=int(pos/10),
399     ...     dtype=_numpy.float)
400     >>> output_data = output_data.reshape((len(output_data), 1))
401     >>> input_data = p.ramp(data=output_data, frequency=10,
402     ...     output_names=['z'], input_names=['z-monitor', 'some-input'])
403     Traceback (most recent call last):
404       ...
405     ValueError: output dtype float64 does not match expected <type 'numpy.uint16'>
406     >>> output_data = _numpy.arange(0, int(pos), step=int(pos/10),
407     ...     dtype=p.channel_dtype('z', direction='output'))
408     >>> output_data = output_data.reshape((len(output_data), 1))
409     >>> input_data = p.ramp(data=output_data, frequency=10,
410     ...     output_names=['z'], input_names=['z-monitor', 'some-input'])
411     >>> input_data  # doctest: +SKIP
412     array([[    0, 25219],
413            [ 3101, 23553],
414            [ 6384, 22341],
415            [ 9664, 21465],
416            [12949, 20896],
417            [16232, 20614],
418            [19516, 20588],
419            [22799, 20801],
420            [26081, 21233],
421            [29366, 21870],
422            [32646, 22686]], dtype=uint16)
423
424     >>> p.last_output == {'z': output_data[-1]}
425     True
426
427     >>> data = p.named_ramp(data=output_data, frequency=10,
428     ...     output_names=['z'], input_names=['z-monitor', 'some-input'])
429     >>> pprint(data)  # doctest: +ELLIPSIS, +SKIP
430     {'some-input': array([21666, 20566, ..., 22395], dtype=uint16),
431      'z': array([    0,  3276,  ..., 32760], dtype=uint16),
432      'z-monitor': array([ 3102,  6384,  ..., 32647], dtype=uint16)}
433
434     >>> d.close()
435     """
436     def __init__(self, axes, inputs, name=None):
437         self.axes = axes
438         self.inputs = inputs
439         self.config = _config.PiezoConfig()
440         self.name = name
441         self.config['name'] = name
442         self.config['axes'] = [x.config for x in axes]
443         self.config['inputs'] = [x.config for x in inputs]
444         self.last_output = {}
445
446     def axis_by_name(self, name):
447         "Get an axis by its name."
448         for axis in self.axes:
449             if axis.name == name:
450                 return axis
451         raise ValueError(name)
452
453     def input_channel_by_name(self, name):
454         "Get an input channel by its name."
455         for input_channel in self.inputs:
456             if input_channel.name == name:
457                 return input_channel
458         raise ValueError(name)
459
460     def channels(self, direction=None):
461         """Iterate through all `(name, channel)` tuples.
462
463         ===========  ===================
464         `direction`  Returned channels
465         ===========  ===================
466         'input'      all input channels
467         'output'     all output channels
468         None         all channels
469         ===========  ===================
470         """
471         if direction not in ('input', 'output', None):
472             raise ValueError(direction)
473         for a in self.axes:
474             if direction != 'input':
475                 yield (a.name, a.axis_channel)
476             if a.monitor_channel and direction != 'output':
477                 yield ('%s-monitor' % a.name, a.monitor_channel)
478         if direction != 'output':
479             for c in self.inputs:
480                 yield (c.name, c.channel)
481
482     def channel_by_name(self, name, direction=None):
483         """Get a channel by its name.
484
485         Setting `direction` (see :meth:`channels`) may allow a more
486         efficient search.
487         """
488         for n,channel in self.channels(direction=direction):
489             if n == name:
490                 return channel
491         raise ValueError(name)
492
493     def channel_dtype(self, channel_name, direction=None):
494         """Get a channel's data type by name.
495
496         Setting `direction` (see :meth:`channels`) may allow a more
497         efficient search.
498         """
499         channel = self.channel_by_name(name=channel_name, direction=direction)
500         return channel.subdevice.get_dtype()
501
502     def read_inputs(self):
503         "Read all inputs and return a `name`->`value` dictionary."
504         # There is no multi-channel read instruction, so preform reads
505         # sequentially.
506         ret = dict([(n, c.data_read()) for n,c in self.channels('input')])
507         _LOG.debug('current position: %s' % ret)
508         return ret
509
510     def jump(self, axis_name, position):
511         "Move the output named `axis_name` to `position`."
512         _LOG.debug('jump %s to %s' % (axis_name, position))
513         position = int(position)
514         channel = self.channel_by_name(name=axis_name)
515         channel.data_write(position)
516         self.last_output[axis_name] = position
517
518     def ramp(self, data, frequency, output_names, input_names=()):
519         """Synchronized IO ramp writing `data` and reading `in_names`.
520
521         Parameters
522         ----------
523         data : numpy array-like
524             Row for each cycle, column for each output channel.
525         frequency : float
526             Target cycle frequency in Hz.
527         output_names : list of strings
528             Names of output channels in the same order as the columns
529             of `data`.
530         input_names : list of strings
531             Names of input channels to monitor in the same order as
532             the columns of the returned array.
533         """
534         if len(data.shape) != 2:
535             raise ValueError(
536                 'ramp data must be two dimensional, not %d' % len(data.shape))
537         if data.shape[1] != len(output_names):
538             raise ValueError(
539                 'ramp data should have on column for each input, '
540                 'but has %d columns for %d inputs'
541                 % (data.shape[1], len(output_names)))
542         n_samps = data.shape[0]
543         log_string = 'ramp %d samples at %g Hz.  out: %s, in: %s' % (
544             n_samps, frequency, output_names, input_names)
545         _LOG.debug(log_string)  # _LOG on one line for easy commenting-out
546         # TODO: check range?
547         output_channels = [self.channel_by_name(name=n, direction='output')
548                            for n in output_names]
549         inputs = [self.channel_by_name(name=n, direction='input')
550                           for n in input_names]
551
552         ao_subdevice = output_channels[0].subdevice
553         ai_subdevice = inputs[0].subdevice
554         device = ao_subdevice.device
555
556         output_dtype = ao_subdevice.get_dtype()
557         if data.dtype != output_dtype:
558             raise ValueError('output dtype %s does not match expected %s'
559                              % (data.dtype, output_dtype))
560         input_data = _numpy.ndarray(
561             (n_samps, len(inputs)), dtype=ai_subdevice.get_dtype())
562
563         _LOG.debug('setup ramp commands')
564         scan_period_ns = int(1e9 / frequency)
565         ai_cmd = ai_subdevice.get_cmd_generic_timed(
566             len(inputs), scan_period_ns)
567         ao_cmd = ao_subdevice.get_cmd_generic_timed(
568             len(output_channels), scan_period_ns)
569
570         ai_cmd.start_src = TRIG_SRC.int
571         ai_cmd.start_arg = 0
572         ai_cmd.stop_src = TRIG_SRC.count
573         ai_cmd.stop_arg = n_samps
574         ai_cmd.chanlist = inputs
575         #ao_cmd.start_src = TRIG_SRC.ext
576         #ao_cmd.start_arg = 18  # NI card AI_START1 internal AI start signal
577         ao_cmd.start_src = TRIG_SRC.int
578         ao_cmd.start_arg = 0
579         ao_cmd.stop_src = TRIG_SRC.count
580         ao_cmd.stop_arg = n_samps-1
581         ao_cmd.chanlist = output_channels
582
583         ai_subdevice.cmd = ai_cmd
584         ao_subdevice.cmd = ao_cmd
585         for i in range(3):
586             rc = ai_subdevice.command_test()
587             if rc is None: break
588             _LOG.debug('analog input test %d: %s' % (i, rc))
589         for i in range(3):
590             rc = ao_subdevice.command_test()
591             if rc is None: break
592             _LOG.debug('analog output test %d: %s' % (i, rc))
593
594         _LOG.debug('lock subdevices for ramp')
595         ao_subdevice.lock()
596         try:
597             ai_subdevice.lock()
598             try:
599                 _LOG.debug('load ramp commands')
600                 ao_subdevice.command()
601                 ai_subdevice.command()
602
603                 writer = Writer(ao_subdevice, data)
604                 writer.start()
605                 reader = Reader(ai_subdevice, input_data)
606                 reader.start()
607
608                 _LOG.debug('arm analog output')
609                 device.do_insn(inttrig_insn(ao_subdevice))
610                 _LOG.debug('trigger ramp (via analog input)')
611                 device.do_insn(inttrig_insn(ai_subdevice))
612                 _LOG.debug('ramp running')
613
614                 writer.join()
615                 reader.join()
616                 _LOG.debug('ramp complete')
617             finally:
618                 #_LOG.debug('AI flags: %s' % ai_subdevice.get_flags())
619                 ai_subdevice.cancel()
620                 ai_subdevice.unlock()
621         finally:
622             # release busy flag, which seems to not be cleared
623             # automatically.  See
624             #   http://groups.google.com/group/comedi_list/browse_thread/thread/4c7040989197abad/
625             #_LOG.debug('AO flags: %s' % ao_subdevice.get_flags())
626             ao_subdevice.cancel()
627             ao_subdevice.unlock()
628             _LOG.debug('unlocked subdevices after ramp')
629
630         for i,name in enumerate(output_names):
631             self.last_output[name] = data[-1,i]
632
633         if _package_config['matplotlib']:
634             if not _matplotlib:
635                 raise _matplotlib_import_error
636             figure = _matplotlib_pyplot.figure()
637             axes = figure.add_subplot(1, 1, 1)
638             axes.hold(True)
639             timestamp = _time.strftime('%H%M%S')
640             axes.set_title('piezo ramp %s' % timestamp)
641             for d,names in [(data, output_names),
642                             (input_data, input_names)]:
643                 for i,name in enumerate(names):
644                     axes.plot(d[:,i], label=name)
645             figure.show()
646         return input_data
647
648     def named_ramp(self, data, frequency, output_names, input_names=()):
649         input_data = self.ramp(
650             data=data, frequency=frequency, output_names=output_names,
651             input_names=input_names)
652         ret = {}
653         for i,name in enumerate(output_names):
654             ret[name] = data[:,i]
655         for i,name in enumerate(input_names):
656             ret[name] = input_data[:,i]
657         return ret