1 # Copyright (C) 2008-2012 A. Seeholzer
2 # Alberto Gomez-Casado <a.gomezcasado@tnw.utwente.nl>
4 # Rolf Schmidt <rschmidt@alcor.concordia.ca>
5 # W. Trevor King <wking@drexel.edu>
7 # This file is part of Hooke.
9 # Hooke is free software: you can redistribute it and/or modify it under the
10 # terms of the GNU Lesser General Public License as published by the Free
11 # Software Foundation, either version 3 of the License, or (at your option) any
14 # Hooke is distributed in the hope that it will be useful, but WITHOUT ANY
15 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
16 # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
19 # You should have received a copy of the GNU Lesser General Public License
20 # along with Hooke. If not, see <http://www.gnu.org/licenses/>.
22 """Driver for MFP-3D files.
24 This driver reads IGOR binary waves.
27 Matlab version: Richard Naud August 2008 (http://lcn.epfl.ch/~naud/)
28 Python port: A. Seeholzer October 2008
29 Hooke submission: Rolf Schmidt, Alberto Gomez-Casado 2009
37 from igor.binarywave import load as _loadibw
39 from .. import curve as curve
40 from . import Driver as Driver
43 __version__='0.0.0.20100604'
46 class MFP3DDriver (Driver):
47 """Handle Asylum Research's MFP3D data format.
50 super(MFP3DDriver, self).__init__(name='mfp3d')
52 def is_me(self, path):
53 """Look for identifying fields in the IBW note.
55 if os.path.isdir(path):
57 if not path.endswith('.ibw'):
59 targets = ['Version:', 'XOPVersion:', 'ForceNote:']
60 found = [False]*len(targets)
61 for line in open(path, 'rU'):
62 for i,ft in enumerate(zip(found, targets)):
64 if f == False and line.startswith(t):
66 if min(found) == True:
70 def read(self, path, info=None):
72 blocks,info = self._translate_ibw(data)
75 def _translate_ibw(self, data):
76 if data['version'] != 5:
77 raise NotImplementedError(
78 'IBW version {} (< 5) not supported'.format(data['version']))
79 # We need version 5 for multidimensional arrays.
81 # Parse the note into a dictionary
83 for line in data['wave']['note'].split('\r'):
84 fields = [x.strip() for x in line.split(':', 1)]
91 data['wave']['note'] = note
93 # Ensure a valid MFP3D file version.
94 if note['VerDate'] not in ['80501.041', '80501.0207']:
95 raise Exception(note['VerDate'])
96 raise NotImplementedError(
97 '%s file version %s not supported (yet!)\n%s'
98 % (self.name, note['VerDate'], pprint.pformat(note)))
100 # Parse known parameters into standard Hooke format.
103 'time':note['Seconds'],
104 'spring constant (N/m)':float(note['SpringConstant']),
105 'temperature (K)':self._temperature(note),
108 # Extract data blocks
110 indexes = [int(i) for i in note['Indexes'].split(',')]
111 assert indexes[0] == 0, indexes
112 for i,start in enumerate(indexes[:-1]):
116 data['wave']['wData'][start:stop+1,:], info, i))
118 return (blocks, info)
120 def _scale_block(self, data, info, index):
121 """Convert the block from its native format to a `numpy.float`
124 # MFP3D's native data dimensions match Hooke's (<point>, <column>) layout.
127 columns = info['raw info']['wave']['labels'][1][1:]
128 # Depending on your MFP3D version:
129 # VerDate 80501.0207: ['Raw', 'Defl', 'LVDT', 'Time']
130 # VerDate 80501.041: ['Raw', 'Defl', 'LVDT']
131 if 'Time' in columns:
136 shape=(data.shape[0], n_col),
138 info=copy.deepcopy(info)
141 version = info['raw info']['wave']['note']['VerDate']
142 if version == '80501.041':
143 name = ['approach', 'retract', 'pause'][index]
144 elif version == '80501.0207':
145 name = ['approach', 'pause', 'retract'][index]
147 raise NotImplementedError()
148 ret.info['name'] = name
149 ret.info['raw data'] = data # store the raw data
151 z_rcol = columns.index('LVDT')
152 d_rcol = columns.index('Defl')
154 # scaled column indices
155 ret.info['columns'] = ['z piezo (m)', 'deflection (m)']
156 z_scol = ret.info['columns'].index('z piezo (m)')
157 d_scol = ret.info['columns'].index('deflection (m)')
159 # Leading '-' because increasing voltage extends the piezo,
160 # moving the tip towards the surface (positive indentation),
161 # but it makes more sense to me to have it increase away from
162 # the surface (positive separation).
163 ret[:,z_scol] = -data[:,z_rcol].astype(ret.dtype)
165 # Leading '-' because deflection voltage increases as the tip
166 # moves away from the surface, but it makes more sense to me
167 # to have it increase as it moves toward the surface (positive
168 # tension on the protein chain).
169 ret[:,d_scol] = -data[:,d_rcol]
171 if 'Time' in columns:
172 ret.info['columns'].append('time (s)')
173 t_rcol = columns.index('Time')
174 t_scol = ret.info['columns'].index('time (s)')
175 ret[:,t_scol] = data[:,t_rcol]
179 def _temperature(self, note):
180 # I'm not sure which field we should be using here. Options are:
186 # I imagine the 'Start*Temp' fields were measured at
187 # 'StartTempSeconds' at the beginning of a series of curves,
188 # while our particular curve was initiated at 'Seconds'.
189 # python -c "from hooke.hooke import Hooke;
191 # h.run_command('load playlist',
192 # {'input':'test/data/vclamp_mfp3d/playlist'});
193 # x = [(int(c.info['raw info']['wave']['note']['Seconds'])
194 # - int(c.info['raw info']['wave']['note']['StartTempSeconds']))
195 # for c in h.playlists.current().items()];
196 # print 'average', float(sum(x))/len(x);
197 # print 'range', min(x), max(x);
199 # For the Line*Point*.ibw series, the difference increases slowly
200 # 46, 46, 47, 47, 48, 49, 49, 50, 50, 51, 51, 52, 52, 53, 53, 54,...
201 # However, for the Image*.ibw series, the difference increase
203 # 21, 38, 145, 150, 171, 181
204 # This makes the 'Start*Temp' fields less and less relevant as
205 # the experiment continues. Still, I suppose it's better than
208 # The 'Thermal' fields seem to be related to cantilever calibration.
209 celsius = unicode(note['StartHeadTemp'], 'latin-1')
210 if celsius.endswith(u' \u00b0C'):
211 number = celsius.split(None, 1)[0]
212 return float(number) + 273.15 # Convert to Kelvin.
214 raise NotImplementedError(
215 'unkown temperature format: %s' % repr(celsius))