94d8fe0c1d0595f7675b3aafa20dabb1b74efd49
[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             if n == name:
487                 return channel
488         raise ValueError(name)
489
490     def channel_dtype(self, channel_name, direction=None):
491         """Get a channel's data type by name.
492
493         Setting `direction` (see :meth:`channels`) may allow a more
494         efficient search.
495         """
496         channel = self.channel_by_name(name=channel_name, direction=direction)
497         return channel.subdevice.get_dtype()
498
499     def read_inputs(self):
500         "Read all inputs and return a `name`->`value` dictionary."
501         # There is no multi-channel read instruction, so preform reads
502         # sequentially.
503         ret = dict([(n, c.data_read()) for n,c in self.channels('input')])
504         _LOG.debug('current position: %s' % ret)
505         return ret
506
507     def jump(self, axis_name, position):
508         "Move the output named `axis_name` to `position`."
509         _LOG.debug('jump %s to %s' % (axis_name, position))
510         position = int(position)
511         channel = self.channel_by_name(name=axis_name)
512         channel.data_write(position)
513         self.last_output[axis_name] = position
514
515     def ramp(self, data, frequency, output_names, input_names=()):
516         """Synchronized IO ramp writing `data` and reading `in_names`.
517
518         Parameters
519         ----------
520         data : numpy array-like
521             Row for each cycle, column for each output channel.
522         frequency : float
523             Target cycle frequency in Hz.
524         output_names : list of strings
525             Names of output channels in the same order as the columns
526             of `data`.
527         input_names : list of strings
528             Names of input channels to monitor in the same order as
529             the columns of the returned array.
530         """
531         if len(data.shape) != 2:
532             raise ValueError(
533                 'ramp data must be two dimensional, not %d' % len(data.shape))
534         if data.shape[1] != len(output_names):
535             raise ValueError(
536                 'ramp data should have on column for each input, '
537                 'but has %d columns for %d inputs'
538                 % (data.shape[1], len(output_names)))
539         n_samps = data.shape[0]
540         log_string = 'ramp %d samples at %g Hz.  out: %s, in: %s' % (
541             n_samps, frequency, output_names, input_names)
542         _LOG.debug(log_string)  # _LOG on one line for easy commenting-out
543         # TODO: check range?
544         output_channels = [self.channel_by_name(name=n, direction='output')
545                            for n in output_names]
546         inputs = [self.channel_by_name(name=n, direction='input')
547                           for n in input_names]
548
549         ao_subdevice = output_channels[0].subdevice
550         ai_subdevice = inputs[0].subdevice
551         device = ao_subdevice.device
552
553         output_dtype = ao_subdevice.get_dtype()
554         if data.dtype != output_dtype:
555             raise ValueError('output dtype %s does not match expected %s'
556                              % (data.dtype, output_dtype))
557         input_data = _numpy.ndarray(
558             (n_samps, len(inputs)), dtype=ai_subdevice.get_dtype())
559
560         _LOG.debug('setup ramp commands')
561         scan_period_ns = int(1e9 / frequency)
562         ai_cmd = ai_subdevice.get_cmd_generic_timed(
563             len(inputs), scan_period_ns)
564         ao_cmd = ao_subdevice.get_cmd_generic_timed(
565             len(output_channels), scan_period_ns)
566
567         ai_cmd.start_src = TRIG_SRC.int
568         ai_cmd.start_arg = 0
569         ai_cmd.stop_src = TRIG_SRC.count
570         ai_cmd.stop_arg = n_samps
571         ai_cmd.chanlist = inputs
572         #ao_cmd.start_src = TRIG_SRC.ext
573         #ao_cmd.start_arg = 18  # NI card AI_START1 internal AI start signal
574         ao_cmd.start_src = TRIG_SRC.int
575         ao_cmd.start_arg = 0
576         ao_cmd.stop_src = TRIG_SRC.count
577         ao_cmd.stop_arg = n_samps-1
578         ao_cmd.chanlist = output_channels
579
580         ai_subdevice.cmd = ai_cmd
581         ao_subdevice.cmd = ao_cmd
582         for i in range(3):
583             rc = ai_subdevice.command_test()
584             if rc is None: break
585             _LOG.debug('analog input test %d: %s' % (i, rc))
586         for i in range(3):
587             rc = ao_subdevice.command_test()
588             if rc is None: break
589             _LOG.debug('analog output test %d: %s' % (i, rc))
590
591         _LOG.debug('lock subdevices for ramp')
592         ao_subdevice.lock()
593         try:
594             ai_subdevice.lock()
595             try:
596                 _LOG.debug('load ramp commands')
597                 ao_subdevice.command()
598                 ai_subdevice.command()
599
600                 writer = Writer(ao_subdevice, data)
601                 writer.start()
602                 reader = Reader(ai_subdevice, input_data)
603                 reader.start()
604
605                 _LOG.debug('arm analog output')
606                 device.do_insn(inttrig_insn(ao_subdevice))
607                 _LOG.debug('trigger ramp (via analog input)')
608                 device.do_insn(inttrig_insn(ai_subdevice))
609                 _LOG.debug('ramp running')
610
611                 writer.join()
612                 reader.join()
613                 _LOG.debug('ramp complete')
614             finally:
615                 #_LOG.debug('AI flags: %s' % ai_subdevice.get_flags())
616                 ai_subdevice.cancel()
617                 ai_subdevice.unlock()
618         finally:
619             # release busy flag, which seems to not be cleared
620             # automatically.  See
621             #   http://groups.google.com/group/comedi_list/browse_thread/thread/4c7040989197abad/
622             #_LOG.debug('AO flags: %s' % ao_subdevice.get_flags())
623             ao_subdevice.cancel()
624             ao_subdevice.unlock()
625             _LOG.debug('unlocked subdevices after ramp')
626
627         for i,name in enumerate(output_names):
628             self.last_output[name] = data[-1,i]
629
630         if _package_config['matplotlib']:
631             if not _matplotlib:
632                 raise _matplotlib_import_error
633             figure = _matplotlib_pyplot.figure()
634             axes = figure.add_subplot(1, 1, 1)
635             axes.hold(True)
636             timestamp = _time.strftime('%H%M%S')
637             axes.set_title('piezo ramp %s' % timestamp)
638             for d,names in [(data, output_names),
639                             (input_data, input_names)]:
640                 for i,name in enumerate(names):
641                     axes.plot(d[:,i], label=name)
642             figure.show()
643         return input_data
644
645     def named_ramp(self, data, frequency, output_names, input_names=()):
646         input_data = self.ramp(
647             data=data, frequency=frequency, output_names=output_names,
648             input_names=input_names)
649         ret = {}
650         for i,name in enumerate(output_names):
651             ret[name] = data[:,i]
652         for i,name in enumerate(input_names):
653             ret[name] = input_data[:,i]
654         return ret