Update to use new h5config, pycomedi, etc.
[unfold-protein.git] / unfold_protein / unfolder.py
1 # Copyright (C) 2011 W. Trevor King <wking@drexel.edu>
2 #
3 # This file is part of unfold_protein.
4 #
5 # Unfold_protein is free software: you can redistribute it and/or
6 # modify it under the terms of the GNU Lesser General Public
7 # License as published by the Free Software Foundation, either
8 # version 3 of the License, or (at your option) any later version.
9 #
10 # Unfold_protein is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU Lesser General Public License for more details.
14 #
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with unfold_protein.  If not, see
17 # <http://www.gnu.org/licenses/>.
18
19 """Define classes for carrying out an unfolding cycle with an AFM."""
20
21 import email.utils as _email_utils
22 import os.path as _os_path
23 import time as _time
24
25 import h5py as _h5py
26 import pypiezo.base as _pypiezo_base
27 from h5config.storage.hdf5 import HDF5_Storage as _HDF5_Storage
28 from h5config.storage.hdf5 import h5_create_group as _h5_create_group
29
30 from . import LOG as _LOG
31 from . import package_config as _package_config
32
33 try:
34     import numpy as _numpy
35     from matplotlib import pyplot as _pyplot
36     FIGURE = _pyplot.figure()
37 except (ImportError, RuntimeError), _matplotlib_import_error:
38     _pyplot = None
39 #    from pylab import figure, plot, title, legend, hold, subplot, draw
40
41
42 class ExceptionTooClose (Exception):
43     """
44     The piezo is too close to the surface.
45     """
46     pass
47
48 class ExceptionTooFar (Exception):
49     """
50     The piezo is too far from the surface.
51     """
52     pass
53
54
55 class Unfolder (object):
56     def __init__(self, config, afm):
57         self.config = config
58         self.afm = afm
59         self.zero_piezo()
60
61     def run(self):
62         """Approach-bind-unfold-save[-plot] cycle.
63         """
64         ret = {}
65         ret['timestamp'] = _email_utils.formatdate(localtime=True)
66         ret['temperature'] = self.afm.get_temperature()
67         ret['approach'] = self._approach()
68         self._bind()
69         ret['unfold'] = self._unfold()
70         self._save(**ret)
71         if _package_config['matplotlib']:
72             self._plot(**ret)
73         return ret
74
75     def _approach(self):
76         """Approach the surface using the piezo
77
78         Steps in until a given setpoint is reached.
79         """
80         config = self.config['approach']
81         deflection = self.read_deflection()
82         setpoint = deflection + config['relative setpoint']
83         _LOG.info('approach with setpoint = {}'.format(setpoint))
84         axis_config = self.afm.piezo.config.select_config(
85                 'axes', self.afm.axis_name,
86                 get_attribute=_pypiezo_base.get_axis_name
87                 )
88         def_config = self.afm.piezo.config.select_config(
89             'inputs', 'deflection')
90         start_pos = self.afm.piezo.last_output[self.afm.axis_name]
91
92         # calculate parameters for move_to_pos_or_def from config
93         setpoint_bits = _pypiezo_base.convert_volts_to_bits(
94             def_config, setpoint)
95         mid_pos_bits = _pypiezo_base.convert_meters_to_bits(
96             axis_config, 0)
97         step_pos_bits = _pypiezo_base.convert_meters_to_bits(
98             axis_config, config['step'])
99         step_bits = step_pos_bits - mid_pos_bits
100         frequency = config['velocity'] / config['step']
101
102         # run the approach
103         data = self.afm.piezo.move_to_pos_or_def(
104             axis_name=self.afm.axis_name, deflection=setpoint_bits,
105             step=step_bits, frequency=frequency, return_data=True)
106         data['setpoint'] = setpoint
107         # check the output
108         if data['deflection'][-1] < setpoint_bits:
109             _LOG.info('unfolding too far from the surface')
110             self.afm.piezo.jump(self.afm.axis_name, start_pos)
111             #if PYLAB_INTERACTIVE_VERBOSE == True:
112             #    figure(BASE_FIG_NUM+1)
113             #    hold(False)
114             #    plot_dict(data, 'Approach')
115             #    hold(True)
116             #    title('Unfolding too far')
117             _LOG.debug('raising ExceptionTooFar')
118             raise ExceptionTooFar
119         return data
120         
121     def _bind(self):
122         """Wait on the surface while the protein binds."""
123         time = self.config['bind time']
124         _LOG.info('binding for {:.3f} seconds'.format(time))
125         _time.sleep(time)
126
127     def _unfold(self):
128         """Pull the bound protein, forcing unfolding events."""
129         config = self.config['unfold']
130         velocity = config['velocity']
131         _LOG.info('unfold at {:g} m/s'.format(velocity))
132         axis = self.afm.piezo.axis_by_name(self.afm.axis_name)
133         d = self.afm.piezo.channel_by_name('deflection')    
134         start_pos = self.afm.piezo.last_output[self.afm.axis_name]
135         start_pos_m = _pypiezo_base.convert_bits_to_meters(axis, start_pos)
136         final_pos_m = bind_pos_m - config['distance']
137         final_pos = _pypiezo_base.convert_meters_to_bits(axis, final_pos_m)
138         dtype = afm.piezo.channel_dtype(self.afm.axis_name, direction='output')
139         num_steps = int(
140             config['distance'] / config['velocity'] * config['frequency']) + 1
141         #   (m)                * (s/m)              * (samples/s)
142         out = _numpy.linspace(
143             start_pos, final_pos, num_steps).astype(dtype=dtype)
144         _LOG.debug(
145             'unfolding from {:d} to {:d} in {:d} steps at {:g} Hz'.format(
146                 start_pos, final_pos, num_steps, config['frequency']))
147         data = afm.piezo.ramp(
148             data=out, frequency=config['frequency'],
149             output_names=[self.afm.axis_name], input_names=['deflection'])
150         return {self.afm.axis_name:out, 'deflection':data}
151
152     def _save(self, temperature, approach, unfold, timestamp):
153         config = self.config['save']
154         time_tuple = _email_utils.parsedate(timestamp)
155         filename = _os_path.join(
156             config['base directory'],
157             '{0}-{1:02d}-{2:02d}_{3:02d}-{4:02d}-{5:02d}.h5'.format(
158                 time_tuple))
159         _LOG.info('saving {}'.format(filename))
160         with _h5py.File(filename, 'a') as f:
161             storage = _HDF5_Storage()
162             config_cwg = _h5_create_group(f, 'config')
163             storage.save(config=self.config, group=config_cwg)
164             f['timestamp'] = timestamp
165             f['temperature'] = temperature
166             for k,v in approach.items():
167                 f['approach/{}'.format(k)] = v
168             for k,v in unfold.items():
169                 f['unfold/{}'.format(k)] = v
170
171     def _plot(self, temperature, approach, unfold, timestamp):
172         "Plot the unfolding cycle"
173         if not _matplotlib:
174             raise _matplotlib_import_error
175         FIGURE.clear()
176         # TODO...
177         #if PYLAB_INTERACTIVE_VERBOSE == True:
178         #    figure(BASE_FIG_NUM)
179         #    hold(False)
180         #    plot_dict(approach_data, 'Approach')
181         #    hold(True)
182         #    plot_dict(out['data'], 'Unfold')
183         #    legend(loc='best')
184         #    title('Unfolding')
185         #    draw()
186
187     def zero_piezo(self):
188         _LOG.info('zero piezo')
189         x_mid_pos = _pypiezo_base.convert_volts_to_bits(
190             self.afm.piezo.config.select_config(
191                 'axes', 'x', get_attribute=_pypiezo_base.get_axis_name
192                 )['channel'],
193             0)
194         z_mid_pos = _pypiezo_base.convert_volts_to_bits(
195             self.afm.piezo.config.select_config(
196                 'axes', 'z', get_attribute=_pypiezo_base.get_axis_name
197                 )['channel'],
198             0)
199         self.afm.piezo.jump('z', z_mid_pos)
200         self.afm.piezo.jump('x', x_mid_pos)
201
202     def read_deflection(self):
203         bits = self.afm.piezo.read_deflection()
204         return _pypiezo_base.convert_bits_to_volts(
205             self.afm.piezo.config.select_config('inputs', 'deflection'), bits)