Note

You are not reading the most up to date version of Gammapy documentation.
Access the latest stable version v1.3 or the list of Gammapy releases.

Source code for gammapy.utils.fitting.fit

from __future__ import absolute_import, division, print_function, unicode_literals
import logging
import abc

import numpy as np
from astropy.utils.misc import InheritDocstrings

from ...extern import six
from .iminuit import optimize_iminuit, _get_covar
from .sherpa import optimize_sherpa


__all__ = ["Fit"]

log = logging.getLogger(__name__)


class FitMeta(InheritDocstrings, abc.ABCMeta):
    pass


[docs]@six.add_metaclass(FitMeta) class Fit(object): """Abstract Fit base class. """ _optimize_funcs = {"minuit": optimize_iminuit, "sherpa": optimize_sherpa}
[docs] @abc.abstractmethod def total_stat(self, parameters): """Total likelihood given the current model parameters""" pass
[docs] def likelihood_profiles(self, model, parameters="all"): """Compute likelihood profiles for multiple parameters. Parameters ---------- model : `~gammapy.spectrum.models.SpectralModel` or `~gammapy.cube.models.SkyModel` Model to compute the likelihood profile for. parameters : list of str or "all" For which parameters to compute likelihood profiles. """ profiles = {} if parameters == "all": parameters = [par.name for par in model.paramaters] for parname in parameters: profiles[parname] = self.likelihood_profile(model, parname) return profiles
[docs] def likelihood_profile(self, model, parameter, values=None, bounds=2, nvalues=11): """Compute likelihood profile for a single parameter of the model. Parameters ---------- model : `~gammapy.spectrum.models.SpectralModel` Model to compute the likelihood profile for. parameter : str Parameter to calculate profile for values : `~astropy.units.Quantity` (optional) Parameter values to evaluate the likelihood for. bounds : int or tuple of float When an `int` is passed the bounds are computed from `bounds * sigma` from the best fit value of the parameter, where `sigma` corresponds to the one sigma error on the parameter. If a tuple of floats is given those are taken as the min and max values and `nvalues` are linearly spaced between those. nvalues : int Number of parameter grid points to use. Returns ------- likelihood_profile : dict Dict of parameter values and likelihood values. """ self._model = model.copy() likelihood = [] if values is None: if isinstance(bounds, tuple): parmin, parmax = bounds else: parerr = model.parameters.error(parameter) parval = model.parameters[parameter].value parmin, parmax = parval - bounds * parerr, parval + bounds * parerr values = np.linspace(parmin, parmax, nvalues) for value in values: self._model.parameters[parameter].value = value stat = self.total_stat(self._model.parameters) likelihood.append(stat) return {"values": values, "likelihood": np.array(likelihood)}
[docs] def optimize(self, backend="minuit", **kwargs): """Run the optimization Parameters ---------- backend : {"minuit", "sherpa"} Which fitting backend to use. **kwargs : dict Keyword arguments passed to the optimizer. For the `"minuit"` backend see https://iminuit.readthedocs.io/en/latest/api.html#iminuit.Minuit for a detailed description of the available options. For the `"sherpa"` backend you can from the options `method = {"simplex", "levmar", "moncar", "gridsearch"}` Those methods are described and compared in detail on http://cxc.cfa.harvard.edu/sherpa/methods/index.html. The available options of the optimization methods are described on the following pages in detail: * http://cxc.cfa.harvard.edu/sherpa/ahelp/neldermead.html * http://cxc.cfa.harvard.edu/sherpa/ahelp/montecarlo.html * http://cxc.cfa.harvard.edu/sherpa/ahelp/gridsearch.html * http://cxc.cfa.harvard.edu/sherpa/ahelp/levmar.html Returns ------- fit_result : `dict` Optimize info dict with the best fit model and additional information. """ parameters = self._model.parameters if parameters.apply_autoscale: parameters.autoscale() optimize = self._optimize_funcs[backend] factors, info, optimizer = optimize( parameters=parameters, function=self.total_stat, **kwargs ) # As preliminary solution would like to provide a possibility that the user # can access the Minuit object, because it features a lot useful functionality if backend == "minuit": self.minuit = optimizer # Copy final results into the parameters object parameters.set_parameter_factors(factors) return dict( model=self._model.copy(), total_stat=self.total_stat(self._model.parameters), backend=backend, method=kwargs.get("method", backend), **info )
# TODO: this is a preliminary solution to restore the old behaviour, that's # why the method is hidden. def _estimate_errors(self, model): """Run the error estimation""" parameters = model.parameters if hasattr(self, "minuit"): covar = _get_covar(self.minuit) parameters.set_covariance_factors(covar) self._model.parameters.set_covariance_factors(covar) else: log.warning( "No covariance matrix found. Error estimation currently" " only works with iminuit backend." ) parameters.covariance = None return model
[docs] def run(self, steps="all", optimize_opts=None, profile_opts=None): """ Run all fitting steps. Parameters ---------- steps : {"all", "optimize", "errors", "profiles"} Which fitting steps to run. optimize_opts : dict Options passed to `Fit.optimize`. profile_opts : dict Options passed to `Fit.likelihood_profiles`. Returns ------- fit_result : `FitResult` Fit result object with the best fit model and additional information. """ if steps == "all": steps = ["optimize", "errors"] if "optimize" in steps: if optimize_opts == None: optimize_opts = {} result = self.optimize(**optimize_opts) if "errors" in steps: result["model"] = self._estimate_errors(result["model"]) if "profiles" in steps: if profile_opts == None: profile_opts = {} result["likelihood_profiles"] = self.likelihood_profiles( result["model"], **profile_opts ) return FitResult(**result)
class FitResult(object): """Fit result object.""" def __init__( self, model, success, nfev, total_stat, message, backend, method, likelihood_profiles=None, ): self._model = model self._success = success self._nfev = nfev self._total_stat = total_stat self._message = message self._backend = backend self._method = method self._likelihood_profiles = likelihood_profiles @property def model(self): """Best fit model.""" return self._model @property def success(self): """Fit success status flag.""" return self._success @property def nfev(self): """Number of function evaluations until convergence or stop.""" return self._nfev @property def total_stat(self): """Value of the fit statistic at minimum.""" return self._total_stat @property def message(self): """Optimizer status message.""" return self._message @property def backend(self): """Optimizer backend used for the fit.""" return self._backend @property def method(self): """Optimizer method used for the fit.""" return self._method def __repr__(self): str_ = self.__class__.__name__ str_ += "\n\n" str_ += "\tbackend : {}\n".format(self.backend) str_ += "\tmethod : {}\n".format(self.method) str_ += "\tsuccess : {}\n".format(self.success) str_ += "\tnfev : {}\n".format(self.nfev) str_ += "\ttotal stat : {:.2f}\n".format(self.total_stat) str_ += "\tmessage : {}\n".format(self.message) return str_ @property def likelihood_profiles(self): """Likelihood profiles for paramaters.""" return self._likelihood_profiles def plot_likelihood_profile(self, parameter, ax=None, **kwargs): """Plot likelihood profile for a given parameter. Parameters ---------- parameter : str Parameter to plot profile for. Returns ------- ax : `matplotlib.pyplot.Axes` Axes object. """ import matplotlib.pyplot as plt if ax is None: ax = plt.gca() ts_diff = self.likelihood_profiles[parameter]["likelihood"] - self.total_stat values = self.likelihood_profiles[parameter]["values"] ax.plot(values, ts_diff, **kwargs) unit = self.model.parameters[parameter].unit ax.set_xlabel(parameter + "[unit]".format(unit=unit)) ax.set_ylabel("TS difference") return ax