Source code for ixpeobssim.binning.misc

# Copyright (C) 2015--2022, the ixpeobssim team.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

"""Miscellanea binned products.
"""

from __future__ import print_function, division

from astropy.io import fits
import numpy

from ixpeobssim.binning.base import xEventBinningBase, xBinnedFileBase
from ixpeobssim.binning.fmt import xBinTableHDULC, xBinTableHDUPP
from ixpeobssim.core.fitsio import xFITSImageBase
from ixpeobssim.core.hist import xScatterPlot
from ixpeobssim.utils.logging_ import logger, abort
from ixpeobssim.utils.time_ import met_to_mjd


### pylint: disable=invalid-name, attribute-defined-outside-init, too-few-public-methods
### pylint: disable=no-member, arguments-differ


[docs] class xEventBinningCMAP(xEventBinningBase): """Class for CMAP binning. """ INTENT = 'count map in sky coordinates' SUPPORTED_KWARGS = ['mc'] + xEventBinningBase._image_wcs_kwargs()
[docs] def process_kwargs(self): """Overloaded method. """ xEventBinningBase.process_kwargs(self) self.process_image_ref_kwargs()
[docs] def bin_(self): """Overloaded method. """ ra, dec = self.event_file.sky_position_data(self.get('mc')) wcs_ = self._build_image_wcs(**self.kwargs) x, y, binning = self._pixelize_skycoords(ra, dec, wcs_) data, _, _ = numpy.histogram2d(x, y, bins=binning) header = wcs_.to_header() hdu_list = fits.HDUList([self.build_primary_hdu(data, header)]) self.write_output_file(hdu_list)
[docs] class xBinnedMap(xBinnedFileBase): """Display interface to binned CMAP files. While this is essentially an xFITSImage, we need to inherit from xBinnedFileBase, as the latter provides the implementation of the from_file_list() slot. Instead of inheriting from xFITSImage, too, we preferred composition, instead. Warning ------- I am more and more convinced that this is poor design, and maybe the pylint super-init-not-called is there for a reason. We should probably refactor the class and make the implementation cleaner. """ # pylint: disable=super-init-not-called, abstract-method def __init__(self, file_path): """Constructor. """ self.fits_image = xFITSImageBase(file_path) # These are needed to equip the class instance with all the members # that are necessary to call the wite() method of xBinnedFile. self.__data_dict = {} self._xBinnedFileBase__data_dict = self.__data_dict self.hdu_list = self.fits_image.hdu_list def __iadd__(self, other): """Overloaded method for CMAP binned data addition. """ self._check_iadd(other) self.fits_image += other.fits_image return self
[docs] def plot(self, **kwargs): """Plot the data. The kwargs passed to plt.imshow(). """ kwargs.setdefault('zlabel', 'Counts/pixel') return self.fits_image.plot(**kwargs)
[docs] class xEventBinningLC(xEventBinningBase): """Class for LC binning. """ INTENT = 'light curve' SUPPORTED_KWARGS = ['tmin', 'tmax', 'tbins', 'tbinalg', 'tbinfile']
[docs] def process_kwargs(self): """Overloaded method. """ xEventBinningBase.process_kwargs(self) if self.get('tmin') is None: self.set('tmin', self.event_file.start_met()) if self.get('tmax') is None: self.set('tmax', self.event_file.stop_met())
[docs] def make_binning(self): """Build the light-curve binning. """ kwargs = dict(min_val=self.get('tmin'), max_val=self.get('tmax'), num_bins=self.get('tbins'), bin_file=self.get('tbinfile')) return xEventBinningBase.make_binning(self.get('tbinalg'), **kwargs)
def _bin_gti(self, edge_min, edge_max, gti_starts, gti_stops): """ Loop on the GTIs and compute the actual observation time in a given time bin. """ dt = 0. for start, stop in zip(gti_starts, gti_stops): if start >= edge_min and stop <= edge_max: # The entire GTI is inside the bin dt += (stop - start) elif start >= edge_min and stop > edge_max: if start >= edge_max: # The GTI is entirely after the bin, we can stop the loop break # The GTI starts inside the bin and ends after: we add the time # from the GTI start to the end of the bin, then stop the loop dt += (edge_max - start) break elif start < edge_min and stop > edge_max: # The bin is entirely inside the GTI dt += (edge_max - edge_min) break elif start < edge_min and stop <= edge_max: if stop <= edge_min: # The GTI is entirely before the bin, keep running continue # The GTI starts before the bin and ends inside it: we add the # time from bin start to the GTI end dt += (stop - edge_min) return dt
[docs] def bin_(self): """Overloaded method. """ counts, edges = numpy.histogram(self.event_file.time_data(), bins=self.make_binning()) gti = self.event_file.hdu_list['GTI'] starts = gti.data['START'] stops = gti.data['STOP'] exposure = [] # In LV2 data we do not have the LIVETIME information on a event by # event basis, so we can only appply an average correction. This is # a very good approximation if the rate is low and/or it does not # vary too widely. deadc = self.event_file.primary_header['DEADC'] for tmin, tmax in zip(edges[:-1], edges[1:]): dt = self._bin_gti(tmin, tmax, starts, stops) exposure.append(dt * deadc) exposure = numpy.array(exposure) primary_hdu = self.build_primary_hdu() data = [self.bin_centers(edges), self.bin_widths(edges), exposure, counts, numpy.sqrt(counts)] rate_hdu = xBinTableHDULC(data) rate_hdu.setup_header(self.event_file.primary_keywords()) gti_hdu = self.event_file.hdu_list['GTI'] hdu_list = fits.HDUList([primary_hdu, rate_hdu, gti_hdu]) self.write_output_file(hdu_list)
[docs] class xBinnedLightCurve(xBinnedFileBase): """Binned light curve. """ def _read_data(self): """Overloaded method. """ self._read_binary_table_data(xBinTableHDULC.NAME) def __iadd__(self, other): """ Overloaded method for LC binned file addition. Here we have to handle the case of unequal exposures between DUs. The goal is that the final rate should be exactly the sum of the rates of the individual DUs. Since the rate is computed as the ratio between COUNTS and EXPOSURE, we propagate these two quantities in such a way that their ratio is the sum of the two initial ratios. Note that our formula reduces to just summing the counts in the case of equal exposures. """ self._check_iadd(other, ('COUNTS', 'ERROR', 'EXPOSURE'), ('TIME', 'TIMEDEL')) with numpy.errstate(divide='ignore', invalid='ignore'): w1 = 0.5 * (1. + other.EXPOSURE / self.EXPOSURE) w2 = 0.5 * (1. + self.EXPOSURE / other.EXPOSURE) w1[self.EXPOSURE == 0.] = 0. w2[other.EXPOSURE == 0.] = 0. self.COUNTS = self.COUNTS * w1 + other.COUNTS * w2 self.EXPOSURE = 0.5 * (self.EXPOSURE + other.EXPOSURE) self.ERROR = numpy.sqrt(self.ERROR**2. * w1**2 + \ other.ERROR**2. * w2**2) return self
[docs] def rate(self): """ """ with numpy.errstate(divide='ignore', invalid='ignore'): return self.COUNTS / self.EXPOSURE
[docs] def rate_error(self): """ """ with numpy.errstate(divide='ignore', invalid='ignore'): return self.ERROR / self.EXPOSURE
[docs] def plot(self, mjd=False, **plot_opts): """Overloaded plot method. """ if mjd is True: t = met_to_mjd(self.TIME) xlabel = 'MJD' else: t = self.TIME xlabel = 'MET [s]' xScatterPlot(t, self.rate(), self.rate_error(), xlabel=xlabel, ylabel='Rate [Hz]').plot(**plot_opts)
[docs] class xEventBinningPP(xEventBinningBase): """Class for pulse-profile binning. """ INTENT = 'pulse profile' SUPPORTED_KWARGS = ['phasebins']
[docs] def make_binning(self): """Build the light-curve binning. """ kwargs = dict(min_val=0., max_val=1., num_bins=self.get('phasebins')) return xEventBinningBase.make_binning('LIN', **kwargs)
[docs] def bin_(self): """Overloaded method. """ phase = self.event_file.phase_data() if phase is None: logger.error('PHASE column not found, do you need to run xpphase?') abort('Cannot create pulse profile') counts, edges = numpy.histogram(phase, bins=self.make_binning()) primary_hdu = self.build_primary_hdu() data = [self.bin_centers(edges), self.bin_widths(edges), counts, numpy.sqrt(counts)] rate_hdu = xBinTableHDUPP(data) rate_hdu.setup_header(self.event_file.primary_keywords()) gti_hdu = self.event_file.hdu_list['GTI'] hdu_list = fits.HDUList([primary_hdu, rate_hdu, gti_hdu]) self.write_output_file(hdu_list)
[docs] class xBinnedPulseProfile(xBinnedFileBase): """Binned pulse profile. """ def _read_data(self): """Overloaded method. """ self._read_binary_table_data(xBinTableHDUPP.NAME) def __iadd__(self, other): """ Overloaded method for PP binned file addition. """ self._check_iadd(other, ('COUNTS', 'ERROR'), ('PHASE', 'PHASEDEL')) self.COUNTS += other.COUNTS self.ERROR = numpy.sqrt(self.ERROR**2. + other.ERROR**2.) return self
[docs] def plot(self, **kwargs): """Overloaded plot method. """ xScatterPlot(self.PHASE, self.COUNTS, self.ERROR, xlabel='Pulse phase', ylabel='Counts').plot(**kwargs)