Skip to main content
`Ctrl`+`K`

# Source code for silx.math.fit.fitmanager

    # /*#########################################################################
    #
    # 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 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

    """
    import logging
    import numpy
    from numpy.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 = {}
            """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 = {}
            """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.asarray(x)

            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
                if not isinstance(theories, str):
                    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
                if not isinstance(theories, str):
                    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.0, 250, 255, 690.0, 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()