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 ... {'name': 'z', 'gain':20, 'sensitivity':8e-9, 'minimum':-9})
77 >>> axis_config['channel'] = config.OutputChannelConfig()
78 >>> input_config = config.InputChannelConfig()
79 >>> input_config['name'] = 'deflection'
81 >>> a = _base.PiezoAxis(config=axis_config, axis_channel=axis_channel)
84 >>> c = _base.InputChannel(config=input_config, channel=input_channel)
87 >>> p = AFMPiezo(axes=[a], channels=[c], name='Molly')
89 >>> deflection = p.read_deflection()
90 >>> deflection # doctest: +SKIP
92 >>> p.deflection_dtype()
95 We need to know where we are before we can move somewhere
98 >>> pos = _base.convert_volts_to_bits(p.config['axes'][0]['channel'], 0)
101 Usually `.move_to_pos_or_def` is used to approach the surface, but
102 for testing we assume the z output channel is connected directly
103 into the deflection input channel.
105 >>> target_pos = _base.convert_volts_to_bits(
106 ... p.config['axes'][0]['channel'], 2)
107 >>> step = int((target_pos - pos)/5)
108 >>> target_def = _base.convert_volts_to_bits(p.config['inputs'][0], 3)
109 >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
110 ... return_data=True)
111 >>> p.last_output == {'z': int(target_pos)}
113 >>> pprint(data) # doctest: +SKIP
115 array([32655, 33967, 35280, 36593, 37905, 39218, 39222], dtype=uint16),
117 array([32767, 34077, 35387, 36697, 38007, 39317, 39321], dtype=uint16)}
119 That was a working position-limited approach. Now move back to
120 the center and try a deflection-limited approach.
123 >>> target_def = _base.convert_volts_to_bits(
124 ... p.config['inputs'][0]['channel'], 1)
125 >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
126 ... return_data=True)
127 >>> print (p.last_output['z'] < int(target_pos))
129 >>> pprint(data) # doctest: +SKIP
130 {'deflection': array([32655, 33968, 35281, 36593], dtype=uint16),
131 'z': array([32767, 34077, 35387, 36697], dtype=uint16)}
133 >>> p.wiggle_for_interference('z', offset=p.last_output['z'],
134 ... laser_wavelength=650e-9, keypress_test_mode=True)
135 Press any key to continue
138 ... p.get_surface_position('z', max_deflection=target_def)
139 ... except surface.FlatFit, e:
140 ... print 'got FlatFit'
142 >>> print e # doctest: +SKIP
143 slopes not sufficiently different: 1.0021 and 1.0021
144 >>> abs(e.right_slope-1) < 0.1
146 >>> abs(e.left_slope-1) < 0.1
151 def _deflection_channel(self):
152 return self.channel_by_name(name='deflection', direction='input')
154 def read_deflection(self):
155 """Return sensor deflection in bits.
157 TODO: explain how bit <-> volt conversion will work for this
160 return self._deflection_channel().data_read()
162 def deflection_dtype(self):
163 "Return a Numpy dtype suitable for deflection bit values."
164 return self._deflection_channel().subdevice.get_dtype()
166 def move_to_pos_or_def(self, axis_name, position, deflection, step,
167 return_data=False, pre_move_steps=0):
171 number of 'null' steps to take before moving (confirming a
172 stable input deflection).
174 if return_data or _package_config['matplotlib']:
180 raise ValueError('must have non-zero step size')
181 elif step < 0 and position > self.last_output[axis_name]:
183 elif step > 0 and position < self.last_output[axis_name]:
187 'move to position %d or deflection %g on axis %s in steps of %d'
188 % (position, deflection, axis_name, step))
189 _LOG.debug(log_string)
190 current_deflection = self.read_deflection()
191 log_string = 'current position %d and deflection %g' % (
192 self.last_output[axis_name], current_deflection)
193 _LOG.debug(log_string)
196 def_array=[current_deflection]
197 pos_array=[self.last_output[axis_name]]
198 for i in range(pre_move_steps):
199 self.jump(axis_name, piezo.last_output[axis_name])
200 delection = self.read_deflection()
202 def_array.append(current_deflection)
203 pos_array.append(self.last_output[axis_name])
204 # step in until we hit our target position or exceed our target deflection
205 while (self.last_output[axis_name] != position and
206 current_deflection < deflection):
207 dist_to = position - self.last_output[axis_name]
208 if abs(dist_to) < abs(step):
211 jump_to = self.last_output[axis_name] + step
212 self.jump(axis_name, jump_to)
213 current_deflection = self.read_deflection()
215 'current z piezo position %6d, current deflection %6d'
216 % (current_deflection, self.last_output[axis_name]))
217 _LOG.debug(log_string)
219 def_array.append(current_deflection)
220 pos_array.append(self.last_output[axis_name])
223 'move to position %d or deflection %g on axis %s complete'
224 % (position, deflection, axis_name))
225 _LOG.debug(log_string)
226 log_string = 'current position %d and deflection %g' % (
227 self.last_output[axis_name], current_deflection)
228 _LOG.debug(log_string)
229 if _package_config['matplotlib']:
231 raise _matplotlib_import_error
232 figure = _matplotlib_pyplot.figure()
233 axes = figure.add_subplot(1, 1, 1)
235 timestamp = _time.strftime('%H%M%S')
236 axes.set_title('step approach %s' % timestamp)
237 axes.plot(pos_array, def_array, '.', label=timestamp)
238 #_pylab.legend(loc='best')
243 axis_name:_numpy.array(
244 pos_array, dtype=self.channel_dtype(
245 axis_name, direction='output')),
246 'deflection':_numpy.array(
247 def_array, dtype=self.deflection_dtype()),
251 def wiggle_for_interference(
252 self, axis_name, wiggle_frequency=2, n_samples=1024, amplitude=None,
253 offset=None, laser_wavelength=None, plot=True,
254 keypress_test_mode=False):
255 """Output a sine wave and measure interference.
257 With a poorly focused or aligned laser, leaked laser light
258 reflecting off the surface may interfere with the light
259 reflected off the cantilever, causing distance-dependent
260 interference with a period roughly half the laser's
261 wavelength. This method wiggles the cantilever near the
262 surface and monitors the magnitude of deflection oscillation,
263 allowing the operator to adjust the laser alignment in order
264 to minimize the interference.
266 Modern commercial AFMs with computer-aligned lasers must do
267 something like this automatically.
269 if _package_config['matplotlib']:
271 if laser_wavelength and amplitude:
273 'use either laser_wavelength or amplitude, but not both'
274 _LOG.warn(log_string)
276 if None in (amplitude, offset):
277 output_axis = self.axis_by_name(axis_name)
278 maxdata = output_axis.axis_channel.get_maxdata()
279 midpoint = int(maxdata/2)
283 'generated offset for interference wiggle: %g' % offset)
284 _LOG.debug(log_string)
285 if amplitude == None:
286 if offset <= midpoint:
287 max_amplitude = int(offset)
289 max_amplitude = int(maxdata-offset)
290 offset_meters = _base.convert_bits_to_meters(
291 output_axis.config, offset)
292 bit_wavelength = _base.convert_meters_to_bits(
293 output_axis.config, offset_meters + laser_wavelength
295 amplitude = 2*bit_wavelength
297 'generated amplitude for interference wiggle: %g'
299 _LOG.debug(log_string)
300 if amplitude > max_amplitude:
301 raise ValueError('no room for a two wavelength wiggle')
303 scan_frequency = wiggle_frequency * n_samples
304 out = (amplitude * _numpy.sin(
305 _numpy.arange(n_samples) * 4 * _numpy.pi / float(n_samples))
307 # 4 for 2 periods, so you can judge precision
308 out = out.reshape((len(out), 1)).astype(
309 self.channel_dtype(axis_name, direction='output'))
311 _LOG.debug('oscillate for interference wiggle')
313 'amplitude: %d bits, offset: %d bits, scan frequency: %g Hz'
314 % (amplitude, offset, scan_frequency))
315 _LOG.debug(log_string)
319 raise _matplotlib_import_error
320 figure = _matplotlib_pyplot.figure()
321 axes = figure.add_subplot(1, 1, 1)
323 timestamp = _time.strftime('%H%M%S')
324 axes.set_title('wiggle for interference %s' % timestamp)
325 plot_p = axes.plot(out, out, 'b.-')
327 c = _CheckForKeypress(test_mode=keypress_test_mode)
328 while c.input() == None:
329 # input will need processing for multi-segment AFMs...
330 data = self.ramp(out, scan_frequency, output_names=[axis_name],
331 input_names=['deflection'])
332 _LOG.debug('completed a wiggle round')
334 plot_p[0].set_ydata(data[:,0])
335 axes.set_ylim([data.min(), data.max()])
337 self.last_output[axis_name] = out[-1,0]
338 _LOG.debug('interference wiggle complete')
340 get_surface_position = _surface.get_surface_position
344 # if USE_ABCD_DEFLECTION :
345 # for i in range(4) : # i is the photodiode element (0->A, 1->B, ...)
346 # self.curIn[i] = out["Deflection segment"][i][-1]
348 # self.curIn[self.chan_info.def_ind] = out["deflection"][-1]
351 #class FourSegmentAFM (AFM):
352 # def read_deflection(self):
353 # "Return sensor deflection in bits."
354 # A = int(self.curIn[self.chan_info.def_ind[0]])
355 # B = int(self.curIn[self.chan_info.def_ind[1]])
356 # C = int(self.curIn[self.chan_info.def_ind[2]])
357 # D = int(self.curIn[self.chan_info.def_ind[3]])
358 # df = float((A+B)-(C+D))/(A+B+C+D)
359 # dfout = int(df * 2**15) + 2**15
361 # print "Current deflection %d (%d, %d, %d, %d)" \
362 # % (dfout, A, B, C, D)
366 #def test_smoothness(zp, plotVerbose=True):
369 # setpoint = zp.def_V2in(3)
372 # outarray = linspace(posB, posA, 1000)
375 # curVals = zp.jumpToPos(posA)
376 # zp.pCurVals(curVals)
377 # _sleep(1) # let jitters die down
378 # for i in range(10):
379 # print "ramp %d to %d" % (zp.curPos(), posB)
380 # curVals, data = moveToPosOrDef(zp, posB, setpoint, step=steps,
381 # return_data = True)
382 # indata.append(data)
383 # out = zp.ramp(outarray, outfreq)
384 # outdata.append(out)
386 # from pylab import figure, plot, title, legend, hold, subplot
387 # if PYLAB_VERBOSE or plotVerbose:
389 # _pylab.figure(BASE_FIG_NUM+4)
390 # for i in range(10):
391 # _pylab.plot(indata[i]['z'],
392 # indata[i]['deflection'], '+--', label='in')
393 # _pylab.plot(outdata[i]['z'],
394 # outdata[i]['deflection'], '.-', label='out')
395 # _pylab.title('test smoothness (step in, ramp out)')
396 # #_pylab.legend(loc='best')
400 # zp = z_piezo.z_piezo()
401 # curVals = zp.moveToPosOrDef(zp.pos_nm2out(600), defl=zp.curDef()+6000, step=(zp.pos_nm2out(10)-zp.pos_nm2out(0)))
403 # zp.pCurVals(curVals)
404 # pos = zp.getSurfPos(maxDefl=zp.curDef()+6000)
406 # print "Surface at %g nm", pos
408 # if PYLAB_VERBOSE and _final_flush_plot != None:
409 # _final_flush_plot()