bda92d53ea6a4271b144fae81f515e8a70f02beb
[pyafm.git] / pyafm / afm.py
1 # Copyright (C) 2011-2012 W. Trevor King <wking@tremily.us>
2 #
3 # This file is part of pyafm.
4 #
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
8 # version.
9 #
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.
13 #
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/>.
16
17 """Tools for controlling atomic force microscopes.
18
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.
23 """
24
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
31
32 from . import LOG as _LOG
33 from .stepper import Stepper as _Stepper
34 from .temperature import Temperature as _Temperature
35
36
37 class AFM (object):
38     """Atomic force microscope positioning.
39
40     Uses a short range `piezo` and a long range `stepper` to position
41     an AFM tip relative to the surface.
42
43     Parameters
44     ----------
45     piezo | pypiezo.afm.AFMpiezo instance
46         Fine positioning and deflection measurements.
47     stepper | stepper.Stepper instance
48         Coarse positioning.
49     temperature | temperature.Controller instance or None
50         Optional temperature monitoring and control.
51
52     >>> import os
53     >>> import tempfile
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
59
60     >>> fd,filename = tempfile.mkstemp(suffix='.h5', prefix='pyafm-')
61     >>> os.close(fd)
62
63     >>> devices = []
64
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'
82
83     >>> afm = AFM(config=config)
84     >>> afm.load_from_config(devices=devices)
85     >>> afm.setup_config()
86
87     >>> afm.get_temperature()  # doctest: +SKIP
88     297.37
89
90     >>> print(afm.config.dump())  # doctest: +REPORT_UDIFF
91     name: 
92     main-axis: 
93     piezo:
94       name: test piezo
95       axes:
96         0:
97           gain: 1.0
98           sensitivity: 1.0
99           minimum: -10.0
100           maximum: 10.0
101           channel:
102             name: z
103             device: /dev/comedi0
104             subdevice: 1
105             channel: 0
106             maxdata: 65535
107             range: 0
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
113           monitor: 
114       inputs:
115         0:
116           name: deflection
117           device: /dev/comedi0
118           subdevice: 0
119           channel: 0
120           maxdata: 65535
121           range: 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
127     stepper:
128       name: test stepper
129       full-step: yes
130       logic: yes
131       delay: 0.01
132       step-size: 1.7e-07
133       backlash: 100
134       port:
135         name: stepper port
136         device: /dev/comedi0
137         subdevice: 2
138         subdevice-type: dio
139         channels: 1,2,3,4
140         direction: output
141     temperature:
142       name: test temperature
143       units: Celsius
144       controller: 1
145       device: /dev/ttyS0
146       baudrate: 9600
147       max-current: 0.0
148     fallback-temperature: 295.15
149     far: 3e-05
150
151     >>> pyafm.storage.save_afm(afm=afm, filename=filename)
152     >>> pprint_HDF5(filename=filename)  # doctest: +REPORT_UDIFF
153     /
154       <HDF5 dataset "fallback-temperature": shape (), type "<f8">
155         295.15
156       <HDF5 dataset "far": shape (), type "<f8">
157         3e-05
158       <HDF5 dataset "main-axis": shape (), type "|S1">
159     <BLANKLINE>
160       <HDF5 dataset "name": shape (), type "|S1">
161     <BLANKLINE>
162       /piezo
163         /piezo/axes
164           /piezo/axes/0
165             /piezo/axes/0/channel
166               <HDF5 dataset "analog-reference": shape (), type "|S6">
167                 ground
168               <HDF5 dataset "channel": shape (), type "<i4">
169                 0
170               <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
171                 [ -1.00000000e+01   3.05180438e-04]
172               <HDF5 dataset "conversion-origin": shape (), type "<f8">
173                 0.0
174               <HDF5 dataset "device": shape (), type "|S12">
175                 /dev/comedi0
176               <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
177                 [    0.    3276.75]
178               <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
179                 -10.0
180               <HDF5 dataset "maxdata": shape (), type "<i8">
181                 65535
182               <HDF5 dataset "name": shape (), type "|S1">
183                 z
184               <HDF5 dataset "range": shape (), type "<i4">
185                 0
186               <HDF5 dataset "subdevice": shape (), type "<i4">
187                 1
188             <HDF5 dataset "gain": shape (), type "<f8">
189               1.0
190             <HDF5 dataset "maximum": shape (), type "<f8">
191               10.0
192             <HDF5 dataset "minimum": shape (), type "<f8">
193               -10.0
194             <HDF5 dataset "monitor": shape (), type "|S1">
195     <BLANKLINE>
196             <HDF5 dataset "sensitivity": shape (), type "<f8">
197               1.0
198         /piezo/inputs
199           /piezo/inputs/0
200             <HDF5 dataset "analog-reference": shape (), type "|S6">
201               ground
202             <HDF5 dataset "channel": shape (), type "<i4">
203               0
204             <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
205               [ -1.00000000e+01   3.05180438e-04]
206             <HDF5 dataset "conversion-origin": shape (), type "<f8">
207               0.0
208             <HDF5 dataset "device": shape (), type "|S12">
209               /dev/comedi0
210             <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
211               [    0.    3276.75]
212             <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
213               -10.0
214             <HDF5 dataset "maxdata": shape (), type "<i8">
215               65535
216             <HDF5 dataset "name": shape (), type "|S10">
217               deflection
218             <HDF5 dataset "range": shape (), type "<i4">
219               0
220             <HDF5 dataset "subdevice": shape (), type "<i4">
221               0
222         <HDF5 dataset "name": shape (), type "|S10">
223           test piezo
224       /stepper
225         <HDF5 dataset "backlash": shape (), type "<i4">
226           100
227         <HDF5 dataset "delay": shape (), type "<f8">
228           0.01
229         <HDF5 dataset "full-step": shape (), type "|b1">
230           True
231         <HDF5 dataset "logic": shape (), type "|b1">
232           True
233         <HDF5 dataset "name": shape (), type "|S12">
234           test stepper
235         /stepper/port
236           <HDF5 dataset "channels": shape (4,), type "<i4">
237             [1 2 3 4]
238           <HDF5 dataset "device": shape (), type "|S12">
239             /dev/comedi0
240           <HDF5 dataset "direction": shape (), type "|S6">
241             output
242           <HDF5 dataset "name": shape (), type "|S12">
243             stepper port
244           <HDF5 dataset "subdevice": shape (), type "<i4">
245             2
246           <HDF5 dataset "subdevice-type": shape (), type "|S3">
247             dio
248         <HDF5 dataset "step-size": shape (), type "<f8">
249           1.7e-07
250       /temperature
251         <HDF5 dataset "baudrate": shape (), type "<i4">
252           9600
253         <HDF5 dataset "controller": shape (), type "<i4">
254           1
255         <HDF5 dataset "device": shape (), type "|S10">
256           /dev/ttyS0
257         <HDF5 dataset "max-current": shape (), type "<f8">
258           0.0
259         <HDF5 dataset "name": shape (), type "|S16">
260           test temperature
261         <HDF5 dataset "units": shape (), type "|S7">
262           Celsius
263     >>> afm2 = pyafm.storage.load_afm(filename=filename)
264     >>> afm2.load_from_config(devices=devices)
265
266     >>> afm2.get_temperature()  # doctest: +SKIP
267     297.37
268
269     It's hard to test anything else without pugging into an actual AFM.
270
271     >>> for device in devices:
272     ...     device.close()
273
274     Cleanup our temporary config file.
275
276     >>> os.remove(filename)
277     """
278     def __init__(self, config, piezo=None, stepper=None, temperature=None):
279         self.config = config
280         self.piezo = piezo
281         self.stepper = stepper
282         self.temperature = temperature
283
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()
295
296     def setup_config(self):
297         if self.piezo:
298             self.piezo.setup_config()
299             self.config['piezo'] = self.piezo.config
300         else:
301             self.config['piezo'] = None
302         if self.stepper:
303             self.stepper.setup_config()
304             self.config['stepper'] = self.stepper.config
305         else:
306             self.config['stepper'] = None
307         if self.temperature:
308             self.temperature.setup_config()
309             self.config['temperature'] = self.temperature.config
310         else:
311             self.config['temperature'] = None
312
313     def get_temperature(self):
314         """Measure the sample temperature.
315
316         Return the sample temperature in Kelvin or `None` if such a
317         measurement is not possible.
318         """
319         if hasattr(self.temperature, 'get_temperature'):
320             return self.temperature.get_temperature()
321         return self.config['default-temperature']
322
323     def move_just_onto_surface(self, depth=-50e-9, setpoint=2,
324                                min_slope_ratio=10, far=200, steps=20,
325                                sleep=0.0001):
326         """Position the AFM tip close to the surface.
327
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
334
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
338         `setpoint`.
339         """
340         _LOG.info('moving to %g onto the surface' % depth)
341
342         stepper_tolerance = 2*self.stepper.step_size
343
344         axis = self.piezo.axis_by_name(self.config['main-axis'])
345         def_config = self.piezo.config.select_config('inputs', 'deflection')
346
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)
350
351         _LOG.debug('zero the %s piezo output' % self.config['main-axis'])
352         self.piezo.jump(
353             axis_name=self.config['main-axis'], position=zero, steps=steps,
354             sleep=sleep)
355
356         _LOG.debug("see if we're starting near the surface")
357         try:
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)
361         except _FlatFit, e:
362             _LOG.info(e)
363             pos = self._stepper_approach_again(
364                 target_deflection=target_def, min_slope_ratio=min_slope_ratio,
365                 far=far)
366         except _SurfaceError, e:
367             _LOG.info(e)
368             pos = self._stepper_approach_again(
369                 target_deflection=target_def, min_slope_ratio=min_slope_ratio,
370                 far=far)
371
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))
375
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))
380             try:
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)
385             except _FlatFit, e:
386                 _LOG.debug(e)
387                 continue
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))
394             try:
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)
399             except _FlatFit, e:
400                 _LOG.debug(e)
401                 continue
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))
405
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)
410         self.piezo.jump(
411             self.config['main-axis'], target, steps=steps, sleep=sleep)
412
413         _LOG.debug(
414             'positioned %g m into the surface at stepper %d, piezo %d (%g m)'
415             % (depth, self.stepper.position, target, target_m))
416
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)
425         elif deflection < 0:
426             _LOG.error(('requested setpoint ({} bits) is less than the '
427                         'minimum deflection value of 0 bits'
428                         ).format(deflection))
429             raise ValueError(deflection)
430
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))
434         # back away
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)):
438             _LOG.debug(
439                 'additional surface location attempt (stepping backwards)')
440             try:
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)
445                 return pos
446             except _SurfaceError, e:
447                 _LOG.info(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')
451         _LOG.warn(e)
452         raise e
453
454     def stepper_approach(self, target_deflection):
455         _LOG.info('approach with stepper until deflection > {}'.format(
456                 target_deflection))
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()
465
466     def move_toward_surface(self, distance):
467         """Step in approximately `distance` meters.
468         """
469         steps = int(distance/self.stepper.step_size)
470         _LOG.info('step in {} steps (~{} m)'.format(steps, distance))
471         self.stepper.step_relative(steps)
472
473     def move_away_from_surface(self, distance=None):
474         """Step back approximately `distance` meters.
475         """
476         if distance is None:
477             distance = self.config['far']
478         self.move_toward_surface(-distance)