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()
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
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, )
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)
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