Source code for gammapy.datasets.flux_points

# Licensed under a 3-clause BSD style license - see LICENSE.rst
import logging
import numpy as np
from astropy import units as u
from astropy.table import Table
from gammapy.modeling.models import DatasetModels
from gammapy.utils.scripts import make_name, make_path
from .core import Dataset
from .utils import get_axes

log = logging.getLogger(__name__)

__all__ = ["FluxPointsDataset"]


[docs]class FluxPointsDataset(Dataset): """ Fit a set of flux points with a parametric model. Parameters ---------- models : `~gammapy.modeling.models.Models` Models (only spectral part needs to be set) data : `~gammapy.estimators.FluxPoints` Flux points. mask_fit : `numpy.ndarray` Mask to apply for fitting mask_safe : `numpy.ndarray` Mask defining the safe data range. meta_table : `~astropy.table.Table` Table listing informations on observations used to create the dataset. One line per observation for stacked datasets. Examples -------- Load flux points from file and fit with a power-law model:: from gammapy.modeling import Fit from gammapy.modeling.models import PowerLawSpectralModel, SkyModel from gammapy.estimators import FluxPoints from gammapy.datasets import FluxPointsDataset filename = "$GAMMAPY_DATA/tests/spectrum/flux_points/diff_flux_points.fits" flux_points = FluxPoints.read(filename) model = SkyModel(spectral_model=PowerLawSpectralModel()) dataset = FluxPointsDataset(model, flux_points) fit = Fit([dataset]) result = fit.run() print(result) print(result.parameters.to_table()) Note: In order to reproduce the example you need the tests datasets folder. You may download it with the command ``gammapy download datasets --tests --out $GAMMAPY_DATA`` """ stat_type = "chi2" tag = "FluxPointsDataset" def __init__( self, models=None, data=None, mask_fit=None, mask_safe=None, name=None, meta_table=None, ): self.data = data self.mask_fit = mask_fit self._name = make_name(name) self.models = models self.meta_table = meta_table if data.sed_type != "dnde": raise ValueError("Currently only flux points of type 'dnde' are supported.") if mask_safe is None: mask_safe = np.isfinite(data.table["dnde"]) self.mask_safe = mask_safe @property def name(self): return self._name @property def models(self): return self._models @models.setter def models(self, models): if models is None: self._models = None else: self._models = DatasetModels(models).select(self.name)
[docs] def write(self, filename, overwrite=True, **kwargs): """Write flux point dataset to file. Parameters ---------- filename : str Filename to write to. overwrite : bool Overwrite existing file. **kwargs : dict Keyword arguments passed to `~astropy.table.Table.write`. """ table = self.data.table.copy() if self.mask_fit is None: mask_fit = self.mask_safe else: mask_fit = self.mask_fit table["mask_fit"] = mask_fit table["mask_safe"] = self.mask_safe table.write(make_path(filename), overwrite=overwrite, **kwargs)
[docs] @classmethod def from_dict(cls, data, **kwargs): """Create flux point dataset from dict. Parameters ---------- data : dict Dict containing data to create dataset from. Returns ------- dataset : `FluxPointsDataset` Flux point datasets. """ from gammapy.estimators import FluxPoints filename = make_path(data["filename"]) table = Table.read(filename) mask_fit = table["mask_fit"].data.astype("bool") mask_safe = table["mask_safe"].data.astype("bool") table.remove_columns(["mask_fit", "mask_safe"]) return cls( name=data["name"], data=FluxPoints(table), mask_fit=mask_fit, mask_safe=mask_safe, )
[docs] def to_dict(self, filename=""): """Convert to dict for YAML serialization.""" return { "name": self.name, "type": self.tag, "filename": str(filename), }
def __str__(self): str_ = f"{self.__class__.__name__}\n" str_ += "-" * len(self.__class__.__name__) + "\n" str_ += "\n" str_ += "\t{:32}: {} \n\n".format("Name", self.name) # data section n_bins = 0 if self.data is not None: n_bins = len(self.data.table) str_ += "\t{:32}: {} \n".format("Number of total flux points", n_bins) n_fit_bins = 0 if self.mask is not None: n_fit_bins = np.sum(self.mask.data) str_ += "\t{:32}: {} \n\n".format("Number of fit bins", n_fit_bins) # likelihood section str_ += "\t{:32}: {}\n".format("Fit statistic type", self.stat_type) stat = np.nan if self.data is not None and self.models is not None: stat = self.stat_sum() str_ += "\t{:32}: {:.2f}\n\n".format("Fit statistic value (-2 log(L))", stat) # model section n_models = 0 if self.models is not None: n_models = len(self.models) str_ += "\t{:32}: {} \n".format("Number of models", n_models) str_ += "\t{:32}: {}\n".format( "Number of parameters", len(self.models.parameters) ) str_ += "\t{:32}: {}\n\n".format( "Number of free parameters", len(self.models.parameters.free_parameters) ) if self.models is not None: str_ += "\t" + "\n\t".join(str(self.models).split("\n")[2:]) return str_.expandtabs(tabsize=2)
[docs] def data_shape(self): """Shape of the flux points data (tuple).""" return self.data.energy_ref.shape
[docs] def flux_pred(self): """Compute predicted flux.""" flux = 0.0 for model in self.models: flux += model.spectral_model(self.data.energy_ref) return flux
[docs] def stat_array(self): """Fit statistic array.""" model = self.flux_pred() data = self.data.table["dnde"].quantity sigma = self.data.table["dnde_err"].quantity return ((data - model) / sigma).to_value("") ** 2
[docs] def residuals(self, method="diff"): """Compute the flux point residuals (). Parameters ---------- method: {"diff", "diff/model", "diff/sqrt(model)"} Method used to compute the residuals. Available options are: - `diff` (default): data - model - `diff/model`: (data - model) / model - `diff/sqrt(model)`: (data - model) / sqrt(model) - `norm='sqrt_model'` for: (flux points - model)/sqrt(model) Returns ------- residuals : `~numpy.ndarray` Residuals array. """ fp = self.data data = fp.table[fp.sed_type] model = self.flux_pred() residuals = self._compute_residuals(data, model, method) # Remove residuals for upper_limits residuals[fp.is_ul] = np.nan return residuals
[docs] def plot_fit(self, ax_spectrum=None, ax_residuals=None, kwargs_spectrum=None, kwargs_residuals=None): """Plot flux points, best fit model and residuals in two panels. Calls `~FluxPointsDataset.plot_spectrum` and `~FluxPointsDataset.plot_residuals`. Parameters ---------- ax_spectrum : `~matplotlib.axes.Axes` Axes to plot flux points and best fit model on. ax_residuals : `~matplotlib.axes.Axes` Axes to plot residuals on. kwargs_spectrum : dict Keyword arguments passed to `~FluxPointsDataset.plot_spectrum`. kwargs_residuals : dict Keyword arguments passed to `~FluxPointsDataset.plot_residuals`. Returns ------- ax_spectrum, ax_residuals : `~matplotlib.axes.Axes` Flux points, best fit model and residuals plots. """ from matplotlib.gridspec import GridSpec gs = GridSpec(7, 1) ax_spectrum, ax_residuals = get_axes( ax_spectrum, ax_residuals, 8, 7, [gs[:5, :]], [gs[5:, :]], kwargs2={"sharex": ax_spectrum} ) kwargs_spectrum = kwargs_spectrum or {} kwargs_residuals = kwargs_residuals or {} kwargs_residuals.setdefault("method", "diff/model") self.plot_spectrum(ax_spectrum, **kwargs_spectrum) ax_spectrum.label_outer() self.plot_residuals(ax_residuals, **kwargs_residuals) method = kwargs_residuals["method"] label = self._residuals_labels[method] unit = self.data._plot_get_flux_err(self.data.sed_type)[0].unit if method == "diff" else "" ax_residuals.set_ylabel("Residuals\n" + label + (f"\n[{unit}]" if unit else "")) return ax_spectrum, ax_residuals
@property def _energy_range(self): try: return u.Quantity([self.data.energy_min.min(), self.data.energy_max.max()]) except KeyError: return u.Quantity([self.data.energy_ref.min(), self.data.energy_ref.max()]) @property def _energy_unit(self): return self.data.energy_ref.unit
[docs] def plot_residuals(self, ax=None, method="diff", **kwargs): """Plot flux point residuals. Parameters ---------- ax : `~matplotlib.axes.Axes` Axes to plot on. method : {"diff", "diff/model"} Normalization used to compute the residuals, see `FluxPointsDataset.residuals`. **kwargs : dict Keyword arguments passed to `~matplotlib.axes.Axes.errorbar`. Returns ------- ax : `~matplotlib.axes.Axes` Axes object. """ import matplotlib.pyplot as plt ax = ax or plt.gca() fp = self.data residuals = self.residuals(method) xerr = fp._plot_get_energy_err() if xerr is not None: xerr = ( xerr[0].to_value(self._energy_unit), xerr[1].to_value(self._energy_unit), ) yerr = fp._plot_get_flux_err(fp.sed_type) if method == "diff": unit = yerr[0].unit yerr = yerr[0].to_value(unit), yerr[1].to_value(unit) elif method == "diff/model": model = self.flux_pred() unit = "" yerr = (yerr[0] / model).to_value(unit), (yerr[1] / model).to_value(unit) else: raise ValueError('Invalid method, choose between "diff" and "diff/model"') kwargs.setdefault("color", kwargs.pop("c", "black")) kwargs.setdefault("marker", "+") kwargs.setdefault("linestyle", kwargs.pop("ls", "none")) ax.errorbar(fp.energy_ref.value, residuals.value, xerr=xerr, yerr=yerr, **kwargs) ax.axhline(0, color=kwargs["color"], lw=0.5) # format axes ax.set_xlabel(f"Energy [{self._energy_unit}]") ax.set_xscale("log") ax.set_xlim(self._energy_range.to_value(self._energy_unit)) label = self._residuals_labels[method] ax.set_ylabel(f"Residuals ({label}){f' [{unit}]' if unit else ''}") ymin = 1.05 * np.nanmin(residuals.value - yerr[0]) ymax = 1.05 * np.nanmax(residuals.value + yerr[1]) ax.set_ylim(ymin, ymax) return ax
[docs] def plot_spectrum(self, ax=None, kwargs_fp=None, kwargs_model=None, **kwargs): """Plot spectrum including flux points and model. Parameters ---------- ax : `~matplotlib.axes.Axes` Axes to plot on. kwargs_fp : dict Keyword arguments passed to `gammapy.estimators.FluxPoints.plot`. kwargs_model : dict Keyword arguments passed to `gammapy.modeling.models.SpectralModel.plot` and `gammapy.modeling.models.SpectralModel.plot_error`. **kwargs: dict Keyword arguments passed to all plot methods. Returns ------- ax : `~matplotlib.axes.Axes` Axes object. """ kwargs_fp = kwargs_fp or {} kwargs_model = kwargs_model or {} kwargs.setdefault("energy_power", 2) kwargs.setdefault("energy_unit", "TeV") kwargs.setdefault("flux_unit", "erg-1 cm-2 s-1") # plot flux points plot_kwargs = kwargs.copy() plot_kwargs.update(kwargs_fp) plot_kwargs.setdefault("label", "Flux points") ax = self.data.plot(ax, **plot_kwargs) plot_kwargs = kwargs.copy() plot_kwargs.update(kwargs_model) plot_kwargs.setdefault("energy_range", self._energy_range) plot_kwargs.setdefault("label", "Best fit model") plot_kwargs.setdefault("zorder", 10) for model in self.models: if model.datasets_names is None or self.name in model.datasets_names: model.spectral_model.plot(ax=ax, **plot_kwargs) plot_kwargs.setdefault("color", plot_kwargs.pop("c", ax.lines[-1].get_color())) del plot_kwargs["label"] for model in self.models: if model.datasets_names is None or self.name in model.datasets_names: if not np.all(model == 0): model.spectral_model.plot_error(ax=ax, **plot_kwargs) # format axes ax.set_xlim(self._energy_range.to_value(self._energy_unit)) return ax