Skip to main content
`Ctrl`+`K`

# Source code for astropy.modeling.optimizers

    # Licensed under a 3-clause BSD style license - see LICENSE.rst
    # pylint: disable=invalid-name

    """
    Optimization algorithms used in `~astropy.modeling.fitting`.
    """

    import warnings
    from abc import ABC, abstractmethod

    import numpy as np

    from astropy.utils.exceptions import AstropyUserWarning

    __all__ = ["SLSQP", "Optimization", "Simplex"]

    # Maximum number of iterations
    DEFAULT_MAXITER = 100

    # Step for the forward difference approximation of the Jacobian
    DEFAULT_EPS = np.sqrt(np.finfo(float).eps)

    # Default requested accuracy
    DEFAULT_ACC = 1e-07

    DEFAULT_BOUNDS = (-(10**12), 10**12)

    class Optimization(ABC):
        """
        Base class for optimizers.

        Parameters
        ----------
        opt_method : callable
            Implements optimization method

        Notes
        -----
        The base Optimizer does not support any constraints by default; individual
        optimizers should explicitly set this list to the specific constraints
        it supports.

        """

        supported_constraints = []

        @abstractmethod
        def __init__(self) -> None: ...

        def _init_opt_method(self, opt_method):
            self._opt_method = opt_method
            self._maxiter = DEFAULT_MAXITER
            self._eps = DEFAULT_EPS
            self._acc = DEFAULT_ACC

        @property
        def maxiter(self):
            """Maximum number of iterations."""
            return self._maxiter

        @maxiter.setter
        def maxiter(self, val):
            """Set maxiter."""
            self._maxiter = val

        @property
        def eps(self):
            """Step for the forward difference approximation of the Jacobian."""
            return self._eps

        @eps.setter
        def eps(self, val):
            """Set eps value."""
            self._eps = val

        @property
        def acc(self):
            """Requested accuracy."""
            return self._acc

        @acc.setter
        def acc(self, val):
            """Set accuracy."""
            self._acc = val

        def __repr__(self):
            fmt = f"{self.__class__.__name__}()"
            return fmt

        @property
        def opt_method(self):
            """Return the optimization method."""
            return self._opt_method

        @abstractmethod
        def __call__(self):
            raise NotImplementedError("Subclasses should implement this method")

    class SLSQP(Optimization):
        """
        Sequential Least Squares Programming optimization algorithm.

        The algorithm is described in [1]_. It supports tied and fixed
        parameters, as well as bounded constraints. Uses
        `scipy.optimize.fmin_slsqp`.

        References
        ----------
        .. [1] http://www.netlib.org/toms/733
        """

        supported_constraints = ["bounds", "eqcons", "ineqcons", "fixed", "tied"]

        def __init__(self):
            from scipy.optimize import fmin_slsqp

            self._init_opt_method(fmin_slsqp)
            self.fit_info = {
                "final_func_val": None,
                "numiter": None,
                "exit_mode": None,
                "message": None,
            }

        def __call__(self, objfunc, initval, fargs, **kwargs):
            """
            Run the solver.

            Parameters
            ----------
            objfunc : callable
                objection function
            initval : iterable
                initial guess for the parameter values
            fargs : tuple
                other arguments to be passed to the statistic function
            kwargs : dict
                other keyword arguments to be passed to the solver

            """
            kwargs["iter"] = kwargs.pop("maxiter", self._maxiter)

            if "epsilon" not in kwargs:
                kwargs["epsilon"] = self._eps
            if "acc" not in kwargs:
                kwargs["acc"] = self._acc
            # Get the verbosity level
            disp = kwargs.pop("verblevel", None)

            # set the values of constraints to match the requirements of fmin_slsqp
            model = fargs[0]
            pars = [getattr(model, name) for name in model.param_names]
            bounds = [par.bounds for par in pars if not (par.fixed or par.tied)]
            bounds = np.asarray(bounds)
            for i in bounds:
                if i[0] is None:
                    i[0] = DEFAULT_BOUNDS[0]
                if i[1] is None:
                    i[1] = DEFAULT_BOUNDS[1]
            # older versions of scipy require this array to be float
            bounds = np.asarray(bounds, dtype=float)
            eqcons = np.array(model.eqcons)
            ineqcons = np.array(model.ineqcons)
            fitparams, final_func_val, numiter, exit_mode, mess = self.opt_method(
                objfunc,
                initval,
                args=fargs,
                full_output=True,
                disp=disp,
                bounds=bounds,
                eqcons=eqcons,
                ieqcons=ineqcons,
                **kwargs,
            )

            self.fit_info["final_func_val"] = final_func_val
            self.fit_info["numiter"] = numiter
            self.fit_info["exit_mode"] = exit_mode
            self.fit_info["message"] = mess

            if exit_mode != 0:
                warnings.warn(
                    "The fit may be unsuccessful; check "
                    "fit_info['message'] for more information.",
                    AstropyUserWarning,
                )

            return fitparams, self.fit_info

    class Simplex(Optimization):
        """
        Neald-Mead (downhill simplex) algorithm.

        This algorithm [1]_ only uses function values, not derivatives.
        Uses `scipy.optimize.fmin`.

        References
        ----------
        .. [1] Nelder, J.A. and Mead, R. (1965), "A simplex method for function
           minimization", The Computer Journal, 7, pp. 308-313
        """

        supported_constraints = ["bounds", "fixed", "tied"]

        def __init__(self):
            from scipy.optimize import fmin as simplex

            self._init_opt_method(simplex)
            self.fit_info = {
                "final_func_val": None,
                "numiter": None,
                "exit_mode": None,
                "num_function_calls": None,
            }

        def __call__(self, objfunc, initval, fargs, **kwargs):
            """
            Run the solver.

            Parameters
            ----------
            objfunc : callable
                objection function
            initval : iterable
                initial guess for the parameter values
            fargs : tuple
                other arguments to be passed to the statistic function
            kwargs : dict
                other keyword arguments to be passed to the solver

            """
            if "maxiter" not in kwargs:
                kwargs["maxiter"] = self._maxiter
            if "acc" in kwargs:
                self._acc = kwargs["acc"]
                kwargs.pop("acc")
            if "xtol" in kwargs:
                self._acc = kwargs["xtol"]
                kwargs.pop("xtol")
            # Get the verbosity level
            disp = kwargs.pop("verblevel", None)

            fitparams, final_func_val, numiter, funcalls, exit_mode = self.opt_method(
                objfunc,
                initval,
                args=fargs,
                xtol=self._acc,
                disp=disp,
                full_output=True,
                **kwargs,
            )
            self.fit_info["final_func_val"] = final_func_val
            self.fit_info["numiter"] = numiter
            self.fit_info["exit_mode"] = exit_mode
            self.fit_info["num_function_calls"] = funcalls
            if self.fit_info["exit_mode"] == 1:
                warnings.warn(
                    "The fit may be unsuccessful; "
                    "Maximum number of function evaluations reached.",
                    AstropyUserWarning,
                )
            elif self.fit_info["exit_mode"] == 2:
                warnings.warn(
                    "The fit may be unsuccessful; Maximum number of iterations reached.",
                    AstropyUserWarning,
                )
            return fitparams, self.fit_info