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."
20 import numpy as _numpy
23 import matplotlib as _matplotlib
24 import matplotlib.pyplot as _matplotlib_pyplot
25 import time as _time # for timestamping lines on plots
26 except (ImportError, RuntimeError), e:
28 _matplotlib_import_error = e
30 from curses_check_for_keypress import CheckForKeypress as _CheckForKeypress
32 from . import LOG as _LOG
33 from . import base as _base
34 from . import package_config as _package_config
35 from . import surface as _surface
38 class AFMPiezo (_base.Piezo):
39 """A piezo-controlled atomic force microscope.
41 This particular class expects a single input channel for measuring
42 deflection. Other subclasses provide support for multi-segment
43 deflection measurements.
45 >>> from pprint import pprint
46 >>> from pycomedi.device import Device
47 >>> from pycomedi.subdevice import StreamingSubdevice
48 >>> from pycomedi.channel import AnalogChannel
49 >>> from pycomedi.constant import AREF, SUBDEVICE_TYPE, UNIT
50 >>> from . import config
51 >>> from . import surface
53 >>> d = Device('/dev/comedi0')
56 >>> s_in = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
57 ... factory=StreamingSubdevice)
58 >>> s_out = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
59 ... factory=StreamingSubdevice)
61 >>> axis_channel = s_out.channel(
62 ... 0, factory=AnalogChannel, aref=AREF.ground)
63 >>> input_channel = s_in.channel(0, factory=AnalogChannel, aref=AREF.diff)
64 >>> for chan in [axis_channel, input_channel]:
65 ... chan.range = chan.find_range(unit=UNIT.volt, min=-10, max=10)
67 We set the minimum voltage for the `z` axis to -9 (a volt above
68 the minimum possible voltage) to help with testing
69 `.get_surface_position`. Without this minimum voltage, small
70 calibration errors could lead to a railed -10 V input for the
71 first few surface approaching steps, which could lead to an
72 `EdgeKink` error instead of a `FlatFit` error.
74 >>> axis_config = config.AxisConfig()
75 >>> axis_config.update(
76 ... {'gain':20, 'sensitivity':8e-9, 'minimum':-9})
77 >>> axis_config['channel'] = config.OutputChannelConfig()
78 >>> axis_config['channel']['name'] = 'z'
79 >>> input_config = config.InputChannelConfig()
80 >>> input_config['name'] = 'deflection'
82 >>> a = _base.PiezoAxis(config=axis_config, axis_channel=axis_channel)
85 >>> c = _base.InputChannel(config=input_config, channel=input_channel)
88 >>> p = AFMPiezo(axes=[a], inputs=[c], name='Molly')
90 >>> deflection = p.read_deflection()
91 >>> deflection # doctest: +SKIP
93 >>> p.deflection_dtype()
96 We need to know where we are before we can move somewhere
99 >>> pos = _base.convert_volts_to_bits(p.config.select_config(
100 ... 'axes', 'z', get_attribute=_base.get_axis_name)['channel'], 0)
103 Usually `.move_to_pos_or_def` is used to approach the surface, but
104 for testing we assume the z output channel is connected directly
105 into the deflection input channel.
107 >>> target_pos = _base.convert_volts_to_bits(
108 ... p.config.select_config('axes', 'z',
109 ... get_attribute=_base.get_axis_name)['channel'], 2)
110 >>> step = int((target_pos - pos)/5)
111 >>> target_def = _base.convert_volts_to_bits(
112 ... p.config.select_config('inputs', 'deflection'), 3)
113 >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
114 ... return_data=True)
115 >>> p.last_output == {'z': int(target_pos)}
117 >>> pprint(data) # doctest: +SKIP
119 array([32655, 33967, 35280, 36593, 37905, 39218, 39222], dtype=uint16),
121 array([32767, 34077, 35387, 36697, 38007, 39317, 39321], dtype=uint16)}
123 That was a working position-limited approach. Now move back to
124 the center and try a deflection-limited approach.
127 >>> target_def = _base.convert_volts_to_bits(
128 ... p.config.select_config('inputs', 'deflection'), 1)
129 >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
130 ... return_data=True)
131 >>> print (p.last_output['z'] < int(target_pos))
133 >>> pprint(data) # doctest: +SKIP
134 {'deflection': array([32655, 33968, 35281, 36593], dtype=uint16),
135 'z': array([32767, 34077, 35387, 36697], dtype=uint16)}
137 >>> p.wiggle_for_interference('z', offset=p.last_output['z'],
138 ... laser_wavelength=650e-9, keypress_test_mode=True)
139 Press any key to continue
142 ... p.get_surface_position('z', max_deflection=target_def)
143 ... except surface.FlatFit, e:
144 ... print 'got FlatFit'
146 >>> print e # doctest: +SKIP
147 slopes not sufficiently different: 1.0021 and 1.0021
148 >>> abs(e.right_slope-1) < 0.1
150 >>> abs(e.left_slope-1) < 0.1
155 def _deflection_channel(self):
156 return self.channel_by_name(name='deflection', direction='input')
158 def read_deflection(self):
159 """Return sensor deflection in bits.
161 TODO: explain how bit <-> volt conversion will work for this
164 return self._deflection_channel().data_read()
166 def deflection_dtype(self):
167 "Return a Numpy dtype suitable for deflection bit values."
168 return self._deflection_channel().subdevice.get_dtype()
170 def move_to_pos_or_def(self, axis_name, position, deflection, step,
171 return_data=False, pre_move_steps=0):
175 number of 'null' steps to take before moving (confirming a
176 stable input deflection).
178 if return_data or _package_config['matplotlib']:
184 raise ValueError('must have non-zero step size')
185 elif step < 0 and position > self.last_output[axis_name]:
187 elif step > 0 and position < self.last_output[axis_name]:
191 'move to position %d or deflection %g on axis %s in steps of %d'
192 % (position, deflection, axis_name, step))
193 _LOG.debug(log_string)
194 current_deflection = self.read_deflection()
195 log_string = 'current position %d and deflection %g' % (
196 self.last_output[axis_name], current_deflection)
197 _LOG.debug(log_string)
200 def_array=[current_deflection]
201 pos_array=[self.last_output[axis_name]]
202 for i in range(pre_move_steps):
203 self.jump(axis_name, piezo.last_output[axis_name])
204 delection = self.read_deflection()
206 def_array.append(current_deflection)
207 pos_array.append(self.last_output[axis_name])
208 # step in until we hit our target position or exceed our target deflection
209 while (self.last_output[axis_name] != position and
210 current_deflection < deflection):
211 dist_to = position - self.last_output[axis_name]
212 if abs(dist_to) < abs(step):
215 jump_to = self.last_output[axis_name] + step
216 self.jump(axis_name, jump_to)
217 current_deflection = self.read_deflection()
219 'current z piezo position %6d, current deflection %6d'
220 % (current_deflection, self.last_output[axis_name]))
221 _LOG.debug(log_string)
223 def_array.append(current_deflection)
224 pos_array.append(self.last_output[axis_name])
227 'move to position %d or deflection %g on axis %s complete'
228 % (position, deflection, axis_name))
229 _LOG.debug(log_string)
230 log_string = 'current position %d and deflection %g' % (
231 self.last_output[axis_name], current_deflection)
232 _LOG.debug(log_string)
233 if _package_config['matplotlib']:
235 raise _matplotlib_import_error
236 figure = _matplotlib_pyplot.figure()
237 axes = figure.add_subplot(1, 1, 1)
239 timestamp = _time.strftime('%H%M%S')
240 axes.set_title('step approach %s' % timestamp)
241 axes.plot(pos_array, def_array, '.', label=timestamp)
242 #_pylab.legend(loc='best')
247 axis_name:_numpy.array(
248 pos_array, dtype=self.channel_dtype(
249 axis_name, direction='output')),
250 'deflection':_numpy.array(
251 def_array, dtype=self.deflection_dtype()),
255 def wiggle_for_interference(
256 self, axis_name, wiggle_frequency=2, n_samples=1024, amplitude=None,
257 offset=None, laser_wavelength=None, plot=True,
258 keypress_test_mode=False):
259 """Output a sine wave and measure interference.
261 With a poorly focused or aligned laser, leaked laser light
262 reflecting off the surface may interfere with the light
263 reflected off the cantilever, causing distance-dependent
264 interference with a period roughly half the laser's
265 wavelength. This method wiggles the cantilever near the
266 surface and monitors the magnitude of deflection oscillation,
267 allowing the operator to adjust the laser alignment in order
268 to minimize the interference.
270 Modern commercial AFMs with computer-aligned lasers must do
271 something like this automatically.
273 if _package_config['matplotlib']:
275 if laser_wavelength and amplitude:
277 'use either laser_wavelength or amplitude, but not both'
278 _LOG.warn(log_string)
280 if None in (amplitude, offset):
281 output_axis = self.axis_by_name(axis_name)
282 maxdata = output_axis.axis_channel.get_maxdata()
283 midpoint = int(maxdata/2)
287 'generated offset for interference wiggle: %g' % offset)
288 _LOG.debug(log_string)
289 if amplitude == None:
290 if offset <= midpoint:
291 max_amplitude = int(offset)
293 max_amplitude = int(maxdata-offset)
294 offset_meters = _base.convert_bits_to_meters(
295 output_axis.config, offset)
296 bit_wavelength = _base.convert_meters_to_bits(
297 output_axis.config, offset_meters + laser_wavelength
299 amplitude = 2*bit_wavelength
301 'generated amplitude for interference wiggle: %g'
303 _LOG.debug(log_string)
304 if amplitude > max_amplitude:
305 raise ValueError('no room for a two wavelength wiggle')
307 scan_frequency = wiggle_frequency * n_samples
308 out = (amplitude * _numpy.sin(
309 _numpy.arange(n_samples) * 4 * _numpy.pi / float(n_samples))
311 # 4 for 2 periods, so you can judge precision
312 out = out.reshape((len(out), 1)).astype(
313 self.channel_dtype(axis_name, direction='output'))
315 _LOG.debug('oscillate for interference wiggle')
317 'amplitude: %d bits, offset: %d bits, scan frequency: %g Hz'
318 % (amplitude, offset, scan_frequency))
319 _LOG.debug(log_string)
323 raise _matplotlib_import_error
324 figure = _matplotlib_pyplot.figure()
325 axes = figure.add_subplot(1, 1, 1)
327 timestamp = _time.strftime('%H%M%S')
328 axes.set_title('wiggle for interference %s' % timestamp)
329 plot_p = axes.plot(out, out, 'b.-')
331 c = _CheckForKeypress(test_mode=keypress_test_mode)
332 while c.input() == None:
333 # input will need processing for multi-segment AFMs...
334 data = self.ramp(out, scan_frequency, output_names=[axis_name],
335 input_names=['deflection'])
336 _LOG.debug('completed a wiggle round')
338 plot_p[0].set_ydata(data[:,0])
339 axes.set_ylim([data.min(), data.max()])
341 self.last_output[axis_name] = out[-1,0]
342 _LOG.debug('interference wiggle complete')
344 get_surface_position = _surface.get_surface_position
348 # if USE_ABCD_DEFLECTION :
349 # for i in range(4) : # i is the photodiode element (0->A, 1->B, ...)
350 # self.curIn[i] = out["Deflection segment"][i][-1]
352 # self.curIn[self.chan_info.def_ind] = out["deflection"][-1]
355 #class FourSegmentAFM (AFM):
356 # def read_deflection(self):
357 # "Return sensor deflection in bits."
358 # A = int(self.curIn[self.chan_info.def_ind[0]])
359 # B = int(self.curIn[self.chan_info.def_ind[1]])
360 # C = int(self.curIn[self.chan_info.def_ind[2]])
361 # D = int(self.curIn[self.chan_info.def_ind[3]])
362 # df = float((A+B)-(C+D))/(A+B+C+D)
363 # dfout = int(df * 2**15) + 2**15
365 # print "Current deflection %d (%d, %d, %d, %d)" \
366 # % (dfout, A, B, C, D)
370 #def test_smoothness(zp, plotVerbose=True):
373 # setpoint = zp.def_V2in(3)
376 # outarray = linspace(posB, posA, 1000)
379 # curVals = zp.jumpToPos(posA)
380 # zp.pCurVals(curVals)
381 # _sleep(1) # let jitters die down
382 # for i in range(10):
383 # print "ramp %d to %d" % (zp.curPos(), posB)
384 # curVals, data = moveToPosOrDef(zp, posB, setpoint, step=steps,
385 # return_data = True)
386 # indata.append(data)
387 # out = zp.ramp(outarray, outfreq)
388 # outdata.append(out)
390 # from pylab import figure, plot, title, legend, hold, subplot
391 # if PYLAB_VERBOSE or plotVerbose:
393 # _pylab.figure(BASE_FIG_NUM+4)
394 # for i in range(10):
395 # _pylab.plot(indata[i]['z'],
396 # indata[i]['deflection'], '+--', label='in')
397 # _pylab.plot(outdata[i]['z'],
398 # outdata[i]['deflection'], '.-', label='out')
399 # _pylab.title('test smoothness (step in, ramp out)')
400 # #_pylab.legend(loc='best')
404 # zp = z_piezo.z_piezo()
405 # curVals = zp.moveToPosOrDef(zp.pos_nm2out(600), defl=zp.curDef()+6000, step=(zp.pos_nm2out(10)-zp.pos_nm2out(0)))
407 # zp.pCurVals(curVals)
408 # pos = zp.getSurfPos(maxDefl=zp.curDef()+6000)
410 # print "Surface at %g nm", pos
412 # if PYLAB_VERBOSE and _final_flush_plot != None:
413 # _final_flush_plot()