Better errors from analyze_all when we're missing raw data.
[calibcant.git] / calibcant / analyze.py
1 # calibcant - tools for thermally calibrating AFM cantilevers
2 #
3 # Copyright (C) 2008-2012 W. Trevor King <wking@drexel.edu>
4 #
5 # This file is part of calibcant.
6 #
7 # calibcant is free software: you can redistribute it and/or modify it under
8 # the terms of the GNU General Public License as published by the Free Software
9 # Foundation, either version 3 of the License, or (at your option) any later
10 # version.
11 #
12 # calibcant is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License along with
17 # calibcant.  If not, see <http://www.gnu.org/licenses/>.
18
19 """Calculate `k` from arrays of bumps, temperatures, and vibrations.
20
21 Separate the more general `analyze()` from the other calibration
22 functions in calibcant.
23
24 The relevent physical quantities are :
25   Vzp_out  Output z-piezo voltage (what we generate)
26   Vzp      Applied z-piezo voltage (after external ZPGAIN)
27   Zp       The z-piezo position
28   Zcant    The cantilever vertical deflection
29   Vphoto   The photodiode vertical deflection voltage (what we measure)
30   Fcant    The force on the cantilever
31   T        The temperature of the cantilever and surrounding solution
32            (another thing we measure)
33   k_b      Boltzmann's constant
34
35 Which are related by the parameters:
36   zp_gain           Vzp_out / Vzp
37   zp_sensitivity    Zp / Vzp
38   photo_sensitivity Vphoto / Zcant
39   k_cant            Fcant / Zcant
40
41
42 >>> import numpy
43 >>> from .config import CalibrateConfig
44
45 >>> config = CalibrateConfig()
46 >>> bumps = numpy.array((15.9e6, 16.9e6, 16.3e6))
47 >>> temperatures = numpy.array((295, 295.2, 294.8))
48 >>> vibrations = numpy.array((2.20e-5, 2.22e-5, 2.21e-5))
49
50 >>> k,k_s = analyze(bumps=bumps, temperatures=temperatures,
51 ...     vibrations=vibrations)
52 >>> (k, k_s)  # doctest: +ELLIPSIS
53 (0.0493..., 0.00248...)
54
55 Most of the error in this example comes from uncertainty in the
56 photodiode sensitivity (bumps).
57
58 >>> k_s/k  # doctest: +ELLIPSIS
59 0.0503...
60 >>> bumps.std()/bumps.mean()  # doctest: +ELLIPSIS
61 0.0251...
62 >>> temperatures.std()/temperatures.mean()  # doctest: +ELLIPSIS
63 0.000553...
64 >>> vibrations.std()/vibrations.mean()  # doctest: +ELLIPSIS
65 0.00369...
66 """
67
68 import h5py as _h5py
69 import numpy as _numpy
70 try:
71     from scipy.constants import Boltzmann as _kB  # in J/K
72 except ImportError:
73     from scipy.constants import Bolzmann as _kB  # in J/K
74 # Bolzmann -> Boltzmann patch submitted:
75 #   http://projects.scipy.org/scipy/ticket/1417
76 # Fixed in scipy commit 4716d91, Apr 2, 2011, during work after v0.9.0rc5.
77
78 try:
79     import matplotlib as _matplotlib
80     import matplotlib.pyplot as _matplotlib_pyplot
81     import time as _time  # for timestamping lines on plots
82 except (ImportError, RuntimeError), e:
83     _matplotlib = None
84     _matplotlib_import_error = e
85
86 from h5config.storage.hdf5 import h5_create_group as _h5_create_group
87 from pypiezo.base import get_axis_name as _get_axis_name
88
89 from . import LOG as _LOG
90 from . import package_config as _package_config
91
92 from .bump_analyze import analyze as _bump_analyze
93 from .bump_analyze import save as _bump_save
94 from .temperature_analyze import analyze as _temperature_analyze
95 from .temperature_analyze import save as _temperature_save
96 from .vibration_analyze import analyze as _vibration_analyze
97 from .vibration_analyze import save as _vibration_save
98 from .util import SaveSpec as _SaveSpec
99 from .util import save as _save
100
101
102 def analyze(bumps, temperatures, vibrations):
103     """Analyze data from `get_calibration_data()`
104
105     Inputs (all are arrays of recorded data):
106       bumps measured (V_photodiode / nm_tip) proportionality constant
107       temperatures    measured temperature (K)
108       vibrations  measured V_photodiode variance in free solution (V**2)
109     Outputs:
110       k    cantilever spring constant (in N/m, or equivalently nN/nm)
111       k_s  standard deviation in our estimate of k
112
113     Notes:
114
115     We're assuming vib is mostly from thermal cantilever vibrations
116     (and then only from vibrations in the single vertical degree of
117     freedom), and not from other noise sources.
118
119     If the error is large, check the relative errors
120     (`x.std()/x.mean()`)of your input arrays.  If one of them is
121     small, don't bother repeating that measurment too often.  If one
122     is large, try repeating that measurement more.  Remember that you
123     need enough samples to have a valid error estimate in the first
124     place, and that none of this addresses any systematic errors.
125     """
126     ps_m = bumps.mean()  # ps for photo-sensitivity
127     ps_s = bumps.std()
128     T_m = temperatures.mean()
129     T_s = temperatures.std()
130     v2_m = vibrations.mean()  # average voltage variance
131     v2_s = vibrations.std()
132
133     if ps_m == 0:
134         raise ValueError('invalid bumps: {}'.format(bumps))
135     if T_m == 0:
136         raise ValueError('invalid temperatures: {}'.format(temperatures))
137     if v2_m == 0:
138         raise ValueError('invalid vibrations: {}'.format(vibrations))
139
140     # Vphoto / photo_sensitivity = x
141     # k = kB T / <x**2> = kB T photo_sensitivity**2 / Vphoto_var
142     #
143     # units,  photo_sensitivity =  Vphoto/(Zcant in m),
144     # so Vphoto/photo_sensitivity = Zcant in m
145     # so k = J/K * K / m^2 = J / m^2 = N/m
146     k  = _kB * T_m * ps_m**2 / v2_m
147
148     # propogation of errors
149     # dk/dT = k/T
150     dk_T = k/T_m * T_s
151     # dk/dps = 2k/ps
152     dk_ps = 2*k/ps_m * ps_s
153     # dk/dv2 = -k/v2
154     dk_v = -k/v2_m * v2_s
155
156     k_s = _numpy.sqrt(dk_T**2 + dk_ps**2 + dk_v**2)
157
158     _LOG.info('variable (units)         : '
159               'mean +/- std. dev. (relative error)')
160     _LOG.info('cantilever k (N/m)       : %g +/- %g (%g)' % (k, k_s, k_s/k))
161     _LOG.info('photo sensitivity (V/m)  : %g +/- %g (%g)'
162               % (ps_m, ps_s, ps_s/ps_m))
163     _LOG.info('T (K)                    : %g +/- %g (%g)'
164               % (T_m, T_s, T_s/T_m))
165     _LOG.info('vibration variance (V^2) : %g +/- %g (%g)'
166               % (v2_m, v2_s, v2_s/v2_m))
167
168     if _package_config['matplotlib']:
169         plot(bumps, temperatures, vibrations)
170
171     return (k, k_s)
172
173
174 def plot(bumps, temperatures, vibrations):
175     if not _matplotlib:
176         raise _matplotlib_import_error
177     figure = _matplotlib_pyplot.figure()
178
179     bump_axes = figure.add_subplot(3, 1, 1)
180     T_axes = figure.add_subplot(3, 1, 2)
181     vib_axes = figure.add_subplot(3, 1, 3)
182
183     timestamp = _time.strftime('%H%M%S')
184     bump_axes.set_title('cantilever calibration %s' % timestamp)
185
186     bump_axes.autoscale(tight=True)
187     bump_axes.plot(bumps, 'g.-')
188     bump_axes.set_ylabel('photodiode sensitivity (V/m)')
189     T_axes.autoscale(tight=True)
190     T_axes.plot(temperatures, 'r.-')
191     T_axes.set_ylabel('temperature (K)')
192     vib_axes.autoscale(tight=True)
193     vib_axes.plot(vibrations, 'b.-')
194     vib_axes.set_ylabel('thermal deflection variance (V^2)')
195
196     if hasattr(figure, 'show'):
197         figure.show()
198     return figure
199 _plot = plot  # alternative name for use inside analyze_all()
200
201 def save_results(filename=None, group='/', bump=None,
202                  temperature=None, vibration=None, spring_constant=None,
203                  spring_constant_deviation=None):
204     specs = [
205         _SaveSpec(item=bump, relpath='raw/photodiode-sensitivity',
206                   array=True, units='V/m'),
207         _SaveSpec(item=temperature, relpath='raw/temperature',
208                   array=True, units='K'),
209         _SaveSpec(item=vibration, relpath='raw/vibration',
210                   array=True, units='V^2/Hz'),
211         _SaveSpec(item=spring_constant, relpath='processed/spring-constant',
212                   units='N/m', deviation=spring_constant_deviation),
213         ]
214     _save(filename=filename, group=group, specs=specs)
215
216 def analyze_all(config, data, raw_data, maximum_relative_error=1e-5,
217                 filename=None, group=None, plot=False, dry_run=False):
218     "(Re)analyze (and possibly plot) all data from a `calib()` run."
219     if not data.get('bump', None):
220         data['bump'] = _numpy.zeros((config['num-bumps'],), dtype=float)
221     if not data.get('temperature', None):
222         data['temperature'] = _numpy.zeros(
223             (config['num-temperatures'],), dtype=float)
224     if not data.get('vibrations', None):
225         data['vibration'] = _numpy.zeros(
226                 (config['num-vibrations'],), dtype=float)
227     axis_config = config['afm']['piezo'].select_config(
228         setting_name='axes',
229         attribute_value=config['afm']['main-axis'],
230         get_attribute=_get_axis_name)
231     input_config = config['afm']['piezo'].select_config(
232         setting_name='inputs', attribute_value='deflection')
233     bumps_changed = temperatures_changed = vibrations_changed = False
234     calibration_group = None
235     if not isinstance(group, _h5py.Group) and not dry_run:
236         f = _h5py.File(filename, mode='a')
237         group = _h5_create_group(f, group)
238     else:
239         f = None
240     try:
241         if len(data.get('raw/bump', [])) != len(data['bump']):
242             bumps_changed = True
243         for i,bump in enumerate(raw_data['bump']):
244             data['bump'][i],changed = check_bump(
245                 index=i, bump=bump, z_axis_config=axis_config,
246                 deflection_channel_config=input_config, plot=plot,
247                 maximum_relative_error=maximum_relative_error)
248             if changed and not dry_run:
249                 bumps_changed = True
250                 bump_group = _h5_create_group(group, 'bump/{}'.format(i))
251                 _bump_save(group=bump_group, processed=data['bump'][i])
252         if len(data.get('raw/temperature', [])) != len(data['temperature']):
253             temperatures_changed = True
254         for i,temperature in enumerate(raw_data['temperature']):
255             data['temperature'][i],changed = check_temperature(
256                 index=i, temperature=temperature,
257                 maximum_relative_error=maximum_relative_error)
258             if changed and not dry_run:
259                 temperatures_changed = True
260                 temperature_group = _h5_create_group(
261                     group, 'temperature/{}'.format(i))
262                 _temperature_save(
263                     group=temerature_group, processed=data['temperature'][i])
264         if len(data.get('raw/vibration', [])) != len(data['vibration']):
265             vibrations_changed = True
266         for i,vibration in enumerate(raw_data['vibration']):
267             data['vibration'][i],changed = check_vibration(
268                     index=i, vibration=vibration,
269                     deflection_channel_config=input_config, plot=plot,
270                     maximum_relative_error=maximum_relative_error)
271             if changed and not dry_run:
272                 vibrations_changed = True
273                 vibration_group = _h5_create_group(
274                     group, 'vibration/{}'.format(i))
275                 _vibration_save(
276                     group=vibration_group, processed=data['vibration'])
277         if (bumps_changed or temperatures_changed or vibrations_changed
278             ) and not dry_run:
279             calibration_group = _h5_create_group(group, 'calibration')
280             if bumps_changed:
281                 save_results(
282                     group=calibration_group, bump=data['bump'])
283             if temperatures_changed:
284                 save_results(
285                     group=calibration_group, temperature=data['temperature'])
286             if vibrations_changed:
287                 save_results(
288                     group=calibration_group, vibration=data['vibration'])
289         if len(raw_data['bump']) != len(data['bump']):
290             raise ValueError(
291                 'not enough raw bump data: {} of {}'.format(
292                     len(raw_data['bump']), len(data['bump'])))
293         if len(raw_data['temperature']) != len(data['temperature']):
294             raise ValueError(
295                 'not enough raw temperature data: {} of {}'.format(
296                     len(raw_data['temperature']), len(data['temperature'])))
297         if len(raw_data['vibration']) != len(data['vibration']):
298             raise ValueError(
299                 'not enough raw vibration data: {} of {}'.format(
300                     len(raw_data['vibration']), len(data['vibration'])))
301         k,k_s,changed = check_calibration(
302             k=data.get('processed/spring_constant', None),
303             k_s=data.get('processed/spring_constant_deviation', None),
304             bumps=data['bump'],
305             temperatures=data['temperature'], vibrations=data['vibration'],
306             maximum_relative_error=maximum_relative_error)
307         if changed and not dry_run:
308             if calibration_group is None:
309                 calibration_group = _h5_create_group(group, 'calibration')
310             save_results(
311                 group=calibration_group,
312                 spring_constant=k, spring_constant_deviation=k_s)
313     finally:
314         if f:
315             f.close()
316     if plot:
317         _plot(bumps=data['raw']['bump'],
318              temperatures=data['raw']['temperature'],
319              vibrations=data['raw']['vibration'])
320     return (k, k_s)
321
322 def check_bump(index, bump, maximum_relative_error, **kwargs):
323     changed = False
324     sensitivity = _bump_analyze(
325         config=bump['config']['bump'], data=bump['raw'], **kwargs)
326     if bump.get('processed', None) is None:
327         changed = True            
328         _LOG.warn('new analysis for bump {}: {}'.format(index, sensitivity))
329     else:
330         rel_error = abs(sensitivity - bump['processed'])/bump['processed']
331         if rel_error > maximum_relative_error:
332             changed = True
333             _LOG.warn(("new analysis doesn't match for bump {}: {} -> {} "
334                        "(difference: {}, relative error: {})").format(
335                     index, bump['processed'], sensitivity,
336                     sensitivity-bump['processed'], rel_error))
337     return (sensitivity, changed)
338
339 def check_temperature(index, temperature, maximum_relative_error, **kwargs):
340     changed = False
341     temp = _temperature_analyze(
342         config=temperature['config']['temperature'],
343         temperature=temperature['raw'], **kwargs)
344     if temperature.get('processed', None) is None:
345         changed = True            
346         _LOG.warn('new analysis for temperature {}: {}'.format(index, temp))
347     else:
348         rel_error = abs(temp - temperature['processed']
349                         )/temperature['processed']
350         if rel_error > maximum_relative_error:
351             changed = True
352             _LOG.warn(("new analysis doesn't match for temperature "
353                        "{} -> {} (difference: {}, relative error: {})"
354                        ).format(
355                     index, temperature['processed'], temp,
356                     temp-temperature['processed'], rel_error))
357     return (temp, changed)
358
359 def check_vibration(index, vibration, maximum_relative_error, **kwargs):
360     changed = False
361     variance = _vibration_analyze(
362         config=vibration['config']['vibration'],
363         deflection=vibration['raw'], **kwargs)
364     if vibration.get('processed', None) is None:
365         changed = True
366         _LOG.warn('new analysis for temperature {}: {}'.format(
367                 index, variance))
368     else:
369         rel_error = abs(variance-vibration['processed'])/vibration['processed']
370         if rel_error > maximum_relative_error:
371             _LOG.warn(("new analysis doesn't match for vibration {}: {} != {} "
372                        "(difference: {}, relative error: {})").format(
373                     index, variance, vibration['processed'],
374                     variance-vibration['processed'], rel_error))
375     return (variance, changed)
376
377 def check_calibration(k, k_s, maximum_relative_error, **kwargs):
378     changed = False
379     new_k,new_k_s = analyze(**kwargs)
380     if k is None:
381         changed = True
382         _LOG.warn('new analysis for the spring constant: {}'.format(new_k))
383     else:
384         rel_error = abs(new_k-k)/k
385         if rel_error > maximum_relative_error:
386             changed = True
387             _LOG.warn(("new analysis doesn't match for the spring constant: "
388                        "{} != {} (difference: {}, relative error: {})").format(
389                     new_k, k, new_k-k, rel_error))
390     if k_s is None:
391         changed = True
392         _LOG.warn('new analysis for the spring constant deviation: {}'.format(
393                 new_k_s))
394     else:
395         rel_error = abs(new_k_s-k_s)/k_s
396         if rel_error > maximum_relative_error:
397             changed = True
398             _LOG.warn(
399                 ("new analysis doesn't match for the spring constant deviation"
400                  ": {} != {} (difference: {}, relative error: {})").format(
401                     new_k_s, k_s, new_k_s-k_s, rel_error))
402     return (new_k, new_k_s, changed)