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 from matplotlib import pyplot as _pyplot
38 FIGURE = _pyplot.figure()
39 except (ImportError, RuntimeError), _matplotlib_import_error:
41 # from pylab import figure, plot, title, legend, hold, subplot, draw
44 class ExceptionTooClose (Exception):
46 The piezo is too close to the surface.
50 class ExceptionTooFar (Exception):
52 The piezo is too far from the surface.
57 class Unfolder (object):
58 def __init__(self, config, afm):
64 """Approach-bind-unfold-save[-plot] cycle.
67 ret['timestamp'] = _email_utils.formatdate(localtime=True)
68 ret['temperature'] = self.afm.get_temperature()
69 ret['approach'] = self._approach()
71 ret['unfold'] = self._unfold()
73 if _package_config['matplotlib']:
78 """Approach the surface using the piezo
80 Steps in until a given setpoint is reached.
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
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']]
94 # calculate parameters for move_to_pos_or_def from config
95 setpoint_bits = _pypiezo_base.convert_volts_to_bits(
97 mid_pos_bits = _pypiezo_base.convert_meters_to_bits(
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']
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
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']:
118 axes = FIGURE.add_subplot(1, 1, 1)
119 axes.plot(data['z'], data['deflection'], label='Approach')
120 axes.set_title('Unfolding too far')
122 _LOG.debug('raising ExceptionTooFar')
123 raise ExceptionTooFar
127 """Wait on the surface while the protein binds."""
128 time = self.config['bind time']
129 _LOG.info('binding for {:.3f} seconds'.format(time))
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
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']]
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']
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']))
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))
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'])
176 'frequency': frequency, self.afm.config['main-axis']:out, 'deflection':data}
178 def _get_max_samples(self):
179 """Return the maximum number of samples that will fit on the card.
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
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:
195 # <type 'numpy.uint16'>
196 # >>> numpy.dtype(numpy.uint16)
198 dt = _numpy.dtype(dtype)
199 sample_size = dt.itemsize
200 max_output_samples = buffer_size // sample_size
201 return max_output_samples
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(
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
225 def _plot(self, temperature, approach, unfold, timestamp):
226 "Plot the unfolding cycle"
228 raise _matplotlib_import_error
230 axes = FIGURE.add_subplot(1, 1, 1)
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')
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
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
251 self.afm.piezo.jump('z', z_mid_pos)
252 self.afm.piezo.jump('x', x_mid_pos)
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)