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.
25 from pypiezo.afm import AFMPiezo as _AFMPiezo
26 from pypiezo.base import convert_bits_to_meters as _convert_bits_to_meters
27 from pypiezo.base import convert_meters_to_bits as _convert_meters_to_bits
28 from pypiezo.base import convert_volts_to_bits as _convert_volts_to_bits
29 from pypiezo.surface import FlatFit as _FlatFit
30 from pypiezo.surface import SurfaceError as _SurfaceError
32 from . import LOG as _LOG
33 from .stepper import Stepper as _Stepper
34 from .temperature import Temperature as _Temperature
38 """Atomic force microscope positioning.
40 Uses a short range `piezo` and a long range `stepper` to position
41 an AFM tip relative to the surface.
45 piezo | pypiezo.afm.AFMpiezo instance
46 Fine positioning and deflection measurements.
47 stepper | stepper.Stepper instance
49 temperature | temperature.Controller instance or None
50 Optional temperature monitoring and control.
54 >>> from pycomedi import constant
55 >>> import pypiezo.config
56 >>> import pyafm.config
57 >>> import pyafm.storage
58 >>> from h5config.storage.hdf5 import pprint_HDF5
60 >>> fd,filename = tempfile.mkstemp(suffix='.h5', prefix='pyafm-')
65 >>> config = pyafm.config.AFMConfig()
66 >>> config['piezo'] = pypiezo.config.PiezoConfig()
67 >>> config['piezo']['name'] = 'test piezo'
68 >>> config['piezo']['axes'] = [pypiezo.config.AxisConfig()]
69 >>> config['piezo']['axes'][0]['channel'] = (
70 ... pypiezo.config.OutputChannelConfig())
71 >>> config['piezo']['axes'][0]['channel']['name'] = 'z'
72 >>> config['piezo']['inputs'] = [pypiezo.config.InputChannelConfig()]
73 >>> config['piezo']['inputs'][0]['name'] = 'deflection'
74 >>> config['stepper'] = pyafm.config.StepperConfig()
75 >>> config['stepper']['port'] = pyafm.config.DigitalPortConfig()
76 >>> config['stepper']['port']['channels'] = [1, 2, 3, 4]
77 >>> config['stepper']['port']['direction'] = constant.IO_DIRECTION.output
78 >>> config['stepper']['port']['name'] = 'stepper port'
79 >>> config['stepper']['name'] = 'test stepper'
80 >>> config['temperature'] = pyafm.config.TemperatureConfig()
81 >>> config['temperature']['name'] = 'test temperature'
83 >>> afm = AFM(config=config)
84 >>> afm.load_from_config(devices=devices)
85 >>> afm.setup_config()
87 >>> afm.get_temperature() # doctest: +SKIP
90 >>> print(afm.config.dump()) # doctest: +REPORT_UDIFF
108 analog-reference: ground
109 conversion-coefficients: -10.0,0.000305180437934
110 conversion-origin: 0.0
111 inverse-conversion-coefficients: 0.0,3276.75
112 inverse-conversion-origin: -10.0
122 analog-reference: ground
123 conversion-coefficients: -10.0,0.000305180437934
124 conversion-origin: 0.0
125 inverse-conversion-coefficients: 0.0,3276.75
126 inverse-conversion-origin: -10.0
142 name: test temperature
148 fallback-temperature: 295.15
151 >>> pyafm.storage.save_afm(afm=afm, filename=filename)
152 >>> pprint_HDF5(filename=filename) # doctest: +REPORT_UDIFF
154 <HDF5 dataset "fallback-temperature": shape (), type "<f8">
156 <HDF5 dataset "far": shape (), type "<f8">
158 <HDF5 dataset "main-axis": shape (), type "|S1">
160 <HDF5 dataset "name": shape (), type "|S1">
165 /piezo/axes/0/channel
166 <HDF5 dataset "analog-reference": shape (), type "|S6">
168 <HDF5 dataset "channel": shape (), type "<i4">
170 <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
171 [ -1.00000000e+01 3.05180438e-04]
172 <HDF5 dataset "conversion-origin": shape (), type "<f8">
174 <HDF5 dataset "device": shape (), type "|S12">
176 <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
178 <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
180 <HDF5 dataset "maxdata": shape (), type "<i8">
182 <HDF5 dataset "name": shape (), type "|S1">
184 <HDF5 dataset "range": shape (), type "<i4">
186 <HDF5 dataset "subdevice": shape (), type "<i4">
188 <HDF5 dataset "gain": shape (), type "<f8">
190 <HDF5 dataset "maximum": shape (), type "<f8">
192 <HDF5 dataset "minimum": shape (), type "<f8">
194 <HDF5 dataset "monitor": shape (), type "|S1">
196 <HDF5 dataset "sensitivity": shape (), type "<f8">
200 <HDF5 dataset "analog-reference": shape (), type "|S6">
202 <HDF5 dataset "channel": shape (), type "<i4">
204 <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
205 [ -1.00000000e+01 3.05180438e-04]
206 <HDF5 dataset "conversion-origin": shape (), type "<f8">
208 <HDF5 dataset "device": shape (), type "|S12">
210 <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
212 <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
214 <HDF5 dataset "maxdata": shape (), type "<i8">
216 <HDF5 dataset "name": shape (), type "|S10">
218 <HDF5 dataset "range": shape (), type "<i4">
220 <HDF5 dataset "subdevice": shape (), type "<i4">
222 <HDF5 dataset "name": shape (), type "|S10">
225 <HDF5 dataset "backlash": shape (), type "<i4">
227 <HDF5 dataset "delay": shape (), type "<f8">
229 <HDF5 dataset "full-step": shape (), type "|b1">
231 <HDF5 dataset "logic": shape (), type "|b1">
233 <HDF5 dataset "name": shape (), type "|S12">
236 <HDF5 dataset "channels": shape (4,), type "<i4">
238 <HDF5 dataset "device": shape (), type "|S12">
240 <HDF5 dataset "direction": shape (), type "|S6">
242 <HDF5 dataset "name": shape (), type "|S12">
244 <HDF5 dataset "subdevice": shape (), type "<i4">
246 <HDF5 dataset "subdevice-type": shape (), type "|S3">
248 <HDF5 dataset "step-size": shape (), type "<f8">
251 <HDF5 dataset "baudrate": shape (), type "<i4">
253 <HDF5 dataset "controller": shape (), type "<i4">
255 <HDF5 dataset "device": shape (), type "|S10">
257 <HDF5 dataset "max-current": shape (), type "<f8">
259 <HDF5 dataset "name": shape (), type "|S16">
261 <HDF5 dataset "units": shape (), type "|S7">
263 >>> afm2 = pyafm.storage.load_afm(filename=filename)
264 >>> afm2.load_from_config(devices=devices)
266 >>> afm2.get_temperature() # doctest: +SKIP
269 It's hard to test anything else without pugging into an actual AFM.
271 >>> for device in devices:
274 Cleanup our temporary config file.
276 >>> os.remove(filename)
278 def __init__(self, config, piezo=None, stepper=None, temperature=None):
281 self.stepper = stepper
282 self.temperature = temperature
284 def load_from_config(self, devices):
285 c = self.config # reduce verbosity
286 if self.piezo is None and c['piezo']:
287 self.piezo = _AFMPiezo(config=c['piezo'])
288 self.piezo.load_from_config(devices=devices)
289 if self.stepper is None and c['stepper']:
290 self.stepper = _Stepper(config=c['stepper'])
291 self.stepper.load_from_config(devices=devices)
292 if self.temperature is None and c['temperature']:
293 self.temperature = _Temperature(config=c['temperature'])
294 self.temperature.load_from_config()
296 def setup_config(self):
298 self.piezo.setup_config()
299 self.config['piezo'] = self.piezo.config
301 self.config['piezo'] = None
303 self.stepper.setup_config()
304 self.config['stepper'] = self.stepper.config
306 self.config['stepper'] = None
308 self.temperature.setup_config()
309 self.config['temperature'] = self.temperature.config
311 self.config['temperature'] = None
313 def get_temperature(self):
314 """Measure the sample temperature.
316 Return the sample temperature in Kelvin or `None` if such a
317 measurement is not possible.
319 if hasattr(self.temperature, 'get_temperature'):
320 return self.temperature.get_temperature()
321 return self.config['default-temperature']
323 def move_just_onto_surface(self, depth=-50e-9, setpoint=2,
324 min_slope_ratio=10, far=200, steps=20,
326 """Position the AFM tip close to the surface.
328 Uses `.piezo.get_surface_position()` to pinpoint the position
329 of the surface. Adjusts the stepper position as required via
330 `.stepper.single_step()` to get within
331 `2*.stepper.step_size` meters of the surface. Then adjusts
332 the piezo to place the cantilever `depth` meters onto the
333 surface. Negative `depth`\s place the tip off the surface
335 If `.piezo.get_surface_position()` fails to find the surface,
336 backs off `far` half steps (for safety) and steps in (without
337 moving the zpiezo) until deflection voltage is greater than
340 _LOG.info('moving to %g onto the surface' % depth)
342 stepper_tolerance = 2*self.stepper.step_size
344 axis = self.piezo.axis_by_name(self.config['main-axis'])
345 def_config = self.piezo.config.select_config('inputs', 'deflection')
347 zero = _convert_volts_to_bits(axis.config['channel'], 0)
348 target_def = _convert_volts_to_bits(def_config, setpoint)
349 self._check_target_deflection(deflection=target_def)
351 _LOG.debug('zero the %s piezo output' % self.config['main-axis'])
353 axis_name=self.config['main-axis'], position=zero, steps=steps,
356 _LOG.debug("see if we're starting near the surface")
358 pos = self.piezo.get_surface_position(
359 axis_name=self.config['main-axis'], max_deflection=target_def,
360 min_slope_ratio=min_slope_ratio)
363 pos = self._stepper_approach_again(
364 target_deflection=target_def, min_slope_ratio=min_slope_ratio,
366 except _SurfaceError, e:
368 pos = self._stepper_approach_again(
369 target_deflection=target_def, min_slope_ratio=min_slope_ratio,
372 pos_m = _convert_bits_to_meters(axis.config, pos)
373 _LOG.debug('located surface at stepper %d, piezo %d (%g m)'
374 % (self.stepper.position, pos, pos_m))
376 _LOG.debug('fine tune the stepper position')
377 while pos_m < -stepper_tolerance: # step back if we need to
378 self.stepper.single_step(-1)
379 _LOG.debug('step back to {}'.format(self.stepper.position))
381 pos = self.piezo.get_surface_position(
382 axis_name=self.config['main-axis'],
383 max_deflection=target_def,
384 min_slope_ratio=min_slope_ratio)
388 pos_m = _convert_bits_to_meters(axis.config, pos)
389 _LOG.debug('located surface at stepper %d, piezo %d (%g m)'
390 % (self.stepper.position, pos, pos_m))
391 while pos_m > stepper_tolerance: # step forward if we need to
392 self.stepper.single_step(1)
393 _LOG.debug('step forward to {}'.format(self.stepper.position))
395 pos = self.piezo.get_surface_position(
396 axis_name=self.config['main-axis'],
397 max_deflection=target_def,
398 min_slope_ratio=min_slope_ratio)
402 pos_m = _convert_bits_to_meters(axis.config, pos)
403 _LOG.debug('located surface at stepper %d, piezo %d (%g m)'
404 % (self.stepper.position, pos, pos_m))
406 _LOG.debug('adjust the %s piezo to place us just onto the surface'
407 % self.config['main-axis'])
408 target_m = pos_m + depth
409 target = _convert_meters_to_bits(axis.config, target_m)
411 self.config['main-axis'], target, steps=steps, sleep=sleep)
414 'positioned %g m into the surface at stepper %d, piezo %d (%g m)'
415 % (depth, self.stepper.position, target, target_m))
417 def _check_target_deflection(self, deflection):
418 defc = self.piezo._deflection_channel()
419 max_def = defc.get_maxdata()
420 if deflection > max_def:
421 _LOG.error(('requested setpoint ({} bits) is larger than the '
422 'maximum deflection value of {} bits'
423 ).format(deflection, max_def))
424 raise ValueError(deflection)
426 _LOG.error(('requested setpoint ({} bits) is less than the '
427 'minimum deflection value of 0 bits'
428 ).format(deflection))
429 raise ValueError(deflection)
431 def _stepper_approach_again(self, target_deflection, min_slope_ratio, far):
432 _LOG.info('back off %d half steps and approach until deflection > %g'
433 % (far, target_deflection))
435 self.stepper.step_relative(-far, backlash_safe=True)
436 self.stepper_approach(target_deflection=target_deflection)
437 for i in range(2*max(1, self.stepper.backlash)):
439 'additional surface location attempt (stepping backwards)')
441 pos = self.piezo.get_surface_position(
442 axis_name=self.config['main-axis'],
443 max_deflection=target_deflection,
444 min_slope_ratio=min_slope_ratio)
446 except _SurfaceError, e:
448 self.stepper.single_step(-1) # step out
449 _LOG.debug('stepped back to {}'.format(self.stepper.position))
450 _LOG.debug('giving up on finding the surface')
454 def stepper_approach(self, target_deflection):
455 _LOG.info('approach with stepper until deflection > {}'.format(
457 self._check_target_deflection(deflection=target_deflection)
458 cd = self.piezo.read_deflection() # cd = current deflection in bits
459 _LOG.debug('single stepping approach')
460 while cd < target_deflection:
461 _LOG.debug('deflection {} < setpoint {}. step closer'.format(
462 cd, target_deflection))
463 self.stepper.single_step(1) # step in
464 cd = self.piezo.read_deflection()
466 def move_toward_surface(self, distance):
467 """Step in approximately `distance` meters.
469 steps = int(distance/self.stepper.step_size)
470 _LOG.info('step in {} steps (~{} m)'.format(steps, distance))
471 self.stepper.step_relative(steps)
473 def move_away_from_surface(self, distance=None):
474 """Step back approximately `distance` meters.
477 distance = self.config['far']
478 self.move_toward_surface(-distance)