Don't set handler log level; just use the logger log level.
[pyafm.git] / pyafm.py
1 """Tools for controlling atomic force microscopes.
2
3 Provides control of AFM postition using both short-range (piezo) and
4 long range (stepper) vertical positioning.  There are separate modules
5 for controlling the piezo (`pypiezo`) and stepper (`stepper`), this
6 module only contains methods that require the capabilities of both.
7 """
8
9 import logging as _logging
10
11 from pypiezo.base import convert_volts_to_bits as _convert_volts_to_bits
12 from pypiezo.base import convert_meters_to_bits as _convert_meters_to_bits
13 from pypiezo.base import convert_bits_to_meters as _convert_bits_to_meters
14 from pypiezo.surface import SurfaceError as _SurfaceError
15 from pypiezo.surface import FlatFit as _FlatFit
16
17
18 __version__ = '0.1'
19
20
21 LOG = _logging.getLogger('pyafm')
22 "pyafm logger"
23
24 LOG.setLevel(_logging.DEBUG)
25 h = _logging.StreamHandler()
26 f = _logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
27 h.setFormatter(f)
28 LOG.addHandler(h)
29 del h, f
30
31
32 class AFM (object):
33     """Atomic force microscope positioning.
34
35     Uses a short range `piezo` and a long range `stepper` to position
36     an AFM tip relative to the surface.
37
38     Parameters
39     ----------
40     piezo | pypiezo.afm.AFMpiezo instance
41         Fine positioning and deflection measurements.
42     stepper | stepper.Stepper instance
43         Coarse positioning.
44     temperature | temperature.Controller instance or None
45         Optional temperature monitoring and control.
46     """
47     def __init__(self, piezo, stepper, temperature=None, axis_name='z'):
48         self.piezo = piezo
49         self.stepper = stepper
50         self.temperature = temperature
51         self.axis_name = axis_name
52
53     def get_temperature(self):
54         """Measure the sample temperature.
55
56         Return the sample temperature in Kelvin or `None` if such a
57         measurement is not possible.
58         """
59         if hasattr(self.temperature, 'get'):
60             return self.temperature.get_temperature()
61
62     def move_just_onto_surface(self, depth=-50e-9, setpoint=2, far=200):
63         """Position the AFM tip close to the surface.
64
65         Uses `.piezo.get_surface_position()` to pinpoint the position
66         of the surface.  Adjusts the stepper position as required via
67         `.stepper.step_relative()` to get within
68         `2*.stepper.step_size` meters of the surface.  Then adjusts
69         the piezo to place the cantilever `depth` meters onto the
70         surface.  Negative `depth`\s place the tip off the surface
71
72         If `.piezo.get_surface_position()` fails to find the surface,
73         backs off `far` half steps (for safety) and steps in (without
74         moving the zpiezo) until deflection voltage is greater than
75         `setpoint`.
76         """
77         LOG.info('moving to %g onto the surface' % depth)
78
79         stepper_tolerance = 2*self.stepper.step_size
80
81         axis = self.piezo.axis_by_name(self.axis_name)
82
83         zero = _convert_volts_to_bits(axis.axis_channel_config, 0)
84         target_def = _convert_volts_to_bits(axis.axis_channel_config, setpoint)
85
86         LOG.debug('zero the %s piezo output' % self.axis_name)
87         self.piezo.jump(axis_name=self.axis_name, position=zero)
88
89         LOG.debug("see if we're starting near the surface")
90         try:
91             pos = self.piezo.get_surface_position(
92                 axis_name=self.axis_name, max_deflection=target_def)
93         except _FlatFit, e:
94             LOG.info(e)
95             pos = self._stepper_approach_again(
96                 target_deflection=target_def, far=far)
97         except _SurfaceError, e:
98             LOG.info(e)
99             pos = self._stepper_approach_again(
100                 target_deflection=target_def, far=far)
101
102         pos_m = _convert_bits_to_meters(
103             axis.axis_channel_config, axis.axis_config, pos)
104         LOG.debug('located surface at stepper %d, piezo %d (%g m)'
105                   % (self.stepper.position, pos, pos_m))
106
107         LOG.debug('fine tune the stepper position')
108         while pos_m < -stepper_tolerance:  # step back if we need to
109             LOG.debug('step back')
110             self.stepper.step_relative(-1, backlash_safe=True)
111             try:
112                 pos = self.piezo.get_surface_position(
113                     axis_name=self.axis_name, max_deflection=target_def)
114             except _FlatFit, e:
115                 LOG.debug(e)
116                 continue
117             pos_m = _convert_bits_to_meters(
118                 axis.axis_channel_config, axis.axis_config, pos)
119             LOG.debug('located surface at stepper %d, piezo %d (%g m)'
120                       % (self.stepper.position, pos, pos_m))
121         while pos_m > stepper_tolerance:  # step forward if we need to
122             LOG.debug('step forward')
123             self.stepper.step_relative(1)
124             try:
125                 pos = self.piezo.get_surface_position(
126                     axis_name=self.axis_name, max_deflection=target_def)
127             except _FlatFit, e:
128                 LOG.debug(e)
129                 continue
130             pos_m = _convert_bits_to_meters(
131                 axis.axis_channel_config, axis.axis_config, pos)
132             LOG.debug('located surface at stepper %d, piezo %d (%g m)'
133                       % (self.stepper.position, pos, pos_m))
134
135         LOG.debug('adjust the %s piezo to place us just onto the surface'
136                   % self.axis_name)
137         target_m = pos_m + depth
138         target = _convert_meters_to_bits(
139             axis.axis_channel_config, axis.axis_config, target_m)
140         self.piezo.jump(self.axis_name, target)
141
142         LOG.debug(
143             'positioned %g m into the surface at stepper %d, piezo %d (%g m)'
144             % (depth, self.stepper.position, target, target_m))
145
146
147     def _stepper_approach_again(self, target_deflection, far):
148         LOG.info('back off %d half steps and approach until deflection > %g'
149                  % (far, target_deflection))
150
151         # back away
152         self.stepper.step_relative(-far, backlash_safe=True)
153
154         cd = self.piezo.read_deflection()  # cd = current deflection in bits
155         LOG.debug('single stepping approach')
156         while cd < target_deflection:
157             LOG.debug('deflection %g < setpoint %g.  step closer'
158                       % (cd, target_deflection))
159             self.stepper.single_step(1)  # step in
160             cd = self.piezo.read_deflection()
161
162         for i in range(2*max(1, self.stepper.backlash)):
163             LOG.debug('additional surface location attempt')
164             try:
165                 pos = self.piezo.get_surface_position(
166                     axis_name=self.axis_name, max_deflection=target_deflection)
167                 return pos
168             except _SurfaceError, e:
169                 LOG.info(e)
170             self.stepper.single_step(-1)  # step out
171         LOG.debug('giving up on finding the surface')
172         LOG.warn(e)
173         raise e