1 # Copyright (C) 2008-2011 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
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation, either version 3 of the License, or (at your
8 # option) any later version.
10 # pypiezo is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 # General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with pypiezo. If not, see <http://www.gnu.org/licenses/>.
18 "Basic piezo control."
21 from time import sleep as _sleep
23 import numpy as _numpy
24 from scipy.stats import linregress as _linregress
27 import matplotlib as _matplotlib
28 import matplotlib.pyplot as _matplotlib_pyplot
29 import time as _time # for timestamping lines on plots
30 except (ImportError, RuntimeError), e:
32 _matplotlib_import_error = e
34 from pycomedi.constant import TRIG_SRC, SDF
35 from pycomedi.utility import inttrig_insn, Reader, Writer
37 from . import LOG as _LOG
38 from . import config as _config
39 from . import package_config as _package_config
42 def convert_bits_to_volts(config, data):
43 """Convert bit-valued data to volts.
45 >>> config = _config.ChannelConfig()
46 >>> config['conversion-coefficients'] = [1, 2, 3]
47 >>> config['conversion-origin'] = -1
48 >>> convert_bits_to_volts(config, -1)
50 >>> convert_bits_to_volts(
51 ... config, _numpy.array([-1, 0, 1, 2], dtype=_numpy.float))
52 array([ 1., 6., 17., 34.])
54 coefficients = config['conversion-coefficients']
55 origin = config['conversion-origin']
56 return _numpy.polyval(list(reversed(coefficients)), data-origin)
58 def convert_volts_to_bits(config, data):
59 """Convert bit-valued data to volts.
61 >>> config = _config.ChannelConfig()
62 >>> config['inverse-conversion-coefficients'] = [1, 2, 3]
63 >>> config['inverse-conversion-origin'] = -1
64 >>> convert_volts_to_bits(config, -1)
66 >>> convert_volts_to_bits(
67 ... config, _numpy.array([-1, 0, 1, 2], dtype=_numpy.float))
68 array([ 1., 6., 17., 34.])
70 Note that the inverse coeffiecient and offset are difficult to
71 derive from the forward coefficient and offset. The current
72 Comedilib conversion functions, `comedi_to_physical()` and
73 `comedi_from_physical()` take `comedi_polynomial_t` conversion
74 arguments. `comedi_polynomial_t` is defined in `comedilib.h`_,
75 and holds a polynomial of length
76 `COMEDI_MAX_NUM_POLYNOMIAL_COEFFICIENTS`, currently set to 4. The
77 inverse of this cubic polynomial might be another polynomial, but
78 it might also have a more complicated form. A general inversion
79 solution is considered too complicated, so when you're setting up
80 your configuration, you should use Comedilib to save both the
81 forward and inverse coefficients and offsets.
83 .. _comedilib.h: http://www.comedi.org/git?p=comedi/comedilib.git;a=blob;f=include/comedilib.h;hb=HEAD
85 origin = config['inverse-conversion-origin']
86 inverse_coefficients = config['inverse-conversion-coefficients']
87 if len(inverse_coefficients) == 0:
88 raise NotImplementedError('cubic polynomial inversion')
89 return _numpy.polyval(list(reversed(inverse_coefficients)), data-origin)
91 def convert_volts_to_meters(config, data):
92 """Convert volt-valued data to meters.
94 >>> config = _config.AxisConfig()
95 >>> config['gain'] = 20.0
96 >>> config['sensitivity'] = 8e-9
97 >>> convert_volts_to_meters(config, 1)
99 >>> convert_volts_to_meters(
100 ... config, _numpy.array([1, 6, 17, 34], dtype=_numpy.float))
101 ... # doctest: +ELLIPSIS
102 array([ 1.6...e-07, 9.6...e-07, 2.7...e-06,
105 return data * config['gain'] * config['sensitivity']
107 def convert_meters_to_volts(config, data):
108 """Convert bit-valued data to volts.
110 >>> config = _config.AxisConfig()
111 >>> config['gain'] = 20.0
112 >>> config['sensitivity'] = 8e-9
113 >>> convert_meters_to_volts(config, 1.6e-7)
115 >>> convert_meters_to_volts(
116 ... config, _numpy.array([1.6e-7, 9.6e-7, 2.72e-6, 5.44e-6],
117 ... dtype=_numpy.float))
118 array([ 1., 6., 17., 34.])
120 return data / (config['gain'] * config['sensitivity'])
122 def convert_bits_to_meters(axis_config, data):
123 """Convert bit-valued data to meters.
125 >>> channel_config = _config.ChannelConfig()
126 >>> channel_config['conversion-coefficients'] = [1, 2, 3]
127 >>> channel_config['conversion-origin'] = -1
128 >>> axis_config = _config.AxisConfig()
129 >>> axis_config['gain'] = 20.0
130 >>> axis_config['sensitivity'] = 8e-9
131 >>> axis_config['channel'] = channel_config
132 >>> convert_bits_to_meters(axis_config, 1)
133 ... # doctest: +ELLIPSIS
135 >>> convert_bits_to_meters(
136 ... axis_config, _numpy.array([-1, 0, 1, 2], dtype=_numpy.float))
137 ... # doctest: +ELLIPSIS
138 array([ 1.6...e-07, 9.6...e-07, 2.7...e-06,
141 data = convert_bits_to_volts(axis_config['channel'], data)
142 return convert_volts_to_meters(axis_config, data)
144 def convert_meters_to_bits(axis_config, data):
145 """Convert meter-valued data to volts.
147 >>> channel_config = _config.ChannelConfig()
148 >>> channel_config['inverse-conversion-coefficients'] = [1, 2, 3]
149 >>> channel_config['inverse-conversion-origin'] = -1
150 >>> axis_config = _config.AxisConfig()
151 >>> axis_config['gain'] = 20.0
152 >>> axis_config['sensitivity'] = 8e-9
153 >>> axis_config['channel'] = channel_config
154 >>> convert_meters_to_bits(axis_config, 1.6e-7)
156 >>> convert_meters_to_bits(
158 ... _numpy.array([1.6e-7, 9.6e-7, 2.72e-6, 5.44e-6],
159 ... dtype=_numpy.float))
160 array([ 17., 162., 1009., 3746.])
162 data = convert_meters_to_volts(axis_config, data)
163 return convert_volts_to_bits(axis_config['channel'], data)
165 def _setup_channel_config(config, channel):
166 """Initialize the `ChannelConfig` `config` using the
167 `AnalogChannel` `channel`.
169 config['device'] = channel.subdevice.device.filename
170 config['subdevice'] = channel.subdevice.index
171 config['channel'] = channel.index
172 config['maxdata'] = channel.get_maxdata()
173 config['range'] = channel.range.value
174 converter = channel.get_converter()
175 config['conversion-origin'
176 ] = converter.get_to_physical_expansion_origin()
177 config['conversion-coefficients'
178 ] = converter.get_to_physical_coefficients()
179 config['inverse-conversion-origin'
180 ] = converter.get_from_physical_expansion_origin()
181 config['inverse-conversion-coefficients'
182 ] = converter.get_from_physical_coefficients()
185 class PiezoAxis (object):
186 """A one-dimensional piezoelectric actuator.
188 If used, the montoring channel must (as of now) be on the same
189 device as the controlling channel.
191 >>> from pycomedi.device import Device
192 >>> from pycomedi.subdevice import StreamingSubdevice
193 >>> from pycomedi.channel import AnalogChannel
194 >>> from pycomedi.constant import (AREF, SUBDEVICE_TYPE, UNIT)
196 >>> d = Device('/dev/comedi0')
199 >>> s_in = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
200 ... factory=StreamingSubdevice)
201 >>> s_out = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
202 ... factory=StreamingSubdevice)
204 >>> axis_channel = s_out.channel(
205 ... 0, factory=AnalogChannel, aref=AREF.ground)
206 >>> monitor_channel = s_in.channel(
207 ... 0, factory=AnalogChannel, aref=AREF.diff)
208 >>> for chan in [axis_channel, monitor_channel]:
209 ... chan.range = chan.find_range(unit=UNIT.volt, min=-10, max=10)
211 >>> config = _config.AxisConfig()
212 >>> config.update({'gain':20, 'sensitivity':8e-9})
213 >>> config['channel'] = _config.OutputChannelConfig()
214 >>> config['monitor'] = _config.InputChannelConfig()
215 >>> config['monitor']['device'] = '/dev/comediX'
217 >>> p = PiezoAxis(config=config)
218 ... # doctest: +NORMALIZE_WHITESPACE
219 Traceback (most recent call last):
221 NotImplementedError: piezo axis control and monitor on different devices
222 (/dev/comedi0 and /dev/comediX)
224 >>> config['monitor']['device'] = config['channel']['device']
225 >>> p = PiezoAxis(config=config,
226 ... axis_channel=axis_channel, monitor_channel=monitor_channel)
229 >>> print(config['channel'].dump())
236 conversion-coefficients: -10.0, 0.000305180437934
237 conversion-origin: 0.0
238 inverse-conversion-coefficients: 0.0, 3276.75
239 inverse-conversion-origin: -10.0
240 >>> print(config['monitor'].dump())
247 conversion-coefficients: -10.0, 0.000305180437934
248 conversion-origin: 0.0
249 inverse-conversion-coefficients: 0.0, 3276.75
250 inverse-conversion-origin: -10.0
252 >>> convert_bits_to_meters(p.config, 0)
253 ... # doctest: +ELLIPSIS
258 def __init__(self, config, axis_channel=None, monitor_channel=None):
260 if (config['monitor'] and
261 config['channel']['device'] != config['monitor']['device']):
262 raise NotImplementedError(
263 ('piezo axis control and monitor on different devices '
265 config['channel']['device'],
266 config['monitor']['device']))
268 raise NotImplementedError(
269 'pypiezo not yet capable of opening its own axis channel')
270 #axis_channel = pycomedi...
271 self.axis_channel = axis_channel
272 if config['monitor'] and not monitor_channel:
273 raise NotImplementedError(
274 'pypiezo not yet capable of opening its own monitor channel')
275 #monitor_channel = pycomedi...
276 self.monitor_channel = monitor_channel
277 self.name = config['channel']['name']
279 def setup_config(self):
280 "Initialize the axis (and monitor) configs."
281 _setup_channel_config(self.config['channel'], self.axis_channel)
282 if self.monitor_channel:
283 _setup_channel_config(
284 self.config['monitor'], self.monitor_channel)
285 if self.config['minimum'] is None:
286 self.config['minimum'] = convert_bits_to_volts(
287 self.config['channel'], 0)
288 if self.config['maximum'] is None:
289 self.config['maximum'] = convert_bits_to_volts(
290 self.config['channel'], self.axis_channel.get_maxdata())
293 class InputChannel(object):
294 """An input channel monitoring some interesting parameter.
296 >>> from pycomedi.device import Device
297 >>> from pycomedi.subdevice import StreamingSubdevice
298 >>> from pycomedi.channel import AnalogChannel
299 >>> from pycomedi.constant import AREF, SUBDEVICE_TYPE, UNIT
301 >>> d = Device('/dev/comedi0')
304 >>> s = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
305 ... factory=StreamingSubdevice)
307 >>> channel = s.channel(0, factory=AnalogChannel, aref=AREF.diff)
308 >>> channel.range = channel.find_range(unit=UNIT.volt, min=-10, max=10)
310 >>> channel_config = _config.InputChannelConfig()
312 >>> c = InputChannel(config=channel_config, channel=channel)
314 >>> print(channel_config.dump())
321 conversion-coefficients: -10.0, 0.000305180437934
322 conversion-origin: 0.0
323 inverse-conversion-coefficients: 0.0, 3276.75
324 inverse-conversion-origin: -10.0
326 >>> convert_bits_to_volts(c.config, 0)
331 def __init__(self, config, channel=None):
334 raise NotImplementedError(
335 'pypiezo not yet capable of opening its own channel')
336 #channel = pycomedi...
337 self.channel = channel
338 self.name = config['name']
340 def setup_config(self):
341 _setup_channel_config(self.config, self.channel)
344 class Piezo (object):
345 """A piezo actuator-controlled experiment.
347 >>> from pprint import pprint
348 >>> from pycomedi.device import Device
349 >>> from pycomedi.subdevice import StreamingSubdevice
350 >>> from pycomedi.channel import AnalogChannel
351 >>> from pycomedi.constant import AREF, SUBDEVICE_TYPE, UNIT
353 >>> d = Device('/dev/comedi0')
356 >>> s_in = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
357 ... factory=StreamingSubdevice)
358 >>> s_out = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
359 ... factory=StreamingSubdevice)
361 >>> axis_channel = s_out.channel(
362 ... 0, factory=AnalogChannel, aref=AREF.ground)
363 >>> monitor_channel = s_in.channel(
364 ... 0, factory=AnalogChannel, aref=AREF.diff)
365 >>> input_channel = s_in.channel(1, factory=AnalogChannel, aref=AREF.diff)
366 >>> for chan in [axis_channel, monitor_channel, input_channel]:
367 ... chan.range = chan.find_range(unit=UNIT.volt, min=-10, max=10)
369 >>> axis_config = _config.AxisConfig()
370 >>> axis_config.update({'gain':20, 'sensitivity':8e-9})
371 >>> axis_config['channel'] = _config.OutputChannelConfig()
372 >>> axis_config['channel']['name'] = 'z'
373 >>> axis_config['monitor'] = _config.InputChannelConfig()
374 >>> input_config = _config.InputChannelConfig()
375 >>> input_config['name'] = 'some-input'
377 >>> a = PiezoAxis(config=axis_config, axis_channel=axis_channel,
378 ... monitor_channel=monitor_channel)
381 >>> c = InputChannel(config=input_config, channel=input_channel)
384 >>> p = Piezo(axes=[a], inputs=[c], name='Charlie')
385 >>> inputs = p.read_inputs()
386 >>> pprint(inputs) # doctest: +SKIP
387 {'some-input': 34494L, 'z-monitor': 32669L}
389 >>> pos = convert_volts_to_bits(p.config['axes'][0]['channel'], 0)
393 >>> p.last_output == {'z': int(pos)}
396 :meth:`ramp` raises an error if passed an invalid `data` `dtype`.
398 >>> output_data = _numpy.arange(0, int(pos), step=int(pos/10),
399 ... dtype=_numpy.float)
400 >>> output_data = output_data.reshape((len(output_data), 1))
401 >>> input_data = p.ramp(data=output_data, frequency=10,
402 ... output_names=['z'], input_names=['z-monitor', 'some-input'])
403 Traceback (most recent call last):
405 ValueError: output dtype float64 does not match expected <type 'numpy.uint16'>
406 >>> output_data = _numpy.arange(0, int(pos), step=int(pos/10),
407 ... dtype=p.channel_dtype('z', direction='output'))
408 >>> output_data = output_data.reshape((len(output_data), 1))
409 >>> input_data = p.ramp(data=output_data, frequency=10,
410 ... output_names=['z'], input_names=['z-monitor', 'some-input'])
411 >>> input_data # doctest: +SKIP
422 [32646, 22686]], dtype=uint16)
424 >>> p.last_output == {'z': output_data[-1]}
427 >>> data = p.named_ramp(data=output_data, frequency=10,
428 ... output_names=['z'], input_names=['z-monitor', 'some-input'])
429 >>> pprint(data) # doctest: +ELLIPSIS, +SKIP
430 {'some-input': array([21666, 20566, ..., 22395], dtype=uint16),
431 'z': array([ 0, 3276, ..., 32760], dtype=uint16),
432 'z-monitor': array([ 3102, 6384, ..., 32647], dtype=uint16)}
436 def __init__(self, axes, inputs, name=None):
439 self.config = _config.PiezoConfig()
441 self.config['name'] = name
442 self.config['axes'] = [x.config for x in axes]
443 self.config['inputs'] = [x.config for x in inputs]
444 self.last_output = {}
446 def axis_by_name(self, name):
447 "Get an axis by its name."
448 for axis in self.axes:
449 if axis.name == name:
451 raise ValueError(name)
453 def input_channel_by_name(self, name):
454 "Get an input channel by its name."
455 for input_channel in self.inputs:
456 if input_channel.name == name:
458 raise ValueError(name)
460 def channels(self, direction=None):
461 """Iterate through all `(name, channel)` tuples.
463 =========== ===================
464 `direction` Returned channels
465 =========== ===================
466 'input' all input channels
467 'output' all output channels
469 =========== ===================
471 if direction not in ('input', 'output', None):
472 raise ValueError(direction)
474 if direction != 'input':
475 yield (a.name, a.axis_channel)
476 if a.monitor_channel and direction != 'output':
477 yield ('%s-monitor' % a.name, a.monitor_channel)
478 if direction != 'output':
479 for c in self.inputs:
480 yield (c.name, c.channel)
482 def channel_by_name(self, name, direction=None):
483 """Get a channel by its name.
485 Setting `direction` (see :meth:`channels`) may allow a more
488 for n,channel in self.channels(direction=direction):
491 raise ValueError(name)
493 def channel_dtype(self, channel_name, direction=None):
494 """Get a channel's data type by name.
496 Setting `direction` (see :meth:`channels`) may allow a more
499 channel = self.channel_by_name(name=channel_name, direction=direction)
500 return channel.subdevice.get_dtype()
502 def read_inputs(self):
503 "Read all inputs and return a `name`->`value` dictionary."
504 # There is no multi-channel read instruction, so preform reads
506 ret = dict([(n, c.data_read()) for n,c in self.channels('input')])
507 _LOG.debug('current position: %s' % ret)
510 def jump(self, axis_name, position):
511 "Move the output named `axis_name` to `position`."
512 _LOG.debug('jump %s to %s' % (axis_name, position))
513 position = int(position)
514 channel = self.channel_by_name(name=axis_name)
515 channel.data_write(position)
516 self.last_output[axis_name] = position
518 def ramp(self, data, frequency, output_names, input_names=()):
519 """Synchronized IO ramp writing `data` and reading `in_names`.
523 data : numpy array-like
524 Row for each cycle, column for each output channel.
526 Target cycle frequency in Hz.
527 output_names : list of strings
528 Names of output channels in the same order as the columns
530 input_names : list of strings
531 Names of input channels to monitor in the same order as
532 the columns of the returned array.
534 if len(data.shape) != 2:
536 'ramp data must be two dimensional, not %d' % len(data.shape))
537 if data.shape[1] != len(output_names):
539 'ramp data should have on column for each input, '
540 'but has %d columns for %d inputs'
541 % (data.shape[1], len(output_names)))
542 n_samps = data.shape[0]
543 log_string = 'ramp %d samples at %g Hz. out: %s, in: %s' % (
544 n_samps, frequency, output_names, input_names)
545 _LOG.debug(log_string) # _LOG on one line for easy commenting-out
547 output_channels = [self.channel_by_name(name=n, direction='output')
548 for n in output_names]
549 inputs = [self.channel_by_name(name=n, direction='input')
550 for n in input_names]
552 ao_subdevice = output_channels[0].subdevice
553 ai_subdevice = inputs[0].subdevice
554 device = ao_subdevice.device
556 output_dtype = ao_subdevice.get_dtype()
557 if data.dtype != output_dtype:
558 raise ValueError('output dtype %s does not match expected %s'
559 % (data.dtype, output_dtype))
560 input_data = _numpy.ndarray(
561 (n_samps, len(inputs)), dtype=ai_subdevice.get_dtype())
563 _LOG.debug('setup ramp commands')
564 scan_period_ns = int(1e9 / frequency)
565 ai_cmd = ai_subdevice.get_cmd_generic_timed(
566 len(inputs), scan_period_ns)
567 ao_cmd = ao_subdevice.get_cmd_generic_timed(
568 len(output_channels), scan_period_ns)
570 ai_cmd.start_src = TRIG_SRC.int
572 ai_cmd.stop_src = TRIG_SRC.count
573 ai_cmd.stop_arg = n_samps
574 ai_cmd.chanlist = inputs
575 #ao_cmd.start_src = TRIG_SRC.ext
576 #ao_cmd.start_arg = 18 # NI card AI_START1 internal AI start signal
577 ao_cmd.start_src = TRIG_SRC.int
579 ao_cmd.stop_src = TRIG_SRC.count
580 ao_cmd.stop_arg = n_samps-1
581 ao_cmd.chanlist = output_channels
583 ai_subdevice.cmd = ai_cmd
584 ao_subdevice.cmd = ao_cmd
586 rc = ai_subdevice.command_test()
588 _LOG.debug('analog input test %d: %s' % (i, rc))
590 rc = ao_subdevice.command_test()
592 _LOG.debug('analog output test %d: %s' % (i, rc))
594 _LOG.debug('lock subdevices for ramp')
599 _LOG.debug('load ramp commands')
600 ao_subdevice.command()
601 ai_subdevice.command()
603 writer = Writer(ao_subdevice, data)
605 reader = Reader(ai_subdevice, input_data)
608 _LOG.debug('arm analog output')
609 device.do_insn(inttrig_insn(ao_subdevice))
610 _LOG.debug('trigger ramp (via analog input)')
611 device.do_insn(inttrig_insn(ai_subdevice))
612 _LOG.debug('ramp running')
616 _LOG.debug('ramp complete')
618 #_LOG.debug('AI flags: %s' % ai_subdevice.get_flags())
619 ai_subdevice.cancel()
620 ai_subdevice.unlock()
622 # release busy flag, which seems to not be cleared
624 # http://groups.google.com/group/comedi_list/browse_thread/thread/4c7040989197abad/
625 #_LOG.debug('AO flags: %s' % ao_subdevice.get_flags())
626 ao_subdevice.cancel()
627 ao_subdevice.unlock()
628 _LOG.debug('unlocked subdevices after ramp')
630 for i,name in enumerate(output_names):
631 self.last_output[name] = data[-1,i]
633 if _package_config['matplotlib']:
635 raise _matplotlib_import_error
636 figure = _matplotlib_pyplot.figure()
637 axes = figure.add_subplot(1, 1, 1)
639 timestamp = _time.strftime('%H%M%S')
640 axes.set_title('piezo ramp %s' % timestamp)
641 for d,names in [(data, output_names),
642 (input_data, input_names)]:
643 for i,name in enumerate(names):
644 axes.plot(d[:,i], label=name)
648 def named_ramp(self, data, frequency, output_names, input_names=()):
649 input_data = self.ramp(
650 data=data, frequency=frequency, output_names=output_names,
651 input_names=input_names)
653 for i,name in enumerate(output_names):
654 ret[name] = data[:,i]
655 for i,name in enumerate(input_names):
656 ret[name] = input_data[:,i]