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 "Control of a piezo-based atomic force microscope."
22 import numpy as _numpy
25 import matplotlib as _matplotlib
26 import matplotlib.pyplot as _matplotlib_pyplot
27 except (ImportError, RuntimeError), e:
29 _matplotlib_import_error = e
31 from curses_check_for_keypress import CheckForKeypress as _CheckForKeypress
33 from . import LOG as _LOG
34 from . import base as _base
35 from . import package_config as _package_config
36 from . import surface as _surface
39 class AFMPiezo (_base.Piezo):
40 """A piezo-controlled atomic force microscope.
42 This particular class expects a single input channel for measuring
43 deflection. Other subclasses provide support for multi-segment
44 deflection measurements.
46 >>> from pprint import pprint
47 >>> from pycomedi.device import Device
48 >>> from pycomedi.subdevice import StreamingSubdevice
49 >>> from pycomedi.channel import AnalogChannel
50 >>> from pycomedi.constant import AREF, SUBDEVICE_TYPE, UNIT
51 >>> from . import config
52 >>> from . import surface
54 >>> d = Device('/dev/comedi0')
57 >>> s_in = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
58 ... factory=StreamingSubdevice)
59 >>> s_out = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
60 ... factory=StreamingSubdevice)
62 >>> axis_channel = s_out.channel(
63 ... 0, factory=AnalogChannel, aref=AREF.ground)
64 >>> input_channel = s_in.channel(0, factory=AnalogChannel, aref=AREF.diff)
65 >>> for chan in [axis_channel, input_channel]:
66 ... chan.range = chan.find_range(unit=UNIT.volt, min=-10, max=10)
68 We set the minimum voltage for the `z` axis to -9 (a volt above
69 the minimum possible voltage) to help with testing
70 `.get_surface_position`. Without this minimum voltage, small
71 calibration errors could lead to a railed -10 V input for the
72 first few surface approaching steps, which could lead to an
73 `EdgeKink` error instead of a `FlatFit` error.
75 >>> axis_config = config.AxisConfig()
76 >>> axis_config.update(
77 ... {'gain':20, 'sensitivity':8e-9, 'minimum':-9})
78 >>> axis_config['channel'] = config.OutputChannelConfig()
79 >>> axis_config['channel']['name'] = 'z'
80 >>> input_config = config.InputChannelConfig()
81 >>> input_config['name'] = 'deflection'
83 >>> a = _base.PiezoAxis(config=axis_config, axis_channel=axis_channel)
86 >>> c = _base.InputChannel(config=input_config, channel=input_channel)
89 >>> p = AFMPiezo(axes=[a], inputs=[c], name='Molly')
91 >>> deflection = p.read_deflection()
92 >>> deflection # doctest: +SKIP
94 >>> p.deflection_dtype()
97 We need to know where we are before we can move somewhere
100 >>> pos = _base.convert_volts_to_bits(p.config.select_config(
101 ... 'axes', 'z', get_attribute=_base.get_axis_name)['channel'], 0)
104 Usually `.move_to_pos_or_def` is used to approach the surface, but
105 for testing we assume the z output channel is connected directly
106 into the deflection input channel.
108 >>> target_pos = _base.convert_volts_to_bits(
109 ... p.config.select_config('axes', 'z',
110 ... get_attribute=_base.get_axis_name)['channel'], 2)
111 >>> step = int((target_pos - pos)/5)
112 >>> target_def = _base.convert_volts_to_bits(
113 ... p.config.select_config('inputs', 'deflection'), 3)
114 >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
115 ... return_data=True)
116 >>> p.last_output == {'z': int(target_pos)}
118 >>> pprint(data) # doctest: +SKIP
120 array([32655, 33967, 35280, 36593, 37905, 39218, 39222], dtype=uint16),
122 array([32767, 34077, 35387, 36697, 38007, 39317, 39321], dtype=uint16)}
124 That was a working position-limited approach. Now move back to
125 the center and try a deflection-limited approach.
128 >>> target_def = _base.convert_volts_to_bits(
129 ... p.config.select_config('inputs', 'deflection'), 1)
130 >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
131 ... return_data=True)
132 >>> print (p.last_output['z'] < int(target_pos))
134 >>> pprint(data) # doctest: +SKIP
135 {'deflection': array([32655, 33968, 35281, 36593], dtype=uint16),
136 'z': array([32767, 34077, 35387, 36697], dtype=uint16)}
138 >>> p.wiggle_for_interference('z', offset=p.last_output['z'],
139 ... laser_wavelength=650e-9, keypress_test_mode=True)
140 Press any key to continue
143 ... p.get_surface_position('z', max_deflection=target_def)
144 ... except surface.FlatFit, e:
145 ... print 'got FlatFit'
147 >>> print e # doctest: +SKIP
148 slopes not sufficiently different: 1.0021 and 1.0021
149 >>> abs(e.right_slope-1) < 0.1
151 >>> abs(e.left_slope-1) < 0.1
156 def _deflection_channel(self):
157 return self.channel_by_name(name='deflection', direction='input')
159 def read_deflection(self):
160 """Return sensor deflection in bits.
162 TODO: explain how bit <-> volt conversion will work for this
165 return self._deflection_channel().data_read()
167 def deflection_dtype(self):
168 "Return a Numpy dtype suitable for deflection bit values."
169 return self._deflection_channel().subdevice.get_dtype()
171 def move_to_pos_or_def(self, axis_name, position=None, deflection=None,
172 step=1, return_data=False, pre_move_steps=0,
177 number of 'null' steps to take before moving (confirming a
178 stable input deflection).
180 The target step frequency in hertz. If `Null`, go as fast
181 as possible. Note that this is software timing, so it
182 should not be relied upon for precise results.
184 if position is None and deflection is None:
185 raise ValueError('must specify position, deflection, or both')
187 if return_data or _package_config['matplotlib']:
193 # default to the extreme value in the step direction
195 axis = self.axis_by_name(axis_name)
196 position = axis.channel.get_maxdata()
199 elif deflection is None:
200 # default to the extreme value
201 channel = self._deflection_channel(self)
202 deflection = channel.get_maxdata()
205 raise ValueError('must have non-zero step size')
206 elif step < 0 and position > self.last_output[axis_name]:
208 elif step > 0 and position < self.last_output[axis_name]:
212 'move to position %d or deflection %g on axis %s in steps of %d'
213 % (position, deflection, axis_name, step))
214 _LOG.debug(log_string)
215 current_deflection = self.read_deflection()
216 log_string = 'current position %d and deflection %g' % (
217 self.last_output[axis_name], current_deflection)
218 _LOG.debug(log_string)
221 def_array=[current_deflection]
222 pos_array=[self.last_output[axis_name]]
223 for i in range(pre_move_steps):
224 self.jump(axis_name, piezo.last_output[axis_name])
225 delection = self.read_deflection()
227 def_array.append(current_deflection)
228 pos_array.append(self.last_output[axis_name])
229 if frequency is not None:
230 time_step = 1./frequency
231 next_time = _time.time() + time_step
232 # step in until we hit our target position or exceed our target deflection
233 while (self.last_output[axis_name] != position and
234 current_deflection < deflection):
235 dist_to = position - self.last_output[axis_name]
236 if abs(dist_to) < abs(step):
239 jump_to = self.last_output[axis_name] + step
240 self.jump(axis_name, jump_to)
241 current_deflection = self.read_deflection()
243 'current z piezo position %6d, current deflection %6d'
244 % (current_deflection, self.last_output[axis_name]))
245 _LOG.debug(log_string)
247 def_array.append(current_deflection)
248 pos_array.append(self.last_output[axis_name])
249 if frequency is not None:
252 _time.sleep(next_time - now)
253 next_time += time_step
256 'move to position %d or deflection %g on axis %s complete'
257 % (position, deflection, axis_name))
258 _LOG.debug(log_string)
259 log_string = 'current position %d and deflection %g' % (
260 self.last_output[axis_name], current_deflection)
261 _LOG.debug(log_string)
262 if _package_config['matplotlib']:
264 raise _matplotlib_import_error
265 figure = _matplotlib_pyplot.figure()
266 axes = figure.add_subplot(1, 1, 1)
268 timestamp = _time.strftime('%H%M%S')
269 axes.set_title('step approach %s' % timestamp)
270 axes.plot(pos_array, def_array, '.', label=timestamp)
271 #_pylab.legend(loc='best')
276 axis_name:_numpy.array(
277 pos_array, dtype=self.channel_dtype(
278 axis_name, direction='output')),
279 'deflection':_numpy.array(
280 def_array, dtype=self.deflection_dtype()),
284 def wiggle_for_interference(
285 self, axis_name, wiggle_frequency=2, n_samples=1024, amplitude=None,
286 offset=None, laser_wavelength=None, plot=True,
287 keypress_test_mode=False):
288 """Output a sine wave and measure interference.
290 With a poorly focused or aligned laser, leaked laser light
291 reflecting off the surface may interfere with the light
292 reflected off the cantilever, causing distance-dependent
293 interference with a period roughly half the laser's
294 wavelength. This method wiggles the cantilever near the
295 surface and monitors the magnitude of deflection oscillation,
296 allowing the operator to adjust the laser alignment in order
297 to minimize the interference.
299 Modern commercial AFMs with computer-aligned lasers must do
300 something like this automatically.
302 if _package_config['matplotlib']:
304 if laser_wavelength and amplitude:
306 'use either laser_wavelength or amplitude, but not both'
307 _LOG.warn(log_string)
309 if None in (amplitude, offset):
310 output_axis = self.axis_by_name(axis_name)
311 maxdata = output_axis.axis_channel.get_maxdata()
312 midpoint = int(maxdata/2)
316 'generated offset for interference wiggle: %g' % offset)
317 _LOG.debug(log_string)
318 if amplitude == None:
319 if offset <= midpoint:
320 max_amplitude = int(offset)
322 max_amplitude = int(maxdata-offset)
323 offset_meters = _base.convert_bits_to_meters(
324 output_axis.config, offset)
325 bit_wavelength = _base.convert_meters_to_bits(
326 output_axis.config, offset_meters + laser_wavelength
328 amplitude = 2*bit_wavelength
330 'generated amplitude for interference wiggle: %g'
332 _LOG.debug(log_string)
333 if amplitude > max_amplitude:
334 raise ValueError('no room for a two wavelength wiggle')
336 scan_frequency = wiggle_frequency * n_samples
337 out = (amplitude * _numpy.sin(
338 _numpy.arange(n_samples) * 4 * _numpy.pi / float(n_samples))
340 # 4 for 2 periods, so you can judge precision
341 out = out.reshape((len(out), 1)).astype(
342 self.channel_dtype(axis_name, direction='output'))
344 _LOG.debug('oscillate for interference wiggle')
346 'amplitude: %d bits, offset: %d bits, scan frequency: %g Hz'
347 % (amplitude, offset, scan_frequency))
348 _LOG.debug(log_string)
352 raise _matplotlib_import_error
353 figure = _matplotlib_pyplot.figure()
354 axes = figure.add_subplot(1, 1, 1)
356 timestamp = _time.strftime('%H%M%S')
357 axes.set_title('wiggle for interference %s' % timestamp)
358 plot_p = axes.plot(out, out, 'b.-')
360 c = _CheckForKeypress(test_mode=keypress_test_mode)
361 while c.input() == None:
362 # input will need processing for multi-segment AFMs...
363 data = self.ramp(out, scan_frequency, output_names=[axis_name],
364 input_names=['deflection'])
365 _LOG.debug('completed a wiggle round')
367 plot_p[0].set_ydata(data[:,0])
368 axes.set_ylim([data.min(), data.max()])
370 self.last_output[axis_name] = out[-1,0]
371 _LOG.debug('interference wiggle complete')
373 get_surface_position = _surface.get_surface_position
377 # if USE_ABCD_DEFLECTION :
378 # for i in range(4) : # i is the photodiode element (0->A, 1->B, ...)
379 # self.curIn[i] = out["Deflection segment"][i][-1]
381 # self.curIn[self.chan_info.def_ind] = out["deflection"][-1]
384 #class FourSegmentAFM (AFM):
385 # def read_deflection(self):
386 # "Return sensor deflection in bits."
387 # A = int(self.curIn[self.chan_info.def_ind[0]])
388 # B = int(self.curIn[self.chan_info.def_ind[1]])
389 # C = int(self.curIn[self.chan_info.def_ind[2]])
390 # D = int(self.curIn[self.chan_info.def_ind[3]])
391 # df = float((A+B)-(C+D))/(A+B+C+D)
392 # dfout = int(df * 2**15) + 2**15
394 # print "Current deflection %d (%d, %d, %d, %d)" \
395 # % (dfout, A, B, C, D)
399 #def test_smoothness(zp, plotVerbose=True):
402 # setpoint = zp.def_V2in(3)
405 # outarray = linspace(posB, posA, 1000)
408 # curVals = zp.jumpToPos(posA)
409 # zp.pCurVals(curVals)
410 # _sleep(1) # let jitters die down
411 # for i in range(10):
412 # print "ramp %d to %d" % (zp.curPos(), posB)
413 # curVals, data = moveToPosOrDef(zp, posB, setpoint, step=steps,
414 # return_data = True)
415 # indata.append(data)
416 # out = zp.ramp(outarray, outfreq)
417 # outdata.append(out)
419 # from pylab import figure, plot, title, legend, hold, subplot
420 # if PYLAB_VERBOSE or plotVerbose:
422 # _pylab.figure(BASE_FIG_NUM+4)
423 # for i in range(10):
424 # _pylab.plot(indata[i]['z'],
425 # indata[i]['deflection'], '+--', label='in')
426 # _pylab.plot(outdata[i]['z'],
427 # outdata[i]['deflection'], '.-', label='out')
428 # _pylab.title('test smoothness (step in, ramp out)')
429 # #_pylab.legend(loc='best')
433 # zp = z_piezo.z_piezo()
434 # curVals = zp.moveToPosOrDef(zp.pos_nm2out(600), defl=zp.curDef()+6000, step=(zp.pos_nm2out(10)-zp.pos_nm2out(0)))
436 # zp.pCurVals(curVals)
437 # pos = zp.getSurfPos(maxDefl=zp.curDef()+6000)
439 # print "Surface at %g nm", pos
441 # if PYLAB_VERBOSE and _final_flush_plot != None:
442 # _final_flush_plot()