Serialize hooke.experiment.Experiment instances in Curves.
[hooke.git] / hooke / driver / mfp3d.py
index fce318c0a8e87d1c346b4124e8a9d1a4fc0e3a43..3347e3d0f863a5905b2ba983c16f061800154df5 100644 (file)
@@ -6,15 +6,15 @@
 #
 # This file is part of Hooke.
 #
-# Hooke is free software: you can redistribute it and/or
-# modify it under the terms of the GNU Lesser General Public
-# License as published by the Free Software Foundation, either
-# version 3 of the License, or (at your option) any later version.
+# Hooke is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
 #
-# Hooke is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Lesser General Public License for more details.
+# Hooke is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General
+# Public License for more details.
 #
 # You should have received a copy of the GNU Lesser General Public
 # License along with Hooke.  If not, see
@@ -31,13 +31,13 @@ Hooke submission: Rolf Schmidt, Alberto Gomez-Casado 2009
 """
 
 import copy
+import os.path
 import pprint
 
 import numpy
 
 from .. import curve as curve
 from .. import experiment as experiment
-from ..config import Setting
 from . import Driver as Driver
 from .igorbinarywave import loadibw
 
@@ -54,6 +54,8 @@ class MFP3DDriver (Driver):
     def is_me(self, path):
         """Look for identifying fields in the IBW note.
         """
+        if os.path.isdir(path):
+            return False
         if not path.endswith('.ibw'):
             return False
         targets = ['Version:', 'XOPVersion:', 'ForceNote:']
@@ -69,10 +71,10 @@ class MFP3DDriver (Driver):
     
     def read(self, path, info=None):
         data,bin_info,wave_info = loadibw(path)
-        approach,retract = self._translate_ibw(data, bin_info, wave_info)
-
-        info = {'filetype':self.name, 'experiment':experiment.VelocityClamp}
-        return ([approach, retract], info)
+        blocks,info = self._translate_ibw(data, bin_info, wave_info)
+        info['filetype'] = self.name
+        info['experiment'] = experiment.VelocityClamp()
+        return (blocks, info)
      
     def _translate_ibw(self, data, bin_info, wave_info):
         if bin_info['version'] != 5:
@@ -91,27 +93,38 @@ class MFP3DDriver (Driver):
                 value = None
             note[key] = value
         bin_info['note'] = note
+
+        # Ensure a valid MFP3D file version.
         if note['VerDate'] not in ['80501.041', '80501.0207']:
             raise Exception(note['VerDate'])
             raise NotImplementedError(
                 '%s file version %s not supported (yet!)\n%s'
                 % (self.name, note['VerDate'], pprint.pformat(note)))
 
+        # Parse known parameters into standard Hooke format.
         info = {
             'raw info':{'bin':bin_info,
                         'wave':wave_info},
-            'time':wave_info['creationDate'],
-            'spring constant (N/m)':note['SpringConstant'],
+            'time':note['Seconds'],
+            'spring constant (N/m)':float(note['SpringConstant']),
+            'temperature (K)':self._temperature(note),
             }
-        # MFP3D's native data dimensions match Hooke's (<point>, <column>) layout.
-        approach = self._scale_block(data[:wave_info['npnts']/2,:], info, 'approach')
-        retract = self._scale_block(data[wave_info['npnts']/2:,:], info, 'retract')
-        return (approach, retract)
 
-    def _scale_block(self, data, info, name):
+        # Extract data blocks
+        blocks = []
+        indexes = [int(i) for i in note['Indexes'].split(',')]
+        assert indexes[0] == 0, indexes
+        for i,start in enumerate(indexes[:-1]):
+            stop = indexes[i+1]
+            blocks.append(self._scale_block(data[start:stop+1,:], info, i))
+
+        return (blocks, info)
+
+    def _scale_block(self, data, info, index):
         """Convert the block from its native format to a `numpy.float`
         array in SI units.
         """
+        # MFP3D's native data dimensions match Hooke's (<point>, <column>) layout.
         shape = 3
         # raw column indices
         columns = info['raw info']['bin']['dimLabels'][1]
@@ -127,6 +140,14 @@ class MFP3DDriver (Driver):
             dtype=numpy.float,
             info=copy.deepcopy(info)
             )
+
+        version = info['raw info']['bin']['note']['VerDate']
+        if version == '80501.041':
+            name = ['approach', 'retract', 'pause'][index]
+        elif version == '80501.0207':
+            name = ['approach', 'pause', 'retract'][index]
+        else:
+            raise NotImplementedError()
         ret.info['name'] = name
         ret.info['raw data'] = data # store the raw data
 
@@ -157,3 +178,41 @@ class MFP3DDriver (Driver):
             ret[:,t_scol] = data[:,t_rcol]
 
         return ret
+
+    def _temperature(self, note):
+        # I'm not sure which field we should be using here.  Options are:
+        #   StartHeadTemp
+        #   StartScannerTemp
+        #   StartBioHeaterTemp
+        #   EndScannerTemp
+        #   EndHeadTemp
+        # I imagine the 'Start*Temp' fields were measured at
+        # 'StartTempSeconds' at the beginning of a series of curves,
+        # while our particular curve was initiated at 'Seconds'.
+        #   python -c "from hooke.hooke import Hooke;
+        #              h=Hooke();
+        #              h.run_command('load playlist',
+        #                  {'input':'test/data/vclamp_mfp3d/playlist'});
+        #              x = [(int(c.info['raw info']['bin']['note']['Seconds'])
+        #                    - int(c.info['raw info']['bin']['note']['StartTempSeconds']))
+        #                   for c in h.playlists.current().items()];
+        #              print 'average', float(sum(x))/len(x);
+        #              print 'range', min(x), max(x);
+        #              print x"
+        # For the Line*Point*.ibw series, the difference increases slowly
+        #   46, 46, 47, 47, 48, 49, 49, 50, 50, 51, 51, 52, 52, 53, 53, 54,...
+        # However, for the Image*.ibw series, the difference increase
+        # is much faster:
+        #   21, 38, 145, 150, 171, 181
+        # This makes the 'Start*Temp' fields less and less relevant as
+        # the experiment continues.  Still, I suppose it's better than
+        # nothing.
+        #
+        # The 'Thermal' fields seem to be related to cantilever calibration.
+        celsius = unicode(note['StartHeadTemp'], 'latin-1')
+        if celsius.endswith(u' \u00b0C'):
+            number = celsius.split(None, 1)[0]
+            return float(number) + 273.15  # Convert to Kelvin.
+        else:
+            raise NotImplementedError(
+                'unkown temperature format: %s' % repr(celsius))