Fix move_just_onto_surface() for limited axes.
[pypiezo.git] / pypiezo / afm.py
1 # Copyright (C) 2011-2012 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of pypiezo.
4 #
5 # pypiezo 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 # pypiezo 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 # pypiezo.  If not, see <http://www.gnu.org/licenses/>.
16
17 "Control of a piezo-based atomic force microscope."
18
19 import time as _time
20
21 import numpy as _numpy
22
23 try:
24     import matplotlib as _matplotlib
25     import matplotlib.pyplot as _matplotlib_pyplot
26 except (ImportError, RuntimeError), e:
27     _matplotlib = None
28     _matplotlib_import_error = e
29
30 from . import LOG as _LOG
31 from . import base as _base
32 from . import package_config as _package_config
33 from . import surface as _surface
34 from . import wiggle as _wiggle
35
36
37 class AFMPiezo (_base.Piezo):
38     """A piezo-controlled atomic force microscope.
39
40     This particular class expects a single input channel for measuring
41     deflection.  Other subclasses provide support for multi-segment
42     deflection measurements.
43
44     >>> from pprint import pprint
45     >>> from pycomedi.device import Device
46     >>> from pycomedi.constant import AREF
47     >>> from . import config
48     >>> from . import surface
49
50     >>> d = Device('/dev/comedi0')
51     >>> d.open()
52
53     >>> piezo_config = config.PiezoConfig()
54     >>> piezo_config['name'] = 'Molly'
55     >>> piezo_config['axes'] = [config.AxisConfig()]
56     >>> piezo_config['axes'][0]['channel'] = config.OutputChannelConfig()
57     >>> piezo_config['axes'][0]['channel']['analog-reference'] = AREF.ground
58     >>> piezo_config['axes'][0]['channel']['name'] = 'z'
59     >>> piezo_config['inputs'] = [config.InputChannelConfig()]
60     >>> piezo_config['inputs'][0]['analog-reference'] = AREF.diff
61     >>> piezo_config['inputs'][0]['name'] = 'deflection'
62
63     We set the minimum voltage for the `z` axis to -9 (a volt above
64     the minimum possible voltage) to help with testing
65     `.get_surface_position`.  Without this minimum voltage, small
66     calibration errors could lead to a railed -10 V input for the
67     first few surface approaching steps, which could lead to an
68     `EdgeKink` error instead of a `FlatFit` error.
69
70     >>> piezo_config['axes'][0].update(
71     ...     {'gain':20, 'sensitivity':8e-9, 'minimum':-9})
72
73     >>> a = _base.PiezoAxis(config=piezo_config['axes'][0], devices=[d])
74     >>> c = _base.InputChannel(config=piezo_config['inputs'][0], devices=[d])
75
76     >>> p = AFMPiezo(config=piezo_config, axes=[a], inputs=[c])
77     >>> p.setup_config()
78
79     >>> deflection = p.read_deflection()
80     >>> deflection  # doctest: +SKIP
81     34494L
82     >>> p.deflection_dtype()
83     <type 'numpy.uint16'>
84
85     We need to know where we are before we can move somewhere
86     smoothly.
87
88     >>> pos = _base.convert_volts_to_bits(p.config.select_config(
89     ...     'axes', 'z', get_attribute=_base.get_axis_name)['channel'], 0)
90     >>> p.jump('z', pos)
91
92     Usually `.move_to_pos_or_def` is used to approach the surface, but
93     for testing we assume the z output channel is connected directly
94     into the deflection input channel.
95
96     >>> target_pos = _base.convert_volts_to_bits(
97     ...     p.config.select_config('axes', 'z',
98     ...     get_attribute=_base.get_axis_name)['channel'], 2)
99     >>> step = int((target_pos - pos)/5)
100     >>> target_def = _base.convert_volts_to_bits(
101     ...     p.config.select_config('inputs', 'deflection'), 3)
102     >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
103     ...     return_data=True)
104     >>> p.last_output == {'z': int(target_pos)}
105     True
106     >>> pprint(data)  # doctest: +SKIP
107     {'deflection':
108        array([32655, 33967, 35280, 36593, 37905, 39218, 39222], dtype=uint16),
109      'z':
110        array([32767, 34077, 35387, 36697, 38007, 39317, 39321], dtype=uint16)}
111
112     That was a working position-limited approach.  Now move back to
113     the center and try a deflection-limited approach.
114
115     >>> p.jump('z', pos)
116     >>> target_def = _base.convert_volts_to_bits(
117     ...     p.config.select_config('inputs', 'deflection'), 1)
118     >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
119     ...     return_data=True)
120     >>> print (p.last_output['z'] < int(target_pos))
121     True
122     >>> pprint(data)  # doctest: +SKIP
123     {'deflection': array([32655, 33968, 35281, 36593], dtype=uint16),
124      'z': array([32767, 34077, 35387, 36697], dtype=uint16)}
125
126     >>> wiggle_config = config.WiggleConfig()
127     >>> wiggle_config['offset'] = p.last_output['z']
128     >>> wiggle_config['wavelength'] = 650e-9
129     >>> p.wiggle_for_interference(config=wiggle_config,
130     ...     keypress_test_mode=True)
131     Press any key to continue
132
133     >>> try:
134     ...     p.get_surface_position('z', max_deflection=target_def)
135     ... except surface.FlatFit, e:
136     ...     print 'got FlatFit'
137     got FlatFit
138     >>> print e  # doctest: +SKIP
139     slopes not sufficiently different: 1.0021 and 1.0021
140     >>> abs(e.right_slope-1) < 0.1
141     True
142     >>> abs(e.left_slope-1) < 0.1
143     True
144
145     >>> d.close()
146     """
147     def _deflection_channel(self):
148         return self.channel_by_name(name='deflection', direction='input')
149
150     def read_deflection(self):
151         """Return sensor deflection in bits.
152
153         TODO: explain how bit <-> volt conversion will work for this
154         "virtual" channel.
155         """
156         return self._deflection_channel().data_read()
157
158     def deflection_dtype(self):
159         "Return a Numpy dtype suitable for deflection bit values."
160         return self._deflection_channel().subdevice.get_dtype()
161
162     def move_to_pos_or_def(self, axis_name, position=None, deflection=None,
163                            step=1, return_data=False, pre_move_steps=0,
164                            frequency=None):
165         """TODO
166
167         pre_move_steps : int
168             number of 'null' steps to take before moving (confirming a
169             stable input deflection).
170         frequency : float
171             The target step frequency in hertz.  If `Null`, go as fast
172             as possible.  Note that this is software timing, so it
173             should not be relied upon for precise results.
174         """
175         if position is None and deflection is None:
176             raise ValueError('must specify position, deflection, or both')
177
178         if return_data or _package_config['matplotlib']:
179             aquire_data = True
180         else:
181             aquire_data = False
182
183         if position is None:
184             # default to the extreme value in the step direction
185             if step > 0:
186                 axis = self.axis_by_name(axis_name)
187                 position = axis.axis_channel.get_maxdata()
188             else:
189                 position = 0
190         elif deflection is None:
191             # default to the extreme value
192             channel = self._deflection_channel(self)
193             deflection = channel.get_maxdata()
194         position = int(position)  # round down to nearest integer
195
196         if step == 0:
197             raise ValueError('must have non-zero step size')
198         elif step < 0 and position > self.last_output[axis_name]:
199             step = -step
200         elif step > 0 and position < self.last_output[axis_name]:
201             step = -step
202
203         log_string = (
204             'move to position %d or deflection %g on axis %s in steps of %d'
205             % (position, deflection, axis_name, step))
206         _LOG.debug(log_string)
207         current_deflection = self.read_deflection()
208         log_string = 'current position %d and deflection %g' % (
209             self.last_output[axis_name], current_deflection)
210         _LOG.debug(log_string)
211
212         if aquire_data:
213             def_array=[current_deflection]
214             pos_array=[self.last_output[axis_name]]
215         for i in range(pre_move_steps):
216             self.jump(axis_name, piezo.last_output[axis_name])
217             current_deflection = self.read_deflection()
218             if aquire_data:
219                 def_array.append(current_deflection)
220                 pos_array.append(self.last_output[axis_name])
221         if frequency is not None:
222             time_step = 1./frequency
223             next_time = _time.time() + time_step
224         # step in until we hit our target position or exceed our target deflection
225         while (self.last_output[axis_name] != position and
226                current_deflection < deflection):
227             dist_to = position - self.last_output[axis_name]
228             if abs(dist_to) < abs(step):
229                 jump_to = position
230             else:
231                 jump_to = self.last_output[axis_name] + step
232             self.jump(axis_name, jump_to)
233             current_deflection = self.read_deflection()
234             log_string = (
235                 ('current z piezo position {} (target {}), '
236                  'current deflection {} (target {})').format(
237                     self.last_output[axis_name], position,
238                     current_deflection, deflection))
239             _LOG.debug(log_string)
240             if aquire_data:
241                 def_array.append(current_deflection)
242                 pos_array.append(self.last_output[axis_name])
243             if frequency is not None:
244                 now = _time.time()
245                 if now < next_time:
246                     _time.sleep(next_time - now)
247                 next_time += time_step
248
249         log_string = (
250             'move to position %d or deflection %g on axis %s complete'
251             % (position, deflection, axis_name))
252         _LOG.debug(log_string)
253         log_string = 'current position %d and deflection %g' % (
254             self.last_output[axis_name], current_deflection)
255         _LOG.debug(log_string)
256         if _package_config['matplotlib']:
257             if not _matplotlib:
258                 raise _matplotlib_import_error
259             figure = _matplotlib_pyplot.figure()
260             axes = figure.add_subplot(1, 1, 1)
261             axes.hold(True)
262             timestamp = _time.strftime('%H%M%S')
263             axes.set_title('step approach %s' % timestamp)
264             axes.plot(pos_array, def_array, '.', label=timestamp)
265             #_pylab.legend(loc='best')
266             figure.show()
267
268         if return_data:
269             data = {
270                 axis_name:_numpy.array(
271                     pos_array, dtype=self.channel_dtype(
272                         axis_name, direction='output')),
273                 'deflection':_numpy.array(
274                     def_array, dtype=self.deflection_dtype()),
275                 }
276             return data
277
278     wiggle_for_interference = _wiggle.wiggle_for_interference
279     get_surface_position = _surface.get_surface_position
280
281
282 #def ramp
283 #        if USE_ABCD_DEFLECTION :
284 #            for i in range(4) : # i is the photodiode element (0->A, 1->B, ...)
285 #                self.curIn[i] = out["Deflection segment"][i][-1]
286 #        else :
287 #            self.curIn[self.chan_info.def_ind] = out["deflection"][-1]
288
289
290 #class FourSegmentAFM (AFM):
291 #    def read_deflection(self):
292 #        "Return sensor deflection in bits."
293 #        A = int(self.curIn[self.chan_info.def_ind[0]])
294 #        B = int(self.curIn[self.chan_info.def_ind[1]])
295 #        C = int(self.curIn[self.chan_info.def_ind[2]])
296 #        D = int(self.curIn[self.chan_info.def_ind[3]])
297 #        df = float((A+B)-(C+D))/(A+B+C+D)
298 #        dfout = int(df * 2**15) + 2**15
299 #        if TEXT_VERBOSE :
300 #            print "Current deflection %d (%d, %d, %d, %d)" \
301 #                % (dfout, A, B, C, D)
302 #        return dfout
303
304
305 #def test_smoothness(zp, plotVerbose=True):
306 #    posA = 20000
307 #    posB = 50000
308 #    setpoint = zp.def_V2in(3)
309 #    steps = 200
310 #    outfreq = 1e5
311 #    outarray = linspace(posB, posA, 1000)
312 #    indata=[]
313 #    outdata=[]
314 #    curVals = zp.jumpToPos(posA)
315 #    zp.pCurVals(curVals)
316 #    _sleep(1) # let jitters die down
317 #    for i in range(10):
318 #        print "ramp %d to %d" % (zp.curPos(), posB)
319 #        curVals, data = moveToPosOrDef(zp, posB, setpoint, step=steps,
320 #                                       return_data = True)
321 #        indata.append(data)
322 #        out = zp.ramp(outarray, outfreq)
323 #        outdata.append(out)
324 #    if plotVerbose:
325 #        from pylab import figure, plot, title, legend, hold, subplot        
326 #    if PYLAB_VERBOSE or plotVerbose:
327 #        _import_pylab()
328 #        _pylab.figure(BASE_FIG_NUM+4)
329 #        for i in range(10):
330 #            _pylab.plot(indata[i]['z'],
331 #                        indata[i]['deflection'], '+--', label='in')
332 #            _pylab.plot(outdata[i]['z'],
333 #                        outdata[i]['deflection'], '.-', label='out')
334 #        _pylab.title('test smoothness (step in, ramp out)')
335 #        #_pylab.legend(loc='best')
336 #    
337 #def test():
338 #    import z_piezo
339 #    zp = z_piezo.z_piezo()
340 #    curVals = zp.moveToPosOrDef(zp.pos_nm2out(600), defl=zp.curDef()+6000, step=(zp.pos_nm2out(10)-zp.pos_nm2out(0)))
341 #    if TEXT_VERBOSE:
342 #        zp.pCurVals(curVals)
343 #    pos = zp.getSurfPos(maxDefl=zp.curDef()+6000)
344 #    if TEXT_VERBOSE:
345 #        print "Surface at %g nm", pos
346 #    print "success"
347 #    if PYLAB_VERBOSE and _final_flush_plot != None:
348 #        _final_flush_plot()
349