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