616c6f4b6c3d039bf9b916ab961bca1a4dda6d6d
[pypiezo.git] / pypiezo / afm.py
1 # Copyright (C) 2008-2011 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
6 # under the terms of the GNU General Public License as published by the
7 # Free Software Foundation, either version 3 of the License, or (at your
8 # option) any later version.
9 #
10 # pypiezo is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 # General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with pypiezo.  If not, see <http://www.gnu.org/licenses/>.
17
18 "Control of a piezo-based atomic force microscope."
19
20 import numpy as _numpy
21
22 try:
23     import matplotlib as _matplotlib
24     import matplotlib.pyplot as _matplotlib_pyplot
25     import time as _time  # for timestamping lines on plots
26 except (ImportError, RuntimeError), e:
27     _matplotlib = None
28     _matplotlib_import_error = e
29
30 from curses_check_for_keypress import CheckForKeypress as _CheckForKeypress
31
32 from . import LOG as _LOG
33 from . import base as _base
34 from . import package_config as _package_config
35 from . import surface as _surface
36
37
38 class AFMPiezo (_base.Piezo):
39     """A piezo-controlled atomic force microscope.
40
41     This particular class expects a single input channel for measuring
42     deflection.  Other subclasses provide support for multi-segment
43     deflection measurements.
44
45     >>> from pprint import pprint
46     >>> from pycomedi.device import Device
47     >>> from pycomedi.subdevice import StreamingSubdevice
48     >>> from pycomedi.channel import AnalogChannel
49     >>> from pycomedi.constant import AREF, SUBDEVICE_TYPE, UNIT
50     >>> from . import config
51     >>> from . import surface
52
53     >>> d = Device('/dev/comedi0')
54     >>> d.open()
55
56     >>> s_in = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
57     ...     factory=StreamingSubdevice)
58     >>> s_out = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
59     ...     factory=StreamingSubdevice)
60
61     >>> axis_channel = s_out.channel(
62     ...     0, factory=AnalogChannel, aref=AREF.ground)
63     >>> input_channel = s_in.channel(0, factory=AnalogChannel, aref=AREF.diff)
64     >>> for chan in [axis_channel, input_channel]:
65     ...     chan.range = chan.find_range(unit=UNIT.volt, min=-10, max=10)
66
67     We set the minimum voltage for the `z` axis to -9 (a volt above
68     the minimum possible voltage) to help with testing
69     `.get_surface_position`.  Without this minimum voltage, small
70     calibration errors could lead to a railed -10 V input for the
71     first few surface approaching steps, which could lead to an
72     `EdgeKink` error instead of a `FlatFit` error.
73
74     >>> axis_config = config.AxisConfig()
75     >>> axis_config.update(
76     ...     {'gain':20, 'sensitivity':8e-9, 'minimum':-9})
77     >>> axis_config['channel'] = config.OutputChannelConfig()
78     >>> axis_config['channel']['name'] = 'z'
79     >>> input_config = config.InputChannelConfig()
80     >>> input_config['name'] = 'deflection'
81
82     >>> a = _base.PiezoAxis(config=axis_config, axis_channel=axis_channel)
83     >>> a.setup_config()
84
85     >>> c = _base.InputChannel(config=input_config, channel=input_channel)
86     >>> c.setup_config()
87
88     >>> p = AFMPiezo(axes=[a], inputs=[c], name='Molly')
89
90     >>> deflection = p.read_deflection()
91     >>> deflection  # doctest: +SKIP
92     34494L
93     >>> p.deflection_dtype()
94     <type 'numpy.uint16'>
95
96     We need to know where we are before we can move somewhere
97     smoothly.
98
99     >>> pos = _base.convert_volts_to_bits(p.config.select_config(
100     ...     'axes', 'z', get_attribute=_base.get_axis_name)['channel'], 0)
101     >>> p.jump('z', pos)
102
103     Usually `.move_to_pos_or_def` is used to approach the surface, but
104     for testing we assume the z output channel is connected directly
105     into the deflection input channel.
106
107     >>> target_pos = _base.convert_volts_to_bits(
108     ...     p.config.select_config('axes', 'z',
109     ...     get_attribute=_base.get_axis_name)['channel'], 2)
110     >>> step = int((target_pos - pos)/5)
111     >>> target_def = _base.convert_volts_to_bits(
112     ...     p.config.select_config('inputs', 'deflection'), 3)
113     >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
114     ...     return_data=True)
115     >>> p.last_output == {'z': int(target_pos)}
116     True
117     >>> pprint(data)  # doctest: +SKIP
118     {'deflection':
119        array([32655, 33967, 35280, 36593, 37905, 39218, 39222], dtype=uint16),
120      'z':
121        array([32767, 34077, 35387, 36697, 38007, 39317, 39321], dtype=uint16)}
122
123     That was a working position-limited approach.  Now move back to
124     the center and try a deflection-limited approach.
125
126     >>> p.jump('z', pos)
127     >>> target_def = _base.convert_volts_to_bits(
128     ...     p.config.select_config('inputs', 'deflection'), 1)
129     >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
130     ...     return_data=True)
131     >>> print (p.last_output['z'] < int(target_pos))
132     True
133     >>> pprint(data)  # doctest: +SKIP
134     {'deflection': array([32655, 33968, 35281, 36593], dtype=uint16),
135      'z': array([32767, 34077, 35387, 36697], dtype=uint16)}
136
137     >>> p.wiggle_for_interference('z', offset=p.last_output['z'],
138     ...     laser_wavelength=650e-9, keypress_test_mode=True)
139     Press any key to continue
140
141     >>> try:
142     ...     p.get_surface_position('z', max_deflection=target_def)
143     ... except surface.FlatFit, e:
144     ...     print 'got FlatFit'
145     got FlatFit
146     >>> print e  # doctest: +SKIP
147     slopes not sufficiently different: 1.0021 and 1.0021
148     >>> abs(e.right_slope-1) < 0.1
149     True
150     >>> abs(e.left_slope-1) < 0.1
151     True
152
153     >>> d.close()
154     """
155     def _deflection_channel(self):
156         return self.channel_by_name(name='deflection', direction='input')
157
158     def read_deflection(self):
159         """Return sensor deflection in bits.
160
161         TODO: explain how bit <-> volt conversion will work for this
162         "virtual" channel.
163         """
164         return self._deflection_channel().data_read()
165
166     def deflection_dtype(self):
167         "Return a Numpy dtype suitable for deflection bit values."
168         return self._deflection_channel().subdevice.get_dtype()
169
170     def move_to_pos_or_def(self, axis_name, position, deflection, step,
171                            return_data=False, pre_move_steps=0):
172         """TODO
173
174         pre_move_steps : int
175             number of 'null' steps to take before moving (confirming a
176             stable input deflection).
177         """
178         if return_data or _package_config['matplotlib']:
179             aquire_data = True
180         else:
181             aquire_data = False
182
183         if step == 0:
184             raise ValueError('must have non-zero step size')
185         elif step < 0 and position > self.last_output[axis_name]:
186             step = -step
187         elif step > 0 and position < self.last_output[axis_name]:
188             step = -step
189
190         log_string = (
191             'move to position %d or deflection %g on axis %s in steps of %d'
192             % (position, deflection, axis_name, step))
193         _LOG.debug(log_string)
194         current_deflection = self.read_deflection()
195         log_string = 'current position %d and deflection %g' % (
196             self.last_output[axis_name], current_deflection)
197         _LOG.debug(log_string)
198
199         if aquire_data:
200             def_array=[current_deflection]
201             pos_array=[self.last_output[axis_name]]
202         for i in range(pre_move_steps):
203             self.jump(axis_name, piezo.last_output[axis_name])
204             delection = self.read_deflection()
205             if aquire_data:
206                 def_array.append(current_deflection)
207                 pos_array.append(self.last_output[axis_name])
208         # step in until we hit our target position or exceed our target deflection
209         while (self.last_output[axis_name] != position and
210                current_deflection < deflection):
211             dist_to = position - self.last_output[axis_name]
212             if abs(dist_to) < abs(step):
213                 jump_to = position
214             else:
215                 jump_to = self.last_output[axis_name] + step
216             self.jump(axis_name, jump_to)
217             current_deflection = self.read_deflection()
218             log_string = (
219                 'current z piezo position %6d, current deflection %6d'
220                 % (current_deflection, self.last_output[axis_name]))
221             _LOG.debug(log_string)
222             if aquire_data:
223                 def_array.append(current_deflection)
224                 pos_array.append(self.last_output[axis_name])
225
226         log_string = (
227             'move to position %d or deflection %g on axis %s complete'
228             % (position, deflection, axis_name))
229         _LOG.debug(log_string)
230         log_string = 'current position %d and deflection %g' % (
231             self.last_output[axis_name], current_deflection)
232         _LOG.debug(log_string)
233         if _package_config['matplotlib']:
234             if not _matplotlib:
235                 raise _matplotlib_import_error
236             figure = _matplotlib_pyplot.figure()
237             axes = figure.add_subplot(1, 1, 1)
238             axes.hold(True)
239             timestamp = _time.strftime('%H%M%S')
240             axes.set_title('step approach %s' % timestamp)
241             axes.plot(pos_array, def_array, '.', label=timestamp)
242             #_pylab.legend(loc='best')
243             figure.show()
244
245         if return_data:
246             data = {
247                 axis_name:_numpy.array(
248                     pos_array, dtype=self.channel_dtype(
249                         axis_name, direction='output')),
250                 'deflection':_numpy.array(
251                     def_array, dtype=self.deflection_dtype()),
252                 }
253             return data
254
255     def wiggle_for_interference(
256         self, axis_name, wiggle_frequency=2, n_samples=1024, amplitude=None,
257         offset=None, laser_wavelength=None, plot=True,
258         keypress_test_mode=False):
259         """Output a sine wave and measure interference.
260
261         With a poorly focused or aligned laser, leaked laser light
262         reflecting off the surface may interfere with the light
263         reflected off the cantilever, causing distance-dependent
264         interference with a period roughly half the laser's
265         wavelength.  This method wiggles the cantilever near the
266         surface and monitors the magnitude of deflection oscillation,
267         allowing the operator to adjust the laser alignment in order
268         to minimize the interference.
269
270         Modern commercial AFMs with computer-aligned lasers must do
271         something like this automatically.
272         """
273         if _package_config['matplotlib']:
274             plot = True
275         if laser_wavelength and amplitude:
276             log_string = \
277                 'use either laser_wavelength or amplitude, but not both'
278             _LOG.warn(log_string)
279
280         if None in (amplitude, offset):
281             output_axis = self.axis_by_name(axis_name)
282             maxdata = output_axis.axis_channel.get_maxdata()
283             midpoint = int(maxdata/2)
284             if offset == None:
285                 offset = midpoint
286                 log_string = (
287                     'generated offset for interference wiggle: %g' % offset)
288                 _LOG.debug(log_string)
289             if amplitude == None:
290                 if offset <= midpoint:
291                     max_amplitude = int(offset)
292                 else:
293                     max_amplitude = int(maxdata-offset)
294                 offset_meters = _base.convert_bits_to_meters(
295                     output_axis.config, offset)
296                 bit_wavelength = _base.convert_meters_to_bits(
297                     output_axis.config, offset_meters + laser_wavelength
298                     ) - offset
299                 amplitude = 2*bit_wavelength
300                 log_string = (
301                     'generated amplitude for interference wiggle: %g'
302                     % amplitude)
303                 _LOG.debug(log_string)
304                 if amplitude > max_amplitude:
305                     raise ValueError('no room for a two wavelength wiggle')
306
307         scan_frequency = wiggle_frequency * n_samples
308         out = (amplitude * _numpy.sin(
309                 _numpy.arange(n_samples) * 4 * _numpy.pi / float(n_samples))
310                + offset)
311         # 4 for 2 periods, so you can judge precision
312         out = out.reshape((len(out), 1)).astype(
313             self.channel_dtype(axis_name, direction='output'))
314
315         _LOG.debug('oscillate for interference wiggle')
316         log_string = (
317             'amplitude: %d bits, offset: %d bits, scan frequency: %g Hz'
318             % (amplitude, offset, scan_frequency))
319         _LOG.debug(log_string)
320
321         if plot:
322             if not _matplotlib:
323                 raise _matplotlib_import_error
324             figure = _matplotlib_pyplot.figure()
325             axes = figure.add_subplot(1, 1, 1)
326             axes.hold(False)
327             timestamp = _time.strftime('%H%M%S')
328             axes.set_title('wiggle for interference %s' % timestamp)
329             plot_p = axes.plot(out, out, 'b.-')
330             figure.show()
331         c = _CheckForKeypress(test_mode=keypress_test_mode)
332         while c.input() == None:
333             # input will need processing for multi-segment AFMs...
334             data = self.ramp(out, scan_frequency, output_names=[axis_name],
335                              input_names=['deflection'])
336             _LOG.debug('completed a wiggle round')
337             if plot:
338                 plot_p[0].set_ydata(data[:,0])
339                 axes.set_ylim([data.min(), data.max()])
340                 #_flush_plot()
341         self.last_output[axis_name] = out[-1,0]
342         _LOG.debug('interference wiggle complete')
343
344     get_surface_position = _surface.get_surface_position
345
346
347 #def ramp
348 #        if USE_ABCD_DEFLECTION :
349 #            for i in range(4) : # i is the photodiode element (0->A, 1->B, ...)
350 #                self.curIn[i] = out["Deflection segment"][i][-1]
351 #        else :
352 #            self.curIn[self.chan_info.def_ind] = out["deflection"][-1]
353
354
355 #class FourSegmentAFM (AFM):
356 #    def read_deflection(self):
357 #        "Return sensor deflection in bits."
358 #        A = int(self.curIn[self.chan_info.def_ind[0]])
359 #        B = int(self.curIn[self.chan_info.def_ind[1]])
360 #        C = int(self.curIn[self.chan_info.def_ind[2]])
361 #        D = int(self.curIn[self.chan_info.def_ind[3]])
362 #        df = float((A+B)-(C+D))/(A+B+C+D)
363 #        dfout = int(df * 2**15) + 2**15
364 #        if TEXT_VERBOSE :
365 #            print "Current deflection %d (%d, %d, %d, %d)" \
366 #                % (dfout, A, B, C, D)
367 #        return dfout
368
369
370 #def test_smoothness(zp, plotVerbose=True):
371 #    posA = 20000
372 #    posB = 50000
373 #    setpoint = zp.def_V2in(3)
374 #    steps = 200
375 #    outfreq = 1e5
376 #    outarray = linspace(posB, posA, 1000)
377 #    indata=[]
378 #    outdata=[]
379 #    curVals = zp.jumpToPos(posA)
380 #    zp.pCurVals(curVals)
381 #    _sleep(1) # let jitters die down
382 #    for i in range(10):
383 #        print "ramp %d to %d" % (zp.curPos(), posB)
384 #        curVals, data = moveToPosOrDef(zp, posB, setpoint, step=steps,
385 #                                       return_data = True)
386 #        indata.append(data)
387 #        out = zp.ramp(outarray, outfreq)
388 #        outdata.append(out)
389 #    if plotVerbose:
390 #        from pylab import figure, plot, title, legend, hold, subplot        
391 #    if PYLAB_VERBOSE or plotVerbose:
392 #        _import_pylab()
393 #        _pylab.figure(BASE_FIG_NUM+4)
394 #        for i in range(10):
395 #            _pylab.plot(indata[i]['z'],
396 #                        indata[i]['deflection'], '+--', label='in')
397 #            _pylab.plot(outdata[i]['z'],
398 #                        outdata[i]['deflection'], '.-', label='out')
399 #        _pylab.title('test smoothness (step in, ramp out)')
400 #        #_pylab.legend(loc='best')
401 #    
402 #def test():
403 #    import z_piezo
404 #    zp = z_piezo.z_piezo()
405 #    curVals = zp.moveToPosOrDef(zp.pos_nm2out(600), defl=zp.curDef()+6000, step=(zp.pos_nm2out(10)-zp.pos_nm2out(0)))
406 #    if TEXT_VERBOSE:
407 #        zp.pCurVals(curVals)
408 #    pos = zp.getSurfPos(maxDefl=zp.curDef()+6000)
409 #    if TEXT_VERBOSE:
410 #        print "Surface at %g nm", pos
411 #    print "success"
412 #    if PYLAB_VERBOSE and _final_flush_plot != None:
413 #        _final_flush_plot()
414