Split AFMPiezo.wiggle_for_interference() into it's own module.
[pypiezo.git] / pypiezo / wiggle.py
1 # Copyright (C) 2011-2012 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 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 """Utilities for wiggling a the piezo near the surface.
18
19 This helps you detect interference between the laser bouncing off the
20 tip and the surface.  The wiggling continues until you stop it, which
21 gives you feedback while you reposition the laser to minimize the
22 interference.  One day we'll all have fancy AFMs with software control
23 over the laser alignment, but until then, we've got this module
24 helping you with your thumbscrews.
25 """
26
27 import time as _time
28
29 try:
30     import h5py as _h5py
31     import h5config as _h5config
32     from h5config.storage.hdf5 import HDF5_Storage as _HDF5_Storage
33     from h5config.storage.hdf5 import h5_create_group as _h5_create_group
34 except ImportError, e:
35     _h5py = None
36     _h5py_import_error = e
37
38 try:
39     import matplotlib as _matplotlib
40     import matplotlib.pyplot as _matplotlib_pyplot
41 except (ImportError, RuntimeError), e:
42     _matplotlib = None
43     _matplotlib_import_error = e
44
45 from curses_check_for_keypress import CheckForKeypress as _CheckForKeypress
46
47 from . import LOG as _LOG
48
49
50 def _setup_config(peizo, config):
51     if config['wavelength'] and config['amplitude']:
52         log_string = \
53             'use either laser_wavelength or amplitude, but not both'
54         _LOG.warn(log_string)
55
56     if None in (config['amplitude'], config['offset']):
57         output_axis = piezo.axis_by_name(config['axis'])
58         maxdata = output_axis.axis_channel.get_maxdata()
59         midpoint = int(maxdata/2)
60         if config['offset'] is None:
61             offset = midpoint
62             _LOG.debug(('generated offset for interference wiggle: {}'
63                         ).format(config['offset']))
64         if config['amplitude'] is None:
65             if config['offset'] <= midpoint:
66                 max_amplitude = int(config['offset'])
67             else:
68                 max_amplitude = int(maxdata-config['offset'])
69             offset_meters = _base.convert_bits_to_meters(
70                 output_axis.config, config['offset'])
71             if config['wavelength'] is None:
72                 config['amplitude'] = 0.5*max_amplitude
73             else:
74                 bit_wavelength = _base.convert_meters_to_bits(
75                     output_axis.config,
76                     offset_meters + config['wavelength']
77                     ) - config['offset']
78                 config['amplitude'] = 2*bit_wavelength
79             _LOG.debug(('generated amplitude for interference wiggle: {}'
80                         ).format(config['amplitude']))
81             if config['amplitude'] > max_amplitude:
82                 raise ValueError(
83                     'no room for a two wavelength wiggle ({} > {})'.format(
84                         config['amplitude'], max_amplitude))
85
86 def _construct_output(piezo, config):
87     n = config['samples']  # samples in a single oscillation
88     out = (config['amplitude']
89            * _numpy.sin(_numpy.arange(2*n)*2*_numpy.pi/n)
90            + config['offset'])
91     # 2*n for 2 periods, so you can judge precision
92     out = out.reshape((len(out), 1)).astype(
93         piezo.channel_dtype(config['axis'], direction='output'))
94     return out
95
96 def _setup_datafile(filename, group, piezo, config, output):
97     if not _h5py:
98         raise _h5py_import_error
99     output_axis = afm.piezo.axis_by_name(config['axis'])
100     input_channel = afm.piezo.input_channel_by_name(config['input'])
101     with _h5py.File(filename, 'w') as f:
102         cwg = _h5_create_group(f, group)
103         storage = _HDF5_Storage()
104         for config_,key in [
105             (config, 'config/wiggle'),
106             (output.axis.config,
107              'config/{}/axis'.format(config['axis'])),
108             (input_channel.config,
109              'config/{}/channel'.format(config['input']))]:
110             if config_ is None:
111                 continue
112             config_cwg = _h5_create_group(cwg, key)
113             storage.save(config=config, group=config_cwg)
114         cwg['wiggle/raw/{}'.format(config['axis'])] = output
115
116 def _update_datafile(filename, group, config, cycle, data):
117     timestamp = ('{0}-{1:02d}-{2:02d}T{3:02d}-{4:02d}-{5:02d}'
118                  ).format(*_time.localtime())
119     with _h5py.File(filename, 'a') as f:
120         wiggle_group = _h5_create_group(f, group)
121         cwg = _h5_create_group(
122             wiggle_group, 'wiggle/{}'.format(cycle))
123         cwg['time'] = timestamp
124         cwg['raw/{}'.format(config['input'])] = data
125
126 def _setup_plot(piezo, config):
127     if not _matplotlib:
128         raise _matplotlib_import_error
129     _matplotlib.interactive(True)
130     figure = _matplotlib_pyplot.figure()
131     axes = figure.add_subplot(1, 1, 1)
132     axes.hold(False)
133     timestamp = _time.strftime('%H%M%S')
134     axes.set_title('wiggle for interference %s' % timestamp)
135     plot = axes.plot(out, out, 'b.-')
136     figure.show()
137     _matplotlib_pyplot.draw()
138     _matplotlib_pyplot.show()
139     return plot
140
141 def _update_plot(plot, cycle, data):
142     plot[0].set_ydata(data[:,0])
143     axes.set_ylim([data.min(), data.max()])
144     _matplotlib_pyplot.draw()
145
146 def _run_wiggles(piezo, config, plot, output, filename=None, group='/',
147                  keypress_test_mode=False):
148     scan_frequency = config['frequency'] * n
149     cycle = 0
150     c = _CheckForKeypress(test_mode=keypress_test_mode)
151     while c.input() == None:
152         # input will need processing for multi-segment AFMs...
153         data = piezo.ramp(
154             output, scan_frequency, output_names=[config['axis']],
155             input_names=[config['input']])
156         _LOG.debug('completed a wiggle round')
157         if filename:
158             _update_datafile(
159                 filename=filename, group=group, config=config,
160                 cycle=cycle, data=data)
161         if plot:
162             _update_plot(plot=plot, cycle=cycle, data=data)
163         cycle += 1
164
165 def wiggle_for_interference(
166     piezo, config, plot=True, filename=None, group='/',
167     keypress_test_mode=False):
168     """Output a sine wave and measure interference.
169
170     With a poorly focused or aligned laser, leaked laser light
171     reflecting off the surface may interfere with the light
172     reflected off the cantilever, causing distance-dependent
173     interference with a period roughly half the laser's
174     wavelength.  This method wiggles the cantilever near the
175     surface and monitors the magnitude of deflection oscillation,
176     allowing the operator to adjust the laser alignment in order
177     to minimize the interference.
178
179     Modern commercial AFMs with computer-aligned lasers must do
180     something like this automatically.
181     """
182     if _package_config['matplotlib']:
183         plot = True
184     _setup_config(piezo=piezo, config=config)
185     _LOG.debug('oscillate for interference wiggle ({})'.format(config))
186     output = _construct_output(piezo=piezo, config=config)
187
188     if filename:
189         _setup_datafile(
190             filename=filename, group=group, piezo=piezo, config=config,
191             output=output)
192     if plot:
193         interactive = _matplotlib.is_interactive()
194         plot_ = _setup_plot(piezo=piezo, config=config, output=output)
195     _run_wiggles(piezo=piezo, config=config, plot=plot_)
196     if plot:
197         _matplotlib.interactive(interactive)
198     piezo.last_output[config['axis']] = out[-1,0]
199     _LOG.debug('interference wiggle complete')