Fix '@Sens. ZSensorSens' KeyError loading picoforce 0x07200000 curves.
[hooke.git] / hooke / driver / picoforce.py
index c1c914336ce9c0bc3935c8a56f6ccbeece7d0766..94339d438964b76f97b29e1ee132e5998a5f388f 100644 (file)
@@ -1,24 +1,24 @@
-# Copyright (C) 2006-2010 Alberto Gomez-Kasai
+# Copyright (C) 2006-2010 Alberto Gomez-Casado
 #                         Massimo Sandal <devicerandom@gmail.com>
 #                         W. Trevor King <wking@drexel.edu>
 #
 # 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
 # <http://www.gnu.org/licenses/>.
 
-"""Library for interpreting Veeco PicoForce force spectroscopy files.
+"""Driver for Veeco PicoForce force spectroscopy files.
 """
 
 import pprint
@@ -29,7 +29,6 @@ import numpy
 
 from .. import curve as curve # this module defines data containers.
 from .. import experiment as experiment # this module defines expt. types
-from ..config import Setting # configurable setting class
 from . import Driver as Driver # this is the Driver base class
 
 
@@ -48,7 +47,7 @@ class PicoForceDriver (Driver):
 
         return header[2:17] == 'Force file list'
 
-    def read(self, path):
+    def read(self, path, info=None):
         info = self._read_header_path(path)
         self._check_version(info)
         data = self._read_data_path(path, info)
@@ -152,8 +151,8 @@ class PicoForceDriver (Driver):
         Otherwise, raise `ValueError`.
         """
         version = info['Force file list'].get('Version', None)
-        if version not in ['0x06120002']:
-            raise ValueError(
+        if version not in ['0x06120002', '0x06130001', '0x07200000']:
+            raise NotImplementedError(
                 '%s file version %s not supported (yet!)\n%s'
                 % (self.name, version,
                    pprint.pformat(info['Force file list'])))
@@ -171,35 +170,37 @@ class PicoForceDriver (Driver):
     def _read_data_file(self, file, info):
         file.seek(0)
         traces = self._extract_traces(buffer(file.read()), info)
-        deflection,z_piezo,deflection_B = traces
-        self._validate_traces(z_piezo, deflection, deflection_B)
-        L = len(deflection)
+        self._validate_traces(
+            traces['Z sensor'], traces['Deflection'])
+        L = len(traces['Deflection'])
         approach = self._extract_block(
-            info, z_piezo, deflection, 0, L/2, 'approach')
+            info, traces['Z sensor'], traces['Deflection'], 0, L/2, 'approach')
         retract = self._extract_block(
-            info, z_piezo, deflection, L/2, L, 'retract')
+            info, traces['Z sensor'], traces['Deflection'], L/2, L, 'retract')
         data = [approach, retract]
         return data
 
     def _extract_traces(self, buffer, info):
         """Extract each of the three vector blocks in a PicoForce file.
-        
-        The blocks are:
 
-        * Deflection input
+        The blocks are (in variable order):
+
         * Z piezo sensor input
+        * Deflection input
         * Deflection again?
 
         And their headers are marked with 'Ciao force image list'.
         """
-        traces = [] 
+        traces = {}
+        version = info['Force file list']['Version']
+        type_re = re.compile('S \[(\w*)\] "([\w\s.]*)"')
         for image in info['Ciao force image list']:
             offset = int(image['Data offset'])
             length = int(image['Data length'])
             sample_size = int(image['Bytes/pixel'])
-            rows = length / sample_size
             if sample_size != 2:
                 raise NotImplementedError('Size: %s' % sample_size)
+            rows = length / sample_size
             d = curve.Data(
                 shape=(rows),
                 dtype=numpy.int16,
@@ -207,19 +208,44 @@ class PicoForceDriver (Driver):
                 offset=offset,
                 info=image,
                 )
-            traces.append(d)
+            if version in ['0x06120002', '0x06130001']:
+                match = type_re.match(image['@4:Image Data'])
+                assert match != None, 'Bad regexp for %s, %s' \
+                    % ('@4:Image Data', image['@4:Image Data'])
+                if version == '0x06130001' and match.group(1) == 'ZLowVoltage':
+                    assert match.group(2) == 'Low Voltage Z', \
+                        'Name missmatch: "%s", "%s"' % (match.group(1), match.group(2))
+                else:
+                    assert match.group(1).lower() == match.group(2).replace(' ','').lower(), \
+                        'Name missmatch: "%s", "%s"' % (match.group(1), match.group(2))
+                tname = match.group(2)
+            else:
+                assert version == '0x07200000', version
+                match = type_re.match(image['@4:Image Data'])
+                assert match != None, 'Bad regexp for %s, %s' \
+                    % ('@4:Image Data', image['@4:Image Data'])
+                if match.group(1) == 'PulseFreq1':
+                    assert match.group(2) == 'Freq. 1', match.group(2)
+                else:
+                    assert match.group(1).lower() == match.group(2).replace(' ','').lower(), \
+                        'Name missmatch: "%s", "%s"' % (match.group(1), match.group(2))
+                tname = match.group(2)
+                if tname == 'Freq. 1':  # Normalize trace names between versions
+                    tname = 'Z sensor'
+                elif tname == 'Deflection Error':
+                    tname = 'Deflection'
+            if tname in traces:
+                #d.tofile('%s-2.dat' % tname, sep='\n')
+                tname = self._replace_name(tname, d, traces, info)
+                if tname == None:
+                    continue  # Don't replace anything
+            else:
+                #d.tofile('%s.dat' % tname, sep='\n')
+                pass
+            traces[tname] = d
         return traces
 
-    def _validate_traces(self, z_piezo, deflection, deflection_B):
-        key = 'Spring Constant'
-        spring_constant = z_piezo.info[key]
-        for trace in [deflection, deflection_B]:
-            if trace.info[key] != spring_constant:
-                raise NotImplementedError(
-                    'spring constant missmatch: %s != %s'
-                    % (spring_constant, trace.info[key]))
-        if max(abs(deflection_B[:-1]-deflection[:-1])) != 0:
-            raise NotImplementedError('trace 0 != trace 2')
+    def _validate_traces(self, z_piezo, deflection):
         if len(z_piezo) != len(deflection):
             raise ValueError('Trace length missmatch: %d != %d'
                              % (len(z_piezo), len(deflection)))
@@ -232,44 +258,89 @@ class PicoForceDriver (Driver):
         block[:,1] = deflection[start:stop]
         block.info = self._translate_block_info(
             info, z_piezo.info, deflection.info, name)
+        block.info['columns'] = ['z piezo (m)', 'deflection (m)']
         block = self._scale_block(block)
         return block
 
+    def _replace_name(self, trace_name, trace, traces, info):
+        """Determine if a duplicate trace name should replace an earlier trace.
+
+        Return the target trace name if it should be replaced by the
+        new trace, or `None` if the new trace should be dropped.
+        """
+        #msg = []
+        #target = traces[trace_name]
+        #
+        ## Compare the info dictionaries for each trace
+        #ik = set(trace.info.keys())
+        #ok = set(traces[trace_name].info.keys())
+        #if ik != ok:  # Ensure we have the same set of keys for both traces
+        #    msg.append('extra keys: %s, missing keys %s' % (ik-ok, ok-ik))
+        #else:
+        #    # List keys we *require* to change between traces
+        #    variable_keys = ['Data offset', 'X data type']  # TODO: What is X data type?
+        #    for key in trace.info.keys():
+        #        if key in variable_keys:
+        #            if target.info[key] == trace.info[key]:
+        #                msg.append('constant %s (%s == %s)'
+        #                           % (key, target.info[key], trace.info[key]))
+        #        else:
+        #            if target.info[key] != trace.info[key]:
+        #                msg.append('variable %s (%s != %s)'
+        #                           % (key, target.info[key], trace.info[key]))
+        # Compare the data
+        #if not (traces[trace_name] == trace).all():
+        #    msg.append('data difference')
+        #if len(msg) > 0:
+        #    raise NotImplementedError(
+        #        'Missmatched duplicate traces for %s: %s'
+        #        % (trace_name, ', '.join(msg)))
+        import logging
+        log = logging.getLogger('hooke')
+        for name,t in traces.items():
+            if (t == trace).all():
+                log.debug('replace %s with %s-2' % (name, trace_name))
+                return name  # Replace this identical dataset.
+        log.debug('store %s-2 as Other' % (trace_name))
+        return 'Other'
+        # return None
+
     def _translate_block_info(self, info, z_piezo_info, deflection_info, name):
+        version = info['Force file list']['Version']
         ret = {
-            'name':name,
-            'raw info':info,
+            'name': name,
+            'raw info': info,
             'raw z piezo info': z_piezo_info,
             'raw deflection info': deflection_info,
-            'spring constant (N/m)':float(z_piezo_info['Spring Constant'])
+            'spring constant (N/m)': float(z_piezo_info['Spring Constant']),
             }
 
         t = info['Force file list']['Date'] # 04:42:34 PM Tue Sep 11 2007
         ret['time'] = time.strptime(t, '%I:%M:%S %p %a %b %d %Y')
 
-        type_re = re.compile('S \[(\w*)\] "([\w\s]*)"')
-        match = type_re.match(z_piezo_info['@4:Image Data'])
-        assert match.group(1).lower() == match.group(2).replace(' ','').lower(), \
-            'Name missmatch: "%s", "%s"' % (match.group(1), match.group(2))
-        ret['columns'] = [match.group(2)]
-        match = type_re.match(deflection_info['@4:Image Data'])
-        assert match.group(1).lower() == match.group(2).replace(' ','').lower(), \
-            'Name missmatch: "%s", "%s"' % (match.group(1), match.group(2))
-        ret['columns'].append(match.group(2))
-        assert ret['columns'] == ['Z sensor', 'Deflection'], \
-            'Unexpected columns: %s' % ret['columns']
-        ret['columns'] = ['z piezo (m)', 'deflection (m)']
-
         volt_re = re.compile(
-            'V \[Sens. (\w*)\] \(([.0-9]*) V/LSB\) ([.0-9]*) V')
-        match = volt_re.match(z_piezo_info['@4:Z scale'])
-        assert match.group(1) == 'ZSensorSens', z_piezo_info['@4:Z scale']
+            'V \[Sens. ([\w\s.]*)\] \(([.0-9]*) V/LSB\) (-?[.0-9]*) V')
+        hz_re = re.compile(
+            'V \[Sens. ([\w\s.]*)\] \(([.0-9]*) kHz/LSB\) (-?[.0-9]*) kHz')
+        if version in ['0x06120002', '0x06130001']:
+            match = volt_re.match(z_piezo_info['@4:Z scale'])
+            assert match != None, 'Bad regexp for %s, %s' \
+                % ('@4:Z scale', z_piezo_info['@4:Z scale'])
+            assert match.group(1) == 'ZSensorSens', z_piezo_info['@4:Z scale']
+        else:
+            assert version == '0x07200000', version
+            match = hz_re.match(z_piezo_info['@4:Z scale'])
+            assert match != None, 'Bad regexp for %s, %s' \
+                % ('@4:Z scale', z_piezo_info['@4:Z scale'])
+            assert match.group(1) == 'Freq. 1', z_piezo_info['@4:Z scale']
         ret['z piezo sensitivity (V/bit)'] = float(match.group(2))
         ret['z piezo range (V)'] = float(match.group(3))
         ret['z piezo offset (V)'] = 0.0
         # offset assumed if raw data is signed...
 
         match = volt_re.match(deflection_info['@4:Z scale'])
+        assert match != None, 'Bad regexp for %s, %s' \
+            % ('@4:Z scale', deflection_info['@4:Z scale'])
         assert match.group(1) == 'DeflSens', z_piezo_info['@4:Z scale']
         ret['deflection sensitivity (V/bit)'] = float(match.group(2))
         ret['deflection range (V)'] = float(match.group(3))
@@ -277,42 +348,70 @@ class PicoForceDriver (Driver):
         # offset assumed if raw data is signed...
 
         nm_sens_re = re.compile('V ([.0-9]*) nm/V')
-        match = nm_sens_re.match(info['Scanner list']['@Sens. Zsens'])
+        if version in ['0x06120002', '0x06130001']:        
+            match = nm_sens_re.match(info['Ciao scan list']['@Sens. ZSensorSens'])
+            assert match != None, 'Bad regexp for %s/%s, %s' \
+                % ('Ciao scan list', '@Sens. ZSensorSens',
+                   info['Ciao scan list']['@Sens. ZSensorSens'])
+        else:
+            assert version == '0x07200000', version
+            match = nm_sens_re.match(info['Ciao scan list']['@Sens. ZsensSens'])
+            assert match != None, 'Bad regexp for %s/%s, %s' \
+                % ('Ciao scan list', '@Sens. ZsensSens',
+                   info['Ciao scan list']['@Sens. ZsensSens'])
         ret['z piezo sensitivity (m/V)'] = float(match.group(1))*1e-9
 
         match = nm_sens_re.match(info['Ciao scan list']['@Sens. DeflSens'])
+        assert match != None, 'Bad regexp for %s/%s, %s' \
+            % ('Ciao scan list', '@Sens. DeflSens', info['Ciao scan list']['@Sens. DeflSens'])
         ret['deflection sensitivity (m/V)'] = float(match.group(1))*1e-9
 
         match = volt_re.match(info['Ciao force list']['@Z scan start'])
+        assert match != None, 'Bad regexp for %s/%s, %s' \
+            % ('Ciao force list', '@Z scan start', info['Ciao force list']['@Z scan start'])
         ret['z piezo scan (V/bit)'] = float(match.group(2))
         ret['z piezo scan start (V)'] = float(match.group(3))
 
         match = volt_re.match(info['Ciao force list']['@Z scan size'])
+        assert match != None, 'Bad regexp for %s/%s, %s' \
+            % ('Ciao force list', '@Z scan size', info['Ciao force list']['@Z scan size'])
         ret['z piezo scan size (V)'] = float(match.group(3))
 
         const_re = re.compile('C \[([:\w\s]*)\] ([.0-9]*)')
         match = const_re.match(z_piezo_info['@Z magnify'])
+        assert match != None, 'Bad regexp for %s, %s' \
+            % ('@Z magnify', info['@Z magnify'])
         assert match.group(1) == '4:Z scale', match.group(1)
-        ret['z piezo magnification'] = match.group(2)
-
-        match = volt_re.match(z_piezo_info['@4:Z scale'])
-        assert match.group(1) == 'ZSensorSens', match.group(1)
-        ret['z piezo scale (V/bit)'] = float(match.group(2))
-        ret['z piezo scale (V)'] = float(match.group(3))
+        ret['z piezo gain'] = float(match.group(2))
+
+        if version in ['0x06120002', '0x06130001']:        
+            match = volt_re.match(z_piezo_info['@4:Z scale'])
+            assert match != None, 'Bad regexp for %s, %s' \
+                % ('@4:Z scale', info['@4:Z scale'])
+            assert match.group(1) == 'ZSensorSens', match.group(1)
+            ret['z piezo sensitivity (V/bit)'] = float(match.group(2))
+            ret['z piezo range (V)'] = float(match.group(3))
+        else:
+            assert version == '0x07200000', version
+            pass
 
         match = volt_re.match(z_piezo_info['@4:Ramp size'])
+        assert match != None, 'Bad regexp for %s, %s' \
+            % ('@4:Ramp size', info['@4:Ramp size'])
         assert match.group(1) == 'Zsens', match.group(1)
         ret['z piezo ramp size (V/bit)'] = float(match.group(2))
         ret['z piezo ramp size (V)'] = float(match.group(3))
 
         match = volt_re.match(z_piezo_info['@4:Ramp offset'])
+        assert match != None, 'Bad regexp for %s, %s' \
+            % ('@4:Ramp offset', info['@4:Ramp offset'])
         assert match.group(1) == 'Zsens', match.group(1)
         ret['z piezo ramp offset (V/bit)'] = float(match.group(2))
         ret['z piezo ramp offset (V)'] = float(match.group(3))
-        
+
         # Unaccounted for:
         #   Samps*
-        
+
         return ret
 
     def _scale_block(self, data):
@@ -325,7 +424,7 @@ class PicoForceDriver (Driver):
             )
         info = data.info
         ret.info = info
-        ret.info['raw-data'] = data # store the raw data
+        ret.info['raw data'] = data # store the raw data
         data.info = {} # break circular reference info <-> data
 
         z_col = info['columns'].index('z piezo (m)')
@@ -333,16 +432,21 @@ class PicoForceDriver (Driver):
 
         # Leading '-' because Veeco's z increases towards the surface
         # (positive indentation), but it makes more sense to me to
-        # have it inzrease away from the surface (positive
+        # have it increase away from the surface (positive
         # separation).
         ret[:,z_col] = -(
             (data[:,z_col].astype(ret.dtype)
              * info['z piezo sensitivity (V/bit)']
              - info['z piezo offset (V)'])
+            * info['z piezo gain']
             * info['z piezo sensitivity (m/V)']
             )
 
-        ret[:,d_col] = (
+        # Leading '-' because deflection voltage increases as the tip
+        # moves away from the surface, but it makes more sense to me
+        # to have it increase as it moves toward the surface (positive
+        # tension on the protein chain).
+        ret[:,d_col] = -(
             (data[:,d_col]
              * info['deflection sensitivity (V/bit)']
              - info['deflection offset (V)'])
@@ -350,4 +454,3 @@ class PicoForceDriver (Driver):
             )
 
         return ret
-