gbm data tools 1.1.1
This commit is contained in:
commit
95923512bc
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# cache
|
||||||
|
__pycache__/
|
BIN
McIlwainL_Coeffs.npy
Normal file
BIN
McIlwainL_Coeffs.npy
Normal file
Binary file not shown.
45
__init__.py
Normal file
45
__init__.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
# Version of the GBM data tools
|
||||||
|
__version__ = '1.1.1'
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import webbrowser
|
||||||
|
|
||||||
|
try:
|
||||||
|
package_dir = str(pathlib.Path(__file__).parent.absolute())
|
||||||
|
test_dir = os.path.join(package_dir, 'test')
|
||||||
|
test_data_dir = os.path.join(test_dir, 'data')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
home_path = os.path.expanduser('~')
|
||||||
|
home_path = os.path.join(home_path, '.gbm_data_tools', __version__)
|
||||||
|
|
||||||
|
help_path = os.path.join(home_path, 'docs')
|
||||||
|
tutorial_path = os.path.join(home_path, 'tutorials')
|
||||||
|
data_path = os.path.join(home_path, 'data')
|
1
background/__init__.py
Normal file
1
background/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .background import BackgroundFitter, BackgroundRates, BackgroundSpectrum
|
587
background/background.py
Normal file
587
background/background.py
Normal file
@ -0,0 +1,587 @@
|
|||||||
|
# background.py: Module containing background fitting and model classes
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import numpy as np
|
||||||
|
from ..data.primitives import EventList, TimeEnergyBins, EnergyBins
|
||||||
|
from ..data import PHAII, TTE
|
||||||
|
|
||||||
|
|
||||||
|
class BackgroundFitter:
|
||||||
|
"""Class for fitting a background, given a fitting algorithm,
|
||||||
|
to time-energy data (e.g. :class:`~gbm.data.phaii.PHAII`,
|
||||||
|
:class:`~gbm.data.TTE`).
|
||||||
|
|
||||||
|
When a BackgroundFitter is created, an algorithm must be specified. In
|
||||||
|
particular, the algorithm must be a class that has two public methods:
|
||||||
|
``fit()`` and ``interpolate()``.
|
||||||
|
|
||||||
|
For PHAII data, the class, upon initialization, must take the following as
|
||||||
|
arguments:
|
||||||
|
|
||||||
|
1. a 2D array of counts with shape (``numtimes``, ``numchans``);
|
||||||
|
2. a 1D array of time bin start times, shape (``numtimes``,);
|
||||||
|
3. a 1D array of time bin end times, shape (``numtimes``,);
|
||||||
|
4. a 1D array of exposures for each time bin, shape (``numtimes``,).
|
||||||
|
|
||||||
|
While for TTE data, the class, upon initialization, must take the following
|
||||||
|
as an argument:
|
||||||
|
|
||||||
|
1. a list, of length ``numchans``, where each item is a np.array
|
||||||
|
of event times.
|
||||||
|
|
||||||
|
The ``fit()`` method takes no arguments, hoewever, parameters that are
|
||||||
|
required for the algorithm may be specified as keywords.
|
||||||
|
|
||||||
|
The interpolate() method must take in the following as arguments:
|
||||||
|
|
||||||
|
1. a 1D array of time bin start times, shape (``numtimes``,);
|
||||||
|
2. a 1D array of time bin end times, shape (``numtimes``,).
|
||||||
|
|
||||||
|
Any additional parameters required can be specified as keywords.
|
||||||
|
The ``interpolate()`` method must return:
|
||||||
|
|
||||||
|
1. a 2D rates array, shape (``numtimes``, ``numchans``);
|
||||||
|
2. a 2D rate uncertainty array, shape (``numtimes``, ``numchans``).
|
||||||
|
|
||||||
|
Additionally, the class can provide the following public attributes that
|
||||||
|
will be exposed by BackgroundFitter:
|
||||||
|
|
||||||
|
- ``dof``: The degrees of freedom of the fits, array shape (``numchans``,)
|
||||||
|
- ``statistic``: The fit statistic for each fit, array shape (``numchans``,)
|
||||||
|
- ``statistic_name``: A string of the fit statistic used
|
||||||
|
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
dof (np.array): If available, the degrees-of-freedom of the fit for
|
||||||
|
each energy channel
|
||||||
|
livetime (float): The total livetime of the data used for the background
|
||||||
|
method (str): The name of the fitting algorithm class
|
||||||
|
parameters (dict): All parameters passed to the fitting algorithm
|
||||||
|
statistic (np.array): If available, the fit statistic for each energy
|
||||||
|
channel
|
||||||
|
statistic_name (str): If available, the name of the fit statistic
|
||||||
|
type (str): The type of background algorithm, either 'binned' or 'unbinned'
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._data_obj = None
|
||||||
|
self._method = None
|
||||||
|
self._type = None
|
||||||
|
self._statistic = None
|
||||||
|
self._dof = None
|
||||||
|
self._livetime = None
|
||||||
|
self._parameters = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def method(self):
|
||||||
|
return self._method.__class__.__name__
|
||||||
|
|
||||||
|
@property
|
||||||
|
def type(self):
|
||||||
|
return self._type
|
||||||
|
|
||||||
|
@property
|
||||||
|
def statistic_name(self):
|
||||||
|
return getattr(self._method, 'statistic_name', None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def statistic(self):
|
||||||
|
return getattr(self._method, 'statistic', None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dof(self):
|
||||||
|
return getattr(self._method, 'dof', None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def livetime(self):
|
||||||
|
return self._livetime
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parameters(self):
|
||||||
|
return self._parameters
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_phaii(cls, phaii, method, time_ranges=None):
|
||||||
|
"""Create a background fitter from a PHAII object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
phaii (:class:`~gbm.data.phaii.PHAII`): A PHAII data object
|
||||||
|
method (<class>): A background fitting/estimation class for binned data
|
||||||
|
time_ranges ([(float, float), ...]):
|
||||||
|
The time range or time ranges over which to fit the background.
|
||||||
|
If omitted, uses the full time range of the data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`BackgroundFitter`: The initialized background fitting object
|
||||||
|
"""
|
||||||
|
if not isinstance(phaii, PHAII):
|
||||||
|
raise TypeError('Input data must be a PHAII object')
|
||||||
|
|
||||||
|
obj = cls()
|
||||||
|
obj._data_obj = phaii
|
||||||
|
obj._validate_method(method)
|
||||||
|
time_ranges = obj._validate_time_ranges(time_ranges)
|
||||||
|
|
||||||
|
# Slice the PHAII data and merge if multiple slices
|
||||||
|
data = [phaii.data.slice_time(trange[0], trange[1]) for trange in
|
||||||
|
time_ranges]
|
||||||
|
data = TimeEnergyBins.merge_time(data)
|
||||||
|
obj._method = method(data.counts, data.tstart, data.tstop,
|
||||||
|
data.exposure)
|
||||||
|
obj._type = 'binned'
|
||||||
|
obj._livetime = np.sum(data.exposure)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_tte(cls, tte, method, time_ranges=None):
|
||||||
|
"""Create a background fitter from a TTE object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tte (:class:`~gbm.data.TTE`): A TTE data object
|
||||||
|
method (<class>): A background fitting/estimation class for unbinned data
|
||||||
|
time_ranges ([(float, float), ...]):
|
||||||
|
The time range or time ranges over which to fit the background.
|
||||||
|
If omitted, uses the full time range of the data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`BackgroundFitter`: The initialized background fitting object
|
||||||
|
"""
|
||||||
|
if not isinstance(tte, TTE):
|
||||||
|
raise TypeError('Input data must be a TTE object')
|
||||||
|
|
||||||
|
obj = cls()
|
||||||
|
obj._data_obj = tte
|
||||||
|
obj._validate_method(method)
|
||||||
|
time_ranges = obj._validate_time_ranges(time_ranges)
|
||||||
|
|
||||||
|
# Slice the TTE data and merge if multiple slices
|
||||||
|
data = [tte.data.time_slice(trange[0], trange[1]) for trange in
|
||||||
|
time_ranges]
|
||||||
|
data = EventList.merge(data)
|
||||||
|
data.sort('TIME')
|
||||||
|
# pull out the events in each channel
|
||||||
|
events = [data.channel_slice(i, i).time for i in
|
||||||
|
range(tte.numchans)]
|
||||||
|
|
||||||
|
obj._method = method(events)
|
||||||
|
obj._type = 'unbinned'
|
||||||
|
obj._livetime = data.get_exposure(time_ranges=time_ranges)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def fit(self, **kwargs):
|
||||||
|
"""Perform a background fit of the data
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: Options to be passed as parameters to the fitting class
|
||||||
|
"""
|
||||||
|
self._parameters = kwargs
|
||||||
|
self._method.fit(**kwargs)
|
||||||
|
|
||||||
|
def interpolate_bins(self, tstart, tstop, **kwargs):
|
||||||
|
"""Interpolate the fitted background model over a set of bins.
|
||||||
|
The exposure is calculated for each bin of the background model
|
||||||
|
in case the background model counts is needed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tstart (np.array): The starting times
|
||||||
|
tstop (np.array): The ending times
|
||||||
|
**kwargs: Options to be passed as parameters to the interpolation method
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`BackgroundRates`: The background rates/uncertainty object
|
||||||
|
"""
|
||||||
|
tstart_, tstop_ = (tstart.copy(), tstop.copy())
|
||||||
|
# do the interpolation
|
||||||
|
rate, rate_uncert = self._method.interpolate(tstart_, tstop_, **kwargs)
|
||||||
|
# get the exposure
|
||||||
|
numtimes = tstart_.shape[0]
|
||||||
|
exposure = np.array([self._data_obj.get_exposure((tstart_[i], tstop_[i])) \
|
||||||
|
for i in range(numtimes)])
|
||||||
|
# create the rates object
|
||||||
|
det = self._data_obj.detector
|
||||||
|
dtype = self._data_obj.datatype
|
||||||
|
trigtime = self._data_obj.trigtime
|
||||||
|
rates = BackgroundRates(rate, rate_uncert, tstart_, tstop_,
|
||||||
|
self._data_obj.data.emin.copy(),
|
||||||
|
self._data_obj.data.emax.copy(),
|
||||||
|
exposure=exposure,
|
||||||
|
detector=det, datatype=dtype,
|
||||||
|
trigtime=trigtime)
|
||||||
|
|
||||||
|
return rates
|
||||||
|
|
||||||
|
def interpolate_times(self, times, **kwargs):
|
||||||
|
"""Interpolate the fitted background model over a set of times.
|
||||||
|
Does not calculate an exposure since this returns a set of point
|
||||||
|
estimates of the background rates.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tstart (np.array): The sampling times
|
||||||
|
**kwargs: Options to be passed as parameters to the interpolation method
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`BackgroundRates`: The background rates/uncertainty object
|
||||||
|
"""
|
||||||
|
# do the interpolation
|
||||||
|
rate, rate_uncert = self._method.interpolate(times, times, **kwargs)
|
||||||
|
|
||||||
|
# create the rates object
|
||||||
|
det = self._data_obj.detector
|
||||||
|
dtype = self._data_obj.datatype
|
||||||
|
trigtime = self._data_obj.trigtime
|
||||||
|
rates = BackgroundRates(rate, rate_uncert, times, times,
|
||||||
|
self._data_obj.data.emin.copy(),
|
||||||
|
self._data_obj.data.emax.copy(), detector=det,
|
||||||
|
datatype=dtype, trigtime=trigtime)
|
||||||
|
|
||||||
|
return rates
|
||||||
|
|
||||||
|
def _validate_method(self, method):
|
||||||
|
try:
|
||||||
|
method
|
||||||
|
except:
|
||||||
|
raise NameError('Input method is not a known function')
|
||||||
|
|
||||||
|
has_fit = callable(method.fit)
|
||||||
|
has_interp = callable(method.interpolate)
|
||||||
|
if (not has_fit) or (not has_interp):
|
||||||
|
raise NotImplementedError(
|
||||||
|
"User-defined Background class must have "
|
||||||
|
"both fit() and an interpolate() methods")
|
||||||
|
|
||||||
|
def _validate_time_ranges(self, time_ranges):
|
||||||
|
if time_ranges is None:
|
||||||
|
time_ranges = [self._data_obj.time_range]
|
||||||
|
try:
|
||||||
|
iter(time_ranges[0])
|
||||||
|
except:
|
||||||
|
raise TypeError('time_ranges must be a list of tuples')
|
||||||
|
return time_ranges
|
||||||
|
|
||||||
|
|
||||||
|
class BackgroundRates(TimeEnergyBins):
|
||||||
|
"""Class containing the background rate data.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
rates (np.array): The array of background rates in each bin
|
||||||
|
rate_uncertainty (np.array): The array of background rate uncertainties
|
||||||
|
in each bin
|
||||||
|
tstart (np.array): The low-value edges of the time bins
|
||||||
|
tstop (np.array): The high-value edges of the time bins
|
||||||
|
emin (np.array): The low-value edges of the energy bins
|
||||||
|
emax (np.array): The high-value edges of the energy bins
|
||||||
|
exposure (np.array, optional): The exposure of each bin
|
||||||
|
detector (str, optional): The associated detector
|
||||||
|
datatype (str, optional): The datatype associated with the background
|
||||||
|
trigtime (float, optional): The trigger time
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
chanwidths (np.array): The bin widths along the energy axis
|
||||||
|
count_uncertainty (np.array): The counts uncertainty in each bin
|
||||||
|
energy_centroids (np.array): The bin centroids along the energy axis
|
||||||
|
energy_range (float, float): The range of the data along the energy axis
|
||||||
|
numchans (int): The number of energy channels along the energy axis
|
||||||
|
numtimes (int): The number of bins along the time axis
|
||||||
|
rates (np.array): The rates in each Time-Energy Bin
|
||||||
|
rates_per_kev (np.array): The differential rates in units of counts/s/keV
|
||||||
|
rate_uncertainty (np.array): The rate uncertainty in each bin
|
||||||
|
rate_uncertainty_per_kev (np.array):
|
||||||
|
The differential rate uncertainty in units of counts/s/keV
|
||||||
|
size (int, int): The number of bins along both axes (numtimes, numchans)
|
||||||
|
time_centroids (np.array): The bin centroids along the time axis
|
||||||
|
time_range (float, float): The range of the data along the time axis
|
||||||
|
time_widths (np.array): The bin widths along the time axis
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, rates, rate_uncertainty, tstart, tstop, emin, emax,
|
||||||
|
exposure=None, detector=None, datatype=None, trigtime=None):
|
||||||
|
if exposure is None:
|
||||||
|
exposure = np.zeros_like(tstart)
|
||||||
|
|
||||||
|
counts = np.squeeze(rates * exposure[:, np.newaxis])
|
||||||
|
super().__init__(counts, tstart, tstop, exposure, emin, emax)
|
||||||
|
self._count_uncertainty = np.squeeze(rate_uncertainty * exposure[:, np.newaxis])
|
||||||
|
self._rates = rates.squeeze()
|
||||||
|
self._rate_uncertainty = rate_uncertainty.squeeze()
|
||||||
|
self._detector = detector
|
||||||
|
self._datatype = datatype
|
||||||
|
self._trigtime = trigtime
|
||||||
|
|
||||||
|
@property
|
||||||
|
def count_uncertainty(self):
|
||||||
|
return self._count_uncertainty
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rates(self):
|
||||||
|
return self._rates
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rate_uncertainty(self):
|
||||||
|
return self._rate_uncertainty
|
||||||
|
|
||||||
|
def integrate_energy(self, emin=None, emax=None):
|
||||||
|
"""Integrate the over the energy axis.
|
||||||
|
Limits on the integration smaller than the full range can be set.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
emin (float, optional): The low end of the integration range. If not
|
||||||
|
set, uses the lowest energy edge of the histogram
|
||||||
|
emax (float, optional): The high end of the integration range. If not
|
||||||
|
set, uses the highest energy edge of the histogram
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`gbm.data.primitives.TimeBins`: A TimeBins object containing \
|
||||||
|
the lightcurve histogram
|
||||||
|
"""
|
||||||
|
if emin is None:
|
||||||
|
emin = self.energy_range[0]
|
||||||
|
if emax is None:
|
||||||
|
emax = self.energy_range[1]
|
||||||
|
|
||||||
|
mask = self._slice_energy_mask(emin, emax)
|
||||||
|
emin = self.emin[mask][0]
|
||||||
|
emax = self.emax[mask][-1]
|
||||||
|
rates = np.nansum(self.rates[:, mask], axis=1).reshape(-1,1)
|
||||||
|
rate_uncert = np.sqrt(
|
||||||
|
np.nansum(self.rate_uncertainty[:, mask] ** 2, axis=1)).reshape(-1,1)
|
||||||
|
|
||||||
|
obj = BackgroundRates(rates, rate_uncert, self.tstart.copy(),
|
||||||
|
self.tstop.copy(), np.array([emin]),
|
||||||
|
np.array([emax]), exposure=self.exposure.copy())
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def integrate_time(self, tstart=None, tstop=None):
|
||||||
|
"""Integrate the background over the time axis (producing a count rate
|
||||||
|
spectrum). Limits on the integration smaller than the full range can
|
||||||
|
be set.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tstart (float, optional): The low end of the integration range.
|
||||||
|
If not set, uses the lowest time edge of the histogram
|
||||||
|
tstop (float, optional): The high end of the integration range.
|
||||||
|
If not set, uses the highest time edge of the histogram
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`BackgroundSpectrum`: A BackgroundSpectrum object containing \
|
||||||
|
the count rate spectrum
|
||||||
|
"""
|
||||||
|
if tstart is None:
|
||||||
|
tstart = self.time_range[0]
|
||||||
|
if tstop is None:
|
||||||
|
tstop = self.time_range[1]
|
||||||
|
|
||||||
|
mask = self._slice_time_mask(tstart, tstop)
|
||||||
|
exposure = np.nansum(self.exposure[mask])
|
||||||
|
rates = np.nansum(self.counts[mask, :], axis=0) / exposure
|
||||||
|
rate_uncert = np.sqrt(np.nansum(self.count_uncertainty[mask, :] ** 2,
|
||||||
|
axis=0)) / exposure
|
||||||
|
exposure = np.full(rates.size, exposure)
|
||||||
|
|
||||||
|
obj = BackgroundSpectrum(rates, rate_uncert, self.emin.copy(),
|
||||||
|
self.emax.copy(), exposure)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def to_bak(self, time_range=None, **kwargs):
|
||||||
|
"""Integrate over the time axis and produce a BAK object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
time_range ((float, float), optional):
|
||||||
|
The time range to integrate over
|
||||||
|
**kwargs: Options to pass to BAK.from_data()
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`gbm.data.BAK`: The background BAK object
|
||||||
|
"""
|
||||||
|
from gbm.data.pha import BAK
|
||||||
|
|
||||||
|
if time_range is None:
|
||||||
|
time_range = self.time_range
|
||||||
|
back_spec = self.integrate_time(*time_range)
|
||||||
|
dtype = self._datatype.lower()
|
||||||
|
bak = BAK.from_data(back_spec, *time_range, datatype=dtype,
|
||||||
|
detector=self._detector, trigtime=self._trigtime,
|
||||||
|
**kwargs)
|
||||||
|
return bak
|
||||||
|
|
||||||
|
def rebin_time(self, method, *args, tstart=None, tstop=None):
|
||||||
|
"""Not Implemented
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def rebin_energy(self, method, *args, emin=None, emax=None):
|
||||||
|
"""Not Implemented
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def merge_time(cls, histos):
|
||||||
|
"""Merge multiple BackroundRates together along the time axis.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
histos (list of :class:`BackgroundRates`):
|
||||||
|
A list containing the BackgroundRates to be merged
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`BackgroundRates`: A new BackgroundRates object containing \
|
||||||
|
the merged BackgroundRates
|
||||||
|
"""
|
||||||
|
rates = np.vstack((histo.rates for histo in histos))
|
||||||
|
rate_uncertainty = np.vstack(
|
||||||
|
(histo.rate_uncertainty for histo in histos))
|
||||||
|
bins = TimeEnergyBins.merge_time(histos)
|
||||||
|
obj = cls(rates, rate_uncertainty, bins.tstart, bins.tstop,
|
||||||
|
bins.emin, bins.emax, exposure=bins.exposure)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sum_time(cls, bkgds):
|
||||||
|
"""Sum multiple TimeBins together if they have the same time range.
|
||||||
|
Example use would be summing two backgrounds from two detectors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bkgds (list of :class:`BackgroundRates):
|
||||||
|
A list containing the BackgroundRates to be summed
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`BackgroundRates`: A new BackgroundRates object containing \
|
||||||
|
the summed BackgroundRates
|
||||||
|
"""
|
||||||
|
rates = np.zeros_like(bkgds[0].rates)
|
||||||
|
rates_var = np.zeros_like(bkgds[0].rates)
|
||||||
|
for bkgd in bkgds:
|
||||||
|
assert bkgd.numtimes == bkgds[0].numtimes, \
|
||||||
|
"The backgrounds must all have the same support"
|
||||||
|
rates += bkgd.rates
|
||||||
|
rates_var += bkgd.rate_uncertainty ** 2
|
||||||
|
|
||||||
|
# averaged exposure, sampling times
|
||||||
|
exposure = np.mean([bkgd.exposure for bkgd in bkgds], axis=0)
|
||||||
|
tstart = np.mean([bkgd.tstart for bkgd in bkgds], axis=0)
|
||||||
|
tstop = np.mean([bkgd.tstop for bkgd in bkgds], axis=0)
|
||||||
|
emin = np.array([np.min([bkgd.emin for bkgd in bkgds])])
|
||||||
|
emax = np.array([np.min([bkgd.emax for bkgd in bkgds])])
|
||||||
|
|
||||||
|
sum_bkgd = cls(rates, np.sqrt(rates_var), tstart, tstop, emin, emax,
|
||||||
|
exposure=exposure, datatype=bkgds[0]._datatype,
|
||||||
|
trigtime=bkgds[0]._trigtime)
|
||||||
|
return sum_bkgd
|
||||||
|
|
||||||
|
|
||||||
|
class BackgroundSpectrum(EnergyBins):
|
||||||
|
"""A class defining a Background Spectrum.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
rates (np.array): The array of background rates in each bin
|
||||||
|
rate_uncertainty (np.array): The array of background rate uncertainties
|
||||||
|
in each bin
|
||||||
|
lo_edges (np.array): The low-value edges of the bins
|
||||||
|
hi_edges (np.array): The high-value edges of the bins
|
||||||
|
exposure (np.array): The exposure of each bin
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
centroids (np.array): The geometric centroids of the bins
|
||||||
|
counts (np.array): The counts in each bin
|
||||||
|
count_uncertainty (np.array): The count uncertainty in each bin
|
||||||
|
exposure (np.array): The exposure of each bin
|
||||||
|
hi_edges (np.array): The high-value edges of the bins
|
||||||
|
lo_edges (np.array): The low-value edges of the bins
|
||||||
|
range (float, float): The range of the bin edges
|
||||||
|
rates (np.array): count rate of each bin
|
||||||
|
rate_uncertainty (np.array): The count rate uncertainty of each bin
|
||||||
|
size (int): Number of bins
|
||||||
|
widths (np.array): The widths of the bins
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, rates, rate_uncertainty, lo_edges, hi_edges, exposure):
|
||||||
|
counts = rates * exposure
|
||||||
|
super().__init__(counts, lo_edges, hi_edges, exposure)
|
||||||
|
self._count_uncertainty = rate_uncertainty * exposure
|
||||||
|
self._rates = rates
|
||||||
|
self._rate_uncertainty = rate_uncertainty
|
||||||
|
|
||||||
|
@property
|
||||||
|
def count_uncertainty(self):
|
||||||
|
return self._count_uncertainty
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rates(self):
|
||||||
|
return self._rates
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rate_uncertainty(self):
|
||||||
|
return self._rate_uncertainty
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sum(cls, histos):
|
||||||
|
"""Sum multiple BackgroundSpectrums together if they have the same
|
||||||
|
energy range (support).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
histos (list of :class:`BackgroundSpectrum`):
|
||||||
|
A list containing the background spectra to be summed
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`BackgroundSpectrum`: A new object containing the \
|
||||||
|
summed BackgroundSpectrum
|
||||||
|
"""
|
||||||
|
counts = np.zeros(histos[0].size)
|
||||||
|
count_variance = np.zeros(histos[0].size)
|
||||||
|
exposure = 0.0
|
||||||
|
for histo in histos:
|
||||||
|
assert histo.size == histos[0].size, \
|
||||||
|
"The histograms must all have the same size"
|
||||||
|
assert np.all(histo.lo_edges == histos[0].lo_edges), \
|
||||||
|
"The histograms must all have the same support"
|
||||||
|
counts += histo.counts
|
||||||
|
count_variance += histo.count_uncertainty**2
|
||||||
|
exposure += histo.exposure
|
||||||
|
|
||||||
|
rates = counts/exposure
|
||||||
|
rate_uncertainty = np.sqrt(count_variance)/exposure
|
||||||
|
|
||||||
|
sum_bins = cls(rates, rate_uncertainty, histos[0].lo_edges,
|
||||||
|
histos[0].hi_edges, exposure)
|
||||||
|
return sum_bins
|
||||||
|
|
||||||
|
def slice(self, emin, emax):
|
||||||
|
"""Perform a slice over an energy range and return a new
|
||||||
|
BackgroundSpectrum object. Note that the emin and emax values that fall
|
||||||
|
inside a bin will result in that bin being included.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
emin (float): The low energy edge of the slice
|
||||||
|
emax (float): The high energy of the slice
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`BackgroundSpectrum`: A new object containing the energy slice
|
||||||
|
"""
|
||||||
|
emin_snap = self.closest_edge(emin, which='low')
|
||||||
|
emax_snap = self.closest_edge(emax, which='high')
|
||||||
|
|
||||||
|
mask = (self.lo_edges < emax_snap) & (self.hi_edges > emin_snap)
|
||||||
|
obj = self.__class__(self.rates[mask], self.rate_uncertainty[mask],
|
||||||
|
self.lo_edges[mask], self.hi_edges[mask],
|
||||||
|
self.exposure[mask])
|
||||||
|
return obj
|
268
background/binned.py
Normal file
268
background/binned.py
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
# binned.py: Module containing background fitting classes for pre-binned data
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
class Polynomial:
|
||||||
|
"""Class for performing a polynomial fit on Time-Energy data.
|
||||||
|
The fit is performed over the time axis, treating each energy channel
|
||||||
|
separately, although performing the fits simultaneously.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
counts (np.array): The array of counts in each bin, shape
|
||||||
|
(``numtimes``, ``numchans``)
|
||||||
|
tstart (np.array): The low-value edges of the time bins, shape
|
||||||
|
(``numtimes``,)
|
||||||
|
tstop (np.array): The high-value edges of the time bins, shape
|
||||||
|
(``numtimes``,)
|
||||||
|
exposure (np.array): The exposure of each bin, shape (``numtimes``,)
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
dof (np.array): The degrees-of-freedom for each channel
|
||||||
|
statistic (np.array): The fit chi-squared statistic for each channel
|
||||||
|
statistic_name (str): 'chisq'
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, counts, tstart, tstop, exposure):
|
||||||
|
self._tstart = tstart
|
||||||
|
self._tstop = tstop
|
||||||
|
self._rate = counts / exposure[:, np.newaxis]
|
||||||
|
self._livetime = exposure
|
||||||
|
self._numtimes, self._numchans = self._rate.shape
|
||||||
|
|
||||||
|
self._chisq = None
|
||||||
|
self._dof = None
|
||||||
|
self._order = None
|
||||||
|
self._coeff = None
|
||||||
|
self._covar = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def statistic_name(self):
|
||||||
|
return 'chisq'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def statistic(self):
|
||||||
|
return self._chisq
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dof(self):
|
||||||
|
return self._dof
|
||||||
|
|
||||||
|
def fit(self, order=0):
|
||||||
|
"""Fit the data with a polynomial. Model variances are used for
|
||||||
|
chi-squared via two fitting passes. Adapted from RMfit polynomial
|
||||||
|
fitter.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
order (int): The order of the polynomial
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array): The fitted model value and model uncertainty \
|
||||||
|
at each input bin
|
||||||
|
"""
|
||||||
|
assert order >= 0, 'Polynomial order must be non-negative'
|
||||||
|
self._order = order
|
||||||
|
self._coeff = np.zeros((order + 1, self._numchans))
|
||||||
|
self._covar = np.zeros((order + 1, order + 1, self._numchans))
|
||||||
|
|
||||||
|
# get basis functions and set up weights array
|
||||||
|
tstart, tstop = self._tstart, self._tstop
|
||||||
|
basis_func = self._eval_basis(tstart, tstop)
|
||||||
|
weights = np.zeros((self._order + 1, self._numtimes, self._numchans))
|
||||||
|
|
||||||
|
# Two-pass fitting
|
||||||
|
# First pass uses the data variances calculated from data rates
|
||||||
|
# Second pass uses the data variances calculated from model rates
|
||||||
|
# 1) rate * livetime = counts
|
||||||
|
# 2) variance of counts = counts
|
||||||
|
# 3) variance of rate = variance of counts/livetime^2 = rate/livetime
|
||||||
|
for iPass in range(2):
|
||||||
|
if np.max(self._rate) <= 0.0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if iPass == 0:
|
||||||
|
variance = self._rate / self._livetime[:, np.newaxis]
|
||||||
|
idx = variance > 0.0
|
||||||
|
for iCoeff in range(self._order + 1):
|
||||||
|
weights[iCoeff, idx] = basis_func[iCoeff, idx] / variance[
|
||||||
|
idx]
|
||||||
|
else:
|
||||||
|
variance = model / self._livetime[:, np.newaxis]
|
||||||
|
idx = variance > 0.0
|
||||||
|
if np.sum(idx) > 0:
|
||||||
|
for iCoeff in range(self._order + 1):
|
||||||
|
weights[iCoeff, idx] = basis_func[iCoeff, idx] / \
|
||||||
|
variance[idx]
|
||||||
|
else:
|
||||||
|
raise RuntimeError(
|
||||||
|
'SEVERE ERROR: Background model negative')
|
||||||
|
|
||||||
|
# covariance matrix
|
||||||
|
basis_func_list = np.squeeze(
|
||||||
|
np.split(basis_func, self._numchans, axis=2))
|
||||||
|
weights_list = np.squeeze(
|
||||||
|
np.split(weights, self._numchans, axis=2)).swapaxes(1, 2)
|
||||||
|
rates_list = np.squeeze(
|
||||||
|
np.split(self._rate, self._numchans, axis=1))
|
||||||
|
covar = np.array(
|
||||||
|
list(map(np.dot, basis_func_list, weights_list))).T
|
||||||
|
coeff = np.array(list(map(np.dot, rates_list, weights_list))).T
|
||||||
|
|
||||||
|
if self._order >= 1:
|
||||||
|
self._covar = np.linalg.inv(covar.T).T
|
||||||
|
else:
|
||||||
|
self._covar = 1.0 / covar
|
||||||
|
|
||||||
|
# coefficients
|
||||||
|
coeff_list = np.squeeze(np.split(coeff, self._numchans, axis=1))
|
||||||
|
covar_list = np.squeeze(
|
||||||
|
np.split(self._covar, self._numchans, axis=2))
|
||||||
|
coeff = np.array(list(map(np.dot, coeff_list, covar_list))).T
|
||||||
|
|
||||||
|
# evaluate model
|
||||||
|
self._coeff = coeff
|
||||||
|
model = self._eval_model(tstart, tstop)
|
||||||
|
|
||||||
|
# evaluate model uncertainty
|
||||||
|
model_uncert = self._eval_uncertainty(tstart, tstop)
|
||||||
|
|
||||||
|
# evaluate goodness-of-fit
|
||||||
|
self._chisq, self._dof = self._calc_chisq(model)
|
||||||
|
|
||||||
|
return model, model_uncert
|
||||||
|
|
||||||
|
def interpolate(self, tstart, tstop):
|
||||||
|
"""Interpolation of the fitted polynomial
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tstart (np.array): The starting edge of each bin
|
||||||
|
tstop (np.array): The ending edge of each bin
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array): The interpolated model value and model \
|
||||||
|
uncertainty in each bin
|
||||||
|
"""
|
||||||
|
interp = self._eval_model(tstart, tstop)
|
||||||
|
interp_uncert = self._eval_uncertainty(tstart, tstop)
|
||||||
|
return interp, interp_uncert
|
||||||
|
|
||||||
|
def _eval_basis(self, tstart, tstop):
|
||||||
|
"""Evaluates basis functions, which are the various polynomials
|
||||||
|
averaged over the time bins.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tstart (np.array): The starting edge of each bin
|
||||||
|
tstop (np.array): The ending edge of each bin
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The basis functions for each bin, shape \
|
||||||
|
(``order`` + 1, ``numtimes``, ``numchans``)
|
||||||
|
"""
|
||||||
|
numtimes = tstart.size
|
||||||
|
dt = tstop - tstart
|
||||||
|
zerowidth = (dt == 0.0)
|
||||||
|
tstop[zerowidth] += 2e-6
|
||||||
|
dt[zerowidth] = 2e-6
|
||||||
|
|
||||||
|
basis_func = np.array(
|
||||||
|
[(tstop ** (i + 1.0) - tstart ** (i + 1.0)) / ((i + 1.0) * dt) \
|
||||||
|
for i in range(self._order + 1)])
|
||||||
|
return np.tile(basis_func[:, :, np.newaxis], self._numchans)
|
||||||
|
|
||||||
|
def _eval_model(self, tstart, tstop):
|
||||||
|
"""Evaluates the fitted model over the data
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tstart (np.array): The starting edge of each bin
|
||||||
|
tstop (np.array): The ending edge of each bin
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The model value for each bin, shape \
|
||||||
|
(``numtimes``, ``numchans``)
|
||||||
|
"""
|
||||||
|
numtimes = tstart.size
|
||||||
|
dt = tstop - tstart
|
||||||
|
zerowidth = (dt == 0.0)
|
||||||
|
tstop[zerowidth] += 2e-6
|
||||||
|
dt[zerowidth] = 2e-6
|
||||||
|
model = np.zeros((numtimes, self._numchans))
|
||||||
|
for i in range(self._order + 1):
|
||||||
|
model += (self._coeff[i, :] * (tstop[:, np.newaxis] ** (i + 1.0) - \
|
||||||
|
tstart[:, np.newaxis] ** (
|
||||||
|
i + 1.0)) / (
|
||||||
|
(i + 1.0) * dt[:, np.newaxis])).astype(float)
|
||||||
|
return model
|
||||||
|
|
||||||
|
def _eval_uncertainty(self, tstart, tstop):
|
||||||
|
"""Evaluates the uncertainty in the model-predicted values for the data
|
||||||
|
intervals based on the uncertainty in the model coefficients.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tstart (np.array): The starting edge of each bin
|
||||||
|
tstop (np.array): The ending edge of each bin
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The model uncertainty for each bin, shape \
|
||||||
|
(``numtimes``, ``numchans``)
|
||||||
|
"""
|
||||||
|
numtimes = tstart.size
|
||||||
|
uncertainty = np.zeros((numtimes, self._numchans))
|
||||||
|
basis_func = self._eval_basis(tstart, tstop)
|
||||||
|
|
||||||
|
# formal propagation of uncertainty of fit coefficients to uncertainty of model
|
||||||
|
covar_list = np.squeeze(np.split(self._covar, self._numchans, axis=2))
|
||||||
|
basis_func_list = np.squeeze(
|
||||||
|
np.split(basis_func, self._numchans, axis=2))
|
||||||
|
for i in range(numtimes):
|
||||||
|
dot1 = np.array(
|
||||||
|
list(map(np.dot, covar_list, basis_func_list[:, :, i])))
|
||||||
|
uncertainty[i, :] = np.array(
|
||||||
|
list(map(np.dot, basis_func_list[:, :, i], dot1)))
|
||||||
|
|
||||||
|
uncertainty = np.sqrt(uncertainty)
|
||||||
|
return uncertainty
|
||||||
|
|
||||||
|
def _calc_chisq(self, model):
|
||||||
|
"""Calculate the chi-squared goodness-of-fit for the fitted model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model (np.array): The fitted model, shape (``numtimes``, ``numchans``)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array) : The chi-squared goodness-of-fit and \
|
||||||
|
degrees-of-freedom for each fitted channel
|
||||||
|
"""
|
||||||
|
variance = model / self._livetime[:, np.newaxis]
|
||||||
|
# do not calculate using bins with value <= 0.0
|
||||||
|
idx = self._rate > 0.0
|
||||||
|
chisq = [np.sum(
|
||||||
|
(self._rate[idx[:, i], i] - model[idx[:, i], i]) ** 2 / variance[
|
||||||
|
idx[:, i], i]) \
|
||||||
|
for i in range(self._numchans)]
|
||||||
|
chisq = np.array(chisq)
|
||||||
|
dof = np.sum(idx, axis=0) - (self._order + 1.0)
|
||||||
|
return chisq, dof
|
246
background/unbinned.py
Normal file
246
background/unbinned.py
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
# unbinned.py: Module containing background fitting classes for unbinned data
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import numpy as np
|
||||||
|
from scipy.interpolate import interp1d
|
||||||
|
|
||||||
|
|
||||||
|
class NaivePoisson:
|
||||||
|
""" A class to estimate the background of un-binned data using the naive Poisson
|
||||||
|
maximum likelihood.
|
||||||
|
|
||||||
|
This method is approximately equivalent to sliding a window of fixed length
|
||||||
|
through un-binned data and calculating the Poisson maximum likelihood for
|
||||||
|
the rate. The rate estimate is applied to the center of the sliding window,
|
||||||
|
therefore, the amount of data equivalent to half of the sliding window at
|
||||||
|
the beginning and half of the window at the end of the data is a constant.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This naive approach assumes there is either no 'strong' signal in the \
|
||||||
|
data, or the presence of a 'weaker' signal has a duration much less \
|
||||||
|
than the window width of the sliding window.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
counts (list of np.array): A list of length numchans, and each element
|
||||||
|
of the list is an array of event times in
|
||||||
|
that channel.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, times):
|
||||||
|
self._times = times
|
||||||
|
self._numchans = len(times)
|
||||||
|
self._min_dt = 2e-6
|
||||||
|
self._window_width = None
|
||||||
|
self._actual_widths = None
|
||||||
|
self._rates = None
|
||||||
|
|
||||||
|
def fit(self, window_width=100.0, fast=True):
|
||||||
|
"""Fit the data via Naive Poisson Maximum Likelihood
|
||||||
|
|
||||||
|
Args:
|
||||||
|
window_width (float):
|
||||||
|
The width of the sliding window in seconds.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
If the range of the data is shorter than ``window_width``,
|
||||||
|
the ``window_width`` will automatically be shortened to the
|
||||||
|
range of the data.
|
||||||
|
fast (bool): If True, then will use the fast approximation of the
|
||||||
|
algorithm that allows the ``window_width`` to change
|
||||||
|
throughout the data (the number of counts in the window
|
||||||
|
is constant). If False, uses the exact algorithm with a
|
||||||
|
fixed window, but is much slower.
|
||||||
|
"""
|
||||||
|
self._window_width = window_width
|
||||||
|
actual_widths = []
|
||||||
|
rates = []
|
||||||
|
uncerts = []
|
||||||
|
for i in range(self._numchans):
|
||||||
|
if fast:
|
||||||
|
r, u, w = self._fit_one_fast(i)
|
||||||
|
else:
|
||||||
|
r, u, w = self._fit_one_exact(i)
|
||||||
|
rates.append(r)
|
||||||
|
uncerts.append(u)
|
||||||
|
actual_widths.append(w)
|
||||||
|
|
||||||
|
self._actual_widths = actual_widths
|
||||||
|
self._rates = rates
|
||||||
|
|
||||||
|
def _fit_one_exact(self, channel):
|
||||||
|
"""Fit a single channel of event data. This is the exact (not
|
||||||
|
approximate) algorithm that uses a fixed window duration throughout the
|
||||||
|
data, except where the window must be necessarily truncated at the ends
|
||||||
|
of the data. This function is much slower than the approximate version.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel (int): The energy channel
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array, np.array): The background rates, uncertainty, \
|
||||||
|
and actual window width
|
||||||
|
"""
|
||||||
|
window_width = self._window_width
|
||||||
|
events = self._times[channel]
|
||||||
|
|
||||||
|
num_events = len(events)
|
||||||
|
if num_events == 0:
|
||||||
|
return (np.array([]), np.array([]), np.array([]))
|
||||||
|
|
||||||
|
# get the different parts of the array
|
||||||
|
# pre: before the full sliding window width
|
||||||
|
# mid: during the full sliding window width
|
||||||
|
# post: after the full sliding window width
|
||||||
|
pre_mask = (events < events[0] + window_width / 2.0)
|
||||||
|
post_mask = (events > events[-1] - window_width / 2.0)
|
||||||
|
mid_mask = (~pre_mask & ~post_mask)
|
||||||
|
|
||||||
|
# do the sliding window
|
||||||
|
idx = np.sum(pre_mask)
|
||||||
|
mid_bins = [
|
||||||
|
np.sum(np.abs(events - events[i + idx]) <= window_width / 2.0) \
|
||||||
|
for i in range(np.sum(mid_mask))]
|
||||||
|
mid_rates = np.array(mid_bins) / window_width
|
||||||
|
mid_uncert = np.sqrt(mid_rates / window_width)
|
||||||
|
|
||||||
|
# now considering the pre- and post-full-window-width data:
|
||||||
|
# assume a constant rate at either end, but shorten the window
|
||||||
|
# appropriately
|
||||||
|
|
||||||
|
pre_events = events[pre_mask]
|
||||||
|
pre_dt = (pre_events - events[0])
|
||||||
|
pre_rates = np.full(np.sum(pre_mask) - 1, mid_rates[0])
|
||||||
|
pre_uncert = np.sqrt(pre_rates / pre_dt[1:])
|
||||||
|
|
||||||
|
idx = num_events - np.sum(post_mask)
|
||||||
|
post_events = events[post_mask]
|
||||||
|
post_dt = events[-1] - post_events
|
||||||
|
post_rates = np.full(np.sum(post_mask) - 1, mid_rates[-1])
|
||||||
|
post_uncert = np.sqrt(post_rates / post_dt[:-1])
|
||||||
|
|
||||||
|
# put it all together
|
||||||
|
brates = np.hstack((pre_rates, mid_rates, post_rates))
|
||||||
|
uncert = np.hstack((pre_uncert, mid_uncert, post_uncert))
|
||||||
|
dt = np.hstack((pre_dt[1:], np.full(np.sum(mid_mask), window_width),
|
||||||
|
post_dt[:-1]))
|
||||||
|
|
||||||
|
return brates, uncert, dt
|
||||||
|
|
||||||
|
def _fit_one_fast(self, channel):
|
||||||
|
"""Fit a single channel of event data. This performs an approximation
|
||||||
|
to the NaivePoisson algorithm to considerably speed up the computation.
|
||||||
|
Instead of assuming a fixed window duration throughout the data, the
|
||||||
|
window is initialized by determining the number of counts in the first
|
||||||
|
window, and fixing the window width by the total number of counts in
|
||||||
|
the data. This allows the window duration to change, and is similar
|
||||||
|
to smoothing the data. For slowly varying data, this is a good
|
||||||
|
approximation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
channel (int): The energy channel
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array, np.array): The background rates, uncertainty, \
|
||||||
|
and actual window width
|
||||||
|
"""
|
||||||
|
window_width = self._window_width
|
||||||
|
events = self._times[channel]
|
||||||
|
num_events = len(events)
|
||||||
|
if num_events == 0:
|
||||||
|
return (np.array([]), np.array([]), np.array([]))
|
||||||
|
|
||||||
|
# get extent of window at the start of the data
|
||||||
|
end_idx = np.sum(events <= events[0] + window_width)
|
||||||
|
# from the middle of the sliding window
|
||||||
|
mid_idx = int(np.floor(end_idx / 2.0))
|
||||||
|
|
||||||
|
# number of 'bins' containing a count (= number of counts in window)
|
||||||
|
full_bins = end_idx
|
||||||
|
# number of 'bins' containing a count not in the window
|
||||||
|
num_back_bins = num_events - end_idx
|
||||||
|
array_back = np.arange(num_back_bins)
|
||||||
|
|
||||||
|
# actual window widths and the rate
|
||||||
|
dts = (events[end_idx + array_back] - events[array_back])
|
||||||
|
back_rates = full_bins / dts
|
||||||
|
|
||||||
|
# Now we want to estimate the rate at the ends. This means that the
|
||||||
|
# sliding window will truncate at either end until it's the width of
|
||||||
|
# one event. The main concern is to calculate the width of the
|
||||||
|
# truncated window correctly so that the uncertainty in the rate will
|
||||||
|
# properly increase as the window width decreases
|
||||||
|
|
||||||
|
pre_full_bins = np.arange(mid_idx) + 1
|
||||||
|
pre_dts = events[2 * (pre_full_bins[1:] - 1)] - events[0]
|
||||||
|
# pre_back_rates = np.full(mid_idx-1, back_rates[0])
|
||||||
|
pre_back_rates = (2.0 * pre_full_bins[1:]) / pre_dts
|
||||||
|
|
||||||
|
post_full_bins = pre_full_bins[::-1]
|
||||||
|
post_dts = events[-1] - events[-1 - 2 * (post_full_bins[:-1] - 1)]
|
||||||
|
post_back_rates = (2.0 * post_full_bins[:-1]) / post_dts
|
||||||
|
# post_back_rates = np.full(mid_idx-1, back_rates[-1])
|
||||||
|
|
||||||
|
# put all of it together
|
||||||
|
brates = np.hstack((pre_back_rates, back_rates, post_back_rates))
|
||||||
|
dts = np.hstack((pre_dts, dts, post_dts))
|
||||||
|
|
||||||
|
# this is if we ended up with an odd number of events
|
||||||
|
if num_events - 2 > brates.shape[0]:
|
||||||
|
brates = np.append(brates, brates[-1])
|
||||||
|
dts = np.append(dts, dts[-1])
|
||||||
|
|
||||||
|
# now the uncertainty
|
||||||
|
uncert = np.sqrt(brates / dts)
|
||||||
|
|
||||||
|
return (brates, uncert, dts)
|
||||||
|
|
||||||
|
def interpolate(self, tstart, tstop):
|
||||||
|
"""Interpolate the background at the given times
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tstart (np.array): The start times of the bins to interpolate
|
||||||
|
tstop (np.array): The end times of the bins to interpolate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array): The interpolated model value and model \
|
||||||
|
uncertainty in each bin
|
||||||
|
"""
|
||||||
|
times = (tstart + tstop) / 2.0
|
||||||
|
rates = []
|
||||||
|
uncert = []
|
||||||
|
for i in range(self._numchans):
|
||||||
|
rates_interp = interp1d(self._times[i][1:-1], self._rates[i],
|
||||||
|
fill_value='extrapolate')
|
||||||
|
width_interp = interp1d(self._times[i][1:-1],
|
||||||
|
self._actual_widths[i],
|
||||||
|
fill_value='extrapolate')
|
||||||
|
r = rates_interp(times)
|
||||||
|
widths = width_interp(times)
|
||||||
|
uncert.append(np.sqrt(r / widths))
|
||||||
|
rates.append(r)
|
||||||
|
|
||||||
|
rates = np.array(rates).T
|
||||||
|
uncert = np.array(uncert).T
|
||||||
|
return rates, uncert
|
0
binning/__init__.py
Normal file
0
binning/__init__.py
Normal file
243
binning/binned.py
Normal file
243
binning/binned.py
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
# binned.py: Module containing data binning functions for pre-binned data
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import numpy as np
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
|
def combine_by_factor(counts, exposure, old_edges, bin_factor):
|
||||||
|
"""Rebins binned data to a multiple factor
|
||||||
|
|
||||||
|
Args:
|
||||||
|
counts (np.array): The counts in each bin
|
||||||
|
exposure (np.array): The exposure of each bin
|
||||||
|
old_edges (np.array): The time edges of each bin
|
||||||
|
bin_factor (int): The number of consecutive bins to be combined
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array, np.array): The counts and exposure in each bin \
|
||||||
|
and the new bin edges
|
||||||
|
"""
|
||||||
|
assert bin_factor >= 1, "bin_factor must be a positive integer"
|
||||||
|
bin_factor = int(bin_factor)
|
||||||
|
new_edges = old_edges[::bin_factor]
|
||||||
|
# make sure the number of bins is a multiple of the bin_factor
|
||||||
|
mod = counts.shape[0] % bin_factor
|
||||||
|
# if the number of bins is not a multiple of the bin_factor,
|
||||||
|
# then remove the modulo bins
|
||||||
|
if mod != 0:
|
||||||
|
counts = counts[:-mod]
|
||||||
|
exposure = exposure[:-mod]
|
||||||
|
# reshape and combine counts and exposure
|
||||||
|
new_counts = np.sum(counts.reshape(-1, bin_factor), axis=1)
|
||||||
|
new_exposure = np.sum(exposure.reshape(-1, bin_factor), axis=1)
|
||||||
|
return new_counts, new_exposure, new_edges
|
||||||
|
|
||||||
|
|
||||||
|
def rebin_by_time(counts, exposure, old_edges, dt):
|
||||||
|
"""Rebins binned data to a specified temporal bin width.
|
||||||
|
|
||||||
|
If the requested bin width is smaller than some of the original bin widths,
|
||||||
|
those bins will be left as is.
|
||||||
|
|
||||||
|
If the requested bin width is an exact factor of all the current bin widths,
|
||||||
|
the resulting bin width will be exactly as requested. If the requested bin
|
||||||
|
width is not an exact factor, then the resulting bin width will be
|
||||||
|
approximately the requested bin width without exceeding the requested bin
|
||||||
|
width.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
counts (np.array): The counts in each bin
|
||||||
|
exposure (np.array): The exposure of each bin
|
||||||
|
old_edges (np.array): The time edges of each bin
|
||||||
|
dt (float): The requested temporal bin width in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array, np.array): The counts and exposure in each bin \
|
||||||
|
and the new bin edges
|
||||||
|
"""
|
||||||
|
assert dt > 0.0, "Requested bin width must be > 0.0 s"
|
||||||
|
|
||||||
|
num_old_bins = counts.size
|
||||||
|
dts = old_edges[1:] - old_edges[0:-1]
|
||||||
|
|
||||||
|
# if the the requested bin width is a factor of the current bin width for all bins,
|
||||||
|
# call combine_by_factor. this is an easier task
|
||||||
|
warnings.filterwarnings("ignore", category=RuntimeWarning)
|
||||||
|
if np.sum((dt % dts) == 0.0) == num_old_bins:
|
||||||
|
return combine_by_factor(counts, exposure, old_edges, int(dt / dts[0]))
|
||||||
|
|
||||||
|
# print('Temporal factor for rebinning is not a perfect multiple of all bin widths.')
|
||||||
|
# print('Bin widths will be approximate to and will not exceed the temporal factor.')
|
||||||
|
# cycle through current bins and add up bins until we have approximately reached,
|
||||||
|
# but not exceeded, the requested binwidth
|
||||||
|
new_edges = [0]
|
||||||
|
istart = 0
|
||||||
|
while True:
|
||||||
|
dtscum = np.cumsum(dts[istart:])
|
||||||
|
iend = istart + np.sum(dtscum <= dt)
|
||||||
|
if iend < istart:
|
||||||
|
iend = istart
|
||||||
|
new_edges.append(iend)
|
||||||
|
if iend >= num_old_bins - 1:
|
||||||
|
break
|
||||||
|
istart = iend + 1
|
||||||
|
|
||||||
|
# create the new rebinned arrays
|
||||||
|
new_edges = np.array(new_edges)
|
||||||
|
bounds_idx = np.array((new_edges[0:-1], new_edges[1:])).T
|
||||||
|
new_exposure = np.array(
|
||||||
|
[np.sum(exposure[i[0]:i[1] + 1]) for i in bounds_idx])
|
||||||
|
new_counts = np.array([np.sum(counts[i[0]:i[1] + 1]) for i in bounds_idx])
|
||||||
|
new_edges = old_edges[new_edges]
|
||||||
|
return new_counts, new_exposure, new_edges
|
||||||
|
|
||||||
|
|
||||||
|
def combine_into_one(counts, exposure, old_edges):
|
||||||
|
"""Combines binned data into a single bin
|
||||||
|
|
||||||
|
Args:
|
||||||
|
counts (np.array): The counts in each bin
|
||||||
|
exposure (np.array): The exposure of each bin
|
||||||
|
old_edges (np.array): The time edges of each bin
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array, np.array): The counts and exposure in each bin \
|
||||||
|
and the new bin edges
|
||||||
|
"""
|
||||||
|
new_counts = np.array([np.sum(counts)])
|
||||||
|
new_exposure = np.array([np.sum(exposure)])
|
||||||
|
new_edges = old_edges[[0, -1]]
|
||||||
|
return new_counts, new_exposure, new_edges
|
||||||
|
|
||||||
|
|
||||||
|
def rebin_by_snr(counts, exposure, old_edges, background_counts, snr):
|
||||||
|
"""Rebins binned data such that each bin is above a minimum signal-to-noise ratio
|
||||||
|
|
||||||
|
Args:
|
||||||
|
counts (np.array): The counts in each bin
|
||||||
|
exposure (np.array): The exposure of each bin
|
||||||
|
old_edges (np.array): The time edges of each bin
|
||||||
|
background_counts (np.array): The background counts in each bin
|
||||||
|
snr (float): The minimum signal-to-ratio threshold
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array, np.array): The counts and exposure in each bin \
|
||||||
|
and the new bin edges
|
||||||
|
"""
|
||||||
|
num_old_bins = counts.size
|
||||||
|
# make sure there is a non-zero background
|
||||||
|
mask = (background_counts > 0.0)
|
||||||
|
if np.sum(mask) == 0:
|
||||||
|
raise ValueError(
|
||||||
|
'Background counts are all non-positive. Cannot bin by SNR.')
|
||||||
|
|
||||||
|
# cycle through current bins and combine bins until we have exceeded the requested snr
|
||||||
|
new_edges = [0]
|
||||||
|
istart = 0
|
||||||
|
while True:
|
||||||
|
countscum = np.cumsum(counts[istart:])
|
||||||
|
backgroundscum = np.cumsum(background_counts[istart:])
|
||||||
|
snrcum = np.cumsum(
|
||||||
|
(countscum - backgroundscum) / np.sqrt(backgroundscum))
|
||||||
|
iend = istart + np.sum(snrcum < snr) + 1
|
||||||
|
if iend < istart:
|
||||||
|
iend = istart
|
||||||
|
new_edges.append(iend)
|
||||||
|
if (iend >= num_old_bins - 1):
|
||||||
|
break
|
||||||
|
istart = iend
|
||||||
|
if len(old_edges) - 1 not in new_edges:
|
||||||
|
new_edges.append(len(old_edges) - 1)
|
||||||
|
|
||||||
|
# create the new rebinned arrays
|
||||||
|
new_edges = np.array(new_edges)
|
||||||
|
numbins = len(new_edges) - 1
|
||||||
|
new_counts = [np.sum(counts[new_edges[i]:new_edges[i + 1]]) for i in
|
||||||
|
range(numbins)]
|
||||||
|
new_exposure = [np.sum(exposure[new_edges[i]:new_edges[i + 1]]) for i in
|
||||||
|
range(numbins)]
|
||||||
|
new_edges = old_edges[new_edges]
|
||||||
|
|
||||||
|
return np.array(new_counts), np.array(new_exposure), new_edges
|
||||||
|
|
||||||
|
|
||||||
|
def rebin_by_edge_index(counts, exposure, old_edges, new_edge_index):
|
||||||
|
"""Rebins binned data based on an array of bin edge indices
|
||||||
|
|
||||||
|
Args:
|
||||||
|
counts (np.array): The counts in each bin
|
||||||
|
exposure (np.array): The exposure of each bin
|
||||||
|
old_edges (np.array): The time edges of each bin
|
||||||
|
new_edge_index (np.array): The edge indices for the new binned data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array, np.array): The counts and exposure in each bin \
|
||||||
|
and the new bin edges
|
||||||
|
"""
|
||||||
|
# new edges
|
||||||
|
num_bins = new_edge_index.size - 1
|
||||||
|
nei = new_edge_index.astype(dtype=int)
|
||||||
|
new_edges = old_edges[nei]
|
||||||
|
|
||||||
|
# combine the counts and exposure
|
||||||
|
new_exposure = np.zeros(num_bins, dtype=float)
|
||||||
|
new_counts = np.zeros(num_bins, dtype=float)
|
||||||
|
for i in range(num_bins):
|
||||||
|
start_idx = new_edge_index[i]
|
||||||
|
end_idx = new_edge_index[i + 1]
|
||||||
|
new_counts[i] = np.sum(counts[start_idx:end_idx])
|
||||||
|
new_exposure[i] = np.sum(exposure[start_idx:end_idx])
|
||||||
|
|
||||||
|
return new_counts, new_exposure, new_edges
|
||||||
|
|
||||||
|
|
||||||
|
def rebin_by_edge(counts, exposure, old_edges, new_edges):
|
||||||
|
"""Rebins binned data based on an array of bin edge indices
|
||||||
|
|
||||||
|
Args:
|
||||||
|
counts (np.array): The counts in each bin
|
||||||
|
exposure (np.array): The exposure of each bin
|
||||||
|
old_edges (np.array): The time edges of each bin
|
||||||
|
new_edges (np.array): The new edges of the binned data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array, np.array): The counts and exposure in each bin \
|
||||||
|
and the new bin edges
|
||||||
|
"""
|
||||||
|
# new edges
|
||||||
|
num_bins = new_edges.size - 1
|
||||||
|
|
||||||
|
# combine the counts and exposure
|
||||||
|
old_edges_list = old_edges.tolist()
|
||||||
|
new_exposure = np.zeros(num_bins, dtype=float)
|
||||||
|
new_counts = np.zeros(num_bins, dtype=float)
|
||||||
|
for i in range(num_bins):
|
||||||
|
start_idx = old_edges_list.index(new_edges[i])
|
||||||
|
end_idx = old_edges_list.index(new_edges[i + 1])
|
||||||
|
new_counts[i] = np.sum(counts[start_idx:end_idx])
|
||||||
|
new_exposure[i] = np.sum(exposure[start_idx:end_idx])
|
||||||
|
|
||||||
|
return new_counts, new_exposure, new_edges
|
199
binning/unbinned.py
Normal file
199
binning/unbinned.py
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
# unbinned.py: Module containing data binning functions for unbinned data
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def bin_by_time(times, dt, tstart=None, tstop=None, time_ref=None):
|
||||||
|
"""Bins unbinned data to a specified temporal bin width.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (np.array): The time of each event
|
||||||
|
dt (float): The requested temporal bin width in seconds
|
||||||
|
tstart (float, optional): The first bin edge time. Will use the first
|
||||||
|
event time if omitted.
|
||||||
|
tstop: (float, optional): The last bin edge time. Will use the last
|
||||||
|
event time if omitted.
|
||||||
|
time_ref (float, optional):
|
||||||
|
The reference time at which the binning will be based. If the set,
|
||||||
|
the binning will proceed starting at ``time_ref`` and moving forward
|
||||||
|
in time as well as starting at ``time_ref`` and moving backward in
|
||||||
|
time. If not set, the binning will start at the beginning of the data.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The edges of the binned data
|
||||||
|
"""
|
||||||
|
assert dt > 0.0, "Requested bin width must be > 0.0 s"
|
||||||
|
if time_ref is not None:
|
||||||
|
time_ref = float(time_ref)
|
||||||
|
|
||||||
|
if tstart is None:
|
||||||
|
tstart = np.min(times)
|
||||||
|
if tstop is None:
|
||||||
|
tstop = np.max(times)
|
||||||
|
|
||||||
|
# if we are using a reference time
|
||||||
|
if time_ref is not None:
|
||||||
|
pre_edges = np.arange(time_ref, tstart - dt, -dt)[::-1]
|
||||||
|
post_edges = np.arange(time_ref, tstop + dt, dt)
|
||||||
|
edges = np.concatenate((pre_edges[:-1], post_edges))
|
||||||
|
else:
|
||||||
|
edges = np.arange(tstart, tstop + dt, dt)
|
||||||
|
|
||||||
|
return edges
|
||||||
|
|
||||||
|
|
||||||
|
def combine_into_one(times, tstart, tstop):
|
||||||
|
"""Bins unbinned data to a single bin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (np.array): The time of each event
|
||||||
|
tstart (float): The first bin edge time. Will use the first
|
||||||
|
event time if omitted.
|
||||||
|
tstop: (float): The last bin edge time. Will use the last
|
||||||
|
event time if omitted.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The edges of the binned data
|
||||||
|
"""
|
||||||
|
assert tstart < tstop, "The time range must be set in " \
|
||||||
|
"ascending order"
|
||||||
|
|
||||||
|
# if no events, then the we have no counts
|
||||||
|
if times.size == 0:
|
||||||
|
return np.array([0]), np.array((tstart, tstop))
|
||||||
|
|
||||||
|
if (np.min(times) > tstop) | (np.max(times) < tstart):
|
||||||
|
raise ValueError("Requested time range is outside data range")
|
||||||
|
|
||||||
|
time_range = (tstart, tstop)
|
||||||
|
return np.array(time_range)
|
||||||
|
|
||||||
|
|
||||||
|
def combine_by_factor(times, old_edges, bin_factor, tstart=None, tstop=None):
|
||||||
|
"""Bins individual events to a multiple factor of bins given a
|
||||||
|
set of bin edges
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (np.array): The time of each event
|
||||||
|
old_edges (np.array): The edges to be combined
|
||||||
|
bin_factor (int): The number of bins to be combined
|
||||||
|
tstart (float, optional): The first bin edge time. Will use the first
|
||||||
|
event time if omitted.
|
||||||
|
tstop: (float, optional): The last bin edge time. Will use the last
|
||||||
|
event time if omitted.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The edges of the binned data
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert bin_factor >= 1, "bin_factor must be a positive integer"
|
||||||
|
bin_factor = int(bin_factor)
|
||||||
|
|
||||||
|
# mask the old edges for the desired data range
|
||||||
|
if tstart is None:
|
||||||
|
tstart = old_edges[0]
|
||||||
|
if tstop is None:
|
||||||
|
tstop = old_edges[-1]
|
||||||
|
old_edges = old_edges[old_edges >= tstart]
|
||||||
|
old_edges = old_edges[old_edges <= tstop]
|
||||||
|
|
||||||
|
# create the new edges
|
||||||
|
new_edges = old_edges[::bin_factor]
|
||||||
|
return new_edges
|
||||||
|
|
||||||
|
|
||||||
|
def bin_by_snr(times, back_rates, snr):
|
||||||
|
"""Bins unbinned data by SNR
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (np.array): The time of each event
|
||||||
|
back_rates (np.array): The background rate at the time of each event
|
||||||
|
snr (float): The signal-to-noise ratio
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The edges of the binned data
|
||||||
|
"""
|
||||||
|
# get the background rates and differential counts at each event time
|
||||||
|
back_counts = np.zeros_like(back_rates)
|
||||||
|
back_counts[:-1] = back_rates[:-1] * (times[1:] - times[:-1])
|
||||||
|
back_counts[-1] = back_counts[-2]
|
||||||
|
|
||||||
|
num_events = len(times)
|
||||||
|
istart = 0
|
||||||
|
edges = []
|
||||||
|
while True:
|
||||||
|
# cumulative sum of the counts, background counts, and snr
|
||||||
|
countscum = np.arange(1, num_events + 1 - istart)
|
||||||
|
backgroundscum = np.cumsum(back_counts[istart:])
|
||||||
|
snrcum = (countscum - backgroundscum) / np.sqrt(backgroundscum)
|
||||||
|
# determine where to make the cut
|
||||||
|
below_thresh = np.sum(snrcum <= snr)
|
||||||
|
if below_thresh == 0:
|
||||||
|
iend = istart
|
||||||
|
else:
|
||||||
|
iend = istart + below_thresh - 1
|
||||||
|
edges.append(iend)
|
||||||
|
if iend >= num_events - 1:
|
||||||
|
break
|
||||||
|
istart = iend + 1
|
||||||
|
|
||||||
|
# get the finalized edges
|
||||||
|
if edges[-1] != num_events - 1:
|
||||||
|
edges.append(num_events - 1)
|
||||||
|
edges = times[np.array(edges)]
|
||||||
|
return edges
|
||||||
|
|
||||||
|
|
||||||
|
def time_to_spill(times, threshold):
|
||||||
|
"""Time-to-Spill Binning for an event list
|
||||||
|
Bins an event list by accumulating counts until the set threshold is reached,
|
||||||
|
and then creating a new bin.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (np.array): The time of each event
|
||||||
|
threshold (int): The count threshold for the histogram
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The edges of the binned data
|
||||||
|
"""
|
||||||
|
threshold = int(threshold)
|
||||||
|
assert threshold > 0, "Threshold must be positive"
|
||||||
|
# this is easy: take only the nth time (set by threshold) as the bin edge
|
||||||
|
edges = times[::threshold]
|
||||||
|
return edges
|
||||||
|
|
||||||
|
|
||||||
|
def bin_by_edges(times, time_edges):
|
||||||
|
"""Bins unbinned data by pre-defined edges. A rather trivial function :)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (np.array): The time of each event
|
||||||
|
time_edges (np.array): The pre-defined time edges
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The edges of the binned data
|
||||||
|
"""
|
||||||
|
return time_edges
|
707
coords.py
Normal file
707
coords.py
Normal file
@ -0,0 +1,707 @@
|
|||||||
|
# coords.py: Module containing coordinate conversion functions
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import astropy.coordinates as coordinates
|
||||||
|
import astropy.time
|
||||||
|
import numpy as np
|
||||||
|
from astropy.utils.data import download_file
|
||||||
|
from astropy.utils.iers import IERS_A, IERS_A_URL
|
||||||
|
from .time import TimeFermiSec
|
||||||
|
|
||||||
|
def haversine(lon1, lat1, lon2, lat2, deg=True):
|
||||||
|
"""Calculates the angular separation between two points using the
|
||||||
|
haversine equation. If degrees are passed, degrees are returned. else
|
||||||
|
the input/output is assumed to be radians.
|
||||||
|
lon -> azimuth
|
||||||
|
lat -> zenith
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lon1 (float): lon/az of first point
|
||||||
|
lat1 (float): lat/zen of first point
|
||||||
|
lon2 (float): lon/az of second point
|
||||||
|
lat2 (float): lat/zen of second point
|
||||||
|
deg (bool, optional): True if input/output in degrees.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Angular separation between points
|
||||||
|
"""
|
||||||
|
if deg:
|
||||||
|
lon1, lat1, lon2, lat2 = map(np.deg2rad, [lon1, lat1, lon2, lat2])
|
||||||
|
d_lat = 0.5 * (lat2 - lat1)
|
||||||
|
d_lon = 0.5 * (lon2 - lon1)
|
||||||
|
|
||||||
|
a = np.sin(d_lat) ** 2 + (np.sin(d_lon) ** 2 * np.cos(lat1) * np.cos(lat2))
|
||||||
|
alpha = 2. * np.arctan2(np.sqrt(a), np.sqrt(1.0 - a))
|
||||||
|
|
||||||
|
if deg:
|
||||||
|
alpha = np.rad2deg(alpha)
|
||||||
|
|
||||||
|
return alpha
|
||||||
|
|
||||||
|
|
||||||
|
def azzen_to_cartesian(az, zen, deg=True):
|
||||||
|
"""Convert spacecraft azimuth/zenith to Cartesian coordinates
|
||||||
|
|
||||||
|
Args:
|
||||||
|
az (float or np.array): Spacecraft azimuth
|
||||||
|
zen (float or np.array): Spacecraft zenith
|
||||||
|
deg (bool, optional): True (default) if input is in degrees,
|
||||||
|
otherwise input is radians
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: 3-element Cartesian coordinate
|
||||||
|
"""
|
||||||
|
if deg:
|
||||||
|
az = np.deg2rad(az)
|
||||||
|
zen = np.deg2rad(zen)
|
||||||
|
|
||||||
|
sinZen = np.sin(zen)
|
||||||
|
|
||||||
|
return np.array(
|
||||||
|
[sinZen * np.cos(az), sinZen * np.sin(az), np.cos(zen)])
|
||||||
|
|
||||||
|
|
||||||
|
def radec_to_cartesian(ra, dec, deg=True):
|
||||||
|
"""Convert RA/Dec to Cartesian coordinates
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ra (float or np.array): Right Ascension
|
||||||
|
dec (float or np.array): Declination
|
||||||
|
deg (bool, optional): True (default) if input is in degrees,
|
||||||
|
otherwise input is radians
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: 3-element Cartesian coordinate
|
||||||
|
"""
|
||||||
|
if deg:
|
||||||
|
ra = np.deg2rad(ra)
|
||||||
|
dec = np.deg2rad(dec)
|
||||||
|
|
||||||
|
return np.array(
|
||||||
|
[np.cos(dec) * np.cos(ra), np.cos(dec) * np.sin(ra), np.sin(dec)])
|
||||||
|
|
||||||
|
|
||||||
|
def quaternion_conj(quat):
|
||||||
|
"""Calculate conjugate of quaternion
|
||||||
|
|
||||||
|
Note:
|
||||||
|
GBM quaternions are defined with the last element as the scalar value
|
||||||
|
|
||||||
|
Args:
|
||||||
|
quat (np.array): 4-element quaternion
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: quaternion conjugate
|
||||||
|
"""
|
||||||
|
cquat = np.copy(quat)
|
||||||
|
cquat[0:3] = -cquat[0:3]
|
||||||
|
return cquat
|
||||||
|
|
||||||
|
|
||||||
|
def quaternion_inv(quat):
|
||||||
|
"""Calculate inverse of quaternion
|
||||||
|
|
||||||
|
Note:
|
||||||
|
GBM quaternions are defined with the last element as the scalar value
|
||||||
|
|
||||||
|
Args:
|
||||||
|
quat (np.array): 4-element quaternion
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: quaternion inverse
|
||||||
|
"""
|
||||||
|
cquat = quaternion_conj(quat)
|
||||||
|
iquat = cquat / np.sum(quat ** 2)
|
||||||
|
return iquat
|
||||||
|
|
||||||
|
|
||||||
|
def quaternion_prod(quat1, quat2):
|
||||||
|
"""Calculate product of two quaternions
|
||||||
|
|
||||||
|
Note:
|
||||||
|
GBM quaternions are defined with the last element as the scalar value
|
||||||
|
|
||||||
|
Args:
|
||||||
|
quat1 (np.array): 4-element quaternion
|
||||||
|
quat2 (np.array): 4-element quaternion
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: product of the quaternions
|
||||||
|
"""
|
||||||
|
q = np.copy(quat1)
|
||||||
|
r = np.copy(quat2)
|
||||||
|
q[0] = quat1[3]
|
||||||
|
q[1:] = quat1[0:3]
|
||||||
|
r[0] = quat2[3]
|
||||||
|
r[1:] = quat2[0:3]
|
||||||
|
|
||||||
|
t = np.copy(quat1)
|
||||||
|
t[0] = r[0] * q[0] - r[1] * q[1] - r[2] * q[2] - r[3] * q[3]
|
||||||
|
t[1] = r[0] * q[1] + r[1] * q[0] - r[2] * q[3] + r[3] * q[2]
|
||||||
|
t[2] = r[0] * q[2] + r[1] * q[3] + r[2] * q[0] - r[3] * q[1]
|
||||||
|
t[3] = r[0] * q[3] - r[1] * q[2] + r[2] * q[1] + r[3] * q[0]
|
||||||
|
quatprod = np.copy(t)
|
||||||
|
quatprod[3] = t[0]
|
||||||
|
quatprod[0:3] = t[1:]
|
||||||
|
return quatprod
|
||||||
|
|
||||||
|
|
||||||
|
def spacecraft_direction_cosines(quat):
|
||||||
|
"""Convert `n` spacecraft quaternions to direction cosine matrix
|
||||||
|
|
||||||
|
Args:
|
||||||
|
quat (np.array): (4, `n`) element array of quaternions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: (3,3, `n`) array containing the spacecraft direction cosines
|
||||||
|
"""
|
||||||
|
n_elements = quat.shape[0]
|
||||||
|
if n_elements != 4:
|
||||||
|
raise ValueError(
|
||||||
|
'Quaternion must have 4 elements, {0} given'.format(n_elements))
|
||||||
|
|
||||||
|
ndim = len(quat.shape)
|
||||||
|
if ndim == 2:
|
||||||
|
numquats = quat.shape[1]
|
||||||
|
else:
|
||||||
|
numquats = 1
|
||||||
|
quat /= np.linalg.norm(quat, axis=0)
|
||||||
|
|
||||||
|
sc_cosines = np.zeros((3, 3, numquats), dtype=float)
|
||||||
|
sc_cosines[0, 0, np.newaxis] = (
|
||||||
|
quat[0, np.newaxis] ** 2 - quat[1, np.newaxis] ** 2 - quat[
|
||||||
|
2, np.newaxis] ** 2 +
|
||||||
|
quat[3, np.newaxis] ** 2)
|
||||||
|
sc_cosines[1, 0, np.newaxis] = 2.0 * (
|
||||||
|
quat[0, np.newaxis] * quat[1, np.newaxis] + quat[
|
||||||
|
3, np.newaxis] *
|
||||||
|
quat[2, np.newaxis])
|
||||||
|
sc_cosines[2, 0, np.newaxis] = 2.0 * (
|
||||||
|
quat[0, np.newaxis] * quat[2, np.newaxis] - quat[
|
||||||
|
3, np.newaxis] *
|
||||||
|
quat[1, np.newaxis])
|
||||||
|
sc_cosines[0, 1, np.newaxis] = 2.0 * (
|
||||||
|
quat[0, np.newaxis] * quat[1, np.newaxis] - quat[
|
||||||
|
3, np.newaxis] *
|
||||||
|
quat[2, np.newaxis])
|
||||||
|
sc_cosines[1, 1, np.newaxis] = (
|
||||||
|
-quat[0, np.newaxis] ** 2 + quat[1, np.newaxis] ** 2 - quat[
|
||||||
|
2, np.newaxis] ** 2 +
|
||||||
|
quat[3, np.newaxis] ** 2)
|
||||||
|
sc_cosines[2, 1, np.newaxis] = 2.0 * (
|
||||||
|
quat[1, np.newaxis] * quat[2, np.newaxis] + quat[
|
||||||
|
3, np.newaxis] *
|
||||||
|
quat[0, np.newaxis])
|
||||||
|
sc_cosines[0, 2, np.newaxis] = 2.0 * (
|
||||||
|
quat[0, np.newaxis] * quat[2, np.newaxis] + quat[
|
||||||
|
3, np.newaxis] *
|
||||||
|
quat[1, np.newaxis])
|
||||||
|
sc_cosines[1, 2, np.newaxis] = 2.0 * (
|
||||||
|
quat[1, np.newaxis] * quat[2, np.newaxis] - quat[
|
||||||
|
3, np.newaxis] *
|
||||||
|
quat[0, np.newaxis])
|
||||||
|
sc_cosines[2, 2, np.newaxis] = (
|
||||||
|
-quat[0, np.newaxis] ** 2 - quat[1, np.newaxis] ** 2 + quat[
|
||||||
|
2, np.newaxis] ** 2 +
|
||||||
|
quat[3, np.newaxis] ** 2)
|
||||||
|
return np.squeeze(sc_cosines)
|
||||||
|
|
||||||
|
|
||||||
|
def geocenter_direction_cosines(gz, az, zen, deg=True):
|
||||||
|
"""Create `n` geocenter direction cosine matrix(es) for a position or
|
||||||
|
positions in spacecraft coordinates
|
||||||
|
|
||||||
|
Args:
|
||||||
|
gz (np.array): Geocenter vector in spacecraft Cartesian coordinates
|
||||||
|
az (float or np.array): The azimuth location of positions in spacecraft
|
||||||
|
coordinates
|
||||||
|
zen (float or np.array): The zenith location of positions in spacecraft
|
||||||
|
coordinates
|
||||||
|
deg (bool, optional): If True, the input is in degrees. Default is True.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: (3,3, `n`) array containing the geocenter direction cosines \
|
||||||
|
for each point
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
numpts = len(az)
|
||||||
|
except:
|
||||||
|
az = np.asarray([az])
|
||||||
|
zen = np.asarray([zen])
|
||||||
|
numpts = len(az)
|
||||||
|
|
||||||
|
geo_cosines = np.zeros((3, 3, numpts), dtype=float)
|
||||||
|
|
||||||
|
# the vector from the source position to the geocenter defines our frame.
|
||||||
|
# specifically, the source->geocenter vector is our z-axis
|
||||||
|
|
||||||
|
# geocenter vector in cartesian sc coordinates
|
||||||
|
# gz = azzen_to_cartesian(geo_az, geo_zen)
|
||||||
|
gz /= np.linalg.norm(gz) # np.sqrt(np.sum(gz**2))
|
||||||
|
|
||||||
|
# source vector in cartesian sc coordinates
|
||||||
|
sl = azzen_to_cartesian(az, zen)
|
||||||
|
sl /= np.linalg.norm(sl, axis=0)
|
||||||
|
|
||||||
|
# y-axis of our frame new frame
|
||||||
|
gy = np.array([gz[1] * sl[2, :] - gz[2] * sl[1, :],
|
||||||
|
gz[2] * sl[0, :] - gz[0] * sl[2, :],
|
||||||
|
gz[0] * sl[1, :] - gz[1] * sl[0, :]])
|
||||||
|
gy /= np.linalg.norm(gy, axis=0)
|
||||||
|
|
||||||
|
# x-axis of our new frame
|
||||||
|
gx = np.array([gy[1, :] * gz[2] - gy[2, :] * gz[1],
|
||||||
|
gy[2, :] * gz[0] - gy[0, :] * gz[2],
|
||||||
|
gy[0, :] * gz[1] - gy[1, :] * gz[0]])
|
||||||
|
gx /= np.linalg.norm(gx, axis=0)
|
||||||
|
|
||||||
|
# complete direction cosine matrix
|
||||||
|
geo_cosines[0, :, :] = gx
|
||||||
|
geo_cosines[1, :, :] = gy
|
||||||
|
geo_cosines[2, :, :] = gz[:, np.newaxis]
|
||||||
|
return np.squeeze(geo_cosines)
|
||||||
|
|
||||||
|
|
||||||
|
def geocenter_to_spacecraft(phi, theta, geo_cosines):
|
||||||
|
"""Convert `n` points from geocenter frame to spacecraft frame
|
||||||
|
|
||||||
|
Args:
|
||||||
|
phi (float or np.array): The azimuthal location of positions in
|
||||||
|
geocentric coordinates
|
||||||
|
theta (float or np.array): The polar location of positions in
|
||||||
|
geocentric coordinates
|
||||||
|
geo_cosines (np.array): (3,3) array containing the direction cosines
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array): Azimuth and zenith positions in the spacecraft frame
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
numpts = len(phi)
|
||||||
|
except:
|
||||||
|
phi = np.asarray([phi])
|
||||||
|
theta = np.asarray([theta])
|
||||||
|
numpts = len(phi)
|
||||||
|
|
||||||
|
# spherical coordinate vector in cartesian coordinates
|
||||||
|
a = azzen_to_cartesian(phi, theta)
|
||||||
|
|
||||||
|
gx = geo_cosines[0, :]
|
||||||
|
gy = geo_cosines[1, :]
|
||||||
|
gz = geo_cosines[2, :]
|
||||||
|
|
||||||
|
# the cartesian vector rotated into the spacecraft frame
|
||||||
|
dir = [a[0, :] * gx[0] + a[1, :] * gy[0] + a[2, :] * gz[0],
|
||||||
|
a[0, :] * gx[1] + a[1, :] * gy[1] + a[2, :] * gz[1],
|
||||||
|
a[0, :] * gx[2] + a[1, :] * gy[2] + a[2, :] * gz[2]]
|
||||||
|
dir = -np.array(dir)
|
||||||
|
dir /= np.linalg.norm(dir, axis=0)
|
||||||
|
|
||||||
|
# convert cartesian vector to az/zen
|
||||||
|
az = np.rad2deg(np.arctan2(dir[1, :], dir[0, :]))
|
||||||
|
az[az < 0.0] += 360.0
|
||||||
|
zen = np.rad2deg(np.arccos(dir[2, :]))
|
||||||
|
return (np.squeeze(az), np.squeeze(zen), np.squeeze(dir))
|
||||||
|
|
||||||
|
|
||||||
|
def geocenter_in_radec(coord):
|
||||||
|
"""Calculate the location of the Earth center RA and Dec
|
||||||
|
|
||||||
|
Args:
|
||||||
|
coord (np.array): (3, `n`) array containing Geocentric cartesian
|
||||||
|
coordinates in meters
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array): RA and Dec of Earth center as viewed by \
|
||||||
|
Fermi in degrees
|
||||||
|
"""
|
||||||
|
unit_vec = -coord / np.linalg.norm(-coord, axis=0)
|
||||||
|
dec = np.pi / 2.0 - np.arccos(unit_vec[2, np.newaxis])
|
||||||
|
ra = np.arctan2(unit_vec[1, np.newaxis], unit_vec[0, np.newaxis])
|
||||||
|
ra[ra < 0.0] += 2.0 * np.pi
|
||||||
|
return np.squeeze(np.rad2deg(ra)), np.squeeze(np.rad2deg(dec))
|
||||||
|
|
||||||
|
|
||||||
|
def spacecraft_to_radec(az, zen, quat, deg=True):
|
||||||
|
"""Convert a position in spacecraft coordinates (Az/Zen) to J2000 RA/Dec
|
||||||
|
The options for input for this function are as follows:
|
||||||
|
|
||||||
|
* a single Az/Zen position and multiple attitude transforms
|
||||||
|
* multiple Az/Zen positions and a single attitude transform
|
||||||
|
* multiple Az/Zen positions each with a corresponding attitude transform
|
||||||
|
|
||||||
|
Args:
|
||||||
|
az (float or np.array): Spacecraft azimuth
|
||||||
|
zen (float or np.array): Spacecraft zenith
|
||||||
|
quat (np.array): (4, `n`) spacecraft attitude quaternion array
|
||||||
|
deg (bool, optional): True if input/output in degrees.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array): RA and Dec of the transformed position
|
||||||
|
"""
|
||||||
|
ndim = len(quat.shape)
|
||||||
|
if ndim == 2:
|
||||||
|
numquats = quat.shape[1]
|
||||||
|
else:
|
||||||
|
numquats = 1
|
||||||
|
|
||||||
|
# convert az/zen to cartesian coordinates
|
||||||
|
pos = azzen_to_cartesian(az, zen, deg=deg)
|
||||||
|
ndim = len(pos.shape)
|
||||||
|
if ndim == 2:
|
||||||
|
numpos = pos.shape[1]
|
||||||
|
else:
|
||||||
|
numpos = 1
|
||||||
|
|
||||||
|
# spacecraft direction cosine matrix
|
||||||
|
sc_cosines = spacecraft_direction_cosines(quat)
|
||||||
|
|
||||||
|
# can do one sky position over many transforms, many sky positions over one
|
||||||
|
# transform, or a transform for each sky position
|
||||||
|
if (numpos == 1) & (numquats > 1):
|
||||||
|
pos = np.repeat(pos, numquats).reshape(3, -1)
|
||||||
|
numdo = numquats
|
||||||
|
elif (numpos > 1) & (numquats == 1):
|
||||||
|
sc_cosines = np.repeat(sc_cosines, numpos).reshape(3, 3, -1)
|
||||||
|
numdo = numpos
|
||||||
|
elif numpos == numquats:
|
||||||
|
numdo = numpos
|
||||||
|
if numdo == 1:
|
||||||
|
sc_cosines = sc_cosines[:, :, np.newaxis]
|
||||||
|
pos = pos[:, np.newaxis]
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
'If the size of az/zen coordinates is > 1 AND the sizeof quaternions is > 1, then they must be of same size'
|
||||||
|
)
|
||||||
|
|
||||||
|
# convert numpy arrays to list of arrays for vectorized calculations
|
||||||
|
sc_cosines_list = np.squeeze(np.split(sc_cosines, numdo, axis=2))
|
||||||
|
pos_list = np.squeeze(np.split(pos, numdo, axis=1))
|
||||||
|
if numdo == 1:
|
||||||
|
sc_cosines_list = [sc_cosines_list]
|
||||||
|
pos_list = [pos_list]
|
||||||
|
|
||||||
|
# convert position to J2000 frame
|
||||||
|
cartesian_pos = np.array(list(map(np.dot, sc_cosines_list, pos_list))).T
|
||||||
|
cartesian_pos[2, (cartesian_pos[2, np.newaxis] < -1.0).reshape(-1)] = -1.0
|
||||||
|
cartesian_pos[2, (cartesian_pos[2, np.newaxis] > 1.0).reshape(-1)] = 1.0
|
||||||
|
|
||||||
|
# transform cartesian position to RA/Dec in J2000 frame
|
||||||
|
dec = np.arcsin(cartesian_pos[2, np.newaxis])
|
||||||
|
ra = np.arctan2(cartesian_pos[1, np.newaxis], cartesian_pos[0, np.newaxis])
|
||||||
|
ra[(np.abs(cartesian_pos[1, np.newaxis]) < 1e-6) & (
|
||||||
|
np.abs(cartesian_pos[0, np.newaxis]) < 1e-6)] = 0.0
|
||||||
|
ra[ra < 0.0] += 2.0 * np.pi
|
||||||
|
|
||||||
|
if deg:
|
||||||
|
ra = np.rad2deg(ra)
|
||||||
|
dec = np.rad2deg(dec)
|
||||||
|
|
||||||
|
return np.squeeze(ra), np.squeeze(dec)
|
||||||
|
|
||||||
|
|
||||||
|
def radec_to_spacecraft(ra, dec, quat, deg=True):
|
||||||
|
"""Convert a position in J2000 RA/Dec to spacecraft coordinates (Az/Zen).
|
||||||
|
The options for input for this function are as follows:
|
||||||
|
|
||||||
|
* a single RA/Dec position and multiple attitude transforms
|
||||||
|
* multiple RA/Dec positions and a single attitude transform
|
||||||
|
* multiple RA/Dec positions each with a corresponding attitude transform
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ra (float or np.array): Right Ascension
|
||||||
|
dec (float or np.array): Declination
|
||||||
|
quat (np.array): (4, `n`) spacecraft attitude quaternion array
|
||||||
|
deg (bool, optional): True if input/output in degrees.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array): Spacecraft azimuth and zenith of the transformed \
|
||||||
|
position
|
||||||
|
"""
|
||||||
|
ndim = len(quat.shape)
|
||||||
|
if ndim == 2:
|
||||||
|
numquats = quat.shape[1]
|
||||||
|
else:
|
||||||
|
numquats = 1
|
||||||
|
|
||||||
|
# convert az/zen to cartesian coordinates
|
||||||
|
pos = radec_to_cartesian(ra, dec, deg=deg)
|
||||||
|
ndim = len(pos.shape)
|
||||||
|
if ndim == 2:
|
||||||
|
numpos = pos.shape[1]
|
||||||
|
else:
|
||||||
|
numpos = 1
|
||||||
|
|
||||||
|
# spacecraft direction cosine matrix
|
||||||
|
sc_cosines = spacecraft_direction_cosines(quat)
|
||||||
|
|
||||||
|
# can do one sky position over many transforms, many sky positions over one
|
||||||
|
# transform, or a transform for each sky position
|
||||||
|
if (numpos == 1) & (numquats > 1):
|
||||||
|
pos = np.repeat(pos, numquats).reshape(3, -1)
|
||||||
|
numdo = numquats
|
||||||
|
elif (numpos > 1) & (numquats == 1):
|
||||||
|
sc_cosines = np.repeat(sc_cosines, numpos).reshape(3, 3, -1)
|
||||||
|
numdo = numpos
|
||||||
|
elif numpos == numquats:
|
||||||
|
numdo = numpos
|
||||||
|
if numdo == 1:
|
||||||
|
sc_cosines = sc_cosines[:, :, np.newaxis]
|
||||||
|
pos = pos[:, np.newaxis]
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
'If the size of az/zen coordinates is > 1 AND the sizeof quaternions is > 1, then they must be of same size'
|
||||||
|
)
|
||||||
|
|
||||||
|
# convert numpy arrays to list of arrays for vectorized calculations
|
||||||
|
sc_cosines_list = np.squeeze(np.split(sc_cosines, numdo, axis=2))
|
||||||
|
pos_list = np.squeeze(np.split(pos, numdo, axis=1))
|
||||||
|
if numdo == 1:
|
||||||
|
sc_cosines_list = [sc_cosines_list]
|
||||||
|
pos_list = [pos_list]
|
||||||
|
|
||||||
|
# convert position from J2000 frame
|
||||||
|
cartesian_pos = np.array(list(map(np.dot, pos_list, sc_cosines_list))).T
|
||||||
|
|
||||||
|
# convert Cartesian coordinates to spherical
|
||||||
|
zen = np.arccos(cartesian_pos[2, np.newaxis])
|
||||||
|
az = np.arctan2(cartesian_pos[1, np.newaxis], cartesian_pos[0, np.newaxis])
|
||||||
|
az[(np.abs(cartesian_pos[1, np.newaxis]) < 1e-6) & (
|
||||||
|
np.abs(cartesian_pos[0, np.newaxis]) < 1e-6)] = 0.0
|
||||||
|
az[az < 0.0] += 2.0 * np.pi
|
||||||
|
|
||||||
|
if deg:
|
||||||
|
az = np.rad2deg(az)
|
||||||
|
zen = np.rad2deg(zen)
|
||||||
|
|
||||||
|
return np.squeeze(az), np.squeeze(zen)
|
||||||
|
|
||||||
|
|
||||||
|
def latitude_from_geocentric_coords_simple(coord):
|
||||||
|
"""Calculate latitude from Geocentric Cartesian coordinates. Assumes the
|
||||||
|
Earth is a simple sphere. Also returns altitude.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
coord (np.array): (3, `n`) array containing Geocentric cartesian
|
||||||
|
coordinates in meters
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array): Position of Fermi in Earth Latitude and altitude \
|
||||||
|
in meters
|
||||||
|
"""
|
||||||
|
mean_earth_radius = 6371009.0 # m
|
||||||
|
radius = np.sqrt(np.sum(coord[:, np.newaxis] ** 2, axis=0))
|
||||||
|
latitude = np.rad2deg(np.arcsin(coord[2, np.newaxis] / radius))
|
||||||
|
altitude = radius - mean_earth_radius
|
||||||
|
return np.squeeze(latitude), np.squeeze(altitude)
|
||||||
|
|
||||||
|
|
||||||
|
def latitude_from_geocentric_coords_complex(coord):
|
||||||
|
"""Calculate latitude from Geocentric Cartesian coordinates. Uses the
|
||||||
|
WGS 1984 model of the shape of the Earth. Also returns altitude.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
coord (np.array): (3, `n`) array containing Geocentric cartesian
|
||||||
|
coordinates in meters
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array): Position of Fermi in Earth Latitude and altitude \
|
||||||
|
in meters
|
||||||
|
"""
|
||||||
|
ndim = len(coord.shape)
|
||||||
|
if ndim == 2:
|
||||||
|
numpoints = coord.shape[1]
|
||||||
|
else:
|
||||||
|
numpoints = 1
|
||||||
|
|
||||||
|
# Parameters of the World Geodetic System 1984
|
||||||
|
# semi-major axis
|
||||||
|
wgs84_a = 6378137.0 # km
|
||||||
|
# reciprocal of flattening
|
||||||
|
wgs84_1overf = 298.257223563
|
||||||
|
|
||||||
|
rho = np.sqrt(coord[0, np.newaxis] ** 2 + coord[1, np.newaxis] ** 2)
|
||||||
|
f = 1.0 / wgs84_1overf
|
||||||
|
e_sq = 2.0 * f - f ** 2
|
||||||
|
|
||||||
|
# should completely converge in 3 iterations
|
||||||
|
n_iter = 3
|
||||||
|
kappa = np.zeros((n_iter + 1, numpoints), dtype=float)
|
||||||
|
kappa[0, :np.newaxis] = 1.0 / (1.0 - e_sq)
|
||||||
|
for i in range(1, n_iter + 1):
|
||||||
|
c = (rho ** 2 + (1.0 - e_sq) * coord[2, np.newaxis] ** 2 * kappa[
|
||||||
|
i - 1, np.newaxis] ** 2) ** 1.5 / (
|
||||||
|
wgs84_a * e_sq)
|
||||||
|
kappa[i, np.newaxis] = (c + (1.0 - e_sq) * coord[2, np.newaxis] ** 2 *
|
||||||
|
kappa[i - 1, np.newaxis] ** 3) / (
|
||||||
|
c - rho ** 2)
|
||||||
|
|
||||||
|
phi = np.arctan(kappa[-1, np.newaxis] * coord[2, np.newaxis] / rho)
|
||||||
|
h = (1.0 / kappa[-1, np.newaxis] - 1.0 / kappa[0, np.newaxis]) * \
|
||||||
|
np.sqrt(rho ** 2 + coord[2, np.newaxis] ** 2 * kappa[
|
||||||
|
-1, np.newaxis] ** 2) / e_sq
|
||||||
|
latitude = np.rad2deg(phi)
|
||||||
|
altitude = h
|
||||||
|
return np.squeeze(latitude), np.squeeze(altitude)
|
||||||
|
|
||||||
|
|
||||||
|
def longitude_from_geocentric_coords(coord, met, ut1=False):
|
||||||
|
"""Calculate the East longitude from Geocentric coordinates. Requires the
|
||||||
|
conversion of Fermi MET to sidereal time to rotate the Earth under the
|
||||||
|
spacecraft. The conversion can either be performed using UTC (less
|
||||||
|
accurate) or UT1 (more accurate) which uses IERS tables to correct for
|
||||||
|
variations in Earth rotation velocity
|
||||||
|
|
||||||
|
Args:
|
||||||
|
coord (np.array): (3, `n`) array containing Geocentric cartesian
|
||||||
|
coordinates in meters
|
||||||
|
met: (float or np.array): The MET time(s)
|
||||||
|
ut1 (bool, optional): If True, use UT1 instead of UTC to calculate
|
||||||
|
sidereal time. Default is False
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array or float: Position of Fermi in East Longitude
|
||||||
|
"""
|
||||||
|
ndim = len(coord.shape)
|
||||||
|
if ndim == 2:
|
||||||
|
if coord.shape[1] != met.shape[0]:
|
||||||
|
raise ValueError(
|
||||||
|
'The number of coordinate positions must match the number of times')
|
||||||
|
|
||||||
|
# get Fermi time object in UTC standard; need astropy for sidereal time conversion
|
||||||
|
utc_time = astropy.time.Time(met, format='fermi').utc
|
||||||
|
if ut1 is True:
|
||||||
|
try:
|
||||||
|
# Use IERS table for sidereal time conversion
|
||||||
|
utc_time.delta_ut1_utc = utc_time.get_delta_ut1_utc()
|
||||||
|
theta_deg = utc_time.sidereal_time('apparent', 'greenwich').degree
|
||||||
|
except:
|
||||||
|
# IERS table is not current. Update IERS table
|
||||||
|
print("IERS table is not current. Checking for updated Table.")
|
||||||
|
iers_a_file = download_file(IERS_A_URL, cache=True)
|
||||||
|
iers_a = IERS_A.open(iers_a_file)
|
||||||
|
utc_time.delta_ut1_utc = utc_time.get_delta_ut1_utc(iers_a)
|
||||||
|
theta_deg = utc_time.sidereal_time('apparent', 'greenwich').degree
|
||||||
|
else:
|
||||||
|
# Use less accurate UTC for sidereal time conversion
|
||||||
|
utc_time.delta_ut1_utc = 0.0
|
||||||
|
theta_deg = utc_time.sidereal_time('apparent', 'greenwich').degree
|
||||||
|
|
||||||
|
inertial_longitude = np.arctan2(coord[1, np.newaxis], coord[0, np.newaxis])
|
||||||
|
east_longitude = np.rad2deg(inertial_longitude) - theta_deg
|
||||||
|
|
||||||
|
east_longitude[east_longitude < 0.0] += 360.0
|
||||||
|
east_longitude[east_longitude < 0.0] += 360.0
|
||||||
|
return np.squeeze(east_longitude)
|
||||||
|
|
||||||
|
|
||||||
|
def calc_mcilwain_l(latitude, longitude):
|
||||||
|
"""Estimate the McIlwain L value given the latitude (-30, +30) and
|
||||||
|
East Longitude. This uses a cubic polynomial approximation to the full
|
||||||
|
calculation and is similar to the approach used by the GBM FSW.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
latitude (np.array): Latitude in degrees from -180 to 180
|
||||||
|
longitude (np.array): East longitude in degrees from 0 to 360
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: McIlwain L value
|
||||||
|
"""
|
||||||
|
|
||||||
|
latitude = np.asarray([latitude])
|
||||||
|
longitude = np.asarray([longitude])
|
||||||
|
orig_shape = latitude.shape
|
||||||
|
latitude = latitude.flatten()
|
||||||
|
longitude = longitude.flatten()
|
||||||
|
# numPts = latitude.shape[0]
|
||||||
|
coeffs_file = os.path.join(os.path.dirname(__file__),
|
||||||
|
'McIlwainL_Coeffs.npy')
|
||||||
|
poly_coeffs = np.load(coeffs_file)
|
||||||
|
longitude[longitude < 0.0] += 360.0
|
||||||
|
longitude[longitude == 360.0] = 0.0
|
||||||
|
|
||||||
|
bad_idx = (latitude < -30.0) | (latitude > 30.0) | (longitude < 0.0) | (
|
||||||
|
longitude >= 360.0)
|
||||||
|
if np.sum(bad_idx) != 0:
|
||||||
|
raise ValueError(
|
||||||
|
'Out of range coordinates for McIlwain L for {0} locations'.format(
|
||||||
|
np.sum(bad_idx)))
|
||||||
|
|
||||||
|
idx = np.asarray((longitude / 10.0).astype(int))
|
||||||
|
idx2 = np.asarray(idx + 1)
|
||||||
|
idx2[idx2 >= 36] = 0
|
||||||
|
idx2 = idx2.astype(int)
|
||||||
|
|
||||||
|
longitude_left = 10.0 * idx
|
||||||
|
f = (longitude - longitude_left) / 10.0 # interpolation weight, 0 to 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
num_pts = len(latitude)
|
||||||
|
except:
|
||||||
|
num_pts = 1
|
||||||
|
mc_l = np.zeros(num_pts)
|
||||||
|
for i in range(num_pts):
|
||||||
|
mc_l[i] = (1.0 - f[i]) * (
|
||||||
|
poly_coeffs[idx[i], 0] + poly_coeffs[idx[i], 1] * latitude[i] +
|
||||||
|
poly_coeffs[idx[i], 2] *
|
||||||
|
latitude[i] ** 2 + poly_coeffs[idx[i], 3] * latitude[i] ** 3) + \
|
||||||
|
f[i] * (
|
||||||
|
poly_coeffs[idx2[i], 0] + poly_coeffs[idx2[i], 1] *
|
||||||
|
latitude[i] + poly_coeffs[idx2[i], 2] *
|
||||||
|
latitude[i] ** 2 + poly_coeffs[idx2[i], 3] *
|
||||||
|
latitude[i] ** 3)
|
||||||
|
mc_l = mc_l.reshape(orig_shape)
|
||||||
|
return np.squeeze(mc_l)
|
||||||
|
|
||||||
|
|
||||||
|
def get_sun_loc(met):
|
||||||
|
"""Calculate sun location in RA/Dec for a given MET.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
met (float): The MET time(s)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(float, float): RA and Dec of the sun
|
||||||
|
"""
|
||||||
|
|
||||||
|
utc_time = astropy.time.Time(met, format='fermi').utc
|
||||||
|
sun = coordinates.get_sun(utc_time)
|
||||||
|
return sun.ra.degree, sun.dec.degree
|
||||||
|
|
||||||
|
|
||||||
|
def saa_boundary():
|
||||||
|
"""The coordinates of the SAA boundary in latitude and East longitude
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array): The latitude and East longitude values
|
||||||
|
"""
|
||||||
|
lat_saa = np.array([-30.000, -19.867, -9.733, 0.400, 2.000, 2.000, -1.000,
|
||||||
|
-6.155, -8.880, -14.220, -18.404, -30.000, -30.000])
|
||||||
|
lon_saa = np.array(
|
||||||
|
[33.900, 12.398, -9.103, -30.605, -38.400, -45.000, -65.000,
|
||||||
|
-84.000, -89.200, -94.300, -94.300, -86.100, 33.900])
|
||||||
|
return (lat_saa, lon_saa)
|
9
data/__init__.py
Normal file
9
data/__init__.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from .collection import DataCollection, GbmDetectorCollection
|
||||||
|
from .drm import RSP
|
||||||
|
from .localization import HealPix, GbmHealPix
|
||||||
|
from .phaii import PHAII, Ctime, Cspec, TTE
|
||||||
|
from .pha import PHA, BAK
|
||||||
|
from .poshist import PosHist
|
||||||
|
from .tcat import Tcat
|
||||||
|
from .trigdat import Trigdat
|
||||||
|
from .scat import Scat
|
377
data/collection.py
Normal file
377
data/collection.py
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
# collection.py: Data collection classes
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import re
|
||||||
|
from functools import partial
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
|
||||||
|
class DataCollection:
|
||||||
|
"""A container for a collection of like data objects, such as a collection
|
||||||
|
of :class:`Ctime` objects. This class exposes the individual objects'
|
||||||
|
attributes, and it exposes the methods of the object class so that methods
|
||||||
|
can be called on the collection as a whole. For that reason, each object in
|
||||||
|
the DataCollection must be of the same type, otherwise an error is raised.
|
||||||
|
The type of the collection is set by the first object inserted into the
|
||||||
|
collection and the collection type is immutable thereafter.
|
||||||
|
|
||||||
|
Objects are stored in the collection in the order they are added.
|
||||||
|
|
||||||
|
The number of items in the collection can be retrieved by ``len()`` and
|
||||||
|
one can iterate over the items::
|
||||||
|
[data_item for data_item in DataCollection]
|
||||||
|
|
||||||
|
In addition to the DataCollection methods, all of the individual object
|
||||||
|
attributes and methods are exposed, and they become methods of the
|
||||||
|
DataCollection. Note that individual object attributes become *methods*
|
||||||
|
i.e. if you have an item attribute called item.name, then the corresponding
|
||||||
|
DataCollection method would be item.name().
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
items (list): The names of the items in the DataCollection
|
||||||
|
types (str): The type of the objects in the DataCollection
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._data_dict = OrderedDict()
|
||||||
|
self._type = None
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
for item in self._data_dict.values():
|
||||||
|
yield item
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
return len(self._data_dict)
|
||||||
|
|
||||||
|
def _enforce_type(self, data_item):
|
||||||
|
if not isinstance(data_item, self._type) and self._type is not None:
|
||||||
|
raise TypeError(
|
||||||
|
'Incorrect data item for {}'.format(self.__class__.__name__))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def items(self):
|
||||||
|
return list(self._data_dict.keys())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def types(self):
|
||||||
|
return self._type
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_list(cls, data_list, names=None):
|
||||||
|
"""Given a list of objects and optionally a list of corresponding names,
|
||||||
|
create a new DataCollection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_list (list of :obj:`objects`):
|
||||||
|
The list of objects to be in the collection
|
||||||
|
names (list of :obj:`str`, optional):
|
||||||
|
The list of corresponding names to the objects. If not set,
|
||||||
|
will try to retrieve a name from object.filename (assuming it's
|
||||||
|
a data object). If that fails, each item will be named
|
||||||
|
ambiguously 'item1', 'item2', etc.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
:py:class:`DataCollection`: The newly created collection
|
||||||
|
"""
|
||||||
|
obj = cls()
|
||||||
|
|
||||||
|
# set the names
|
||||||
|
if names is not None:
|
||||||
|
if len(names) != len(data_list):
|
||||||
|
raise ValueError('Names list must be same size as data list')
|
||||||
|
else:
|
||||||
|
names = [None] * len(data_list)
|
||||||
|
|
||||||
|
# include the objects
|
||||||
|
for data_item, name in zip(data_list, names):
|
||||||
|
obj.include(data_item, name=name)
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def to_list(self):
|
||||||
|
"""Return the objects contained in the DataCollection as a list.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(list of :obj:`objects`):
|
||||||
|
The list of objects, in the order that they were inserted
|
||||||
|
"""
|
||||||
|
return [self.get_item(name) for name in self.items]
|
||||||
|
|
||||||
|
def include(self, data_item, name=None):
|
||||||
|
"""Insert an object into the collection. The first item inserted will
|
||||||
|
set the immutable type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_item (:obj:`object`): A data object to include
|
||||||
|
name (str, optional):
|
||||||
|
An optional corresponding name. If not set, will try to
|
||||||
|
retrieve a name from object.filename (assuming it's a data
|
||||||
|
object). If that fails, each item will be named ambiguously
|
||||||
|
'item1', 'item2', etc.
|
||||||
|
"""
|
||||||
|
# if this is the first item inserted, set the type of the Collection
|
||||||
|
# and expose the attributes and methods of the object
|
||||||
|
if len(self) == 0:
|
||||||
|
self._type = type(data_item)
|
||||||
|
dir = [key for key in data_item.__dir__() if
|
||||||
|
not re.match('_.', key)]
|
||||||
|
for key in dir:
|
||||||
|
setattr(self, key, partial(self._method_call, key))
|
||||||
|
else:
|
||||||
|
# otherwise, ensure that each object inserted is of the same type
|
||||||
|
if type(data_item) != self._type:
|
||||||
|
raise TypeError('A DataCollection must contain like objects')
|
||||||
|
|
||||||
|
# insert with user-defined name
|
||||||
|
if name is not None:
|
||||||
|
self._data_dict[name] = data_item
|
||||||
|
else:
|
||||||
|
# or try to insert using filename attribute
|
||||||
|
try:
|
||||||
|
self._data_dict[data_item.filename] = data_item
|
||||||
|
# otherwise default to ambiguity
|
||||||
|
except AttributeError:
|
||||||
|
self._data_dict['item{}'.format(len(self) + 1)] = data_item
|
||||||
|
|
||||||
|
def remove(self, item_name):
|
||||||
|
"""Remove an object from the collection given the name
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item_name (str): The name of the item to remove
|
||||||
|
"""
|
||||||
|
self._data_dict.pop(item_name)
|
||||||
|
|
||||||
|
def get_item(self, item_name):
|
||||||
|
"""Retrieve an object from the DataCollection by name
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item_name (str): The name of the item to retrieve
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:obj:`object`: The retrieved data item
|
||||||
|
"""
|
||||||
|
return self._data_dict[item_name]
|
||||||
|
|
||||||
|
def _method_call(self, method_name, *args, **kwargs):
|
||||||
|
"""This is the wrapper for the exposde attribute and method calls.
|
||||||
|
Applies method_name over all items in the DataCollection
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method_name (str): The name of the method or attribute
|
||||||
|
*args: Additional arguments to be passed to the method
|
||||||
|
**kwargs: Additional keyword arguments to be passed to the method
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None or list: If not None, will return the results from all
|
||||||
|
objects in the list
|
||||||
|
"""
|
||||||
|
# get the attributes/methods for each item
|
||||||
|
refs = [getattr(obj, method_name) for obj in self._data_dict.values()]
|
||||||
|
|
||||||
|
# if method_name is a method, then it will be callable
|
||||||
|
if callable(refs[0]):
|
||||||
|
res = [getattr(obj, method_name)(*args, **kwargs)
|
||||||
|
for obj in self._data_dict.values()]
|
||||||
|
# otherwise, method_name will not be callable if it is an attribute
|
||||||
|
else:
|
||||||
|
# we are setting an attribute
|
||||||
|
if len(args) != 0:
|
||||||
|
res = [setattr(obj, method_name, *args)
|
||||||
|
for obj in self._data_dict.values()]
|
||||||
|
# we are retrieving an attribute
|
||||||
|
else:
|
||||||
|
res = refs
|
||||||
|
|
||||||
|
if res[0] is not None:
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
class GbmDetectorCollection(DataCollection):
|
||||||
|
"""A container for a collection of GBM-specific data objects, such as a
|
||||||
|
collection of ``Ctime`` objects from different detectors.
|
||||||
|
|
||||||
|
The special behavior of this class is to provide a way to interact with
|
||||||
|
a collection of detector data that may contain a mix of different *types*
|
||||||
|
of detectors. For example, many times we want a collection of GBM NaI
|
||||||
|
and GBM BGO detectors. These detectors have very different energy ranges,
|
||||||
|
and so may require different inputs for a variety of functions. This
|
||||||
|
collection allows one to specify the different arguments for NaI and BGO
|
||||||
|
data without having to implement many ugly and space-wasting loops and
|
||||||
|
``if...else`` decisions.
|
||||||
|
|
||||||
|
In addition to the GbmDetectorCollection methods, all of the individual
|
||||||
|
object attributes and methods are exposed, and they become methods of the
|
||||||
|
DataCollection. Note that individual object attributes become *methods*
|
||||||
|
i.e. if you have an item attribute called item.name, then the corresponding
|
||||||
|
DataCollection method would be item.name().
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
items (list): The names of the items in the DataCollection
|
||||||
|
types (str): The type of the objects in the DataCollection
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._dets = []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_list(cls, data_list, names=None, dets=None):
|
||||||
|
"""Given a list of objects and optionally a list of corresponding names
|
||||||
|
and corresponding detector names, create a new GbmDetectorCollection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_list (list of :obj:`objects`):
|
||||||
|
The list of objects to be in the collection
|
||||||
|
names (list of :obj:`str`, optional):
|
||||||
|
The list of corresponding names to the objects. If not set,
|
||||||
|
will try to retrieve a name from object.filename (assuming it's
|
||||||
|
a data object). If that fails, each item will be named
|
||||||
|
ambiguously 'item1', 'item2', etc.
|
||||||
|
dets (list of :obj:`str`, optional):
|
||||||
|
The detector names for each object. If not set, will try to
|
||||||
|
retrieve from the object.detector attribute. If that attribute
|
||||||
|
doesn't exist, an error will be raised, and the user will need
|
||||||
|
to specify this list.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
:py:class:`GbmDetectorCollection`: The newly created collection
|
||||||
|
"""
|
||||||
|
obj = cls()
|
||||||
|
|
||||||
|
# set the detector names
|
||||||
|
if dets is not None:
|
||||||
|
if len(dets) != len(data_list):
|
||||||
|
raise ValueError(
|
||||||
|
'Detector list must be same size as data list')
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
dets = [data_item.detector for data_item in data_list]
|
||||||
|
except:
|
||||||
|
raise AttributeError('Cannot find detector information. '
|
||||||
|
'Need to manually set')
|
||||||
|
# set the names
|
||||||
|
if names is not None:
|
||||||
|
if len(names) != len(data_list):
|
||||||
|
raise ValueError('Names list must be same size as data list')
|
||||||
|
else:
|
||||||
|
names = [None] * len(data_list)
|
||||||
|
|
||||||
|
# include the objects
|
||||||
|
[obj.include(data_item, det, name=name) for (data_item, det, name)
|
||||||
|
in zip(data_list, dets, names)]
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def remove(self, item_name):
|
||||||
|
"""Remove an object from the collection given the name
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item_name (str): The name of the item to remove
|
||||||
|
"""
|
||||||
|
index = [item == item_name for item in self.items].index(True)
|
||||||
|
self._dets.pop(index)
|
||||||
|
self._data_dict.pop(item_name)
|
||||||
|
|
||||||
|
def include(self, data_item, det, name=None):
|
||||||
|
"""Insert an object into the GbmDetectorCollection. The first item
|
||||||
|
inserted will set the immutable type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_item (:obj:`object`): A data object to include
|
||||||
|
det (str): The corresponding detector for the item
|
||||||
|
name (str, optional):
|
||||||
|
An optional corresponding name. If not set, will try to
|
||||||
|
retrieve a name from object.filename (assuming it's a data
|
||||||
|
object). If that fails, each item will be named ambiguously
|
||||||
|
'item1', 'item2', etc.
|
||||||
|
"""
|
||||||
|
super().include(data_item, name=None)
|
||||||
|
self._dets.append(det)
|
||||||
|
|
||||||
|
def _method_call(self, method_name, *args, nai_args=(), nai_kwargs=None,
|
||||||
|
bgo_args=(), bgo_kwargs=None, **kwargs):
|
||||||
|
"""This is the wrapper for the attribute and method calls. Applies
|
||||||
|
method_name over all items in the GbmDetectorCollection.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method_name (str): The name of the method or attribute
|
||||||
|
*args: Additional arguments to be passed to the method
|
||||||
|
nai_args: Arguments to be applied only to the NaI objects
|
||||||
|
bgo_args: Arguments to be applied only to the BGO objects
|
||||||
|
nai_kwargs: Keywords to be applied only to the NaI objects
|
||||||
|
bgo_kwargs: Keywords to be applied only to the BGO objects
|
||||||
|
**kwargs: Additional keyword arguments to be passed to the
|
||||||
|
method. Will be applied to both NaI and BGO objects
|
||||||
|
and will be appended to any existing keywords from
|
||||||
|
nai_kwargs or bgo_kwargs
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None or list: If not None, will return the results from all objects
|
||||||
|
in the list
|
||||||
|
"""
|
||||||
|
if nai_kwargs is None:
|
||||||
|
nai_kwargs = {}
|
||||||
|
if bgo_kwargs is None:
|
||||||
|
bgo_kwargs = {}
|
||||||
|
|
||||||
|
if len(args) > 0:
|
||||||
|
nai_args = args
|
||||||
|
bgo_args = args
|
||||||
|
if len(kwargs) > 0:
|
||||||
|
nai_kwargs.update(kwargs)
|
||||||
|
bgo_kwargs.update(kwargs)
|
||||||
|
|
||||||
|
# get the attributes/methods for each item
|
||||||
|
refs = [getattr(obj, method_name) for obj in self._data_dict.values()]
|
||||||
|
|
||||||
|
# if method_name is a method, then it will be callable
|
||||||
|
if callable(refs[0]):
|
||||||
|
res = []
|
||||||
|
for obj, det in zip(self._data_dict.values(), self._dets):
|
||||||
|
if 'n' in det:
|
||||||
|
our_args = nai_args
|
||||||
|
our_kwargs = nai_kwargs
|
||||||
|
elif 'b' in det:
|
||||||
|
our_args = bgo_args
|
||||||
|
our_kwargs = bgo_kwargs
|
||||||
|
res.append(getattr(obj, method_name)(*our_args, **our_kwargs))
|
||||||
|
|
||||||
|
# otherwise, method_name will not be callable if it is an attribute
|
||||||
|
else:
|
||||||
|
# we are setting an attribute
|
||||||
|
if len(nai_args) != 0 or len(bgo_args) != 0:
|
||||||
|
res = []
|
||||||
|
for obj, det in zip(self._data_dict.values(), self._dets):
|
||||||
|
if 'n' in obj.detector:
|
||||||
|
our_args = nai_args
|
||||||
|
elif 'b' in obj.detector:
|
||||||
|
our_args = bgo_args
|
||||||
|
res.append(setattr(obj, method_name, *args))
|
||||||
|
# we are retrieving an attribute
|
||||||
|
else:
|
||||||
|
res = refs
|
||||||
|
|
||||||
|
if res[0] is not None:
|
||||||
|
return res
|
147
data/data.py
Normal file
147
data/data.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
# data.py: Data file class definition
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
import os
|
||||||
|
from gbm.file import GbmFile
|
||||||
|
from gbm.time import Met
|
||||||
|
|
||||||
|
|
||||||
|
class DataFile:
|
||||||
|
"""Base class for an interface to data files
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This class should not be used directly, instead use one that inherits
|
||||||
|
from it and is specific to your data type e.g. :class:`~gbm.data.TTE`
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
datatype (str): The datatype of the file
|
||||||
|
detector (str): The GBM detector the file is associated with
|
||||||
|
directory (str): The directory the file is located in
|
||||||
|
filename (str): The filename
|
||||||
|
full_path (str): The full path+filename
|
||||||
|
id (str): The GBM file ID
|
||||||
|
is_gbm_file (bool): True if the file is a valid GBM standard file,
|
||||||
|
False if it is not.
|
||||||
|
is_trigger (bool): True if the file is a GBM trigger file, False if not
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self._full_path = None
|
||||||
|
self._dir = None
|
||||||
|
self._filename = None
|
||||||
|
self._is_gbm_file = None
|
||||||
|
self._filename_obj = None
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.filename
|
||||||
|
|
||||||
|
def _file_properties(self, filename, ignore_file_check=False):
|
||||||
|
|
||||||
|
if not ignore_file_check:
|
||||||
|
if not os.path.isfile(filename):
|
||||||
|
raise IOError("File {0} does not exist".format(filename))
|
||||||
|
|
||||||
|
self._full_path = filename
|
||||||
|
self._dir = os.path.dirname(filename)
|
||||||
|
self._filename = os.path.basename(filename)
|
||||||
|
self._is_gbm_file = False
|
||||||
|
try:
|
||||||
|
self._filename_obj = GbmFile.from_path(filename)
|
||||||
|
self._is_gbm_file = True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_gbm_file(self):
|
||||||
|
return self._is_gbm_file
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
if self.is_gbm_file:
|
||||||
|
return self._filename_obj.uid
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filename(self):
|
||||||
|
return self._filename
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_trigger(self):
|
||||||
|
if self.is_gbm_file:
|
||||||
|
if self._filename_obj.trigger == 'bn':
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def detector(self):
|
||||||
|
if self.is_gbm_file:
|
||||||
|
try:
|
||||||
|
return self._filename_obj.detector.short_name
|
||||||
|
except:
|
||||||
|
return 'all'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def datatype(self):
|
||||||
|
if self.is_gbm_file:
|
||||||
|
return self._filename_obj.data_type.upper()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def directory(self):
|
||||||
|
return self._dir
|
||||||
|
|
||||||
|
@property
|
||||||
|
def full_path(self):
|
||||||
|
return self._full_path
|
||||||
|
|
||||||
|
def set_properties(self, detector=None, trigtime=None, tstart=None,
|
||||||
|
datatype=None, extension=None, meta=None):
|
||||||
|
"""Set the properties of the data file. Useful for creating new
|
||||||
|
data files. A standardized filename will be built from the
|
||||||
|
parameters
|
||||||
|
|
||||||
|
Args:
|
||||||
|
detector (str, optional): The detector the data file belongs to
|
||||||
|
trigtime (float, optional): The trigger time, if applicable
|
||||||
|
tstart (float): The start time of the data file.
|
||||||
|
Must be set if trigtime is not.
|
||||||
|
datatype (str, optional): The type of data the file contains
|
||||||
|
extension (str, optional): The extension of the data file
|
||||||
|
meta (str, optional): A metadata atttribute to be added to the filename
|
||||||
|
"""
|
||||||
|
if (trigtime is None) and (tstart is None):
|
||||||
|
raise KeyError('Either trigtime or tstart need to be defined')
|
||||||
|
|
||||||
|
if trigtime is not None:
|
||||||
|
trigger = True
|
||||||
|
met = Met(trigtime)
|
||||||
|
id = met.bn
|
||||||
|
else:
|
||||||
|
trigger = False
|
||||||
|
met = Met(tstart)
|
||||||
|
id = met.ymd_h
|
||||||
|
|
||||||
|
filename = GbmFile.create(uid=id, data_type=datatype, detector=detector,
|
||||||
|
trigger=trigger, extension=extension, meta=meta)
|
||||||
|
|
||||||
|
self._file_properties(filename.basename(), ignore_file_check=True)
|
1086
data/drm.py
Normal file
1086
data/drm.py
Normal file
File diff suppressed because it is too large
Load Diff
790
data/headers.py
Normal file
790
data/headers.py
Normal file
@ -0,0 +1,790 @@
|
|||||||
|
# headers.py: GBM data header definitions
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import astropy.io.fits as fits
|
||||||
|
from gbm.time import Met
|
||||||
|
from gbm.detectors import Detector
|
||||||
|
import gbm
|
||||||
|
|
||||||
|
|
||||||
|
class GBMDefinitions:
|
||||||
|
def __init__(self):
|
||||||
|
self.telescope = ('telescop', 'GLAST', 'Name of mission/satellite')
|
||||||
|
self.instrument = (
|
||||||
|
'instrume', 'GBM', 'Specific instrument used for observation')
|
||||||
|
self.observer = ('observer', 'Meegan', 'GLAST Burst Monitor P.I.')
|
||||||
|
self.origin = ('origin', 'GIOC', 'Name of organization making file')
|
||||||
|
self.timesys = ('timesys', 'TT', 'Time system used in time keywords')
|
||||||
|
self.timeunit = (
|
||||||
|
'timeunit', 's', 'Time since MJDREF, used in TSTART and TSTOP')
|
||||||
|
self.mjdrefi = (
|
||||||
|
'mjdrefi', 51910, 'MJD of GLAST reference epoch, integer part')
|
||||||
|
self.mjdreff = ('mjdreff', '7.428703703703703e-4',
|
||||||
|
'MJD of GLAST reference epoch, fractional part')
|
||||||
|
self.radecsys = ('radecsys', 'FK5', 'Stellar reference frame')
|
||||||
|
self.equinox = ('equinox', 2000.0, 'Equinox for RA and Dec')
|
||||||
|
|
||||||
|
|
||||||
|
def _set_str(val):
|
||||||
|
return str(val) if val is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def _set_float(val):
|
||||||
|
return float(val) if val is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def _set_int(val):
|
||||||
|
return int(val) if val is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def _met_to_utc(met):
|
||||||
|
if met is not None:
|
||||||
|
_met = Met(met)
|
||||||
|
return _met.iso()
|
||||||
|
|
||||||
|
|
||||||
|
def _creator(version):
|
||||||
|
return ('creator',
|
||||||
|
'GBM Data Tools {} Software and version creating file'.format(
|
||||||
|
version))
|
||||||
|
|
||||||
|
|
||||||
|
def _filetype(ftype):
|
||||||
|
return ('filetype', ftype, 'Name for this type of FITS file')
|
||||||
|
|
||||||
|
|
||||||
|
def _detnam(detnam):
|
||||||
|
try:
|
||||||
|
det = Detector.from_str(detnam).long_name
|
||||||
|
except:
|
||||||
|
det = detnam
|
||||||
|
return ('detnam', det, 'Individual detector name')
|
||||||
|
|
||||||
|
|
||||||
|
def _date(date):
|
||||||
|
return ('date', date, 'file creation date (YYYY-MM-DDThh:mm:ss UT)')
|
||||||
|
|
||||||
|
|
||||||
|
def _dateobs(date_obs):
|
||||||
|
return ('date-obs', date_obs, 'Date of start of observation')
|
||||||
|
|
||||||
|
|
||||||
|
def _dateend(date_end):
|
||||||
|
return ('date-end', date_end, 'Date of end of observation')
|
||||||
|
|
||||||
|
|
||||||
|
def _tstart(tstart):
|
||||||
|
return ('tstart', tstart, '[GLAST MET] Observation start time')
|
||||||
|
|
||||||
|
|
||||||
|
def _tstop(tstop):
|
||||||
|
return ('tstop', tstop, '[GLAST MET] Observation stop time')
|
||||||
|
|
||||||
|
|
||||||
|
def _trigtime(trigtime):
|
||||||
|
return (
|
||||||
|
'trigtime', trigtime, 'Trigger time relative to MJDREF, double precision')
|
||||||
|
|
||||||
|
|
||||||
|
def _object(object):
|
||||||
|
return ('object', object, 'Burst name in standard format, yymmddfff')
|
||||||
|
|
||||||
|
|
||||||
|
def _raobj(ra_obj):
|
||||||
|
return ('ra_obj', ra_obj, 'Calculated RA of burst')
|
||||||
|
|
||||||
|
|
||||||
|
def _decobj(dec_obj):
|
||||||
|
return ('dec_obj', dec_obj, 'Calculated Dec of burst')
|
||||||
|
|
||||||
|
|
||||||
|
def _errrad(err_rad):
|
||||||
|
return ('err_rad', err_rad, 'Calculated Location Error Radius')
|
||||||
|
|
||||||
|
|
||||||
|
def _infile01(infile):
|
||||||
|
return ('infile01', infile, 'Level 1 input lookup table file')
|
||||||
|
|
||||||
|
|
||||||
|
def _extname(extname):
|
||||||
|
return ('extname', extname, 'name of this binary table extension')
|
||||||
|
|
||||||
|
|
||||||
|
def _hduclass():
|
||||||
|
return (
|
||||||
|
'hduclass', 'OGIP', 'Conforms to OGIP standard indicated in HDUCLAS1')
|
||||||
|
|
||||||
|
|
||||||
|
def _hduvers():
|
||||||
|
return ('hduvers', '1.2.1', 'Version of HDUCLAS1 format in use')
|
||||||
|
|
||||||
|
|
||||||
|
def _chantype():
|
||||||
|
return ('chantype', 'PHA', 'No corrections have been applied')
|
||||||
|
|
||||||
|
|
||||||
|
def _filter():
|
||||||
|
return ('filter', 'None', 'The instrument filter in use (if any)')
|
||||||
|
|
||||||
|
|
||||||
|
def _detchans(detchans):
|
||||||
|
return ('detchans', detchans, 'Total number of channels in each rate')
|
||||||
|
|
||||||
|
|
||||||
|
def _extver():
|
||||||
|
return ('extver', 1, 'Version of this extension format')
|
||||||
|
|
||||||
|
|
||||||
|
def _tzero_(i, trigtime):
|
||||||
|
return ('tzero{}'.format(i), trigtime, 'Offset, equal to TRIGTIME')
|
||||||
|
|
||||||
|
|
||||||
|
def primary(detnam=None, filetype=None, tstart=None, tstop=None, filename=None,
|
||||||
|
trigtime=None, object=None, ra_obj=None, dec_obj=None,
|
||||||
|
err_rad=None,
|
||||||
|
infile=None):
|
||||||
|
# enforce strings
|
||||||
|
detnam = _set_str(detnam)
|
||||||
|
filetype = _set_str(filetype)
|
||||||
|
filename = _set_str(filename)
|
||||||
|
object = _set_str(object)
|
||||||
|
infile = _set_str(infile)
|
||||||
|
|
||||||
|
# enforce floats
|
||||||
|
tstart = _set_float(tstart)
|
||||||
|
tstop = _set_float(tstop)
|
||||||
|
trigtime = _set_float(trigtime)
|
||||||
|
ra_obj = _set_float(ra_obj)
|
||||||
|
dec_obj = _set_float(dec_obj)
|
||||||
|
err_rad = _set_float(err_rad)
|
||||||
|
|
||||||
|
# do MET -> UTC time conversion
|
||||||
|
date_obs = _met_to_utc(tstart)
|
||||||
|
date_end = _met_to_utc(tstop)
|
||||||
|
|
||||||
|
header = fits.Header()
|
||||||
|
gbmdefs = GBMDefinitions()
|
||||||
|
current_time = Met.now().iso()
|
||||||
|
header.append(_creator(gbm.__version__))
|
||||||
|
header.append(_filetype(filetype))
|
||||||
|
header.append(
|
||||||
|
('file-ver', '1.0.0', 'Version of the format for this filetype'))
|
||||||
|
header.append(gbmdefs.telescope)
|
||||||
|
header.append(gbmdefs.instrument)
|
||||||
|
header.append(_detnam(detnam))
|
||||||
|
header.append(gbmdefs.observer)
|
||||||
|
header.append(gbmdefs.origin)
|
||||||
|
header.append(_date(current_time))
|
||||||
|
header.append(_dateobs(date_obs))
|
||||||
|
header.append(_dateend(date_end))
|
||||||
|
header.append(gbmdefs.timesys)
|
||||||
|
header.append(gbmdefs.timeunit)
|
||||||
|
header.append(gbmdefs.mjdrefi)
|
||||||
|
header.append(gbmdefs.mjdreff)
|
||||||
|
header.append(_tstart(tstart))
|
||||||
|
header.append(_tstop(tstop))
|
||||||
|
header.append(('filename', filename, 'Name of this file'))
|
||||||
|
header.append(_trigtime(trigtime))
|
||||||
|
header.append(_object(object))
|
||||||
|
header.append(gbmdefs.radecsys)
|
||||||
|
header.append(gbmdefs.equinox)
|
||||||
|
header.append(_raobj(ra_obj))
|
||||||
|
header.append(_decobj(dec_obj))
|
||||||
|
header.append(_errrad(err_rad))
|
||||||
|
header.append(_infile01(infile))
|
||||||
|
|
||||||
|
return header
|
||||||
|
|
||||||
|
|
||||||
|
def ebounds(detnam=None, tstart=None, tstop=None, trigtime=None, object=None,
|
||||||
|
ra_obj=None, dec_obj=None, err_rad=None, detchans=None,
|
||||||
|
ch2e_ver=None,
|
||||||
|
gain=None, infile=None):
|
||||||
|
# enforce strings
|
||||||
|
detnam = _set_str(detnam)
|
||||||
|
object = _set_str(object)
|
||||||
|
ch2e_ver = _set_str(ch2e_ver)
|
||||||
|
infile = _set_str(infile)
|
||||||
|
|
||||||
|
# enforce floats
|
||||||
|
tstart = _set_float(tstart)
|
||||||
|
tstop = _set_float(tstop)
|
||||||
|
trigtime = _set_float(trigtime)
|
||||||
|
ra_obj = _set_float(ra_obj)
|
||||||
|
dec_obj = _set_float(dec_obj)
|
||||||
|
err_rad = _set_float(err_rad)
|
||||||
|
gain = _set_float(gain)
|
||||||
|
|
||||||
|
# enforce ints
|
||||||
|
detchans = _set_int(detchans)
|
||||||
|
|
||||||
|
# do MET -> UTC time conversion
|
||||||
|
date_obs = _met_to_utc(tstart)
|
||||||
|
date_end = _met_to_utc(tstop)
|
||||||
|
|
||||||
|
header = fits.Header()
|
||||||
|
gbmdefs = GBMDefinitions()
|
||||||
|
current_time = Met.now().iso()
|
||||||
|
header.append(_extname('EBOUNDS'))
|
||||||
|
header.append(gbmdefs.telescope)
|
||||||
|
header.append(gbmdefs.instrument)
|
||||||
|
header.append(_detnam(detnam))
|
||||||
|
header.append(gbmdefs.observer)
|
||||||
|
header.append(gbmdefs.origin)
|
||||||
|
header.append(_date(current_time))
|
||||||
|
header.append(_dateobs(date_obs))
|
||||||
|
header.append(_dateend(date_end))
|
||||||
|
header.append(gbmdefs.timesys)
|
||||||
|
header.append(gbmdefs.timeunit)
|
||||||
|
header.append(gbmdefs.mjdrefi)
|
||||||
|
header.append(gbmdefs.mjdreff)
|
||||||
|
header.append(_tstart(tstart))
|
||||||
|
header.append(_tstop(tstop))
|
||||||
|
header.append(_trigtime(trigtime))
|
||||||
|
header.append(_object(object))
|
||||||
|
header.append(gbmdefs.radecsys)
|
||||||
|
header.append(gbmdefs.equinox)
|
||||||
|
header.append(_raobj(ra_obj))
|
||||||
|
header.append(_decobj(dec_obj))
|
||||||
|
header.append(_errrad(err_rad))
|
||||||
|
header.append(_hduclass())
|
||||||
|
header.append(
|
||||||
|
('hduclas1', 'RESPONSE', 'These are typically found in RMF files'))
|
||||||
|
header.append(('hduclas2', 'EBOUNDS', 'From CAL/GEN/92-002'))
|
||||||
|
header.append(_hduvers())
|
||||||
|
header.append(_chantype())
|
||||||
|
header.append(_filter())
|
||||||
|
header.append(_detchans(detchans))
|
||||||
|
header.append(_extver())
|
||||||
|
header.append(
|
||||||
|
('ch2e_ver', ch2e_ver, 'Channel to energy conversion scheme used'))
|
||||||
|
header.append(
|
||||||
|
('gain_cor', gain, 'Gain correction factor applied to energy edges'))
|
||||||
|
header.append(_infile01(infile))
|
||||||
|
|
||||||
|
return header
|
||||||
|
|
||||||
|
|
||||||
|
def spectrum(detnam=None, tstart=None, tstop=None, trigtime=None, object=None,
|
||||||
|
ra_obj=None, dec_obj=None, err_rad=None, detchans=None,
|
||||||
|
poisserr=True):
|
||||||
|
# enforce strings
|
||||||
|
detnam = _set_str(detnam)
|
||||||
|
object = _set_str(object)
|
||||||
|
|
||||||
|
# enforce floats
|
||||||
|
tstart = _set_float(tstart)
|
||||||
|
tstop = _set_float(tstop)
|
||||||
|
trigtime = _set_float(trigtime)
|
||||||
|
ra_obj = _set_float(ra_obj)
|
||||||
|
dec_obj = _set_float(dec_obj)
|
||||||
|
err_rad = _set_float(err_rad)
|
||||||
|
|
||||||
|
# enforce ints
|
||||||
|
detchans = _set_int(detchans)
|
||||||
|
|
||||||
|
# do MET -> UTC time conversion
|
||||||
|
date_obs = _met_to_utc(tstart)
|
||||||
|
date_end = _met_to_utc(tstop)
|
||||||
|
|
||||||
|
header = fits.Header()
|
||||||
|
gbmdefs = GBMDefinitions()
|
||||||
|
current_time = Met.now().iso()
|
||||||
|
header.append(_tzero_(4, trigtime))
|
||||||
|
header.append(_tzero_(5, trigtime))
|
||||||
|
header.append(_extname('SPECTRUM'))
|
||||||
|
header.append(gbmdefs.telescope)
|
||||||
|
header.append(gbmdefs.instrument)
|
||||||
|
header.append(_detnam(detnam))
|
||||||
|
header.append(gbmdefs.observer)
|
||||||
|
header.append(gbmdefs.origin)
|
||||||
|
header.append(_date(current_time))
|
||||||
|
header.append(_dateobs(date_obs))
|
||||||
|
header.append(_dateend(date_end))
|
||||||
|
header.append(gbmdefs.timesys)
|
||||||
|
header.append(gbmdefs.timeunit)
|
||||||
|
header.append(gbmdefs.mjdrefi)
|
||||||
|
header.append(gbmdefs.mjdreff)
|
||||||
|
header.append(_tstart(tstart))
|
||||||
|
header.append(_tstop(tstop))
|
||||||
|
header.append(_trigtime(trigtime))
|
||||||
|
header.append(_object(object))
|
||||||
|
header.append(gbmdefs.radecsys)
|
||||||
|
header.append(gbmdefs.equinox)
|
||||||
|
header.append(_raobj(ra_obj))
|
||||||
|
header.append(_decobj(dec_obj))
|
||||||
|
header.append(_errrad(err_rad))
|
||||||
|
header.append(_filter())
|
||||||
|
header.append(
|
||||||
|
('areascal', 1., 'No special scaling of effective area by channel'))
|
||||||
|
header.append(
|
||||||
|
('backfile', 'none', 'Name of corresponding background file (if any)'))
|
||||||
|
header.append(('backscal', 1., 'No scaling of background'))
|
||||||
|
header.append(
|
||||||
|
('corrfile', 'none', 'Name of corresponding correction file (if any)'))
|
||||||
|
header.append(('corrscal', 1., 'Correction scaling file'))
|
||||||
|
header.append(
|
||||||
|
('respfile', 'none', 'Name of corresponding RMF file (if any)'))
|
||||||
|
header.append(
|
||||||
|
('ancrfile', 'none', 'Name of corresponding ARF file (if any)'))
|
||||||
|
header.append(('sys_err', 0., 'No systematic errors'))
|
||||||
|
header.append(('poisserr', poisserr, 'Assume Poisson Errors'))
|
||||||
|
header.append(('grouping', 0, 'No special grouping has been applied'))
|
||||||
|
header.append(_hduclass())
|
||||||
|
header.append(
|
||||||
|
('hduclas1', 'SPECTRUM', 'PHA dataset (OGIP memo OGIP-92-007)'))
|
||||||
|
header.append(
|
||||||
|
('hduclas2', 'TOTAL', 'Indicates gross data (source + background)'))
|
||||||
|
header.append(('hduclas3', 'COUNT', 'Indicates data stored as counts'))
|
||||||
|
header.append(('hduclas4', 'TYPEII', 'Indicates PHA Type II file format'))
|
||||||
|
header.append(_hduvers())
|
||||||
|
header.append(_chantype())
|
||||||
|
header.append(_detchans(detchans))
|
||||||
|
header.append(_extver())
|
||||||
|
|
||||||
|
return header
|
||||||
|
|
||||||
|
|
||||||
|
def events(detnam=None, tstart=None, tstop=None, trigtime=None, object=None,
|
||||||
|
ra_obj=None, dec_obj=None, err_rad=None, detchans=None):
|
||||||
|
# enforce strings
|
||||||
|
detnam = _set_str(detnam)
|
||||||
|
object = _set_str(object)
|
||||||
|
|
||||||
|
# enforce floats
|
||||||
|
tstart = _set_float(tstart)
|
||||||
|
tstop = _set_float(tstop)
|
||||||
|
trigtime = _set_float(trigtime)
|
||||||
|
ra_obj = _set_float(ra_obj)
|
||||||
|
dec_obj = _set_float(dec_obj)
|
||||||
|
err_rad = _set_float(err_rad)
|
||||||
|
|
||||||
|
# enforce ints
|
||||||
|
detchans = _set_int(detchans)
|
||||||
|
|
||||||
|
# do MET -> UTC time conversion
|
||||||
|
date_obs = _met_to_utc(tstart)
|
||||||
|
date_end = _met_to_utc(tstop)
|
||||||
|
|
||||||
|
header = fits.Header()
|
||||||
|
gbmdefs = GBMDefinitions()
|
||||||
|
current_time = Met.now().iso()
|
||||||
|
header.append(_tzero_(1, trigtime))
|
||||||
|
header.append(_extname('EVENTS'))
|
||||||
|
header.append(gbmdefs.telescope)
|
||||||
|
header.append(gbmdefs.instrument)
|
||||||
|
header.append(_detnam(detnam))
|
||||||
|
header.append(gbmdefs.observer)
|
||||||
|
header.append(gbmdefs.origin)
|
||||||
|
header.append(_date(current_time))
|
||||||
|
header.append(_dateobs(date_obs))
|
||||||
|
header.append(_dateend(date_end))
|
||||||
|
header.append(gbmdefs.timesys)
|
||||||
|
header.append(gbmdefs.timeunit)
|
||||||
|
header.append(gbmdefs.mjdrefi)
|
||||||
|
header.append(gbmdefs.mjdreff)
|
||||||
|
header.append(_tstart(tstart))
|
||||||
|
header.append(_tstop(tstop))
|
||||||
|
header.append(_trigtime(trigtime))
|
||||||
|
header.append(_object(object))
|
||||||
|
header.append(gbmdefs.radecsys)
|
||||||
|
header.append(gbmdefs.equinox)
|
||||||
|
header.append(_raobj(ra_obj))
|
||||||
|
header.append(_decobj(dec_obj))
|
||||||
|
header.append(_errrad(err_rad))
|
||||||
|
header.append(
|
||||||
|
('respfile', 'none', 'Name of corresponding RMF file (if any)'))
|
||||||
|
header.append(('evt_dead', 2.6e-6, 'Deadtime per event (s)'))
|
||||||
|
header.append(_detchans(detchans))
|
||||||
|
header.append(_hduclass())
|
||||||
|
header.append(('hduclas1', 'EVENTS', 'Extension contains Events'))
|
||||||
|
header.append(_extver())
|
||||||
|
|
||||||
|
return header
|
||||||
|
|
||||||
|
|
||||||
|
def gti(detnam=None, tstart=None, tstop=None, trigtime=None, object=None,
|
||||||
|
ra_obj=None, dec_obj=None, err_rad=None):
|
||||||
|
# enforce strings
|
||||||
|
detnam = _set_str(detnam)
|
||||||
|
object = _set_str(object)
|
||||||
|
|
||||||
|
# enforce floats
|
||||||
|
tstart = _set_float(tstart)
|
||||||
|
tstop = _set_float(tstop)
|
||||||
|
trigtime = _set_float(trigtime)
|
||||||
|
ra_obj = _set_float(ra_obj)
|
||||||
|
dec_obj = _set_float(dec_obj)
|
||||||
|
err_rad = _set_float(err_rad)
|
||||||
|
|
||||||
|
# do MET -> UTC time conversion
|
||||||
|
date_obs = _met_to_utc(tstart)
|
||||||
|
date_end = _met_to_utc(tstop)
|
||||||
|
|
||||||
|
header = fits.Header()
|
||||||
|
gbmdefs = GBMDefinitions()
|
||||||
|
current_time = Met.now().iso()
|
||||||
|
header.append(_tzero_(1, trigtime))
|
||||||
|
header.append(_tzero_(2, trigtime))
|
||||||
|
header.append(_extname('GTI'))
|
||||||
|
header.append(gbmdefs.telescope)
|
||||||
|
header.append(gbmdefs.instrument)
|
||||||
|
header.append(_detnam(detnam))
|
||||||
|
header.append(gbmdefs.observer)
|
||||||
|
header.append(gbmdefs.origin)
|
||||||
|
header.append(_date(current_time))
|
||||||
|
header.append(_dateobs(date_obs))
|
||||||
|
header.append(_dateend(date_end))
|
||||||
|
header.append(gbmdefs.timesys)
|
||||||
|
header.append(gbmdefs.timeunit)
|
||||||
|
header.append(gbmdefs.mjdrefi)
|
||||||
|
header.append(gbmdefs.mjdreff)
|
||||||
|
header.append(_tstart(tstart))
|
||||||
|
header.append(_tstop(tstop))
|
||||||
|
header.append(_hduclass())
|
||||||
|
header.append(('hduclas1', 'GTI', 'Indicates good time intervals'))
|
||||||
|
header.append(_hduvers())
|
||||||
|
header.append(_extver())
|
||||||
|
header.append(_trigtime(trigtime))
|
||||||
|
header.append(_object(object))
|
||||||
|
header.append(gbmdefs.radecsys)
|
||||||
|
header.append(gbmdefs.equinox)
|
||||||
|
header.append(_raobj(ra_obj))
|
||||||
|
header.append(_decobj(dec_obj))
|
||||||
|
header.append(_errrad(err_rad))
|
||||||
|
|
||||||
|
return header
|
||||||
|
|
||||||
|
|
||||||
|
def specresp(detnam=None, tstart=None, tstop=None, trigtime=None, object=None,
|
||||||
|
ra_obj=None, dec_obj=None, mat_type=None, rsp_num=None,
|
||||||
|
src_az=None,
|
||||||
|
src_el=None, geo_az=None, geo_el=None, det_ang=None, geo_ang=None,
|
||||||
|
numebins=None, detchans=None, infiles=None, atscat=None):
|
||||||
|
# enforce strings
|
||||||
|
detnam = _set_str(detnam)
|
||||||
|
object = _set_str(object)
|
||||||
|
mat_type = _set_str(mat_type)
|
||||||
|
if infiles is None:
|
||||||
|
infiles = [None] * 3
|
||||||
|
else:
|
||||||
|
infiles = [str(infile) for infile in infiles]
|
||||||
|
atscat = _set_str(atscat)
|
||||||
|
|
||||||
|
# enforce floats
|
||||||
|
tstart = _set_float(tstart)
|
||||||
|
tstop = _set_float(tstop)
|
||||||
|
trigtime = _set_float(trigtime)
|
||||||
|
ra_obj = _set_float(ra_obj)
|
||||||
|
dec_obj = _set_float(dec_obj)
|
||||||
|
src_az = _set_float(src_az)
|
||||||
|
src_el = _set_float(src_el)
|
||||||
|
geo_az = _set_float(geo_az)
|
||||||
|
geo_el = _set_float(geo_el)
|
||||||
|
det_ang = _set_float(det_ang)
|
||||||
|
geo_ang = _set_float(geo_ang)
|
||||||
|
|
||||||
|
# enforce ints
|
||||||
|
detchans = _set_int(detchans)
|
||||||
|
rsp_num = _set_int(rsp_num)
|
||||||
|
numebins = _set_int(numebins)
|
||||||
|
|
||||||
|
# do MET -> UTC time conversion
|
||||||
|
date_obs = _met_to_utc(tstart)
|
||||||
|
date_end = _met_to_utc(tstop)
|
||||||
|
|
||||||
|
header = fits.Header()
|
||||||
|
gbmdefs = GBMDefinitions()
|
||||||
|
current_time = Met.now().iso()
|
||||||
|
header.append(_extname('SPECRESP MATRIX'))
|
||||||
|
header.append(_extver())
|
||||||
|
header.append(_date(current_time))
|
||||||
|
header.append(_dateobs(date_obs))
|
||||||
|
header.append(_dateend(date_end))
|
||||||
|
header.append(gbmdefs.mjdrefi)
|
||||||
|
header.append(gbmdefs.mjdreff)
|
||||||
|
header.append(_tstart(tstart))
|
||||||
|
header.append(_tstop(tstop))
|
||||||
|
header.append(_trigtime(trigtime))
|
||||||
|
header.append(gbmdefs.timesys)
|
||||||
|
header.append(gbmdefs.timeunit)
|
||||||
|
header.append(gbmdefs.telescope)
|
||||||
|
header.append(gbmdefs.instrument)
|
||||||
|
header.append(_detnam(detnam))
|
||||||
|
header.append(gbmdefs.observer)
|
||||||
|
header.append(gbmdefs.origin)
|
||||||
|
header.append(('mat_type', mat_type, 'Response Matrix Type'))
|
||||||
|
header.append(('rsp_num', rsp_num, 'Response matrix index number'))
|
||||||
|
header.append(_object(object))
|
||||||
|
header.append(gbmdefs.radecsys)
|
||||||
|
header.append(gbmdefs.equinox)
|
||||||
|
header.append(_raobj(ra_obj))
|
||||||
|
header.append(_decobj(dec_obj))
|
||||||
|
header.append(
|
||||||
|
('src_az', src_az, 'Azimuth of source in spacecraft coordinates'))
|
||||||
|
header.append(
|
||||||
|
('src_el', src_el, 'Elevation of source in spacecraft coordinates'))
|
||||||
|
header.append(
|
||||||
|
('geo_az', geo_az, 'Azimuth of geocenter in spacecraft coordinates'))
|
||||||
|
header.append(
|
||||||
|
('geo_el', geo_el, 'Elevation of geocenter in spacecraft coordinates'))
|
||||||
|
header.append(
|
||||||
|
('det_ang', det_ang, 'Angle between source and detector normal'))
|
||||||
|
header.append(
|
||||||
|
('geo_ang', geo_ang, 'Angle between geocenter and detector normal'))
|
||||||
|
header.append(_filter())
|
||||||
|
header.append(_chantype())
|
||||||
|
header.append(
|
||||||
|
('numebins', numebins, 'Number of true energy bins of the MATRIX'))
|
||||||
|
header.append(_detchans(detchans))
|
||||||
|
header.append(('infile01', infiles[0], 'Detector response database in'))
|
||||||
|
header.append(('infile02', infiles[1], 'Detector response database in'))
|
||||||
|
header.append(('infile03', infiles[2], 'Detector response database in'))
|
||||||
|
header.append(('infile04', atscat, 'Atmospheric scattering datab'))
|
||||||
|
header.append(_hduclass())
|
||||||
|
header.append(_hduvers())
|
||||||
|
header.append(('hduclas1', 'RESPONSE', 'Typically found in RMF files'))
|
||||||
|
header.append(('hduclas2', 'RSP_MATRIX', 'From CAL/GEN/92-002'))
|
||||||
|
|
||||||
|
return header
|
||||||
|
|
||||||
|
|
||||||
|
def pha_spectrum(detnam=None, tstart=None, tstop=None, trigtime=None,
|
||||||
|
object=None,
|
||||||
|
ra_obj=None, dec_obj=None, err_rad=None, detchans=None,
|
||||||
|
poisserr=True,
|
||||||
|
datatype=None, backfile=None, rspfile=None, exposure=None):
|
||||||
|
# enforce strings
|
||||||
|
detnam = _set_str(detnam)
|
||||||
|
object = _set_str(object)
|
||||||
|
datatype = _set_str(datatype)
|
||||||
|
backfile = _set_str(backfile)
|
||||||
|
rspfile = _set_str(rspfile)
|
||||||
|
|
||||||
|
# enforce floats
|
||||||
|
tstart = _set_float(tstart)
|
||||||
|
tstop = _set_float(tstop)
|
||||||
|
trigtime = _set_float(trigtime)
|
||||||
|
ra_obj = _set_float(ra_obj)
|
||||||
|
dec_obj = _set_float(dec_obj)
|
||||||
|
err_rad = _set_float(err_rad)
|
||||||
|
exposure = _set_float(exposure)
|
||||||
|
|
||||||
|
# enforce ints
|
||||||
|
detchans = _set_int(detchans)
|
||||||
|
|
||||||
|
# do MET -> UTC time conversion
|
||||||
|
date_obs = _met_to_utc(tstart)
|
||||||
|
date_end = _met_to_utc(tstop)
|
||||||
|
|
||||||
|
header = fits.Header()
|
||||||
|
gbmdefs = GBMDefinitions()
|
||||||
|
current_time = Met.now().iso()
|
||||||
|
# header.append(_tzero_(4, trigtime))
|
||||||
|
# header.append(_tzero_(5, trigtime))
|
||||||
|
header.append(_extname('SPECTRUM'))
|
||||||
|
header.append(gbmdefs.telescope)
|
||||||
|
header.append(gbmdefs.instrument)
|
||||||
|
header.append(_detnam(detnam))
|
||||||
|
header.append(gbmdefs.observer)
|
||||||
|
header.append(gbmdefs.origin)
|
||||||
|
header.append(_date(current_time))
|
||||||
|
header.append(_dateobs(date_obs))
|
||||||
|
header.append(_dateend(date_end))
|
||||||
|
header.append(gbmdefs.timesys)
|
||||||
|
header.append(gbmdefs.timeunit)
|
||||||
|
header.append(gbmdefs.mjdrefi)
|
||||||
|
header.append(gbmdefs.mjdreff)
|
||||||
|
header.append(_tstart(tstart))
|
||||||
|
header.append(_tstop(tstop))
|
||||||
|
header.append(_trigtime(trigtime))
|
||||||
|
header.append(('datatype', datatype, 'GBM datatype used for this file'))
|
||||||
|
header.append(_object(object))
|
||||||
|
header.append(gbmdefs.radecsys)
|
||||||
|
header.append(gbmdefs.equinox)
|
||||||
|
header.append(_raobj(ra_obj))
|
||||||
|
header.append(_decobj(dec_obj))
|
||||||
|
header.append(_errrad(err_rad))
|
||||||
|
header.append(_filter())
|
||||||
|
header.append(
|
||||||
|
('areascal', 1., 'No special scaling of effective area by channel'))
|
||||||
|
header.append(('backfile', backfile,
|
||||||
|
'Name of corresponding background file (if any)'))
|
||||||
|
header.append(('backscal', 1., 'background file scaling factor'))
|
||||||
|
header.append(
|
||||||
|
('corrfile', 'none', 'Name of corresponding correction file (if any)'))
|
||||||
|
header.append(('corrscal', 1., 'Correction scaling file'))
|
||||||
|
header.append(
|
||||||
|
('respfile', rspfile, 'Name of corresponding RMF file (if any)'))
|
||||||
|
header.append(
|
||||||
|
('ancrfile', 'none', 'Name of corresponding ARF file (if any)'))
|
||||||
|
header.append(('sys_err', 0., 'No systematic errors'))
|
||||||
|
header.append(('poisserr', poisserr, 'Assume Poisson Errors'))
|
||||||
|
header.append(('grouping', 0, 'No special grouping has been applied'))
|
||||||
|
header.append(_hduclass())
|
||||||
|
header.append(
|
||||||
|
('hduclas1', 'SPECTRUM', 'PHA dataset (OGIP memo OGIP-92-007)'))
|
||||||
|
header.append(
|
||||||
|
('hduclas2', 'TOTAL', 'Indicates gross data (source + background)'))
|
||||||
|
header.append(('hduclas3', 'COUNT', 'Indicates data stored as counts'))
|
||||||
|
header.append(('hduclas4', 'TYPEI', 'Indicates PHA Type I file format'))
|
||||||
|
header.append(_hduvers())
|
||||||
|
header.append(_chantype())
|
||||||
|
header.append(_detchans(detchans))
|
||||||
|
header.append(('exposure', exposure, 'Accumulation time - deadtime'))
|
||||||
|
header.append(_extver())
|
||||||
|
|
||||||
|
return header
|
||||||
|
|
||||||
|
|
||||||
|
def healpix_primary(tcat=None, trigtime=0.0):
|
||||||
|
"""Write the primary header of a HEALPix FITS file
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
tcat: Tcat, optional
|
||||||
|
The tcat object. If set, then it will copy the relevant info from
|
||||||
|
the tcat into the primary header of the FITS file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
--------
|
||||||
|
header: astropy.io.fits.header
|
||||||
|
The header
|
||||||
|
"""
|
||||||
|
|
||||||
|
header = fits.Header()
|
||||||
|
gbmdefs = GBMDefinitions()
|
||||||
|
current_time = Met.now().iso()
|
||||||
|
header.append(_creator(gbm.__version__))
|
||||||
|
header.append(('filetype', 'IMAGE', 'Name for this type of FITS file'))
|
||||||
|
header.append(gbmdefs.telescope)
|
||||||
|
header.append(gbmdefs.instrument)
|
||||||
|
header.append(gbmdefs.observer)
|
||||||
|
header.append(gbmdefs.origin)
|
||||||
|
header.append(_date(current_time))
|
||||||
|
header.append(_dateobs(''))
|
||||||
|
header.append(_dateend(''))
|
||||||
|
header.append(gbmdefs.timesys)
|
||||||
|
header.append(gbmdefs.timeunit)
|
||||||
|
header.append(gbmdefs.mjdrefi)
|
||||||
|
header.append(gbmdefs.mjdreff)
|
||||||
|
header.append(_tstart(0.0))
|
||||||
|
header.append(_tstop(0.0))
|
||||||
|
header.append(('filename', '', 'Name of this file'))
|
||||||
|
header.append(_trigtime(trigtime))
|
||||||
|
header.append(_object(''))
|
||||||
|
header.append(gbmdefs.radecsys)
|
||||||
|
header.append(gbmdefs.equinox)
|
||||||
|
header.append(_raobj(0.0))
|
||||||
|
header.append(_decobj(0.0))
|
||||||
|
header.append(_errrad(0.0))
|
||||||
|
header.append(('theta', 0.0, '[deg] Angle from spacecraft zenith'))
|
||||||
|
header.append(
|
||||||
|
('phi', 0.0, '[deg] Angle from spacecraft +X axis toward +Y'))
|
||||||
|
header.append(('loc_src', 'Fermi, GBM',
|
||||||
|
'Mission/Instrument providing the localization'))
|
||||||
|
header.append(('class', 'GRB', 'Classification of trigger'))
|
||||||
|
header.append(('obj_clas', 'GRB', 'Classification of trigger'))
|
||||||
|
header.append(
|
||||||
|
('geo_long', 0.0, '[deg] Spacecraft geographical east longitude'))
|
||||||
|
header.append(
|
||||||
|
('geo_lat', 0.0, '[deg] Spacecraft geographical north latitude'))
|
||||||
|
header.append(('ra_scx', 0.0, '[deg] Pointing of spacecraft x-axis: RA'))
|
||||||
|
header.append(('dec_scx', 0.0, '[deg] Pointing of spacecraft x-axis: Dec'))
|
||||||
|
header.append(('ra_scz', 0.0, '[deg] Pointing of spacecraft z-axis: RA'))
|
||||||
|
header.append(('dec_scz', 0.0, '[deg] Pointing of spacecraft z-axis: Dec'))
|
||||||
|
header.append(('loc_ver', '', 'Version string of localizing software'))
|
||||||
|
header.append(
|
||||||
|
('loc_enrg', '(50, 300)', 'Energy range used for localization'))
|
||||||
|
header.append(('lmethod', 'Interactive', 'Method of localization'))
|
||||||
|
|
||||||
|
if tcat is not None:
|
||||||
|
|
||||||
|
def insert_key(key):
|
||||||
|
if key in tcat.headers['PRIMARY']:
|
||||||
|
header[key] = tcat.headers['PRIMARY'][key]
|
||||||
|
|
||||||
|
# copy tcat values to primary header
|
||||||
|
insert_key('DATE-OBS')
|
||||||
|
insert_key('DATE-END')
|
||||||
|
insert_key('TSTART')
|
||||||
|
insert_key('TSTOP')
|
||||||
|
insert_key('TRIGTIME')
|
||||||
|
insert_key('OBJECT')
|
||||||
|
insert_key('RA_OBJ')
|
||||||
|
insert_key('DEC_OBJ')
|
||||||
|
insert_key('ERR_RAD')
|
||||||
|
insert_key('THETA')
|
||||||
|
insert_key('PHI')
|
||||||
|
insert_key('GEO_LONG')
|
||||||
|
insert_key('GEO_LAT')
|
||||||
|
insert_key('RA_SCX')
|
||||||
|
insert_key('DEC_SCX')
|
||||||
|
insert_key('RA_SCZ')
|
||||||
|
insert_key('DEC_SCZ')
|
||||||
|
insert_key('LOC_VER')
|
||||||
|
|
||||||
|
return header
|
||||||
|
|
||||||
|
|
||||||
|
def healpix_image(nside=128, object=None, extra_keys=None):
|
||||||
|
"""Write the image extension header of a HEALPix FITS file
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
nside: int, optional
|
||||||
|
The nside of the HEALPix map
|
||||||
|
extra_keys: list, optional
|
||||||
|
An additional keys to be added to the header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
--------
|
||||||
|
header: astropy.io.fits.header
|
||||||
|
The header
|
||||||
|
"""
|
||||||
|
from healpy import nside2npix
|
||||||
|
header = fits.Header()
|
||||||
|
header.append(
|
||||||
|
('TTYPE1', 'PROBABILITY', 'Differential probability per pixel'))
|
||||||
|
header.append(('TTYPE2', 'SIGNIFICANCE', 'Integrated probability'))
|
||||||
|
header.append(('PIXTYPE', 'HEALPIX', 'HEALPIX pixelisation'))
|
||||||
|
header.append(
|
||||||
|
('ORDERING', 'NESTED', 'Pixel ordering scheme, either RING or NESTED'))
|
||||||
|
header.append(
|
||||||
|
('COORDSYS', 'C', 'Ecliptic, Galactic or Celestial (equatorial)'))
|
||||||
|
header.append(
|
||||||
|
('EXTNAME', 'HEALPIX', 'name of this binary table extension'))
|
||||||
|
header.append(('NSIDE', nside, 'Resolution parameter of HEALPIX'))
|
||||||
|
header.append(('FIRSTPIX', 0, 'First pixel # (0 based)'))
|
||||||
|
header.append(('LASTPIX', nside2npix(nside), 'Last pixel # (0 based)'))
|
||||||
|
header.append(('INDXSCHM', 'IMPLICIT', 'Indexing: IMPLICIT or EXPLICIT'))
|
||||||
|
header.append(
|
||||||
|
('OBJECT', object, 'Sky coverage, either FULLSKY or PARTIAL'))
|
||||||
|
|
||||||
|
if extra_keys is not None:
|
||||||
|
for key in extra_keys:
|
||||||
|
header.append(key)
|
||||||
|
|
||||||
|
return header
|
1445
data/localization.py
Normal file
1445
data/localization.py
Normal file
File diff suppressed because it is too large
Load Diff
658
data/pha.py
Normal file
658
data/pha.py
Normal file
@ -0,0 +1,658 @@
|
|||||||
|
# pha.py: PHA and BAK classes
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import os
|
||||||
|
import astropy.io.fits as fits
|
||||||
|
import numpy as np
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from .data import DataFile
|
||||||
|
from . import headers as hdr
|
||||||
|
from .primitives import EnergyBins
|
||||||
|
|
||||||
|
|
||||||
|
class PHA(DataFile):
|
||||||
|
"""PHA class for count spectra.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
data (:class:`~.primitives.EnergyBins`): The PHA data
|
||||||
|
datatype (str): The datatype of the file
|
||||||
|
detector (str): The GBM detector the file is associated with
|
||||||
|
directory (str): The directory the file is located in
|
||||||
|
energy_range (float, float): The energy range of the spectrum
|
||||||
|
exposure (float): The exposure of the PHA data
|
||||||
|
filename (str): The filename
|
||||||
|
full_path (str): The full path+filename
|
||||||
|
gti ([(float, float), ...]): The good time intervals
|
||||||
|
headers (dict): The headers for each extension of the PHA
|
||||||
|
id (str): The GBM file ID
|
||||||
|
is_gbm_file (bool): True if the file is a valid GBM standard file,
|
||||||
|
False if it is not.
|
||||||
|
is_trigger (bool): True if the file is a GBM trigger file, False if not
|
||||||
|
numchans (int): The number of energy channels
|
||||||
|
tcent (float): The center time of the data
|
||||||
|
time_range (float, float): The time range of the spectrum
|
||||||
|
trigtime (float): The trigger time of the data, if available
|
||||||
|
valid_channels (np.array(dtype=bool)): The channels that are marked
|
||||||
|
valid and available for fitting
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
super(PHA, self).__init__()
|
||||||
|
self._headers = OrderedDict()
|
||||||
|
self._data = None
|
||||||
|
self._gti = None
|
||||||
|
self._trigtime = 0.0
|
||||||
|
|
||||||
|
def _assert_range(self, valrange):
|
||||||
|
assert valrange[0] <= valrange[1], \
|
||||||
|
'Range must be in increasing order: (lo, hi)'
|
||||||
|
return valrange
|
||||||
|
|
||||||
|
def _assert_range_list(self, range_list):
|
||||||
|
try:
|
||||||
|
iter(range_list[0])
|
||||||
|
except:
|
||||||
|
range_list = [range_list]
|
||||||
|
return range_list
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gti(self):
|
||||||
|
return self._gti.tolist()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def headers(self):
|
||||||
|
return self._headers
|
||||||
|
|
||||||
|
@property
|
||||||
|
def trigtime(self):
|
||||||
|
return self._trigtime
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_range(self):
|
||||||
|
return (self._gti['START'][0], self._gti['STOP'][-1])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tcent(self):
|
||||||
|
return sum(self.time_range) / 2.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def energy_range(self):
|
||||||
|
return self._data.range
|
||||||
|
|
||||||
|
@property
|
||||||
|
def numchans(self):
|
||||||
|
return self._data.size
|
||||||
|
|
||||||
|
@property
|
||||||
|
def valid_channels(self):
|
||||||
|
return np.arange(self.numchans, dtype=int)[self._channel_mask]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exposure(self):
|
||||||
|
return self._data.exposure[0]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def open(cls, filename):
|
||||||
|
"""Open a PHA FITS file and return the PHA object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename (str): The filename of the FITS file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`PHA`: The PHA object
|
||||||
|
"""
|
||||||
|
obj = cls()
|
||||||
|
obj._file_properties(filename)
|
||||||
|
# open FITS file
|
||||||
|
with fits.open(filename) as hdulist:
|
||||||
|
# store headers
|
||||||
|
for hdu in hdulist:
|
||||||
|
obj._headers.update({hdu.name: hdu.header})
|
||||||
|
|
||||||
|
# trigger time
|
||||||
|
if 'TRIGTIME' in list(obj._headers['PRIMARY'].keys()):
|
||||||
|
if obj._headers['PRIMARY']['TRIGTIME'] != '':
|
||||||
|
obj._trigtime = float(obj._headers['PRIMARY']['TRIGTIME'])
|
||||||
|
else:
|
||||||
|
obj._headers['PRIMARY']['TRIGTIME'] = 0.0
|
||||||
|
else:
|
||||||
|
obj._headers['PRIMARY']['TRIGTIME'] = 0.0
|
||||||
|
|
||||||
|
# if trigger, shift to trigger time reference
|
||||||
|
spec = hdulist['SPECTRUM'].data
|
||||||
|
ebounds = hdulist['EBOUNDS'].data
|
||||||
|
gti = np.asarray(hdulist['GTI'].data)
|
||||||
|
if obj._trigtime is not None:
|
||||||
|
gti['START'] -= obj._trigtime
|
||||||
|
gti['STOP'] -= obj._trigtime
|
||||||
|
|
||||||
|
# create the energy histogram, the core of the PHA
|
||||||
|
exposure = np.full(ebounds.shape[0],
|
||||||
|
obj._headers['SPECTRUM']['EXPOSURE'])
|
||||||
|
obj._data = EnergyBins(spec['COUNTS'], ebounds['E_MIN'],
|
||||||
|
ebounds['E_MAX'], exposure)
|
||||||
|
obj._gti = gti
|
||||||
|
obj._set_channel_mask(spec['QUALITY'])
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_data(cls, data, tstart, tstop, gti=None, trigtime=0.0,
|
||||||
|
detector=None, object=None,
|
||||||
|
ra_obj=None, dec_obj=None, err_rad=None, datatype=None,
|
||||||
|
backfile=None, rspfile=None, meta=None, channel_mask=None):
|
||||||
|
"""Create a PHA object from an EnergyBins data object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (:class:`~.primitives.EnergyBins`): The PHA count spectrum data
|
||||||
|
tstart (float): The start time of the data
|
||||||
|
tstop (float): The end time of the data
|
||||||
|
gti ([(float, float), ...], optional):
|
||||||
|
The list of tuples representing the good time intervals
|
||||||
|
(start, stop). If omitted, the GTI is assumed to be
|
||||||
|
[(tstart, tstop)].
|
||||||
|
trigtime (float, optional):
|
||||||
|
The trigger time, if applicable. If provided, the data times
|
||||||
|
will be shifted relative to the trigger time. Default is zero.
|
||||||
|
detector (str, optional): The detector that produced the data
|
||||||
|
object (str, optional): The object being observed
|
||||||
|
ra_obj (float, optional): The RA of the object
|
||||||
|
dec_obj (float, optional): The Dec of the object
|
||||||
|
err_rad (float, optional): The localization error radius of the object
|
||||||
|
datatype (str, optional): The datatype from which the PHA is created
|
||||||
|
backfile (str, optional): The associated background file
|
||||||
|
rspfile (str, optional): The associated response file
|
||||||
|
meta (str, optional): Additional metadata string to be added to
|
||||||
|
standard filename
|
||||||
|
channel_mask (np.array(dtype=bool)):
|
||||||
|
A boolean array representing the valid channels to be used for
|
||||||
|
fitting. If omitted, assumes all non-zero count channels are valid.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`PHA`: The PHA object
|
||||||
|
"""
|
||||||
|
obj = cls()
|
||||||
|
filetype = 'SPECTRUM'
|
||||||
|
obj._data = data
|
||||||
|
detchans = data.size
|
||||||
|
|
||||||
|
try:
|
||||||
|
trigtime = float(trigtime)
|
||||||
|
except:
|
||||||
|
raise TypeError('trigtime must be a float')
|
||||||
|
if trigtime < 0.0:
|
||||||
|
raise ValueError('trigtime must be non-negative')
|
||||||
|
|
||||||
|
obj._trigtime = trigtime
|
||||||
|
tstart += trigtime
|
||||||
|
tstop += trigtime
|
||||||
|
|
||||||
|
# create the primary extension
|
||||||
|
primary_header = hdr.primary(detnam=detector, filetype=filetype,
|
||||||
|
tstart=tstart,
|
||||||
|
tstop=tstop, trigtime=trigtime,
|
||||||
|
object=object,
|
||||||
|
ra_obj=ra_obj, dec_obj=dec_obj,
|
||||||
|
err_rad=err_rad)
|
||||||
|
headers = [primary_header]
|
||||||
|
header_names = ['PRIMARY']
|
||||||
|
|
||||||
|
# ebounds extension
|
||||||
|
ebounds_header = hdr.ebounds(detnam=detector, tstart=tstart,
|
||||||
|
tstop=tstop,
|
||||||
|
trigtime=trigtime, object=object,
|
||||||
|
ra_obj=ra_obj, dec_obj=dec_obj,
|
||||||
|
err_rad=err_rad, detchans=detchans)
|
||||||
|
headers.append(ebounds_header)
|
||||||
|
header_names.append('EBOUNDS')
|
||||||
|
|
||||||
|
# spectrum extension
|
||||||
|
spectrum_header = hdr.pha_spectrum(exposure=data.exposure[0],
|
||||||
|
detnam=detector, tstart=tstart,
|
||||||
|
tstop=tstop,
|
||||||
|
trigtime=trigtime, object=object,
|
||||||
|
ra_obj=ra_obj,
|
||||||
|
dec_obj=dec_obj, err_rad=err_rad,
|
||||||
|
detchans=detchans,
|
||||||
|
backfile=backfile, rspfile=rspfile)
|
||||||
|
headers.append(spectrum_header)
|
||||||
|
header_names.append('SPECTRUM')
|
||||||
|
|
||||||
|
# gti extension
|
||||||
|
if gti is None:
|
||||||
|
gti = [(tstart, tstop)]
|
||||||
|
gti = np.array(gti, dtype=[('START', '>f8'), ('STOP', '>f8')])
|
||||||
|
gti['START'] += trigtime
|
||||||
|
gti['STOP'] += trigtime
|
||||||
|
gti_header = hdr.gti(detnam=detector, tstart=tstart, tstop=tstop,
|
||||||
|
trigtime=trigtime, object=object, ra_obj=ra_obj,
|
||||||
|
dec_obj=dec_obj, err_rad=err_rad)
|
||||||
|
headers.append(gti_header)
|
||||||
|
header_names.append('GTI')
|
||||||
|
obj._gti = gti
|
||||||
|
|
||||||
|
# set the channel mask
|
||||||
|
# if no channel mask is given, assume zero-count channels are bad
|
||||||
|
if channel_mask is None:
|
||||||
|
channel_mask = np.zeros(data.size, dtype=bool)
|
||||||
|
channel_mask[data.counts > 0] = True
|
||||||
|
obj._channel_mask = channel_mask
|
||||||
|
|
||||||
|
# store headers and set data properties
|
||||||
|
obj._headers = {name: header for name, header in
|
||||||
|
zip(header_names, headers)}
|
||||||
|
|
||||||
|
# set file info
|
||||||
|
obj.set_properties(detector=detector, trigtime=trigtime,
|
||||||
|
tstart=obj.time_range[0], datatype=datatype,
|
||||||
|
extension='pha', meta=meta)
|
||||||
|
|
||||||
|
gti['START'] -= obj.trigtime
|
||||||
|
gti['STOP'] -= obj.trigtime
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def write(self, directory, filename=None, backfile=None):
|
||||||
|
"""Writes the data to a FITS file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
directory (str): The directory to write the file
|
||||||
|
filename (str, optional): If set, will override the standardized name
|
||||||
|
backfile (str, optional): If set, will add the associated BAK file
|
||||||
|
name to the PHA SPECTRUM header
|
||||||
|
"""
|
||||||
|
# set filename
|
||||||
|
if (self.filename is None) and (filename is None):
|
||||||
|
raise NameError('Filename not set')
|
||||||
|
if filename is None:
|
||||||
|
filename = self.filename
|
||||||
|
self._full_path = os.path.join(directory, filename)
|
||||||
|
self._headers['PRIMARY']['FILENAME'] = filename
|
||||||
|
|
||||||
|
if backfile is not None:
|
||||||
|
self._headers['SPECTRUM']['BACKFILE'] = backfile
|
||||||
|
|
||||||
|
# make ebounds table
|
||||||
|
chan_idx = np.arange(self.numchans, dtype=int)
|
||||||
|
ebounds = np.recarray(self.numchans, dtype=[('CHANNEL', '>i2'),
|
||||||
|
('E_MIN', '>f4'),
|
||||||
|
('E_MAX', '>f4')])
|
||||||
|
ebounds['CHANNEL'] = chan_idx
|
||||||
|
ebounds['E_MIN'] = self._data.lo_edges
|
||||||
|
ebounds['E_MAX'] = self._data.hi_edges
|
||||||
|
|
||||||
|
# make spectrum table
|
||||||
|
trigtime = self.trigtime
|
||||||
|
spec = np.recarray(self.numchans, dtype=[('CHANNEL', '>i2'),
|
||||||
|
('COUNTS', '>i4'),
|
||||||
|
('QUALITY', '>i2')])
|
||||||
|
spec['CHANNEL'] = chan_idx
|
||||||
|
spec['COUNTS'] = self._data.counts
|
||||||
|
spec['QUALITY'] = (~self._channel_mask).astype(int)
|
||||||
|
|
||||||
|
# create FITS
|
||||||
|
hdulist = fits.HDUList()
|
||||||
|
primary_hdu = fits.PrimaryHDU(header=self._headers['PRIMARY'])
|
||||||
|
hdulist.append(primary_hdu)
|
||||||
|
|
||||||
|
ebounds_hdu = fits.BinTableHDU(data=ebounds, name='EBOUNDS',
|
||||||
|
header=self._headers['EBOUNDS'])
|
||||||
|
hdulist.append(ebounds_hdu)
|
||||||
|
|
||||||
|
spectrum_hdu = fits.BinTableHDU(data=spec, name='SPECTRUM',
|
||||||
|
header=self._headers['SPECTRUM'])
|
||||||
|
hdulist.append(spectrum_hdu)
|
||||||
|
|
||||||
|
gti = np.copy(self._gti)
|
||||||
|
gti['START'] += trigtime
|
||||||
|
gti['STOP'] += trigtime
|
||||||
|
gti_hdu = fits.BinTableHDU(data=gti, header=self._headers['GTI'],
|
||||||
|
name='GTI')
|
||||||
|
hdulist.append(gti_hdu)
|
||||||
|
|
||||||
|
hdulist.writeto(self.full_path, checksum=True)
|
||||||
|
|
||||||
|
def slice_energy(self, energy_ranges):
|
||||||
|
"""Slice the PHA by one or more energy range. Produces a new PHA object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
energy_ranges ([(float, float), ...]): The energy ranges to slice over
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`PHA`: A new PHA object with the requested energy range
|
||||||
|
"""
|
||||||
|
energy_ranges = self._assert_range_list(energy_ranges)
|
||||||
|
data = [self.data.slice(*self._assert_range(energy_range)) \
|
||||||
|
for energy_range in energy_ranges]
|
||||||
|
data = EnergyBins.merge(data)
|
||||||
|
|
||||||
|
tstart = self.headers['PRIMARY']['TSTART'] - self.trigtime
|
||||||
|
tstop = self.headers['PRIMARY']['TSTOP'] - self.trigtime
|
||||||
|
obj = self.headers['PRIMARY']['OBJECT']
|
||||||
|
ra_obj = self.headers['PRIMARY']['RA_OBJ']
|
||||||
|
dec_obj = self.headers['PRIMARY']['DEC_OBJ']
|
||||||
|
err_rad = self.headers['PRIMARY']['ERR_RAD']
|
||||||
|
backfile = self.headers['SPECTRUM']['BACKFIL']
|
||||||
|
rspfile = self.headers['SPECTRUM']['RESPFILE']
|
||||||
|
meta = self._filename_obj.meta
|
||||||
|
|
||||||
|
pha = self.from_data(data, tstart, tstop, gti=self.gti,
|
||||||
|
trigtime=self.trigtime, detector=self.detector,
|
||||||
|
object=obj, ra_obj=ra_obj, dec_obj=dec_obj,
|
||||||
|
err_rad=err_rad, datatype=self.datatype,
|
||||||
|
backfile=backfile, rspfile=rspfile, meta=meta)
|
||||||
|
return pha
|
||||||
|
|
||||||
|
def rebin_energy(self, method, *args, emin=None, emax=None):
|
||||||
|
"""Rebin the PHA in energy given a rebinning method.
|
||||||
|
Produces a new PHA object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method (<function>): The rebinning function
|
||||||
|
*args: Arguments to be passed to the rebinning function
|
||||||
|
emin (float, optional): The minimum energy to rebin. If omitted,
|
||||||
|
uses the lowest energy edge of the data
|
||||||
|
emax (float, optional): The maximum energy to rebin. If omitted,
|
||||||
|
uses the highest energy edge of the data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`PHA`: A new PHA object with the requested rebinned data
|
||||||
|
"""
|
||||||
|
data = self.data.rebin(method, *args, emin=None, emax=None)
|
||||||
|
|
||||||
|
tstart = self.headers['PRIMARY']['TSTART'] - self.trigtime
|
||||||
|
tstop = self.headers['PRIMARY']['TSTOP'] - self.trigtime
|
||||||
|
obj = self.headers['PRIMARY']['OBJECT']
|
||||||
|
ra_obj = self.headers['PRIMARY']['RA_OBJ']
|
||||||
|
dec_obj = self.headers['PRIMARY']['DEC_OBJ']
|
||||||
|
err_rad = self.headers['PRIMARY']['ERR_RAD']
|
||||||
|
backfile = self.headers['SPECTRUM']['BACKFIL']
|
||||||
|
rspfile = self.headers['SPECTRUM']['RESPFILE']
|
||||||
|
meta = self._filename_obj.meta
|
||||||
|
pha = self.from_data(data, tstart, tstop, gti=self.gti,
|
||||||
|
trigtime=self.trigtime, detector=self.detector,
|
||||||
|
object=obj, ra_obj=ra_obj, dec_obj=dec_obj,
|
||||||
|
err_rad=err_rad, datatype=self.datatype,
|
||||||
|
backfile=backfile, rspfile=rspfile, meta=meta)
|
||||||
|
return pha
|
||||||
|
|
||||||
|
def _set_channel_mask(self, quality_flags):
|
||||||
|
self._channel_mask = np.zeros_like(quality_flags, dtype=bool)
|
||||||
|
mask = (quality_flags == 0)
|
||||||
|
self._channel_mask[mask] = True
|
||||||
|
|
||||||
|
|
||||||
|
class BAK(PHA):
|
||||||
|
"""Class for a PHA background spectrum.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
data (:class:`~.primitives.EnergyBins`): The PHA data
|
||||||
|
datatype (str): The datatype of the file
|
||||||
|
detector (str): The GBM detector the file is associated with
|
||||||
|
directory (str): The directory the file is located in
|
||||||
|
energy_range (float, float): The energy range of the spectrum
|
||||||
|
exposure (float): The exposure of the PHA data
|
||||||
|
filename (str): The filename
|
||||||
|
full_path (str): The full path+filename
|
||||||
|
gti ([(float, float), ...]): The good time intervals
|
||||||
|
headers (dict): The headers for each extension of the PHA
|
||||||
|
id (str): The GBM file ID
|
||||||
|
is_gbm_file (bool): True if the file is a valid GBM standard file,
|
||||||
|
False if it is not.
|
||||||
|
is_trigger (bool): True if the file is a GBM trigger file, False if not
|
||||||
|
numchans (int): The number of energy channels
|
||||||
|
tcent (float): The center time of the data
|
||||||
|
time_range (float, float): The time range of the spectrum
|
||||||
|
trigtime (float): The trigger time of the data, if available
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
PHA.__init__(self)
|
||||||
|
self._channel_mask = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def open(cls, filename):
|
||||||
|
"""Open a BAK FITS file and return the BAK object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename (str): The filename of the FITS file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`BAK`: The BAK object
|
||||||
|
"""
|
||||||
|
from ..background import BackgroundSpectrum
|
||||||
|
obj = cls()
|
||||||
|
obj._file_properties(filename)
|
||||||
|
# open FITS file
|
||||||
|
with fits.open(filename) as hdulist:
|
||||||
|
# store headers
|
||||||
|
for hdu in hdulist:
|
||||||
|
obj._headers.update({hdu.name: hdu.header})
|
||||||
|
|
||||||
|
|
||||||
|
# trigger time
|
||||||
|
if 'TRIGTIME' in list(obj._headers['PRIMARY'].keys()):
|
||||||
|
if obj._headers['PRIMARY']['TRIGTIME'] != '':
|
||||||
|
obj._trigtime = float(obj._headers['PRIMARY']['TRIGTIME'])
|
||||||
|
else:
|
||||||
|
obj._headers['PRIMARY']['TRIGTIME'] = 0.0
|
||||||
|
else:
|
||||||
|
obj._headers['PRIMARY']['TRIGTIME'] = 0.0
|
||||||
|
|
||||||
|
spec = hdulist['SPECTRUM'].data
|
||||||
|
ebounds = hdulist['EBOUNDS'].data
|
||||||
|
gti = np.asarray(hdulist['GTI'].data)
|
||||||
|
if obj._trigtime is not None:
|
||||||
|
gti['START'] -= obj._trigtime
|
||||||
|
gti['STOP'] -= obj._trigtime
|
||||||
|
|
||||||
|
# create the background spectrum, the core of the BAK
|
||||||
|
exposure = np.full(ebounds.shape[0],
|
||||||
|
obj._headers['SPECTRUM']['EXPOSURE'])
|
||||||
|
obj._data = BackgroundSpectrum(spec['RATE'], spec['STAT_ERR'],
|
||||||
|
ebounds['E_MIN'], ebounds['E_MAX'],
|
||||||
|
exposure)
|
||||||
|
obj._gti = gti
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_data(cls, data, tstart, tstop, gti=None, trigtime=0.0,
|
||||||
|
detector=None,
|
||||||
|
object=None, ra_obj=None, dec_obj=None, err_rad=None,
|
||||||
|
datatype=None, meta=None):
|
||||||
|
"""Create a BAK object from a :class:`~gbm.background.BackgroundSpectrum`
|
||||||
|
data object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (:class:`~gbm.background.BackgroundSpectrum`):
|
||||||
|
The background count rate spectrum
|
||||||
|
tstart (float): The start time of the data
|
||||||
|
tstop (float): The end time of the data
|
||||||
|
gti ([(float, float), ...], optional):
|
||||||
|
The list of tuples representing the good time intervals
|
||||||
|
(start, stop). If omitted, the GTI is assumed to be
|
||||||
|
[(tstart, tstop)].
|
||||||
|
trigtime (float, optional):
|
||||||
|
The trigger time, if applicable. If provided, the data times
|
||||||
|
will be shifted relative to the trigger time. Default is zero.
|
||||||
|
detector (str, optional): The detector that produced the data
|
||||||
|
object (str, optional): The object being observed
|
||||||
|
ra_obj (float, optional): The RA of the object
|
||||||
|
dec_obj (float, optional): The Dec of the object
|
||||||
|
err_rad (float, optional): The localization error radius of the object
|
||||||
|
datatype (str, optional): The datatype from which the PHA is created
|
||||||
|
meta (str, optional): Additional metadata string to be added to
|
||||||
|
standard filename
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`BAK`: The BAK object
|
||||||
|
"""
|
||||||
|
obj = cls()
|
||||||
|
filetype = 'BACKGROUND'
|
||||||
|
obj._data = data
|
||||||
|
detchans = data.size
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
trigtime = float(trigtime)
|
||||||
|
except:
|
||||||
|
raise TypeError('trigtime must be a float')
|
||||||
|
|
||||||
|
if trigtime < 0.0:
|
||||||
|
raise ValueError('trigtime must be non-negative')
|
||||||
|
|
||||||
|
obj._trigtime = trigtime
|
||||||
|
tstart += trigtime
|
||||||
|
tstop += trigtime
|
||||||
|
|
||||||
|
# create the primary extension
|
||||||
|
primary_header = hdr.primary(detnam=detector, filetype=filetype,
|
||||||
|
tstart=tstart,
|
||||||
|
tstop=tstop, trigtime=trigtime,
|
||||||
|
object=object,
|
||||||
|
ra_obj=ra_obj, dec_obj=dec_obj,
|
||||||
|
err_rad=err_rad)
|
||||||
|
headers = [primary_header]
|
||||||
|
header_names = ['PRIMARY']
|
||||||
|
|
||||||
|
# ebounds extension
|
||||||
|
ebounds_header = hdr.ebounds(detnam=detector, tstart=tstart,
|
||||||
|
tstop=tstop,
|
||||||
|
trigtime=trigtime, object=object,
|
||||||
|
ra_obj=ra_obj, dec_obj=dec_obj,
|
||||||
|
err_rad=err_rad, detchans=detchans)
|
||||||
|
headers.append(ebounds_header)
|
||||||
|
header_names.append('EBOUNDS')
|
||||||
|
|
||||||
|
# spectrum extension
|
||||||
|
spectrum_header = hdr.pha_spectrum(exposure=data.exposure[0],
|
||||||
|
detnam=detector, tstart=tstart,
|
||||||
|
tstop=tstop,
|
||||||
|
trigtime=trigtime, object=object,
|
||||||
|
ra_obj=ra_obj,
|
||||||
|
dec_obj=dec_obj, err_rad=err_rad,
|
||||||
|
detchans=detchans,
|
||||||
|
poisserr=False)
|
||||||
|
spectrum_header['HDUCLAS2'] = ('BKG', 'Background PHA Spectrum')
|
||||||
|
spectrum_header['HDUCLAS3'] = ('RATE', 'PHA data stored as rates')
|
||||||
|
spectrum_header['TUNIT2'] = 'count/s'
|
||||||
|
spectrum_header['TUNIT3'] = 'count/s'
|
||||||
|
headers.append(spectrum_header)
|
||||||
|
header_names.append('SPECTRUM')
|
||||||
|
|
||||||
|
# gti extension
|
||||||
|
if gti is None:
|
||||||
|
gti = [(tstart, tstop)]
|
||||||
|
gti = np.array(gti, dtype=[('START', '>f8'), ('STOP', '>f8')])
|
||||||
|
gti['START'] += trigtime
|
||||||
|
gti['STOP'] += trigtime
|
||||||
|
gti_header = hdr.gti(detnam=detector, tstart=tstart, tstop=tstop,
|
||||||
|
trigtime=trigtime, object=object, ra_obj=ra_obj,
|
||||||
|
dec_obj=dec_obj, err_rad=err_rad)
|
||||||
|
headers.append(gti_header)
|
||||||
|
header_names.append('GTI')
|
||||||
|
obj._gti = gti
|
||||||
|
|
||||||
|
# store headers and set data properties
|
||||||
|
obj._headers = {name: header for name, header in
|
||||||
|
zip(header_names, headers)}
|
||||||
|
|
||||||
|
# set file info
|
||||||
|
obj.set_properties(detector=detector, trigtime=trigtime,
|
||||||
|
tstart=obj.time_range[0], datatype=datatype,
|
||||||
|
extension='bak', meta=meta)
|
||||||
|
|
||||||
|
gti['START'] -= obj.trigtime
|
||||||
|
gti['STOP'] -= obj.trigtime
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def write(self, directory, filename=None):
|
||||||
|
"""Writes the data to a FITS file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
directory (str): The directory to write the file
|
||||||
|
filename (str, optional): If set, will override the standardized name
|
||||||
|
"""
|
||||||
|
# set filename
|
||||||
|
if (self.filename is None) and (filename is None):
|
||||||
|
raise NameError('Filename not set')
|
||||||
|
if filename is None:
|
||||||
|
filename = self.filename
|
||||||
|
self._full_path = os.path.join(directory, filename)
|
||||||
|
self._headers['PRIMARY']['FILENAME'] = filename
|
||||||
|
|
||||||
|
# make ebounds table
|
||||||
|
chan_idx = np.arange(self.numchans, dtype=int)
|
||||||
|
ebounds = np.recarray(self.numchans, dtype=[('CHANNEL', '>i2'),
|
||||||
|
('E_MIN', '>f4'),
|
||||||
|
('E_MAX', '>f4')])
|
||||||
|
ebounds['CHANNEL'] = chan_idx
|
||||||
|
ebounds['E_MIN'] = self._data.lo_edges
|
||||||
|
ebounds['E_MAX'] = self._data.hi_edges
|
||||||
|
|
||||||
|
# make spectrum table
|
||||||
|
trigtime = self.trigtime
|
||||||
|
spec = np.recarray(self.numchans, dtype=[('CHANNEL', '>i2'),
|
||||||
|
('RATE', '>f8'),
|
||||||
|
('STAT_ERR', '>f8')])
|
||||||
|
spec['CHANNEL'] = chan_idx
|
||||||
|
spec['RATE'] = self._data.rates
|
||||||
|
spec['STAT_ERR'] = self._data.rate_uncertainty
|
||||||
|
|
||||||
|
# create FITS
|
||||||
|
hdulist = fits.HDUList()
|
||||||
|
primary_hdu = fits.PrimaryHDU(header=self._headers['PRIMARY'])
|
||||||
|
hdulist.append(primary_hdu)
|
||||||
|
|
||||||
|
ebounds_hdu = fits.BinTableHDU(data=ebounds, name='EBOUNDS',
|
||||||
|
header=self._headers['EBOUNDS'])
|
||||||
|
hdulist.append(ebounds_hdu)
|
||||||
|
|
||||||
|
spectrum_hdu = fits.BinTableHDU(data=spec, name='SPECTRUM',
|
||||||
|
header=self._headers['SPECTRUM'])
|
||||||
|
hdulist.append(spectrum_hdu)
|
||||||
|
|
||||||
|
gti = np.copy(self._gti)
|
||||||
|
gti['START'] += trigtime
|
||||||
|
gti['STOP'] += trigtime
|
||||||
|
gti_hdu = fits.BinTableHDU(data=gti, header=self._headers['GTI'],
|
||||||
|
name='GTI')
|
||||||
|
hdulist.append(gti_hdu)
|
||||||
|
|
||||||
|
hdulist.writeto(self.full_path, checksum=True)
|
||||||
|
|
||||||
|
def rebin_energy(self, method, *args, emin=None, emax=None):
|
||||||
|
"""Not Implemented"""
|
||||||
|
raise NotImplementedError('Function not available for BAK objects')
|
||||||
|
|
||||||
|
def slice_energy(self, method, *args, emin=None, emax=None):
|
||||||
|
"""Not Implemented"""
|
||||||
|
raise NotImplementedError('Function not available for BAK objects')
|
1372
data/phaii.py
Normal file
1372
data/phaii.py
Normal file
File diff suppressed because it is too large
Load Diff
615
data/poshist.py
Normal file
615
data/poshist.py
Normal file
@ -0,0 +1,615 @@
|
|||||||
|
# poshist.py: GBM Position History class
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import astropy.io.fits as fits
|
||||||
|
import numpy as np
|
||||||
|
from numpy.lib.recfunctions import stack_arrays
|
||||||
|
from collections import OrderedDict
|
||||||
|
from scipy.interpolate import interp1d
|
||||||
|
from warnings import warn
|
||||||
|
from gbm import coords
|
||||||
|
from .data import DataFile
|
||||||
|
from gbm.detectors import Detector
|
||||||
|
|
||||||
|
|
||||||
|
class PosHist(DataFile):
|
||||||
|
"""Class for Fermi Position History
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
datatype (str): The datatype of the file
|
||||||
|
detector (str): The GBM detector the file is associated with
|
||||||
|
directory (str): The directory the file is located in
|
||||||
|
filename (str): The filename
|
||||||
|
full_path (str): The full path+filename
|
||||||
|
gti ([(float, float), ...]): A list of time intervals where Fermi is
|
||||||
|
outside the SAA
|
||||||
|
headers (dict): The headers for each extension of the poshist file
|
||||||
|
id (str): The GBM file ID
|
||||||
|
is_gbm_file (bool): True if the file is a valid GBM standard file,
|
||||||
|
False if it is not.
|
||||||
|
is_trigger (bool): True if the file is a GBM trigger file, False if not
|
||||||
|
sun_occulted ([(float, float), ...]): A list of time intervals where the
|
||||||
|
sun is occulted by the Earth
|
||||||
|
time_range (float, float): The time range of the poshist
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(PosHist, self).__init__()
|
||||||
|
self._headers = OrderedDict()
|
||||||
|
self._data = None
|
||||||
|
self._times = None
|
||||||
|
self._eic_interp = None
|
||||||
|
self._quat_interp = None
|
||||||
|
self._lat_interp = None
|
||||||
|
self._lon_interp = None
|
||||||
|
self._alt_interp = None
|
||||||
|
self._vel_interp = None
|
||||||
|
self._angvel_interp = None
|
||||||
|
self._sun_interp = None
|
||||||
|
self._saa_interp = None
|
||||||
|
self._gti = None
|
||||||
|
self._sun_occulted = None
|
||||||
|
self._earth_radius_interp = None
|
||||||
|
self._geocenter_interp = None
|
||||||
|
self._detectors = [det.short_name for det in Detector]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def headers(self):
|
||||||
|
return self._headers
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_range(self):
|
||||||
|
return (self._times[0], self._times[-1])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gti(self):
|
||||||
|
return self._gti
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sun_occulted(self):
|
||||||
|
return self._sun_occulted
|
||||||
|
|
||||||
|
def get_eic(self, times):
|
||||||
|
"""Retrieve the position of Fermi in Earth inertial coordinates
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (float or np.array): Time(s) in MET
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: A (3, `n`) array of coordinates at `n` times
|
||||||
|
"""
|
||||||
|
return self._eic_interp(times)
|
||||||
|
|
||||||
|
def get_quaternions(self, times):
|
||||||
|
"""Retrieve the Fermi attitude quaternions
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (float or np.array): Time(s) in MET
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: A (4, `n`) array of quaternions at `n` times
|
||||||
|
"""
|
||||||
|
return self._quat_interp(times)
|
||||||
|
|
||||||
|
def get_latitude(self, times):
|
||||||
|
"""Retrieve the position of Fermi in Earth latitude
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (float or np.array): Time(s) in MET
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: An array of latitudes at the requested times
|
||||||
|
"""
|
||||||
|
return self._lat_interp(times)
|
||||||
|
|
||||||
|
def get_longitude(self, times):
|
||||||
|
"""Retrieve the position of Fermi in Earth East longitude
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (float or np.array): Time(s) in MET
|
||||||
|
|
||||||
|
np.array: An array of East longitudes at the requested times
|
||||||
|
"""
|
||||||
|
return self._lon_interp(times)
|
||||||
|
|
||||||
|
def get_altitude(self, times):
|
||||||
|
"""Retrieve the altitude of Fermi in orbit
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (float or np.array): Time(s) in MET
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: An array of altitudes at the requested times
|
||||||
|
"""
|
||||||
|
return self._alt_interp(times)
|
||||||
|
|
||||||
|
def get_velocity(self, times):
|
||||||
|
"""Retrieve the velocity of Fermi in Earth inertial coordinates
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (float or np.array): Time(s) in MET
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: A (3, `n`) array of velocity components at `n` times
|
||||||
|
"""
|
||||||
|
return self._vel_interp(times)
|
||||||
|
|
||||||
|
def get_angular_velocity(self, times):
|
||||||
|
"""Retrieve the angular velocity of Fermi
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (float or np.array): Time(s) in MET
|
||||||
|
|
||||||
|
Returns
|
||||||
|
np.array: A (3, `n`) array of angular velocity components at `n` times
|
||||||
|
"""
|
||||||
|
return self._angvel_interp(times)
|
||||||
|
|
||||||
|
def get_sun_visibility(self, times):
|
||||||
|
"""Determine if the sun is visible (not occulted)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (float or np.array): Time(s) in MET
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array(dtype=bool): A boolean array, where True indicates the \
|
||||||
|
sun is visible
|
||||||
|
"""
|
||||||
|
return self._sun_interp(times).astype(bool)
|
||||||
|
|
||||||
|
def get_saa_passage(self, times):
|
||||||
|
"""Determine if Fermi is in an SAA passage
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (float or np.array): Time(s) in MET
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array(dtype=bool): A boolean array, where True indicates Fermi \
|
||||||
|
is in SAA
|
||||||
|
"""
|
||||||
|
return self._saa_interp(times).astype(bool)
|
||||||
|
|
||||||
|
def get_earth_radius(self, times):
|
||||||
|
"""Retrieve the angular radius of the Earth as observed by Fermi
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (float or np.array): Time(s) in MET
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: An array of the apparent angular radius of the Earth
|
||||||
|
"""
|
||||||
|
return self._earth_radius_interp(times)
|
||||||
|
|
||||||
|
def get_geocenter_radec(self, times):
|
||||||
|
"""Retrieve the Equatorial position of the Geocenter as observed by Fermi
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (float or np.array): Time(s) in MET
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array): The RA and Dec of the geocenter
|
||||||
|
"""
|
||||||
|
eic = self.get_eic(times)
|
||||||
|
return coords.geocenter_in_radec(eic)
|
||||||
|
|
||||||
|
def get_mcilwain_l(self, times):
|
||||||
|
"""Retrieve the approximate McIlwain L value as determined by the
|
||||||
|
orbital position of Fermi
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (float or np.array): Time(s) in MET
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: An array of McIlwain L values
|
||||||
|
"""
|
||||||
|
lat = self.get_latitude(times)
|
||||||
|
lon = self.get_longitude(times)
|
||||||
|
ml = coords.calc_mcilwain_l(lat, lon)
|
||||||
|
return ml
|
||||||
|
|
||||||
|
def to_fermi_frame(self, ra, dec, times):
|
||||||
|
"""Convert an equatorial position to a position in spacecraft coordinates
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ra (float): The RA of a sky position
|
||||||
|
dec (float): The Dec of a sky position
|
||||||
|
times (float or np.array): Time(s) in MET
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array): The Fermi azimuth, zenith
|
||||||
|
"""
|
||||||
|
quats = self.get_quaternions(times)
|
||||||
|
az, zen = coords.radec_to_spacecraft(ra, dec, quats)
|
||||||
|
return az, zen
|
||||||
|
|
||||||
|
def to_equatorial(self, fermi_az, fermi_zen, times):
|
||||||
|
"""Convert a position in spacecraft coordinates to equatorial coordinates
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fermi_az (float): The Fermi azimuth of a position
|
||||||
|
fermi_zen (float): The Fermi zenith of a position
|
||||||
|
times (float or np.array): Time(s) in MET
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array): The RA, Dec of the position
|
||||||
|
"""
|
||||||
|
quats = self.get_quaternions(times)
|
||||||
|
ra, dec = coords.spacecraft_to_radec(fermi_az, fermi_zen, quats)
|
||||||
|
return ra, dec
|
||||||
|
|
||||||
|
def location_visible(self, ra, dec, times):
|
||||||
|
"""Determine if a sky location is visible or occulted by the Earth
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ra (float): The RA of a sky position
|
||||||
|
dec (float): The Dec of a sky position
|
||||||
|
times (float or np.array): Time(s) in MET
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array(dtype=bool): A boolean array where True indicates the \
|
||||||
|
position is visible.
|
||||||
|
"""
|
||||||
|
geo = np.array(self.get_geocenter_radec(times)).reshape(2, -1)
|
||||||
|
angle = coords.haversine(geo[0, :], geo[1, :], ra, dec)
|
||||||
|
visible = (angle > self.get_earth_radius(times))
|
||||||
|
return visible
|
||||||
|
|
||||||
|
def detector_pointing(self, det, times):
|
||||||
|
"""Retrieve the pointing of a GBM detector in equatorial coordinates
|
||||||
|
|
||||||
|
Args:
|
||||||
|
det (str): The GBM detector
|
||||||
|
times (float or np.array): Time(s) in MET
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array): The RA, Dec of the detector pointing
|
||||||
|
"""
|
||||||
|
det = det.lower()
|
||||||
|
if det not in self._detectors:
|
||||||
|
raise ValueError('Illegal detector name')
|
||||||
|
d = Detector.from_str(det)
|
||||||
|
az, zen = d.azimuth, d.zenith
|
||||||
|
ra, dec = self.to_equatorial(az, zen, times)
|
||||||
|
return ra, dec
|
||||||
|
|
||||||
|
def detector_angle(self, ra, dec, det, times):
|
||||||
|
"""Determine the angle between a sky location and a GBM detector
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ra (float): The RA of a sky position
|
||||||
|
dec (float): The Dec of a sky position
|
||||||
|
det (str): The GBM detector
|
||||||
|
times (float or np.array): Time(s) in MET
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The angle, in degrees, between the position and \
|
||||||
|
detector pointing
|
||||||
|
"""
|
||||||
|
det_ra, det_dec = self.detector_pointing(det, times)
|
||||||
|
angle = coords.haversine(det_ra, det_dec, ra, dec)
|
||||||
|
return angle
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def open(cls, filename):
|
||||||
|
"""Open and read a position history file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename (str): The filename of the FITS file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`PosHist`: The PosHist object
|
||||||
|
"""
|
||||||
|
obj = cls()
|
||||||
|
obj._file_properties(filename)
|
||||||
|
# open FITS file
|
||||||
|
with fits.open(filename) as hdulist:
|
||||||
|
for hdu in hdulist:
|
||||||
|
obj._headers.update({hdu.name: hdu.header})
|
||||||
|
data = hdulist['GLAST POS HIST'].data
|
||||||
|
|
||||||
|
times = data['SCLK_UTC']
|
||||||
|
obj._times = times
|
||||||
|
obj._data = data
|
||||||
|
|
||||||
|
# set the interpolators
|
||||||
|
obj._set_interpolators()
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def merge(cls, poshists):
|
||||||
|
"""Merge multiple PosHists into a single PosHist object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
poshists (list of :class:`PosHist`): List of PosHist objects
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`PosHist`: The merged PosHist object
|
||||||
|
"""
|
||||||
|
# sort by tstart, and remove overlapping times
|
||||||
|
idx = np.argsort([poshist.time_range[0] for poshist in poshists])
|
||||||
|
times = [poshists[idx[0]]._times]
|
||||||
|
|
||||||
|
data = [poshists[idx[0]]._data]
|
||||||
|
for i in idx[1:]:
|
||||||
|
mask = (poshists[i]._times > times[-1][-1])
|
||||||
|
times.append(poshists[i]._times[mask])
|
||||||
|
data.append(poshists[i]._data[mask])
|
||||||
|
|
||||||
|
# create object with merged data
|
||||||
|
obj = cls()
|
||||||
|
obj._file_properties(poshists[idx[0]].full_path)
|
||||||
|
obj._times = np.concatenate(times)
|
||||||
|
obj._data = stack_arrays(data, asrecarray=True, usemask=False)
|
||||||
|
obj._headers = poshists[idx[0]].headers
|
||||||
|
obj._set_interpolators()
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def _set_interpolators(self):
|
||||||
|
times = self._times
|
||||||
|
|
||||||
|
data = self._data
|
||||||
|
# Earth inertial coordinates interpolator
|
||||||
|
eic = np.array((data['POS_X'], data['POS_Y'], data['POS_Z']))
|
||||||
|
self._eic_interp = interp1d(times, eic)
|
||||||
|
|
||||||
|
# quaternions interpolator
|
||||||
|
quat = np.array(
|
||||||
|
(data['QSJ_1'], data['QSJ_2'], data['QSJ_3'], data['QSJ_4']))
|
||||||
|
self._quat_interp = interp1d(times, quat)
|
||||||
|
|
||||||
|
# Orbital position interpolator
|
||||||
|
# mark TODO: poshist uses the "simple" version of lat/lon calc
|
||||||
|
|
||||||
|
if 'SC_LAT' in data.dtype.names:
|
||||||
|
self._lat_interp = interp1d(times, data['SC_LAT'])
|
||||||
|
self._lon_interp = interp1d(times, data['SC_LON'])
|
||||||
|
alt = self._altitude_from_scpos(eic)
|
||||||
|
else:
|
||||||
|
lat, lon, alt = self._lat_lon_from_scpos(times, eic, complex=False)
|
||||||
|
self._lat_interp = interp1d(times, lat)
|
||||||
|
self._lon_interp = interp1d(times, lon)
|
||||||
|
self._alt_interp = interp1d(times, alt)
|
||||||
|
|
||||||
|
# Earth radius and geocenter interpolators
|
||||||
|
self._earth_radius_interp = interp1d(times, self._geo_half_angle(alt))
|
||||||
|
self._geocenter_interp = interp1d(times,
|
||||||
|
coords.geocenter_in_radec(eic))
|
||||||
|
|
||||||
|
# Velocity interpolator
|
||||||
|
vel = np.array((data['VEL_X'], data['VEL_Y'], data['VEL_Z']))
|
||||||
|
self._vel_interp = interp1d(times, vel)
|
||||||
|
|
||||||
|
# Angular velocity interpolator
|
||||||
|
angvel = np.array((data['WSJ_1'], data['WSJ_2'], data['WSJ_3']))
|
||||||
|
self._angvel_interp = interp1d(times, angvel)
|
||||||
|
|
||||||
|
# mark FIXME: the FLAGS=0 and FLAGS=2 never appear in daily poshist
|
||||||
|
# Interpolators for sun visibility and SAA passage
|
||||||
|
sun_visible = ((data['FLAGS'] == 1) | (data['FLAGS'] == 3))
|
||||||
|
in_saa = ((data['FLAGS']) == 2 | (data['FLAGS'] == 3))
|
||||||
|
self._sun_interp = interp1d(times, sun_visible)
|
||||||
|
self._saa_interp = interp1d(times, in_saa)
|
||||||
|
|
||||||
|
# set GTI based on SAA passages
|
||||||
|
# also times where sun is occulted
|
||||||
|
self._gti = self._split_bool_mask(in_saa, times)
|
||||||
|
self._sun_occulted = self._split_bool_mask(sun_visible, times)
|
||||||
|
|
||||||
|
def _from_trigdat(self, times, quats, scpos):
|
||||||
|
"""Initialize the PosHist object with info from a Trigdat object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (np.array): An array of Trigdat bin start times
|
||||||
|
quats (np.array): An array of quaternions from the Trigdat
|
||||||
|
scpos (np.array): An array of EICs from the Trigdat
|
||||||
|
"""
|
||||||
|
if quats.shape[0] == times.shape[0]:
|
||||||
|
quats = quats.T
|
||||||
|
if scpos.shape[0] == times.shape[0]:
|
||||||
|
scpos = scpos.T
|
||||||
|
|
||||||
|
# EIC is in km in Trigdat. We need it in m.
|
||||||
|
scpos *= 1000.0
|
||||||
|
|
||||||
|
# interpolators for EIC and quaternions
|
||||||
|
self._eic_interp = interp1d(times, scpos, fill_value='extrapolate')
|
||||||
|
self._quat_interp = interp1d(times, quats, fill_value='extrapolate')
|
||||||
|
|
||||||
|
# orbital position interpolators
|
||||||
|
lat, lon, alt = self._lat_lon_from_scpos(times, scpos, complex=True)
|
||||||
|
self._lat_interp = interp1d(times, lat, fill_value='extrapolate')
|
||||||
|
self._lon_interp = interp1d(times, lon, fill_value='extrapolate')
|
||||||
|
self._alt_interp = interp1d(times, alt, fill_value='extrapolate')
|
||||||
|
|
||||||
|
# georadius and geocenter interpolators
|
||||||
|
self._earth_radius_interp = interp1d(times, self._geo_half_angle(alt),
|
||||||
|
fill_value='extrapolate')
|
||||||
|
self._geocenter_interp = interp1d(times,
|
||||||
|
coords.geocenter_in_radec(scpos),
|
||||||
|
fill_value='extrapolate')
|
||||||
|
|
||||||
|
# velocity and angular velocity interpolators
|
||||||
|
vel = self._velocity_from_scpos(times, scpos)
|
||||||
|
self._vel_interp = interp1d(times, vel, fill_value='extrapolate')
|
||||||
|
angvel = self._angular_velocity_from_quaternion(times, quats)
|
||||||
|
self._angvel_interp = interp1d(times, angvel, fill_value='extrapolate')
|
||||||
|
|
||||||
|
# sun visibility
|
||||||
|
sun_visible = self._sun_visible_from_times(times)
|
||||||
|
self._sun_interp = interp1d(times, sun_visible,
|
||||||
|
fill_value='extrapolate')
|
||||||
|
self._sun_occulted = self._split_bool_mask(sun_visible, times)
|
||||||
|
|
||||||
|
def _split_bool_mask(self, mask, times):
|
||||||
|
"""Split a boolean mask into segments of contiguous values and apply
|
||||||
|
to array of times
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mask (np.array(dtype=bool)): The boolean mask
|
||||||
|
times (np.array): The array of times. Must be the same size as mask
|
||||||
|
|
||||||
|
Returns
|
||||||
|
[(float, float),...]: A list of time ranges
|
||||||
|
"""
|
||||||
|
# split a boolean mask array into segments based on True/False
|
||||||
|
indices = np.nonzero(mask[1:] != mask[:-1])[0] + 1
|
||||||
|
time_segs = np.split(times, indices)
|
||||||
|
mask_segs = np.split(mask, indices)
|
||||||
|
|
||||||
|
# retrieve the start and stop times for the "off" intervals
|
||||||
|
segs = []
|
||||||
|
numsegs = len(indices) + 1
|
||||||
|
for i in range(numsegs):
|
||||||
|
if mask_segs[i][0] == 0:
|
||||||
|
segs.append((time_segs[i][0], time_segs[i][-1]))
|
||||||
|
|
||||||
|
# mark FIXME: null if mask is all True or all False
|
||||||
|
# currently assuming that it must be all True
|
||||||
|
if len(segs) == 0:
|
||||||
|
segs = [self.time_range]
|
||||||
|
return segs
|
||||||
|
|
||||||
|
def _altitude_from_scpos(self, scpos):
|
||||||
|
"""Calculate altitude from the EIC.
|
||||||
|
Will attempt to do the more complex calculation taking into account
|
||||||
|
the shape of the Earth and the variable rotational velocity of the
|
||||||
|
Earth. This requires having up-to-date IERS tables. If local tables
|
||||||
|
are not up-to-date and astropy cannot query for a new table, will
|
||||||
|
default to a simpler approximation (spherical Earth, constant rotational
|
||||||
|
velocity).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
scpos (np.array): The Earth inertial coordinates
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The altitudes
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
_, alt = coords.latitude_from_geocentric_coords_complex(scpos)
|
||||||
|
except:
|
||||||
|
warn('Using simple spheroidal Earth approximation')
|
||||||
|
_, alt = coords.latitude_from_geocentric_coords_simple(scpos)
|
||||||
|
return alt
|
||||||
|
|
||||||
|
def _angular_velocity_from_quaternion(self, times, quats):
|
||||||
|
"""Calculate angular velocity from changes in quaternion over time.
|
||||||
|
This is accurate for small rotations over very short times.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (np.array): Times in MET
|
||||||
|
quats (np.array): Quaternions; one for each time
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The angular velocity
|
||||||
|
"""
|
||||||
|
dt = times[1:] - times[0:-1]
|
||||||
|
dquat = quats[:, 1:] - quats[:, :-1]
|
||||||
|
prod_quat = dquat
|
||||||
|
for i in range(len(dt)):
|
||||||
|
conj_quat = coords.quaternion_conj(quats[:, i])
|
||||||
|
prod_quat[:, i] = coords.quaternion_prod(conj_quat, dquat[:, i])
|
||||||
|
ang_vel = 2.0 / dt[np.newaxis, :] * prod_quat[0:3, :]
|
||||||
|
ang_vel = np.append(ang_vel, ang_vel[:, -1:], axis=1)
|
||||||
|
return ang_vel
|
||||||
|
|
||||||
|
def _velocity_from_scpos(self, times, scpos):
|
||||||
|
"""Calculate velocity from changes in position over time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (np.array): Times in MET
|
||||||
|
scpos (np.array): Positions of the spacecraft in Earth inertial
|
||||||
|
coordinates
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The velocity
|
||||||
|
"""
|
||||||
|
dt = times[1:] - times[0:-1]
|
||||||
|
dpos = scpos[:, 1:] - scpos[:, 0:-1]
|
||||||
|
velocity = dpos / dt
|
||||||
|
# copy last entry
|
||||||
|
velocity = np.append(velocity, velocity[:, -1:], axis=1)
|
||||||
|
return velocity
|
||||||
|
|
||||||
|
def _lat_lon_from_scpos(self, times, scpos, complex=False):
|
||||||
|
"""Convert coordinates of the spacecraft in Earth inertial coordinates
|
||||||
|
to latitude, East longitude, and altitude.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (np.array): Times in MET
|
||||||
|
scpos (np.array): Positions of the spacecraft in Earth inertial
|
||||||
|
coordinates
|
||||||
|
complex (bool, optional):
|
||||||
|
If True, then uses the non-spherical Earth model and accurate
|
||||||
|
Earth rotation model. Default is False; Earth is a sphere and
|
||||||
|
the Earth rotation model is less accurate.
|
||||||
|
GBM FSW uses the latter option.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple - 3-tuple containing:
|
||||||
|
|
||||||
|
- *np.array*: latitude
|
||||||
|
- *np.array*: longitude
|
||||||
|
- *np.array*: altitude
|
||||||
|
"""
|
||||||
|
if complex is False:
|
||||||
|
lat, alt = coords.latitude_from_geocentric_coords_simple(scpos)
|
||||||
|
else:
|
||||||
|
lat, alt = coords.latitude_from_geocentric_coords_complex(scpos)
|
||||||
|
try:
|
||||||
|
lon = coords.longitude_from_geocentric_coords(scpos, times,
|
||||||
|
ut1=complex)
|
||||||
|
except:
|
||||||
|
lon = coords.longitude_from_geocentric_coords(scpos, times,
|
||||||
|
ut1=False)
|
||||||
|
|
||||||
|
return (lat, lon, alt)
|
||||||
|
|
||||||
|
def _geo_half_angle(self, altitudes):
|
||||||
|
"""Calculate the apparent angular radius of the Earth
|
||||||
|
|
||||||
|
Args:
|
||||||
|
altitudes (np.array): The altitude of Fermi
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The angular radius, in degrees
|
||||||
|
"""
|
||||||
|
r = 6371.0 * 1000.0
|
||||||
|
half_angle = np.rad2deg(np.arcsin(r / (r + altitudes)))
|
||||||
|
return half_angle
|
||||||
|
|
||||||
|
def _sun_visible_from_times(self, times):
|
||||||
|
"""Calculate the sun visibility mask from the MET
|
||||||
|
|
||||||
|
Args:
|
||||||
|
times (np.array): The METs
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array(dtype=bool): A boolean mask, where True indicates the sun \
|
||||||
|
is visible.
|
||||||
|
"""
|
||||||
|
sun = np.array(coords.get_sun_loc(times))
|
||||||
|
return self.location_visible(sun[0, :], sun[1, :], times)
|
1854
data/primitives.py
Normal file
1854
data/primitives.py
Normal file
File diff suppressed because it is too large
Load Diff
1153
data/scat.py
Normal file
1153
data/scat.py
Normal file
File diff suppressed because it is too large
Load Diff
113
data/tcat.py
Normal file
113
data/tcat.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
# tcat.py: GBM Trigger Catalog (TCAT) file class
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import astropy.io.fits as fits
|
||||||
|
from collections import OrderedDict
|
||||||
|
from .data import DataFile
|
||||||
|
|
||||||
|
|
||||||
|
class Tcat(DataFile):
|
||||||
|
"""Class for Trigger Catalog (TCAT) files
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
datatype (str): The datatype of the file
|
||||||
|
detector (str): The GBM detector the file is associated with
|
||||||
|
directory (str): The directory the file is located in
|
||||||
|
fermi_location (float, float): Fermi's orbital longitude and latitude
|
||||||
|
filename (str): The filename
|
||||||
|
full_path (str): The full path+filename
|
||||||
|
headers (dict): The headers for each extension of the file
|
||||||
|
id (str): The GBM file ID
|
||||||
|
is_gbm_file (bool): True if the file is a valid GBM standard file,
|
||||||
|
False if it is not.
|
||||||
|
is_trigger (bool): True if the file is a GBM trigger file, False if not
|
||||||
|
localizing_instrument (str): The localizing instrument
|
||||||
|
location (float, float, float): RA, Dec, and localization uncertainty
|
||||||
|
location_fermi_frame (float, float): Location in Fermi azimuth and zenith
|
||||||
|
name (str): Name of the trigger
|
||||||
|
time_range (float, float): The time range
|
||||||
|
trigtime (float): The trigger time
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(Tcat, self).__init__()
|
||||||
|
self._headers = OrderedDict()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def headers(self):
|
||||||
|
return self._headers
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_range(self):
|
||||||
|
return (self.headers['PRIMARY']['TSTART'],
|
||||||
|
self.headers['PRIMARY']['TSTOP'])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def trigtime(self):
|
||||||
|
return self.headers['PRIMARY']['TRIGTIME']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def location(self):
|
||||||
|
return (self.headers['PRIMARY']['RA_OBJ'],
|
||||||
|
self.headers['PRIMARY']['DEC_OBJ'],
|
||||||
|
self.headers['PRIMARY']['ERR_RAD'])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.headers['PRIMARY']['OBJECT']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def location_fermi_frame(self):
|
||||||
|
return (self.headers['PRIMARY']['PHI'],
|
||||||
|
self.headers['PRIMARY']['THETA'])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def localizing_instrument(self):
|
||||||
|
return self.headers['PRIMARY']['LOC_SRC']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fermi_location(self):
|
||||||
|
return (self.headers['PRIMARY']['GEO_LONG'],
|
||||||
|
self.headers['PRIMARY']['GEO_LAT'])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def open(cls, filename):
|
||||||
|
"""Open a TCAT file and return the Tcat object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename (str): The filename of the TCAT file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`Tcat`: The Tcat object
|
||||||
|
"""
|
||||||
|
obj = cls()
|
||||||
|
obj._file_properties(filename)
|
||||||
|
|
||||||
|
# open FITS file
|
||||||
|
with fits.open(filename) as hdulist:
|
||||||
|
for hdu in hdulist:
|
||||||
|
obj._headers.update({hdu.name: hdu.header})
|
||||||
|
|
||||||
|
return obj
|
643
data/trigdat.py
Normal file
643
data/trigdat.py
Normal file
@ -0,0 +1,643 @@
|
|||||||
|
# trigdat.py: GBM trigger data class
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import astropy.io.fits as fits
|
||||||
|
import numpy as np
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from gbm.detectors import Detector
|
||||||
|
from .phaii import Ctime
|
||||||
|
from .poshist import PosHist
|
||||||
|
from .primitives import TimeEnergyBins
|
||||||
|
|
||||||
|
|
||||||
|
# Map the classification numbers to the string names
|
||||||
|
classifications = {0: 'ERROR', 1: 'UNRELOC', 2: 'LOCLPAR', 3: 'BELOWHZ',
|
||||||
|
4: 'GRB', 5: 'SGR', 6: 'TRANSNT', 7: 'DISTPAR',
|
||||||
|
8: 'SFL', 9: 'CYGX1', 10: 'SGR1806', 11: 'GROJ422',
|
||||||
|
19: 'TGF', 20: 'UNCERT', 21: 'GALBIN'}
|
||||||
|
# localization spectra
|
||||||
|
spectrum = ['hard', 'normal', 'soft']
|
||||||
|
|
||||||
|
|
||||||
|
class Trigdat(PosHist):
|
||||||
|
"""Class for the GBM Trigger Data.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
backrates (:class:`~gbm.data.trigdat.BackRates`):
|
||||||
|
A BackRates object containing the info from the on-board background
|
||||||
|
datatype (str): The datatype of the file
|
||||||
|
detector (str): The GBM detector the file is associated with
|
||||||
|
directory (str): The directory the file is located in
|
||||||
|
filename (str): The filename
|
||||||
|
fsw_locations: (:class:`~gbm.data.trigdat.FswLocation`):
|
||||||
|
A list of flight-software-determined locations for the event
|
||||||
|
full_path (str): The full path+filename
|
||||||
|
headers (dict): The headers for each extension of the file
|
||||||
|
id (str): The GBM file ID
|
||||||
|
is_gbm_file (bool): True if the file is a valid GBM standard file,
|
||||||
|
False if it is not.
|
||||||
|
is_trigger (bool): True if the file is a GBM trigger file, False if not
|
||||||
|
maxrates (list of :class:`~gbm.data.trigdat.MaxRates`):
|
||||||
|
A list of MaxRates objects, each containing maxrates info
|
||||||
|
num_maxrates (int): The number of MaxRates issued by the flight software
|
||||||
|
time_range (float, float): The time range of the data
|
||||||
|
triggered_detectors: (list of str): The detectors that were triggered
|
||||||
|
trigrates (:class:`~gbm.data.trigdat.MaxRates`):
|
||||||
|
A MaxRates object containing the trigger information and rates
|
||||||
|
trigtime (float): The trigger time
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(Trigdat, self).__init__()
|
||||||
|
self._headers = OrderedDict()
|
||||||
|
self._data = None
|
||||||
|
self._rates = None
|
||||||
|
self._trigrates = None
|
||||||
|
self._maxrates = None
|
||||||
|
self._backrates = None
|
||||||
|
self._fsw_locations = None
|
||||||
|
self._emin = np.array([3.4, 10.0, 22.0, 44.0, 95.0, 300., 500., 800.])
|
||||||
|
self._emax = np.array([10., 22.0, 44.0, 95.0, 300., 500., 800., 2000.])
|
||||||
|
self._bins64 = None
|
||||||
|
self._bins256 = None
|
||||||
|
self._bins1024 = None
|
||||||
|
self._bins8192 = None
|
||||||
|
self._time_range = None
|
||||||
|
self._detectors = [det.short_name for det in Detector]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def trigtime(self):
|
||||||
|
return self._headers['PRIMARY']['TRIGTIME']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def headers(self):
|
||||||
|
return self._headers
|
||||||
|
|
||||||
|
@property
|
||||||
|
def num_maxrates(self):
|
||||||
|
return len(self._maxrates)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def trigrates(self):
|
||||||
|
return self._trigrates
|
||||||
|
|
||||||
|
@property
|
||||||
|
def backrates(self):
|
||||||
|
return self._backrates
|
||||||
|
|
||||||
|
@property
|
||||||
|
def maxrates(self):
|
||||||
|
return [self.get_maxrates(i) for i in range(self.num_maxrates)]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fsw_locations(self):
|
||||||
|
return [self.get_fsw_locations(i) for i in range(self.num_maxrates)]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_range(self):
|
||||||
|
return self._time_range
|
||||||
|
|
||||||
|
@property
|
||||||
|
def triggered_detectors(self):
|
||||||
|
detmask = self.headers['PRIMARY']['DET_MASK']
|
||||||
|
detmask = np.array(list(detmask)).astype(bool)
|
||||||
|
if detmask.size == 14:
|
||||||
|
return (np.array(self._detectors)[detmask]).tolist()
|
||||||
|
elif detmask.size == 12:
|
||||||
|
return (np.array(self._detectors[:-2])[detmask]).tolist()
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def open(cls, filename):
|
||||||
|
"""Open and read a trigdat file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename(str): The filename of the trigdat file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`Trigdat`: The Trigdat object
|
||||||
|
"""
|
||||||
|
obj = cls()
|
||||||
|
obj._file_properties(filename)
|
||||||
|
# open FITS file
|
||||||
|
with fits.open(filename) as hdulist:
|
||||||
|
# store headers
|
||||||
|
for hdu in hdulist:
|
||||||
|
obj._headers.update({hdu.name: hdu.header})
|
||||||
|
|
||||||
|
# store trigrate, maxrates, backrates, and fsw location
|
||||||
|
obj._trigrates = MaxRates(hdulist['TRIGRATE'].data[0])
|
||||||
|
obj._maxrates = [MaxRates(maxrate) for maxrate in
|
||||||
|
hdulist['MAXRATES'].data]
|
||||||
|
obj._backrates = BackRates(hdulist['BCKRATES'].data[0])
|
||||||
|
obj._fsw_locations = [FswLocation(ob_calc) \
|
||||||
|
for ob_calc in hdulist['OB_CALC'].data]
|
||||||
|
obj._data = hdulist['EVNTRATE'].data
|
||||||
|
obj._data.sort(order='TIME')
|
||||||
|
|
||||||
|
# store position history
|
||||||
|
idx, dt = obj._time_indices(1024)
|
||||||
|
obj._from_trigdat(obj._data['TIME'][idx],
|
||||||
|
obj._data['SCATTITD'][idx],
|
||||||
|
obj._data['EIC'][idx])
|
||||||
|
|
||||||
|
# store the time history
|
||||||
|
obj._rates = obj._data['RATE'].reshape(-1, 14, 8)
|
||||||
|
obj._time_range = (
|
||||||
|
obj._data['ENDTIME'][0] - dt[0], obj._data['ENDTIME'][-1])
|
||||||
|
obj._gti = obj._gti_from_times(obj._data['TIME'],
|
||||||
|
obj._data['ENDTIME'])
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def get_maxrates(self, index):
|
||||||
|
"""Retrieve a MaxRates
|
||||||
|
|
||||||
|
Args:
|
||||||
|
index (int): The index of the MaxRates to retrieve. Not to
|
||||||
|
exceed num_maxrates-1
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`~gbm.data.trigdat.MaxRates`: The MaxRates object
|
||||||
|
"""
|
||||||
|
if index > self.num_maxrates:
|
||||||
|
raise ValueError('index out of range. ' \
|
||||||
|
'{} available maxrates'.format(self.num_maxrates))
|
||||||
|
return self._maxrates[index]
|
||||||
|
|
||||||
|
def get_fsw_locations(self, index):
|
||||||
|
"""Retrieve a flight software localization
|
||||||
|
|
||||||
|
Args:
|
||||||
|
index (int): The index of the localization to retrieve. Not to
|
||||||
|
exceed num_maxrates-1
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`~gbm.data.trigdat.FswLocation`: The flight software localization info
|
||||||
|
"""
|
||||||
|
if index > self.num_maxrates:
|
||||||
|
raise ValueError('index out of range. ' \
|
||||||
|
'{} available locations'.format(
|
||||||
|
self.num_maxrates))
|
||||||
|
return self._fsw_locations[index]
|
||||||
|
|
||||||
|
def to_ctime(self, detector, timescale=1024):
|
||||||
|
"""Convert the data for a detector to a CTIME-like
|
||||||
|
:class:`~gbm.data.phaii.PHAII` object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
detector (str): The detector to convert
|
||||||
|
timescale (int, optional):
|
||||||
|
The minimum timescale in ms of the data to return. Available
|
||||||
|
options are 1024, 256, and 64.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`~gbm.data.Ctime`: The CTIME-like PHAII object with the \
|
||||||
|
trigdat data
|
||||||
|
"""
|
||||||
|
# check for valid detectors and timescale
|
||||||
|
detector = detector.lower()
|
||||||
|
if detector not in self._detectors:
|
||||||
|
raise ValueError('Illegal detector name')
|
||||||
|
|
||||||
|
if (timescale != 1024) and (timescale != 256) and (timescale != 64):
|
||||||
|
raise ValueError('Illegal Trigdat resolution. Available resolutions: \
|
||||||
|
1024, 256, 64')
|
||||||
|
|
||||||
|
# grab the correct detector rates (stored as 14 dets x 8 channels)
|
||||||
|
det_idx = self._detectors.index(detector)
|
||||||
|
|
||||||
|
# return the requested timescales
|
||||||
|
time_idx, dt = self._time_indices(timescale)
|
||||||
|
|
||||||
|
# calculate counts and exposure
|
||||||
|
counts = self._rates[time_idx, det_idx, :] * (
|
||||||
|
dt[:, np.newaxis] / 1.024)
|
||||||
|
exposure = self._calc_exposure(counts, dt)
|
||||||
|
|
||||||
|
# the 'TIME' array is incorrect in the trigdat. we know this because
|
||||||
|
# the 'ENDTIME' is the value in the packet, so we must calculate tstart
|
||||||
|
# ourselves and forget about using 'TIME'
|
||||||
|
tstop = self._data['ENDTIME'][time_idx] - self.trigtime
|
||||||
|
tstart = tstop - dt
|
||||||
|
# create the Time-Energy histogram
|
||||||
|
bins = TimeEnergyBins(counts, tstart, tstop, exposure,
|
||||||
|
self._emin, self._emax)
|
||||||
|
# create the CTIME object
|
||||||
|
object = self.headers['PRIMARY']['OBJECT']
|
||||||
|
ra = self.headers['PRIMARY']['RA_OBJ']
|
||||||
|
dec = self.headers['PRIMARY']['DEC_OBJ']
|
||||||
|
err = self.headers['PRIMARY']['ERR_RAD']
|
||||||
|
obj = Ctime.from_data(bins, gti=self.gti, trigtime=self.trigtime,
|
||||||
|
detector=detector, object=object, ra_obj=ra,
|
||||||
|
dec_obj=dec, err_rad=err)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def sum_detectors(self, detectors, timescale=1024):
|
||||||
|
"""Sum the data from a list of detectors and convert to a CTIME-like
|
||||||
|
:class:`~gbm.data.phaii.PHAII` object
|
||||||
|
|
||||||
|
Args:
|
||||||
|
detectors (list of str): The detectors to sum
|
||||||
|
timescale (int, optional):
|
||||||
|
The minimum timescale in ms of the data to return. Available
|
||||||
|
options are 1024, 256, and 64.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`~gbm.data.Ctime`: The CTIME-like PHAII object with the detector-summed data
|
||||||
|
"""
|
||||||
|
# check for valid detectors and timescale
|
||||||
|
for det in detectors:
|
||||||
|
det = det.lower()
|
||||||
|
if det not in self._detectors:
|
||||||
|
raise ValueError('Illegal detector name')
|
||||||
|
|
||||||
|
if (timescale != 1024) and (timescale != 256) and (timescale != 64):
|
||||||
|
raise ValueError('Illegal Trigdat resolution. Available resolutions: \
|
||||||
|
1024, 256, 64')
|
||||||
|
|
||||||
|
counts = None
|
||||||
|
for det in detectors:
|
||||||
|
# grab the correct detector rates (stored as 14 dets x 8 channels)
|
||||||
|
det_idx = self._detectors.index(det)
|
||||||
|
|
||||||
|
# return the requested timescales
|
||||||
|
time_idx, dt = self._time_indices(timescale)
|
||||||
|
|
||||||
|
# calculate counts
|
||||||
|
if counts is None:
|
||||||
|
counts = self._rates[time_idx, det_idx, :] * (
|
||||||
|
dt[:, np.newaxis] / 1.024)
|
||||||
|
else:
|
||||||
|
counts += self._rates[time_idx, det_idx, :] * (
|
||||||
|
dt[:, np.newaxis] / 1.024)
|
||||||
|
|
||||||
|
exposure = dt
|
||||||
|
|
||||||
|
# the 'TIME' array is incorrect in the trigdat. we know this because
|
||||||
|
# the 'ENDTIME' is the value in the packet, so we must calculate tstart
|
||||||
|
# ourselves and forget about using 'TIME'
|
||||||
|
tstop = self._data['ENDTIME'][time_idx] - self.trigtime
|
||||||
|
tstart = self._fix_tstart(tstop, dt)
|
||||||
|
|
||||||
|
# create the Time-Energy histogram
|
||||||
|
bins = TimeEnergyBins(counts, tstart, tstop, exposure,
|
||||||
|
self._emin, self._emax)
|
||||||
|
|
||||||
|
det_str = '+'.join(detectors)
|
||||||
|
# create the CTIME object
|
||||||
|
object = self.headers['PRIMARY']['OBJECT']
|
||||||
|
ra = self.headers['PRIMARY']['RA_OBJ']
|
||||||
|
dec = self.headers['PRIMARY']['DEC_OBJ']
|
||||||
|
err = self.headers['PRIMARY']['ERR_RAD']
|
||||||
|
obj = Ctime.from_data(bins, gti=self.gti, trigtime=self.trigtime,
|
||||||
|
detector=det_str, object=object, ra_obj=ra,
|
||||||
|
dec_obj=dec, err_rad=err)
|
||||||
|
# Have to set the datatype property. The 8 energy channels is most like
|
||||||
|
# CTIME.
|
||||||
|
obj.set_properties(datatype='ctime', trigtime=self.trigtime)
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def get_saa_passage(self, times):
|
||||||
|
in_saa = np.ones_like(times, dtype=bool)
|
||||||
|
for interval in self.gti:
|
||||||
|
mask = (times >= interval[0]) & (times <= interval[1])
|
||||||
|
in_saa[mask] = False
|
||||||
|
return in_saa
|
||||||
|
|
||||||
|
def _fix_tstart(self, tstop, dt):
|
||||||
|
# this ensures that edge differences < 1 ms get fixed
|
||||||
|
tstart = tstop - dt
|
||||||
|
mask = (np.abs(tstart[1:] - tstop[:-1]) < 0.001)
|
||||||
|
tstart[1:][mask] = tstop[:-1][mask]
|
||||||
|
return tstart
|
||||||
|
|
||||||
|
def _calc_exposure(self, counts, dt):
|
||||||
|
"""Calculate the exposure
|
||||||
|
|
||||||
|
Args:
|
||||||
|
counts (np.array): The observed counts in each bin
|
||||||
|
dt (np.array): The time bin widths
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The exposure of each bin
|
||||||
|
"""
|
||||||
|
deadtime = np.copy(counts)
|
||||||
|
deadtime[:, :7] *= 2.6e-6 # 2.6 us for each count
|
||||||
|
deadtime[:, 7] *= 1e-5 # 10 us for each count in overflow
|
||||||
|
total_deadtime = np.sum(deadtime, axis=1)
|
||||||
|
exposure = (1.0 - total_deadtime) * dt
|
||||||
|
return exposure
|
||||||
|
|
||||||
|
def _time_indices(self, time_res):
|
||||||
|
"""Indices into the Trigdat arrays corresponding to the desired time
|
||||||
|
resolution(s)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
time_res (int): The time resolution in ms of the data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(np.array, np.array): Indices into the trigdat arrays and the \
|
||||||
|
bin widths in seconds
|
||||||
|
"""
|
||||||
|
# bin widths
|
||||||
|
dt = np.round((self._data['ENDTIME'] - self._data['TIME']) * 1000)
|
||||||
|
# background bins
|
||||||
|
back_idx = np.where(dt == 8192)[0]
|
||||||
|
# 1 s scale bins - this is the minimum amount returned
|
||||||
|
idx = np.where(dt == 1024)[0]
|
||||||
|
cnt = len(idx)
|
||||||
|
# reconcile 8 s and 1 s data
|
||||||
|
idx = self._reconcile_timescales(back_idx, idx)
|
||||||
|
|
||||||
|
# reconcile 8 s + 1 s and 256 ms data
|
||||||
|
if time_res <= 256:
|
||||||
|
tidx = np.where(dt == 256)[0]
|
||||||
|
idx = self._reconcile_timescales(idx, tidx)
|
||||||
|
|
||||||
|
# reconcile 8 s + 1 s + 256 ms and 64 ms data
|
||||||
|
if time_res == 64:
|
||||||
|
tidx = np.where(dt == 64)[0]
|
||||||
|
idx = self._reconcile_timescales(idx, tidx)
|
||||||
|
|
||||||
|
# return reconciled indices
|
||||||
|
return idx, np.reshape(dt[idx] / 1000.0, len(idx))
|
||||||
|
|
||||||
|
def _reconcile_timescales(self, idx1, idx2):
|
||||||
|
"""Reconcile indices representing different timescales and glue them
|
||||||
|
together to form a complete (mostly) continuous set of indices
|
||||||
|
|
||||||
|
Args:
|
||||||
|
idx1 (np.array): Indices of the "bracketing" timescale
|
||||||
|
idx2 (np.array): Indices of the "inserted" timescale
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: Indices of idx2 spliced into idx1
|
||||||
|
"""
|
||||||
|
# bin edges for both selections
|
||||||
|
start_times1 = self._data['TIME'][idx1]
|
||||||
|
end_times1 = self._data['ENDTIME'][idx1]
|
||||||
|
start_times2 = self._data['TIME'][idx2]
|
||||||
|
end_times2 = self._data['ENDTIME'][idx2]
|
||||||
|
|
||||||
|
# find where bracketing timescale ends and inserted timescale begins
|
||||||
|
start_idx = (np.where(end_times1 >= start_times2[0]))[0][0]
|
||||||
|
idx = np.concatenate((idx1[0:start_idx], idx2))
|
||||||
|
|
||||||
|
# find wehere inserted timescale ends and bracketing timescale begins again
|
||||||
|
end_idx = (np.where(start_times1 >= end_times2[-1]))[0][0]
|
||||||
|
idx = np.concatenate((idx, idx1[end_idx:]))
|
||||||
|
|
||||||
|
return idx
|
||||||
|
|
||||||
|
def _gti_from_times(self, tstarts, tstops):
|
||||||
|
"""Estimate the GTI from the bin start and stop times.
|
||||||
|
This may return multiple GTIs if several background packets are missing
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tstarts (np.array): The start times of the bins
|
||||||
|
tstops (np.array): The end times of the bins
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
[(float, float), ...]: A list of time ranges
|
||||||
|
"""
|
||||||
|
tstart = tstarts[0]
|
||||||
|
tstop = tstops[-1]
|
||||||
|
dt = tstarts[1:] - tstops[:-1]
|
||||||
|
idx = np.where(np.abs(dt) > 10.0)[0]
|
||||||
|
if np.sum(idx) > 0:
|
||||||
|
gti = [(tstart, tstops[idx[0] - 1]), (tstarts[idx[0]], tstop)]
|
||||||
|
else:
|
||||||
|
gti = [(tstart, tstop)]
|
||||||
|
return np.array(gti)
|
||||||
|
|
||||||
|
|
||||||
|
class MaxRates:
|
||||||
|
"""Class for the MaxRates data in Trigdat.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
rec_array (np.recarray): The FITS TRIGRATE or MAXRATES record array
|
||||||
|
from the trigdat file
|
||||||
|
Attributes:
|
||||||
|
all_rates (np.array): An array (:attr:`numchans`, :attr:`numdets`) of
|
||||||
|
the maxrates
|
||||||
|
eic (np.array): The position of Fermi in Earth inertial coordinates
|
||||||
|
numchans (int): The number of energy channels
|
||||||
|
numdets (int): The number of detectors
|
||||||
|
quaternions (np.array): The quaternions at the maxrates time
|
||||||
|
timescale (float): The timescale of the maxrates
|
||||||
|
time_range (float, float): The time range of the maxrates
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, rec_array):
|
||||||
|
self._time_range = (rec_array['TIME'], rec_array['ENDTIME'])
|
||||||
|
self._quats = rec_array['SCATTITD']
|
||||||
|
self._eic = rec_array['EIC']
|
||||||
|
try:
|
||||||
|
self._rates = rec_array['TRIGRATE']
|
||||||
|
except:
|
||||||
|
self._rates = rec_array['MAXRATES']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def numchans(self):
|
||||||
|
return self._rates.shape[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def numdets(self):
|
||||||
|
return self._rates.shape[1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_range(self):
|
||||||
|
return self._time_range
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timescale(self):
|
||||||
|
return self.time_range[1] - self.time_range[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def quaternions(self):
|
||||||
|
return self._quats
|
||||||
|
|
||||||
|
@property
|
||||||
|
def eic(self):
|
||||||
|
return self._eic
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_rates(self):
|
||||||
|
return self._rates
|
||||||
|
|
||||||
|
def get_detector(self, det):
|
||||||
|
"""Retrieve the maxrates for a detector
|
||||||
|
|
||||||
|
Args:
|
||||||
|
det (str): The detector
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: An array of size (:attr:`numchans`,) of rates for the detector
|
||||||
|
"""
|
||||||
|
mask = (np.array(self._detectors) == det)
|
||||||
|
return self._rates[:, mask].squeeze()
|
||||||
|
|
||||||
|
|
||||||
|
class BackRates:
|
||||||
|
"""Class for the background rates data in Trigdat.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
rec_array (np.recarray): The FITS BCKRATES record array from the
|
||||||
|
trigdat file
|
||||||
|
Attributes:
|
||||||
|
all_rates (np.array): An array (:attr:`numchans`, :attr:`numdets`) of
|
||||||
|
the maxrates
|
||||||
|
numchans (int): The number of energy channels
|
||||||
|
numdets (int): The number of detectors
|
||||||
|
quality (int, int): The quality flags for the background
|
||||||
|
time_range (float, float): The time range of the maxrates
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, rec_array):
|
||||||
|
self._time_range = (rec_array['TIME'], rec_array['ENDTIME'])
|
||||||
|
self._quality = rec_array['QUALITY']
|
||||||
|
self._rates = rec_array['BCKRATES']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def numchans(self):
|
||||||
|
return self._rates.shape[0]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def numdets(self):
|
||||||
|
return self._rates.shape[1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_range(self):
|
||||||
|
return self._time_range
|
||||||
|
|
||||||
|
@property
|
||||||
|
def quality(self):
|
||||||
|
return self._quality
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_rates(self):
|
||||||
|
return self._rates
|
||||||
|
|
||||||
|
def get_detector(self, det):
|
||||||
|
"""Retrieve the background rates for a detector
|
||||||
|
|
||||||
|
Args:
|
||||||
|
det (str): The detector
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: An array of size (:attr:`numchans`,) of background rates \
|
||||||
|
for the detector
|
||||||
|
"""
|
||||||
|
mask = (np.array(self._detectors) == det)
|
||||||
|
return self._rates[:, mask].squeeze()
|
||||||
|
|
||||||
|
|
||||||
|
class FswLocation:
|
||||||
|
"""Class for the flight software localization info
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
rec_array (np.recarray): The FITS OB_CALC record array from the
|
||||||
|
trigdat file
|
||||||
|
Attributes:
|
||||||
|
fluence (float): The fluence of the localization interval
|
||||||
|
hardness_ratio (float): The hardness ratio for the localization interval
|
||||||
|
intensity (float): The brightness of the signal
|
||||||
|
location (float, float, float): The RA, Dec, and statistical error of
|
||||||
|
the onboard localization
|
||||||
|
location_sc (float): The localization in spacecraft coordinates:
|
||||||
|
Azimuth, Zenith
|
||||||
|
next_classification (str, float):
|
||||||
|
The next most likely classification of the trigger and the probability
|
||||||
|
significance (float): The S/N ratio of the localization interval
|
||||||
|
spectrum (str): The spectrum used in the localization
|
||||||
|
time (float): Time at which the localization was calculated
|
||||||
|
timescale (float): The localization interval timescale
|
||||||
|
top_classification (str, float):
|
||||||
|
The most likely classification of the trigger and the probability
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, rec_array):
|
||||||
|
self._time = rec_array['TIME']
|
||||||
|
self._location = (
|
||||||
|
rec_array['RA'], rec_array['DEC'], rec_array['STATERR'])
|
||||||
|
self._algorithm = spectrum[rec_array['LOCALG'] - 1]
|
||||||
|
self._class1 = (classifications[rec_array['EVTCLASS'][0]],
|
||||||
|
rec_array['RELIABLT'][0])
|
||||||
|
self._class2 = (classifications[rec_array['EVTCLASS'][1]],
|
||||||
|
rec_array['RELIABLT'][1])
|
||||||
|
self._intensity = rec_array['INTNSITY']
|
||||||
|
self._hardness = rec_array['HDRATIO']
|
||||||
|
self._fluence = rec_array['FLUENCE']
|
||||||
|
self._sigma = rec_array['SIGMA']
|
||||||
|
self._timescale = rec_array['TRIG_TS']
|
||||||
|
self._azzen = (rec_array['TR_SCAZ'], rec_array['TR_SCZEN'])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time(self):
|
||||||
|
return self._time
|
||||||
|
|
||||||
|
@property
|
||||||
|
def location(self):
|
||||||
|
return self._location
|
||||||
|
|
||||||
|
@property
|
||||||
|
def spectrum(self):
|
||||||
|
return self._algorithm
|
||||||
|
|
||||||
|
@property
|
||||||
|
def top_classification(self):
|
||||||
|
return self._class1
|
||||||
|
|
||||||
|
@property
|
||||||
|
def next_classification(self):
|
||||||
|
return self._class2
|
||||||
|
|
||||||
|
@property
|
||||||
|
def intensity(self):
|
||||||
|
return self._intensity
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hardness_ratio(self):
|
||||||
|
return self._hardness
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fluence(self):
|
||||||
|
return self._fluence
|
||||||
|
|
||||||
|
@property
|
||||||
|
def significance(self):
|
||||||
|
return self._sigma
|
||||||
|
|
||||||
|
@property
|
||||||
|
def timescale(self):
|
||||||
|
return self._timescale
|
||||||
|
|
||||||
|
@property
|
||||||
|
def location_sc(self):
|
||||||
|
return self._azzen
|
135
detectors.py
Normal file
135
detectors.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
# detectors.py: Module containing the GBM detector definitions
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
class Detector(Enum):
|
||||||
|
"""The GBM detector names and pointings.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
pointing (float, float): The spacecraft Azimuth/Zenith pointing
|
||||||
|
short_name (str): The short name of the detector (e.g. NaI 0 -> 'n0')
|
||||||
|
"""
|
||||||
|
N0 = ('NAI_00', 0, 45.89, 20.58)
|
||||||
|
N1 = ('NAI_01', 1, 45.11, 45.31)
|
||||||
|
N2 = ('NAI_02', 2, 58.44, 90.21)
|
||||||
|
N3 = ('NAI_03', 3, 314.87, 45.24)
|
||||||
|
N4 = ('NAI_04', 4, 303.15, 90.27)
|
||||||
|
N5 = ('NAI_05', 5, 3.35, 89.79)
|
||||||
|
N6 = ('NAI_06', 6, 224.93, 20.43)
|
||||||
|
N7 = ('NAI_07', 7, 224.62, 46.18)
|
||||||
|
N8 = ('NAI_08', 8, 236.61, 89.97)
|
||||||
|
N9 = ('NAI_09', 9, 135.19, 45.55)
|
||||||
|
NA = ('NAI_10', 10, 123.73, 90.42)
|
||||||
|
NB = ('NAI_11', 11, 183.74, 90.32)
|
||||||
|
B0 = ('BGO_00', 12, 0.00, 90.00)
|
||||||
|
B1 = ('BGO_01', 13, 180.00, 90.00)
|
||||||
|
|
||||||
|
def __init__(self, long_name, number, azimuth, zenith):
|
||||||
|
self.long_name = long_name
|
||||||
|
self.number = number
|
||||||
|
self.azimuth = azimuth
|
||||||
|
self.zenith = zenith
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "Detector(\"{}\", \"{}\", {})".format(self.name, self.long_name,
|
||||||
|
self.number)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str.lower(self.name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def short_name(self):
|
||||||
|
return self.__str__()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pointing(self):
|
||||||
|
return self.azimuth, self.zenith
|
||||||
|
|
||||||
|
def is_nai(self):
|
||||||
|
"""Check if detector is an NaI
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if detector is NaI, False otherwise.
|
||||||
|
"""
|
||||||
|
return self.name[0] == 'N'
|
||||||
|
|
||||||
|
def is_bgo(self):
|
||||||
|
"""Check if detector is a BGO
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if detector is BGO, False otherwise.
|
||||||
|
"""
|
||||||
|
return self.name[0] == 'B'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_str(cls, value):
|
||||||
|
"""Create a Detector from a short string name (e.g. 'n0')
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (str): The short name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`Detector`: The detector enum
|
||||||
|
"""
|
||||||
|
if value.upper() in cls.__members__:
|
||||||
|
return cls[value.upper()]
|
||||||
|
# TODO: Reconsider returning None
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_num(cls, num):
|
||||||
|
"""Create a Detector from an index number
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num (int): The index number
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`Detector`: The detector enum
|
||||||
|
"""
|
||||||
|
for d in cls:
|
||||||
|
if d.number == num:
|
||||||
|
return d
|
||||||
|
# TODO: Reconsider returning None
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def nai(cls):
|
||||||
|
"""Get all detectors that are NaIs
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list of :class:`Detector`: The NaI detectors
|
||||||
|
"""
|
||||||
|
return [x for x in cls if x.is_nai()]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def bgo(cls):
|
||||||
|
"""Get all detectors that are BGOs
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list of :class:`Detector`: The BGO detectors
|
||||||
|
"""
|
||||||
|
return [x for x in Detector if x.is_bgo()]
|
358
file.py
Normal file
358
file.py
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
# file.py: Module containing GBM filenaming convention and operations
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import datetime
|
||||||
|
import os.path
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .detectors import Detector
|
||||||
|
from .time import Met
|
||||||
|
|
||||||
|
|
||||||
|
class GbmFile:
|
||||||
|
"""Parse or construct a GBM standardized filename.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
data_type (str): The datatype of the file
|
||||||
|
detector (str): The detector with which the file is associated
|
||||||
|
directory (str): The directory hosting the file
|
||||||
|
extension (str): The filename extension
|
||||||
|
meta (str): Additional metadata in the filename
|
||||||
|
trigger (bool): True if the file is from a trigger. False otherwise
|
||||||
|
uid (str): The unique id of the file
|
||||||
|
version (int): The version number of the file
|
||||||
|
"""
|
||||||
|
REGEX_PATTERN = r'^glg_(?P<data_type>.+)_(?P<detector>[bn][0-9ab]|all)_(?P<trigger>(?:bn)?)(?P<uid>(?:\d{9}|\d{6}' \
|
||||||
|
r'_\d\dz|\d{6}))(?P<meta>(?:_.+)?)_v(?P<version>\d\d)\.(?P<extension>.+)$'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.directory = ''
|
||||||
|
self.trigger = False
|
||||||
|
self.data_type = None
|
||||||
|
self._detector = None
|
||||||
|
self.uid = None
|
||||||
|
self.meta = None
|
||||||
|
self.version = 0
|
||||||
|
self.extension = 'fit'
|
||||||
|
|
||||||
|
def _init_by_dict(self, values):
|
||||||
|
for key, val in values.items():
|
||||||
|
# handle properties differently
|
||||||
|
try:
|
||||||
|
p = getattr(self, key)
|
||||||
|
if isinstance(p, property):
|
||||||
|
p.__set__(self, val)
|
||||||
|
else:
|
||||||
|
self.__setattr__(key, val)
|
||||||
|
except AttributeError:
|
||||||
|
raise ValueError("{} is not a valid attribute.".format(key))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def detector(self):
|
||||||
|
if not self._detector:
|
||||||
|
return 'all'
|
||||||
|
return self._detector
|
||||||
|
|
||||||
|
@detector.setter
|
||||||
|
def detector(self, value):
|
||||||
|
if value == 'all':
|
||||||
|
self._detector = None
|
||||||
|
elif isinstance(value, Detector):
|
||||||
|
self._detector = value
|
||||||
|
else:
|
||||||
|
if isinstance(value, str):
|
||||||
|
d = Detector.from_str(value)
|
||||||
|
self._detector = d if d else value
|
||||||
|
elif isinstance(value, int):
|
||||||
|
d = Detector.from_num(value)
|
||||||
|
if d:
|
||||||
|
self._detector = d
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid detector value")
|
||||||
|
|
||||||
|
def version_str(self):
|
||||||
|
"""Return the file version number as a string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The file version
|
||||||
|
"""
|
||||||
|
if isinstance(self.version, int):
|
||||||
|
v = "{:02d}".format(self.version)
|
||||||
|
else:
|
||||||
|
v = self.version
|
||||||
|
return v
|
||||||
|
|
||||||
|
def basename(self):
|
||||||
|
"""The file basename
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The file basename
|
||||||
|
"""
|
||||||
|
if self.trigger:
|
||||||
|
u = 'bn' + self.uid
|
||||||
|
else:
|
||||||
|
u = self.uid
|
||||||
|
|
||||||
|
if self.meta:
|
||||||
|
return str.format("glg_{}_{}_{}{}_v{}.{}",
|
||||||
|
self.data_type, self.detector, u, self.meta,
|
||||||
|
self.version_str(), self.extension)
|
||||||
|
|
||||||
|
return str.format("glg_{}_{}_{}_v{}.{}",
|
||||||
|
self.data_type, self.detector, u, self.version_str(),
|
||||||
|
self.extension)
|
||||||
|
|
||||||
|
def path(self):
|
||||||
|
"""The file path
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The path
|
||||||
|
"""
|
||||||
|
return os.path.join(self.directory, self.basename())
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.path()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.path()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, **kwargs):
|
||||||
|
"""Create a GbmFile from keywords
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: The properties of a GbmFile
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`GbmFile`: The new filename object
|
||||||
|
"""
|
||||||
|
obj = cls()
|
||||||
|
obj._init_by_dict(kwargs)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_path(cls, path):
|
||||||
|
"""Create a GbmFile from parsing a filename
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): A filename path
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`GbmFile`: The new filename object
|
||||||
|
"""
|
||||||
|
m = re.match(cls.REGEX_PATTERN, os.path.basename(path), re.I | re.S)
|
||||||
|
|
||||||
|
result = None
|
||||||
|
if m:
|
||||||
|
result = cls.create(**m.groupdict())
|
||||||
|
result.directory = os.path.dirname(path)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def detector_list(self):
|
||||||
|
"""Generate a list of GbmFile objects, one for each GBM detector
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list of :class:`GbmFile`: The new filename objects
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
for d in Detector:
|
||||||
|
x = copy.copy(self)
|
||||||
|
x.detector = d
|
||||||
|
result.append(x)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def list_from_paths(cls, path_list, unknown=None):
|
||||||
|
"""Create a many GbmFiles from a list of filepaths
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path_list (list of str): List of filepaths
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list of :class:`GbmFile`: The new filename object(s)
|
||||||
|
"""
|
||||||
|
result = []
|
||||||
|
for p in path_list:
|
||||||
|
f = GbmFile.from_path(p)
|
||||||
|
if f:
|
||||||
|
result.append(f)
|
||||||
|
else:
|
||||||
|
if unknown is not None:
|
||||||
|
unknown.append(p)
|
||||||
|
else:
|
||||||
|
raise ValueError('Unrecognized file name')
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def scan_dir(path, hidden=False, recursive=False, absolute=False, regex=None):
|
||||||
|
"""
|
||||||
|
Scans the given directory for files.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): The root directory to scan.
|
||||||
|
hidden (bool, optional): Set True if you want to include hidden files.
|
||||||
|
recursive (bool, optional): Set True if you want to scan subdirectories
|
||||||
|
within the given path.
|
||||||
|
absolute (bool, optional): Set true if you want the absolute path of
|
||||||
|
each file returned.
|
||||||
|
regex (str): Set if you want to only return files matching the given
|
||||||
|
regular expression.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
str: Full path to a file for each iteration.
|
||||||
|
"""
|
||||||
|
for f in os.listdir(path):
|
||||||
|
if not hidden:
|
||||||
|
if f.startswith('.'):
|
||||||
|
continue
|
||||||
|
file_path = os.path.join(path, f)
|
||||||
|
if absolute:
|
||||||
|
file_path = os.path.abspath(file_path)
|
||||||
|
if os.path.isfile(file_path):
|
||||||
|
if regex and re.search(regex, f) is None:
|
||||||
|
continue
|
||||||
|
yield file_path
|
||||||
|
elif recursive:
|
||||||
|
yield from scan_dir(file_path, hidden, recursive, absolute, regex)
|
||||||
|
|
||||||
|
|
||||||
|
def all_exists(file_list, parent_dir=None):
|
||||||
|
"""
|
||||||
|
Do all the files in the list exist in the filesystem?
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_list (list of str): List of file names to check
|
||||||
|
parent_dir (str, optional): parent directory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if all files exist
|
||||||
|
"""
|
||||||
|
if not file_list:
|
||||||
|
return False
|
||||||
|
for f in file_list:
|
||||||
|
if parent_dir is not None:
|
||||||
|
path = os.path.join(parent_dir, f.basename())
|
||||||
|
else:
|
||||||
|
path = str(f)
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def has_detector(file_list, detector):
|
||||||
|
"""
|
||||||
|
Does the file list contain a file for the given detector?
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_list (list of str): List of file names
|
||||||
|
detector (str): Detector being searched
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the list of file names includes the given detector
|
||||||
|
"""
|
||||||
|
for f in file_list:
|
||||||
|
if f.detector == detector:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def is_complete(file_list):
|
||||||
|
"""
|
||||||
|
Does the file list contain a file for every detector?
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_list (list of str): List of files that represent a detector set
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the file list contains a file for every detector
|
||||||
|
"""
|
||||||
|
for d in Detector:
|
||||||
|
if not has_detector(file_list, d):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def max_version(file_list):
|
||||||
|
"""
|
||||||
|
Returns the maximum _version of file name in the given list
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_list (list of str): list of file names
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Largest _version number in the list
|
||||||
|
"""
|
||||||
|
result = None
|
||||||
|
for f in file_list:
|
||||||
|
try:
|
||||||
|
v = int(f.version)
|
||||||
|
if result is None or v > result:
|
||||||
|
result = v
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def min_version(file_list):
|
||||||
|
"""
|
||||||
|
Returns the minimum _version of file name in the given list
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_list (list of str): list of file names
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Smallest _version number in the list
|
||||||
|
"""
|
||||||
|
result = None
|
||||||
|
for f in file_list:
|
||||||
|
try:
|
||||||
|
v = int(f.version)
|
||||||
|
if result is None or v < result:
|
||||||
|
result = v
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def ymd_path(base, name):
|
||||||
|
if isinstance(name, str) or isinstance(name, GbmFile):
|
||||||
|
v = name if isinstance(name, str) else name.basename()
|
||||||
|
m = re.match(r'.*_(?:(?:bn)?)(\d{6})(?:(\d{3})|(_\d\d)?)_.*', v,
|
||||||
|
re.I | re.S)
|
||||||
|
if m:
|
||||||
|
d = datetime.datetime.strptime(m.group(1), "%y%m%d")
|
||||||
|
return os.path.join(base, d.strftime('%Y-%m-%d'),
|
||||||
|
os.path.basename(name))
|
||||||
|
elif isinstance(name, Met):
|
||||||
|
return os.path.join(base, name.datetime.strftime('%Y-%m-%d'))
|
||||||
|
elif isinstance(name, datetime.datetime) or isinstance(name,
|
||||||
|
datetime.date):
|
||||||
|
return os.path.join(base, name.strftime('%Y-%m-%d'))
|
||||||
|
raise ValueError("Can't parse a YMD value")
|
0
lookup/__init__.py
Normal file
0
lookup/__init__.py
Normal file
227
lookup/apply.py
Normal file
227
lookup/apply.py
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
# apply.py: Module contain functions to apply actions from lookups
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from ..data.phaii import PHAII, TTE
|
||||||
|
from ..background.background import BackgroundFitter
|
||||||
|
|
||||||
|
|
||||||
|
def rebin_time(data_obj, data_lookup):
|
||||||
|
"""Temporal rebin based on a lookup
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
data_obj: PHAII or TTE
|
||||||
|
The data to rebin
|
||||||
|
data_lookup: DataFileLookup
|
||||||
|
The lookup for the data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
-----------
|
||||||
|
phaii: PHAII
|
||||||
|
The rebinned PHAII object
|
||||||
|
"""
|
||||||
|
if isinstance(data_obj, TTE):
|
||||||
|
func = data_obj.to_phaii
|
||||||
|
elif isinstance(data_obj, PHAII):
|
||||||
|
func = data_obj.rebin_time
|
||||||
|
else:
|
||||||
|
raise TypeError('Data must either be PHAII or TTE')
|
||||||
|
|
||||||
|
binnings = data_lookup.binnings.time
|
||||||
|
if binnings is None:
|
||||||
|
return data_obj
|
||||||
|
|
||||||
|
for binning in binnings:
|
||||||
|
phaii = func(binning.method, *binning.args, time_range=(binning.start,
|
||||||
|
binning.stop))
|
||||||
|
func = phaii.rebin_time
|
||||||
|
return phaii
|
||||||
|
|
||||||
|
|
||||||
|
def rebin_energy(data_obj, data_lookup):
|
||||||
|
"""Energy rebin based on a lookup
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
data_obj: PHAII or TTE
|
||||||
|
The data to rebin
|
||||||
|
data_lookup: DataFileLookup
|
||||||
|
The lookup for the data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
-----------
|
||||||
|
phaii: PHAII
|
||||||
|
The rebinned PHAII object
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
func = data_obj.rebin_energy
|
||||||
|
except:
|
||||||
|
raise TypeError('Not a valid data object for energy rebinning')
|
||||||
|
|
||||||
|
binnings = data_lookup.binnings.energy
|
||||||
|
if binnings is None:
|
||||||
|
return data_obj
|
||||||
|
|
||||||
|
for binning in binnings:
|
||||||
|
phaii = func(binning.method, *binning.args, emin=binning.start,
|
||||||
|
emax=binning.stop)
|
||||||
|
func = phaii.rebin_time
|
||||||
|
return phaii
|
||||||
|
|
||||||
|
|
||||||
|
def source_selection(data_obj, data_lookup):
|
||||||
|
"""Source selection (temporal) based on a lookup
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
data_obj: PHAII or TTE
|
||||||
|
The data to rebin
|
||||||
|
data_lookup: DataFileLookup
|
||||||
|
The lookup for the data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
-----------
|
||||||
|
new_obj: PHAII or TTE
|
||||||
|
A new data object containing the source selection
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
func = data_obj.slice_time
|
||||||
|
except:
|
||||||
|
raise TypeError('Not a valid data object for source selection')
|
||||||
|
|
||||||
|
new_obj = func(data_lookup.selections.source)
|
||||||
|
return new_obj
|
||||||
|
|
||||||
|
|
||||||
|
def energy_selection(data_obj, data_lookup):
|
||||||
|
"""Energy selection based on a lookup
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
data_obj: PHAII or TTE
|
||||||
|
The data to rebin
|
||||||
|
data_lookup: DataFileLookup
|
||||||
|
The lookup for the data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
-----------
|
||||||
|
new_obj: PHAII or TTE
|
||||||
|
A new data object containing the energy selection
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
func = data_obj.slice_energy
|
||||||
|
except:
|
||||||
|
raise TypeError('Not a valid data object for energy selection')
|
||||||
|
|
||||||
|
new_obj = func(data_lookup.selections.energy)
|
||||||
|
return new_obj
|
||||||
|
|
||||||
|
|
||||||
|
def background_selection(data_obj, data_lookup):
|
||||||
|
"""Background selection (temporal) based on a lookup
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
data_obj: PHAII or TTE
|
||||||
|
The data to rebin
|
||||||
|
data_lookup: DataFileLookup
|
||||||
|
The lookup for the data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
-----------
|
||||||
|
new_obj: PHAII or TTE
|
||||||
|
A new data object containing the background selection
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
func = data_obj.slice_time
|
||||||
|
except:
|
||||||
|
raise TypeError('Not a valid data object for background selection')
|
||||||
|
|
||||||
|
new_obj = func(data_lookup.selections.background)
|
||||||
|
return new_obj
|
||||||
|
|
||||||
|
|
||||||
|
def background_fit(data_obj, data_lookup):
|
||||||
|
"""Perform a fit of the background based on a lookup
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
data_obj: PHAII or TTE
|
||||||
|
The data to rebin
|
||||||
|
data_lookup: DataFileLookup
|
||||||
|
The lookup for the data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
-----------
|
||||||
|
fitter: BackgroundFitter
|
||||||
|
The fitter class containing the fitted background model
|
||||||
|
"""
|
||||||
|
if isinstance(data_obj, PHAII):
|
||||||
|
fitter_func = BackgroundFitter.from_phaii
|
||||||
|
elif isinstance(data_obj, TTE):
|
||||||
|
fitter_func = BackgroundFitter.from_tte
|
||||||
|
else:
|
||||||
|
raise TypeError('Not a valid data object for background fitting')
|
||||||
|
|
||||||
|
fitter = fitter_func(data_obj, data_lookup.background.method,
|
||||||
|
time_ranges=data_lookup.selections.background)
|
||||||
|
fitter.fit(*data_lookup.background.args, **data_lookup.background.kwargs)
|
||||||
|
|
||||||
|
return fitter
|
||||||
|
|
||||||
|
|
||||||
|
def lightcurve_view(plot_obj, data_lookup):
|
||||||
|
"""Set the view range of the lightcurve based on a lookup
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
plot_obj: Lightcurve
|
||||||
|
The lightcurve plot object
|
||||||
|
data_lookup: DataFileLookup
|
||||||
|
The lookup for the data
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
plot_obj.xlim = data_lookup.views.time.xrange()
|
||||||
|
plot_obj.ylim = data_lookup.views.time.yrange()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def spectrum_view(plot_obj, data_lookup):
|
||||||
|
"""Set the view range of the spectrum based on a lookup
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
plot_obj: Spectrum
|
||||||
|
The spectrum plot object
|
||||||
|
data_lookup: DataFileLookup
|
||||||
|
The lookup for the data
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
plot_obj.xlim = data_lookup.views.energy.xrange()
|
||||||
|
plot_obj.ylim = data_lookup.views.energy.yrange()
|
||||||
|
except:
|
||||||
|
pass
|
697
lookup/lookup.py
Normal file
697
lookup/lookup.py
Normal file
@ -0,0 +1,697 @@
|
|||||||
|
# lookup.py: GSpec and RMfit lookup classes
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import datetime as dt
|
||||||
|
import json
|
||||||
|
import os.path
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from gbm.detectors import Detector
|
||||||
|
from gbm.file import GbmFile
|
||||||
|
from gbm.types import ListReader
|
||||||
|
|
||||||
|
|
||||||
|
class LookupMethod:
|
||||||
|
"""Defines the attributes of a method call"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.method = None
|
||||||
|
self.args = None
|
||||||
|
self.kwargs = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, d):
|
||||||
|
r = cls()
|
||||||
|
r.method = d.get('method', None)
|
||||||
|
r.args = tuple(d.get('args', None))
|
||||||
|
r.kwargs = d.get('kwargs', {})
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
class LookupBackground(LookupMethod):
|
||||||
|
"""Defines the attributes of a background binning method"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(LookupBackground, self).__init__()
|
||||||
|
self.datatype = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, d):
|
||||||
|
r = super(LookupBackground, cls).from_dict(d)
|
||||||
|
r.datatype = d.get('datatype', None)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
class LookupEnergyBinning(LookupMethod):
|
||||||
|
"""Defines the attributes of an energy binning method"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(LookupEnergyBinning, self).__init__()
|
||||||
|
self.start = None
|
||||||
|
self.stop = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, d):
|
||||||
|
r = super(LookupEnergyBinning, cls).from_dict(d)
|
||||||
|
r.start = d.get('start', None)
|
||||||
|
r.stop = d.get('stop', None)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
class LookupTimeBinning(LookupEnergyBinning):
|
||||||
|
"""Defines the attributes of a time binning method"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(LookupTimeBinning, self).__init__()
|
||||||
|
self.datatype = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, d):
|
||||||
|
r = super(LookupTimeBinning, cls).from_dict(d)
|
||||||
|
r.datatype = d.get('datatype', None)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
class View:
|
||||||
|
"""Defines the bounds of a view"""
|
||||||
|
|
||||||
|
def __init__(self, xmin=None, xmax=None, ymin=None, ymax=None):
|
||||||
|
self.xmin = xmin
|
||||||
|
self.xmax = xmax
|
||||||
|
self.ymin = ymin
|
||||||
|
self.ymax = ymax
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.xmin == other.xmin and self.xmax == other.xmax and self.ymin == other.ymin \
|
||||||
|
and self.ymax == other.ymax
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, d):
|
||||||
|
r = cls()
|
||||||
|
if d:
|
||||||
|
r.xmin = d.get('xmin', None)
|
||||||
|
r.xmax = d.get('xmax', None)
|
||||||
|
r.ymin = d.get('ymin', None)
|
||||||
|
r.ymax = d.get('ymax', None)
|
||||||
|
return r
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_list(cls, l):
|
||||||
|
r = cls()
|
||||||
|
r.xmin = l[0]
|
||||||
|
r.xmax = l[1]
|
||||||
|
r.ymin = l[2]
|
||||||
|
r.ymax = l[3]
|
||||||
|
return r
|
||||||
|
|
||||||
|
def to_list(self):
|
||||||
|
return [self.xmin, self.xmax, self.ymin, self.ymax]
|
||||||
|
|
||||||
|
def xrange(self):
|
||||||
|
return self.xmin, self.xmax
|
||||||
|
|
||||||
|
def yrange(self):
|
||||||
|
return self.ymin, self.ymax
|
||||||
|
|
||||||
|
|
||||||
|
class Binnings:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.energy = None
|
||||||
|
self.time = None
|
||||||
|
|
||||||
|
|
||||||
|
class Selections:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.background = None
|
||||||
|
self.energy = None
|
||||||
|
self.source = None
|
||||||
|
|
||||||
|
def add(self, type, item):
|
||||||
|
if getattr(self, type) is None:
|
||||||
|
setattr(self, type, list())
|
||||||
|
getattr(self, type).append(item)
|
||||||
|
|
||||||
|
|
||||||
|
class Views:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.energy = None
|
||||||
|
self.time = None
|
||||||
|
|
||||||
|
|
||||||
|
class DataFileLookup:
|
||||||
|
"""Defines all the information associated with a datafile"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.filename = None
|
||||||
|
self.detector = None
|
||||||
|
self.response = None
|
||||||
|
self.background = None
|
||||||
|
self.binnings = Binnings()
|
||||||
|
self.selections = Selections()
|
||||||
|
self.views = Views()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, d):
|
||||||
|
|
||||||
|
def set_attributes(obj, d):
|
||||||
|
for k, v in d.items():
|
||||||
|
setattr(obj, k, v)
|
||||||
|
|
||||||
|
r = cls()
|
||||||
|
r.filename = d.get('filename', None)
|
||||||
|
|
||||||
|
det = d.get('detector', None)
|
||||||
|
if det:
|
||||||
|
r.detector = Detector.from_str(det)
|
||||||
|
|
||||||
|
r.response = d.get('response', None)
|
||||||
|
|
||||||
|
bkg = d.get('background', None)
|
||||||
|
if bkg:
|
||||||
|
r.background = LookupBackground.from_dict(bkg)
|
||||||
|
|
||||||
|
binnings = d.get('binnings', None)
|
||||||
|
if binnings:
|
||||||
|
energies = binnings.get('energy', None)
|
||||||
|
if energies:
|
||||||
|
for e in energies:
|
||||||
|
if r.binnings.energy is None:
|
||||||
|
r.binnings.energy = [LookupEnergyBinning.from_dict(e)]
|
||||||
|
else:
|
||||||
|
r.binnings.energy.append(
|
||||||
|
LookupEnergyBinning.from_dict(e))
|
||||||
|
times = binnings.get('time', None)
|
||||||
|
if times:
|
||||||
|
for t in times:
|
||||||
|
if r.binnings.time is None:
|
||||||
|
r.binnings.time = [LookupTimeBinning.from_dict(t)]
|
||||||
|
else:
|
||||||
|
r.binnings.time.append(LookupTimeBinning.from_dict(t))
|
||||||
|
|
||||||
|
if 'selections' in d:
|
||||||
|
set_attributes(r.selections, d['selections'])
|
||||||
|
|
||||||
|
views = d.get('views', None)
|
||||||
|
if views:
|
||||||
|
e = views.get('energy', None)
|
||||||
|
if e:
|
||||||
|
r.views.energy = View.from_dict(e)
|
||||||
|
t = views.get('time', None)
|
||||||
|
if t:
|
||||||
|
r.views.time = View.from_dict(t)
|
||||||
|
return r
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def assert_selections(selections):
|
||||||
|
"""Check to ensure the selections are of the correct form.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
selections: tuple or list of tuples
|
||||||
|
The selection(s) to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
--------
|
||||||
|
selections: list
|
||||||
|
"""
|
||||||
|
if (all(isinstance(selection, list) for selection in selections)) | \
|
||||||
|
(
|
||||||
|
all(isinstance(selection, tuple) for selection in selections)):
|
||||||
|
if any(len(selection) != 2 for selection in selections):
|
||||||
|
raise ValueError('Each range in selections must be of the '
|
||||||
|
'form (lo, hi)')
|
||||||
|
else:
|
||||||
|
return selections
|
||||||
|
else:
|
||||||
|
if len(selections) != 2:
|
||||||
|
raise ValueError('Selections must either be a range of '
|
||||||
|
'the form (lo, hi) or a list of ranges')
|
||||||
|
else:
|
||||||
|
return [selections]
|
||||||
|
|
||||||
|
def set_response(self, rsp_filename):
|
||||||
|
"""Add a response file for the data
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
--------------
|
||||||
|
rsp_filename: str
|
||||||
|
The filename of the response file
|
||||||
|
"""
|
||||||
|
if rsp_filename is None:
|
||||||
|
self.response = None
|
||||||
|
else:
|
||||||
|
self.response = os.path.basename(rsp_filename)
|
||||||
|
|
||||||
|
def set_background_model(self, background_name, datatype, *args, **kwargs):
|
||||||
|
"""Add a new background model for the data file
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
--------------
|
||||||
|
background_class: str
|
||||||
|
The background fitting/estimation name
|
||||||
|
datatype: str
|
||||||
|
The datatype the background is applied to. Either 'binned' or 'unbinned'
|
||||||
|
*args:
|
||||||
|
Additional arguments used by the background class
|
||||||
|
**kwargs:
|
||||||
|
Additional keywords used by the background class
|
||||||
|
"""
|
||||||
|
bkg = LookupBackground()
|
||||||
|
bkg.method = background_name
|
||||||
|
bkg.datatype = datatype
|
||||||
|
bkg.args = args
|
||||||
|
bkg.kwargs = kwargs
|
||||||
|
self.background = bkg
|
||||||
|
|
||||||
|
def set_time_binning(self, binning_name, datatype, *args, start=None,
|
||||||
|
stop=None, **kwargs):
|
||||||
|
"""Add a new time binning function for the data file
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
--------------
|
||||||
|
binning_function: str
|
||||||
|
The binning function name
|
||||||
|
datatype: str
|
||||||
|
The datatype the binning is applied to. Either 'binned' or 'unbinned'
|
||||||
|
*args:
|
||||||
|
Additional arguments used by the binning function
|
||||||
|
start: float, optional
|
||||||
|
The start of the data range to be rebinned. The default is to start at the
|
||||||
|
beginning of the histogram segment.
|
||||||
|
stop: float, optional
|
||||||
|
The end of the data range to be rebinned. The default is to stop at
|
||||||
|
the end of the histogram segment.
|
||||||
|
**kwargs:
|
||||||
|
Additional keywords used by the binning function
|
||||||
|
"""
|
||||||
|
time_bin = LookupTimeBinning()
|
||||||
|
time_bin.method = binning_name
|
||||||
|
time_bin.datatype = datatype
|
||||||
|
time_bin.args = args
|
||||||
|
time_bin.start = start
|
||||||
|
time_bin.stop = stop
|
||||||
|
time_bin.kwargs = kwargs
|
||||||
|
|
||||||
|
if self.binnings.time is None:
|
||||||
|
self.binnings.time = [time_bin]
|
||||||
|
else:
|
||||||
|
self.binnings.time.append(time_bin)
|
||||||
|
|
||||||
|
def set_energy_binning(self, binning_function, *args, start=None,
|
||||||
|
stop=None, **kwargs):
|
||||||
|
"""Add a new energy binning function for the data file
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
--------------
|
||||||
|
binning_function: function
|
||||||
|
The binning function
|
||||||
|
*args:
|
||||||
|
Additional arguments used by the binning function
|
||||||
|
start: float, optional
|
||||||
|
The start of the data range to be rebinned. The default is to start at the
|
||||||
|
beginning of the histogram segment.
|
||||||
|
stop: float, optional
|
||||||
|
The end of the data range to be rebinned. The default is to stop at
|
||||||
|
the end of the histogram segment.
|
||||||
|
**kwargs:
|
||||||
|
Additional keywords used by the binning function
|
||||||
|
"""
|
||||||
|
energy_bin = LookupEnergyBinning()
|
||||||
|
energy_bin.method = binning_function
|
||||||
|
energy_bin.args = args
|
||||||
|
energy_bin.start = start
|
||||||
|
energy_bin.stop = stop
|
||||||
|
energy_bin.kwargs = kwargs
|
||||||
|
|
||||||
|
if self.binnings.energy is None:
|
||||||
|
self.binnings.energy = [energy_bin]
|
||||||
|
else:
|
||||||
|
self.binnings.energy.append(energy_bin)
|
||||||
|
|
||||||
|
def set_source_selection(self, source_intervals):
|
||||||
|
"""Add source selection(s) for the data file
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
--------------
|
||||||
|
dataname: str
|
||||||
|
The data filename
|
||||||
|
source_intervals: list
|
||||||
|
A list of source selection intervals, each item of the list being a tuple
|
||||||
|
of the format (low, high)
|
||||||
|
"""
|
||||||
|
source_intervals = self.assert_selections(source_intervals)
|
||||||
|
self.selections.source = source_intervals
|
||||||
|
|
||||||
|
def set_energy_selection(self, energy_intervals):
|
||||||
|
"""Add energy selection(s) for the data file
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
--------------
|
||||||
|
energy_intervals: list
|
||||||
|
A list of energy selection intervals, each item of the list being a tuple
|
||||||
|
of the format (low, high)
|
||||||
|
"""
|
||||||
|
energy_intervals = self.assert_selections(energy_intervals)
|
||||||
|
self.selections.energy = energy_intervals
|
||||||
|
|
||||||
|
def set_background_selection(self, background_intervals):
|
||||||
|
"""Add background selection(s) for the data file
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
--------------
|
||||||
|
background_intervals: list
|
||||||
|
A list of background selection intervals, each item of the list being a tuple
|
||||||
|
of the format (low, high)
|
||||||
|
"""
|
||||||
|
self.selections.background = background_intervals
|
||||||
|
|
||||||
|
def add_time_display_view(self, display_range):
|
||||||
|
"""Add the display range of the lightcurve for the data file
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
--------------
|
||||||
|
display_range: list
|
||||||
|
The values of the lightcurve display window in the format
|
||||||
|
[xmin, xmax, ymin, ymax]
|
||||||
|
"""
|
||||||
|
self.views.time = View(display_range[0], display_range[1],
|
||||||
|
display_range[2], display_range[3])
|
||||||
|
|
||||||
|
def add_energy_display_view(self, display_range):
|
||||||
|
"""Add the display range of the count spectrum for the data file
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
--------------
|
||||||
|
dataname: str
|
||||||
|
The data filename
|
||||||
|
display_range: list
|
||||||
|
The values of the count spectrum display window in the format
|
||||||
|
[xmin, xmax, ymin, ymax]
|
||||||
|
"""
|
||||||
|
self.views.energy = View(display_range[0], display_range[1],
|
||||||
|
display_range[2], display_range[3])
|
||||||
|
|
||||||
|
|
||||||
|
class LookupFile:
|
||||||
|
"""Class for an Gspec lookup file
|
||||||
|
|
||||||
|
The lookup file contains one or more data files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.file_date = None
|
||||||
|
self.datafiles = dict()
|
||||||
|
|
||||||
|
def __getitem__(self, name):
|
||||||
|
return self.datafiles[name]
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
del self.datafiles[key]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
if isinstance(value, DataFileLookup):
|
||||||
|
self.datafiles[key] = value
|
||||||
|
else:
|
||||||
|
raise ValueError("not a DataFile")
|
||||||
|
|
||||||
|
def files(self):
|
||||||
|
"""Return the data filenames contained within the lookup"""
|
||||||
|
return self.datafiles.keys()
|
||||||
|
|
||||||
|
def assert_has_datafile(self, dataname):
|
||||||
|
"""Check to see if the data file has been added to the lookup
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
--------------
|
||||||
|
dataname: str
|
||||||
|
The data file name
|
||||||
|
"""
|
||||||
|
if dataname not in self.datafiles.keys():
|
||||||
|
raise KeyError('File {0} not currently tracked. Add this file to '
|
||||||
|
'the lookup and try again.'.format(dataname))
|
||||||
|
|
||||||
|
def add_data_file(self, filepath):
|
||||||
|
df = DataFileLookup()
|
||||||
|
fn = GbmFile.from_path(filepath)
|
||||||
|
df.filename = fn.basename()
|
||||||
|
df.detector = fn.detector
|
||||||
|
self.datafiles[df.filename] = df
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, d):
|
||||||
|
r = cls()
|
||||||
|
r.file_date = d.get('file_date', None)
|
||||||
|
datafiles = d.get('datafiles', None)
|
||||||
|
if datafiles:
|
||||||
|
for k, v in datafiles.items():
|
||||||
|
df = DataFileLookup.from_dict(v)
|
||||||
|
df.filename = k
|
||||||
|
r.datafiles[k] = df
|
||||||
|
return r
|
||||||
|
|
||||||
|
def write_to(self, fpath):
|
||||||
|
"""
|
||||||
|
Write contents of LookupFile to the given file path as a JSON file.
|
||||||
|
:param fpath: full pathname for JSON file
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.file_date = dt.datetime.utcnow().isoformat()
|
||||||
|
with open(fpath, "w") as fp:
|
||||||
|
json.dump(self, fp, cls=LookupEncoder, indent=4)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def read_from(cls, fpath):
|
||||||
|
"""
|
||||||
|
Load values to LookupFile from the JSON file at the given path.
|
||||||
|
:param fpath: full pathname for JSON file
|
||||||
|
:return: new LookupFile object
|
||||||
|
"""
|
||||||
|
with open(fpath, "r") as fp:
|
||||||
|
j = json.load(fp)
|
||||||
|
return cls.from_dict(j)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def read_from_rmfit(cls, fpath, ti_file=None, dataname=None):
|
||||||
|
"""
|
||||||
|
Load values to LookupFile from the RMFIT created lookup file at the given path.
|
||||||
|
:param fpath: full pathname for RMFIT created lookup file
|
||||||
|
:param ti_file: full pathname for RMFIT created ti file.
|
||||||
|
:param dataname: the name of the datafile to associate this lookup file with
|
||||||
|
:return: new LookupFile object
|
||||||
|
"""
|
||||||
|
|
||||||
|
# RMFit selections are an 2xN array where the first element is the start values and the second element
|
||||||
|
# are the end values. It needs to be transposed into a Nx2 array. Drop first is used to drop the convex
|
||||||
|
# hull if the selections contain one.
|
||||||
|
def transform_selections(x, drop_first=False):
|
||||||
|
result = None
|
||||||
|
if x:
|
||||||
|
result = np.array(x).reshape(2, -1).transpose().tolist()
|
||||||
|
if drop_first:
|
||||||
|
result = result[1:]
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Begin loading RMFit lookup file making the contents a list of tokens.
|
||||||
|
tokens = []
|
||||||
|
with open(fpath, 'r') as contents:
|
||||||
|
for line in contents:
|
||||||
|
x = line.strip().split()
|
||||||
|
if x:
|
||||||
|
try:
|
||||||
|
# If the first element a number? Then add the array to the tokens.
|
||||||
|
float(x[0])
|
||||||
|
tokens += x
|
||||||
|
except ValueError:
|
||||||
|
# Otherwise, it's a string and we will append the entire line as a token.
|
||||||
|
tokens.append(line)
|
||||||
|
|
||||||
|
# Let's create the DataFile object
|
||||||
|
data_file = DataFileLookup()
|
||||||
|
|
||||||
|
# The input data file is based on the lookup filename
|
||||||
|
f = GbmFile.from_path(fpath)
|
||||||
|
|
||||||
|
if f.extension == 'lu':
|
||||||
|
if f.data_type == 'ctime' or f.data_type == 'cspec':
|
||||||
|
f.extension = 'pha'
|
||||||
|
elif f.data_type == 'tte':
|
||||||
|
f.extension = 'fit'
|
||||||
|
else:
|
||||||
|
raise ValueError('Not a valid lookup filename')
|
||||||
|
else:
|
||||||
|
raise ValueError("Not a valid lookup filename")
|
||||||
|
|
||||||
|
if dataname:
|
||||||
|
data_file.filename = os.path.basename(dataname)
|
||||||
|
else:
|
||||||
|
data_file.filename = f.basename()
|
||||||
|
|
||||||
|
lr = ListReader(tokens)
|
||||||
|
|
||||||
|
# energy edges, if None is returned we need to read the next value anyway which should be zero.
|
||||||
|
energy_edges = lr.get_n(int, rmfit=True)
|
||||||
|
if energy_edges:
|
||||||
|
# TODO: add_energy_binning unresolved for class 'DataFileLookup'
|
||||||
|
data_file.add_energy_binning('By Edge Index',
|
||||||
|
np.array(energy_edges))
|
||||||
|
|
||||||
|
# energy selections, if None is returned we need to read the next value anyway which should be zero.
|
||||||
|
data_file.selections.energy = transform_selections(
|
||||||
|
lr.get_n(float, rmfit=True), drop_first=True)
|
||||||
|
|
||||||
|
# rebinned time edges, if None is returned we need to read the next value anyway which should be zero.
|
||||||
|
time_edges = lr.get_n(int, rmfit=True)
|
||||||
|
if time_edges:
|
||||||
|
if f.data_type == 'ctime' or f.data_type == 'cspec':
|
||||||
|
data_file.set_time_binning('By Edge Index', 'binned',
|
||||||
|
np.array(time_edges))
|
||||||
|
elif f.data_type == 'tte':
|
||||||
|
# Read TI file
|
||||||
|
if ti_file:
|
||||||
|
with open(ti_file, 'r') as fp:
|
||||||
|
txt = list(fp)
|
||||||
|
txt = txt[1:]
|
||||||
|
tte_edges = np.array([t.strip() for t in txt], dtype=float)
|
||||||
|
data_file.set_time_binning('By Time Edge', 'unbinned',
|
||||||
|
np.array(tte_edges))
|
||||||
|
else:
|
||||||
|
warnings.warn("No TTE edges found. Need '.ti' file")
|
||||||
|
|
||||||
|
# time selections, if None is returned we need to read the next value anyway which should be zero.
|
||||||
|
data_file.selections.source = transform_selections(
|
||||||
|
lr.get_n(float, rmfit=True), drop_first=True)
|
||||||
|
|
||||||
|
# background selections, if None is returned we need to read the next value anyway which should be zero.
|
||||||
|
data_file.selections.background = transform_selections(
|
||||||
|
lr.get_n(float, rmfit=True))
|
||||||
|
|
||||||
|
# TODO: For now skip over binning names
|
||||||
|
lr.skip(3) # Assuming 'STACKED SPECTRA', 'LOG', 'LOG'
|
||||||
|
|
||||||
|
# time and energy window ranges: (xmin, xmax, ymin, ymax)
|
||||||
|
v = lr.get(4, float)
|
||||||
|
data_file.views.time = View(v[0], v[1], v[2], v[3])
|
||||||
|
v = lr.get(4, float)
|
||||||
|
data_file.views.energy = View(v[0], v[1], v[2], v[3])
|
||||||
|
|
||||||
|
# polynomial background order
|
||||||
|
# data_file.background = {'poly_order': lr.get(cls=int)}
|
||||||
|
poly_order = lr.get(cls=int)
|
||||||
|
data_file.set_background_model('Polynomial', 'binned', poly_order)
|
||||||
|
|
||||||
|
# Add the data file to a newly created lookup file
|
||||||
|
lu = cls()
|
||||||
|
lu.datafiles[data_file.filename] = data_file
|
||||||
|
return lu
|
||||||
|
|
||||||
|
def merge_lookup(self, lookup, overwrite=False):
|
||||||
|
"""Merge an existing lookup into this lookup
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
--------------
|
||||||
|
lookup: GspecLookup
|
||||||
|
The lookup object to be merged into this lookup
|
||||||
|
overwrite: bool, optional
|
||||||
|
If set to True, then any datanames in the current lookup will be overwritten
|
||||||
|
if those same datanames are in the input lookup. Default is False
|
||||||
|
"""
|
||||||
|
# get datanames of the input lookup
|
||||||
|
datanames = lookup.datafiles.keys()
|
||||||
|
for dataname in datanames:
|
||||||
|
# if dataname is already in this lookup and we don't want to overwrite
|
||||||
|
if (dataname in self.datafiles) & (not overwrite):
|
||||||
|
continue
|
||||||
|
self.datafiles[dataname] = lookup.datafiles[dataname]
|
||||||
|
|
||||||
|
# TODO: Remove?
|
||||||
|
def split_off_dataname(self, dataname):
|
||||||
|
"""Return a new lookup object containing only the requested data file
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
--------------
|
||||||
|
dataname: str
|
||||||
|
The requested data filename
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
-----------
|
||||||
|
new_lookup: GspecLookup
|
||||||
|
The new lookup object
|
||||||
|
"""
|
||||||
|
self.assert_has_datafile(dataname)
|
||||||
|
new_lookup = LookupFile()
|
||||||
|
new_lookup[dataname] = self.datafiles[dataname]
|
||||||
|
return new_lookup
|
||||||
|
|
||||||
|
def display_lookup(self):
|
||||||
|
"""Pretty print a lookup for display (in json format)
|
||||||
|
"""
|
||||||
|
lu = json.dumps(self.datafiles, indent=4, separators=(',', ': '),
|
||||||
|
cls=LookupEncoder)
|
||||||
|
return lu
|
||||||
|
|
||||||
|
|
||||||
|
class LookupEncoder(json.JSONEncoder):
|
||||||
|
"""Custom JSON encoder for numpy arrays. Converts them to a list.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def default(self, obj):
|
||||||
|
if isinstance(obj, DataFileLookup):
|
||||||
|
d = dict(obj.__dict__)
|
||||||
|
del d['filename']
|
||||||
|
return d
|
||||||
|
if isinstance(obj, np.ndarray):
|
||||||
|
return obj.tolist()
|
||||||
|
if isinstance(obj, Detector):
|
||||||
|
return obj.short_name
|
||||||
|
elif hasattr(obj, 'to_dict'):
|
||||||
|
return obj.to_dict()
|
||||||
|
elif hasattr(obj, '__dict__'):
|
||||||
|
return obj.__dict__
|
||||||
|
return json.JSONEncoder.default(self, obj)
|
||||||
|
|
||||||
|
|
||||||
|
class LookupDecoder(json.JSONDecoder):
|
||||||
|
"""Custom JSON decoder to turn JSON lists into numpy arrays
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
json.JSONDecoder.__init__(self, object_hook=self.object_hook,
|
||||||
|
*args, **kwargs)
|
||||||
|
|
||||||
|
def object_hook(self, obj):
|
||||||
|
# if object is a dictionary
|
||||||
|
if type(obj) == dict:
|
||||||
|
for key in obj.keys():
|
||||||
|
# and if the value is a list, change to numpy array
|
||||||
|
obj_type = type(obj[key])
|
||||||
|
if obj_type == list:
|
||||||
|
obj[key] = np.array(obj[key], dtype=type(obj[key]))
|
||||||
|
|
||||||
|
return obj
|
9
plot/__init__.py
Normal file
9
plot/__init__.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from .drm import ResponseMatrix, PhotonEffectiveArea, ChannelEffectiveArea
|
||||||
|
from .lightcurve import Lightcurve
|
||||||
|
from .model import ModelFit
|
||||||
|
from .skyplot import SkyPlot, FermiSkyPlot
|
||||||
|
from .spectrum import Spectrum
|
||||||
|
try:
|
||||||
|
from .earthplot import EarthPlot
|
||||||
|
except ImportError:
|
||||||
|
pass
|
311
plot/drm.py
Normal file
311
plot/drm.py
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
# drm.py: Plot class for responses
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
from .globals import *
|
||||||
|
from .gbmplot import GbmPlot, HeatMap, EffectiveArea
|
||||||
|
|
||||||
|
|
||||||
|
class ResponseMatrix(GbmPlot):
|
||||||
|
"""Class for plotting a response matrix.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
data (:class:`~gbm.data.RSP`, optional): The response object
|
||||||
|
colorbar (bool, optional): If True, plot the colorbar for the
|
||||||
|
effective area scale. Default is True.
|
||||||
|
multi (bool, optional):
|
||||||
|
If True, plots a multiplot window showing the matrix and the integrated
|
||||||
|
effective area as a function of incident energy and recorded energy
|
||||||
|
num_contours (int, optional): Number of contours to plot. Default is 100
|
||||||
|
**kwargs: Options to pass to :class:`~.gbmplot.GbmPlot`
|
||||||
|
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
ax (:class:`matplotlib.axes`): The matplotlib axes object for the plot
|
||||||
|
canvas (Canvas Backend object): The plotting canvas, if set upon
|
||||||
|
initialization.
|
||||||
|
data (:class:`~gbm.plot.gbmplot.HeatMap'): The matrix plot object
|
||||||
|
fig (:class:`matplotlib.figure`): The matplotlib figure object
|
||||||
|
xlim (float, float): The plotting range of the x axis.
|
||||||
|
This attribute can be set.
|
||||||
|
xscale (str): The scale of the x axis, either 'linear' or 'log'.
|
||||||
|
This attribute can be set.
|
||||||
|
ylim (float, float): The plotting range of the y axis.
|
||||||
|
This attribute can be set.
|
||||||
|
yscale (str): The scale of the y axis, either 'linear' or 'log'.
|
||||||
|
This attribute can be set.
|
||||||
|
"""
|
||||||
|
_background = 'black'
|
||||||
|
|
||||||
|
def __init__(self, data=None, colorbar=True, multi=False, canvas=None,
|
||||||
|
axis=None, num_contours=100, **kwargs):
|
||||||
|
|
||||||
|
self._drm = None
|
||||||
|
self._colorbar = colorbar
|
||||||
|
self._multi = multi
|
||||||
|
|
||||||
|
# do the multiplot
|
||||||
|
if multi:
|
||||||
|
self._colorbar = False
|
||||||
|
axis, ax_x, ax_y = self._init_multiplot()
|
||||||
|
self._p = PhotonEffectiveArea(data=data, canvas=canvas, axis=ax_x,
|
||||||
|
**kwargs)
|
||||||
|
self._c = ChannelEffectiveArea(data=data, canvas=canvas, axis=ax_y,
|
||||||
|
orientation='horizontal', **kwargs)
|
||||||
|
ax_x.get_xaxis().set_visible(False)
|
||||||
|
ax_y.get_yaxis().set_visible(False)
|
||||||
|
|
||||||
|
super().__init__(canvas=canvas, axis=axis, **kwargs)
|
||||||
|
|
||||||
|
self._ax.set_facecolor(self._background)
|
||||||
|
|
||||||
|
# initialize the plot axes, labels, ticks, and scales
|
||||||
|
self._ax.set_xlabel('Photon Energy (keV)', fontsize=PLOTFONTSIZE)
|
||||||
|
self._ax.set_ylabel('Channel Energy (keV)', fontsize=PLOTFONTSIZE)
|
||||||
|
self._ax.xaxis.set_tick_params(labelsize=PLOTFONTSIZE)
|
||||||
|
self._ax.yaxis.set_tick_params(labelsize=PLOTFONTSIZE)
|
||||||
|
self._ax.set_xscale('log')
|
||||||
|
self._ax.set_yscale('log')
|
||||||
|
|
||||||
|
# plot data and/or background if set on init
|
||||||
|
if data is not None:
|
||||||
|
self.set_response(data, index=0, num_contours=num_contours)
|
||||||
|
self._ax.set_xlim(data.photon_bins[0][0], data.photon_bins[1][-1])
|
||||||
|
self._ax.set_ylim(data.ebounds['E_MIN'][0],
|
||||||
|
data.ebounds['E_MAX'][-1])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
return self._drm
|
||||||
|
|
||||||
|
def _init_multiplot(self):
|
||||||
|
# initialize the multiplot
|
||||||
|
|
||||||
|
# dimensions
|
||||||
|
left, width = 0.12, 0.55
|
||||||
|
bottom, height = 0.12, 0.55
|
||||||
|
bottom_h = left_h = left + width
|
||||||
|
matrix = [left, bottom, width, height]
|
||||||
|
histx = [left, bottom_h, width, 0.40]
|
||||||
|
histy = [left_h, bottom, 0.40, height]
|
||||||
|
|
||||||
|
# create the plot axes
|
||||||
|
main_ax = plt.axes(matrix)
|
||||||
|
ax_x = plt.axes(histx)
|
||||||
|
ax_y = plt.axes(histy)
|
||||||
|
|
||||||
|
return (main_ax, ax_x, ax_y)
|
||||||
|
|
||||||
|
def set_response(self, data, index=None, atime=None, **kwargs):
|
||||||
|
"""Set the response data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (:class:`~gbm.data.RSP`): The response object
|
||||||
|
index (int, optional): The index of the DRM to display
|
||||||
|
atime (float, optional): The time corresponding to a DRM
|
||||||
|
**kwargs: Arguments to pass to :class:`~.gbmplot.HeatMap`
|
||||||
|
"""
|
||||||
|
if index is not None:
|
||||||
|
drm = data.drm(index)
|
||||||
|
elif atime is not None:
|
||||||
|
drm = data.nearest_drm(atime)
|
||||||
|
else:
|
||||||
|
drm = data.drm(0)
|
||||||
|
self._drm = HeatMap(data.photon_bin_centroids, data.channel_centroids,
|
||||||
|
drm.T, self.ax, colorbar=self._colorbar, **kwargs)
|
||||||
|
|
||||||
|
# update the background color of the colorbar
|
||||||
|
if self._colorbar:
|
||||||
|
self._drm._artists[-1].patch.set_facecolor(self._background)
|
||||||
|
|
||||||
|
|
||||||
|
class PhotonEffectiveArea(GbmPlot):
|
||||||
|
"""Class for plotting the incident photon effective area
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
data (:class:`~gbm.data.RSP`, optional): The response object
|
||||||
|
**kwargs: Options to pass to :class:`~.gbmplot.GbmPlot`
|
||||||
|
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
ax (:class:`matplotlib.axes`): The matplotlib axes object for the plot
|
||||||
|
canvas (Canvas Backend object): The plotting canvas, if set upon
|
||||||
|
initialization.
|
||||||
|
data (:class:`~gbm.plot.gbmplot.EffectiveArea`): The effective area
|
||||||
|
plot object
|
||||||
|
fig (:class:`matplotlib.figure`): The matplotlib figure object
|
||||||
|
xlim (float, float): The plotting range of the x axis.
|
||||||
|
This attribute can be set.
|
||||||
|
xscale (str): The scale of the x axis, either 'linear' or 'log'.
|
||||||
|
This attribute can be set.
|
||||||
|
ylim (float, float): The plotting range of the y axis.
|
||||||
|
This attribute can be set.
|
||||||
|
yscale (str): The scale of the y axis, either 'linear' or 'log'.
|
||||||
|
This attribute can be set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, data=None, canvas=None, axis=None, **kwargs):
|
||||||
|
super().__init__(canvas=canvas, axis=axis, **kwargs)
|
||||||
|
self._data = None
|
||||||
|
|
||||||
|
# initialize the plot axes, labels, ticks, and scales
|
||||||
|
self._ax.set_xlabel('Photon Energy (keV)', fontsize=PLOTFONTSIZE)
|
||||||
|
self._ax.set_ylabel(r'Effective Area (cm$^2$)', fontsize=PLOTFONTSIZE)
|
||||||
|
self._ax.xaxis.set_tick_params(labelsize=PLOTFONTSIZE)
|
||||||
|
self._ax.yaxis.set_tick_params(labelsize=PLOTFONTSIZE)
|
||||||
|
self._ax.set_xscale('log')
|
||||||
|
self._ax.set_yscale('log')
|
||||||
|
|
||||||
|
# plot data and/or background if set on init
|
||||||
|
if data is not None:
|
||||||
|
self.set_response(data, index=0)
|
||||||
|
self._ax.set_xlim(data.photon_bins[0][0], data.photon_bins[1][-1])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
def set_response(self, data, index=None, atime=None, **kwargs):
|
||||||
|
"""Set the response data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (:class:`~gbm.data.RSP`): The response object
|
||||||
|
index (int, optional): The index of the DRM to display
|
||||||
|
atime (float, optional): The time corresponding to a DRM
|
||||||
|
**kwargs: Arguments to pass to :class:`~.gbmplot.EffectiveArea`
|
||||||
|
"""
|
||||||
|
if (index is None) and (atime is None):
|
||||||
|
index = 0
|
||||||
|
_color, _alpha, _kwargs = self._settings()
|
||||||
|
effarea = data.photon_effective_area(index=index, atime=atime)
|
||||||
|
self._data = EffectiveArea(effarea, self._ax, color=_color,
|
||||||
|
alpha=_alpha, **_kwargs)
|
||||||
|
plt.draw()
|
||||||
|
|
||||||
|
def _settings(self):
|
||||||
|
"""The default settings for the plot. If a plot already
|
||||||
|
exists, use its settings instead.
|
||||||
|
"""
|
||||||
|
if self._data is None:
|
||||||
|
_color = DATA_COLOR
|
||||||
|
_alpha = None
|
||||||
|
_kwargs = {}
|
||||||
|
else:
|
||||||
|
_color = self._data.color
|
||||||
|
_alpha = self._data.alpha
|
||||||
|
_kwargs = self._data._kwargs
|
||||||
|
return (_color, _alpha, _kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelEffectiveArea(GbmPlot):
|
||||||
|
"""Class for plotting the recorded channel energy effective area.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
data (:class:`~gbm.data.RSP`, optional): The response object
|
||||||
|
**kwargs: Options to pass to :class:`~.gbmplot.GbmPlot`
|
||||||
|
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
ax (:class:`matplotlib.axes`): The matplotlib axes object for the plot
|
||||||
|
canvas (Canvas Backend object): The plotting canvas, if set upon
|
||||||
|
initialization.
|
||||||
|
data (:class:`~gbm.plot.gbmplot.EffectiveArea`): The effective area
|
||||||
|
plot object
|
||||||
|
fig (:class:`matplotlib.figure`): The matplotlib figure object
|
||||||
|
xlim (float, float): The plotting range of the x axis.
|
||||||
|
This attribute can be set.
|
||||||
|
xscale (str): The scale of the x axis, either 'linear' or 'log'.
|
||||||
|
This attribute can be set.
|
||||||
|
ylim (float, float): The plotting range of the y axis.
|
||||||
|
This attribute can be set.
|
||||||
|
yscale (str): The scale of the y axis, either 'linear' or 'log'.
|
||||||
|
This attribute can be set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, data=None, canvas=None, axis=None,
|
||||||
|
orientation='vertical',
|
||||||
|
**kwargs):
|
||||||
|
super().__init__(canvas=canvas, axis=axis, **kwargs)
|
||||||
|
self._data = None
|
||||||
|
self._orientation = orientation
|
||||||
|
|
||||||
|
# initialize the plot axes, labels, ticks, and scales
|
||||||
|
if self._orientation == 'horizontal':
|
||||||
|
self._ax.set_ylabel('Channel Energy (keV)', fontsize=PLOTFONTSIZE)
|
||||||
|
self._ax.set_xlabel(r'Effective Area (cm$^2$)',
|
||||||
|
fontsize=PLOTFONTSIZE)
|
||||||
|
else:
|
||||||
|
self._ax.set_xlabel('Channel Energy (keV)', fontsize=PLOTFONTSIZE)
|
||||||
|
self._ax.set_ylabel(r'Effective Area (cm$^2$)',
|
||||||
|
fontsize=PLOTFONTSIZE)
|
||||||
|
self._ax.xaxis.set_tick_params(labelsize=PLOTFONTSIZE)
|
||||||
|
self._ax.yaxis.set_tick_params(labelsize=PLOTFONTSIZE)
|
||||||
|
self._ax.set_xscale('log')
|
||||||
|
self._ax.set_yscale('log')
|
||||||
|
|
||||||
|
# plot data and/or background if set on init
|
||||||
|
if data is not None:
|
||||||
|
self.set_response(data, index=0)
|
||||||
|
xrange = (data.ebounds['E_MIN'][0], data.ebounds['E_MAX'][-1])
|
||||||
|
if self._orientation == 'horizontal':
|
||||||
|
self._ax.set_ylim(xrange)
|
||||||
|
else:
|
||||||
|
self._ax.set_xlim(xrange)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self):
|
||||||
|
return self._data
|
||||||
|
|
||||||
|
def set_response(self, data, index=None, atime=None, **kwargs):
|
||||||
|
"""Set the response data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (:class:`~gbm.data.RSP`): The response object
|
||||||
|
index (int, optional): The index of the DRM to display
|
||||||
|
atime (float, optional): The time corresponding to a DRM
|
||||||
|
**kwargs: Arguments to pass to :class:`~.gbmplot.EffectiveArea`
|
||||||
|
"""
|
||||||
|
if (index is None) and (atime is None):
|
||||||
|
index = 0
|
||||||
|
_color, _alpha, _kwargs = self._settings()
|
||||||
|
effarea = data.photon_effective_area(index=index, atime=atime)
|
||||||
|
self._data = EffectiveArea(effarea, self._ax, color=_color,
|
||||||
|
alpha=_alpha, orientation=self._orientation,
|
||||||
|
**_kwargs)
|
||||||
|
|
||||||
|
def _settings(self):
|
||||||
|
"""The default settings for the plot. If a plor already
|
||||||
|
exists, use its settings instead.
|
||||||
|
"""
|
||||||
|
if self._data is None:
|
||||||
|
_color = DATA_COLOR
|
||||||
|
_alpha = None
|
||||||
|
_kwargs = {}
|
||||||
|
else:
|
||||||
|
_color = self._data.color
|
||||||
|
_alpha = self._data.alpha
|
||||||
|
_kwargs = self._data._kwargs
|
||||||
|
return (_color, _alpha, _kwargs)
|
169
plot/earthplot.py
Normal file
169
plot/earthplot.py
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
# earthplot.py: Plot class for Fermi Earth orbital position
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import numpy as np
|
||||||
|
try:
|
||||||
|
from mpl_toolkits.basemap import Basemap
|
||||||
|
except:
|
||||||
|
raise ImportError('Basemap not installed. Cannot use EarthPlot.')
|
||||||
|
|
||||||
|
from .gbmplot import GbmPlot, SAA, McIlwainL, EarthLine, FermiIcon
|
||||||
|
from .globals import *
|
||||||
|
|
||||||
|
|
||||||
|
class EarthPlot(GbmPlot):
|
||||||
|
"""Class for plotting the Earth, primarily related to Fermi's position in
|
||||||
|
orbit.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
This class requires installation of Matplotlib Basemap.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
lat_range ((float, float), optional):
|
||||||
|
The latitude range of the plot. Default value is the extent of the
|
||||||
|
Fermi orbit: (-27, 27)
|
||||||
|
lon_range ((float, float), optional):
|
||||||
|
The longitude range of the plot. Default value is (-180, 180).
|
||||||
|
saa (bool, optional): If True, display the SAA polygon. Default is True.
|
||||||
|
mcilwain (bool, optional): If True, display the McIlwain L heatmap.
|
||||||
|
Default is True.
|
||||||
|
**kwargs: Options to pass to :class:`~.gbmplot.GbmPlot`
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
ax (:class:`matplotlib.axes`): The matplotlib axes object for the plot
|
||||||
|
canvas (Canvas Backend object): The plotting canvas, if set upon
|
||||||
|
initialization.
|
||||||
|
fermi (:class:`~.gbmplot.FermiIcon`): Fermi spacecraft plot element
|
||||||
|
fig (:class:`matplotlib.figure`): The matplotlib figure object
|
||||||
|
map (:class:`mpl_toolkits.basemap.Basemap`): The Basemap object
|
||||||
|
mcilwainl (:class:`~.gbmplot.McIlwainL`): The McIlwain L heatmap
|
||||||
|
orbit (:class:`~.gbmplot.EarthLine`): The orbital path of Fermi
|
||||||
|
saa (:class:`~.gbmplot.SAA`): The SAA polygon
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, lat_range=None, lon_range=None, saa=True, mcilwain=True,
|
||||||
|
canvas=None, **kwargs):
|
||||||
|
# Default is to cover Fermi orbit which is latitude ~27S to ~27N
|
||||||
|
if lat_range is None:
|
||||||
|
lat_range = (-29.999, 30.001)
|
||||||
|
if lon_range is None:
|
||||||
|
lon_range = (-180.0, 180.0)
|
||||||
|
|
||||||
|
# scale figure size by lat and lon ranges
|
||||||
|
xsize = (lon_range[1] - lon_range[0]) / 30.0
|
||||||
|
ysize = (lat_range[1] - lat_range[0]) / 30.0
|
||||||
|
figsize = (xsize, ysize)
|
||||||
|
|
||||||
|
super().__init__(figsize=figsize, canvas=canvas, **kwargs)
|
||||||
|
self._trig_mcilwain = None
|
||||||
|
self._saa = None
|
||||||
|
self._mcilwain = None
|
||||||
|
self._orbit = None
|
||||||
|
self._fermi = None
|
||||||
|
|
||||||
|
# initialize the map, mercator projection, coarse resolution
|
||||||
|
self._m = Basemap(projection='cyl', llcrnrlat=lat_range[0],
|
||||||
|
urcrnrlat=lat_range[1], llcrnrlon=lon_range[0],
|
||||||
|
urcrnrlon=lon_range[1], lat_ts=0, resolution='c',
|
||||||
|
ax=self._ax)
|
||||||
|
self._m.drawcoastlines()
|
||||||
|
self._m.drawparallels(np.arange(-90., 91., 30.), labels=[1, 0, 0, 0],
|
||||||
|
fontsize=12)
|
||||||
|
self._m.drawmeridians(np.arange(-180., 181., 30.), labels=[0, 0, 0, 1],
|
||||||
|
fontsize=12)
|
||||||
|
|
||||||
|
if saa:
|
||||||
|
self._saa = SAA(self._m, self._ax, color='darkred', alpha=0.4)
|
||||||
|
|
||||||
|
if mcilwain:
|
||||||
|
self._mcilwain = McIlwainL(self._m, self._ax, alpha=0.5,
|
||||||
|
saa_mask=saa)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def map(self):
|
||||||
|
return self._m
|
||||||
|
|
||||||
|
@property
|
||||||
|
def saa(self):
|
||||||
|
return self._saa
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mcilwainl(self):
|
||||||
|
return self._mcilwain
|
||||||
|
|
||||||
|
@property
|
||||||
|
def orbit(self):
|
||||||
|
return self._orbit
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fermi(self):
|
||||||
|
return self._fermi
|
||||||
|
|
||||||
|
def add_poshist(self, data, time_range=None, trigtime=None, numpts=1000):
|
||||||
|
"""Add a Position History or Trigdat object to plot the orbital path
|
||||||
|
of Fermi and optional the position of Fermi at a particular time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (:class:`~gbm.data.PosHist` or :class:`~gbm.data.Trigdat`):
|
||||||
|
A Position History or Trigdat object
|
||||||
|
time_range: ((float, float), optional)
|
||||||
|
The time range over which to plot the orbit. If omitted, plots
|
||||||
|
the orbit over the entire time range of the file.
|
||||||
|
trigtime (float, optional):
|
||||||
|
If data is PosHist, set trigtime to a particular time of interest
|
||||||
|
to plot Fermi's orbital location. The Trigdat trigger time
|
||||||
|
overrides this.
|
||||||
|
numpts (int, optional): The number of interpolation points for
|
||||||
|
plotting the orbit. Default is 1000.
|
||||||
|
"""
|
||||||
|
if time_range is None:
|
||||||
|
time_range = data.time_range
|
||||||
|
|
||||||
|
# get latitude and longitude over the time range and produce the orbit
|
||||||
|
times = np.linspace(*time_range, numpts)
|
||||||
|
lat = data.get_latitude(times)
|
||||||
|
lon = data.get_longitude(times)
|
||||||
|
self._orbit = EarthLine(lat, lon, self._m, self._ax, color='black',
|
||||||
|
alpha=0.4, lw=5.0)
|
||||||
|
|
||||||
|
# if trigtime is set or Trigdat, produce Fermi location
|
||||||
|
if hasattr(data, 'trigtime'):
|
||||||
|
trigtime = data.trigtime
|
||||||
|
if trigtime is not None:
|
||||||
|
lat = data.get_latitude(trigtime)
|
||||||
|
lon = data.get_longitude(trigtime)
|
||||||
|
self._trig_mcilwain = data.get_mcilwain_l(trigtime)
|
||||||
|
self._fermi = FermiIcon(lat, lon, self._m, self._ax)
|
||||||
|
|
||||||
|
def standard_title(self):
|
||||||
|
"""Add the standard plot title to the plot if a Trigdat object or a
|
||||||
|
PosHist object + trigtime has been added.
|
||||||
|
"""
|
||||||
|
if self.fermi is not None:
|
||||||
|
coord = self.fermi.coordinates
|
||||||
|
title = 'Latitude, East Longitude: ({0}, {1})\n'.format(*coord)
|
||||||
|
title += 'McIlwain L: {}'.format(
|
||||||
|
'{:.2f}'.format(float(self._trig_mcilwain)))
|
||||||
|
self._ax.set_title(title)
|
2104
plot/gbmplot.py
Normal file
2104
plot/gbmplot.py
Normal file
File diff suppressed because it is too large
Load Diff
47
plot/globals.py
Normal file
47
plot/globals.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# globals.py: Default plot settings
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
# data and data error colors
|
||||||
|
DATA_COLOR = '#394264'
|
||||||
|
DATA_ERROR_COLOR = 'dimgrey'
|
||||||
|
DATA_ERROR_ALPHA = 0.5
|
||||||
|
DATA_SELECTED_COLOR = '#9a4e0e'
|
||||||
|
DATA_SELECTED_ALPHA = 0.2
|
||||||
|
DATA_ERROR_SELECTED_COLOR = '#9a4e0e'
|
||||||
|
DATA_ERROR_SELECTED_ALPHA = 0.5
|
||||||
|
BINNING_SELECTED_COLOR = 'darkgreen'
|
||||||
|
BINNING_SELECTED_ALPHA = 0.2
|
||||||
|
|
||||||
|
# background and background error colors
|
||||||
|
BKGD_COLOR = 'firebrick'
|
||||||
|
BKGD_ALPHA = 0.85
|
||||||
|
BKGD_WIDTH = 0.75
|
||||||
|
BKGD_ERROR_ALPHA = 0.50
|
||||||
|
|
||||||
|
# plotting font
|
||||||
|
PLOTFONT = None
|
||||||
|
PLOTFONTSIZE = 10
|
||||||
|
|
187
plot/lal_post_subs.py
Normal file
187
plot/lal_post_subs.py
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
# Copyright (C) 2012-2016 Leo Singer
|
||||||
|
#
|
||||||
|
# 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, see <http://www.gnu.org/licenses/>.
|
||||||
|
import warnings
|
||||||
|
import numpy as np
|
||||||
|
from astropy.utils.exceptions import AstropyDeprecationWarning
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore", category=AstropyDeprecationWarning)
|
||||||
|
"""
|
||||||
|
LalInference post-processing plotting subroutines
|
||||||
|
"""
|
||||||
|
import astropy.coordinates
|
||||||
|
import astropy.units as u
|
||||||
|
|
||||||
|
try:
|
||||||
|
from astropy.coordinates.angles import rotation_matrix
|
||||||
|
except:
|
||||||
|
from astropy.coordinates.matrix_utilities import rotation_matrix
|
||||||
|
|
||||||
|
def find_greedy_credible_levels(p, ranking=None):
|
||||||
|
p = np.asarray(p)
|
||||||
|
pflat = p.ravel()
|
||||||
|
if ranking is None:
|
||||||
|
ranking = pflat
|
||||||
|
else:
|
||||||
|
ranking = np.ravel(ranking)
|
||||||
|
i = np.flipud(np.argsort(ranking))
|
||||||
|
cs = np.cumsum(pflat[i])
|
||||||
|
cls = np.empty_like(pflat)
|
||||||
|
cls[i] = cs
|
||||||
|
return cls.reshape(p.shape)
|
||||||
|
|
||||||
|
|
||||||
|
def reference_angle(a):
|
||||||
|
"""Convert an angle to a reference angle between -pi and pi."""
|
||||||
|
a = np.mod(a, 2 * np.pi)
|
||||||
|
return np.where(a <= np.pi, a, a - 2 * np.pi)
|
||||||
|
|
||||||
|
|
||||||
|
def wrapped_angle(a):
|
||||||
|
"""Convert an angle to a reference angle between 0 and 2*pi."""
|
||||||
|
return np.mod(a, 2 * np.pi)
|
||||||
|
|
||||||
|
|
||||||
|
def make_circle_poly(radius, theta, phi, n=12, closed=False):
|
||||||
|
"""RA and Dec of polygonized cone about celestial pole"""
|
||||||
|
ra_v = 2 * np.pi * np.arange(n) / n
|
||||||
|
dec_v = np.ones_like(ra_v) * (0.5 * np.pi - radius)
|
||||||
|
M1 = rotation_matrix(phi, 'z', unit=astropy.units.radian)
|
||||||
|
M2 = rotation_matrix(theta, 'y', unit=astropy.units.radian)
|
||||||
|
R = np.asarray(np.dot(M2, M1))
|
||||||
|
xyz = np.dot(R.T,
|
||||||
|
astropy.coordinates.spherical_to_cartesian(1, dec_v, ra_v))
|
||||||
|
_, dec_v, ra_v = astropy.coordinates.cartesian_to_spherical(*xyz)
|
||||||
|
ra_v = ra_v.to(u.rad).value
|
||||||
|
dec_v = dec_v.to(u.rad).value
|
||||||
|
ra_v = np.mod(ra_v, 2 * np.pi)
|
||||||
|
if closed:
|
||||||
|
ra_v = np.concatenate((ra_v, [ra_v[0]]))
|
||||||
|
dec_v = np.concatenate((dec_v, [dec_v[0]]))
|
||||||
|
return np.transpose((ra_v, dec_v))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from mpl_toolkits.basemap import _geoslib as geos
|
||||||
|
|
||||||
|
def cut_prime_meridian(vertices):
|
||||||
|
"""Cut a polygon across the prime meridian, possibly splitting it into multiple
|
||||||
|
polygons. Vertices consist of (longitude, latitude) pairs where longitude
|
||||||
|
is always given in terms of a wrapped angle (between 0 and 2*pi).
|
||||||
|
|
||||||
|
This routine is not meant to cover all possible cases; it will only work for
|
||||||
|
convex polygons that extend over less than a hemisphere."""
|
||||||
|
|
||||||
|
out_vertices = []
|
||||||
|
|
||||||
|
# Ensure that the list of vertices does not contain a repeated endpoint.
|
||||||
|
if (vertices[0, :] == vertices[-1, :]).all():
|
||||||
|
vertices = vertices[:-1, :]
|
||||||
|
|
||||||
|
# Ensure that the longitudes are wrapped from 0 to 2*pi.
|
||||||
|
vertices = np.column_stack((wrapped_angle(vertices[:, 0]), vertices[:, 1]))
|
||||||
|
|
||||||
|
def count_meridian_crossings(phis):
|
||||||
|
n = 0
|
||||||
|
for i in range(len(phis)):
|
||||||
|
if crosses_meridian(phis[i - 1], phis[i]):
|
||||||
|
n += 1
|
||||||
|
return n
|
||||||
|
|
||||||
|
def crosses_meridian(phi0, phi1):
|
||||||
|
"""Test if the segment consisting of v0 and v1 croses the meridian."""
|
||||||
|
# If the two angles are in [0, 2pi), then the shortest arc connecting
|
||||||
|
# them crosses the meridian if the difference of the angles is greater
|
||||||
|
# than pi.
|
||||||
|
phi0, phi1 = sorted((phi0, phi1))
|
||||||
|
return phi1 - phi0 > np.pi
|
||||||
|
|
||||||
|
# Count the number of times that the polygon crosses the meridian.
|
||||||
|
meridian_crossings = count_meridian_crossings(vertices[:, 0])
|
||||||
|
|
||||||
|
if meridian_crossings % 2:
|
||||||
|
# FIXME: Use this simple heuristic to decide which pole to enclose.
|
||||||
|
sign_lat = np.sign(np.sum(vertices[:, 1]))
|
||||||
|
|
||||||
|
# If there are an odd number of meridian crossings, then the polygon
|
||||||
|
# encloses the pole. Any meridian-crossing edge has to be extended
|
||||||
|
# into a curve following the nearest polar edge of the map.
|
||||||
|
for i in range(len(vertices)):
|
||||||
|
v0 = vertices[i - 1, :]
|
||||||
|
v1 = vertices[i, :]
|
||||||
|
# Loop through the edges until we find one that crosses the meridian.
|
||||||
|
if crosses_meridian(v0[0], v1[0]):
|
||||||
|
# If this segment crosses the meridian, then fill it to
|
||||||
|
# the edge of the map by inserting new line segments.
|
||||||
|
|
||||||
|
# Find the latitude at which the meridian crossing occurs by
|
||||||
|
# linear interpolation.
|
||||||
|
delta_lon = abs(reference_angle(v1[0] - v0[0]))
|
||||||
|
lat = abs(reference_angle(v0[0])) / delta_lon * v0[1] + abs(
|
||||||
|
reference_angle(v1[0])) / delta_lon * v1[1]
|
||||||
|
|
||||||
|
# Find the closer of the left or the right map boundary for
|
||||||
|
# each vertex in the line segment.
|
||||||
|
lon_0 = 0. if v0[0] < np.pi else 2 * np.pi
|
||||||
|
lon_1 = 0. if v1[0] < np.pi else 2 * np.pi
|
||||||
|
|
||||||
|
# Set the output vertices to the polar cap plus the original
|
||||||
|
# vertices.
|
||||||
|
out_vertices += [np.vstack((vertices[:i, :], [
|
||||||
|
[lon_0, lat],
|
||||||
|
[lon_0, sign_lat * np.pi / 2],
|
||||||
|
[lon_1, sign_lat * np.pi / 2],
|
||||||
|
[lon_1, lat],
|
||||||
|
], vertices[i:, :]))]
|
||||||
|
|
||||||
|
# Since the polygon is assumed to be convex, the only possible
|
||||||
|
# odd number of meridian crossings is 1, so we are now done.
|
||||||
|
break
|
||||||
|
elif meridian_crossings:
|
||||||
|
# Since the polygon is assumed to be convex, if there is an even number
|
||||||
|
# of meridian crossings, we know that the polygon does not enclose
|
||||||
|
# either pole. Then we can use ordinary Euclidean polygon intersection
|
||||||
|
# algorithms.
|
||||||
|
|
||||||
|
# Construct polygon representing map boundaries in longitude and latitude.
|
||||||
|
frame_poly = geos.Polygon(np.asarray(
|
||||||
|
[[0., np.pi / 2], [0., -np.pi / 2], [2 * np.pi, -np.pi / 2],
|
||||||
|
[2 * np.pi, np.pi / 2]]))
|
||||||
|
|
||||||
|
# Intersect with polygon re-wrapped to lie in [pi, 3*pi).
|
||||||
|
poly = geos.Polygon(np.column_stack(
|
||||||
|
(reference_angle(vertices[:, 0]) + 2 * np.pi, vertices[:, 1])))
|
||||||
|
if poly.intersects(frame_poly):
|
||||||
|
out_vertices += [p.get_coords() for p in
|
||||||
|
poly.intersection(frame_poly)]
|
||||||
|
|
||||||
|
# Intersect with polygon re-wrapped to lie in [-pi, pi).
|
||||||
|
poly = geos.Polygon(
|
||||||
|
np.column_stack((reference_angle(vertices[:, 0]), vertices[:, 1])))
|
||||||
|
if poly.intersects(frame_poly):
|
||||||
|
out_vertices += [p.get_coords() for p in
|
||||||
|
poly.intersection(frame_poly)]
|
||||||
|
else:
|
||||||
|
# Otherwise, there were zero meridian crossings, so we can use the
|
||||||
|
# original vertices as is.
|
||||||
|
out_vertices += [vertices]
|
||||||
|
|
||||||
|
# Done!
|
||||||
|
return out_vertices
|
||||||
|
|
||||||
|
except:
|
||||||
|
warnings.warn('Basemap not installed. Some functionality not available.')
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
887
plot/lib.py
Normal file
887
plot/lib.py
Normal file
@ -0,0 +1,887 @@
|
|||||||
|
# lib.py: Module containing various plotting functions
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from astropy.coordinates import SkyCoord
|
||||||
|
from matplotlib.colors import colorConverter
|
||||||
|
from matplotlib.patches import Polygon
|
||||||
|
|
||||||
|
from .globals import *
|
||||||
|
from .lal_post_subs import *
|
||||||
|
from ..coords import calc_mcilwain_l, saa_boundary, radec_to_spacecraft
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Lightcurve and Spectra ----------#
|
||||||
|
def histo(bins, ax, color='C0', edges_to_zero=False, **kwargs):
|
||||||
|
"""Plot a rate histogram either lightcurves or count spectra
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bins (:class:`gbm.data.primitives.TimeBins` or \
|
||||||
|
:class:`gbm.data.primitives.EnergyBins`)
|
||||||
|
The lightcurve or count spectrum histograms
|
||||||
|
ax (:class:`matplotlib.axes`): The axis on which to plot
|
||||||
|
color (str, optional): The color of the histogram. Default is 'C0'
|
||||||
|
edges_to_zero (bool, optional):
|
||||||
|
If True, then the farthest edges of the histogram will drop to zero.
|
||||||
|
Default is True.
|
||||||
|
**kwargs: Other plotting options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: The reference to the plot objects
|
||||||
|
"""
|
||||||
|
bin_segs = bins.contiguous_bins()
|
||||||
|
refs = []
|
||||||
|
for seg in bin_segs:
|
||||||
|
edges = np.concatenate(
|
||||||
|
([seg.lo_edges[0]], seg.lo_edges, [seg.hi_edges[-1]]))
|
||||||
|
if edges_to_zero:
|
||||||
|
rates = np.concatenate(([0.0], seg.rates, [0.0]))
|
||||||
|
else:
|
||||||
|
rates = np.concatenate(
|
||||||
|
([seg.rates[0]], seg.rates, [seg.rates[-1]]))
|
||||||
|
|
||||||
|
p = ax.step(edges, rates, where='post', color=color, **kwargs)
|
||||||
|
refs.append(p)
|
||||||
|
return refs
|
||||||
|
|
||||||
|
|
||||||
|
def histo_errorbars(bins, ax, color='C0', **kwargs):
|
||||||
|
"""Plot errorbars for lightcurves or count spectra
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bins (:class:`gbm.data.primitives.TimeBins` or \
|
||||||
|
:class:`gbm.data.primitives.EnergyBins`)
|
||||||
|
The lightcurve or count spectrum histograms
|
||||||
|
ax (:class:`matplotlib.axes`): The axis on which to plot
|
||||||
|
color (str, optional): The color of the errorbars. Default is 'C0'
|
||||||
|
**kwargs: Other plotting options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: The reference to the plot objects
|
||||||
|
"""
|
||||||
|
bin_segs = bins.contiguous_bins()
|
||||||
|
refs = []
|
||||||
|
for seg in bin_segs:
|
||||||
|
p = ax.errorbar(seg.centroids, seg.rates, seg.rate_uncertainty,
|
||||||
|
capsize=0, fmt='none', color=color, **kwargs)
|
||||||
|
refs.append(p)
|
||||||
|
return refs
|
||||||
|
|
||||||
|
|
||||||
|
def histo_filled(bins, ax, color=DATA_SELECTED_COLOR,
|
||||||
|
fill_alpha=DATA_SELECTED_ALPHA, **kwargs):
|
||||||
|
"""Plot a filled histogram
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bins (:class:`gbm.data.primitives.TimeBins` or \
|
||||||
|
:class:`gbm.data.primitives.EnergyBins`)
|
||||||
|
The lightcurve or count spectrum histograms
|
||||||
|
ax (:class:`matplotlib.axes`): The axis on which to plot
|
||||||
|
color (str, optional): The color of the filled histogram
|
||||||
|
fill_alpha (float, optional): The alpha of the fill
|
||||||
|
**kwargs: Other plotting options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: The reference to the plot objects
|
||||||
|
"""
|
||||||
|
h = histo(bins, ax, color=color, zorder=3, **kwargs)
|
||||||
|
zeros = np.zeros(bins.size + 1)
|
||||||
|
rates = np.append(bins.rates, bins.rates[-1])
|
||||||
|
edges = np.append(bins.lo_edges, bins.hi_edges[-1])
|
||||||
|
b1 = ax.plot((edges[0], edges[0]), (zeros[0], rates[0]), color=color,
|
||||||
|
zorder=2)
|
||||||
|
b2 = ax.plot((edges[-1], edges[-1]), (zeros[-1], rates[-1]), color=color,
|
||||||
|
zorder=2)
|
||||||
|
f = ax.fill_between(edges, zeros, rates, color=color, step='post',
|
||||||
|
alpha=fill_alpha, zorder=4, **kwargs)
|
||||||
|
refs = [h, b1, b2, f]
|
||||||
|
return refs
|
||||||
|
|
||||||
|
|
||||||
|
def selection_line(xpos, ax, **kwargs):
|
||||||
|
"""Plot a selection line
|
||||||
|
|
||||||
|
Args:
|
||||||
|
xpos (float): The position of the selection line
|
||||||
|
ax (:class:`matplotlib.axes`): The axis on which to plot
|
||||||
|
**kwargs: Other plotting options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: The reference to the plot objects
|
||||||
|
"""
|
||||||
|
ylim = ax.get_ylim()
|
||||||
|
ref = ax.plot([xpos, xpos], ylim, **kwargs)
|
||||||
|
return ref
|
||||||
|
|
||||||
|
|
||||||
|
def selections(bounds, ax, **kwargs):
|
||||||
|
"""Plot selection bounds
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bounds (list of tuples): List of selection bounds
|
||||||
|
ax (:class:`matplotlib.axes`): The axis on which to plot
|
||||||
|
**kwargs: Other plotting options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: The reference to the lower and upper selection
|
||||||
|
"""
|
||||||
|
refs1 = []
|
||||||
|
refs2 = []
|
||||||
|
for bound in bounds:
|
||||||
|
p = selection_line(bound[0], ax, **kwargs)
|
||||||
|
refs1.append(p[0])
|
||||||
|
p = selection_line(bound[1], ax, **kwargs)
|
||||||
|
refs2.append(p[0])
|
||||||
|
|
||||||
|
return (refs1, refs2)
|
||||||
|
|
||||||
|
|
||||||
|
def errorband(x, y_upper, y_lower, ax, **kwargs):
|
||||||
|
"""Plot an error band
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x (np.array): The x values
|
||||||
|
y_upper (np.array): The upper y values of the error band
|
||||||
|
y_lower (np.array): The lower y values of the error band
|
||||||
|
ax (:class:`matplotlib.axes`): The axis on which to plot
|
||||||
|
**kwargs: Other plotting options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: The reference to the lower and upper selection
|
||||||
|
"""
|
||||||
|
refs = ax.fill_between(x, y_upper.squeeze(), y_lower.squeeze(), **kwargs)
|
||||||
|
return refs
|
||||||
|
|
||||||
|
|
||||||
|
def lightcurve_background(backrates, ax, cent_color=None, err_color=None,
|
||||||
|
cent_alpha=None, err_alpha=None, **kwargs):
|
||||||
|
"""Plot a lightcurve background model with an error band
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backrates (:class:`~gbm.background.BackgroundRates`):
|
||||||
|
The background rates object integrated over energy. If there is more
|
||||||
|
than one remaining energy channel, the background will be integrated
|
||||||
|
over the remaining energy channels.
|
||||||
|
ax (:class:`matplotlib.axes`): The axis on which to plot
|
||||||
|
cent_color (str): Color of the centroid line
|
||||||
|
err_color (str): Color of the errorband
|
||||||
|
cent_alpha (float): Alpha of the centroid line
|
||||||
|
err_alpha (fl): Alpha of the errorband
|
||||||
|
**kwargs: Other plotting options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: The reference to the lower and upper selection
|
||||||
|
"""
|
||||||
|
if backrates.numchans > 1:
|
||||||
|
backrates = backrates.integrate_energy()
|
||||||
|
times = backrates.time_centroids
|
||||||
|
rates = backrates.rates
|
||||||
|
uncert = backrates.rate_uncertainty
|
||||||
|
p2 = errorband(times, rates + uncert, rates - uncert, ax, alpha=err_alpha,
|
||||||
|
color=err_color, linestyle='-', **kwargs)
|
||||||
|
p1 = ax.plot(times, rates, color=cent_color, alpha=cent_alpha,
|
||||||
|
**kwargs)
|
||||||
|
refs = [p1, p2]
|
||||||
|
return refs
|
||||||
|
|
||||||
|
|
||||||
|
def spectrum_background(backspec, ax, cent_color=None, err_color=None,
|
||||||
|
cent_alpha=None, err_alpha=None, **kwargs):
|
||||||
|
"""Plot a count spectrum background model with an error band
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backspec (:class:`~gbm.background.BackgroundSpectrum`):
|
||||||
|
The background rates object integrated over energy. If there is more
|
||||||
|
than one remaining energy channel, the background will be integrated
|
||||||
|
over the remaining energy channels.
|
||||||
|
ax (:class:`matplotlib.axes`): The axis on which to plot
|
||||||
|
cent_color (str): Color of the centroid line
|
||||||
|
err_color (str): Color of the errorband
|
||||||
|
cent_alpha (float): Alpha of the centroid line
|
||||||
|
err_alpha (fl): Alpha of the errorband
|
||||||
|
**kwargs: Other plotting options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: The reference to the lower and upper selection
|
||||||
|
"""
|
||||||
|
rates = backspec.rates / backspec.widths
|
||||||
|
uncert = backspec.rate_uncertainty / backspec.widths
|
||||||
|
edges = np.append(backspec.lo_edges, backspec.hi_edges[-1])
|
||||||
|
# plot the centroid of the model
|
||||||
|
p1 = ax.step(edges, np.append(rates, rates[-1]), where='post',
|
||||||
|
color=cent_color, alpha=cent_alpha, zorder=1, **kwargs)
|
||||||
|
|
||||||
|
# construct the stepped errorband to fill between
|
||||||
|
energies = np.array(
|
||||||
|
(backspec.lo_edges, backspec.hi_edges)).T.flatten() # .tolist()
|
||||||
|
upper = np.array((rates + uncert, rates + uncert)).T.flatten() # .tolist()
|
||||||
|
lower = np.array((rates - uncert, rates - uncert)).T.flatten() # .tolist()
|
||||||
|
p2 = errorband(energies, upper, lower, ax, color=err_color,
|
||||||
|
alpha=err_alpha,
|
||||||
|
linestyle='-', **kwargs)
|
||||||
|
refs = [p1, p2]
|
||||||
|
return refs
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- DRM ----------#
|
||||||
|
def response_matrix(phot_energies, chan_energies, matrix, ax, cmap='Greens',
|
||||||
|
num_contours=100, norm=None, **kwargs):
|
||||||
|
"""Make a filled contour plot of a response matrix
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
phot_energies: np.array
|
||||||
|
The incident photon energy bin centroids
|
||||||
|
chan_energies: np.array
|
||||||
|
The recorded energy channel centroids
|
||||||
|
matrix: np.array
|
||||||
|
The effective area matrix corresponding to the photon bin and
|
||||||
|
energy channels
|
||||||
|
ax: matplotlib.axes
|
||||||
|
The axis on which to plot
|
||||||
|
cmap: str, optional
|
||||||
|
The color map to use. Default is 'Greens'
|
||||||
|
num_contours: int, optional
|
||||||
|
The number of contours to draw. These will be equally spaced in
|
||||||
|
log-space. Default is 100
|
||||||
|
norm: matplotlib.colors.Normalize or similar, optional
|
||||||
|
The normalization used to scale the colormapping to the heatmap values.
|
||||||
|
This can be initialized by Normalize, LogNorm, SymLogNorm, PowerNorm,
|
||||||
|
or some custom normalization.
|
||||||
|
**kwargs: optional
|
||||||
|
Other keyword arguments to be passed to matplotlib.pyplot.contourf
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
-----------
|
||||||
|
image: matplotlib.collections.QuadMesh
|
||||||
|
The reference to the plot object
|
||||||
|
"""
|
||||||
|
mask = (matrix > 0.0)
|
||||||
|
levels = np.logspace(np.log10(matrix[mask].min()),
|
||||||
|
np.log10(matrix.max()), num_contours)
|
||||||
|
|
||||||
|
image = ax.contourf(phot_energies, chan_energies, matrix, levels=levels,
|
||||||
|
cmap=cmap, norm=norm)
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
def effective_area(bins, ax, color='C0', orientation='vertical', **kwargs):
|
||||||
|
"""Plot a histogram of the effective area of an instrument response
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
bins: Bins
|
||||||
|
The histogram of effective area
|
||||||
|
ax: matplotlib.axes
|
||||||
|
The axis on which to plot
|
||||||
|
**kwargs: optional
|
||||||
|
Other plotting options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
-----------
|
||||||
|
refs: list
|
||||||
|
The reference to the plot objects
|
||||||
|
"""
|
||||||
|
bin_segs = bins.contiguous_bins()
|
||||||
|
refs = []
|
||||||
|
for seg in bin_segs:
|
||||||
|
edges = np.concatenate(
|
||||||
|
([seg.lo_edges[0]], seg.lo_edges, [seg.lo_edges[-1]]))
|
||||||
|
counts = np.concatenate(([0.0], seg.counts, [0.0]))
|
||||||
|
if orientation == 'horizontal':
|
||||||
|
p = ax.step(counts, edges, where='post', color=color, **kwargs)
|
||||||
|
else:
|
||||||
|
p = ax.step(edges, counts, where='post', color=color, **kwargs)
|
||||||
|
refs.append(p)
|
||||||
|
return refs
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Earth and Orbital ----------#
|
||||||
|
def saa_polygon(m, color='darkred', alpha=0.4, **kwargs):
|
||||||
|
"""Plot the SAA polygon on Basemap
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
m: Basemap
|
||||||
|
The basemap references
|
||||||
|
color: str, optional
|
||||||
|
The color of the polygon
|
||||||
|
alpha: float, optional
|
||||||
|
The alpha opacity of the interior of the polygon
|
||||||
|
kwargs: optional
|
||||||
|
Other plotting keywordss
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
-----------
|
||||||
|
poly: Polygon
|
||||||
|
The SAA polygon object
|
||||||
|
"""
|
||||||
|
# Define the SAA boundaries
|
||||||
|
lat_saa, lon_saa = saa_boundary()
|
||||||
|
|
||||||
|
edge = colorConverter.to_rgba(color, alpha=1.0)
|
||||||
|
face = colorConverter.to_rgba(color, alpha=alpha)
|
||||||
|
|
||||||
|
# plot the polygon
|
||||||
|
x, y = m(lon_saa, lat_saa)
|
||||||
|
xy = list(zip(x, y))
|
||||||
|
poly = Polygon(xy, edgecolor=edge, facecolor=face, **kwargs)
|
||||||
|
return poly
|
||||||
|
|
||||||
|
|
||||||
|
def mcilwain_map(lat_range, lon_range, m, ax, saa_mask=False, color=None,
|
||||||
|
alpha=0.5, **kwargs):
|
||||||
|
"""Plot the McIlwain L heatmap on a Basemap
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
lat_range: (float, float)
|
||||||
|
The latitude range
|
||||||
|
lon_range: (float, float)
|
||||||
|
The longitude range
|
||||||
|
m: Basemap
|
||||||
|
The basemap references
|
||||||
|
ax: matplotlib.axes
|
||||||
|
The plot axes references
|
||||||
|
saa_mask: bool, optional
|
||||||
|
If True, mask out the SAA from the heatmap. Default is False.
|
||||||
|
color: str, optional
|
||||||
|
The color of the heatmap
|
||||||
|
alpha: float, optional
|
||||||
|
The alpha opacity of the heatmap
|
||||||
|
kwargs: optional
|
||||||
|
Other plotting keywords
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
-----------
|
||||||
|
image: QuadContourSet
|
||||||
|
The heatmap plot object
|
||||||
|
"""
|
||||||
|
# do create an array on the earth
|
||||||
|
lat_array = np.linspace(*lat_range, 108)
|
||||||
|
lon_array = np.linspace(*lon_range, 720)
|
||||||
|
LAT, LON = np.meshgrid(lat_array, lon_array)
|
||||||
|
# convert to projection coordinates
|
||||||
|
mLON, mLAT = m(LON, LAT)
|
||||||
|
# mcilwain l over the grid
|
||||||
|
mcl = calc_mcilwain_l(LAT, LON)
|
||||||
|
|
||||||
|
# if we want to mask out the SAA
|
||||||
|
if saa_mask:
|
||||||
|
saa_path = saa_polygon(m).get_path()
|
||||||
|
mask = saa_path.contains_points(
|
||||||
|
np.array((mLON.ravel(), mLAT.ravel())).T)
|
||||||
|
mcl[mask.reshape(mcl.shape)] = 0.0
|
||||||
|
|
||||||
|
# do the plot
|
||||||
|
levels = [0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7]
|
||||||
|
image = ax.contourf(mLON, mLAT, mcl, levels=levels, alpha=alpha, **kwargs)
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
def earth_line(lat, lon, m, color='black', alpha=0.4, **kwargs):
|
||||||
|
"""Plot a line on the Earth (e.g. orbit)
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
lat: np.array
|
||||||
|
Array of latitudes
|
||||||
|
lon: np.array
|
||||||
|
Array of longitudes
|
||||||
|
m: Basemap
|
||||||
|
The basemap references
|
||||||
|
color: str, optional
|
||||||
|
The color of the lines
|
||||||
|
alpha: float, optional
|
||||||
|
The alpha opacity of line
|
||||||
|
kwargs: optional
|
||||||
|
Other plotting keywords
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
-----------
|
||||||
|
refs: list
|
||||||
|
The list of line plot object references
|
||||||
|
"""
|
||||||
|
lon[(lon > 180.0)] -= 360.0
|
||||||
|
path = np.vstack((lon, lat))
|
||||||
|
isplit = np.nonzero(np.abs(np.diff(path[0])) > 5.0)[0]
|
||||||
|
segments = np.split(path, isplit + 1, axis=1)
|
||||||
|
|
||||||
|
refs = []
|
||||||
|
for segment in segments:
|
||||||
|
x, y = m(segment[0], segment[1])
|
||||||
|
refs.append(m.plot(x, y, color=color, alpha=alpha, **kwargs))
|
||||||
|
return refs
|
||||||
|
|
||||||
|
|
||||||
|
def earth_points(lat, lon, m, color='black', alpha=1.0, **kwargs):
|
||||||
|
"""Plot a point or points on the Earth
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
-----------
|
||||||
|
lat: np.array
|
||||||
|
Array of latitudes
|
||||||
|
lon: np.array
|
||||||
|
Array of longitudes
|
||||||
|
m: Basemap
|
||||||
|
The basemap references
|
||||||
|
color: str, optional
|
||||||
|
The color of the lines
|
||||||
|
alpha: float, optional
|
||||||
|
The alpha opacity of line
|
||||||
|
kwargs: optional
|
||||||
|
Other plotting keywords
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
-----------
|
||||||
|
ref:
|
||||||
|
The scatter plot object reference
|
||||||
|
"""
|
||||||
|
lon[(lon > 180.0)] -= 360.0
|
||||||
|
x, y = m(lon, lat)
|
||||||
|
if 's' not in kwargs.keys():
|
||||||
|
kwargs['s'] = 1000
|
||||||
|
ref = m.scatter(x, y, color=color, alpha=alpha, **kwargs)
|
||||||
|
return [ref]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Sky and Fermi Inertial Coordinates ----------#
|
||||||
|
def sky_point(x, y, ax, flipped=True, fermi=False, **kwargs):
|
||||||
|
"""Plot a point on the sky defined by the RA and Dec
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
-----------
|
||||||
|
x: float
|
||||||
|
The azimuthal coordinate (RA or Fermi azimuth), in degrees
|
||||||
|
y: float
|
||||||
|
The polar coordinate (Dec or Fermi zenith), in degrees
|
||||||
|
ax: matplotlib.axes
|
||||||
|
Plot axes object
|
||||||
|
flipped: bool, optional
|
||||||
|
If True, the azimuthal axis is flipped, following equatorial convention
|
||||||
|
fermi: bool, optional
|
||||||
|
If True, plot in Fermi spacecraft coordinates, else plot in equatorial.
|
||||||
|
Default is False.
|
||||||
|
**kwargs: optional
|
||||||
|
Other plotting options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
----------
|
||||||
|
point: matplotlib.collections.PathCollection
|
||||||
|
The plot object
|
||||||
|
"""
|
||||||
|
theta = np.array(np.deg2rad(y))
|
||||||
|
phi = np.array(np.deg2rad(x - 180.0))
|
||||||
|
if fermi:
|
||||||
|
flipped = False
|
||||||
|
theta = np.pi / 2.0 - theta
|
||||||
|
phi -= np.pi
|
||||||
|
phi[phi < -np.pi] += 2 * np.pi
|
||||||
|
|
||||||
|
if flipped:
|
||||||
|
phi = -phi
|
||||||
|
point = ax.scatter(phi, theta, **kwargs)
|
||||||
|
return point
|
||||||
|
|
||||||
|
|
||||||
|
def sky_line(x, y, ax, flipped=True, fermi=False, **kwargs):
|
||||||
|
"""Plot a line on a sky map, wrapping at the meridian
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
-----------
|
||||||
|
x: np.array
|
||||||
|
The azimuthal coordinates (RA or Fermi azimuth), in degrees
|
||||||
|
y: np.array
|
||||||
|
The polar coordinates (Dec or Fermi zenith), in degrees
|
||||||
|
ax: matplotlib.axes
|
||||||
|
Plot axes object
|
||||||
|
flipped: bool, optional
|
||||||
|
If True, the azimuthal axis is flipped, following equatorial convention
|
||||||
|
fermi: bool, optional
|
||||||
|
If True, plot in Fermi spacecraft coordinates, else plot in equatorial.
|
||||||
|
Default is False.
|
||||||
|
**kwargs: optional
|
||||||
|
Other plotting options
|
||||||
|
"""
|
||||||
|
theta = np.deg2rad(y)
|
||||||
|
phi = np.deg2rad(x - 180.0)
|
||||||
|
if fermi:
|
||||||
|
flipped = False
|
||||||
|
theta = np.pi / 2.0 - theta
|
||||||
|
phi -= np.pi
|
||||||
|
phi[phi < -np.pi] += 2 * np.pi
|
||||||
|
|
||||||
|
if flipped:
|
||||||
|
phi = -phi
|
||||||
|
seg = np.vstack((phi, theta))
|
||||||
|
|
||||||
|
# here is where we split the segments at the meridian
|
||||||
|
isplit = np.nonzero(np.abs(np.diff(seg[0])) > np.pi / 16.0)[0]
|
||||||
|
subsegs = np.split(seg, isplit + 1, axis=1)
|
||||||
|
|
||||||
|
# plot each path segment
|
||||||
|
segrefs = []
|
||||||
|
for seg in subsegs:
|
||||||
|
ref = ax.plot(seg[0], seg[1], **kwargs)
|
||||||
|
segrefs.append(ref)
|
||||||
|
|
||||||
|
return segrefs
|
||||||
|
|
||||||
|
|
||||||
|
def sky_circle(center_x, center_y, radius, ax, flipped=True, fermi=False,
|
||||||
|
face_color=None, face_alpha=None, edge_color=None,
|
||||||
|
edge_alpha=None, **kwargs):
|
||||||
|
"""Plot a circle on the sky
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
-----------
|
||||||
|
center_x: float
|
||||||
|
The azimuthal center (RA or Fermi azimuth), in degrees
|
||||||
|
center_y: float
|
||||||
|
The polar center (Dec or Fermi zenith), in degrees
|
||||||
|
radius: float
|
||||||
|
The ROI radius in degrees
|
||||||
|
color: str
|
||||||
|
The color of the ROI
|
||||||
|
ax: matplotlib.axes
|
||||||
|
Plot axes object
|
||||||
|
flipped: bool, optional
|
||||||
|
If True, the azimuthal axis is flipped, following equatorial convention
|
||||||
|
fermi: bool, optional
|
||||||
|
If True, plot in Fermi spacecraft coordinates, else plot in equatorial.
|
||||||
|
Default is False.
|
||||||
|
face_color: str, optional
|
||||||
|
The color of the circle fill
|
||||||
|
face_alpha: float, optional
|
||||||
|
The alpha of the circle fill
|
||||||
|
edge_color: str, optional
|
||||||
|
The color of the circle edge
|
||||||
|
edge_alpha: float, optional
|
||||||
|
The alpha of the circle edge
|
||||||
|
**kwargs: optional
|
||||||
|
Other plotting options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
-----------
|
||||||
|
patches: list of matplotlib.patches.Polygon
|
||||||
|
The circle polygons
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import mpl_toolkits.basemap
|
||||||
|
except:
|
||||||
|
raise ImportError('Cannot execute sky_circle due to missing Basemap.')
|
||||||
|
theta = np.deg2rad(90.0 - center_y)
|
||||||
|
phi = np.deg2rad(center_x)
|
||||||
|
if fermi:
|
||||||
|
flipped = False
|
||||||
|
theta = np.pi / 2.0 - theta
|
||||||
|
phi -= np.pi
|
||||||
|
if phi < -np.pi:
|
||||||
|
phi += 2 * np.pi
|
||||||
|
|
||||||
|
rad = np.deg2rad(radius)
|
||||||
|
|
||||||
|
# Call Leo's lalinference plotting helper functions
|
||||||
|
# The native matplotlib functions don't cut and display the circle polygon
|
||||||
|
# correctly on map projections
|
||||||
|
roi = make_circle_poly(rad, theta, phi, 100)
|
||||||
|
roi = cut_prime_meridian(roi)
|
||||||
|
|
||||||
|
# plot each polygon section
|
||||||
|
edge = colorConverter.to_rgba(edge_color, alpha=edge_alpha)
|
||||||
|
face = colorConverter.to_rgba(face_color, alpha=face_alpha)
|
||||||
|
patches = []
|
||||||
|
for section in roi:
|
||||||
|
section[:, 0] -= np.pi
|
||||||
|
if flipped:
|
||||||
|
section[:, 0] *= -1.0
|
||||||
|
patch = ax.add_patch(
|
||||||
|
plt.Polygon(section, facecolor=face, edgecolor=edge, \
|
||||||
|
**kwargs))
|
||||||
|
patches.append(patch)
|
||||||
|
return patches
|
||||||
|
|
||||||
|
|
||||||
|
def sky_annulus(center_x, center_y, radius, width, ax, color='black',
|
||||||
|
alpha=0.3,
|
||||||
|
fermi=False, flipped=True, **kwargs):
|
||||||
|
"""Plot an annulus on the sky defined by its center, radius, and width
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
-----------
|
||||||
|
center_x: float
|
||||||
|
The azimuthal center (RA or Fermi azimuth), in degrees
|
||||||
|
center_y: float
|
||||||
|
The polar center (Dec or Fermi zenith), in degrees
|
||||||
|
radius: float
|
||||||
|
The radius in degrees, defined as the angular distance from the center
|
||||||
|
to the middle of the width of the annulus band
|
||||||
|
width: float
|
||||||
|
The width of the annulus in degrees
|
||||||
|
ax: matplotlib.axes
|
||||||
|
Plot axes object
|
||||||
|
color: string, optional
|
||||||
|
The color of the annulus. Default is black.
|
||||||
|
alpha: float, optional
|
||||||
|
The opacity of the annulus. Default is 0.3
|
||||||
|
fermi: bool, optional
|
||||||
|
If True, plot in Fermi spacecraft coordinates, else plot in equatorial.
|
||||||
|
Default is False.
|
||||||
|
flipped: bool, optional
|
||||||
|
If True, the azimuthal axis is flipped, following equatorial convention
|
||||||
|
**kwargs: optional
|
||||||
|
Other plotting options
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import mpl_toolkits.basemap
|
||||||
|
except:
|
||||||
|
raise ImportError('Cannot execute sky_annulus due to missing Basemap.')
|
||||||
|
edge = colorConverter.to_rgba(color, alpha=1.0)
|
||||||
|
face = colorConverter.to_rgba(color, alpha=alpha)
|
||||||
|
|
||||||
|
inner_radius = np.deg2rad(radius - width / 2.0)
|
||||||
|
outer_radius = np.deg2rad(radius + width / 2.0)
|
||||||
|
center_theta = np.deg2rad(90.0 - center_y)
|
||||||
|
center_phi = np.deg2rad(center_x)
|
||||||
|
if fermi:
|
||||||
|
flipped = False
|
||||||
|
center_theta = np.pi / 2.0 - center_theta
|
||||||
|
center_phi = np.deg2rad(center_x + 180.0)
|
||||||
|
|
||||||
|
# get the plot points for the inner and outer circles
|
||||||
|
inner = make_circle_poly(inner_radius, center_theta, center_phi, 100)
|
||||||
|
inner = cut_prime_meridian(inner)
|
||||||
|
outer = make_circle_poly(outer_radius, center_theta, center_phi, 100)
|
||||||
|
outer = cut_prime_meridian(outer)
|
||||||
|
|
||||||
|
x1 = []
|
||||||
|
y1 = []
|
||||||
|
x2 = []
|
||||||
|
y2 = []
|
||||||
|
polys = []
|
||||||
|
# plot the inner circle
|
||||||
|
for section in inner:
|
||||||
|
section[:, 0] -= np.pi
|
||||||
|
if flipped:
|
||||||
|
section[:, 0] *= -1.0
|
||||||
|
polys.append(plt.Polygon(section, ec=edge, fill=False, **kwargs))
|
||||||
|
ax.add_patch(polys[-1])
|
||||||
|
x1.extend(section[:, 0])
|
||||||
|
y1.extend(section[:, 1])
|
||||||
|
|
||||||
|
# plot the outer circle
|
||||||
|
for section in outer:
|
||||||
|
section[:, 0] -= np.pi
|
||||||
|
if flipped:
|
||||||
|
section[:, 0] *= -1.0
|
||||||
|
polys.append(plt.Polygon(section, ec=edge, fill=False, **kwargs))
|
||||||
|
ax.add_patch(polys[-1])
|
||||||
|
x2.extend(section[:, 0])
|
||||||
|
y2.extend(section[:, 1])
|
||||||
|
|
||||||
|
# organize and plot the fill between the circles
|
||||||
|
# organize and plot the fill between the circles
|
||||||
|
x1.append(x1[0])
|
||||||
|
y1.append(y1[0])
|
||||||
|
x2.append(x2[0])
|
||||||
|
y2.append(y2[0])
|
||||||
|
x2 = x2[::-1]
|
||||||
|
y2 = y2[::-1]
|
||||||
|
xs = np.concatenate((x1, x2))
|
||||||
|
ys = np.concatenate((y1, y2))
|
||||||
|
f = ax.fill(np.ravel(xs), np.ravel(ys), facecolor=face, zorder=1000)
|
||||||
|
|
||||||
|
refs = polys
|
||||||
|
refs.append(f)
|
||||||
|
return refs
|
||||||
|
|
||||||
|
|
||||||
|
def sky_polygon(x, y, ax, face_color=None, edge_color=None, edge_alpha=1.0,
|
||||||
|
face_alpha=0.3, flipped=True, fermi=False, **kwargs):
|
||||||
|
"""Plot single polygon on a sky map, wrapping at the meridian
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
-----------
|
||||||
|
paths: matplotlib.path.Path
|
||||||
|
Object containing the contour path
|
||||||
|
ax: matplotlib.axes
|
||||||
|
Plot axes object
|
||||||
|
face_color: str, optional
|
||||||
|
The color of the polygon fill
|
||||||
|
face_alpha: float, optional
|
||||||
|
The alpha of the polygon fill
|
||||||
|
edge_color: str, optional
|
||||||
|
The color of the polygon edge
|
||||||
|
edge_alpha: float, optional
|
||||||
|
The alpha of the polygon edge
|
||||||
|
fermi: bool, optional
|
||||||
|
If True, plot in Fermi spacecraft coordinates, else plot in equatorial.
|
||||||
|
Default is False.
|
||||||
|
flipped: bool, optional
|
||||||
|
If True, the azimuthal axis is flipped, following equatorial convention
|
||||||
|
**kwargs: optional
|
||||||
|
Other plotting options
|
||||||
|
"""
|
||||||
|
line = sky_line(x, y, ax, color=edge_color, alpha=edge_alpha,
|
||||||
|
flipped=flipped,
|
||||||
|
fermi=fermi, **kwargs)
|
||||||
|
refs = line
|
||||||
|
|
||||||
|
theta = np.deg2rad(y)
|
||||||
|
phi = np.deg2rad(x - 180.0)
|
||||||
|
if fermi:
|
||||||
|
flipped = False
|
||||||
|
theta = np.pi / 2.0 - theta
|
||||||
|
phi -= np.pi
|
||||||
|
phi[phi < -np.pi] += 2 * np.pi
|
||||||
|
|
||||||
|
if flipped:
|
||||||
|
phi = -phi
|
||||||
|
|
||||||
|
# this is needed to determine when contour spans the full ra range
|
||||||
|
if (np.min(phi) == -np.pi) and (np.max(phi) == np.pi):
|
||||||
|
if np.mean(theta) > 0.0: # fill in positive dec
|
||||||
|
theta = np.insert(theta, 0, np.pi / 2.0)
|
||||||
|
theta = np.append(theta, np.pi / 2.0)
|
||||||
|
phi = np.insert(phi, 0, -np.pi)
|
||||||
|
phi = np.append(phi, np.pi)
|
||||||
|
else: # fill in negative dec
|
||||||
|
theta = np.insert(theta, 0, -np.pi / 2.0)
|
||||||
|
theta = np.append(theta, -np.pi / 2.0)
|
||||||
|
phi = np.insert(phi, 0, np.pi)
|
||||||
|
phi = np.append(phi, -np.pi)
|
||||||
|
|
||||||
|
f = ax.fill(phi, theta, color=face_color, alpha=face_alpha, **kwargs)
|
||||||
|
refs.append(f)
|
||||||
|
|
||||||
|
return refs
|
||||||
|
|
||||||
|
|
||||||
|
def galactic_plane(ax, flipped=True, fermi_quat=None, outer_color='dimgray',
|
||||||
|
inner_color='black', line_alpha=0.5, center_alpha=0.75,
|
||||||
|
**kwargs):
|
||||||
|
"""Plot the galactic plane on the sky
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
-----------
|
||||||
|
ax: matplotlib.axes
|
||||||
|
Plot axes object
|
||||||
|
outer_color: str, optional
|
||||||
|
The color of the outer line
|
||||||
|
inner_color: str, optional
|
||||||
|
The color of the inner line
|
||||||
|
line_alpha: float, optional
|
||||||
|
The alpha of the line
|
||||||
|
center_alpha: float, optional
|
||||||
|
The alpha of the center
|
||||||
|
fermi_quat: np.array, optional
|
||||||
|
If set, rotate the galactic plane into the Fermi frame
|
||||||
|
flipped: bool, optional
|
||||||
|
If True, the azimuthal axis is flipped, following equatorial convention
|
||||||
|
**kwargs: optional
|
||||||
|
Other plotting options
|
||||||
|
"""
|
||||||
|
fermi = False
|
||||||
|
# if a quaternion is sent, then plot in the Fermi frame
|
||||||
|
if fermi_quat is not None:
|
||||||
|
flipped = False
|
||||||
|
fermi = True
|
||||||
|
|
||||||
|
# longitude and latitude arrays
|
||||||
|
lon_array = np.arange(0, 360, dtype=float)
|
||||||
|
lat = np.zeros_like(lon_array)
|
||||||
|
gc = SkyCoord(l=lon_array * u.degree, b=lat * u.degree, frame='galactic')
|
||||||
|
ra, dec = gc.icrs.ra.deg, gc.icrs.dec.deg
|
||||||
|
# plot in Fermi frame
|
||||||
|
if fermi_quat is not None:
|
||||||
|
ra, dec = radec_to_spacecraft(ra, dec, fermi_quat)
|
||||||
|
|
||||||
|
line1 = sky_line(ra, dec, ax, color=outer_color, linewidth=3,
|
||||||
|
alpha=line_alpha, flipped=flipped, fermi=fermi)
|
||||||
|
line2 = sky_line(ra, dec, ax, color=inner_color, linewidth=1,
|
||||||
|
alpha=line_alpha, flipped=flipped, fermi=fermi)
|
||||||
|
|
||||||
|
# plot Galactic center
|
||||||
|
pt1 = sky_point(ra[0], dec[0], ax, marker='o', c=outer_color, s=100,
|
||||||
|
alpha=center_alpha, edgecolor=None, flipped=flipped,
|
||||||
|
fermi=fermi)
|
||||||
|
pt2 = sky_point(ra[0], dec[0], ax, marker='o', c=inner_color, s=20,
|
||||||
|
alpha=center_alpha, edgecolor=None, flipped=flipped,
|
||||||
|
fermi=fermi)
|
||||||
|
|
||||||
|
return [line1, line2, pt1, pt2]
|
||||||
|
|
||||||
|
|
||||||
|
def sky_heatmap(x, y, heatmap, ax, cmap='RdPu', norm=None, flipped=True,
|
||||||
|
fermi=False, **kwargs):
|
||||||
|
"""Plot a heatmap on the sky as a colormap gradient
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
-----------
|
||||||
|
x: np.array
|
||||||
|
The azimuthal coordinate array of the heatmap grid
|
||||||
|
y: np.array
|
||||||
|
The polar coordinate array of the heatmap grid
|
||||||
|
heatmap: np.array
|
||||||
|
The heatmap array, of shape (num_x, num_y)
|
||||||
|
ax: matplotlib.axes
|
||||||
|
Plot axes object
|
||||||
|
cmap: str, optional
|
||||||
|
The colormap. Default is 'RdPu'
|
||||||
|
norm: matplotlib.colors.Normalize or similar, optional
|
||||||
|
The normalization used to scale the colormapping to the heatmap values.
|
||||||
|
This can be initialized by Normalize, LogNorm, SymLogNorm, PowerNorm,
|
||||||
|
or some custom normalization. Default is PowerNorm(gamma=0.3).
|
||||||
|
flipped: bool, optional
|
||||||
|
If True, the azimuthal axis is flipped, following equatorial convention
|
||||||
|
fermi: bool, optional
|
||||||
|
If True, plot in Fermi spacecraft coordinates, else plot in equatorial.
|
||||||
|
Default is False.
|
||||||
|
**kwargs: optional
|
||||||
|
Other plotting options
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
--------
|
||||||
|
image: matplotlib.collections.QuadMesh
|
||||||
|
The reference to the heatmap plot object
|
||||||
|
"""
|
||||||
|
theta = np.deg2rad(y)
|
||||||
|
phi = np.deg2rad(x - 180.0)
|
||||||
|
if fermi:
|
||||||
|
flipped = False
|
||||||
|
theta = np.pi / 2.0 - theta
|
||||||
|
phi -= np.pi
|
||||||
|
phi[phi < -np.pi] += 2 * np.pi
|
||||||
|
shift = int(phi.shape[0] / 2.0)
|
||||||
|
phi = np.roll(phi, shift)
|
||||||
|
heatmap = np.roll(heatmap, shift, axis=1)
|
||||||
|
|
||||||
|
if flipped:
|
||||||
|
phi = -phi
|
||||||
|
|
||||||
|
image = ax.pcolormesh(phi, theta, heatmap, rasterized=True, cmap=cmap,
|
||||||
|
norm=norm)
|
||||||
|
return image
|
||||||
|
|
237
plot/lightcurve.py
Normal file
237
plot/lightcurve.py
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
# lightcurve.py: Plot class for lightcurves
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
from .gbmplot import GbmPlot, Histo, HistoErrorbars, HistoFilled, \
|
||||||
|
LightcurveBackground
|
||||||
|
from .lib import *
|
||||||
|
|
||||||
|
|
||||||
|
class Lightcurve(GbmPlot):
|
||||||
|
"""Class for plotting lightcurves and lightcurve paraphernalia.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
data (:class:`~gbm.data.primitives.TimeBins`, optional):
|
||||||
|
The lightcurve data to plot
|
||||||
|
background (:class:`~gbm.background.BackgroundRates`, optional):
|
||||||
|
The background rates to plot
|
||||||
|
**kwargs: Options to pass to :class:`~.gbmplot.GbmPlot`
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
ax (:class:`matplotlib.axes`): The matplotlib axes object for the plot
|
||||||
|
background (:class:`~.gbmplot.LightcurveBackground`):
|
||||||
|
The background plot element
|
||||||
|
canvas (Canvas Backend object): The plotting canvas, if set upon
|
||||||
|
initialization.
|
||||||
|
errorbars (:class:`~.gbmplot.HistoErrorbars`): The error bars plot element
|
||||||
|
fig (:class:`matplotlib.figure`): The matplotlib figure object
|
||||||
|
lightcurve (:class:`~.gbmplot.Histo`): The lightcurve plot element
|
||||||
|
selections (list of :class:`~.gbmplot.HistoFilled`):
|
||||||
|
The list of selection plot elements
|
||||||
|
xlim (float, float): The plotting range of the x axis.
|
||||||
|
This attribute can be set.
|
||||||
|
xscale (str): The scale of the x axis, either 'linear' or 'log'.
|
||||||
|
This attribute can be set.
|
||||||
|
ylim (float, float): The plotting range of the y axis.
|
||||||
|
This attribute can be set.
|
||||||
|
yscale (str): The scale of the y axis, either 'linear' or 'log'.
|
||||||
|
This attribute can be set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, data=None, background=None, canvas=None, axis=None,
|
||||||
|
**kwargs):
|
||||||
|
super().__init__(canvas=canvas, axis=axis, **kwargs)
|
||||||
|
|
||||||
|
self._lc = None
|
||||||
|
self._errorbars = None
|
||||||
|
self._bkgd = None
|
||||||
|
self._selections = []
|
||||||
|
|
||||||
|
# initialize the plot axes, labels, ticks, and scales
|
||||||
|
self._ax.set_xlabel('Time (s)', fontsize=PLOTFONTSIZE)
|
||||||
|
self._ax.set_ylabel('Count Rate (count/s)', fontsize=PLOTFONTSIZE)
|
||||||
|
self._ax.xaxis.set_tick_params(labelsize=PLOTFONTSIZE)
|
||||||
|
self._ax.yaxis.set_tick_params(labelsize=PLOTFONTSIZE)
|
||||||
|
self._ax.set_xscale('linear')
|
||||||
|
self._ax.set_yscale('linear')
|
||||||
|
|
||||||
|
# plot data and/or background if set on init
|
||||||
|
if data is not None:
|
||||||
|
self.set_data(data)
|
||||||
|
self._ax.set_xlim(data.range)
|
||||||
|
|
||||||
|
self._ax.set_ylim(0.9 * np.min(data.rates),
|
||||||
|
1.1 * np.max(data.rates))
|
||||||
|
if background is not None:
|
||||||
|
self.set_background(background)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lightcurve(self):
|
||||||
|
return self._lc
|
||||||
|
|
||||||
|
@property
|
||||||
|
def errorbars(self):
|
||||||
|
return self._errorbars
|
||||||
|
|
||||||
|
@property
|
||||||
|
def background(self):
|
||||||
|
return self._bkgd
|
||||||
|
|
||||||
|
@property
|
||||||
|
def selections(self):
|
||||||
|
return self._selections
|
||||||
|
|
||||||
|
def set_data(self, data):
|
||||||
|
"""Set the lightcurve plotting data. If a lightcurve already exists,
|
||||||
|
this triggers a replot of the lightcurve.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (:class:`~gbm.data.primitives.TimeBins`):
|
||||||
|
The lightcurve data to plot
|
||||||
|
"""
|
||||||
|
lc_color, lc_alpha, lc_kwargs = self._lc_settings()
|
||||||
|
self._lc = Histo(data, self._ax, color=lc_color, alpha=lc_alpha,
|
||||||
|
**lc_kwargs)
|
||||||
|
eb_color, eb_alpha, eb_kwargs = self._eb_settings()
|
||||||
|
self._errorbars = HistoErrorbars(data, self._ax, color=eb_color,
|
||||||
|
alpha=eb_alpha, **eb_kwargs)
|
||||||
|
|
||||||
|
def add_selection(self, data):
|
||||||
|
"""Add a selection to the plot. This adds a new selection to a list
|
||||||
|
of existing selections.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (:class:`~gbm.data.primitives.TimeBins`):
|
||||||
|
The lightcurve data selection to plot
|
||||||
|
"""
|
||||||
|
color, alpha, kwargs = self._selection_settings()
|
||||||
|
select = HistoFilled(data, self._ax, color=color, alpha=alpha,
|
||||||
|
**kwargs)
|
||||||
|
self._selections.append(select)
|
||||||
|
|
||||||
|
def set_background(self, background):
|
||||||
|
"""Set the background plotting data. If a background already exists,
|
||||||
|
this triggers a replot of the background.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
background (:class:`~gbm.background.BackgroundRates`):
|
||||||
|
The background model to plot
|
||||||
|
"""
|
||||||
|
color, cent_color, err_color, alpha, cent_alpha, err_alpha, \
|
||||||
|
kwargs = self._bkgd_settings()
|
||||||
|
self._bkgd = LightcurveBackground(background, self._ax, color=color,
|
||||||
|
cent_color=cent_color,
|
||||||
|
err_color=err_color,
|
||||||
|
alpha=alpha, cent_alpha=BKGD_ALPHA,
|
||||||
|
err_alpha=BKGD_ERROR_ALPHA,
|
||||||
|
zorder=1000)
|
||||||
|
|
||||||
|
def remove_data(self):
|
||||||
|
"""Remove the lightcurve from the plot.
|
||||||
|
"""
|
||||||
|
self._lc.remove()
|
||||||
|
self._lc = None
|
||||||
|
|
||||||
|
def remove_errorbars(self):
|
||||||
|
"""Remove the lightcurve error bars from the plot.
|
||||||
|
"""
|
||||||
|
self._errorbars.remove()
|
||||||
|
self._errorbars = None
|
||||||
|
|
||||||
|
def remove_background(self):
|
||||||
|
"""Remove the background from the plot.
|
||||||
|
"""
|
||||||
|
self._bkgd.remove()
|
||||||
|
self._bkgd = None
|
||||||
|
|
||||||
|
def remove_selections(self):
|
||||||
|
"""Remove the selections from the plot.
|
||||||
|
"""
|
||||||
|
[selection.remove() for selection in self._selections]
|
||||||
|
self._selections = []
|
||||||
|
|
||||||
|
def _lc_settings(self):
|
||||||
|
"""The default settings for the lightcurve. If a lightcurve already
|
||||||
|
exists, use its settings instead.
|
||||||
|
"""
|
||||||
|
if self._lc is None:
|
||||||
|
lc_color = DATA_COLOR
|
||||||
|
lc_alpha = None
|
||||||
|
lc_kwargs = {}
|
||||||
|
else:
|
||||||
|
lc_color = self._lc.color
|
||||||
|
lc_alpha = self._lc.alpha
|
||||||
|
lc_kwargs = self._lc._kwargs
|
||||||
|
return (lc_color, lc_alpha, lc_kwargs)
|
||||||
|
|
||||||
|
def _eb_settings(self):
|
||||||
|
"""The default settings for the errorbars. If a lightcurve already
|
||||||
|
exists, use its errorbars settings instead.
|
||||||
|
"""
|
||||||
|
if self._errorbars is None:
|
||||||
|
eb_color = DATA_ERROR_COLOR
|
||||||
|
eb_alpha = None
|
||||||
|
eb_kwargs = {}
|
||||||
|
else:
|
||||||
|
eb_color = self._errorbars.color
|
||||||
|
eb_alpha = self._errorbars.alpha
|
||||||
|
eb_kwargs = self._errorbars._kwargs
|
||||||
|
return (eb_color, eb_alpha, eb_kwargs)
|
||||||
|
|
||||||
|
def _bkgd_settings(self):
|
||||||
|
"""The default settings for the background. If a background already
|
||||||
|
exists, use its settings instead.
|
||||||
|
"""
|
||||||
|
if self._bkgd is None:
|
||||||
|
color = BKGD_COLOR
|
||||||
|
cent_color = None
|
||||||
|
err_color = None
|
||||||
|
alpha = None
|
||||||
|
cent_alpha = BKGD_ALPHA
|
||||||
|
err_alpha = BKGD_ERROR_ALPHA
|
||||||
|
kwargs = {'linewidth': BKGD_WIDTH}
|
||||||
|
else:
|
||||||
|
color = self._bkgd.color
|
||||||
|
cent_color = self._bkgd.cent_color
|
||||||
|
err_color = self._bkgd.err_color
|
||||||
|
alpha = self._bkgd.alpha
|
||||||
|
cent_alpha = self._bkgd.cent_alpha
|
||||||
|
err_alpha = self._bkgd.err_alpha
|
||||||
|
kwargs = self._bkgd._kwargs
|
||||||
|
return color, cent_color, err_color, alpha, cent_alpha, err_alpha, kwargs
|
||||||
|
|
||||||
|
def _selection_settings(self):
|
||||||
|
"""The default settings for a selection. If a selection already
|
||||||
|
exists, use its settings instead.
|
||||||
|
"""
|
||||||
|
if len(self._selections) == 0:
|
||||||
|
color = DATA_SELECTED_COLOR
|
||||||
|
alpha = DATA_SELECTED_ALPHA
|
||||||
|
kwargs = {}
|
||||||
|
else:
|
||||||
|
color = self._selections[0].color
|
||||||
|
alpha = self._selections[0].alpha
|
||||||
|
kwargs = self._selections[0]._kwargs
|
||||||
|
return color, alpha, kwargs
|
309
plot/model.py
Normal file
309
plot/model.py
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
# model.py: Plot class for spectral fits and models
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
from .gbmplot import GbmPlot, Histo, ModelData, Collection, ModelSamples
|
||||||
|
from .lib import *
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
class ModelFit(GbmPlot):
|
||||||
|
"""Class for plotting spectral fits.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
fitter (:class:`~gbm.spectra.fitting.SpectralFitter`, optional):
|
||||||
|
The spectral fitter
|
||||||
|
view (str, optional): The plot view, one of 'counts', 'photon',
|
||||||
|
'energy' or 'nufnu'. Default is 'counts'
|
||||||
|
resid (bool, optional): If True, plots the residuals in counts view.
|
||||||
|
Default is True.
|
||||||
|
**kwargs: Options to pass to :class:`~.gbmplot.GbmPlot`
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
ax (:class:`matplotlib.axes`): The matplotlib axes object for the plot
|
||||||
|
canvas (Canvas Backend object): The plotting canvas, if set upon
|
||||||
|
initialization.
|
||||||
|
count_data (Collection of :class:`~.gbmplot.ModelData`):
|
||||||
|
The count data plot elements
|
||||||
|
count_models (Collection of :class:`~.gbmplot.Histo`):
|
||||||
|
The count model plot elements
|
||||||
|
fig (:class:`matplotlib.figure`): The matplotlib figure object
|
||||||
|
model_spectrum (Collection of :class:`~.gbmplot.ModelSamples`):
|
||||||
|
The model spectrum sample elements
|
||||||
|
residuals (Collection of :class:`~gbmplot.ModelData`):
|
||||||
|
The fit residual plot elements
|
||||||
|
view (str): The current plot view
|
||||||
|
xlim (float, float): The plotting range of the x axis.
|
||||||
|
This attribute can be set.
|
||||||
|
xscale (str): The scale of the x axis, either 'linear' or 'log'.
|
||||||
|
This attribute can be set.
|
||||||
|
ylim (float, float): The plotting range of the y axis.
|
||||||
|
This attribute can be set.
|
||||||
|
yscale (str): The scale of the y axis, either 'linear' or 'log'.
|
||||||
|
This attribute can be set.
|
||||||
|
"""
|
||||||
|
# Define a list of default plotting colors to cycle through
|
||||||
|
colors = '#7F3C8D,#11A579,#3969AC,#F2B701,#E73F74,#80BA5A,#E68310,#008695,#CF1C90,#f97b72,#4b4b8f,#A5AA99'.split(',')
|
||||||
|
_min_y = 1e-10
|
||||||
|
|
||||||
|
def __init__(self, fitter=None, canvas=None, view='counts', resid=True,
|
||||||
|
interactive=True):
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore", category=np.VisibleDeprecationWarning)
|
||||||
|
|
||||||
|
self._figure, axes = plt.subplots(2, 1, sharex=True, sharey=False,
|
||||||
|
figsize=(5.7, 6.7), dpi=100,
|
||||||
|
gridspec_kw={'height_ratios': [3,1]})
|
||||||
|
plt.subplots_adjust(hspace=0)
|
||||||
|
self._ax = axes[0]
|
||||||
|
self._resid_ax = axes[1]
|
||||||
|
|
||||||
|
self._view = view
|
||||||
|
self._fitter = None
|
||||||
|
self._count_models = Collection()
|
||||||
|
self._count_data = Collection()
|
||||||
|
self._resids = Collection()
|
||||||
|
self._model_spectrum = None
|
||||||
|
|
||||||
|
|
||||||
|
# plot data and/or background if set on init
|
||||||
|
if fitter is not None:
|
||||||
|
self.set_fit(fitter, resid=resid)
|
||||||
|
if interactive:
|
||||||
|
plt.ion()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def view(self):
|
||||||
|
return self._view
|
||||||
|
|
||||||
|
@property
|
||||||
|
def count_models(self):
|
||||||
|
return self._count_models
|
||||||
|
|
||||||
|
@property
|
||||||
|
def count_data(self):
|
||||||
|
return self._count_data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def residuals(self):
|
||||||
|
return self._resids
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model_spectrum(self):
|
||||||
|
return self._model_spectrum
|
||||||
|
|
||||||
|
def set_fit(self, fitter, resid=False):
|
||||||
|
"""Set the fitter. If a fitter already exists, this triggers a replot of
|
||||||
|
the fit.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fitter (:class:`~gbm.spectra.fitting.SpectralFitter`):
|
||||||
|
The spectral fitter for which a fit has been performed
|
||||||
|
resid (bool, optional): If True, plot the fit residuals
|
||||||
|
"""
|
||||||
|
self._fitter = fitter
|
||||||
|
|
||||||
|
if self._view == 'counts':
|
||||||
|
self.count_spectrum()
|
||||||
|
if resid:
|
||||||
|
self.show_residuals()
|
||||||
|
else:
|
||||||
|
self.hide_residuals()
|
||||||
|
|
||||||
|
elif self._view == 'photon':
|
||||||
|
self.photon_spectrum()
|
||||||
|
|
||||||
|
elif self._view == 'energy':
|
||||||
|
self.energy_spectrum()
|
||||||
|
|
||||||
|
elif self._view == 'nufnu':
|
||||||
|
self.nufnu_spectrum()
|
||||||
|
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def count_spectrum(self):
|
||||||
|
"""Plot the count spectrum fit
|
||||||
|
"""
|
||||||
|
self._view = 'counts'
|
||||||
|
self._ax.clear()
|
||||||
|
|
||||||
|
model_counts = self._fitter.model_count_spectrum()
|
||||||
|
energy, chanwidths, data_counts, data_counts_err, ulmasks = \
|
||||||
|
self._fitter.data_count_spectrum()
|
||||||
|
|
||||||
|
for i in range(self._fitter.num_sets):
|
||||||
|
det = self._fitter.detectors[i]
|
||||||
|
self._count_models.insert(det, Histo(model_counts[i], self._ax,
|
||||||
|
edges_to_zero=False, color=self.colors[i],
|
||||||
|
alpha=1.0, label=det))
|
||||||
|
self._count_data.insert(det, ModelData(energy[i], data_counts[i],
|
||||||
|
chanwidths[i], data_counts_err[i],
|
||||||
|
self._ax, ulmask=ulmasks[i],
|
||||||
|
color=self.colors[i],
|
||||||
|
alpha=0.7, linewidth=0.9))
|
||||||
|
|
||||||
|
self._ax.set_ylabel(r'Rate [count s$^{-1}$ keV$^{-1}$]')
|
||||||
|
self._set_view()
|
||||||
|
self._ax.legend()
|
||||||
|
|
||||||
|
def photon_spectrum(self, **kwargs):
|
||||||
|
"""Plot the photon spectrum model
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_samples (int, optional): The number of sample spectra.
|
||||||
|
Default is 10.
|
||||||
|
"""
|
||||||
|
self._view = 'photon'
|
||||||
|
self._plot_spectral_model(**kwargs)
|
||||||
|
self._ax.set_ylabel(r'Photon Flux [ph cm$^{-2}$ s$^{-1}$ keV$^{-1}$]', fontsize=PLOTFONTSIZE)
|
||||||
|
|
||||||
|
def energy_spectrum(self, **kwargs):
|
||||||
|
"""Plot the energy spectrum model
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_samples (int, optional): The number of sample spectra.
|
||||||
|
Default is 100.
|
||||||
|
"""
|
||||||
|
self._view = 'energy'
|
||||||
|
self._plot_spectral_model(**kwargs)
|
||||||
|
self._ax.set_ylabel(r'Energy Flux [ph cm$^{-2}$ s$^{-1}$]', fontsize=PLOTFONTSIZE)
|
||||||
|
|
||||||
|
def nufnu_spectrum(self, **kwargs):
|
||||||
|
"""Plot the nuFnu spectrum model
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_samples (int, optional): The number of sample spectra.
|
||||||
|
Default is 100.
|
||||||
|
"""
|
||||||
|
self._view = 'nufnu'
|
||||||
|
self._plot_spectral_model(**kwargs)
|
||||||
|
self._ax.set_ylabel(r'$\nu F_\nu$ [keV ph cm$^{-2}$ s$^{-1}$]', fontsize=PLOTFONTSIZE)
|
||||||
|
|
||||||
|
def show_residuals(self, sigma=True):
|
||||||
|
"""Show the fit residuals
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sigma (bool, optional): If True, plot the residuals in units of
|
||||||
|
model sigma, otherwise in units of counts.
|
||||||
|
Default is True.
|
||||||
|
"""
|
||||||
|
# if we don't already have residuals axis
|
||||||
|
if len(self._figure.axes) == 1:
|
||||||
|
self._figure.add_axes(self._resid_ax)
|
||||||
|
|
||||||
|
# get the residuals
|
||||||
|
energy, chanwidths, resid, resid_err = self._fitter.residuals(sigma=sigma)
|
||||||
|
|
||||||
|
# plot for each detector/dataset
|
||||||
|
ymin, ymax = ([], [])
|
||||||
|
for i in range(self._fitter.num_sets):
|
||||||
|
det = self._fitter.detectors[i]
|
||||||
|
self._resids.insert(det, ModelData(energy[i], resid[i], chanwidths[i],
|
||||||
|
resid_err[i], self._resid_ax, color=self.colors[i],
|
||||||
|
alpha=0.7, linewidth=0.9))
|
||||||
|
ymin.append((resid[i]-resid_err[i]).min())
|
||||||
|
ymax.append((resid[i]+resid_err[i]).max())
|
||||||
|
# the zero line
|
||||||
|
self._resid_ax.axhline(0.0, color='black')
|
||||||
|
self._resid_ax.set_xlabel('Energy [kev]', fontsize=PLOTFONTSIZE)
|
||||||
|
|
||||||
|
if sigma:
|
||||||
|
self._resid_ax.set_ylabel('Residuals [sigma]', fontsize=PLOTFONTSIZE)
|
||||||
|
else:
|
||||||
|
self._resid_ax.set_ylabel('Residuals [counts]', fontsize=PLOTFONTSIZE)
|
||||||
|
|
||||||
|
# we have to set the y-axis range manually, because the y-axis
|
||||||
|
# autoscale is broken (known issue) in matplotlib for this situation
|
||||||
|
ymin = np.min(ymin)
|
||||||
|
ymax = np.max(ymax)
|
||||||
|
self._resid_ax.set_ylim((1.0-np.sign(ymin)*0.1)*ymin,
|
||||||
|
(1.0+np.sign(ymax)*0.1)*ymax)
|
||||||
|
|
||||||
|
def hide_residuals(self):
|
||||||
|
"""Hide the fit residuals
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._figure.delaxes(self._resid_ax)
|
||||||
|
self._ax.xaxis.set_tick_params(which='both', labelbottom=True)
|
||||||
|
self._ax.set_xlabel('Energy (keV)', fontsize=PLOTFONTSIZE)
|
||||||
|
except:
|
||||||
|
print('Residuals already hidden')
|
||||||
|
|
||||||
|
def _set_view(self):
|
||||||
|
"""Set the view properties
|
||||||
|
"""
|
||||||
|
self._ax.set_xlim(self._fitter.energy_range)
|
||||||
|
self._ax.yaxis.set_tick_params(labelsize=PLOTFONTSIZE)
|
||||||
|
self._ax.set_xscale('log')
|
||||||
|
self._ax.set_yscale('log')
|
||||||
|
self._ax.set_xlabel('Energy [kev]', fontsize=PLOTFONTSIZE)
|
||||||
|
|
||||||
|
def _plot_spectral_model(self, num_samples=100, plot_components=True):
|
||||||
|
"""Plot the spectral model by sampling from the Gaussian approximation
|
||||||
|
to the parameters' posterior.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_samples (int, optional): The number of sample spectra.
|
||||||
|
Default is 100.
|
||||||
|
"""
|
||||||
|
# clean plot and hide residuals if any
|
||||||
|
warnings.filterwarnings("ignore", category=UserWarning)
|
||||||
|
self._ax.clear()
|
||||||
|
self.hide_residuals()
|
||||||
|
|
||||||
|
|
||||||
|
num_comp = self._fitter.num_components
|
||||||
|
comps = self._fitter.function_components
|
||||||
|
name = self._fitter.function_name
|
||||||
|
|
||||||
|
# if the number of model components is > 1, plot each one
|
||||||
|
if (num_comp > 1) and (plot_components):
|
||||||
|
energies, samples = self._fitter.sample_spectrum(which=self._view,
|
||||||
|
num_samples=num_samples,
|
||||||
|
components=True)
|
||||||
|
self._spectrum_model = [ModelSamples(energies, samples[:,i,:], self._ax,
|
||||||
|
label=comps[i], color=self.colors[i+1],
|
||||||
|
alpha=0.1, lw=0.3) for i in range(num_comp)]
|
||||||
|
|
||||||
|
samples = samples.sum(axis=1)
|
||||||
|
else:
|
||||||
|
# or just plot the function
|
||||||
|
self._spectrum_model = []
|
||||||
|
energies, samples = self._fitter.sample_spectrum(which=self._view,
|
||||||
|
num_samples=num_samples)
|
||||||
|
y_max = samples.max(axis=(1,0))
|
||||||
|
self._spectrum_model.append(ModelSamples(energies, samples, self._ax,
|
||||||
|
label=name, color=self.colors[0],
|
||||||
|
alpha=0.1, lw=0.3))
|
||||||
|
self._set_view()
|
||||||
|
|
||||||
|
# fix the alphas for the legend
|
||||||
|
legend = self._ax.legend()
|
||||||
|
for lh in legend.legendHandles:
|
||||||
|
lh.set_alpha(1)
|
||||||
|
lh.set_linewidth(1.0)
|
||||||
|
|
||||||
|
if self._ax.get_ylim()[0] < self._min_y:
|
||||||
|
self._ax.set_ylim(self._min_y, 10.0*y_max)
|
486
plot/skyplot.py
Normal file
486
plot/skyplot.py
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
# skyplot.py: Plot class for Fermi observing sky maps
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from .gbmplot import Collection
|
||||||
|
from .gbmplot import DetectorPointing, GalacticPlane, SkyHeatmap, SkyPolygon
|
||||||
|
from .gbmplot import GbmPlot, SkyLine, SkyCircle, Sun
|
||||||
|
from .lib import *
|
||||||
|
from ..coords import get_sun_loc
|
||||||
|
|
||||||
|
|
||||||
|
class SkyPlot(GbmPlot):
|
||||||
|
"""Class for plotting on the sky in equatorial coordinates
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
projection (str, optional): The projection of the map.
|
||||||
|
Default is 'mollweide'
|
||||||
|
flipped (bool, optional):
|
||||||
|
If True, the RA axis is flipped, following astronomical convention. \
|
||||||
|
Default is True.
|
||||||
|
**kwargs: Options to pass to :class:`~.gbmplot.GbmPlot`
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
ax (:class:`matplotlib.axes`): The matplotlib axes object for the plot
|
||||||
|
background_color (str): The color of the plot background. This attribute
|
||||||
|
can be set.
|
||||||
|
canvas (Canvas Backend object): The plotting canvas, if set upon
|
||||||
|
initialization.
|
||||||
|
canvas_color (str): The color of the plotting canvas. This attribute
|
||||||
|
can be set.
|
||||||
|
detectors (:class:`~.gbmplot.Collection` of :class:`~.gbmplot.DetectorPointing`):
|
||||||
|
The collection of detector plot elements
|
||||||
|
earth (:class:`~.gbmplot.SkyCircle`): The Earth plot element
|
||||||
|
fig (:class:`matplotlib.figure`): The matplotlib figure object
|
||||||
|
fontsize (int): The font size of the text labels. This attribute can be set.
|
||||||
|
gc (:class:`~.gbmplot.GalacticPlane`):
|
||||||
|
The reference to the galactic plane plot element
|
||||||
|
loc_contours: (:class:`~.gbmplot.Collection` of :class:`~.gbmplot.SkyLine` \
|
||||||
|
or :class:`~.gbplot.SkyPolygon`):
|
||||||
|
The localization contour plot elements
|
||||||
|
loc_posterior (:class:`~.gbmplot.SkyHeatmap`):
|
||||||
|
The localization gradient plot element
|
||||||
|
sun (:class:`~.gbmplot.Sun`): The Sun plot element
|
||||||
|
text_color (str): The color of the text labels
|
||||||
|
"""
|
||||||
|
_background = 'antiquewhite'
|
||||||
|
_textcolor = 'black'
|
||||||
|
_canvascolor = 'white'
|
||||||
|
_x_origin = 180
|
||||||
|
_y_origin = 0
|
||||||
|
_fontsize = 10
|
||||||
|
|
||||||
|
def __init__(self, canvas=None, projection='mollweide', flipped=True,
|
||||||
|
**kwargs):
|
||||||
|
super().__init__(figsize=(10, 5), canvas=canvas, projection=projection,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
# set up the plot background color and the sky grid
|
||||||
|
self._figure.set_facecolor(self._canvascolor)
|
||||||
|
self._ax.set_facecolor(self._background)
|
||||||
|
self._ax.grid(True, linewidth=0.5)
|
||||||
|
|
||||||
|
# create the axes tick labels
|
||||||
|
self._longitude_axis(flipped)
|
||||||
|
self._latitude_axis()
|
||||||
|
|
||||||
|
self._flipped = flipped
|
||||||
|
self._sun = None
|
||||||
|
self._earth = None
|
||||||
|
self._detectors = Collection()
|
||||||
|
self._galactic_plane = None
|
||||||
|
self._posterior = None
|
||||||
|
self._clevels = Collection()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sun(self):
|
||||||
|
return self._sun
|
||||||
|
|
||||||
|
@property
|
||||||
|
def earth(self):
|
||||||
|
return self._earth
|
||||||
|
|
||||||
|
@property
|
||||||
|
def detectors(self):
|
||||||
|
return self._detectors
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gc(self):
|
||||||
|
return self._galactic_plane
|
||||||
|
|
||||||
|
@property
|
||||||
|
def loc_posterior(self):
|
||||||
|
return self._posterior
|
||||||
|
|
||||||
|
@property
|
||||||
|
def loc_contours(self):
|
||||||
|
return self._clevels
|
||||||
|
|
||||||
|
@property
|
||||||
|
def canvas_color(self):
|
||||||
|
return self._canvascolor
|
||||||
|
|
||||||
|
@canvas_color.setter
|
||||||
|
def canvas_color(self, color):
|
||||||
|
self._figure.set_facecolor(color)
|
||||||
|
self._canvascolor = color
|
||||||
|
|
||||||
|
@property
|
||||||
|
def background_color(self):
|
||||||
|
return self._background
|
||||||
|
|
||||||
|
@background_color.setter
|
||||||
|
def background_color(self, color):
|
||||||
|
self.ax.set_facecolor(color)
|
||||||
|
self._background = color
|
||||||
|
|
||||||
|
@property
|
||||||
|
def text_color(self):
|
||||||
|
return self._textcolor
|
||||||
|
|
||||||
|
@text_color.setter
|
||||||
|
def text_color(self, color):
|
||||||
|
self._ax.set_yticklabels(self._ytick_labels, fontsize=self._fontsize,
|
||||||
|
color=color)
|
||||||
|
self._ax.set_xticklabels(self._xtick_labels, fontsize=self._fontsize,
|
||||||
|
color=color)
|
||||||
|
self._textcolor = color
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fontsize(self):
|
||||||
|
return self._fontsize
|
||||||
|
|
||||||
|
@fontsize.setter
|
||||||
|
def fontsize(self, size):
|
||||||
|
self._ax.set_yticklabels(self._ytick_labels, fontsize=size,
|
||||||
|
color=self._textcolor)
|
||||||
|
self._ax.set_xticklabels(self._xtick_labels, fontsize=size,
|
||||||
|
color=self._textcolor)
|
||||||
|
self._fontsize = size
|
||||||
|
|
||||||
|
def add_poshist(self, data, trigtime=None, detectors='all', geo=True,
|
||||||
|
sun=True,
|
||||||
|
galactic_plane=True):
|
||||||
|
"""Add a Position History or Trigdat object to plot the location of the
|
||||||
|
Earth, Sun, and detector pointings
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (:class:`~gbm.data.PosHist` or :class:`~gbm.data.Trigdat`)
|
||||||
|
A Position History or Trigdat object
|
||||||
|
trigtime (float, optional): If data is PosHist, set trigtime to a
|
||||||
|
particular time of interest.
|
||||||
|
The Trigdat trigger time overrides this
|
||||||
|
detectors ('all' or list): A list of detectors or "all" to plot the
|
||||||
|
pointings on the sky
|
||||||
|
geo (bool, optional): If True, plot the Earth. Default is True.
|
||||||
|
sun (bool, optional): If True, plot the Sun. Default is True.
|
||||||
|
galactic_plane (bool, optional):
|
||||||
|
If True, plot the Galactic plane. Default is True.
|
||||||
|
"""
|
||||||
|
if hasattr(data, 'trigtime'):
|
||||||
|
trigtime = data.trigtime
|
||||||
|
|
||||||
|
if detectors == 'all':
|
||||||
|
dets = ['n0', 'n1', 'n2', 'n3', 'n4', 'n5', 'n6',
|
||||||
|
'n7', 'n8', 'n9', 'na', 'nb', 'b0', 'b1']
|
||||||
|
else:
|
||||||
|
dets = detectors
|
||||||
|
|
||||||
|
if trigtime is not None:
|
||||||
|
if sun:
|
||||||
|
sun_loc = get_sun_loc(trigtime)
|
||||||
|
self.plot_sun(*sun_loc)
|
||||||
|
if geo:
|
||||||
|
geo_ra, geo_dec = data.get_geocenter_radec(trigtime)
|
||||||
|
radius = data.get_earth_radius(trigtime)
|
||||||
|
self.plot_earth(geo_ra, geo_dec, radius)
|
||||||
|
for det in dets:
|
||||||
|
ra, dec = data.detector_pointing(det, trigtime)
|
||||||
|
self.plot_detector(ra, dec, det)
|
||||||
|
|
||||||
|
# testing
|
||||||
|
# lon = data.get_longitude(trigtime)
|
||||||
|
# lat = data.get_latitude(trigtime)
|
||||||
|
# alt = data.get_altitude(trigtime)
|
||||||
|
# test_footprint(self.ax, self._earth._artists[0], lon, lat, alt,
|
||||||
|
# geo_ra, geo_dec, radius)
|
||||||
|
|
||||||
|
if galactic_plane:
|
||||||
|
self.plot_galactic_plane()
|
||||||
|
|
||||||
|
def add_healpix(self, hpx, gradient=True, clevels=None, sun=True,
|
||||||
|
earth=True,
|
||||||
|
detectors='all', galactic_plane=True):
|
||||||
|
"""Add HealPix object to plot a localization and optionally the location
|
||||||
|
of the Earth, Sun, and detector pointings
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hpx (:class:`~gbm.data.HealPix`): The HealPix object
|
||||||
|
gradient (bool, optional):
|
||||||
|
If True, plots the posterior as a color gradient. If False,
|
||||||
|
plot the posterior as color-filled confidence regions.
|
||||||
|
clevels (list of float, optional):
|
||||||
|
The confidence levels to plot contours. By default plots at
|
||||||
|
the 1, 2, and 3 sigma level.
|
||||||
|
detectors ('all' or list):
|
||||||
|
A list of detectors or "all" to plot the pointings on the sky
|
||||||
|
earth (bool, optional): If True, plot the Earth. Default is True.
|
||||||
|
sun (bool, optional): If True, plot the Sun. Default is True.
|
||||||
|
galactic_plane (bool, optional):
|
||||||
|
If True, plot the Galactic plane. Default is True.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
Setting `gradient=False` when plotting an annulus may produce
|
||||||
|
unexpected results at this time. It is suggested to use
|
||||||
|
`gradient=True` for plotting annuli maps.
|
||||||
|
"""
|
||||||
|
if clevels is None:
|
||||||
|
clevels = [0.997, 0.955, 0.687]
|
||||||
|
if detectors == 'all':
|
||||||
|
detectors = ['n0', 'n1', 'n2', 'n3', 'n4', 'n5', 'n6',
|
||||||
|
'n7', 'n8', 'n9', 'na', 'nb', 'b0', 'b1']
|
||||||
|
|
||||||
|
# determine what the resolution of the sky grid should be based on the
|
||||||
|
# resolution of the healpix
|
||||||
|
approx_res = np.sqrt(hpx.pixel_area)
|
||||||
|
numpts_ra = int(np.floor(0.5*360.0/approx_res))
|
||||||
|
numpts_dec = int(np.floor(0.5*180.0/approx_res))
|
||||||
|
|
||||||
|
if gradient:
|
||||||
|
prob_arr, ra_arr, dec_arr = hpx.prob_array(numpts_ra=numpts_ra,
|
||||||
|
numpts_dec=numpts_dec)
|
||||||
|
self._posterior = self.plot_heatmap(prob_arr, ra_arr, dec_arr)
|
||||||
|
|
||||||
|
for clevel in clevels:
|
||||||
|
paths = hpx.confidence_region_path(clevel, numpts_ra=numpts_ra,
|
||||||
|
numpts_dec=numpts_dec)
|
||||||
|
numpaths = len(paths)
|
||||||
|
if gradient:
|
||||||
|
for i in range(numpaths):
|
||||||
|
contour = SkyLine(paths[i][:, 0], paths[i][:, 1], self.ax,
|
||||||
|
color='black',
|
||||||
|
alpha=0.7, linewidth=2,
|
||||||
|
flipped=self._flipped)
|
||||||
|
self._clevels.insert(str(clevel) + '_' + str(i), contour)
|
||||||
|
else:
|
||||||
|
for i in range(numpaths):
|
||||||
|
contour = SkyPolygon(paths[i][:, 0], paths[i][:, 1],
|
||||||
|
self.ax, color='purple',
|
||||||
|
face_alpha=0.3, flipped=self._flipped)
|
||||||
|
self._clevels.insert(str(clevel) + '_' + str(i), contour)
|
||||||
|
|
||||||
|
# plot sun
|
||||||
|
if sun:
|
||||||
|
try:
|
||||||
|
self.plot_sun(*hpx.sun_location)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# plot earth
|
||||||
|
if earth:
|
||||||
|
try:
|
||||||
|
geo_rad = 67.0 if hpx.geo_radius is None else hpx.geo_radius
|
||||||
|
self.plot_earth(*hpx.geo_location, geo_rad)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# plot detector pointings
|
||||||
|
try:
|
||||||
|
for det in detectors:
|
||||||
|
self.plot_detector(*getattr(hpx, det + '_pointing'), det)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# plot galactic plane
|
||||||
|
if galactic_plane:
|
||||||
|
self.plot_galactic_plane()
|
||||||
|
|
||||||
|
def plot_sun(self, x, y, **kwargs):
|
||||||
|
"""Plot the sun
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x (float): The RA of the Sun
|
||||||
|
y (float): The Dec of the Sun
|
||||||
|
**kwargs: Options to pass to :class:`~.gbmplot.Sun`
|
||||||
|
"""
|
||||||
|
self._sun = Sun(x, y, self.ax, flipped=self._flipped, **kwargs)
|
||||||
|
|
||||||
|
def plot_earth(self, x, y, radius, **kwargs):
|
||||||
|
"""Plot the Earth
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x (float): The RA of the geocenter
|
||||||
|
y (float): The Dec of the geocenter
|
||||||
|
radius (float): The radius of the Earth, in degrees
|
||||||
|
**kwargs: Options to pass to :class:`~.gbmplot.SkyCircle`
|
||||||
|
"""
|
||||||
|
self._earth = SkyCircle(x, y, radius, self.ax, flipped=self._flipped,
|
||||||
|
color='deepskyblue', face_alpha=0.25,
|
||||||
|
edge_alpha=0.50, **kwargs)
|
||||||
|
|
||||||
|
def plot_detector(self, x, y, det, radius=10.0, **kwargs):
|
||||||
|
"""Plot a detector pointing
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x (float): The RA of the detector normal
|
||||||
|
y (float): The Dec of the detector normal
|
||||||
|
det (str): The detector name
|
||||||
|
radius (float, optional): The radius of pointing, in degrees.
|
||||||
|
Default is 10.0
|
||||||
|
**kwargs: Options to pass to :class:`~.gbmplot.SkyCircle`
|
||||||
|
"""
|
||||||
|
pointing = DetectorPointing(x, y, radius, det, self.ax,
|
||||||
|
flipped=self._flipped, **kwargs)
|
||||||
|
self._detectors.insert(det, pointing)
|
||||||
|
|
||||||
|
def plot_galactic_plane(self):
|
||||||
|
"""Plot the Galactic plane
|
||||||
|
"""
|
||||||
|
self._galactic_plane = GalacticPlane(self.ax, flipped=self._flipped)
|
||||||
|
|
||||||
|
def plot_heatmap(self, heatmap, ra_array, dec_array, **kwargs):
|
||||||
|
"""Plot a heatmap on the sky
|
||||||
|
|
||||||
|
Args:
|
||||||
|
heatmap (np.array): A 2D array of values
|
||||||
|
ra_array (np.array): The array of RA gridpoints
|
||||||
|
dec_array (np.array): The array of Dec gridpoints
|
||||||
|
radius (float): The radius of pointing, in degrees
|
||||||
|
**kwargs: Options to pass to :class:`~.gbmplot.SkyHeatmap`
|
||||||
|
"""
|
||||||
|
heatmap = SkyHeatmap(ra_array, dec_array, heatmap, self.ax,
|
||||||
|
flipped=self._flipped, **kwargs)
|
||||||
|
return heatmap
|
||||||
|
|
||||||
|
def _longitude_axis(self, flipped):
|
||||||
|
# longitude labels
|
||||||
|
# these have to be shifted on the plot because matplotlib natively
|
||||||
|
# goes from -180 to +180
|
||||||
|
tick_labels = np.array(
|
||||||
|
[210, 240, 270, 300, 330, 0, 30, 60, 90, 120, 150])
|
||||||
|
tick_labels = tick_labels - 360 - int(self._x_origin)
|
||||||
|
# flip coordinates
|
||||||
|
if flipped:
|
||||||
|
tick_labels = -tick_labels
|
||||||
|
tick_labels = np.remainder(tick_labels, 360)
|
||||||
|
|
||||||
|
# format the tick labels with degrees
|
||||||
|
self._xtick_labels = [str(t) + '$^\circ$' for t in tick_labels]
|
||||||
|
self._ax.set_xticklabels(self._xtick_labels, fontsize=self._fontsize,
|
||||||
|
color=self._textcolor)
|
||||||
|
|
||||||
|
def _latitude_axis(self):
|
||||||
|
# latitude labels
|
||||||
|
# matplotlib natively plots from -90 to +90 from bottom to top.
|
||||||
|
# this is fine for equatorial coordinates, but we have to shift if
|
||||||
|
# we are plotting in spacecraft coordinates
|
||||||
|
tick_labels = np.array(
|
||||||
|
[75, 60, 45, 30, 15, 0, -15, -30, -45, -60, -75])
|
||||||
|
tick_labels = (self._y_origin - tick_labels)
|
||||||
|
if np.sign(self._y_origin) == -1:
|
||||||
|
tick_labels *= -1
|
||||||
|
self._ytick_labels = [str(t) + '$^\circ$' for t in tick_labels]
|
||||||
|
self._ax.set_yticklabels(self._ytick_labels, fontsize=self._fontsize,
|
||||||
|
color=self._textcolor)
|
||||||
|
|
||||||
|
|
||||||
|
class FermiSkyPlot(SkyPlot):
|
||||||
|
"""Class for plotting in Fermi spacecraft coordinates.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
projection (str, optional): The projection of the map.
|
||||||
|
Default is 'mollweide'
|
||||||
|
**kwargs: Options to pass to :class:`~.gbmplot.GbmPlot`
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
ax (:class:`matplotlib.axes`): The matplotlib axes object for the plot
|
||||||
|
background_color (str): The color of the plot background. This attribute
|
||||||
|
can be set.
|
||||||
|
canvas (Canvas Backend object): The plotting canvas, if set upon
|
||||||
|
initialization.
|
||||||
|
canvas_color (str): The color of the plotting canvas. This attribute
|
||||||
|
can be set.
|
||||||
|
detectors (:class:`~.gbmplot.Collection` of :class:`~.gbmplot.SkyCircle`):
|
||||||
|
The collection of detector plot elements
|
||||||
|
earth (:class:`~.gbmplot.SkyCircle`): The Earth plot element
|
||||||
|
fig (:class:`matplotlib.figure`): The matplotlib figure object
|
||||||
|
fontsize (int): The font size of the text labels. This attribute can be set.
|
||||||
|
gc (:class:`~.gbmplot.GalacticPlane`):
|
||||||
|
The reference to the galactic plane plot element
|
||||||
|
loc_contours: (:class:`~.gbmplot.Collection` of :class:`~.gbmplot.SkyLine` \
|
||||||
|
or :class:`~.gbplot.SkyPolygon`):
|
||||||
|
The localization contour plot elements
|
||||||
|
loc_posterior (:class:`~.gbmplot.SkyHeatmap`):
|
||||||
|
The localization gradient plot element
|
||||||
|
sun (:class:`~.gbmplot.Sun`): The Sun plot element
|
||||||
|
text_color (str): The color of the text labels
|
||||||
|
"""
|
||||||
|
_y_origin = -90
|
||||||
|
_x_origin = 0
|
||||||
|
|
||||||
|
def __init__(self, canvas=None, projection='mollweide', **kwargs):
|
||||||
|
super(FermiSkyPlot, self).__init__(canvas=canvas, flipped=False,
|
||||||
|
projection=projection, **kwargs)
|
||||||
|
|
||||||
|
def add_poshist(self, data, trigtime=None, detectors='all', geo=True,
|
||||||
|
sun=True, galactic_plane=True):
|
||||||
|
if hasattr(data, 'trigtime'):
|
||||||
|
trigtime = data.trigtime
|
||||||
|
|
||||||
|
if detectors == 'all':
|
||||||
|
dets = ['n0', 'n1', 'n2', 'n3', 'n4', 'n5', 'n6',
|
||||||
|
'n7', 'n8', 'n9', 'na', 'nb', 'b0', 'b1']
|
||||||
|
|
||||||
|
if trigtime is not None:
|
||||||
|
if sun:
|
||||||
|
sun_loc = get_sun_loc(trigtime)
|
||||||
|
sun_loc = data.to_fermi_frame(*sun_loc, trigtime)
|
||||||
|
self.plot_sun(*sun_loc)
|
||||||
|
if geo:
|
||||||
|
ra, dec = data.get_geocenter_radec(trigtime)
|
||||||
|
az, zen = data.to_fermi_frame(ra, dec, trigtime)
|
||||||
|
radius = data.get_earth_radius(trigtime)
|
||||||
|
self.plot_earth(az, zen, radius)
|
||||||
|
|
||||||
|
for det in dets:
|
||||||
|
self.plot_detector(det)
|
||||||
|
|
||||||
|
if galactic_plane:
|
||||||
|
quat = data.get_quaternions(trigtime)
|
||||||
|
self.plot_galactic_plane(quat)
|
||||||
|
|
||||||
|
def plot_detector(self, det, radius=10.0, **kwargs):
|
||||||
|
"""Plot a detector pointing
|
||||||
|
|
||||||
|
Args:
|
||||||
|
det (str): The detector name
|
||||||
|
radius (float, optional): The radius of pointing, in degrees.
|
||||||
|
Default is 10.0
|
||||||
|
**kwargs: Options to pass to :class:`~.gbmplot.SkyCircle`
|
||||||
|
"""
|
||||||
|
super(FermiSkyPlot, self).plot_detector(0.0, 0.0, det, radius=radius,
|
||||||
|
fermi=True, **kwargs)
|
||||||
|
|
||||||
|
def plot_earth(self, x, y, radius, **kwargs):
|
||||||
|
super(FermiSkyPlot, self).plot_earth(x, y, radius, fermi=True,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
def plot_sun(self, x, y, **kwargs):
|
||||||
|
super(FermiSkyPlot, self).plot_sun(x, y, fermi=True, **kwargs)
|
||||||
|
|
||||||
|
# mark TODO: enable loc plotting
|
||||||
|
|
||||||
|
# will need the quaternion
|
||||||
|
def add_healpix(*args, **kwargs):
|
||||||
|
"""Not yet implemented"""
|
||||||
|
raise NotImplementedError('Not yet implemented for the Fermi frame')
|
||||||
|
|
||||||
|
def plot_galactic_plane(self, quat):
|
||||||
|
"""Plot the Galactic plane
|
||||||
|
|
||||||
|
Args:
|
||||||
|
quat (np.array): The Fermi attitude quaternion
|
||||||
|
"""
|
||||||
|
self._galactic_plane = GalacticPlane(self.ax, flipped=self._flipped,
|
||||||
|
fermi_quat=quat)
|
236
plot/spectrum.py
Normal file
236
plot/spectrum.py
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
# spectrum.py: Plot class for count spectra
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
from .gbmplot import GbmPlot, Histo, HistoErrorbars, HistoFilled, \
|
||||||
|
SpectrumBackground
|
||||||
|
from .lib import *
|
||||||
|
|
||||||
|
|
||||||
|
class Spectrum(GbmPlot):
|
||||||
|
"""Class for plotting count spectra and count spectra paraphernalia.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
data (:class:`~gbm.data.primitives.EnergyBins`, optional):
|
||||||
|
The count spectrum data to plot
|
||||||
|
background (:class:`~gbm.background.BackgroundSpectrum`, optional):
|
||||||
|
The background spectrum to plot
|
||||||
|
**kwargs: Options to pass to :class:`~.gbmplot.GbmPlot`
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
ax (:class:`matplotlib.axes`): The matplotlib axes object for the plot
|
||||||
|
background (:class:`~.gbmplot.SpectrumBackground`):
|
||||||
|
The count spectrum background plot element
|
||||||
|
canvas (Canvas Backend object): The plotting canvas, if set upon
|
||||||
|
initialization.
|
||||||
|
errorbars (:class:`~.gbmplot.HistoErrorbars`): The error bars plot element
|
||||||
|
fig (:class:`matplotlib.figure`): The matplotlib figure object
|
||||||
|
selections (:class:`~.gbmplot.HistoFilled`):
|
||||||
|
The count spectrum selection plot element
|
||||||
|
spectrum (:class:`~.gbmplot.Histo`): The count spectrum plot element
|
||||||
|
xlim (float, float): The plotting range of the x axis.
|
||||||
|
This attribute can be set.
|
||||||
|
xscale (str): The scale of the x axis, either 'linear' or 'log'.
|
||||||
|
This attribute can be set.
|
||||||
|
ylim (float, float): The plotting range of the y axis.
|
||||||
|
This attribute can be set.
|
||||||
|
yscale (str): The scale of the y axis, either 'linear' or 'log'.
|
||||||
|
This attribute can be set.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, data=None, background=None, canvas=None, **kwargs):
|
||||||
|
super().__init__(canvas=canvas, **kwargs)
|
||||||
|
|
||||||
|
self._spec = None
|
||||||
|
self._errorbars = None
|
||||||
|
self._bkgd = None
|
||||||
|
self._selections = []
|
||||||
|
|
||||||
|
# initialize the plot axes, labels, ticks, and scales
|
||||||
|
self._ax.set_xlabel('Energy (keV)', fontsize=PLOTFONTSIZE)
|
||||||
|
self._ax.set_ylabel('Rate (count/s-keV)', fontsize=PLOTFONTSIZE)
|
||||||
|
self._ax.xaxis.set_tick_params(labelsize=PLOTFONTSIZE)
|
||||||
|
self._ax.yaxis.set_tick_params(labelsize=PLOTFONTSIZE)
|
||||||
|
self._ax.set_xscale('log')
|
||||||
|
self._ax.set_yscale('log')
|
||||||
|
|
||||||
|
# plot data and/or background if set on init
|
||||||
|
if data is not None:
|
||||||
|
self.set_data(data)
|
||||||
|
if background is not None:
|
||||||
|
self.set_background(background)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def spectrum(self):
|
||||||
|
return self._spec
|
||||||
|
|
||||||
|
@property
|
||||||
|
def errorbars(self):
|
||||||
|
return self._errorbars
|
||||||
|
|
||||||
|
@property
|
||||||
|
def background(self):
|
||||||
|
return self._bkgd
|
||||||
|
|
||||||
|
@property
|
||||||
|
def selections(self):
|
||||||
|
return self._selections
|
||||||
|
|
||||||
|
def set_data(self, data):
|
||||||
|
"""Set the count spectrum plotting data. If a count spectrum already
|
||||||
|
exists, this triggers a replot of the count spectrum.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (:class:`~gbm.data.primitives.EnergyBins`):
|
||||||
|
The count spectrum data to plot
|
||||||
|
"""
|
||||||
|
spec_color, spec_alpha, spec_kwargs = self._spec_settings()
|
||||||
|
self._spec = Histo(data, self._ax, color=spec_color, alpha=spec_alpha,
|
||||||
|
**spec_kwargs)
|
||||||
|
eb_color, eb_alpha, eb_kwargs = self._eb_settings()
|
||||||
|
self._errorbars = HistoErrorbars(data, self._ax, color=eb_color,
|
||||||
|
alpha=eb_alpha, **eb_kwargs)
|
||||||
|
|
||||||
|
self._ax.set_xlim(data.range)
|
||||||
|
mask = (data.rates > 0.0)
|
||||||
|
self._ax.set_ylim(0.9 * np.min(data.rates[mask]),
|
||||||
|
1.1 * np.max(data.rates))
|
||||||
|
|
||||||
|
def add_selection(self, data):
|
||||||
|
"""Add a selection to the plot. This adds a new selection to a list
|
||||||
|
of existing selections.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (:class:`~gbm.data.primitives.EnergyBins`):
|
||||||
|
The count spectrum data selections to plot
|
||||||
|
"""
|
||||||
|
color, alpha, kwargs = self._selection_settings()
|
||||||
|
select = HistoFilled(data, self._ax, color=color, alpha=alpha,
|
||||||
|
**kwargs)
|
||||||
|
self._selections.append(select)
|
||||||
|
|
||||||
|
def set_background(self, background):
|
||||||
|
"""Set the background plotting data. If a background already exists,
|
||||||
|
this triggers a replot of the background.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
background (:class:`~gbm.background.BackgroundSpectrum`):
|
||||||
|
The background spectrum to plot
|
||||||
|
"""
|
||||||
|
color, cent_color, err_color, alpha, cent_alpha, err_alpha, \
|
||||||
|
kwargs = self._bkgd_settings()
|
||||||
|
self._bkgd = SpectrumBackground(background, self._ax, color=color,
|
||||||
|
cent_color=cent_color,
|
||||||
|
err_color=err_color,
|
||||||
|
alpha=alpha, cent_alpha=BKGD_ALPHA,
|
||||||
|
err_alpha=BKGD_ERROR_ALPHA)
|
||||||
|
|
||||||
|
def remove_data(self):
|
||||||
|
"""Remove the count spectrum from the plot.
|
||||||
|
"""
|
||||||
|
self._spec.remove()
|
||||||
|
self._spec = None
|
||||||
|
|
||||||
|
def remove_errorbars(self):
|
||||||
|
"""Remove the count spectrum errorbars from the plot.
|
||||||
|
"""
|
||||||
|
self._errorbars.remove()
|
||||||
|
self._errorbars = None
|
||||||
|
|
||||||
|
def remove_background(self):
|
||||||
|
"""Remove the background from the plot.
|
||||||
|
"""
|
||||||
|
self._bkgd.remove()
|
||||||
|
self._bkgd = None
|
||||||
|
|
||||||
|
def remove_selections(self):
|
||||||
|
"""Remove the selections from the plot.
|
||||||
|
"""
|
||||||
|
[selection.remove() for selection in self._selections]
|
||||||
|
self._selections = []
|
||||||
|
|
||||||
|
def _spec_settings(self):
|
||||||
|
"""The default settings for the count spectrum. If a count spectrum
|
||||||
|
already exists, use its settings instead.
|
||||||
|
"""
|
||||||
|
if self._spec is None:
|
||||||
|
spec_color = DATA_COLOR
|
||||||
|
spec_alpha = None
|
||||||
|
spec_kwargs = {}
|
||||||
|
else:
|
||||||
|
spec_color = self._spec.color
|
||||||
|
spec_alpha = self._spec.alpha
|
||||||
|
spec_kwargs = self._spec._kwargs
|
||||||
|
return (spec_color, spec_alpha, spec_kwargs)
|
||||||
|
|
||||||
|
def _eb_settings(self):
|
||||||
|
"""The default settings for the errorbars. If a lightcurve already
|
||||||
|
exists, use its errorbars settings instead.
|
||||||
|
"""
|
||||||
|
if self._errorbars is None:
|
||||||
|
eb_color = DATA_ERROR_COLOR
|
||||||
|
eb_alpha = DATA_ERROR_ALPHA
|
||||||
|
eb_kwargs = {}
|
||||||
|
else:
|
||||||
|
eb_color = self._errorbars.color
|
||||||
|
eb_alpha = self._errorbars.alpha
|
||||||
|
eb_kwargs = self._errorbars._kwargs
|
||||||
|
return (eb_color, eb_alpha, eb_kwargs)
|
||||||
|
|
||||||
|
def _bkgd_settings(self):
|
||||||
|
"""The default settings for the background. If a background already
|
||||||
|
exists, use its settings instead.
|
||||||
|
"""
|
||||||
|
if self._bkgd is None:
|
||||||
|
color = BKGD_COLOR
|
||||||
|
cent_color = None
|
||||||
|
err_color = None
|
||||||
|
alpha = None
|
||||||
|
cent_alpha = BKGD_ALPHA
|
||||||
|
err_alpha = BKGD_ERROR_ALPHA
|
||||||
|
kwargs = {'linewidth': BKGD_WIDTH}
|
||||||
|
else:
|
||||||
|
color = self._bkgd.color
|
||||||
|
cent_color = self._bkgd.cent_color
|
||||||
|
err_color = self._bkgd.err_color
|
||||||
|
alpha = self._bkgd.alpha
|
||||||
|
cent_alpha = self._bkgd.cent_alpha
|
||||||
|
err_alpha = self._bkgd.err_alpha
|
||||||
|
kwargs = self._bkgd._kwargs
|
||||||
|
return color, cent_color, err_color, alpha, cent_alpha, err_alpha, kwargs
|
||||||
|
|
||||||
|
def _selection_settings(self):
|
||||||
|
"""The default settings for a selection. If a selection already
|
||||||
|
exists, use its settings instead.
|
||||||
|
"""
|
||||||
|
if len(self._selections) == 0:
|
||||||
|
color = DATA_SELECTED_COLOR
|
||||||
|
alpha = DATA_SELECTED_ALPHA
|
||||||
|
kwargs = {}
|
||||||
|
else:
|
||||||
|
color = self._selections[0].color
|
||||||
|
alpha = self._selections[0].alpha
|
||||||
|
kwargs = self._selections[0]._kwargs
|
||||||
|
return color, alpha, kwargs
|
2
simulate/__init__.py
Normal file
2
simulate/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .pha import PhaSimulator
|
||||||
|
from .tte import TteSourceSimulator, TteBackgroundSimulator
|
388
simulate/generators.py
Normal file
388
simulate/generators.py
Normal file
@ -0,0 +1,388 @@
|
|||||||
|
# generators.py: Various generators for sources and backgrounds
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ..background.background import BackgroundSpectrum
|
||||||
|
from ..data.primitives import EnergyBins
|
||||||
|
|
||||||
|
|
||||||
|
class SimGenerator:
|
||||||
|
"""Base class for a simulation generator
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
return self._simulate()
|
||||||
|
|
||||||
|
def _simulate(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PoissonBackgroundGenerator(SimGenerator):
|
||||||
|
"""Simulation generator for Poisson Background.
|
||||||
|
Once initialized, a single deviate or many deviates can be generated::
|
||||||
|
gen = PoissonBackgroundGenerator(bkgd)
|
||||||
|
|
||||||
|
# generate a single deviate
|
||||||
|
next(gen)
|
||||||
|
|
||||||
|
# generate 10 deviates
|
||||||
|
[next(gen) for i in range(10)]
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
bkgd (:class:`~gbm.background.BackgroundSpectrum`):
|
||||||
|
A modeled background spectrum
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
:class:`~gbm.background.BackgroundSpectrum`:
|
||||||
|
A Poisson random deviate of the initialized spectrum
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bkgd):
|
||||||
|
super().__init__()
|
||||||
|
self._bkgd = bkgd
|
||||||
|
|
||||||
|
def _simulate(self):
|
||||||
|
# the poisson count deviates in each channel
|
||||||
|
counts = np.random.poisson(self._bkgd.counts, size=(self._bkgd.size,))
|
||||||
|
# convert to rates...
|
||||||
|
rates = counts / self._bkgd.exposure
|
||||||
|
rate_uncert = np.sqrt(counts) / self._bkgd.exposure
|
||||||
|
# ...so we can populate our background spectrum
|
||||||
|
return BackgroundSpectrum(rates, rate_uncert, self._bkgd.lo_edges,
|
||||||
|
self._bkgd.hi_edges, self._bkgd.exposure)
|
||||||
|
|
||||||
|
|
||||||
|
class GaussianBackgroundGenerator(SimGenerator):
|
||||||
|
"""Simulation generator for Gaussian Background.
|
||||||
|
Once initialized, a single deviate or many deviates can be generated::
|
||||||
|
gen = GaussianBackgroundGenerator(bkgd)
|
||||||
|
|
||||||
|
# generate a single deviate
|
||||||
|
next(gen)
|
||||||
|
|
||||||
|
# generate 10 deviates
|
||||||
|
[next(gen) for i in range(10)]
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
bkgd (:class:`~gbm.background.BackgroundSpectrum`):
|
||||||
|
A modeled background spectrum
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
:class:`~gbm.background.BackgroundSpectrum`:
|
||||||
|
A Gaussian random deviate of the initialized spectrum
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bkgd):
|
||||||
|
super().__init__()
|
||||||
|
self._bkgd = bkgd
|
||||||
|
|
||||||
|
def _simulate(self):
|
||||||
|
# the gaussian rate deviates given the "centroid" rates and
|
||||||
|
# rate uncertainties
|
||||||
|
counts = np.random.normal(self._bkgd.counts, self._bkgd.count_uncertainty,
|
||||||
|
size=(self._bkgd.size,))
|
||||||
|
rates = counts/self._bkgd.exposure
|
||||||
|
return BackgroundSpectrum(rates, self._bkgd.rate_uncertainty,
|
||||||
|
self._bkgd.lo_edges, self._bkgd.hi_edges,
|
||||||
|
self._bkgd.exposure)
|
||||||
|
|
||||||
|
|
||||||
|
class SourceSpectrumGenerator(SimGenerator):
|
||||||
|
"""Simulation generator for a Poisson source spectrum.
|
||||||
|
Once initialized, a single deviate or many deviates can be generated::
|
||||||
|
gen = SourceSpectrumGenerator(rsp, function params, exposure)
|
||||||
|
|
||||||
|
# generate a single deviate
|
||||||
|
next(gen)
|
||||||
|
|
||||||
|
# generate 10 deviates
|
||||||
|
[next(gen) for i in range(10)]
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
rsp (:class:`~gbm.data.RSP`): A detector response object
|
||||||
|
function (:class:`~gbm.spectra.functions.Function`):
|
||||||
|
A photon model function
|
||||||
|
params (iterable): The parameters for the function
|
||||||
|
exposure (float): The source exposure
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
:class:`~gbm.data.primitives.EnergyBins`:
|
||||||
|
A Poisson random deviate of the initialized source spectrum
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, rsp, function, params, exposure):
|
||||||
|
super().__init__()
|
||||||
|
self._rates = rsp.fold_spectrum(function.fit_eval, params) * exposure
|
||||||
|
self._rsp = rsp
|
||||||
|
self._exposure = [exposure] * rsp.numchans
|
||||||
|
|
||||||
|
def _simulate(self):
|
||||||
|
counts = np.random.poisson(self._rates, size=(self._rsp.numchans,))
|
||||||
|
return EnergyBins(counts, self._rsp.ebounds['E_MIN'],
|
||||||
|
self._rsp.ebounds['E_MAX'], self._exposure)
|
||||||
|
|
||||||
|
|
||||||
|
class VariablePoissonBackground(PoissonBackgroundGenerator):
|
||||||
|
"""Simulation generator for a variable Poisson Background. This
|
||||||
|
non-homogeneous approximation allows the amplitude of the spectrum to
|
||||||
|
be adjusted, thereby scaling the simulated counts. Once initialized, a
|
||||||
|
single deviate or many deviates can be generated::
|
||||||
|
gen = VariablePoissonBackground(bkgd)
|
||||||
|
|
||||||
|
# generate a single deviate
|
||||||
|
next(gen)
|
||||||
|
|
||||||
|
# generate 10 deviates
|
||||||
|
[next(gen) for i in range(10)]
|
||||||
|
|
||||||
|
# change the amplitude to half of the initialized amplitude
|
||||||
|
gen.amp = 0.5
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
bkgd (:class:`~gbm.background.BackgroundSpectrum`):
|
||||||
|
A modeled background spectrum
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
amp (float): The amplitude, relative to initialized spectrum.
|
||||||
|
Setting ``amp=1`` gives the initialized amplitude.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
:class:`~gbm.background.BackgroundSpectrum`:
|
||||||
|
A Poisson random deviate of the spectrum
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bkgd):
|
||||||
|
super().__init__(bkgd)
|
||||||
|
self.amp = 1.0
|
||||||
|
|
||||||
|
def _simulate(self):
|
||||||
|
# the poisson count deviates in each channel
|
||||||
|
counts = np.random.poisson(self._bkgd.counts * self.amp,
|
||||||
|
size=(self._bkgd.size,))
|
||||||
|
# convert to rates...
|
||||||
|
rates = counts / self._bkgd.exposure
|
||||||
|
rate_uncert = np.sqrt(counts) / self._bkgd.exposure
|
||||||
|
# ...so we can populate our background spectrum
|
||||||
|
return BackgroundSpectrum(rates, rate_uncert, self._bkgd.lo_edges,
|
||||||
|
self._bkgd.hi_edges, self._bkgd.exposure)
|
||||||
|
|
||||||
|
|
||||||
|
class VariableGaussianBackground(GaussianBackgroundGenerator):
|
||||||
|
"""Simulation generator for a variable Gaussian Background. This
|
||||||
|
non-homogeneous approximation allows the amplitude of the spectrum to
|
||||||
|
be adjusted, thereby scaling the simulated counts. Once initialized, a
|
||||||
|
single deviate or many deviates can be generated::
|
||||||
|
gen = VariableGaussianBackground(bkgd)
|
||||||
|
|
||||||
|
# generate a single deviate
|
||||||
|
next(gen)
|
||||||
|
|
||||||
|
# generate 10 deviates
|
||||||
|
[next(gen) for i in range(10)]
|
||||||
|
|
||||||
|
# change the amplitude to twice of the initialized amplitude
|
||||||
|
gen.amp = 2.0
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
bkgd (:class:`~gbm.background.BackgroundSpectrum`):
|
||||||
|
A modeled background spectrum
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
amp (float): The amplitude, relative to initialized spectrum.
|
||||||
|
Setting ``amp=1`` gives the initialized amplitude.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
:class:`~gbm.background.BackgroundSpectrum`:
|
||||||
|
A Gaussian random deviate of the spectrum
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bkgd):
|
||||||
|
super().__init__(bkgd)
|
||||||
|
self.amp = 1.0
|
||||||
|
|
||||||
|
def _simulate(self):
|
||||||
|
# the gaussian rate deviates given the "centroid" rates and
|
||||||
|
# rate uncertainties
|
||||||
|
rates = np.random.normal(self._bkgd.rates * self.amp,
|
||||||
|
self._bkgd.rate_uncertainty * self.amp,
|
||||||
|
size=(self._bkgd.size,))
|
||||||
|
rate_uncert = self._bkgd.rate_uncertainty * self.amp
|
||||||
|
|
||||||
|
return BackgroundSpectrum(rates, rate_uncert,
|
||||||
|
self._bkgd.lo_edges, self._bkgd.hi_edges,
|
||||||
|
self._bkgd.exposure)
|
||||||
|
|
||||||
|
|
||||||
|
class VariableSourceSpectrumGenerator(SourceSpectrumGenerator):
|
||||||
|
"""Simulation generator for a Poisson source spectrum, efficient for
|
||||||
|
generating deviates when the source spectrum amplitude changes.
|
||||||
|
Once initialized, a single deviate or many deviates can be generated::
|
||||||
|
gen = AmpFreeSourceSpectrumGenerator(rsp, function params, exposure)
|
||||||
|
|
||||||
|
# generate a single deviate
|
||||||
|
next(gen)
|
||||||
|
|
||||||
|
# generate 10 deviates
|
||||||
|
[next(gen) for i in range(10)]
|
||||||
|
|
||||||
|
# change amplitude, and generate a new deviate
|
||||||
|
gen.amp = 0.01
|
||||||
|
next(gen)
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
rsp (:class:`~gbm.data.RSP`): A detector response object
|
||||||
|
function (:class:`~gbm.spectra.functions.Function`):
|
||||||
|
A photon model function
|
||||||
|
params (iterable): The parameters for the function
|
||||||
|
exposure (float): The source exposure
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
amp (float): The amplitude. This value can be set.
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
:class:`~gbm.data.primitives.EnergyBins`:
|
||||||
|
A Poisson random deviate of the initialized source spectrum
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, rsp, function, params, exposure):
|
||||||
|
params_temp = [1.0]
|
||||||
|
params_temp.extend(params[1:])
|
||||||
|
super().__init__(rsp, function, params_temp, exposure)
|
||||||
|
self.amp = params[0]
|
||||||
|
|
||||||
|
def _simulate(self):
|
||||||
|
if self.amp < 0.0:
|
||||||
|
self.amp = 0.0
|
||||||
|
counts = np.random.poisson(self.amp * self._rates,
|
||||||
|
size=(self._rsp.numchans,))
|
||||||
|
return EnergyBins(counts, self._rsp.ebounds['E_MIN'],
|
||||||
|
self._rsp.ebounds['E_MAX'], self._exposure)
|
||||||
|
|
||||||
|
|
||||||
|
class EventSpectrumGenerator(SimGenerator):
|
||||||
|
"""Simulation generator producing Poisson arrival times for a source
|
||||||
|
spectrum during a finite slice of time. Photon losses from deadtime and
|
||||||
|
detection/electronic processes can be accounted for by setting the
|
||||||
|
``min_sep > 0``.
|
||||||
|
Once initialized, a single deviate or many deviates can be generated::
|
||||||
|
gen = EventSpectrumGenerator(spectrum, dt)
|
||||||
|
|
||||||
|
# generate a single deviate
|
||||||
|
next(gen)
|
||||||
|
|
||||||
|
# generate 10 deviates
|
||||||
|
[next(gen) for i in range(10)]
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
count_spectrum (np.array): An array of counts in each energy channel
|
||||||
|
dt (float): The width of the time slice in seconds
|
||||||
|
min_sep (float, optional): The minimum possible time separation between
|
||||||
|
events. Default is 2e-6 seconds.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
spectrum (np.array): The counts in each channel. This value can be set.
|
||||||
|
The counts array will be converted to integer type.
|
||||||
|
Yields:
|
||||||
|
(np.array, np.array): The arrival times and energy channels for each event
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, count_spectrum, dt, min_sep=2e-6):
|
||||||
|
super().__init__()
|
||||||
|
self._min_sep = min_sep
|
||||||
|
self._dt = dt
|
||||||
|
self._chan_nums = None
|
||||||
|
self._beta = None
|
||||||
|
self.spectrum = count_spectrum
|
||||||
|
|
||||||
|
@property
|
||||||
|
def spectrum(self):
|
||||||
|
return self._spectrum
|
||||||
|
|
||||||
|
@spectrum.setter
|
||||||
|
def spectrum(self, spectrum):
|
||||||
|
self._spectrum = spectrum.astype(int)
|
||||||
|
if self._spectrum.sum() == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# where do we have counts?
|
||||||
|
chanmask = (self._spectrum > 0)
|
||||||
|
# the 1/rate in the time slice
|
||||||
|
self._beta = self._dt / self._spectrum.sum()
|
||||||
|
|
||||||
|
# get the list of channels corresponding to each count
|
||||||
|
chan_idx = np.arange(self._spectrum.size)[chanmask]
|
||||||
|
idx = [[idx] * counts for idx, counts in
|
||||||
|
zip(chan_idx, self._spectrum[chanmask])]
|
||||||
|
if len(idx) > 0:
|
||||||
|
self._chan_nums = np.concatenate(idx)
|
||||||
|
else:
|
||||||
|
self._chan_nums = []
|
||||||
|
|
||||||
|
def _simulate(self):
|
||||||
|
# no counts
|
||||||
|
if self.spectrum.sum() == 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Simulate arrival times for each count. Since we are simulating
|
||||||
|
# counts within a finite bounded window, repeat this until all arrival
|
||||||
|
# times are within our window
|
||||||
|
while (True):
|
||||||
|
times = np.random.exponential(self._beta,
|
||||||
|
size=(self.spectrum.sum(),))
|
||||||
|
times = times.cumsum()
|
||||||
|
chans = self._chan_nums
|
||||||
|
|
||||||
|
# at least one event is outside our window
|
||||||
|
if (times[-1] > self._dt):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If more than one event, check if all events have >= minimum spacing.
|
||||||
|
# If there are events with spacing less than minimum spacing, we
|
||||||
|
# have to throw away some of those events. The reason is that we
|
||||||
|
# are simulating the reality of recording events with real
|
||||||
|
# instruments and electronics, and if the event rate is high enough
|
||||||
|
# that events are arriving faster than the detector/electronics can
|
||||||
|
# process, we will lose some of those events.
|
||||||
|
while (True):
|
||||||
|
if times.size > 1:
|
||||||
|
diff = (times[1:] - times[:-1])
|
||||||
|
if (diff.min() < self._min_sep):
|
||||||
|
goodmask = (diff >= self._min_sep)
|
||||||
|
goodmask = np.concatenate(([True], goodmask))
|
||||||
|
times = times[goodmask]
|
||||||
|
chans = chans[goodmask]
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
return (times, chans)
|
224
simulate/pha.py
Normal file
224
simulate/pha.py
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
# pha.py: Class for simulating count spectra
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from ..data import PHA, BAK, PHAII
|
||||||
|
from ..data.primitives import TimeEnergyBins
|
||||||
|
from .generators import *
|
||||||
|
|
||||||
|
|
||||||
|
class PhaSimulator:
|
||||||
|
"""Simulate PHA data given a modeled background spectrum, detector response,
|
||||||
|
source spectrum, and exposure.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
rsp (:class:`~gbm.data.RSP`): A detector response object
|
||||||
|
function (:class:`~gbm.spectra.functions.Function`):
|
||||||
|
A photon model function
|
||||||
|
params (iterable): The parameters for the function
|
||||||
|
exposure (float): The source exposure
|
||||||
|
bkgd (:class:`~gbm.background.BackgroundSpectrum`):
|
||||||
|
A modeled background spectrum
|
||||||
|
bkgd_distrib (str): The distribution from which the background is
|
||||||
|
simulated; either 'Poisson' or 'Gaussian'
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, rsp, function, params, exposure, bkgd, bkgd_distrib):
|
||||||
|
self._rsp = rsp
|
||||||
|
self._function = function
|
||||||
|
self._params = params
|
||||||
|
self._exposure = exposure
|
||||||
|
self._src_gen = SourceSpectrumGenerator(rsp, function, params,
|
||||||
|
exposure)
|
||||||
|
self.set_background(bkgd, bkgd_distrib)
|
||||||
|
|
||||||
|
def set_rsp(self, rsp):
|
||||||
|
"""Set/change the detector response.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rsp (:class:`~gbm.data.RSP`): A detector response object
|
||||||
|
"""
|
||||||
|
self._src_gen = SourceSpectrumGenerator(rsp, self._function,
|
||||||
|
self._params,
|
||||||
|
self._exposure)
|
||||||
|
self._rsp = rsp
|
||||||
|
|
||||||
|
def set_source(self, function, params, exposure):
|
||||||
|
"""Set/change the source spectrum.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
function (:class:`~gbm.spectra.functions.Function`):
|
||||||
|
A photon model function
|
||||||
|
params (iterable): The parameters for the function
|
||||||
|
exposure (float): The source exposure
|
||||||
|
"""
|
||||||
|
self._src_gen = SourceSpectrumGenerator(self._rsp, function, params,
|
||||||
|
exposure)
|
||||||
|
self._bkgd_gen._bkgd.exposure = [self._exposure] * self._rsp.numchans
|
||||||
|
self._function = function
|
||||||
|
self._params = params
|
||||||
|
self._exposure = exposure
|
||||||
|
|
||||||
|
def set_background(self, bkgd, bkgd_distrib):
|
||||||
|
"""Set/change the background model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bkgd (:class:`~gbm.background.BackgroundSpectrum`):
|
||||||
|
A modeled background spectrum
|
||||||
|
bkgd_distrib (str): The distribution from which the background is
|
||||||
|
simulated; either 'Poisson' or 'Gaussian'
|
||||||
|
"""
|
||||||
|
bkgd_spectrum = BackgroundSpectrum(bkgd.rates, bkgd.rate_uncertainty,
|
||||||
|
bkgd.lo_edges, bkgd.hi_edges,
|
||||||
|
[self._exposure] * bkgd.size)
|
||||||
|
if bkgd_distrib == 'Poisson':
|
||||||
|
self._bkgd_gen = PoissonBackgroundGenerator(bkgd_spectrum)
|
||||||
|
elif bkgd_distrib == 'Gaussian':
|
||||||
|
self._bkgd_gen = GaussianBackgroundGenerator(bkgd_spectrum)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"bkgd_distrib can only be 'Poisson' or 'Gaussian'")
|
||||||
|
|
||||||
|
def simulate_background(self, num_sims):
|
||||||
|
"""Generate simulations of the modeled background spectrum
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_sims (int): Number of simulations
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list of :class:`~gbm.background.BackgroundSpectrum`:
|
||||||
|
The deviates of the background spectrum
|
||||||
|
"""
|
||||||
|
return [next(self._bkgd_gen) for i in range(num_sims)]
|
||||||
|
|
||||||
|
def simulate_source(self, num_sims):
|
||||||
|
"""Generate simulations of the source spectrum
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_sims (int): Number of simulations
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list of :class:`~gbm.data.primitives.EnergyBins`:
|
||||||
|
The deviates of the source spectrum
|
||||||
|
"""
|
||||||
|
return [next(self._src_gen) for i in range(num_sims)]
|
||||||
|
|
||||||
|
def simulate_sum(self, num_sims):
|
||||||
|
"""Generate simulations of the background + source spectrum.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_sims (int): Number of simulations
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list of :class:`~gbm.data.primitives.EnergyBins`:
|
||||||
|
The deviates of the total background + source spectrum
|
||||||
|
"""
|
||||||
|
summed = [None] * num_sims
|
||||||
|
for i in range(num_sims):
|
||||||
|
bkgd = next(self._bkgd_gen)
|
||||||
|
src = next(self._src_gen)
|
||||||
|
|
||||||
|
# since background model is formed from a rate, the background
|
||||||
|
# "counts" won't be integers. So we use the fractional part as
|
||||||
|
# a probability to determine if we round up or truncate.
|
||||||
|
bkgd_counts = bkgd.counts
|
||||||
|
bkgd_counts[bkgd_counts < 0] = 0
|
||||||
|
bkgd_counts_int = bkgd_counts.astype(int)
|
||||||
|
bkgd_counts_frac = bkgd_counts - bkgd_counts_int
|
||||||
|
extra_counts = (np.random.random(
|
||||||
|
bkgd_counts_frac.size) > bkgd_counts_frac)
|
||||||
|
bkgd_counts_int += extra_counts.astype(int)
|
||||||
|
|
||||||
|
counts = bkgd_counts_int + src.counts
|
||||||
|
summed[i] = EnergyBins(counts, src.lo_edges, src.hi_edges,
|
||||||
|
src.exposure)
|
||||||
|
return summed
|
||||||
|
|
||||||
|
def to_bak(self, num_sims, tstart=None, tstop=None, **kwargs):
|
||||||
|
"""Produce BAK objects from simulations
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_sims (int): Number of simulations
|
||||||
|
tstart (float, optional): The start time. If not set, then is zero.
|
||||||
|
tstop (float, optional): Then end time. If not set, then is the exposure.
|
||||||
|
**kwargs: Options passed to :class:`~gbm.data.BAK`
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list of :class:`~gbm.data.BAK`: The simulated BAK objects
|
||||||
|
"""
|
||||||
|
if tstart is None:
|
||||||
|
tstart = 0.0
|
||||||
|
if tstop is None:
|
||||||
|
tstop = tstart + self._exposure
|
||||||
|
|
||||||
|
baks = self.simulate_background(num_sims)
|
||||||
|
baks = [BAK.from_data(bak, tstart, tstop, **kwargs) for bak in baks]
|
||||||
|
return baks
|
||||||
|
|
||||||
|
def to_pha(self, num_sims, tstart=None, tstop=None, **kwargs):
|
||||||
|
"""Produce PHA objects of the background + source from simulations
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_sims (int): Number of simulations
|
||||||
|
tstart (float, optional): The start time. If not set, then is zero.
|
||||||
|
tstop (float, optional): Then end time. If not set, then is the exposure.
|
||||||
|
**kwargs: Options passed to :class:`~gbm.data.PHA`
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list of :class:`~gbm.data.PHA`: The simulated PHA objects
|
||||||
|
"""
|
||||||
|
if tstart is None:
|
||||||
|
tstart = 0.0
|
||||||
|
if tstop is None:
|
||||||
|
tstop = tstart + self._exposure
|
||||||
|
|
||||||
|
phas = self.simulate_sum(num_sims)
|
||||||
|
phas = [PHA.from_data(pha, tstart, tstop, **kwargs) for pha in phas]
|
||||||
|
return phas
|
||||||
|
|
||||||
|
def to_phaii(self, num_sims, bin_width=None, **kwargs):
|
||||||
|
"""Produce a PHAII object by concatenating the simulations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
num_sims (int): Number of simulations
|
||||||
|
bin_widths (float, optional): The width of each time bin. Must be
|
||||||
|
>= the exposure. If not set, the is the exposure.
|
||||||
|
**kwargs: Options passed to :class:`~gbm.data.PHAII`
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`~gbm.data.PHAII`: The simulated PHAII object
|
||||||
|
"""
|
||||||
|
if bin_width is None:
|
||||||
|
bin_width = self._exposure
|
||||||
|
if bin_width < self._exposure:
|
||||||
|
raise ValueError('bin_width cannot be less than exposure')
|
||||||
|
|
||||||
|
phas = self.simulate_sum(num_sims)
|
||||||
|
counts = np.vstack([pha.counts for pha in phas])
|
||||||
|
edges = np.arange(num_sims + 1) * bin_width
|
||||||
|
data = TimeEnergyBins(counts, edges[:-1], edges[1:],
|
||||||
|
[self._exposure] * num_sims, phas[0].lo_edges,
|
||||||
|
phas[0].hi_edges)
|
||||||
|
phaii = PHAII.from_data(data, **kwargs)
|
||||||
|
return phaii
|
129
simulate/profiles.py
Normal file
129
simulate/profiles.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# profiles.py: Functions for lightcurve and background time profiles
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
from .generators import *
|
||||||
|
|
||||||
|
|
||||||
|
# pulse shapes
|
||||||
|
def tophat(x, amp, tstart, tstop):
|
||||||
|
"""A tophat (rectangular) pulse function.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x (np.array): Array of times
|
||||||
|
amp (float): The tophat amplitude
|
||||||
|
tstart (float): The start time of the tophat
|
||||||
|
tstop (float): The end time of the tophat
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The tophat evaluated at ``x`` times
|
||||||
|
"""
|
||||||
|
mask = (x >= tstart) & (x <= tstop)
|
||||||
|
fxn = np.zeros_like(x)
|
||||||
|
fxn[mask] = amp
|
||||||
|
return fxn
|
||||||
|
|
||||||
|
|
||||||
|
def norris(x, amp, tstart, t_rise, t_decay):
|
||||||
|
r"""A Norris pulse-shape function:
|
||||||
|
|
||||||
|
:math:`I(t) = A \lambda e^{-\tau_1/t - t/\tau_2} \text{ for } t > 0;\\
|
||||||
|
\text{ where } \lambda = e^{2\sqrt(\tau_1/\tau_2)};`
|
||||||
|
|
||||||
|
and where
|
||||||
|
|
||||||
|
* :math:`A` is the pulse amplitude
|
||||||
|
* :math:`\tau_1` is the rise time
|
||||||
|
* :math:`\tau_2` is the decay time
|
||||||
|
|
||||||
|
References:
|
||||||
|
`Norris, J. P., et al. 2005 ApJ 627 324
|
||||||
|
<https://iopscience.iop.org/article/10.1086/430294>`_
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x (np.array): Array of times
|
||||||
|
amp (float): The amplitude of the pulse
|
||||||
|
tstart (float): The start time of the pulse
|
||||||
|
t_rise (float): The rise timescal of the pulse
|
||||||
|
t_decay (flaot): The decay timescale of the pulse
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The Norris pulse shape evaluated at ``x`` times
|
||||||
|
"""
|
||||||
|
x = np.asarray(x)
|
||||||
|
fxn = np.zeros_like(x)
|
||||||
|
mask = (x > tstart)
|
||||||
|
lam = amp * np.exp(2.0 * np.sqrt(t_rise / t_decay))
|
||||||
|
fxn[mask] = lam * np.exp(
|
||||||
|
-t_rise / (x[mask] - tstart) - (x[mask] - tstart) / t_decay)
|
||||||
|
return fxn
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# background profiles
|
||||||
|
def constant(x, amp):
|
||||||
|
"""A constant background function.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x (np.array): Array of times
|
||||||
|
amp (float): The background amplitude
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The background evaluated at ``x`` times
|
||||||
|
"""
|
||||||
|
fxn = np.empty(x.size)
|
||||||
|
fxn.fill(amp)
|
||||||
|
return fxn
|
||||||
|
|
||||||
|
|
||||||
|
def linear(x, c0, c1):
|
||||||
|
"""A linear background function.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x (np.array): Array of times
|
||||||
|
c0 (float): The constant coefficient
|
||||||
|
c1 (float): The linear coefficient
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The background evaluated at ``x`` times
|
||||||
|
"""
|
||||||
|
fxn = c0 + c1 * x
|
||||||
|
return fxn
|
||||||
|
|
||||||
|
|
||||||
|
def quadratic(x, c0, c1, c2):
|
||||||
|
"""A quadratic background function.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x (np.array): Array of times
|
||||||
|
c0 (float): The constant coefficient
|
||||||
|
c1 (float): The linear coefficient
|
||||||
|
c2 (float): The quadratic coefficient
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.array: The background evaluated at ``x`` times
|
||||||
|
"""
|
||||||
|
fxn = linear(x, c0, c1) + c2 * x ** 2
|
||||||
|
return fxn
|
308
simulate/tte.py
Normal file
308
simulate/tte.py
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
# tte.py: Class for simulating TTE (event) data
|
||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import warnings
|
||||||
|
from ..data import TTE
|
||||||
|
from ..data.primitives import EventList
|
||||||
|
from .generators import *
|
||||||
|
|
||||||
|
|
||||||
|
class TteSourceSimulator:
|
||||||
|
"""Simulate TTE or EventList data for a source spectrum given a detector
|
||||||
|
response, spectral model and time profile model. The spectral shape is
|
||||||
|
fixed throughout the time profile of the signal, but the amplitude of the
|
||||||
|
spectrum is time-dependent, set by the time profile function.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
rsp (:class:`~gbm.data.RSP`): A detector response object
|
||||||
|
spec_func (:class:`~gbm.spectra.functions.Function`):
|
||||||
|
A photon model function
|
||||||
|
spec_params (iterable): The parameters for the function
|
||||||
|
time_func (<function>): A time profile function
|
||||||
|
time_params (iterable): Parameters for the time profile function
|
||||||
|
sample_period (float, optional): The sampling period of the simulator
|
||||||
|
in seconds. Default is 0.001. The simulator will produce arrival
|
||||||
|
times consistent with a spectrum over a finite time slice. This
|
||||||
|
time slice should be short enough to approximate a non-homogeneous
|
||||||
|
Poisson process, but long enough to allow for a tractable
|
||||||
|
computation time.
|
||||||
|
deadtime (float, optional): The dead time in seconds for each recorded
|
||||||
|
count during which another count cannot be
|
||||||
|
recorded. Default is 2e-6 s.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, rsp, spec_func, spec_params, time_func, time_params,
|
||||||
|
sample_period=0.001, deadtime=2e-6):
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore", category=UserWarning)
|
||||||
|
|
||||||
|
if sample_period <= 0.0:
|
||||||
|
raise ValueError('Sample period must be positive')
|
||||||
|
if deadtime < 0.0:
|
||||||
|
raise ValueError('Deadtime must be non-negative')
|
||||||
|
|
||||||
|
self._rsp = rsp
|
||||||
|
self._spec_func = spec_func
|
||||||
|
self._spec_params = spec_params
|
||||||
|
self._time_func = time_func
|
||||||
|
self._time_params = time_params
|
||||||
|
self._sample_per = sample_period
|
||||||
|
self._spec_gen = VariableSourceSpectrumGenerator(rsp, spec_func,
|
||||||
|
spec_params,
|
||||||
|
sample_period)
|
||||||
|
self._event_gen = EventSpectrumGenerator(np.zeros(rsp.numchans),
|
||||||
|
self._sample_per,
|
||||||
|
min_sep=deadtime)
|
||||||
|
|
||||||
|
def set_response(self, rsp):
|
||||||
|
"""Set/change the detector response.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rsp (:class:`~gbm.data.RSP`): A detector response object
|
||||||
|
"""
|
||||||
|
self._rsp = rsp
|
||||||
|
self._spec_gen = VariableSourceSpectrumGenerator(rsp, self._spec_func,
|
||||||
|
self._spec_params,
|
||||||
|
self._sample_per)
|
||||||
|
|
||||||
|
def set_spectrum(self, spec_func, spec_params):
|
||||||
|
"""Set/change the spectrum.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec_func (:class:`~gbm.spectra.functions.Function`):
|
||||||
|
A photon model function
|
||||||
|
spec_params (iterable): The parameters for the function
|
||||||
|
"""
|
||||||
|
self._spec_func = spec_func
|
||||||
|
self._spec_params = spec_params
|
||||||
|
self._spec_gen = VariableSourceSpectrumGenerator(self._rsp,
|
||||||
|
self._spec_func,
|
||||||
|
self._spec_params,
|
||||||
|
self._sample_per)
|
||||||
|
|
||||||
|
def set_time_profile(self, time_func, time_params):
|
||||||
|
"""Set/change the time profile.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
time_func (<function>): A time profile function
|
||||||
|
time_params (iterable): Parameters for the time profile function
|
||||||
|
"""
|
||||||
|
self._time_func = time_func
|
||||||
|
self._time_params = time_params
|
||||||
|
|
||||||
|
def simulate(self, tstart, tstop):
|
||||||
|
"""Generate an EventList containing the individual counts from the
|
||||||
|
simulation
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tstart (float): The start time of the simulation
|
||||||
|
tstop (float): The stop time of the simulation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`~gbm.data.primitives.EventList`:
|
||||||
|
The simulated EventList
|
||||||
|
"""
|
||||||
|
# create the time grid
|
||||||
|
dur = (tstop - tstart)
|
||||||
|
numpts = int(round(dur / self._sample_per))
|
||||||
|
time_array = np.linspace(tstart, tstop, numpts)
|
||||||
|
|
||||||
|
# calculate the spectral amplitudes over the grid
|
||||||
|
amps = self._time_func(time_array, *self._time_params)
|
||||||
|
|
||||||
|
times = []
|
||||||
|
chans = []
|
||||||
|
for i in range(numpts):
|
||||||
|
# update amplitude and generate the count spectrum
|
||||||
|
self._spec_gen.amp = amps[i]
|
||||||
|
self._event_gen.spectrum = next(self._spec_gen).counts
|
||||||
|
# generate the count arrival times for the time slice spectrum
|
||||||
|
events = next(self._event_gen)
|
||||||
|
if events is not None:
|
||||||
|
times.extend((events[0] + time_array[i]).tolist())
|
||||||
|
chans.extend(events[1].tolist())
|
||||||
|
|
||||||
|
# create the eventlist
|
||||||
|
eventlist = EventList.from_lists(times, chans,
|
||||||
|
self._rsp.ebounds['E_MIN'],
|
||||||
|
self._rsp.ebounds['E_MAX'])
|
||||||
|
eventlist.sort('TIME')
|
||||||
|
return eventlist
|
||||||
|
|
||||||
|
def to_tte(self, tstart, tstop, trigtime=None, **kwargs):
|
||||||
|
"""Generate an TTE object containing the individual counts from the
|
||||||
|
simulation
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tstart (float): The start time of the simulation
|
||||||
|
tstop (float): The stop time of the simulation
|
||||||
|
trigtime (float, optional): The trigger time. Default is 0.
|
||||||
|
**kwargs: Options to pass to :class:`~gbm.data.TTE`
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`~gbm.data.TTE`:
|
||||||
|
The simulated TTE object
|
||||||
|
"""
|
||||||
|
if trigtime is None:
|
||||||
|
trigtime = 0.0
|
||||||
|
eventlist = self.simulate(tstart, tstop)
|
||||||
|
tte = TTE.from_data(eventlist, trigtime=trigtime, **kwargs)
|
||||||
|
return tte
|
||||||
|
|
||||||
|
|
||||||
|
class TteBackgroundSimulator:
|
||||||
|
"""Simulate TTE or EventList data given a modeled background spectrum and
|
||||||
|
time profile model. The spectrum is fixed throughout the time profile of
|
||||||
|
the background, but the amplitude of the background is time-dependent, set
|
||||||
|
by the time profile function.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
bkgd_spectrum (:class:`~gbm.background.BackgroundSpectrum`):
|
||||||
|
A modeled background spectrum
|
||||||
|
distrib (str): The distribution from which the background is
|
||||||
|
simulated; either 'Poisson' or 'Gaussian'
|
||||||
|
time_func (<function>): A time profile function
|
||||||
|
time_params (iterable): Parameters for the time profile function
|
||||||
|
sample_period (float, optional): The sampling period of the simulator
|
||||||
|
in seconds. Default is 0.001. The simulator will produce arrival
|
||||||
|
times consistent with a spectrum over a finite time slice. This
|
||||||
|
time slice should be short enough to approximate a non-homogeneous
|
||||||
|
Poisson process, but long enough to allow for a tractable
|
||||||
|
computation time.
|
||||||
|
deadtime (float, optional): The dead time in seconds for each recorded
|
||||||
|
count during which another count cannot be
|
||||||
|
recorded. Default is 2e-6 s.
|
||||||
|
"""
|
||||||
|
def __init__(self, bkgd_spectrum, distrib, time_func, time_params,
|
||||||
|
sample_period=0.001, deadtime=2e-6):
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore", category=UserWarning)
|
||||||
|
|
||||||
|
if sample_period <= 0.0:
|
||||||
|
raise ValueError('Sample period must be positive')
|
||||||
|
if deadtime < 0.0:
|
||||||
|
raise ValueError('Deadtime must be non-negative')
|
||||||
|
|
||||||
|
self._spec_gen = None
|
||||||
|
self._bkgd = bkgd_spectrum
|
||||||
|
self._time_func = time_func
|
||||||
|
self._time_params = time_params
|
||||||
|
self._sample_per = sample_period
|
||||||
|
self._deadtime = deadtime
|
||||||
|
self._event_gen = EventSpectrumGenerator(np.zeros(self._bkgd.size),
|
||||||
|
self._sample_per,
|
||||||
|
min_sep=deadtime)
|
||||||
|
self.set_background(bkgd_spectrum, distrib)
|
||||||
|
|
||||||
|
def set_background(self, bkgd_spectrum, distrib):
|
||||||
|
"""Set/change the spectrum.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bkgd_spectrum (:class:`~gbm.background.BackgroundSpectrum`):
|
||||||
|
A modeled background spectrum
|
||||||
|
distrib (str): The distribution from which the background is
|
||||||
|
simulated; either 'Poisson' or 'Gaussian'
|
||||||
|
"""
|
||||||
|
bkgd = BackgroundSpectrum(bkgd_spectrum.rates,
|
||||||
|
bkgd_spectrum.rate_uncertainty,
|
||||||
|
bkgd_spectrum.lo_edges,
|
||||||
|
bkgd_spectrum.hi_edges,
|
||||||
|
[self._sample_per] * bkgd_spectrum.size)
|
||||||
|
|
||||||
|
if distrib == 'Poisson':
|
||||||
|
self._spec_gen = VariablePoissonBackground(bkgd)
|
||||||
|
elif distrib == 'Gaussian':
|
||||||
|
self._spec_gen = VariableGaussianBackground(bkgd)
|
||||||
|
else:
|
||||||
|
raise ValueError("distrib can only be 'Poisson' or 'Gaussian'")
|
||||||
|
|
||||||
|
def simulate(self, tstart, tstop):
|
||||||
|
"""Generate an EventList containing the individual counts from the
|
||||||
|
background simulation
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tstart (float): The start time of the simulation
|
||||||
|
tstop (float): The stop time of the simulation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`~gbm.data.primitives.EventList`:
|
||||||
|
The simulated EventList
|
||||||
|
"""
|
||||||
|
# create the time grid
|
||||||
|
dur = (tstop - tstart)
|
||||||
|
numpts = int(round(dur / self._sample_per))
|
||||||
|
time_array = np.linspace(tstart, tstop, numpts)
|
||||||
|
|
||||||
|
# calculate the spectral amplitudes over the grid
|
||||||
|
amps = self._time_func(time_array, *self._time_params)
|
||||||
|
|
||||||
|
times = []
|
||||||
|
chans = []
|
||||||
|
for i in range(numpts):
|
||||||
|
# update amplitude and generate the count spectrum
|
||||||
|
self._spec_gen.amp = amps[i]
|
||||||
|
self._event_gen.spectrum = self._whole_counts(
|
||||||
|
next(self._spec_gen).counts)
|
||||||
|
# generate the count arrival times for the time slice spectrum
|
||||||
|
events = next(self._event_gen)
|
||||||
|
if events is not None:
|
||||||
|
times.extend((events[0] + time_array[i]).tolist())
|
||||||
|
chans.extend(events[1].tolist())
|
||||||
|
|
||||||
|
# create the eventlist
|
||||||
|
eventlist = EventList.from_lists(times, chans, self._bkgd.lo_edges,
|
||||||
|
self._bkgd.hi_edges)
|
||||||
|
eventlist.sort('TIME')
|
||||||
|
return eventlist
|
||||||
|
|
||||||
|
def to_tte(self, tstart, tstop, trigtime=None, **kwargs):
|
||||||
|
"""Generate an TTE object containing the individual counts from the
|
||||||
|
background simulation
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tstart (float): The start time of the simulation
|
||||||
|
tstop (float): The stop time of the simulation
|
||||||
|
trigtime (float, optional): The trigger time. Default is 0.
|
||||||
|
**kwargs: Options to pass to :class:`~gbm.data.TTE`
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`~gbm.data.TTE`:
|
||||||
|
The simulated TTE object
|
||||||
|
"""
|
||||||
|
if trigtime is None:
|
||||||
|
trigtime = 0.0
|
||||||
|
eventlist = self.simulate(tstart, tstop)
|
||||||
|
tte = TTE.from_data(eventlist, trigtime=trigtime, **kwargs)
|
||||||
|
return tte
|
||||||
|
|
||||||
|
def _whole_counts(self, counts):
|
||||||
|
# because we can end up with fractional counts for the background
|
||||||
|
# (the *rate* is what is typically modeled, and so no guarantee that
|
||||||
|
# counts will come out to whole integers)
|
||||||
|
u = np.random.random(counts.size)
|
||||||
|
whole_counts = counts.astype(int)
|
||||||
|
mask = (counts - whole_counts) > u
|
||||||
|
whole_counts[mask] += 1
|
||||||
|
return whole_counts
|
0
spectra/__init__.py
Normal file
0
spectra/__init__.py
Normal file
1296
spectra/fitting.py
Normal file
1296
spectra/fitting.py
Normal file
File diff suppressed because it is too large
Load Diff
1255
spectra/functions.py
Normal file
1255
spectra/functions.py
Normal file
File diff suppressed because it is too large
Load Diff
0
test/__init__.py
Normal file
0
test/__init__.py
Normal file
146
test/data/160509374/glg_cspec_all_bn160509374_lookup_v00.json
Normal file
146
test/data/160509374/glg_cspec_all_bn160509374_lookup_v00.json
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
{
|
||||||
|
"file_date": "2018-10-18T20:45:25.282419",
|
||||||
|
"datafiles": {
|
||||||
|
"glg_cspec_n0_bn160509374_v01.pha": {
|
||||||
|
"response": "glg_cspec_n0_bn160509374_v00.rsp2",
|
||||||
|
"views": {
|
||||||
|
"time": {
|
||||||
|
"ymax": 9000.403088726776,
|
||||||
|
"xmin": -32.781242393304524,
|
||||||
|
"xmax": 102.27991366563288,
|
||||||
|
"ymin": 0.0
|
||||||
|
},
|
||||||
|
"energy": null
|
||||||
|
},
|
||||||
|
"detector": "n0",
|
||||||
|
"background": {
|
||||||
|
"args": [
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"kwargs": {},
|
||||||
|
"datatype": "binned",
|
||||||
|
"method": "Polynomial"
|
||||||
|
},
|
||||||
|
"selections": {
|
||||||
|
"source": [
|
||||||
|
[
|
||||||
|
-0.7665819823741913,
|
||||||
|
6.144056022167206
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"background": [
|
||||||
|
[
|
||||||
|
-533.1992179515761,
|
||||||
|
-298.93657220330556
|
||||||
|
],
|
||||||
|
[
|
||||||
|
677.6648470849393,
|
||||||
|
960.6054451964869
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"energy": [
|
||||||
|
9.0,
|
||||||
|
900.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"binnings": {
|
||||||
|
"time": null,
|
||||||
|
"energy": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"glg_cspec_n1_bn160509374_v01.pha": {
|
||||||
|
"response": "glg_cspec_n1_bn160509374_v00.rsp2",
|
||||||
|
"views": {
|
||||||
|
"time": {
|
||||||
|
"ymax": 8000.601402163079,
|
||||||
|
"xmin": -32.781242393304524,
|
||||||
|
"xmax": 102.27991366563288,
|
||||||
|
"ymin": 0.0
|
||||||
|
},
|
||||||
|
"energy": null
|
||||||
|
},
|
||||||
|
"detector": "n1",
|
||||||
|
"background": {
|
||||||
|
"args": [
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"kwargs": {},
|
||||||
|
"datatype": "binned",
|
||||||
|
"method": "Polynomial"
|
||||||
|
},
|
||||||
|
"selections": {
|
||||||
|
"source": [
|
||||||
|
[
|
||||||
|
-0.7665819823741913,
|
||||||
|
6.144056022167206
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"background": [
|
||||||
|
[
|
||||||
|
-533.1992179515761,
|
||||||
|
-298.93657220330556
|
||||||
|
],
|
||||||
|
[
|
||||||
|
677.6648470849393,
|
||||||
|
960.6054451964869
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"energy": [
|
||||||
|
9.0,
|
||||||
|
900.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"binnings": {
|
||||||
|
"time": null,
|
||||||
|
"energy": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"glg_cspec_b0_bn160509374_v01.pha": {
|
||||||
|
"response": "glg_cspec_b0_bn160509374_v00.rsp2",
|
||||||
|
"views": {
|
||||||
|
"time": {
|
||||||
|
"ymax": 5110.982632484237,
|
||||||
|
"xmin": -32.781242393304524,
|
||||||
|
"xmax": 102.27991366563288,
|
||||||
|
"ymin": 0.0
|
||||||
|
},
|
||||||
|
"energy": null
|
||||||
|
},
|
||||||
|
"detector": "b0",
|
||||||
|
"background": {
|
||||||
|
"args": [
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"kwargs": {},
|
||||||
|
"datatype": "binned",
|
||||||
|
"method": "Polynomial"
|
||||||
|
},
|
||||||
|
"selections": {
|
||||||
|
"source": [
|
||||||
|
[
|
||||||
|
-0.7665819823741913,
|
||||||
|
6.144056022167206
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"background": [
|
||||||
|
[
|
||||||
|
-533.1992179515761,
|
||||||
|
-298.93657220330556
|
||||||
|
],
|
||||||
|
[
|
||||||
|
677.6648470849393,
|
||||||
|
960.6054451964869
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"energy": [
|
||||||
|
250.0,
|
||||||
|
38000.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"binnings": {
|
||||||
|
"time": null,
|
||||||
|
"energy": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1687
test/data/160509374/glg_cspec_b0_bn160509374_v00.rsp2
Normal file
1687
test/data/160509374/glg_cspec_b0_bn160509374_v00.rsp2
Normal file
File diff suppressed because one or more lines are too long
22
test/data/160509374/glg_cspec_b0_bn160509374_v01.lu
Normal file
22
test/data/160509374/glg_cspec_b0_bn160509374_v01.lu
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
0
|
||||||
|
0
|
||||||
|
4
|
||||||
|
267.292 267.292 40352.0 40352.0
|
||||||
|
0
|
||||||
|
0
|
||||||
|
4
|
||||||
|
-0.54949043 -0.54949043 4.9407055 4.9407055
|
||||||
|
4
|
||||||
|
-384.32550 145.08625 -82.364724 305.87056
|
||||||
|
STACKED SPECTRA
|
||||||
|
LOG
|
||||||
|
LOG
|
||||||
|
-20.0000
|
||||||
|
60.0000
|
||||||
|
975.735
|
||||||
|
4119.82
|
||||||
|
113.007
|
||||||
|
50000.0
|
||||||
|
0.000928329
|
||||||
|
2.31709
|
||||||
|
2
|
7917
test/data/160509374/glg_cspec_b0_bn160509374_v01.pha
Normal file
7917
test/data/160509374/glg_cspec_b0_bn160509374_v01.pha
Normal file
File diff suppressed because one or more lines are too long
2232
test/data/160509374/glg_cspec_n0_bn160509374_v00.rsp2
Normal file
2232
test/data/160509374/glg_cspec_n0_bn160509374_v00.rsp2
Normal file
File diff suppressed because one or more lines are too long
22
test/data/160509374/glg_cspec_n0_bn160509374_v01.lu
Normal file
22
test/data/160509374/glg_cspec_n0_bn160509374_v01.lu
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
0
|
||||||
|
0
|
||||||
|
4
|
||||||
|
8.99418 8.99418 931.094 931.094
|
||||||
|
0
|
||||||
|
0
|
||||||
|
4
|
||||||
|
-0.54949043 -0.54949043 4.9407055 4.9407055
|
||||||
|
4
|
||||||
|
-384.32550 145.08625 -82.364724 305.87056
|
||||||
|
STACKED SPECTRA
|
||||||
|
LOG
|
||||||
|
LOG
|
||||||
|
-20.0000
|
||||||
|
60.0000
|
||||||
|
0.00000
|
||||||
|
8190.18
|
||||||
|
4.08936
|
||||||
|
2000.00
|
||||||
|
0.0201348
|
||||||
|
27.8027
|
||||||
|
2
|
8543
test/data/160509374/glg_cspec_n0_bn160509374_v01.pha
Normal file
8543
test/data/160509374/glg_cspec_n0_bn160509374_v01.pha
Normal file
File diff suppressed because one or more lines are too long
25
test/data/160509374/glg_cspec_n0_bn160509374_v02.lu
Normal file
25
test/data/160509374/glg_cspec_n0_bn160509374_v02.lu
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
36
|
||||||
|
0 1 5 9 13 17 21 25 29 33
|
||||||
|
37 41 45 49 53 57 61 65 69 73
|
||||||
|
77 81 85 89 93 97 101 105 109 113
|
||||||
|
117 121 125 126 127 128
|
||||||
|
4
|
||||||
|
8.99418 8.99418 931.094 931.094
|
||||||
|
0
|
||||||
|
0
|
||||||
|
4
|
||||||
|
-0.54949043 -0.54949043 4.9407055 4.9407055
|
||||||
|
4
|
||||||
|
-384.32550 145.08625 -82.364724 305.87056
|
||||||
|
STACKED SPECTRA
|
||||||
|
LOG
|
||||||
|
LOG
|
||||||
|
-20.0000
|
||||||
|
60.0000
|
||||||
|
797.629
|
||||||
|
8279.07
|
||||||
|
4.08936
|
||||||
|
2000.00
|
||||||
|
0.0530329
|
||||||
|
25.5599
|
||||||
|
2
|
2312
test/data/160509374/glg_cspec_n1_bn160509374_v00.rsp2
Normal file
2312
test/data/160509374/glg_cspec_n1_bn160509374_v00.rsp2
Normal file
File diff suppressed because one or more lines are too long
22
test/data/160509374/glg_cspec_n1_bn160509374_v01.lu
Normal file
22
test/data/160509374/glg_cspec_n1_bn160509374_v01.lu
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
0
|
||||||
|
0
|
||||||
|
4
|
||||||
|
8.99418 8.99418 931.094 931.094
|
||||||
|
0
|
||||||
|
0
|
||||||
|
4
|
||||||
|
-0.54949043 -0.54949043 4.9407055 4.9407055
|
||||||
|
4
|
||||||
|
-384.32550 145.08625 -82.364724 305.87056
|
||||||
|
STACKED SPECTRA
|
||||||
|
LOG
|
||||||
|
LOG
|
||||||
|
-20.0000
|
||||||
|
60.0000
|
||||||
|
0.00000
|
||||||
|
8000.00
|
||||||
|
4.08936
|
||||||
|
2000.00
|
||||||
|
0.0368540
|
||||||
|
24.7507
|
||||||
|
2
|
8092
test/data/160509374/glg_cspec_n1_bn160509374_v01.pha
Normal file
8092
test/data/160509374/glg_cspec_n1_bn160509374_v01.pha
Normal file
File diff suppressed because one or more lines are too long
41169
test/data/chi2grid_bn190531568_v00.dat
Normal file
41169
test/data/chi2grid_bn190531568_v00.dat
Normal file
File diff suppressed because it is too large
Load Diff
3292
test/data/glg_cspec_b0_bn120415958_v00.pha
Normal file
3292
test/data/glg_cspec_b0_bn120415958_v00.pha
Normal file
File diff suppressed because one or more lines are too long
22
test/data/glg_cspec_b1_bn090926181_v04.lu
Normal file
22
test/data/glg_cspec_b1_bn090926181_v04.lu
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
0
|
||||||
|
0
|
||||||
|
4
|
||||||
|
186.453 186.453 36701.9 36701.9
|
||||||
|
0
|
||||||
|
0
|
||||||
|
4
|
||||||
|
0.54854876 0.54854876 18.901489 18.901489
|
||||||
|
4
|
||||||
|
-44.712900 44.698800 -9.4188300 858.81600
|
||||||
|
STACKED SPECTRA
|
||||||
|
LOG
|
||||||
|
LOG
|
||||||
|
-20.0000
|
||||||
|
60.0000
|
||||||
|
0.00000
|
||||||
|
6388.14
|
||||||
|
104.791
|
||||||
|
50000.0
|
||||||
|
0.000846787
|
||||||
|
13.1466
|
||||||
|
3
|
2107
test/data/glg_cspec_n4_bn120415958_v00.rsp2
Normal file
2107
test/data/glg_cspec_n4_bn120415958_v00.rsp2
Normal file
File diff suppressed because one or more lines are too long
1364
test/data/glg_cspec_n9_bn090131090_v00.rsp2
Normal file
1364
test/data/glg_cspec_n9_bn090131090_v00.rsp2
Normal file
File diff suppressed because one or more lines are too long
333
test/data/glg_ctime_nb_bn120415958_v00.lu
Normal file
333
test/data/glg_ctime_nb_bn120415958_v00.lu
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
0
|
||||||
|
0
|
||||||
|
4
|
||||||
|
18.8469 18.8469 767.693 767.693
|
||||||
|
1861
|
||||||
|
0 3 7 11 15 19
|
||||||
|
23 27 31 35 39 43
|
||||||
|
47 51 55 59 63 67
|
||||||
|
71 75 79 83 87 91
|
||||||
|
95 99 103 107 111 115
|
||||||
|
119 123 127 131 135 139
|
||||||
|
143 147 151 155 159 163
|
||||||
|
167 171 175 179 183 187
|
||||||
|
191 195 199 203 207 211
|
||||||
|
215 219 223 227 231 235
|
||||||
|
239 243 247 251 255 259
|
||||||
|
263 267 271 275 279 283
|
||||||
|
287 291 295 299 303 307
|
||||||
|
311 315 319 323 327 331
|
||||||
|
335 339 343 347 351 355
|
||||||
|
359 363 367 371 375 379
|
||||||
|
383 387 391 395 399 403
|
||||||
|
407 411 415 419 423 427
|
||||||
|
431 435 439 443 447 451
|
||||||
|
455 459 463 467 471 475
|
||||||
|
479 483 487 491 495 499
|
||||||
|
503 507 511 515 519 523
|
||||||
|
527 531 535 539 543 547
|
||||||
|
551 555 559 563 567 571
|
||||||
|
575 579 583 587 591 595
|
||||||
|
599 603 607 611 615 619
|
||||||
|
623 627 631 635 639 643
|
||||||
|
647 651 655 659 663 667
|
||||||
|
671 675 679 683 687 691
|
||||||
|
695 699 703 707 711 715
|
||||||
|
719 723 727 731 735 739
|
||||||
|
743 747 751 755 759 763
|
||||||
|
767 771 775 779 783 787
|
||||||
|
791 795 799 803 807 811
|
||||||
|
815 819 823 827 831 835
|
||||||
|
839 843 847 851 855 859
|
||||||
|
863 867 871 875 879 883
|
||||||
|
887 891 895 899 903 907
|
||||||
|
911 915 919 923 927 931
|
||||||
|
935 939 943 947 951 955
|
||||||
|
959 963 967 971 975 979
|
||||||
|
983 987 991 995 999 1003
|
||||||
|
1007 1011 1015 1019 1023 1027
|
||||||
|
1031 1035 1039 1043 1047 1051
|
||||||
|
1055 1059 1063 1067 1071 1075
|
||||||
|
1079 1083 1087 1091 1095 1099
|
||||||
|
1103 1107 1111 1115 1119 1123
|
||||||
|
1127 1131 1135 1139 1143 1147
|
||||||
|
1151 1155 1159 1163 1167 1171
|
||||||
|
1175 1179 1183 1187 1191 1195
|
||||||
|
1199 1203 1207 1211 1215 1219
|
||||||
|
1223 1227 1231 1235 1239 1243
|
||||||
|
1247 1251 1255 1259 1263 1267
|
||||||
|
1271 1275 1279 1283 1287 1291
|
||||||
|
1295 1299 1303 1307 1311 1315
|
||||||
|
1319 1323 1327 1331 1335 1339
|
||||||
|
1343 1347 1351 1355 1359 1363
|
||||||
|
1367 1371 1375 1379 1383 1387
|
||||||
|
1391 1395 1399 1403 1407 1411
|
||||||
|
1415 1419 1423 1427 1431 1435
|
||||||
|
1439 1443 1447 1451 1455 1459
|
||||||
|
1463 1467 1471 1475 1479 1483
|
||||||
|
1487 1491 1495 1499 1503 1507
|
||||||
|
1511 1515 1519 1523 1527 1531
|
||||||
|
1535 1539 1543 1547 1551 1555
|
||||||
|
1559 1563 1567 1571 1575 1579
|
||||||
|
1583 1587 1591 1595 1599 1603
|
||||||
|
1607 1611 1615 1619 1623 1627
|
||||||
|
1631 1635 1639 1643 1647 1651
|
||||||
|
1655 1659 1663 1667 1671 1675
|
||||||
|
1679 1683 1687 1691 1695 1699
|
||||||
|
1703 1707 1711 1715 1719 1723
|
||||||
|
1727 1731 1735 1739 1743 1747
|
||||||
|
1751 1755 1759 1763 1767 1771
|
||||||
|
1775 1779 1783 1787 1791 1795
|
||||||
|
1799 1803 1807 1811 1815 1819
|
||||||
|
1823 1827 1831 1835 1839 1843
|
||||||
|
1847 1851 1855 1859 1863 1867
|
||||||
|
1871 1875 1879 1883 1887 1891
|
||||||
|
1895 1899 1903 1907 1911 1915
|
||||||
|
1919 1923 1927 1931 1935 1939
|
||||||
|
1943 1947 1951 1955 1959 1963
|
||||||
|
1967 1971 1975 1979 1983 1987
|
||||||
|
1991 1995 1999 2003 2007 2011
|
||||||
|
2015 2019 2023 2027 2031 2035
|
||||||
|
2039 2043 2047 2051 2055 2059
|
||||||
|
2063 2067 2071 2075 2079 2083
|
||||||
|
2087 2091 2095 2099 2103 2107
|
||||||
|
2111 2115 2119 2123 2127 2131
|
||||||
|
2135 2139 2143 2147 2151 2155
|
||||||
|
2159 2163 2167 2171 2175 2179
|
||||||
|
2183 2187 2191 2195 2199 2203
|
||||||
|
2207 2211 2215 2219 2223 2227
|
||||||
|
2231 2235 2239 2243 2247 2251
|
||||||
|
2255 2259 2263 2267 2271 2275
|
||||||
|
2279 2283 2287 2291 2295 2299
|
||||||
|
2303 2307 2311 2315 2319 2323
|
||||||
|
2327 2331 2335 2339 2343 2347
|
||||||
|
2351 2355 2359 2363 2367 2371
|
||||||
|
2375 2379 2383 2387 2391 2395
|
||||||
|
2399 2403 2407 2411 2415 2419
|
||||||
|
2423 2427 2431 2435 2439 2443
|
||||||
|
2447 2451 2455 2459 2463 2467
|
||||||
|
2471 2475 2479 2483 2487 2491
|
||||||
|
2495 2499 2503 2507 2511 2515
|
||||||
|
2519 2523 2527 2531 2535 2539
|
||||||
|
2543 2547 2551 2555 2559 2563
|
||||||
|
2567 2571 2575 2579 2583 2587
|
||||||
|
2591 2595 2599 2603 2607 2611
|
||||||
|
2615 2619 2623 2627 2631 2635
|
||||||
|
2639 2643 2647 2651 2655 2659
|
||||||
|
2663 2667 2671 2675 2679 2683
|
||||||
|
2687 2691 2695 2699 2703 2707
|
||||||
|
2711 2715 2719 2723 2727 2731
|
||||||
|
2735 2739 2743 2747 2751 2755
|
||||||
|
2759 2763 2767 2771 2775 2779
|
||||||
|
2783 2787 2791 2795 2799 2803
|
||||||
|
2807 2811 2815 2819 2823 2827
|
||||||
|
2831 2835 2839 2843 2847 2851
|
||||||
|
2855 2859 2863 2867 2871 2875
|
||||||
|
2879 2883 2887 2891 2895 2899
|
||||||
|
2903 2907 2911 2915 2919 2923
|
||||||
|
2927 2931 2935 2939 2943 2947
|
||||||
|
2951 2955 2959 2963 2967 2971
|
||||||
|
2975 2979 2983 2987 2991 2995
|
||||||
|
2999 3003 3007 3011 3015 3019
|
||||||
|
3023 3027 3031 3035 3039 3043
|
||||||
|
3047 3051 3055 3059 3063 3067
|
||||||
|
3071 3075 3079 3083 3087 3091
|
||||||
|
3095 3099 3103 3107 3111 3115
|
||||||
|
3119 3123 3127 3131 3135 3139
|
||||||
|
3143 3147 3151 3155 3159 3163
|
||||||
|
3167 3171 3175 3179 3183 3187
|
||||||
|
3191 3195 3199 3203 3207 3211
|
||||||
|
3215 3219 3223 3227 3231 3235
|
||||||
|
3239 3243 3247 3251 3255 3259
|
||||||
|
3263 3267 3271 3275 3279 3283
|
||||||
|
3287 3291 3295 3299 3303 3307
|
||||||
|
3311 3315 3319 3323 3327 3331
|
||||||
|
3335 3339 3343 3347 3351 3355
|
||||||
|
3359 3363 3367 3371 3375 3379
|
||||||
|
3383 3387 3391 3395 3399 3403
|
||||||
|
3407 3411 3415 3419 3423 3427
|
||||||
|
3431 3435 3439 3443 3447 3451
|
||||||
|
3455 3459 3463 3467 3471 3475
|
||||||
|
3479 3483 3487 3491 3495 3499
|
||||||
|
3503 3507 3511 3512 3513 3529
|
||||||
|
3545 3561 3577 3593 3609 3625
|
||||||
|
3641 3657 3673 3689 3705 3721
|
||||||
|
3737 3753 3769 3785 3801 3817
|
||||||
|
3833 3849 3865 3881 3897 3913
|
||||||
|
3929 3945 3961 3977 3993 4009
|
||||||
|
4025 4041 4057 4073 4089 4105
|
||||||
|
4121 4137 4153 4169 4185 4201
|
||||||
|
4217 4233 4249 4265 4281 4297
|
||||||
|
4313 4329 4345 4361 4377 4393
|
||||||
|
4409 4425 4441 4457 4473 4489
|
||||||
|
4505 4521 4537 4553 4569 4585
|
||||||
|
4601 4617 4633 4649 4665 4681
|
||||||
|
4697 4713 4729 4745 4761 4777
|
||||||
|
4793 4809 4825 4841 4857 4873
|
||||||
|
4889 4905 4921 4937 4953 4969
|
||||||
|
4985 5001 5017 5033 5049 5065
|
||||||
|
5081 5097 5113 5129 5145 5161
|
||||||
|
5177 5193 5209 5225 5241 5257
|
||||||
|
5273 5289 5305 5321 5337 5353
|
||||||
|
5369 5385 5401 5417 5433 5449
|
||||||
|
5465 5481 5497 5513 5529 5545
|
||||||
|
5561 5577 5593 5609 5625 5641
|
||||||
|
5657 5673 5689 5705 5721 5737
|
||||||
|
5753 5769 5785 5801 5817 5833
|
||||||
|
5849 5865 5881 5897 5913 5929
|
||||||
|
5945 5961 5977 5993 6009 6025
|
||||||
|
6041 6057 6073 6089 6105 6121
|
||||||
|
6137 6153 6169 6185 6201 6217
|
||||||
|
6233 6249 6265 6281 6297 6313
|
||||||
|
6329 6345 6361 6377 6393 6409
|
||||||
|
6425 6441 6457 6473 6489 6505
|
||||||
|
6521 6537 6553 6569 6585 6601
|
||||||
|
6617 6633 6649 6665 6681 6697
|
||||||
|
6713 6729 6745 6761 6777 6793
|
||||||
|
6809 6825 6841 6857 6873 6889
|
||||||
|
6905 6921 6937 6953 6969 6985
|
||||||
|
7001 7017 7033 7049 7065 7081
|
||||||
|
7097 7113 7129 7145 7161 7177
|
||||||
|
7193 7209 7225 7241 7257 7273
|
||||||
|
7289 7305 7321 7337 7353 7369
|
||||||
|
7385 7401 7417 7433 7449 7465
|
||||||
|
7481 7497 7513 7529 7545 7561
|
||||||
|
7577 7593 7609 7625 7641 7657
|
||||||
|
7673 7689 7705 7721 7737 7753
|
||||||
|
7769 7785 7801 7817 7833 7849
|
||||||
|
7865 7881 7897 7913 7929 7945
|
||||||
|
7961 7977 7993 8009 8025 8041
|
||||||
|
8057 8073 8089 8105 8121 8137
|
||||||
|
8153 8169 8185 8201 8217 8233
|
||||||
|
8249 8265 8281 8297 8313 8329
|
||||||
|
8345 8361 8377 8393 8409 8425
|
||||||
|
8441 8457 8473 8489 8505 8521
|
||||||
|
8537 8553 8569 8585 8601 8617
|
||||||
|
8633 8649 8665 8681 8697 8713
|
||||||
|
8729 8745 8761 8777 8793 8809
|
||||||
|
8825 8841 8857 8873 8889 8905
|
||||||
|
8921 8937 8953 8969 8985 9001
|
||||||
|
9017 9033 9049 9065 9081 9097
|
||||||
|
9113 9129 9145 9161 9177 9193
|
||||||
|
9209 9225 9241 9257 9273 9289
|
||||||
|
9305 9321 9337 9353 9369 9385
|
||||||
|
9401 9417 9433 9449 9465 9481
|
||||||
|
9497 9513 9529 9545 9561 9577
|
||||||
|
9593 9609 9625 9641 9657 9673
|
||||||
|
9689 9705 9721 9737 9753 9769
|
||||||
|
9785 9801 9817 9833 9849 9865
|
||||||
|
9881 9897 9913 9929 9945 9961
|
||||||
|
9977 9993 10009 10025 10041 10057
|
||||||
|
10073 10089 10105 10121 10137 10153
|
||||||
|
10169 10185 10201 10217 10233 10249
|
||||||
|
10265 10281 10297 10313 10329 10345
|
||||||
|
10361 10377 10393 10409 10425 10441
|
||||||
|
10457 10473 10489 10505 10521 10537
|
||||||
|
10553 10569 10585 10601 10617 10633
|
||||||
|
10649 10665 10681 10697 10713 10729
|
||||||
|
10745 10761 10777 10793 10809 10825
|
||||||
|
10841 10857 10873 10889 10905 10921
|
||||||
|
10937 10953 10969 10985 11001 11017
|
||||||
|
11033 11049 11065 11081 11097 11113
|
||||||
|
11129 11145 11161 11177 11193 11209
|
||||||
|
11225 11241 11257 11273 11289 11305
|
||||||
|
11321 11337 11353 11369 11385 11401
|
||||||
|
11417 11433 11449 11465 11481 11497
|
||||||
|
11513 11529 11545 11561 11577 11593
|
||||||
|
11609 11625 11641 11657 11673 11689
|
||||||
|
11705 11721 11737 11753 11769 11785
|
||||||
|
11801 11817 11833 11849 11865 11881
|
||||||
|
11897 11913 11929 11945 11961 11977
|
||||||
|
11993 12009 12025 12041 12057 12073
|
||||||
|
12089 12105 12121 12137 12153 12169
|
||||||
|
12185 12201 12217 12233 12249 12265
|
||||||
|
12281 12297 12313 12329 12345 12361
|
||||||
|
12377 12393 12409 12425 12441 12457
|
||||||
|
12473 12489 12505 12521 12537 12553
|
||||||
|
12569 12585 12601 12617 12633 12649
|
||||||
|
12665 12681 12697 12713 12729 12745
|
||||||
|
12761 12777 12793 12809 12825 12841
|
||||||
|
12850 12851 12855 12859 12863 12867
|
||||||
|
12871 12875 12879 12883 12887 12891
|
||||||
|
12895 12899 12903 12907 12911 12915
|
||||||
|
12919 12923 12927 12931 12935 12939
|
||||||
|
12943 12947 12951 12955 12959 12963
|
||||||
|
12967 12971 12975 12979 12983 12987
|
||||||
|
12991 12995 12999 13003 13007 13011
|
||||||
|
13015 13019 13023 13027 13031 13035
|
||||||
|
13039 13043 13047 13051 13055 13059
|
||||||
|
13063 13067 13071 13075 13079 13083
|
||||||
|
13087 13091 13095 13099 13103 13107
|
||||||
|
13111 13115 13119 13123 13127 13131
|
||||||
|
13135 13139 13143 13147 13151 13155
|
||||||
|
13159 13163 13167 13171 13175 13179
|
||||||
|
13183 13187 13191 13195 13199 13203
|
||||||
|
13207 13211 13215 13219 13223 13227
|
||||||
|
13231 13235 13239 13243 13247 13251
|
||||||
|
13255 13259 13263 13267 13271 13275
|
||||||
|
13279 13283 13287 13291 13295 13299
|
||||||
|
13303 13307 13311 13315 13319 13323
|
||||||
|
13327 13331 13335 13339 13343 13347
|
||||||
|
13351 13355 13359 13363 13367 13371
|
||||||
|
13375 13379 13383 13387 13391 13395
|
||||||
|
13399 13403 13407 13411 13415 13419
|
||||||
|
13423 13427 13431 13435 13439 13443
|
||||||
|
13447 13451 13455 13459 13463 13467
|
||||||
|
13471 13475 13479 13483 13487 13491
|
||||||
|
13495 13499 13503 13507 13511 13515
|
||||||
|
13519 13523 13527 13531 13535 13539
|
||||||
|
13543 13547 13551 13555 13559 13563
|
||||||
|
13567 13571 13575 13579 13583 13587
|
||||||
|
13591 13595 13599 13603 13607 13611
|
||||||
|
13615 13619 13623 13627 13631 13635
|
||||||
|
13639 13643 13647 13651 13655 13659
|
||||||
|
13663 13667 13671 13675 13679 13683
|
||||||
|
13687 13691 13695 13699 13703 13707
|
||||||
|
13711 13715 13719 13723 13727 13731
|
||||||
|
13735 13739 13743 13747 13751 13755
|
||||||
|
13759 13763 13767 13771 13775 13779
|
||||||
|
13783 13787 13791 13795 13799 13803
|
||||||
|
13807 13811 13815 13819 13823 13827
|
||||||
|
13831 13835 13839 13843 13847 13851
|
||||||
|
13855 13859 13863 13867 13871 13875
|
||||||
|
13879 13883 13887 13891 13895 13899
|
||||||
|
13903 13907 13911 13915 13919 13923
|
||||||
|
13927 13931 13935 13939 13943 13947
|
||||||
|
13951 13955 13959 13963 13967 13971
|
||||||
|
13975 13979 13983 13987 13991 13995
|
||||||
|
13999 14003 14007 14011 14015 14019
|
||||||
|
14023 14027 14031 14035 14039 14043
|
||||||
|
14047 14051 14055 14059 14063 14067
|
||||||
|
14071 14075 14079 14083 14087 14091
|
||||||
|
14095 14099 14103 14107 14111 14115
|
||||||
|
14119 14123 14127 14131 14135 14139
|
||||||
|
14143 14147 14151 14155 14159 14163
|
||||||
|
14167 14171 14175 14179 14183 14187
|
||||||
|
14191 14195 14199 14203 14207 14211
|
||||||
|
14215 14219 14223 14227 14231 14235
|
||||||
|
14239 14243 14247 14251 14255 14259
|
||||||
|
14263 14267 14271 14275 14279 14283
|
||||||
|
14287 14291 14295 14299 14303 14307
|
||||||
|
14311 14315 14319 14323 14327 14331
|
||||||
|
14335 14339 14343 14347 14351 14355
|
||||||
|
14359 14363 14367 14371 14375 14379
|
||||||
|
14383 14387 14391 14395 14399 14403
|
||||||
|
14407 14411 14415 14419 14423 14425
|
||||||
|
14426
|
||||||
|
6
|
||||||
|
-17.647648 -17.647648 -5.2947070 2.7445084 -14.902550
|
||||||
|
2.7445084
|
||||||
|
4
|
||||||
|
-480.00706 72.934103 -115.30119 522.34586
|
||||||
|
STACKED SPECTRA
|
||||||
|
LOG
|
||||||
|
LOG
|
||||||
|
-60.0000
|
||||||
|
40.0000
|
||||||
|
400.000
|
||||||
|
1400.00
|
||||||
|
4.32375
|
||||||
|
2000.00
|
||||||
|
0.0676773
|
||||||
|
22.8421
|
||||||
|
2
|
4149
test/data/glg_ctime_nb_bn120415958_v00.pha
Normal file
4149
test/data/glg_ctime_nb_bn120415958_v00.pha
Normal file
File diff suppressed because one or more lines are too long
4213
test/data/glg_healpix_all_bn190915240_v00.fit
Normal file
4213
test/data/glg_healpix_all_bn190915240_v00.fit
Normal file
File diff suppressed because one or more lines are too long
35559
test/data/glg_poshist_all_170101_v00.fit
Normal file
35559
test/data/glg_poshist_all_170101_v00.fit
Normal file
File diff suppressed because one or more lines are too long
41
test/data/glg_scat_all_bn170817529_flnc_comp_v02.fit
Normal file
41
test/data/glg_scat_all_bn170817529_flnc_comp_v02.fit
Normal file
File diff suppressed because one or more lines are too long
1
test/data/glg_tcat_all_bn190222537_v01.fit
Normal file
1
test/data/glg_tcat_all_bn190222537_v01.fit
Normal file
File diff suppressed because one or more lines are too long
154
test/data/glg_trigdat_all_bn170101116_v01.fit
Normal file
154
test/data/glg_trigdat_all_bn170101116_v01.fit
Normal file
File diff suppressed because one or more lines are too long
18
test/data/glg_tte_n0_bn160509374_xspec_v00.bak
Normal file
18
test/data/glg_tte_n0_bn160509374_xspec_v00.bak
Normal file
File diff suppressed because one or more lines are too long
16096
test/data/glg_tte_n9_bn090131090_v00.fit
Normal file
16096
test/data/glg_tte_n9_bn090131090_v00.fit
Normal file
File diff suppressed because one or more lines are too long
22
test/data/glg_tte_n9_bn090131090_v00.lu
Normal file
22
test/data/glg_tte_n9_bn090131090_v00.lu
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
0
|
||||||
|
0
|
||||||
|
4
|
||||||
|
9.22656 9.22656 890.680 890.680
|
||||||
|
0
|
||||||
|
0
|
||||||
|
4
|
||||||
|
23.040000 23.040000 41.472000 41.472000
|
||||||
|
4
|
||||||
|
-20.589412 71.959605 -2.9423535 141.76352
|
||||||
|
STACKED SPECTRA
|
||||||
|
LOG
|
||||||
|
LOG
|
||||||
|
-20.0000
|
||||||
|
80.0000
|
||||||
|
0.00000
|
||||||
|
5000.00
|
||||||
|
4.38973
|
||||||
|
2000.00
|
||||||
|
0.0281823
|
||||||
|
55.7819
|
||||||
|
1
|
320
test/data/glg_tte_n9_bn090131090_v00.ti
Normal file
320
test/data/glg_tte_n9_bn090131090_v00.ti
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
319
|
||||||
|
-25.600000
|
||||||
|
-24.576000
|
||||||
|
-23.552000
|
||||||
|
-22.528000
|
||||||
|
-21.504000
|
||||||
|
-20.480000
|
||||||
|
-19.456000
|
||||||
|
-18.432000
|
||||||
|
-17.408000
|
||||||
|
-16.384000
|
||||||
|
-15.360000
|
||||||
|
-14.336000
|
||||||
|
-13.312000
|
||||||
|
-12.288000
|
||||||
|
-11.264000
|
||||||
|
-10.240000
|
||||||
|
-9.2160000
|
||||||
|
-8.1920000
|
||||||
|
-7.1680000
|
||||||
|
-6.1440000
|
||||||
|
-5.1200000
|
||||||
|
-4.0960000
|
||||||
|
-3.0720000
|
||||||
|
-2.0480000
|
||||||
|
-1.0240000
|
||||||
|
0.0000000
|
||||||
|
1.0240000
|
||||||
|
2.0480000
|
||||||
|
3.0720000
|
||||||
|
4.0960000
|
||||||
|
5.1200000
|
||||||
|
6.1440000
|
||||||
|
7.1680000
|
||||||
|
8.1920000
|
||||||
|
9.2160000
|
||||||
|
10.240000
|
||||||
|
11.264000
|
||||||
|
12.288000
|
||||||
|
13.312000
|
||||||
|
14.336000
|
||||||
|
15.360000
|
||||||
|
16.384000
|
||||||
|
17.408000
|
||||||
|
18.432000
|
||||||
|
19.456000
|
||||||
|
20.480000
|
||||||
|
21.504000
|
||||||
|
22.528000
|
||||||
|
23.552000
|
||||||
|
24.576000
|
||||||
|
25.600000
|
||||||
|
26.624000
|
||||||
|
27.648000
|
||||||
|
28.672000
|
||||||
|
29.696000
|
||||||
|
30.720000
|
||||||
|
31.744000
|
||||||
|
32.768000
|
||||||
|
33.792000
|
||||||
|
34.816000
|
||||||
|
35.840000
|
||||||
|
36.864000
|
||||||
|
37.888000
|
||||||
|
38.912000
|
||||||
|
39.936000
|
||||||
|
40.960000
|
||||||
|
41.984000
|
||||||
|
43.008000
|
||||||
|
44.032000
|
||||||
|
45.056000
|
||||||
|
46.080000
|
||||||
|
47.104000
|
||||||
|
48.128000
|
||||||
|
49.152000
|
||||||
|
50.176000
|
||||||
|
51.200000
|
||||||
|
52.224000
|
||||||
|
53.248000
|
||||||
|
54.272000
|
||||||
|
55.296000
|
||||||
|
56.320000
|
||||||
|
57.344000
|
||||||
|
58.368000
|
||||||
|
59.392000
|
||||||
|
60.416000
|
||||||
|
61.440000
|
||||||
|
62.464000
|
||||||
|
63.488000
|
||||||
|
64.512000
|
||||||
|
65.536000
|
||||||
|
66.560000
|
||||||
|
67.584000
|
||||||
|
68.608000
|
||||||
|
69.632000
|
||||||
|
70.656000
|
||||||
|
71.680000
|
||||||
|
72.704000
|
||||||
|
73.728000
|
||||||
|
74.752000
|
||||||
|
75.776000
|
||||||
|
76.800000
|
||||||
|
77.824000
|
||||||
|
78.848000
|
||||||
|
79.872000
|
||||||
|
80.896000
|
||||||
|
81.920000
|
||||||
|
82.944000
|
||||||
|
83.968000
|
||||||
|
84.992000
|
||||||
|
86.016000
|
||||||
|
87.040000
|
||||||
|
88.064000
|
||||||
|
89.088000
|
||||||
|
90.112000
|
||||||
|
91.136000
|
||||||
|
92.160000
|
||||||
|
93.184000
|
||||||
|
94.208000
|
||||||
|
95.232000
|
||||||
|
96.256000
|
||||||
|
97.280000
|
||||||
|
98.304000
|
||||||
|
99.328000
|
||||||
|
100.35200
|
||||||
|
101.37600
|
||||||
|
102.40000
|
||||||
|
103.42400
|
||||||
|
104.44800
|
||||||
|
105.47200
|
||||||
|
106.49600
|
||||||
|
107.52000
|
||||||
|
108.54400
|
||||||
|
109.56800
|
||||||
|
110.59200
|
||||||
|
111.61600
|
||||||
|
112.64000
|
||||||
|
113.66400
|
||||||
|
114.68800
|
||||||
|
115.71200
|
||||||
|
116.73600
|
||||||
|
117.76000
|
||||||
|
118.78400
|
||||||
|
119.80800
|
||||||
|
120.83200
|
||||||
|
121.85600
|
||||||
|
122.88000
|
||||||
|
123.90400
|
||||||
|
124.92800
|
||||||
|
125.95200
|
||||||
|
126.97600
|
||||||
|
128.00000
|
||||||
|
129.02400
|
||||||
|
130.04800
|
||||||
|
131.07200
|
||||||
|
132.09600
|
||||||
|
133.12000
|
||||||
|
134.14400
|
||||||
|
135.16800
|
||||||
|
136.19200
|
||||||
|
137.21600
|
||||||
|
138.24000
|
||||||
|
139.26400
|
||||||
|
140.28800
|
||||||
|
141.31200
|
||||||
|
142.33600
|
||||||
|
143.36000
|
||||||
|
144.38400
|
||||||
|
145.40800
|
||||||
|
146.43200
|
||||||
|
147.45600
|
||||||
|
148.48000
|
||||||
|
149.50400
|
||||||
|
150.52800
|
||||||
|
151.55200
|
||||||
|
152.57600
|
||||||
|
153.60000
|
||||||
|
154.62400
|
||||||
|
155.64800
|
||||||
|
156.67200
|
||||||
|
157.69600
|
||||||
|
158.72000
|
||||||
|
159.74400
|
||||||
|
160.76800
|
||||||
|
161.79200
|
||||||
|
162.81600
|
||||||
|
163.84000
|
||||||
|
164.86400
|
||||||
|
165.88800
|
||||||
|
166.91200
|
||||||
|
167.93600
|
||||||
|
168.96000
|
||||||
|
169.98400
|
||||||
|
171.00800
|
||||||
|
172.03200
|
||||||
|
173.05600
|
||||||
|
174.08000
|
||||||
|
175.10400
|
||||||
|
176.12800
|
||||||
|
177.15200
|
||||||
|
178.17600
|
||||||
|
179.20000
|
||||||
|
180.22400
|
||||||
|
181.24800
|
||||||
|
182.27200
|
||||||
|
183.29600
|
||||||
|
184.32000
|
||||||
|
185.34400
|
||||||
|
186.36800
|
||||||
|
187.39200
|
||||||
|
188.41600
|
||||||
|
189.44000
|
||||||
|
190.46400
|
||||||
|
191.48800
|
||||||
|
192.51200
|
||||||
|
193.53600
|
||||||
|
194.56000
|
||||||
|
195.58400
|
||||||
|
196.60800
|
||||||
|
197.63200
|
||||||
|
198.65600
|
||||||
|
199.68000
|
||||||
|
200.70400
|
||||||
|
201.72800
|
||||||
|
202.75200
|
||||||
|
203.77600
|
||||||
|
204.80000
|
||||||
|
205.82400
|
||||||
|
206.84800
|
||||||
|
207.87200
|
||||||
|
208.89600
|
||||||
|
209.92000
|
||||||
|
210.94400
|
||||||
|
211.96800
|
||||||
|
212.99200
|
||||||
|
214.01600
|
||||||
|
215.04000
|
||||||
|
216.06400
|
||||||
|
217.08800
|
||||||
|
218.11200
|
||||||
|
219.13600
|
||||||
|
220.16000
|
||||||
|
221.18400
|
||||||
|
222.20800
|
||||||
|
223.23200
|
||||||
|
224.25600
|
||||||
|
225.28000
|
||||||
|
226.30400
|
||||||
|
227.32800
|
||||||
|
228.35200
|
||||||
|
229.37600
|
||||||
|
230.40000
|
||||||
|
231.42400
|
||||||
|
232.44800
|
||||||
|
233.47200
|
||||||
|
234.49600
|
||||||
|
235.52000
|
||||||
|
236.54400
|
||||||
|
237.56800
|
||||||
|
238.59200
|
||||||
|
239.61600
|
||||||
|
240.64000
|
||||||
|
241.66400
|
||||||
|
242.68800
|
||||||
|
243.71200
|
||||||
|
244.73600
|
||||||
|
245.76000
|
||||||
|
246.78400
|
||||||
|
247.80800
|
||||||
|
248.83200
|
||||||
|
249.85600
|
||||||
|
250.88000
|
||||||
|
251.90400
|
||||||
|
252.92800
|
||||||
|
253.95200
|
||||||
|
254.97600
|
||||||
|
256.00000
|
||||||
|
257.02400
|
||||||
|
258.04800
|
||||||
|
259.07200
|
||||||
|
260.09600
|
||||||
|
261.12000
|
||||||
|
262.14400
|
||||||
|
263.16800
|
||||||
|
264.19200
|
||||||
|
265.21600
|
||||||
|
266.24000
|
||||||
|
267.26400
|
||||||
|
268.28800
|
||||||
|
269.31200
|
||||||
|
270.33600
|
||||||
|
271.36000
|
||||||
|
272.38400
|
||||||
|
273.40800
|
||||||
|
274.43200
|
||||||
|
275.45600
|
||||||
|
276.48000
|
||||||
|
277.50400
|
||||||
|
278.52800
|
||||||
|
279.55200
|
||||||
|
280.57600
|
||||||
|
281.60000
|
||||||
|
282.62400
|
||||||
|
283.64800
|
||||||
|
284.67200
|
||||||
|
285.69600
|
||||||
|
286.72000
|
||||||
|
287.74400
|
||||||
|
288.76800
|
||||||
|
289.79200
|
||||||
|
290.81600
|
||||||
|
291.84000
|
||||||
|
292.86400
|
||||||
|
293.88800
|
||||||
|
294.91200
|
||||||
|
295.93600
|
||||||
|
296.96000
|
||||||
|
297.98400
|
||||||
|
299.00800
|
||||||
|
300.73552
|
BIN
test/data/sim_bkgd.npy
Normal file
BIN
test/data/sim_bkgd.npy
Normal file
Binary file not shown.
13
test/data/sim_pha.pha
Normal file
13
test/data/sim_pha.pha
Normal file
File diff suppressed because one or more lines are too long
214
test/test_background.py
Normal file
214
test/test_background.py
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import os
|
||||||
|
import numpy as np
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from gbm.background.binned import Polynomial
|
||||||
|
from gbm.background.unbinned import NaivePoisson
|
||||||
|
from gbm.background.background import BackgroundFitter, BackgroundRates, BackgroundSpectrum
|
||||||
|
from gbm.data.phaii import Cspec, TTE
|
||||||
|
|
||||||
|
data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data')
|
||||||
|
|
||||||
|
class TestPolynomialBackground(TestCase):
|
||||||
|
counts = np.array([[78.0, 58.0, 40.0, 26.0, 14.0, 6.0, 2.0, 0.0, 2.0, 6.0],
|
||||||
|
[6.0, 2.0, 0.0, 2.0, 6.0, 14.0, 26.0, 40.0, 58.0, 78.0]]).T
|
||||||
|
exposure = np.array([2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0, 2.0])
|
||||||
|
edges = np.array([-10.0, -8.0, -6.0, -4.0, -2.0, 0.0, 2.0, 4.0, 6.0, 8.0, 10.0])
|
||||||
|
tstart = edges[:-1]
|
||||||
|
tstop = edges[1:]
|
||||||
|
|
||||||
|
vals = np.array([[39.0, 29.0, 20.0, 13.0, 7.0, 3.0, 1.0, 0.0, 1.0, 3.0],
|
||||||
|
[3.0, 1.0, 0.0, 1.0, 3.0, 7.0, 13.0, 20.0, 29.0, 39.0]]).T
|
||||||
|
|
||||||
|
def test_fit(self):
|
||||||
|
bkgd = Polynomial(self.counts, self.tstart, self.tstop, self.exposure)
|
||||||
|
bkgd.fit(order=2)
|
||||||
|
self.assertEqual(bkgd.statistic_name, 'chisq')
|
||||||
|
self.assertCountEqual(bkgd.dof, np.array([6, 6]))
|
||||||
|
self.assertAlmostEqual(bkgd.statistic[0], 0.23, delta=0.01)
|
||||||
|
self.assertAlmostEqual(bkgd.statistic[1], 0.23, delta=0.01)
|
||||||
|
|
||||||
|
def test_interpolate(self):
|
||||||
|
bkgd = Polynomial(self.counts, self.tstart, self.tstop, self.exposure)
|
||||||
|
bkgd.fit(order=2)
|
||||||
|
rates, rate_uncert = bkgd.interpolate(self.tstart, self.tstop)
|
||||||
|
for i in range(10):
|
||||||
|
self.assertAlmostEqual(rates[i,0], self.vals[i,0], delta=0.5)
|
||||||
|
self.assertAlmostEqual(rates[i,1], self.vals[i,1], delta=0.5)
|
||||||
|
|
||||||
|
class TestNaivePoissonBackground(TestCase):
|
||||||
|
times = [np.array([0., 1.14, 1.22, 1.28, 1.76, 3.45, 4.29, 4.78, 4.75, 5.42,
|
||||||
|
5.97, 7.40, 7.61, 7.98, 8.10, 8.16, 10.18, 10.13,
|
||||||
|
13.22, 14.03])]*2
|
||||||
|
|
||||||
|
def test_fit(self):
|
||||||
|
bkgd = NaivePoisson(self.times)
|
||||||
|
bkgd.fit(window_width=5.0, fast=True)
|
||||||
|
bkgd.fit(window_width=5.0, fast=False)
|
||||||
|
|
||||||
|
def test_interpolate(self):
|
||||||
|
bkgd = NaivePoisson(self.times)
|
||||||
|
bkgd.fit(window_width=5.0, fast=True)
|
||||||
|
x = np.linspace(0.0, 14.0, 15)
|
||||||
|
rates, uncert = bkgd.interpolate(x[:-1], x[1:])
|
||||||
|
for i in range(14):
|
||||||
|
self.assertAlmostEqual(rates[i,0], 1.0, delta=2.)
|
||||||
|
self.assertAlmostEqual(rates[i,1], 1.0, delta=2.)
|
||||||
|
|
||||||
|
bkgd.fit(window_width=5.0, fast=False)
|
||||||
|
x = np.linspace(0.0, 14.0, 15)
|
||||||
|
rates, uncert = bkgd.interpolate(x[:-1], x[1:])
|
||||||
|
for i in range(14):
|
||||||
|
self.assertAlmostEqual(rates[i,0], 1.0, delta=2.)
|
||||||
|
self.assertAlmostEqual(rates[i,1], 1.0, delta=2.)
|
||||||
|
|
||||||
|
class TestBackgroundFitterBinned(TestCase):
|
||||||
|
file = os.path.join(data_dir, 'glg_cspec_b0_bn120415958_v00.pha')
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
cspec = Cspec.open(self.file)
|
||||||
|
b = BackgroundFitter.from_phaii(cspec, Polynomial,
|
||||||
|
time_ranges=[(-100, -10.0), (100.0, 200.0)])
|
||||||
|
b.fit(order=3)
|
||||||
|
self.assertEqual(b.method, 'Polynomial')
|
||||||
|
self.assertEqual(b.type, 'binned')
|
||||||
|
self.assertEqual(b.statistic_name, 'chisq')
|
||||||
|
self.assertEqual(b.dof.size, 128)
|
||||||
|
self.assertEqual(b.statistic.size, 128)
|
||||||
|
self.assertAlmostEqual(b.livetime, 190.0, delta=5.0)
|
||||||
|
self.assertEqual(b.parameters, {'order': 3})
|
||||||
|
|
||||||
|
def test_interpolate_bins(self):
|
||||||
|
cspec = Cspec.open(self.file)
|
||||||
|
b = BackgroundFitter.from_phaii(cspec, Polynomial,
|
||||||
|
time_ranges=[(-100, -10.0), (100.0, 200.0)])
|
||||||
|
b.fit(order=2)
|
||||||
|
x = np.linspace(-10.0, 11.0, 21)
|
||||||
|
tstart = x[:-1]
|
||||||
|
tstop = x[1:]
|
||||||
|
brates = b.interpolate_bins(tstart, tstop)
|
||||||
|
self.assertEqual(brates.__class__.__name__, 'BackgroundRates')
|
||||||
|
|
||||||
|
class TestBackgroundFitterUnbinned(TestCase):
|
||||||
|
file = os.path.join(data_dir, 'glg_tte_n9_bn090131090_v00.fit')
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
tte = TTE.open(self.file)
|
||||||
|
b = BackgroundFitter.from_tte(tte, NaivePoisson,
|
||||||
|
time_ranges=[(-20, 200.0)])
|
||||||
|
b.fit(window_width=30.0, fast=True)
|
||||||
|
self.assertEqual(b.method, 'NaivePoisson')
|
||||||
|
self.assertEqual(b.type, 'unbinned')
|
||||||
|
self.assertAlmostEqual(b.livetime, 220.0, delta=1.0)
|
||||||
|
self.assertEqual(b.parameters, {'fast': True, 'window_width': 30.0})
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
tte = TTE.open(self.file)
|
||||||
|
b = BackgroundFitter.from_tte(tte, NaivePoisson,
|
||||||
|
time_ranges=[(-20, 200.0)])
|
||||||
|
b.fit(window_width=30.0, fast=True)
|
||||||
|
x = np.linspace(-20.0, 200.0, 221)
|
||||||
|
tstart = x[:-1]
|
||||||
|
tstop = x[1:]
|
||||||
|
brates = b.interpolate_bins(tstart, tstop)
|
||||||
|
self.assertEqual(brates.__class__.__name__, 'BackgroundRates')
|
||||||
|
|
||||||
|
class TestBackgroundRates(TestCase):
|
||||||
|
file = os.path.join(data_dir, 'glg_cspec_b0_bn120415958_v00.pha')
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
cspec = Cspec.open(self.file)
|
||||||
|
b = BackgroundFitter.from_phaii(cspec, Polynomial,
|
||||||
|
time_ranges=[(-100, -10.0), (100.0, 200.0)])
|
||||||
|
b.fit(order=3)
|
||||||
|
|
||||||
|
b.fit(order=2)
|
||||||
|
tstart = cspec.data.tstart
|
||||||
|
mask = (tstart >= -10.0) & (tstart < 11.0)
|
||||||
|
tstart = tstart[mask]
|
||||||
|
tstop = cspec.data.tstop[mask]
|
||||||
|
brates = b.interpolate_bins(tstart, tstop)
|
||||||
|
|
||||||
|
self.assertEqual(brates.numchans, 128)
|
||||||
|
self.assertEqual(brates.numtimes, 12)
|
||||||
|
self.assertEqual(brates.size, (12, 128))
|
||||||
|
self.assertCountEqual(brates.emin, cspec.data.emin)
|
||||||
|
self.assertCountEqual(brates.emax, cspec.data.emax)
|
||||||
|
self.assertAlmostEqual(np.sum(brates.exposure), 17.0, delta=1.0)
|
||||||
|
|
||||||
|
bspec = brates.integrate_time()
|
||||||
|
self.assertEqual(bspec.__class__.__name__, 'BackgroundSpectrum')
|
||||||
|
bak = brates.to_bak()
|
||||||
|
self.assertEqual(bak.__class__.__name__, 'BAK')
|
||||||
|
|
||||||
|
brates2 = brates.integrate_energy()
|
||||||
|
self.assertEqual(brates2.counts.size, brates2.rates.size)
|
||||||
|
self.assertEqual(brates2.count_uncertainty.size, brates2.rates.size)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBackgroundSpectrum(TestCase):
|
||||||
|
file = os.path.join(data_dir, 'glg_cspec_b0_bn120415958_v00.pha')
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
cspec = Cspec.open(self.file)
|
||||||
|
b = BackgroundFitter.from_phaii(cspec, Polynomial,
|
||||||
|
time_ranges=[(-100, -10.0), (100.0, 200.0)])
|
||||||
|
b.fit(order=3)
|
||||||
|
|
||||||
|
b.fit(order=2)
|
||||||
|
x = np.linspace(-10.0, 11.0, 21)
|
||||||
|
tstart = x[:-1]
|
||||||
|
tstop = x[1:]
|
||||||
|
brates = b.interpolate_bins(tstart, tstop)
|
||||||
|
bspec = brates.integrate_time(tstart=-5.0, tstop=5.0)
|
||||||
|
self.assertEqual(bspec.size, 128)
|
||||||
|
self.assertCountEqual(bspec.lo_edges, cspec.data.emin)
|
||||||
|
self.assertCountEqual(bspec.hi_edges, cspec.data.emax)
|
||||||
|
|
||||||
|
def test_slice(self):
|
||||||
|
cspec = Cspec.open(self.file)
|
||||||
|
b = BackgroundFitter.from_phaii(cspec, Polynomial,
|
||||||
|
time_ranges=[(-100, -10.0), (100.0, 200.0)])
|
||||||
|
b.fit(order=3)
|
||||||
|
x = np.linspace(-10.0, 11.0, 21)
|
||||||
|
tstart = x[:-1]
|
||||||
|
tstop = x[1:]
|
||||||
|
brates = b.interpolate_bins(tstart, tstop)
|
||||||
|
bspec = brates.integrate_time(tstart=-5.0, tstop=5.0)
|
||||||
|
bspec_sliced = bspec.slice(100.0, 300.0)
|
||||||
|
self.assertCountEqual(bspec.counts[:4], bspec_sliced.counts)
|
||||||
|
self.assertCountEqual(bspec.rates[:4], bspec_sliced.rates)
|
||||||
|
self.assertCountEqual(bspec.exposure[:4], bspec_sliced.exposure)
|
||||||
|
self.assertCountEqual(bspec.lo_edges[:4], bspec_sliced.lo_edges)
|
||||||
|
self.assertCountEqual(bspec.hi_edges[:4], bspec_sliced.hi_edges)
|
||||||
|
self.assertCountEqual(bspec.rate_uncertainty[:4], bspec_sliced.rate_uncertainty)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
|
|
||||||
|
|
||||||
|
|
178
test/test_binning.py
Normal file
178
test/test_binning.py
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import unittest, os, shutil, sys
|
||||||
|
import numpy as np
|
||||||
|
from gbm.binning.binned import *
|
||||||
|
py_version = sys.version_info[0]
|
||||||
|
|
||||||
|
counts = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0])
|
||||||
|
exposure = np.array([1.024, 1.01, 1.00, 0.99, 1.02, 1.024, 0.80, 1.01])
|
||||||
|
edges = np.linspace(0.0, 8.192, 9)
|
||||||
|
index = np.array([0,2,4,6,8])
|
||||||
|
|
||||||
|
times = np.array([0.0, 0.01, 0.02, 0.03, 0.04, 0.05, 1.0, 1.01, 1.02, 1.03, 1.04])
|
||||||
|
|
||||||
|
class TestRebinningBinned(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_combine_by_factor(self):
|
||||||
|
if py_version == 2:
|
||||||
|
assertArray = self.assertItemsEqual
|
||||||
|
elif py_version == 3:
|
||||||
|
assertArray = self.assertCountEqual
|
||||||
|
new_counts, new_exposure, new_edges = combine_by_factor(counts, exposure, edges, 4)
|
||||||
|
assertArray(new_counts, np.array([10.0, 26.0]))
|
||||||
|
assertArray(new_exposure, np.array([4.024, 3.854]))
|
||||||
|
assertArray(new_edges, np.array([0.0, 4.096, 8.192]))
|
||||||
|
|
||||||
|
self.assertRaises(AssertionError, combine_by_factor, counts, exposure, edges, 0)
|
||||||
|
|
||||||
|
def test_rebin_by_time(self):
|
||||||
|
if py_version == 2:
|
||||||
|
assertArray = self.assertItemsEqual
|
||||||
|
elif py_version == 3:
|
||||||
|
assertArray = self.assertCountEqual
|
||||||
|
new_counts, new_exposure, new_edges = rebin_by_time(counts, exposure, edges, 4.096)
|
||||||
|
assertArray(new_counts, np.array([10.0, 26.0]))
|
||||||
|
assertArray(new_exposure, np.array([4.024, 3.854]))
|
||||||
|
assertArray(new_edges, np.array([0.0, 4.096, 8.192]))
|
||||||
|
|
||||||
|
self.assertRaises(AssertionError, rebin_by_time, counts, exposure, edges, -1.0)
|
||||||
|
|
||||||
|
def test_combine_into_one(self):
|
||||||
|
if py_version == 2:
|
||||||
|
assertArray = self.assertItemsEqual
|
||||||
|
elif py_version == 3:
|
||||||
|
assertArray = self.assertCountEqual
|
||||||
|
new_counts, new_exposure, new_edges = combine_into_one(counts, exposure, edges)
|
||||||
|
assertArray(new_counts, np.array([36.0]))
|
||||||
|
assertArray(new_exposure, np.array([7.878]))
|
||||||
|
assertArray(new_edges, np.array([0.0, 8.192]))
|
||||||
|
|
||||||
|
def test_rebin_by_edge_index(self):
|
||||||
|
if py_version == 2:
|
||||||
|
assertArray = self.assertItemsEqual
|
||||||
|
elif py_version == 3:
|
||||||
|
assertArray = self.assertCountEqual
|
||||||
|
new_counts, new_exposure, new_edges =rebin_by_edge_index(counts, exposure, edges, index)
|
||||||
|
assertArray(new_counts, np.array([3.0, 7.0, 11.0, 15.0]))
|
||||||
|
assertArray(new_exposure, np.array([2.034, 1.99, 2.044, 1.81]))
|
||||||
|
assertArray(new_edges, np.array([0.0, 2.048, 4.096, 6.144, 8.192]))
|
||||||
|
|
||||||
|
def test_rebin_by_edge(self):
|
||||||
|
if py_version == 2:
|
||||||
|
assertArray = self.assertItemsEqual
|
||||||
|
elif py_version == 3:
|
||||||
|
assertArray = self.assertCountEqual
|
||||||
|
new_edges = np.array([0.0, 2.048, 4.096, 6.144, 8.192])
|
||||||
|
new_counts, new_exposure, new_edges =rebin_by_edge(counts, exposure, edges, new_edges)
|
||||||
|
assertArray(new_counts, np.array([3.0, 7.0, 11.0, 15.0]))
|
||||||
|
assertArray(new_exposure, np.array([2.034, 1.99, 2.044, 1.81]))
|
||||||
|
assertArray(new_edges, np.array([0.0, 2.048, 4.096, 6.144, 8.192]))
|
||||||
|
|
||||||
|
def test_rebin_by_snr(self):
|
||||||
|
if py_version == 2:
|
||||||
|
assertArray = self.assertItemsEqual
|
||||||
|
elif py_version == 3:
|
||||||
|
assertArray = self.assertCountEqual
|
||||||
|
back_counts = np.ones_like(counts)
|
||||||
|
new_counts, new_exposure, new_edges = rebin_by_snr(counts, exposure, edges,
|
||||||
|
back_counts, 3.0)
|
||||||
|
self.assertEqual(new_counts.size, 5)
|
||||||
|
assertArray(new_counts, np.array([10.0, 5.0, 6.0, 7.0, 8.0]))
|
||||||
|
assertArray(new_exposure, np.array([4.024, 1.02, 1.024, 0.8, 1.01]))
|
||||||
|
assertArray(new_edges, np.array([0.0, 4.096, 5.120, 6.144, 7.168, 8.192]))
|
||||||
|
|
||||||
|
self.assertRaises(ValueError, rebin_by_snr, counts, exposure, edges,
|
||||||
|
-1.0*back_counts, 3.0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestBinningUnbinned(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_bin_by_time(self):
|
||||||
|
from gbm.binning.unbinned import bin_by_time
|
||||||
|
if py_version == 2:
|
||||||
|
assertArray = self.assertItemsEqual
|
||||||
|
elif py_version == 3:
|
||||||
|
assertArray = self.assertCountEqual
|
||||||
|
|
||||||
|
new_edges = bin_by_time(times, 1.0)
|
||||||
|
assertArray(new_edges, np.array([0.0, 1.0, 2.0]))
|
||||||
|
|
||||||
|
new_edges = bin_by_time(times, 0.5, time_ref=1.0)
|
||||||
|
assertArray(new_edges, np.array([0.0, 0.5, 1.0, 1.5]))
|
||||||
|
|
||||||
|
self.assertRaises(AssertionError, bin_by_time, times, -1.0)
|
||||||
|
|
||||||
|
def test_combine_into_one(self):
|
||||||
|
from gbm.binning.unbinned import combine_into_one
|
||||||
|
if py_version == 2:
|
||||||
|
assertArray = self.assertItemsEqual
|
||||||
|
elif py_version == 3:
|
||||||
|
assertArray = self.assertCountEqual
|
||||||
|
|
||||||
|
new_edges = combine_into_one(times, 0.5, 2.0)
|
||||||
|
assertArray(new_edges, np.array([0.5, 2.0]))
|
||||||
|
|
||||||
|
self.assertRaises(AssertionError, combine_into_one, times, 2.0, 0.5)
|
||||||
|
self.assertRaises(ValueError, combine_into_one, times, 10.0, 20.0)
|
||||||
|
|
||||||
|
def test_combine_by_factor(self):
|
||||||
|
from gbm.binning.unbinned import combine_by_factor
|
||||||
|
if py_version == 2:
|
||||||
|
assertArray = self.assertItemsEqual
|
||||||
|
elif py_version == 3:
|
||||||
|
assertArray = self.assertCountEqual
|
||||||
|
old_edges = np.array([0.0, 0.5, 1.0, 1.5])
|
||||||
|
new_edges = combine_by_factor(times, old_edges, 2)
|
||||||
|
assertArray(new_edges, np.array([0.0, 1.0]))
|
||||||
|
|
||||||
|
self.assertRaises(AssertionError, combine_by_factor, times, old_edges, -1)
|
||||||
|
|
||||||
|
def test_bin_by_snr(self):
|
||||||
|
from gbm.binning.unbinned import bin_by_snr
|
||||||
|
if py_version == 2:
|
||||||
|
assertArray = self.assertItemsEqual
|
||||||
|
elif py_version == 3:
|
||||||
|
assertArray = self.assertCountEqual
|
||||||
|
back_rates = np.ones_like(times)
|
||||||
|
new_edges = bin_by_snr(times, back_rates, 5.0)
|
||||||
|
|
||||||
|
assertArray(new_edges, np.array([0.0, 0.02, 1.0, 1.01, 1.02, 1.03, 1.04]))
|
||||||
|
|
||||||
|
def test_time_to_spill(self):
|
||||||
|
from gbm.binning.unbinned import time_to_spill
|
||||||
|
if py_version == 2:
|
||||||
|
assertArray = self.assertItemsEqual
|
||||||
|
elif py_version == 3:
|
||||||
|
assertArray = self.assertCountEqual
|
||||||
|
|
||||||
|
new_edges = time_to_spill(times, 5)
|
||||||
|
assertArray(new_edges, np.array([0.0, 0.05, 1.04]))
|
||||||
|
|
||||||
|
self.assertRaises(AssertionError, time_to_spill, times, -1)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
|
|
141
test/test_coords.py
Normal file
141
test/test_coords.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import unittest
|
||||||
|
import numpy as np
|
||||||
|
from gbm.coords import *
|
||||||
|
|
||||||
|
|
||||||
|
class TestCoords(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_haversine(self):
|
||||||
|
angle = haversine(10.0, 0.0, 20.0, 0.0)
|
||||||
|
self.assertEqual(angle, 10.0)
|
||||||
|
angle = haversine(0.0, -10.0, 0.0, -20.0)
|
||||||
|
self.assertEqual(angle, 10.0)
|
||||||
|
angle = np.rad2deg(haversine(np.deg2rad(10.0), np.deg2rad(0.0), \
|
||||||
|
np.deg2rad(20.0), np.deg2rad(0.0), deg=False))
|
||||||
|
self.assertEqual(angle, 10.0)
|
||||||
|
|
||||||
|
def test_sphere2cartesian(self):
|
||||||
|
true_cart = np.array([0.25, 0.4330127019, 0.8660254038])
|
||||||
|
cart = azzen_to_cartesian(60.0, 30.0)
|
||||||
|
for i in range(3):
|
||||||
|
self.assertAlmostEqual(cart[i], true_cart[i])
|
||||||
|
|
||||||
|
cart = radec_to_cartesian(60.0, 90.0-30.0)
|
||||||
|
for i in range(3):
|
||||||
|
self.assertAlmostEqual(cart[i], true_cart[i])
|
||||||
|
|
||||||
|
def test_quaternions(self):
|
||||||
|
quat = np.array([1.0, 1.0, 1.0, 1.0])
|
||||||
|
true_conj = np.array([-1.0, -1.0, -1.0, 1.0])
|
||||||
|
conj = quaternion_conj(quat)
|
||||||
|
for i in range(4):
|
||||||
|
self.assertEqual(conj[i], true_conj[i])
|
||||||
|
|
||||||
|
true_inverse = np.array([-0.25, -0.25, -0.25, 0.25])
|
||||||
|
inverse = quaternion_inv(quat)
|
||||||
|
for i in range(4):
|
||||||
|
self.assertEqual(inverse[i], true_inverse[i])
|
||||||
|
|
||||||
|
quat2 = np.array([-1.0, -1.0, -1.0, -1.0])
|
||||||
|
true_product = np.array([-2.0, -2.0, -2.0, 2.0])
|
||||||
|
product = quaternion_prod(quat, quat2)
|
||||||
|
for i in range(4):
|
||||||
|
self.assertEqual(product[i], true_product[i])
|
||||||
|
|
||||||
|
true_dcm = np.array([[0,0,1], [1,0,0], [0,1,0]])
|
||||||
|
dcm = spacecraft_direction_cosines(quat)
|
||||||
|
for i in range(3):
|
||||||
|
for j in range(3):
|
||||||
|
self.assertEqual(dcm[i,j], true_dcm[i,j])
|
||||||
|
|
||||||
|
def test_geocenter(self):
|
||||||
|
coord = np.array([-3227.0, 6092.0, 471.0])
|
||||||
|
true_geocenter = np.array([297.911, -3.908])
|
||||||
|
geocenter = geocenter_in_radec(coord)
|
||||||
|
for i in range(2):
|
||||||
|
self.assertAlmostEqual(geocenter[i], true_geocenter[i], 3)
|
||||||
|
|
||||||
|
def test_fermi2radec(self):
|
||||||
|
quat = np.array([1.0, 1.0, 1.0, 1.0])
|
||||||
|
az = 180.0
|
||||||
|
zen = 0.0
|
||||||
|
true_loc = np.array([0.0, 0.0])
|
||||||
|
loc = spacecraft_to_radec(az, zen, quat)
|
||||||
|
for i in range(2):
|
||||||
|
self.assertEqual(loc[i], true_loc[i])
|
||||||
|
|
||||||
|
def test_radec2fermi(self):
|
||||||
|
quat = np.array([1.0, 1.0, 1.0, 1.0])
|
||||||
|
ra = 180.0
|
||||||
|
dec = 0.0
|
||||||
|
true_loc = np.array([0.0, 180.0])
|
||||||
|
loc = radec_to_spacecraft(ra, dec, quat)
|
||||||
|
for i in range(2):
|
||||||
|
self.assertEqual(loc[i], true_loc[i])
|
||||||
|
|
||||||
|
def test_latitude(self):
|
||||||
|
coord = np.array([-3227.0, 6092.0, 471.0])*1000.0
|
||||||
|
true_lat = 3.91
|
||||||
|
true_alt = 538.972*1000.0
|
||||||
|
lat, alt = latitude_from_geocentric_coords_simple(coord)
|
||||||
|
self.assertAlmostEqual(lat, true_lat, 2)
|
||||||
|
self.assertAlmostEqual(np.round(alt), true_alt, 2)
|
||||||
|
|
||||||
|
true_lat = 3.93
|
||||||
|
true_alt = 531.944*1000.0
|
||||||
|
lat, alt = latitude_from_geocentric_coords_complex(coord)
|
||||||
|
self.assertAlmostEqual(lat, true_lat, 2)
|
||||||
|
self.assertAlmostEqual(np.round(alt), true_alt, 2)
|
||||||
|
|
||||||
|
def test_longitude(self):
|
||||||
|
coord = np.array([-3227.0, 6092.0, 471.0])
|
||||||
|
met = 524666471.0
|
||||||
|
true_lon = 321.552
|
||||||
|
lon = longitude_from_geocentric_coords(coord, met)
|
||||||
|
self.assertAlmostEqual(lon, true_lon, 3)
|
||||||
|
|
||||||
|
lon = longitude_from_geocentric_coords(coord, met, ut1=True)
|
||||||
|
self.assertAlmostEqual(lon, true_lon, 2)
|
||||||
|
|
||||||
|
def test_mcilwainl(self):
|
||||||
|
lon = 321.552
|
||||||
|
lat = 3.93
|
||||||
|
true_mcilwainl = 1.12
|
||||||
|
mcilwainl = calc_mcilwain_l(lat, lon)
|
||||||
|
self.assertAlmostEqual(mcilwainl, true_mcilwainl, 2)
|
||||||
|
|
||||||
|
def test_sun_loc(self):
|
||||||
|
met = 524666471.0
|
||||||
|
true_ra = 146.85
|
||||||
|
true_dec = 13.34
|
||||||
|
ra, dec = get_sun_loc(met)
|
||||||
|
self.assertAlmostEqual(ra, true_ra, 2)
|
||||||
|
self.assertAlmostEqual(dec, true_dec, 2)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
|
|
125
test/test_data_collection.py
Normal file
125
test/test_data_collection.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from gbm.data.phaii import Cspec
|
||||||
|
from gbm.data.primitives import TimeBins
|
||||||
|
from gbm.data.collection import DataCollection, GbmDetectorCollection
|
||||||
|
|
||||||
|
data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data',
|
||||||
|
'160509374')
|
||||||
|
|
||||||
|
class TestDataCollection(TestCase):
|
||||||
|
b0 = Cspec.open(os.path.join(data_dir, 'glg_cspec_b0_bn160509374_v01.pha'))
|
||||||
|
n0 = Cspec.open(os.path.join(data_dir, 'glg_cspec_n0_bn160509374_v01.pha'))
|
||||||
|
n1 = Cspec.open(os.path.join(data_dir, 'glg_cspec_n1_bn160509374_v01.pha'))
|
||||||
|
collection = DataCollection.from_list([b0, n0, n1])
|
||||||
|
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
self.assertEqual(len(self.collection), 3)
|
||||||
|
self.assertCountEqual(self.collection.items, [self.b0.filename,
|
||||||
|
self.n0.filename,
|
||||||
|
self.n1.filename])
|
||||||
|
self.assertEqual(self.collection.types, Cspec)
|
||||||
|
|
||||||
|
def test_get_item(self):
|
||||||
|
item = self.collection.get_item(self.collection.items[0])
|
||||||
|
self.assertEqual(item, self.b0)
|
||||||
|
|
||||||
|
def test_remove_and_include(self):
|
||||||
|
# remove
|
||||||
|
self.collection.remove(self.collection.items[2])
|
||||||
|
self.assertEqual(len(self.collection), 2)
|
||||||
|
self.assertCountEqual(self.collection.items, [self.b0.filename,
|
||||||
|
self.n0.filename])
|
||||||
|
# include
|
||||||
|
self.collection.include(self.n1)
|
||||||
|
self.test_attributes()
|
||||||
|
|
||||||
|
def test_to_list(self):
|
||||||
|
thelist = self.collection.to_list()
|
||||||
|
self.assertCountEqual(thelist, [self.b0, self.n0, self.n1])
|
||||||
|
|
||||||
|
def test_item_attributes(self):
|
||||||
|
numchans = self.collection.numchans()
|
||||||
|
self.assertCountEqual(numchans, [128, 128, 128])
|
||||||
|
erange = self.collection.energy_range()
|
||||||
|
elo = (113.00731, 4.5702357, 4.089358)
|
||||||
|
ehi = (50000.0, 2000.0, 2000.0)
|
||||||
|
[self.assertAlmostEqual(erange[i][0], elo[i], places=3) for i in range(3)]
|
||||||
|
[self.assertAlmostEqual(erange[i][1], ehi[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_item_methods(self):
|
||||||
|
exposure = self.collection.get_exposure()
|
||||||
|
test_exp = [7967., 7979., 7978.]
|
||||||
|
[self.assertAlmostEqual(exposure[i], test_exp[i], places=0) for i in range(3)]
|
||||||
|
|
||||||
|
lcs = self.collection.to_lightcurve()
|
||||||
|
[self.assertIsInstance(lc, TimeBins) for lc in lcs]
|
||||||
|
|
||||||
|
|
||||||
|
class TestGbmDetectorCollection(TestCase):
|
||||||
|
b0 = Cspec.open(os.path.join(data_dir, 'glg_cspec_b0_bn160509374_v01.pha'))
|
||||||
|
n0 = Cspec.open(os.path.join(data_dir, 'glg_cspec_n0_bn160509374_v01.pha'))
|
||||||
|
n1 = Cspec.open(os.path.join(data_dir, 'glg_cspec_n1_bn160509374_v01.pha'))
|
||||||
|
collection = GbmDetectorCollection.from_list([b0, n0, n1])
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
self.assertEqual(len(self.collection), 3)
|
||||||
|
self.assertCountEqual(self.collection.items, [self.b0.filename,
|
||||||
|
self.n0.filename,
|
||||||
|
self.n1.filename])
|
||||||
|
self.assertEqual(self.collection.types, Cspec)
|
||||||
|
|
||||||
|
def test_remove_and_include(self):
|
||||||
|
# remove
|
||||||
|
self.collection.remove(self.collection.items[2])
|
||||||
|
self.assertEqual(len(self.collection), 2)
|
||||||
|
self.assertCountEqual(self.collection.items, [self.b0.filename,
|
||||||
|
self.n0.filename])
|
||||||
|
# include
|
||||||
|
self.collection.include(self.n1, 'n1')
|
||||||
|
self.test_attributes()
|
||||||
|
|
||||||
|
def test_item_methods(self):
|
||||||
|
slices = self.collection.slice_time(nai_args=((0.0, 10.0),),
|
||||||
|
bgo_args=((0.0, 100.0),))
|
||||||
|
test_exp = [102., 12., 12.]
|
||||||
|
[self.assertAlmostEqual(slices[i].get_exposure(), test_exp[i], places=0) \
|
||||||
|
for i in range(3)]
|
||||||
|
|
||||||
|
specs = self.collection.to_spectrum(nai_kwargs={'energy_range':(8.0, 900.)},
|
||||||
|
bgo_kwargs={'energy_range':(350.0, 38000.0)})
|
||||||
|
elo = (318., 7., 8.)
|
||||||
|
ehi = (40058., 924., 913.)
|
||||||
|
[self.assertAlmostEqual(specs[i].range[0], elo[i], places=0) for i in range(3)]
|
||||||
|
[self.assertAlmostEqual(specs[i].range[1], ehi[i], places=0) for i in range(3)]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
170
test/test_data_download.py
Normal file
170
test/test_data_download.py
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import unittest, os, shutil
|
||||||
|
from gbm.finder import TriggerFtp, ContinuousFtp, TriggerCatalog, BurstCatalog
|
||||||
|
|
||||||
|
download_dir = data_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
class TestTriggerFtp(unittest.TestCase):
|
||||||
|
finder = TriggerFtp()
|
||||||
|
|
||||||
|
def test_set_trigger(self):
|
||||||
|
self.finder.set_trigger('080916009')
|
||||||
|
self.assertEqual(self.finder.num_files, 109)
|
||||||
|
self.finder.set_trigger('170817529')
|
||||||
|
self.assertEqual(self.finder.num_files, 128)
|
||||||
|
|
||||||
|
def test_ls(self):
|
||||||
|
self.finder.set_trigger('170817529')
|
||||||
|
[self.assertTrue('ctime' in file) for file in self.finder.ls_ctime()]
|
||||||
|
[self.assertTrue('cspec' in file) for file in self.finder.ls_cspec()]
|
||||||
|
[self.assertTrue('tte' in file) for file in self.finder.ls_tte()]
|
||||||
|
[self.assertTrue('cspec' in file) for file in self.finder.ls_rsp(ctime=False)]
|
||||||
|
[self.assertTrue('ctime' in file) for file in self.finder.ls_rsp(cspec=False)]
|
||||||
|
[self.assertTrue('cspec' in file) for file in self.finder.ls_rsp2(ctime=False)]
|
||||||
|
[self.assertTrue('ctime' in file) for file in self.finder.ls_rsp2(cspec=False)]
|
||||||
|
[self.assertTrue('lc' in file) for file in self.finder.ls_lightcurve()]
|
||||||
|
[self.assertTrue('.fit' in file) for file in self.finder.ls_cat_files()]
|
||||||
|
self.assertTrue('trigdat' in self.finder.ls_trigdat()[0])
|
||||||
|
[self.assertTrue(('healpix' in file) or ('skymap' in file) or
|
||||||
|
('loclist' in file) or ('locprob' in file) or
|
||||||
|
('locplot' in file)) for file in self.finder.ls_localization()]
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
self.finder.set_trigger('170817529')
|
||||||
|
self.finder.get_cat_files(download_dir)
|
||||||
|
cat_files = self.finder.ls_cat_files()
|
||||||
|
[os.remove(os.path.join(download_dir, file)) for file in cat_files]
|
||||||
|
|
||||||
|
|
||||||
|
class TestContinuousFtp(unittest.TestCase):
|
||||||
|
finder = ContinuousFtp()
|
||||||
|
|
||||||
|
def test_set_time(self):
|
||||||
|
self.finder.set_time(met=604741251.0)
|
||||||
|
self.assertEqual(self.finder.num_files, 379)
|
||||||
|
self.finder.set_time(utc='2019-01-14T20:57:02.63')
|
||||||
|
self.assertEqual(self.finder.num_files, 379)
|
||||||
|
self.finder.set_time(gps=1263097406.735840)
|
||||||
|
self.assertEqual(self.finder.num_files, 379)
|
||||||
|
|
||||||
|
def test_ls(self):
|
||||||
|
self.finder.set_time(met=604741251.0)
|
||||||
|
[self.assertTrue('ctime' in file) for file in self.finder.ls_ctime()]
|
||||||
|
[self.assertTrue('cspec' in file) for file in self.finder.ls_cspec()]
|
||||||
|
[self.assertTrue('poshist' in file) for file in self.finder.ls_poshist()]
|
||||||
|
[self.assertTrue('spechist' in file) for file in self.finder.ls_spechist()]
|
||||||
|
[self.assertTrue('tte' in file) for file in self.finder.ls_tte()]
|
||||||
|
[self.assertTrue('tte' in file) for file in self.finder.ls_tte(full_day=True)]
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
self.finder.set_time(met=604741251.0)
|
||||||
|
self.finder.get_poshist(download_dir)
|
||||||
|
self.finder.get_ctime(download_dir, dets=('n0', 'n1', 'n2'))
|
||||||
|
|
||||||
|
files = self.finder.ls_poshist()
|
||||||
|
files.extend(self.finder.ls_ctime())
|
||||||
|
for file in files:
|
||||||
|
try:
|
||||||
|
os.remove(os.path.join(download_dir, file))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_reconnect(self):
|
||||||
|
finder = ContinuousFtp()
|
||||||
|
finder = ContinuousFtp()
|
||||||
|
self.finder.set_time(met=604741251.0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTriggerCatalog(unittest.TestCase):
|
||||||
|
catalog = TriggerCatalog()
|
||||||
|
def test_attributes(self):
|
||||||
|
self.assertEqual(self.catalog.num_cols, len(self.catalog.columns))
|
||||||
|
|
||||||
|
def test_get_table(self):
|
||||||
|
table = self.catalog.get_table()
|
||||||
|
self.assertEqual(len(table.dtype), self.catalog.num_cols)
|
||||||
|
table = self.catalog.get_table(columns=('trigger_name', 'ra', 'dec'))
|
||||||
|
self.assertEqual(len(table.dtype), 3)
|
||||||
|
|
||||||
|
def test_column_range(self):
|
||||||
|
lo, hi = self.catalog.column_range('trigger_type')
|
||||||
|
self.assertEqual(lo, 'DISTPAR')
|
||||||
|
self.assertEqual(hi, 'UNRELOC')
|
||||||
|
lo, hi = self.catalog.column_range('error_radius')
|
||||||
|
self.assertEqual(lo, 0)
|
||||||
|
self.assertEqual(hi, 93.54)
|
||||||
|
|
||||||
|
def test_slice(self):
|
||||||
|
sliced = self.catalog.slice('trigger_type', lo='GRB', hi='GRB')
|
||||||
|
self.assertTrue(sliced.num_rows < self.catalog.num_rows)
|
||||||
|
sliced = self.catalog.slice('ra', lo=50.0, hi=100.0)
|
||||||
|
self.assertTrue(sliced.num_rows < self.catalog.num_rows)
|
||||||
|
sliced = self.catalog.slice('ra', hi=100.0)
|
||||||
|
self.assertTrue(sliced.num_rows < self.catalog.num_rows)
|
||||||
|
sliced = self.catalog.slice('ra', lo=100.0)
|
||||||
|
self.assertTrue(sliced.num_rows < self.catalog.num_rows)
|
||||||
|
|
||||||
|
sliced2 = self.catalog.slices([('trigger_type', 'GRB', 'GRB'),
|
||||||
|
('ra', None, 100.0),
|
||||||
|
('dec', 20.0, None)])
|
||||||
|
self.assertTrue(sliced2.num_rows < self.catalog.num_rows)
|
||||||
|
|
||||||
|
class TestBurstCatalog(unittest.TestCase):
|
||||||
|
catalog = BurstCatalog()
|
||||||
|
def test_attributes(self):
|
||||||
|
self.assertEqual(self.catalog.num_cols, len(self.catalog.columns))
|
||||||
|
|
||||||
|
def test_get_table(self):
|
||||||
|
table = self.catalog.get_table()
|
||||||
|
self.assertEqual(len(table.dtype), self.catalog.num_cols)
|
||||||
|
table = self.catalog.get_table(columns=('name', 'ra', 'dec'))
|
||||||
|
self.assertEqual(len(table.dtype), 3)
|
||||||
|
|
||||||
|
def test_column_range(self):
|
||||||
|
lo, hi = self.catalog.column_range('flnc_best_fitting_model')
|
||||||
|
self.assertEqual(lo, 'flnc_band')
|
||||||
|
self.assertEqual(hi, 'nan')
|
||||||
|
lo, hi = self.catalog.column_range('error_radius')
|
||||||
|
self.assertEqual(lo, 0)
|
||||||
|
|
||||||
|
def test_slice(self):
|
||||||
|
sliced = self.catalog.slice('flnc_best_fitting_model', lo='flnc_band',
|
||||||
|
hi='flnc_band')
|
||||||
|
self.assertTrue(sliced.num_rows < self.catalog.num_rows)
|
||||||
|
sliced = self.catalog.slice('ra', lo=50.0, hi=100.0)
|
||||||
|
self.assertTrue(sliced.num_rows < self.catalog.num_rows)
|
||||||
|
sliced = self.catalog.slice('ra', hi=100.0)
|
||||||
|
self.assertTrue(sliced.num_rows < self.catalog.num_rows)
|
||||||
|
sliced = self.catalog.slice('ra', lo=100.0)
|
||||||
|
self.assertTrue(sliced.num_rows < self.catalog.num_rows)
|
||||||
|
|
||||||
|
sliced2 = self.catalog.slices([('flnc_best_fitting_model', 'flnc_band', 'flnc_band'),
|
||||||
|
('ra', None, 100.0),
|
||||||
|
('dec', 20.0, None)])
|
||||||
|
self.assertTrue(sliced2.num_rows < self.catalog.num_rows)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
110
test/test_detectors.py
Normal file
110
test/test_detectors.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from gbm.detectors import *
|
||||||
|
from copy import copy
|
||||||
|
|
||||||
|
|
||||||
|
class TestDetectors(unittest.TestCase):
|
||||||
|
|
||||||
|
expected_nai = ['n0', 'n1', 'n2', 'n3', 'n4', 'n5', 'n6', 'n7', 'n8', 'n9', 'na', 'nb']
|
||||||
|
expected_bgo = ['b0', 'b1']
|
||||||
|
expected_all = expected_nai + expected_bgo
|
||||||
|
|
||||||
|
def test_detector_list(self):
|
||||||
|
detectors = copy(self.expected_all)
|
||||||
|
for d in Detector:
|
||||||
|
if d.short_name in detectors:
|
||||||
|
detectors.remove(d.short_name)
|
||||||
|
else:
|
||||||
|
self.fail("Detector was not expected")
|
||||||
|
if detectors:
|
||||||
|
self.fail("All of the detectors weren't removed")
|
||||||
|
|
||||||
|
def test_is_nai(self):
|
||||||
|
for d in self.expected_nai:
|
||||||
|
self.assertTrue(Detector.is_nai(Detector.from_str(d)))
|
||||||
|
for d in self.expected_bgo:
|
||||||
|
self.assertFalse(Detector.is_nai(Detector.from_str(d)))
|
||||||
|
|
||||||
|
def test_is_bgo(self):
|
||||||
|
for d in self.expected_bgo:
|
||||||
|
self.assertTrue(Detector.is_bgo(Detector.from_str(d)))
|
||||||
|
for d in self.expected_nai:
|
||||||
|
self.assertFalse(Detector.is_bgo(Detector.from_str(d)))
|
||||||
|
|
||||||
|
def test_nai(self):
|
||||||
|
l = Detector.nai()
|
||||||
|
self.assertEqual(len(l), len(self.expected_nai))
|
||||||
|
for d in self.expected_nai:
|
||||||
|
self.assertIsNotNone(Detector.from_str(d))
|
||||||
|
|
||||||
|
def test_bgo(self):
|
||||||
|
l = Detector.bgo()
|
||||||
|
self.assertEqual(len(l), len(self.expected_bgo))
|
||||||
|
for d in self.expected_bgo:
|
||||||
|
self.assertIsNotNone(Detector.from_str(d))
|
||||||
|
|
||||||
|
def test_from_number(self):
|
||||||
|
num = 0
|
||||||
|
for d in self.expected_all:
|
||||||
|
self.assertEqual(d, Detector.from_num(num).short_name)
|
||||||
|
num += 1
|
||||||
|
|
||||||
|
def test_meta_data(self):
|
||||||
|
expected_data = [
|
||||||
|
('N0', 'NAI_00', 0, 45.89, 20.58),
|
||||||
|
('N1', 'NAI_01', 1, 45.11, 45.31),
|
||||||
|
('N2', 'NAI_02', 2, 58.44, 90.21),
|
||||||
|
('N3', 'NAI_03', 3, 314.87, 45.24),
|
||||||
|
('N4', 'NAI_04', 4, 303.15, 90.27),
|
||||||
|
('N5', 'NAI_05', 5, 3.35, 89.79),
|
||||||
|
('N6', 'NAI_06', 6, 224.93, 20.43),
|
||||||
|
('N7', 'NAI_07', 7, 224.62, 46.18),
|
||||||
|
('N8', 'NAI_08', 8, 236.61, 89.97),
|
||||||
|
('N9', 'NAI_09', 9, 135.19, 45.55),
|
||||||
|
('NA', 'NAI_10', 10, 123.73, 90.42),
|
||||||
|
('NB', 'NAI_11', 11, 183.74, 90.32),
|
||||||
|
('B0', 'BGO_00', 12, 0.00, 90.00),
|
||||||
|
('B1', 'BGO_01', 13, 180.00, 90.00),
|
||||||
|
]
|
||||||
|
for d in expected_data:
|
||||||
|
det = Detector.from_num(d[2])
|
||||||
|
self.assertEqual(det.name, d[0])
|
||||||
|
self.assertEqual(det.long_name, d[1])
|
||||||
|
self.assertEqual(det.azimuth, d[3])
|
||||||
|
self.assertEqual(det.zenith, d[4])
|
||||||
|
self.assertEqual(det.pointing, (d[3], d[4]))
|
||||||
|
self.assertEqual(det.__repr__(), "Detector(\"{}\", \"{}\", {})".format(d[0], d[1], d[2]))
|
||||||
|
|
||||||
|
def test_invalid_str(self):
|
||||||
|
self.assertIsNone(Detector.from_str("FAKE"))
|
||||||
|
|
||||||
|
def test_invalid_num(self):
|
||||||
|
self.assertIsNone(Detector.from_num(20))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
483
test/test_file.py
Normal file
483
test/test_file.py
Normal file
@ -0,0 +1,483 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
|
||||||
|
from shutil import rmtree
|
||||||
|
from subprocess import call
|
||||||
|
|
||||||
|
from gbm.file import *
|
||||||
|
from gbm.time import Met
|
||||||
|
|
||||||
|
|
||||||
|
class TestFilename(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def create_directory_structure(cls):
|
||||||
|
files = [
|
||||||
|
|
||||||
|
# Sample dir structure
|
||||||
|
|
||||||
|
"aaa/file_1.txt",
|
||||||
|
"aaa/file_2.txt",
|
||||||
|
"aaa/file_3.txt",
|
||||||
|
"aaa/file_4.txt",
|
||||||
|
"aaa/.hidden_1.txt",
|
||||||
|
"bbb/file_1.txt",
|
||||||
|
"bbb/file_2.txt",
|
||||||
|
"bbb/file_3.txt",
|
||||||
|
"bbb/file_4.txt",
|
||||||
|
"bbb/.hidden_1.txt",
|
||||||
|
"ccc/file_1.txt",
|
||||||
|
"ccc/file_2.txt",
|
||||||
|
"ccc/file_3.txt",
|
||||||
|
"ccc/file_4.txt",
|
||||||
|
"ccc/.hidden_1.txt",
|
||||||
|
".ddd/file_1.txt",
|
||||||
|
".ddd/file_2.txt",
|
||||||
|
".ddd/file_3.txt",
|
||||||
|
".ddd/file_4.txt",
|
||||||
|
"complete/glg_poshist_all_160115_12z_v02.fit",
|
||||||
|
"complete/glg_tte_n0_160115_12z_v00.fit",
|
||||||
|
"complete/glg_tte_n1_160115_12z_v00.fit",
|
||||||
|
"complete/glg_tte_n2_160115_12z_v00.fit",
|
||||||
|
"complete/glg_tte_n3_160115_12z_v00.fit",
|
||||||
|
"complete/glg_tte_n4_160115_12z_v00.fit",
|
||||||
|
"complete/glg_tte_n5_160115_12z_v00.fit",
|
||||||
|
"complete/glg_tte_n6_160115_12z_v00.fit",
|
||||||
|
"complete/glg_tte_n7_160115_12z_v00.fit",
|
||||||
|
"complete/glg_tte_n8_160115_12z_v00.fit",
|
||||||
|
"complete/glg_tte_n9_160115_12z_v00.fit",
|
||||||
|
"complete/glg_tte_na_160115_12z_v00.fit",
|
||||||
|
"complete/glg_tte_nb_160115_12z_v00.fit",
|
||||||
|
"complete/glg_tte_b0_160115_12z_v00.fit",
|
||||||
|
"complete/glg_tte_b1_160115_12z_v00.fit",
|
||||||
|
"incomplete/glg_tte_n0_160115_12z_v00.fit",
|
||||||
|
"incomplete/glg_tte_n1_160115_12z_v00.fit",
|
||||||
|
"incomplete/glg_tte_n2_160115_12z_v00.fit",
|
||||||
|
"incomplete/glg_tte_n3_160115_12z_v00.fit",
|
||||||
|
"incomplete/glg_tte_n4_160115_12z_v00.fit",
|
||||||
|
"incomplete/glg_tte_n5_160115_12z_v00.fit",
|
||||||
|
"incomplete/glg_tte_n6_160115_12z_v00.fit",
|
||||||
|
"incomplete/glg_tte_n7_160115_12z_v00.fit",
|
||||||
|
"incomplete/glg_tte_n9_160115_12z_v00.fit",
|
||||||
|
"incomplete/glg_tte_na_160115_12z_v00.fit",
|
||||||
|
"incomplete/glg_tte_nb_160115_12z_v00.fit",
|
||||||
|
"incomplete/glg_tte_b0_160115_12z_v00.fit",
|
||||||
|
"incomplete/glg_tte_b1_160115_12z_v00.fit",
|
||||||
|
"_version/glg_tte_n0_160115_12z_v05.fit",
|
||||||
|
"_version/glg_tte_n1_160115_12z_v06.fit",
|
||||||
|
"_version/glg_tte_n2_160115_12z_v07.fit",
|
||||||
|
"_version/glg_tte_n3_160115_12z_v08.fit",
|
||||||
|
"_version/glg_tte_n4_160115_12z_v09.fit",
|
||||||
|
"_version/glg_tte_n5_160115_12z_v10.fit",
|
||||||
|
"_version/glg_tte_n6_160115_12z_v11.fit",
|
||||||
|
"_version/glg_tte_n7_160115_12z_v12.fit",
|
||||||
|
"_version/glg_tte_n8_160115_12z_v13.fit",
|
||||||
|
"_version/glg_tte_n9_160115_12z_v14.fit",
|
||||||
|
"_version/glg_tte_na_160115_12z_v15.fit",
|
||||||
|
"_version/glg_tte_nb_160115_12z_v16.fit",
|
||||||
|
"_version/glg_tte_b0_160115_12z_v17.fit",
|
||||||
|
"_version/glg_tte_b1_160115_12z_v18.fit",
|
||||||
|
]
|
||||||
|
|
||||||
|
def create_if_not_exists(path):
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
os.makedirs(path)
|
||||||
|
|
||||||
|
old_cwd = os.getcwd()
|
||||||
|
create_if_not_exists("test_files")
|
||||||
|
os.chdir("test_files")
|
||||||
|
|
||||||
|
for f in files:
|
||||||
|
directory = os.path.dirname(f)
|
||||||
|
create_if_not_exists(directory)
|
||||||
|
call(["touch", f])
|
||||||
|
|
||||||
|
os.chdir(old_cwd)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.create_directory_structure()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
rmtree("test_files")
|
||||||
|
|
||||||
|
def test_daily(self):
|
||||||
|
f = GbmFile.create(uid=Met.from_datetime(datetime.datetime(2015, 9, 11, 12)).ymd, data_type='test',
|
||||||
|
detector='n0')
|
||||||
|
self.assertEqual(f.basename(), 'glg_test_n0_150911_v00.fit')
|
||||||
|
|
||||||
|
def test_daily_all(self):
|
||||||
|
f = GbmFile.create(uid=Met.from_datetime(datetime.datetime(2015, 9, 11, 12)).ymd, data_type='test')
|
||||||
|
self.assertEqual(f.basename(), 'glg_test_all_150911_v00.fit')
|
||||||
|
|
||||||
|
def test_hourly(self):
|
||||||
|
f = GbmFile.create(uid=Met.from_datetime(datetime.datetime(2015, 9, 11, 12)).ymd_h, data_type='test',
|
||||||
|
detector='b1')
|
||||||
|
self.assertEqual(f.basename(), 'glg_test_b1_150911_12z_v00.fit')
|
||||||
|
|
||||||
|
def test_hourly_all(self):
|
||||||
|
f = GbmFile.create(uid=Met.from_datetime(datetime.datetime(2015, 9, 11, 12)).ymd_h, data_type='test')
|
||||||
|
self.assertEqual(f.basename(), 'glg_test_all_150911_12z_v00.fit')
|
||||||
|
|
||||||
|
def test_trig(self):
|
||||||
|
f = GbmFile.create(uid=Met.from_datetime(datetime.datetime(2015, 9, 11, 12)).bn, data_type='test',
|
||||||
|
detector='n0',
|
||||||
|
trigger=True)
|
||||||
|
self.assertEqual(f.basename(), 'glg_test_n0_bn150911500_v00.fit')
|
||||||
|
|
||||||
|
def test_trig_all(self):
|
||||||
|
f = GbmFile.create(uid=Met.from_datetime(datetime.datetime(2015, 9, 11, 12)).bn, data_type='test', trigger=True)
|
||||||
|
self.assertEqual(f.basename(), 'glg_test_all_bn150911500_v00.fit')
|
||||||
|
|
||||||
|
def test_detector_list(self):
|
||||||
|
|
||||||
|
expected = [
|
||||||
|
'glg_test_n0_170514999_v00.fit',
|
||||||
|
'glg_test_n1_170514999_v00.fit',
|
||||||
|
'glg_test_n2_170514999_v00.fit',
|
||||||
|
'glg_test_n3_170514999_v00.fit',
|
||||||
|
'glg_test_n4_170514999_v00.fit',
|
||||||
|
'glg_test_n5_170514999_v00.fit',
|
||||||
|
'glg_test_n6_170514999_v00.fit',
|
||||||
|
'glg_test_n7_170514999_v00.fit',
|
||||||
|
'glg_test_n8_170514999_v00.fit',
|
||||||
|
'glg_test_n9_170514999_v00.fit',
|
||||||
|
'glg_test_na_170514999_v00.fit',
|
||||||
|
'glg_test_nb_170514999_v00.fit',
|
||||||
|
'glg_test_b0_170514999_v00.fit',
|
||||||
|
'glg_test_b1_170514999_v00.fit'
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
f = GbmFile.create(uid='170514999', version=0, data_type='test')
|
||||||
|
|
||||||
|
file_list = f.detector_list()
|
||||||
|
|
||||||
|
result = [str(x) for x in file_list]
|
||||||
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
|
def test_from_path_triggered(self):
|
||||||
|
f = "fake/glg_tte_n5_bn171115500_v05.fit"
|
||||||
|
g = GbmFile.from_path(f)
|
||||||
|
self.assertIsNotNone(g)
|
||||||
|
self.assertTrue(g.trigger)
|
||||||
|
self.assertEqual(f, g.path())
|
||||||
|
|
||||||
|
def test_from_path_continuous(self):
|
||||||
|
f = "fake/glg_tte_n5_171115500_v05.fit"
|
||||||
|
g = GbmFile.from_path(f)
|
||||||
|
self.assertIsNotNone(g)
|
||||||
|
self.assertFalse(g.trigger)
|
||||||
|
self.assertEqual(f, g.path())
|
||||||
|
|
||||||
|
def test_from_path_scat(self):
|
||||||
|
f = "fake/glg_scat_all_bn080714086_flnc_band_v00.fit"
|
||||||
|
g = GbmFile.from_path(f)
|
||||||
|
self.assertIsNotNone(g)
|
||||||
|
self.assertEqual('scat', g.data_type)
|
||||||
|
self.assertEqual('all', g.detector)
|
||||||
|
self.assertTrue(g.trigger)
|
||||||
|
self.assertEqual('080714086', g.uid)
|
||||||
|
self.assertEqual('_flnc_band', g.meta)
|
||||||
|
self.assertEqual('00', g.version)
|
||||||
|
self.assertEqual('fit', g.extension)
|
||||||
|
self.assertEqual(f, g.path())
|
||||||
|
|
||||||
|
def test_scan_dir(self):
|
||||||
|
expected = [
|
||||||
|
"test_files/aaa/file_1.txt",
|
||||||
|
"test_files/aaa/file_2.txt",
|
||||||
|
"test_files/aaa/file_3.txt",
|
||||||
|
"test_files/aaa/file_4.txt",
|
||||||
|
"test_files/bbb/file_1.txt",
|
||||||
|
"test_files/bbb/file_2.txt",
|
||||||
|
"test_files/bbb/file_3.txt",
|
||||||
|
"test_files/bbb/file_4.txt",
|
||||||
|
"test_files/ccc/file_1.txt",
|
||||||
|
"test_files/ccc/file_2.txt",
|
||||||
|
"test_files/ccc/file_3.txt",
|
||||||
|
"test_files/ccc/file_4.txt",
|
||||||
|
"test_files/complete/glg_poshist_all_160115_12z_v02.fit",
|
||||||
|
"test_files/complete/glg_tte_n0_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n1_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n2_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n3_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n4_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n5_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n6_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n7_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n8_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n9_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_na_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_nb_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_b0_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_b1_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n0_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n1_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n2_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n3_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n4_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n5_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n6_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n7_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n9_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_na_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_nb_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_b0_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_b1_160115_12z_v00.fit",
|
||||||
|
"test_files/_version/glg_tte_n0_160115_12z_v05.fit",
|
||||||
|
"test_files/_version/glg_tte_n1_160115_12z_v06.fit",
|
||||||
|
"test_files/_version/glg_tte_n2_160115_12z_v07.fit",
|
||||||
|
"test_files/_version/glg_tte_n3_160115_12z_v08.fit",
|
||||||
|
"test_files/_version/glg_tte_n4_160115_12z_v09.fit",
|
||||||
|
"test_files/_version/glg_tte_n5_160115_12z_v10.fit",
|
||||||
|
"test_files/_version/glg_tte_n6_160115_12z_v11.fit",
|
||||||
|
"test_files/_version/glg_tte_n7_160115_12z_v12.fit",
|
||||||
|
"test_files/_version/glg_tte_n8_160115_12z_v13.fit",
|
||||||
|
"test_files/_version/glg_tte_n9_160115_12z_v14.fit",
|
||||||
|
"test_files/_version/glg_tte_na_160115_12z_v15.fit",
|
||||||
|
"test_files/_version/glg_tte_nb_160115_12z_v16.fit",
|
||||||
|
"test_files/_version/glg_tte_b0_160115_12z_v17.fit",
|
||||||
|
"test_files/_version/glg_tte_b1_160115_12z_v18.fit",
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
files = [x for x in scan_dir("test_files", recursive=True)]
|
||||||
|
|
||||||
|
files.sort()
|
||||||
|
expected.sort()
|
||||||
|
|
||||||
|
self.assertListEqual(expected, files)
|
||||||
|
|
||||||
|
def test_scan_dir_absolute(self):
|
||||||
|
expected = [
|
||||||
|
"test_files/aaa/file_1.txt",
|
||||||
|
"test_files/aaa/file_2.txt",
|
||||||
|
"test_files/aaa/file_3.txt",
|
||||||
|
"test_files/aaa/file_4.txt",
|
||||||
|
"test_files/bbb/file_1.txt",
|
||||||
|
"test_files/bbb/file_2.txt",
|
||||||
|
"test_files/bbb/file_3.txt",
|
||||||
|
"test_files/bbb/file_4.txt",
|
||||||
|
"test_files/ccc/file_1.txt",
|
||||||
|
"test_files/ccc/file_2.txt",
|
||||||
|
"test_files/ccc/file_3.txt",
|
||||||
|
"test_files/ccc/file_4.txt",
|
||||||
|
"test_files/complete/glg_poshist_all_160115_12z_v02.fit",
|
||||||
|
"test_files/complete/glg_tte_n0_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n1_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n2_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n3_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n4_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n5_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n6_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n7_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n8_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n9_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_na_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_nb_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_b0_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_b1_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n0_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n1_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n2_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n3_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n4_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n5_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n6_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n7_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n9_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_na_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_nb_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_b0_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_b1_160115_12z_v00.fit",
|
||||||
|
"test_files/_version/glg_tte_n0_160115_12z_v05.fit",
|
||||||
|
"test_files/_version/glg_tte_n1_160115_12z_v06.fit",
|
||||||
|
"test_files/_version/glg_tte_n2_160115_12z_v07.fit",
|
||||||
|
"test_files/_version/glg_tte_n3_160115_12z_v08.fit",
|
||||||
|
"test_files/_version/glg_tte_n4_160115_12z_v09.fit",
|
||||||
|
"test_files/_version/glg_tte_n5_160115_12z_v10.fit",
|
||||||
|
"test_files/_version/glg_tte_n6_160115_12z_v11.fit",
|
||||||
|
"test_files/_version/glg_tte_n7_160115_12z_v12.fit",
|
||||||
|
"test_files/_version/glg_tte_n8_160115_12z_v13.fit",
|
||||||
|
"test_files/_version/glg_tte_n9_160115_12z_v14.fit",
|
||||||
|
"test_files/_version/glg_tte_na_160115_12z_v15.fit",
|
||||||
|
"test_files/_version/glg_tte_nb_160115_12z_v16.fit",
|
||||||
|
"test_files/_version/glg_tte_b0_160115_12z_v17.fit",
|
||||||
|
"test_files/_version/glg_tte_b1_160115_12z_v18.fit",
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
files = [x for x in scan_dir("test_files", absolute=True, recursive=True)]
|
||||||
|
|
||||||
|
files.sort()
|
||||||
|
expected.sort()
|
||||||
|
for f in files:
|
||||||
|
e = os.path.abspath(expected.pop(0))
|
||||||
|
self.assertEqual(f, e)
|
||||||
|
|
||||||
|
def test_scan_dir_hidden(self):
|
||||||
|
expected = [
|
||||||
|
"test_files/aaa/file_1.txt",
|
||||||
|
"test_files/aaa/file_2.txt",
|
||||||
|
"test_files/aaa/file_3.txt",
|
||||||
|
"test_files/aaa/file_4.txt",
|
||||||
|
"test_files/aaa/.hidden_1.txt",
|
||||||
|
"test_files/bbb/file_1.txt",
|
||||||
|
"test_files/bbb/file_2.txt",
|
||||||
|
"test_files/bbb/file_3.txt",
|
||||||
|
"test_files/bbb/file_4.txt",
|
||||||
|
"test_files/bbb/.hidden_1.txt",
|
||||||
|
"test_files/ccc/file_1.txt",
|
||||||
|
"test_files/ccc/file_2.txt",
|
||||||
|
"test_files/ccc/file_3.txt",
|
||||||
|
"test_files/ccc/file_4.txt",
|
||||||
|
"test_files/ccc/.hidden_1.txt",
|
||||||
|
"test_files/.ddd/file_1.txt",
|
||||||
|
"test_files/.ddd/file_2.txt",
|
||||||
|
"test_files/.ddd/file_3.txt",
|
||||||
|
"test_files/.ddd/file_4.txt",
|
||||||
|
"test_files/complete/glg_poshist_all_160115_12z_v02.fit",
|
||||||
|
"test_files/complete/glg_tte_n0_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n1_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n2_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n3_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n4_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n5_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n6_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n7_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n8_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_n9_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_na_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_nb_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_b0_160115_12z_v00.fit",
|
||||||
|
"test_files/complete/glg_tte_b1_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n0_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n1_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n2_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n3_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n4_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n5_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n6_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n7_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_n9_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_na_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_nb_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_b0_160115_12z_v00.fit",
|
||||||
|
"test_files/incomplete/glg_tte_b1_160115_12z_v00.fit",
|
||||||
|
"test_files/_version/glg_tte_n0_160115_12z_v05.fit",
|
||||||
|
"test_files/_version/glg_tte_n1_160115_12z_v06.fit",
|
||||||
|
"test_files/_version/glg_tte_n2_160115_12z_v07.fit",
|
||||||
|
"test_files/_version/glg_tte_n3_160115_12z_v08.fit",
|
||||||
|
"test_files/_version/glg_tte_n4_160115_12z_v09.fit",
|
||||||
|
"test_files/_version/glg_tte_n5_160115_12z_v10.fit",
|
||||||
|
"test_files/_version/glg_tte_n6_160115_12z_v11.fit",
|
||||||
|
"test_files/_version/glg_tte_n7_160115_12z_v12.fit",
|
||||||
|
"test_files/_version/glg_tte_n8_160115_12z_v13.fit",
|
||||||
|
"test_files/_version/glg_tte_n9_160115_12z_v14.fit",
|
||||||
|
"test_files/_version/glg_tte_na_160115_12z_v15.fit",
|
||||||
|
"test_files/_version/glg_tte_nb_160115_12z_v16.fit",
|
||||||
|
"test_files/_version/glg_tte_b0_160115_12z_v17.fit",
|
||||||
|
"test_files/_version/glg_tte_b1_160115_12z_v18.fit",
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
files = [x for x in scan_dir("test_files", recursive=True, hidden=True)]
|
||||||
|
|
||||||
|
files.sort()
|
||||||
|
expected.sort()
|
||||||
|
|
||||||
|
self.assertListEqual(expected, files)
|
||||||
|
|
||||||
|
def test_hidden_regex(self):
|
||||||
|
expected = [
|
||||||
|
"test_files/aaa/.hidden_1.txt",
|
||||||
|
"test_files/bbb/.hidden_1.txt",
|
||||||
|
"test_files/ccc/.hidden_1.txt",
|
||||||
|
]
|
||||||
|
|
||||||
|
files = [x for x in scan_dir("test_files", recursive=True, hidden=True, regex="hidden")]
|
||||||
|
|
||||||
|
files.sort()
|
||||||
|
expected.sort()
|
||||||
|
|
||||||
|
self.assertListEqual(expected, files)
|
||||||
|
|
||||||
|
def test_is_complete(self):
|
||||||
|
dt = datetime.datetime(2016, 1, 15, 12, 0)
|
||||||
|
glob_expression = GbmFile.create(data_type='tte', uid=Met.from_datetime(dt).ymd_h, detector='*').basename()
|
||||||
|
files = glob.glob(os.path.join('test_files', 'complete', glob_expression))
|
||||||
|
fn_list = GbmFile.list_from_paths(files)
|
||||||
|
self.assertEqual(is_complete(fn_list), True)
|
||||||
|
files = glob.glob(os.path.join('test_files', 'incomplete', glob_expression))
|
||||||
|
fn_list = GbmFile.list_from_paths(files)
|
||||||
|
self.assertEqual(is_complete(fn_list), False)
|
||||||
|
|
||||||
|
def test_max_min_versions(self):
|
||||||
|
dt = datetime.datetime(2016, 1, 15, 12, 0)
|
||||||
|
glob_expression = GbmFile.create(data_type='tte', uid=Met.from_datetime(dt).ymd_h, detector='*',
|
||||||
|
version='??').basename()
|
||||||
|
files = glob.glob(os.path.join('test_files', '_version', glob_expression))
|
||||||
|
fn_list = GbmFile.list_from_paths(files)
|
||||||
|
self.assertEqual(max_version(fn_list), 18)
|
||||||
|
self.assertEqual(min_version(fn_list), 5)
|
||||||
|
|
||||||
|
def test_all_exists(self):
|
||||||
|
dt = datetime.datetime(2016, 1, 15, 12, 0)
|
||||||
|
files = GbmFile.create(data_type='tte', uid=Met.from_datetime(dt).ymd_h, detector=Detector.N0,
|
||||||
|
version=0).detector_list()
|
||||||
|
self.assertEqual(all_exists(files, os.path.join('test_files', 'complete')), True)
|
||||||
|
self.assertEqual(all_exists(files, os.path.join('test_files', 'incomplete')), False)
|
||||||
|
|
||||||
|
def test_ymd_path_hourly_str(self):
|
||||||
|
b = '/data'
|
||||||
|
f = 'glg_tte_n5_160712_12z_v00.fit'
|
||||||
|
p = ymd_path(b, f)
|
||||||
|
self.assertEqual(p, '/data/2016-07-12/glg_tte_n5_160712_12z_v00.fit')
|
||||||
|
|
||||||
|
def test_ymd_path_trig_str(self):
|
||||||
|
b = '/data'
|
||||||
|
f = 'glg_tte_n0_bn170710500_v00.fit'
|
||||||
|
p = ymd_path(b, f)
|
||||||
|
self.assertEqual(p, '/data/2017-07-10/glg_tte_n0_bn170710500_v00.fit')
|
||||||
|
|
||||||
|
def test_ymd_path_daily_str(self):
|
||||||
|
b = '/data'
|
||||||
|
f = 'glg_poshist_all_170802_v00.fit'
|
||||||
|
p = ymd_path(b, f)
|
||||||
|
self.assertEqual(p, '/data/2017-08-02/glg_poshist_all_170802_v00.fit')
|
||||||
|
|
||||||
|
def test_ymd_path_legacy_str(self):
|
||||||
|
b = '/data'
|
||||||
|
f = 'glg_ctime_na_150413125_v00.pha'
|
||||||
|
p = ymd_path(b, f)
|
||||||
|
self.assertEqual(p, '/data/2015-04-13/glg_ctime_na_150413125_v00.pha')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
180
test/test_healpix.py
Normal file
180
test/test_healpix.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from gbm.data.localization import *
|
||||||
|
|
||||||
|
data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data')
|
||||||
|
|
||||||
|
|
||||||
|
class TestHealPix(TestCase):
|
||||||
|
filename = os.path.join(data_dir, 'glg_healpix_all_bn190915240_v00.fit')
|
||||||
|
trigtime = 590219102.911008
|
||||||
|
npix = 196608
|
||||||
|
nside = 128
|
||||||
|
pixel_area = 0.2098
|
||||||
|
sun = (172.5011935415178, 3.23797213866954)
|
||||||
|
geo = (319.8312390218318, 17.40612934717674)
|
||||||
|
geo_radius = 67.2950460311874
|
||||||
|
n0 = (146.5959532829778, 36.96759511828569)
|
||||||
|
scpos = [-5039500., 4254000., -2067500.]
|
||||||
|
quat = [-0.223915, 0.447149, 0.860062, -0.101055]
|
||||||
|
centroid = (48.8671875, 4.181528273111476)
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
h = GbmHealPix.open(self.filename)
|
||||||
|
self.assertEqual(h.trigtime, self.trigtime)
|
||||||
|
self.assertEqual(h.npix, self.npix)
|
||||||
|
self.assertEqual(h.nside, self.nside)
|
||||||
|
self.assertAlmostEqual(h.pixel_area, self.pixel_area, places=4)
|
||||||
|
self.assertEqual(h.sun_location, self.sun)
|
||||||
|
self.assertEqual(h.geo_location, self.geo)
|
||||||
|
self.assertEqual(h.geo_radius, self.geo_radius)
|
||||||
|
self.assertEqual(h.n0_pointing, self.n0)
|
||||||
|
self.assertEqual(h.scpos.tolist(), self.scpos)
|
||||||
|
self.assertEqual(h.quaternion.tolist(), self.quat)
|
||||||
|
|
||||||
|
def test_geo(self):
|
||||||
|
h = GbmHealPix.open(self.filename)
|
||||||
|
self.assertAlmostEqual(h.geo_probability, 0.0, places=4)
|
||||||
|
self.assertAlmostEqual(h.observable_fraction(h), 1.0, places=4)
|
||||||
|
h2 = h.remove_earth(h)
|
||||||
|
self.assertEqual(h2.geo_probability, 0.0)
|
||||||
|
self.assertEqual(h2._prob.sum(), 1.0)
|
||||||
|
self.assertAlmostEqual(h2.observable_fraction(h2), 1.0, places=4)
|
||||||
|
|
||||||
|
def test_assoc_prob(self):
|
||||||
|
h = GbmHealPix.open(self.filename)
|
||||||
|
self.assertAlmostEqual(h.source_probability(*h.centroid), 0.997, places=3)
|
||||||
|
self.assertAlmostEqual(h.region_probability(h), 0.995, places=3)
|
||||||
|
|
||||||
|
self.assertEqual(h.source_probability(*h.centroid, prior=0.0), 0.0)
|
||||||
|
self.assertEqual(h.source_probability(*h.centroid, prior=1.0), 1.0)
|
||||||
|
self.assertEqual(h.region_probability(h, prior=0.0), 0.0)
|
||||||
|
self.assertEqual(h.region_probability(h, prior=1.0), 1.0)
|
||||||
|
|
||||||
|
g = GbmHealPix.from_gaussian(100.0, 60.0, 5.0, nside=128)
|
||||||
|
self.assertAlmostEqual(h.region_probability(g), 0.0, places=6)
|
||||||
|
self.assertEqual(h.source_probability(100.0, 60.0), 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_annulus(self):
|
||||||
|
h = GbmHealPix.from_annulus(300.0, 10.0, 70.0, 10.0, nside=128)
|
||||||
|
self.assertEqual(h.nside, self.nside)
|
||||||
|
self.assertEqual(h.npix, self.npix)
|
||||||
|
|
||||||
|
def test_multiply(self):
|
||||||
|
h1 = GbmHealPix.open(self.filename)
|
||||||
|
h2 = GbmHealPix.from_annulus(300.0, 10.0, 70.0, 10.0)
|
||||||
|
h = GbmHealPix.multiply(h1, h2)
|
||||||
|
self.assertEqual(h.nside, self.nside)
|
||||||
|
self.assertEqual(h.npix, self.npix)
|
||||||
|
|
||||||
|
class TestChi2Grid(TestCase):
|
||||||
|
filename = os.path.join(data_dir, 'chi2grid_bn190531568_v00.dat')
|
||||||
|
trigtime = 581002688.0
|
||||||
|
scpos = [5761500., -3302750., 1907250.]
|
||||||
|
quat = [-0.592056, -0.192717, -0.517305, -0.587134]
|
||||||
|
numpts = 41168
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
c = Chi2Grid.open(self.filename)
|
||||||
|
self.assertEqual(c.numpts, self.numpts)
|
||||||
|
self.assertEqual(c.azimuth.size, self.numpts)
|
||||||
|
self.assertEqual(c.zenith.size, self.numpts)
|
||||||
|
self.assertEqual(c.ra.size, self.numpts)
|
||||||
|
self.assertEqual(c.dec.size, self.numpts)
|
||||||
|
self.assertEqual(c.chisq.size, self.numpts)
|
||||||
|
self.assertEqual(c.significance.size, self.numpts)
|
||||||
|
|
||||||
|
def test_from_data(self):
|
||||||
|
c = Chi2Grid.open(self.filename)
|
||||||
|
c2 = Chi2Grid.from_data(c.azimuth, c.zenith, c.ra, c.dec, c.chisq)
|
||||||
|
self.assertCountEqual(c.azimuth, c2.azimuth)
|
||||||
|
self.assertCountEqual(c.zenith, c2.zenith)
|
||||||
|
self.assertCountEqual(c.ra, c2.ra)
|
||||||
|
self.assertCountEqual(c.dec, c2.dec)
|
||||||
|
self.assertCountEqual(c.chisq, c2.chisq)
|
||||||
|
self.assertCountEqual(c.significance, c2.significance)
|
||||||
|
|
||||||
|
def test_healpix_from_chi2grid(self):
|
||||||
|
c = Chi2Grid.open(self.filename)
|
||||||
|
c.quaternion = self.quat
|
||||||
|
c.scpos = self.scpos
|
||||||
|
c.trigtime = self.trigtime
|
||||||
|
|
||||||
|
h = GbmHealPix.from_chi2grid(c, nside=512)
|
||||||
|
self.assertEqual(h.quaternion.tolist(), self.quat)
|
||||||
|
self.assertEqual(h.scpos.tolist(), self.scpos)
|
||||||
|
self.assertEqual(h.trigtime, self.trigtime)
|
||||||
|
self.assertAlmostEqual(h.centroid[0], c.ra[c.chisq.argmin()], places=-1)
|
||||||
|
self.assertAlmostEqual(h.centroid[1], c.dec[c.chisq.argmin()], places=-1)
|
||||||
|
|
||||||
|
class TestSystematics(TestCase):
|
||||||
|
|
||||||
|
def test_gbuts(self):
|
||||||
|
sig, frac = GBUTS_Model_O3()
|
||||||
|
self.assertEqual(frac, [1.0])
|
||||||
|
self.assertAlmostEqual(sig[0], 0.047, places=3)
|
||||||
|
|
||||||
|
def test_hitl(self):
|
||||||
|
sig, frac = HitL_Model(50.0)
|
||||||
|
self.assertEqual(frac, [0.918])
|
||||||
|
self.assertAlmostEqual(sig[0], 0.073, places=3)
|
||||||
|
self.assertAlmostEqual(sig[1], 0.267, places=3)
|
||||||
|
|
||||||
|
sig, frac = HitL_Model(100.0)
|
||||||
|
self.assertEqual(frac, [0.884])
|
||||||
|
self.assertAlmostEqual(sig[0], 0.040, places=3)
|
||||||
|
self.assertAlmostEqual(sig[1], 0.230, places=3)
|
||||||
|
|
||||||
|
def test_ground(self):
|
||||||
|
sig, frac = GA_Model()
|
||||||
|
self.assertEqual(frac, [0.804])
|
||||||
|
self.assertAlmostEqual(sig[0], 0.065, places=3)
|
||||||
|
self.assertAlmostEqual(sig[1], 0.239, places=3)
|
||||||
|
|
||||||
|
def test_roboba(self):
|
||||||
|
sig, frac = RoboBA_Function('long')
|
||||||
|
self.assertEqual(frac, [0.579])
|
||||||
|
self.assertAlmostEqual(sig[0], 0.032, places=3)
|
||||||
|
self.assertAlmostEqual(sig[1], 0.072, places=3)
|
||||||
|
|
||||||
|
sig, frac = RoboBA_Function('short')
|
||||||
|
self.assertEqual(frac, [0.390])
|
||||||
|
self.assertAlmostEqual(sig[0], 0.045, places=3)
|
||||||
|
self.assertAlmostEqual(sig[1], 0.077, places=3)
|
||||||
|
|
||||||
|
def test_untargeted(self):
|
||||||
|
sig, frac = Untargeted_Search_Model()
|
||||||
|
self.assertEqual(frac, [1.0])
|
||||||
|
self.assertAlmostEqual(sig[0], 0.097, places=3)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
|
|
263
test/test_lookup.py
Normal file
263
test/test_lookup.py
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import unittest
|
||||||
|
from gbm.lookup.lookup import *
|
||||||
|
|
||||||
|
|
||||||
|
class TestLookup(unittest.TestCase):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(TestLookup, self).__init__(*args, **kwargs)
|
||||||
|
self.datafiles = [
|
||||||
|
{
|
||||||
|
"filename": "glg_cspec_b1_bn090926181_v00.pha",
|
||||||
|
"response": None,
|
||||||
|
"background": {
|
||||||
|
"method": "Polynomial",
|
||||||
|
"datatype": "binned",
|
||||||
|
"args": [2],
|
||||||
|
"kwargs": {}
|
||||||
|
},
|
||||||
|
"binnings": {
|
||||||
|
"time": None,
|
||||||
|
"energy": None,
|
||||||
|
},
|
||||||
|
"selections": {
|
||||||
|
"source": [[0.7054115, 19.058352]],
|
||||||
|
"energy": [[250.0, 38000.0]],
|
||||||
|
"background": [[69.57779188816323, 240.8593380212668], [-45.687147962415935, -4.751935679032698]],
|
||||||
|
},
|
||||||
|
"views": {
|
||||||
|
"time": {
|
||||||
|
'xmin': -14.447117535623448,
|
||||||
|
'xmax': 50.187428174981676,
|
||||||
|
'ymin': 1831.095164143789,
|
||||||
|
'ymax': 5137.280091318506
|
||||||
|
},
|
||||||
|
"energy": None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "glg_cspec_n6_bn090926181_v00.pha",
|
||||||
|
"response": "glg_cspec_n6_bn090926181_v00.rsp2",
|
||||||
|
"background": {
|
||||||
|
"method": "Polynomial",
|
||||||
|
"datatype": "binned",
|
||||||
|
"args": [3],
|
||||||
|
"kwargs": {}
|
||||||
|
},
|
||||||
|
"binnings": {
|
||||||
|
"time": None,
|
||||||
|
"energy": None,
|
||||||
|
},
|
||||||
|
"selections": {
|
||||||
|
"source": [[0.7054115, 19.058352]],
|
||||||
|
"energy": [[8.47545, 905.397]],
|
||||||
|
"background": [[-1529.48, -1200.07], [-53.5312, -17.6488], [141.106, 1764.64]],
|
||||||
|
},
|
||||||
|
"views": {
|
||||||
|
"time": {
|
||||||
|
'xmin': -10.0,
|
||||||
|
'xmax': 50.0,
|
||||||
|
'ymin': 0.0,
|
||||||
|
'ymax': 9556.3
|
||||||
|
},
|
||||||
|
"energy": {
|
||||||
|
'xmin': 4.44444,
|
||||||
|
'xmax': 2000.0,
|
||||||
|
'ymin': 0.0352183,
|
||||||
|
'ymax': 69.4991
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
def assert_datafile(self, df, d):
|
||||||
|
self.assertEqual(df.filename, d.get('filename', None))
|
||||||
|
self.assertEqual(df.response, d.get('response', None))
|
||||||
|
|
||||||
|
bkg = d.get('background', None)
|
||||||
|
if bkg is None:
|
||||||
|
self.assertIsNone(df.background)
|
||||||
|
else:
|
||||||
|
self.assertIsNotNone(df.background)
|
||||||
|
self.assertEqual(df.background.method, bkg.get('method', None))
|
||||||
|
self.assertEqual(list(df.background.args), bkg.get('args', None))
|
||||||
|
self.assertEqual(df.background.kwargs, bkg.get('kwargs', None))
|
||||||
|
self.assertEqual(df.background.datatype, bkg.get('datatype', None))
|
||||||
|
|
||||||
|
if 'binnings' in d:
|
||||||
|
b = d['binnings']
|
||||||
|
self.assertEqual(df.binnings.energy, b.get('energy', None))
|
||||||
|
self.assertEqual(df.binnings.time, b.get('time', None))
|
||||||
|
else:
|
||||||
|
self.assertIsNone(df.binnings.energy)
|
||||||
|
self.assertIsNone(df.binnings.time)
|
||||||
|
|
||||||
|
if 'selections' in d:
|
||||||
|
b = d['selections']
|
||||||
|
self.assertEqual(df.selections.background, b.get('background', None))
|
||||||
|
self.assertEqual(df.selections.energy, b.get('energy', None))
|
||||||
|
self.assertEqual(df.selections.source, b.get('source', None))
|
||||||
|
else:
|
||||||
|
self.assertIsNone(df.selections.background)
|
||||||
|
self.assertIsNone(df.selections.energy)
|
||||||
|
self.assertIsNone(df.selections.source)
|
||||||
|
|
||||||
|
if 'views' in d:
|
||||||
|
b = d['views']
|
||||||
|
v = b.get('energy', None)
|
||||||
|
if v is None:
|
||||||
|
self.assertIsNone(df.views.energy)
|
||||||
|
else:
|
||||||
|
self.assertIsInstance(df.views.energy, View)
|
||||||
|
self.assertEqual(df.views.energy, View.from_dict(v))
|
||||||
|
v = b.get('time', None)
|
||||||
|
if v is None:
|
||||||
|
self.assertIsNone(df.views.time)
|
||||||
|
else:
|
||||||
|
self.assertIsInstance(df.views.time, View)
|
||||||
|
self.assertEqual(df.views.time, View.from_dict(v))
|
||||||
|
else:
|
||||||
|
self.assertIsNone(df.views.energy)
|
||||||
|
self.assertIsNone(df.views.time)
|
||||||
|
|
||||||
|
def test_create_datafile_from_dict(self):
|
||||||
|
for d in self.datafiles:
|
||||||
|
df = DataFileLookup.from_dict(d)
|
||||||
|
self.assert_datafile(df, d)
|
||||||
|
|
||||||
|
def test_create_datafile(self):
|
||||||
|
"""Create a DataFile object with:
|
||||||
|
{
|
||||||
|
"filename": "glg_cspec_n6_bn090926181_v00.pha",
|
||||||
|
"response": "glg_cspec_n6_bn090926181_v00.rsp2",
|
||||||
|
"background": {
|
||||||
|
"method": "Polynomial",
|
||||||
|
"datatype": "binned",
|
||||||
|
"args": [ 3 ],
|
||||||
|
"kwargs": {}
|
||||||
|
},
|
||||||
|
"time_binning": null,
|
||||||
|
"energy_binning": null,
|
||||||
|
"source_selection": [[0.7054115, 19.058352]],
|
||||||
|
"energy_selection": [[8.47545, 905.397]],
|
||||||
|
"background_selection": [[-1529.48, -1200.07], [-53.5312, -17.6488], [141.106, 1764.64]],
|
||||||
|
"time_display_view": [-10.0, 50.0, 0.0, 9556.3],
|
||||||
|
"energy_display_view": [4.44444, 2000.0, 0.0352183, 69.4991]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
df = DataFileLookup()
|
||||||
|
df.filename = "glg_cspec_n6_bn090926181_v00.pha"
|
||||||
|
df.response = "glg_cspec_n6_bn090926181_v00.rsp2"
|
||||||
|
|
||||||
|
df.background = LookupBackground()
|
||||||
|
df.background.method = "Polynomial"
|
||||||
|
df.background.datatype = "binned"
|
||||||
|
df.background.args = (3, )
|
||||||
|
df.background.kwargs = {}
|
||||||
|
|
||||||
|
df.selections.source = [[0.7054115, 19.058352]]
|
||||||
|
df.selections.energy = [[8.47545, 905.397]]
|
||||||
|
df.selections.background = [[-1529.48, -1200.07], [-53.5312, -17.6488], [141.106, 1764.64]]
|
||||||
|
df.views.time = View(-10.0, 50.0, 0.0, 9556.3)
|
||||||
|
df.views.energy = View(4.44444, 2000.0, 0.0352183, 69.4991)
|
||||||
|
|
||||||
|
self.assert_datafile(df, self.datafiles[1])
|
||||||
|
|
||||||
|
def test_read_write_lookup(self):
|
||||||
|
lu_w = LookupFile()
|
||||||
|
for d in self.datafiles:
|
||||||
|
df = DataFileLookup.from_dict(d)
|
||||||
|
lu_w[df.filename] = df
|
||||||
|
lu_w.write_to("test.json")
|
||||||
|
|
||||||
|
lu_r = LookupFile.read_from("test.json")
|
||||||
|
|
||||||
|
self.assertEqual(lu_r.file_date, lu_w.file_date)
|
||||||
|
self.assertEqual(len(lu_r.datafiles), len(lu_w.datafiles))
|
||||||
|
for k, df_r in lu_r.datafiles.items():
|
||||||
|
df_w = lu_w.datafiles[k]
|
||||||
|
|
||||||
|
self.assertEqual(df_r.filename, df_w.filename)
|
||||||
|
self.assertEqual(df_r.response, df_w.response)
|
||||||
|
|
||||||
|
self.assertEqual(df_r.binnings.energy, df_w.binnings.energy)
|
||||||
|
self.assertEqual(df_r.binnings.time, df_w.binnings.time)
|
||||||
|
|
||||||
|
self.assertEqual(df_r.selections.background, df_w.selections.background)
|
||||||
|
self.assertEqual(df_r.selections.energy, df_w.selections.energy)
|
||||||
|
self.assertEqual(df_r.selections.source, df_w.selections.source)
|
||||||
|
|
||||||
|
self.assertEqual(df_r.views.energy, df_w.views.energy)
|
||||||
|
self.assertEqual(df_r.views.time, df_w.views.time)
|
||||||
|
|
||||||
|
def test_read_from_rmfit(self):
|
||||||
|
lu = LookupFile.read_from_rmfit('data/glg_ctime_nb_bn120415958_v00.lu')
|
||||||
|
df = lu.datafiles['glg_ctime_nb_bn120415958_v00.pha']
|
||||||
|
|
||||||
|
# TODO: Finalize the background format for the Lookup file
|
||||||
|
self.assertEqual(df.background.method, "Polynomial")
|
||||||
|
self.assertEqual(df.background.args[0], 2)
|
||||||
|
|
||||||
|
self.assertEqual(df.views.time, View(-60.0, 40.0, 400.0, 1400.0))
|
||||||
|
self.assertEqual(df.views.energy, View(4.32375, 2000.0, 0.0676773, 22.8421))
|
||||||
|
|
||||||
|
self.assertIsNone(df.binnings.energy)
|
||||||
|
time_edges = df.binnings.time[0].args[0]
|
||||||
|
self.assertEqual(1861, len(time_edges))
|
||||||
|
self.assertEqual(0, time_edges[0])
|
||||||
|
self.assertEqual(14426, time_edges[-1])
|
||||||
|
|
||||||
|
energy_select = df.selections.energy
|
||||||
|
self.assertEqual(len(energy_select), 1)
|
||||||
|
self.assertEqual(18.8469, energy_select[0][0])
|
||||||
|
self.assertEqual(767.693, energy_select[0][1])
|
||||||
|
|
||||||
|
time_select = df.selections.source
|
||||||
|
self.assertEqual(len(time_select), 2)
|
||||||
|
self.assertEqual(time_select[0][0], -17.647648)
|
||||||
|
self.assertEqual(time_select[0][1], -14.902550)
|
||||||
|
self.assertEqual(time_select[1][0], -5.2947070)
|
||||||
|
self.assertEqual(time_select[1][1], 2.7445084)
|
||||||
|
|
||||||
|
bkgd_select = df.selections.background
|
||||||
|
self.assertEqual(len(bkgd_select), 2)
|
||||||
|
self.assertEqual(bkgd_select[0][0], -480.00706)
|
||||||
|
self.assertEqual(bkgd_select[0][1], -115.30119)
|
||||||
|
self.assertEqual(bkgd_select[1][0], 72.934103)
|
||||||
|
self.assertEqual(bkgd_select[1][1], 522.34586)
|
||||||
|
|
||||||
|
def test_read_ti(self):
|
||||||
|
lu = LookupFile.read_from_rmfit('data/glg_tte_n9_bn090131090_v00.lu', ti_file='data/glg_tte_n9_bn090131090_v00.ti')
|
||||||
|
|
||||||
|
tte_edges = lu.datafiles['glg_tte_n9_bn090131090_v00.fit'].binnings.time[0].args[0]
|
||||||
|
self.assertEqual(len(tte_edges), 319)
|
||||||
|
self.assertEqual(tte_edges[0], -25.600000)
|
||||||
|
self.assertEqual(tte_edges[-1], 300.73552)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
|
|
105
test/test_pha_data.py
Normal file
105
test/test_pha_data.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from gbm.data.pha import PHA, BAK
|
||||||
|
|
||||||
|
data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data')
|
||||||
|
|
||||||
|
class TestBAK(TestCase):
|
||||||
|
filename = os.path.join(data_dir, 'glg_tte_n0_bn160509374_xspec_v00.bak')
|
||||||
|
trigtime = 484477130.219298
|
||||||
|
header_names = ['PRIMARY', 'EBOUNDS', 'SPECTRUM', 'GTI']
|
||||||
|
numchans = 128
|
||||||
|
trange = (484477128.411298, 484477165.275298)
|
||||||
|
tcent = np.sum(trange)/2.0
|
||||||
|
erange = (4.525152, 2000)
|
||||||
|
gti = [(-1.80800002813339, 35.055999994278)]
|
||||||
|
exposure = 36.864
|
||||||
|
valid_channels = np.arange(128).tolist()
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
bak = BAK.open(self.filename)
|
||||||
|
trigtime = bak.trigtime
|
||||||
|
self.assertAlmostEqual(trigtime, self.trigtime, places=6)
|
||||||
|
self.assertEqual(bak.is_gbm_file, True)
|
||||||
|
self.assertEqual(bak.id, '160509374')
|
||||||
|
self.assertEqual(bak.filename, os.path.basename(self.filename))
|
||||||
|
self.assertEqual(bak.is_trigger, True)
|
||||||
|
self.assertEqual(bak.detector, 'n0')
|
||||||
|
self.assertEqual(bak.datatype, 'TTE')
|
||||||
|
self.assertAlmostEqual(bak.energy_range[0], self.erange[0], places=4)
|
||||||
|
self.assertAlmostEqual(bak.energy_range[1], self.erange[1], places=4)
|
||||||
|
self.assertEqual(len(bak.gti), 1)
|
||||||
|
self.assertAlmostEqual(bak.gti[0][0], self.gti[0][0], places=6)
|
||||||
|
self.assertAlmostEqual(bak.gti[0][1], self.gti[0][1], places=6)
|
||||||
|
self.assertCountEqual(bak.headers.keys(), self.header_names)
|
||||||
|
self.assertEqual(bak.numchans, self.numchans)
|
||||||
|
self.assertAlmostEqual(bak.time_range[0], self.trange[0]-trigtime, places=6)
|
||||||
|
self.assertAlmostEqual(bak.time_range[1], self.trange[1]-trigtime, places=6)
|
||||||
|
self.assertAlmostEqual(bak.tcent, self.tcent-trigtime, places=6)
|
||||||
|
self.assertCountEqual(bak.valid_channels[0], self.valid_channels)
|
||||||
|
|
||||||
|
def test_write(self):
|
||||||
|
bak = BAK.open(self.filename)
|
||||||
|
new_file = os.path.join(data_dir, 'glg_tte_n0_bn160509374_xspec_v01.bak')
|
||||||
|
bak.write(os.path.dirname(self.filename),
|
||||||
|
filename='glg_tte_n0_bn160509374_xspec_v01.bak')
|
||||||
|
bak2 = BAK.open(new_file)
|
||||||
|
os.remove(new_file)
|
||||||
|
|
||||||
|
self.assertCountEqual(bak2.time_range, bak.time_range)
|
||||||
|
self.assertCountEqual(bak2.data.lo_edges, bak.data.lo_edges)
|
||||||
|
self.assertCountEqual(bak2.data.hi_edges, bak.data.hi_edges)
|
||||||
|
self.assertCountEqual(bak2.data.exposure, bak.data.exposure)
|
||||||
|
self.assertCountEqual(bak2.data.rates, bak.data.rates)
|
||||||
|
self.assertCountEqual(bak2.data.rate_uncertainty, bak.data.rate_uncertainty)
|
||||||
|
|
||||||
|
class TestPHA(TestCase):
|
||||||
|
pha = PHA.open(os.path.join(data_dir, 'sim_pha.pha'))
|
||||||
|
|
||||||
|
def test_write(self):
|
||||||
|
self.pha.write(data_dir, filename='test_pha.pha')
|
||||||
|
test_pha = PHA.open(os.path.join(data_dir, 'test_pha.pha'))
|
||||||
|
os.remove(os.path.join(data_dir, 'test_pha.pha'))
|
||||||
|
|
||||||
|
self.assertCountEqual(self.pha.energy_range, test_pha.energy_range)
|
||||||
|
self.assertEqual(self.pha.exposure, test_pha.exposure)
|
||||||
|
self.assertCountEqual(self.pha.gti, test_pha.gti)
|
||||||
|
self.assertEqual(self.pha.numchans, test_pha.numchans)
|
||||||
|
self.assertEqual(self.pha.tcent, test_pha.tcent)
|
||||||
|
self.assertCountEqual(self.pha.time_range, test_pha.time_range)
|
||||||
|
self.assertEqual(self.pha.trigtime, test_pha.trigtime)
|
||||||
|
self.assertCountEqual(self.pha.valid_channels, test_pha.valid_channels)
|
||||||
|
|
||||||
|
self.assertCountEqual(self.pha.data.counts, test_pha.data.counts)
|
||||||
|
self.assertCountEqual(self.pha.data.lo_edges, test_pha.data.lo_edges)
|
||||||
|
self.assertCountEqual(self.pha.data.hi_edges, test_pha.data.hi_edges)
|
||||||
|
self.assertCountEqual(self.pha.data.exposure, test_pha.data.exposure)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
393
test/test_phaii_data.py
Normal file
393
test/test_phaii_data.py
Normal file
@ -0,0 +1,393 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from gbm.data.phaii import Ctime, Cspec
|
||||||
|
from gbm.binning.binned import combine_by_factor
|
||||||
|
|
||||||
|
data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data')
|
||||||
|
|
||||||
|
class TestCtime(TestCase):
|
||||||
|
filename = os.path.join(data_dir, 'glg_ctime_nb_bn120415958_v00.pha')
|
||||||
|
trigtime = 356223561.133346
|
||||||
|
numchans = 8
|
||||||
|
erange = (4.323754, 2000.0)
|
||||||
|
gti = (356222661.790904, 356224561.991216)
|
||||||
|
gti_rel = (gti[0]-trigtime, gti[1]-trigtime)
|
||||||
|
trange = (356222661.790904, 356224561.991216)
|
||||||
|
trange_rel = (trange[0]-trigtime, trange[1]-trigtime)
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
ctime = Ctime.open(self.filename)
|
||||||
|
trigtime = ctime.trigtime
|
||||||
|
self.assertAlmostEqual(trigtime, self.trigtime, places=6)
|
||||||
|
self.assertEqual(ctime.is_gbm_file, True)
|
||||||
|
self.assertEqual(ctime.id, '120415958')
|
||||||
|
self.assertEqual(ctime.filename, os.path.basename(self.filename))
|
||||||
|
self.assertEqual(ctime.is_trigger, True)
|
||||||
|
self.assertEqual(ctime.detector, 'nb')
|
||||||
|
self.assertEqual(ctime.datatype, 'CTIME')
|
||||||
|
self.assertAlmostEqual(ctime.energy_range[0], self.erange[0], places=6)
|
||||||
|
self.assertAlmostEqual(ctime.energy_range[1], self.erange[1], places=6)
|
||||||
|
self.assertEqual(len(ctime.gti), 1)
|
||||||
|
self.assertAlmostEqual(ctime.gti[0][0], self.gti_rel[0], places=6)
|
||||||
|
self.assertAlmostEqual(ctime.gti[0][1], self.gti_rel[1], places=6)
|
||||||
|
self.assertCountEqual(ctime.headers.keys(), ['PRIMARY', 'EBOUNDS',
|
||||||
|
'SPECTRUM', 'GTI'])
|
||||||
|
self.assertEqual(ctime.numchans, self.numchans)
|
||||||
|
self.assertAlmostEqual(ctime.time_range[0], self.trange_rel[0], places=6)
|
||||||
|
self.assertAlmostEqual(ctime.time_range[1], self.trange_rel[1], places=6)
|
||||||
|
self.assertEqual(len(ctime.data.contiguous_time_bins()), 1)
|
||||||
|
self.assertEqual(len(ctime.data.contiguous_energy_bins()), 1)
|
||||||
|
|
||||||
|
def test_get_exposure(self):
|
||||||
|
ctime = Ctime.open(self.filename)
|
||||||
|
exposure = ctime.get_exposure(time_ranges=(0.0, 10.0))
|
||||||
|
self.assertAlmostEqual(exposure, 10.01, delta=0.1)
|
||||||
|
exposure = ctime.get_exposure(time_ranges=[(0.0, 10.0)])
|
||||||
|
self.assertAlmostEqual(exposure, 10.01, delta=0.1)
|
||||||
|
exposure = ctime.get_exposure(time_ranges=[(0.0, 10.0), (20.0, 30.0)])
|
||||||
|
self.assertAlmostEqual(exposure, 20.02, delta=0.1)
|
||||||
|
|
||||||
|
def test_to_lightcurve(self):
|
||||||
|
ctime = Ctime.open(self.filename)
|
||||||
|
lc1 = ctime.to_lightcurve()
|
||||||
|
self.assertAlmostEqual(lc1.range[0], self.trange[0]-self.trigtime, delta=0.1)
|
||||||
|
self.assertAlmostEqual(lc1.range[1], self.trange[1]-self.trigtime, delta=0.1)
|
||||||
|
self.assertEqual(lc1.size, 14433)
|
||||||
|
|
||||||
|
lc2 = ctime.to_lightcurve(time_range=(-100.0, 100.0))
|
||||||
|
self.assertAlmostEqual(lc2.range[0], 356223461.035746-self.trigtime, delta=0.1)
|
||||||
|
self.assertAlmostEqual(lc2.range[1], 356223661.16687-self.trigtime, delta=0.1)
|
||||||
|
self.assertEqual(lc2.size, 1955)
|
||||||
|
|
||||||
|
lc3 = ctime.to_lightcurve(energy_range=(50.0, 300.0))
|
||||||
|
self.assertAlmostEqual(lc3.range[0], self.trange[0]-self.trigtime, delta=0.1)
|
||||||
|
self.assertAlmostEqual(lc3.range[1], self.trange[1]-self.trigtime, delta=0.1)
|
||||||
|
self.assertEqual(lc3.size, 14433)
|
||||||
|
|
||||||
|
lc4 = ctime.to_lightcurve(channel_range=(1, 6))
|
||||||
|
self.assertAlmostEqual(lc4.range[0], self.trange[0]-self.trigtime, delta=0.1)
|
||||||
|
self.assertAlmostEqual(lc4.range[1], self.trange[1]-self.trigtime, delta=0.1)
|
||||||
|
self.assertEqual(lc4.size, 14433)
|
||||||
|
|
||||||
|
def test_to_spectrum(self):
|
||||||
|
ctime = Ctime.open(self.filename)
|
||||||
|
lc1 = ctime.to_spectrum()
|
||||||
|
self.assertAlmostEqual(lc1.range[0], self.erange[0], delta=0.1)
|
||||||
|
self.assertAlmostEqual(lc1.range[1], self.erange[1], delta=0.1)
|
||||||
|
self.assertEqual(lc1.size, 8)
|
||||||
|
|
||||||
|
lc2 = ctime.to_spectrum(time_range=(-100.0, 100.0))
|
||||||
|
self.assertAlmostEqual(lc2.range[0], self.erange[0], delta=0.1)
|
||||||
|
self.assertAlmostEqual(lc2.range[1], self.erange[1], delta=0.1)
|
||||||
|
self.assertEqual(lc2.size, 8)
|
||||||
|
|
||||||
|
lc3 = ctime.to_spectrum(energy_range=(50.0, 300.0))
|
||||||
|
self.assertAlmostEqual(lc3.range[0], 49.60019, delta=0.1)
|
||||||
|
self.assertAlmostEqual(lc3.range[1], 538.144, delta=0.1)
|
||||||
|
self.assertEqual(lc3.size, 3)
|
||||||
|
|
||||||
|
lc4 = ctime.to_spectrum(channel_range=(3, 4))
|
||||||
|
self.assertAlmostEqual(lc4.range[0], 49.60019, delta=0.1)
|
||||||
|
self.assertAlmostEqual(lc4.range[1], 290.4606, delta=0.1)
|
||||||
|
self.assertEqual(lc4.size, 2)
|
||||||
|
|
||||||
|
def test_to_pha(self):
|
||||||
|
ctime = Ctime.open(self.filename)
|
||||||
|
pha1 = ctime.to_pha()
|
||||||
|
self.assertCountEqual(pha1.time_range, ctime.time_range)
|
||||||
|
self.assertCountEqual(pha1.gti, ctime.gti)
|
||||||
|
self.assertEqual(pha1.trigtime, ctime.trigtime)
|
||||||
|
self.assertAlmostEqual(pha1.energy_range[0], self.erange[0], delta=0.1)
|
||||||
|
self.assertAlmostEqual(pha1.energy_range[1], self.erange[1], delta=0.1)
|
||||||
|
self.assertEqual(pha1.numchans, self.numchans)
|
||||||
|
self.assertCountEqual(pha1.valid_channels, np.arange(self.numchans))
|
||||||
|
self.assertAlmostEqual(pha1.exposure, np.sum(ctime.data.exposure), delta=0.1)
|
||||||
|
|
||||||
|
pha2 = ctime.to_pha(channel_range=(3,4))
|
||||||
|
self.assertAlmostEqual(pha2.energy_range[0], self.erange[0], delta=0.1)
|
||||||
|
self.assertAlmostEqual(pha2.energy_range[1], self.erange[1], delta=0.1)
|
||||||
|
self.assertEqual(pha2.numchans, self.numchans)
|
||||||
|
self.assertCountEqual(pha2.valid_channels, np.array([3,4]))
|
||||||
|
|
||||||
|
pha3 = ctime.to_pha(time_ranges=[(0.0, 10.0), (20.0, 30.0)])
|
||||||
|
self.assertAlmostEqual(pha3.time_range[0], 0.0, delta=0.1)
|
||||||
|
self.assertAlmostEqual(pha3.time_range[1], 30.0, delta=0.1)
|
||||||
|
self.assertAlmostEqual(pha3.exposure, 20., delta=0.1)
|
||||||
|
|
||||||
|
def test_write(self):
|
||||||
|
ctime = Ctime.open(self.filename)
|
||||||
|
new_file = os.path.join(data_dir, 'glg_ctime_nb_bn120415958_v01.pha')
|
||||||
|
ctime.write(os.path.dirname(self.filename),
|
||||||
|
filename='glg_ctime_nb_bn120415958_v01.pha')
|
||||||
|
ctime2 = Ctime.open(new_file)
|
||||||
|
os.remove(new_file)
|
||||||
|
|
||||||
|
self.assertCountEqual(ctime2.data.tstart, ctime.data.tstart)
|
||||||
|
self.assertCountEqual(ctime2.data.tstop, ctime.data.tstop)
|
||||||
|
self.assertCountEqual(ctime2.data.emin, ctime.data.emin)
|
||||||
|
self.assertCountEqual(ctime2.data.emax, ctime.data.emax)
|
||||||
|
self.assertCountEqual(ctime2.data.exposure, ctime.data.exposure)
|
||||||
|
for i in range(ctime.numchans):
|
||||||
|
self.assertCountEqual(ctime2.data.counts[:,i],
|
||||||
|
ctime.data.counts[:,i])
|
||||||
|
|
||||||
|
def test_slice_time(self):
|
||||||
|
ctime = Ctime.open(self.filename)
|
||||||
|
ctime2 = ctime.slice_time((-10.0, 10.0))
|
||||||
|
self.assertEqual(ctime2.gti, [ctime2.time_range])
|
||||||
|
self.assertEqual(ctime2.data.numtimes, 198)
|
||||||
|
|
||||||
|
ctime3 = ctime.slice_time([(-10.0, 10.0), (20.0, 30.0)])
|
||||||
|
self.assertEqual(len(ctime3.gti), 2)
|
||||||
|
self.assertEqual(ctime3.data.numtimes, 355)
|
||||||
|
|
||||||
|
def test_slice_energy(self):
|
||||||
|
ctime = Ctime.open(self.filename)
|
||||||
|
ctime2 = ctime.slice_energy((50.0, 250.0))
|
||||||
|
self.assertAlmostEqual(ctime2.energy_range[0], 50.0, delta=1.0)
|
||||||
|
self.assertAlmostEqual(ctime2.energy_range[1], 250.0, delta=50.0)
|
||||||
|
self.assertEqual(ctime2.data.numchans, 2)
|
||||||
|
|
||||||
|
ctime3 = ctime.slice_energy([(50.0, 250.0), (500.0, 2000.0)])
|
||||||
|
self.assertAlmostEqual(ctime3.energy_range[0], 50.0, delta=1.0)
|
||||||
|
self.assertAlmostEqual(ctime3.energy_range[1], 2000.0, delta=50.0)
|
||||||
|
self.assertEqual(ctime3.data.numchans, 5)
|
||||||
|
|
||||||
|
def test_rebin_time(self):
|
||||||
|
ctime = Ctime.open(self.filename)
|
||||||
|
ctime2 = ctime.rebin_time(combine_by_factor, 4)
|
||||||
|
self.assertAlmostEqual(ctime2.gti[0][0], ctime.gti[0][0], delta=1.0)
|
||||||
|
self.assertAlmostEqual(ctime2.gti[0][1], ctime.gti[0][1], delta=1.0)
|
||||||
|
self.assertEqual(ctime2.data.numtimes, 3608)
|
||||||
|
|
||||||
|
ctime3 = ctime.rebin_time(combine_by_factor, 4, time_range=(-100.0, 100.0))
|
||||||
|
self.assertEqual(ctime3.gti, ctime.gti)
|
||||||
|
self.assertEqual(ctime3.data.numtimes, 12968)
|
||||||
|
|
||||||
|
def test_rebin_energy(self):
|
||||||
|
ctime = Ctime.open(self.filename)
|
||||||
|
ctime2 = ctime.rebin_energy(combine_by_factor, 2)
|
||||||
|
self.assertEqual(ctime2.energy_range, ctime.energy_range)
|
||||||
|
self.assertEqual(ctime2.data.numchans, 4)
|
||||||
|
|
||||||
|
ctime3 = ctime.rebin_energy(combine_by_factor, 4, energy_range=(30.0, 300.0))
|
||||||
|
self.assertEqual(ctime3.energy_range, ctime.energy_range)
|
||||||
|
self.assertEqual(ctime3.data.numchans, 6)
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
with self.assertRaises(IOError):
|
||||||
|
ctime = Ctime.open('wakawaka.pha')
|
||||||
|
|
||||||
|
ctime = Ctime.open(self.filename)
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
ctime.to_lightcurve(time_range=(100.0, 10.0))
|
||||||
|
ctime.to_lightcurve(energy_range=(30.0, 10.0))
|
||||||
|
ctime.to_lightcurve(channel_range=(30, 10))
|
||||||
|
ctime.to_spectrum(time_range=(100.0, 10.0))
|
||||||
|
ctime.to_spectrum(energy_range=(30.0, 10.0))
|
||||||
|
ctime.to_spectrum(channel_range=(30, 10))
|
||||||
|
|
||||||
|
|
||||||
|
class TestCspec(TestCase):
|
||||||
|
filename = os.path.join(data_dir, 'glg_cspec_b0_bn120415958_v00.pha')
|
||||||
|
trigtime = 356223561.133346
|
||||||
|
numchans = 128
|
||||||
|
erange = (114.5423, 50000.0)
|
||||||
|
gti = [(356219559.260874, 356221211.030666), (356222657.950904, 356227367.05709)]
|
||||||
|
gti_rel = [(gti[0][0]-trigtime, gti[0][1]-trigtime),
|
||||||
|
(gti[1][0]-trigtime, gti[1][1]-trigtime)]
|
||||||
|
trange = (356219559.260874, 356227367.057090 )
|
||||||
|
trange_rel = (trange[0]-trigtime, trange[1]-trigtime)
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
cspec = Cspec.open(self.filename)
|
||||||
|
trigtime = cspec.trigtime
|
||||||
|
self.assertAlmostEqual(trigtime, self.trigtime, places=6)
|
||||||
|
self.assertEqual(cspec.is_gbm_file, True)
|
||||||
|
self.assertEqual(cspec.id, '120415958')
|
||||||
|
self.assertEqual(cspec.filename, os.path.basename(self.filename))
|
||||||
|
self.assertEqual(cspec.is_trigger, True)
|
||||||
|
self.assertEqual(cspec.detector, 'b0')
|
||||||
|
self.assertEqual(cspec.datatype, 'CSPEC')
|
||||||
|
self.assertAlmostEqual(cspec.energy_range[0], self.erange[0], places=4)
|
||||||
|
self.assertAlmostEqual(cspec.energy_range[1], self.erange[1], places=4)
|
||||||
|
self.assertEqual(len(cspec.gti), 2)
|
||||||
|
self.assertAlmostEqual(cspec.gti[0][0], self.gti_rel[0][0], places=6)
|
||||||
|
self.assertAlmostEqual(cspec.gti[0][1], self.gti_rel[0][1], places=6)
|
||||||
|
self.assertAlmostEqual(cspec.gti[1][0], self.gti_rel[1][0], places=6)
|
||||||
|
self.assertAlmostEqual(cspec.gti[1][1], self.gti_rel[1][1], places=6)
|
||||||
|
self.assertCountEqual(cspec.headers.keys(), ['PRIMARY', 'EBOUNDS',
|
||||||
|
'SPECTRUM', 'GTI'])
|
||||||
|
self.assertEqual(cspec.numchans, self.numchans)
|
||||||
|
self.assertAlmostEqual(cspec.time_range[0], self.trange_rel[0], places=6)
|
||||||
|
self.assertAlmostEqual(cspec.time_range[1], self.trange_rel[1], places=6)
|
||||||
|
self.assertEqual(len(cspec.data.contiguous_time_bins()), 3)
|
||||||
|
self.assertEqual(len(cspec.data.contiguous_energy_bins()), 1)
|
||||||
|
|
||||||
|
def test_to_lightcurve(self):
|
||||||
|
cspec = Cspec.open(self.filename)
|
||||||
|
lc1 = cspec.to_lightcurve()
|
||||||
|
self.assertAlmostEqual(lc1.range[0], self.trange[0]-self.trigtime, places=6)
|
||||||
|
self.assertAlmostEqual(lc1.range[1], self.trange[1]-self.trigtime, places=6)
|
||||||
|
self.assertEqual(lc1.size, 1994)
|
||||||
|
|
||||||
|
lc2 = cspec.to_lightcurve(time_range=(-100.0, 100.0))
|
||||||
|
self.assertAlmostEqual(lc2.range[0], 356223460.77972996-self.trigtime, places=6)
|
||||||
|
self.assertAlmostEqual(lc2.range[1], 356223661.48687-self.trigtime, places=6)
|
||||||
|
self.assertEqual(lc2.size, 122)
|
||||||
|
|
||||||
|
lc3 = cspec.to_lightcurve(energy_range=(500.0, 3000.0))
|
||||||
|
self.assertAlmostEqual(lc3.range[0], self.trange[0]-self.trigtime, places=6)
|
||||||
|
self.assertAlmostEqual(lc3.range[1], self.trange[1]-self.trigtime, places=6)
|
||||||
|
self.assertEqual(lc3.size, 1994)
|
||||||
|
|
||||||
|
lc4 = cspec.to_lightcurve(channel_range=(50, 80))
|
||||||
|
self.assertAlmostEqual(lc4.range[0], self.trange[0]-self.trigtime, places=6)
|
||||||
|
self.assertAlmostEqual(lc4.range[1], self.trange[1]-self.trigtime, places=6)
|
||||||
|
self.assertEqual(lc4.size, 1994)
|
||||||
|
|
||||||
|
def test_to_spectrum(self):
|
||||||
|
cspec = Cspec.open(self.filename)
|
||||||
|
lc1 = cspec.to_spectrum()
|
||||||
|
self.assertAlmostEqual(lc1.range[0], self.erange[0], places=4)
|
||||||
|
self.assertAlmostEqual(lc1.range[1], self.erange[1], places=4)
|
||||||
|
self.assertEqual(lc1.size, self.numchans)
|
||||||
|
|
||||||
|
lc2 = cspec.to_spectrum(time_range=(-100.0, 100.0))
|
||||||
|
self.assertAlmostEqual(lc2.range[0], self.erange[0], places=4)
|
||||||
|
self.assertAlmostEqual(lc2.range[1], self.erange[1], places=4)
|
||||||
|
self.assertEqual(lc2.size, self.numchans)
|
||||||
|
|
||||||
|
lc3 = cspec.to_spectrum(energy_range=(500.0, 3000.0))
|
||||||
|
self.assertAlmostEqual(lc3.range[0], 488.1802, places=3)
|
||||||
|
self.assertAlmostEqual(lc3.range[1], 3087.304, places=3)
|
||||||
|
self.assertEqual(lc3.size, 43)
|
||||||
|
|
||||||
|
lc4 = cspec.to_spectrum(channel_range=(50, 80))
|
||||||
|
self.assertAlmostEqual(lc4.range[0], 2894.153, places=3)
|
||||||
|
self.assertAlmostEqual(lc4.range[1], 7464.066, places=3)
|
||||||
|
self.assertEqual(lc4.size, 31)
|
||||||
|
|
||||||
|
def test_to_pha(self):
|
||||||
|
cspec = Cspec.open(self.filename)
|
||||||
|
pha1 = cspec.to_pha()
|
||||||
|
self.assertCountEqual(pha1.time_range, cspec.time_range)
|
||||||
|
self.assertCountEqual(pha1.gti[0], cspec.time_range)
|
||||||
|
self.assertEqual(pha1.trigtime, cspec.trigtime)
|
||||||
|
self.assertAlmostEqual(pha1.energy_range[0], self.erange[0], places=4)
|
||||||
|
self.assertAlmostEqual(pha1.energy_range[1], self.erange[1], places=4)
|
||||||
|
self.assertEqual(pha1.numchans, self.numchans)
|
||||||
|
self.assertCountEqual(pha1.valid_channels, np.arange(self.numchans))
|
||||||
|
self.assertAlmostEqual(pha1.exposure, np.sum(cspec.data.exposure), places=5)
|
||||||
|
|
||||||
|
pha2 = cspec.to_pha(channel_range=(50,80))
|
||||||
|
self.assertAlmostEqual(pha2.energy_range[0], self.erange[0], places=4)
|
||||||
|
self.assertAlmostEqual(pha2.energy_range[1], self.erange[1], places=4)
|
||||||
|
self.assertEqual(pha2.numchans, self.numchans)
|
||||||
|
self.assertCountEqual(pha2.valid_channels, np.arange(50, 81))
|
||||||
|
|
||||||
|
pha3 = cspec.to_pha(time_ranges=[(0.0, 10.0), (20.0, 30.0)])
|
||||||
|
self.assertAlmostEqual(pha3.time_range[0], -2.048, places=1)
|
||||||
|
self.assertAlmostEqual(pha3.time_range[1], 31.0, places=0)
|
||||||
|
self.assertAlmostEqual(pha3.exposure, 23.3, places=0)
|
||||||
|
|
||||||
|
def test_write(self):
|
||||||
|
cspec = Cspec.open(self.filename)
|
||||||
|
new_file = os.path.join(data_dir, 'glg_cspec_b0_bn120415958_v01.pha')
|
||||||
|
cspec.write(os.path.dirname(self.filename),
|
||||||
|
filename='glg_cspec_b0_bn120415958_v01.pha')
|
||||||
|
cspec2 = Cspec.open(new_file)
|
||||||
|
os.remove(new_file)
|
||||||
|
|
||||||
|
self.assertCountEqual(cspec2.data.tstart, cspec.data.tstart)
|
||||||
|
self.assertCountEqual(cspec2.data.tstop, cspec.data.tstop)
|
||||||
|
self.assertCountEqual(cspec2.data.emin, cspec.data.emin)
|
||||||
|
self.assertCountEqual(cspec2.data.emax, cspec.data.emax)
|
||||||
|
self.assertCountEqual(cspec2.data.exposure, cspec.data.exposure)
|
||||||
|
for i in range(cspec.numchans):
|
||||||
|
self.assertCountEqual(cspec2.data.counts[:,i],
|
||||||
|
cspec.data.counts[:,i])
|
||||||
|
|
||||||
|
def test_slice_time(self):
|
||||||
|
cspec = Cspec.open(self.filename)
|
||||||
|
cspec2 = cspec.slice_time((-10.0, 10.0))
|
||||||
|
self.assertEqual(cspec2.gti, [cspec2.time_range])
|
||||||
|
self.assertEqual(cspec2.data.numtimes, 12)
|
||||||
|
|
||||||
|
cspec3 = cspec.slice_time([(-10.0, 10.0), (20.0, 30.0)])
|
||||||
|
self.assertEqual(len(cspec3.gti), 2)
|
||||||
|
self.assertEqual(cspec3.data.numtimes, 23)
|
||||||
|
|
||||||
|
def test_slice_energy(self):
|
||||||
|
cspec = Cspec.open(self.filename)
|
||||||
|
cspec2 = cspec.slice_energy((300.0, 1000.0))
|
||||||
|
self.assertAlmostEqual(cspec2.energy_range[0], 300.0, delta=50.0)
|
||||||
|
self.assertAlmostEqual(cspec2.energy_range[1], 1000.0, delta=50.0)
|
||||||
|
self.assertEqual(cspec2.data.numchans, 19)
|
||||||
|
|
||||||
|
cspec3 = cspec.slice_energy([(300.0, 1000.0), (5000.0, 10000.0)])
|
||||||
|
self.assertAlmostEqual(cspec3.energy_range[0], 300.0, delta=50.0)
|
||||||
|
self.assertAlmostEqual(cspec3.energy_range[1], 10000.0, delta=200.0)
|
||||||
|
self.assertEqual(cspec3.data.numchans, 43)
|
||||||
|
|
||||||
|
def test_rebin_time(self):
|
||||||
|
cspec = Cspec.open(self.filename)
|
||||||
|
cspec2 = cspec.rebin_time(combine_by_factor, 4)
|
||||||
|
self.assertAlmostEqual(cspec2.gti[0][0], cspec.gti[0][0], delta=1.0)
|
||||||
|
self.assertAlmostEqual(cspec2.gti[-1][1], cspec.gti[-1][1], delta=10.0)
|
||||||
|
self.assertEqual(cspec2.data.numtimes, 497)
|
||||||
|
|
||||||
|
cspec3 = cspec.rebin_time(combine_by_factor, 4, time_range=(-100.0, 100.0))
|
||||||
|
self.assertEqual(cspec3.data.numtimes, 1904)
|
||||||
|
|
||||||
|
def test_rebin_energy(self):
|
||||||
|
cspec = Cspec.open(self.filename)
|
||||||
|
cspec2 = cspec.rebin_energy(combine_by_factor, 4)
|
||||||
|
self.assertEqual(cspec2.energy_range, cspec.energy_range)
|
||||||
|
self.assertEqual(cspec2.data.numchans, 32)
|
||||||
|
|
||||||
|
cspec3 = cspec.rebin_energy(combine_by_factor, 4, energy_range=(500., 2000.0))
|
||||||
|
self.assertEqual(cspec3.energy_range, cspec.energy_range)
|
||||||
|
self.assertEqual(cspec3.data.numchans, 107)
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
with self.assertRaises(IOError):
|
||||||
|
cspec = Cspec.open('michaelscott.pha')
|
||||||
|
|
||||||
|
cspec = Ctime.open(self.filename)
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
cspec.to_lightcurve(time_range=(100.0, 10.0))
|
||||||
|
cspec.to_lightcurve(energy_range=(30.0, 10.0))
|
||||||
|
cspec.to_lightcurve(channel_range=(30, 10))
|
||||||
|
cspec.to_spectrum(time_range=(100.0, 10.0))
|
||||||
|
cspec.to_spectrum(energy_range=(30.0, 10.0))
|
||||||
|
cspec.to_spectrum(channel_range=(30, 10))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
125
test/test_poshist.py
Normal file
125
test/test_poshist.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from gbm.data.poshist import PosHist
|
||||||
|
|
||||||
|
data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data')
|
||||||
|
|
||||||
|
class TestPosHist(TestCase):
|
||||||
|
filename = os.path.join(data_dir, 'glg_poshist_all_170101_v00.fit')
|
||||||
|
tstart = 504921540.740104
|
||||||
|
tstop = 505008061.340078
|
||||||
|
test_time = 504931642.739272 #trigger 170101.116
|
||||||
|
def test_attributes(self):
|
||||||
|
p = PosHist.open(self.filename)
|
||||||
|
self.assertCountEqual(p.headers.keys(), ['PRIMARY', 'GLAST POS HIST'])
|
||||||
|
self.assertCountEqual(p.time_range, (self.tstart, self.tstop))
|
||||||
|
self.assertEqual(len(p.gti), 10)
|
||||||
|
#mark TODO: Need to figure out what to do for daily poshist flags
|
||||||
|
# The flags are incorrect in the file: The Sun is never visible
|
||||||
|
|
||||||
|
def test_interpolators(self):
|
||||||
|
test_eic = np.array([-1572., 6370., 2164.])
|
||||||
|
test_quat = np.array([0.2229096, 0.06231983, 0.5392869, -0.8096896])
|
||||||
|
test_lat, test_lon = 18.23, 321.02
|
||||||
|
test_alt = np.sqrt(np.sum(test_eic**2))-6371.0 # simple altitude calc
|
||||||
|
test_vel = np.array([-6820.45, -2465.3848, 2270.4202])/1000.0
|
||||||
|
test_angvel = np.array([0.000706, 6.29781e-06, -0.000714])
|
||||||
|
test_geoloc = np.array([283.85, -18.25])
|
||||||
|
test_earth_radius = 67.335
|
||||||
|
test_mcilwain = 1.22
|
||||||
|
|
||||||
|
p = PosHist.open(self.filename)
|
||||||
|
|
||||||
|
eic = p.get_eic(self.test_time)/1000.0
|
||||||
|
[self.assertAlmostEqual(eic[i], test_eic[i], delta=1.0) for i in range(3)]
|
||||||
|
quat = p.get_quaternions(self.test_time)
|
||||||
|
[self.assertAlmostEqual(quat[i], test_quat[i], places=4) for i in range(4)]
|
||||||
|
|
||||||
|
lat = p.get_latitude(self.test_time)
|
||||||
|
lon = p.get_longitude(self.test_time)
|
||||||
|
alt = p.get_altitude(self.test_time)/1000.0
|
||||||
|
self.assertAlmostEqual(lat, test_lat, delta=0.1)
|
||||||
|
self.assertAlmostEqual(lon, test_lon, delta=0.1)
|
||||||
|
self.assertAlmostEqual(alt, test_alt, delta=5.0)
|
||||||
|
|
||||||
|
vel = p.get_velocity(self.test_time)
|
||||||
|
[self.assertAlmostEqual(vel[i]/1000.0, test_vel[i], delta=0.01) for i in range(3)]
|
||||||
|
angvel = p.get_angular_velocity(self.test_time)
|
||||||
|
[self.assertAlmostEqual(angvel[i], test_angvel[i], places=5) for i in range(3)]
|
||||||
|
|
||||||
|
geo_radec = p.get_geocenter_radec(self.test_time)
|
||||||
|
self.assertAlmostEqual(geo_radec[0], test_geoloc[0], delta=0.1)
|
||||||
|
self.assertAlmostEqual(geo_radec[1], test_geoloc[1], delta=0.1)
|
||||||
|
geo_radius = p.get_earth_radius(self.test_time)
|
||||||
|
self.assertAlmostEqual(geo_radius, test_earth_radius, delta=0.01)
|
||||||
|
|
||||||
|
sun_visible = p.get_sun_visibility(self.test_time)
|
||||||
|
#self.assertEqual(sun_visible, False)
|
||||||
|
in_saa = p.get_saa_passage(self.test_time)
|
||||||
|
self.assertEqual(in_saa, False)
|
||||||
|
ml = p.get_mcilwain_l(self.test_time)
|
||||||
|
self.assertAlmostEqual(ml, test_mcilwain, delta=0.01)
|
||||||
|
|
||||||
|
def test_coordinate_conversions(self):
|
||||||
|
test_ra, test_dec = 70.64, -1.58
|
||||||
|
test_az, test_zen = 138.0, 65.0
|
||||||
|
test_n0_ra, test_n0_dec = 30.908, 57.679
|
||||||
|
test_n0_angle = 67.42
|
||||||
|
|
||||||
|
p = PosHist.open(self.filename)
|
||||||
|
|
||||||
|
az, zen = p.to_fermi_frame(test_ra, test_dec, self.test_time)
|
||||||
|
self.assertAlmostEqual(az, test_az, delta=1.0)
|
||||||
|
self.assertAlmostEqual(zen, test_zen, delta=1.0)
|
||||||
|
|
||||||
|
ra, dec = p.to_equatorial(test_az, test_zen, self.test_time)
|
||||||
|
self.assertAlmostEqual(ra, test_ra, delta=1.0)
|
||||||
|
self.assertAlmostEqual(dec, test_dec, delta=1.0)
|
||||||
|
|
||||||
|
loc_vis = p.location_visible(test_ra, test_dec, self.test_time)
|
||||||
|
self.assertEqual(loc_vis, True)
|
||||||
|
|
||||||
|
ra, dec = p.detector_pointing('n0', self.test_time)
|
||||||
|
self.assertAlmostEqual(ra, test_n0_ra, delta=0.5)
|
||||||
|
self.assertAlmostEqual(dec, test_n0_dec, delta=0.5)
|
||||||
|
angle = p.detector_angle(test_ra, test_dec, 'n0', self.test_time)
|
||||||
|
self.assertAlmostEqual(angle, test_n0_angle, delta=0.5)
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
with self.assertRaises(IOError):
|
||||||
|
p = PosHist.open('roland.fit')
|
||||||
|
|
||||||
|
p = PosHist.open(self.filename)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
p.get_eic(1.0)
|
||||||
|
p.detector_pointing('n11', 1.0)
|
||||||
|
p.detector_angle(0.0, 0.0, 'a1', 1.0)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
582
test/test_primitives.py
Normal file
582
test/test_primitives.py
Normal file
@ -0,0 +1,582 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import unittest
|
||||||
|
import numpy as np
|
||||||
|
from gbm.data.primitives import EventList, TimeBins, EnergyBins, TimeEnergyBins
|
||||||
|
from gbm.data.primitives import TimeRange, GTI
|
||||||
|
from gbm.binning.binned import combine_by_factor
|
||||||
|
from gbm.binning.unbinned import bin_by_time
|
||||||
|
|
||||||
|
class TestEventList(unittest.TestCase):
|
||||||
|
events = [(1.4, 1), (1.5, 0), (2.3, 0), (2.4, 1), (3.9, 0),
|
||||||
|
(6.8, 0), (7.6, 0), (7.7, 2), (8.6, 1), (9.8, 3)]
|
||||||
|
events = np.array(events, dtype=[('TIME', '>f8'), ('PHA', '>i2')])
|
||||||
|
ebounds = [(0, 4.2, 5.2), (1, 5.2, 6.1), (2, 6.1, 7.0), (3, 7.0, 7.8)]
|
||||||
|
ebounds = np.array(ebounds, dtype=[('CHANNEL', '>i2'), ('E_MIN', '>f4'), ('E_MAX', '>f4')])
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
el = EventList.from_fits_array(self.events, self.ebounds)
|
||||||
|
self.assertEqual(el.size, 10)
|
||||||
|
self.assertCountEqual(el.time, self.events['TIME'])
|
||||||
|
self.assertCountEqual(el.pha, self.events['PHA'])
|
||||||
|
self.assertCountEqual(el.emin, self.ebounds['E_MIN'])
|
||||||
|
self.assertCountEqual(el.emax, self.ebounds['E_MAX'])
|
||||||
|
self.assertEqual(el.numchans, 4)
|
||||||
|
self.assertEqual(el.time_range, (self.events['TIME'][0], self.events['TIME'][-1]))
|
||||||
|
self.assertEqual(el.channel_range, (self.ebounds['CHANNEL'][0], self.ebounds['CHANNEL'][-1]))
|
||||||
|
self.assertEqual(el.energy_range, (self.ebounds['E_MIN'][0], self.ebounds['E_MAX'][-1]))
|
||||||
|
self.assertAlmostEqual(el.get_exposure(), 8.39997, places=4)
|
||||||
|
self.assertIsInstance(el.count_spectrum, EnergyBins)
|
||||||
|
|
||||||
|
def test_channel_slice(self):
|
||||||
|
el = EventList.from_fits_array(self.events, self.ebounds)
|
||||||
|
el2 = el.channel_slice(0, 1)
|
||||||
|
self.assertEqual(el2.size, 8)
|
||||||
|
self.assertEqual(el2.time_range, (1.4, 8.6))
|
||||||
|
|
||||||
|
def test_count_spectrum(self):
|
||||||
|
el = EventList.from_fits_array(self.events, self.ebounds)
|
||||||
|
bins = el.count_spectrum
|
||||||
|
self.assertEqual(bins.size, 4)
|
||||||
|
self.assertCountEqual(bins.counts, np.array([5,3,1,1]))
|
||||||
|
|
||||||
|
def test_energy_slice(self):
|
||||||
|
el = EventList.from_fits_array(self.events, self.ebounds)
|
||||||
|
el2 = el.energy_slice(5.0, 6.2)
|
||||||
|
self.assertEqual(el2.size, 9)
|
||||||
|
self.assertEqual(el2.time_range, (1.4, 8.6))
|
||||||
|
|
||||||
|
def test_time_slice(self):
|
||||||
|
el = EventList.from_fits_array(self.events, self.ebounds)
|
||||||
|
el2 = el.time_slice(1.0, 3.0)
|
||||||
|
self.assertEqual(el2.size, 4)
|
||||||
|
self.assertEqual(el2.time_range, (1.4, 2.4))
|
||||||
|
|
||||||
|
def test_sort(self):
|
||||||
|
el = EventList.from_fits_array(self.events, self.ebounds)
|
||||||
|
el.sort('PHA')
|
||||||
|
self.assertEqual(el.pha[0], 0)
|
||||||
|
|
||||||
|
def test_bin_time(self):
|
||||||
|
el = EventList.from_fits_array(self.events, self.ebounds)
|
||||||
|
bins1 = el.bin(bin_by_time, 1.0)
|
||||||
|
bins2 = el.bin(bin_by_time, 1.0, tstart=2.0, tstop=8.0)
|
||||||
|
self.assertEqual(bins1.size, (9, 4))
|
||||||
|
self.assertEqual(bins2.size, (6, 4))
|
||||||
|
|
||||||
|
def test_rebin_energy(self):
|
||||||
|
el = EventList.from_fits_array(self.events, self.ebounds)
|
||||||
|
el2 = el.rebin_energy(combine_by_factor, 2)
|
||||||
|
self.assertEqual(el2.numchans, 2)
|
||||||
|
|
||||||
|
def test_merge(self):
|
||||||
|
el = EventList.from_fits_array(self.events, self.ebounds)
|
||||||
|
el1 = el.time_slice(1.0, 5.0)
|
||||||
|
el2 = el.time_slice(5.0, 10.0)
|
||||||
|
el3 = EventList.merge([el1, el2], sort_attrib='TIME')
|
||||||
|
self.assertEqual(el3.size, el.size)
|
||||||
|
self.assertCountEqual(el3.time, el.time)
|
||||||
|
|
||||||
|
def test_exposure(self):
|
||||||
|
el = EventList.from_fits_array(self.events, self.ebounds)
|
||||||
|
exposure = el.get_exposure()
|
||||||
|
self.assertAlmostEqual(exposure, (9.8-1.4)-(10.0*2.6e-6), places=5)
|
||||||
|
exposure = el.get_exposure(time_ranges=(0.0, 5.0))
|
||||||
|
self.assertAlmostEqual(exposure, (5.0)-(5.0*2.6e-6), places=5)
|
||||||
|
exposure = el.get_exposure(time_ranges=[(0.0, 2.0), (5.0, 9.0)])
|
||||||
|
self.assertAlmostEqual(exposure, (2.0+4.0)-(6.0*2.6e-6))
|
||||||
|
|
||||||
|
def test_from_lists(self):
|
||||||
|
times = [1.4, 1.5, 2.3, 2.4, 3.9, 6.8, 7.6, 7.7, 8.6, 9.8]
|
||||||
|
phas = [1,0,0,1,0,0,0,2,1,3]
|
||||||
|
chan_lo = [4.2, 5.2, 6.1, 7.0]
|
||||||
|
chan_hi = [5.2, 6.1, 7.0, 7.8]
|
||||||
|
el = EventList.from_lists(times, phas, chan_lo, chan_hi)
|
||||||
|
el2 = EventList.from_fits_array(self.events, self.ebounds)
|
||||||
|
self.assertCountEqual(el.time, el2.time)
|
||||||
|
self.assertCountEqual(el.pha, el2.pha)
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
el = EventList.from_fits_array(self.events, self.ebounds)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
el.sort('PIGLET')
|
||||||
|
|
||||||
|
|
||||||
|
class TestTimeBins(unittest.TestCase):
|
||||||
|
counts = np.array([113, 94, 103, 100, 98, 115, 101, 86, 104, 102])
|
||||||
|
tstart = np.array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])
|
||||||
|
tstop = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0])
|
||||||
|
exposure = np.array([0.99971, 0.99976, 0.99973, 0.99974, 0.99975,
|
||||||
|
0.99970, 0.99973, 0.99978, 0.99973, 0.99973])
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
bins = TimeBins(self.counts, self.tstart, self.tstop, self.exposure)
|
||||||
|
self.assertCountEqual(bins.centroids, (self.tstart+self.tstop)/2.0)
|
||||||
|
self.assertCountEqual(bins.counts, self.counts)
|
||||||
|
self.assertCountEqual(bins.count_uncertainty, np.sqrt(self.counts))
|
||||||
|
self.assertCountEqual(bins.exposure, self.exposure)
|
||||||
|
self.assertCountEqual(bins.hi_edges, self.tstop)
|
||||||
|
self.assertCountEqual(bins.lo_edges, self.tstart)
|
||||||
|
self.assertEqual(bins.range, (0.0, 10.0))
|
||||||
|
self.assertCountEqual(bins.rates, self.counts/self.exposure)
|
||||||
|
self.assertCountEqual(bins.rate_uncertainty, np.sqrt(self.counts)/self.exposure)
|
||||||
|
self.assertEqual(bins.size, 10)
|
||||||
|
self.assertCountEqual(bins.widths, self.tstop-self.tstart)
|
||||||
|
|
||||||
|
def test_closest_edge(self):
|
||||||
|
bins = TimeBins(self.counts, self.tstart, self.tstop, self.exposure)
|
||||||
|
self.assertEqual(bins.closest_edge(1.3), self.tstart[1])
|
||||||
|
self.assertEqual(bins.closest_edge(1.3, which='high'), self.tstart[2])
|
||||||
|
self.assertEqual(bins.closest_edge(1.7, which='low'), self.tstart[1])
|
||||||
|
|
||||||
|
self.assertEqual(bins.closest_edge(-1.0), self.tstart[0])
|
||||||
|
self.assertEqual(bins.closest_edge(-1.0, which='low'), self.tstart[0])
|
||||||
|
self.assertEqual(bins.closest_edge(-1.0, which='high'), self.tstart[0])
|
||||||
|
|
||||||
|
self.assertEqual(bins.closest_edge(11.0), self.tstop[-1])
|
||||||
|
self.assertEqual(bins.closest_edge(11.0, which='low'), self.tstop[-1])
|
||||||
|
self.assertEqual(bins.closest_edge(11.0, which='high'), self.tstop[-1])
|
||||||
|
|
||||||
|
def test_slice(self):
|
||||||
|
bins = TimeBins(self.counts, self.tstart, self.tstop, self.exposure)
|
||||||
|
bins2 = bins.slice(2.5, 7.5)
|
||||||
|
self.assertEqual(bins2.size, 6)
|
||||||
|
self.assertCountEqual(bins2.counts, self.counts[2:8])
|
||||||
|
self.assertEqual(bins2.range, (2.0, 8.0))
|
||||||
|
|
||||||
|
def test_rebin(self):
|
||||||
|
bins = TimeBins(self.counts, self.tstart, self.tstop, self.exposure)
|
||||||
|
bins2 = bins.rebin(combine_by_factor, 2)
|
||||||
|
bins3 = bins.rebin(combine_by_factor, 2, tstart=2.5, tstop=7.5)
|
||||||
|
self.assertEqual(bins2.size, 5)
|
||||||
|
self.assertCountEqual(bins2.counts, np.sum(self.counts.reshape(-1,2), axis=1))
|
||||||
|
self.assertCountEqual(bins2.exposure, np.sum(self.exposure.reshape(-1,2), axis=1))
|
||||||
|
self.assertEqual(bins3.size, 8)
|
||||||
|
|
||||||
|
def test_merge(self):
|
||||||
|
bins = TimeBins(self.counts, self.tstart, self.tstop, self.exposure)
|
||||||
|
bins1 = bins.slice(0.1, 4.9)
|
||||||
|
bins2 = bins.slice(5.1, 9.9)
|
||||||
|
bins3 = TimeBins.merge([bins1, bins2])
|
||||||
|
self.assertCountEqual(bins3.counts, bins.counts)
|
||||||
|
self.assertCountEqual(bins3.exposure, bins.exposure)
|
||||||
|
self.assertCountEqual(bins3.lo_edges, bins.lo_edges)
|
||||||
|
self.assertCountEqual(bins3.hi_edges, bins.hi_edges)
|
||||||
|
|
||||||
|
def test_sum(self):
|
||||||
|
bins1 = TimeBins(self.counts, self.tstart, self.tstop, self.exposure)
|
||||||
|
bins2 = TimeBins(self.counts, self.tstart, self.tstop, self.exposure)
|
||||||
|
bins3 = TimeBins.sum([bins1, bins2])
|
||||||
|
self.assertCountEqual(bins3.counts, self.counts*2)
|
||||||
|
self.assertCountEqual(bins3.exposure, self.exposure)
|
||||||
|
|
||||||
|
def test_contiguous(self):
|
||||||
|
tstart = np.array([0.0, 1.0, 2.0, 3.0, 4.0, 5.5, 6.0, 7.0, 8.0, 9.0])
|
||||||
|
exposure = np.copy(self.exposure)
|
||||||
|
exposure[5] = 0.4998
|
||||||
|
bins = TimeBins(self.counts, tstart, self.tstop, exposure)
|
||||||
|
split_bins = bins.contiguous_bins()
|
||||||
|
self.assertEqual(len(split_bins), 2)
|
||||||
|
self.assertEqual(split_bins[0].range, (0.0, 5.0))
|
||||||
|
self.assertEqual(split_bins[1].range, (5.5, 10.0))
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
bins = TimeBins(self.counts, self.tstart, self.tstop, self.exposure)
|
||||||
|
bins2 = TimeBins(self.counts[1:], self.tstart[1:], self.tstop[1:], self.exposure[1:])
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
bins.exposure = 1.0
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
bins.counts = 1.0
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
bins.lo_edges = 1.0
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
bins.hi_edges = 1.0
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
TimeBins.sum([bins, bins2])
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnergyBins(unittest.TestCase):
|
||||||
|
counts = np.array([113, 94, 103, 100, 98, 115, 101, 86, 104, 102])
|
||||||
|
emin = np.array([4.2, 5.2, 6.1, 7.0, 7.8, 8.7, 9.5, 10.4, 11.4, 12.4])
|
||||||
|
emax = np.array([5.2, 6.1, 7.0, 7.8, 8.7, 9.5, 10.4, 11.4, 12.4, 13.5])
|
||||||
|
exposure = np.full(10, 9.9)
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
bins = EnergyBins(self.counts, self.emin, self.emax, self.exposure)
|
||||||
|
self.assertCountEqual(bins.centroids, np.sqrt(self.emin*self.emax))
|
||||||
|
self.assertCountEqual(bins.counts, self.counts)
|
||||||
|
self.assertCountEqual(bins.count_uncertainty, np.sqrt(self.counts))
|
||||||
|
self.assertCountEqual(bins.exposure, self.exposure)
|
||||||
|
self.assertCountEqual(bins.hi_edges, self.emax)
|
||||||
|
self.assertCountEqual(bins.lo_edges, self.emin)
|
||||||
|
self.assertEqual(bins.range, (4.2, 13.5))
|
||||||
|
self.assertCountEqual(bins.rates, self.counts/(self.exposure*(self.emax-self.emin)))
|
||||||
|
self.assertCountEqual(bins.rate_uncertainty, np.sqrt(self.counts)/(self.exposure*(self.emax-self.emin)))
|
||||||
|
self.assertEqual(bins.size, 10)
|
||||||
|
self.assertCountEqual(bins.widths, self.emax-self.emin)
|
||||||
|
|
||||||
|
def test_slice(self):
|
||||||
|
bins = EnergyBins(self.counts, self.emin, self.emax, self.exposure)
|
||||||
|
bins2 = bins.slice(5.5, 10.5)
|
||||||
|
self.assertEqual(bins2.size, 7)
|
||||||
|
self.assertCountEqual(bins2.counts, self.counts[1:8])
|
||||||
|
self.assertEqual(bins2.range, (5.2, 11.4))
|
||||||
|
|
||||||
|
def test_rebin(self):
|
||||||
|
bins = EnergyBins(self.counts, self.emin, self.emax, self.exposure)
|
||||||
|
bins2 = bins.rebin(combine_by_factor, 2)
|
||||||
|
bins3 = bins.rebin(combine_by_factor, 2, emin=5.5, emax=9.7)
|
||||||
|
self.assertEqual(bins2.size, 5)
|
||||||
|
self.assertCountEqual(bins2.counts, np.sum(self.counts.reshape(-1,2), axis=1))
|
||||||
|
self.assertCountEqual(bins2.exposure, self.exposure[:5])
|
||||||
|
self.assertEqual(bins3.size, 8)
|
||||||
|
|
||||||
|
def test_merge(self):
|
||||||
|
bins = EnergyBins(self.counts, self.emin, self.emax, self.exposure)
|
||||||
|
bins1 = bins.slice(4.5, 8.0)
|
||||||
|
bins2 = bins.slice(9.0, 13.4)
|
||||||
|
bins3 = EnergyBins.merge([bins1, bins2])
|
||||||
|
self.assertCountEqual(bins3.counts, bins.counts)
|
||||||
|
self.assertCountEqual(bins3.exposure, bins.exposure)
|
||||||
|
self.assertCountEqual(bins3.lo_edges, bins.lo_edges)
|
||||||
|
self.assertCountEqual(bins3.hi_edges, bins.hi_edges)
|
||||||
|
|
||||||
|
def test_sum(self):
|
||||||
|
bins1 = EnergyBins(self.counts, self.emin, self.emax, self.exposure)
|
||||||
|
bins2 = EnergyBins(self.counts, self.emin, self.emax, self.exposure)
|
||||||
|
bins3 = EnergyBins.sum([bins1, bins2])
|
||||||
|
self.assertCountEqual(bins3.counts, self.counts*2)
|
||||||
|
self.assertCountEqual(bins3.exposure, self.exposure*2)
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
bins = EnergyBins(self.counts, self.emin, self.emax, self.exposure)
|
||||||
|
bins2 = EnergyBins(self.counts[1:], self.emin[1:], self.emax[1:], self.exposure[1:])
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
bins.exposure = 1.0
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
bins.counts = 1.0
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
bins.lo_edges = 1.0
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
bins.hi_edges = 1.0
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
EnergyBins.sum([bins, bins2])
|
||||||
|
|
||||||
|
|
||||||
|
class TestTimeEnergyBins(unittest.TestCase):
|
||||||
|
counts = np.array([[113, 94, 103], [100, 98, 115], [101, 86, 104]])
|
||||||
|
tstart = np.array([1.0, 2.0, 3.0])
|
||||||
|
tstop = np.array([2.0, 3.0, 4.0])
|
||||||
|
exposure = np.array([0.99971, 0.99976, 0.99973])
|
||||||
|
emin = np.array([4.2, 5.2, 6.1])
|
||||||
|
emax = np.array([5.2, 6.1, 7.0])
|
||||||
|
chan_widths = (emax-emin)
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
bins = TimeEnergyBins(self.counts, self.tstart, self.tstop, self.exposure,
|
||||||
|
self.emin, self.emax)
|
||||||
|
self.assertCountEqual(bins.chan_widths, self.emax-self.emin)
|
||||||
|
self.assertCountEqual(bins.emax, self.emax)
|
||||||
|
self.assertCountEqual(bins.emin, self.emin)
|
||||||
|
self.assertCountEqual(bins.energy_centroids, np.sqrt(self.emax*self.emin))
|
||||||
|
self.assertEqual(bins.energy_range, (4.2, 7.0))
|
||||||
|
self.assertCountEqual(bins.exposure, self.exposure)
|
||||||
|
self.assertEqual(bins.numchans, 3)
|
||||||
|
self.assertEqual(bins.numtimes, 3)
|
||||||
|
self.assertEqual(bins.size, (3,3))
|
||||||
|
self.assertCountEqual(bins.time_centroids, (self.tstart+self.tstop)/2.0)
|
||||||
|
self.assertEqual(bins.time_range, (1.0, 4.0))
|
||||||
|
self.assertCountEqual(bins.time_widths, self.tstop-self.tstart)
|
||||||
|
self.assertCountEqual(bins.tstart, self.tstart)
|
||||||
|
self.assertCountEqual(bins.tstop, self.tstop)
|
||||||
|
for i in range(3):
|
||||||
|
self.assertCountEqual(bins.counts[:,i], self.counts[:,i])
|
||||||
|
self.assertCountEqual(bins.count_uncertainty[:,i], np.sqrt(self.counts[:,i]))
|
||||||
|
self.assertCountEqual(bins.rates[:,i],
|
||||||
|
self.counts[:,i]/self.exposure)
|
||||||
|
self.assertCountEqual(bins.rate_uncertainty[:,i],
|
||||||
|
np.sqrt(self.counts[:,i])/self.exposure)
|
||||||
|
|
||||||
|
def test_integrate_energy(self):
|
||||||
|
bins = TimeEnergyBins(self.counts, self.tstart, self.tstop, self.exposure,
|
||||||
|
self.emin, self.emax)
|
||||||
|
lc1 = bins.integrate_energy()
|
||||||
|
lc2 = bins.integrate_energy(emin=4.5, emax=5.5)
|
||||||
|
self.assertCountEqual(lc1.counts, np.sum(self.counts, axis=1))
|
||||||
|
self.assertCountEqual(lc2.counts, np.sum(self.counts[:,:-1], axis=1))
|
||||||
|
|
||||||
|
def test_integrate_time(self):
|
||||||
|
bins = TimeEnergyBins(self.counts, self.tstart, self.tstop, self.exposure,
|
||||||
|
self.emin, self.emax)
|
||||||
|
spec1 = bins.integrate_time()
|
||||||
|
spec2 = bins.integrate_time(tstart=1.5, tstop=2.5)
|
||||||
|
self.assertCountEqual(spec1.counts, np.sum(self.counts, axis=0))
|
||||||
|
self.assertCountEqual(spec2.counts, np.sum(self.counts[:-1,:], axis=0))
|
||||||
|
|
||||||
|
def test_rebin_energy(self):
|
||||||
|
bins = TimeEnergyBins(self.counts, self.tstart, self.tstop, self.exposure,
|
||||||
|
self.emin, self.emax)
|
||||||
|
bins2 = bins.rebin_energy(combine_by_factor, 2)
|
||||||
|
bins3 = bins.rebin_energy(combine_by_factor, 2, emin=4.5, emax=6.0)
|
||||||
|
self.assertEqual(bins2.numchans, 1)
|
||||||
|
self.assertEqual(bins3.numchans, 3)
|
||||||
|
|
||||||
|
def test_rebin_time(self):
|
||||||
|
bins = TimeEnergyBins(self.counts, self.tstart, self.tstop, self.exposure,
|
||||||
|
self.emin, self.emax)
|
||||||
|
bins2 = bins.rebin_time(combine_by_factor, 2)
|
||||||
|
bins3 = bins.rebin_time(combine_by_factor, 2, tstart=1.5, tstop=2.5)
|
||||||
|
self.assertEqual(bins2.numtimes, 1)
|
||||||
|
self.assertEqual(bins3.numtimes, 3)
|
||||||
|
|
||||||
|
def test_slice_energy(self):
|
||||||
|
bins = TimeEnergyBins(self.counts, self.tstart, self.tstop, self.exposure,
|
||||||
|
self.emin, self.emax)
|
||||||
|
bins2 = bins.slice_energy(4.5, 5.5)
|
||||||
|
self.assertEqual(bins2.numchans, 2)
|
||||||
|
self.assertCountEqual(bins2.counts.flatten(), self.counts[:,:2].flatten())
|
||||||
|
self.assertEqual(bins2.energy_range, (4.2, 6.1))
|
||||||
|
|
||||||
|
def test_slice_time(self):
|
||||||
|
bins = TimeEnergyBins(self.counts, self.tstart, self.tstop, self.exposure,
|
||||||
|
self.emin, self.emax)
|
||||||
|
bins2 = bins.slice_time(1.5, 2.5)
|
||||||
|
self.assertEqual(bins2.numtimes, 2)
|
||||||
|
self.assertCountEqual(bins2.counts.flatten(), self.counts[:2,:].flatten())
|
||||||
|
self.assertEqual(bins2.time_range, (1.0, 3.0))
|
||||||
|
|
||||||
|
def test_closest_time(self):
|
||||||
|
bins = TimeEnergyBins(self.counts, self.tstart, self.tstop, self.exposure,
|
||||||
|
self.emin, self.emax)
|
||||||
|
self.assertEqual(bins.closest_time_edge(2.3), 2.0)
|
||||||
|
self.assertEqual(bins.closest_time_edge(2.3, which='low'), 2.0)
|
||||||
|
self.assertEqual(bins.closest_time_edge(2.3, which='high'), 3.0)
|
||||||
|
|
||||||
|
def test_closest_energy(self):
|
||||||
|
bins = TimeEnergyBins(self.counts, self.tstart, self.tstop, self.exposure,
|
||||||
|
self.emin, self.emax)
|
||||||
|
self.assertEqual(bins.closest_energy_edge(5.7), 6.1)
|
||||||
|
self.assertEqual(bins.closest_energy_edge(5.7, which='low'), 5.2)
|
||||||
|
self.assertEqual(bins.closest_energy_edge(5.7, which='high'), 6.1)
|
||||||
|
|
||||||
|
def test_contiguous_time(self):
|
||||||
|
tstart = np.array([1.0, 2.5, 3.0])
|
||||||
|
exposure = np.copy(self.exposure)
|
||||||
|
exposure[1] = 0.4998
|
||||||
|
bins = TimeEnergyBins(self.counts, tstart, self.tstop, exposure,
|
||||||
|
self.emin, self.emax)
|
||||||
|
split_bins = bins.contiguous_time_bins()
|
||||||
|
self.assertEqual(len(split_bins), 2)
|
||||||
|
self.assertEqual(split_bins[0].time_range, (1.0, 2.0))
|
||||||
|
self.assertEqual(split_bins[1].time_range, (2.5, 4.0))
|
||||||
|
|
||||||
|
def test_contiguous_energy(self):
|
||||||
|
emin = np.array([4.2, 5.7, 6.1])
|
||||||
|
bins = TimeEnergyBins(self.counts, self.tstart, self.tstop, self.exposure,
|
||||||
|
emin, self.emax)
|
||||||
|
split_bins = bins.contiguous_energy_bins()
|
||||||
|
self.assertEqual(len(split_bins), 2)
|
||||||
|
self.assertEqual(split_bins[0].energy_range, (4.2, 5.2))
|
||||||
|
self.assertEqual(split_bins[1].energy_range, (5.7, 7.0))
|
||||||
|
|
||||||
|
def test_exposure(self):
|
||||||
|
bins = TimeEnergyBins(self.counts, self.tstart, self.tstop, self.exposure,
|
||||||
|
self.emin, self.emax)
|
||||||
|
self.assertEqual(bins.get_exposure(), self.exposure.sum())
|
||||||
|
self.assertEqual(bins.get_exposure(time_ranges=(1.5, 2.5)),
|
||||||
|
self.exposure[0:2].sum())
|
||||||
|
|
||||||
|
def test_merge_energy(self):
|
||||||
|
bins = TimeEnergyBins(self.counts, self.tstart, self.tstop, self.exposure,
|
||||||
|
self.emin, self.emax)
|
||||||
|
bins1 = bins.slice_energy(4.2, 5.5)
|
||||||
|
bins2 = bins.slice_energy(6.0, 7.0)
|
||||||
|
bins3 = TimeEnergyBins.merge_energy([bins1, bins2])
|
||||||
|
self.assertCountEqual(bins3.counts.flatten(), bins.counts.flatten())
|
||||||
|
self.assertCountEqual(bins3.exposure, bins.exposure)
|
||||||
|
self.assertCountEqual(bins3.tstart, bins.tstart)
|
||||||
|
self.assertCountEqual(bins3.tstop, bins.tstop)
|
||||||
|
self.assertCountEqual(bins3.emin, bins.emin)
|
||||||
|
self.assertCountEqual(bins3.emax, bins.emax)
|
||||||
|
|
||||||
|
def test_merge_time(self):
|
||||||
|
bins = TimeEnergyBins(self.counts, self.tstart, self.tstop, self.exposure,
|
||||||
|
self.emin, self.emax)
|
||||||
|
bins1 = bins.slice_time(1.5, 2.5)
|
||||||
|
bins2 = bins.slice_time(3.1, 4.0)
|
||||||
|
bins3 = TimeEnergyBins.merge_time([bins1, bins2])
|
||||||
|
self.assertCountEqual(bins3.counts.flatten(), bins.counts.flatten())
|
||||||
|
self.assertCountEqual(bins3.exposure, bins.exposure)
|
||||||
|
self.assertCountEqual(bins3.tstart, bins.tstart)
|
||||||
|
self.assertCountEqual(bins3.tstop, bins.tstop)
|
||||||
|
self.assertCountEqual(bins3.emin, bins.emin)
|
||||||
|
self.assertCountEqual(bins3.emax, bins.emax)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTimeRange(unittest.TestCase):
|
||||||
|
tstart = -5.0
|
||||||
|
tstop = 10.0
|
||||||
|
time_range = TimeRange(tstart, tstop)
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
self.assertEqual(self.time_range.tstart, self.tstart)
|
||||||
|
self.assertEqual(self.time_range.tstop, self.tstop)
|
||||||
|
self.assertEqual(self.time_range.duration, 15.0)
|
||||||
|
self.assertEqual(self.time_range.center, 2.5)
|
||||||
|
tstart, tstop = self.time_range.as_tuple()
|
||||||
|
self.assertEqual(tstart, self.tstart)
|
||||||
|
self.assertEqual(tstop, self.tstop)
|
||||||
|
|
||||||
|
def test_contains(self):
|
||||||
|
c = self.time_range.contains(1.0)
|
||||||
|
self.assertTrue(c)
|
||||||
|
c = self.time_range.contains(-10.0)
|
||||||
|
self.assertFalse(c)
|
||||||
|
c = self.time_range.contains(10.0)
|
||||||
|
self.assertTrue(c)
|
||||||
|
c = self.time_range.contains(10.0, inclusive=False)
|
||||||
|
self.assertFalse(c)
|
||||||
|
c = self.time_range.contains(1.0, inclusive=True)
|
||||||
|
self.assertTrue(c)
|
||||||
|
c = self.time_range.contains(-10.0, inclusive=True)
|
||||||
|
self.assertFalse(c)
|
||||||
|
|
||||||
|
def test_union(self):
|
||||||
|
tr = TimeRange.union(self.time_range, TimeRange(5.0, 20.0))
|
||||||
|
self.assertEqual(tr.tstart, -5.0)
|
||||||
|
self.assertEqual(tr.tstop, 20.0)
|
||||||
|
tr = TimeRange.union(self.time_range, TimeRange(-10.0, 0.0))
|
||||||
|
self.assertEqual(tr.tstart, -10.0)
|
||||||
|
self.assertEqual(tr.tstop, 10.0)
|
||||||
|
tr = TimeRange.union(self.time_range, TimeRange(100, 200.0))
|
||||||
|
self.assertEqual(tr.tstart, -5.0)
|
||||||
|
self.assertEqual(tr.tstop, 200.0)
|
||||||
|
tr = TimeRange.union(self.time_range, TimeRange(0.0, 5.0))
|
||||||
|
self.assertEqual(tr.tstart, -5.0)
|
||||||
|
self.assertEqual(tr.tstop, 10.0)
|
||||||
|
tr = TimeRange.union(self.time_range, TimeRange(-100.0, 100.0))
|
||||||
|
self.assertEqual(tr.tstart, -100.0)
|
||||||
|
self.assertEqual(tr.tstop, 100.0)
|
||||||
|
|
||||||
|
def test_intersection(self):
|
||||||
|
tr = TimeRange.intersection(self.time_range, TimeRange(5.0, 20.0))
|
||||||
|
self.assertEqual(tr.tstart, 5.0)
|
||||||
|
self.assertEqual(tr.tstop, 10.0)
|
||||||
|
tr = TimeRange.intersection(self.time_range, TimeRange(-10.0, 0.0))
|
||||||
|
self.assertEqual(tr.tstart, -5.0)
|
||||||
|
self.assertEqual(tr.tstop, 0.0)
|
||||||
|
tr = TimeRange.intersection(self.time_range, TimeRange(1.0, 5.0))
|
||||||
|
self.assertEqual(tr.tstart, 1.0)
|
||||||
|
self.assertEqual(tr.tstop, 5.0)
|
||||||
|
tr = TimeRange.intersection(self.time_range, TimeRange(-100.0, 100.0))
|
||||||
|
self.assertEqual(tr.tstart, -5.0)
|
||||||
|
self.assertEqual(tr.tstop, 10.0)
|
||||||
|
tr = TimeRange.intersection(self.time_range, TimeRange(50.0, 100.0))
|
||||||
|
self.assertIsNone(tr)
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
self.time_range.contains('')
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
tr = TimeRange(1.0, 'pennywise')
|
||||||
|
|
||||||
|
|
||||||
|
class TestGti(unittest.TestCase):
|
||||||
|
time_list = [(1.0, 5.0), (20.0, 30.0), (50.0, 100.0)]
|
||||||
|
gti = GTI.from_list(time_list)
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
self.assertEqual(self.gti.num_intervals, 3)
|
||||||
|
tstart, tstop = self.gti.range
|
||||||
|
self.assertEqual(tstart, 1.0)
|
||||||
|
self.assertEqual(tstop, 100.0)
|
||||||
|
|
||||||
|
def test_as_list(self, gti=None, time_list=None):
|
||||||
|
if gti is None:
|
||||||
|
gti = self.gti
|
||||||
|
if time_list is None:
|
||||||
|
time_list = self.time_list
|
||||||
|
self.assertCountEqual(gti.as_list(), time_list)
|
||||||
|
|
||||||
|
def test_insert(self):
|
||||||
|
gti2 = GTI.from_list(self.time_list[:-1])
|
||||||
|
gti2.insert(*self.time_list[-1])
|
||||||
|
self.test_as_list(gti=gti2)
|
||||||
|
gti2 = GTI.from_list(self.time_list[1:])
|
||||||
|
gti2.insert(*self.time_list[0])
|
||||||
|
self.test_as_list(gti=gti2)
|
||||||
|
gti2 = GTI.from_list([(1.0, 5.0), (50.0, 100.0)])
|
||||||
|
gti2.insert(*self.time_list[1])
|
||||||
|
self.test_as_list(gti=gti2)
|
||||||
|
|
||||||
|
gti2 = GTI.from_list(self.time_list)
|
||||||
|
gti2.insert(2.0, 15.0)
|
||||||
|
self.test_as_list(gti=gti2,
|
||||||
|
time_list=[(1.0, 15.0), (20.0, 30.0), (50.0, 100.0)])
|
||||||
|
gti2 = GTI.from_list(self.time_list)
|
||||||
|
gti2.insert(2.0, 25.0)
|
||||||
|
self.test_as_list(gti=gti2, time_list=[(1.0, 30.0), (50.0, 100.0)])
|
||||||
|
gti2 = GTI.from_list(self.time_list)
|
||||||
|
gti2.insert(-10.0, 2.0)
|
||||||
|
self.test_as_list(gti=gti2,
|
||||||
|
time_list=[(-10.0, 5.0), (20.0, 30.0), (50.0, 100.0)])
|
||||||
|
|
||||||
|
def test_merge(self):
|
||||||
|
gti2 = GTI.from_list([(8.0, 15.0), (150.0, 200.0)])
|
||||||
|
gti3 = GTI.merge(self.gti, gti2)
|
||||||
|
self.test_as_list(gti=gti3, time_list=[(1.0, 5.0), (8.0, 15.0),
|
||||||
|
(20.0, 30.0), (50.0, 100.0),
|
||||||
|
(150.0, 200.0)])
|
||||||
|
|
||||||
|
gti2 = GTI.from_list([(2.0, 15.0), (25.0, 75.0)])
|
||||||
|
gti3 = GTI.merge(self.gti, gti2)
|
||||||
|
self.test_as_list(gti=gti3, time_list=[(1.0, 15.0), (20.0, 100.0)])
|
||||||
|
|
||||||
|
gti2 = GTI.from_list([(-10.0, 2.0), (75.0, 90.0)])
|
||||||
|
gti3 = GTI.merge(self.gti, gti2)
|
||||||
|
self.test_as_list(gti=gti3, time_list=[(-10.0, 5.0), (20.0, 30.0),
|
||||||
|
(50.0, 100.0)])
|
||||||
|
|
||||||
|
def test_contains(self):
|
||||||
|
self.assertTrue(self.gti.contains(2.0))
|
||||||
|
self.assertFalse(self.gti.contains(10.0))
|
||||||
|
self.assertTrue(self.gti.contains(5.0))
|
||||||
|
self.assertFalse(self.gti.contains(5.0, inclusive=False))
|
||||||
|
|
||||||
|
def test_from_boolean_mask(self):
|
||||||
|
times = np.arange(10)+1
|
||||||
|
mask = np.array([0,0,0,1,1,1,0,0,0,0], dtype=bool)
|
||||||
|
gti = GTI.from_boolean_mask(times, mask)
|
||||||
|
self.assertEqual(gti.as_list()[0], (1.0, 3.0))
|
||||||
|
self.assertEqual(gti.as_list()[1], (7.0, 10.0))
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
self.gti.contains('')
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
self.gti.insert(1.0, 'fivethirtyeight')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
256
test/test_rsp.py
Normal file
256
test/test_rsp.py
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
from unittest import TestCase
|
||||||
|
from gbm.data.drm import RSP
|
||||||
|
|
||||||
|
data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data')
|
||||||
|
|
||||||
|
class TestRSP(TestCase):
|
||||||
|
filename = os.path.join(data_dir, 'glg_cspec_n4_bn120415958_v00.rsp2')
|
||||||
|
numchans = 128
|
||||||
|
numebins = 140
|
||||||
|
numdrms = 12
|
||||||
|
trigtime = 356223561.133346
|
||||||
|
tstart1 = 3.562234321073E+08
|
||||||
|
tstop1 = 3.562234812601E+08
|
||||||
|
tcent1 = (tstart1+tstop1)/2.0
|
||||||
|
tstart12 = 3.562240055561E+08
|
||||||
|
tstop12 = 3.562240383246E+08
|
||||||
|
tcent12 = (tstart12+tstop12)/2.0
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
rsp = RSP.open(self.filename)
|
||||||
|
trigtime = rsp.trigtime
|
||||||
|
self.assertAlmostEqual(trigtime, self.trigtime, places=6)
|
||||||
|
self.assertEqual(rsp.is_gbm_file, True)
|
||||||
|
self.assertEqual(rsp.id, '120415958')
|
||||||
|
self.assertEqual(rsp.filename, os.path.basename(self.filename))
|
||||||
|
self.assertEqual(rsp.is_trigger, True)
|
||||||
|
self.assertEqual(rsp.detector, 'n4')
|
||||||
|
self.assertEqual(rsp.datatype, 'CSPEC')
|
||||||
|
self.assertEqual(rsp.numchans, self.numchans)
|
||||||
|
self.assertEqual(rsp.numebins, self.numebins)
|
||||||
|
self.assertEqual(rsp.numdrms, self.numdrms)
|
||||||
|
self.assertAlmostEqual(rsp.tstart[0], self.tstart1-self.trigtime)
|
||||||
|
self.assertAlmostEqual(rsp.tstop[0], self.tstop1-self.trigtime)
|
||||||
|
self.assertAlmostEqual(rsp.tcent[0], self.tcent1-self.trigtime)
|
||||||
|
self.assertAlmostEqual(rsp.tstart[-1], self.tstart12-self.trigtime)
|
||||||
|
self.assertAlmostEqual(rsp.tstop[-1], self.tstop12-self.trigtime)
|
||||||
|
self.assertAlmostEqual(rsp.tcent[-1], self.tcent12-self.trigtime)
|
||||||
|
self.assertEqual(list(rsp.headers.keys()),
|
||||||
|
['PRIMARY', 'EBOUNDS', 'SPECRESP MATRIX1',
|
||||||
|
'SPECRESP MATRIX2', 'SPECRESP MATRIX3', 'SPECRESP MATRIX4',
|
||||||
|
'SPECRESP MATRIX5', 'SPECRESP MATRIX6', 'SPECRESP MATRIX7',
|
||||||
|
'SPECRESP MATRIX8', 'SPECRESP MATRIX9', 'SPECRESP MATRIX10',
|
||||||
|
'SPECRESP MATRIX11', 'SPECRESP MATRIX12'])
|
||||||
|
self.assertCountEqual(rsp.photon_bin_centroids,
|
||||||
|
np.sqrt(rsp.photon_bins[0]*rsp.photon_bins[1]))
|
||||||
|
self.assertCountEqual(rsp.photon_bin_widths,
|
||||||
|
rsp.photon_bins[1]-rsp.photon_bins[0])
|
||||||
|
self.assertCountEqual(rsp.channel_centroids,
|
||||||
|
np.sqrt(rsp.ebounds['E_MIN']*rsp.ebounds['E_MAX']))
|
||||||
|
self.assertCountEqual(rsp.channel_widths,
|
||||||
|
rsp.ebounds['E_MAX']-rsp.ebounds['E_MIN'])
|
||||||
|
|
||||||
|
def test_extract_one(self):
|
||||||
|
rsp = RSP.open(self.filename)
|
||||||
|
one_drm = rsp.extract_drm(index=0)
|
||||||
|
self.assertEqual(one_drm.numdrms, 1)
|
||||||
|
self.assertCountEqual(one_drm.drm(0).flatten(), rsp.drm(0).flatten())
|
||||||
|
|
||||||
|
one_drm = rsp.extract_drm(atime=0.0)
|
||||||
|
self.assertEqual(one_drm.numdrms, 1)
|
||||||
|
self.assertCountEqual(one_drm.drm(0).flatten(), rsp.drm(2).flatten())
|
||||||
|
|
||||||
|
def test_interpolate(self):
|
||||||
|
rsp = RSP.open(self.filename)
|
||||||
|
interp_drm = rsp.interpolate(-8.0)
|
||||||
|
for rsp_bin, interp_bin in zip(rsp.drm(2).flatten(),
|
||||||
|
interp_drm.drm(0).flatten()):
|
||||||
|
self.assertAlmostEqual(rsp_bin, interp_bin, places=2)
|
||||||
|
|
||||||
|
interp_drm = rsp.interpolate(-1e5)
|
||||||
|
self.assertCountEqual(interp_drm.drm(0).flatten(), rsp.drm(0).flatten())
|
||||||
|
interp_drm = rsp.interpolate(1e5)
|
||||||
|
self.assertCountEqual(interp_drm.drm(0).flatten(),
|
||||||
|
rsp.drm(rsp.numdrms-1).flatten())
|
||||||
|
|
||||||
|
def test_drm_index(self):
|
||||||
|
rsp = RSP.open(self.filename)
|
||||||
|
idx = rsp.drm_index((0.0, 0.0))
|
||||||
|
self.assertCountEqual(idx, [2])
|
||||||
|
idx = rsp.drm_index((0.0, 100.0))
|
||||||
|
self.assertCountEqual(idx, [2,3,4])
|
||||||
|
|
||||||
|
def test_nearest_drm(self):
|
||||||
|
rsp = RSP.open(self.filename)
|
||||||
|
drm = rsp.nearest_drm(0.0)
|
||||||
|
self.assertCountEqual(drm.flatten(), rsp._drm_list[2].flatten())
|
||||||
|
|
||||||
|
def test_photon_effarea(self):
|
||||||
|
rsp = RSP.open(self.filename)
|
||||||
|
effarea = rsp.photon_effective_area(index=0)
|
||||||
|
self.assertEqual(effarea.size, 140)
|
||||||
|
self.assertCountEqual(effarea.lo_edges, rsp.photon_bins[0])
|
||||||
|
self.assertCountEqual(effarea.hi_edges, rsp.photon_bins[1])
|
||||||
|
self.assertCountEqual(effarea.counts, rsp.drm(0).sum(axis=1))
|
||||||
|
|
||||||
|
def test_channel_effarea(self):
|
||||||
|
rsp = RSP.open(self.filename)
|
||||||
|
effarea = rsp.channel_effective_area(index=0)
|
||||||
|
self.assertEqual(effarea.size, 128)
|
||||||
|
self.assertCountEqual(effarea.lo_edges, rsp.ebounds['E_MIN'])
|
||||||
|
self.assertCountEqual(effarea.hi_edges, rsp.ebounds['E_MAX'])
|
||||||
|
self.assertCountEqual(effarea.counts, rsp.drm(0).sum(axis=0))
|
||||||
|
|
||||||
|
def test_weighted(self):
|
||||||
|
rsp = RSP.open(self.filename)
|
||||||
|
drm = rsp.weighted([0.5, 0.5], [-10.0, 30.0])
|
||||||
|
test_drm = rsp._drm_list[2]*0.5 + rsp._drm_list[3]*0.5
|
||||||
|
self.assertCountEqual(drm.flatten(), test_drm.flatten())
|
||||||
|
|
||||||
|
def test_write_weighted(self):
|
||||||
|
rsp = RSP.open(self.filename)
|
||||||
|
rsp.write_weighted(np.array([-10.0, 30.0]), np.array([30.0, 7.0]),
|
||||||
|
np.array([0.5, 0.5]), data_dir,
|
||||||
|
filename='glg_cspec_n4_bn120415958_v01.rsp2')
|
||||||
|
os.remove(os.path.join(data_dir, 'glg_cspec_n4_bn120415958_v01.rsp2'))
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
with self.assertRaises(IOError):
|
||||||
|
rsp = RSP.open('42.rsp')
|
||||||
|
|
||||||
|
rsp = RSP.open(self.filename)
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
rsp.drm_index((100, -10.0))
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
rsp.weighted([0.5, 0.5], [1,2,3.])
|
||||||
|
|
||||||
|
def test_write(self):
|
||||||
|
rsp = RSP.open(self.filename)
|
||||||
|
one_drm = rsp.extract_drm(index=0)
|
||||||
|
one_drm.write(data_dir, filename='test_drm.rsp')
|
||||||
|
one_drm2 = RSP.open(os.path.join(data_dir, 'test_drm.rsp'))
|
||||||
|
os.remove(os.path.join(data_dir, 'test_drm.rsp'))
|
||||||
|
|
||||||
|
self.assertCountEqual(one_drm.drm(0).flatten(), one_drm2.drm(0).flatten())
|
||||||
|
self.assertCountEqual(rsp.drm(0).flatten(), one_drm2.drm(0).flatten())
|
||||||
|
|
||||||
|
def test_from_arrays(self):
|
||||||
|
rsp = RSP.open(self.filename)
|
||||||
|
minimal_rsp = RSP.from_arrays(rsp.ebounds['E_MIN'], rsp.ebounds['E_MAX'],
|
||||||
|
*rsp.photon_bins, rsp.drm(0))
|
||||||
|
rsp = RSP.open(self.filename)
|
||||||
|
minimal_rsp = RSP.from_arrays(rsp.ebounds['E_MIN'], rsp.ebounds['E_MAX'],
|
||||||
|
*rsp.photon_bins, rsp.drm(0))
|
||||||
|
|
||||||
|
self.assertCountEqual(minimal_rsp.ebounds['E_MIN'], rsp.ebounds['E_MIN'])
|
||||||
|
self.assertCountEqual(minimal_rsp.ebounds['E_MAX'], rsp.ebounds['E_MAX'])
|
||||||
|
self.assertCountEqual(minimal_rsp.photon_bins[0], rsp.photon_bins[0])
|
||||||
|
self.assertCountEqual(minimal_rsp.photon_bins[1], rsp.photon_bins[1])
|
||||||
|
self.assertCountEqual(minimal_rsp.drm(0).flatten(), rsp.drm(0).flatten())
|
||||||
|
self.assertEqual(minimal_rsp.trigtime, None)
|
||||||
|
self.assertEqual(minimal_rsp.tstart, None)
|
||||||
|
self.assertEqual(minimal_rsp.tstop, None)
|
||||||
|
self.assertEqual(minimal_rsp.detector, 'all')
|
||||||
|
|
||||||
|
full_rsp = RSP.from_arrays(rsp.ebounds['E_MIN'], rsp.ebounds['E_MAX'],
|
||||||
|
*rsp.photon_bins, rsp.drm(0),
|
||||||
|
trigtime=rsp.trigtime,
|
||||||
|
tstart=rsp.tstart[0]+rsp.trigtime,
|
||||||
|
tstop=rsp.tstop[0]+rsp.trigtime,
|
||||||
|
detnam=rsp.detector,
|
||||||
|
filename='test.rsp')
|
||||||
|
self.assertEqual(full_rsp.trigtime, rsp.trigtime)
|
||||||
|
self.assertEqual(full_rsp.tstart, rsp.tstart[0])
|
||||||
|
self.assertEqual(full_rsp.tstop, rsp.tstop[0])
|
||||||
|
self.assertEqual(full_rsp.detector, rsp.detector)
|
||||||
|
|
||||||
|
def test_effarea(self):
|
||||||
|
rsp = RSP.open(self.filename)
|
||||||
|
e1 = rsp.photon_bin_centroids[35] # ~51 keV
|
||||||
|
e2 = rsp.photon_bin_centroids[62] # ~305 keV
|
||||||
|
a1 = rsp.photon_effective_area(index=0).counts[35] # ~12 cm^2
|
||||||
|
a2 = rsp.photon_effective_area(index=0).counts[62] # ~33 cm^2
|
||||||
|
|
||||||
|
self.assertAlmostEqual(rsp.effective_area(e1, index=0), a1, places=0)
|
||||||
|
self.assertAlmostEqual(rsp.effective_area(e2, index=0), a2, places=0)
|
||||||
|
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
rsp.effective_area('hello', index=0)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
rsp.effective_area(-10.0, index=0)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
rsp.effective_area(np.array([10.0, -10.0]), index=0)
|
||||||
|
|
||||||
|
def test_resample(self):
|
||||||
|
rsp = RSP.open(self.filename)
|
||||||
|
rsp_hires = rsp.resample(num_photon_bins=280)
|
||||||
|
self.assertEqual(rsp_hires.numebins, 280)
|
||||||
|
self.assertEqual(rsp_hires.numchans, rsp.numchans)
|
||||||
|
|
||||||
|
rsp_lores = rsp.resample(num_photon_bins=70)
|
||||||
|
self.assertEqual(rsp_lores.numebins, 70)
|
||||||
|
self.assertEqual(rsp_lores.numchans, rsp.numchans)
|
||||||
|
|
||||||
|
rsp_edges = rsp.resample(photon_bin_edges=np.array([10.0, 50.0, 300.0, 1000.0]))
|
||||||
|
self.assertEqual(rsp_edges.numebins, 3)
|
||||||
|
self.assertEqual(rsp_edges.numchans, rsp.numchans)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
rsp.resample()
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
rsp.resample(num_photon_bins='hello')
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
rsp.resample(num_photon_bins=-10)
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
rsp.resample(photon_bin_edges=10.0)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
rsp.resample(photon_bin_edges=np.array([0.1, 10.0, 2e6]))
|
||||||
|
|
||||||
|
def test_rebin(self):
|
||||||
|
rsp = RSP.open(self.filename)
|
||||||
|
|
||||||
|
rsp2 = rsp.rebin(factor=4)
|
||||||
|
self.assertEqual(rsp2.numebins, 140)
|
||||||
|
self.assertEqual(rsp2.numchans, 32)
|
||||||
|
|
||||||
|
rsp3 = rsp.rebin(edge_indices=np.array([1, 10, 20, 30, 127]))
|
||||||
|
self.assertEqual(rsp3.numebins, 140)
|
||||||
|
self.assertEqual(rsp3.numchans, 4)
|
||||||
|
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
rsp.rebin(factor='hello')
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
rsp.rebin(factor=-4)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
rsp.rebin(factor=17)
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
rsp.rebin(edge_indices=10)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
rsp.rebin(edge_indices=[1, 10, 300])
|
||||||
|
|
196
test/test_scat.py
Normal file
196
test/test_scat.py
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from gbm.data.scat import *
|
||||||
|
|
||||||
|
data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data')
|
||||||
|
|
||||||
|
class TestParameter(TestCase):
|
||||||
|
def test_attributes(self):
|
||||||
|
param = Parameter(10.0, 0.1, 'test', units='km', support=(0, np.inf))
|
||||||
|
self.assertEqual(param.value, 10.0)
|
||||||
|
self.assertTupleEqual(param.uncertainty, (0.1, 0.1))
|
||||||
|
self.assertEqual(param.name, 'test')
|
||||||
|
self.assertEqual(param.units, 'km')
|
||||||
|
self.assertTupleEqual(param.support, (0.0, np.inf))
|
||||||
|
self.assertTrue(param.valid_value())
|
||||||
|
self.assertTupleEqual(param.one_sigma_range(), (9.9, 10.1))
|
||||||
|
self.assertTupleEqual(param.to_fits_value(), (10.0, 0.1, 0.1))
|
||||||
|
|
||||||
|
def test_asymmetric(self):
|
||||||
|
param = Parameter(10.0, (0.1, 0.5), 'test')
|
||||||
|
self.assertTupleEqual(param.uncertainty, (0.1, 0.5))
|
||||||
|
self.assertTupleEqual(param.one_sigma_range(), (9.9, 10.5))
|
||||||
|
self.assertTupleEqual(param.to_fits_value(), (10.0, 0.5, 0.1))
|
||||||
|
|
||||||
|
def test_invalid_value(self):
|
||||||
|
param = Parameter(-10.0, (0.1, 0.5), 'test', support=(0.0, np.inf))
|
||||||
|
self.assertFalse(param.valid_value())
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
Parameter('hello', 0.1, 'test')
|
||||||
|
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
Parameter(10.0, 'hello', 'test')
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
Parameter(10.0, (1.0, 1.0, 1.0), 'test')
|
||||||
|
|
||||||
|
def test_photon_flux(self):
|
||||||
|
pflux = PhotonFlux(10.0, 0.1, (50.0, 300.0))
|
||||||
|
self.assertTupleEqual(pflux.energy_range, (50.0, 300.0))
|
||||||
|
self.assertEqual(pflux.name, 'Photon Flux')
|
||||||
|
self.assertEqual(pflux.units, 'ph/cm^2/s')
|
||||||
|
self.assertTupleEqual(pflux.support, (0.0, np.inf))
|
||||||
|
|
||||||
|
def test_photon_fluence(self):
|
||||||
|
pfluence = PhotonFluence(10.0, 0.1, (50.0, 300.0))
|
||||||
|
self.assertTupleEqual(pfluence.energy_range, (50.0, 300.0))
|
||||||
|
self.assertEqual(pfluence.name, 'Photon Fluence')
|
||||||
|
self.assertEqual(pfluence.units, 'ph/cm^2')
|
||||||
|
self.assertTupleEqual(pfluence.support, (0.0, np.inf))
|
||||||
|
|
||||||
|
def test_energy_flux(self):
|
||||||
|
eflux = EnergyFlux(1e-6, 1e-7, (50.0, 300.0))
|
||||||
|
self.assertTupleEqual(eflux.energy_range, (50.0, 300.0))
|
||||||
|
self.assertEqual(eflux.name, 'Energy Flux')
|
||||||
|
self.assertEqual(eflux.units, 'erg/cm^2/s')
|
||||||
|
self.assertTupleEqual(eflux.support, (0.0, np.inf))
|
||||||
|
|
||||||
|
def test_energy_fluence(self):
|
||||||
|
efluence = EnergyFluence(1e-6, 1e-7, (50.0, 300.0))
|
||||||
|
self.assertTupleEqual(efluence.energy_range, (50.0, 300.0))
|
||||||
|
self.assertEqual(efluence.name, 'Energy Fluence')
|
||||||
|
self.assertEqual(efluence.units, 'erg/cm^2')
|
||||||
|
self.assertTupleEqual(efluence.support, (0.0, np.inf))
|
||||||
|
|
||||||
|
|
||||||
|
class TestModelFit(TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(self):
|
||||||
|
filename = os.path.join(data_dir,
|
||||||
|
'glg_scat_all_bn170817529_flnc_comp_v02.fit')
|
||||||
|
scat = Scat.open(filename)
|
||||||
|
self.model_fit = scat.model_fits[0]
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
self.assertTupleEqual(self.model_fit.time_range, (-0.192, 0.064))
|
||||||
|
self.assertEqual(self.model_fit.parameters[0].name, 'Amplitude')
|
||||||
|
self.assertAlmostEqual(self.model_fit.parameters[0].value, 0.03199916)
|
||||||
|
self.assertEqual(self.model_fit.parameters[1].name, 'Epeak')
|
||||||
|
self.assertAlmostEqual(self.model_fit.parameters[1].value, 215.0943,
|
||||||
|
places=4)
|
||||||
|
self.assertEqual(self.model_fit.parameters[2].name, 'Index')
|
||||||
|
self.assertAlmostEqual(self.model_fit.parameters[2].value, 0.1438237)
|
||||||
|
self.assertEqual(self.model_fit.parameters[3].name, 'Pivot E =fix')
|
||||||
|
self.assertAlmostEqual(self.model_fit.parameters[3].value, 100.0)
|
||||||
|
self.assertAlmostEqual(self.model_fit.photon_flux.value, 2.812537,
|
||||||
|
places=6)
|
||||||
|
self.assertAlmostEqual(self.model_fit.energy_flux.value, 5.502238E-07)
|
||||||
|
self.assertAlmostEqual(self.model_fit.photon_fluence.value, 0.7173939)
|
||||||
|
self.assertAlmostEqual(self.model_fit.energy_fluence.value, 1.403456E-07)
|
||||||
|
self.assertTupleEqual(self.model_fit.flux_energy_range, (10.0, 1000.0))
|
||||||
|
self.assertEqual(self.model_fit.stat_name, 'Castor C-STAT')
|
||||||
|
self.assertAlmostEqual(self.model_fit.stat_value, 479.6157, places=4)
|
||||||
|
self.assertEqual(self.model_fit.dof, 479)
|
||||||
|
self.assertAlmostEqual(self.model_fit.photon_flux_50_300.value,
|
||||||
|
1.825902, places=6)
|
||||||
|
self.assertAlmostEqual(self.model_fit.energy_fluence_50_300.value,
|
||||||
|
9.852592E-08)
|
||||||
|
self.assertAlmostEqual(self.model_fit.duration_fluence.value, 0.4657327)
|
||||||
|
|
||||||
|
def test_param_list(self):
|
||||||
|
self.assertListEqual(self.model_fit.parameter_list(),
|
||||||
|
['Amplitude', 'Epeak', 'Index', 'Pivot E =fix'])
|
||||||
|
|
||||||
|
def test_to_fits(self):
|
||||||
|
fits_row = self.model_fit.to_fits_row()
|
||||||
|
|
||||||
|
|
||||||
|
class TestDetectorData(TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(self):
|
||||||
|
filename = os.path.join(data_dir,
|
||||||
|
'glg_scat_all_bn170817529_flnc_comp_v02.fit')
|
||||||
|
scat = Scat.open(filename)
|
||||||
|
self.det_data = scat.detectors[0]
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
self.assertEqual(self.det_data.instrument, 'GBM')
|
||||||
|
self.assertEqual(self.det_data.detector, 'BGO_00')
|
||||||
|
self.assertEqual(self.det_data.datatype, 'TTE')
|
||||||
|
self.assertEqual(self.det_data.filename, 'glg_tte_b0_bn170817529_v00.fit')
|
||||||
|
self.assertEqual(self.det_data.numchans, 128)
|
||||||
|
self.assertTrue(self.det_data.active)
|
||||||
|
self.assertEqual(self.det_data.response, 'glg_cspec_b0_bn170817529_v04.rsp')
|
||||||
|
self.assertTupleEqual(self.det_data.time_range, (-0.192, 0.064))
|
||||||
|
self.assertTupleEqual(self.det_data.energy_range, (284.65, 40108.))
|
||||||
|
self.assertTupleEqual(self.det_data.channel_range, (3, 124))
|
||||||
|
test_mask = np.zeros(128, dtype=bool)
|
||||||
|
test_mask[3:125] = True
|
||||||
|
self.assertListEqual(self.det_data.channel_mask.tolist(), test_mask.tolist())
|
||||||
|
self.assertEqual(self.det_data.energy_edges.size, 129)
|
||||||
|
|
||||||
|
def test_to_fits(self):
|
||||||
|
fits_row = self.det_data.to_fits_row()
|
||||||
|
|
||||||
|
|
||||||
|
class TestScat(TestCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(self):
|
||||||
|
filename = os.path.join(data_dir,
|
||||||
|
'glg_scat_all_bn170817529_flnc_comp_v02.fit')
|
||||||
|
self.scat = Scat.open(filename)
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
self.assertEqual(self.scat.num_detectors, 4)
|
||||||
|
self.assertIsInstance(self.scat.detectors[0], DetectorData)
|
||||||
|
self.assertEqual(self.scat.num_fits, 1)
|
||||||
|
self.assertIsInstance(self.scat.model_fits[0], GbmModelFit)
|
||||||
|
self.assertTupleEqual(tuple(self.scat.headers.keys()),
|
||||||
|
('PRIMARY', 'DETECTOR DATA', 'FIT PARAMS'))
|
||||||
|
|
||||||
|
def test_add_detector(self):
|
||||||
|
self.scat.add_detector_data(self.scat.detectors[0])
|
||||||
|
self.scat._detectors = self.scat._detectors[:-1]
|
||||||
|
|
||||||
|
def test_add_fit(self):
|
||||||
|
self.scat.add_model_fit(self.scat.model_fits[0])
|
||||||
|
self.scat._model_fits = self.scat._model_fits[:-1]
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
self.scat.add_detector_data(1.0)
|
||||||
|
with self.assertRaises(TypeError):
|
||||||
|
self.scat.add_model_fit(2.0)
|
||||||
|
with self.assertRaises(NotImplementedError):
|
||||||
|
self.scat.write('.')
|
338
test/test_sims.py
Normal file
338
test/test_sims.py
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import os
|
||||||
|
import numpy as np
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from gbm.simulate.generators import *
|
||||||
|
from gbm.simulate.profiles import *
|
||||||
|
from gbm.simulate import PhaSimulator, TteSourceSimulator, TteBackgroundSimulator
|
||||||
|
from gbm.data import BAK, RSP, PHA, PHAII, TTE
|
||||||
|
from gbm.spectra.functions import Band
|
||||||
|
|
||||||
|
data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data')
|
||||||
|
|
||||||
|
class TestGenerators(TestCase):
|
||||||
|
bkgd = BAK.open(os.path.join(data_dir, 'glg_tte_n0_bn160509374_xspec_v00.bak'))
|
||||||
|
bkgd = bkgd.data
|
||||||
|
rsp = RSP.open(os.path.join(data_dir, 'glg_cspec_n4_bn120415958_v00.rsp2'))
|
||||||
|
fxn = Band()
|
||||||
|
params = (1.0, 300.0, -1.0, -2.5)
|
||||||
|
exposure = 2.048
|
||||||
|
|
||||||
|
def test_poisson_background(self):
|
||||||
|
gen = PoissonBackgroundGenerator(self.bkgd)
|
||||||
|
dev = next(gen)
|
||||||
|
self.assertEqual(dev.size, self.bkgd.size)
|
||||||
|
self.assertCountEqual(dev.lo_edges, self.bkgd.lo_edges)
|
||||||
|
self.assertCountEqual(dev.hi_edges, self.bkgd.hi_edges)
|
||||||
|
self.assertCountEqual(dev.exposure, self.bkgd.exposure)
|
||||||
|
devs = [next(gen) for i in range(10)]
|
||||||
|
|
||||||
|
def test_variable_poisson_background(self):
|
||||||
|
gen = VariablePoissonBackground(self.bkgd)
|
||||||
|
dev = next(gen)
|
||||||
|
self.assertEqual(dev.size, self.bkgd.size)
|
||||||
|
self.assertCountEqual(dev.lo_edges, self.bkgd.lo_edges)
|
||||||
|
self.assertCountEqual(dev.hi_edges, self.bkgd.hi_edges)
|
||||||
|
self.assertCountEqual(dev.exposure, self.bkgd.exposure)
|
||||||
|
devs = [next(gen) for i in range(10)]
|
||||||
|
|
||||||
|
gen.amp = 10.0
|
||||||
|
dev = next(gen)
|
||||||
|
ratio = dev.counts/devs[0].counts
|
||||||
|
for i in range(self.bkgd.size):
|
||||||
|
if not np.isnan(ratio[i]):
|
||||||
|
self.assertTrue(ratio[i] > 1.0)
|
||||||
|
|
||||||
|
gen.amp = 0.1
|
||||||
|
dev = next(gen)
|
||||||
|
ratio = dev.counts/devs[0].counts
|
||||||
|
for i in range(self.bkgd.size):
|
||||||
|
if not np.isnan(ratio[i]):
|
||||||
|
self.assertTrue(ratio[i] < 1.0)
|
||||||
|
|
||||||
|
def test_gaussian_background(self):
|
||||||
|
gen = GaussianBackgroundGenerator(self.bkgd)
|
||||||
|
dev = next(gen)
|
||||||
|
self.assertEqual(dev.size, self.bkgd.size)
|
||||||
|
self.assertCountEqual(dev.lo_edges, self.bkgd.lo_edges)
|
||||||
|
self.assertCountEqual(dev.hi_edges, self.bkgd.hi_edges)
|
||||||
|
self.assertCountEqual(dev.exposure, self.bkgd.exposure)
|
||||||
|
devs = [next(gen) for i in range(10)]
|
||||||
|
|
||||||
|
def test_variable_poisson_background(self):
|
||||||
|
gen = VariableGaussianBackground(self.bkgd)
|
||||||
|
dev = next(gen)
|
||||||
|
self.assertEqual(dev.size, self.bkgd.size)
|
||||||
|
self.assertCountEqual(dev.lo_edges, self.bkgd.lo_edges)
|
||||||
|
self.assertCountEqual(dev.hi_edges, self.bkgd.hi_edges)
|
||||||
|
self.assertCountEqual(dev.exposure, self.bkgd.exposure)
|
||||||
|
devs = [next(gen) for i in range(10)]
|
||||||
|
|
||||||
|
gen.amp = 10.0
|
||||||
|
dev = next(gen)
|
||||||
|
ratio = dev.counts/devs[0].counts
|
||||||
|
for i in range(self.bkgd.size):
|
||||||
|
if not np.isnan(ratio[i]):
|
||||||
|
self.assertTrue(ratio[i] > 1.0)
|
||||||
|
|
||||||
|
gen.amp = 0.1
|
||||||
|
dev = next(gen)
|
||||||
|
ratio = dev.counts/devs[0].counts
|
||||||
|
for i in range(self.bkgd.size):
|
||||||
|
if not np.isnan(ratio[i]):
|
||||||
|
self.assertTrue(ratio[i] < 1.0)
|
||||||
|
|
||||||
|
def test_source_spectrum(self):
|
||||||
|
gen = SourceSpectrumGenerator(self.rsp, self.fxn, self.params, self.exposure)
|
||||||
|
dev = next(gen)
|
||||||
|
self.assertEqual(dev.size, self.rsp.numchans)
|
||||||
|
self.assertCountEqual(dev.lo_edges, self.rsp.ebounds['E_MIN'])
|
||||||
|
self.assertCountEqual(dev.hi_edges, self.rsp.ebounds['E_MAX'])
|
||||||
|
self.assertEqual(dev.exposure[0], self.exposure)
|
||||||
|
devs = [next(gen) for i in range(10)]
|
||||||
|
|
||||||
|
def test_variable_source_spectrum(self):
|
||||||
|
gen = VariableSourceSpectrumGenerator(self.rsp, self.fxn, self.params,
|
||||||
|
self.exposure)
|
||||||
|
dev = next(gen)
|
||||||
|
self.assertEqual(dev.size, self.rsp.numchans)
|
||||||
|
self.assertCountEqual(dev.lo_edges, self.rsp.ebounds['E_MIN'])
|
||||||
|
self.assertCountEqual(dev.hi_edges, self.rsp.ebounds['E_MAX'])
|
||||||
|
self.assertEqual(dev.exposure[0], self.exposure)
|
||||||
|
devs = [next(gen) for i in range(10)]
|
||||||
|
|
||||||
|
gen.amp = 10.0
|
||||||
|
dev = next(gen)
|
||||||
|
ratio = dev.counts/devs[0].counts
|
||||||
|
for i in range(self.bkgd.size):
|
||||||
|
if not np.isnan(ratio[i]):
|
||||||
|
self.assertTrue(ratio[i] > 1.0)
|
||||||
|
|
||||||
|
def test_event_spectrum(self):
|
||||||
|
spec_gen = PoissonBackgroundGenerator(self.bkgd)
|
||||||
|
event_gen = EventSpectrumGenerator(next(spec_gen).counts, 0.001)
|
||||||
|
dev_times, dev_chans = next(event_gen)
|
||||||
|
self.assertEqual(dev_times.size, dev_chans.size)
|
||||||
|
|
||||||
|
event_gen.spectrum = next(spec_gen).counts
|
||||||
|
dev_times, dev_chans = next(event_gen)
|
||||||
|
self.assertEqual(dev_times.size, dev_chans.size)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPhaSimulator(TestCase):
|
||||||
|
bkgd = BAK.open(os.path.join(data_dir, 'glg_tte_n0_bn160509374_xspec_v00.bak'))
|
||||||
|
bkgd = bkgd.data
|
||||||
|
rsp = RSP.open(os.path.join(data_dir, 'glg_cspec_n4_bn120415958_v00.rsp2'))
|
||||||
|
fxn = Band()
|
||||||
|
params = (1.0, 300.0, -1.0, -2.5)
|
||||||
|
exposure = 2.048
|
||||||
|
|
||||||
|
def test_run(self):
|
||||||
|
pha_sims = PhaSimulator(self.rsp, self.fxn, self.params, self.exposure,
|
||||||
|
self.bkgd, 'Gaussian')
|
||||||
|
|
||||||
|
sim = pha_sims.simulate_background(1)
|
||||||
|
self.assertEqual(sim[0].size, self.bkgd.size)
|
||||||
|
sim = pha_sims.simulate_source(1)
|
||||||
|
self.assertEqual(sim[0].size, self.rsp.numchans)
|
||||||
|
sim = pha_sims.simulate_sum(1)
|
||||||
|
self.assertEqual(sim[0].size, self.rsp.numchans)
|
||||||
|
|
||||||
|
sims = pha_sims.simulate_sum(10)
|
||||||
|
self.assertEqual(len(sims), 10)
|
||||||
|
|
||||||
|
def test_set_rsp(self):
|
||||||
|
pha_sims = PhaSimulator(self.rsp, self.fxn, self.params, self.exposure,
|
||||||
|
self.bkgd, 'Gaussian')
|
||||||
|
pha_sims.set_rsp(self.rsp.extract_drm(5))
|
||||||
|
sim = pha_sims.simulate_background(1)
|
||||||
|
self.assertEqual(sim[0].size, self.bkgd.size)
|
||||||
|
sim = pha_sims.simulate_source(1)
|
||||||
|
self.assertEqual(sim[0].size, self.rsp.numchans)
|
||||||
|
sim = pha_sims.simulate_sum(1)
|
||||||
|
self.assertEqual(sim[0].size, self.rsp.numchans)
|
||||||
|
|
||||||
|
def test_set_background(self):
|
||||||
|
pha_sims = PhaSimulator(self.rsp, self.fxn, self.params, self.exposure,
|
||||||
|
self.bkgd, 'Gaussian')
|
||||||
|
pha_sims.set_background(self.bkgd, 'Poisson')
|
||||||
|
sim = pha_sims.simulate_background(1)
|
||||||
|
self.assertEqual(sim[0].size, self.bkgd.size)
|
||||||
|
sim = pha_sims.simulate_source(1)
|
||||||
|
self.assertEqual(sim[0].size, self.rsp.numchans)
|
||||||
|
sim = pha_sims.simulate_sum(1)
|
||||||
|
self.assertEqual(sim[0].size, self.rsp.numchans)
|
||||||
|
|
||||||
|
def test_set_source(self):
|
||||||
|
pha_sims = PhaSimulator(self.rsp, self.fxn, self.params, self.exposure,
|
||||||
|
self.bkgd, 'Gaussian')
|
||||||
|
pha_sims.set_source(self.fxn, self.params, 10.0)
|
||||||
|
sim = pha_sims.simulate_background(1)
|
||||||
|
self.assertEqual(sim[0].size, self.bkgd.size)
|
||||||
|
sim = pha_sims.simulate_source(1)
|
||||||
|
self.assertEqual(sim[0].size, self.rsp.numchans)
|
||||||
|
sim = pha_sims.simulate_sum(1)
|
||||||
|
self.assertEqual(sim[0].size, self.rsp.numchans)
|
||||||
|
|
||||||
|
def test_to_bak(self):
|
||||||
|
pha_sims = PhaSimulator(self.rsp, self.fxn, self.params, self.exposure,
|
||||||
|
self.bkgd, 'Gaussian')
|
||||||
|
baks = pha_sims.to_bak(5)
|
||||||
|
[self.assertIsInstance(bak, BAK) for bak in baks]
|
||||||
|
baks = pha_sims.to_bak(5, tstart=5.0, tstop=8.0)
|
||||||
|
[self.assertIsInstance(bak, BAK) for bak in baks]
|
||||||
|
|
||||||
|
def test_to_pha(self):
|
||||||
|
pha_sims = PhaSimulator(self.rsp, self.fxn, self.params, self.exposure,
|
||||||
|
self.bkgd, 'Gaussian')
|
||||||
|
phas = pha_sims.to_pha(5)
|
||||||
|
[self.assertIsInstance(pha, PHA) for pha in phas]
|
||||||
|
phas = pha_sims.to_pha(5, tstart=5.0, tstop=8.0)
|
||||||
|
[self.assertIsInstance(pha, PHA) for pha in phas]
|
||||||
|
|
||||||
|
def test_to_phaii(self):
|
||||||
|
pha_sims = PhaSimulator(self.rsp, self.fxn, self.params, self.exposure,
|
||||||
|
self.bkgd, 'Gaussian')
|
||||||
|
phaii = pha_sims.to_phaii(5)
|
||||||
|
self.assertIsInstance(phaii, PHAII)
|
||||||
|
phaii = pha_sims.to_phaii(5, bin_width=3.0)
|
||||||
|
self.assertIsInstance(phaii, PHAII)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTteSourceSimulator(TestCase):
|
||||||
|
rsp = RSP.open(os.path.join(data_dir, 'glg_cspec_n4_bn120415958_v00.rsp2'))
|
||||||
|
spec_fxn = Band()
|
||||||
|
spec_params = (0.1, 300.0, -1.0, -2.5)
|
||||||
|
time_fxn = tophat
|
||||||
|
time_params = (0.05, 0.0, 1.0)
|
||||||
|
|
||||||
|
def test_run(self):
|
||||||
|
src_sims = TteSourceSimulator(self.rsp, self.spec_fxn, self.spec_params,
|
||||||
|
tophat, self.time_params)
|
||||||
|
tte = src_sims.to_tte(-5.0, 5.0, trigtime=0.0)
|
||||||
|
self.assertIsInstance(tte, TTE)
|
||||||
|
tte = src_sims.to_tte(-1.0, 1.0)
|
||||||
|
self.assertIsInstance(tte, TTE)
|
||||||
|
|
||||||
|
def test_set_response(self):
|
||||||
|
src_sims = TteSourceSimulator(self.rsp, self.spec_fxn, self.spec_params,
|
||||||
|
tophat, self.time_params)
|
||||||
|
src_sims.set_response(self.rsp.extract_drm(5))
|
||||||
|
tte = src_sims.to_tte(-1.0, 1.0)
|
||||||
|
self.assertIsInstance(tte, TTE)
|
||||||
|
|
||||||
|
def test_set_spectrum(self):
|
||||||
|
src_sims = TteSourceSimulator(self.rsp, self.spec_fxn, self.spec_params,
|
||||||
|
tophat, self.time_params)
|
||||||
|
src_sims.set_spectrum(self.spec_fxn, (0.1, 100., -0.5, -3.0))
|
||||||
|
tte = src_sims.to_tte(-1.0, 1.0)
|
||||||
|
self.assertIsInstance(tte, TTE)
|
||||||
|
|
||||||
|
def test_time_profile(self):
|
||||||
|
src_sims = TteSourceSimulator(self.rsp, self.spec_fxn, self.spec_params,
|
||||||
|
tophat, self.time_params)
|
||||||
|
src_sims.set_time_profile(tophat, (0.1, 0.0, 2.0))
|
||||||
|
tte = src_sims.to_tte(-1.0, 1.0)
|
||||||
|
self.assertIsInstance(tte, TTE)
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
src_sims = TteSourceSimulator(self.rsp, self.spec_fxn, self.spec_params,
|
||||||
|
tophat, self.time_params, sample_period=0.0)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
src_sims = TteSourceSimulator(self.rsp, self.spec_fxn, self.spec_params,
|
||||||
|
tophat, self.time_params, deadtime=-1e-4)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class TestTteBackgroundSimulator(TestCase):
|
||||||
|
bkgd = BAK.open(os.path.join(data_dir, 'glg_tte_n0_bn160509374_xspec_v00.bak'))
|
||||||
|
bkgd = bkgd.data
|
||||||
|
time_fxn = linear
|
||||||
|
time_params = (1.0, -0.1)
|
||||||
|
|
||||||
|
def test_run(self):
|
||||||
|
src_sims = TteBackgroundSimulator(self.bkgd, 'Gaussian', linear,
|
||||||
|
self.time_params)
|
||||||
|
tte = src_sims.to_tte(-5.0, 5.0)
|
||||||
|
self.assertIsInstance(tte, TTE)
|
||||||
|
tte = src_sims.to_tte(-1.0, 1.0, trigtime=0)
|
||||||
|
self.assertIsInstance(tte, TTE)
|
||||||
|
|
||||||
|
def test_set_background(self):
|
||||||
|
src_sims = TteBackgroundSimulator(self.bkgd, 'Gaussian', linear,
|
||||||
|
self.time_params)
|
||||||
|
src_sims.set_background(self.bkgd, 'Poisson')
|
||||||
|
tte = src_sims.to_tte(-1.0, 1.0)
|
||||||
|
self.assertIsInstance(tte, TTE)
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
src_sims = TteBackgroundSimulator(self.bkgd, 'Gaussian', linear,
|
||||||
|
self.time_params, sample_period=0.0)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
src_sims = TteBackgroundSimulator(self.bkgd, 'Gaussian', linear,
|
||||||
|
self.time_params, deadtime=-1e-4)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTimeProfiles(TestCase):
|
||||||
|
times = np.array([-10.0, 0.0, 10.0])
|
||||||
|
|
||||||
|
def test_tophat(self):
|
||||||
|
params = (1.0, 0.0, 20.0)
|
||||||
|
y = tophat(self.times, *params)
|
||||||
|
self.assertCountEqual(y, np.array([0.0, 1.0, 1.0]))
|
||||||
|
|
||||||
|
def test_norris(self):
|
||||||
|
params = (1.0, -1.0, 0.1, 2.0)
|
||||||
|
y = norris(self.times, *params)
|
||||||
|
true = np.array((0.0, 0.858, 0.006))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_constant(self):
|
||||||
|
params = (1.0,)
|
||||||
|
y = constant(self.times, *params)
|
||||||
|
self.assertCountEqual(y, np.array([1.0, 1.0, 1.0]))
|
||||||
|
|
||||||
|
def test_linear(self):
|
||||||
|
params = (1.0, -2.0,)
|
||||||
|
y = linear(self.times, *params)
|
||||||
|
self.assertCountEqual(y, np.array([21.0, 1.0, -19.0]))
|
||||||
|
|
||||||
|
def test_quadratic(self):
|
||||||
|
params = (1.0, -2.0, 2.0)
|
||||||
|
y = quadratic(self.times, *params)
|
||||||
|
self.assertCountEqual(y, np.array([221.0, 1.0, 181.0]))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
|
|
||||||
|
|
||||||
|
|
117
test/test_specfitter.py
Normal file
117
test/test_specfitter.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import unittest
|
||||||
|
import os
|
||||||
|
import numpy as np
|
||||||
|
from gbm.spectra.fitting import SpectralFitterPgstat, SpectralFitterChisq
|
||||||
|
from gbm.spectra.fitting import SpectralFitterPstat, SpectralFitterCstat
|
||||||
|
from gbm.spectra.functions import Comptonized
|
||||||
|
from gbm.data import PHA, RSP
|
||||||
|
|
||||||
|
data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data')
|
||||||
|
|
||||||
|
|
||||||
|
class TestSpectralFitter(unittest.TestCase):
|
||||||
|
|
||||||
|
pha = PHA.open(os.path.join(data_dir, 'sim_pha.pha'))
|
||||||
|
bkgd = np.load(os.path.join(data_dir, 'sim_bkgd.npy'), allow_pickle=True)[0]
|
||||||
|
rsp = RSP.open(os.path.join(data_dir, 'glg_cspec_n9_bn090131090_v00.rsp2'))
|
||||||
|
|
||||||
|
def test_pgstat(self):
|
||||||
|
fitter = SpectralFitterPgstat([self.pha], [self.bkgd], [self.rsp],
|
||||||
|
method='TNC')
|
||||||
|
fitter.fit(Comptonized(), options={'maxiter': 10000})
|
||||||
|
self.assertEqual(fitter.dof, 117)
|
||||||
|
self.assertEqual(fitter.function_name, 'Comptonized')
|
||||||
|
self.assertEqual(fitter.num_components, 1)
|
||||||
|
self.assertEqual(fitter.detectors, ['n9'])
|
||||||
|
self.assertAlmostEqual(fitter.energy_range[0], 8.0, places=0)
|
||||||
|
self.assertAlmostEqual(fitter.energy_range[1], 908.0, places=0)
|
||||||
|
self.assertEqual(fitter.num_sets, 1)
|
||||||
|
self.assertEqual(fitter.success, True)
|
||||||
|
self.assertAlmostEqual(fitter.parameters[0], 1.0, delta=0.2)
|
||||||
|
self.assertAlmostEqual(fitter.parameters[1], 220., delta=100.)
|
||||||
|
self.assertAlmostEqual(fitter.parameters[2], -1.2, delta=0.2)
|
||||||
|
self.assertEqual(fitter.jacobian.size, 3)
|
||||||
|
self.assertEqual(fitter.hessian.size, 9)
|
||||||
|
self.assertEqual(fitter.covariance.size, 9)
|
||||||
|
|
||||||
|
def test_chisq(self):
|
||||||
|
fitter = SpectralFitterChisq([self.pha], [self.bkgd], [self.rsp],
|
||||||
|
method='TNC')
|
||||||
|
fitter.fit(Comptonized(), options={'maxiter': 10000})
|
||||||
|
self.assertEqual(fitter.dof, 117)
|
||||||
|
self.assertEqual(fitter.function_name, 'Comptonized')
|
||||||
|
self.assertEqual(fitter.num_components, 1)
|
||||||
|
self.assertEqual(fitter.detectors, ['n9'])
|
||||||
|
self.assertAlmostEqual(fitter.energy_range[0], 8.0, places=0)
|
||||||
|
self.assertAlmostEqual(fitter.energy_range[1], 908.0, places=0)
|
||||||
|
self.assertEqual(fitter.num_sets, 1)
|
||||||
|
self.assertEqual(fitter.success, True)
|
||||||
|
self.assertAlmostEqual(fitter.parameters[0], 1.0, delta=0.1)
|
||||||
|
self.assertAlmostEqual(fitter.parameters[1], 220.0, delta=10.)
|
||||||
|
self.assertAlmostEqual(fitter.parameters[2], -1.2, delta=0.1)
|
||||||
|
self.assertEqual(fitter.jacobian.size, 3)
|
||||||
|
self.assertEqual(fitter.hessian.size, 9)
|
||||||
|
self.assertEqual(fitter.covariance.size, 9)
|
||||||
|
|
||||||
|
def test_cstat(self):
|
||||||
|
fitter = SpectralFitterCstat([self.pha], [self.bkgd], [self.rsp],
|
||||||
|
method='TNC')
|
||||||
|
fitter.fit(Comptonized(), options={'maxiter': 10000})
|
||||||
|
self.assertEqual(fitter.dof, 117)
|
||||||
|
self.assertEqual(fitter.function_name, 'Comptonized')
|
||||||
|
self.assertEqual(fitter.num_components, 1)
|
||||||
|
self.assertEqual(fitter.detectors, ['n9'])
|
||||||
|
self.assertAlmostEqual(fitter.energy_range[0], 8.0, places=0)
|
||||||
|
self.assertAlmostEqual(fitter.energy_range[1], 908.0, places=0)
|
||||||
|
self.assertEqual(fitter.num_sets, 1)
|
||||||
|
self.assertEqual(fitter.success, True)
|
||||||
|
self.assertAlmostEqual(fitter.parameters[0], 1.0, delta=0.1)
|
||||||
|
self.assertAlmostEqual(fitter.parameters[1], 220.0, delta=30.)
|
||||||
|
self.assertAlmostEqual(fitter.parameters[2], -1.2, delta=0.1)
|
||||||
|
self.assertEqual(fitter.jacobian.size, 3)
|
||||||
|
self.assertEqual(fitter.hessian.size, 9)
|
||||||
|
self.assertEqual(fitter.covariance.size, 9)
|
||||||
|
|
||||||
|
def test_pstat(self):
|
||||||
|
fitter = SpectralFitterPstat([self.pha], [self.bkgd], [self.rsp],
|
||||||
|
method='TNC')
|
||||||
|
fitter.fit(Comptonized(), options={'maxiter': 10000})
|
||||||
|
self.assertEqual(fitter.dof, 117)
|
||||||
|
self.assertEqual(fitter.function_name, 'Comptonized')
|
||||||
|
self.assertEqual(fitter.num_components, 1)
|
||||||
|
self.assertEqual(fitter.detectors, ['n9'])
|
||||||
|
self.assertAlmostEqual(fitter.energy_range[0], 8.0, places=0)
|
||||||
|
self.assertAlmostEqual(fitter.energy_range[1], 908.0, places=0)
|
||||||
|
self.assertEqual(fitter.num_sets, 1)
|
||||||
|
self.assertEqual(fitter.success, True)
|
||||||
|
self.assertAlmostEqual(fitter.parameters[0], 1.0, delta=0.1)
|
||||||
|
self.assertAlmostEqual(fitter.parameters[1], 220.0, delta=30.)
|
||||||
|
self.assertAlmostEqual(fitter.parameters[2], -1.2, delta=0.1)
|
||||||
|
self.assertEqual(fitter.jacobian.size, 3)
|
||||||
|
self.assertEqual(fitter.hessian.size, 9)
|
||||||
|
self.assertEqual(fitter.covariance.size, 9)
|
||||||
|
|
322
test/test_spectral_functions.py
Normal file
322
test/test_spectral_functions.py
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import unittest
|
||||||
|
import numpy as np
|
||||||
|
from gbm.spectra.functions import *
|
||||||
|
|
||||||
|
class MyFirstFunction(Function):
|
||||||
|
nparams = 2
|
||||||
|
param_list = [('C0', 'units1', 'Coeff1'),
|
||||||
|
('C1', 'units2', 'Coeff2')]
|
||||||
|
default_values = [0.0, 0.0]
|
||||||
|
delta_abs = [0.1, 0.1]
|
||||||
|
delta_rel = [0.01, 0.01]
|
||||||
|
min_values = [-np.inf, -np.inf]
|
||||||
|
max_values = [np.inf, np.inf]
|
||||||
|
free = [True, True]
|
||||||
|
|
||||||
|
def eval(self, params, x):
|
||||||
|
return params[0]+params[1]*x
|
||||||
|
|
||||||
|
class MySecondFunction(Function):
|
||||||
|
nparams = 2
|
||||||
|
param_list = [('C0', 'units1', 'Coeff1'),
|
||||||
|
('C1', 'units2', 'Coeff2')]
|
||||||
|
default_values = [0.0, 0.0]
|
||||||
|
delta_abs = [0.1, 0.1]
|
||||||
|
delta_rel = [0.01, 0.01]
|
||||||
|
min_values = [-np.inf, -np.inf]
|
||||||
|
max_values = [np.inf, np.inf]
|
||||||
|
free = [True, True]
|
||||||
|
|
||||||
|
def eval(self, params, x):
|
||||||
|
return params[0]+params[1]*x**2
|
||||||
|
|
||||||
|
class MyThirdFunction(Function):
|
||||||
|
nparams = 1
|
||||||
|
def eval(self, params, x):
|
||||||
|
arr = np.empty(x.size)
|
||||||
|
arr.fill(params[0])
|
||||||
|
return arr
|
||||||
|
|
||||||
|
class TestFunction(unittest.TestCase):
|
||||||
|
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
myfunc = MyFirstFunction()
|
||||||
|
self.assertEqual(myfunc.name, 'MyFirstFunction')
|
||||||
|
self.assertEqual(myfunc.nparams, 2)
|
||||||
|
self.assertEqual(myfunc.num_components, 1)
|
||||||
|
self.assertEqual(myfunc.param_list,
|
||||||
|
[('C0', 'units1', 'Coeff1'), ('C1', 'units2', 'Coeff2')])
|
||||||
|
self.assertEqual(myfunc.free, [True, True])
|
||||||
|
myfunc.free = [False, True]
|
||||||
|
self.assertEqual(myfunc.free, [False, True])
|
||||||
|
|
||||||
|
def test_eval(self):
|
||||||
|
myfunc = MyFirstFunction()
|
||||||
|
x = np.array([0.0, 1.0, 2.0])
|
||||||
|
params = [10.0, 1.0]
|
||||||
|
y = myfunc.eval(params, x)
|
||||||
|
self.assertCountEqual(y, np.array([10.0, 11.0, 12.0]))
|
||||||
|
|
||||||
|
def test_fit_eval(self):
|
||||||
|
myfunc = MyFirstFunction()
|
||||||
|
myfunc.free = [False, True]
|
||||||
|
myfunc.default_values = [-10.0, 0.0]
|
||||||
|
|
||||||
|
params = [1.0]
|
||||||
|
x = np.array([0.0, 1.0, 2.0])
|
||||||
|
y = myfunc.fit_eval(params, x)
|
||||||
|
self.assertCountEqual(y, np.array([-10.0, -9.0, -8.0]))
|
||||||
|
|
||||||
|
def test_parameter_bounds(self):
|
||||||
|
myfunc = MyFirstFunction()
|
||||||
|
myfunc.free = [False, True]
|
||||||
|
bounds = myfunc.parameter_bounds(apply_state=False)
|
||||||
|
self.assertEqual(bounds, [(-np.inf, np.inf), (-np.inf, np.inf)])
|
||||||
|
bounds = myfunc.parameter_bounds(apply_state=True)
|
||||||
|
self.assertEqual(bounds, [(-np.inf, np.inf)])
|
||||||
|
|
||||||
|
def test_integrate(self):
|
||||||
|
myfunc = MyFirstFunction()
|
||||||
|
params = [10.0, 1.0]
|
||||||
|
integral = myfunc.integrate(params, (1.0, 10.0), log=False, energy=False)
|
||||||
|
self.assertEqual(integral, 139.5)
|
||||||
|
integral = myfunc.integrate(params, (1.0, 10.0), log=True, energy=False)
|
||||||
|
self.assertEqual(integral, 139.5)
|
||||||
|
integral = myfunc.integrate(params, (1.0, 10.0), log=False, energy=True)
|
||||||
|
self.assertAlmostEqual(integral, 828.0*1.602e-9)
|
||||||
|
|
||||||
|
myfunc.free = [False, True]
|
||||||
|
integral = myfunc.integrate([1.0], (1.0, 10.0), log=False, energy=False)
|
||||||
|
self.assertAlmostEqual(integral, 49.5)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAdditiveSuperFunction(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
myfunc = MyFirstFunction() + MySecondFunction()
|
||||||
|
self.assertEqual(myfunc.name, 'MyFirstFunction+MySecondFunction')
|
||||||
|
self.assertEqual(myfunc.nparams, 4)
|
||||||
|
self.assertEqual(myfunc.num_components, 2)
|
||||||
|
self.assertEqual(myfunc.param_list,
|
||||||
|
[('MyFirstFunction: C0', 'units1', 'Coeff1'),
|
||||||
|
('MyFirstFunction: C1', 'units2', 'Coeff2'),
|
||||||
|
('MySecondFunction: C0', 'units1', 'Coeff1'),
|
||||||
|
('MySecondFunction: C1', 'units2', 'Coeff2')])
|
||||||
|
self.assertEqual(myfunc.free, [True]*4)
|
||||||
|
|
||||||
|
def test_eval(self):
|
||||||
|
myfunc = MyFirstFunction() + MySecondFunction()
|
||||||
|
x = np.array([0.0, 1.0, 2.0])
|
||||||
|
params = [10.0, 1.0, 10.0, 2.0]
|
||||||
|
y = myfunc.eval(params, x)
|
||||||
|
self.assertCountEqual(y, np.array([20.0, 23.0, 30.0]))
|
||||||
|
|
||||||
|
y = myfunc.eval(params, x, components=True)
|
||||||
|
self.assertCountEqual(y[0], np.array([10.0, 11.0, 12.0]))
|
||||||
|
self.assertCountEqual(y[1], np.array([10.0, 12.0, 18.0]))
|
||||||
|
|
||||||
|
|
||||||
|
class TestMultiplicativeSuperFunction(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
myfunc = MyFirstFunction() * MySecondFunction()
|
||||||
|
self.assertEqual(myfunc.name, 'MyFirstFunction*MySecondFunction')
|
||||||
|
self.assertEqual(myfunc.nparams, 4)
|
||||||
|
self.assertEqual(myfunc.num_components, 2)
|
||||||
|
self.assertEqual(myfunc.param_list,
|
||||||
|
[('MyFirstFunction: C0', 'units1', 'Coeff1'),
|
||||||
|
('MyFirstFunction: C1', 'units2', 'Coeff2'),
|
||||||
|
('MySecondFunction: C0', 'units1', 'Coeff1'),
|
||||||
|
('MySecondFunction: C1', 'units2', 'Coeff2')])
|
||||||
|
self.assertEqual(myfunc.free, [True]*4)
|
||||||
|
|
||||||
|
def test_eval(self):
|
||||||
|
myfunc = MyFirstFunction() * MySecondFunction()
|
||||||
|
x = np.array([0.0, 1.0, 2.0])
|
||||||
|
params = [10.0, 1.0, 10.0, 2.0]
|
||||||
|
y = myfunc.eval(params, x)
|
||||||
|
self.assertCountEqual(y, np.array([100., 132., 216.]))
|
||||||
|
|
||||||
|
y = myfunc.eval(params, x, components=True)
|
||||||
|
self.assertCountEqual(y[0], np.array([10.0, 11.0, 12.0]))
|
||||||
|
self.assertCountEqual(y[1], np.array([10.0, 12.0, 18.0]))
|
||||||
|
|
||||||
|
|
||||||
|
class TestMixedMultipleSuperFunctions(unittest.TestCase):
|
||||||
|
def test_attributes(self):
|
||||||
|
myfunc = (MyFirstFunction() + MySecondFunction()) * MyThirdFunction()
|
||||||
|
self.assertEqual(myfunc.name, 'MyFirstFunction+MySecondFunction*MyThirdFunction')
|
||||||
|
self.assertEqual(myfunc.nparams, 5)
|
||||||
|
self.assertEqual(myfunc.num_components, 3)
|
||||||
|
|
||||||
|
def test_eval(self):
|
||||||
|
myfunc = MyFirstFunction() + MySecondFunction() * MyThirdFunction()
|
||||||
|
x = np.array([0.0, 1.0, 2.0])
|
||||||
|
params = [10.0, 1.0, 10.0, 2.0, 100.0]
|
||||||
|
y = myfunc.eval(params, x)
|
||||||
|
self.assertCountEqual(y, np.array([2000.0, 2300.0, 3000.0]))
|
||||||
|
|
||||||
|
y = myfunc.eval(params, x, components=True)
|
||||||
|
self.assertCountEqual(y[0], np.array([10.0, 11.0, 12.0]))
|
||||||
|
self.assertCountEqual(y[1], np.array([10.0, 12.0, 18.0]))
|
||||||
|
self.assertCountEqual(y[2], np.array([100.0, 100.0, 100.0]))
|
||||||
|
|
||||||
|
class TestSpectralFunctions(unittest.TestCase):
|
||||||
|
energies = np.array([10.0, 100.0, 1000.0])
|
||||||
|
|
||||||
|
def test_powerlaw(self):
|
||||||
|
params = (0.1, -2.0, 100.0)
|
||||||
|
y = PowerLaw().eval(params, self.energies)
|
||||||
|
self.assertCountEqual(y, np.array([10.0, 0.10, 0.001]))
|
||||||
|
|
||||||
|
def test_comptonized(self):
|
||||||
|
params = (0.1, 200.0, -1.0, 100.0)
|
||||||
|
y = Comptonized().eval(params, self.energies)
|
||||||
|
true = np.array((0.951, 0.061, 6.74e-5))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_band(self):
|
||||||
|
params = (0.1, 200.0, -1.0, -2.3, 100.0)
|
||||||
|
y = Band().eval(params, self.energies)
|
||||||
|
true = np.array((0.951, 0.061, 4.73e-4))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_band_old(self):
|
||||||
|
params = (0.1, 200.0, -1.0, -2.3, 100.0)
|
||||||
|
y = BandOld().eval(params, self.energies)
|
||||||
|
true = np.array((0.951, 0.061, 4.73e-4))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_bpl(self):
|
||||||
|
params = (0.01, 700.0, -1.0, -2.0, 100.0)
|
||||||
|
y = BrokenPowerLaw().eval(params, self.energies)
|
||||||
|
true = np.array((0.100, 0.010, 7e-4))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_2bpl(self):
|
||||||
|
params = (0.01, 90.0, 500, -0.5, -1.0, -2.0, 100.0)
|
||||||
|
y = DoubleBrokenPowerLaw().eval(params, self.energies)
|
||||||
|
true = np.array((0.032, 0.009, 0.015))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_smoothly_bpl(self):
|
||||||
|
params = (0.01, 700.0, 0.3, -1.0, -2.0, 100.0)
|
||||||
|
y = SmoothlyBrokenPowerLaw().eval(params, self.energies)
|
||||||
|
true = np.array((0.100, 0.010, 6.3e-4))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_lognormal(self):
|
||||||
|
params = (0.1, 5.0, 1.0)
|
||||||
|
y = LogNormal().eval(params, self.energies)
|
||||||
|
true = np.array((1.0e-4, 3.7e-4, 6.5e-6))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_gauss_log(self):
|
||||||
|
params = (0.1, 100.0, 1.0)
|
||||||
|
y = GaussianLog().eval(params, self.energies)
|
||||||
|
true = np.array((0.006, 0.094, 0.006))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_gauss_log_fwhm(self):
|
||||||
|
params = (0.1, 100.0, 1.0, 0.1)
|
||||||
|
y = GaussianLogVaryingFWHM().eval(params, self.energies)
|
||||||
|
true = np.array((0.003, 0.094, 0.009))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_sunyaev_titarchuk(self):
|
||||||
|
params = (0.1, 30.0, 10.0, 3.0)
|
||||||
|
y = SunyaevTitarchuk().eval(params, self.energies)
|
||||||
|
true = np.array((0.003, 2e-40, 0.00))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_ottb(self):
|
||||||
|
params = (0.1, 30.0, 100.0)
|
||||||
|
y = OTTB().eval(params, self.energies)
|
||||||
|
true = np.array((20.086, 0.100, 9.4e-16))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_blackbody(self):
|
||||||
|
params = (0.1, 30.0)
|
||||||
|
y = BlackBody().eval(params, self.energies)
|
||||||
|
true = np.array((25.277, 36.994, 3.3e-10))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_yang_soong(self):
|
||||||
|
params = (0.1, -1.0, 200.0, 300.0, 50.0, 200.0, 50.0, 100.0)
|
||||||
|
y = YangSoongPulsar().eval(params, self.energies)
|
||||||
|
true = np.array((0.010, 0.100, 3.5e-6))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_tanaka(self):
|
||||||
|
params = (0.10, -1.0, 200.0, 1.0, 1.0, 1, 100.0, 50.0, 100.0)
|
||||||
|
y = TanakaPulsar().eval(params, self.energies)
|
||||||
|
true = np.array((0.004, 0.010, 0.002))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_otts(self):
|
||||||
|
params = (0.1, 100.0)
|
||||||
|
y = OTTS().eval(params, self.energies)
|
||||||
|
true = np.array((0.159, 0.272, 0.862))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_gaussline(self):
|
||||||
|
params = (0.1, 100.0, 8.0)
|
||||||
|
y = GaussLine().eval(params, self.energies)
|
||||||
|
true = np.array((5.6e-4, 5.6e-5, 0.0))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_low_cutoff(self):
|
||||||
|
params = (100.0, 10.0)
|
||||||
|
y = LowEnergyCutoff().eval(params, self.energies)
|
||||||
|
true = np.array((8.1e-7, 1.0, 1.0))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_high_cutoff(self):
|
||||||
|
params = (100.0, 100.0)
|
||||||
|
y = HighEnergyCutoff().eval(params, self.energies)
|
||||||
|
true = np.array((1., 1., 0.001))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_multiplicative_pl(self):
|
||||||
|
params = (-2.0, 100.0)
|
||||||
|
y = PowerLawMult().eval(params, self.energies)
|
||||||
|
true = np.array((100., 1.0, 0.01))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_multiplicative_gaussline(self):
|
||||||
|
params = (1.0, 10.0, 8.0)
|
||||||
|
y = GaussLineMult().eval(params, self.energies)
|
||||||
|
true = np.array((1.125, 1.0, 1.0))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
def test_multiplicative_lorentzline(self):
|
||||||
|
params = (1.0, 10.0, 8.0)
|
||||||
|
y = LorentzLineMult().eval(params, self.energies)
|
||||||
|
print(y)
|
||||||
|
true = np.array((1.016, 1.000, 1.0))
|
||||||
|
[self.assertAlmostEqual(y[i], true[i], places=3) for i in range(3)]
|
52
test/test_tcat.py
Normal file
52
test/test_tcat.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from gbm.data.tcat import Tcat
|
||||||
|
|
||||||
|
data_dir = 'data'
|
||||||
|
|
||||||
|
class TestTcat(TestCase):
|
||||||
|
filename = os.path.join(data_dir, 'glg_tcat_all_bn190222537_v01.fit')
|
||||||
|
time_range = (572532678.644472, 572533293.055776)
|
||||||
|
trigtime = 572532812.150778
|
||||||
|
location = (147.320, 60.9400, 1.50000)
|
||||||
|
azzen = (207.094, 142.186)
|
||||||
|
name = 'GRB190222537'
|
||||||
|
localizer = 'Fermi, GBM'
|
||||||
|
lonlat = (74.2500, -24.8500)
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
t = Tcat.open(self.filename)
|
||||||
|
self.assertEqual(len(t.headers), 1)
|
||||||
|
self.assertEqual(t.time_range, self.time_range)
|
||||||
|
self.assertEqual(t.trigtime, self.trigtime)
|
||||||
|
self.assertEqual(t.location, self.location)
|
||||||
|
self.assertEqual(t.location_fermi_frame, self.azzen)
|
||||||
|
self.assertEqual(t.name, self.name)
|
||||||
|
self.assertEqual(t.localizing_instrument, self.localizer)
|
||||||
|
self.assertEqual(t.fermi_location, self.lonlat)
|
254
test/test_time.py
Normal file
254
test/test_time.py
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
from unittest import TestCase
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from gbm.time import *
|
||||||
|
import csv
|
||||||
|
|
||||||
|
|
||||||
|
class TestTime(TestCase):
|
||||||
|
def test_bn(self):
|
||||||
|
# Test against historic BN numbers
|
||||||
|
with open("bn_test.csv", "r") as bn_file:
|
||||||
|
bn_reader = csv.DictReader(bn_file)
|
||||||
|
for row in bn_reader:
|
||||||
|
m = Met(float(row['trigger_time']))
|
||||||
|
msg = "For met = %f\n" \
|
||||||
|
"Expected : '%s'\n" \
|
||||||
|
"Actual : '%s'" % (m.met, row['burst_number'], m.bn)
|
||||||
|
self.assertEqual(m.bn, row['burst_number'], msg)
|
||||||
|
|
||||||
|
def test_unix_to_met(self):
|
||||||
|
unix = 1329307200 # Wed, 15 Feb 2012 12:00:00 GMT
|
||||||
|
expect = 351000002.000 # Wed, 15 Feb 2012 12:00:00 GMT
|
||||||
|
|
||||||
|
m = Met.from_unix(unix)
|
||||||
|
self.assertAlmostEqual(expect, m.met, places=6)
|
||||||
|
|
||||||
|
unix = 1244628900 # Wed, 10 Jun 2009 10:15:00 GMT
|
||||||
|
expect = 266321702.000 # Wed, 10 Jun 2009 10:15:00 GMT
|
||||||
|
|
||||||
|
m = Met.from_unix(unix)
|
||||||
|
self.assertAlmostEqual(expect, m.met, places=6)
|
||||||
|
|
||||||
|
unix = 1221084000 # Wed, 10 Sep 2008 22:00:00 GMT
|
||||||
|
expect = 242776801.000 # Wed, 10 Sep 2008 22:00:00 GMT
|
||||||
|
|
||||||
|
m = Met.from_unix(unix)
|
||||||
|
self.assertAlmostEqual(expect, m.met, places=6)
|
||||||
|
|
||||||
|
unix = 1461164288 # Wed, 20 Apr 2016 14:58:08 GMT
|
||||||
|
expect = 482857092.000 # Wed, 20 Apr 2016 14:58:08 GMT
|
||||||
|
|
||||||
|
m = Met.from_unix(unix)
|
||||||
|
self.assertAlmostEqual(expect, m.met, places=6)
|
||||||
|
|
||||||
|
def test_utc_to_met(self):
|
||||||
|
|
||||||
|
# Wed, 15 Feb 2012 12:00:00 GMT
|
||||||
|
utc = datetime.datetime(2012, 2, 15, 12, 0)
|
||||||
|
expect = 351000002.000
|
||||||
|
|
||||||
|
m = Met.from_datetime(utc)
|
||||||
|
self.assertAlmostEqual(expect, m.met, places=6)
|
||||||
|
|
||||||
|
# Wed, 10 Jun 2009 10:15:00 GMT
|
||||||
|
utc = datetime.datetime(2009, 6, 10, 10, 15)
|
||||||
|
expect = 266321702.000
|
||||||
|
|
||||||
|
m = Met.from_datetime(utc)
|
||||||
|
self.assertAlmostEqual(expect, m.met, places=6)
|
||||||
|
|
||||||
|
# Wed, 10 Sep 2008 22:00:00 GMT
|
||||||
|
utc = datetime.datetime(2008, 9, 10, 22, 00)
|
||||||
|
expect = 242776801.000
|
||||||
|
|
||||||
|
m = Met.from_datetime(utc)
|
||||||
|
self.assertAlmostEqual(expect, m.met, places=6)
|
||||||
|
|
||||||
|
# Wed, 20 Apr 2016 14:58:08 GMT
|
||||||
|
utc = datetime.datetime(2016, 4, 20, 14, 58, 8)
|
||||||
|
expect = 482857092.000
|
||||||
|
|
||||||
|
m = Met.from_datetime(utc)
|
||||||
|
self.assertAlmostEqual(expect, m.met, places=6)
|
||||||
|
|
||||||
|
def test_now(self):
|
||||||
|
|
||||||
|
met = Met.now()
|
||||||
|
print(met)
|
||||||
|
|
||||||
|
def test_frac_of_day(self):
|
||||||
|
d = datetime.datetime(2016, 1, 1, 12, 30)
|
||||||
|
self.assertEqual(hms_to_fraction_of_day(d), 521)
|
||||||
|
|
||||||
|
def check_leap_second(self, beg_next_day_utc, beg_next_day_met):
|
||||||
|
# Test utc->met
|
||||||
|
self.assertAlmostEqual(Met.from_unix(beg_next_day_utc - 2.0).met, beg_next_day_met - 3.0, places=6,
|
||||||
|
msg="Unix->Met m:58 seconds")
|
||||||
|
self.assertAlmostEqual(Met.from_unix(beg_next_day_utc - 1.0).met, beg_next_day_met - 2.0, places=6,
|
||||||
|
msg="Unix->Met m:59 seconds")
|
||||||
|
# (beg_next_day_met - 1) = No Unix 60 seconds
|
||||||
|
self.assertAlmostEqual(Met.from_unix(beg_next_day_utc).met, beg_next_day_met, places=6,
|
||||||
|
msg="Unix->Met m+1:00 seconds")
|
||||||
|
self.assertAlmostEqual(Met.from_unix(beg_next_day_utc + 1.0).met, beg_next_day_met + 1.0, places=6,
|
||||||
|
msg="Unix->Met m+1:01 seconds")
|
||||||
|
|
||||||
|
# Test met->utc
|
||||||
|
self.assertAlmostEqual(Met(beg_next_day_met - 3.0).unix, beg_next_day_utc - 2.0, places=6,
|
||||||
|
msg="Met->Unix m:58 seconds")
|
||||||
|
self.assertAlmostEqual(Met(beg_next_day_met - 2.0).unix, beg_next_day_utc - 1.0, places=6,
|
||||||
|
msg="Met->Unix m:59 seconds")
|
||||||
|
self.assertAlmostEqual(Met(beg_next_day_met - 1.0).unix, beg_next_day_utc - 1.0, places=6,
|
||||||
|
msg="Met->Unix m:60 seconds (repeat :59)")
|
||||||
|
self.assertAlmostEqual(Met(beg_next_day_met).unix, beg_next_day_utc, places=6,
|
||||||
|
msg="Met->Unix m+1:00 seconds")
|
||||||
|
self.assertAlmostEqual(Met(beg_next_day_met + 1.0).unix, beg_next_day_utc + 1.0, places=6,
|
||||||
|
msg="Met->Unix m+1:01 seconds")
|
||||||
|
|
||||||
|
def test_2017_leap_second(self):
|
||||||
|
self.check_leap_second(1483228800.0, 504921605.0)
|
||||||
|
|
||||||
|
def test_2015_leap_second(self):
|
||||||
|
self.check_leap_second(1435708800.0, 457401604.0)
|
||||||
|
|
||||||
|
def test_2012_leap_second(self):
|
||||||
|
self.check_leap_second(1341100800.0, 362793603.0)
|
||||||
|
|
||||||
|
def test_2008_leap_second(self):
|
||||||
|
self.check_leap_second(1230768000.0, 252460802.0)
|
||||||
|
|
||||||
|
def test_2005_leap_second(self):
|
||||||
|
self.check_leap_second(1136073600.0, 157766401.0)
|
||||||
|
|
||||||
|
def test_met_to_gps(self):
|
||||||
|
# Time values from HEASARC's xTime website were used with the exception of GPS time.
|
||||||
|
# LIGO's converter webpage was used to convert UTC (from MET) to GPS time
|
||||||
|
|
||||||
|
met = Met(157766410) # 2006-01-01 00:00:09.000 UTC
|
||||||
|
self.assertAlmostEqual(met.gps, 820108823, places=6)
|
||||||
|
|
||||||
|
met = Met(252460802) # 2009-01-01 00:00:00.000 UTC
|
||||||
|
self.assertAlmostEqual(met.gps, 914803215, places=6)
|
||||||
|
|
||||||
|
met = Met(362793605) # 2012-07-01 00:00:02.000 UTC
|
||||||
|
self.assertAlmostEqual(met.gps, 1025136018, places=6)
|
||||||
|
|
||||||
|
met = Met(457401613) # 2015-07-01 00:00:09.000 UTC
|
||||||
|
self.assertAlmostEqual(met.gps, 1119744026, places=6)
|
||||||
|
|
||||||
|
met = Met(506174405) # 2017-01-15 12:00:00.000 UTC
|
||||||
|
self.assertAlmostEqual(met.gps, 1168516818, places=6)
|
||||||
|
|
||||||
|
def test_gps_to_met(self):
|
||||||
|
# LIGO's converter webpage was used to convert UTC to GPS time
|
||||||
|
# HEASARC's converter webpage was used to convert UTC to MET time
|
||||||
|
|
||||||
|
met = Met.from_gps(825598814.0) # 2006-03-05 13:00:00.000 UTC
|
||||||
|
self.assertAlmostEqual(met.met, 163256401.0, places=6)
|
||||||
|
|
||||||
|
met = Met.from_gps(934369815.0) # 2009-08-15 11:10:00.000 UTC
|
||||||
|
self.assertAlmostEqual(met.met, 272027402.0, places=6)
|
||||||
|
|
||||||
|
met = Met.from_gps(1030610731.0) # 2012-09-02 08:45:15.000 UTC
|
||||||
|
self.assertAlmostEqual(met.met, 368268318.0, places=6)
|
||||||
|
|
||||||
|
met = Met.from_gps(1113102199.0) # 2015-04-15 03:03:03.000 UTC
|
||||||
|
self.assertAlmostEqual(met.met, 450759786.0, places=6)
|
||||||
|
|
||||||
|
met = Met.from_gps(1180827063.0) # 2017-06-06 23:30:45.000 UTC
|
||||||
|
self.assertAlmostEqual(met.met, 518484650.0, places=6)
|
||||||
|
|
||||||
|
with self.assertRaises(Exception):
|
||||||
|
Met.from_gps(662342412.0)
|
||||||
|
|
||||||
|
def test_datetime_range_from(self):
|
||||||
|
dates = hours_range_from(5, dt=datetime.datetime(2016, 8, 2, 2, 15))
|
||||||
|
expect = [
|
||||||
|
datetime.datetime(2016, 8, 1, 22, 0),
|
||||||
|
datetime.datetime(2016, 8, 1, 23, 0),
|
||||||
|
datetime.datetime(2016, 8, 2, 0, 0),
|
||||||
|
datetime.datetime(2016, 8, 2, 1, 0),
|
||||||
|
datetime.datetime(2016, 8, 2, 2, 0),
|
||||||
|
]
|
||||||
|
count = 0
|
||||||
|
for d in dates:
|
||||||
|
self.assertEqual(d, expect[count])
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
def test_date_range_from(self):
|
||||||
|
dates = dates_range_from(5, dt=datetime.date(2016, 8, 2))
|
||||||
|
expect = [
|
||||||
|
datetime.date(2016, 7, 29),
|
||||||
|
datetime.date(2016, 7, 30),
|
||||||
|
datetime.date(2016, 7, 31),
|
||||||
|
datetime.date(2016, 8, 1),
|
||||||
|
datetime.date(2016, 8, 2),
|
||||||
|
]
|
||||||
|
count = 0
|
||||||
|
for d in dates:
|
||||||
|
self.assertEqual(d, expect[count])
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
def test_inclusive_date_range_inc(self):
|
||||||
|
expect = [
|
||||||
|
datetime.date(2016, 7, 29),
|
||||||
|
datetime.date(2016, 7, 30),
|
||||||
|
datetime.date(2016, 7, 31),
|
||||||
|
datetime.date(2016, 8, 1),
|
||||||
|
datetime.date(2016, 8, 2),
|
||||||
|
]
|
||||||
|
dates = inclusive_date_range(expect[0], expect[-1])
|
||||||
|
self.assertEqual(dates, expect)
|
||||||
|
|
||||||
|
def test_inclusive_date_range_dec(self):
|
||||||
|
expect = [
|
||||||
|
datetime.date(2016, 8, 2),
|
||||||
|
datetime.date(2016, 8, 1),
|
||||||
|
datetime.date(2016, 7, 31),
|
||||||
|
datetime.date(2016, 7, 30),
|
||||||
|
datetime.date(2016, 7, 29),
|
||||||
|
]
|
||||||
|
dates = inclusive_date_range(expect[0], expect[-1], datetime.timedelta(days=-1))
|
||||||
|
self.assertEqual(dates, expect)
|
||||||
|
|
||||||
|
def test_dates_from_hours(self):
|
||||||
|
|
||||||
|
expect = [
|
||||||
|
datetime.date(2016, 7, 29),
|
||||||
|
datetime.date(2016, 7, 30),
|
||||||
|
datetime.date(2016, 7, 31),
|
||||||
|
datetime.date(2016, 8, 1),
|
||||||
|
datetime.date(2016, 8, 2),
|
||||||
|
]
|
||||||
|
|
||||||
|
hours = inclusive_date_range(datetime.datetime(2016, 7, 29, 0, 0), datetime.datetime(2016, 8, 2, 23, 0),
|
||||||
|
step=datetime.timedelta(hours=1))
|
||||||
|
dates = dates_from_hours(hours)
|
||||||
|
self.assertEqual(dates, expect)
|
||||||
|
|
||||||
|
|
179
test/test_trigdat.py
Normal file
179
test/test_trigdat.py
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
#
|
||||||
|
# Authors: William Cleveland (USRA),
|
||||||
|
# Adam Goldstein (USRA) and
|
||||||
|
# Daniel Kocevski (NASA)
|
||||||
|
#
|
||||||
|
# Portions of the code are Copyright 2020 William Cleveland and
|
||||||
|
# Adam Goldstein, Universities Space Research Association
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Written for the Fermi Gamma-ray Burst Monitor (Fermi-GBM)
|
||||||
|
#
|
||||||
|
# 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from gbm.data.trigdat import Trigdat
|
||||||
|
from gbm.data.tcat import Tcat
|
||||||
|
#from gbm.binning.unbinned import bin_by_time
|
||||||
|
#from gbm.binning.binned import combine_by_factor
|
||||||
|
|
||||||
|
data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data')
|
||||||
|
|
||||||
|
class TestTrigdat(TestCase):
|
||||||
|
filename = os.path.join(data_dir, 'glg_trigdat_all_bn170101116_v01.fit')
|
||||||
|
trigtime = 504931642.867272
|
||||||
|
header_names = ['PRIMARY', 'TRIGRATE', 'BCKRATES', 'OB_CALC', 'MAXRATES', 'EVNTRATE']
|
||||||
|
num_maxrates = 3
|
||||||
|
time_range = (504931505.008880, 504932119.419874)
|
||||||
|
test_time = 504931642.739272 #trigger 170101.116
|
||||||
|
|
||||||
|
def test_attributes(self):
|
||||||
|
t = Trigdat.open(self.filename)
|
||||||
|
self.assertEqual(t.trigtime, self.trigtime)
|
||||||
|
self.assertEqual(t.is_gbm_file, True)
|
||||||
|
self.assertEqual(t.id, '170101116')
|
||||||
|
self.assertEqual(t.filename, os.path.basename(self.filename))
|
||||||
|
self.assertEqual(t.is_trigger, True)
|
||||||
|
self.assertEqual(t.detector, 'all')
|
||||||
|
self.assertEqual(t.datatype, 'TRIGDAT')
|
||||||
|
self.assertCountEqual(t.headers.keys(), self.header_names)
|
||||||
|
self.assertEqual(t.num_maxrates, self.num_maxrates)
|
||||||
|
self.assertAlmostEqual(t.time_range[0], self.time_range[0], places=6)
|
||||||
|
self.assertAlmostEqual(t.time_range[1], self.time_range[1], places=6)
|
||||||
|
self.assertEqual(t.maxrates[0].numchans, 8)
|
||||||
|
self.assertEqual(t.maxrates[0].numdets, 14)
|
||||||
|
self.assertEqual(t.maxrates[0].time_range, (504931642.227346, 504931646.323346))
|
||||||
|
self.assertAlmostEqual(t.maxrates[0].timescale, 4.096)
|
||||||
|
self.assertEqual(t.backrates.numchans, 8)
|
||||||
|
self.assertEqual(t.backrates.numdets, 14)
|
||||||
|
self.assertEqual(t.backrates.time_range, (504931609.913434, 504931642.68143404))
|
||||||
|
loc = t.fsw_locations[0]
|
||||||
|
self.assertEqual(loc.time, 504931644.275308)
|
||||||
|
self.assertAlmostEqual(loc.location[0], 64.25, places=4)
|
||||||
|
self.assertAlmostEqual(loc.location[1], 8.55, places=4)
|
||||||
|
self.assertAlmostEqual(loc.location[2], 8.016666, places=4)
|
||||||
|
self.assertEqual(loc.top_classification[0], 'GRB')
|
||||||
|
self.assertAlmostEqual(loc.top_classification[1], 0.854902, places=4)
|
||||||
|
self.assertEqual(loc.next_classification[0], 'GROJ422')
|
||||||
|
self.assertAlmostEqual(loc.next_classification[1], 0.1019608, places=4)
|
||||||
|
self.assertAlmostEqual(loc.intensity, 277.1126, places=4)
|
||||||
|
self.assertAlmostEqual(loc.hardness_ratio, 0.5011158, places=4)
|
||||||
|
self.assertAlmostEqual(loc.fluence, 0, places=4)
|
||||||
|
self.assertAlmostEqual(loc.significance, 19.9, places=4)
|
||||||
|
self.assertAlmostEqual(loc.timescale, 2.048, places=4)
|
||||||
|
self.assertEqual(loc.spectrum, 'soft')
|
||||||
|
self.assertEqual(loc.location_sc, (130, 55))
|
||||||
|
self.assertEqual(t.triggered_detectors, ['n9', 'na', 'nb'])
|
||||||
|
|
||||||
|
def test_get_maxrates(self):
|
||||||
|
t = Trigdat.open(self.filename)
|
||||||
|
self.assertEqual(t.get_maxrates(1), t.maxrates[1])
|
||||||
|
self.assertEqual(t.get_fsw_locations(1), t.fsw_locations[1])
|
||||||
|
|
||||||
|
def test_to_ctime(self):
|
||||||
|
t = Trigdat.open(self.filename)
|
||||||
|
c = t.to_ctime('n5')
|
||||||
|
self.assertEqual(c.detector, 'n5')
|
||||||
|
self.assertEqual(c.datatype, 'CTIME')
|
||||||
|
self.assertAlmostEqual(c.time_range[0], t.time_range[0]-t.trigtime)
|
||||||
|
self.assertAlmostEqual(c.time_range[1], t.time_range[1]-t.trigtime)
|
||||||
|
self.assertEqual(c.trigtime, t.trigtime)
|
||||||
|
self.assertEqual(c.numchans, 8)
|
||||||
|
|
||||||
|
def test_sum_detectors(self):
|
||||||
|
t = Trigdat.open(self.filename)
|
||||||
|
c1 = t.to_ctime('n5')
|
||||||
|
c2 = t.sum_detectors(['n5', 'n5'])
|
||||||
|
self.assertCountEqual(c1.data.counts.flatten()*2,
|
||||||
|
c2.data.counts.flatten())
|
||||||
|
|
||||||
|
def test_interpolators(self):
|
||||||
|
test_eic = np.array([-1572., 6370., 2164.])
|
||||||
|
test_quat = np.array([0.2229096, 0.06231983, 0.5392869, -0.8096896])
|
||||||
|
test_lat, test_lon = 18.23, 321.02
|
||||||
|
test_alt = np.sqrt(np.sum(test_eic**2))-6371.0 # simple altitude calc
|
||||||
|
test_vel = np.array([-6820.45, -2465.3848, 2270.4202])/1000.0
|
||||||
|
test_angvel = np.array([0.000706, 6.29781e-06, -0.000714])
|
||||||
|
test_geoloc = np.array([283.85, -18.25])
|
||||||
|
test_earth_radius = 67.335
|
||||||
|
test_mcilwain = 1.22
|
||||||
|
|
||||||
|
p = Trigdat.open(self.filename)
|
||||||
|
|
||||||
|
eic = p.get_eic(self.test_time)/1000.0
|
||||||
|
[self.assertAlmostEqual(eic[i], test_eic[i], delta=5.0) for i in range(3)]
|
||||||
|
quat = p.get_quaternions(self.test_time)
|
||||||
|
[self.assertAlmostEqual(quat[i], test_quat[i], places=2) for i in range(4)]
|
||||||
|
|
||||||
|
lat = p.get_latitude(self.test_time)
|
||||||
|
lon = p.get_longitude(self.test_time)
|
||||||
|
alt = p.get_altitude(self.test_time)/1000.0
|
||||||
|
self.assertAlmostEqual(lat, test_lat, delta=1.0)
|
||||||
|
self.assertAlmostEqual(lon, test_lon, delta=1.0)
|
||||||
|
self.assertAlmostEqual(alt, test_alt, delta=5.0)
|
||||||
|
|
||||||
|
vel = p.get_velocity(self.test_time)
|
||||||
|
[self.assertAlmostEqual(vel[i]/1000.0, test_vel[i], delta=1.0) for i in range(3)]
|
||||||
|
angvel = p.get_angular_velocity(self.test_time)
|
||||||
|
[self.assertAlmostEqual(angvel[i], test_angvel[i], places=3) for i in range(3)]
|
||||||
|
|
||||||
|
geo_radec = p.get_geocenter_radec(self.test_time)
|
||||||
|
self.assertAlmostEqual(geo_radec[0], test_geoloc[0], delta=0.1)
|
||||||
|
self.assertAlmostEqual(geo_radec[1], test_geoloc[1], delta=0.1)
|
||||||
|
geo_radius = p.get_earth_radius(self.test_time)
|
||||||
|
self.assertAlmostEqual(geo_radius, test_earth_radius, delta=0.01)
|
||||||
|
|
||||||
|
sun_visible = p.get_sun_visibility(self.test_time)
|
||||||
|
self.assertEqual(sun_visible, False)
|
||||||
|
in_saa = p.get_saa_passage(self.test_time)
|
||||||
|
self.assertEqual(in_saa, False)
|
||||||
|
ml = p.get_mcilwain_l(self.test_time)
|
||||||
|
self.assertAlmostEqual(ml, test_mcilwain, delta=0.01)
|
||||||
|
|
||||||
|
def test_coordinate_conversions(self):
|
||||||
|
test_ra, test_dec = 70.64, -1.58
|
||||||
|
test_az, test_zen = 138.0, 65.0
|
||||||
|
test_n0_ra, test_n0_dec = 30.908, 57.679
|
||||||
|
test_n0_angle = 67.42
|
||||||
|
|
||||||
|
p = Trigdat.open(self.filename)
|
||||||
|
|
||||||
|
az, zen = p.to_fermi_frame(test_ra, test_dec, self.test_time)
|
||||||
|
self.assertAlmostEqual(az, test_az, delta=1.0)
|
||||||
|
self.assertAlmostEqual(zen, test_zen, delta=1.0)
|
||||||
|
|
||||||
|
ra, dec = p.to_equatorial(test_az, test_zen, self.test_time)
|
||||||
|
self.assertAlmostEqual(ra, test_ra, delta=1.0)
|
||||||
|
self.assertAlmostEqual(dec, test_dec, delta=1.0)
|
||||||
|
|
||||||
|
loc_vis = p.location_visible(test_ra, test_dec, self.test_time)
|
||||||
|
self.assertEqual(loc_vis, True)
|
||||||
|
|
||||||
|
ra, dec = p.detector_pointing('n0', self.test_time)
|
||||||
|
self.assertAlmostEqual(ra, test_n0_ra, delta=0.5)
|
||||||
|
self.assertAlmostEqual(dec, test_n0_dec, delta=0.5)
|
||||||
|
angle = p.detector_angle(test_ra, test_dec, 'n0', self.test_time)
|
||||||
|
self.assertAlmostEqual(angle, test_n0_angle, delta=0.5)
|
||||||
|
|
||||||
|
def test_errors(self):
|
||||||
|
with self.assertRaises(IOError):
|
||||||
|
t = Trigdat.open('deschain.fit')
|
||||||
|
|
||||||
|
t = Trigdat.open(self.filename)
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
t.to_ctime('b4')
|
||||||
|
t.to_ctime('n0', timescale=123)
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user