Get calibcant working with the new load_from_config-based pyafm.
[calibcant.git] / calibcant / bump_analyze.py
index 2ca6083854dd8e1a3151cdba6142e53d7e7f438d..d5256fc84bd1ea660c05c81b6bfc73d188d35de9 100644 (file)
@@ -1,26 +1,24 @@
 # calibcant - tools for thermally calibrating AFM cantilevers
 #
-# Copyright (C) 2008-2011 W. Trevor King <wking@drexel.edu>
+# Copyright (C) 2008-2012 W. Trevor King <wking@drexel.edu>
 #
 # This file is part of calibcant.
 #
-# calibcant 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.
+# calibcant is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free Software
+# Foundation, either version 3 of the License, or (at your option) any later
+# version.
 #
-# calibcant 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.
+# calibcant 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 General Public License for more details.
 #
-# You should have received a copy of the GNU Lesser General Public
-# License along with calibcant.  If not, see
-# <http://www.gnu.org/licenses/>.
+# You should have received a copy of the GNU General Public License along with
+# calibcant.  If not, see <http://www.gnu.org/licenses/>.
 
 """Surface bump analysis (measures photodiode sensitivity).
 
-Separate the more general `bump_analyze()` from the other `bump_*()`
+Separate the more general `analyze()` from the other `bump_*()`
 functions in calibcant.
 
 The relevant physical quantities are:
@@ -40,133 +38,94 @@ Which are related by the parameters:
 `photo_sensitivity` is measured by bumping the cantilever against the
 surface, where `Zp = Zcant` (see `calibrate.bump_acquire()`).  The
 measured slope `Vphoto/Vout` is converted to `photo_sensitivity` with
-`bump_analyze()`.
+`analyze()`.
 
 >>> import os
 >>> from pprint import pprint
 >>> import tempfile
 >>> import numpy
->>> from .config import HDF5_BumpConfig
->>> from pypiezo.config import pprint_HDF5, HDF5_ChannelConfig, HDF5_AxisConfig
+>>> from h5config.storage.hdf5 import pprint_HDF5
+>>> from pypiezo.config import ChannelConfig, AxisConfig
+>>> from .config import BumpConfig
 
 >>> fd,filename = tempfile.mkstemp(suffix='.h5', prefix='calibcant-')
 >>> os.close(fd)
 
->>> bump_config = HDF5_BumpConfig(filename=filename, group='/bump/config/')
->>> bump_config.save()
->>> z_channel_config = HDF5_ChannelConfig(filename=None)
+>>> config = BumpConfig()
+>>> z_channel_config = ChannelConfig()
+>>> z_channel_config['name'] = 'z'
 >>> z_channel_config['maxdata'] = 200
 >>> z_channel_config['conversion-coefficients'] = (0,1)
 >>> z_channel_config['conversion-origin'] = 0
->>> z_axis_config = HDF5_AxisConfig(filename=None)
->>> deflection_channel_config = HDF5_ChannelConfig(filename=None)
+>>> z_axis_config = AxisConfig()
+>>> z_axis_config['channel'] = z_channel_config
+>>> deflection_channel_config = ChannelConfig()
+>>> deflection_channel_config['name'] = 'deflection'
 >>> deflection_channel_config['maxdata'] = 200
 >>> deflection_channel_config['conversion-coefficients'] = (0,1)
 >>> deflection_channel_config['conversion-origin'] = 0
 
->>> raw_bump = {
+>>> raw = {
 ...     'z': numpy.arange(100, dtype=numpy.uint16),
 ...     'deflection': numpy.arange(100, dtype=numpy.uint16),
 ...     }
->>> raw_bump['deflection'][:50] = 50
->>> processed_bump = bump_analyze(
-...     raw_bump, bump_config, z_channel_config, z_axis_config,
-...     deflection_channel_config)
->>> bump_plot(data=raw_bump)  # TODO: convert to V and m
->>> bump_save(filename=filename, group='/bump/', raw_bump=raw_bump,
-...     z_channel_config=z_channel_config, z_axis_config=z_axis_config,
-...     deflection_channel_config=deflection_channel_config,
-...     processed_bump=processed_bump)
+>>> raw['deflection'][:50] = 50
+>>> processed = analyze(
+...     config=config, data=raw, z_axis_config=z_axis_config,
+...     deflection_channel_config=deflection_channel_config)
+>>> plot(data=raw)  # TODO: convert to V and m
+>>> save(filename=filename, group='/bump/',
+...     config=config, raw=raw, processed=processed)
 
 >>> pprint_HDF5(filename)  # doctest: +ELLIPSIS, +REPORT_UDIFF
 /
   /bump
     /bump/config
-      /bump/config/deflection
-        /bump/config/deflection/channel
-          <HDF5 dataset "channel": shape (), type "|S1">
-            0
-          <HDF5 dataset "conversion-coefficients": shape (), type "|S4">
-            0, 1
-          <HDF5 dataset "conversion-origin": shape (), type "|S1">
-            0
-          <HDF5 dataset "device": shape (), type "|S12">
-            /dev/comedi0
-          <HDF5 dataset "inverse-conversion-coefficients": shape (), type "|S1">
-<BLANKLINE>
-          <HDF5 dataset "inverse-conversion-origin": shape (), type "|S3">
-            1.0
-          <HDF5 dataset "maxdata": shape (), type "|S3">
-            200
-          <HDF5 dataset "range": shape (), type "|S1">
-            1
-          <HDF5 dataset "subdevice": shape (), type "|S2">
-            -1
-      <HDF5 dataset "far-steps": shape (), type "|S3">
-        200
-      <HDF5 dataset "initial-position": shape (), type "|S6">
-        -5e-08
-      <HDF5 dataset "model": shape (), type "|S9">
-        quadratic
-      <HDF5 dataset "push-depth": shape (), type "|S5">
-        2e-07
-      <HDF5 dataset "push-speed": shape (), type "|S5">
-        1e-06
-      <HDF5 dataset "samples": shape (), type "|S4">
-        1024
-      <HDF5 dataset "setpoint": shape (), type "|S3">
-        2.0
-      /bump/config/z
-        /bump/config/z/axis
-          <HDF5 dataset "gain": shape (), type "|S3">
-            1.0
-          <HDF5 dataset "maximum": shape (), type "|S4">
-            None
-          <HDF5 dataset "minimum": shape (), type "|S4">
-            None
-          <HDF5 dataset "sensitivity": shape (), type "|S3">
-            1.0
-        /bump/config/z/channel
-          <HDF5 dataset "channel": shape (), type "|S1">
-            0
-          <HDF5 dataset "conversion-coefficients": shape (), type "|S4">
-            0, 1
-          <HDF5 dataset "conversion-origin": shape (), type "|S1">
-            0
-          <HDF5 dataset "device": shape (), type "|S12">
-            /dev/comedi0
-          <HDF5 dataset "inverse-conversion-coefficients": shape (), type "|S1">
-<BLANKLINE>
-          <HDF5 dataset "inverse-conversion-origin": shape (), type "|S3">
-            1.0
-          <HDF5 dataset "maxdata": shape (), type "|S3">
-            200
-          <HDF5 dataset "range": shape (), type "|S1">
-            1
-          <HDF5 dataset "subdevice": shape (), type "|S2">
-            -1
-    <HDF5 dataset "processed": shape (), type "<f8">
-      1.00000002115
+      /bump/config/bump
+        <HDF5 dataset "far-steps": shape (), type "<i4">
+          200
+        <HDF5 dataset "initial-position": shape (), type "<f8">
+          -5e-08
+        <HDF5 dataset "min-slope-ratio": shape (), type "<f8">
+          10.0
+        <HDF5 dataset "model": shape (), type "|S9">
+          quadratic
+        <HDF5 dataset "push-depth": shape (), type "<f8">
+          2e-07
+        <HDF5 dataset "push-speed": shape (), type "<f8">
+          1e-06
+        <HDF5 dataset "samples": shape (), type "<i4">
+          1024
+        <HDF5 dataset "setpoint": shape (), type "<f8">
+          2.0
+    /bump/processed
+      <HDF5 dataset "data": shape (), type "<f8">
+        1.00...
+      <HDF5 dataset "units": shape (), type "|S3">
+        V/m
     /bump/raw
-      <HDF5 dataset "deflection": shape (100,), type "<u2">
-        [50 50 ... 50 51 52 ... 97 98 99]
-      <HDF5 dataset "z": shape (100,), type "<u2">
-        [ 0  1  2  3  ... 97 98 99]
-
->>> (raw_bump,bump_config,z_channel_config,z_axis_config,
-...  deflection_channel_config,processed_bump) = bump_load(
-...     filename=filename, group='/bump/')
-
->>> pprint(raw_bump)  # doctest: +ELLIPSIS
-{'deflection': array([50, 50, ... 51, 52, 53, ..., 97, 98, 99], dtype=uint16),
- 'z': array([ 0,  1,  2,  ..., 97, 98, 99], dtype=uint16)}
->>> processed_bump  # doctest: +ELLIPSIS
-1.00...
+      /bump/raw/deflection
+        <HDF5 dataset "data": shape (100,), type "<u2">
+          [50 50 ... 50 51 52 ... 97 98 99]
+        <HDF5 dataset "units": shape (), type "|S4">
+          bits
+      /bump/raw/z
+        <HDF5 dataset "data": shape (100,), type "<u2">
+          [ 0  1  2  3  ... 97 98 99]
+        <HDF5 dataset "units": shape (), type "|S4">
+          bits
+
+>>> data = load(filename=filename, group='/bump/')
+
+>>> pprint(data)  # doctest: +ELLIPSIS, +REPORT_UDIFF
+{'config': {'bump': <BumpConfig ...>},
+ 'processed': 1.00...,
+ 'raw': {'deflection': array([50, 50, ..., 52, 53, ..., 98, 99], dtype=uint16),
+         'z': array([ 0,  1,  2,  ..., 97, 98, 99], dtype=uint16)}}
 
 >>> os.remove(filename)
 """
 
-import h5py as _h5py
 import numpy as _numpy
 from scipy.optimize import leastsq as _leastsq
 
@@ -180,28 +139,29 @@ except (ImportError, RuntimeError), e:
 
 from pypiezo.base import convert_bits_to_volts as _convert_bits_to_volts
 from pypiezo.base import convert_bits_to_meters as _convert_bits_to_meters
-from pypiezo.config import HDF5_ChannelConfig as _HDF5_ChannelConfig
-from pypiezo.config import HDF5_AxisConfig as _HDF5_AxisConfig
-from pypiezo.config import h5_create_group as _h5_create_group
+from pypiezo.config import AxisConfig as _AxisConfig
+from pypiezo.config import InputChannelConfig as _InputChannelConfig
 
 from . import LOG as _LOG
-from . import base_config as _base_config
+from . import package_config as _package_config
 from .config import Linear as _Linear
 from .config import Quadratic as _Quadratic
-from .config import HDF5_BumpConfig as _HDF5_BumpConfig
+from .config import BumpConfig as _BumpConfig
+from .util import SaveSpec as _SaveSpec
+from .util import save as _save
+from .util import load as _load
 
 
-def bump_analyze(data, bump_config, z_channel_config, z_axis_config,
-                 deflection_channel_config, plot=False):
+def analyze(config, data, z_axis_config,
+            deflection_channel_config, plot=False):
     """Return the slope of the bump.
 
     Inputs:
       data              dictionary of data in DAC/ADC bits
-      bump_config       `.config._BumpConfig` instance
-      z_channel_config  z `pypiezo.config._ChannelConfig` instance
-      z_axis_config     z `pypiezo.config._AxisConfig` instance
+      config            `.config._BumpConfig` instance
+      z_axis_config     z `pypiezo.config.AxisConfig` instance
       deflection_channel_config
-                        deflection `pypiezo.config._ChannelConfig` instance
+                        deflection `pypiezo.config.InputChannelConfig` instance
       plot              boolean overriding matplotlib config setting.
     Returns:
       photo_sensitivity (Vphoto/Zcant) in Volts/m
@@ -209,10 +169,12 @@ def bump_analyze(data, bump_config, z_channel_config, z_axis_config,
     Checks for strong correlation (r-value) and low randomness chance
     (p-value).
     """
-    z = _convert_bits_to_meters(z_channel_config, z_axis_config, data['z'])
+    z = _convert_bits_to_meters(z_axis_config, data['z'])
     deflection = _convert_bits_to_volts(
         deflection_channel_config, data['deflection'])
-    if bump_config['model'] == _Linear:
+    high_voltage_rail = _convert_bits_to_volts(
+        deflection_channel_config, deflection_channel_config['maxdata'])
+    if config['model'] == _Linear:
         kwargs = {
             'param_guesser': limited_linear_param_guess,
             'model': limited_linear,
@@ -224,10 +186,12 @@ def bump_analyze(data, bump_config, z_channel_config, z_axis_config,
             'model': limited_quadratic,
             'sensitivity_from_fit_params': limited_quadratic_sensitivity,
             }
-    photo_sensitivity = bump_fit(z, deflection, plot=plot, **kwargs)
+    photo_sensitivity = fit(
+        z, deflection, high_voltage_rail=high_voltage_rail, plot=plot,
+        **kwargs)
     return photo_sensitivity
 
-def limited_linear(x, params):
+def limited_linear(x, params, high_voltage_rail):
     """
     Model the bump as:
       flat region (off-surface)
@@ -238,9 +202,11 @@ def limited_linear(x, params):
       y_contact (y value for the surface-contact kink)
       slope (dy/dx at the surface-contact kink)
     """
-    high_voltage_rail = 2**16 - 1 # bits
     x_contact,y_contact,slope = params
-    y = slope*(x-x_contact) + y_contact
+    off_surface_mask = x <= x_contact
+    on_surface_mask = x > x_contact
+    y = (off_surface_mask * y_contact +
+         on_surface_mask * (y_contact + slope*(x-x_contact)))
     y = _numpy.clip(y, y_contact, high_voltage_rail)
     return y
 
@@ -266,6 +232,8 @@ def limited_linear_param_guess(x, y):
     i_high = i
     x_contact = float(x[i_low])
     x_high = float(x[i_high])
+    if x_high == x_contact:  # things must be pretty flat
+        x_contact = (x_contact + x[0]) / 2
     slope = (y_high - y_contact) / (x_high - x_contact)
     return (x_contact, y_contact, slope)
 
@@ -277,7 +245,7 @@ def limited_linear_sensitivity(params):
     slope = params[2]
     return slope
 
-def limited_quadratic(x, params):
+def limited_quadratic(x, params, high_voltage_rail):
     """
     Model the bump as:
       flat region (off-surface)
@@ -289,9 +257,12 @@ def limited_quadratic(x, params):
       slope (dy/dx at the surface-contact kink)
       quad (d**2 y / dx**2, allow decreasing sensitivity with increased x)
     """
-    high_voltage_rail = 2**16 - 1 # bits
     x_contact,y_contact,slope,quad = params
-    y = slope*(x-x_contact) + quad*(x-x_contact)**2+ y_contact
+    off_surface_mask = x <= x_contact
+    on_surface_mask = x > x_contact
+    y = (off_surface_mask * y_contact +
+         on_surface_mask * (
+            y_contact + slope*(x-x_contact) + quad*(x-x_contact)**2))
     y = _numpy.clip(y, y_contact, high_voltage_rail)
     return y
 
@@ -321,11 +292,11 @@ def limited_quadratic_sensitivity(params):
     slope = params[2]
     return slope
 
-def bump_fit(z, deflection,
-             param_guesser=limited_quadratic_param_guess,
-             model=limited_quadratic,
-             sensitivity_from_fit_params=limited_quadratic_sensitivity,
-             plot=False):
+def fit(z, deflection, high_voltage_rail,
+        param_guesser=limited_quadratic_param_guess,
+        model=limited_quadratic,
+        sensitivity_from_fit_params=limited_quadratic_sensitivity,
+        plot=False):
     """Fit a aurface bump and return the photodiode sensitivitiy.
 
     Parameters:
@@ -340,11 +311,18 @@ def bump_fit(z, deflection,
     """
     _LOG.debug('fit bump data with model %s' % model)
     def residual(p, deflection, z):
-        return model(z, p) - deflection
+        return model(z, p, high_voltage_rail=high_voltage_rail) - deflection
     param_guess = param_guesser(z, deflection)
-    p,cov,info,mesg,ier = _leastsq(
-        residual, param_guess, args=(deflection, z), full_output=True,
-        maxfev=int(10e3))
+    try:
+        p,cov,info,mesg,ier = _leastsq(
+            residual, param_guess, args=(deflection, z), full_output=True,
+            maxfev=int(10e3))
+    except ValueError:
+        zd = _numpy.ndarray(list(z.shape) + [2], dtype=z.dtype)
+        zd[:,0] = z
+        zd[:,1] = d
+        _numpy.savetxt('/tmp/z-deflection.dat', zd, delimiter='\t')
+        raise
     _LOG.debug('fitted params: %s' % p)
     _LOG.debug('covariance matrix: %s' % cov)
     #_LOG.debug('info: %s' % info)
@@ -353,74 +331,44 @@ def bump_fit(z, deflection,
         _LOG.debug('solution converged')
     else:
         _LOG.debug('solution did not converge')
-    if plot or _base_config['matplotlib']:
-        yguess = model(z, param_guess)
-        yfit = model(z, p)
-        bump_plot({'z': z, 'deflection': deflection}, yguess=yguess, yfit=yfit)
+    if plot or _package_config['matplotlib']:
+        yguess = model(z, param_guess, high_voltage_rail=high_voltage_rail)
+        yfit = model(z, p, high_voltage_rail=high_voltage_rail)
+        _plot({'z': z, 'deflection': deflection}, yguess=yguess, yfit=yfit)
     return sensitivity_from_fit_params(p)
 
-def bump_save(filename, group='/', raw_bump=None, bump_config=None,
-              z_channel_config=None, z_axis_config=None,
-              deflection_channel_config=None, processed_bump=None):
-    with _h5py.File(filename, 'a') as f:
-        cwg = _h5_create_group(f, group)
-        if raw_bump is not None:
-            try:
-                del cwg['raw/z']
-            except KeyError:
-                pass
-            try:
-                del cwg['raw/deflection']
-            except KeyError:
-                pass
-            cwg['raw/z'] = raw_bump['z']
-            cwg['raw/deflection'] = raw_bump['deflection']
-        for config,key in [(bump_config, 'config/bump'),
-                           (z_channel_config, 'config/z/channel'),
-                           (z_axis_config, 'config/z/axis'),
-                           (deflection_channel_config,
-                            'config/deflection/channel')]:
-            if config is None:
-                continue
-            config_cwg = _h5_create_group(cwg, key)
-            config.save(group=config_cwg)
-        if processed_bump is not None:
-            try:
-                del cwg['processed']
-            except KeyError:
-                pass
-            cwg['processed'] = processed_bump
-
-def bump_load(filename, group='/'):
-    assert group.endswith('/')
-    raw_bump = processed_bump = None
-    configs = []
-    with _h5py.File(filename, 'a') as f:
-        try:
-            raw_bump = {
-                'z': f[group+'raw/z'][...],
-                'deflection': f[group+'raw/deflection'][...],
-                }
-        except KeyError:
-            pass
-        for Config,key in [(_HDF5_BumpConfig, 'config/bump'),
-                           (_HDF5_ChannelConfig, 'config/z/channel'),
-                           (_HDF5_AxisConfig, 'config/z/axis'),
-                           (_HDF5_ChannelConfig, 'config/deflection/channel')]:
-            config = Config(filename=filename, group=group+key)
-            configs.append(config)
-        try:
-            processed_bump = f[group+'processed'][...]
-        except KeyError:
-            pass
-    ret = [raw_bump]
-    ret.extend(configs)
-    ret.append(processed_bump)
-    for config in configs:
-        config.load()
-    return tuple(ret)
-
-def bump_plot(data, yguess=None, yfit=None):
+def save(filename=None, group='/', config=None, z_axis_config=None,
+         deflection_channel_config=None, raw=None, processed=None):
+    specs = [
+        _SaveSpec(item=config, relpath='config/bump', config=_BumpConfig),
+        _SaveSpec(item=z_axis_config, relpath='config/z', config=_AxisConfig),
+        _SaveSpec(item=deflection_channel_config, relpath='config/deflection',
+                  config=_InputChannelConfig),
+        _SaveSpec(item=processed, relpath='processed', units='V/m'),
+        ]
+    if raw is not None:
+        for key in raw.keys():
+            specs.append(_SaveSpec(
+                    item=raw[key], relpath='raw/{}'.format(key), array=True,
+                    units='bits'))
+    _save(filename=filename, group=group, specs=specs)
+
+def load(filename=None, group='/'):
+    specs = [
+        _SaveSpec(key=('config', 'bump'), relpath='config/bump',
+                  config=_BumpConfig),
+        _SaveSpec(key=('config', 'z_axis_config'), relpath='config/z',
+                  config=_AxisConfig),
+        _SaveSpec(key=('config', 'deflection_channel_config'),
+                  relpath='config/deflection', config=_InputChannelConfig),
+        _SaveSpec(key=('raw', 'z'), relpath='raw/z', array=True, units='bits'),
+        _SaveSpec(key=('raw', 'deflection'), relpath='raw/deflection',
+                  array=True, units='bits'),
+        _SaveSpec(key=('processed',), relpath='processed', units='V/m'),
+        ]
+    return _load(filename=filename, group=group, specs=specs)
+
+def plot(data, yguess=None, yfit=None):
     "Plot the bump (Vphoto vs Vzp)"
     if not _matplotlib:
         raise _matplotlib_import_error
@@ -450,4 +398,6 @@ def bump_plot(data, yguess=None, yfit=None):
         #residual_axes.legend(loc='upper right')
         residual_axes.set_xlabel('Z piezo (meters)')
         residual_axes.set_ylabel('Photodiode (Volts)')
-    figure.show()
+    if hasattr(figure, 'show'):
+        figure.show()
+_plot = plot  # alternative name for use inside fit()