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, 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_config as _base_config
34 from . import base as _base
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_channel_config = config._ChannelConfig()
78 >>> input_channel_config = config._ChannelConfig()
80 >>> a = _base.PiezoAxis(axis_config=axis_config,
81 ... axis_channel_config=axis_channel_config,
82 ... axis_channel=axis_channel, name='z')
85 >>> c = _base.InputChannel(
86 ... channel_config=input_channel_config, channel=input_channel,
87 ... name='deflection')
90 >>> p = AFMPiezo(axes=[a], input_channels=[c])
92 >>> deflection = p.read_deflection()
93 >>> deflection # doctest: +SKIP
95 >>> p.deflection_dtype()
98 We need to know where we are before we can move somewhere
101 >>> pos = _base.convert_volts_to_bits(p.axes[0].axis_channel_config, 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.axes[0].axis_channel_config, 2)
110 >>> step = int((target_pos - pos)/5)
111 >>> target_def = _base.convert_volts_to_bits(
112 ... p.input_channels[0].channel_config, 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.input_channels[0].channel_config, 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 _base_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 _base_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 _base_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.axis_channel_config, output_axis.axis_config,
297 bit_wavelength = _base.convert_meters_to_bits(
298 output_axis.axis_channel_config, output_axis.axis_config,
299 offset_meters + laser_wavelength) - offset
300 amplitude = 2*bit_wavelength
302 'generated amplitude for interference wiggle: %g'
304 _LOG.debug(log_string)
305 if amplitude > max_amplitude:
306 raise ValueError('no room for a two wavelength wiggle')
308 scan_frequency = wiggle_frequency * n_samples
309 out = (amplitude * _numpy.sin(
310 _numpy.arange(n_samples) * 4 * _numpy.pi / float(n_samples))
312 # 4 for 2 periods, so you can judge precision
313 out = out.reshape((len(out), 1)).astype(
314 self.channel_dtype(axis_name, direction='output'))
316 _LOG.debug('oscillate for interference wiggle')
318 'amplitude: %d bits, offset: %d bits, scan frequency: %g Hz'
319 % (amplitude, offset, scan_frequency))
320 _LOG.debug(log_string)
324 raise _matplotlib_import_error
325 figure = _matplotlib_pyplot.figure()
326 axes = figure.add_subplot(1, 1, 1)
328 timestamp = _time.strftime('%H%M%S')
329 axes.set_title('wiggle for interference %s' % timestamp)
330 plot_p = axes.plot(out, out, 'b.-')
332 c = _CheckForKeypress(test_mode=keypress_test_mode)
333 while c.input() == None:
334 # input will need processing for multi-segment AFMs...
335 data = self.ramp(out, scan_frequency, output_names=[axis_name],
336 input_names=['deflection'])
337 _LOG.debug('completed a wiggle round')
339 plot_p[0].set_ydata(data[:,0])
340 axes.set_ylim([data.min(), data.max()])
342 self.last_output[axis_name] = out[-1,0]
343 _LOG.debug('interference wiggle complete')
345 get_surface_position = _surface.get_surface_position
349 # if USE_ABCD_DEFLECTION :
350 # for i in range(4) : # i is the photodiode element (0->A, 1->B, ...)
351 # self.curIn[i] = out["Deflection segment"][i][-1]
353 # self.curIn[self.chan_info.def_ind] = out["deflection"][-1]
356 #class FourSegmentAFM (AFM):
357 # def read_deflection(self):
358 # "Return sensor deflection in bits."
359 # A = int(self.curIn[self.chan_info.def_ind[0]])
360 # B = int(self.curIn[self.chan_info.def_ind[1]])
361 # C = int(self.curIn[self.chan_info.def_ind[2]])
362 # D = int(self.curIn[self.chan_info.def_ind[3]])
363 # df = float((A+B)-(C+D))/(A+B+C+D)
364 # dfout = int(df * 2**15) + 2**15
366 # print "Current deflection %d (%d, %d, %d, %d)" \
367 # % (dfout, A, B, C, D)
371 #def test_smoothness(zp, plotVerbose=True):
374 # setpoint = zp.def_V2in(3)
377 # outarray = linspace(posB, posA, 1000)
380 # curVals = zp.jumpToPos(posA)
381 # zp.pCurVals(curVals)
382 # _sleep(1) # let jitters die down
383 # for i in range(10):
384 # print "ramp %d to %d" % (zp.curPos(), posB)
385 # curVals, data = moveToPosOrDef(zp, posB, setpoint, step=steps,
386 # return_data = True)
387 # indata.append(data)
388 # out = zp.ramp(outarray, outfreq)
389 # outdata.append(out)
391 # from pylab import figure, plot, title, legend, hold, subplot
392 # if PYLAB_VERBOSE or plotVerbose:
394 # _pylab.figure(BASE_FIG_NUM+4)
395 # for i in range(10):
396 # _pylab.plot(indata[i]['z'],
397 # indata[i]['deflection'], '+--', label='in')
398 # _pylab.plot(outdata[i]['z'],
399 # outdata[i]['deflection'], '.-', label='out')
400 # _pylab.title('test smoothness (step in, ramp out)')
401 # #_pylab.legend(loc='best')
405 # zp = z_piezo.z_piezo()
406 # curVals = zp.moveToPosOrDef(zp.pos_nm2out(600), defl=zp.curDef()+6000, step=(zp.pos_nm2out(10)-zp.pos_nm2out(0)))
408 # zp.pCurVals(curVals)
409 # pos = zp.getSurfPos(maxDefl=zp.curDef()+6000)
411 # print "Surface at %g nm", pos
413 # if PYLAB_VERBOSE and _final_flush_plot != None:
414 # _final_flush_plot()