1 # Copyright (C) 2011-2012 W. Trevor King <wking@drexel.edu>
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, 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...>
397 def __init__(self, config, axis_channel=None, monitor_channel=None,
400 self.axis_channel = axis_channel
401 self.monitor_channel = monitor_channel
402 self.load_from_config(devices=devices)
404 def load_from_config(self, devices):
405 c = self.config # reduce verbosity
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']
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())
435 class OutputChannel(object):
436 """An input channel monitoring some interesting parameter.
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
443 >>> d = Device('/dev/comedi0')
446 >>> s = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
447 ... factory=StreamingSubdevice)
449 >>> channel = s.channel(0, factory=AnalogChannel, aref=AREF.diff)
450 >>> channel.range = channel.find_range(unit=UNIT.volt, min=-10, max=10)
452 >>> channel_config = _config.OutputChannelConfig()
454 >>> c = OutputChannel(config=channel_config, channel=channel)
456 >>> print(channel_config.dump())
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
469 >>> convert_volts_to_bits(c.config, -10)
472 Opening from the config alone:
474 >>> c = OutputChannel(config=channel_config, devices=[d])
475 >>> c.channel # doctest: +ELLIPSIS
476 <pycomedi.channel.AnalogChannel object at 0x...>
480 def __init__(self, config, channel=None, devices=None):
482 self.channel = channel
483 self.load_from_config(devices=devices)
485 def load_from_config(self, devices):
486 _load_channel_from_config(
487 channel=self, devices=devices, subdevice_type=SUBDEVICE_TYPE.ao)
489 def setup_config(self):
490 _setup_channel_config(self.config, self.channel)
493 class InputChannel(object):
494 """An input channel monitoring some interesting parameter.
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
501 >>> d = Device('/dev/comedi0')
504 >>> s = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
505 ... factory=StreamingSubdevice)
507 >>> channel = s.channel(0, factory=AnalogChannel, aref=AREF.diff)
508 >>> channel.range = channel.find_range(unit=UNIT.volt, min=-10, max=10)
510 >>> channel_config = _config.InputChannelConfig()
512 >>> c = InputChannel(config=channel_config, channel=channel)
514 >>> print(channel_config.dump())
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
527 >>> convert_bits_to_volts(c.config, 0)
530 Opening from the config alone:
532 >>> c = InputChannel(config=channel_config, devices=[d])
533 >>> c.channel # doctest: +ELLIPSIS
534 <pycomedi.channel.AnalogChannel object at 0x...>
538 def __init__(self, config, channel=None, devices=None):
540 self.channel = channel
541 self.load_from_config(devices=devices)
543 def load_from_config(self, devices):
544 _load_channel_from_config(
545 channel=self, devices=devices, subdevice_type=SUBDEVICE_TYPE.ai)
547 def setup_config(self):
548 _setup_channel_config(self.config, self.channel)
551 class Piezo (object):
552 """A piezo actuator-controlled experiment.
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
560 >>> d = Device('/dev/comedi0')
563 >>> axis_config = _config.AxisConfig()
564 >>> axis_config['gain'] = 20.0
565 >>> axis_config['sensitivity'] = 8e-9
566 >>> axis_config['channel'] = _config.OutputChannelConfig()
567 >>> axis_config['channel']['analog-reference'] = AREF.ground
568 >>> axis_config['channel']['name'] = 'z'
569 >>> axis_config['monitor'] = _config.InputChannelConfig()
570 >>> axis_config['monitor']['analog-reference'] = AREF.diff
571 >>> a = PiezoAxis(config=axis_config, devices=[d])
574 >>> input_config = _config.InputChannelConfig()
575 >>> input_config['analog-reference'] = AREF.diff
576 >>> input_config['name'] = 'some-input'
577 >>> c = InputChannel(config=input_config, devices=[d])
580 >>> config = _config.PiezoConfig()
581 >>> config['name'] = 'Charlie'
583 >>> p = Piezo(config=config, axes=[a], inputs=[c])
585 >>> inputs = p.read_inputs()
586 >>> pprint(inputs) # doctest: +SKIP
587 {'some-input': 34494L, 'z-monitor': 32669L}
589 >>> pos = convert_volts_to_bits(p.config['axes'][0]['channel'], 0)
593 >>> p.last_output == {'z': int(pos)}
596 :meth:`ramp` raises an error if passed an invalid `data` `dtype`.
598 >>> output_data = _numpy.arange(0, int(pos), step=int(pos/10),
599 ... dtype=_numpy.float)
600 >>> output_data = output_data.reshape((len(output_data), 1))
601 >>> input_data = p.ramp(data=output_data, frequency=10,
602 ... output_names=['z'], input_names=['z-monitor', 'some-input'])
603 Traceback (most recent call last):
605 ValueError: output dtype float64 does not match expected <type 'numpy.uint16'>
606 >>> output_data = _numpy.arange(0, int(pos), step=int(pos/10),
607 ... dtype=p.channel_dtype('z', direction='output'))
608 >>> output_data = output_data.reshape((len(output_data), 1))
609 >>> input_data = p.ramp(data=output_data, frequency=10,
610 ... output_names=['z'], input_names=['z-monitor', 'some-input'])
611 >>> input_data # doctest: +SKIP
622 [32646, 22686]], dtype=uint16)
624 >>> p.last_output == {'z': output_data[-1]}
627 >>> data = p.named_ramp(data=output_data, frequency=10,
628 ... output_names=['z'], input_names=['z-monitor', 'some-input'])
629 >>> pprint(data) # doctest: +ELLIPSIS, +SKIP
630 {'some-input': array([21666, 20566, ..., 22395], dtype=uint16),
631 'z': array([ 0, 3276, ..., 32760], dtype=uint16),
632 'z-monitor': array([ 3102, 6384, ..., 32647], dtype=uint16)}
634 Opening from the config alone:
636 >>> p = Piezo(config=config, devices=[d])
637 >>> for axis in p.axes:
638 ... print(axis.axis_channel)
639 ... print(axis.monitor_channel)
640 ... # doctest: +ELLIPSIS
641 <pycomedi.channel.AnalogChannel object at 0x...>
642 <pycomedi.channel.AnalogChannel object at 0x...>
643 >>> for input in p.inputs:
644 ... print(input.channel)
645 ... # doctest: +ELLIPSIS
646 <pycomedi.channel.AnalogChannel object at 0x...>
650 def __init__(self, config, axes=None, inputs=None, devices=None):
654 self.last_output = {}
655 self.load_from_config(devices=devices)
657 def load_from_config(self, devices):
660 for config in self.config['axes']:
661 self.axes.append(PiezoAxis(config=config, devices=devices))
662 self.last_output.clear()
665 for config in self.config['inputs']:
667 InputChannel(config=config, devices=devices))
668 self.name = self.config['name']
670 def setup_config(self):
671 "Initialize the axis and input configs."
672 for x in self.axes + self.inputs:
674 self.config['axes'] = [x.config for x in self.axes]
675 self.config['inputs'] = [x.config for x in self.inputs]
677 def axis_by_name(self, name):
678 "Get an axis by its name."
679 for axis in self.axes:
680 if axis.name == name:
682 raise ValueError(name)
684 def input_channel_by_name(self, name):
685 "Get an input channel by its name."
686 for input_channel in self.inputs:
687 if input_channel.name == name:
689 raise ValueError(name)
691 def channels(self, direction=None):
692 """Iterate through all `(name, channel)` tuples.
694 =========== ===================
695 `direction` Returned channels
696 =========== ===================
697 'input' all input channels
698 'output' all output channels
700 =========== ===================
702 if direction not in ('input', 'output', None):
703 raise ValueError(direction)
705 if direction != 'input':
706 yield (a.name, a.axis_channel)
707 if a.monitor_channel and direction != 'output':
708 yield ('%s-monitor' % a.name, a.monitor_channel)
709 if direction != 'output':
710 for c in self.inputs:
711 yield (c.name, c.channel)
713 def channel_by_name(self, name, direction=None):
714 """Get a channel by its name.
716 Setting `direction` (see :meth:`channels`) may allow a more
719 for n,channel in self.channels(direction=direction):
722 raise ValueError(name)
724 def channel_dtype(self, channel_name, direction=None):
725 """Get a channel's data type by name.
727 Setting `direction` (see :meth:`channels`) may allow a more
730 channel = self.channel_by_name(name=channel_name, direction=direction)
731 return channel.subdevice.get_dtype()
733 def read_inputs(self):
734 "Read all inputs and return a `name`->`value` dictionary."
735 # There is no multi-channel read instruction, so preform reads
737 ret = dict([(n, c.data_read())
738 for n,c in self.channels(direction='input')])
739 _LOG.debug('current position: %s' % ret)
742 def jump(self, axis_name, position, steps=1, sleep=None):
743 "Move the output named `axis_name` to `position`."
744 _LOG.debug('jump {} to {} in {} steps'.format(
745 axis_name, position, steps))
748 orig_pos = self.last_output[axis_name]
751 ("cannot make a soft jump to {} because we don't have a "
752 'last-output position for {}').format(
753 position, axis_name))
754 return self.jump(axis_name=axis_name, position=position)
756 for i,pos in enumerate(_numpy.linspace(
757 orig_pos, position, steps+1)[1:]):
758 _LOG.debug('jump {} to {} ({} of {} steps)'.format(
759 axis_name, pos, i, steps))
760 self.jump(axis_name=axis_name, position=pos)
764 position = int(position)
765 channel = self.channel_by_name(name=axis_name)
766 channel.data_write(position)
767 self.last_output[axis_name] = position
769 def ramp(self, data, frequency, output_names, input_names=()):
770 """Synchronized IO ramp writing `data` and reading `in_names`.
774 data : numpy array-like
775 Row for each cycle, column for each output channel.
777 Target cycle frequency in Hz.
778 output_names : list of strings
779 Names of output channels in the same order as the columns
781 input_names : list of strings
782 Names of input channels to monitor in the same order as
783 the columns of the returned array.
785 if len(data.shape) != 2:
787 'ramp data must be two dimensional, not %d' % len(data.shape))
788 if data.shape[1] != len(output_names):
790 'ramp data should have on column for each input, '
791 'but has %d columns for %d inputs'
792 % (data.shape[1], len(output_names)))
793 n_samps = data.shape[0]
794 log_string = 'ramp %d samples at %g Hz. out: %s, in: %s' % (
795 n_samps, frequency, output_names, input_names)
796 _LOG.debug(log_string) # _LOG on one line for easy commenting-out
798 output_channels = [self.channel_by_name(name=n, direction='output')
799 for n in output_names]
800 inputs = [self.channel_by_name(name=n, direction='input')
801 for n in input_names]
803 ao_subdevice = output_channels[0].subdevice
804 ai_subdevice = inputs[0].subdevice
805 device = ao_subdevice.device
807 output_dtype = ao_subdevice.get_dtype()
808 if data.dtype != output_dtype:
809 raise ValueError('output dtype %s does not match expected %s'
810 % (data.dtype, output_dtype))
811 input_data = _numpy.ndarray(
812 (n_samps, len(inputs)), dtype=ai_subdevice.get_dtype())
814 _LOG.debug('setup ramp commands')
815 scan_period_ns = int(1e9 / frequency)
816 ai_cmd = ai_subdevice.get_cmd_generic_timed(
817 len(inputs), scan_period_ns)
818 ao_cmd = ao_subdevice.get_cmd_generic_timed(
819 len(output_channels), scan_period_ns)
821 ai_cmd.start_src = TRIG_SRC.int
823 ai_cmd.stop_src = TRIG_SRC.count
824 ai_cmd.stop_arg = n_samps
825 ai_cmd.chanlist = inputs
826 #ao_cmd.start_src = TRIG_SRC.ext
827 #ao_cmd.start_arg = 18 # NI card AI_START1 internal AI start signal
828 ao_cmd.start_src = TRIG_SRC.int
830 ao_cmd.stop_src = TRIG_SRC.count
831 ao_cmd.stop_arg = n_samps-1
832 ao_cmd.chanlist = output_channels
834 ai_subdevice.cmd = ai_cmd
835 ao_subdevice.cmd = ao_cmd
837 rc = ai_subdevice.command_test()
839 _LOG.debug('analog input test %d: %s' % (i, rc))
841 rc = ao_subdevice.command_test()
843 _LOG.debug('analog output test %d: %s' % (i, rc))
845 _LOG.debug('lock subdevices for ramp')
850 _LOG.debug('load ramp commands')
851 ao_subdevice.command()
852 ai_subdevice.command()
854 writer = Writer(ao_subdevice, data)
856 reader = Reader(ai_subdevice, input_data)
859 _LOG.debug('arm analog output')
860 device.do_insn(inttrig_insn(ao_subdevice))
861 _LOG.debug('trigger ramp (via analog input)')
862 device.do_insn(inttrig_insn(ai_subdevice))
863 _LOG.debug('ramp running')
867 _LOG.debug('ramp complete')
869 #_LOG.debug('AI flags: %s' % ai_subdevice.get_flags())
870 ai_subdevice.cancel()
871 ai_subdevice.unlock()
873 # release busy flag, which seems to not be cleared
875 # http://groups.google.com/group/comedi_list/browse_thread/thread/4c7040989197abad/
876 #_LOG.debug('AO flags: %s' % ao_subdevice.get_flags())
877 ao_subdevice.cancel()
878 ao_subdevice.unlock()
879 _LOG.debug('unlocked subdevices after ramp')
881 for i,name in enumerate(output_names):
882 self.last_output[name] = data[-1,i]
884 if _package_config['matplotlib']:
886 raise _matplotlib_import_error
887 figure = _matplotlib_pyplot.figure()
888 axes = figure.add_subplot(1, 1, 1)
890 timestamp = _time.strftime('%H%M%S')
891 axes.set_title('piezo ramp %s' % timestamp)
892 for d,names in [(data, output_names),
893 (input_data, input_names)]:
894 for i,name in enumerate(names):
895 axes.plot(d[:,i], label=name)
899 def named_ramp(self, data, frequency, output_names, input_names=()):
900 input_data = self.ramp(
901 data=data, frequency=frequency, output_names=output_names,
902 input_names=input_names)
904 for i,name in enumerate(output_names):
905 ret[name] = data[:,i]
906 for i,name in enumerate(input_names):
907 ret[name] = input_data[:,i]