diff options
Diffstat (limited to 'silx/math/calibration.py')
-rw-r--r-- | silx/math/calibration.py | 178 |
1 files changed, 178 insertions, 0 deletions
diff --git a/silx/math/calibration.py b/silx/math/calibration.py new file mode 100644 index 0000000..2328bd7 --- /dev/null +++ b/silx/math/calibration.py @@ -0,0 +1,178 @@ +# coding: utf-8 +# /*########################################################################## +# Copyright (C) 2017 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ############################################################################*/ +""" +This module provides classes to calibrate data. + +Classes +------- + +- :class:`NoCalibration` +- :class:`LinearCalibration` +- :class:`ArrayCalibration` + +""" +import numpy + + +class AbstractCalibration(object): + """A calibration is a transformation to be applied to an axis (i.e. a 1D array). + + """ + def __init__(self): + super(AbstractCalibration, self).__init__() + + def __call__(self, x): + """Apply calibration to an axis or to a value. + + :param x: Axis (1-D array), or value""" + raise NotImplementedError( + "AbstractCalibration can not be used directly. " + + "You must subclass it and implement __call__") + + def is_affine(self): + """Returns True for an affine calibration of the form + :math:`x \mapsto a + b * x`, or else False. + """ + return False + + def get_slope(self): + raise NotImplementedError( + "get_slope is implemented only for affine calibrations") + + +class NoCalibration(AbstractCalibration): + """No calibration :math:`x \mapsto x` + """ + def __init__(self): + super(NoCalibration, self).__init__() + + def __call__(self, x): + return x + + def is_affine(self): + return True + + def get_slope(self): + return 1. + + +class LinearCalibration(AbstractCalibration): + """Linear calibration :math:`x \mapsto a + b x`, + where *a* is the y-intercept and *b* is the slope. + + :param y_intercept: y-intercept + :param slope: Slope of the affine transformation + """ + def __init__(self, y_intercept, slope): + super(LinearCalibration, self).__init__() + self.constant = y_intercept + self.slope = slope + + def __call__(self, x): + return self.constant + self.slope * x + + def is_affine(self): + return True + + def get_slope(self): + return self.slope + + +class ArrayCalibration(AbstractCalibration): + """One-to-one mapping calibration, defined by an array *x'*, + such as :math:`x \mapsto x'`*. + + This calibration can only be applied to x arrays of the same length as the + calibration array *x'*. + It is typically applied to an axis of indices or + channels (:math:`0, 1, ..., n-1`). + + :param x1: Calibration array""" + def __init__(self, x1): + super(ArrayCalibration, self).__init__() + if not isinstance(x1, (list, tuple)) and not hasattr(x1, "shape"): + raise TypeError( + "The calibration array must be a sequence (list, dataset, array)") + self.calibration_array = numpy.array(x1) + self._is_affine = None + + def __call__(self, x): + # calibrate the entire axis + if isinstance(x, (list, tuple, numpy.ndarray)): + assert len(self.calibration_array) == len(x) + return self.calibration_array + # calibrate one value, by index + if isinstance(x, int): + assert x < len(self.calibration_array) + return self.calibration_array[x] + raise ValueError("") + + def is_affine(self): + """If all values in the calibration array are regularly spaced, + return True.""" + if self._is_affine is None: + delta_x = self.calibration_array[1:] - self.calibration_array[:-1] + if not numpy.isclose(delta_x, delta_x[0]).all(): + self._is_affine = False + else: + self._is_affine = True + return self._is_affine + + def get_slope(self): + """If the calibration array is regularly spaced, return the spacing.""" + if not self.is_affine(): + raise AttributeError( + "get_slope only makes sense for affine transformations" + ) + return self.calibration_array[1] - self.calibration_array[0] + + +class FunctionCalibration(AbstractCalibration): + """Calibration defined by a function *f*, such as :math:`x \mapsto f(x)`*. + + :param function: Calibration function""" + def __init__(self, function, is_affine=False): + super(FunctionCalibration, self).__init__() + if not hasattr(function, "__call__"): + raise TypeError("The calibration function must be a callable") + self.function = function + self._is_affine = is_affine + + def __call__(self, x): + return self.function(x) + + def is_affine(self): + """Return True if calibration is affine. + This is False by default, unless the object is instantiated with + ``is_affine=True``.""" + return self._is_affine + + def get_slope(self): + """If the calibration array is regularly spaced, return the spacing.""" + if not self.is_affine(): + raise AttributeError( + "get_slope only makes sense for affine transformations" + ) + # fixme: what if function is not defined at x=1 or x=2? + return self.function(2) - self.function(1) |