gbm data tools 1.1.1

This commit is contained in:
liuyihui 2022-07-15 07:36:07 +00:00
commit 95923512bc
103 changed files with 169206 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# cache
__pycache__/

BIN
McIlwainL_Coeffs.npy Normal file

Binary file not shown.

45
__init__.py Normal file
View 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
View File

@ -0,0 +1 @@
from .background import BackgroundFitter, BackgroundRates, BackgroundSpectrum

587
background/background.py Normal file
View 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
View 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
View 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
View File

243
binning/binned.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

790
data/headers.py Normal file
View 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

File diff suppressed because it is too large Load Diff

658
data/pha.py Normal file
View 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

File diff suppressed because it is too large Load Diff

615
data/poshist.py Normal file
View 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

File diff suppressed because it is too large Load Diff

1153
data/scat.py Normal file

File diff suppressed because it is too large Load Diff

113
data/tcat.py Normal file
View 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
View 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
View 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
View 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")

1157
finder.py Normal file

File diff suppressed because it is too large Load Diff

0
lookup/__init__.py Normal file
View File

227
lookup/apply.py Normal file
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

47
plot/globals.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,2 @@
from .pha import PhaSimulator
from .tte import TteSourceSimulator, TteBackgroundSimulator

388
simulate/generators.py Normal file
View 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
View 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
View 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
View 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
View File

1296
spectra/fitting.py Normal file

File diff suppressed because it is too large Load Diff

1255
spectra/functions.py Normal file

File diff suppressed because it is too large Load Diff

0
test/__init__.py Normal file
View File

View 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
}
}
}
}

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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

View 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

Binary file not shown.

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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)

View 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
View 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
View 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
View 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