5a827169850f51c4fdc1118d9b331351a53d917b
[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     from matplotlib import pyplot as _pyplot
37     _pyplot.ion()
38     FIGURE = _pyplot.figure()
39 except (ImportError, RuntimeError), _matplotlib_import_error:
40     _pyplot = None
41 #    from pylab import figure, plot, title, legend, hold, subplot, draw
42
43
44 class ExceptionTooClose (Exception):
45     """
46     The piezo is too close to the surface.
47     """
48     pass
49
50 class ExceptionTooFar (Exception):
51     """
52     The piezo is too far from the surface.
53     """
54     pass
55
56
57 class Unfolder (object):
58     def __init__(self, config, afm):
59         self.config = config
60         self.afm = afm
61         self.zero_piezo()
62
63     def run(self):
64         """Approach-bind-unfold-save[-plot] cycle.
65         """
66         ret = {}
67         ret['timestamp'] = _email_utils.formatdate(localtime=True)
68         ret['temperature'] = self.afm.get_temperature()
69         ret['approach'] = self._approach()
70         self._bind()
71         ret['unfold'] = self._unfold()
72         self._save(**ret)
73         if _package_config['matplotlib']:
74             self._plot(**ret)
75         return ret
76
77     def _approach(self):
78         """Approach the surface using the piezo
79
80         Steps in until a given setpoint is reached.
81         """
82         config = self.config['approach']
83         deflection = self.read_deflection()
84         setpoint = deflection + config['relative setpoint']
85         _LOG.info('approach with setpoint = {}'.format(setpoint))
86         axis_config = self.afm.piezo.config.select_config(
87                 'axes', self.afm.config['main-axis'],
88                 get_attribute=_pypiezo_base.get_axis_name
89                 )
90         def_config = self.afm.piezo.config.select_config(
91             'inputs', 'deflection')
92         start_pos = self.afm.piezo.last_output[self.afm.config['main-axis']]
93
94         # calculate parameters for move_to_pos_or_def from config
95         setpoint_bits = _pypiezo_base.convert_volts_to_bits(
96             def_config, setpoint)
97         mid_pos_bits = _pypiezo_base.convert_meters_to_bits(
98             axis_config, 0)
99         step_pos_bits = _pypiezo_base.convert_meters_to_bits(
100             axis_config, config['step'])
101         step_bits = step_pos_bits - mid_pos_bits
102         frequency = config['velocity'] / config['step']
103
104         # run the approach
105         data = self.afm.piezo.move_to_pos_or_def(
106             axis_name=self.afm.config['main-axis'], deflection=setpoint_bits,
107             step=step_bits, frequency=frequency, return_data=True)
108         data['setpoint'] = setpoint
109         # check the output
110         if data['deflection'].max() < setpoint_bits:
111             _LOG.info(('unfolding too far from the surface '
112                        '(def {} < target {})').format(
113                         data['deflection'].max(), setpoint_bits))
114             self.afm.piezo.jump(self.afm.config['main-axis'], start_pos)
115             if _package_config['matplotlib']:
116                 print data
117                 FIGURE.clear()
118                 axes = FIGURE.add_subplot(1, 1, 1)
119                 axes.plot(data['z'], data['deflection'], label='Approach')
120                 axes.set_title('Unfolding too far')
121                 _pyplot.show()
122             _LOG.debug('raising ExceptionTooFar')
123             raise ExceptionTooFar
124         return data
125
126     def _bind(self):
127         """Wait on the surface while the protein binds."""
128         time = self.config['bind time']
129         _LOG.info('binding for {:.3f} seconds'.format(time))
130         _time.sleep(time)
131
132     def _unfold(self):
133         """Pull the bound protein, forcing unfolding events."""
134         config = self.config['unfold']
135         velocity = config['velocity']
136         _LOG.info('unfold at {:g} m/s'.format(velocity))
137         axis = self.afm.piezo.axis_by_name(self.afm.config['main-axis'])
138         axis_config = self.afm.piezo.config.select_config(
139                 'axes', self.afm.config['main-axis'],
140                 get_attribute=_pypiezo_base.get_axis_name
141                 )
142         d = self.afm.piezo.channel_by_name('deflection')
143         def_config = self.afm.piezo.config.select_config(
144             'inputs', 'deflection')
145         start_pos = self.afm.piezo.last_output[self.afm.config['main-axis']]
146
147         start_pos_m = _pypiezo_base.convert_bits_to_meters(
148             axis_config, start_pos)
149         final_pos_m = start_pos_m - config['distance']
150         final_pos = _pypiezo_base.convert_meters_to_bits(
151             axis_config, final_pos_m)
152         dtype = self.afm.piezo.channel_dtype(
153             self.afm.config['main-axis'], direction='output')
154         frequency = config['frequency']
155         num_steps = int(
156             config['distance'] / config['velocity'] * frequency) + 1
157         #   (m)                * (s/m)              * (samples/s)
158         max_samples = self._get_max_samples()
159         if num_steps > max_samples:
160             num_steps = max_samples
161             frequency = (num_steps - 1)*config['velocity']/config['distance']
162             _LOG.info(('limit frequency to {} Hz (from {} Hz) to fit in DAQ '
163                        'card buffer').format(frequency, config['frequency']))
164
165         out = _numpy.linspace(
166             start_pos, final_pos, num_steps).astype(dtype)
167         # TODO: check size of output buffer.
168         out = out.reshape((len(out), 1))
169         _LOG.debug(
170             'unfolding from {} to {} in {} steps at {} Hz'.format(
171                 start_pos, final_pos, num_steps, frequency))
172         data = self.afm.piezo.ramp(
173             data=out, frequency=frequency, output_names=[self.afm.config['main-axis']],
174             input_names=['deflection'])
175         return {
176             'frequency': frequency, self.afm.config['main-axis']:out, 'deflection':data}
177
178     def _get_max_samples(self):
179         """Return the maximum number of samples that will fit on the card.
180
181         `pycomedi.utility.Writer` seems to have trouble when the the
182         output buffer is bigger than the card's onboard memory, so
183         we reduce the frequency if neccessary to fit the scan in
184         memory.
185         """
186         axis = self.afm.piezo.axis_by_name(self.afm.config['main-axis'])
187         buffer_size = axis.axis_channel.subdevice.get_buffer_size()
188         dtype = self.afm.piezo.channel_dtype(
189             self.afm.config['main-axis'], direction='output')
190         # `channel_dtype` returns `numpy.uint16`, `numpy.uint32`,
191         # etc., which are "generic types".  We use `numpy.dtype` to
192         # construct a `dtype` object:
193         #   >>> import numpy
194         #   >>> numpy.uint16
195         #   <type 'numpy.uint16'>
196         #   >>> numpy.dtype(numpy.uint16)
197         #   dtype('uint16')
198         dt = _numpy.dtype(dtype)
199         sample_size = dt.itemsize
200         max_output_samples = buffer_size // sample_size
201         return max_output_samples
202
203     def _save(self, temperature, approach, unfold, timestamp):
204         config = self.config['save']
205         time_tuple = _email_utils.parsedate(timestamp)
206         filename = _os_path.join(
207             config['base directory'],
208             '{0}-{1:02d}-{2:02d}T{3:02d}-{4:02d}-{5:02d}.h5'.format(
209                 *time_tuple))
210         _LOG.info('saving {}'.format(filename))
211         with _h5py.File(filename, 'a') as f:
212             storage = _HDF5_Storage()
213             config_cwg = _h5_create_group(f, 'config')
214             storage.save(config=self.config, group=config_cwg)
215             afm_piezo_cwg = _h5_create_group(config_cwg, 'afm/piezo')
216             storage.save(config=self.afm.piezo.config, group=afm_piezo_cwg)
217             f['timestamp'] = timestamp
218             if temperature is not None:
219                 f['temperature'] = temperature
220             for k,v in approach.items():
221                 f['approach/{}'.format(k)] = v
222             for k,v in unfold.items():
223                 f['unfold/{}'.format(k)] = v
224
225     def _plot(self, temperature, approach, unfold, timestamp):
226         "Plot the unfolding cycle"
227         if not _pyplot:
228             raise _matplotlib_import_error
229         FIGURE.clear()
230         axes = FIGURE.add_subplot(1, 1, 1)
231         axes.hold(True)
232         axes.plot(approach['z'], approach['deflection'], label='Approach')
233         axes.plot(unfold['z'], unfold['deflection'], label='Unfold')
234         axes.legend(loc='best')
235         axes.set_title('Unfolding')
236         _pyplot.draw()
237         _pyplot.show()
238
239     def zero_piezo(self):
240         _LOG.info('zero piezo')
241         x_mid_pos = _pypiezo_base.convert_volts_to_bits(
242             self.afm.piezo.config.select_config(
243                 'axes', 'x', get_attribute=_pypiezo_base.get_axis_name
244                 )['channel'],
245             0)
246         z_mid_pos = _pypiezo_base.convert_volts_to_bits(
247             self.afm.piezo.config.select_config(
248                 'axes', 'z', get_attribute=_pypiezo_base.get_axis_name
249                 )['channel'],
250             0)
251         self.afm.piezo.jump('z', z_mid_pos)
252         self.afm.piezo.jump('x', x_mid_pos)
253
254     def read_deflection(self):
255         bits = self.afm.piezo.read_deflection()
256         return _pypiezo_base.convert_bits_to_volts(
257             self.afm.piezo.config.select_config('inputs', 'deflection'), bits)