1 # Copyright (C) 2011-2012 W. Trevor King <wking@tremily.us>
3 # This file is part of pyafm.
5 # pyafm 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 # pyafm 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 # pyafm. If not, see <http://www.gnu.org/licenses/>.
17 """Tools for controlling atomic force microscopes.
19 Provides control of AFM postition using both short-range (piezo) and
20 long range (stepper) vertical positioning. There are separate modules
21 for controlling the piezo (`pypiezo`) and stepper (`stepper`), this
22 module only contains methods that require the capabilities of both.
26 import matplotlib as _matplotlib
27 import matplotlib.pyplot as _matplotlib_pyplot
28 import time as _time # for timestamping lines on plots
29 except (ImportError, RuntimeError), e:
31 _matplotlib_import_error = e
33 from pypiezo.afm import AFMPiezo as _AFMPiezo
34 from pypiezo.base import convert_bits_to_meters as _convert_bits_to_meters
35 from pypiezo.base import convert_meters_to_bits as _convert_meters_to_bits
36 from pypiezo.base import convert_volts_to_bits as _convert_volts_to_bits
37 from pypiezo.surface import FlatFit as _FlatFit
38 from pypiezo.surface import SurfaceError as _SurfaceError
40 from . import LOG as _LOG
41 from . import package_config as _package_config
42 from .stepper import Stepper as _Stepper
43 from .temperature import Temperature as _Temperature
47 """Atomic force microscope positioning.
49 Uses a short range `piezo` and a long range `stepper` to position
50 an AFM tip relative to the surface.
54 piezo | pypiezo.afm.AFMpiezo instance
55 Fine positioning and deflection measurements.
56 stepper | stepper.Stepper instance
58 temperature | temperature.Controller instance or None
59 Optional temperature monitoring and control.
63 >>> from pycomedi import constant
64 >>> import pypiezo.config
65 >>> import pyafm.config
66 >>> import pyafm.storage
67 >>> from h5config.storage.hdf5 import pprint_HDF5
69 >>> fd,filename = tempfile.mkstemp(suffix='.h5', prefix='pyafm-')
74 >>> config = pyafm.config.AFMConfig()
75 >>> config['piezo'] = pypiezo.config.PiezoConfig()
76 >>> config['piezo']['name'] = 'test piezo'
77 >>> config['piezo']['axes'] = [pypiezo.config.AxisConfig()]
78 >>> config['piezo']['axes'][0]['channel'] = (
79 ... pypiezo.config.OutputChannelConfig())
80 >>> config['piezo']['axes'][0]['channel']['name'] = 'z'
81 >>> config['piezo']['inputs'] = [pypiezo.config.InputChannelConfig()]
82 >>> config['piezo']['inputs'][0]['name'] = 'deflection'
83 >>> config['stepper'] = pyafm.config.StepperConfig()
84 >>> config['stepper']['port'] = pyafm.config.DigitalPortConfig()
85 >>> config['stepper']['port']['channels'] = [1, 2, 3, 4]
86 >>> config['stepper']['port']['direction'] = constant.IO_DIRECTION.output
87 >>> config['stepper']['port']['name'] = 'stepper port'
88 >>> config['stepper']['name'] = 'test stepper'
89 >>> config['temperature'] = pyafm.config.TemperatureConfig()
90 >>> config['temperature']['name'] = 'test temperature'
92 >>> afm = AFM(config=config)
93 >>> afm.load_from_config(devices=devices)
94 >>> afm.setup_config()
96 >>> afm.get_temperature() # doctest: +SKIP
99 >>> print(afm.config.dump()) # doctest: +REPORT_UDIFF
117 analog-reference: ground
118 conversion-coefficients: -10.0,0.000305180437934
119 conversion-origin: 0.0
120 inverse-conversion-coefficients: 0.0,3276.75
121 inverse-conversion-origin: -10.0
131 analog-reference: ground
132 conversion-coefficients: -10.0,0.000305180437934
133 conversion-origin: 0.0
134 inverse-conversion-coefficients: 0.0,3276.75
135 inverse-conversion-origin: -10.0
151 name: test temperature
157 fallback-temperature: 295.15
160 >>> pyafm.storage.save_afm(afm=afm, filename=filename)
161 >>> pprint_HDF5(filename=filename) # doctest: +REPORT_UDIFF
163 <HDF5 dataset "fallback-temperature": shape (), type "<f8">
165 <HDF5 dataset "far": shape (), type "<f8">
167 <HDF5 dataset "main-axis": shape (), type "|S1">
169 <HDF5 dataset "name": shape (), type "|S1">
174 /piezo/axes/0/channel
175 <HDF5 dataset "analog-reference": shape (), type "|S6">
177 <HDF5 dataset "channel": shape (), type "<i4">
179 <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
180 [ -1.00000000e+01 3.05180438e-04]
181 <HDF5 dataset "conversion-origin": shape (), type "<f8">
183 <HDF5 dataset "device": shape (), type "|S12">
185 <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
187 <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
189 <HDF5 dataset "maxdata": shape (), type "<i8">
191 <HDF5 dataset "name": shape (), type "|S1">
193 <HDF5 dataset "range": shape (), type "<i4">
195 <HDF5 dataset "subdevice": shape (), type "<i4">
197 <HDF5 dataset "gain": shape (), type "<f8">
199 <HDF5 dataset "maximum": shape (), type "<f8">
201 <HDF5 dataset "minimum": shape (), type "<f8">
203 <HDF5 dataset "monitor": shape (), type "|S1">
205 <HDF5 dataset "sensitivity": shape (), type "<f8">
209 <HDF5 dataset "analog-reference": shape (), type "|S6">
211 <HDF5 dataset "channel": shape (), type "<i4">
213 <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
214 [ -1.00000000e+01 3.05180438e-04]
215 <HDF5 dataset "conversion-origin": shape (), type "<f8">
217 <HDF5 dataset "device": shape (), type "|S12">
219 <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
221 <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
223 <HDF5 dataset "maxdata": shape (), type "<i8">
225 <HDF5 dataset "name": shape (), type "|S10">
227 <HDF5 dataset "range": shape (), type "<i4">
229 <HDF5 dataset "subdevice": shape (), type "<i4">
231 <HDF5 dataset "name": shape (), type "|S10">
234 <HDF5 dataset "backlash": shape (), type "<i4">
236 <HDF5 dataset "delay": shape (), type "<f8">
238 <HDF5 dataset "full-step": shape (), type "|b1">
240 <HDF5 dataset "logic": shape (), type "|b1">
242 <HDF5 dataset "name": shape (), type "|S12">
245 <HDF5 dataset "channels": shape (4,), type "<i4">
247 <HDF5 dataset "device": shape (), type "|S12">
249 <HDF5 dataset "direction": shape (), type "|S6">
251 <HDF5 dataset "name": shape (), type "|S12">
253 <HDF5 dataset "subdevice": shape (), type "<i4">
255 <HDF5 dataset "subdevice-type": shape (), type "|S3">
257 <HDF5 dataset "step-size": shape (), type "<f8">
260 <HDF5 dataset "baudrate": shape (), type "<i4">
262 <HDF5 dataset "controller": shape (), type "<i4">
264 <HDF5 dataset "device": shape (), type "|S10">
266 <HDF5 dataset "max-current": shape (), type "<f8">
268 <HDF5 dataset "name": shape (), type "|S16">
270 <HDF5 dataset "units": shape (), type "|S7">
272 >>> afm2 = pyafm.storage.load_afm(filename=filename)
273 >>> afm2.load_from_config(devices=devices)
275 >>> afm2.get_temperature() # doctest: +SKIP
278 It's hard to test anything else without pugging into an actual AFM.
280 >>> for device in devices:
283 Cleanup our temporary config file.
285 >>> os.remove(filename)
287 def __init__(self, config, piezo=None, stepper=None, temperature=None):
290 self.stepper = stepper
291 self.temperature = temperature
293 def load_from_config(self, devices):
294 c = self.config # reduce verbosity
295 if self.piezo is None and c['piezo']:
296 self.piezo = _AFMPiezo(config=c['piezo'])
297 self.piezo.load_from_config(devices=devices)
298 if self.stepper is None and c['stepper']:
299 self.stepper = _Stepper(config=c['stepper'])
300 self.stepper.load_from_config(devices=devices)
301 if self.temperature is None and c['temperature']:
302 self.temperature = _Temperature(config=c['temperature'])
303 self.temperature.load_from_config()
305 def setup_config(self):
307 self.piezo.setup_config()
308 self.config['piezo'] = self.piezo.config
310 self.config['piezo'] = None
312 self.stepper.setup_config()
313 self.config['stepper'] = self.stepper.config
315 self.config['stepper'] = None
317 self.temperature.setup_config()
318 self.config['temperature'] = self.temperature.config
320 self.config['temperature'] = None
322 def get_temperature(self):
323 """Measure the sample temperature.
325 Return the sample temperature in Kelvin or `None` if such a
326 measurement is not possible.
328 if hasattr(self.temperature, 'get_temperature'):
329 return self.temperature.get_temperature()
330 return self.config['default-temperature']
332 def move_just_onto_surface(self, depth=-50e-9, setpoint=2,
333 min_slope_ratio=10, far=200, steps=20,
335 """Position the AFM tip close to the surface.
337 Uses `.piezo.get_surface_position()` to pinpoint the position
338 of the surface. Adjusts the stepper position as required via
339 `.stepper.single_step()` to get within
340 `2*.stepper.step_size` meters of the surface. Then adjusts
341 the piezo to place the cantilever `depth` meters onto the
342 surface. Negative `depth`\s place the tip off the surface
344 If `.piezo.get_surface_position()` fails to find the surface,
345 backs off `far` half steps (for safety) and steps in (without
346 moving the zpiezo) until deflection voltage is greater than
349 _LOG.info('moving to %g onto the surface' % depth)
351 stepper_tolerance = 2*self.stepper.step_size
353 axis = self.piezo.axis_by_name(self.config['main-axis'])
354 def_config = self.piezo.config.select_config('inputs', 'deflection')
356 zero = _convert_volts_to_bits(axis.config['channel'], 0)
357 target_def = _convert_volts_to_bits(def_config, setpoint)
358 self._check_target_deflection(deflection=target_def)
360 _LOG.debug('zero the %s piezo output' % self.config['main-axis'])
362 axis_name=self.config['main-axis'], position=zero, steps=steps,
365 _LOG.debug("see if we're starting near the surface")
367 pos = self.piezo.get_surface_position(
368 axis_name=self.config['main-axis'], max_deflection=target_def,
369 min_slope_ratio=min_slope_ratio)
372 pos = self._stepper_approach_again(
373 target_deflection=target_def, min_slope_ratio=min_slope_ratio,
375 except _SurfaceError, e:
377 pos = self._stepper_approach_again(
378 target_deflection=target_def, min_slope_ratio=min_slope_ratio,
381 pos_m = _convert_bits_to_meters(axis.config, pos)
382 _LOG.debug('located surface at stepper %d, piezo %d (%g m)'
383 % (self.stepper.position, pos, pos_m))
385 _LOG.debug('fine tune the stepper position')
386 while pos_m < -stepper_tolerance: # step back if we need to
387 self.stepper.single_step(-1)
388 _LOG.debug('step back to {}'.format(self.stepper.position))
390 pos = self.piezo.get_surface_position(
391 axis_name=self.config['main-axis'],
392 max_deflection=target_def,
393 min_slope_ratio=min_slope_ratio)
397 pos_m = _convert_bits_to_meters(axis.config, pos)
398 _LOG.debug('located surface at stepper %d, piezo %d (%g m)'
399 % (self.stepper.position, pos, pos_m))
400 while pos_m > stepper_tolerance: # step forward if we need to
401 self.stepper.single_step(1)
402 _LOG.debug('step forward to {}'.format(self.stepper.position))
404 pos = self.piezo.get_surface_position(
405 axis_name=self.config['main-axis'],
406 max_deflection=target_def,
407 min_slope_ratio=min_slope_ratio)
411 pos_m = _convert_bits_to_meters(axis.config, pos)
412 _LOG.debug('located surface at stepper %d, piezo %d (%g m)'
413 % (self.stepper.position, pos, pos_m))
415 _LOG.debug('adjust the %s piezo to place us just onto the surface'
416 % self.config['main-axis'])
417 target_m = pos_m + depth
418 target = _convert_meters_to_bits(axis.config, target_m)
420 self.config['main-axis'], target, steps=steps, sleep=sleep)
423 'positioned %g m into the surface at stepper %d, piezo %d (%g m)'
424 % (depth, self.stepper.position, target, target_m))
426 def _check_target_deflection(self, deflection):
427 defc = self.piezo._deflection_channel()
428 max_def = defc.get_maxdata()
429 if deflection > max_def:
430 _LOG.error(('requested setpoint ({} bits) is larger than the '
431 'maximum deflection value of {} bits'
432 ).format(deflection, max_def))
433 raise ValueError(deflection)
435 _LOG.error(('requested setpoint ({} bits) is less than the '
436 'minimum deflection value of 0 bits'
437 ).format(deflection))
438 raise ValueError(deflection)
440 def _stepper_approach_again(self, target_deflection, min_slope_ratio, far):
441 _LOG.info('back off %d half steps and approach until deflection > %g'
442 % (far, target_deflection))
444 self.stepper.step_relative(-far, backlash_safe=True)
445 self.stepper_approach(target_deflection=target_deflection)
446 for i in range(2*max(1, self.stepper.backlash)):
448 'additional surface location attempt (stepping backwards)')
450 pos = self.piezo.get_surface_position(
451 axis_name=self.config['main-axis'],
452 max_deflection=target_deflection,
453 min_slope_ratio=min_slope_ratio)
455 except _SurfaceError, e:
457 self.stepper.single_step(-1) # step out
458 _LOG.debug('stepped back to {}'.format(self.stepper.position))
459 _LOG.debug('giving up on finding the surface')
463 def stepper_approach(self, target_deflection):
464 _LOG.info('approach with stepper until deflection > {}'.format(
466 record_data = _package_config['matplotlib']
470 self._check_target_deflection(deflection=target_deflection)
471 cd = self.piezo.read_deflection() # cd = current deflection in bits
472 _LOG.debug('single stepping approach')
473 while cd < target_deflection:
474 _LOG.debug('deflection {} < setpoint {}. step closer'.format(
475 cd, target_deflection))
476 self.stepper.single_step(1) # step in
477 cd = self.piezo.read_deflection()
479 position.append(self.stepper.position)
480 deflection.append(cd)
481 if _package_config['matplotlib']:
482 figure = _matplotlib_pyplot.figure()
483 axes = figure.add_subplot(1, 1, 1)
485 timestamp = _time.strftime('%H-%M-%S')
486 axes.set_title('stepper approach {}'.format(timestamp))
487 plot = axes.plot(position, deflection, 'b.-')
490 if not _matplotlib.is_interactive():
491 _matplotlib_pyplot.show()
493 def move_toward_surface(self, distance):
494 """Step in approximately `distance` meters.
496 steps = int(distance/self.stepper.step_size)
497 _LOG.info('step in {} steps (~{} m)'.format(steps, distance))
498 self.stepper.step_relative(steps)
500 def move_away_from_surface(self, distance=None):
501 """Step back approximately `distance` meters.
504 distance = self.config['far']
505 self.move_toward_surface(-distance)