summaryrefslogtreecommitdiff
path: root/src/silx/math/fit/fittheories.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/math/fit/fittheories.py')
-rw-r--r--src/silx/math/fit/fittheories.py1580
1 files changed, 1580 insertions, 0 deletions
diff --git a/src/silx/math/fit/fittheories.py b/src/silx/math/fit/fittheories.py
new file mode 100644
index 0000000..f20cbf1
--- /dev/null
+++ b/src/silx/math/fit/fittheories.py
@@ -0,0 +1,1580 @@
+# /*##########################################################################
+#
+# Copyright (c) 2004-2023 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 modules provides a set of fit functions and associated
+estimation functions in a format that can be imported into a
+:class:`silx.math.fit.FitManager` instance.
+
+These functions are well suited for fitting multiple gaussian shaped peaks
+typically found in spectroscopy data. The estimation functions are designed
+to detect how many peaks are present in the data, and provide an initial
+estimate for their height, their center location and their full-width
+at half maximum (fwhm).
+
+The limitation of these estimation algorithms is that only gaussians having a
+similar fwhm can be detected by the peak search algorithm.
+This *search fwhm* can be defined by the user, if
+he knows the characteristics of his data, or can be automatically estimated
+based on the fwhm of the largest peak in the data.
+
+The source code of this module can serve as template for defining your own
+fit functions.
+
+The functions to be imported by :meth:`FitManager.loadtheories` are defined by
+a dictionary :const:`THEORY`: with the following structure::
+
+ from silx.math.fit.fittheory import FitTheory
+
+ THEORY = {
+ 'theory_name_1': FitTheory(
+ description='Description of theory 1',
+ function=fitfunction1,
+ parameters=('param name 1', 'param name 2', …),
+ estimate=estimation_function1,
+ configure=configuration_function1,
+ derivative=derivative_function1),
+
+ 'theory_name_2': FitTheory(…),
+ }
+
+.. note::
+
+ The order of the provided dictionary is taken into account.
+
+Theory names can be customized (e.g. ``gauss, lorentz, splitgauss``…).
+
+The mandatory parameters for :class:`FitTheory` are ``function`` and
+``parameters``.
+
+You can also define an ``INIT`` function that will be executed by
+:meth:`FitManager.loadtheories`.
+
+See the documentation of :class:`silx.math.fit.fittheory.FitTheory`
+for more information.
+
+Module members:
+---------------
+"""
+import numpy
+import logging
+
+from silx.math.fit import functions
+from silx.math.fit.peaks import peak_search, guess_fwhm
+from silx.math.fit.filters import strip, savitsky_golay
+from silx.math.fit.leastsq import leastsq
+from silx.math.fit.fittheory import FitTheory
+
+_logger = logging.getLogger(__name__)
+
+__authors__ = ["V.A. Sole", "P. Knobel"]
+__license__ = "MIT"
+__date__ = "15/05/2017"
+
+
+DEFAULT_CONFIG = {
+ "NoConstraintsFlag": False,
+ "PositiveFwhmFlag": True,
+ "PositiveHeightAreaFlag": True,
+ "SameFwhmFlag": False,
+ "QuotedPositionFlag": False, # peak not outside data range
+ "QuotedEtaFlag": False, # force 0 < eta < 1
+ # Peak detection
+ "AutoScaling": False,
+ "Yscaling": 1.0,
+ "FwhmPoints": 8,
+ "AutoFwhm": True,
+ "Sensitivity": 2.5,
+ "ForcePeakPresence": True,
+ # Hypermet
+ "HypermetTails": 15,
+ "QuotedFwhmFlag": 0,
+ "MaxFwhm2InputRatio": 1.5,
+ "MinFwhm2InputRatio": 0.4,
+ # short tail parameters
+ "MinGaussArea4ShortTail": 50000.0,
+ "InitialShortTailAreaRatio": 0.050,
+ "MaxShortTailAreaRatio": 0.100,
+ "MinShortTailAreaRatio": 0.0010,
+ "InitialShortTailSlopeRatio": 0.70,
+ "MaxShortTailSlopeRatio": 2.00,
+ "MinShortTailSlopeRatio": 0.50,
+ # long tail parameters
+ "MinGaussArea4LongTail": 1000.0,
+ "InitialLongTailAreaRatio": 0.050,
+ "MaxLongTailAreaRatio": 0.300,
+ "MinLongTailAreaRatio": 0.010,
+ "InitialLongTailSlopeRatio": 20.0,
+ "MaxLongTailSlopeRatio": 50.0,
+ "MinLongTailSlopeRatio": 5.0,
+ # step tail
+ "MinGaussHeight4StepTail": 5000.0,
+ "InitialStepTailHeightRatio": 0.002,
+ "MaxStepTailHeightRatio": 0.0100,
+ "MinStepTailHeightRatio": 0.0001,
+ # Hypermet constraints
+ # position in range [estimated position +- estimated fwhm/2]
+ "HypermetQuotedPositionFlag": True,
+ "DeltaPositionFwhmUnits": 0.5,
+ "SameSlopeRatioFlag": 1,
+ "SameAreaRatioFlag": 1,
+ # Strip bg removal
+ "StripBackgroundFlag": True,
+ "SmoothingFlag": True,
+ "SmoothingWidth": 5,
+ "StripWidth": 2,
+ "StripIterations": 5000,
+ "StripThresholdFactor": 1.0,
+}
+"""This dictionary defines default configuration parameters that have effects
+on fit functions and estimation functions, mainly on fit constraints.
+This dictionary is accessible as attribute :attr:`FitTheories.config`,
+which can be modified by configuration functions defined in
+:const:`CONFIGURE`.
+"""
+
+CFREE = 0
+CPOSITIVE = 1
+CQUOTED = 2
+CFIXED = 3
+CFACTOR = 4
+CDELTA = 5
+CSUM = 6
+CIGNORED = 7
+
+
+class FitTheories(object):
+ """Class wrapping functions from :class:`silx.math.fit.functions`
+ and providing estimate functions for all of these fit functions."""
+
+ def __init__(self, config=None):
+ if config is None:
+ self.config = DEFAULT_CONFIG
+ else:
+ self.config = config
+
+ def ahypermet(self, x, *pars):
+ """
+ Wrapping of :func:`silx.math.fit.functions.sum_ahypermet` without
+ the tail flags in the function signature.
+
+ Depending on the value of `self.config['HypermetTails']`, one can
+ activate or deactivate the various terms of the hypermet function.
+
+ `self.config['HypermetTails']` must be an integer between 0 and 15.
+ It is a set of 4 binary flags, one for activating each one of the
+ hypermet terms: *gaussian function, short tail, long tail, step*.
+
+ For example, 15 can be expressed as ``1111`` in base 2, so a flag of
+ 15 means all terms are active.
+ """
+ g_term = self.config["HypermetTails"] & 1
+ st_term = (self.config["HypermetTails"] >> 1) & 1
+ lt_term = (self.config["HypermetTails"] >> 2) & 1
+ step_term = (self.config["HypermetTails"] >> 3) & 1
+ return functions.sum_ahypermet(
+ x,
+ *pars,
+ gaussian_term=g_term,
+ st_term=st_term,
+ lt_term=lt_term,
+ step_term=step_term,
+ )
+
+ def poly(self, x, *pars):
+ """Order n polynomial.
+ The order of the polynomial is defined by the number of
+ coefficients (``*pars``).
+
+ """
+ p = numpy.poly1d(pars)
+ return p(x)
+
+ @staticmethod
+ def estimate_poly(x, y, n=2):
+ """Estimate polynomial coefficients for a degree n polynomial."""
+ pcoeffs = numpy.polyfit(x, y, n)
+ constraints = numpy.zeros((n + 1, 3), numpy.float64)
+ return pcoeffs, constraints
+
+ def estimate_quadratic(self, x, y):
+ """Estimate quadratic coefficients"""
+ return self.estimate_poly(x, y, n=2)
+
+ def estimate_cubic(self, x, y):
+ """Estimate coefficients for a degree 3 polynomial"""
+ return self.estimate_poly(x, y, n=3)
+
+ def estimate_quartic(self, x, y):
+ """Estimate coefficients for a degree 4 polynomial"""
+ return self.estimate_poly(x, y, n=4)
+
+ def estimate_quintic(self, x, y):
+ """Estimate coefficients for a degree 5 polynomial"""
+ return self.estimate_poly(x, y, n=5)
+
+ def strip_bg(self, y):
+ """Return the strip background of y, using parameters from
+ :attr:`config` dictionary (*StripBackgroundFlag, StripWidth,
+ StripIterations, StripThresholdFactor*)"""
+ remove_strip_bg = self.config.get("StripBackgroundFlag", False)
+ if remove_strip_bg:
+ if self.config["SmoothingFlag"]:
+ y = savitsky_golay(y, self.config["SmoothingWidth"])
+ strip_width = self.config["StripWidth"]
+ strip_niterations = self.config["StripIterations"]
+ strip_thr_factor = self.config["StripThresholdFactor"]
+ return strip(
+ y, w=strip_width, niterations=strip_niterations, factor=strip_thr_factor
+ )
+ else:
+ return numpy.zeros_like(y)
+
+ def guess_yscaling(self, y):
+ """Estimate scaling for y prior to peak search.
+ A smoothing filter is applied to y to estimate the noise level
+ (chi-squared)
+
+ :param y: Data array
+ :return: Scaling factor
+ """
+ # ensure y is an array
+ yy = numpy.array(y, copy=False)
+
+ # smooth
+ convolution_kernel = numpy.ones(shape=(3,)) / 3.0
+ ysmooth = numpy.convolve(y, convolution_kernel, mode="same")
+
+ # remove zeros
+ idx_array = numpy.fabs(y) > 0.0
+ yy = yy[idx_array]
+ ysmooth = ysmooth[idx_array]
+
+ # compute scaling factor
+ chisq = numpy.mean((yy - ysmooth) ** 2 / numpy.fabs(yy))
+ if chisq > 0:
+ return 1.0 / chisq
+ else:
+ return 1.0
+
+ def peak_search(self, y, fwhm, sensitivity):
+ """Search for peaks in y array, after padding the array and
+ multiplying its value by a scaling factor.
+
+ :param y: 1-D data array
+ :param int fwhm: Typical full width at half maximum for peaks,
+ in number of points. This parameter is used for to discriminate between
+ true peaks and background fluctuations.
+ :param float sensitivity: Sensitivity parameter. This is a threshold factor
+ for peak detection. Only peaks larger than the standard deviation
+ of the noise multiplied by this sensitivity parameter are detected.
+ :return: List of peak indices
+ """
+ # add padding
+ ysearch = numpy.ones((len(y) + 2 * fwhm,), numpy.float64)
+ ysearch[0:fwhm] = y[0]
+ ysearch[-1 : -fwhm - 1 : -1] = y[len(y) - 1]
+ ysearch[fwhm : fwhm + len(y)] = y[:]
+
+ scaling = (
+ self.guess_yscaling(y)
+ if self.config["AutoScaling"]
+ else self.config["Yscaling"]
+ )
+
+ if len(ysearch) > 1.5 * fwhm:
+ peaks = peak_search(scaling * ysearch, fwhm=fwhm, sensitivity=sensitivity)
+ return [
+ peak_index - fwhm
+ for peak_index in peaks
+ if 0 <= peak_index - fwhm < len(y)
+ ]
+ else:
+ return []
+
+ def estimate_height_position_fwhm(self, x, y):
+ """Estimation of *Height, Position, FWHM* of peaks, for gaussian-like
+ curves.
+
+ This functions finds how many parameters are needed, based on the
+ number of peaks detected. Then it estimates the fit parameters
+ with a few iterations of fitting gaussian functions.
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each peak are:
+ *Height, Position, FWHM*.
+ Fit constraints depend on :attr:`config`.
+ """
+ fittedpar = []
+
+ bg = self.strip_bg(y)
+
+ if self.config["AutoFwhm"]:
+ search_fwhm = guess_fwhm(y)
+ else:
+ search_fwhm = int(float(self.config["FwhmPoints"]))
+ search_sens = float(self.config["Sensitivity"])
+
+ if search_fwhm < 3:
+ _logger.warning("Setting peak fwhm to 3 (lower limit)")
+ search_fwhm = 3
+ self.config["FwhmPoints"] = 3
+
+ if search_sens < 1:
+ _logger.warning(
+ "Setting peak search sensitivity to 1. "
+ + "(lower limit to filter out noise peaks)"
+ )
+ search_sens = 1
+ self.config["Sensitivity"] = 1
+
+ npoints = len(y)
+
+ # Find indices of peaks in data array
+ peaks = self.peak_search(y, fwhm=search_fwhm, sensitivity=search_sens)
+
+ if not len(peaks):
+ forcepeak = int(float(self.config.get("ForcePeakPresence", 0)))
+ if forcepeak:
+ delta = y - bg
+ # get index of global maximum
+ # (first one if several samples are equal to this value)
+ peaks = [numpy.nonzero(delta == delta.max())[0][0]]
+
+ # Find index of largest peak in peaks array
+ index_largest_peak = 0
+ if len(peaks) > 0:
+ # estimate fwhm as 5 * sampling interval
+ sig = 5 * abs(x[npoints - 1] - x[0]) / npoints
+ peakpos = x[int(peaks[0])]
+ if abs(peakpos) < 1.0e-16:
+ peakpos = 0.0
+ param = numpy.array([y[int(peaks[0])] - bg[int(peaks[0])], peakpos, sig])
+ height_largest_peak = param[0]
+ peak_index = 1
+ for i in peaks[1:]:
+ param2 = numpy.array([y[int(i)] - bg[int(i)], x[int(i)], sig])
+ param = numpy.concatenate((param, param2))
+ if param2[0] > height_largest_peak:
+ height_largest_peak = param2[0]
+ index_largest_peak = peak_index
+ peak_index += 1
+
+ # Subtract background
+ xw = x
+ yw = y - bg
+
+ cons = numpy.zeros((len(param), 3), numpy.float64)
+
+ # peak height must be positive
+ cons[0 : len(param) : 3, 0] = CPOSITIVE
+ # force peaks to stay around their position
+ cons[1 : len(param) : 3, 0] = CQUOTED
+
+ # set possible peak range to estimated peak +- guessed fwhm
+ if len(xw) > search_fwhm:
+ fwhmx = numpy.fabs(xw[int(search_fwhm)] - xw[0])
+ cons[1 : len(param) : 3, 1] = param[1 : len(param) : 3] - 0.5 * fwhmx
+ cons[1 : len(param) : 3, 2] = param[1 : len(param) : 3] + 0.5 * fwhmx
+ else:
+ shape = [max(1, int(x)) for x in (param[1 : len(param) : 3])]
+ cons[1 : len(param) : 3, 1] = min(xw) * numpy.ones(shape, numpy.float64)
+ cons[1 : len(param) : 3, 2] = max(xw) * numpy.ones(shape, numpy.float64)
+
+ # ensure fwhm is positive
+ cons[2 : len(param) : 3, 0] = CPOSITIVE
+
+ # run a quick iterative fit (4 iterations) to improve
+ # estimations
+ fittedpar, _, _ = leastsq(
+ functions.sum_gauss,
+ xw,
+ yw,
+ param,
+ max_iter=4,
+ constraints=cons.tolist(),
+ full_output=True,
+ )
+
+ # set final constraints based on config parameters
+ cons = numpy.zeros((len(fittedpar), 3), numpy.float64)
+ peak_index = 0
+ for i in range(len(peaks)):
+ # Setup height area constrains
+ if not self.config["NoConstraintsFlag"]:
+ if self.config["PositiveHeightAreaFlag"]:
+ cons[peak_index, 0] = CPOSITIVE
+ cons[peak_index, 1] = 0
+ cons[peak_index, 2] = 0
+ peak_index += 1
+
+ # Setup position constrains
+ if not self.config["NoConstraintsFlag"]:
+ if self.config["QuotedPositionFlag"]:
+ cons[peak_index, 0] = CQUOTED
+ cons[peak_index, 1] = min(x)
+ cons[peak_index, 2] = max(x)
+ peak_index += 1
+
+ # Setup positive FWHM constrains
+ if not self.config["NoConstraintsFlag"]:
+ if self.config["PositiveFwhmFlag"]:
+ cons[peak_index, 0] = CPOSITIVE
+ cons[peak_index, 1] = 0
+ cons[peak_index, 2] = 0
+ if self.config["SameFwhmFlag"]:
+ if i != index_largest_peak:
+ cons[peak_index, 0] = CFACTOR
+ cons[peak_index, 1] = 3 * index_largest_peak + 2
+ cons[peak_index, 2] = 1.0
+ peak_index += 1
+
+ return fittedpar, cons
+
+ def estimate_agauss(self, x, y):
+ """Estimation of *Area, Position, FWHM* of peaks, for gaussian-like
+ curves.
+
+ This functions uses :meth:`estimate_height_position_fwhm`, then
+ converts the height parameters to area under the curve with the
+ formula ``area = sqrt(2*pi) * height * fwhm / (2 * sqrt(2 * log(2))``
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each peak are:
+ *Area, Position, FWHM*.
+ Fit constraints depend on :attr:`config`.
+ """
+ fittedpar, cons = self.estimate_height_position_fwhm(x, y)
+ # get the number of found peaks
+ npeaks = len(fittedpar) // 3
+ for i in range(npeaks):
+ height = fittedpar[3 * i]
+ fwhm = fittedpar[3 * i + 2]
+ # Replace height with area in fittedpar
+ fittedpar[3 * i] = (
+ numpy.sqrt(2 * numpy.pi)
+ * height
+ * fwhm
+ / (2.0 * numpy.sqrt(2 * numpy.log(2)))
+ )
+ return fittedpar, cons
+
+ def estimate_alorentz(self, x, y):
+ """Estimation of *Area, Position, FWHM* of peaks, for Lorentzian
+ curves.
+
+ This functions uses :meth:`estimate_height_position_fwhm`, then
+ converts the height parameters to area under the curve with the
+ formula ``area = height * fwhm * 0.5 * pi``
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each peak are:
+ *Area, Position, FWHM*.
+ Fit constraints depend on :attr:`config`.
+ """
+ fittedpar, cons = self.estimate_height_position_fwhm(x, y)
+ # get the number of found peaks
+ npeaks = len(fittedpar) // 3
+ for i in range(npeaks):
+ height = fittedpar[3 * i]
+ fwhm = fittedpar[3 * i + 2]
+ # Replace height with area in fittedpar
+ fittedpar[3 * i] = height * fwhm * 0.5 * numpy.pi
+ return fittedpar, cons
+
+ def estimate_splitgauss(self, x, y):
+ """Estimation of *Height, Position, FWHM1, FWHM2* of peaks, for
+ asymmetric gaussian-like curves.
+
+ This functions uses :meth:`estimate_height_position_fwhm`, then
+ adds a second (identical) estimation of FWHM to the fit parameters
+ for each peak, and the corresponding constraint.
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each peak are:
+ *Height, Position, FWHM1, FWHM2*.
+ Fit constraints depend on :attr:`config`.
+ """
+ fittedpar, cons = self.estimate_height_position_fwhm(x, y)
+ # get the number of found peaks
+ npeaks = len(fittedpar) // 3
+ estimated_parameters = []
+ estimated_constraints = numpy.zeros((4 * npeaks, 3), numpy.float64)
+ for i in range(npeaks):
+ for j in range(3):
+ estimated_parameters.append(fittedpar[3 * i + j])
+ # fwhm2 estimate = fwhm1
+ estimated_parameters.append(fittedpar[3 * i + 2])
+ # height
+ estimated_constraints[4 * i, 0] = cons[3 * i, 0]
+ estimated_constraints[4 * i, 1] = cons[3 * i, 1]
+ estimated_constraints[4 * i, 2] = cons[3 * i, 2]
+ # position
+ estimated_constraints[4 * i + 1, 0] = cons[3 * i + 1, 0]
+ estimated_constraints[4 * i + 1, 1] = cons[3 * i + 1, 1]
+ estimated_constraints[4 * i + 1, 2] = cons[3 * i + 1, 2]
+ # fwhm1
+ estimated_constraints[4 * i + 2, 0] = cons[3 * i + 2, 0]
+ estimated_constraints[4 * i + 2, 1] = cons[3 * i + 2, 1]
+ estimated_constraints[4 * i + 2, 2] = cons[3 * i + 2, 2]
+ # fwhm2
+ estimated_constraints[4 * i + 3, 0] = cons[3 * i + 2, 0]
+ estimated_constraints[4 * i + 3, 1] = cons[3 * i + 2, 1]
+ estimated_constraints[4 * i + 3, 2] = cons[3 * i + 2, 2]
+ if cons[3 * i + 2, 0] == CFACTOR:
+ # convert indices of related parameters
+ # (this happens if SameFwhmFlag == True)
+ estimated_constraints[4 * i + 2, 1] = (
+ int(cons[3 * i + 2, 1] / 3) * 4 + 2
+ )
+ estimated_constraints[4 * i + 3, 1] = (
+ int(cons[3 * i + 2, 1] / 3) * 4 + 3
+ )
+ return estimated_parameters, estimated_constraints
+
+ def estimate_pvoigt(self, x, y):
+ """Estimation of *Height, Position, FWHM, eta* of peaks, for
+ pseudo-Voigt curves.
+
+ Pseudo-Voigt are a sum of a gaussian curve *G(x)* and a lorentzian
+ curve *L(x)* with the same height, center, fwhm parameters:
+ ``y(x) = eta * G(x) + (1-eta) * L(x)``
+
+ This functions uses :meth:`estimate_height_position_fwhm`, then
+ adds a constant estimation of *eta* (0.5) to the fit parameters
+ for each peak, and the corresponding constraint.
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each peak are:
+ *Height, Position, FWHM, eta*.
+ Constraint for the eta parameter can be set to QUOTED (0.--1.)
+ by setting :attr:`config`['QuotedEtaFlag'] to ``True``.
+ If this is not the case, the constraint code is set to FREE.
+ """
+ fittedpar, cons = self.estimate_height_position_fwhm(x, y)
+ npeaks = len(fittedpar) // 3
+ newpar = []
+ newcons = numpy.zeros((4 * npeaks, 3), numpy.float64)
+ # find out related parameters proper index
+ if not self.config["NoConstraintsFlag"]:
+ if self.config["SameFwhmFlag"]:
+ j = 0
+ # get the index of the free FWHM
+ for i in range(npeaks):
+ if cons[3 * i + 2, 0] != 4:
+ j = i
+ for i in range(npeaks):
+ if i != j:
+ cons[3 * i + 2, 1] = 4 * j + 2
+ for i in range(npeaks):
+ newpar.append(fittedpar[3 * i])
+ newpar.append(fittedpar[3 * i + 1])
+ newpar.append(fittedpar[3 * i + 2])
+ newpar.append(0.5)
+ # height
+ newcons[4 * i, 0] = cons[3 * i, 0]
+ newcons[4 * i, 1] = cons[3 * i, 1]
+ newcons[4 * i, 2] = cons[3 * i, 2]
+ # position
+ newcons[4 * i + 1, 0] = cons[3 * i + 1, 0]
+ newcons[4 * i + 1, 1] = cons[3 * i + 1, 1]
+ newcons[4 * i + 1, 2] = cons[3 * i + 1, 2]
+ # fwhm
+ newcons[4 * i + 2, 0] = cons[3 * i + 2, 0]
+ newcons[4 * i + 2, 1] = cons[3 * i + 2, 1]
+ newcons[4 * i + 2, 2] = cons[3 * i + 2, 2]
+ # Eta constrains
+ newcons[4 * i + 3, 0] = CFREE
+ newcons[4 * i + 3, 1] = 0
+ newcons[4 * i + 3, 2] = 0
+ if self.config["QuotedEtaFlag"]:
+ newcons[4 * i + 3, 0] = CQUOTED
+ newcons[4 * i + 3, 1] = 0.0
+ newcons[4 * i + 3, 2] = 1.0
+ return newpar, newcons
+
+ def estimate_splitpvoigt(self, x, y):
+ """Estimation of *Height, Position, FWHM1, FWHM2, eta* of peaks, for
+ asymmetric pseudo-Voigt curves.
+
+ This functions uses :meth:`estimate_height_position_fwhm`, then
+ adds an identical FWHM2 parameter and a constant estimation of
+ *eta* (0.5) to the fit parameters for each peak, and the corresponding
+ constraints.
+
+ Constraint for the eta parameter can be set to QUOTED (0.--1.)
+ by setting :attr:`config`['QuotedEtaFlag'] to ``True``.
+ If this is not the case, the constraint code is set to FREE.
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each peak are:
+ *Height, Position, FWHM1, FWHM2, eta*.
+ """
+ fittedpar, cons = self.estimate_height_position_fwhm(x, y)
+ npeaks = len(fittedpar) // 3
+ newpar = []
+ newcons = numpy.zeros((5 * npeaks, 3), numpy.float64)
+ # find out related parameters proper index
+ if not self.config["NoConstraintsFlag"]:
+ if self.config["SameFwhmFlag"]:
+ j = 0
+ # get the index of the free FWHM
+ for i in range(npeaks):
+ if cons[3 * i + 2, 0] != 4:
+ j = i
+ for i in range(npeaks):
+ if i != j:
+ cons[3 * i + 2, 1] = 4 * j + 2
+ for i in range(npeaks):
+ # height
+ newpar.append(fittedpar[3 * i])
+ # position
+ newpar.append(fittedpar[3 * i + 1])
+ # fwhm1
+ newpar.append(fittedpar[3 * i + 2])
+ # fwhm2 estimate equal to fwhm1
+ newpar.append(fittedpar[3 * i + 2])
+ # eta
+ newpar.append(0.5)
+ # constraint codes
+ # ----------------
+ # height
+ newcons[5 * i, 0] = cons[3 * i, 0]
+ # position
+ newcons[5 * i + 1, 0] = cons[3 * i + 1, 0]
+ # fwhm1
+ newcons[5 * i + 2, 0] = cons[3 * i + 2, 0]
+ # fwhm2
+ newcons[5 * i + 3, 0] = cons[3 * i + 2, 0]
+ # cons 1
+ # ------
+ newcons[5 * i, 1] = cons[3 * i, 1]
+ newcons[5 * i + 1, 1] = cons[3 * i + 1, 1]
+ newcons[5 * i + 2, 1] = cons[3 * i + 2, 1]
+ newcons[5 * i + 3, 1] = cons[3 * i + 2, 1]
+ # cons 2
+ # ------
+ newcons[5 * i, 2] = cons[3 * i, 2]
+ newcons[5 * i + 1, 2] = cons[3 * i + 1, 2]
+ newcons[5 * i + 2, 2] = cons[3 * i + 2, 2]
+ newcons[5 * i + 3, 2] = cons[3 * i + 2, 2]
+
+ if cons[3 * i + 2, 0] == CFACTOR:
+ # fwhm2 connstraint depends on fwhm1
+ newcons[5 * i + 3, 1] = newcons[5 * i + 2, 1] + 1
+ # eta constraints
+ newcons[5 * i + 4, 0] = CFREE
+ newcons[5 * i + 4, 1] = 0
+ newcons[5 * i + 4, 2] = 0
+ if self.config["QuotedEtaFlag"]:
+ newcons[5 * i + 4, 0] = CQUOTED
+ newcons[5 * i + 4, 1] = 0.0
+ newcons[5 * i + 4, 2] = 1.0
+ return newpar, newcons
+
+ def estimate_splitpvoigt2(self, x, y):
+ """Estimation of *Height, Position, FWHM1, FWHM2, eta1, eta2* of peaks, for
+ asymmetric pseudo-Voigt curves.
+
+ This functions uses :meth:`estimate_height_position_fwhm`, then
+ adds an identical FWHM2 parameter and a constant estimation of
+ *eta1* and *eta2* (0.5) to the fit parameters for each peak, and the corresponding
+ constraints.
+
+ Constraint for the eta parameter can be set to QUOTED (0.--1.)
+ by setting :attr:`config`['QuotedEtaFlag'] to ``True``.
+ If this is not the case, the constraint code is set to FREE.
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each peak are:
+ *Height, Position, FWHM1, FWHM2, eta1, eta2*.
+ """
+ fittedpar, cons = self.estimate_height_position_fwhm(x, y)
+ npeaks = len(fittedpar) // 3
+ newpar = []
+ newcons = numpy.zeros((5 * npeaks, 3), numpy.float64)
+ # find out related parameters proper index
+ if not self.config["NoConstraintsFlag"]:
+ if self.config["SameFwhmFlag"]:
+ j = 0
+ # get the index of the free FWHM
+ for i in range(npeaks):
+ if cons[3 * i + 2, 0] != 4:
+ j = i
+ for i in range(npeaks):
+ if i != j:
+ cons[3 * i + 2, 1] = 4 * j + 2
+ for i in range(npeaks):
+ # height
+ newpar.append(fittedpar[3 * i])
+ # position
+ newpar.append(fittedpar[3 * i + 1])
+ # fwhm1
+ newpar.append(fittedpar[3 * i + 2])
+ # fwhm2 estimate equal to fwhm1
+ newpar.append(fittedpar[3 * i + 2])
+ # eta1
+ newpar.append(0.5)
+ # eta2
+ newpar.append(0.5)
+ # constraint codes
+ # ----------------
+ # height
+ newcons[6 * i, 0] = cons[3 * i, 0]
+ # position
+ newcons[6 * i + 1, 0] = cons[3 * i + 1, 0]
+ # fwhm1
+ newcons[6 * i + 2, 0] = cons[3 * i + 2, 0]
+ # fwhm2
+ newcons[6 * i + 3, 0] = cons[3 * i + 2, 0]
+ # cons 1
+ # ------
+ newcons[6 * i, 1] = cons[3 * i, 1]
+ newcons[6 * i + 1, 1] = cons[3 * i + 1, 1]
+ newcons[6 * i + 2, 1] = cons[3 * i + 2, 1]
+ newcons[6 * i + 3, 1] = cons[3 * i + 2, 1]
+ # cons 2
+ # ------
+ newcons[6 * i, 2] = cons[3 * i, 2]
+ newcons[6 * i + 1, 2] = cons[3 * i + 1, 2]
+ newcons[6 * i + 2, 2] = cons[3 * i + 2, 2]
+ newcons[6 * i + 3, 2] = cons[3 * i + 2, 2]
+
+ if cons[3 * i + 2, 0] == CFACTOR:
+ # fwhm2 constraint depends on fwhm1
+ newcons[6 * i + 3, 1] = newcons[6 * i + 2, 1] + 1
+ # eta1 constraints
+ newcons[6 * i + 4, 0] = CFREE
+ newcons[6 * i + 4, 1] = 0
+ newcons[6 * i + 4, 2] = 0
+ if self.config["QuotedEtaFlag"]:
+ newcons[6 * i + 4, 0] = CQUOTED
+ newcons[6 * i + 4, 1] = 0.0
+ newcons[6 * i + 4, 2] = 1.0
+ # eta2 constraints
+ newcons[6 * i + 5, 0] = CFREE
+ newcons[6 * i + 5, 1] = 0
+ newcons[6 * i + 5, 2] = 0
+ if self.config["QuotedEtaFlag"]:
+ newcons[6 * i + 5, 0] = CQUOTED
+ newcons[6 * i + 5, 1] = 0.0
+ newcons[6 * i + 5, 2] = 1.0
+ return newpar, newcons
+
+ def estimate_apvoigt(self, x, y):
+ """Estimation of *Area, Position, FWHM1, eta* of peaks, for
+ pseudo-Voigt curves.
+
+ This functions uses :meth:`estimate_pvoigt`, then converts the height
+ parameter to area.
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each peak are:
+ *Area, Position, FWHM, eta*.
+ """
+ fittedpar, cons = self.estimate_pvoigt(x, y)
+ npeaks = len(fittedpar) // 4
+ # Assume 50% of the area is determined by the gaussian and 50% by
+ # the Lorentzian.
+ for i in range(npeaks):
+ height = fittedpar[4 * i]
+ fwhm = fittedpar[4 * i + 2]
+ fittedpar[4 * i] = 0.5 * (height * fwhm * 0.5 * numpy.pi) + 0.5 * (
+ height * fwhm / (2.0 * numpy.sqrt(2 * numpy.log(2)))
+ ) * numpy.sqrt(2 * numpy.pi)
+ return fittedpar, cons
+
+ def estimate_ahypermet(self, x, y):
+ """Estimation of *area, position, fwhm, st_area_r, st_slope_r,
+ lt_area_r, lt_slope_r, step_height_r* of peaks, for hypermet curves.
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each peak are:
+ *area, position, fwhm, st_area_r, st_slope_r,
+ lt_area_r, lt_slope_r, step_height_r* .
+ """
+ yscaling = self.config.get("Yscaling", 1.0)
+ if yscaling == 0:
+ yscaling = 1.0
+ fittedpar, cons = self.estimate_height_position_fwhm(x, y)
+ npeaks = len(fittedpar) // 3
+ newpar = []
+ newcons = numpy.zeros((8 * npeaks, 3), numpy.float64)
+ main_peak = 0
+ # find out related parameters proper index
+ if not self.config["NoConstraintsFlag"]:
+ if self.config["SameFwhmFlag"]:
+ j = 0
+ # get the index of the free FWHM
+ for i in range(npeaks):
+ if cons[3 * i + 2, 0] != 4:
+ j = i
+ for i in range(npeaks):
+ if i != j:
+ cons[3 * i + 2, 1] = 8 * j + 2
+ main_peak = j
+ for i in range(npeaks):
+ if fittedpar[3 * i] > fittedpar[3 * main_peak]:
+ main_peak = i
+
+ for i in range(npeaks):
+ height = fittedpar[3 * i]
+ position = fittedpar[3 * i + 1]
+ fwhm = fittedpar[3 * i + 2]
+ area = (height * fwhm / (2.0 * numpy.sqrt(2 * numpy.log(2)))) * numpy.sqrt(
+ 2 * numpy.pi
+ )
+ # the gaussian parameters
+ newpar.append(area)
+ newpar.append(position)
+ newpar.append(fwhm)
+ # print "area, pos , fwhm = ",area,position,fwhm
+ # Avoid zero derivatives because of not calculating contribution
+ g_term = 1
+ st_term = 1
+ lt_term = 1
+ step_term = 1
+ if self.config["HypermetTails"] != 0:
+ g_term = self.config["HypermetTails"] & 1
+ st_term = (self.config["HypermetTails"] >> 1) & 1
+ lt_term = (self.config["HypermetTails"] >> 2) & 1
+ step_term = (self.config["HypermetTails"] >> 3) & 1
+ if g_term == 0:
+ # fix the gaussian parameters
+ newcons[8 * i, 0] = CFIXED
+ newcons[8 * i + 1, 0] = CFIXED
+ newcons[8 * i + 2, 0] = CFIXED
+ # the short tail parameters
+ if ((area * yscaling) < self.config["MinGaussArea4ShortTail"]) | (
+ st_term == 0
+ ):
+ newpar.append(0.0)
+ newpar.append(0.0)
+ newcons[8 * i + 3, 0] = CFIXED
+ newcons[8 * i + 3, 1] = 0.0
+ newcons[8 * i + 3, 2] = 0.0
+ newcons[8 * i + 4, 0] = CFIXED
+ newcons[8 * i + 4, 1] = 0.0
+ newcons[8 * i + 4, 2] = 0.0
+ else:
+ newpar.append(self.config["InitialShortTailAreaRatio"])
+ newpar.append(self.config["InitialShortTailSlopeRatio"])
+ newcons[8 * i + 3, 0] = CQUOTED
+ newcons[8 * i + 3, 1] = self.config["MinShortTailAreaRatio"]
+ newcons[8 * i + 3, 2] = self.config["MaxShortTailAreaRatio"]
+ newcons[8 * i + 4, 0] = CQUOTED
+ newcons[8 * i + 4, 1] = self.config["MinShortTailSlopeRatio"]
+ newcons[8 * i + 4, 2] = self.config["MaxShortTailSlopeRatio"]
+ # the long tail parameters
+ if ((area * yscaling) < self.config["MinGaussArea4LongTail"]) | (
+ lt_term == 0
+ ):
+ newpar.append(0.0)
+ newpar.append(0.0)
+ newcons[8 * i + 5, 0] = CFIXED
+ newcons[8 * i + 5, 1] = 0.0
+ newcons[8 * i + 5, 2] = 0.0
+ newcons[8 * i + 6, 0] = CFIXED
+ newcons[8 * i + 6, 1] = 0.0
+ newcons[8 * i + 6, 2] = 0.0
+ else:
+ newpar.append(self.config["InitialLongTailAreaRatio"])
+ newpar.append(self.config["InitialLongTailSlopeRatio"])
+ newcons[8 * i + 5, 0] = CQUOTED
+ newcons[8 * i + 5, 1] = self.config["MinLongTailAreaRatio"]
+ newcons[8 * i + 5, 2] = self.config["MaxLongTailAreaRatio"]
+ newcons[8 * i + 6, 0] = CQUOTED
+ newcons[8 * i + 6, 1] = self.config["MinLongTailSlopeRatio"]
+ newcons[8 * i + 6, 2] = self.config["MaxLongTailSlopeRatio"]
+ # the step parameters
+ if ((height * yscaling) < self.config["MinGaussHeight4StepTail"]) | (
+ step_term == 0
+ ):
+ newpar.append(0.0)
+ newcons[8 * i + 7, 0] = CFIXED
+ newcons[8 * i + 7, 1] = 0.0
+ newcons[8 * i + 7, 2] = 0.0
+ else:
+ newpar.append(self.config["InitialStepTailHeightRatio"])
+ newcons[8 * i + 7, 0] = CQUOTED
+ newcons[8 * i + 7, 1] = self.config["MinStepTailHeightRatio"]
+ newcons[8 * i + 7, 2] = self.config["MaxStepTailHeightRatio"]
+ # if self.config['NoConstraintsFlag'] == 1:
+ # newcons=numpy.zeros((8*npeaks, 3),numpy.float64)
+ if npeaks > 0:
+ if g_term:
+ if self.config["PositiveHeightAreaFlag"]:
+ for i in range(npeaks):
+ newcons[8 * i, 0] = CPOSITIVE
+ if self.config["PositiveFwhmFlag"]:
+ for i in range(npeaks):
+ newcons[8 * i + 2, 0] = CPOSITIVE
+ if self.config["SameFwhmFlag"]:
+ for i in range(npeaks):
+ if i != main_peak:
+ newcons[8 * i + 2, 0] = CFACTOR
+ newcons[8 * i + 2, 1] = 8 * main_peak + 2
+ newcons[8 * i + 2, 2] = 1.0
+ if self.config["HypermetQuotedPositionFlag"]:
+ for i in range(npeaks):
+ delta = self.config["DeltaPositionFwhmUnits"] * fwhm
+ newcons[8 * i + 1, 0] = CQUOTED
+ newcons[8 * i + 1, 1] = newpar[8 * i + 1] - delta
+ newcons[8 * i + 1, 2] = newpar[8 * i + 1] + delta
+ if self.config["SameSlopeRatioFlag"]:
+ for i in range(npeaks):
+ if i != main_peak:
+ newcons[8 * i + 4, 0] = CFACTOR
+ newcons[8 * i + 4, 1] = 8 * main_peak + 4
+ newcons[8 * i + 4, 2] = 1.0
+ newcons[8 * i + 6, 0] = CFACTOR
+ newcons[8 * i + 6, 1] = 8 * main_peak + 6
+ newcons[8 * i + 6, 2] = 1.0
+ if self.config["SameAreaRatioFlag"]:
+ for i in range(npeaks):
+ if i != main_peak:
+ newcons[8 * i + 3, 0] = CFACTOR
+ newcons[8 * i + 3, 1] = 8 * main_peak + 3
+ newcons[8 * i + 3, 2] = 1.0
+ newcons[8 * i + 5, 0] = CFACTOR
+ newcons[8 * i + 5, 1] = 8 * main_peak + 5
+ newcons[8 * i + 5, 2] = 1.0
+ return newpar, newcons
+
+ def estimate_stepdown(self, x, y):
+ """Estimation of parameters for stepdown curves.
+
+ The functions estimates gaussian parameters for the derivative of
+ the data, takes the largest gaussian peak and uses its estimated
+ parameters to define the center of the step and its fwhm. The
+ estimated amplitude returned is simply ``max(y) - min(y)``.
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit newconstraints.
+ Parameters to be estimated for each stepdown are:
+ *height, centroid, fwhm* .
+ """
+ crappyfilter = [-0.25, -0.75, 0.0, 0.75, 0.25]
+ cutoff = len(crappyfilter) // 2
+ y_deriv = numpy.convolve(y, crappyfilter, mode="valid")
+
+ # make the derivative's peak have the same amplitude as the step
+ if max(y_deriv) > 0:
+ y_deriv = y_deriv * max(y) / max(y_deriv)
+
+ fittedpar, newcons = self.estimate_height_position_fwhm(
+ x[cutoff:-cutoff], y_deriv
+ )
+
+ data_amplitude = max(y) - min(y)
+
+ # use parameters from largest gaussian found
+ if len(fittedpar):
+ npeaks = len(fittedpar) // 3
+ largest_index = 0
+ largest = [
+ data_amplitude,
+ fittedpar[3 * largest_index + 1],
+ fittedpar[3 * largest_index + 2],
+ ]
+ for i in range(npeaks):
+ if fittedpar[3 * i] > largest[0]:
+ largest_index = i
+ largest = [
+ data_amplitude,
+ fittedpar[3 * largest_index + 1],
+ fittedpar[3 * largest_index + 2],
+ ]
+ else:
+ # no peak was found
+ largest = [
+ data_amplitude, # height
+ x[len(x) // 2], # center: middle of x range
+ self.config["FwhmPoints"] * (x[1] - x[0]),
+ ] # fwhm: default value
+
+ # Setup constrains
+ newcons = numpy.zeros((3, 3), numpy.float64)
+ if not self.config["NoConstraintsFlag"]:
+ # Setup height constrains
+ if self.config["PositiveHeightAreaFlag"]:
+ newcons[0, 0] = CPOSITIVE
+ newcons[0, 1] = 0
+ newcons[0, 2] = 0
+
+ # Setup position constrains
+ if self.config["QuotedPositionFlag"]:
+ newcons[1, 0] = CQUOTED
+ newcons[1, 1] = min(x)
+ newcons[1, 2] = max(x)
+
+ # Setup positive FWHM constrains
+ if self.config["PositiveFwhmFlag"]:
+ newcons[2, 0] = CPOSITIVE
+ newcons[2, 1] = 0
+ newcons[2, 2] = 0
+
+ return largest, newcons
+
+ def estimate_slit(self, x, y):
+ """Estimation of parameters for slit curves.
+
+ The functions estimates stepup and stepdown parameters for the largest
+ steps, and uses them for calculating the center (middle between stepup
+ and stepdown), the height (maximum amplitude in data), the fwhm
+ (distance between the up- and down-step centers) and the beamfwhm
+ (average of FWHM for up- and down-step).
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each slit are:
+ *height, position, fwhm, beamfwhm* .
+ """
+ largestup, cons = self.estimate_stepup(x, y)
+ largestdown, cons = self.estimate_stepdown(x, y)
+ fwhm = numpy.fabs(largestdown[1] - largestup[1])
+ beamfwhm = 0.5 * (largestup[2] + largestdown[1])
+ beamfwhm = min(beamfwhm, fwhm / 10.0)
+ beamfwhm = max(beamfwhm, (max(x) - min(x)) * 3.0 / len(x))
+
+ y_minus_bg = y - self.strip_bg(y)
+ height = max(y_minus_bg)
+
+ i1 = numpy.nonzero(y_minus_bg >= 0.5 * height)[0]
+ xx = numpy.take(x, i1)
+ position = (xx[0] + xx[-1]) / 2.0
+ fwhm = xx[-1] - xx[0]
+ largest = [height, position, fwhm, beamfwhm]
+ cons = numpy.zeros((4, 3), numpy.float64)
+ # Setup constrains
+ if not self.config["NoConstraintsFlag"]:
+ # Setup height constrains
+ if self.config["PositiveHeightAreaFlag"]:
+ cons[0, 0] = CPOSITIVE
+ cons[0, 1] = 0
+ cons[0, 2] = 0
+
+ # Setup position constrains
+ if self.config["QuotedPositionFlag"]:
+ cons[1, 0] = CQUOTED
+ cons[1, 1] = min(x)
+ cons[1, 2] = max(x)
+
+ # Setup positive FWHM constrains
+ if self.config["PositiveFwhmFlag"]:
+ cons[2, 0] = CPOSITIVE
+ cons[2, 1] = 0
+ cons[2, 2] = 0
+
+ # Setup positive FWHM constrains
+ if self.config["PositiveFwhmFlag"]:
+ cons[3, 0] = CPOSITIVE
+ cons[3, 1] = 0
+ cons[3, 2] = 0
+ return largest, cons
+
+ def estimate_stepup(self, x, y):
+ """Estimation of parameters for a single step up curve.
+
+ The functions estimates gaussian parameters for the derivative of
+ the data, takes the largest gaussian peak and uses its estimated
+ parameters to define the center of the step and its fwhm. The
+ estimated amplitude returned is simply ``max(y) - min(y)``.
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ Parameters to be estimated for each stepup are:
+ *height, centroid, fwhm* .
+ """
+ crappyfilter = [0.25, 0.75, 0.0, -0.75, -0.25]
+ cutoff = len(crappyfilter) // 2
+ y_deriv = numpy.convolve(y, crappyfilter, mode="valid")
+ if max(y_deriv) > 0:
+ y_deriv = y_deriv * max(y) / max(y_deriv)
+
+ fittedpar, cons = self.estimate_height_position_fwhm(x[cutoff:-cutoff], y_deriv)
+
+ # for height, use the data amplitude after removing the background
+ data_amplitude = max(y) - min(y)
+
+ # find params of the largest gaussian found
+ if len(fittedpar):
+ npeaks = len(fittedpar) // 3
+ largest_index = 0
+ largest = [
+ data_amplitude,
+ fittedpar[3 * largest_index + 1],
+ fittedpar[3 * largest_index + 2],
+ ]
+ for i in range(npeaks):
+ if fittedpar[3 * i] > largest[0]:
+ largest_index = i
+ largest = [
+ fittedpar[3 * largest_index],
+ fittedpar[3 * largest_index + 1],
+ fittedpar[3 * largest_index + 2],
+ ]
+ else:
+ # no peak was found
+ largest = [
+ data_amplitude, # height
+ x[len(x) // 2], # center: middle of x range
+ self.config["FwhmPoints"] * (x[1] - x[0]),
+ ] # fwhm: default value
+
+ newcons = numpy.zeros((3, 3), numpy.float64)
+ # Setup constrains
+ if not self.config["NoConstraintsFlag"]:
+ # Setup height constraints
+ if self.config["PositiveHeightAreaFlag"]:
+ newcons[0, 0] = CPOSITIVE
+ newcons[0, 1] = 0
+ newcons[0, 2] = 0
+
+ # Setup position constraints
+ if self.config["QuotedPositionFlag"]:
+ newcons[1, 0] = CQUOTED
+ newcons[1, 1] = min(x)
+ newcons[1, 2] = max(x)
+
+ # Setup positive FWHM constraints
+ if self.config["PositiveFwhmFlag"]:
+ newcons[2, 0] = CPOSITIVE
+ newcons[2, 1] = 0
+ newcons[2, 2] = 0
+
+ return largest, newcons
+
+ def estimate_periodic_gauss(self, x, y):
+ """Estimation of parameters for periodic gaussian curves:
+ *number of peaks, distance between peaks, height, position of the
+ first peak, fwhm*
+
+ The functions detects all peaks, then computes the parameters the
+ following way:
+
+ - *distance*: average of distances between detected peaks
+ - *height*: average height of detected peaks
+ - *fwhm*: fwhm of the highest peak (in number of samples) if
+ field ``'AutoFwhm'`` in :attr:`config` is ``True``, else take
+ the default value (field ``'FwhmPoints'`` in :attr:`config`)
+
+ :param x: Array of abscissa values
+ :param y: Array of ordinate values (``y = f(x)``)
+ :return: Tuple of estimated fit parameters and fit constraints.
+ """
+ yscaling = self.config.get("Yscaling", 1.0)
+ if yscaling == 0:
+ yscaling = 1.0
+
+ bg = self.strip_bg(y)
+
+ if self.config["AutoFwhm"]:
+ search_fwhm = guess_fwhm(y)
+ else:
+ search_fwhm = int(float(self.config["FwhmPoints"]))
+ search_sens = float(self.config["Sensitivity"])
+
+ if search_fwhm < 3:
+ search_fwhm = 3
+
+ if search_sens < 1:
+ search_sens = 1
+
+ if len(y) > 1.5 * search_fwhm:
+ peaks = peak_search(yscaling * y, fwhm=search_fwhm, sensitivity=search_sens)
+ else:
+ peaks = []
+ npeaks = len(peaks)
+ if not npeaks:
+ fittedpar = []
+ cons = numpy.zeros((len(fittedpar), 3), numpy.float64)
+ return fittedpar, cons
+
+ fittedpar = [0.0, 0.0, 0.0, 0.0, 0.0]
+
+ # The number of peaks
+ fittedpar[0] = npeaks
+
+ # The separation between peaks in x units
+ delta = 0.0
+ height = 0.0
+ for i in range(npeaks):
+ height += y[int(peaks[i])] - bg[int(peaks[i])]
+ if i != npeaks - 1:
+ delta += x[int(peaks[i + 1])] - x[int(peaks[i])]
+
+ # delta between peaks
+ if npeaks > 1:
+ fittedpar[1] = delta / (npeaks - 1)
+
+ # starting height
+ fittedpar[2] = height / npeaks
+
+ # position of the first peak
+ fittedpar[3] = x[int(peaks[0])]
+
+ # Estimate the fwhm
+ fittedpar[4] = search_fwhm
+
+ # setup constraints
+ cons = numpy.zeros((5, 3), numpy.float64)
+ cons[0, 0] = CFIXED # the number of gaussians
+ if npeaks == 1:
+ cons[1, 0] = CFIXED # the delta between peaks
+ else:
+ cons[1, 0] = CFREE
+ j = 2
+ # Setup height area constrains
+ if not self.config["NoConstraintsFlag"]:
+ if self.config["PositiveHeightAreaFlag"]:
+ # POSITIVE = 1
+ cons[j, 0] = CPOSITIVE
+ cons[j, 1] = 0
+ cons[j, 2] = 0
+ j += 1
+
+ # Setup position constrains
+ if not self.config["NoConstraintsFlag"]:
+ if self.config["QuotedPositionFlag"]:
+ # QUOTED = 2
+ cons[j, 0] = CQUOTED
+ cons[j, 1] = min(x)
+ cons[j, 2] = max(x)
+ j += 1
+
+ # Setup positive FWHM constrains
+ if not self.config["NoConstraintsFlag"]:
+ if self.config["PositiveFwhmFlag"]:
+ # POSITIVE=1
+ cons[j, 0] = CPOSITIVE
+ cons[j, 1] = 0
+ cons[j, 2] = 0
+ j += 1
+ return fittedpar, cons
+
+ def configure(self, **kw):
+ """Add new / unknown keyword arguments to :attr:`config`,
+ update entries in :attr:`config` if the parameter name is a existing
+ key.
+
+ :param kw: Dictionary of keyword arguments.
+ :return: Configuration dictionary :attr:`config`
+ """
+ if not kw.keys():
+ return self.config
+ for key in kw.keys():
+ notdone = 1
+ # take care of lower / upper case problems ...
+ for config_key in self.config.keys():
+ if config_key.lower() == key.lower():
+ self.config[config_key] = kw[key]
+ notdone = 0
+ if notdone:
+ self.config[key] = kw[key]
+ return self.config
+
+
+fitfuns = FitTheories()
+
+THEORY = dict(
+ (
+ (
+ "Gaussians",
+ FitTheory(
+ description="Gaussian functions",
+ function=functions.sum_gauss,
+ parameters=("Height", "Position", "FWHM"),
+ estimate=fitfuns.estimate_height_position_fwhm,
+ configure=fitfuns.configure,
+ ),
+ ),
+ (
+ "Lorentz",
+ FitTheory(
+ description="Lorentzian functions",
+ function=functions.sum_lorentz,
+ parameters=("Height", "Position", "FWHM"),
+ estimate=fitfuns.estimate_height_position_fwhm,
+ configure=fitfuns.configure,
+ ),
+ ),
+ (
+ "Area Gaussians",
+ FitTheory(
+ description="Gaussian functions (area)",
+ function=functions.sum_agauss,
+ parameters=("Area", "Position", "FWHM"),
+ estimate=fitfuns.estimate_agauss,
+ configure=fitfuns.configure,
+ ),
+ ),
+ (
+ "Area Lorentz",
+ FitTheory(
+ description="Lorentzian functions (area)",
+ function=functions.sum_alorentz,
+ parameters=("Area", "Position", "FWHM"),
+ estimate=fitfuns.estimate_alorentz,
+ configure=fitfuns.configure,
+ ),
+ ),
+ (
+ "Pseudo-Voigt Line",
+ FitTheory(
+ description="Pseudo-Voigt functions",
+ function=functions.sum_pvoigt,
+ parameters=("Height", "Position", "FWHM", "Eta"),
+ estimate=fitfuns.estimate_pvoigt,
+ configure=fitfuns.configure,
+ ),
+ ),
+ (
+ "Area Pseudo-Voigt",
+ FitTheory(
+ description="Pseudo-Voigt functions (area)",
+ function=functions.sum_apvoigt,
+ parameters=("Area", "Position", "FWHM", "Eta"),
+ estimate=fitfuns.estimate_apvoigt,
+ configure=fitfuns.configure,
+ ),
+ ),
+ (
+ "Split Gaussian",
+ FitTheory(
+ description="Asymmetric gaussian functions",
+ function=functions.sum_splitgauss,
+ parameters=("Height", "Position", "LowFWHM", "HighFWHM"),
+ estimate=fitfuns.estimate_splitgauss,
+ configure=fitfuns.configure,
+ ),
+ ),
+ (
+ "Split Lorentz",
+ FitTheory(
+ description="Asymmetric lorentzian functions",
+ function=functions.sum_splitlorentz,
+ parameters=("Height", "Position", "LowFWHM", "HighFWHM"),
+ estimate=fitfuns.estimate_splitgauss,
+ configure=fitfuns.configure,
+ ),
+ ),
+ (
+ "Split Pseudo-Voigt",
+ FitTheory(
+ description="Asymmetric pseudo-Voigt functions",
+ function=functions.sum_splitpvoigt,
+ parameters=("Height", "Position", "LowFWHM", "HighFWHM", "Eta"),
+ estimate=fitfuns.estimate_splitpvoigt,
+ configure=fitfuns.configure,
+ ),
+ ),
+ (
+ "Split Pseudo-Voigt 2",
+ FitTheory(
+ description="Asymmetric pseudo-Voigt functions",
+ function=functions.sum_splitpvoigt2,
+ parameters=(
+ "Height",
+ "Position",
+ "LowFWHM",
+ "HighFWHM",
+ "LowEta",
+ "HighEta",
+ ),
+ estimate=fitfuns.estimate_splitpvoigt2,
+ configure=fitfuns.configure,
+ ),
+ ),
+ (
+ "Step Down",
+ FitTheory(
+ description="Step down function",
+ function=functions.sum_stepdown,
+ parameters=("Height", "Position", "FWHM"),
+ estimate=fitfuns.estimate_stepdown,
+ configure=fitfuns.configure,
+ ),
+ ),
+ (
+ "Step Up",
+ FitTheory(
+ description="Step up function",
+ function=functions.sum_stepup,
+ parameters=("Height", "Position", "FWHM"),
+ estimate=fitfuns.estimate_stepup,
+ configure=fitfuns.configure,
+ ),
+ ),
+ (
+ "Slit",
+ FitTheory(
+ description="Slit function",
+ function=functions.sum_slit,
+ parameters=("Height", "Position", "FWHM", "BeamFWHM"),
+ estimate=fitfuns.estimate_slit,
+ configure=fitfuns.configure,
+ ),
+ ),
+ (
+ "Atan",
+ FitTheory(
+ description="Arctan step up function",
+ function=functions.atan_stepup,
+ parameters=("Height", "Position", "Width"),
+ estimate=fitfuns.estimate_stepup,
+ configure=fitfuns.configure,
+ ),
+ ),
+ (
+ "Hypermet",
+ FitTheory(
+ description="Hypermet functions",
+ function=fitfuns.ahypermet, # customized version of functions.sum_ahypermet
+ parameters=(
+ "G_Area",
+ "Position",
+ "FWHM",
+ "ST_Area",
+ "ST_Slope",
+ "LT_Area",
+ "LT_Slope",
+ "Step_H",
+ ),
+ estimate=fitfuns.estimate_ahypermet,
+ configure=fitfuns.configure,
+ ),
+ ),
+ # ('Periodic Gaussians',
+ # FitTheory(description='Periodic gaussian functions',
+ # function=functions.periodic_gauss,
+ # parameters=('N', 'Delta', 'Height', 'Position', 'FWHM'),
+ # estimate=fitfuns.estimate_periodic_gauss,
+ # configure=fitfuns.configure))
+ (
+ "Degree 2 Polynomial",
+ FitTheory(
+ description="Degree 2 polynomial" "\ny = a*x^2 + b*x +c",
+ function=fitfuns.poly,
+ parameters=["a", "b", "c"],
+ estimate=fitfuns.estimate_quadratic,
+ ),
+ ),
+ (
+ "Degree 3 Polynomial",
+ FitTheory(
+ description="Degree 3 polynomial" "\ny = a*x^3 + b*x^2 + c*x + d",
+ function=fitfuns.poly,
+ parameters=["a", "b", "c", "d"],
+ estimate=fitfuns.estimate_cubic,
+ ),
+ ),
+ (
+ "Degree 4 Polynomial",
+ FitTheory(
+ description="Degree 4 polynomial"
+ "\ny = a*x^4 + b*x^3 + c*x^2 + d*x + e",
+ function=fitfuns.poly,
+ parameters=["a", "b", "c", "d", "e"],
+ estimate=fitfuns.estimate_quartic,
+ ),
+ ),
+ (
+ "Degree 5 Polynomial",
+ FitTheory(
+ description="Degree 5 polynomial"
+ "\ny = a*x^5 + b*x^4 + c*x^3 + d*x^2 + e*x + f",
+ function=fitfuns.poly,
+ parameters=["a", "b", "c", "d", "e", "f"],
+ estimate=fitfuns.estimate_quintic,
+ ),
+ ),
+ )
+)
+"""Dictionary of fit theories: fit functions and their associated estimation
+function, parameters list, configuration function and description.
+"""
+
+
+def test(a):
+ from silx.math.fit import fitmanager
+
+ x = numpy.arange(1000).astype(numpy.float64)
+ p = [1500, 100.0, 50.0, 1500, 700.0, 50.0]
+ y_synthetic = functions.sum_gauss(x, *p) + 1
+
+ fit = fitmanager.FitManager(x, y_synthetic)
+ fit.addtheory(
+ "Gaussians",
+ functions.sum_gauss,
+ ["Height", "Position", "FWHM"],
+ a.estimate_height_position_fwhm,
+ )
+ fit.settheory("Gaussians")
+ fit.setbackground("Linear")
+
+ fit.estimate()
+ fit.runfit()
+
+ y_fit = fit.gendata()
+
+ print("Fit parameter names: %s" % str(fit.get_names()))
+ print("Theoretical parameters: %s" % str(numpy.append([1, 0], p)))
+ print("Fitted parameters: %s" % str(fit.get_fitted_parameters()))
+
+ try:
+ from silx.gui import qt
+ from silx.gui.plot import plot1D
+
+ app = qt.QApplication([])
+
+ # Offset of 1 to see the difference in log scale
+ plot1D(x, (y_synthetic + 1, y_fit), "Input data + 1, Fit")
+
+ app.exec()
+ except ImportError:
+ _logger.warning("Unable to load qt binding, can't plot results.")
+
+
+if __name__ == "__main__":
+ test(fitfuns)