Standardize matplotlib rendering on figure.canvas.draw() and figure.show().
[unfold-protein.git] / unfold_protein / unfolder.py
1 # Copyright (C) 2012 W. Trevor King <wking@tremily.us>
2 #
3 # This file is part of unfold_protein.
4 #
5 # unfold_protein 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 Free
7 # Software Foundation, either version 3 of the License, or (at your option) any
8 # later version.
9 #
10 # unfold_protein is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
13 # details.
14 #
15 # You should have received a copy of the GNU General Public License along with
16 # unfold_protein.  If not, see <http://www.gnu.org/licenses/>.
17
18 """Define classes for carrying out an unfolding cycle with an AFM."""
19
20 from __future__ import division
21
22 import email.utils as _email_utils
23 import os.path as _os_path
24 import time as _time
25
26 import h5py as _h5py
27 import pypiezo.base as _pypiezo_base
28 from h5config.storage.hdf5 import HDF5_Storage as _HDF5_Storage
29 from h5config.storage.hdf5 import h5_create_group as _h5_create_group
30
31 from . import LOG as _LOG
32 from . import package_config as _package_config
33
34 try:
35     import numpy as _numpy
36     import matplotlib as _matplotlib
37     from matplotlib import pyplot as _pyplot
38     _pyplot.ion()
39     FIGURE = _pyplot.figure()
40 except (ImportError, RuntimeError), _matplotlib_import_error:
41     _pyplot = None
42 #    from pylab import figure, plot, title, legend, hold, subplot, draw
43
44
45 class ExceptionTooClose (Exception):
46     """
47     The piezo is too close to the surface.
48     """
49     pass
50
51 class ExceptionTooFar (Exception):
52     """
53     The piezo is too far from the surface.
54     """
55     pass
56
57
58 class Unfolder (object):
59     def __init__(self, config, afm):
60         self.config = config
61         self.afm = afm
62         self.zero_piezo()
63
64     def run(self):
65         """Approach-bind-unfold-save[-plot] cycle.
66         """
67         ret = {}
68         ret['timestamp'] = _email_utils.formatdate(localtime=True)
69         ret['temperature'] = self.afm.get_temperature()
70         ret['approach'] = self._approach()
71         self._bind()
72         ret['unfold'] = self._unfold()
73         self._save(**ret)
74         if _package_config['matplotlib']:
75             self._plot(**ret)
76         return ret
77
78     def _approach(self):
79         """Approach the surface using the piezo
80
81         Steps in until a given setpoint is reached.
82         """
83         config = self.config['approach']
84         deflection = self.read_deflection()
85         setpoint = deflection + config['relative setpoint']
86         _LOG.info('approach with setpoint = {}'.format(setpoint))
87         axis_config = self.afm.piezo.config.select_config(
88                 'axes', self.afm.config['main-axis'],
89                 get_attribute=_pypiezo_base.get_axis_name
90                 )
91         def_config = self.afm.piezo.config.select_config(
92             'inputs', 'deflection')
93         start_pos = self.afm.piezo.last_output[self.afm.config['main-axis']]
94
95         # calculate parameters for move_to_pos_or_def from config
96         setpoint_bits = _pypiezo_base.convert_volts_to_bits(
97             def_config, setpoint)
98         mid_pos_bits = _pypiezo_base.convert_meters_to_bits(
99             axis_config, 0)
100         step_pos_bits = _pypiezo_base.convert_meters_to_bits(
101             axis_config, config['step'])
102         step_bits = step_pos_bits - mid_pos_bits
103         frequency = config['velocity'] / config['step']
104
105         # run the approach
106         data = self.afm.piezo.move_to_pos_or_def(
107             axis_name=self.afm.config['main-axis'], deflection=setpoint_bits,
108             step=step_bits, frequency=frequency, return_data=True)
109         data['setpoint'] = setpoint
110         # check the output
111         if data['deflection'].max() < setpoint_bits:
112             _LOG.info(('unfolding too far from the surface '
113                        '(def {} < target {})').format(
114                         data['deflection'].max(), setpoint_bits))
115             self.afm.piezo.jump(self.afm.config['main-axis'], start_pos)
116             if _package_config['matplotlib']:
117                 print data
118                 FIGURE.clear()
119                 axes = FIGURE.add_subplot(1, 1, 1)
120                 axes.plot(data['z'], data['deflection'], label='Approach')
121                 axes.set_title('Unfolding too far')
122                 FIGURE.canvas.draw()
123                 if hasattr(FIGURE, 'show'):
124                     FIGURE.show()
125                 if not _matplotlib.is_interactive():
126                     _pyplot.show()
127             _LOG.debug('raising ExceptionTooFar')
128             raise ExceptionTooFar
129         return data
130
131     def _bind(self):
132         """Wait on the surface while the protein binds."""
133         time = self.config['bind time']
134         _LOG.info('binding for {:.3f} seconds'.format(time))
135         _time.sleep(time)
136
137     def _unfold(self):
138         """Pull the bound protein, forcing unfolding events."""
139         config = self.config['unfold']
140         velocity = config['velocity']
141         _LOG.info('unfold at {:g} m/s'.format(velocity))
142         axis = self.afm.piezo.axis_by_name(self.afm.config['main-axis'])
143         axis_config = self.afm.piezo.config.select_config(
144                 'axes', self.afm.config['main-axis'],
145                 get_attribute=_pypiezo_base.get_axis_name
146                 )
147         d = self.afm.piezo.channel_by_name('deflection')
148         def_config = self.afm.piezo.config.select_config(
149             'inputs', 'deflection')
150         start_pos = self.afm.piezo.last_output[self.afm.config['main-axis']]
151
152         start_pos_m = _pypiezo_base.convert_bits_to_meters(
153             axis_config, start_pos)
154         final_pos_m = start_pos_m - config['distance']
155         final_pos = _pypiezo_base.convert_meters_to_bits(
156             axis_config, final_pos_m)
157         dtype = self.afm.piezo.channel_dtype(
158             self.afm.config['main-axis'], direction='output')
159         frequency = config['frequency']
160         num_steps = int(
161             config['distance'] / config['velocity'] * frequency) + 1
162         #   (m)                * (s/m)              * (samples/s)
163         max_samples = self._get_max_samples()
164         if num_steps > max_samples:
165             num_steps = max_samples
166             frequency = (num_steps - 1)*config['velocity']/config['distance']
167             _LOG.info(('limit frequency to {} Hz (from {} Hz) to fit in DAQ '
168                        'card buffer').format(frequency, config['frequency']))
169
170         out = _numpy.linspace(
171             start_pos, final_pos, num_steps).astype(dtype)
172         # TODO: check size of output buffer.
173         out = out.reshape((len(out), 1))
174         _LOG.debug(
175             'unfolding from {} to {} in {} steps at {} Hz'.format(
176                 start_pos, final_pos, num_steps, frequency))
177         data = self.afm.piezo.ramp(
178             data=out, frequency=frequency, output_names=[self.afm.config['main-axis']],
179             input_names=['deflection'])
180         return {
181             'frequency': frequency, self.afm.config['main-axis']:out, 'deflection':data}
182
183     def _get_max_samples(self):
184         """Return the maximum number of samples that will fit on the card.
185
186         `pycomedi.utility.Writer` seems to have trouble when the the
187         output buffer is bigger than the card's onboard memory, so
188         we reduce the frequency if neccessary to fit the scan in
189         memory.
190         """
191         axis = self.afm.piezo.axis_by_name(self.afm.config['main-axis'])
192         buffer_size = axis.axis_channel.subdevice.get_buffer_size()
193         dtype = self.afm.piezo.channel_dtype(
194             self.afm.config['main-axis'], direction='output')
195         # `channel_dtype` returns `numpy.uint16`, `numpy.uint32`,
196         # etc., which are "generic types".  We use `numpy.dtype` to
197         # construct a `dtype` object:
198         #   >>> import numpy
199         #   >>> numpy.uint16
200         #   <type 'numpy.uint16'>
201         #   >>> numpy.dtype(numpy.uint16)
202         #   dtype('uint16')
203         dt = _numpy.dtype(dtype)
204         sample_size = dt.itemsize
205         max_output_samples = buffer_size // sample_size
206         return max_output_samples
207
208     def _save(self, temperature, approach, unfold, timestamp):
209         config = self.config['save']
210         time_tuple = _email_utils.parsedate(timestamp)
211         filename = _os_path.join(
212             config['base directory'],
213             '{0}-{1:02d}-{2:02d}T{3:02d}-{4:02d}-{5:02d}.h5'.format(
214                 *time_tuple))
215         _LOG.info('saving {}'.format(filename))
216         with _h5py.File(filename, 'a') as f:
217             storage = _HDF5_Storage()
218             config_cwg = _h5_create_group(f, 'config')
219             storage.save(config=self.config, group=config_cwg)
220             afm_piezo_cwg = _h5_create_group(config_cwg, 'afm/piezo')
221             storage.save(config=self.afm.piezo.config, group=afm_piezo_cwg)
222             f['timestamp'] = timestamp
223             if temperature is not None:
224                 f['temperature'] = temperature
225             for k,v in approach.items():
226                 f['approach/{}'.format(k)] = v
227             for k,v in unfold.items():
228                 f['unfold/{}'.format(k)] = v
229
230     def _plot(self, temperature, approach, unfold, timestamp):
231         "Plot the unfolding cycle"
232         if not _pyplot:
233             raise _matplotlib_import_error
234         FIGURE.clear()
235         axes = FIGURE.add_subplot(1, 1, 1)
236         axes.hold(True)
237         axes.plot(approach['z'], approach['deflection'], label='Approach')
238         axes.plot(unfold['z'], unfold['deflection'], label='Unfold')
239         axes.legend(loc='best')
240         axes.set_title('Unfolding')
241         FIGURE.canvas.draw()
242         if hasattr(FIGURE, 'show'):
243             FIGURE.show()
244         if not _matplotlib.is_interactive():
245             _pyplot.show()
246
247     def zero_piezo(self):
248         _LOG.info('zero piezo')
249         x_mid_pos = _pypiezo_base.convert_volts_to_bits(
250             self.afm.piezo.config.select_config(
251                 'axes', 'x', get_attribute=_pypiezo_base.get_axis_name
252                 )['channel'],
253             0)
254         z_mid_pos = _pypiezo_base.convert_volts_to_bits(
255             self.afm.piezo.config.select_config(
256                 'axes', 'z', get_attribute=_pypiezo_base.get_axis_name
257                 )['channel'],
258             0)
259         self.afm.piezo.jump('z', z_mid_pos)
260         self.afm.piezo.jump('x', x_mid_pos)
261
262     def read_deflection(self):
263         bits = self.afm.piezo.read_deflection()
264         return _pypiezo_base.convert_bits_to_volts(
265             self.afm.piezo.config.select_config('inputs', 'deflection'), bits)