# 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