Optional config-based-setup for PiezoAxis, OutputChannel, and InputChannel.
[pypiezo.git] / pypiezo / base.py
1 # Copyright (C) 2011-2012 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 under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 3 of the License, or (at your option) any later
8 # version.
9 #
10 # pypiezo is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # pypiezo.  If not, see <http://www.gnu.org/licenses/>.
16
17 """Basic piezo control.
18
19 Several of the classes defined in this module are simple wrappers for
20 combining a `pycomedi` class instance (e.g. `Channel`) with the
21 appropriate config data (e.g. `InputChannelConfig`).  The idea is that
22 the `h5config`-based config data will make it easy for you to save
23 your hardware configuration to disk (so that you have a record of what
24 you did).  It should also make it easy to load your configuration from
25 the disk (so that you can do the same thing again).  Because this
26 `h5config` <-> `pycomedi` communication works both ways, you have two
27 options when you're initializing a class:
28
29 1) On your first run, it's probably easiest to setup your channels and
30    such using the usual `pycomedi` interface
31    (`device.find_subdevice_by_type`, etc.)  After you have setup your
32    channels, you can initialize them with a stock config instance, and
33    call the `setup_config` method to copy the channel configuration
34    into the config file.  Now the config instance is ready to be saved
35    to disk.
36 2) On later runs, you have the option of loading the `pycomedi`
37    objects from your old configuration.  After loading the config data
38    from disk, initialize your class by passing in a `devices` list,
39    but without passing in the `Channel` instances.  The class will
40    take care of setting up the channel instances internally,
41    recreating your earlier setup.
42
43 For examples of how to apply either approach to a particular class,
44 see that class' docstring.
45 """
46
47 import math as _math
48 from time import sleep as _sleep
49
50 import numpy as _numpy
51 from scipy.stats import linregress as _linregress
52
53 try:
54     import matplotlib as _matplotlib
55     import matplotlib.pyplot as _matplotlib_pyplot
56     import time as _time  # for timestamping lines on plots
57 except (ImportError, RuntimeError), e:
58     _matplotlib = None
59     _matplotlib_import_error = e
60
61 from pycomedi.device import Device
62 from pycomedi.subdevice import StreamingSubdevice
63 from pycomedi.channel import AnalogChannel
64 from pycomedi.constant import AREF, TRIG_SRC, SDF, SUBDEVICE_TYPE, UNIT
65 from pycomedi.utility import inttrig_insn, Reader, Writer
66
67 from . import LOG as _LOG
68 from . import config as _config
69 from . import package_config as _package_config
70
71
72 def convert_bits_to_volts(config, data):
73     """Convert bit-valued data to volts.
74
75     >>> config = _config.ChannelConfig()
76     >>> config['conversion-coefficients'] = [1, 2, 3]
77     >>> config['conversion-origin'] = -1
78     >>> convert_bits_to_volts(config, -1)
79     1
80     >>> convert_bits_to_volts(
81     ...     config, _numpy.array([-1, 0, 1, 2], dtype=_numpy.float))
82     array([  1.,   6.,  17.,  34.])
83     """
84     coefficients = config['conversion-coefficients']
85     origin = config['conversion-origin']
86     return _numpy.polyval(list(reversed(coefficients)), data-origin)
87
88 def convert_volts_to_bits(config, data):
89     """Convert bit-valued data to volts.
90
91     >>> config = _config.ChannelConfig()
92     >>> config['inverse-conversion-coefficients'] = [1, 2, 3]
93     >>> config['inverse-conversion-origin'] = -1
94     >>> convert_volts_to_bits(config, -1)
95     1
96     >>> convert_volts_to_bits(
97     ...     config, _numpy.array([-1, 0, 1, 2], dtype=_numpy.float))
98     array([  1.,   6.,  17.,  34.])
99
100     Note that the inverse coeffiecient and offset are difficult to
101     derive from the forward coefficient and offset.  The current
102     Comedilib conversion functions, `comedi_to_physical()` and
103     `comedi_from_physical()` take `comedi_polynomial_t` conversion
104     arguments.  `comedi_polynomial_t` is defined in `comedilib.h`_,
105     and holds a polynomial of length
106     `COMEDI_MAX_NUM_POLYNOMIAL_COEFFICIENTS`, currently set to 4.  The
107     inverse of this cubic polynomial might be another polynomial, but
108     it might also have a more complicated form.  A general inversion
109     solution is considered too complicated, so when you're setting up
110     your configuration, you should use Comedilib to save both the
111     forward and inverse coefficients and offsets.
112
113     .. _comedilib.h: http://www.comedi.org/git?p=comedi/comedilib.git;a=blob;f=include/comedilib.h;hb=HEAD
114     """
115     origin = config['inverse-conversion-origin']
116     inverse_coefficients = config['inverse-conversion-coefficients']
117     if len(inverse_coefficients) == 0:
118         raise NotImplementedError('cubic polynomial inversion')
119     return _numpy.polyval(list(reversed(inverse_coefficients)), data-origin)
120
121 def convert_volts_to_meters(config, data):
122     """Convert volt-valued data to meters.
123
124     >>> config = _config.AxisConfig()
125     >>> config['gain'] = 20.0
126     >>> config['sensitivity'] = 8e-9
127     >>> convert_volts_to_meters(config, 1)
128     1.6e-07
129     >>> convert_volts_to_meters(
130     ...     config, _numpy.array([1, 6, 17, 34], dtype=_numpy.float))
131     ... # doctest: +ELLIPSIS
132     array([  1.6...e-07,   9.6...e-07,   2.7...e-06,
133              5.4...e-06])
134     """
135     return data * config['gain'] * config['sensitivity']
136
137 def convert_meters_to_volts(config, data):
138     """Convert bit-valued data to volts.
139
140     >>> config = _config.AxisConfig()
141     >>> config['gain'] = 20.0
142     >>> config['sensitivity'] = 8e-9
143     >>> convert_meters_to_volts(config, 1.6e-7)
144     1.0
145     >>> convert_meters_to_volts(
146     ...     config, _numpy.array([1.6e-7, 9.6e-7, 2.72e-6, 5.44e-6],
147     ...                          dtype=_numpy.float))
148     array([  1.,   6.,  17.,  34.])
149     """
150     return data / (config['gain'] * config['sensitivity'])
151
152 def convert_bits_to_meters(axis_config, data):
153     """Convert bit-valued data to meters.
154
155     >>> channel_config = _config.ChannelConfig()
156     >>> channel_config['conversion-coefficients'] = [1, 2, 3]
157     >>> channel_config['conversion-origin'] = -1
158     >>> axis_config = _config.AxisConfig()
159     >>> axis_config['gain'] = 20.0
160     >>> axis_config['sensitivity'] = 8e-9
161     >>> axis_config['channel'] = channel_config
162     >>> convert_bits_to_meters(axis_config, 1)
163     ... # doctest: +ELLIPSIS
164     2.7...e-06
165     >>> convert_bits_to_meters(
166     ...     axis_config, _numpy.array([-1, 0, 1, 2], dtype=_numpy.float))
167     ... # doctest: +ELLIPSIS
168     array([  1.6...e-07,   9.6...e-07,   2.7...e-06,
169              5.4...e-06])
170     """
171     data = convert_bits_to_volts(axis_config['channel'], data)
172     return convert_volts_to_meters(axis_config, data)
173
174 def convert_meters_to_bits(axis_config, data):
175     """Convert meter-valued data to volts.
176
177     >>> channel_config = _config.ChannelConfig()
178     >>> channel_config['inverse-conversion-coefficients'] = [1, 2, 3]
179     >>> channel_config['inverse-conversion-origin'] = -1
180     >>> axis_config = _config.AxisConfig()
181     >>> axis_config['gain'] = 20.0
182     >>> axis_config['sensitivity'] = 8e-9
183     >>> axis_config['channel'] = channel_config
184     >>> convert_meters_to_bits(axis_config, 1.6e-7)
185     17.0
186     >>> convert_meters_to_bits(
187     ...     axis_config,
188     ...     _numpy.array([1.6e-7, 9.6e-7, 2.72e-6, 5.44e-6],
189     ...                  dtype=_numpy.float))
190     array([   17.,   162.,  1009.,  3746.])
191     """
192     data = convert_meters_to_volts(axis_config, data)
193     return convert_volts_to_bits(axis_config['channel'], data)
194
195 def get_axis_name(axis_config):
196     """Return the name of an axis from the `AxisConfig`
197
198     This is useful, for example, with
199     `Config.select_config(get_attribute=get_axis_name)`.
200     """
201     channel_config = axis_config['channel']
202     return channel_config['name']
203
204 def _load_device(filename, devices):
205     """Return an open device from `devices` which has a given `filename`.
206
207     Sometimes a caller will already have the required `Device`, in
208     which case we just pull that instance out of `devices`, check that
209     it's open, and return it.  Other times, the caller may want us to
210     open the device ourselves, so if we can't find an appropriate
211     device in `devices`, we create a new one, append it to `devices`
212     (so the caller can close it later), and return it.
213
214     You will have to open the `Device` yourself, though, because the
215     open device instance should not be held by a particular
216     `PiezoAxis` instance.  If you don't want to open devices yourself,
217     you can pass in a blank list of devices, and the initialization
218     routine will append any necessary-but-missing devices to it.
219
220     >>> from pycomedi.device import Device
221
222     >>> devices = [Device('/dev/comedi0')]
223     >>> device = _load_device(filename='/dev/comedi0', devices=devices)
224     >>> device.filename
225     '/dev/comedi0'
226     >>> device.file is not None
227     True
228     >>> device.close()
229
230     >>> devices = []
231     >>> device = _load_device(filename='/dev/comedi0', devices=devices)
232     >>> devices == [device]
233     True
234     >>> device.filename
235     '/dev/comedi0'
236     >>> device.file is not None
237     True
238     >>> device.close()
239
240     We try and return helpful errors when things go wrong:
241
242     >>> device = _load_device(filename='/dev/comedi0', devices=None)
243     Traceback (most recent call last):
244       ...
245     TypeError: 'NoneType' object is not iterable
246     >>> device = _load_device(filename='/dev/comedi0', devices=tuple())
247     Traceback (most recent call last):
248       ...
249     ValueError: none of the available devices ([]) match /dev/comedi0, and we cannot append to ()
250     >>> device = _load_device(filename='/dev/comediX', devices=[])
251     Traceback (most recent call last):
252       ...
253     PyComediError: comedi_open (/dev/comediX): No such file or directory (None)
254     """
255     try:
256         matching_devices = [d for d in devices if d.filename == filename]
257     except TypeError:
258         _LOG.error('non-iterable devices? ({})'.format(devices))
259         raise
260     if matching_devices:
261         device = matching_devices[0]
262         if device.file is None:
263             device.open()
264     else:
265         device = Device(filename)
266         device.open()
267         try:
268             devices.append(device)  # pass new device back to caller
269         except AttributeError:
270             device.close()
271             raise ValueError(
272                 ('none of the available devices ({}) match {}, and we '
273                  'cannot append to {}').format(
274                     [d.filename for d in devices], filename, devices))
275     return device
276
277 def _load_channel_from_config(channel, devices, subdevice_type):
278     c = channel.config  # reduce verbosity
279     if not channel.channel:
280         device = _load_device(filename=c['device'], devices=devices)
281         if c['subdevice'] < 0:
282             subdevice = device.find_subdevice_by_type(
283                 subdevice_type, factory=StreamingSubdevice)
284         else:
285             subdevice = device.subdevice(
286                 index=c['subdevice'], factory=StreamingSubdevice)
287         channel.channel = subdevice.channel(
288             index=c['channel'], factory=AnalogChannel,
289             aref=c['analog-reference'])
290         channel.channel.range = channel.channel.get_range(index=c['range'])
291     channel.name = c['name']
292
293 def _setup_channel_config(config, channel):
294     """Initialize the `ChannelConfig` `config` using the
295     `AnalogChannel` `channel`.
296     """
297     config['device'] = channel.subdevice.device.filename
298     config['subdevice'] = channel.subdevice.index
299     config['channel'] = channel.index
300     config['maxdata'] = channel.get_maxdata()
301     config['range'] = channel.range.value
302     config['analog-reference'] = AREF.index_by_value(channel.aref.value)
303     converter = channel.get_converter()
304     config['conversion-origin'
305            ] = converter.get_to_physical_expansion_origin()
306     config['conversion-coefficients'
307            ] = converter.get_to_physical_coefficients()
308     config['inverse-conversion-origin'
309            ] = converter.get_from_physical_expansion_origin()
310     config['inverse-conversion-coefficients'
311            ] = converter.get_from_physical_coefficients()
312
313
314 class PiezoAxis (object):
315     """A one-dimensional piezoelectric actuator.
316
317     If used, the montoring channel must (as of now) be on the same
318     device as the controlling channel.
319
320     >>> from pycomedi.device import Device
321     >>> from pycomedi.subdevice import StreamingSubdevice
322     >>> from pycomedi.channel import AnalogChannel
323     >>> from pycomedi.constant import (AREF, SUBDEVICE_TYPE, UNIT)
324
325     >>> d = Device('/dev/comedi0')
326     >>> d.open()
327
328     >>> s_in = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
329     ...     factory=StreamingSubdevice)
330     >>> s_out = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
331     ...     factory=StreamingSubdevice)
332
333     >>> axis_channel = s_out.channel(
334     ...     0, factory=AnalogChannel, aref=AREF.ground)
335     >>> monitor_channel = s_in.channel(
336     ...     0, factory=AnalogChannel, aref=AREF.diff)
337     >>> for chan in [axis_channel, monitor_channel]:
338     ...     chan.range = chan.find_range(unit=UNIT.volt, min=-10, max=10)
339
340     >>> config = _config.AxisConfig()
341     >>> config.update({'gain':20, 'sensitivity':8e-9})
342     >>> config['channel'] = _config.OutputChannelConfig()
343     >>> config['monitor'] = _config.InputChannelConfig()
344     >>> config['monitor']['device'] = '/dev/comediX'
345
346     >>> p = PiezoAxis(config=config)
347     ... # doctest: +NORMALIZE_WHITESPACE
348     Traceback (most recent call last):
349       ...
350     NotImplementedError: piezo axis control and monitor on different devices
351      (/dev/comedi0 and /dev/comediX)
352
353     >>> config['monitor']['device'] = config['channel']['device']
354     >>> p = PiezoAxis(config=config,
355     ...     axis_channel=axis_channel, monitor_channel=monitor_channel)
356
357     >>> p.setup_config()
358     >>> print(config['channel'].dump())
359     name: 
360     device: /dev/comedi0
361     subdevice: 1
362     channel: 0
363     maxdata: 65535
364     range: 0
365     analog-reference: ground
366     conversion-coefficients: -10.0, 0.000305180437934
367     conversion-origin: 0.0
368     inverse-conversion-coefficients: 0.0, 3276.75
369     inverse-conversion-origin: -10.0
370     >>> print(config['monitor'].dump())
371     name: 
372     device: /dev/comedi0
373     subdevice: 0
374     channel: 0
375     maxdata: 65535
376     range: 0
377     analog-reference: diff
378     conversion-coefficients: -10.0, 0.000305180437934
379     conversion-origin: 0.0
380     inverse-conversion-coefficients: 0.0, 3276.75
381     inverse-conversion-origin: -10.0
382
383     >>> convert_bits_to_meters(p.config, 0)
384     ... # doctest: +ELLIPSIS
385     -1.6...e-06
386
387     Opening from the config alone:
388
389     >>> p = PiezoAxis(config=config, devices=[d])
390     >>> p.axis_channel  # doctest: +ELLIPSIS
391     <pycomedi.channel.AnalogChannel object at 0x...>
392     >>> p.monitor_channel  # doctest: +ELLIPSIS
393     <pycomedi.channel.AnalogChannel object at 0x...>
394
395     >>> d.close()
396     """
397     def __init__(self, config, axis_channel=None, monitor_channel=None,
398                  devices=None):
399         self.config = config
400         self.axis_channel = axis_channel
401         self.monitor_channel = monitor_channel
402         self.load_from_config(devices=devices)
403
404     def load_from_config(self, devices):
405         c = self.config  # reduce verbosity
406         if (c['monitor'] and
407             c['channel']['device'] != c['monitor']['device']):
408             raise NotImplementedError(
409                 ('piezo axis control and monitor on different devices '
410                  '({} and {})').format(
411                     c['channel']['device'], c['monitor']['device']))
412         if not self.axis_channel:
413             self.axis_channel = OutputChannel(
414                 config=c['channel'], devices=devices).channel
415         if c['monitor'] and not self.monitor_channel:
416             self.monitor_channel = InputChannel(
417                 config=c['monitor'], devices=devices).channel
418         self.name = c['channel']['name']
419
420     def setup_config(self):
421         "Initialize the axis (and monitor) configs."
422         _setup_channel_config(self.config['channel'], self.axis_channel)
423         if self.monitor_channel:
424             _setup_channel_config(
425                 self.config['monitor'], self.monitor_channel)
426         if self.config['minimum'] is None:
427             self.config['minimum'] = convert_bits_to_volts(
428                 self.config['channel'], 0)
429         if self.config['maximum'] is None:
430             self.config['maximum'] = convert_bits_to_volts(
431                 self.config['channel'], self.axis_channel.get_maxdata())
432
433
434
435 class OutputChannel(object):
436     """An input channel monitoring some interesting parameter.
437
438     >>> from pycomedi.device import Device
439     >>> from pycomedi.subdevice import StreamingSubdevice
440     >>> from pycomedi.channel import AnalogChannel
441     >>> from pycomedi.constant import AREF, SUBDEVICE_TYPE, UNIT
442
443     >>> d = Device('/dev/comedi0')
444     >>> d.open()
445
446     >>> s = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
447     ...     factory=StreamingSubdevice)
448
449     >>> channel = s.channel(0, factory=AnalogChannel, aref=AREF.diff)
450     >>> channel.range = channel.find_range(unit=UNIT.volt, min=-10, max=10)
451
452     >>> channel_config = _config.OutputChannelConfig()
453
454     >>> c = OutputChannel(config=channel_config, channel=channel)
455     >>> c.setup_config()
456     >>> print(channel_config.dump())
457     name: 
458     device: /dev/comedi0
459     subdevice: 1
460     channel: 0
461     maxdata: 65535
462     range: 0
463     analog-reference: diff
464     conversion-coefficients: -10.0, 0.000305180437934
465     conversion-origin: 0.0
466     inverse-conversion-coefficients: 0.0, 3276.75
467     inverse-conversion-origin: -10.0
468
469     >>> convert_volts_to_bits(c.config, -10)
470     0.0
471
472     Opening from the config alone:
473
474     >>> c = OutputChannel(config=channel_config, devices=[d])
475     >>> c.channel  # doctest: +ELLIPSIS
476     <pycomedi.channel.AnalogChannel object at 0x...>
477
478     >>> d.close()
479     """
480     def __init__(self, config, channel=None, devices=None):
481         self.config = config
482         self.channel = channel
483         self.load_from_config(devices=devices)
484
485     def load_from_config(self, devices):
486         _load_channel_from_config(
487             channel=self, devices=devices, subdevice_type=SUBDEVICE_TYPE.ao)
488
489     def setup_config(self):
490         _setup_channel_config(self.config, self.channel)
491
492
493 class InputChannel(object):
494     """An input channel monitoring some interesting parameter.
495
496     >>> from pycomedi.device import Device
497     >>> from pycomedi.subdevice import StreamingSubdevice
498     >>> from pycomedi.channel import AnalogChannel
499     >>> from pycomedi.constant import AREF, SUBDEVICE_TYPE, UNIT
500
501     >>> d = Device('/dev/comedi0')
502     >>> d.open()
503
504     >>> s = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
505     ...     factory=StreamingSubdevice)
506
507     >>> channel = s.channel(0, factory=AnalogChannel, aref=AREF.diff)
508     >>> channel.range = channel.find_range(unit=UNIT.volt, min=-10, max=10)
509
510     >>> channel_config = _config.InputChannelConfig()
511
512     >>> c = InputChannel(config=channel_config, channel=channel)
513     >>> c.setup_config()
514     >>> print(channel_config.dump())
515     name: 
516     device: /dev/comedi0
517     subdevice: 0
518     channel: 0
519     maxdata: 65535
520     range: 0
521     analog-reference: diff
522     conversion-coefficients: -10.0, 0.000305180437934
523     conversion-origin: 0.0
524     inverse-conversion-coefficients: 0.0, 3276.75
525     inverse-conversion-origin: -10.0
526
527     >>> convert_bits_to_volts(c.config, 0)
528     -10.0
529
530     Opening from the config alone:
531
532     >>> c = InputChannel(config=channel_config, devices=[d])
533     >>> c.channel  # doctest: +ELLIPSIS
534     <pycomedi.channel.AnalogChannel object at 0x...>
535
536     >>> d.close()
537     """
538     def __init__(self, config, channel=None, devices=None):
539         self.config = config
540         self.channel = channel
541         self.load_from_config(devices=devices)
542
543     def load_from_config(self, devices):
544         _load_channel_from_config(
545             channel=self, devices=devices, subdevice_type=SUBDEVICE_TYPE.ai)
546
547     def setup_config(self):
548         _setup_channel_config(self.config, self.channel)
549
550
551 class Piezo (object):
552     """A piezo actuator-controlled experiment.
553
554     >>> from pprint import pprint
555     >>> from pycomedi.device import Device
556     >>> from pycomedi.subdevice import StreamingSubdevice
557     >>> from pycomedi.channel import AnalogChannel
558     >>> from pycomedi.constant import AREF, SUBDEVICE_TYPE, UNIT
559
560     >>> d = Device('/dev/comedi0')
561     >>> d.open()
562
563     >>> s_in = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
564     ...     factory=StreamingSubdevice)
565     >>> s_out = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
566     ...     factory=StreamingSubdevice)
567
568     >>> axis_channel = s_out.channel(
569     ...     0, factory=AnalogChannel, aref=AREF.ground)
570     >>> monitor_channel = s_in.channel(
571     ...     0, factory=AnalogChannel, aref=AREF.diff)
572     >>> input_channel = s_in.channel(1, factory=AnalogChannel, aref=AREF.diff)
573     >>> for chan in [axis_channel, monitor_channel, input_channel]:
574     ...     chan.range = chan.find_range(unit=UNIT.volt, min=-10, max=10)
575
576     >>> axis_config = _config.AxisConfig()
577     >>> axis_config.update({'gain':20, 'sensitivity':8e-9})
578     >>> axis_config['channel'] = _config.OutputChannelConfig()
579     >>> axis_config['channel']['name'] = 'z'
580     >>> axis_config['monitor'] = _config.InputChannelConfig()
581     >>> input_config = _config.InputChannelConfig()
582     >>> input_config['name'] = 'some-input'
583
584     >>> a = PiezoAxis(config=axis_config, axis_channel=axis_channel,
585     ...     monitor_channel=monitor_channel)
586     >>> a.setup_config()
587
588     >>> c = InputChannel(config=input_config, channel=input_channel)
589     >>> c.setup_config()
590
591     >>> p = Piezo(axes=[a], inputs=[c], name='Charlie')
592     >>> inputs = p.read_inputs()
593     >>> pprint(inputs)  # doctest: +SKIP
594     {'some-input': 34494L, 'z-monitor': 32669L}
595
596     >>> pos = convert_volts_to_bits(p.config['axes'][0]['channel'], 0)
597     >>> pos
598     32767.5
599     >>> p.jump('z', pos)
600     >>> p.last_output == {'z': int(pos)}
601     True
602
603     :meth:`ramp` raises an error if passed an invalid `data` `dtype`.
604
605     >>> output_data = _numpy.arange(0, int(pos), step=int(pos/10),
606     ...     dtype=_numpy.float)
607     >>> output_data = output_data.reshape((len(output_data), 1))
608     >>> input_data = p.ramp(data=output_data, frequency=10,
609     ...     output_names=['z'], input_names=['z-monitor', 'some-input'])
610     Traceback (most recent call last):
611       ...
612     ValueError: output dtype float64 does not match expected <type 'numpy.uint16'>
613     >>> output_data = _numpy.arange(0, int(pos), step=int(pos/10),
614     ...     dtype=p.channel_dtype('z', direction='output'))
615     >>> output_data = output_data.reshape((len(output_data), 1))
616     >>> input_data = p.ramp(data=output_data, frequency=10,
617     ...     output_names=['z'], input_names=['z-monitor', 'some-input'])
618     >>> input_data  # doctest: +SKIP
619     array([[    0, 25219],
620            [ 3101, 23553],
621            [ 6384, 22341],
622            [ 9664, 21465],
623            [12949, 20896],
624            [16232, 20614],
625            [19516, 20588],
626            [22799, 20801],
627            [26081, 21233],
628            [29366, 21870],
629            [32646, 22686]], dtype=uint16)
630
631     >>> p.last_output == {'z': output_data[-1]}
632     True
633
634     >>> data = p.named_ramp(data=output_data, frequency=10,
635     ...     output_names=['z'], input_names=['z-monitor', 'some-input'])
636     >>> pprint(data)  # doctest: +ELLIPSIS, +SKIP
637     {'some-input': array([21666, 20566, ..., 22395], dtype=uint16),
638      'z': array([    0,  3276,  ..., 32760], dtype=uint16),
639      'z-monitor': array([ 3102,  6384,  ..., 32647], dtype=uint16)}
640
641     >>> d.close()
642     """
643     def __init__(self, axes, inputs, name=None):
644         self.axes = axes
645         self.inputs = inputs
646         self.config = _config.PiezoConfig()
647         self.name = name
648         self.config['name'] = name
649         self.config['axes'] = [x.config for x in axes]
650         self.config['inputs'] = [x.config for x in inputs]
651         self.last_output = {}
652
653     def axis_by_name(self, name):
654         "Get an axis by its name."
655         for axis in self.axes:
656             if axis.name == name:
657                 return axis
658         raise ValueError(name)
659
660     def input_channel_by_name(self, name):
661         "Get an input channel by its name."
662         for input_channel in self.inputs:
663             if input_channel.name == name:
664                 return input_channel
665         raise ValueError(name)
666
667     def channels(self, direction=None):
668         """Iterate through all `(name, channel)` tuples.
669
670         ===========  ===================
671         `direction`  Returned channels
672         ===========  ===================
673         'input'      all input channels
674         'output'     all output channels
675         None         all channels
676         ===========  ===================
677         """
678         if direction not in ('input', 'output', None):
679             raise ValueError(direction)
680         for a in self.axes:
681             if direction != 'input':
682                 yield (a.name, a.axis_channel)
683             if a.monitor_channel and direction != 'output':
684                 yield ('%s-monitor' % a.name, a.monitor_channel)
685         if direction != 'output':
686             for c in self.inputs:
687                 yield (c.name, c.channel)
688
689     def channel_by_name(self, name, direction=None):
690         """Get a channel by its name.
691
692         Setting `direction` (see :meth:`channels`) may allow a more
693         efficient search.
694         """
695         for n,channel in self.channels(direction=direction):
696             if n == name:
697                 return channel
698         raise ValueError(name)
699
700     def channel_dtype(self, channel_name, direction=None):
701         """Get a channel's data type by name.
702
703         Setting `direction` (see :meth:`channels`) may allow a more
704         efficient search.
705         """
706         channel = self.channel_by_name(name=channel_name, direction=direction)
707         return channel.subdevice.get_dtype()
708
709     def read_inputs(self):
710         "Read all inputs and return a `name`->`value` dictionary."
711         # There is no multi-channel read instruction, so preform reads
712         # sequentially.
713         ret = dict([(n, c.data_read())
714                     for n,c in self.channels(direction='input')])
715         _LOG.debug('current position: %s' % ret)
716         return ret
717
718     def jump(self, axis_name, position, steps=1, sleep=None):
719         "Move the output named `axis_name` to `position`."
720         _LOG.debug('jump %s to %s in %d steps' % (axis_name, position, steps))
721         if steps > 1:
722             try:
723                 orig_pos = self.last_output[axis_name]
724             except KeyError, e:
725                 _LOG.warn(
726                     ("cannot make a soft jump to {} because we don't have a "
727                      'last-output position for {}').format(
728                         position, axis_name))
729                 steps = 1
730             else:
731                 for pos in _numpy.linspace(orig_pos, position, steps+1)[1:]:
732                     self.jump(axis_name=axis_name, position=pos)
733                     if sleep:
734                         _sleep(sleep)
735                 return
736         position = int(position)
737         channel = self.channel_by_name(name=axis_name)
738         channel.data_write(position)
739         self.last_output[axis_name] = position
740
741     def ramp(self, data, frequency, output_names, input_names=()):
742         """Synchronized IO ramp writing `data` and reading `in_names`.
743
744         Parameters
745         ----------
746         data : numpy array-like
747             Row for each cycle, column for each output channel.
748         frequency : float
749             Target cycle frequency in Hz.
750         output_names : list of strings
751             Names of output channels in the same order as the columns
752             of `data`.
753         input_names : list of strings
754             Names of input channels to monitor in the same order as
755             the columns of the returned array.
756         """
757         if len(data.shape) != 2:
758             raise ValueError(
759                 'ramp data must be two dimensional, not %d' % len(data.shape))
760         if data.shape[1] != len(output_names):
761             raise ValueError(
762                 'ramp data should have on column for each input, '
763                 'but has %d columns for %d inputs'
764                 % (data.shape[1], len(output_names)))
765         n_samps = data.shape[0]
766         log_string = 'ramp %d samples at %g Hz.  out: %s, in: %s' % (
767             n_samps, frequency, output_names, input_names)
768         _LOG.debug(log_string)  # _LOG on one line for easy commenting-out
769         # TODO: check range?
770         output_channels = [self.channel_by_name(name=n, direction='output')
771                            for n in output_names]
772         inputs = [self.channel_by_name(name=n, direction='input')
773                           for n in input_names]
774
775         ao_subdevice = output_channels[0].subdevice
776         ai_subdevice = inputs[0].subdevice
777         device = ao_subdevice.device
778
779         output_dtype = ao_subdevice.get_dtype()
780         if data.dtype != output_dtype:
781             raise ValueError('output dtype %s does not match expected %s'
782                              % (data.dtype, output_dtype))
783         input_data = _numpy.ndarray(
784             (n_samps, len(inputs)), dtype=ai_subdevice.get_dtype())
785
786         _LOG.debug('setup ramp commands')
787         scan_period_ns = int(1e9 / frequency)
788         ai_cmd = ai_subdevice.get_cmd_generic_timed(
789             len(inputs), scan_period_ns)
790         ao_cmd = ao_subdevice.get_cmd_generic_timed(
791             len(output_channels), scan_period_ns)
792
793         ai_cmd.start_src = TRIG_SRC.int
794         ai_cmd.start_arg = 0
795         ai_cmd.stop_src = TRIG_SRC.count
796         ai_cmd.stop_arg = n_samps
797         ai_cmd.chanlist = inputs
798         #ao_cmd.start_src = TRIG_SRC.ext
799         #ao_cmd.start_arg = 18  # NI card AI_START1 internal AI start signal
800         ao_cmd.start_src = TRIG_SRC.int
801         ao_cmd.start_arg = 0
802         ao_cmd.stop_src = TRIG_SRC.count
803         ao_cmd.stop_arg = n_samps-1
804         ao_cmd.chanlist = output_channels
805
806         ai_subdevice.cmd = ai_cmd
807         ao_subdevice.cmd = ao_cmd
808         for i in range(3):
809             rc = ai_subdevice.command_test()
810             if rc is None: break
811             _LOG.debug('analog input test %d: %s' % (i, rc))
812         for i in range(3):
813             rc = ao_subdevice.command_test()
814             if rc is None: break
815             _LOG.debug('analog output test %d: %s' % (i, rc))
816
817         _LOG.debug('lock subdevices for ramp')
818         ao_subdevice.lock()
819         try:
820             ai_subdevice.lock()
821             try:
822                 _LOG.debug('load ramp commands')
823                 ao_subdevice.command()
824                 ai_subdevice.command()
825
826                 writer = Writer(ao_subdevice, data)
827                 writer.start()
828                 reader = Reader(ai_subdevice, input_data)
829                 reader.start()
830
831                 _LOG.debug('arm analog output')
832                 device.do_insn(inttrig_insn(ao_subdevice))
833                 _LOG.debug('trigger ramp (via analog input)')
834                 device.do_insn(inttrig_insn(ai_subdevice))
835                 _LOG.debug('ramp running')
836
837                 writer.join()
838                 reader.join()
839                 _LOG.debug('ramp complete')
840             finally:
841                 #_LOG.debug('AI flags: %s' % ai_subdevice.get_flags())
842                 ai_subdevice.cancel()
843                 ai_subdevice.unlock()
844         finally:
845             # release busy flag, which seems to not be cleared
846             # automatically.  See
847             #   http://groups.google.com/group/comedi_list/browse_thread/thread/4c7040989197abad/
848             #_LOG.debug('AO flags: %s' % ao_subdevice.get_flags())
849             ao_subdevice.cancel()
850             ao_subdevice.unlock()
851             _LOG.debug('unlocked subdevices after ramp')
852
853         for i,name in enumerate(output_names):
854             self.last_output[name] = data[-1,i]
855
856         if _package_config['matplotlib']:
857             if not _matplotlib:
858                 raise _matplotlib_import_error
859             figure = _matplotlib_pyplot.figure()
860             axes = figure.add_subplot(1, 1, 1)
861             axes.hold(True)
862             timestamp = _time.strftime('%H%M%S')
863             axes.set_title('piezo ramp %s' % timestamp)
864             for d,names in [(data, output_names),
865                             (input_data, input_names)]:
866                 for i,name in enumerate(names):
867                     axes.plot(d[:,i], label=name)
868             figure.show()
869         return input_data
870
871     def named_ramp(self, data, frequency, output_names, input_names=()):
872         input_data = self.ramp(
873             data=data, frequency=frequency, output_names=output_names,
874             input_names=input_names)
875         ret = {}
876         for i,name in enumerate(output_names):
877             ret[name] = data[:,i]
878         for i,name in enumerate(input_names):
879             ret[name] = input_data[:,i]
880         return ret