1 # Copyright (C) 2011-2012 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 under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 3 of the License, or (at your option) any later
10 # pypiezo is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License along with
15 # pypiezo. If not, see <http://www.gnu.org/licenses/>.
17 "Control of a piezo-based atomic force microscope."
21 import numpy as _numpy
24 import matplotlib as _matplotlib
25 import matplotlib.pyplot as _matplotlib_pyplot
26 except (ImportError, RuntimeError), e:
28 _matplotlib_import_error = e
32 import h5config as _h5config
33 from h5config.storage.hdf5 import HDF5_Storage as _HDF5_Storage
34 from h5config.storage.hdf5 import h5_create_group as _h5_create_group
35 except ImportError, e:
37 _h5py_import_error = e
39 from curses_check_for_keypress import CheckForKeypress as _CheckForKeypress
41 from . import LOG as _LOG
42 from . import base as _base
43 from . import package_config as _package_config
44 from . import surface as _surface
47 class AFMPiezo (_base.Piezo):
48 """A piezo-controlled atomic force microscope.
50 This particular class expects a single input channel for measuring
51 deflection. Other subclasses provide support for multi-segment
52 deflection measurements.
54 >>> from pprint import pprint
55 >>> from pycomedi.device import Device
56 >>> from pycomedi.subdevice import StreamingSubdevice
57 >>> from pycomedi.channel import AnalogChannel
58 >>> from pycomedi.constant import AREF, SUBDEVICE_TYPE, UNIT
59 >>> from . import config
60 >>> from . import surface
62 >>> d = Device('/dev/comedi0')
65 >>> s_in = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
66 ... factory=StreamingSubdevice)
67 >>> s_out = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
68 ... factory=StreamingSubdevice)
70 >>> axis_channel = s_out.channel(
71 ... 0, factory=AnalogChannel, aref=AREF.ground)
72 >>> input_channel = s_in.channel(0, factory=AnalogChannel, aref=AREF.diff)
73 >>> for chan in [axis_channel, input_channel]:
74 ... chan.range = chan.find_range(unit=UNIT.volt, min=-10, max=10)
76 We set the minimum voltage for the `z` axis to -9 (a volt above
77 the minimum possible voltage) to help with testing
78 `.get_surface_position`. Without this minimum voltage, small
79 calibration errors could lead to a railed -10 V input for the
80 first few surface approaching steps, which could lead to an
81 `EdgeKink` error instead of a `FlatFit` error.
83 >>> axis_config = config.AxisConfig()
84 >>> axis_config.update(
85 ... {'gain':20, 'sensitivity':8e-9, 'minimum':-9})
86 >>> axis_config['channel'] = config.OutputChannelConfig()
87 >>> axis_config['channel']['name'] = 'z'
88 >>> input_config = config.InputChannelConfig()
89 >>> input_config['name'] = 'deflection'
91 >>> a = _base.PiezoAxis(config=axis_config, axis_channel=axis_channel)
94 >>> c = _base.InputChannel(config=input_config, channel=input_channel)
97 >>> p = AFMPiezo(axes=[a], inputs=[c], name='Molly')
99 >>> deflection = p.read_deflection()
100 >>> deflection # doctest: +SKIP
102 >>> p.deflection_dtype()
103 <type 'numpy.uint16'>
105 We need to know where we are before we can move somewhere
108 >>> pos = _base.convert_volts_to_bits(p.config.select_config(
109 ... 'axes', 'z', get_attribute=_base.get_axis_name)['channel'], 0)
112 Usually `.move_to_pos_or_def` is used to approach the surface, but
113 for testing we assume the z output channel is connected directly
114 into the deflection input channel.
116 >>> target_pos = _base.convert_volts_to_bits(
117 ... p.config.select_config('axes', 'z',
118 ... get_attribute=_base.get_axis_name)['channel'], 2)
119 >>> step = int((target_pos - pos)/5)
120 >>> target_def = _base.convert_volts_to_bits(
121 ... p.config.select_config('inputs', 'deflection'), 3)
122 >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
123 ... return_data=True)
124 >>> p.last_output == {'z': int(target_pos)}
126 >>> pprint(data) # doctest: +SKIP
128 array([32655, 33967, 35280, 36593, 37905, 39218, 39222], dtype=uint16),
130 array([32767, 34077, 35387, 36697, 38007, 39317, 39321], dtype=uint16)}
132 That was a working position-limited approach. Now move back to
133 the center and try a deflection-limited approach.
136 >>> target_def = _base.convert_volts_to_bits(
137 ... p.config.select_config('inputs', 'deflection'), 1)
138 >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
139 ... return_data=True)
140 >>> print (p.last_output['z'] < int(target_pos))
142 >>> pprint(data) # doctest: +SKIP
143 {'deflection': array([32655, 33968, 35281, 36593], dtype=uint16),
144 'z': array([32767, 34077, 35387, 36697], dtype=uint16)}
146 >>> wiggle_config = config.WiggleConfig()
147 >>> wiggle_config['offset'] = p.last_output['z']
148 >>> wiggle_config['wavelength'] = 650e-9
149 >>> p.wiggle_for_interference(config=wiggle_config,
150 ... keypress_test_mode=True)
151 Press any key to continue
154 ... p.get_surface_position('z', max_deflection=target_def)
155 ... except surface.FlatFit, e:
156 ... print 'got FlatFit'
158 >>> print e # doctest: +SKIP
159 slopes not sufficiently different: 1.0021 and 1.0021
160 >>> abs(e.right_slope-1) < 0.1
162 >>> abs(e.left_slope-1) < 0.1
167 def _deflection_channel(self):
168 return self.channel_by_name(name='deflection', direction='input')
170 def read_deflection(self):
171 """Return sensor deflection in bits.
173 TODO: explain how bit <-> volt conversion will work for this
176 return self._deflection_channel().data_read()
178 def deflection_dtype(self):
179 "Return a Numpy dtype suitable for deflection bit values."
180 return self._deflection_channel().subdevice.get_dtype()
182 def move_to_pos_or_def(self, axis_name, position=None, deflection=None,
183 step=1, return_data=False, pre_move_steps=0,
188 number of 'null' steps to take before moving (confirming a
189 stable input deflection).
191 The target step frequency in hertz. If `Null`, go as fast
192 as possible. Note that this is software timing, so it
193 should not be relied upon for precise results.
195 if position is None and deflection is None:
196 raise ValueError('must specify position, deflection, or both')
198 if return_data or _package_config['matplotlib']:
204 # default to the extreme value in the step direction
206 axis = self.axis_by_name(axis_name)
207 position = axis.axis_channel.get_maxdata()
210 elif deflection is None:
211 # default to the extreme value
212 channel = self._deflection_channel(self)
213 deflection = channel.get_maxdata()
216 raise ValueError('must have non-zero step size')
217 elif step < 0 and position > self.last_output[axis_name]:
219 elif step > 0 and position < self.last_output[axis_name]:
223 'move to position %d or deflection %g on axis %s in steps of %d'
224 % (position, deflection, axis_name, step))
225 _LOG.debug(log_string)
226 current_deflection = self.read_deflection()
227 log_string = 'current position %d and deflection %g' % (
228 self.last_output[axis_name], current_deflection)
229 _LOG.debug(log_string)
232 def_array=[current_deflection]
233 pos_array=[self.last_output[axis_name]]
234 for i in range(pre_move_steps):
235 self.jump(axis_name, piezo.last_output[axis_name])
236 delection = self.read_deflection()
238 def_array.append(current_deflection)
239 pos_array.append(self.last_output[axis_name])
240 if frequency is not None:
241 time_step = 1./frequency
242 next_time = _time.time() + time_step
243 # step in until we hit our target position or exceed our target deflection
244 while (self.last_output[axis_name] != position and
245 current_deflection < deflection):
246 dist_to = position - self.last_output[axis_name]
247 if abs(dist_to) < abs(step):
250 jump_to = self.last_output[axis_name] + step
251 self.jump(axis_name, jump_to)
252 current_deflection = self.read_deflection()
254 'current z piezo position %6d, current deflection %6d'
255 % (current_deflection, self.last_output[axis_name]))
256 _LOG.debug(log_string)
258 def_array.append(current_deflection)
259 pos_array.append(self.last_output[axis_name])
260 if frequency is not None:
263 _time.sleep(next_time - now)
264 next_time += time_step
267 'move to position %d or deflection %g on axis %s complete'
268 % (position, deflection, axis_name))
269 _LOG.debug(log_string)
270 log_string = 'current position %d and deflection %g' % (
271 self.last_output[axis_name], current_deflection)
272 _LOG.debug(log_string)
273 if _package_config['matplotlib']:
275 raise _matplotlib_import_error
276 figure = _matplotlib_pyplot.figure()
277 axes = figure.add_subplot(1, 1, 1)
279 timestamp = _time.strftime('%H%M%S')
280 axes.set_title('step approach %s' % timestamp)
281 axes.plot(pos_array, def_array, '.', label=timestamp)
282 #_pylab.legend(loc='best')
287 axis_name:_numpy.array(
288 pos_array, dtype=self.channel_dtype(
289 axis_name, direction='output')),
290 'deflection':_numpy.array(
291 def_array, dtype=self.deflection_dtype()),
295 def wiggle_for_interference(
296 self, config, plot=True, filename=None, group='/',
297 keypress_test_mode=False):
298 """Output a sine wave and measure interference.
300 With a poorly focused or aligned laser, leaked laser light
301 reflecting off the surface may interfere with the light
302 reflected off the cantilever, causing distance-dependent
303 interference with a period roughly half the laser's
304 wavelength. This method wiggles the cantilever near the
305 surface and monitors the magnitude of deflection oscillation,
306 allowing the operator to adjust the laser alignment in order
307 to minimize the interference.
309 Modern commercial AFMs with computer-aligned lasers must do
310 something like this automatically.
312 if _package_config['matplotlib']:
314 if config['wavelength'] and config['amplitude']:
316 'use either laser_wavelength or amplitude, but not both'
317 _LOG.warn(log_string)
319 if None in (config['amplitude'], config['offset']):
320 output_axis = self.axis_by_name(config['axis'])
321 maxdata = output_axis.axis_channel.get_maxdata()
322 midpoint = int(maxdata/2)
323 if config['offset'] is None:
325 _LOG.debug(('generated offset for interference wiggle: {}'
326 ).format(config['offset']))
327 if config['amplitude'] is None:
328 if config['offset'] <= midpoint:
329 max_amplitude = int(config['offset'])
331 max_amplitude = int(maxdata-config['offset'])
332 offset_meters = _base.convert_bits_to_meters(
333 output_axis.config, config['offset'])
334 if config['wavelength'] is None:
335 config['amplitude'] = 0.5*max_amplitude
337 bit_wavelength = _base.convert_meters_to_bits(
339 offset_meters + config['wavelength']
341 config['amplitude'] = 2*bit_wavelength
342 _LOG.debug(('generated amplitude for interference wiggle: {}'
343 ).format(config['amplitude']))
344 if config['amplitude'] > max_amplitude:
346 'no room for a two wavelength wiggle ({} > {})'.format(
347 config['amplitude'], max_amplitude))
349 n = config['samples'] # samples in a single oscillation
350 scan_frequency = config['frequency'] * n
351 out = (config['amplitude']
352 * _numpy.sin(_numpy.arange(2*n)*2*_numpy.pi/n)
354 # 2*n for 2 periods, so you can judge precision
355 out = out.reshape((len(out), 1)).astype(
356 self.channel_dtype(config['axis'], direction='output'))
358 _LOG.debug('oscillate for interference wiggle ({})'.format(config))
362 raise _h5py_import_error
363 if not output_axis: # from amplitude/offset setup
364 output_axis = afm.piezo.axis_by_name(config['axis'])
365 input_channel = afm.piezo.input_channel_by_name(config['input'])
366 with _h5py.File(filename, 'w') as f:
367 cwg = _h5_create_group(f, group)
368 storage = _HDF5_Storage()
370 (config, 'config/wiggle'),
372 'config/{}/axis'.format(config['axis'])),
373 (input_channel.config,
374 'config/{}/channel'.format(config['input']))]:
377 config_cwg = _h5_create_group(cwg, key)
378 storage.save(config=config, group=config_cwg)
381 raise _matplotlib_import_error
382 figure = _matplotlib_pyplot.figure()
383 axes = figure.add_subplot(1, 1, 1)
385 timestamp = _time.strftime('%H%M%S')
386 axes.set_title('wiggle for interference %s' % timestamp)
387 plot_p = axes.plot(out, out, 'b.-')
390 c = _CheckForKeypress(test_mode=keypress_test_mode)
391 while c.input() == None:
392 # input will need processing for multi-segment AFMs...
394 out, scan_frequency, output_names=[config['axis']],
395 input_names=[config['input']])
396 _LOG.debug('completed a wiggle round')
398 timestamp = ('{0}-{1:02d}-{2:02d}T{3:02d}-{4:02d}-{5:02d}'
399 ).format(*_time.localtime())
400 with _h5py.File(filename, 'a') as f:
401 wiggle_group = _h5_create_group(f, group)
402 cwg = _h5_create_group(
403 wiggle_group, 'wiggle/{}'.format(cycle))
404 cwg['time'] = timestamp
405 cwg['raw/{}'.format(config['axis'])] = out
406 cwg['raw/{}'.format(config['input'])] = data
408 plot_p[0].set_ydata(data[:,0])
409 axes.set_ylim([data.min(), data.max()])
412 self.last_output[config['axis']] = out[-1,0]
413 _LOG.debug('interference wiggle complete')
415 get_surface_position = _surface.get_surface_position
419 # if USE_ABCD_DEFLECTION :
420 # for i in range(4) : # i is the photodiode element (0->A, 1->B, ...)
421 # self.curIn[i] = out["Deflection segment"][i][-1]
423 # self.curIn[self.chan_info.def_ind] = out["deflection"][-1]
426 #class FourSegmentAFM (AFM):
427 # def read_deflection(self):
428 # "Return sensor deflection in bits."
429 # A = int(self.curIn[self.chan_info.def_ind[0]])
430 # B = int(self.curIn[self.chan_info.def_ind[1]])
431 # C = int(self.curIn[self.chan_info.def_ind[2]])
432 # D = int(self.curIn[self.chan_info.def_ind[3]])
433 # df = float((A+B)-(C+D))/(A+B+C+D)
434 # dfout = int(df * 2**15) + 2**15
436 # print "Current deflection %d (%d, %d, %d, %d)" \
437 # % (dfout, A, B, C, D)
441 #def test_smoothness(zp, plotVerbose=True):
444 # setpoint = zp.def_V2in(3)
447 # outarray = linspace(posB, posA, 1000)
450 # curVals = zp.jumpToPos(posA)
451 # zp.pCurVals(curVals)
452 # _sleep(1) # let jitters die down
453 # for i in range(10):
454 # print "ramp %d to %d" % (zp.curPos(), posB)
455 # curVals, data = moveToPosOrDef(zp, posB, setpoint, step=steps,
456 # return_data = True)
457 # indata.append(data)
458 # out = zp.ramp(outarray, outfreq)
459 # outdata.append(out)
461 # from pylab import figure, plot, title, legend, hold, subplot
462 # if PYLAB_VERBOSE or plotVerbose:
464 # _pylab.figure(BASE_FIG_NUM+4)
465 # for i in range(10):
466 # _pylab.plot(indata[i]['z'],
467 # indata[i]['deflection'], '+--', label='in')
468 # _pylab.plot(outdata[i]['z'],
469 # outdata[i]['deflection'], '.-', label='out')
470 # _pylab.title('test smoothness (step in, ramp out)')
471 # #_pylab.legend(loc='best')
475 # zp = z_piezo.z_piezo()
476 # curVals = zp.moveToPosOrDef(zp.pos_nm2out(600), defl=zp.curDef()+6000, step=(zp.pos_nm2out(10)-zp.pos_nm2out(0)))
478 # zp.pCurVals(curVals)
479 # pos = zp.getSurfPos(maxDefl=zp.curDef()+6000)
481 # print "Surface at %g nm", pos
483 # if PYLAB_VERBOSE and _final_flush_plot != None:
484 # _final_flush_plot()