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