1 # Copyright (C) 2011-2012 W. Trevor King <wking@tremily.us>
3 # This file is part of pypiezo.
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
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.
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/>.
17 """Basic piezo control.
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:
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
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.
43 For examples of how to apply either approach to a particular class,
44 see that class' docstring.
48 from time import sleep as _sleep
50 import numpy as _numpy
51 from scipy.stats import linregress as _linregress
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:
59 _matplotlib_import_error = e
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
67 from . import LOG as _LOG
68 from . import config as _config
69 from . import package_config as _package_config
72 def convert_bits_to_volts(config, data):
73 """Convert bit-valued data to volts.
75 >>> config = _config.ChannelConfig()
76 >>> config['conversion-coefficients'] = [1, 2, 3]
77 >>> config['conversion-origin'] = -1
78 >>> convert_bits_to_volts(config, -1)
80 >>> convert_bits_to_volts(
81 ... config, _numpy.array([-1, 0, 1, 2], dtype=_numpy.float))
82 array([ 1., 6., 17., 34.])
84 coefficients = config['conversion-coefficients']
85 origin = config['conversion-origin']
86 return _numpy.polyval(list(reversed(coefficients)), data-origin)
88 def convert_volts_to_bits(config, data):
89 """Convert bit-valued data to volts.
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)
96 >>> convert_volts_to_bits(
97 ... config, _numpy.array([-1, 0, 1, 2], dtype=_numpy.float))
98 array([ 1., 6., 17., 34.])
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.
113 .. _comedilib.h: http://www.comedi.org/git?p=comedi/comedilib.git;a=blob;f=include/comedilib.h;hb=HEAD
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)
121 def convert_volts_to_meters(config, data):
122 """Convert volt-valued data to meters.
124 >>> config = _config.AxisConfig()
125 >>> config['gain'] = 20.0
126 >>> config['sensitivity'] = 8e-9
127 >>> convert_volts_to_meters(config, 1)
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,
135 return data * config['gain'] * config['sensitivity']
137 def convert_meters_to_volts(config, data):
138 """Convert bit-valued data to volts.
140 >>> config = _config.AxisConfig()
141 >>> config['gain'] = 20.0
142 >>> config['sensitivity'] = 8e-9
143 >>> convert_meters_to_volts(config, 1.6e-7)
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.])
150 return data / (config['gain'] * config['sensitivity'])
152 def convert_bits_to_meters(axis_config, data):
153 """Convert bit-valued data to meters.
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
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,
171 data = convert_bits_to_volts(axis_config['channel'], data)
172 return convert_volts_to_meters(axis_config, data)
174 def convert_meters_to_bits(axis_config, data):
175 """Convert meter-valued data to volts.
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)
186 >>> convert_meters_to_bits(
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.])
192 data = convert_meters_to_volts(axis_config, data)
193 return convert_volts_to_bits(axis_config['channel'], data)
195 def get_axis_name(axis_config):
196 """Return the name of an axis from the `AxisConfig`
198 This is useful, for example, with
199 `Config.select_config(get_attribute=get_axis_name)`.
201 channel_config = axis_config['channel']
202 return channel_config['name']
204 def load_device(filename, devices):
205 """Return an open device from `devices` which has a given `filename`.
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.
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.
220 >>> from pycomedi.device import Device
222 >>> devices = [Device('/dev/comedi0')]
223 >>> device = load_device(filename='/dev/comedi0', devices=devices)
226 >>> device.file is not None
231 >>> device = load_device(filename='/dev/comedi0', devices=devices)
232 >>> devices == [device]
236 >>> device.file is not None
240 We try and return helpful errors when things go wrong:
242 >>> device = load_device(filename='/dev/comedi0', devices=None)
243 Traceback (most recent call last):
245 TypeError: 'NoneType' object is not iterable
246 >>> device = load_device(filename='/dev/comedi0', devices=tuple())
247 Traceback (most recent call last):
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):
253 PyComediError: comedi_open (/dev/comediX): No such file or directory (None)
256 matching_devices = [d for d in devices if d.filename == filename]
258 _LOG.error('non-iterable devices? ({})'.format(devices))
261 device = matching_devices[0]
262 if device.file is None:
265 device = Device(filename)
268 devices.append(device) # pass new device back to caller
269 except AttributeError:
272 ('none of the available devices ({}) match {}, and we '
273 'cannot append to {}').format(
274 [d.filename for d in devices], filename, devices))
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)
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']
293 def _setup_channel_config(config, channel):
294 """Initialize the `ChannelConfig` `config` using the
295 `AnalogChannel` `channel`.
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()
314 class PiezoAxis (object):
315 """A one-dimensional piezoelectric actuator.
317 If used, the montoring channel must (as of now) be on the same
318 device as the controlling channel.
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)
325 >>> d = Device('/dev/comedi0')
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)
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)
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'
346 >>> p = PiezoAxis(config=config)
347 ... # doctest: +NORMALIZE_WHITESPACE
348 Traceback (most recent call last):
350 NotImplementedError: piezo axis control and monitor on different devices
351 (/dev/comedi0 and /dev/comediX)
353 >>> config['monitor']['device'] = config['channel']['device']
354 >>> p = PiezoAxis(config=config,
355 ... axis_channel=axis_channel, monitor_channel=monitor_channel)
358 >>> print(config['channel'].dump())
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())
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
383 >>> convert_bits_to_meters(p.config, 0)
384 ... # doctest: +ELLIPSIS
387 Opening from the config alone:
389 >>> p = PiezoAxis(config=config)
390 >>> p.load_from_config(devices=[d])
391 >>> p.axis_channel # doctest: +ELLIPSIS
392 <pycomedi.channel.AnalogChannel object at 0x...>
393 >>> p.monitor_channel # doctest: +ELLIPSIS
394 <pycomedi.channel.AnalogChannel object at 0x...>
398 def __init__(self, config, axis_channel=None, monitor_channel=None):
400 self.axis_channel = axis_channel
401 self.monitor_channel = monitor_channel
403 def load_from_config(self, devices):
404 c = self.config # reduce verbosity
406 c['channel']['device'] != c['monitor']['device']):
407 raise NotImplementedError(
408 ('piezo axis control and monitor on different devices '
409 '({} and {})').format(
410 c['channel']['device'], c['monitor']['device']))
411 if not self.axis_channel:
412 output = OutputChannel(config=c['channel'])
413 output.load_from_config(devices=devices)
414 self.axis_channel = output.channel
415 if c['monitor'] and not self.monitor_channel:
416 monitor = InputChannel(config=c['monitor'])
417 monitor.load_from_config(devices=devices)
418 self.monitor_channel = monitor.channel
419 self.name = c['channel']['name']
421 def setup_config(self):
422 "Initialize the axis (and monitor) configs."
423 _setup_channel_config(self.config['channel'], self.axis_channel)
424 if self.monitor_channel:
425 _setup_channel_config(
426 self.config['monitor'], self.monitor_channel)
427 if self.config['minimum'] is None:
428 self.config['minimum'] = convert_bits_to_volts(
429 self.config['channel'], 0)
430 if self.config['maximum'] is None:
431 self.config['maximum'] = convert_bits_to_volts(
432 self.config['channel'], self.axis_channel.get_maxdata())
436 class OutputChannel(object):
437 """An input channel monitoring some interesting parameter.
439 >>> from pycomedi.device import Device
440 >>> from pycomedi.subdevice import StreamingSubdevice
441 >>> from pycomedi.channel import AnalogChannel
442 >>> from pycomedi.constant import AREF, SUBDEVICE_TYPE, UNIT
444 >>> d = Device('/dev/comedi0')
447 >>> s = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
448 ... factory=StreamingSubdevice)
450 >>> channel = s.channel(0, factory=AnalogChannel, aref=AREF.diff)
451 >>> channel.range = channel.find_range(unit=UNIT.volt, min=-10, max=10)
453 >>> channel_config = _config.OutputChannelConfig()
455 >>> c = OutputChannel(config=channel_config, channel=channel)
457 >>> print(channel_config.dump())
464 analog-reference: diff
465 conversion-coefficients: -10.0, 0.000305180437934
466 conversion-origin: 0.0
467 inverse-conversion-coefficients: 0.0, 3276.75
468 inverse-conversion-origin: -10.0
470 >>> convert_volts_to_bits(c.config, -10)
473 Opening from the config alone:
475 >>> c = OutputChannel(config=channel_config)
476 >>> c.load_from_config(devices=[d])
477 >>> c.channel # doctest: +ELLIPSIS
478 <pycomedi.channel.AnalogChannel object at 0x...>
482 def __init__(self, config, channel=None):
484 self.channel = channel
486 def load_from_config(self, devices):
487 _load_channel_from_config(
488 channel=self, devices=devices, subdevice_type=SUBDEVICE_TYPE.ao)
490 def setup_config(self):
491 _setup_channel_config(self.config, self.channel)
494 class InputChannel(object):
495 """An input channel monitoring some interesting parameter.
497 >>> from pycomedi.device import Device
498 >>> from pycomedi.subdevice import StreamingSubdevice
499 >>> from pycomedi.channel import AnalogChannel
500 >>> from pycomedi.constant import AREF, SUBDEVICE_TYPE, UNIT
502 >>> d = Device('/dev/comedi0')
505 >>> s = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
506 ... factory=StreamingSubdevice)
508 >>> channel = s.channel(0, factory=AnalogChannel, aref=AREF.diff)
509 >>> channel.range = channel.find_range(unit=UNIT.volt, min=-10, max=10)
511 >>> channel_config = _config.InputChannelConfig()
513 >>> c = InputChannel(config=channel_config, channel=channel)
515 >>> print(channel_config.dump())
522 analog-reference: diff
523 conversion-coefficients: -10.0, 0.000305180437934
524 conversion-origin: 0.0
525 inverse-conversion-coefficients: 0.0, 3276.75
526 inverse-conversion-origin: -10.0
528 >>> convert_bits_to_volts(c.config, 0)
531 Opening from the config alone:
533 >>> c = InputChannel(config=channel_config)
534 >>> c.load_from_config(devices=[d])
535 >>> c.channel # doctest: +ELLIPSIS
536 <pycomedi.channel.AnalogChannel object at 0x...>
540 def __init__(self, config, channel=None):
542 self.channel = channel
544 def load_from_config(self, devices):
545 _load_channel_from_config(
546 channel=self, devices=devices, subdevice_type=SUBDEVICE_TYPE.ai)
548 def setup_config(self):
549 _setup_channel_config(self.config, self.channel)
552 class Piezo (object):
553 """A piezo actuator-controlled experiment.
555 >>> from pprint import pprint
556 >>> from pycomedi.device import Device
557 >>> from pycomedi.subdevice import StreamingSubdevice
558 >>> from pycomedi.channel import AnalogChannel
559 >>> from pycomedi.constant import AREF, SUBDEVICE_TYPE, UNIT
561 >>> d = Device('/dev/comedi0')
564 >>> axis_config = _config.AxisConfig()
565 >>> axis_config['gain'] = 20.0
566 >>> axis_config['sensitivity'] = 8e-9
567 >>> axis_config['channel'] = _config.OutputChannelConfig()
568 >>> axis_config['channel']['analog-reference'] = AREF.ground
569 >>> axis_config['channel']['name'] = 'z'
570 >>> axis_config['monitor'] = _config.InputChannelConfig()
571 >>> axis_config['monitor']['analog-reference'] = AREF.diff
572 >>> a = PiezoAxis(config=axis_config)
573 >>> a.load_from_config(devices=[d])
576 >>> input_config = _config.InputChannelConfig()
577 >>> input_config['analog-reference'] = AREF.diff
578 >>> input_config['name'] = 'some-input'
579 >>> c = InputChannel(config=input_config)
580 >>> c.load_from_config(devices=[d])
583 >>> config = _config.PiezoConfig()
584 >>> config['name'] = 'Charlie'
586 >>> p = Piezo(config=config, axes=[a], inputs=[c])
588 >>> inputs = p.read_inputs()
589 >>> pprint(inputs) # doctest: +SKIP
590 {'some-input': 34494L, 'z-monitor': 32669L}
592 >>> pos = convert_volts_to_bits(p.config['axes'][0]['channel'], 0)
596 >>> p.last_output == {'z': int(pos)}
599 :meth:`ramp` raises an error if passed an invalid `data` `dtype`.
601 >>> output_data = _numpy.arange(0, int(pos), step=int(pos/10),
602 ... dtype=_numpy.float)
603 >>> output_data = output_data.reshape((len(output_data), 1))
604 >>> input_data = p.ramp(data=output_data, frequency=10,
605 ... output_names=['z'], input_names=['z-monitor', 'some-input'])
606 Traceback (most recent call last):
608 ValueError: output dtype float64 does not match expected <type 'numpy.uint16'>
609 >>> output_data = _numpy.arange(0, int(pos), step=int(pos/10),
610 ... dtype=p.channel_dtype('z', direction='output'))
611 >>> output_data = output_data.reshape((len(output_data), 1))
612 >>> input_data = p.ramp(data=output_data, frequency=10,
613 ... output_names=['z'], input_names=['z-monitor', 'some-input'])
614 >>> input_data # doctest: +SKIP
625 [32646, 22686]], dtype=uint16)
627 >>> p.last_output == {'z': output_data[-1]}
630 >>> data = p.named_ramp(data=output_data, frequency=10,
631 ... output_names=['z'], input_names=['z-monitor', 'some-input'])
632 >>> pprint(data) # doctest: +ELLIPSIS, +SKIP
633 {'some-input': array([21666, 20566, ..., 22395], dtype=uint16),
634 'z': array([ 0, 3276, ..., 32760], dtype=uint16),
635 'z-monitor': array([ 3102, 6384, ..., 32647], dtype=uint16)}
637 Opening from the config alone:
639 >>> p = Piezo(config=config)
640 >>> p.load_from_config(devices=[d])
641 >>> for axis in p.axes:
642 ... print(axis.axis_channel)
643 ... print(axis.monitor_channel)
644 ... # doctest: +ELLIPSIS
645 <pycomedi.channel.AnalogChannel object at 0x...>
646 <pycomedi.channel.AnalogChannel object at 0x...>
647 >>> for input in p.inputs:
648 ... print(input.channel)
649 ... # doctest: +ELLIPSIS
650 <pycomedi.channel.AnalogChannel object at 0x...>
654 def __init__(self, config, axes=None, inputs=None):
658 self.last_output = {}
660 def load_from_config(self, devices):
663 for config in self.config['axes']:
664 axis = PiezoAxis(config=config)
665 axis.load_from_config(devices=devices)
666 self.axes.append(axis)
667 self.last_output.clear()
670 for config in self.config['inputs']:
671 input = InputChannel(config=config)
672 input.load_from_config(devices=devices)
673 self.inputs.append(input)
674 self.name = self.config['name']
676 def setup_config(self):
677 "Initialize the axis and input configs."
678 for x in self.axes + self.inputs:
680 self.config['axes'] = [x.config for x in self.axes]
681 self.config['inputs'] = [x.config for x in self.inputs]
683 def axis_by_name(self, name):
684 "Get an axis by its name."
685 for axis in self.axes:
686 if axis.name == name:
688 raise ValueError(name)
690 def input_channel_by_name(self, name):
691 "Get an input channel by its name."
692 for input_channel in self.inputs:
693 if input_channel.name == name:
695 raise ValueError(name)
697 def channels(self, direction=None):
698 """Iterate through all `(name, channel)` tuples.
700 =========== ===================
701 `direction` Returned channels
702 =========== ===================
703 'input' all input channels
704 'output' all output channels
706 =========== ===================
708 if direction not in ('input', 'output', None):
709 raise ValueError(direction)
711 if direction != 'input':
712 yield (a.name, a.axis_channel)
713 if a.monitor_channel and direction != 'output':
714 yield ('%s-monitor' % a.name, a.monitor_channel)
715 if direction != 'output':
716 for c in self.inputs:
717 yield (c.name, c.channel)
719 def channel_by_name(self, name, direction=None):
720 """Get a channel by its name.
722 Setting `direction` (see :meth:`channels`) may allow a more
725 for n,channel in self.channels(direction=direction):
728 raise ValueError(name)
730 def channel_dtype(self, channel_name, direction=None):
731 """Get a channel's data type by name.
733 Setting `direction` (see :meth:`channels`) may allow a more
736 channel = self.channel_by_name(name=channel_name, direction=direction)
737 return channel.subdevice.get_dtype()
739 def read_inputs(self):
740 "Read all inputs and return a `name`->`value` dictionary."
741 # There is no multi-channel read instruction, so preform reads
743 ret = dict([(n, c.data_read())
744 for n,c in self.channels(direction='input')])
745 _LOG.debug('current position: %s' % ret)
748 def jump(self, axis_name, position, steps=1, sleep=None):
749 "Move the output named `axis_name` to `position`."
750 _LOG.debug('jump {} to {} in {} steps'.format(
751 axis_name, position, steps))
754 orig_pos = self.last_output[axis_name]
757 ("cannot make a soft jump to {} because we don't have a "
758 'last-output position for {}').format(
759 position, axis_name))
760 return self.jump(axis_name=axis_name, position=position)
762 for i,pos in enumerate(_numpy.linspace(
763 orig_pos, position, steps+1)[1:]):
764 _LOG.debug('jump {} to {} ({} of {} steps)'.format(
765 axis_name, pos, i, steps))
766 self.jump(axis_name=axis_name, position=pos)
770 position = int(position)
771 channel = self.channel_by_name(name=axis_name)
772 channel.data_write(position)
773 self.last_output[axis_name] = position
775 def ramp(self, data, frequency, output_names, input_names=()):
776 """Synchronized IO ramp writing `data` and reading `in_names`.
780 data : numpy array-like
781 Row for each cycle, column for each output channel.
783 Target cycle frequency in Hz.
784 output_names : list of strings
785 Names of output channels in the same order as the columns
787 input_names : list of strings
788 Names of input channels to monitor in the same order as
789 the columns of the returned array.
791 if len(data.shape) != 2:
793 'ramp data must be two dimensional, not %d' % len(data.shape))
794 if data.shape[1] != len(output_names):
796 'ramp data should have on column for each input, '
797 'but has %d columns for %d inputs'
798 % (data.shape[1], len(output_names)))
799 n_samps = data.shape[0]
800 log_string = 'ramp %d samples at %g Hz. out: %s, in: %s' % (
801 n_samps, frequency, output_names, input_names)
802 _LOG.debug(log_string) # _LOG on one line for easy commenting-out
804 output_channels = [self.channel_by_name(name=n, direction='output')
805 for n in output_names]
806 inputs = [self.channel_by_name(name=n, direction='input')
807 for n in input_names]
809 ao_subdevice = output_channels[0].subdevice
810 ai_subdevice = inputs[0].subdevice
811 device = ao_subdevice.device
813 output_dtype = ao_subdevice.get_dtype()
814 if data.dtype != output_dtype:
815 raise ValueError('output dtype %s does not match expected %s'
816 % (data.dtype, output_dtype))
817 input_data = _numpy.ndarray(
818 (n_samps, len(inputs)), dtype=ai_subdevice.get_dtype())
820 _LOG.debug('setup ramp commands')
821 scan_period_ns = int(1e9 / frequency)
822 ai_cmd = ai_subdevice.get_cmd_generic_timed(
823 len(inputs), scan_period_ns)
824 ao_cmd = ao_subdevice.get_cmd_generic_timed(
825 len(output_channels), scan_period_ns)
827 ai_cmd.start_src = TRIG_SRC.int
829 ai_cmd.stop_src = TRIG_SRC.count
830 ai_cmd.stop_arg = n_samps
831 ai_cmd.chanlist = inputs
832 #ao_cmd.start_src = TRIG_SRC.ext
833 #ao_cmd.start_arg = 18 # NI card AI_START1 internal AI start signal
834 ao_cmd.start_src = TRIG_SRC.int
836 ao_cmd.stop_src = TRIG_SRC.count
837 ao_cmd.stop_arg = n_samps-1
838 ao_cmd.chanlist = output_channels
840 ai_subdevice.cmd = ai_cmd
841 ao_subdevice.cmd = ao_cmd
843 rc = ai_subdevice.command_test()
845 _LOG.debug('analog input test %d: %s' % (i, rc))
847 rc = ao_subdevice.command_test()
849 _LOG.debug('analog output test %d: %s' % (i, rc))
851 _LOG.debug('lock subdevices for ramp')
856 _LOG.debug('load ramp commands')
857 ao_subdevice.command()
858 ai_subdevice.command()
860 writer = Writer(ao_subdevice, data)
862 reader = Reader(ai_subdevice, input_data)
865 _LOG.debug('arm analog output')
866 device.do_insn(inttrig_insn(ao_subdevice))
867 _LOG.debug('trigger ramp (via analog input)')
868 device.do_insn(inttrig_insn(ai_subdevice))
869 _LOG.debug('ramp running')
873 _LOG.debug('ramp complete')
875 #_LOG.debug('AI flags: %s' % ai_subdevice.get_flags())
876 ai_subdevice.cancel()
877 ai_subdevice.unlock()
879 # release busy flag, which seems to not be cleared
881 # http://groups.google.com/group/comedi_list/browse_thread/thread/4c7040989197abad/
882 #_LOG.debug('AO flags: %s' % ao_subdevice.get_flags())
883 ao_subdevice.cancel()
884 ao_subdevice.unlock()
885 _LOG.debug('unlocked subdevices after ramp')
887 for i,name in enumerate(output_names):
888 self.last_output[name] = data[-1,i]
890 if _package_config['matplotlib']:
892 raise _matplotlib_import_error
893 figure = _matplotlib_pyplot.figure()
894 axes = figure.add_subplot(1, 1, 1)
896 timestamp = _time.strftime('%H%M%S')
897 axes.set_title('piezo ramp %s' % timestamp)
898 for d,names in [(data, output_names),
899 (input_data, input_names)]:
900 for i,name in enumerate(names):
901 axes.plot(d[:,i], label=name)
903 if hasattr(figure, 'show'):
905 if not _matplotlib.is_interactive():
906 _matplotlib_pyplot.show()
909 def named_ramp(self, data, frequency, output_names, input_names=()):
910 input_data = self.ramp(
911 data=data, frequency=frequency, output_names=output_names,
912 input_names=input_names)
914 for i,name in enumerate(output_names):
915 ret[name] = data[:,i]
916 for i,name in enumerate(input_names):
917 ret[name] = input_data[:,i]
920 def zero(self, axis_names=None, **kwargs):
922 if axis_names is None:
923 axis_names = [axis.name for axis in self.axes]
924 for axis_name in axis_names:
925 axis = self.axis_by_name(axis_name)
926 config = self.config.select_config(
927 'axes', axis_name, get_attribute=get_axis_name)['channel']
928 zero = convert_volts_to_bits(config, 0)
930 self.jump(axis_name, zero)