X-Git-Url: http://git.tremily.us/?p=calibcant.git;a=blobdiff_plain;f=calibcant%2Fcalibrate.py;h=8a68cbe01dbf8694b49f8c5a0738ea29b5a7bf52;hp=e7b02042c50c38c35c2d090a6a553cbbe7a5dee6;hb=4e6279a398a5aa9b3be92de1b1d7675be7693bd3;hpb=607535583cd8f3f4315cda669114e759e5b269ec diff --git a/calibcant/calibrate.py b/calibcant/calibrate.py index e7b0204..8a68cbe 100644 --- a/calibcant/calibrate.py +++ b/calibcant/calibrate.py @@ -27,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: @@ -76,188 +76,86 @@ 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.package_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 - - -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, filename=None, group='/'): - """Acquire data for calibrating a cantilever in one function. - - Inputs: - afm a pyafm.AFM instance - calibration_config a .config._CalibrationConfig instance +from numpy import zeros as _zeros +from numpy import float as _float - 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 +import h5py as _h5py +from pyafm.afm import AFM as _AFM +from h5config.storage.hdf5 import HDF5_Storage as _HDF5_Storage - 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=calibration_config['bump'], - 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=calibration_config['temperature'], - 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=calibration_config['vibration'], - filename=filename, group='%svibration/%d/' % (group, i)) - _LOG.debug('vibrations: %s' % vibs) - - return (bumps, Ts, vibs) - -def calib(afm, calibration_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 h5config.storage.hdf5 import pprint_HDF5 - >>> 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 ChannelConfig, AxisConfig - >>> from stepper import Stepper - >>> from pyafm.afm import AFM - >>> from .config import (CalibrationConfig, BumpConfig, + >>> from pyafm.storage import load_afm + >>> from .config import (CalibrateConfig, BumpConfig, ... TemperatureConfig, VibrationConfig) - >>> from .analyze import calib_load_all >>> 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 = AxisConfig() - >>> axis_config.update( - ... {'gain':20, 'sensitivity':8e-9, 'minimum':-9}) - >>> axis_channel_config = ChannelConfig() - >>> axis_channel_config['name'] = 'z' - >>> axis_config['channel'] = axis_channel_config - >>> input_channel_config = ChannelConfig() - >>> input_channel_config['name'] = 'deflection' - - >>> a = PiezoAxis(config=axis_config, axis_channel=axis_channel) - >>> a.setup_config() - - >>> c = InputChannel(config=input_channel_config, channel=input_channel) + >>> 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], inputs=[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 = CalibrationConfig() - >>> calibration_config['bump'] = BumpConfig() - >>> calibration_config['temperature'] = TemperatureConfig() - >>> calibration_config['vibration'] = VibrationConfig() - >>> calib(afm, calibration_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 / /bump @@ -266,137 +164,448 @@ def calib(afm, calibration_config, filename=None, group='/'): /bump/0/config/bump 200 + + -5e-08 ... - /bump/0/config/deflection - /bump/0/config/deflection/channel - - 0 - ... - /bump/0/config/z - /bump/0/config/z/axis - /bump/0/config/z/axis/channel - - 0 - ... - - 20 - ... - - ... + /bump/0/processed + + ... + + V/m /bump/0/raw - - [...] - - [...] + /bump/0/raw/deflection + + [...] + + bits + /bump/0/raw/z + + [...] + + bits /bump/1 ... - /calibration - /calibration/config - /calibration/config/bump - - 200 - ... - - 10 - ... - /calibration/processed - /calibration/processed/spring-constant + /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 + ... - - N/m - /calibration/raw - /calibration/raw/photodiode-sensitivity - - [...] - - V/m - /calibration/raw/temperature - - [...] K - /calibration/raw/thermal-vibration-variance - - [...] - - V^2 - /temperature - /temperature/0 - /temperature/0/config - - False - - Celsius - - 295.15 - - 22 /temperature/1 ... /vibration /vibration/0 /vibration/0/config /vibration/0/config/deflection - - 0 ... /vibration/0/config/vibration 2048 + + 50000.0 ... - - ... + /vibration/0/processed + + ... + + V^2/Hz /vibration/0/raw - + [...] - /vibration/1 + + bits ... - /vibration/19 - ... - /vibration/19/raw - - [...] - >>> everything = calib_load_all(filename, '/') - >>> pprint(everything) # doctest: +ELLIPSIS, +REPORT_UDIFF - {'bump_details': [{'bump_config': , - 'deflection_channel_config': , - 'processed_bump': ..., - 'raw_bump': {'deflection': array([...], dtype=uint16), - 'z': array([...], dtype=uint16)}, - 'z_axis_config': }, - ...], - 'bumps': array([...]), - 'calibration_config': , - 'k': ..., - 'k_s': ..., - 'temperature_details': [{'processed_temperature': ..., - 'raw_temperature': array(22), - 'temperature_config': }, - ...], - 'temperatures': array([...]), - 'vibration_details': [{'deflection_channel_config': , - 'processed_vibration': ..., - 'raw_vibration': array([...], dtype=uint16), - 'vibration_config': }, - ...], - 'vibrations': array([...])} - - Close the Comedi device. - - >>> d.close() + + >>> 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, filename=filename, group=group) - # TODO: convert vib units? - k,k_s = _calib_analyze(bumps, Ts, vibs) - _calib_save(filename, group=group+'calibration/', bumps=bumps, - temperatures=Ts, vibrations=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