X-Git-Url: http://git.tremily.us/?p=calibcant.git;a=blobdiff_plain;f=calibcant%2Fcalibrate.py;h=8a68cbe01dbf8694b49f8c5a0738ea29b5a7bf52;hp=e8b7293c7760c9759916fea38f005b43f0cf5808;hb=4e6279a398a5aa9b3be92de1b1d7675be7693bd3;hpb=82cd9c169d26de338e7399439062937f31389864 diff --git a/calibcant/calibrate.py b/calibcant/calibrate.py index e8b7293..8a68cbe 100644 --- a/calibcant/calibrate.py +++ b/calibcant/calibrate.py @@ -1,22 +1,20 @@ # calibcant - tools for thermally calibrating AFM cantilevers # -# Copyright (C) 2008-2011 W. Trevor King +# Copyright (C) 2008-2012 W. Trevor King # # 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 -# . +# You should have received a copy of the GNU General Public License along with +# calibcant. If not, see . """Acquire and analyze cantilever calibration data. @@ -29,7 +27,7 @@ The relevent physical quantities are: * Vphoto The photodiode vertical deflection voltage (what we measure) * Fcant The force on the cantilever * T The temperature of the cantilever and surrounding solution -* (another thing we measure or guess) + (another thing we measure or guess) * k_b Boltzmann's constant Which are related by the parameters: @@ -78,217 +76,536 @@ the average variance . We do all these measurements a few times to estimate statistical errors. - -The functions are layed out in the families:: - - bump_*(), vib_*(), T_*(), and calib_*() - -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()`, `*_analyze()`, and `*_save()`. `*_analyze()` will run -`*_plot()` if `matplotlib` is set in `calibcant.base_config`. """ -from numpy import zeros as _zeros -from numpy import float as _float from time import sleep as _sleep -from . import LOG as _LOG - -from .bump import bump as _bump -from .T import T as _T -from .vib import vib as _vib -from .analyze import calib_analyze as _calib_analyze -from .analyze import calib_save as _calib_save +from numpy import zeros as _zeros +from numpy import float as _float +import h5py as _h5py +from pyafm.afm import AFM as _AFM +from h5config.storage.hdf5 import HDF5_Storage as _HDF5_Storage -def move_far_from_surface(stepper, distance): - """Step back approximately `distance` meters. - """ - steps = int(distance/stepper.step_size) - _LOG.info('step back %d steps (~%g m)' % (steps, distance)) - stepper.step_relative(-steps) - -def calib_acquire(afm, calibration_config, bump_config, temperature_config, - vibration_config, filename=None, group='/'): - """Acquire data for calibrating a cantilever in one function. - - Inputs: - afm a pyafm.AFM instance - calibration_config a .config._CalibrationConfig instance - bump_config a .config._BumpConfig instance - temperature_config a .config._TConfig instance - vibration_config a .config._VibrationConfig instance - - Outputs (all are arrays of recorded data): - bumps measured (V_photodiode / nm_tip) proportionality constant - Ts measured temperature (K) - vibs measured V_photodiode variance (Volts**2) in free solution - - The temperatures are collected after moving far from the surface - but before and vibrations are measured to give everything time to - settle after the big move. - """ +from . import LOG as _LOG +from .config import CalibrateConfig as _CalibrateConfig +from .bump import run as _bump +from .bump_analyze import load as _bump_load +from .temperature import run as _temperature +from .temperature_analyze import load as _temperature_load +from .vibration import run as _vibration +from .vibration_analyze import load as _vibration_load +from .analyze import analyze as _analyze +from .util import SaveSpec as _SaveSpec +from .util import save as _save +from .util import load as _load + + +def load(filename=None, group='/'): + config = _CalibrateConfig(storage=_HDF5_Storage( + filename=filename, group=group)) + config.load() + return Calibrator(config=config) + +def load_all(filename=None, group='/', raw=True): + "Load all data from a `Calibration.calibrate()` run." assert group.endswith('/'), group + calibrator = load( + filename=filename, group='{}config/'.format(group)) + data = calibrator.load_results( + filename=filename, group='{}calibration/'.format(group)) + if raw: + raw_data = calibrator.load_raw(filename=filename, group=group) + else: + raw_data = None + return (calibrator, data, raw_data) + - bumps = _zeros((calibration_config['num-bumps'],), dtype=_float) - for i in range(calibration_config['num-bumps']): - _LOG.info('acquire bump %d of %d' % (i, calibration_config['num-bumps'])) - bumps[i] = _bump(afm=afm, bump_config=bump_config, - filename=filename, group='%sbump/%d/' % (group, i)) - _LOG.debug('bumps: %s' % bumps) - - move_far_from_surface( - afm.stepper, distance=calibration_config['vibration-spacing']) - - Ts = _zeros((calibration_config['num-temperatures'],), dtype=_float) - for i in range(calibration_config['num-temperatures']): - _LOG.info('acquire T %d of %d' - % (i, calibration_config['num-temperatures'])) - Ts[i] = _T( - get_T=afm.get_temperature, temperature_config=temperature_config, - filename=filename, group='%stemperature/%d/' % (group, i)) - _sleep(calibration_config['temperature-sleep']) - _LOG.debug('temperatures: %s' % Ts) - - # get vibs - vibs = _zeros((calibration_config['num-vibrations'],), dtype=_float) - for i in range(calibration_config['num-vibrations']): - vibs[i] = _vib( - piezo=afm.piezo, vibration_config=vibration_config, - filename=filename, group='%svibration/%d/' % (group, i)) - _LOG.debug('vibrations: %s' % vibs) - - return (bumps, Ts, vibs) - -def calib(afm, calibration_config, bump_config, temperature_config, - vibration_config, filename=None, group='/'): - """Calibrate a cantilever in one function. - - Inputs: - (see `calib_acquire()`) - - Outputs: - k cantilever spring constant (in N/m, or equivalently nN/nm) - k_s standard deviation in our estimate of k +class Calibrator (object): + """Calibrate a cantilever spring constant using the thermal tune method. >>> import os >>> from pprint import pprint >>> 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_CalibrationConfig, HDF5_BumpConfig, - ... HDF5_TemperatureConfig, HDF5_VibrationConfig) - >>> from .analyze import calib_load_all + >>> from h5config.storage.hdf5 import pprint_HDF5 + >>> from pyafm.storage import load_afm + >>> from .config import (CalibrateConfig, BumpConfig, + ... TemperatureConfig, VibrationConfig) >>> 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') + >>> devices = [] + + >>> afm = load_afm() + >>> afm.load_from_config(devices=devices) + >>> if afm.piezo is None: + ... raise NotImplementedError('save a better default AFM!') + >>> config = CalibrateConfig() + >>> config['bump'] = BumpConfig() + >>> config['temperature'] = TemperatureConfig() + >>> config['vibration'] = VibrationConfig() + >>> c = Calibrator(config=config, afm=afm) >>> 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 calibration: - - >>> calibration_config = HDF5_CalibrationConfig( - ... filename=filename, group='/bump/config/calibration/') - >>> bump_config = HDF5_BumpConfig( - ... filename=filename, group='/bump/config/bump/') - >>> temperature_config = HDF5_TemperatureConfig( - ... filename=filename, group='/bump/config/temperature/') - >>> vibration_config = HDF5_VibrationConfig( - ... filename=filename, group='/bump/config/vibration') - >>> calib(afm, calibration_config, bump_config, temperature_config, - ... vibration_config, filename=filename, group='/') - TODO: replace skipped example data with real-world values + >>> k,k_s,data = c.calibrate(filename=filename) + >>> k # doctest: +SKIP + 0.058402262154840491 + >>> k_s # doctest: +SKIP + 0.0010609833397949553 + >>> pprint(data) # doctest: +ELLIPSIS, +REPORT_UDIFF + {'bump': array([...]), + 'temperature': array([...]), + 'vibration': array([...])} >>> pprint_HDF5(filename) # doctest: +ELLIPSIS, +REPORT_UDIFF - >>> everything = calib_load_all(filename, '/') - >>> pprint(everything) - - Close the Comedi device. - - >>> d.close() + / + /bump + /bump/0 + /bump/0/config + /bump/0/config/bump + + 200 + + -5e-08 + ... + /bump/0/processed + + ... + + V/m + /bump/0/raw + /bump/0/raw/deflection + + [...] + + bits + /bump/0/raw/z + + [...] + + bits + /bump/1 + ... + /config + /config/afm + + 295.15 + + 3e-05 + + z + + 1B3D9 + /config/afm/piezo + /config/afm/piezo/axes + /config/afm/piezo/axes/0 + /config/afm/piezo/axes/0/channel + + ground + + 0 + + [ -1.00000000e+01 3.05180438e-04] + + 0.0 + + /dev/comedi0 + + [ 0. 3276.75] + + -10.0 + + 65535 + + z + + 0 + + 1 + + 20.0 + + 9.0 + + -9.0 + + + + 8.8e-09 + /config/afm/piezo/axes/1 + /config/afm/piezo/axes/1/channel + + ground + + 1 + + [ -1.00000000e+01 3.05180438e-04] + + 0.0 + + /dev/comedi0 + + [ 0. 3276.75] + + -10.0 + + 65535 + + x + + 0 + + 1 + + 20.0 + + 8.0 + + -8.0 + + + + 4.16e-09 + /config/afm/piezo/inputs + /config/afm/piezo/inputs/0 + + diff + + 0 + + [ -1.00000000e+01 3.05180438e-04] + + 0.0 + + /dev/comedi0 + + [ 0. 3276.75] + + -10.0 + + 65535 + + deflection + + 0 + + 0 + + 2253E + /config/afm/stepper + + 100 + + 0.01 + + True + + True + + z-stepper + /config/afm/stepper/port + + [0 1 2 3] + + /dev/comedi0 + + output + + stepper DB-9 + + 2 + + dio + + 1.7e-07 + /config/afm/temperature + + 9600 + + 1 + + /dev/ttyS0 + + 0.0 + + room (ambient) + + Celsius + /config/bump + + 200 + + -5e-08 + + 10.0 + + quadratic + + 2e-07 + + 1e-06 + + 1024 + + 2.0 + + 10 + + 10 + + 20 + /config/temperature + + 1 + /config/vibration + + 2048 + + 50000.0 + + 25000.0 + + 500.0 + + Breit-Wigner + + False + + 1 + + Hann + + 5e-05 + /temperature + /temperature/0 + /temperature/0/config + /temperature/0/config/temperature + + 1 + /temperature/0/processed + + ... + + K + /temperature/0/raw + + ... + + K + /temperature/1 + ... + /vibration + /vibration/0 + /vibration/0/config + /vibration/0/config/deflection + ... + /vibration/0/config/vibration + + 2048 + + 50000.0 + ... + /vibration/0/processed + + ... + + V^2/Hz + /vibration/0/raw + + [...] + + bits + ... + + >>> calibrator,data,raw_data = load_all(filename=filename) + >>> calibrator.load_from_config(devices=devices) + >>> print(calibrator.config.dump()) # doctest: +ELLIPSIS, +REPORT_UDIFF + afm: + name: 1B3D9 + main-axis: z + piezo: + name: 2253E + ... + >>> pprint(data) # doctest: +ELLIPSIS, +REPORT_UDIFF + {'processed': {'spring_constant': ... + 'spring_constant_deviation': ...}, + 'raw': {'bump': array([...]), + 'temperature': array([...]), + 'vibration': array([...])}} + + >>> pprint(raw_data) # doctest: +ELLIPSIS, +REPORT_UDIFF + {'bump': [{'config': {'bump': }, + 'processed': ..., + 'raw': {'deflection': array([...], dtype=uint16), + 'z': array([...], dtype=uint16)}}, + {...}, + ...], + 'temperature': [{'config': {'temperature': }, + 'processed': ..., + 'raw': ...}, + {...}, + ...], + 'vibration': [{'config': {'vibration': }, + 'processed': ... + 'raw': array([...], dtype=uint16)}, + {...}, + ...]} + + Close the Comedi devices. + + >>> for device in devices: + ... device.close() Cleanup our temporary config file. - os.remove(filename) + >>> os.remove(filename) """ - bumps, Ts, vibs = calib_acquire( - afm, calibration_config, bump_config, temperature_config, - vibration_config, filename=filename, group=group) - # TODO: convert vib units? - k,k_s = _calib_analyze(bumps, Ts, vibs) - _calib_save(filename, group=group+'calibration/', bumps=bumps, Ts=Ts, - vibs=vibs, calibration_config=calibration_config, k=k, k_s=k_s) - return (k, k_s) + def __init__(self, config, afm=None): + self.config = config + self.afm = afm + + def load_from_config(self, devices): + if self.afm is None: + self.afm = _AFM(config=self.config['afm']) + self.afm.load_from_config(devices=devices) + + def setup_config(self): + if self.afm: + self.afm.setup_config() + self.config['afm'] = self.afm.config + + def calibrate(self, filename=None, group='/'): + """Main calibration method. + + Outputs: + k cantilever spring constant (in N/m, or equivalently nN/nm) + k_s standard deviation in our estimate of k + data the data used to determine k + """ + data = self.acquire(filename=filename, group=group) + k = k_s = bumps = temperatures = vibrations = None + bumps = data.get('bump', None) + temperatures = data.get('temperature', None) + vibrations = data.get('vibration', None) + if None not in [bumps, temperatures, vibrations]: + k,k_s = _analyze( + bumps=bumps, temperatures=temperatures, vibrations=vibrations) + if filename is not None: + self.save_results( + filename=filename, group='{}calibration/'.format(group), + spring_constant=k, spring_constant_deviation=k_s, **data) + return (k, k_s, data) + + def acquire(self, filename=None, group='/'): + """Acquire data for calibrating a cantilever in one function. + + Outputs a dict of `action` -> `data_array` pairs, for each + action (bump, temperature, vibration) that is actually + configured. For example, if you wanted to skip the surface + approach, bumping, and retraction, you could just set + `.config['bump']` to `None`. + + The temperatures are collected after moving far from the + surface but before and vibrations are measured to give + everything time to settle after the big move. + + Because theres a fair amount of data coming in during a + calibration, we save the data as it comes in. So the + procedure is bump-0, save-bump-0, bump-1, save-bump-0, etc. + To disable the saving, just set `filename` to `None`. + """ + if filename is not None: + assert group.endswith('/'), group + self.save(filename=filename, group='{}config/'.format(group)) + data = {} + if self.config['bump'] and self.config['num-bumps'] > 0: + data['bump'] = _zeros((self.config['num-bumps'],), dtype=_float) + for i in range(self.config['num-bumps']): + _LOG.info('acquire bump {} of {}'.format( + i, self.config['num-bumps'])) + data['bump'][i] = _bump( + afm=self.afm, config=self.config['bump'], + filename=filename, group='{}bump/{}/'.format(group, i)) + _LOG.debug('bumps: {}'.format(data['bump'])) + self.afm.move_away_from_surface( + distance=self.config['vibration-spacing']) + if self.config['temperature'] and self.config['num-temperatures'] > 0: + data['temperature'] = _zeros( + (self.config['num-temperatures'],), dtype=_float) + for i in range(self.config['num-temperatures']): + _LOG.info('acquire temperature {} of {}'.format( + i, self.config['num-temperatures'])) + data['temperature'][i] = _temperature( + get=self.afm.get_temperature, + config=self.config['temperature'], + filename=filename, + group='{}temperature/{}/'.format(group, i)) + _sleep(self.config['temperature']['sleep']) + _LOG.debug('temperatures: {}'.format(data['temperature'])) + if self.config['vibration'] and self.config['num-vibrations'] > 0: + data['vibration'] = _zeros( + (self.config['num-vibrations'],), dtype=_float) + for i in range(self.config['num-vibrations']): + data['vibration'][i] = _vibration( + piezo=self.afm.piezo, config=self.config['vibration'], + filename=filename, + group='{}vibration/{}/'.format(group, i)) + _LOG.debug('vibrations: {}'.format(data['vibration'])) + return data + + def save(self, filename=None, group='/'): + storage = _HDF5_Storage(filename=filename, group=group) + storage.save(config=self.config) + + @staticmethod + def save_results(filename=None, group='/', bump=None, + temperature=None, vibration=None, spring_constant=None, + spring_constant_deviation=None): + specs = [ + _SaveSpec(item=bump, relpath='raw/photodiode-sensitivity', + array=True, units='V/m'), + _SaveSpec(item=temperature, relpath='raw/temperature', + array=True, units='K'), + _SaveSpec(item=vibration, relpath='raw/vibration', + array=True, units='V^2/Hz'), + _SaveSpec(item=spring_constant, relpath='processed/spring-constant', + units='N/m', deviation=spring_constant_deviation), + ] + _save(filename=filename, group=group, specs=specs) + + @staticmethod + def load_results(filename, group='/'): + """Load results saved with `.save_results()`.""" + specs = [ + _SaveSpec(key=('raw', 'bump'), + relpath='raw/photodiode-sensitivity', + array=True, units='V/m'), + _SaveSpec(key=('raw', 'temperature'), relpath='raw/temperature', + array=True, units='K'), + _SaveSpec(key=('raw', 'vibration'), + relpath='raw/vibration', + array=True, units='V^2/Hz'), + _SaveSpec(key=('processed', 'spring_constant'), + relpath='processed/spring-constant', + units='N/m', deviation='spring_constant_deviation'), + ] + return _load(filename=filename, group=group, specs=specs) + + def load_raw(self, filename=None, group='/'): + """Load results saved during `.aquire()` by bumps, etc.""" + data = {} + with _h5py.File(filename, 'r') as f: + for name,loader in [('bump',_bump_load), + ('temperature', _temperature_load), + ('vibration', _vibration_load), + ]: + n = self.config['num-{}s'.format(name)] + if n > 0: + data[name] = [] + for i in range(n): + try: + cwg = f['{}{}/{}/'.format(group, name, i)] + except KeyError: + pass + else: + data[name].append(loader(group=cwg)) + return data