Debugging printouts in AFMPiezo doctest.
[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['axes'][0]['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['axes'][0]['channel'], 2)
108     >>> step = int((target_pos - pos)/5)
109     >>> target_def = _base.convert_volts_to_bits(p.config['inputs'][0], 3)
110     >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
111     ...     return_data=True)
112     >>> p.last_output == {'z': int(target_pos)}
113     True
114     >>> pprint(data)  # doctest: +SKIP
115     {'deflection':
116        array([32655, 33967, 35280, 36593, 37905, 39218, 39222], dtype=uint16),
117      'z':
118        array([32767, 34077, 35387, 36697, 38007, 39317, 39321], dtype=uint16)}
119
120     That was a working position-limited approach.  Now move back to
121     the center and try a deflection-limited approach.
122
123     >>> p.jump('z', pos)
124     >>> p.config['inputs'][0]
125     >>> p.config['inputs']['channel']
126     >>> target_def = _base.convert_volts_to_bits(
127     ...     p.config['inputs'][0]['channel'], 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     >>> p.wiggle_for_interference('z', offset=p.last_output['z'],
137     ...     laser_wavelength=650e-9, keypress_test_mode=True)
138     Press any key to continue
139
140     >>> try:
141     ...     p.get_surface_position('z', max_deflection=target_def)
142     ... except surface.FlatFit, e:
143     ...     print 'got FlatFit'
144     got FlatFit
145     >>> print e  # doctest: +SKIP
146     slopes not sufficiently different: 1.0021 and 1.0021
147     >>> abs(e.right_slope-1) < 0.1
148     True
149     >>> abs(e.left_slope-1) < 0.1
150     True
151
152     >>> d.close()
153     """
154     def _deflection_channel(self):
155         return self.channel_by_name(name='deflection', direction='input')
156
157     def read_deflection(self):
158         """Return sensor deflection in bits.
159
160         TODO: explain how bit <-> volt conversion will work for this
161         "virtual" channel.
162         """
163         return self._deflection_channel().data_read()
164
165     def deflection_dtype(self):
166         "Return a Numpy dtype suitable for deflection bit values."
167         return self._deflection_channel().subdevice.get_dtype()
168
169     def move_to_pos_or_def(self, axis_name, position, deflection, step,
170                            return_data=False, pre_move_steps=0):
171         """TODO
172
173         pre_move_steps : int
174             number of 'null' steps to take before moving (confirming a
175             stable input deflection).
176         """
177         if return_data or _package_config['matplotlib']:
178             aquire_data = True
179         else:
180             aquire_data = False
181
182         if step == 0:
183             raise ValueError('must have non-zero step size')
184         elif step < 0 and position > self.last_output[axis_name]:
185             step = -step
186         elif step > 0 and position < self.last_output[axis_name]:
187             step = -step
188
189         log_string = (
190             'move to position %d or deflection %g on axis %s in steps of %d'
191             % (position, deflection, axis_name, step))
192         _LOG.debug(log_string)
193         current_deflection = self.read_deflection()
194         log_string = 'current position %d and deflection %g' % (
195             self.last_output[axis_name], current_deflection)
196         _LOG.debug(log_string)
197
198         if aquire_data:
199             def_array=[current_deflection]
200             pos_array=[self.last_output[axis_name]]
201         for i in range(pre_move_steps):
202             self.jump(axis_name, piezo.last_output[axis_name])
203             delection = self.read_deflection()
204             if aquire_data:
205                 def_array.append(current_deflection)
206                 pos_array.append(self.last_output[axis_name])
207         # step in until we hit our target position or exceed our target deflection
208         while (self.last_output[axis_name] != position and
209                current_deflection < deflection):
210             dist_to = position - self.last_output[axis_name]
211             if abs(dist_to) < abs(step):
212                 jump_to = position
213             else:
214                 jump_to = self.last_output[axis_name] + step
215             self.jump(axis_name, jump_to)
216             current_deflection = self.read_deflection()
217             log_string = (
218                 'current z piezo position %6d, current deflection %6d'
219                 % (current_deflection, self.last_output[axis_name]))
220             _LOG.debug(log_string)
221             if aquire_data:
222                 def_array.append(current_deflection)
223                 pos_array.append(self.last_output[axis_name])
224
225         log_string = (
226             'move to position %d or deflection %g on axis %s complete'
227             % (position, deflection, axis_name))
228         _LOG.debug(log_string)
229         log_string = 'current position %d and deflection %g' % (
230             self.last_output[axis_name], current_deflection)
231         _LOG.debug(log_string)
232         if _package_config['matplotlib']:
233             if not _matplotlib:
234                 raise _matplotlib_import_error
235             figure = _matplotlib_pyplot.figure()
236             axes = figure.add_subplot(1, 1, 1)
237             axes.hold(True)
238             timestamp = _time.strftime('%H%M%S')
239             axes.set_title('step approach %s' % timestamp)
240             axes.plot(pos_array, def_array, '.', label=timestamp)
241             #_pylab.legend(loc='best')
242             figure.show()
243
244         if return_data:
245             data = {
246                 axis_name:_numpy.array(
247                     pos_array, dtype=self.channel_dtype(
248                         axis_name, direction='output')),
249                 'deflection':_numpy.array(
250                     def_array, dtype=self.deflection_dtype()),
251                 }
252             return data
253
254     def wiggle_for_interference(
255         self, axis_name, wiggle_frequency=2, n_samples=1024, amplitude=None,
256         offset=None, laser_wavelength=None, plot=True,
257         keypress_test_mode=False):
258         """Output a sine wave and measure interference.
259
260         With a poorly focused or aligned laser, leaked laser light
261         reflecting off the surface may interfere with the light
262         reflected off the cantilever, causing distance-dependent
263         interference with a period roughly half the laser's
264         wavelength.  This method wiggles the cantilever near the
265         surface and monitors the magnitude of deflection oscillation,
266         allowing the operator to adjust the laser alignment in order
267         to minimize the interference.
268
269         Modern commercial AFMs with computer-aligned lasers must do
270         something like this automatically.
271         """
272         if _package_config['matplotlib']:
273             plot = True
274         if laser_wavelength and amplitude:
275             log_string = \
276                 'use either laser_wavelength or amplitude, but not both'
277             _LOG.warn(log_string)
278
279         if None in (amplitude, offset):
280             output_axis = self.axis_by_name(axis_name)
281             maxdata = output_axis.axis_channel.get_maxdata()
282             midpoint = int(maxdata/2)
283             if offset == None:
284                 offset = midpoint
285                 log_string = (
286                     'generated offset for interference wiggle: %g' % offset)
287                 _LOG.debug(log_string)
288             if amplitude == None:
289                 if offset <= midpoint:
290                     max_amplitude = int(offset)
291                 else:
292                     max_amplitude = int(maxdata-offset)
293                 offset_meters = _base.convert_bits_to_meters(
294                     output_axis.config, offset)
295                 bit_wavelength = _base.convert_meters_to_bits(
296                     output_axis.config, offset_meters + laser_wavelength
297                     ) - offset
298                 amplitude = 2*bit_wavelength
299                 log_string = (
300                     'generated amplitude for interference wiggle: %g'
301                     % amplitude)
302                 _LOG.debug(log_string)
303                 if amplitude > max_amplitude:
304                     raise ValueError('no room for a two wavelength wiggle')
305
306         scan_frequency = wiggle_frequency * n_samples
307         out = (amplitude * _numpy.sin(
308                 _numpy.arange(n_samples) * 4 * _numpy.pi / float(n_samples))
309                + offset)
310         # 4 for 2 periods, so you can judge precision
311         out = out.reshape((len(out), 1)).astype(
312             self.channel_dtype(axis_name, direction='output'))
313
314         _LOG.debug('oscillate for interference wiggle')
315         log_string = (
316             'amplitude: %d bits, offset: %d bits, scan frequency: %g Hz'
317             % (amplitude, offset, scan_frequency))
318         _LOG.debug(log_string)
319
320         if plot:
321             if not _matplotlib:
322                 raise _matplotlib_import_error
323             figure = _matplotlib_pyplot.figure()
324             axes = figure.add_subplot(1, 1, 1)
325             axes.hold(False)
326             timestamp = _time.strftime('%H%M%S')
327             axes.set_title('wiggle for interference %s' % timestamp)
328             plot_p = axes.plot(out, out, 'b.-')
329             figure.show()
330         c = _CheckForKeypress(test_mode=keypress_test_mode)
331         while c.input() == None:
332             # input will need processing for multi-segment AFMs...
333             data = self.ramp(out, scan_frequency, output_names=[axis_name],
334                              input_names=['deflection'])
335             _LOG.debug('completed a wiggle round')
336             if plot:
337                 plot_p[0].set_ydata(data[:,0])
338                 axes.set_ylim([data.min(), data.max()])
339                 #_flush_plot()
340         self.last_output[axis_name] = out[-1,0]
341         _LOG.debug('interference wiggle complete')
342
343     get_surface_position = _surface.get_surface_position
344
345
346 #def ramp
347 #        if USE_ABCD_DEFLECTION :
348 #            for i in range(4) : # i is the photodiode element (0->A, 1->B, ...)
349 #                self.curIn[i] = out["Deflection segment"][i][-1]
350 #        else :
351 #            self.curIn[self.chan_info.def_ind] = out["deflection"][-1]
352
353
354 #class FourSegmentAFM (AFM):
355 #    def read_deflection(self):
356 #        "Return sensor deflection in bits."
357 #        A = int(self.curIn[self.chan_info.def_ind[0]])
358 #        B = int(self.curIn[self.chan_info.def_ind[1]])
359 #        C = int(self.curIn[self.chan_info.def_ind[2]])
360 #        D = int(self.curIn[self.chan_info.def_ind[3]])
361 #        df = float((A+B)-(C+D))/(A+B+C+D)
362 #        dfout = int(df * 2**15) + 2**15
363 #        if TEXT_VERBOSE :
364 #            print "Current deflection %d (%d, %d, %d, %d)" \
365 #                % (dfout, A, B, C, D)
366 #        return dfout
367
368
369 #def test_smoothness(zp, plotVerbose=True):
370 #    posA = 20000
371 #    posB = 50000
372 #    setpoint = zp.def_V2in(3)
373 #    steps = 200
374 #    outfreq = 1e5
375 #    outarray = linspace(posB, posA, 1000)
376 #    indata=[]
377 #    outdata=[]
378 #    curVals = zp.jumpToPos(posA)
379 #    zp.pCurVals(curVals)
380 #    _sleep(1) # let jitters die down
381 #    for i in range(10):
382 #        print "ramp %d to %d" % (zp.curPos(), posB)
383 #        curVals, data = moveToPosOrDef(zp, posB, setpoint, step=steps,
384 #                                       return_data = True)
385 #        indata.append(data)
386 #        out = zp.ramp(outarray, outfreq)
387 #        outdata.append(out)
388 #    if plotVerbose:
389 #        from pylab import figure, plot, title, legend, hold, subplot        
390 #    if PYLAB_VERBOSE or plotVerbose:
391 #        _import_pylab()
392 #        _pylab.figure(BASE_FIG_NUM+4)
393 #        for i in range(10):
394 #            _pylab.plot(indata[i]['z'],
395 #                        indata[i]['deflection'], '+--', label='in')
396 #            _pylab.plot(outdata[i]['z'],
397 #                        outdata[i]['deflection'], '.-', label='out')
398 #        _pylab.title('test smoothness (step in, ramp out)')
399 #        #_pylab.legend(loc='best')
400 #    
401 #def test():
402 #    import z_piezo
403 #    zp = z_piezo.z_piezo()
404 #    curVals = zp.moveToPosOrDef(zp.pos_nm2out(600), defl=zp.curDef()+6000, step=(zp.pos_nm2out(10)-zp.pos_nm2out(0)))
405 #    if TEXT_VERBOSE:
406 #        zp.pCurVals(curVals)
407 #    pos = zp.getSurfPos(maxDefl=zp.curDef()+6000)
408 #    if TEXT_VERBOSE:
409 #        print "Surface at %g nm", pos
410 #    print "success"
411 #    if PYLAB_VERBOSE and _final_flush_plot != None:
412 #        _final_flush_plot()
413