Extract 'z piezo sensitivity (m/V)' from JPK files into Curve.info.
[hooke.git] / hooke / driver / jpk.py
index 6c95fe9fd2aac14f124b9a513ac7fd4cd3b9c57e..24d9777248d6e32f6cf20fd7d1468ed0bf367550 100644 (file)
@@ -18,6 +18,9 @@
 # <http://www.gnu.org/licenses/>.
 
 """Driver for JPK ForceRobot's velocity clamp data format.
+
+This driver is based on JPK's :file:`JPKForceSpec.txt` version 0.12.
+The specs are freely available from JPK, just email support@jpk.com.
 """
 
 import os.path
@@ -27,7 +30,6 @@ import zipfile
 import numpy
 
 from .. import curve as curve
-from .. import experiment as experiment
 from ..util.util import Closing as Closing
 from ..util.si import join_data_label, split_data_label
 from . import Driver as Driver
@@ -75,18 +77,14 @@ class JPKDriver (Driver):
             for i in range(len([p for p in f.namelist()
                                 if p.endswith('segment-header.properties')])):
                 segments.append(self._zip_segment(f, path, info, zip_info, i))
-        if zip_info['file-format-version'] not in ['0.5']:
+        if zip_info['file-format-version'] not in ['0.%d' % i
+                                                   for i in range(12)]:
             raise NotImplementedError(
                 'JPK file version %s not supported (yet).'
                 % zip_info['file-format-version'])
-        for name in ['approach', 'retract']:
-            if len([s for s in segments if s.info['name'] == name]) == 0:
-                raise ValueError(
-                    'No segment for %s in %s, only %s'
-                    % (name, path, [s.info['name'] for s in segments]))
         curve_info = self._zip_translate_params(zip_info,
                                                 segments[0].info['raw info'])
-        for segment in segments:
+        for segment in segments:  # HACK, should use curve-level spring constant
             segment.info['spring constant (N/m)'] = \
                 curve_info['spring constant (N/m)']
         return (segments, curve_info)
@@ -103,17 +101,26 @@ class JPKDriver (Driver):
         prop_file.close()
         expected_shape = (int(prop['force-segment-header']['num-points']),)
         channels = []
+        if 'list' not in prop['channels']:
+            prop['channels'] = {'list': prop['channels'].split()}
         for chan in prop['channels']['list']:
             chan_info = prop['channel'][chan]
-            channels.append(self._zip_channel(zipfile, index, chan, chan_info))
+            channels.append(self._zip_channel(
+                    zipfile, index, chan, chan_info))
             if channels[-1].shape != expected_shape:
-                    raise NotImplementedError(
-                        'Channel %d:%s in %s has strange shape %s != %s'
-                        % (index, chan, zipfile.path,
-                           channels[-1].shape, expected_shape))
+                raise NotImplementedError(
+                    'Channel %d:%s in %s has strange shape %s != %s'
+                    % (index, chan, zipfile.path,
+                       channels[-1].shape, expected_shape))
+        if len(channels) > 0:
+            shape = (len(channels[0]), len(channels))
+            dtype = channels[0].dtype
+        else:  # no channels for this data block
+            shape = (0,0)
+            dtype = numpy.float32
         d = curve.Data(
-            shape=(len(channels[0]), len(channels)),
-            dtype=channels[0].dtype,
+            shape=shape,
+            dtype=dtype,
             info=self._zip_translate_segment_params(prop))
         for i,chan in enumerate(channels):
             d[:,i] = chan
@@ -145,25 +152,24 @@ class JPKDriver (Driver):
     def _zip_translate_params(self, params, chan_info):
         info = {
             'raw info':params,
-            'filetype':self.name,
             #'time':self._time_from_TODO(raw_info[]),
             }
-        # TODO: distinguish between force clamp and velocity clamp
-        # experiments.  Note that the JPK file format is flexible
-        # enough to support mixed experiments (i.e. both force clamp
-        # and velocity clamp segments in a single experiment), but I
-        # have no idea what sort of analysis such experiments would
-        # require ;).
-        info['experiment'] = experiment.VelocityClamp()
         force_unit = chan_info['channel']['vDeflection']['conversion-set']['conversion']['force']['scaling']['unit']['unit']
         assert force_unit == 'N', force_unit
         force_base = chan_info['channel']['vDeflection']['conversion-set']['conversion']['force']['base-calibration-slot']
         assert force_base == 'distance', force_base
         dist_unit = chan_info['channel']['vDeflection']['conversion-set']['conversion']['distance']['scaling']['unit']['unit']
         assert dist_unit == 'm', dist_unit
+        distance_base = chan_info['channel']['vDeflection']['conversion-set']['conversion']['distance']['base-calibration-slot']
+        assert distance_base == 'volts', distance_base
+        # Assume volts unit is V, but it is not specified in the JPK
+        # file format.
         force_mult = float(
             chan_info['channel']['vDeflection']['conversion-set']['conversion']['force']['scaling']['multiplier'])
+        sens_mult = float(
+            chan_info['channel']['vDeflection']['conversion-set']['conversion']['distance']['scaling']['multiplier'])
         info['spring constant (N/m)'] = force_mult
+        info['z piezo sensitivity (m/V)'] = sens_mult
         return info
 
     def _zip_translate_segment_params(self, params):
@@ -281,4 +287,7 @@ class JPKDriver (Driver):
         return info
 
     def _read_old(self, path, info):
-        raise NotImplementedError('No old-style JPK files were available for testing, please send us yours: %s' % path)
+        raise NotImplementedError(
+            "Early JPK files (pre-zip) are not supported by Hooke.  Please "
+            "use JPK's `out2jpk-force` script to convert your old files "
+            "to a more recent format before loading them with Hooke.")