Massive rewrite (v 0.6) to base everything on Cython and revamped pypiezo.
[calibcant.git] / calibcant / bump.py
index 7d36864d6c1f6d8090eee7bfd1f069c7e4f95f60..e0b64d5c60367d548aab72c37e655f1d813de2d9 100644 (file)
 # License along with calibcant.  If not, see
 # <http://www.gnu.org/licenses/>.
 
-"""
-Aquire, save, and load cantilever calibration bump data.
+"""Acquire, save, and load cantilever calibration bump data.
+
 For measuring photodiode sensitivity.
 
-W. Trevor King Dec. 2007 - Oct. 2008
+The relevent physical quantities are:
+  Vzp_out  Output z-piezo voltage (what we generate)
+  Vzp      Applied z-piezo voltage (after external ZPGAIN)
+  Zp       The z-piezo position
+  Zcant    The cantilever vertical deflection
+  Vphoto   The photodiode vertical deflection voltage (what we measure)
 
-The relevent physical quantities are :
- Vzp_out  Output z-piezo voltage (what we generate)
- Vzp      Applied z-piezo voltage (after external ZPGAIN)
- Zp       The z-piezo position
- Zcant    The cantilever vertical deflection
- Vphoto   The photodiode vertical deflection voltage (what we measure)
+Which are related by the parameters:
+  zp_gain           Vzp_out / Vzp
+  zp_sensitivity    Zp / Vzp
+  photo_sensitivity Vphoto / Zcant
 
-Which are related by the parameters :
- zpGain           Vzp_out / Vzp
- zpSensitivity    Zp / Vzp
- photoSensitivity Vphoto / Zcant
+Cantilever calibration assumes a pre-calibrated z-piezo
+(zp_sensitivity) and amplifier (zp_gain).  In our lab, the z-piezo is
+calibrated by imaging a calibration sample, which has features with
+well defined sizes, and the gain is set with a knob on our modified
+NanoScope.
 
-Cantilever calibration assumes a pre-calibrated z-piezo (zpSensitivity) and
-amplifier (zpGain).  In our lab, the z-piezo is calibrated by imaging a
-calibration sample, which has features with well defined sizes, and the gain
-is set with a knob on the Nanoscope.
+Photo-sensitivity is measured by bumping the cantilever against the
+surface, where `Zp = Zcant` (see the `bump_*()` family of functions).
+The measured slope Vphoto/Vout is converted to photo-sensitivity via
 
-photoSensitivity is measured by bumping the cantilever against the surface,
-where Zp = Zcant (see the bump_*() family of functions)
-The measured slope Vphoto/Vout is converted to photoSensitivity via
-Vphoto/Vzp_out * Vzp_out/Vzp  * Vzp/Zp   *    Zp/Zcant =    Vphoto/Zcant
- (measured)      (1/zpGain) (1/zpSensitivity)    (1)  (photoSensitivity)
+  Vphoto/Vzp_out * Vzp_out/Vzp  * Vzp/Zp   *    Zp/Zcant =    Vphoto/Zcant
+   (measured)      (1/zp_gain) (1/zp_sensitivity)  (1)    (photo_sensitivity)
 
-We do all these measurements a few times to estimate statistical errors.
+We do all these measurements a few times to estimate statistical
+errors.
 
 The functions are layed out in the families:
- bump_*()
-For each family, * can be any of :
- aquire       get real-world data
- save         store real-world data to disk
- load         get real-world data from disk
- analyze      interperate the real-world data.
- plot         show a nice graphic to convince people we're working :p
- load_analyze_tweaked
-              read a file with a list of paths to previously saved real world data
-              load each file using *_load(), analyze using *_analyze(), and
-              optionally plot using *_plot().
-              Intended for re-processing old data.
-A family name without any _* extension (e.g. bump()),
- runs *_aquire(), *_save(), *_analyze(), *_plot().
+  bump_*()
+For each family, * can be any of:
+  acquire       get real-world data
+  save         store real-world data to disk
+  load         get real-world data from disk
+  analyze      interperate the real-world data.
+  plot         show a nice graphic to convince people we're working :p
+
+A family name without any _* extension (e.g. `bump()`), runs `*_acquire()`,
+`*_save()`, `*_analyze()`.
+
+If `base_config['matplotlib']` is `True`, `*_analyze()` will call
+`*_plot()` internally.
 """
 
-import numpy
-import time 
+import numpy as _numpy
 
-import data_logger
-import FFT_tools
-import piezo.z_piezo_utils as z_piezo_utils
+from pypiezo.base import convert_meters_to_bits as _convert_meters_to_bits
+from pypiezo.base import convert_bits_to_meters as _convert_bits_to_meters
 
-from .bump_analyze import bump_analyze
+from . import LOG as _LOG
+from .bump_analyze import bump_analyze as _bump_analyze
+from .bump_analyze import bump_save as _bump_save
 
 
-LOG_DATA = True  # quietly grab all real-world data and log to LOG_DIR
-LOG_DIR = '${DEFAULT}/calibrate_cantilever'
+def bump_acquire(afm, bump_config):
+    """Ramps `push_depth` closer and returns to the original position.
 
-TEXT_VERBOSE = True      # for debugging
+    Inputs:
+      afm          a pyafm.AFM instance
+      bump_config  a .config._BumpConfig instance
 
+    Returns the acquired ramp data dictionary, with data in DAC/ADC bits.
+    """
+    afm.move_just_onto_surface(
+        depth=bump_config['initial-position'], far=bump_config['far-steps'])
+
+    _LOG.info('bump the surface to a depth of %g m'
+              % bump_config['push-depth'])
+
+    axis = afm.piezo.axis_by_name(afm.axis_name)
+
+    start_pos = afm.piezo.last_output[afm.axis_name]
+    start_pos_m = _convert_bits_to_meters(
+        axis.axis_channel_config, axis.axis_config, start_pos)
+    close_pos_m = start_pos_m + bump_config['push-depth']
+    close_pos = _convert_meters_to_bits(
+        axis.axis_channel_config, axis.axis_config, close_pos_m)
+
+    dtype = afm.piezo.channel_dtype(afm.axis_name, direction='output')
+    appr = _numpy.linspace(
+        start_pos, close_pos, bump_config['samples']).astype(dtype)
+    # switch numpy.append to numpy.concatenate with version 2.0+
+    out = _numpy.append(appr, appr[::-1])
+    out = out.reshape((len(out), 1))
+
+    # (samples) / (meters) * (meters/second) = (samples/second)
+    freq = (bump_config['samples'] / bump_config['push-depth']
+            * bump_config['push-speed'])
+
+    data = afm.piezo.ramp(out, freq, output_names=[afm.axis_name],
+                          input_names=['deflection'])
+
+    out = out.reshape((len(out),))
+    data = data.reshape((data.size,))
+    return {afm.axis_name: out, 'deflection': data}
+
+def bump(afm, bump_config, filename, group='/'):
+    """Wrapper around bump_acquire(), bump_analyze(), bump_save().
+
+    >>> import os
+    >>> import tempfile
+    >>> from pycomedi.device import Device
+    >>> from pycomedi.subdevice import StreamingSubdevice
+    >>> from pycomedi.channel import AnalogChannel, DigitalChannel
+    >>> from pycomedi.constant import AREF, IO_DIRECTION, SUBDEVICE_TYPE, UNIT
+    >>> from pypiezo.afm import AFMPiezo
+    >>> from pypiezo.base import PiezoAxis, InputChannel
+    >>> from pypiezo.config import (HDF5_ChannelConfig, HDF5_AxisConfig,
+    ...     pprint_HDF5)
+    >>> from stepper import Stepper
+    >>> from pyafm import AFM
+    >>> from .config import HDF5_BumpConfig
+
+    >>> fd,filename = tempfile.mkstemp(suffix='.h5', prefix='calibcant-')
+    >>> os.close(fd)
+
+    >>> d = Device('/dev/comedi0')
+    >>> d.open()
+
+    Setup an `AFMPiezo` instance.
+
+    >>> s_in = d.find_subdevice_by_type(SUBDEVICE_TYPE.ai,
+    ...     factory=StreamingSubdevice)
+    >>> s_out = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
+    ...     factory=StreamingSubdevice)
+
+    >>> axis_channel = s_out.channel(
+    ...     0, factory=AnalogChannel, aref=AREF.ground)
+    >>> input_channel = s_in.channel(0, factory=AnalogChannel, aref=AREF.diff)
+    >>> for chan in [axis_channel, input_channel]:
+    ...     chan.range = chan.find_range(unit=UNIT.volt, min=-10, max=10)
+
+    We set the minimum voltage for the `z` axis to -9 (a volt above
+    the minimum possible voltage) to help with testing
+    `.get_surface_position`.  Without this minimum voltage, small
+    calibration errors could lead to a railed -10 V input for the
+    first few surface approaching steps, which could lead to an
+    `EdgeKink` error instead of a `FlatFit` error.
+
+    >>> axis_config = HDF5_AxisConfig(filename, '/bump/config/z/axis')
+    >>> axis_config.update(
+    ...     {'gain':20, 'sensitivity':8e-9, 'minimum':-9})
+    >>> axis_channel_config = HDF5_ChannelConfig(
+    ...     filename, '/bump/config/z/channel')
+    >>> input_channel_config = HDF5_ChannelConfig(
+    ...     filename, '/bump/config/deflection/channel')
+
+    >>> a = PiezoAxis(axis_config=axis_config,
+    ...     axis_channel_config=axis_channel_config,
+    ...     axis_channel=axis_channel, name='z')
+    >>> a.setup_config()
+
+    >>> c = InputChannel(
+    ...     channel_config=input_channel_config, channel=input_channel,
+    ...     name='deflection')
+    >>> c.setup_config()
+
+    >>> piezo = AFMPiezo(axes=[a], input_channels=[c])
+
+    Setup a `stepper` instance.
+
+    >>> s_d = d.find_subdevice_by_type(SUBDEVICE_TYPE.dio)
+    >>> d_channels = [s_d.channel(i, factory=DigitalChannel)
+    ...             for i in (0, 1, 2, 3)]
+    >>> for chan in d_channels:
+    ...     chan.dio_config(IO_DIRECTION.output)
+
+    >>> def write(value):
+    ...     s_d.dio_bitfield(bits=value, write_mask=2**4-1)
+
+    >>> stepper = Stepper(write=write)
+
+    Setup an `AFM` instance.
+
+    >>> afm = AFM(piezo, stepper)
+
+    Test a bump:
 
-# bump family
+    >>> bump_config = HDF5_BumpConfig(
+    ...     filename=filename, group='/bump/config/bump')
+    >>> bump(afm, bump_config, filename, group='/bump')
+    TODO: replace skipped example data with real-world values
+    >>> pprint_HDF5(filename)  # doctest: +ELLIPSIS, +REPORT_UDIFF
 
-def bump_aquire(zpiezo, push_depth, npoints, freq) :
-    """
-    Ramps closer push_depth and returns to the original position.
-    Inputs:
-     zpiezo     an opened zpiezo.zpiezo instance
-     push_depth distance to approach, in nm
-     npoints    number points during the approach and during the retreat
-     freq       rate at which data is aquired
-     log_dir    directory to log data to (see data_logger.py).
-                None to turn off logging (see also the global LOG_DATA).
-    Returns the aquired ramp data dictionary, with data in DAC/ADC bits.
-    """
-    # generate the bump output
-    start_pos = zpiezo.curPos()
-    pos_dist = zpiezo.pos_nm2out(push_depth) - zpiezo.pos_nm2out(0)
-    close_pos = start_pos + pos_dist
-    appr = linspace(start_pos, close_pos, npoints)
-    retr = linspace(close_pos, start_pos, npoints)
-    out = concatenate((appr, retr))
-    # run the bump, and measure deflection
-    if TEXT_VERBOSE :
-        print "Bump %g nm" % push_depth
-    data = zpiezo.ramp(out, freq)
-    # default saving, so we have a log in-case the operator is lazy ;)
-    if LOG_DATA == True :
-        log = data_logger.data_log(LOG_DIR, noclobber_logsubdir=False,
-                                   log_name="bump_surface")
-        log.write_dict_of_arrays(data)
-    return data
-
-def bump_save(data, log_dir) :
-    "Save the dictionary data, using data_logger.data_log()"
-    if log_dir != None :
-        log = data_logger.data_log(log_dir, noclobber_logsubdir=False,
-                                   log_name="bump")
-        log.write_dict_of_arrays(data)
-
-def bump_load(datafile) :
-    "Load the dictionary data, using data_logger.date_load()"
-    dl = data_logger.data_load()
-    data = dl.read_dict_of_arrays(path)
-    return data
-
-def bump_plot(data, plotVerbose) :
-    "Plot the bump (Vphoto vs Vzp) if plotVerbose or PYLAB_VERBOSE == True"
-    if plotVerbose or PYLAB_VERBOSE :
-        _import_pylab()
-        _pylab.figure(BASE_FIGNUM)
-        _pylab.plot(data["Z piezo output"], data["Deflection input"],
-                    '.', label='bump')
-        _pylab.title("bump surface")
-        _pylab.legend(loc='upper left')
-        _flush_plot()
-
-def bump(zpiezo, push_depth, npoints=1024, freq=100e3,
-         log_dir=None,
-         plotVerbose=False) :
-    """
-    Wrapper around bump_aquire(), bump_save(), bump_analyze(), bump_plot()
-    """
-    data = bump_aquire(zpiezo, push_depth, npoints, freq)
-    bump_save(data, log_dir)
-    photoSensitivity = bump_analyze(data, zpiezo.gain, zpiezo.sensitivity,
-                                    zpiezo.pos_out2V, zpiezo.def_in2V)
-    bump_plot(data, plotVerbose)
-    return photoSensitivity
-
-def bump_load_analyze_tweaked(tweak_file, zpGain=_usual_zpGain,
-                              zpSensitivity=_usual_zpSensitivity,
-                              Vzp_out2V=_usual_Vzp_out2V,
-                              Vphoto_in2V=_usual_Vphoto_in2V,
-                              plotVerbose=False) :
-    "Load the output file of tweak_calib_bump.sh, return an array of slopes"
-    photoSensitivity = []
-    for line in file(tweak_file, 'r') :
-        parsed = line.split()
-        path = parsed[0].split('\n')[0]
-        # read the data
-        full_data = bump_load(path)
-        if len(parsed) == 1 :
-            data = full_data # use whole bump
-        else :
-            # use the listed sections
-            zp = []
-            df = []
-            for rng in parsed[1:] :
-                p = rng.split(':')
-                starti = int(p[0])
-                stopi = int(p[1])
-                zp.extend(full_data['Z piezo output'][starti:stopi])
-                df.extend(full_data['Deflection input'][starti:stopi])
-            data = {'Z piezo output': array(zp),
-                    'Deflection input':array(df)}
-        pSi = bump_analyze(data, zpGain, zpSensitivity,
-                           Vzp_out2V, Vphoto_in2V, plotVerbose)
-        photoSensitivity.append(pSi)
-        bump_plot(data, plotVervose)
-    return array(photoSensitivity, dtype=numpy.float)
+    Close the Comedi device.
 
+    >>> d.close()
+
+    Cleanup our temporary config file.
+
+    >>> os.remove(filename)
+    """
+    deflection_channel = afm.piezo.input_channel_by_name('deflection')
+    axis = afm.piezo.axis_by_name(afm.axis_name)
+
+    data = bump_acquire(afm, bump_config)
+    photo_sensitivity = _bump_analyze(
+        data, bump_config, z_channel_config=axis.axis_channel_config,
+        z_axis_config=axis.axis_config,
+        deflection_channel_config=deflection_channel.channel_config)
+    _bump_save(
+        filename, group, data, bump_config,
+        z_channel_config=axis.axis_channel_config,
+        z_axis_config=axis.axis_config,
+        deflection_channel_config=deflection_channel.channel_config,
+        processed_bump=photo_sensitivity)
+    return photo_sensitivity