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 "Control of a piezo-based atomic force microscope."
21 import numpy as _numpy
24 import matplotlib as _matplotlib
25 import matplotlib.pyplot as _matplotlib_pyplot
26 except (ImportError, RuntimeError), e:
28 _matplotlib_import_error = e
30 from . import LOG as _LOG
31 from . import base as _base
32 from . import package_config as _package_config
33 from . import surface as _surface
34 from . import wiggle as _wiggle
37 class AFMPiezo (_base.Piezo):
38 """A piezo-controlled atomic force microscope.
40 This particular class expects a single input channel for measuring
41 deflection. Other subclasses provide support for multi-segment
42 deflection measurements.
44 >>> from pprint import pprint
45 >>> from pycomedi.device import Device
46 >>> from pycomedi.constant import AREF
47 >>> from . import config
48 >>> from . import surface
50 >>> d = Device('/dev/comedi0')
53 >>> piezo_config = config.PiezoConfig()
54 >>> piezo_config['name'] = 'Molly'
55 >>> piezo_config['axes'] = [config.AxisConfig()]
56 >>> piezo_config['axes'][0]['channel'] = config.OutputChannelConfig()
57 >>> piezo_config['axes'][0]['channel']['analog-reference'] = AREF.ground
58 >>> piezo_config['axes'][0]['channel']['name'] = 'z'
59 >>> piezo_config['inputs'] = [config.InputChannelConfig()]
60 >>> piezo_config['inputs'][0]['analog-reference'] = AREF.diff
61 >>> piezo_config['inputs'][0]['name'] = 'deflection'
63 We set the minimum voltage for the `z` axis to -9 (a volt above
64 the minimum possible voltage) to help with testing
65 `.get_surface_position`. Without this minimum voltage, small
66 calibration errors could lead to a railed -10 V input for the
67 first few surface approaching steps, which could lead to an
68 `EdgeKink` error instead of a `FlatFit` error.
70 >>> piezo_config['axes'][0].update(
71 ... {'gain':20, 'sensitivity':8e-9, 'minimum':-9})
73 >>> a = _base.PiezoAxis(config=piezo_config['axes'][0], devices=[d])
74 >>> c = _base.InputChannel(config=piezo_config['inputs'][0], devices=[d])
76 >>> p = AFMPiezo(config=piezo_config, axes=[a], inputs=[c])
79 >>> deflection = p.read_deflection()
80 >>> deflection # doctest: +SKIP
82 >>> p.deflection_dtype()
85 We need to know where we are before we can move somewhere
88 >>> pos = _base.convert_volts_to_bits(p.config.select_config(
89 ... 'axes', 'z', get_attribute=_base.get_axis_name)['channel'], 0)
92 Usually `.move_to_pos_or_def` is used to approach the surface, but
93 for testing we assume the z output channel is connected directly
94 into the deflection input channel.
96 >>> target_pos = _base.convert_volts_to_bits(
97 ... p.config.select_config('axes', 'z',
98 ... get_attribute=_base.get_axis_name)['channel'], 2)
99 >>> step = int((target_pos - pos)/5)
100 >>> target_def = _base.convert_volts_to_bits(
101 ... p.config.select_config('inputs', 'deflection'), 3)
102 >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
103 ... return_data=True)
104 >>> p.last_output == {'z': int(target_pos)}
106 >>> pprint(data) # doctest: +SKIP
108 array([32655, 33967, 35280, 36593, 37905, 39218, 39222], dtype=uint16),
110 array([32767, 34077, 35387, 36697, 38007, 39317, 39321], dtype=uint16)}
112 That was a working position-limited approach. Now move back to
113 the center and try a deflection-limited approach.
116 >>> target_def = _base.convert_volts_to_bits(
117 ... p.config.select_config('inputs', 'deflection'), 1)
118 >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
119 ... return_data=True)
120 >>> print (p.last_output['z'] < int(target_pos))
122 >>> pprint(data) # doctest: +SKIP
123 {'deflection': array([32655, 33968, 35281, 36593], dtype=uint16),
124 'z': array([32767, 34077, 35387, 36697], dtype=uint16)}
126 >>> wiggle_config = config.WiggleConfig()
127 >>> wiggle_config['offset'] = p.last_output['z']
128 >>> wiggle_config['wavelength'] = 650e-9
129 >>> p.wiggle_for_interference(config=wiggle_config,
130 ... keypress_test_mode=True)
131 Press any key to continue
134 ... p.get_surface_position('z', max_deflection=target_def)
135 ... except surface.FlatFit, e:
136 ... print 'got FlatFit'
138 >>> print e # doctest: +SKIP
139 slopes not sufficiently different: 1.0021 and 1.0021
140 >>> abs(e.right_slope-1) < 0.1
142 >>> abs(e.left_slope-1) < 0.1
147 def _deflection_channel(self):
148 return self.channel_by_name(name='deflection', direction='input')
150 def read_deflection(self):
151 """Return sensor deflection in bits.
153 TODO: explain how bit <-> volt conversion will work for this
156 return self._deflection_channel().data_read()
158 def deflection_dtype(self):
159 "Return a Numpy dtype suitable for deflection bit values."
160 return self._deflection_channel().subdevice.get_dtype()
162 def move_to_pos_or_def(self, axis_name, position=None, deflection=None,
163 step=1, return_data=False, pre_move_steps=0,
168 number of 'null' steps to take before moving (confirming a
169 stable input deflection).
171 The target step frequency in hertz. If `Null`, go as fast
172 as possible. Note that this is software timing, so it
173 should not be relied upon for precise results.
175 if position is None and deflection is None:
176 raise ValueError('must specify position, deflection, or both')
178 if return_data or _package_config['matplotlib']:
184 # default to the extreme value in the step direction
186 axis = self.axis_by_name(axis_name)
187 position = axis.axis_channel.get_maxdata()
190 elif deflection is None:
191 # default to the extreme value
192 channel = self._deflection_channel(self)
193 deflection = channel.get_maxdata()
194 position = int(position) # round down to nearest integer
197 raise ValueError('must have non-zero step size')
198 elif step < 0 and position > self.last_output[axis_name]:
200 elif step > 0 and position < self.last_output[axis_name]:
204 'move to position %d or deflection %g on axis %s in steps of %d'
205 % (position, deflection, axis_name, step))
206 _LOG.debug(log_string)
207 current_deflection = self.read_deflection()
208 log_string = 'current position %d and deflection %g' % (
209 self.last_output[axis_name], current_deflection)
210 _LOG.debug(log_string)
213 def_array=[current_deflection]
214 pos_array=[self.last_output[axis_name]]
215 for i in range(pre_move_steps):
216 self.jump(axis_name, piezo.last_output[axis_name])
217 current_deflection = self.read_deflection()
219 def_array.append(current_deflection)
220 pos_array.append(self.last_output[axis_name])
221 if frequency is not None:
222 time_step = 1./frequency
223 next_time = _time.time() + time_step
224 # step in until we hit our target position or exceed our target deflection
225 while (self.last_output[axis_name] != position and
226 current_deflection < deflection):
227 dist_to = position - self.last_output[axis_name]
228 if abs(dist_to) < abs(step):
231 jump_to = self.last_output[axis_name] + step
232 self.jump(axis_name, jump_to)
233 current_deflection = self.read_deflection()
235 ('current z piezo position {} (target {}), '
236 'current deflection {} (target {})').format(
237 self.last_output[axis_name], position,
238 current_deflection, deflection))
239 _LOG.debug(log_string)
241 def_array.append(current_deflection)
242 pos_array.append(self.last_output[axis_name])
243 if frequency is not None:
246 _time.sleep(next_time - now)
247 next_time += time_step
250 'move to position %d or deflection %g on axis %s complete'
251 % (position, deflection, axis_name))
252 _LOG.debug(log_string)
253 log_string = 'current position %d and deflection %g' % (
254 self.last_output[axis_name], current_deflection)
255 _LOG.debug(log_string)
256 if _package_config['matplotlib']:
258 raise _matplotlib_import_error
259 figure = _matplotlib_pyplot.figure()
260 axes = figure.add_subplot(1, 1, 1)
262 timestamp = _time.strftime('%H%M%S')
263 axes.set_title('step approach %s' % timestamp)
264 axes.plot(pos_array, def_array, '.', label=timestamp)
265 #_pylab.legend(loc='best')
270 axis_name:_numpy.array(
271 pos_array, dtype=self.channel_dtype(
272 axis_name, direction='output')),
273 'deflection':_numpy.array(
274 def_array, dtype=self.deflection_dtype()),
278 wiggle_for_interference = _wiggle.wiggle_for_interference
279 get_surface_position = _surface.get_surface_position
283 # if USE_ABCD_DEFLECTION :
284 # for i in range(4) : # i is the photodiode element (0->A, 1->B, ...)
285 # self.curIn[i] = out["Deflection segment"][i][-1]
287 # self.curIn[self.chan_info.def_ind] = out["deflection"][-1]
290 #class FourSegmentAFM (AFM):
291 # def read_deflection(self):
292 # "Return sensor deflection in bits."
293 # A = int(self.curIn[self.chan_info.def_ind[0]])
294 # B = int(self.curIn[self.chan_info.def_ind[1]])
295 # C = int(self.curIn[self.chan_info.def_ind[2]])
296 # D = int(self.curIn[self.chan_info.def_ind[3]])
297 # df = float((A+B)-(C+D))/(A+B+C+D)
298 # dfout = int(df * 2**15) + 2**15
300 # print "Current deflection %d (%d, %d, %d, %d)" \
301 # % (dfout, A, B, C, D)
305 #def test_smoothness(zp, plotVerbose=True):
308 # setpoint = zp.def_V2in(3)
311 # outarray = linspace(posB, posA, 1000)
314 # curVals = zp.jumpToPos(posA)
315 # zp.pCurVals(curVals)
316 # _sleep(1) # let jitters die down
317 # for i in range(10):
318 # print "ramp %d to %d" % (zp.curPos(), posB)
319 # curVals, data = moveToPosOrDef(zp, posB, setpoint, step=steps,
320 # return_data = True)
321 # indata.append(data)
322 # out = zp.ramp(outarray, outfreq)
323 # outdata.append(out)
325 # from pylab import figure, plot, title, legend, hold, subplot
326 # if PYLAB_VERBOSE or plotVerbose:
328 # _pylab.figure(BASE_FIG_NUM+4)
329 # for i in range(10):
330 # _pylab.plot(indata[i]['z'],
331 # indata[i]['deflection'], '+--', label='in')
332 # _pylab.plot(outdata[i]['z'],
333 # outdata[i]['deflection'], '.-', label='out')
334 # _pylab.title('test smoothness (step in, ramp out)')
335 # #_pylab.legend(loc='best')
339 # zp = z_piezo.z_piezo()
340 # curVals = zp.moveToPosOrDef(zp.pos_nm2out(600), defl=zp.curDef()+6000, step=(zp.pos_nm2out(10)-zp.pos_nm2out(0)))
342 # zp.pCurVals(curVals)
343 # pos = zp.getSurfPos(maxDefl=zp.curDef()+6000)
345 # print "Surface at %g nm", pos
347 # if PYLAB_VERBOSE and _final_flush_plot != None:
348 # _final_flush_plot()