1 # Copyright (C) 2012 W. Trevor King <wking@tremily.us>
3 # This file is part of unfold_protein.
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
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
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/>.
18 """Define classes for carrying out an unfolding cycle with an AFM."""
20 from __future__ import division
22 import email.utils as _email_utils
23 import os.path as _os_path
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
31 from . import LOG as _LOG
32 from . import package_config as _package_config
35 import numpy as _numpy
36 import matplotlib as _matplotlib
37 from matplotlib import pyplot as _pyplot
39 FIGURE = _pyplot.figure()
40 except (ImportError, RuntimeError), _matplotlib_import_error:
42 # from pylab import figure, plot, title, legend, hold, subplot, draw
45 class ExceptionTooClose (Exception):
47 The piezo is too close to the surface.
51 class ExceptionTooFar (Exception):
53 The piezo is too far from the surface.
58 class Unfolder (object):
59 def __init__(self, config, afm):
65 """Approach-bind-unfold-save[-plot] cycle.
68 ret['timestamp'] = _email_utils.formatdate(localtime=True)
69 ret['temperature'] = self.afm.get_temperature()
70 ret['approach'] = self._approach()
72 ret['unfold'] = self._unfold()
74 if _package_config['matplotlib']:
79 """Approach the surface using the piezo
81 Steps in until a given setpoint is reached.
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
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']]
95 # calculate parameters for move_to_pos_or_def from config
96 setpoint_bits = _pypiezo_base.convert_volts_to_bits(
98 mid_pos_bits = _pypiezo_base.convert_meters_to_bits(
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']
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
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']:
119 axes = FIGURE.add_subplot(1, 1, 1)
120 axes.plot(data['z'], data['deflection'], label='Approach')
121 axes.set_title('Unfolding too far')
123 if hasattr(FIGURE, 'show'):
125 if not _matplotlib.is_interactive():
127 _LOG.debug('raising ExceptionTooFar')
128 raise ExceptionTooFar
132 """Wait on the surface while the protein binds."""
133 time = self.config['bind time']
134 _LOG.info('binding for {:.3f} seconds'.format(time))
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
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']]
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']
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']))
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))
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'])
181 'frequency': frequency, self.afm.config['main-axis']:out, 'deflection':data}
183 def _get_max_samples(self):
184 """Return the maximum number of samples that will fit on the card.
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
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:
200 # <type 'numpy.uint16'>
201 # >>> numpy.dtype(numpy.uint16)
203 dt = _numpy.dtype(dtype)
204 sample_size = dt.itemsize
205 max_output_samples = buffer_size // sample_size
206 return max_output_samples
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(
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
230 def _plot(self, temperature, approach, unfold, timestamp):
231 "Plot the unfolding cycle"
233 raise _matplotlib_import_error
235 axes = FIGURE.add_subplot(1, 1, 1)
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')
242 if hasattr(FIGURE, 'show'):
244 if not _matplotlib.is_interactive():
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
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
259 self.afm.piezo.jump('z', z_mid_pos)
260 self.afm.piezo.jump('x', x_mid_pos)
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)