summaryrefslogtreecommitdiff
path: root/silx/math/fit/fitmanager.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/math/fit/fitmanager.py')
-rw-r--r--silx/math/fit/fitmanager.py1087
1 files changed, 0 insertions, 1087 deletions
diff --git a/silx/math/fit/fitmanager.py b/silx/math/fit/fitmanager.py
deleted file mode 100644
index b60e073..0000000
--- a/silx/math/fit/fitmanager.py
+++ /dev/null
@@ -1,1087 +0,0 @@
-# coding: utf-8
-# /*#########################################################################
-#
-# Copyright (c) 2004-2020 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 a tool to perform advanced fitting. The actual fit relies
-on :func:`silx.math.fit.leastsq`.
-
-This module deals with:
-
- - handling of the model functions (using a set of default functions or
- loading custom user functions)
- - handling of estimation function, that are used to determine the number
- of parameters to be fitted for functions with unknown number of
- parameters (such as the sum of a variable number of gaussian curves),
- and find reasonable initial parameters for input to the iterative
- fitting algorithm
- - handling of custom derivative functions that can be passed as a
- parameter to :func:`silx.math.fit.leastsq`
- - providing different background models
-
-"""
-from collections import OrderedDict
-import logging
-import numpy
-from numpy.linalg.linalg import LinAlgError
-import os
-import sys
-
-from .filters import strip, smooth1d
-from .leastsq import leastsq
-from .fittheory import FitTheory
-from . import bgtheories
-
-
-__authors__ = ["V.A. Sole", "P. Knobel"]
-__license__ = "MIT"
-__date__ = "16/01/2017"
-
-_logger = logging.getLogger(__name__)
-
-
-class FitManager(object):
- """
- Fit functions manager
-
- :param x: Abscissa data. If ``None``, :attr:`xdata` is set to
- ``numpy.array([0.0, 1.0, 2.0, ..., len(y)-1])``
- :type x: Sequence or numpy array or None
- :param y: The dependant data ``y = f(x)``. ``y`` must have the same
- shape as ``x`` if ``x`` is not ``None``.
- :type y: Sequence or numpy array or None
- :param sigmay: The uncertainties in the ``ydata`` array. These can be
- used as weights in the least-squares problem, if ``weight_flag``
- is ``True``.
- If ``None``, the uncertainties are assumed to be 1, unless
- ``weight_flag`` is ``True``, in which case the square-root
- of ``y`` is used.
- :type sigmay: Sequence or numpy array or None
- :param weight_flag: If this parameter is ``True`` and ``sigmay``
- uncertainties are not specified, the square root of ``y`` is used
- as weights in the least-squares problem. If ``False``, the
- uncertainties are set to 1.
- :type weight_flag: boolean
- """
- def __init__(self, x=None, y=None, sigmay=None, weight_flag=False):
- """
- """
- self.fitconfig = {
- 'WeightFlag': weight_flag,
- 'fitbkg': 'No Background',
- 'fittheory': None,
- # Next few parameters are defined for compatibility with legacy theories
- # which take the background as argument for their estimation function
- 'StripWidth': 2,
- 'StripIterations': 5000,
- 'StripThresholdFactor': 1.0,
- 'SmoothingFlag': False
- }
- """Dictionary of fit configuration parameters.
- These parameters can be modified using the :meth:`configure` method.
-
- Keys are:
-
- - 'fitbkg': name of the function used for fitting a low frequency
- background signal
- - 'FwhmPoints': default full width at half maximum value for the
- peaks'.
- - 'Sensitivity': Sensitivity parameter for the peak detection
- algorithm (:func:`silx.math.fit.peak_search`)
- """
-
- self.theories = OrderedDict()
- """Dictionary of fit theories, defining functions to be fitted
- to individual peaks.
-
- Keys are descriptive theory names (e.g "Gaussians" or "Step up").
- Values are :class:`silx.math.fit.fittheory.FitTheory` objects with
- the following attributes:
-
- - *"function"* is the fit function for an individual peak
- - *"parameters"* is a sequence of parameter names
- - *"estimate"* is the parameter estimation function
- - *"configure"* is the function returning the configuration dict
- for the theory in the format described in the :attr:` fitconfig`
- documentation
- - *"derivative"* (optional) is a custom derivative function, whose
- signature is described in the documentation of
- :func:`silx.math.fit.leastsq.leastsq`
- (``model_deriv(xdata, parameters, index)``).
- - *"description"* is a description string
- """
-
- self.selectedtheory = None
- """Name of currently selected theory. This name matches a key in
- :attr:`theories`."""
-
- self.bgtheories = OrderedDict()
- """Dictionary of background theories.
-
- See :attr:`theories` for documentation on theories.
- """
-
- # Load default theories (constant, linear, strip)
- self.loadbgtheories(bgtheories)
-
- self.selectedbg = 'No Background'
- """Name of currently selected background theory. This name must be
- an existing key in :attr:`bgtheories`."""
-
- self.fit_results = []
- """This list stores detailed information about all fit parameters.
- It is initialized in :meth:`estimate` and completed with final fit
- values in :meth:`runfit`.
-
- Each fit parameter is stored as a dictionary with following fields:
-
- - 'name': Parameter name.
- - 'estimation': Estimated value.
- - 'group': Group number. Group 0 corresponds to the background
- function parameters. Group ``n`` (for ``n>0``) corresponds to
- the fit function parameters for the n-th peak.
- - 'code': Constraint code
-
- - 0 - FREE
- - 1 - POSITIVE
- - 2 - QUOTED
- - 3 - FIXED
- - 4 - FACTOR
- - 5 - DELTA
- - 6 - SUM
-
- - 'cons1':
-
- - Ignored if 'code' is FREE, POSITIVE or FIXED.
- - Min value of the parameter if code is QUOTED
- - Index of fitted parameter to which 'cons2' is related
- if code is FACTOR, DELTA or SUM.
-
- - 'cons2':
-
- - Ignored if 'code' is FREE, POSITIVE or FIXED.
- - Max value of the parameter if QUOTED
- - Factor to apply to related parameter with index 'cons1' if
- 'code' is FACTOR
- - Difference with parameter with index 'cons1' if
- 'code' is DELTA
- - Sum obtained when adding parameter with index 'cons1' if
- 'code' is SUM
-
- - 'fitresult': Fitted value.
- - 'sigma': Standard deviation for the parameter estimate
- - 'xmin': Lower limit of the ``x`` data range on which the fit
- was performed
- - 'xmax': Upeer limit of the ``x`` data range on which the fit
- was performed
- """
-
- self.parameter_names = []
- """This list stores all fit parameter names: background function
- parameters and fit function parameters for every peak. It is filled
- in :meth:`estimate`.
-
- It is the responsibility of the estimate function defined in
- :attr:`theories` to determine how many parameters are needed,
- based on how many peaks are detected and how many parameters are needed
- to fit an individual peak.
- """
-
- self.setdata(x, y, sigmay)
-
- ##################
- # Public methods #
- ##################
- def addbackground(self, bgname, bgtheory):
- """Add a new background theory to dictionary :attr:`bgtheories`.
-
- :param bgname: String with the name describing the function
- :param bgtheory: :class:`FitTheory` object
- :type bgtheory: :class:`silx.math.fit.fittheory.FitTheory`
- """
- self.bgtheories[bgname] = bgtheory
-
- def addtheory(self, name, theory=None,
- function=None, parameters=None,
- estimate=None, configure=None, derivative=None,
- description=None, pymca_legacy=False):
- """Add a new theory to dictionary :attr:`theories`.
-
- You can pass a name and a :class:`FitTheory` object as arguments, or
- alternatively provide all arguments necessary to instantiate a new
- :class:`FitTheory` object.
-
- See :meth:`loadtheories` for more information on estimation functions,
- configuration functions and custom derivative functions.
-
- :param name: String with the name describing the function
- :param theory: :class:`FitTheory` object, defining a fit function and
- associated information (estimation function, description…).
- If this parameter is provided, all other parameters, except for
- ``name``, are ignored.
- :type theory: :class:`silx.math.fit.fittheory.FitTheory`
- :param callable function: Mandatory argument if ``theory`` is not provided.
- See documentation for :attr:`silx.math.fit.fittheory.FitTheory.function`.
- :param List[str] parameters: Mandatory argument if ``theory`` is not provided.
- See documentation for :attr:`silx.math.fit.fittheory.FitTheory.parameters`.
- :param callable estimate: See documentation for
- :attr:`silx.math.fit.fittheory.FitTheory.estimate`
- :param callable configure: See documentation for
- :attr:`silx.math.fit.fittheory.FitTheory.configure`
- :param callable derivative: See documentation for
- :attr:`silx.math.fit.fittheory.FitTheory.derivative`
- :param str description: See documentation for
- :attr:`silx.math.fit.fittheory.FitTheory.description`
- :param config_widget: See documentation for
- :attr:`silx.math.fit.fittheory.FitTheory.config_widget`
- :param bool pymca_legacy: See documentation for
- :attr:`silx.math.fit.fittheory.FitTheory.pymca_legacy`
- """
- if theory is not None:
- self.theories[name] = theory
-
- elif function is not None and parameters is not None:
- self.theories[name] = FitTheory(
- description=description,
- function=function,
- parameters=parameters,
- estimate=estimate,
- configure=configure,
- derivative=derivative,
- pymca_legacy=pymca_legacy
- )
-
- else:
- raise TypeError("You must supply a FitTheory object or define " +
- "a fit function and its parameters.")
-
- def addbgtheory(self, name, theory=None,
- function=None, parameters=None,
- estimate=None, configure=None,
- derivative=None, description=None):
- """Add a new theory to dictionary :attr:`bgtheories`.
-
- You can pass a name and a :class:`FitTheory` object as arguments, or
- alternatively provide all arguments necessary to instantiate a new
- :class:`FitTheory` object.
-
- :param name: String with the name describing the function
- :param theory: :class:`FitTheory` object, defining a fit function and
- associated information (estimation function, description…).
- If this parameter is provided, all other parameters, except for
- ``name``, are ignored.
- :type theory: :class:`silx.math.fit.fittheory.FitTheory`
- :param function function: Mandatory argument if ``theory`` is not provided.
- See documentation for :attr:`silx.math.fit.fittheory.FitTheory.function`.
- :param list[str] parameters: Mandatory argument if ``theory`` is not provided.
- See documentation for :attr:`silx.math.fit.fittheory.FitTheory.parameters`.
- :param function estimate: See documentation for
- :attr:`silx.math.fit.fittheory.FitTheory.estimate`
- :param function configure: See documentation for
- :attr:`silx.math.fit.fittheory.FitTheory.configure`
- :param function derivative: See documentation for
- :attr:`silx.math.fit.fittheory.FitTheory.derivative`
- :param str description: See documentation for
- :attr:`silx.math.fit.fittheory.FitTheory.description`
- """
- if theory is not None:
- self.bgtheories[name] = theory
-
- elif function is not None and parameters is not None:
- self.bgtheories[name] = FitTheory(
- description=description,
- function=function,
- parameters=parameters,
- estimate=estimate,
- configure=configure,
- derivative=derivative,
- is_background=True
- )
-
- else:
- raise TypeError("You must supply a FitTheory object or define " +
- "a background function and its parameters.")
-
- def configure(self, **kw):
- """Configure the current theory by filling or updating the
- :attr:`fitconfig` dictionary.
- Call the custom configuration function, if any. This allows the user
- to modify the behavior of the custom fit function or the custom
- estimate function.
-
- This methods accepts only named parameters. All ``**kw`` parameters
- are expected to be fields of :attr:`fitconfig` to be updated, unless
- they have a special meaning for the custom configuration function
- of the currently selected theory..
-
- This method returns the modified config dictionary returned by the
- custom configuration function.
- """
- # inspect **kw to find known keys, update them in self.fitconfig
- for key in self.fitconfig:
- if key in kw:
- self.fitconfig[key] = kw[key]
-
- # initialize dict with existing config dict
- result = {}
- result.update(self.fitconfig)
-
- if "WeightFlag" in kw:
- if kw["WeightFlag"]:
- self.enableweight()
- else:
- self.disableweight()
-
- if self.selectedtheory is None:
- return result
-
- # Apply custom configuration function
- custom_config_fun = self.theories[self.selectedtheory].configure
- if custom_config_fun is not None:
- result.update(custom_config_fun(**kw))
-
- custom_bg_config_fun = self.bgtheories[self.selectedbg].configure
- if custom_bg_config_fun is not None:
- result.update(custom_bg_config_fun(**kw))
-
- # Update self.fitconfig with custom config
- for key in self.fitconfig:
- if key in result:
- self.fitconfig[key] = result[key]
-
- result.update(self.fitconfig)
- return result
-
- def estimate(self, callback=None):
- """
- Fill :attr:`fit_results` with an estimation of the fit parameters.
-
- At first, the background parameters are estimated, if a background
- model has been specified.
- Then, a custom estimation function related to the model function is
- called.
-
- This process determines the number of needed fit parameters and
- provides an initial estimation for them, to serve as an input for the
- actual iterative fitting performed in :meth:`runfit`.
-
- :param callback: Optional callback function, conforming to the
- signature ``callback(data)`` with ``data`` being a dictionary.
- This callback function is called before and after the estimation
- process, and is given a dictionary containing the values of
- :attr:`state` (``'Estimate in progress'`` or ``'Ready to Fit'``)
- and :attr:`chisq`.
- This is used for instance in :mod:`silx.gui.fit.FitWidget` to
- update a widget displaying a status message.
- :return: Estimated parameters
- """
- self.state = 'Estimate in progress'
- self.chisq = None
-
- if callback is not None:
- callback(data={'chisq': self.chisq,
- 'status': self.state})
-
- CONS = {0: 'FREE',
- 1: 'POSITIVE',
- 2: 'QUOTED',
- 3: 'FIXED',
- 4: 'FACTOR',
- 5: 'DELTA',
- 6: 'SUM',
- 7: 'IGNORE'}
-
- # Filter-out not finite data
- xwork = self.xdata[self._finite_mask]
- ywork = self.ydata[self._finite_mask]
-
- # estimate the background
- bg_params, bg_constraints = self.estimate_bkg(xwork, ywork)
-
- # estimate the function
- try:
- fun_params, fun_constraints = self.estimate_fun(xwork, ywork)
- except LinAlgError:
- self.state = 'Estimate failed'
- if callback is not None:
- callback(data={'status': self.state})
- raise
-
- # build the names
- self.parameter_names = []
-
- for bg_param_name in self.bgtheories[self.selectedbg].parameters:
- self.parameter_names.append(bg_param_name)
-
- fun_param_names = self.theories[self.selectedtheory].parameters
- param_index, peak_index = 0, 0
- while param_index < len(fun_params):
- peak_index += 1
- for fun_param_name in fun_param_names:
- self.parameter_names.append(fun_param_name + "%d" % peak_index)
- param_index += 1
-
- self.fit_results = []
- nb_fun_params_per_group = len(fun_param_names)
- group_number = 0
- xmin = min(xwork)
- xmax = max(xwork)
- nb_bg_params = len(bg_params)
- for (pindex, pname) in enumerate(self.parameter_names):
- # First come background parameters
- if pindex < nb_bg_params:
- estimation_value = bg_params[pindex]
- constraint_code = CONS[int(bg_constraints[pindex][0])]
- cons1 = bg_constraints[pindex][1]
- cons2 = bg_constraints[pindex][2]
- # then come peak function parameters
- else:
- fun_param_index = pindex - nb_bg_params
-
- # increment group_number for each new fitted peak
- if (fun_param_index % nb_fun_params_per_group) == 0:
- group_number += 1
-
- estimation_value = fun_params[fun_param_index]
- constraint_code = CONS[int(fun_constraints[fun_param_index][0])]
- # cons1 is the index of another fit parameter. In the global
- # fit_results, we must adjust the index to account for the bg
- # params added to the start of the list.
- cons1 = fun_constraints[fun_param_index][1]
- if constraint_code in ["FACTOR", "DELTA", "SUM"]:
- cons1 += nb_bg_params
- cons2 = fun_constraints[fun_param_index][2]
-
- self.fit_results.append({'name': pname,
- 'estimation': estimation_value,
- 'group': group_number,
- 'code': constraint_code,
- 'cons1': cons1,
- 'cons2': cons2,
- 'fitresult': 0.0,
- 'sigma': 0.0,
- 'xmin': xmin,
- 'xmax': xmax})
-
- self.state = 'Ready to Fit'
- self.chisq = None
- self.niter = 0
-
- if callback is not None:
- callback(data={'chisq': self.chisq,
- 'status': self.state})
- return numpy.append(bg_params, fun_params)
-
- def fit(self):
- """Convenience method to call :meth:`estimate` followed by :meth:`runfit`.
-
- :return: Output of :meth:`runfit`"""
- self.estimate()
- return self.runfit()
-
- def gendata(self, x=None, paramlist=None, estimated=False):
- """Return a data array using the currently selected fit function
- and the fitted parameters.
-
- :param x: Independent variable where the function is calculated.
- If ``None``, use :attr:`xdata`.
- :param paramlist: List of dictionaries, each dictionary item being a
- fit parameter. The dictionary's format is documented in
- :attr:`fit_results`.
- If ``None`` (default), use parameters from :attr:`fit_results`.
- :param estimated: If *True*, use estimated parameters.
- :return: :meth:`fitfunction` calculated for parameters whose code is
- not set to ``"IGNORE"``.
-
- This calculates :meth:`fitfunction` on `x` data using fit parameters
- from a list of parameter dictionaries, if field ``code`` is not set
- to ``"IGNORE"``.
- """
- x = self.xdata if x is None else numpy.array(x, copy=False)
-
- if paramlist is None:
- paramlist = self.fit_results
- active_params = []
- for param in paramlist:
- if param['code'] not in ['IGNORE', 7]:
- if not estimated:
- active_params.append(param['fitresult'])
- else:
- active_params.append(param['estimation'])
-
- # Mask x with not finite (support nD x)
- finite_mask = numpy.all(numpy.isfinite(x), axis=tuple(range(1, x.ndim)))
-
- if numpy.all(finite_mask): # All values are finite: fast path
- return self.fitfunction(numpy.array(x, copy=True), *active_params)
-
- else: # Only run fitfunction on finite data and complete result with NaNs
- # Create result with same number as elements as x, filling holes with NaNs
- result = numpy.full((x.shape[0],), numpy.nan, dtype=numpy.float64)
- result[finite_mask] = self.fitfunction(
- numpy.array(x[finite_mask], copy=True), *active_params)
- return result
-
- def get_estimation(self):
- """Return the list of fit parameter names."""
- if self.state not in ["Ready to fit", "Fit in progress", "Ready"]:
- _logger.warning("get_estimation() called before estimate() completed")
- return [param["estimation"] for param in self.fit_results]
-
- def get_names(self):
- """Return the list of fit parameter estimations."""
- if self.state not in ["Ready to fit", "Fit in progress", "Ready"]:
- msg = "get_names() called before estimate() completed, "
- msg += "names are not populated at this stage"
- _logger.warning(msg)
- return [param["name"] for param in self.fit_results]
-
- def get_fitted_parameters(self):
- """Return the list of fitted parameters."""
- if self.state not in ["Ready"]:
- msg = "get_fitted_parameters() called before runfit() completed, "
- msg += "results are not available a this stage"
- _logger.warning(msg)
- return [param["fitresult"] for param in self.fit_results]
-
- def loadtheories(self, theories):
- """Import user defined fit functions defined in an external Python
- source file, and save them in :attr:`theories`.
-
- An example of such a file can be found in the sources of
- :mod:`silx.math.fit.fittheories`. It must contain a
- dictionary named ``THEORY`` with the following structure::
-
- 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(…),
- }
-
- See documentation of :mod:`silx.math.fit.fittheories` and
- :mod:`silx.math.fit.fittheory` for more
- information on designing your fit functions file.
-
- This method can also load user defined functions in the legacy
- format used in *PyMca*.
-
- :param theories: Name of python source file, or module containing the
- definition of fit functions.
- :raise: ImportError if theories cannot be imported
- """
- from types import ModuleType
- if isinstance(theories, ModuleType):
- theories_module = theories
- else:
- # if theories is not a module, it must be a string
- string_types = (basestring,) if sys.version_info[0] == 2 else (str,) # noqa
- if not isinstance(theories, string_types):
- raise ImportError("theory must be a python module, a module" +
- "name or a python filename")
- # if theories is a filename
- if os.path.isfile(theories):
- sys.path.append(os.path.dirname(theories))
- f = os.path.basename(os.path.splitext(theories)[0])
- theories_module = __import__(f)
- # if theories is a module name
- else:
- theories_module = __import__(theories)
-
- if hasattr(theories_module, "INIT"):
- theories.INIT()
-
- if not hasattr(theories_module, "THEORY"):
- msg = "File %s does not contain a THEORY dictionary" % theories
- raise ImportError(msg)
-
- elif isinstance(theories_module.THEORY, dict):
- # silx format for theory definition
- for theory_name, fittheory in list(theories_module.THEORY.items()):
- self.addtheory(theory_name, fittheory)
- else:
- self._load_legacy_theories(theories_module)
-
- def loadbgtheories(self, theories):
- """Import user defined background functions defined in an external Python
- module (source file), and save them in :attr:`theories`.
-
- An example of such a file can be found in the sources of
- :mod:`silx.math.fit.fittheories`. It must contain a
- dictionary named ``THEORY`` with the following structure::
-
- 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,
- 'theory_name_2':
- FitTheory(…),
- }
-
- See documentation of :mod:`silx.math.fit.bgtheories` and
- :mod:`silx.math.fit.fittheory` for more
- information on designing your background functions file.
-
- :param theories: Module or name of python source file containing the
- definition of background functions.
- :raise: ImportError if theories cannot be imported
- """
- from types import ModuleType
- if isinstance(theories, ModuleType):
- theories_module = theories
- else:
- # if theories is not a module, it must be a string
- string_types = (basestring,) if sys.version_info[0] == 2 else (str,) # noqa
- if not isinstance(theories, string_types):
- raise ImportError("theory must be a python module, a module" +
- "name or a python filename")
- # if theories is a filename
- if os.path.isfile(theories):
- sys.path.append(os.path.dirname(theories))
- f = os.path.basename(os.path.splitext(theories)[0])
- theories_module = __import__(f)
- # if theories is a module name
- else:
- theories_module = __import__(theories)
-
- if hasattr(theories_module, "INIT"):
- theories.INIT()
-
- if not hasattr(theories_module, "THEORY"):
- msg = "File %s does not contain a THEORY dictionary" % theories
- raise ImportError(msg)
-
- elif isinstance(theories_module.THEORY, dict):
- # silx format for theory definition
- for theory_name, fittheory in list(theories_module.THEORY.items()):
- self.addbgtheory(theory_name, fittheory)
-
- def setbackground(self, theory):
- """Choose a background type from within :attr:`bgtheories`.
-
- This updates :attr:`selectedbg`.
-
- :param theory: The name of the background to be used.
- :raise: KeyError if ``theory`` is not a key of :attr:`bgtheories``.
- """
- if theory in self.bgtheories:
- self.selectedbg = theory
- else:
- msg = "No theory with name %s in bgtheories.\n" % theory
- msg += "Available theories: %s\n" % self.bgtheories.keys()
- raise KeyError(msg)
-
- # run configure to apply our fitconfig to the selected theory
- # through its custom config function
- self.configure(**self.fitconfig)
-
- def setdata(self, x, y, sigmay=None, xmin=None, xmax=None):
- """Set data attributes:
-
- - ``xdata0``, ``ydata0`` and ``sigmay0`` store the initial data
- and uncertainties. These attributes are not modified after
- initialization.
- - ``xdata``, ``ydata`` and ``sigmay`` store the data after
- removing values where ``xdata < xmin`` or ``xdata > xmax``.
- These attributes may be modified at a latter stage by filters.
-
- :param x: Abscissa data. If ``None``, :attr:`xdata`` is set to
- ``numpy.array([0.0, 1.0, 2.0, ..., len(y)-1])``
- :type x: Sequence or numpy array or None
- :param y: The dependant data ``y = f(x)``. ``y`` must have the same
- shape as ``x`` if ``x`` is not ``None``.
- :type y: Sequence or numpy array or None
- :param sigmay: The uncertainties in the ``ydata`` array. These are
- used as weights in the least-squares problem.
- If ``None``, the uncertainties are assumed to be 1.
- :type sigmay: Sequence or numpy array or None
- :param xmin: Lower value of x values to use for fitting
- :param xmax: Upper value of x values to use for fitting
- """
- if y is None:
- self.xdata0 = numpy.array([], numpy.float64)
- self.ydata0 = numpy.array([], numpy.float64)
- # self.sigmay0 = numpy.array([], numpy.float64)
- self.xdata = numpy.array([], numpy.float64)
- self.ydata = numpy.array([], numpy.float64)
- # self.sigmay = numpy.array([], numpy.float64)
-
- else:
- self.ydata0 = numpy.array(y)
- self.ydata = numpy.array(y)
- if x is None:
- self.xdata0 = numpy.arange(len(self.ydata0))
- self.xdata = numpy.arange(len(self.ydata0))
- else:
- self.xdata0 = numpy.array(x)
- self.xdata = numpy.array(x)
-
- # default weight
- if sigmay is None:
- self.sigmay0 = None
- self.sigmay = numpy.sqrt(self.ydata) if self.fitconfig["WeightFlag"] else None
- else:
- self.sigmay0 = numpy.array(sigmay)
- self.sigmay = numpy.array(sigmay) if self.fitconfig["WeightFlag"] else None
-
- # take the data between limits, using boolean array indexing
- if (xmin is not None or xmax is not None) and len(self.xdata):
- xmin = xmin if xmin is not None else min(self.xdata)
- xmax = xmax if xmax is not None else max(self.xdata)
- bool_array = (self.xdata >= xmin) & (self.xdata <= xmax)
- self.xdata = self.xdata[bool_array]
- self.ydata = self.ydata[bool_array]
- self.sigmay = self.sigmay[bool_array] if sigmay is not None else None
-
- self._finite_mask = numpy.logical_and(
- numpy.all(numpy.isfinite(self.xdata), axis=tuple(range(1, self.xdata.ndim))),
- numpy.isfinite(self.ydata))
-
- def enableweight(self):
- """This method can be called to set :attr:`sigmay`. If :attr:`sigmay0` was filled with
- actual uncertainties in :meth:`setdata`, use these values.
- Else, use ``sqrt(self.ydata)``.
- """
- if self.sigmay0 is None:
- self.sigmay = numpy.sqrt(self.ydata) if self.fitconfig["WeightFlag"] else None
- else:
- self.sigmay = self.sigmay0
-
- def disableweight(self):
- """This method can be called to set :attr:`sigmay` equal to ``None``.
- As a result, :func:`leastsq` will consider that the weights in the
- least square problem are 1 for all samples."""
- self.sigmay = None
-
- def settheory(self, theory):
- """Pick a theory from :attr:`theories`.
-
- :param theory: Name of the theory to be used.
- :raise: KeyError if ``theory`` is not a key of :attr:`theories`.
- """
- if theory is None:
- self.selectedtheory = None
- elif theory in self.theories:
- self.selectedtheory = theory
- else:
- msg = "No theory with name %s in theories.\n" % theory
- msg += "Available theories: %s\n" % self.theories.keys()
- raise KeyError(msg)
-
- # run configure to apply our fitconfig to the selected theory
- # through its custom config function
- self.configure(**self.fitconfig)
-
- def runfit(self, callback=None):
- """Run the actual fitting and fill :attr:`fit_results` with fit results.
-
- Before running this method, :attr:`fit_results` must already be
- populated with a list of all parameters and their estimated values.
- For this, run :meth:`estimate` beforehand.
-
- :param callback: Optional callback function, conforming to the
- signature ``callback(data)`` with ``data`` being a dictionary.
- This callback function is called before and after the estimation
- process, and is given a dictionary containing the values of
- :attr:`state` (``'Fit in progress'`` or ``'Ready'``)
- and :attr:`chisq`.
- This is used for instance in :mod:`silx.gui.fit.FitWidget` to
- update a widget displaying a status message.
- :return: Tuple ``(fitted parameters, uncertainties, infodict)``.
- *infodict* is the dictionary returned by
- :func:`silx.math.fit.leastsq` when called with option
- ``full_output=True``. Uncertainties is a sequence of uncertainty
- values associated with each fitted parameter.
- """
- # self.dataupdate()
-
- self.state = 'Fit in progress'
- self.chisq = None
-
- if callback is not None:
- callback(data={'chisq': self.chisq,
- 'status': self.state})
-
- param_val = []
- param_constraints = []
- # Initial values are set to the ones computed in estimate()
- for param in self.fit_results:
- param_val.append(param['estimation'])
- param_constraints.append([param['code'], param['cons1'], param['cons2']])
-
- # Filter-out not finite data
- ywork = self.ydata[self._finite_mask]
- xwork = self.xdata[self._finite_mask]
-
- try:
- params, covariance_matrix, infodict = leastsq(
- self.fitfunction, # bg + actual model function
- xwork, ywork, param_val,
- sigma=self.sigmay,
- constraints=param_constraints,
- model_deriv=self.theories[self.selectedtheory].derivative,
- full_output=True, left_derivative=True)
- except LinAlgError:
- self.state = 'Fit failed'
- callback(data={'status': self.state})
- raise
-
- sigmas = infodict['uncertainties']
-
- for i, param in enumerate(self.fit_results):
- if param['code'] != 'IGNORE':
- param['fitresult'] = params[i]
- param['sigma'] = sigmas[i]
-
- self.chisq = infodict["reduced_chisq"]
- self.niter = infodict["niter"]
- self.state = 'Ready'
-
- if callback is not None:
- callback(data={'chisq': self.chisq,
- 'status': self.state})
-
- return params, sigmas, infodict
-
- ###################
- # Private methods #
- ###################
- def fitfunction(self, x, *pars):
- """Function to be fitted.
-
- This is the sum of the selected background function plus
- the selected fit model function.
-
- :param x: Independent variable where the function is calculated.
- :param pars: Sequence of all fit parameters. The first few parameters
- are background parameters, then come the peak function parameters.
- :return: Output of the fit function with ``x`` as input and ``pars``
- as fit parameters.
- """
- result = numpy.zeros(numpy.shape(x), numpy.float64)
-
- if self.selectedbg is not None:
- bg_pars_list = self.bgtheories[self.selectedbg].parameters
- nb_bg_pars = len(bg_pars_list)
-
- bgfun = self.bgtheories[self.selectedbg].function
- result += bgfun(x, self.ydata, *pars[0:nb_bg_pars])
- else:
- nb_bg_pars = 0
-
- selectedfun = self.theories[self.selectedtheory].function
- result += selectedfun(x, *pars[nb_bg_pars:])
-
- return result
-
- def estimate_bkg(self, x, y):
- """Estimate background parameters using the function defined in
- the current fit configuration.
-
- To change the selected background model, attribute :attr:`selectdbg`
- must be changed using method :meth:`setbackground`.
-
- The actual background function to be used is
- referenced in :attr:`bgtheories`
-
- :param x: Sequence of x data
- :param y: sequence of y data
- :return: Tuple of two sequences and one data array
- ``(estimated_param, constraints, bg_data)``:
-
- - ``estimated_param`` is a list of estimated values for each
- background parameter.
- - ``constraints`` is a 2D sequence of dimension ``(n_parameters, 3)``
-
- - ``constraints[i][0]``: Constraint code.
- See explanation about codes in :attr:`fit_results`
-
- - ``constraints[i][1]``
- See explanation about 'cons1' in :attr:`fit_results`
- documentation.
-
- - ``constraints[i][2]``
- See explanation about 'cons2' in :attr:`fit_results`
- documentation.
- """
- background_estimate_function = self.bgtheories[self.selectedbg].estimate
- if background_estimate_function is not None:
- return background_estimate_function(x, y)
- else:
- return [], []
-
- def estimate_fun(self, x, y):
- """Estimate fit parameters using the function defined in
- the current fit configuration.
-
- :param x: Sequence of x data
- :param y: sequence of y data
- :param bg: Background signal, to be subtracted from ``y`` before fitting.
- :return: Tuple of two sequences ``(estimated_param, constraints)``:
-
- - ``estimated_param`` is a list of estimated values for each
- background parameter.
- - ``constraints`` is a 2D sequence of dimension (n_parameters, 3)
-
- - ``constraints[i][0]``: Constraint code.
- See explanation about codes in :attr:`fit_results`
-
- - ``constraints[i][1]``
- See explanation about 'cons1' in :attr:`fit_results`
- documentation.
-
- - ``constraints[i][2]``
- See explanation about 'cons2' in :attr:`fit_results`
- documentation.
- :raise: ``TypeError`` if estimation function is not callable
-
- """
- estimatefunction = self.theories[self.selectedtheory].estimate
- if hasattr(estimatefunction, '__call__'):
- if not self.theories[self.selectedtheory].pymca_legacy:
- return estimatefunction(x, y)
- else:
- # legacy pymca estimate functions have a different signature
- if self.fitconfig["fitbkg"] == "No Background":
- bg = numpy.zeros_like(y)
- else:
- if self.fitconfig["SmoothingFlag"]:
- y = smooth1d(y)
- bg = strip(y,
- w=self.fitconfig["StripWidth"],
- niterations=self.fitconfig["StripIterations"],
- factor=self.fitconfig["StripThresholdFactor"])
- # fitconfig can be filled by user defined config function
- xscaling = self.fitconfig.get('Xscaling', 1.0)
- yscaling = self.fitconfig.get('Yscaling', 1.0)
- return estimatefunction(x, y, bg, xscaling, yscaling)
- else:
- raise TypeError("Estimation function in attribute " +
- "theories[%s]" % self.selectedtheory +
- " must be callable.")
-
- def _load_legacy_theories(self, theories_module):
- """Load theories from a custom module in the old PyMca format.
-
- See PyMca5.PyMcaMath.fitting.SpecfitFunctions for an example.
- """
- mandatory_attributes = ["THEORY", "PARAMETERS",
- "FUNCTION", "ESTIMATE"]
- err_msg = "Custom fit function file must define: "
- err_msg += ", ".join(mandatory_attributes)
- for attr in mandatory_attributes:
- if not hasattr(theories_module, attr):
- raise ImportError(err_msg)
-
- derivative = theories_module.DERIVATIVE if hasattr(theories_module, "DERIVATIVE") else None
- configure = theories_module.CONFIGURE if hasattr(theories_module, "CONFIGURE") else None
- estimate = theories_module.ESTIMATE if hasattr(theories_module, "ESTIMATE") else None
- if isinstance(theories_module.THEORY, (list, tuple)):
- # multiple fit functions
- for i in range(len(theories_module.THEORY)):
- deriv = derivative[i] if derivative is not None else None
- config = configure[i] if configure is not None else None
- estim = estimate[i] if estimate is not None else None
- self.addtheory(theories_module.THEORY[i],
- FitTheory(
- theories_module.FUNCTION[i],
- theories_module.PARAMETERS[i],
- estim,
- config,
- deriv,
- pymca_legacy=True))
- else:
- # single fit function
- self.addtheory(theories_module.THEORY,
- FitTheory(
- theories_module.FUNCTION,
- theories_module.PARAMETERS,
- estimate,
- configure,
- derivative,
- pymca_legacy=True))
-
-
-def test():
- from .functions import sum_gauss
- from . import fittheories
- from . import bgtheories
-
- # Create synthetic data with a sum of gaussian functions
- x = numpy.arange(1000).astype(numpy.float64)
-
- p = [1000, 100., 250,
- 255, 690., 45,
- 1500, 800.5, 95]
- y = 0.5 * x + 13 + sum_gauss(x, *p)
-
- # Fitting
- fit = FitManager()
- # more sensitivity necessary to resolve
- # overlapping peaks at x=690 and x=800.5
- fit.setdata(x=x, y=y)
- fit.loadtheories(fittheories)
- fit.settheory('Gaussians')
- fit.loadbgtheories(bgtheories)
- fit.setbackground('Linear')
- fit.estimate()
- fit.runfit()
-
- print("Searched parameters = ", p)
- print("Obtained parameters : ")
- dummy_list = []
- for param in fit.fit_results:
- print(param['name'], ' = ', param['fitresult'])
- dummy_list.append(param['fitresult'])
- print("chisq = ", fit.chisq)
-
- # Plot
- constant, slope = dummy_list[:2]
- p1 = dummy_list[2:]
- print(p1)
- y2 = slope * x + constant + sum_gauss(x, *p1)
-
- try:
- from silx.gui import qt
- from silx.gui.plot.PlotWindow import PlotWindow
- app = qt.QApplication([])
- pw = PlotWindow(control=True)
- pw.addCurve(x, y, "Original")
- pw.addCurve(x, y2, "Fit result")
- pw.legendsDockWidget.show()
- pw.show()
- app.exec_()
- except ImportError:
- _logger.warning("Could not import qt to display fit result as curve")
-
-
-if __name__ == "__main__":
- test()