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
161 >>> abs(e.right_slope-1) < 0.1
164 >>> abs(e.left_slope-1) < 0.1
169 def _deflection_channel(self):
170 return self.channel_by_name(name='deflection', direction='input')
172 def read_deflection(self):
173 """Return sensor deflection in bits.
175 TODO: explain how bit <-> volt conversion will work for this
178 return self._deflection_channel().data_read()
180 def deflection_dtype(self):
181 "Return a Numpy dtype suitable for deflection bit values."
182 return self._deflection_channel().subdevice.get_dtype()
184 def move_to_pos_or_def(self, axis_name, position=None, deflection=None,
185 step=1, return_data=False, pre_move_steps=0,
190 number of 'null' steps to take before moving (confirming a
191 stable input deflection).
193 The target step frequency in hertz. If `Null`, go as fast
194 as possible. Note that this is software timing, so it
195 should not be relied upon for precise results.
197 if position is None and deflection is None:
198 raise ValueError('must specify position, deflection, or both')
200 if return_data or _package_config['matplotlib']:
206 # default to the extreme value in the step direction
208 axis = self.axis_by_name(axis_name)
209 position = axis.axis_channel.get_maxdata()
212 elif deflection is None:
213 # default to the extreme value
214 channel = self._deflection_channel(self)
215 deflection = channel.get_maxdata()
218 raise ValueError('must have non-zero step size')
219 elif step < 0 and position > self.last_output[axis_name]:
221 elif step > 0 and position < self.last_output[axis_name]:
225 'move to position %d or deflection %g on axis %s in steps of %d'
226 % (position, deflection, axis_name, step))
227 _LOG.debug(log_string)
228 current_deflection = self.read_deflection()
229 log_string = 'current position %d and deflection %g' % (
230 self.last_output[axis_name], current_deflection)
231 _LOG.debug(log_string)
234 def_array=[current_deflection]
235 pos_array=[self.last_output[axis_name]]
236 for i in range(pre_move_steps):
237 self.jump(axis_name, piezo.last_output[axis_name])
238 delection = self.read_deflection()
240 def_array.append(current_deflection)
241 pos_array.append(self.last_output[axis_name])
242 if frequency is not None:
243 time_step = 1./frequency
244 next_time = _time.time() + time_step
245 # step in until we hit our target position or exceed our target deflection
246 while (self.last_output[axis_name] != position and
247 current_deflection < deflection):
248 dist_to = position - self.last_output[axis_name]
249 if abs(dist_to) < abs(step):
252 jump_to = self.last_output[axis_name] + step
253 self.jump(axis_name, jump_to)
254 current_deflection = self.read_deflection()
256 'current z piezo position %6d, current deflection %6d'
257 % (current_deflection, self.last_output[axis_name]))
258 _LOG.debug(log_string)
260 def_array.append(current_deflection)
261 pos_array.append(self.last_output[axis_name])
262 if frequency is not None:
265 _time.sleep(next_time - now)
266 next_time += time_step
269 'move to position %d or deflection %g on axis %s complete'
270 % (position, deflection, axis_name))
271 _LOG.debug(log_string)
272 log_string = 'current position %d and deflection %g' % (
273 self.last_output[axis_name], current_deflection)
274 _LOG.debug(log_string)
275 if _package_config['matplotlib']:
277 raise _matplotlib_import_error
278 figure = _matplotlib_pyplot.figure()
279 axes = figure.add_subplot(1, 1, 1)
281 timestamp = _time.strftime('%H%M%S')
282 axes.set_title('step approach %s' % timestamp)
283 axes.plot(pos_array, def_array, '.', label=timestamp)
284 #_pylab.legend(loc='best')
289 axis_name:_numpy.array(
290 pos_array, dtype=self.channel_dtype(
291 axis_name, direction='output')),
292 'deflection':_numpy.array(
293 def_array, dtype=self.deflection_dtype()),
297 def wiggle_for_interference(
298 self, config, plot=True, filename=None, group='/',
299 keypress_test_mode=False):
300 """Output a sine wave and measure interference.
302 With a poorly focused or aligned laser, leaked laser light
303 reflecting off the surface may interfere with the light
304 reflected off the cantilever, causing distance-dependent
305 interference with a period roughly half the laser's
306 wavelength. This method wiggles the cantilever near the
307 surface and monitors the magnitude of deflection oscillation,
308 allowing the operator to adjust the laser alignment in order
309 to minimize the interference.
311 Modern commercial AFMs with computer-aligned lasers must do
312 something like this automatically.
314 if _package_config['matplotlib']:
316 if config['wavelength'] and config['amplitude']:
318 'use either laser_wavelength or amplitude, but not both'
319 _LOG.warn(log_string)
321 if None in (config['amplitude'], config['offset']):
322 output_axis = self.axis_by_name(config['axis'])
323 maxdata = output_axis.axis_channel.get_maxdata()
324 midpoint = int(maxdata/2)
325 if config['offset'] is None:
327 _LOG.debug(('generated offset for interference wiggle: {}'
328 ).format(config['offset']))
329 if config['amplitude'] is None:
330 if config['offset'] <= midpoint:
331 max_amplitude = int(config['offset'])
333 max_amplitude = int(maxdata-config['offset'])
334 offset_meters = _base.convert_bits_to_meters(
335 output_axis.config, config['offset'])
336 if config['wavelength'] is None:
337 config['amplitude'] = 0.5*max_amplitude
339 bit_wavelength = _base.convert_meters_to_bits(
341 offset_meters + config['wavelength']
343 config['amplitude'] = 2*bit_wavelength
344 _LOG.debug(('generated amplitude for interference wiggle: {}'
345 ).format(config['amplitude']))
346 if config['amplitude'] > max_amplitude:
348 'no room for a two wavelength wiggle ({} > {})'.format(
349 config['amplitude'], max_amplitude))
351 n = config['samples'] # samples in a single oscillation
352 scan_frequency = config['frequency'] * n
353 out = (config['amplitude']
354 * _numpy.sin(_numpy.arange(2*n)*2*_numpy.pi/n)
356 # 2*n for 2 periods, so you can judge precision
357 out = out.reshape((len(out), 1)).astype(
358 self.channel_dtype(config['axis'], direction='output'))
360 _LOG.debug('oscillate for interference wiggle ({})'.format(config))
364 raise _h5py_import_error
365 if not output_axis: # from amplitude/offset setup
366 output_axis = afm.piezo.axis_by_name(config['axis'])
367 input_channel = afm.piezo.input_channel_by_name(config['input'])
368 with _h5py.File(filename, 'w') as f:
369 cwg = _h5_create_group(f, group)
370 storage = _HDF5_Storage()
372 (config, 'config/wiggle'),
374 'config/{}/axis'.format(config['axis'])),
375 (input_channel.config,
376 'config/{}/channel'.format(config['input']))]:
379 config_cwg = _h5_create_group(cwg, key)
380 storage.save(config=config, group=config_cwg)
383 raise _matplotlib_import_error
384 figure = _matplotlib_pyplot.figure()
385 axes = figure.add_subplot(1, 1, 1)
387 timestamp = _time.strftime('%H%M%S')
388 axes.set_title('wiggle for interference %s' % timestamp)
389 plot_p = axes.plot(out, out, 'b.-')
392 c = _CheckForKeypress(test_mode=keypress_test_mode)
393 while c.input() == None:
394 # input will need processing for multi-segment AFMs...
396 out, scan_frequency, output_names=[config['axis']],
397 input_names=[config['input']])
398 _LOG.debug('completed a wiggle round')
400 timestamp = ('{0}-{1:02d}-{2:02d}T{3:02d}-{4:02d}-{5:02d}'
401 ).format(*_time.localtime())
402 with _h5py.File(filename, 'a') as f:
403 wiggle_group = _h5_create_group(f, group)
404 cwg = _h5_create_group(
405 wiggle_group, 'wiggle/{}'.format(cycle))
406 cwg['time'] = timestamp
407 cwg['raw/{}'.format(config['axis'])] = out
408 cwg['raw/{}'.format(config['input'])] = data
410 plot_p[0].set_ydata(data[:,0])
411 axes.set_ylim([data.min(), data.max()])
414 self.last_output[config['axis']] = out[-1,0]
415 _LOG.debug('interference wiggle complete')
417 get_surface_position = _surface.get_surface_position
421 # if USE_ABCD_DEFLECTION :
422 # for i in range(4) : # i is the photodiode element (0->A, 1->B, ...)
423 # self.curIn[i] = out["Deflection segment"][i][-1]
425 # self.curIn[self.chan_info.def_ind] = out["deflection"][-1]
428 #class FourSegmentAFM (AFM):
429 # def read_deflection(self):
430 # "Return sensor deflection in bits."
431 # A = int(self.curIn[self.chan_info.def_ind[0]])
432 # B = int(self.curIn[self.chan_info.def_ind[1]])
433 # C = int(self.curIn[self.chan_info.def_ind[2]])
434 # D = int(self.curIn[self.chan_info.def_ind[3]])
435 # df = float((A+B)-(C+D))/(A+B+C+D)
436 # dfout = int(df * 2**15) + 2**15
438 # print "Current deflection %d (%d, %d, %d, %d)" \
439 # % (dfout, A, B, C, D)
443 #def test_smoothness(zp, plotVerbose=True):
446 # setpoint = zp.def_V2in(3)
449 # outarray = linspace(posB, posA, 1000)
452 # curVals = zp.jumpToPos(posA)
453 # zp.pCurVals(curVals)
454 # _sleep(1) # let jitters die down
455 # for i in range(10):
456 # print "ramp %d to %d" % (zp.curPos(), posB)
457 # curVals, data = moveToPosOrDef(zp, posB, setpoint, step=steps,
458 # return_data = True)
459 # indata.append(data)
460 # out = zp.ramp(outarray, outfreq)
461 # outdata.append(out)
463 # from pylab import figure, plot, title, legend, hold, subplot
464 # if PYLAB_VERBOSE or plotVerbose:
466 # _pylab.figure(BASE_FIG_NUM+4)
467 # for i in range(10):
468 # _pylab.plot(indata[i]['z'],
469 # indata[i]['deflection'], '+--', label='in')
470 # _pylab.plot(outdata[i]['z'],
471 # outdata[i]['deflection'], '.-', label='out')
472 # _pylab.title('test smoothness (step in, ramp out)')
473 # #_pylab.legend(loc='best')
477 # zp = z_piezo.z_piezo()
478 # curVals = zp.moveToPosOrDef(zp.pos_nm2out(600), defl=zp.curDef()+6000, step=(zp.pos_nm2out(10)-zp.pos_nm2out(0)))
480 # zp.pCurVals(curVals)
481 # pos = zp.getSurfPos(maxDefl=zp.curDef()+6000)
483 # print "Surface at %g nm", pos
485 # if PYLAB_VERBOSE and _final_flush_plot != None:
486 # _final_flush_plot()