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