Exemplo n.º 1
0
class TemplateNPredModel(Model):
    """Background model.

    Create a new map by a tilt and normalization on the available map

    Parameters
    ----------
    map : `~gammapy.maps.Map`
        Background model map
    spectral_model : `~gammapy.modeling.models.SpectralModel`
        Normalized spectral model,
        default is `~gammapy.modeling.models.PowerLawNormSpectralModel`
    """

    tag = "TemplateNPredModel"
    map = LazyFitsData(cache=True)

    def __init__(
        self,
        map,
        spectral_model=None,
        name=None,
        filename=None,
        datasets_names=None,
    ):
        if isinstance(map, Map):
            axis = map.geom.axes["energy"]
            if axis.node_type != "edges":
                raise ValueError(
                    'Need an integrated map, energy axis node_type="edges"')

        self.map = map
        self._name = make_name(name)
        self.filename = filename

        if spectral_model is None:
            spectral_model = PowerLawNormSpectralModel()
            spectral_model.tilt.frozen = True

        self.spectral_model = spectral_model

        if isinstance(datasets_names, str):
            datasets_names = [datasets_names]

        if isinstance(datasets_names, list):
            if len(datasets_names) != 1:
                raise ValueError(
                    "Currently background models can only be assigned to one dataset."
                )
        self.datasets_names = datasets_names
        super().__init__()

    @property
    def name(self):
        return self._name

    @property
    def energy_center(self):
        """True energy axis bin centers (`~astropy.units.Quantity`)"""
        energy_axis = self.map.geom.axes["energy"]
        energy = energy_axis.center
        return energy[:, np.newaxis, np.newaxis]

    @property
    def spectral_model(self):
        """`~gammapy.modeling.models.SpectralModel`"""
        return self._spectral_model

    @spectral_model.setter
    def spectral_model(self, model):
        if not (model is None or isinstance(model, SpectralModel)):
            raise TypeError(f"Invalid type: {model!r}")
        self._spectral_model = model

    @property
    def parameters(self):
        parameters = []
        parameters.append(self.spectral_model.parameters)
        return Parameters.from_stack(parameters)

    def evaluate(self):
        """Evaluate background model.

        Returns
        -------
        background_map : `~gammapy.maps.Map`
            Background evaluated on the Map
        """
        value = self.spectral_model(self.energy_center).value
        back_values = self.map.data * value
        return self.map.copy(data=back_values)

    def to_dict(self, full_output=False):
        data = {}
        data["name"] = self.name
        data["type"] = self.tag
        data["spectral"] = self.spectral_model.to_dict(full_output)

        if self.filename is not None:
            data["filename"] = self.filename

        if self.datasets_names is not None:
            data["datasets_names"] = self.datasets_names

        return data

    @classmethod
    def from_dict(cls, data):
        from gammapy.modeling.models import SPECTRAL_MODEL_REGISTRY

        spectral_data = data.get("spectral")
        if spectral_data is not None:
            model_class = SPECTRAL_MODEL_REGISTRY.get_cls(
                spectral_data["type"])
            spectral_model = model_class.from_dict(spectral_data)
        else:
            spectral_model = None

        if "filename" in data:
            bkg_map = Map.read(data["filename"])
        elif "map" in data:
            bkg_map = data["map"]
        else:
            # TODO: for now create a fake map for serialization,
            # uptdated in MapDataset.from_dict()
            axis = MapAxis.from_edges(np.logspace(-1, 1, 2),
                                      unit=u.TeV,
                                      name="energy")
            geom = WcsGeom.create(skydir=(0, 0),
                                  npix=(1, 1),
                                  frame="galactic",
                                  axes=[axis])
            bkg_map = Map.from_geom(geom)

        return cls(
            map=bkg_map,
            spectral_model=spectral_model,
            name=data["name"],
            datasets_names=data.get("datasets_names"),
            filename=data.get("filename"),
        )

    def copy(self, name=None):
        """A deep copy."""
        new = copy.deepcopy(self)
        new._name = make_name(name)
        return new

    def cutout(self, position, width, mode="trim", name=None):
        """Cutout background model.

        Parameters
        ----------
        position : `~astropy.coordinates.SkyCoord`
            Center position of the cutout region.
        width : tuple of `~astropy.coordinates.Angle`
            Angular sizes of the region in (lon, lat) in that specific order.
            If only one value is passed, a square region is extracted.
        mode : {'trim', 'partial', 'strict'}
            Mode option for Cutout2D, for details see `~astropy.nddata.utils.Cutout2D`.
        name : str
            Name of the returned background model.

        Returns
        -------
        cutout : `TemplateNPredModel`
            Cutout background model.
        """
        cutout_kwargs = {"position": position, "width": width, "mode": mode}

        bkg_map = self.map.cutout(**cutout_kwargs)
        spectral_model = self.spectral_model.copy()
        return self.__class__(bkg_map,
                              spectral_model=spectral_model,
                              name=name)

    def stack(self, other, weights=None):
        """Stack background model in place.

        Stacking the background model resets the current parameters values.

        Parameters
        ----------
        other : `TemplateNPredModel`
            Other background model.
        """
        bkg = self.evaluate()
        other_bkg = other.evaluate()
        bkg.stack(other_bkg, weights=weights)
        self.map = bkg

        # reset parameter values
        self.spectral_model.norm.value = 1
        self.spectral_model.tilt.value = 0

    def __str__(self):
        str_ = self.__class__.__name__ + "\n\n"
        str_ += "\t{:26}: {}\n".format("Name", self.name)
        str_ += "\t{:26}: {}\n".format("Datasets names", self.datasets_names)

        str_ += "\tParameters:\n"
        info = _get_parameters_str(self.parameters)
        lines = info.split("\n")
        str_ += "\t" + "\n\t".join(lines[:-1])

        str_ += "\n\n"
        return str_.expandtabs(tabsize=2)

    @property
    def position(self):
        """`~astropy.coordinates.SkyCoord`"""
        return self.map.geom.center_skydir

    @property
    def evaluation_radius(self):
        """`~astropy.coordinates.Angle`"""
        return np.max(self.map.geom.width) / 2.0

    def freeze(self, model_type="spectral"):
        """Freeze model parameters"""
        if model_type is None or model_type == "spectral":
            self._spectral_model.freeze()

    def unfreeze(self, model_type="spectral"):
        """Restore parameters frozen status to default"""
        if model_type is None or model_type == "spectral":
            self._spectral_model.unfreeze()
Exemplo n.º 2
0
class Observation:
    """In-memory observation.

    Parameters
    ----------
    obs_id : int
        Observation id
    obs_info : dict
        Observation info dict
    aeff : `~gammapy.irf.EffectiveAreaTable2D`
        Effective area
    edisp : `~gammapy.irf.EnergyDispersion2D`
        Energy dispersion
    psf : `~gammapy.irf.PSF3D`
        Point spread function
    bkg : `~gammapy.irf.Background3D`
        Background rate model
    gti : `~gammapy.data.GTI`
        Table with GTI start and stop time
    events : `~gammapy.data.EventList`
        Event list
    obs_filter : `ObservationFilter`
        Observation filter.
    """

    aeff = LazyFitsData(cache=False)
    edisp = LazyFitsData(cache=False)
    psf = LazyFitsData(cache=False)
    bkg = LazyFitsData(cache=False)
    _events = LazyFitsData(cache=False)
    _gti = LazyFitsData(cache=False)

    def __init__(
        self,
        obs_id=None,
        obs_info=None,
        gti=None,
        aeff=None,
        edisp=None,
        psf=None,
        bkg=None,
        events=None,
        obs_filter=None,
    ):
        self.obs_id = obs_id
        self.obs_info = obs_info
        self.aeff = aeff
        self.edisp = edisp
        self.psf = psf
        self.bkg = bkg
        self._gti = gti
        self._events = events
        self.obs_filter = obs_filter or ObservationFilter()

    @property
    def events(self):
        events = self.obs_filter.filter_events(self._events)
        return events

    @property
    def gti(self):
        events = self.obs_filter.filter_gti(self._gti)
        return events

    @staticmethod
    def _get_obs_info(pointing, deadtime_fraction):
        """Create obs info dict from in memory data"""
        return {
            "RA_PNT": pointing.icrs.ra.deg,
            "DEC_PNT": pointing.icrs.dec.deg,
            "DEADC": 1 - deadtime_fraction,
        }

    @classmethod
    def create(
        cls,
        pointing,
        obs_id=0,
        livetime=None,
        tstart=None,
        tstop=None,
        irfs=None,
        deadtime_fraction=0.0,
        reference_time="2000-01-01",
    ):
        """Create an observation.

        User must either provide the livetime, or the start and stop times.

        Parameters
        ----------
        pointing : `~astropy.coordinates.SkyCoord`
            Pointing position
        obs_id : int
            Observation ID as identifier
        livetime : ~astropy.units.Quantity`
            Livetime exposure of the simulated observation
        tstart : `~astropy.units.Quantity`
            Start time of observation w.r.t reference_time
        tstop : `~astropy.units.Quantity` w.r.t reference_time
            Stop time of observation
        irfs : dict
            IRFs used for simulating the observation: `bkg`, `aeff`, `psf`, `edisp`
        deadtime_fraction : float, optional
            Deadtime fraction, defaults to 0
        reference_time : `~astropy.time.Time`
            the reference time to use in GTI definition

        Returns
        -------
        obs : `gammapy.data.MemoryObservation`
        """
        if tstart is None:
            tstart = Quantity(0.0, "hr")

        if tstop is None:
            tstop = tstart + Quantity(livetime)

        gti = GTI.create([tstart], [tstop], reference_time=reference_time)

        obs_info = cls._get_obs_info(pointing=pointing,
                                     deadtime_fraction=deadtime_fraction)

        return cls(
            obs_id=obs_id,
            obs_info=obs_info,
            gti=gti,
            aeff=irfs.get("aeff"),
            bkg=irfs.get("bkg"),
            edisp=irfs.get("edisp"),
            psf=irfs.get("psf"),
        )

    @classmethod
    def from_caldb(
        cls,
        pointing,
        obs_id=None,
        livetime=None,
        tstart=None,
        tstop=None,
        caldb="prod2",
        irf="South0.5hr",
        deadtime_fraction=0.0,
    ):
        """Create an observation using IRFs from a given CTA CALDB.

        Parameters
        ----------
        pointing : `~astropy.coordinates.SkyCoord`
            Pointing position
        obs_id : int
            Observation ID as identifier
        livetime : ~astropy.units.Quantity`
            Livetime exposure of the simulated observation
        tstart : `~astropy.units.Quantity`
            Start time of observation
        tstop : `~astropy.units.Quantity`
            Stop time of observation
        caldb : str
            Calibration database
        irf : str
            Type of Instrumental response function.
        deadtime_fraction : float, optional
            Deadtime fraction, defaults to 0

        Returns
        -------
        obs : `gammapy.data.Observation`
        """
        from .data_store import CalDBIRF

        irf_loc = CalDBIRF("CTA", caldb, irf)
        filename = irf_loc.file_dir + irf_loc.file_name
        irfs = load_cta_irfs(filename)
        return cls.create(
            pointing=pointing,
            obs_id=obs_id,
            livetime=livetime,
            tstart=tstart,
            tstop=tstop,
            irfs=irfs,
            deadtime_fraction=deadtime_fraction,
        )

    @property
    def tstart(self):
        """Observation start time (`~astropy.time.Time`)."""
        return self.gti.time_start[0]

    @property
    def tstop(self):
        """Observation stop time (`~astropy.time.Time`)."""
        return self.gti.time_stop[0]

    @property
    def observation_time_duration(self):
        """Observation time duration in seconds (`~astropy.units.Quantity`).

        The wall time, including dead-time.
        """
        return self.gti.time_sum

    @property
    def observation_live_time_duration(self):
        """Live-time duration in seconds (`~astropy.units.Quantity`).

        The dead-time-corrected observation time.

        Computed as ``t_live = t_observation * (1 - f_dead)``
        where ``f_dead`` is the dead-time fraction.
        """
        return self.observation_time_duration * (
            1 - self.observation_dead_time_fraction)

    @property
    def observation_dead_time_fraction(self):
        """Dead-time fraction (float).

        Defined as dead-time over observation time.

        Dead-time is defined as the time during the observation
        where the detector didn't record events:
        https://en.wikipedia.org/wiki/Dead_time
        https://ui.adsabs.harvard.edu/abs/2004APh....22..285F

        The dead-time fraction is used in the live-time computation,
        which in turn is used in the exposure and flux computation.
        """
        return 1 - self.obs_info["DEADC"]

    @property
    def pointing_radec(self):
        """Pointing RA / DEC sky coordinates (`~astropy.coordinates.SkyCoord`)."""
        lon, lat = (
            self.obs_info.get("RA_PNT", np.nan),
            self.obs_info.get("DEC_PNT", np.nan),
        )
        return SkyCoord(lon, lat, unit="deg", frame="icrs")

    @property
    def pointing_altaz(self):
        """Pointing ALT / AZ sky coordinates (`~astropy.coordinates.SkyCoord`)."""
        alt, az = (
            self.obs_info.get("ALT_PNT", np.nan),
            self.obs_info.get("AZ_PNT", np.nan),
        )
        return SkyCoord(az, alt, unit="deg", frame="altaz")

    @property
    def pointing_zen(self):
        """Pointing zenith angle sky (`~astropy.units.Quantity`)."""
        return Quantity(self.obs_info.get("ZEN_PNT", np.nan), unit="deg")

    @property
    def fixed_pointing_info(self):
        """Fixed pointing info for this observation (`FixedPointingInfo`)."""
        return FixedPointingInfo(self.events.table.meta)

    @property
    def target_radec(self):
        """Target RA / DEC sky coordinates (`~astropy.coordinates.SkyCoord`)."""
        lon, lat = (
            self.obs_info.get("RA_OBJ", np.nan),
            self.obs_info.get("DEC_OBJ", np.nan),
        )
        return SkyCoord(lon, lat, unit="deg", frame="icrs")

    @property
    def observatory_earth_location(self):
        """Observatory location (`~astropy.coordinates.EarthLocation`)."""
        return earth_location_from_dict(self.obs_info)

    @property
    def muoneff(self):
        """Observation muon efficiency."""
        return self.obs_info.get("MUONEFF", 1)

    def __str__(self):
        ra = self.pointing_radec.ra.deg
        dec = self.pointing_radec.dec.deg

        pointing = f"{ra:.1f} deg, {dec:.1f} deg\n"
        # TODO: Which target was observed?
        # TODO: print info about available HDUs for this observation ...
        return (
            f"{self.__class__.__name__}\n\n"
            f"\tobs id            : {self.obs_id} \n "
            f"\ttstart            : {self.tstart.mjd:.2f}\n"
            f"\ttstop             : {self.tstop.mjd:.2f}\n"
            f"\tduration          : {self.observation_time_duration:.2f}\n"
            f"\tpointing (icrs)   : {pointing}\n"
            f"\tdeadtime fraction : {self.observation_dead_time_fraction:.1%}\n"
        )

    def check(self, checks="all"):
        """Run checks.

        This is a generator that yields a list of dicts.
        """
        checker = ObservationChecker(self)
        return checker.run(checks=checks)

    def peek(self, figsize=(12, 10)):
        """Quick-look plots in a few panels.

        Parameters
        ----------
        figszie : tuple
            Figure size
        """
        import matplotlib.pyplot as plt

        fig, ((ax_aeff, ax_bkg), (ax_psf, ax_edisp)) = plt.subplots(
            nrows=2,
            ncols=2,
            figsize=figsize,
            gridspec_kw={
                "wspace": 0.25,
                "hspace": 0.25
            },
        )

        self.aeff.plot(ax=ax_aeff)

        try:
            if isinstance(self.bkg, Background3D):
                bkg = self.bkg.to_2d()
            else:
                bkg = self.bkg

            bkg.plot(ax=ax_bkg)
        except IndexError:
            logging.warning(
                f"No background model found for obs {self.obs_id}.")

        self.psf.plot_containment_vs_energy(ax=ax_psf)
        self.edisp.plot_bias(ax=ax_edisp, add_cbar=True)

        ax_aeff.set_title("Effective area")
        ax_bkg.set_title("Background rate")
        ax_psf.set_title("Point spread function")
        ax_edisp.set_title("Energy dispersion")

    def select_time(self, time_interval):
        """Select a time interval of the observation.

        Parameters
        ----------
        time_interval : `astropy.time.Time`
            Start and stop time of the selected time interval.
            For now we only support a single time interval.

        Returns
        -------
        new_obs : `~gammapy.data.Observation`
            A new observation instance of the specified time interval
        """
        new_obs_filter = self.obs_filter.copy()
        new_obs_filter.time_filter = time_interval
        obs = copy.deepcopy(self)
        obs.obs_filter = new_obs_filter
        return obs
Exemplo n.º 3
0
class Observation:
    """In-memory observation.

    Parameters
    ----------
    obs_id : int
        Observation id
    obs_info : dict
        Observation info dict
    aeff : `~gammapy.irf.EffectiveAreaTable2D`
        Effective area
    edisp : `~gammapy.irf.EnergyDispersion2D`
        Energy dispersion
    psf : `~gammapy.irf.PSF3D`
        Point spread function
    bkg : `~gammapy.irf.Background3D`
        Background rate model
    rad_max: `~gammapy.irf.RadMax2D` or `~astropy.units.Quantity`
        Only for point-like IRFs: RAD_MAX table (energy dependent RAD_MAX)
        or a single angle (global RAD_MAX)
    gti : `~gammapy.data.GTI`
        Table with GTI start and stop time
    events : `~gammapy.data.EventList`
        Event list
    obs_filter : `ObservationFilter`
        Observation filter.
    """

    aeff = LazyFitsData(cache=False)
    edisp = LazyFitsData(cache=False)
    psf = LazyFitsData(cache=False)
    bkg = LazyFitsData(cache=False)
    rad_max = LazyFitsData(cache=False)
    _events = LazyFitsData(cache=False)
    _gti = LazyFitsData(cache=False)

    def __init__(
        self,
        obs_id=None,
        obs_info=None,
        gti=None,
        aeff=None,
        edisp=None,
        psf=None,
        bkg=None,
        rad_max=None,
        events=None,
        obs_filter=None,
    ):
        self.obs_id = obs_id
        self.obs_info = obs_info
        self.aeff = aeff
        self.edisp = edisp
        self.psf = psf
        self.bkg = bkg
        self.rad_max = rad_max
        self._gti = gti
        self._events = events
        self.obs_filter = obs_filter or ObservationFilter()

    @property
    def available_irfs(self):
        """Which irfs are available"""
        available_irf = []

        for irf in ["aeff", "edisp", "psf", "bkg"]:
            available = self.__dict__.get(irf, False)
            available_hdu = self.__dict__.get(f"_{irf}_hdu", False)

            if available or available_hdu:
                available_irf.append(irf)

        return available_irf

    @property
    def events(self):
        events = self.obs_filter.filter_events(self._events)
        return events

    @property
    def gti(self):
        gti = self.obs_filter.filter_gti(self._gti)
        return gti

    @staticmethod
    def _get_obs_info(pointing, deadtime_fraction):
        """Create obs info dict from in memory data"""
        return {
            "RA_PNT": pointing.icrs.ra.deg,
            "DEC_PNT": pointing.icrs.dec.deg,
            "DEADC": 1 - deadtime_fraction,
        }

    @classmethod
    def create(
        cls,
        pointing,
        obs_id=0,
        livetime=None,
        tstart=None,
        tstop=None,
        irfs=None,
        deadtime_fraction=0.0,
        reference_time="2000-01-01",
    ):
        """Create an observation.

        User must either provide the livetime, or the start and stop times.

        Parameters
        ----------
        pointing : `~astropy.coordinates.SkyCoord`
            Pointing position
        obs_id : int
            Observation ID as identifier
        livetime : ~astropy.units.Quantity`
            Livetime exposure of the simulated observation
        tstart : `~astropy.units.Quantity`
            Start time of observation w.r.t reference_time
        tstop : `~astropy.units.Quantity` w.r.t reference_time
            Stop time of observation
        irfs : dict
            IRFs used for simulating the observation: `bkg`, `aeff`, `psf`, `edisp`
        deadtime_fraction : float, optional
            Deadtime fraction, defaults to 0
        reference_time : `~astropy.time.Time`
            the reference time to use in GTI definition

        Returns
        -------
        obs : `gammapy.data.MemoryObservation`
        """
        if tstart is None:
            tstart = Quantity(0.0, "hr")

        if tstop is None:
            tstop = tstart + Quantity(livetime)

        gti = GTI.create([tstart], [tstop], reference_time=reference_time)

        obs_info = cls._get_obs_info(pointing=pointing,
                                     deadtime_fraction=deadtime_fraction)

        return cls(
            obs_id=obs_id,
            obs_info=obs_info,
            gti=gti,
            aeff=irfs.get("aeff"),
            bkg=irfs.get("bkg"),
            edisp=irfs.get("edisp"),
            psf=irfs.get("psf"),
        )

    @property
    def tstart(self):
        """Observation start time (`~astropy.time.Time`)."""
        return self.gti.time_start[0]

    @property
    def tstop(self):
        """Observation stop time (`~astropy.time.Time`)."""
        return self.gti.time_stop[0]

    @property
    def observation_time_duration(self):
        """Observation time duration in seconds (`~astropy.units.Quantity`).

        The wall time, including dead-time.
        """
        return self.gti.time_sum

    @property
    def observation_live_time_duration(self):
        """Live-time duration in seconds (`~astropy.units.Quantity`).

        The dead-time-corrected observation time.

        Computed as ``t_live = t_observation * (1 - f_dead)``
        where ``f_dead`` is the dead-time fraction.
        """
        return self.observation_time_duration * (
            1 - self.observation_dead_time_fraction)

    @property
    def observation_dead_time_fraction(self):
        """Dead-time fraction (float).

        Defined as dead-time over observation time.

        Dead-time is defined as the time during the observation
        where the detector didn't record events:
        https://en.wikipedia.org/wiki/Dead_time
        https://ui.adsabs.harvard.edu/abs/2004APh....22..285F

        The dead-time fraction is used in the live-time computation,
        which in turn is used in the exposure and flux computation.
        """
        return 1 - self.obs_info["DEADC"]

    @property
    def pointing_radec(self):
        """Pointing RA / DEC sky coordinates (`~astropy.coordinates.SkyCoord`)."""
        lon, lat = (
            self.obs_info.get("RA_PNT", np.nan),
            self.obs_info.get("DEC_PNT", np.nan),
        )
        return SkyCoord(lon, lat, unit="deg", frame="icrs")

    @property
    def pointing_altaz(self):
        """Pointing ALT / AZ sky coordinates (`~astropy.coordinates.SkyCoord`)."""
        alt, az = (
            self.obs_info.get("ALT_PNT", np.nan),
            self.obs_info.get("AZ_PNT", np.nan),
        )
        return SkyCoord(az, alt, unit="deg", frame="altaz")

    @property
    def pointing_zen(self):
        """Pointing zenith angle sky (`~astropy.units.Quantity`)."""
        return Quantity(self.obs_info.get("ZEN_PNT", np.nan), unit="deg")

    @property
    def fixed_pointing_info(self):
        """Fixed pointing info for this observation (`FixedPointingInfo`)."""
        return FixedPointingInfo(self.events.table.meta)

    @property
    def target_radec(self):
        """Target RA / DEC sky coordinates (`~astropy.coordinates.SkyCoord`)."""
        lon, lat = (
            self.obs_info.get("RA_OBJ", np.nan),
            self.obs_info.get("DEC_OBJ", np.nan),
        )
        return SkyCoord(lon, lat, unit="deg", frame="icrs")

    @property
    def observatory_earth_location(self):
        """Observatory location (`~astropy.coordinates.EarthLocation`)."""
        return earth_location_from_dict(self.obs_info)

    @property
    def muoneff(self):
        """Observation muon efficiency."""
        return self.obs_info.get("MUONEFF", 1)

    def __str__(self):
        ra = self.pointing_radec.ra.deg
        dec = self.pointing_radec.dec.deg

        pointing = f"{ra:.1f} deg, {dec:.1f} deg\n"
        # TODO: Which target was observed?
        # TODO: print info about available HDUs for this observation ...
        return (
            f"{self.__class__.__name__}\n\n"
            f"\tobs id            : {self.obs_id} \n "
            f"\ttstart            : {self.tstart.mjd:.2f}\n"
            f"\ttstop             : {self.tstop.mjd:.2f}\n"
            f"\tduration          : {self.observation_time_duration:.2f}\n"
            f"\tpointing (icrs)   : {pointing}\n"
            f"\tdeadtime fraction : {self.observation_dead_time_fraction:.1%}\n"
        )

    def check(self, checks="all"):
        """Run checks.

        This is a generator that yields a list of dicts.
        """
        checker = ObservationChecker(self)
        return checker.run(checks=checks)

    def peek(self, figsize=(12, 10)):
        """Quick-look plots in a few panels.

        Parameters
        ----------
        figsize : tuple
            Figure size
        """
        import matplotlib.pyplot as plt

        n_irfs = len(self.available_irfs)

        fig, axes = plt.subplots(
            nrows=n_irfs // 2,
            ncols=2 + n_irfs % 2,
            figsize=figsize,
            gridspec_kw={
                "wspace": 0.25,
                "hspace": 0.25
            },
        )

        axes_dict = dict(zip(self.available_irfs, axes.flatten()))

        if "aeff" in self.available_irfs:
            self.aeff.plot(ax=axes_dict["aeff"])
            axes_dict["aeff"].set_title("Effective area")

        if "bkg" in self.available_irfs:
            bkg = self.bkg

            if not bkg.is_offset_dependent:
                bkg = bkg.to_2d()

            bkg.plot(ax=axes_dict["bkg"])
            axes_dict["bkg"].set_title("Background rate")
        else:
            logging.warning(
                f"No background model found for obs {self.obs_id}.")

        if "psf" in self.available_irfs:
            self.psf.plot_containment_radius_vs_energy(ax=axes_dict["psf"])
            axes_dict["psf"].set_title("Point spread function")
        else:
            logging.warning(f"No PSF found for obs {self.obs_id}.")

        if "edisp" in self.available_irfs:
            self.edisp.plot_bias(ax=axes_dict["edisp"], add_cbar=True)
            axes_dict["edisp"].set_title("Energy dispersion")
        else:
            logging.warning(
                f"No energy dispersion found for obs {self.obs_id}.")

    def select_time(self, time_interval):
        """Select a time interval of the observation.

        Parameters
        ----------
        time_interval : `astropy.time.Time`
            Start and stop time of the selected time interval.
            For now we only support a single time interval.

        Returns
        -------
        new_obs : `~gammapy.data.Observation`
            A new observation instance of the specified time interval
        """
        new_obs_filter = self.obs_filter.copy()
        new_obs_filter.time_filter = time_interval
        obs = copy.deepcopy(self)
        obs.obs_filter = new_obs_filter
        return obs

    @classmethod
    def read(cls, event_file, irf_file=None):
        """Create an Observation from a Event List and an (optional) IRF file.

        Parameters
        ----------
        event_file : str, Path
            path to the .fits file containing the event list and the GTI
        irf_file : str, Path
            (optional) path to the .fits file containing the IRF components,
            if not provided the IRF will be read from the event file

        Returns
        -------
        observation : `~gammapy.data.Observation`
            observation with the events and the irf read from the file
        """
        from gammapy.irf.io import load_irf_dict_from_file

        events = EventList.read(event_file)

        gti = GTI.read(event_file)

        irf_file = irf_file if irf_file is not None else event_file
        irf_dict = load_irf_dict_from_file(irf_file)

        obs_info = events.table.meta
        return cls(
            events=events,
            gti=gti,
            obs_info=obs_info,
            obs_id=obs_info.get("OBS_ID"),
            **irf_dict,
        )
Exemplo n.º 4
0
class Observation:
    """In-memory observation.

    Parameters
    ----------
    obs_id : int
        Observation id
    obs_info : dict
        Observation info dict
    aeff : `~gammapy.irf.EffectiveAreaTable2D`
        Effective area
    edisp : `~gammapy.irf.EnergyDispersion2D`
        Energy dispersion
    psf : `~gammapy.irf.PSF3D`
        Point spread function
    bkg : `~gammapy.irf.Background3D`
        Background rate model
    rad_max: `~gammapy.irf.RadMax2D`
        Only for point-like IRFs: RAD_MAX table (energy dependent RAD_MAX)
        For a fixed RAD_MAX, create a RadMax2D with a single bin.
    gti : `~gammapy.data.GTI`
        Table with GTI start and stop time
    events : `~gammapy.data.EventList`
        Event list
    obs_filter : `ObservationFilter`
        Observation filter.
    """

    aeff = LazyFitsData(cache=False)
    edisp = LazyFitsData(cache=False)
    psf = LazyFitsData(cache=False)
    bkg = LazyFitsData(cache=False)
    _rad_max = LazyFitsData(cache=False)
    _events = LazyFitsData(cache=False)
    _gti = LazyFitsData(cache=False)

    def __init__(
        self,
        obs_id=None,
        obs_info=None,
        gti=None,
        aeff=None,
        edisp=None,
        psf=None,
        bkg=None,
        rad_max=None,
        events=None,
        obs_filter=None,
    ):
        self.obs_id = obs_id
        self._obs_info = obs_info
        self.aeff = aeff
        self.edisp = edisp
        self.psf = psf
        self.bkg = bkg
        self._rad_max = rad_max
        self._gti = gti
        self._events = events
        self.obs_filter = obs_filter or ObservationFilter()

    @property
    def rad_max(self):
        # prevent circular import
        from gammapy.irf import RadMax2D

        if self._rad_max is not None:
            return self._rad_max

        # load once to avoid trigger lazy loading it three times
        aeff = self.aeff
        if aeff is not None and aeff.is_pointlike:
            self._rad_max = RadMax2D.from_irf(aeff)
            return self._rad_max

        edisp = self.edisp
        if edisp is not None and edisp.is_pointlike:
            self._rad_max = RadMax2D.from_irf(self.edisp)

        return self._rad_max

    @property
    def available_hdus(self):
        """Which HDUs are available"""
        available_hdus = []
        keys = ["_events", "_gti", "aeff", "edisp", "psf", "bkg", "_rad_max"]
        hdus = ["events", "gti", "aeff", "edisp", "psf", "bkg", "rad_max"]
        for key, hdu in zip(keys, hdus):
            available = self.__dict__.get(key, False)
            available_hdu = self.__dict__.get(f"_{hdu}_hdu", False)
            available_hdu_ = self.__dict__.get(f"_{key}_hdu", False)
            if available or available_hdu or available_hdu_:
                available_hdus.append(hdu)
        return available_hdus

    @property
    def available_irfs(self):
        """Which IRFs are available"""
        return [_ for _ in self.available_hdus if _ not in ["events", "gti"]]

    @property
    def events(self):
        events = self.obs_filter.filter_events(self._events)
        return events

    @property
    def gti(self):
        gti = self.obs_filter.filter_gti(self._gti)
        return gti

    @staticmethod
    def _get_obs_info(pointing, deadtime_fraction, time_start, time_stop,
                      reference_time, location):
        """Create obs info dict from in memory data"""
        obs_info = {
            "RA_PNT": pointing.icrs.ra.deg,
            "DEC_PNT": pointing.icrs.dec.deg,
            "DEADC": 1 - deadtime_fraction,
        }
        obs_info.update(time_ref_to_dict(reference_time))
        obs_info['TSTART'] = time_relative_to_ref(time_start,
                                                  obs_info).to_value(u.s)
        obs_info['TSTOP'] = time_relative_to_ref(time_stop,
                                                 obs_info).to_value(u.s)

        if location is not None:
            obs_info.update(earth_location_to_dict(location))

        return obs_info

    @classmethod
    def create(
            cls,
            pointing,
            location=None,
            obs_id=0,
            livetime=None,
            tstart=None,
            tstop=None,
            irfs=None,
            deadtime_fraction=0.0,
            reference_time=Time("2000-01-01 00:00:00"),
    ):
        """Create an observation.

        User must either provide the livetime, or the start and stop times.

        Parameters
        ----------
        pointing : `~astropy.coordinates.SkyCoord`
            Pointing position
        obs_id : int
            Observation ID as identifier
        livetime : ~astropy.units.Quantity`
            Livetime exposure of the simulated observation
        tstart: `~astropy.time.Time` or `~astropy.units.Quantity`
            Start time of observation as `~astropy.time.Time` or duration
            relative to `reference_time`
        tstop: `astropy.time.Time` or `~astropy.units.Quantity`
            Stop time of observation as `~astropy.time.Time` or duration
            relative to `reference_time`
        irfs: dict
            IRFs used for simulating the observation: `bkg`, `aeff`, `psf`, `edisp`
        deadtime_fraction : float, optional
            Deadtime fraction, defaults to 0
        reference_time : `~astropy.time.Time`
            the reference time to use in GTI definition

        Returns
        -------
        obs : `gammapy.data.MemoryObservation`
        """
        if tstart is None:
            tstart = reference_time.copy()

        if tstop is None:
            tstop = tstart + Quantity(livetime)

        gti = GTI.create(tstart, tstop, reference_time=reference_time)

        obs_info = cls._get_obs_info(
            pointing=pointing,
            deadtime_fraction=deadtime_fraction,
            time_start=gti.time_start[0],
            time_stop=gti.time_stop[0],
            reference_time=reference_time,
            location=location,
        )

        return cls(
            obs_id=obs_id,
            obs_info=obs_info,
            gti=gti,
            aeff=irfs.get("aeff"),
            bkg=irfs.get("bkg"),
            edisp=irfs.get("edisp"),
            psf=irfs.get("psf"),
        )

    @property
    def tstart(self):
        """Observation start time (`~astropy.time.Time`)."""
        return self.gti.time_start[0]

    @property
    def tstop(self):
        """Observation stop time (`~astropy.time.Time`)."""
        return self.gti.time_stop[0]

    @property
    def observation_time_duration(self):
        """Observation time duration in seconds (`~astropy.units.Quantity`).

        The wall time, including dead-time.
        """
        return self.gti.time_sum

    @property
    def observation_live_time_duration(self):
        """Live-time duration in seconds (`~astropy.units.Quantity`).

        The dead-time-corrected observation time.

        Computed as ``t_live = t_observation * (1 - f_dead)``
        where ``f_dead`` is the dead-time fraction.
        """
        return self.observation_time_duration * (
            1 - self.observation_dead_time_fraction)

    @property
    def observation_dead_time_fraction(self):
        """Dead-time fraction (float).

        Defined as dead-time over observation time.

        Dead-time is defined as the time during the observation
        where the detector didn't record events:
        https://en.wikipedia.org/wiki/Dead_time
        https://ui.adsabs.harvard.edu/abs/2004APh....22..285F

        The dead-time fraction is used in the live-time computation,
        which in turn is used in the exposure and flux computation.
        """
        return 1 - self.obs_info["DEADC"]

    @lazyproperty
    def obs_info(self):
        """Observation info dictionary."""
        meta = self._obs_info.copy() if self._obs_info is not None else {}
        if self.events is not None:
            meta.update({
                k: v
                for k, v in self.events.table.meta.items()
                if not k.startswith('HDU')
            })
        return meta

    @lazyproperty
    def fixed_pointing_info(self):
        """Fixed pointing info for this observation (`FixedPointingInfo`)."""
        return FixedPointingInfo(self.obs_info)

    @property
    def pointing_radec(self):
        """Pointing RA / DEC sky coordinates (`~astropy.coordinates.SkyCoord`)."""
        return self.fixed_pointing_info.radec

    @property
    def pointing_altaz(self):
        return self.fixed_pointing_info.altaz

    @property
    def pointing_zen(self):
        """Pointing zenith angle sky (`~astropy.units.Quantity`)."""
        return self.fixed_pointing_info.altaz.zen

    @property
    def observatory_earth_location(self):
        """Observatory location (`~astropy.coordinates.EarthLocation`)."""
        return self.fixed_pointing_info.location

    @lazyproperty
    def target_radec(self):
        """Target RA / DEC sky coordinates (`~astropy.coordinates.SkyCoord`)."""
        lon, lat = (
            self.obs_info.get("RA_OBJ", np.nan),
            self.obs_info.get("DEC_OBJ", np.nan),
        )
        return SkyCoord(lon, lat, unit="deg", frame="icrs")

    @property
    def muoneff(self):
        """Observation muon efficiency."""
        return self.obs_info.get("MUONEFF", 1)

    def __str__(self):
        ra = self.pointing_radec.ra.deg
        dec = self.pointing_radec.dec.deg

        pointing = f"{ra:.1f} deg, {dec:.1f} deg\n"
        # TODO: Which target was observed?
        # TODO: print info about available HDUs for this observation ...
        return (
            f"{self.__class__.__name__}\n\n"
            f"\tobs id            : {self.obs_id} \n "
            f"\ttstart            : {self.tstart.mjd:.2f}\n"
            f"\ttstop             : {self.tstop.mjd:.2f}\n"
            f"\tduration          : {self.observation_time_duration:.2f}\n"
            f"\tpointing (icrs)   : {pointing}\n"
            f"\tdeadtime fraction : {self.observation_dead_time_fraction:.1%}\n"
        )

    def check(self, checks="all"):
        """Run checks.

        This is a generator that yields a list of dicts.
        """
        checker = ObservationChecker(self)
        return checker.run(checks=checks)

    def peek(self, figsize=(12, 10)):
        """Quick-look plots in a few panels.

        Parameters
        ----------
        figsize : tuple
            Figure size
        """
        import matplotlib.pyplot as plt

        n_irfs = len(self.available_hdus)

        fig, axes = plt.subplots(
            nrows=n_irfs // 2,
            ncols=2 + n_irfs % 2,
            figsize=figsize,
            gridspec_kw={
                "wspace": 0.25,
                "hspace": 0.25
            },
        )

        axes_dict = dict(zip(self.available_hdus, axes.flatten()))

        if "aeff" in self.available_hdus:
            self.aeff.plot(ax=axes_dict["aeff"])
            axes_dict["aeff"].set_title("Effective area")

        if "bkg" in self.available_hdus:
            bkg = self.bkg

            if not bkg.has_offset_axis:
                bkg = bkg.to_2d()

            bkg.plot(ax=axes_dict["bkg"])
            axes_dict["bkg"].set_title("Background rate")
        else:
            logging.warning(
                f"No background model found for obs {self.obs_id}.")

        if "psf" in self.available_hdus:
            self.psf.plot_containment_radius_vs_energy(ax=axes_dict["psf"])
            axes_dict["psf"].set_title("Point spread function")
        else:
            logging.warning(f"No PSF found for obs {self.obs_id}.")

        if "edisp" in self.available_hdus:
            self.edisp.plot_bias(ax=axes_dict["edisp"], add_cbar=True)
            axes_dict["edisp"].set_title("Energy dispersion")
        else:
            logging.warning(
                f"No energy dispersion found for obs {self.obs_id}.")

    def select_time(self, time_interval):
        """Select a time interval of the observation.

        Parameters
        ----------
        time_interval : `astropy.time.Time`
            Start and stop time of the selected time interval.
            For now we only support a single time interval.

        Returns
        -------
        new_obs : `~gammapy.data.Observation`
            A new observation instance of the specified time interval
        """
        new_obs_filter = self.obs_filter.copy()
        new_obs_filter.time_filter = time_interval
        obs = copy.deepcopy(self)
        obs.obs_filter = new_obs_filter
        return obs

    @classmethod
    def read(cls, event_file, irf_file=None):
        """Create an Observation from a Event List and an (optional) IRF file.

        Parameters
        ----------
        event_file : str, Path
            path to the .fits file containing the event list and the GTI
        irf_file : str, Path
            (optional) path to the .fits file containing the IRF components,
            if not provided the IRF will be read from the event file

        Returns
        -------
        observation : `~gammapy.data.Observation`
            observation with the events and the irf read from the file
        """
        from gammapy.irf.io import load_irf_dict_from_file

        events = EventList.read(event_file)

        gti = GTI.read(event_file)

        irf_file = irf_file if irf_file is not None else event_file
        irf_dict = load_irf_dict_from_file(irf_file)

        obs_info = events.table.meta
        return cls(
            events=events,
            gti=gti,
            obs_info=obs_info,
            obs_id=obs_info.get("OBS_ID"),
            **irf_dict,
        )

    def write(self, path, overwrite=False, format="gadf", include_irfs=True):
        """
        Write this observation into `path` using the specified format

        Parameters
        ----------
        path: str or `~pathlib.Path`
            Path for the output file
        overwrite: bool
            If true, existing files are overwritten.
        format: str
            Output format, currently only "gadf" is supported
        include_irfs: bool
            Whether to include irf components in the output file
        """
        if format != "gadf":
            raise ValueError(f'Only the "gadf" format supported, got {format}')

        path = make_path(path)

        primary = fits.PrimaryHDU()
        primary.header["CREATOR"] = f"Gammapy {__version__}"
        primary.header["DATE"] = Time.now().iso

        hdul = fits.HDUList([primary])

        events = self.events
        if events is not None:
            hdul.append(events.to_table_hdu(format=format))

        gti = self.gti
        if gti is not None:
            hdul.append(gti.to_table_hdu(format=format))

        if include_irfs:
            for irf_name in self.available_irfs:
                irf = getattr(self, irf_name)
                if irf is not None:
                    hdul.append(irf.to_table_hdu(format="gadf-dl3"))

        hdul.writeto(path, overwrite=overwrite)
Exemplo n.º 5
0
class BackgroundModel(Model):
    """Background model.

    Create a new map by a tilt and normalization on the available map

    Parameters
    ----------
    map : `~gammapy.maps.Map`
        Background model map
    norm : float
        Background normalization
    tilt : float
        Additional tilt in the spectrum
    reference : `~astropy.units.Quantity`
        Reference energy of the tilt.
    """

    tag = "BackgroundModel"
    norm = Parameter("norm", 1, unit="", min=0)
    tilt = Parameter("tilt", 0, unit="", frozen=True)
    reference = Parameter("reference", "1 TeV", frozen=True)
    map = LazyFitsData(cache=True)

    def __init__(
        self,
        map,
        norm=norm.quantity,
        tilt=tilt.quantity,
        reference=reference.quantity,
        name=None,
        filename=None,
        datasets_names=None,
    ):
        if isinstance(map, Map):
            axis = map.geom.get_axis_by_name("energy")
            if axis.node_type != "edges":
                raise ValueError(
                    'Need an integrated map, energy axis node_type="edges"')

        self.map = map

        self._name = make_name(name)
        self.filename = filename

        if isinstance(datasets_names, list):
            if len(datasets_names) != 1:
                raise ValueError(
                    "Currently background models can only be assigned to one dataset."
                )

        self.datasets_names = datasets_names
        super().__init__(norm=norm, tilt=tilt, reference=reference)

    @property
    def name(self):
        return self._name

    @property
    def energy_center(self):
        """True energy axis bin centers (`~astropy.units.Quantity`)"""
        energy_axis = self.map.geom.get_axis_by_name("energy")
        energy = energy_axis.center
        return energy[:, np.newaxis, np.newaxis]

    def evaluate(self):
        """Evaluate background model.

        Returns
        -------
        background_map : `~gammapy.maps.Map`
            Background evaluated on the Map
        """
        norm = self.norm.value
        tilt = self.tilt.value
        reference = self.reference.quantity
        tilt_factor = np.power((self.energy_center / reference).to(""), -tilt)
        back_values = norm * self.map.data * tilt_factor.value
        return self.map.copy(data=back_values)

    def to_dict(self):
        data = {}
        data["name"] = self.name
        data.update(super().to_dict())

        if self.filename is not None:
            data["filename"] = self.filename

        data["parameters"] = data.pop("parameters")

        if self.datasets_names is not None:
            data["datasets_names"] = self.datasets_names

        return data

    @classmethod
    def from_dict(cls, data):
        if "filename" in data:
            bkg_map = Map.read(data["filename"])
        elif "map" in data:
            bkg_map = data["map"]
        else:
            # TODO: for now create a fake map for serialization,
            # uptdated in MapDataset.from_dict()
            axis = MapAxis.from_edges(np.logspace(-1, 1, 2),
                                      unit=u.TeV,
                                      name="energy")
            geom = WcsGeom.create(skydir=(0, 0),
                                  npix=(1, 1),
                                  frame="galactic",
                                  axes=[axis])
            bkg_map = Map.from_geom(geom)

        parameters = Parameters.from_dict(data["parameters"])

        return cls.from_parameters(
            parameters=parameters,
            map=bkg_map,
            name=data["name"],
            datasets_names=data.get("datasets_names"),
            filename=data.get("filename"),
        )

    def copy(self, name=None):
        """A deep copy."""
        new = copy.deepcopy(self)
        new._name = make_name(name)
        return new

    def cutout(self, position, width, mode="trim", name=None):
        """Cutout background model.

        Parameters
        ----------
        position : `~astropy.coordinates.SkyCoord`
            Center position of the cutout region.
        width : tuple of `~astropy.coordinates.Angle`
            Angular sizes of the region in (lon, lat) in that specific order.
            If only one value is passed, a square region is extracted.
        mode : {'trim', 'partial', 'strict'}
            Mode option for Cutout2D, for details see `~astropy.nddata.utils.Cutout2D`.
        name : str
            Name of the returned background model.

        Returns
        -------
        cutout : `BackgroundModel`
            Cutout background model.
        """
        cutout_kwargs = {"position": position, "width": width, "mode": mode}

        bkg_map = self.map.cutout(**cutout_kwargs)
        parameters = self.parameters.copy()
        return self.__class__.from_parameters(parameters,
                                              map=bkg_map,
                                              name=name)

    def stack(self, other, weights=None):
        """Stack background model in place.

        Stacking the background model resets the current parameters values.

        Parameters
        ----------
        other : `BackgroundModel`
            Other background model.
        """
        bkg = self.evaluate()
        other_bkg = other.evaluate()
        bkg.stack(other_bkg, weights=weights)
        self.map = bkg

        # reset parameter values
        self.norm.value = 1
        self.tilt.value = 0

    def __str__(self):
        str_ = self.__class__.__name__ + "\n\n"
        str_ += "\t{:26}: {}\n".format("Name", self.name)
        str_ += "\t{:26}: {}\n".format("Datasets names", self.datasets_names)

        str_ += "\tParameters:\n"
        info = _get_parameters_str(self.parameters)
        lines = info.split("\n")
        str_ += "\t" + "\n\t".join(lines[:-1])

        str_ += "\n\n"
        return str_.expandtabs(tabsize=2)

    @property
    def position(self):
        """`~astropy.coordinates.SkyCoord`"""
        return self.map.geom.center_skydir

    @property
    def evaluation_radius(self):
        """`~astropy.coordinates.Angle`"""
        return np.max(self.map.geom.width) / 2.0