Standardize matplotlib rendering on figure.canvas.draw() and figure.show().
[pypiezo.git] / pypiezo / afm.py
1 # Copyright (C) 2011-2012 W. Trevor King <wking@tremily.us>
2 #
3 # This file is part of pypiezo.
4 #
5 # pypiezo is free software: you can redistribute it and/or modify it under the
6 # terms of the GNU General Public License as published by the Free Software
7 # Foundation, either version 3 of the License, or (at your option) any later
8 # version.
9 #
10 # pypiezo is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
12 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License along with
15 # pypiezo.  If not, see <http://www.gnu.org/licenses/>.
16
17 "Control of a piezo-based atomic force microscope."
18
19 import time as _time
20
21 import numpy as _numpy
22
23 try:
24     import matplotlib as _matplotlib
25     import matplotlib.pyplot as _matplotlib_pyplot
26 except (ImportError, RuntimeError), e:
27     _matplotlib = None
28     _matplotlib_import_error = e
29
30 from . import LOG as _LOG
31 from . import base as _base
32 from . import package_config as _package_config
33 from . import surface as _surface
34 from . import wiggle as _wiggle
35
36
37 class AFMPiezo (_base.Piezo):
38     """A piezo-controlled atomic force microscope.
39
40     This particular class expects a single input channel for measuring
41     deflection.  Other subclasses provide support for multi-segment
42     deflection measurements.
43
44     >>> from pprint import pprint
45     >>> from pycomedi.constant import AREF
46     >>> from . import config
47     >>> from . import surface
48
49     >>> devices = []
50
51     >>> piezo_config = config.PiezoConfig()
52     >>> piezo_config['name'] = 'Molly'
53     >>> piezo_config['axes'] = [config.AxisConfig()]
54     >>> piezo_config['axes'][0]['channel'] = config.OutputChannelConfig()
55     >>> piezo_config['axes'][0]['channel']['analog-reference'] = AREF.ground
56     >>> piezo_config['axes'][0]['channel']['name'] = 'z'
57     >>> piezo_config['inputs'] = [config.InputChannelConfig()]
58     >>> piezo_config['inputs'][0]['analog-reference'] = AREF.diff
59     >>> piezo_config['inputs'][0]['name'] = 'deflection'
60
61     We set the minimum voltage for the `z` axis to -9 (a volt above
62     the minimum possible voltage) to help with testing
63     `.get_surface_position`.  Without this minimum voltage, small
64     calibration errors could lead to a railed -10 V input for the
65     first few surface approaching steps, which could lead to an
66     `EdgeKink` error instead of a `FlatFit` error.
67
68     >>> piezo_config['axes'][0].update(
69     ...     {'gain':20, 'sensitivity':8e-9, 'minimum':-9})
70
71     >>> p = AFMPiezo(config=piezo_config)
72     >>> p.load_from_config(devices=devices)
73     >>> p.setup_config()
74
75     >>> deflection = p.read_deflection()
76     >>> deflection  # doctest: +SKIP
77     34494L
78     >>> p.deflection_dtype()
79     <type 'numpy.uint16'>
80
81     We need to know where we are before we can move somewhere
82     smoothly.
83
84     >>> zeros = p.zero(axis_names=['z'])
85     >>> pos = zeros[0]
86
87     Usually `.move_to_pos_or_def` is used to approach the surface, but
88     for testing we assume the z output channel is connected directly
89     into the deflection input channel.
90
91     >>> target_pos = _base.convert_volts_to_bits(
92     ...     p.config.select_config('axes', 'z',
93     ...     get_attribute=_base.get_axis_name)['channel'], 2)
94     >>> step = int((target_pos - pos)/5)
95     >>> target_def = _base.convert_volts_to_bits(
96     ...     p.config.select_config('inputs', 'deflection'), 3)
97     >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
98     ...     return_data=True)
99     >>> p.last_output == {'z': int(target_pos)}
100     True
101     >>> pprint(data)  # doctest: +SKIP
102     {'deflection':
103        array([32655, 33967, 35280, 36593, 37905, 39218, 39222], dtype=uint16),
104      'z':
105        array([32767, 34077, 35387, 36697, 38007, 39317, 39321], dtype=uint16)}
106
107     That was a working position-limited approach.  Now move back to
108     the center and try a deflection-limited approach.
109
110     >>> p.jump('z', pos)
111     >>> target_def = _base.convert_volts_to_bits(
112     ...     p.config.select_config('inputs', 'deflection'), 1)
113     >>> data = p.move_to_pos_or_def('z', target_pos, target_def, step=step,
114     ...     return_data=True)
115     >>> print (p.last_output['z'] < int(target_pos))
116     True
117     >>> pprint(data)  # doctest: +SKIP
118     {'deflection': array([32655, 33968, 35281, 36593], dtype=uint16),
119      'z': array([32767, 34077, 35387, 36697], dtype=uint16)}
120
121     >>> wiggle_config = config.WiggleConfig()
122     >>> wiggle_config['offset'] = p.last_output['z']
123     >>> wiggle_config['wavelength'] = 650e-9
124     >>> p.wiggle_for_interference(config=wiggle_config,
125     ...     keypress_test_mode=True)
126     Press any key to continue
127
128     >>> try:
129     ...     p.get_surface_position('z', max_deflection=target_def)
130     ... except surface.FlatFit, e:
131     ...     print 'got FlatFit'
132     got FlatFit
133     >>> print e  # doctest: +SKIP
134     slopes not sufficiently different: 1.0021 and 1.0021
135     >>> abs(e.right_slope-1) < 0.1
136     True
137     >>> abs(e.left_slope-1) < 0.1
138     True
139
140     >>> for device in devices:
141     ...     device.close()
142     """
143     def _deflection_channel(self):
144         return self.channel_by_name(name='deflection', direction='input')
145
146     def read_deflection(self):
147         """Return sensor deflection in bits.
148
149         TODO: explain how bit <-> volt conversion will work for this
150         "virtual" channel.
151         """
152         return self._deflection_channel().data_read()
153
154     def deflection_dtype(self):
155         "Return a Numpy dtype suitable for deflection bit values."
156         return self._deflection_channel().subdevice.get_dtype()
157
158     def move_to_pos_or_def(self, axis_name, position=None, deflection=None,
159                            step=1, return_data=False, pre_move_steps=0,
160                            frequency=None):
161         """TODO
162
163         pre_move_steps : int
164             number of 'null' steps to take before moving (confirming a
165             stable input deflection).
166         frequency : float
167             The target step frequency in hertz.  If `Null`, go as fast
168             as possible.  Note that this is software timing, so it
169             should not be relied upon for precise results.
170         """
171         if position is None and deflection is None:
172             raise ValueError('must specify position, deflection, or both')
173
174         if return_data or _package_config['matplotlib']:
175             aquire_data = True
176         else:
177             aquire_data = False
178
179         if position is None:
180             # default to the extreme value in the step direction
181             if step > 0:
182                 axis = self.axis_by_name(axis_name)
183                 position = axis.axis_channel.get_maxdata()
184             else:
185                 position = 0
186         elif deflection is None:
187             # default to the extreme value
188             channel = self._deflection_channel(self)
189             deflection = channel.get_maxdata()
190         position = int(position)  # round down to nearest integer
191
192         if step == 0:
193             raise ValueError('must have non-zero step size')
194         elif step < 0 and position > self.last_output[axis_name]:
195             step = -step
196         elif step > 0 and position < self.last_output[axis_name]:
197             step = -step
198
199         log_string = (
200             'move to position %d or deflection %g on axis %s in steps of %d'
201             % (position, deflection, axis_name, step))
202         _LOG.debug(log_string)
203         current_deflection = self.read_deflection()
204         log_string = 'current position %d and deflection %g' % (
205             self.last_output[axis_name], current_deflection)
206         _LOG.debug(log_string)
207
208         if aquire_data:
209             def_array=[current_deflection]
210             pos_array=[self.last_output[axis_name]]
211         for i in range(pre_move_steps):
212             self.jump(axis_name, piezo.last_output[axis_name])
213             current_deflection = self.read_deflection()
214             if aquire_data:
215                 def_array.append(current_deflection)
216                 pos_array.append(self.last_output[axis_name])
217         if frequency is not None:
218             time_step = 1./frequency
219             next_time = _time.time() + time_step
220         # step in until we hit our target position or exceed our target deflection
221         while (self.last_output[axis_name] != position and
222                current_deflection < deflection):
223             dist_to = position - self.last_output[axis_name]
224             if abs(dist_to) < abs(step):
225                 jump_to = position
226             else:
227                 jump_to = self.last_output[axis_name] + step
228             self.jump(axis_name, jump_to)
229             current_deflection = self.read_deflection()
230             log_string = (
231                 ('current z piezo position {} (target {}), '
232                  'current deflection {} (target {})').format(
233                     self.last_output[axis_name], position,
234                     current_deflection, deflection))
235             _LOG.debug(log_string)
236             if aquire_data:
237                 def_array.append(current_deflection)
238                 pos_array.append(self.last_output[axis_name])
239             if frequency is not None:
240                 now = _time.time()
241                 if now < next_time:
242                     _time.sleep(next_time - now)
243                 next_time += time_step
244
245         log_string = (
246             'move to position %d or deflection %g on axis %s complete'
247             % (position, deflection, axis_name))
248         _LOG.debug(log_string)
249         log_string = 'current position %d and deflection %g' % (
250             self.last_output[axis_name], current_deflection)
251         _LOG.debug(log_string)
252         if _package_config['matplotlib']:
253             if not _matplotlib:
254                 raise _matplotlib_import_error
255             figure = _matplotlib_pyplot.figure()
256             axes = figure.add_subplot(1, 1, 1)
257             axes.hold(True)
258             timestamp = _time.strftime('%H%M%S')
259             axes.set_title('step approach %s' % timestamp)
260             axes.plot(pos_array, def_array, '.', label=timestamp)
261             #_pylab.legend(loc='best')
262             figure.canvas.draw()
263             figure.show()
264             if not _matplotlib.is_interactive():
265                 _matplotlib_pyplot.show()
266
267         if return_data:
268             data = {
269                 axis_name:_numpy.array(
270                     pos_array, dtype=self.channel_dtype(
271                         axis_name, direction='output')),
272                 'deflection':_numpy.array(
273                     def_array, dtype=self.deflection_dtype()),
274                 }
275             return data
276
277     wiggle_for_interference = _wiggle.wiggle_for_interference
278     get_surface_position = _surface.get_surface_position
279
280
281 #def ramp
282 #        if USE_ABCD_DEFLECTION :
283 #            for i in range(4) : # i is the photodiode element (0->A, 1->B, ...)
284 #                self.curIn[i] = out["Deflection segment"][i][-1]
285 #        else :
286 #            self.curIn[self.chan_info.def_ind] = out["deflection"][-1]
287
288
289 #class FourSegmentAFM (AFM):
290 #    def read_deflection(self):
291 #        "Return sensor deflection in bits."
292 #        A = int(self.curIn[self.chan_info.def_ind[0]])
293 #        B = int(self.curIn[self.chan_info.def_ind[1]])
294 #        C = int(self.curIn[self.chan_info.def_ind[2]])
295 #        D = int(self.curIn[self.chan_info.def_ind[3]])
296 #        df = float((A+B)-(C+D))/(A+B+C+D)
297 #        dfout = int(df * 2**15) + 2**15
298 #        if TEXT_VERBOSE :
299 #            print "Current deflection %d (%d, %d, %d, %d)" \
300 #                % (dfout, A, B, C, D)
301 #        return dfout
302
303
304 #def test_smoothness(zp, plotVerbose=True):
305 #    posA = 20000
306 #    posB = 50000
307 #    setpoint = zp.def_V2in(3)
308 #    steps = 200
309 #    outfreq = 1e5
310 #    outarray = linspace(posB, posA, 1000)
311 #    indata=[]
312 #    outdata=[]
313 #    curVals = zp.jumpToPos(posA)
314 #    zp.pCurVals(curVals)
315 #    _sleep(1) # let jitters die down
316 #    for i in range(10):
317 #        print "ramp %d to %d" % (zp.curPos(), posB)
318 #        curVals, data = moveToPosOrDef(zp, posB, setpoint, step=steps,
319 #                                       return_data = True)
320 #        indata.append(data)
321 #        out = zp.ramp(outarray, outfreq)
322 #        outdata.append(out)
323 #    if plotVerbose:
324 #        from pylab import figure, plot, title, legend, hold, subplot        
325 #    if PYLAB_VERBOSE or plotVerbose:
326 #        _import_pylab()
327 #        _pylab.figure(BASE_FIG_NUM+4)
328 #        for i in range(10):
329 #            _pylab.plot(indata[i]['z'],
330 #                        indata[i]['deflection'], '+--', label='in')
331 #            _pylab.plot(outdata[i]['z'],
332 #                        outdata[i]['deflection'], '.-', label='out')
333 #        _pylab.title('test smoothness (step in, ramp out)')
334 #        #_pylab.legend(loc='best')
335 #    
336 #def test():
337 #    import z_piezo
338 #    zp = z_piezo.z_piezo()
339 #    curVals = zp.moveToPosOrDef(zp.pos_nm2out(600), defl=zp.curDef()+6000, step=(zp.pos_nm2out(10)-zp.pos_nm2out(0)))
340 #    if TEXT_VERBOSE:
341 #        zp.pCurVals(curVals)
342 #    pos = zp.getSurfPos(maxDefl=zp.curDef()+6000)
343 #    if TEXT_VERBOSE:
344 #        print "Surface at %g nm", pos
345 #    print "success"
346 #    if PYLAB_VERBOSE and _final_flush_plot != None:
347 #        _final_flush_plot()
348