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['axes'][0]['channel'], 0)
102 Usually `.move_to_pos_or_def` is used to approach the surface, but
103 for testing we assume the z output channel is connected directly
104 into the deflection input channel.
106 >>> target_pos = _base.convert_volts_to_bits(
107 ... p.config['axes'][0]['channel'], 2)
108 >>> step = int((target_pos - pos)/5)
109 >>> target_def = _base.convert_volts_to_bits(p.config['inputs'][0], 3)
110 >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
111 ... return_data=True)
112 >>> p.last_output == {'z': int(target_pos)}
114 >>> pprint(data) # doctest: +SKIP
116 array([32655, 33967, 35280, 36593, 37905, 39218, 39222], dtype=uint16),
118 array([32767, 34077, 35387, 36697, 38007, 39317, 39321], dtype=uint16)}
120 That was a working position-limited approach. Now move back to
121 the center and try a deflection-limited approach.
124 >>> p.config['inputs'][0]
125 >>> p.config['inputs']['channel']
126 >>> target_def = _base.convert_volts_to_bits(
127 ... p.config['inputs'][0]['channel'], 1)
128 >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
129 ... return_data=True)
130 >>> print (p.last_output['z'] < int(target_pos))
132 >>> pprint(data) # doctest: +SKIP
133 {'deflection': array([32655, 33968, 35281, 36593], dtype=uint16),
134 'z': array([32767, 34077, 35387, 36697], dtype=uint16)}
136 >>> p.wiggle_for_interference('z', offset=p.last_output['z'],
137 ... laser_wavelength=650e-9, keypress_test_mode=True)
138 Press any key to continue
141 ... p.get_surface_position('z', max_deflection=target_def)
142 ... except surface.FlatFit, e:
143 ... print 'got FlatFit'
145 >>> print e # doctest: +SKIP
146 slopes not sufficiently different: 1.0021 and 1.0021
147 >>> abs(e.right_slope-1) < 0.1
149 >>> abs(e.left_slope-1) < 0.1
154 def _deflection_channel(self):
155 return self.channel_by_name(name='deflection', direction='input')
157 def read_deflection(self):
158 """Return sensor deflection in bits.
160 TODO: explain how bit <-> volt conversion will work for this
163 return self._deflection_channel().data_read()
165 def deflection_dtype(self):
166 "Return a Numpy dtype suitable for deflection bit values."
167 return self._deflection_channel().subdevice.get_dtype()
169 def move_to_pos_or_def(self, axis_name, position, deflection, step,
170 return_data=False, pre_move_steps=0):
174 number of 'null' steps to take before moving (confirming a
175 stable input deflection).
177 if return_data or _package_config['matplotlib']:
183 raise ValueError('must have non-zero step size')
184 elif step < 0 and position > self.last_output[axis_name]:
186 elif step > 0 and position < self.last_output[axis_name]:
190 'move to position %d or deflection %g on axis %s in steps of %d'
191 % (position, deflection, axis_name, step))
192 _LOG.debug(log_string)
193 current_deflection = self.read_deflection()
194 log_string = 'current position %d and deflection %g' % (
195 self.last_output[axis_name], current_deflection)
196 _LOG.debug(log_string)
199 def_array=[current_deflection]
200 pos_array=[self.last_output[axis_name]]
201 for i in range(pre_move_steps):
202 self.jump(axis_name, piezo.last_output[axis_name])
203 delection = self.read_deflection()
205 def_array.append(current_deflection)
206 pos_array.append(self.last_output[axis_name])
207 # step in until we hit our target position or exceed our target deflection
208 while (self.last_output[axis_name] != position and
209 current_deflection < deflection):
210 dist_to = position - self.last_output[axis_name]
211 if abs(dist_to) < abs(step):
214 jump_to = self.last_output[axis_name] + step
215 self.jump(axis_name, jump_to)
216 current_deflection = self.read_deflection()
218 'current z piezo position %6d, current deflection %6d'
219 % (current_deflection, self.last_output[axis_name]))
220 _LOG.debug(log_string)
222 def_array.append(current_deflection)
223 pos_array.append(self.last_output[axis_name])
226 'move to position %d or deflection %g on axis %s complete'
227 % (position, deflection, axis_name))
228 _LOG.debug(log_string)
229 log_string = 'current position %d and deflection %g' % (
230 self.last_output[axis_name], current_deflection)
231 _LOG.debug(log_string)
232 if _package_config['matplotlib']:
234 raise _matplotlib_import_error
235 figure = _matplotlib_pyplot.figure()
236 axes = figure.add_subplot(1, 1, 1)
238 timestamp = _time.strftime('%H%M%S')
239 axes.set_title('step approach %s' % timestamp)
240 axes.plot(pos_array, def_array, '.', label=timestamp)
241 #_pylab.legend(loc='best')
246 axis_name:_numpy.array(
247 pos_array, dtype=self.channel_dtype(
248 axis_name, direction='output')),
249 'deflection':_numpy.array(
250 def_array, dtype=self.deflection_dtype()),
254 def wiggle_for_interference(
255 self, axis_name, wiggle_frequency=2, n_samples=1024, amplitude=None,
256 offset=None, laser_wavelength=None, plot=True,
257 keypress_test_mode=False):
258 """Output a sine wave and measure interference.
260 With a poorly focused or aligned laser, leaked laser light
261 reflecting off the surface may interfere with the light
262 reflected off the cantilever, causing distance-dependent
263 interference with a period roughly half the laser's
264 wavelength. This method wiggles the cantilever near the
265 surface and monitors the magnitude of deflection oscillation,
266 allowing the operator to adjust the laser alignment in order
267 to minimize the interference.
269 Modern commercial AFMs with computer-aligned lasers must do
270 something like this automatically.
272 if _package_config['matplotlib']:
274 if laser_wavelength and amplitude:
276 'use either laser_wavelength or amplitude, but not both'
277 _LOG.warn(log_string)
279 if None in (amplitude, offset):
280 output_axis = self.axis_by_name(axis_name)
281 maxdata = output_axis.axis_channel.get_maxdata()
282 midpoint = int(maxdata/2)
286 'generated offset for interference wiggle: %g' % offset)
287 _LOG.debug(log_string)
288 if amplitude == None:
289 if offset <= midpoint:
290 max_amplitude = int(offset)
292 max_amplitude = int(maxdata-offset)
293 offset_meters = _base.convert_bits_to_meters(
294 output_axis.config, offset)
295 bit_wavelength = _base.convert_meters_to_bits(
296 output_axis.config, offset_meters + laser_wavelength
298 amplitude = 2*bit_wavelength
300 'generated amplitude for interference wiggle: %g'
302 _LOG.debug(log_string)
303 if amplitude > max_amplitude:
304 raise ValueError('no room for a two wavelength wiggle')
306 scan_frequency = wiggle_frequency * n_samples
307 out = (amplitude * _numpy.sin(
308 _numpy.arange(n_samples) * 4 * _numpy.pi / float(n_samples))
310 # 4 for 2 periods, so you can judge precision
311 out = out.reshape((len(out), 1)).astype(
312 self.channel_dtype(axis_name, direction='output'))
314 _LOG.debug('oscillate for interference wiggle')
316 'amplitude: %d bits, offset: %d bits, scan frequency: %g Hz'
317 % (amplitude, offset, scan_frequency))
318 _LOG.debug(log_string)
322 raise _matplotlib_import_error
323 figure = _matplotlib_pyplot.figure()
324 axes = figure.add_subplot(1, 1, 1)
326 timestamp = _time.strftime('%H%M%S')
327 axes.set_title('wiggle for interference %s' % timestamp)
328 plot_p = axes.plot(out, out, 'b.-')
330 c = _CheckForKeypress(test_mode=keypress_test_mode)
331 while c.input() == None:
332 # input will need processing for multi-segment AFMs...
333 data = self.ramp(out, scan_frequency, output_names=[axis_name],
334 input_names=['deflection'])
335 _LOG.debug('completed a wiggle round')
337 plot_p[0].set_ydata(data[:,0])
338 axes.set_ylim([data.min(), data.max()])
340 self.last_output[axis_name] = out[-1,0]
341 _LOG.debug('interference wiggle complete')
343 get_surface_position = _surface.get_surface_position
347 # if USE_ABCD_DEFLECTION :
348 # for i in range(4) : # i is the photodiode element (0->A, 1->B, ...)
349 # self.curIn[i] = out["Deflection segment"][i][-1]
351 # self.curIn[self.chan_info.def_ind] = out["deflection"][-1]
354 #class FourSegmentAFM (AFM):
355 # def read_deflection(self):
356 # "Return sensor deflection in bits."
357 # A = int(self.curIn[self.chan_info.def_ind[0]])
358 # B = int(self.curIn[self.chan_info.def_ind[1]])
359 # C = int(self.curIn[self.chan_info.def_ind[2]])
360 # D = int(self.curIn[self.chan_info.def_ind[3]])
361 # df = float((A+B)-(C+D))/(A+B+C+D)
362 # dfout = int(df * 2**15) + 2**15
364 # print "Current deflection %d (%d, %d, %d, %d)" \
365 # % (dfout, A, B, C, D)
369 #def test_smoothness(zp, plotVerbose=True):
372 # setpoint = zp.def_V2in(3)
375 # outarray = linspace(posB, posA, 1000)
378 # curVals = zp.jumpToPos(posA)
379 # zp.pCurVals(curVals)
380 # _sleep(1) # let jitters die down
381 # for i in range(10):
382 # print "ramp %d to %d" % (zp.curPos(), posB)
383 # curVals, data = moveToPosOrDef(zp, posB, setpoint, step=steps,
384 # return_data = True)
385 # indata.append(data)
386 # out = zp.ramp(outarray, outfreq)
387 # outdata.append(out)
389 # from pylab import figure, plot, title, legend, hold, subplot
390 # if PYLAB_VERBOSE or plotVerbose:
392 # _pylab.figure(BASE_FIG_NUM+4)
393 # for i in range(10):
394 # _pylab.plot(indata[i]['z'],
395 # indata[i]['deflection'], '+--', label='in')
396 # _pylab.plot(outdata[i]['z'],
397 # outdata[i]['deflection'], '.-', label='out')
398 # _pylab.title('test smoothness (step in, ramp out)')
399 # #_pylab.legend(loc='best')
403 # zp = z_piezo.z_piezo()
404 # curVals = zp.moveToPosOrDef(zp.pos_nm2out(600), defl=zp.curDef()+6000, step=(zp.pos_nm2out(10)-zp.pos_nm2out(0)))
406 # zp.pCurVals(curVals)
407 # pos = zp.getSurfPos(maxDefl=zp.curDef()+6000)
409 # print "Surface at %g nm", pos
411 # if PYLAB_VERBOSE and _final_flush_plot != None:
412 # _final_flush_plot()