Add matplotlib config option and stepper_approach plot.
[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 try:
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:
30     _matplotlib = None
31     _matplotlib_import_error = e
32
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
39
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
44
45
46 class AFM (object):
47     """Atomic force microscope positioning.
48
49     Uses a short range `piezo` and a long range `stepper` to position
50     an AFM tip relative to the surface.
51
52     Parameters
53     ----------
54     piezo | pypiezo.afm.AFMpiezo instance
55         Fine positioning and deflection measurements.
56     stepper | stepper.Stepper instance
57         Coarse positioning.
58     temperature | temperature.Controller instance or None
59         Optional temperature monitoring and control.
60
61     >>> import os
62     >>> import tempfile
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
68
69     >>> fd,filename = tempfile.mkstemp(suffix='.h5', prefix='pyafm-')
70     >>> os.close(fd)
71
72     >>> devices = []
73
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'
91
92     >>> afm = AFM(config=config)
93     >>> afm.load_from_config(devices=devices)
94     >>> afm.setup_config()
95
96     >>> afm.get_temperature()  # doctest: +SKIP
97     297.37
98
99     >>> print(afm.config.dump())  # doctest: +REPORT_UDIFF
100     name: 
101     main-axis: 
102     piezo:
103       name: test piezo
104       axes:
105         0:
106           gain: 1.0
107           sensitivity: 1.0
108           minimum: -10.0
109           maximum: 10.0
110           channel:
111             name: z
112             device: /dev/comedi0
113             subdevice: 1
114             channel: 0
115             maxdata: 65535
116             range: 0
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
122           monitor: 
123       inputs:
124         0:
125           name: deflection
126           device: /dev/comedi0
127           subdevice: 0
128           channel: 0
129           maxdata: 65535
130           range: 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
136     stepper:
137       name: test stepper
138       full-step: yes
139       logic: yes
140       delay: 0.01
141       step-size: 1.7e-07
142       backlash: 100
143       port:
144         name: stepper port
145         device: /dev/comedi0
146         subdevice: 2
147         subdevice-type: dio
148         channels: 1,2,3,4
149         direction: output
150     temperature:
151       name: test temperature
152       units: Celsius
153       controller: 1
154       device: /dev/ttyS0
155       baudrate: 9600
156       max-current: 0.0
157     fallback-temperature: 295.15
158     far: 3e-05
159
160     >>> pyafm.storage.save_afm(afm=afm, filename=filename)
161     >>> pprint_HDF5(filename=filename)  # doctest: +REPORT_UDIFF
162     /
163       <HDF5 dataset "fallback-temperature": shape (), type "<f8">
164         295.15
165       <HDF5 dataset "far": shape (), type "<f8">
166         3e-05
167       <HDF5 dataset "main-axis": shape (), type "|S1">
168     <BLANKLINE>
169       <HDF5 dataset "name": shape (), type "|S1">
170     <BLANKLINE>
171       /piezo
172         /piezo/axes
173           /piezo/axes/0
174             /piezo/axes/0/channel
175               <HDF5 dataset "analog-reference": shape (), type "|S6">
176                 ground
177               <HDF5 dataset "channel": shape (), type "<i4">
178                 0
179               <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
180                 [ -1.00000000e+01   3.05180438e-04]
181               <HDF5 dataset "conversion-origin": shape (), type "<f8">
182                 0.0
183               <HDF5 dataset "device": shape (), type "|S12">
184                 /dev/comedi0
185               <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
186                 [    0.    3276.75]
187               <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
188                 -10.0
189               <HDF5 dataset "maxdata": shape (), type "<i8">
190                 65535
191               <HDF5 dataset "name": shape (), type "|S1">
192                 z
193               <HDF5 dataset "range": shape (), type "<i4">
194                 0
195               <HDF5 dataset "subdevice": shape (), type "<i4">
196                 1
197             <HDF5 dataset "gain": shape (), type "<f8">
198               1.0
199             <HDF5 dataset "maximum": shape (), type "<f8">
200               10.0
201             <HDF5 dataset "minimum": shape (), type "<f8">
202               -10.0
203             <HDF5 dataset "monitor": shape (), type "|S1">
204     <BLANKLINE>
205             <HDF5 dataset "sensitivity": shape (), type "<f8">
206               1.0
207         /piezo/inputs
208           /piezo/inputs/0
209             <HDF5 dataset "analog-reference": shape (), type "|S6">
210               ground
211             <HDF5 dataset "channel": shape (), type "<i4">
212               0
213             <HDF5 dataset "conversion-coefficients": shape (2,), type "<f8">
214               [ -1.00000000e+01   3.05180438e-04]
215             <HDF5 dataset "conversion-origin": shape (), type "<f8">
216               0.0
217             <HDF5 dataset "device": shape (), type "|S12">
218               /dev/comedi0
219             <HDF5 dataset "inverse-conversion-coefficients": shape (2,), type "<f8">
220               [    0.    3276.75]
221             <HDF5 dataset "inverse-conversion-origin": shape (), type "<f8">
222               -10.0
223             <HDF5 dataset "maxdata": shape (), type "<i8">
224               65535
225             <HDF5 dataset "name": shape (), type "|S10">
226               deflection
227             <HDF5 dataset "range": shape (), type "<i4">
228               0
229             <HDF5 dataset "subdevice": shape (), type "<i4">
230               0
231         <HDF5 dataset "name": shape (), type "|S10">
232           test piezo
233       /stepper
234         <HDF5 dataset "backlash": shape (), type "<i4">
235           100
236         <HDF5 dataset "delay": shape (), type "<f8">
237           0.01
238         <HDF5 dataset "full-step": shape (), type "|b1">
239           True
240         <HDF5 dataset "logic": shape (), type "|b1">
241           True
242         <HDF5 dataset "name": shape (), type "|S12">
243           test stepper
244         /stepper/port
245           <HDF5 dataset "channels": shape (4,), type "<i4">
246             [1 2 3 4]
247           <HDF5 dataset "device": shape (), type "|S12">
248             /dev/comedi0
249           <HDF5 dataset "direction": shape (), type "|S6">
250             output
251           <HDF5 dataset "name": shape (), type "|S12">
252             stepper port
253           <HDF5 dataset "subdevice": shape (), type "<i4">
254             2
255           <HDF5 dataset "subdevice-type": shape (), type "|S3">
256             dio
257         <HDF5 dataset "step-size": shape (), type "<f8">
258           1.7e-07
259       /temperature
260         <HDF5 dataset "baudrate": shape (), type "<i4">
261           9600
262         <HDF5 dataset "controller": shape (), type "<i4">
263           1
264         <HDF5 dataset "device": shape (), type "|S10">
265           /dev/ttyS0
266         <HDF5 dataset "max-current": shape (), type "<f8">
267           0.0
268         <HDF5 dataset "name": shape (), type "|S16">
269           test temperature
270         <HDF5 dataset "units": shape (), type "|S7">
271           Celsius
272     >>> afm2 = pyafm.storage.load_afm(filename=filename)
273     >>> afm2.load_from_config(devices=devices)
274
275     >>> afm2.get_temperature()  # doctest: +SKIP
276     297.37
277
278     It's hard to test anything else without pugging into an actual AFM.
279
280     >>> for device in devices:
281     ...     device.close()
282
283     Cleanup our temporary config file.
284
285     >>> os.remove(filename)
286     """
287     def __init__(self, config, piezo=None, stepper=None, temperature=None):
288         self.config = config
289         self.piezo = piezo
290         self.stepper = stepper
291         self.temperature = temperature
292
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()
304
305     def setup_config(self):
306         if self.piezo:
307             self.piezo.setup_config()
308             self.config['piezo'] = self.piezo.config
309         else:
310             self.config['piezo'] = None
311         if self.stepper:
312             self.stepper.setup_config()
313             self.config['stepper'] = self.stepper.config
314         else:
315             self.config['stepper'] = None
316         if self.temperature:
317             self.temperature.setup_config()
318             self.config['temperature'] = self.temperature.config
319         else:
320             self.config['temperature'] = None
321
322     def get_temperature(self):
323         """Measure the sample temperature.
324
325         Return the sample temperature in Kelvin or `None` if such a
326         measurement is not possible.
327         """
328         if hasattr(self.temperature, 'get_temperature'):
329             return self.temperature.get_temperature()
330         return self.config['default-temperature']
331
332     def move_just_onto_surface(self, depth=-50e-9, setpoint=2,
333                                min_slope_ratio=10, far=200, steps=20,
334                                sleep=0.0001):
335         """Position the AFM tip close to the surface.
336
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
343
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
347         `setpoint`.
348         """
349         _LOG.info('moving to %g onto the surface' % depth)
350
351         stepper_tolerance = 2*self.stepper.step_size
352
353         axis = self.piezo.axis_by_name(self.config['main-axis'])
354         def_config = self.piezo.config.select_config('inputs', 'deflection')
355
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)
359
360         _LOG.debug('zero the %s piezo output' % self.config['main-axis'])
361         self.piezo.jump(
362             axis_name=self.config['main-axis'], position=zero, steps=steps,
363             sleep=sleep)
364
365         _LOG.debug("see if we're starting near the surface")
366         try:
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)
370         except _FlatFit, e:
371             _LOG.info(e)
372             pos = self._stepper_approach_again(
373                 target_deflection=target_def, min_slope_ratio=min_slope_ratio,
374                 far=far)
375         except _SurfaceError, e:
376             _LOG.info(e)
377             pos = self._stepper_approach_again(
378                 target_deflection=target_def, min_slope_ratio=min_slope_ratio,
379                 far=far)
380
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))
384
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))
389             try:
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)
394             except _FlatFit, e:
395                 _LOG.debug(e)
396                 continue
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))
403             try:
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)
408             except _FlatFit, e:
409                 _LOG.debug(e)
410                 continue
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))
414
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)
419         self.piezo.jump(
420             self.config['main-axis'], target, steps=steps, sleep=sleep)
421
422         _LOG.debug(
423             'positioned %g m into the surface at stepper %d, piezo %d (%g m)'
424             % (depth, self.stepper.position, target, target_m))
425
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)
434         elif deflection < 0:
435             _LOG.error(('requested setpoint ({} bits) is less than the '
436                         'minimum deflection value of 0 bits'
437                         ).format(deflection))
438             raise ValueError(deflection)
439
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))
443         # back away
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)):
447             _LOG.debug(
448                 'additional surface location attempt (stepping backwards)')
449             try:
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)
454                 return pos
455             except _SurfaceError, e:
456                 _LOG.info(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')
460         _LOG.warn(e)
461         raise e
462
463     def stepper_approach(self, target_deflection):
464         _LOG.info('approach with stepper until deflection > {}'.format(
465                 target_deflection))
466         record_data = _package_config['matplotlib']
467         if record_data:
468             position = []
469             deflection = []
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()
478             if record_data:
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)
484             axes.hold(False)
485             timestamp = _time.strftime('%H-%M-%S')
486             axes.set_title('stepper approach {}'.format(timestamp))
487             plot = axes.plot(position, deflection, 'b.-')
488             figure.canvas.draw()
489             figure.show()
490             if not _matplotlib.is_interactive():
491                 _matplotlib_pyplot.show()
492
493     def move_toward_surface(self, distance):
494         """Step in approximately `distance` meters.
495         """
496         steps = int(distance/self.stepper.step_size)
497         _LOG.info('step in {} steps (~{} m)'.format(steps, distance))
498         self.stepper.step_relative(steps)
499
500     def move_away_from_surface(self, distance=None):
501         """Step back approximately `distance` meters.
502         """
503         if distance is None:
504             distance = self.config['far']
505         self.move_toward_surface(-distance)