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