`name` is a ChannelConfig setting, not an AxisConfig setting.
[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     {'channel': 0,
231      'conversion-coefficients': array([ -1.00000000e+01,   3.05180438e-04]),
232      'conversion-origin': 0.0,
233      'device': '/dev/comedi0',
234      'inverse-conversion-coefficients': array([    0.  ,  3276.75]),
235      'inverse-conversion-origin': -10.0,
236      'maxdata': 65535L,
237      'range': 0,
238      'subdevice': 1}
239     >>> print(config['monitor'].dump())
240     {'channel': 0,
241      'conversion-coefficients': array([ -1.00000000e+01,   3.05180438e-04]),
242      'conversion-origin': 0.0,
243      'device': '/dev/comedi0',
244      'inverse-conversion-coefficients': array([    0.  ,  3276.75]),
245      'inverse-conversion-origin': -10.0,
246      'maxdata': 65535L,
247      'range': 0,
248      'subdevice': 0}
249
250     >>> convert_bits_to_meters(p.config, 0)
251     ... # doctest: +ELLIPSIS
252     -1.6...e-06
253
254     >>> d.close()
255     """
256     def __init__(self, config, axis_channel=None, monitor_channel=None):
257         self.config = config
258         if (config['monitor'] and
259             config['channel']['device'] != config['monitor']['device']):
260             raise NotImplementedError(
261                 ('piezo axis control and monitor on different devices '
262                  '(%s and %s)') % (
263                     config['channel']['device'],
264                     config['monitor']['device']))
265         if not axis_channel:
266             raise NotImplementedError(
267                 'pypiezo not yet capable of opening its own axis channel')
268             #axis_channel = pycomedi...
269         self.axis_channel = axis_channel
270         if config['monitor'] and not monitor_channel:
271             raise NotImplementedError(
272                 'pypiezo not yet capable of opening its own monitor channel')
273             #monitor_channel = pycomedi...
274         self.monitor_channel = monitor_channel
275         self.name = config['channel']['name']
276
277     def setup_config(self):
278         "Initialize the axis (and monitor) configs."
279         _setup_channel_config(self.config['channel'], self.axis_channel)
280         if self.monitor_channel:
281             _setup_channel_config(
282                 self.config['monitor'], self.monitor_channel)
283         if self.config['minimum'] is None:
284             self.config['minimum'] = convert_bits_to_volts(
285                 self.config['channel'], 0)
286         if self.config['maximum'] is None:
287             self.config['maximum'] = convert_bits_to_volts(
288                 self.config['channel'], self.axis_channel.get_maxdata())
289
290
291 class InputChannel(object):
292     """An input channel monitoring some interesting parameter.
293
294     >>> from pycomedi.device import Device
295     >>> from pycomedi.subdevice import StreamingSubdevice
296     >>> from pycomedi.channel import AnalogChannel
297     >>> from pycomedi.constant import AREF, SUBDEVICE_TYPE, UNIT
298
299     >>> d = Device('/dev/comedi0')
300     >>> d.open()
301
302     >>> s = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
303     ...     factory=StreamingSubdevice)
304
305     >>> channel = s.channel(0, factory=AnalogChannel, aref=AREF.diff)
306     >>> channel.range = channel.find_range(unit=UNIT.volt, min=-10, max=10)
307
308     >>> channel_config = _config.InputChannelConfig()
309
310     >>> c = InputChannel(config=channel_config, channel=channel)
311     >>> c.setup_config()
312     >>> print(channel_config.dump())
313     {'channel': 0,
314      'conversion-coefficients': array([ -1.00000000e+01,   3.05180438e-04]),
315      'conversion-origin': 0.0,
316      'device': '/dev/comedi0',
317      'inverse-conversion-coefficients': array([    0.  ,  3276.75]),
318      'inverse-conversion-origin': -10.0,
319      'maxdata': 65535L,
320      'range': 0,
321      'subdevice': 0}
322
323     >>> convert_bits_to_volts(c.config, 0)
324     -10.0
325
326     >>> d.close()
327     """
328     def __init__(self, config, channel=None):
329         self.config = config
330         if not channel:
331             raise NotImplementedError(
332                 'pypiezo not yet capable of opening its own channel')
333             #channel = pycomedi...
334         self.channel = channel
335         self.name = config['name']
336
337     def setup_config(self):
338         _setup_channel_config(self.config, self.channel)
339
340
341 class Piezo (object):
342     """A piezo actuator-controlled experiment.
343
344     >>> from pprint import pprint
345     >>> from pycomedi.device import Device
346     >>> from pycomedi.subdevice import StreamingSubdevice
347     >>> from pycomedi.channel import AnalogChannel
348     >>> from pycomedi.constant import AREF, SUBDEVICE_TYPE, UNIT
349
350     >>> d = Device('/dev/comedi0')
351     >>> d.open()
352
353     >>> s_in = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
354     ...     factory=StreamingSubdevice)
355     >>> s_out = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
356     ...     factory=StreamingSubdevice)
357
358     >>> axis_channel = s_out.channel(
359     ...     0, factory=AnalogChannel, aref=AREF.ground)
360     >>> monitor_channel = s_in.channel(
361     ...     0, factory=AnalogChannel, aref=AREF.diff)
362     >>> input_channel = s_in.channel(1, factory=AnalogChannel, aref=AREF.diff)
363     >>> for chan in [axis_channel, monitor_channel, input_channel]:
364     ...     chan.range = chan.find_range(unit=UNIT.volt, min=-10, max=10)
365
366     >>> axis_config = _config.AxisConfig()
367     >>> axis_config.update({'gain':20, 'sensitivity':8e-9})
368     >>> axis_config['channel'] = _config.OutputChannelConfig()
369     >>> axis_config['channel']['name'] = 'z'
370     >>> axis_config['monitor'] = _config.InputChannelConfig()
371     >>> input_config = _config.InputChannelConfig()
372     >>> input_config['name'] = 'some-input'
373
374     >>> a = PiezoAxis(config=axis_config, axis_channel=axis_channel,
375     ...     monitor_channel=monitor_channel)
376     >>> a.setup_config()
377
378     >>> c = InputChannel(config=input_config, channel=input_channel)
379     >>> c.setup_config()
380
381     >>> p = Piezo(axes=[a], inputs=[c], name='Charlie')
382     >>> inputs = p.read_inputs()
383     >>> pprint(inputs)  # doctest: +SKIP
384     {'some-input': 34494L, 'z-monitor': 32669L}
385
386     >>> pos = convert_volts_to_bits(p.config['axes'][0]['channel'], 0)
387     >>> pos
388     32767.5
389     >>> p.jump('z', pos)
390     >>> p.last_output == {'z': int(pos)}
391     True
392
393     :meth:`ramp` raises an error if passed an invalid `data` `dtype`.
394
395     >>> output_data = _numpy.arange(0, int(pos), step=int(pos/10),
396     ...     dtype=_numpy.float)
397     >>> output_data = output_data.reshape((len(output_data), 1))
398     >>> input_data = p.ramp(data=output_data, frequency=10,
399     ...     output_names=['z'], input_names=['z-monitor', 'some-input'])
400     Traceback (most recent call last):
401       ...
402     ValueError: output dtype float64 does not match expected <type 'numpy.uint16'>
403     >>> output_data = _numpy.arange(0, int(pos), step=int(pos/10),
404     ...     dtype=p.channel_dtype('z', direction='output'))
405     >>> output_data = output_data.reshape((len(output_data), 1))
406     >>> input_data = p.ramp(data=output_data, frequency=10,
407     ...     output_names=['z'], input_names=['z-monitor', 'some-input'])
408     >>> input_data  # doctest: +SKIP
409     array([[    0, 25219],
410            [ 3101, 23553],
411            [ 6384, 22341],
412            [ 9664, 21465],
413            [12949, 20896],
414            [16232, 20614],
415            [19516, 20588],
416            [22799, 20801],
417            [26081, 21233],
418            [29366, 21870],
419            [32646, 22686]], dtype=uint16)
420
421     >>> p.last_output == {'z': output_data[-1]}
422     True
423
424     >>> data = p.named_ramp(data=output_data, frequency=10,
425     ...     output_names=['z'], input_names=['z-monitor', 'some-input'])
426     >>> pprint(data)  # doctest: +ELLIPSIS, +SKIP
427     {'some-input': array([21666, 20566, ..., 22395], dtype=uint16),
428      'z': array([    0,  3276,  ..., 32760], dtype=uint16),
429      'z-monitor': array([ 3102,  6384,  ..., 32647], dtype=uint16)}
430
431     >>> d.close()
432     """
433     def __init__(self, axes, inputs, name=None):
434         self.axes = axes
435         self.inputs = inputs
436         self.config = _config.PiezoConfig()
437         self.name = name
438         self.config['name'] = name
439         self.config['axes'] = [x.config for x in axes]
440         self.config['inputs'] = [x.config for x in inputs]
441         self.last_output = {}
442
443     def axis_by_name(self, name):
444         "Get an axis by its name."
445         for axis in self.axes:
446             if axis.name == name:
447                 return axis
448         raise ValueError(name)
449
450     def input_channel_by_name(self, name):
451         "Get an input channel by its name."
452         for input_channel in self.inputs:
453             if input_channel.name == name:
454                 return input_channel
455         raise ValueError(name)
456
457     def channels(self, direction=None):
458         """Iterate through all `(name, channel)` tuples.
459
460         ===========  ===================
461         `direction`  Returned channels
462         ===========  ===================
463         'input'      all input channels
464         'output'     all output channels
465         None         all channels
466         ===========  ===================
467         """
468         if direction not in ('input', 'output', None):
469             raise ValueError(direction)
470         for a in self.axes:
471             if direction != 'input':
472                 yield (a.name, a.axis_channel)
473             if a.monitor_channel and direction != 'output':
474                 yield ('%s-monitor' % a.name, a.monitor_channel)
475         if direction != 'output':
476             for c in self.inputs:
477                 yield (c.name, c.channel)
478
479     def channel_by_name(self, name, direction=None):
480         """Get a channel by its name.
481
482         Setting `direction` (see :meth:`channels`) may allow a more
483         efficient search.
484         """
485         for n,channel in self.channels(direction=direction):
486             _LOG.critical('channel name: {}'.format(repr(n)))
487             if n == name:
488                 return channel
489         raise ValueError(name)
490
491     def channel_dtype(self, channel_name, direction=None):
492         """Get a channel's data type by name.
493
494         Setting `direction` (see :meth:`channels`) may allow a more
495         efficient search.
496         """
497         channel = self.channel_by_name(name=channel_name, direction=direction)
498         return channel.subdevice.get_dtype()
499
500     def read_inputs(self):
501         "Read all inputs and return a `name`->`value` dictionary."
502         # There is no multi-channel read instruction, so preform reads
503         # sequentially.
504         ret = dict([(n, c.data_read()) for n,c in self.channels('input')])
505         _LOG.debug('current position: %s' % ret)
506         return ret
507
508     def jump(self, axis_name, position):
509         "Move the output named `axis_name` to `position`."
510         _LOG.debug('jump %s to %s' % (axis_name, position))
511         position = int(position)
512         channel = self.channel_by_name(name=axis_name)
513         channel.data_write(position)
514         self.last_output[axis_name] = position
515
516     def ramp(self, data, frequency, output_names, input_names=()):
517         """Synchronized IO ramp writing `data` and reading `in_names`.
518
519         Parameters
520         ----------
521         data : numpy array-like
522             Row for each cycle, column for each output channel.
523         frequency : float
524             Target cycle frequency in Hz.
525         output_names : list of strings
526             Names of output channels in the same order as the columns
527             of `data`.
528         input_names : list of strings
529             Names of input channels to monitor in the same order as
530             the columns of the returned array.
531         """
532         if len(data.shape) != 2:
533             raise ValueError(
534                 'ramp data must be two dimensional, not %d' % len(data.shape))
535         if data.shape[1] != len(output_names):
536             raise ValueError(
537                 'ramp data should have on column for each input, '
538                 'but has %d columns for %d inputs'
539                 % (data.shape[1], len(output_names)))
540         n_samps = data.shape[0]
541         log_string = 'ramp %d samples at %g Hz.  out: %s, in: %s' % (
542             n_samps, frequency, output_names, input_names)
543         _LOG.debug(log_string)  # _LOG on one line for easy commenting-out
544         # TODO: check range?
545         output_channels = [self.channel_by_name(name=n, direction='output')
546                            for n in output_names]
547         inputs = [self.channel_by_name(name=n, direction='input')
548                           for n in input_names]
549
550         ao_subdevice = output_channels[0].subdevice
551         ai_subdevice = inputs[0].subdevice
552         device = ao_subdevice.device
553
554         output_dtype = ao_subdevice.get_dtype()
555         if data.dtype != output_dtype:
556             raise ValueError('output dtype %s does not match expected %s'
557                              % (data.dtype, output_dtype))
558         input_data = _numpy.ndarray(
559             (n_samps, len(inputs)), dtype=ai_subdevice.get_dtype())
560
561         _LOG.debug('setup ramp commands')
562         scan_period_ns = int(1e9 / frequency)
563         ai_cmd = ai_subdevice.get_cmd_generic_timed(
564             len(inputs), scan_period_ns)
565         ao_cmd = ao_subdevice.get_cmd_generic_timed(
566             len(output_channels), scan_period_ns)
567
568         ai_cmd.start_src = TRIG_SRC.int
569         ai_cmd.start_arg = 0
570         ai_cmd.stop_src = TRIG_SRC.count
571         ai_cmd.stop_arg = n_samps
572         ai_cmd.chanlist = inputs
573         #ao_cmd.start_src = TRIG_SRC.ext
574         #ao_cmd.start_arg = 18  # NI card AI_START1 internal AI start signal
575         ao_cmd.start_src = TRIG_SRC.int
576         ao_cmd.start_arg = 0
577         ao_cmd.stop_src = TRIG_SRC.count
578         ao_cmd.stop_arg = n_samps-1
579         ao_cmd.chanlist = output_channels
580
581         ai_subdevice.cmd = ai_cmd
582         ao_subdevice.cmd = ao_cmd
583         for i in range(3):
584             rc = ai_subdevice.command_test()
585             if rc is None: break
586             _LOG.debug('analog input test %d: %s' % (i, rc))
587         for i in range(3):
588             rc = ao_subdevice.command_test()
589             if rc is None: break
590             _LOG.debug('analog output test %d: %s' % (i, rc))
591
592         _LOG.debug('lock subdevices for ramp')
593         ao_subdevice.lock()
594         try:
595             ai_subdevice.lock()
596             try:
597                 _LOG.debug('load ramp commands')
598                 ao_subdevice.command()
599                 ai_subdevice.command()
600
601                 writer = Writer(ao_subdevice, data)
602                 writer.start()
603                 reader = Reader(ai_subdevice, input_data)
604                 reader.start()
605
606                 _LOG.debug('arm analog output')
607                 device.do_insn(inttrig_insn(ao_subdevice))
608                 _LOG.debug('trigger ramp (via analog input)')
609                 device.do_insn(inttrig_insn(ai_subdevice))
610                 _LOG.debug('ramp running')
611
612                 writer.join()
613                 reader.join()
614                 _LOG.debug('ramp complete')
615             finally:
616                 #_LOG.debug('AI flags: %s' % ai_subdevice.get_flags())
617                 ai_subdevice.cancel()
618                 ai_subdevice.unlock()
619         finally:
620             # release busy flag, which seems to not be cleared
621             # automatically.  See
622             #   http://groups.google.com/group/comedi_list/browse_thread/thread/4c7040989197abad/
623             #_LOG.debug('AO flags: %s' % ao_subdevice.get_flags())
624             ao_subdevice.cancel()
625             ao_subdevice.unlock()
626             _LOG.debug('unlocked subdevices after ramp')
627
628         for i,name in enumerate(output_names):
629             self.last_output[name] = data[-1,i]
630
631         if _package_config['matplotlib']:
632             if not _matplotlib:
633                 raise _matplotlib_import_error
634             figure = _matplotlib_pyplot.figure()
635             axes = figure.add_subplot(1, 1, 1)
636             axes.hold(True)
637             timestamp = _time.strftime('%H%M%S')
638             axes.set_title('piezo ramp %s' % timestamp)
639             for d,names in [(data, output_names),
640                             (input_data, input_names)]:
641                 for i,name in enumerate(names):
642                     axes.plot(d[:,i], label=name)
643             figure.show()
644         return input_data
645
646     def named_ramp(self, data, frequency, output_names, input_names=()):
647         input_data = self.ramp(
648             data=data, frequency=frequency, output_names=output_names,
649             input_names=input_names)
650         ret = {}
651         for i,name in enumerate(output_names):
652             ret[name] = data[:,i]
653         for i,name in enumerate(input_names):
654             ret[name] = input_data[:,i]
655         return ret