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 >>> target_def = _base.convert_volts_to_bits(
125 ... p.config['inputs'][0]['channel'], 1)
126 >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
127 ... return_data=True)
128 >>> print (p.last_output['z'] < int(target_pos))
130 >>> pprint(data) # doctest: +SKIP
131 {'deflection': array([32655, 33968, 35281, 36593], dtype=uint16),
132 'z': array([32767, 34077, 35387, 36697], dtype=uint16)}
134 >>> p.wiggle_for_interference('z', offset=p.last_output['z'],
135 ... laser_wavelength=650e-9, keypress_test_mode=True)
136 Press any key to continue
139 ... p.get_surface_position('z', max_deflection=target_def)
140 ... except surface.FlatFit, e:
141 ... print 'got FlatFit'
143 >>> print e # doctest: +SKIP
144 slopes not sufficiently different: 1.0021 and 1.0021
145 >>> abs(e.right_slope-1) < 0.1
147 >>> abs(e.left_slope-1) < 0.1
152 def _deflection_channel(self):
153 return self.channel_by_name(name='deflection', direction='input')
155 def read_deflection(self):
156 """Return sensor deflection in bits.
158 TODO: explain how bit <-> volt conversion will work for this
161 return self._deflection_channel().data_read()
163 def deflection_dtype(self):
164 "Return a Numpy dtype suitable for deflection bit values."
165 return self._deflection_channel().subdevice.get_dtype()
167 def move_to_pos_or_def(self, axis_name, position, deflection, step,
168 return_data=False, pre_move_steps=0):
172 number of 'null' steps to take before moving (confirming a
173 stable input deflection).
175 if return_data or _package_config['matplotlib']:
181 raise ValueError('must have non-zero step size')
182 elif step < 0 and position > self.last_output[axis_name]:
184 elif step > 0 and position < self.last_output[axis_name]:
188 'move to position %d or deflection %g on axis %s in steps of %d'
189 % (position, deflection, axis_name, step))
190 _LOG.debug(log_string)
191 current_deflection = self.read_deflection()
192 log_string = 'current position %d and deflection %g' % (
193 self.last_output[axis_name], current_deflection)
194 _LOG.debug(log_string)
197 def_array=[current_deflection]
198 pos_array=[self.last_output[axis_name]]
199 for i in range(pre_move_steps):
200 self.jump(axis_name, piezo.last_output[axis_name])
201 delection = self.read_deflection()
203 def_array.append(current_deflection)
204 pos_array.append(self.last_output[axis_name])
205 # step in until we hit our target position or exceed our target deflection
206 while (self.last_output[axis_name] != position and
207 current_deflection < deflection):
208 dist_to = position - self.last_output[axis_name]
209 if abs(dist_to) < abs(step):
212 jump_to = self.last_output[axis_name] + step
213 self.jump(axis_name, jump_to)
214 current_deflection = self.read_deflection()
216 'current z piezo position %6d, current deflection %6d'
217 % (current_deflection, self.last_output[axis_name]))
218 _LOG.debug(log_string)
220 def_array.append(current_deflection)
221 pos_array.append(self.last_output[axis_name])
224 'move to position %d or deflection %g on axis %s complete'
225 % (position, deflection, axis_name))
226 _LOG.debug(log_string)
227 log_string = 'current position %d and deflection %g' % (
228 self.last_output[axis_name], current_deflection)
229 _LOG.debug(log_string)
230 if _package_config['matplotlib']:
232 raise _matplotlib_import_error
233 figure = _matplotlib_pyplot.figure()
234 axes = figure.add_subplot(1, 1, 1)
236 timestamp = _time.strftime('%H%M%S')
237 axes.set_title('step approach %s' % timestamp)
238 axes.plot(pos_array, def_array, '.', label=timestamp)
239 #_pylab.legend(loc='best')
244 axis_name:_numpy.array(
245 pos_array, dtype=self.channel_dtype(
246 axis_name, direction='output')),
247 'deflection':_numpy.array(
248 def_array, dtype=self.deflection_dtype()),
252 def wiggle_for_interference(
253 self, axis_name, wiggle_frequency=2, n_samples=1024, amplitude=None,
254 offset=None, laser_wavelength=None, plot=True,
255 keypress_test_mode=False):
256 """Output a sine wave and measure interference.
258 With a poorly focused or aligned laser, leaked laser light
259 reflecting off the surface may interfere with the light
260 reflected off the cantilever, causing distance-dependent
261 interference with a period roughly half the laser's
262 wavelength. This method wiggles the cantilever near the
263 surface and monitors the magnitude of deflection oscillation,
264 allowing the operator to adjust the laser alignment in order
265 to minimize the interference.
267 Modern commercial AFMs with computer-aligned lasers must do
268 something like this automatically.
270 if _package_config['matplotlib']:
272 if laser_wavelength and amplitude:
274 'use either laser_wavelength or amplitude, but not both'
275 _LOG.warn(log_string)
277 if None in (amplitude, offset):
278 output_axis = self.axis_by_name(axis_name)
279 maxdata = output_axis.axis_channel.get_maxdata()
280 midpoint = int(maxdata/2)
284 'generated offset for interference wiggle: %g' % offset)
285 _LOG.debug(log_string)
286 if amplitude == None:
287 if offset <= midpoint:
288 max_amplitude = int(offset)
290 max_amplitude = int(maxdata-offset)
291 offset_meters = _base.convert_bits_to_meters(
292 output_axis.config, offset)
293 bit_wavelength = _base.convert_meters_to_bits(
294 output_axis.config, offset_meters + laser_wavelength
296 amplitude = 2*bit_wavelength
298 'generated amplitude for interference wiggle: %g'
300 _LOG.debug(log_string)
301 if amplitude > max_amplitude:
302 raise ValueError('no room for a two wavelength wiggle')
304 scan_frequency = wiggle_frequency * n_samples
305 out = (amplitude * _numpy.sin(
306 _numpy.arange(n_samples) * 4 * _numpy.pi / float(n_samples))
308 # 4 for 2 periods, so you can judge precision
309 out = out.reshape((len(out), 1)).astype(
310 self.channel_dtype(axis_name, direction='output'))
312 _LOG.debug('oscillate for interference wiggle')
314 'amplitude: %d bits, offset: %d bits, scan frequency: %g Hz'
315 % (amplitude, offset, scan_frequency))
316 _LOG.debug(log_string)
320 raise _matplotlib_import_error
321 figure = _matplotlib_pyplot.figure()
322 axes = figure.add_subplot(1, 1, 1)
324 timestamp = _time.strftime('%H%M%S')
325 axes.set_title('wiggle for interference %s' % timestamp)
326 plot_p = axes.plot(out, out, 'b.-')
328 c = _CheckForKeypress(test_mode=keypress_test_mode)
329 while c.input() == None:
330 # input will need processing for multi-segment AFMs...
331 data = self.ramp(out, scan_frequency, output_names=[axis_name],
332 input_names=['deflection'])
333 _LOG.debug('completed a wiggle round')
335 plot_p[0].set_ydata(data[:,0])
336 axes.set_ylim([data.min(), data.max()])
338 self.last_output[axis_name] = out[-1,0]
339 _LOG.debug('interference wiggle complete')
341 get_surface_position = _surface.get_surface_position
345 # if USE_ABCD_DEFLECTION :
346 # for i in range(4) : # i is the photodiode element (0->A, 1->B, ...)
347 # self.curIn[i] = out["Deflection segment"][i][-1]
349 # self.curIn[self.chan_info.def_ind] = out["deflection"][-1]
352 #class FourSegmentAFM (AFM):
353 # def read_deflection(self):
354 # "Return sensor deflection in bits."
355 # A = int(self.curIn[self.chan_info.def_ind[0]])
356 # B = int(self.curIn[self.chan_info.def_ind[1]])
357 # C = int(self.curIn[self.chan_info.def_ind[2]])
358 # D = int(self.curIn[self.chan_info.def_ind[3]])
359 # df = float((A+B)-(C+D))/(A+B+C+D)
360 # dfout = int(df * 2**15) + 2**15
362 # print "Current deflection %d (%d, %d, %d, %d)" \
363 # % (dfout, A, B, C, D)
367 #def test_smoothness(zp, plotVerbose=True):
370 # setpoint = zp.def_V2in(3)
373 # outarray = linspace(posB, posA, 1000)
376 # curVals = zp.jumpToPos(posA)
377 # zp.pCurVals(curVals)
378 # _sleep(1) # let jitters die down
379 # for i in range(10):
380 # print "ramp %d to %d" % (zp.curPos(), posB)
381 # curVals, data = moveToPosOrDef(zp, posB, setpoint, step=steps,
382 # return_data = True)
383 # indata.append(data)
384 # out = zp.ramp(outarray, outfreq)
385 # outdata.append(out)
387 # from pylab import figure, plot, title, legend, hold, subplot
388 # if PYLAB_VERBOSE or plotVerbose:
390 # _pylab.figure(BASE_FIG_NUM+4)
391 # for i in range(10):
392 # _pylab.plot(indata[i]['z'],
393 # indata[i]['deflection'], '+--', label='in')
394 # _pylab.plot(outdata[i]['z'],
395 # outdata[i]['deflection'], '.-', label='out')
396 # _pylab.title('test smoothness (step in, ramp out)')
397 # #_pylab.legend(loc='best')
401 # zp = z_piezo.z_piezo()
402 # curVals = zp.moveToPosOrDef(zp.pos_nm2out(600), defl=zp.curDef()+6000, step=(zp.pos_nm2out(10)-zp.pos_nm2out(0)))
404 # zp.pCurVals(curVals)
405 # pos = zp.getSurfPos(maxDefl=zp.curDef()+6000)
407 # print "Surface at %g nm", pos
409 # if PYLAB_VERBOSE and _final_flush_plot != None:
410 # _final_flush_plot()