Clean up and convert to my Cython-based pycomedi implementation.
[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, 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_config as _base_config
34 from . import base as _base
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_channel_config = config._ChannelConfig()
78     >>> input_channel_config = config._ChannelConfig()
79
80     >>> a = _base.PiezoAxis(axis_config=axis_config,
81     ...     axis_channel_config=axis_channel_config,
82     ...     axis_channel=axis_channel, name='z')
83     >>> a.setup_config()
84
85     >>> c = _base.InputChannel(
86     ...     channel_config=input_channel_config, channel=input_channel,
87     ...     name='deflection')
88     >>> c.setup_config()
89
90     >>> p = AFMPiezo(axes=[a], input_channels=[c])
91
92     >>> deflection = p.read_deflection()
93     >>> deflection  # doctest: +SKIP
94     34494L
95     >>> p.deflection_dtype()
96     <type 'numpy.uint16'>
97
98     We need to know where we are before we can move somewhere
99     smoothly.
100
101     >>> pos = _base.convert_volts_to_bits(p.axes[0].axis_channel_config, 0)
102     >>> p.jump('z', pos)
103
104     Usually `.move_to_pos_or_def` is used to approach the surface, but
105     for testing we assume the z output channel is connected directly
106     into the deflection input channel.
107
108     >>> target_pos = _base.convert_volts_to_bits(
109     ...     p.axes[0].axis_channel_config, 2)
110     >>> step = int((target_pos - pos)/5)
111     >>> target_def = _base.convert_volts_to_bits(
112     ...     p.input_channels[0].channel_config, 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.input_channels[0].channel_config, 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 _base_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 _base_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 _base_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.axis_channel_config, output_axis.axis_config,
296                     offset)
297                 bit_wavelength = _base.convert_meters_to_bits(
298                     output_axis.axis_channel_config, output_axis.axis_config,
299                     offset_meters + laser_wavelength) - offset
300                 amplitude = 2*bit_wavelength
301                 log_string = (
302                     'generated amplitude for interference wiggle: %g'
303                     % amplitude)
304                 _LOG.debug(log_string)
305                 if amplitude > max_amplitude:
306                     raise ValueError('no room for a two wavelength wiggle')
307
308         scan_frequency = wiggle_frequency * n_samples
309         out = (amplitude * _numpy.sin(
310                 _numpy.arange(n_samples) * 4 * _numpy.pi / float(n_samples))
311                + offset)
312         # 4 for 2 periods, so you can judge precision
313         out = out.reshape((len(out), 1)).astype(
314             self.channel_dtype(axis_name, direction='output'))
315
316         _LOG.debug('oscillate for interference wiggle')
317         log_string = (
318             'amplitude: %d bits, offset: %d bits, scan frequency: %g Hz'
319             % (amplitude, offset, scan_frequency))
320         _LOG.debug(log_string)
321
322         if plot:
323             if not _matplotlib:
324                 raise _matplotlib_import_error
325             figure = _matplotlib_pyplot.figure()
326             axes = figure.add_subplot(1, 1, 1)
327             axes.hold(False)
328             timestamp = _time.strftime('%H%M%S')
329             axes.set_title('wiggle for interference %s' % timestamp)
330             plot_p = axes.plot(out, out, 'b.-')
331             figure.show()
332         c = _CheckForKeypress(test_mode=keypress_test_mode)
333         while c.input() == None:
334             # input will need processing for multi-segment AFMs...
335             data = self.ramp(out, scan_frequency, output_names=[axis_name],
336                              input_names=['deflection'])
337             _LOG.debug('completed a wiggle round')
338             if plot:
339                 plot_p[0].set_ydata(data[:,0])
340                 axes.set_ylim([data.min(), data.max()])
341                 #_flush_plot()
342         self.last_output[axis_name] = out[-1,0]
343         _LOG.debug('interference wiggle complete')
344
345     get_surface_position = _surface.get_surface_position
346
347
348 #def ramp
349 #        if USE_ABCD_DEFLECTION :
350 #            for i in range(4) : # i is the photodiode element (0->A, 1->B, ...)
351 #                self.curIn[i] = out["Deflection segment"][i][-1]
352 #        else :
353 #            self.curIn[self.chan_info.def_ind] = out["deflection"][-1]
354
355
356 #class FourSegmentAFM (AFM):
357 #    def read_deflection(self):
358 #        "Return sensor deflection in bits."
359 #        A = int(self.curIn[self.chan_info.def_ind[0]])
360 #        B = int(self.curIn[self.chan_info.def_ind[1]])
361 #        C = int(self.curIn[self.chan_info.def_ind[2]])
362 #        D = int(self.curIn[self.chan_info.def_ind[3]])
363 #        df = float((A+B)-(C+D))/(A+B+C+D)
364 #        dfout = int(df * 2**15) + 2**15
365 #        if TEXT_VERBOSE :
366 #            print "Current deflection %d (%d, %d, %d, %d)" \
367 #                % (dfout, A, B, C, D)
368 #        return dfout
369
370
371 #def test_smoothness(zp, plotVerbose=True):
372 #    posA = 20000
373 #    posB = 50000
374 #    setpoint = zp.def_V2in(3)
375 #    steps = 200
376 #    outfreq = 1e5
377 #    outarray = linspace(posB, posA, 1000)
378 #    indata=[]
379 #    outdata=[]
380 #    curVals = zp.jumpToPos(posA)
381 #    zp.pCurVals(curVals)
382 #    _sleep(1) # let jitters die down
383 #    for i in range(10):
384 #        print "ramp %d to %d" % (zp.curPos(), posB)
385 #        curVals, data = moveToPosOrDef(zp, posB, setpoint, step=steps,
386 #                                       return_data = True)
387 #        indata.append(data)
388 #        out = zp.ramp(outarray, outfreq)
389 #        outdata.append(out)
390 #    if plotVerbose:
391 #        from pylab import figure, plot, title, legend, hold, subplot        
392 #    if PYLAB_VERBOSE or plotVerbose:
393 #        _import_pylab()
394 #        _pylab.figure(BASE_FIG_NUM+4)
395 #        for i in range(10):
396 #            _pylab.plot(indata[i]['z'],
397 #                        indata[i]['deflection'], '+--', label='in')
398 #            _pylab.plot(outdata[i]['z'],
399 #                        outdata[i]['deflection'], '.-', label='out')
400 #        _pylab.title('test smoothness (step in, ramp out)')
401 #        #_pylab.legend(loc='best')
402 #    
403 #def test():
404 #    import z_piezo
405 #    zp = z_piezo.z_piezo()
406 #    curVals = zp.moveToPosOrDef(zp.pos_nm2out(600), defl=zp.curDef()+6000, step=(zp.pos_nm2out(10)-zp.pos_nm2out(0)))
407 #    if TEXT_VERBOSE:
408 #        zp.pCurVals(curVals)
409 #    pos = zp.getSurfPos(maxDefl=zp.curDef()+6000)
410 #    if TEXT_VERBOSE:
411 #        print "Surface at %g nm", pos
412 #    print "success"
413 #    if PYLAB_VERBOSE and _final_flush_plot != None:
414 #        _final_flush_plot()
415