1154 lines
44 KiB
Python
1154 lines
44 KiB
Python
# scat.py: GBM SCAT 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 numpy as np
|
|
import astropy.io.fits as fits
|
|
from . import headers as hdr
|
|
from gbm.time import Met
|
|
from .data import DataFile
|
|
|
|
|
|
class Parameter:
|
|
"""A fit parameter class
|
|
|
|
Parameters:
|
|
value (float): The central fit value
|
|
uncert (float or 2-tuple): The 1-sigma uncertainty. If a 2-tuple, then
|
|
is of the form (low, high)
|
|
name (str, optional): The name of the parameter
|
|
units (str, optional): The units of the parameter
|
|
support (2-tuple, optional): The valid support of the parameter
|
|
|
|
Attributes:
|
|
name (str): The name of the parameter
|
|
support (2-tuple): The valid support of the parameter
|
|
uncertainty (2-tuple): The 1-sigma uncertainty
|
|
units (str): The units of the parameter
|
|
value (float): The central fit value
|
|
"""
|
|
def __init__(self, value, uncert, name='', units=None,
|
|
support=(-np.inf, np.inf)):
|
|
|
|
self._value = float(value)
|
|
if isinstance(uncert, (tuple, list)):
|
|
if len(uncert) == 2:
|
|
pass
|
|
elif len(uncert) == 1:
|
|
uncert = (uncert[0], uncert[0])
|
|
else:
|
|
raise ValueError('uncertainty must be a 1- or 2-tuple')
|
|
elif isinstance(uncert, float):
|
|
uncert = (uncert, uncert)
|
|
else:
|
|
raise TypeError('uncertainty must be a float or 1- or 2-tuple')
|
|
self._uncert = uncert
|
|
|
|
self._units = units
|
|
self._name = name
|
|
self._support = support
|
|
|
|
@property
|
|
def value(self):
|
|
return self._value
|
|
@property
|
|
def uncertainty(self):
|
|
return self._uncert
|
|
@property
|
|
def name(self):
|
|
return self._name
|
|
@property
|
|
def units(self):
|
|
return self._units
|
|
@property
|
|
def support(self):
|
|
return self._support
|
|
|
|
def __str__(self):
|
|
value, uncertainty = self._str_format()
|
|
|
|
if uncertainty[0] == uncertainty[1]:
|
|
s = '+/- {0}'.format(uncertainty[0])
|
|
else:
|
|
s = '+{0}/-{1}'.format(uncertainty[0], uncertainty[1])
|
|
if self.units is None:
|
|
return '{0}: {1} {2}'.format(self.name, value, s)
|
|
else:
|
|
return '{0}: {1} {2} {3}'.format(self.name, value, s, self.units)
|
|
|
|
def _str_format(self):
|
|
if (self.value > 0.005) and (self.uncertainty[0] > 0.005):
|
|
value = '{0:.2f}'.format(self.value)
|
|
uncertainty = tuple(
|
|
['{0:.2f}'.format(u) for u in self.uncertainty])
|
|
else:
|
|
value = '{0:.2e}'.format(self.value)
|
|
val_coeff, val_exp = value.split('e')
|
|
val_exp = int(val_exp)
|
|
uncertainty = ['{0:.2e}'.format(u) for u in self.uncertainty]
|
|
uncert_coeff = []
|
|
uncert_exp = []
|
|
for uncert in uncertainty:
|
|
uncert_coeff.append(uncert.split('e')[0])
|
|
uncert_exp.append(int(uncert.split('e')[1]))
|
|
return (value, uncertainty)
|
|
|
|
def valid_value(self):
|
|
"""Check if the parameter value is within the allowed parameter range
|
|
"""
|
|
if (self.value >= self.support[0]) and \
|
|
(self.value <= self.support[1]):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def one_sigma_range(self):
|
|
"""Return the 1 sigma range of the parameter fit
|
|
"""
|
|
return (self.value - self.uncertainty[0],
|
|
self.value + self.uncertainty[1])
|
|
|
|
def to_fits_value(self):
|
|
"""Return as a tuple to be used for a FITS file
|
|
|
|
Returns:
|
|
(tuple): 2-value tuple (value, uncertainty) or 3-value tuple
|
|
(value, +uncertainty, -uncertainty)
|
|
"""
|
|
return (self.value, *self.uncertainty[::-1])
|
|
|
|
class PhotonFlux(Parameter):
|
|
"""A photon flux. Inherits from :class:`Parameter`.
|
|
|
|
Parameters:
|
|
value (float): The central flux value
|
|
uncert (float or 2-tuple): The 1-sigma uncertainty
|
|
energy_range (tuple): A 2-tuple (low, high) for the energy range
|
|
|
|
Attributes:
|
|
energy_range (tuple): The enery range (low, high)
|
|
name (str): 'Photon Flux'
|
|
support (2-tuple): (0.0, np.inf)
|
|
uncertainty (2-tuple): The 1-sigma uncertainty
|
|
units (str): 'ph/cm^2/s'
|
|
value (float): The central flux value
|
|
"""
|
|
def __init__(self, value, uncert, energy_range):
|
|
super().__init__(value, uncert, name='Photon Flux', units='ph/cm^2/s',
|
|
support=(0.0, np.inf))
|
|
self._energy_range = energy_range
|
|
|
|
@property
|
|
def energy_range(self):
|
|
return self._energy_range
|
|
|
|
class PhotonFluence(Parameter):
|
|
"""A photon fluence. Inherits from :class:`Parameter`.
|
|
|
|
Parameters:
|
|
value (float): The central fluence value
|
|
uncert (float or 2-tuple): The 1-sigma uncertainty
|
|
energy_range (tuple): A 2-tuple (low, high) for the energy range
|
|
|
|
Attributes:
|
|
energy_range (tuple): The enery range (low, high)
|
|
name (str): 'Photon Fluence'
|
|
support (2-tuple): (0.0, np.inf)
|
|
uncertainty (2-tuple): The 1-sigma uncertainty
|
|
units (str): 'ph/cm^2'
|
|
value (float): The central fluence value
|
|
"""
|
|
def __init__(self, value, uncert, energy_range):
|
|
super().__init__(value, uncert, name='Photon Fluence', units='ph/cm^2',
|
|
support=(0.0, np.inf))
|
|
self._energy_range = energy_range
|
|
|
|
@property
|
|
def energy_range(self):
|
|
return self._energy_range
|
|
|
|
class EnergyFlux(Parameter):
|
|
"""An energy flux. Inherits from :class:`Parameter`.
|
|
|
|
Parameters:
|
|
value (float): The central flux value
|
|
uncert (float or 2-tuple): The 1-sigma uncertainty
|
|
energy_range (tuple): A 2-tuple (low, high) for the energy range
|
|
|
|
Attributes:
|
|
energy_range (tuple): The enery range (low, high)
|
|
name (str): 'Energy Flux'
|
|
support (2-tuple): (0.0, np.inf)
|
|
uncertainty (2-tuple): The 1-sigma uncertainty
|
|
units (str): 'erg/cm^2/s'
|
|
value (float): The central flux value
|
|
"""
|
|
def __init__(self, value, uncert, energy_range):
|
|
super().__init__(value, uncert, name='Energy Flux', units='erg/cm^2/s',
|
|
support=(0.0, np.inf))
|
|
self._energy_range = energy_range
|
|
|
|
@property
|
|
def energy_range(self):
|
|
return self._energy_range
|
|
|
|
class EnergyFluence(Parameter):
|
|
"""An energy fluence. Inherits from :class:`Parameter`.
|
|
|
|
Parameters:
|
|
value (float): The central fluence value
|
|
uncert (float or 2-tuple): The 1-sigma uncertainty
|
|
energy_range (tuple): A 2-tuple (low, high) for the energy range
|
|
|
|
Attributes:
|
|
energy_range (tuple): The enery range (low, high)
|
|
name (str): 'Energy Fluence'
|
|
support (2-tuple): (0.0, np.inf)
|
|
uncertainty (2-tuple): The 1-sigma uncertainty
|
|
units (str): 'erg/cm^2'
|
|
value (float): The central fluence value
|
|
"""
|
|
def __init__(self, value, uncert, energy_range):
|
|
super().__init__(value, uncert, name='Energy Fluence', units='erg/cm^2',
|
|
support=(0.0, np.inf))
|
|
self._energy_range = energy_range
|
|
|
|
@property
|
|
def energy_range(self):
|
|
return self._energy_range
|
|
|
|
|
|
class ModelFit:
|
|
"""A container for the info from a model fit
|
|
|
|
Parameters:
|
|
name (str): The name of the model
|
|
time_range (float, float): The time range of the model fit, (low, high)
|
|
parameters (list, optional): A list of model parameters
|
|
photon_flux (:class:`PhotonFlux`, optional): The photon flux
|
|
energy_flux (:class:`EnergyFlux`, optional): The energy flux
|
|
photon_fluence (:class:`PhotonFluence`, optional): The photon fluence
|
|
energy_fluence (:class:`EnergyFluence`, optional): The energy fluence
|
|
flux_energy_range (tuple, optional): The energy range of the flux
|
|
and fluence, (low, high)
|
|
stat_name (str, optional): The name of the fit statistic
|
|
stat_value (float, optional): The fit statistic value
|
|
dof (int, optional): The degrees-of-freedom of the fit
|
|
covariance (np.array, optional): The covariance matrix of the fit
|
|
|
|
Attributes:
|
|
covariance (np.array): The covariance matrix of the fit
|
|
dof (int): The degrees-of-freedom of the fit
|
|
energy_fluence (:class:`EnergyFluence`): The energy fluence
|
|
energy_flux (:class:`EnergyFlux``): The energy flux
|
|
flux_energy_range (tuple): The energy range of the flux and fluence,
|
|
(low, high)
|
|
name (str): The name of the model
|
|
parameters (list): A list of model parameters
|
|
photon_fluence (:class:`PhotonFluence`): The photon fluence
|
|
photon_flux (:class:`PhotonFlux`): The photon flux
|
|
stat_name (str): The name of the fit statistic
|
|
stat_value (float): The fit statistic value
|
|
time_range (float, float): The time range of the model fit, (low, high)
|
|
"""
|
|
def __init__(self, name, time_range, **kwargs):
|
|
self._name = str(name)
|
|
if not isinstance(time_range, (list, tuple)):
|
|
raise ValueError('time_range must be a 2-tuple')
|
|
else:
|
|
if len(time_range) != 2:
|
|
raise ValueError('time_range must be a 2-tuple')
|
|
self._time_range = time_range
|
|
|
|
self._parameters = []
|
|
self._photon_flux = None
|
|
self._energy_flux = None
|
|
self._photon_fluence = None
|
|
self._energy_fluence = None
|
|
self._flux_energy_range = None
|
|
self._stat_name = None
|
|
self._stat_value = None
|
|
self._dof = None
|
|
self._covariance = None
|
|
|
|
self._init_by_dict(kwargs)
|
|
|
|
def __str__(self):
|
|
param_str = '\n '.join([str(param) for param in self.parameters])
|
|
return '{0}\n {1}'.format(self.name, param_str)
|
|
|
|
@property
|
|
def name(self):
|
|
return self._name
|
|
@property
|
|
def time_range(self):
|
|
return self._time_range
|
|
|
|
@property
|
|
def parameters(self):
|
|
return self._parameters
|
|
@parameters.setter
|
|
def parameters(self, val):
|
|
if not isinstance(val, (list, tuple)):
|
|
raise TypeError('parameters must be a list of parameters')
|
|
for p in val:
|
|
if not isinstance(p, Parameter):
|
|
raise TypeError('parameters must be of Parameter type')
|
|
self._parameters = val
|
|
|
|
@property
|
|
def photon_flux(self):
|
|
return self._photon_flux
|
|
@photon_flux.setter
|
|
def photon_flux(self, val):
|
|
if not isinstance(val, Parameter):
|
|
raise TypeError('photon_flux must be of Parameter type')
|
|
self._photon_flux = val
|
|
|
|
@property
|
|
def energy_flux(self):
|
|
return self._energy_flux
|
|
@energy_flux.setter
|
|
def energy_flux(self, val):
|
|
if not isinstance(val, Parameter):
|
|
raise TypeError('energy_flux must be of Parameter type')
|
|
self._energy_flux = val
|
|
|
|
@property
|
|
def photon_fluence(self):
|
|
return self._photon_fluence
|
|
@photon_fluence.setter
|
|
def photon_fluence(self, val):
|
|
if not isinstance(val, Parameter):
|
|
raise TypeError('photon_fluence must be of Parameter type')
|
|
self._photon_fluence = val
|
|
|
|
@property
|
|
def energy_fluence(self):
|
|
return self._energy_fluence
|
|
@energy_fluence.setter
|
|
def energy_fluence(self, val):
|
|
if not isinstance(val, Parameter):
|
|
raise TypeError('energy_fluence must be of Parameter type')
|
|
self._energy_fluence = val
|
|
|
|
@property
|
|
def flux_energy_range(self):
|
|
return self._flux_energy_range
|
|
@flux_energy_range.setter
|
|
def flux_energy_range(self, val):
|
|
if not isinstance(val, (list, tuple)):
|
|
raise ValueError('flux_energy_range must be a 2-tuple')
|
|
else:
|
|
if len(val) != 2:
|
|
raise ValueError('flux_energy_range must be a 2-tuple')
|
|
self._flux_energy_range = val
|
|
|
|
@property
|
|
def stat_name(self):
|
|
return self._stat_name
|
|
@stat_name.setter
|
|
def stat_name(self, val):
|
|
self._stat_name = str(val)
|
|
|
|
@property
|
|
def stat_value(self):
|
|
return self._stat_value
|
|
@stat_value.setter
|
|
def stat_value(self, val):
|
|
try:
|
|
float_val = float(val)
|
|
except:
|
|
raise TypeError('stat_value must be a float')
|
|
self._stat_value = float_val
|
|
|
|
@property
|
|
def dof(self):
|
|
return self._dof
|
|
@dof.setter
|
|
def dof(self, val):
|
|
try:
|
|
int_val = int(val)
|
|
except:
|
|
raise TypeError('dof must be an integer')
|
|
self._dof = int_val
|
|
|
|
@property
|
|
def covariance(self):
|
|
return self._covariance
|
|
@covariance.setter
|
|
def covariance(self, val):
|
|
if not isinstance(val, np.ndarray):
|
|
raise TypeError('covariance must an array')
|
|
if len(val.shape) != 2:
|
|
raise ValueError('covariance must be a n x n array')
|
|
if val.shape[0] != val.shape[1]:
|
|
raise ValueError('covariance must be a n x n array')
|
|
|
|
self._covariance = val
|
|
|
|
def parameter_list(self):
|
|
"""Return the list of parameter names
|
|
|
|
Returns:
|
|
(list): The parameter names
|
|
"""
|
|
return [param.name for param in self.parameters]
|
|
|
|
def to_fits_row(self):
|
|
"""Return the contained data as a FITS table row
|
|
|
|
Returns:
|
|
(astropy.io.fits.BinTableHDU): The FITS table
|
|
"""
|
|
numparams = len(self.parameters)
|
|
cols = []
|
|
cols.append(
|
|
fits.Column(name='TIMEBIN', format='2D', array=[self.time_range]))
|
|
i = 0
|
|
for param in self.parameters:
|
|
col = fits.Column(name='PARAM{0}'.format(i), format='3E',
|
|
array=[param.to_fits_value()])
|
|
cols.append(col)
|
|
i += 1
|
|
|
|
cols.append(fits.Column(name='PHTFLUX', format='3E',
|
|
array=[self.photon_flux.to_fits_value()]))
|
|
cols.append(fits.Column(name='PHTFLNC', format='3E',
|
|
array=[self.photon_fluence.to_fits_value()]))
|
|
cols.append(fits.Column(name='NRGFLUX', format='3E',
|
|
array=[self.energy_flux.to_fits_value()]))
|
|
cols.append(fits.Column(name='NRGFLNC', format='3E',
|
|
array=[self.energy_fluence.to_fits_value()]))
|
|
cols.append(fits.Column(name='REDCHSQ', format='2E',
|
|
array=[self.stat_value / self.dof]))
|
|
cols.append(fits.Column(name='CHSQDOF', format='1I', array=[self.dof]))
|
|
cols.append(fits.Column(name='COVARMAT',
|
|
format='{0}E'.format(numparams * numparams),
|
|
dim='({0},{0})'.format(numparams),
|
|
array=[self.covariance]))
|
|
|
|
hdu = fits.BinTableHDU.from_columns(cols, name='FIT PARAMS')
|
|
return hdu
|
|
|
|
def _init_by_dict(self, values):
|
|
for key, val in values.items():
|
|
try:
|
|
p = getattr(self, '_'+key)
|
|
if isinstance(p, property):
|
|
getattr(self, key).__set__(self, val)
|
|
else:
|
|
self.__setattr__(key, val)
|
|
except AttributeError:
|
|
raise ValueError("{} is not a valid attribute".format(key))
|
|
|
|
class GbmModelFit(ModelFit):
|
|
"""A container for the info from a model fit, with values used in the
|
|
GBM SCAT files. Inherits from :class:`ModelFit`.
|
|
|
|
Attributes:
|
|
covariance (np.array): The covariance matrix of the fit
|
|
dof (int): The degrees-of-freedom of the fit
|
|
duration_fluence (:class:`EnergyFluence`): The energy fluence over the
|
|
duration energy range,
|
|
nominally 50-300 keV
|
|
energy_fluence (:class:`EnergyFluence`): The energy fluence, nominally
|
|
over 10-1000 keV
|
|
energy_fluence_50_300 (:class:`EnergyFluence`): The energy fluence over
|
|
50-300 keV
|
|
energy_flux (:class:`EnergyFlux`): The energy flux, nominally over
|
|
10-1000 keV
|
|
flux_energy_range (tuple): The energy range of the flux and fluence,
|
|
(low, high)
|
|
name (str): The name of the model
|
|
parameters (list): A list of model parameters
|
|
photon_fluence (:class:`PhotonFluence`): The photon fluence, nominally
|
|
over 10-1000 keV
|
|
photon_flux (:class:`PhotonFlux`): The photon flux, nominally over
|
|
10-1000 keV
|
|
photon_flux_50_300 (:class:`PhotonFlux`): The photon flux over 50-300 keV
|
|
stat_name (str): The name of the fit statistic
|
|
stat_value (float): The fit statistic value
|
|
time_range (float, float): The time range of the model fit, (low, high)
|
|
"""
|
|
def __init__(self, name, time_range, **kwargs):
|
|
|
|
self._photon_flux_50_300 = None
|
|
self._energy_fluence_50_300 = None
|
|
self._duration_fluence = None
|
|
|
|
super().__init__(name, time_range, **kwargs)
|
|
|
|
@property
|
|
def photon_flux_50_300(self):
|
|
return self._photon_flux_50_300
|
|
@photon_flux_50_300.setter
|
|
def photon_flux_50_300(self, val):
|
|
if not isinstance(val, Parameter):
|
|
raise TypeError('photon_flux_50_300 must be of Parameter type')
|
|
self._photon_flux_50_300 = val
|
|
|
|
@property
|
|
def energy_fluence_50_300(self):
|
|
return self._energy_fluence_50_300
|
|
@energy_fluence_50_300.setter
|
|
def energy_fluence_50_300(self, val):
|
|
if not isinstance(val, Parameter):
|
|
raise TypeError('energy_fluence_50_300 must be of Parameter type')
|
|
self._energy_fluence_50_300 = val
|
|
|
|
@property
|
|
def duration_fluence(self):
|
|
return self._duration_fluence
|
|
@duration_fluence.setter
|
|
def duration_fluence(self, val):
|
|
if not isinstance(val, Parameter):
|
|
raise TypeError('duration_fluence must be of Parameter type')
|
|
self._duration_fluence = val
|
|
|
|
def to_fits_row(self):
|
|
"""Return the contained data as a FITS table row
|
|
|
|
Returns:
|
|
(astropy.io.fits.BinTableHDU): The FITS table
|
|
"""
|
|
numparams = len(self.parameters)
|
|
cols = []
|
|
cols.append(
|
|
fits.Column(name='TIMEBIN', format='2D', array=[self.time_range]))
|
|
i = 0
|
|
for param in self.parameters:
|
|
col = fits.Column(name='PARAM{0}'.format(i), format='3E',
|
|
array=[param.to_fits_value()])
|
|
cols.append(col)
|
|
i += 1
|
|
|
|
cols.append(fits.Column(name='PHTFLUX', format='3E',
|
|
array=[self.photon_flux.to_fits_value()]))
|
|
cols.append(fits.Column(name='PHTFLNC', format='3E',
|
|
array=[self.photon_fluence.to_fits_value()]))
|
|
cols.append(fits.Column(name='NRGFLUX', format='3E',
|
|
array=[self.energy_flux.to_fits_value()]))
|
|
cols.append(fits.Column(name='NRGFLNC', format='3E',
|
|
array=[self.energy_fluence.to_fits_value()]))
|
|
cols.append(fits.Column(name='PHTFLUXB', format='3E',
|
|
array=[
|
|
self.photon_flux_50_300.to_fits_value()]))
|
|
cols.append(fits.Column(name='NRGFLNCB', format='3E',
|
|
array=[
|
|
self.energy_fluence_50_300.to_fits_value()]))
|
|
cols.append(fits.Column(name='DURFLNC', format='3E',
|
|
array=[self.duration_fluence.to_fits_value()]))
|
|
cols.append(fits.Column(name='REDCHSQ', format='2E',
|
|
array=[[self.stat_value / self.dof] * 2]))
|
|
cols.append(fits.Column(name='CHSQDOF', format='1I', array=[self.dof]))
|
|
cols.append(fits.Column(name='COVARMAT',
|
|
format='{0}E'.format(numparams * numparams),
|
|
dim='({0},{0})'.format(numparams),
|
|
array=[self.covariance]))
|
|
|
|
hdu = fits.BinTableHDU.from_columns(cols, name='FIT PARAMS')
|
|
return hdu
|
|
|
|
@classmethod
|
|
def from_fits_row(cls, fits_row, model_name, param_names=None,
|
|
flux_range=(10.0, 1000.0), dur_range=(50.0, 300.0)):
|
|
"""Read a FITS row and return a :class:`GbmModelFit` object
|
|
|
|
Returns:
|
|
(:class:`GbmModelFit`)
|
|
"""
|
|
time_range = tuple(fits_row['TIMEBIN'])
|
|
nparams = sum([1 for name in fits_row.array.dtype.names \
|
|
if 'PARAM' in name])
|
|
if param_names is None:
|
|
param_names = ['']*nparams
|
|
|
|
params = []
|
|
for i in range(nparams):
|
|
param = fits_row['PARAM' + str(i)]
|
|
params.append(Parameter(param[0], tuple(param[1:]),
|
|
name=param_names[i]))
|
|
|
|
pflux = PhotonFlux(fits_row['PHTFLUX'][0],
|
|
tuple(fits_row['PHTFLUX'][1:]), flux_range)
|
|
pflnc = PhotonFluence(fits_row['PHTFLNC'][0],
|
|
tuple(fits_row['PHTFLNC'][1:]), flux_range)
|
|
eflux = EnergyFlux(fits_row['NRGFLUX'][0],
|
|
tuple(fits_row['NRGFLUX'][1:]), flux_range)
|
|
eflnc = EnergyFluence(fits_row['NRGFLNC'][0],
|
|
tuple(fits_row['NRGFLNC'][1:]), flux_range)
|
|
pfluxb = PhotonFlux(fits_row['PHTFLUXB'][0],
|
|
tuple(fits_row['PHTFLUXB'][1:]), (50.0, 300.0))
|
|
eflncb = EnergyFluence(fits_row['NRGFLNCB'][0],
|
|
tuple(fits_row['NRGFLNCB'][1:]), (50.0, 300.0))
|
|
durflnc = PhotonFluence(fits_row['DURFLNC'][0],
|
|
tuple(fits_row['DURFLNC'][1:]), dur_range)
|
|
dof = fits_row['CHSQDOF']
|
|
# scat provides the [fit stat, chisq], while bcat is only the fit stat
|
|
try:
|
|
stat_val = fits_row['REDCHSQ'][0]*dof
|
|
except:
|
|
stat_val = fits_row['REDCHSQ']*dof
|
|
covar = fits_row['COVARMAT']
|
|
|
|
obj = cls(model_name, time_range, parameters=params, photon_flux=pflux,
|
|
photon_fluence=pflnc, energy_flux=eflux, energy_fluence=eflnc,
|
|
flux_energy_range=flux_range, stat_value=stat_val, dof=dof,
|
|
covariance=covar, photon_flux_50_300=pfluxb,
|
|
energy_fluence_50_300=eflncb, duration_fluence=durflnc)
|
|
return obj
|
|
|
|
|
|
class DetectorData():
|
|
"""A container for detector info used in a fit
|
|
|
|
Parameters:
|
|
instrument (str): The name of the instrument
|
|
detector (str): The name of the detector
|
|
datatype (str): The name of the datatype
|
|
filename (str): The filename of the data file
|
|
numchans (int): Number of energy channels used
|
|
active (bool, optional): True if the detector is used in the fit
|
|
response (str, optional): The filename of the detector response
|
|
time_range (tuple, optional): The time range of the data used
|
|
energy_range (tuple, optional): The energy range of the data used
|
|
channel_range (tuple, optional): The energy channel range of the data
|
|
energy_edges (np.array, optional): The edges of the energy channels
|
|
photon_counts (np.array, optional): The deconvolved photon counts for
|
|
the detector
|
|
photon_model (np.array, optional): The photon model for the detector
|
|
photon_errors (np.array, optional): The deconvolved photon count errors
|
|
for the detector
|
|
|
|
Attributes:
|
|
active (bool, optional): True if the detector is used in the fit
|
|
channel_range = (int, int): The energy channel range of the data
|
|
datatype (str): The name of the datatype
|
|
detector (str): The name of the detector
|
|
energy_edges (np.array): The edges of the energy channels
|
|
energy_range (float, float): The energy range of the data used
|
|
filename (str): The filename of the data file
|
|
instrument (str): The name of the instrument
|
|
numchans (int): Number of energy channels used
|
|
photon_counts (np.array): The deconvolved photon counts for the detector
|
|
photon_errors (np.array): The deconvolved photon count errors for the
|
|
detector
|
|
photon_model (np.array): The photon model for the detector
|
|
response (str): The filename of the detector response
|
|
time_range (float, float): The time range of the data used
|
|
"""
|
|
def __init__(self, instrument, detector, datatype, filename, numchans,
|
|
**kwargs):
|
|
self._instrument = instrument
|
|
self._detector = detector
|
|
self._datatype = datatype
|
|
self._filename = filename
|
|
self._numchans = int(numchans)
|
|
|
|
self._active = True
|
|
self._response = ''
|
|
self._time_range = (None, None)
|
|
self._energy_range = (None, None)
|
|
self._channel_range = (None, None)
|
|
self._channel_mask = None
|
|
self._energy_edges = None
|
|
self._photon_counts = None
|
|
self._photon_model = None
|
|
self._photon_errors = None
|
|
|
|
self._init_by_dict(kwargs)
|
|
|
|
# read-only
|
|
@property
|
|
def instrument(self):
|
|
return self._instrument
|
|
@property
|
|
def detector(self):
|
|
return self._detector
|
|
@property
|
|
def datatype(self):
|
|
return self._datatype
|
|
@property
|
|
def filename(self):
|
|
return self._filename
|
|
@property
|
|
def numchans(self):
|
|
return self._numchans
|
|
|
|
@property
|
|
def active(self):
|
|
return self._active
|
|
@active.setter
|
|
def active(self, val):
|
|
try:
|
|
bool_val = bool(val)
|
|
except:
|
|
raise TypeError('active must be Boolean')
|
|
self._active = bool_val
|
|
|
|
@property
|
|
def response(self):
|
|
return self._response
|
|
@response.setter
|
|
def response(self, val):
|
|
if not isinstance(val, str):
|
|
raise TypeError('response filename must be a string')
|
|
self._response = val
|
|
|
|
@property
|
|
def time_range(self):
|
|
return self._time_range
|
|
@time_range.setter
|
|
def time_range(self, val):
|
|
if not isinstance(val, (tuple, list)):
|
|
raise TypeError('time_range must be a 2-tuple')
|
|
elif len(val) != 2:
|
|
raise ValueError('time_range must be a 2-tuple')
|
|
elif val[0] > val[1]:
|
|
raise ValueError('time_range must be of form (low, high)')
|
|
else:
|
|
pass
|
|
|
|
self._time_range = val
|
|
|
|
@property
|
|
def energy_range(self):
|
|
return self._energy_range
|
|
@energy_range.setter
|
|
def energy_range(self, val):
|
|
if not isinstance(val, (tuple, list)):
|
|
raise TypeError('energy_range must be a 2-tuple')
|
|
elif len(val) != 2:
|
|
raise ValueError('energy_range must be a 2-tuple')
|
|
elif val[0] > val[1]:
|
|
raise ValueError('energy_range must be of form (low, high)')
|
|
else:
|
|
pass
|
|
|
|
self._energy_range = val
|
|
|
|
@property
|
|
def channel_range(self):
|
|
return self._channel_range
|
|
@channel_range.setter
|
|
def channel_range(self, val):
|
|
if not isinstance(val, (tuple, list)):
|
|
raise TypeError('channel_range must be a 2-tuple')
|
|
elif len(val) != 2:
|
|
raise ValueError('channel_range must be a 2-tuple')
|
|
elif val[0] > val[1]:
|
|
raise ValueError('channel_range must be of form (low, high)')
|
|
else:
|
|
pass
|
|
|
|
self._channel_range = val
|
|
|
|
@property
|
|
def channel_mask(self):
|
|
return self._channel_mask
|
|
@channel_mask.setter
|
|
def channel_mask(self, val):
|
|
if not isinstance(val, np.ndarray):
|
|
raise TypeError('channel_mask must be an array')
|
|
self._channel_mask = val.astype(bool)
|
|
|
|
@property
|
|
def energy_edges(self):
|
|
return self._energy_edges
|
|
@energy_edges.setter
|
|
def energy_edges(self, val):
|
|
if not isinstance(val, np.ndarray):
|
|
raise TypeError('energy_edges must be an array')
|
|
self._energy_edges = val
|
|
|
|
@property
|
|
def photon_counts(self):
|
|
return self._photon_counts
|
|
@photon_counts.setter
|
|
def photon_counts(self, val):
|
|
if not isinstance(val, np.ndarray):
|
|
raise TypeError('photon_counts must be an array')
|
|
self._photon_counts = val
|
|
|
|
@property
|
|
def photon_model(self):
|
|
return self._photon_model
|
|
@photon_model.setter
|
|
def photon_model(self, val):
|
|
if not isinstance(val, np.ndarray):
|
|
raise TypeError('photon_model must be an array')
|
|
self._photon_model = val
|
|
|
|
@property
|
|
def photon_errors(self):
|
|
return self._photon_errors
|
|
@photon_errors.setter
|
|
def photon_errors(self, val):
|
|
if not isinstance(val, np.ndarray):
|
|
raise TypeError('photon_errors must be an array')
|
|
self._photon_errors = val
|
|
|
|
def to_fits_row(self):
|
|
"""Return the contained data as a FITS table row
|
|
|
|
Returns:
|
|
(astropy.io.fits.BinTableHDU): The FITS row
|
|
"""
|
|
numchans = len(self.energy_edges)
|
|
e_dim = str(numchans) + 'E'
|
|
p_dim = str(numchans - 1) + 'E'
|
|
p_unit = 'Photon cm^-2 s^-1 keV^-1'
|
|
fit_int = '{0}: {1} s, '.format(self.time_range[0], self.time_range[1])
|
|
fit_int += '{0}: {1} keV, '.format(self.energy_range[0],
|
|
self.energy_range[1])
|
|
fit_int += 'channels {0}: {1}'.format(self.channel_range[0],
|
|
self.channel_range[1])
|
|
|
|
col1 = fits.Column(name='INSTRUME', format='20A',
|
|
array=[self.instrument])
|
|
col2 = fits.Column(name='DETNAM', format='20A', array=[self.detector])
|
|
col3 = fits.Column(name='DATATYPE', format='20A',
|
|
array=[self.datatype])
|
|
col4 = fits.Column(name='DETSTAT', format='20A',
|
|
array=['INCLUDED' if self.active else 'OMITTED'])
|
|
col5 = fits.Column(name='DATAFILE', format='60A',
|
|
array=[self.filename])
|
|
col6 = fits.Column(name='RSPFILE', format='60A', array=[self.response])
|
|
col7 = fits.Column(name='FIT_INT', format='60A', array=[fit_int])
|
|
col8 = fits.Column(name='CHANNUM', format='1J', array=[numchans - 1])
|
|
col9 = fits.Column(name='FITCHAN', format='{}J'.format(numchans),
|
|
array=[self.channel_mask])
|
|
col10 = fits.Column(name='E_EDGES', format=e_dim, unit='keV',
|
|
array=[self.energy_edges])
|
|
col11 = fits.Column(name='PHTCNTS', format=p_dim, unit=p_unit,
|
|
array=[self.photon_counts])
|
|
col12 = fits.Column(name='PHTMODL', format=p_dim, unit=p_unit,
|
|
array=[self.photon_model])
|
|
col13 = fits.Column(name='PHTERRS', format=p_dim, unit=p_unit,
|
|
array=[self.photon_errors])
|
|
hdu = fits.BinTableHDU.from_columns(
|
|
[col1, col2, col3, col4, col5, col6,
|
|
col7, col8, col9, col10, col11,
|
|
col12, col13], name='DETECTOR DATA')
|
|
return hdu
|
|
|
|
@classmethod
|
|
def from_fits_row(cls, fits_row):
|
|
"""Read a FITS row and return a DetectorData object
|
|
|
|
Returns:
|
|
(:class:`DetectorData`)
|
|
"""
|
|
instrument = fits_row['INSTRUME']
|
|
det = fits_row['DETNAM']
|
|
datatype = fits_row['DATATYPE']
|
|
if fits_row['DETSTAT'] == 'INCLUDED':
|
|
active = True
|
|
else:
|
|
active = False
|
|
datafile = fits_row['DATAFILE']
|
|
rspfile = fits_row['RSPFILE']
|
|
fit_ints = fits_row['FIT_INT'].split(' ')
|
|
time_range = (float(fit_ints[0][:-1]), float(fit_ints[1]))
|
|
energy_range = (float(fit_ints[3][:-1]), float(fit_ints[4]))
|
|
channel_range = (int(fit_ints[8][:-1]), int(fit_ints[9]))
|
|
numchans = fits_row['CHANNUM']
|
|
channel_mask = np.zeros(numchans, dtype=bool)
|
|
channel_mask[fits_row['FITCHAN'][0]:fits_row['FITCHAN'][1]+1] = True
|
|
energy_edges = fits_row['E_EDGES']
|
|
photon_counts = fits_row['PHTCNTS']
|
|
photon_model = fits_row['PHTMODL']
|
|
photon_errors = fits_row['PHTERRS']
|
|
obj = cls(instrument, det, datatype, datafile, numchans, active=active,
|
|
response=rspfile, time_range=time_range,
|
|
energy_range=energy_range, channel_range=channel_range,
|
|
channel_mask=channel_mask, energy_edges=energy_edges,
|
|
photon_counts=photon_counts, photon_model=photon_model,
|
|
photon_errors=photon_errors)
|
|
return obj
|
|
|
|
def _init_by_dict(self, values):
|
|
for key, val in values.items():
|
|
try:
|
|
p = getattr(self, '_'+key)
|
|
if isinstance(p, property):
|
|
getattr(self, key).__set__(self, val)
|
|
else:
|
|
self.__setattr__(key, val)
|
|
except AttributeError:
|
|
raise ValueError("{} is not a valid attribute".format(key))
|
|
|
|
|
|
class Scat(DataFile):
|
|
"""A container class for the spectral fit data in an SCAT file
|
|
|
|
Attributes:
|
|
detectors (list): The :class:`DetectorData` objects used in the analysis
|
|
headers (dict): The SCAT file headers
|
|
model_fits (list): The :class:`GbmModelFit` objects, one for each model
|
|
fit
|
|
num_detectors (int): The number of detectors in the SCAT file
|
|
num_fits (int): The number of model fits
|
|
"""
|
|
def __init__(self):
|
|
self._detectors = []
|
|
self._model_fits = []
|
|
self._headers = {}
|
|
|
|
@property
|
|
def detectors(self):
|
|
return self._detectors
|
|
@property
|
|
def model_fits(self):
|
|
return self._model_fits
|
|
@property
|
|
def headers(self):
|
|
return self._headers
|
|
@property
|
|
def num_detectors(self):
|
|
return len(self.detectors)
|
|
@property
|
|
def num_fits(self):
|
|
return len(self.model_fits)
|
|
|
|
def add_detector_data(self, detector_data):
|
|
"""Add a new detector to the Scat
|
|
|
|
Args:
|
|
detector_data (:class:`DetectorData`): The detector data
|
|
"""
|
|
if not isinstance(detector_data, DetectorData):
|
|
raise TypeError("Can only add DetectorData objects")
|
|
self._detectors.append(detector_data)
|
|
|
|
def add_model_fit(self, model_fit):
|
|
"""Add a new model fit to the Scat
|
|
|
|
Args:
|
|
model_fit (:class:`GbmModelFit`): The model fit data
|
|
"""
|
|
if not isinstance(model_fit, GbmModelFit):
|
|
raise TypeError("Can only add GbmModelFit objects")
|
|
self._model_fits.append(model_fit)
|
|
|
|
@classmethod
|
|
def open(cls, filename):
|
|
"""Open a SCAT FITS file and create a Scat object
|
|
|
|
Args:
|
|
filename (str): The file to open
|
|
|
|
Returns:
|
|
(:class:`Scat`)
|
|
"""
|
|
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})
|
|
|
|
# read the detector data HDU
|
|
det_data = hdulist['DETECTOR DATA'].data
|
|
for row in det_data:
|
|
obj.add_detector_data(DetectorData.from_fits_row(row))
|
|
|
|
# read the fit params HDU
|
|
fit_hdu = hdulist['FIT PARAMS']
|
|
obj._from_fitparam_hdu(fit_hdu)
|
|
|
|
return obj
|
|
|
|
def write(self, directory, filename=None):
|
|
"""Write a Scat object to a FITS file
|
|
|
|
Args:
|
|
directory (str): The directory where the file is to be written
|
|
filename (str, optional): The filename. If not set, a default
|
|
filename will be used.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
if (self.filename is None) and (filename is None):
|
|
raise NameError('Filename not set')
|
|
if filename is None:
|
|
filename = self.filename_obj.basename()
|
|
self.set_filename(filename, directory=directory)
|
|
|
|
# initialize the FITS file
|
|
hdulist = fits.HDUList()
|
|
prihdr = self._primary_header()
|
|
primary_hdu = fits.PrimaryHDU(header=prihdr)
|
|
primary_hdu.add_checksum()
|
|
hdulist.append(primary_hdu)
|
|
|
|
# construct the detector data extension
|
|
det_hdu = None
|
|
for detector in self._detectors:
|
|
if det_hdu is None:
|
|
det_hdu = detector.to_fits_row()
|
|
else:
|
|
det_hdu.data = np.concatenate(
|
|
(det_hdu.data, detector.to_fits_row().data))
|
|
det_hdu.header = self._update_detector_hdr(det_hdu.header)
|
|
det_hdu.add_checksum()
|
|
hdulist.append(det_hdu)
|
|
|
|
# construct the fit extension
|
|
fit_hdu = None
|
|
for fit in self._model_fits:
|
|
if fit_hdu is None:
|
|
fit_hdu = fit.to_fits_row()
|
|
else:
|
|
fit_hdu.data = np.concatenate(
|
|
(fit_hdu.data, fit.to_fits_row().data))
|
|
fit_hdu.header = self._update_fitparam_hdr(fit_hdu.header)
|
|
fit_hdu.add_checksum()
|
|
hdulist.append(fit_hdu)
|
|
|
|
# write out the file
|
|
filename = directory + filename
|
|
hdulist.writeto(self.filename, checksum=True, clobber=True)
|
|
|
|
def _from_fitparam_hdu(self, fit_hdu):
|
|
|
|
fit_data = fit_hdu.data
|
|
fit_hdr = fit_hdu.header
|
|
nparams = fit_hdr['N_PARAM']
|
|
|
|
# find unique models in the event there are multiple components
|
|
models = [fit_hdr.comments['TTYPE' + str(2 + i)].split(':')[0]
|
|
for i in range(nparams)]
|
|
model = '+'.join(list(set(models)))
|
|
|
|
# the parameter names
|
|
param_names = [
|
|
fit_hdr.comments['TTYPE' + str(2 + i)].split(':')[1].strip()
|
|
for i in range(nparams)]
|
|
|
|
# populate each model fit
|
|
for row in fit_data:
|
|
modelfit = GbmModelFit.from_fits_row(row, model,
|
|
param_names=param_names)
|
|
modelfit.stat_name = fit_hdr['STATISTC']
|
|
self.add_model_fit(modelfit)
|
|
|
|
def _update_detector_hdr(self, det_hdr):
|
|
det_hdr.comments['TTYPE1'] = 'Instrument name for this detector'
|
|
det_hdr.comments[
|
|
'TTYPE2'] = 'Detector number; if one of several available'
|
|
det_hdr.comments['TTYPE3'] = 'Data type used for this analysis'
|
|
det_hdr.comments['TTYPE4'] = 'Was this detector INCLUDED or OMITTED'
|
|
det_hdr.comments['TTYPE5'] = 'Data file name for this dataset'
|
|
det_hdr.comments['TTYPE6'] = 'Response file name for this dataset'
|
|
det_hdr.comments['TTYPE7'] = 'Fit intervals'
|
|
det_hdr.comments[
|
|
'TTYPE8'] = 'Total number of energy channels for this detector'
|
|
det_hdr.comments[
|
|
'TTYPE9'] = 'Channels selected in fitting this detector'
|
|
det_hdr.comments['TTYPE10'] = 'Energy edges for each selected detector'
|
|
det_hdr.comments['TTYPE11'] = 'Array of photon counts data'
|
|
det_hdr.comments['TTYPE12'] = 'Array of photon model data'
|
|
det_hdr.comments['TTYPE13'] = 'Array of errors in photon counts data'
|
|
det_hdr['NUMFITS'] = (
|
|
len(self._model_fits), 'Number of spectral fits in the data')
|
|
prihdu = self._primary_header()
|
|
keys = ['ORIGIN', 'TELESCOP', 'INSTRUME', 'OBSERVER', 'MJDREFI',
|
|
'MJDREFF',
|
|
'TIMESYS', 'TIMEUNIT', 'DATE-OBS', 'DATE-END', 'TSTART',
|
|
'TSTOP',
|
|
'TRIGTIME']
|
|
for key in keys:
|
|
det_hdr[key] = (prihdu[key], prihdu.comments[key])
|
|
return det_hdr
|
|
|
|
def _update_fitparam_hdr(self, fit_hdr):
|
|
e_range = self._model_fits[0].flux_energy_range
|
|
model_name = self._model_fits[0].name
|
|
param_names = self._model_fits[0].parameter_list()
|
|
numparams = len(param_names)
|
|
statistic = self._model_fits[0].stat_name
|
|
|
|
g_range = '({0}-{1} keV)'.format(e_range[0], e_range[1])
|
|
b_range = '(50-300 keV)'
|
|
|
|
fit_hdr.comments['TTYPE1'] = 'Start and stop times relative to trigger'
|
|
for i in range(numparams):
|
|
colname = 'TTYPE{0}'.format(i + 2)
|
|
fit_hdr.comments[colname] = '{0}: {1}'.format(model_name,
|
|
param_names[i])
|
|
ttypes = ['TTYPE' + str(numparams + 2 + i) for i in range(10)]
|
|
fit_hdr.comments[
|
|
ttypes[0]] = 'Photon Flux (ph/s-cm^2) std energy ' + g_range
|
|
fit_hdr.comments[
|
|
ttypes[1]] = 'Photon Fluence (ph/cm^2) std energy ' + g_range
|
|
fit_hdr.comments[
|
|
ttypes[2]] = 'Energy Flux (erg/s-cm^2) std energy ' + g_range
|
|
fit_hdr.comments[
|
|
ttypes[3]] = 'Energy Fluence (erg/cm^2) std energy ' + g_range
|
|
fit_hdr.comments[
|
|
ttypes[4]] = 'Reduced Chi^2 (1) and fitting statistic (2)'
|
|
fit_hdr.comments[ttypes[5]] = 'Degrees of Freedom'
|
|
fit_hdr.comments[
|
|
ttypes[6]] = 'Photon Flux (ph/s-cm^2) BATSE energy ' + b_range
|
|
fit_hdr.comments[
|
|
ttypes[7]] = 'Photon Fluence (ph/cm^2) for durations (user)'
|
|
fit_hdr.comments[
|
|
ttypes[8]] = 'Energy Fluence (erg/cm^2) BATSE energy ' + b_range
|
|
fit_hdr.comments[
|
|
ttypes[9]] = 'Covariance matrix for the fir (N_PARAM^2)'
|
|
fit_hdr['N_PARAM'] = (
|
|
numparams, 'Total number of fit parameters (PARAMn)')
|
|
fit_hdr['FLU_LOW'] = (
|
|
e_range[0], 'Lower limit of flux/fluence integration (keV)')
|
|
fit_hdr['FLU_HIGH'] = (
|
|
e_range[1], 'Upeer limit of flux/fluence integration (keV)')
|
|
fit_hdr['STATISTC'] = (
|
|
statistic, 'Indicates merit function used for fitting')
|
|
fit_hdr['NUMFITS'] = (
|
|
len(self._model_fits), 'Number of spectral fits in the data')
|
|
|
|
prihdu = self._primary_header()
|
|
keys = ['ORIGIN', 'TELESCOP', 'INSTRUME', 'OBSERVER', 'MJDREFI',
|
|
'MJDREFF',
|
|
'TIMESYS', 'TIMEUNIT', 'DATE-OBS', 'DATE-END', 'TSTART',
|
|
'TSTOP',
|
|
'TRIGTIME']
|
|
for key in keys:
|
|
fit_hdr[key] = (prihdu[key], prihdu.comments[key])
|
|
return fit_hdr
|
|
|
|
def _primary_header(self):
|
|
# create standard GBM primary header
|
|
filetype = 'SPECTRAL FITS'
|
|
prihdr = hdr.primary(filetype=filetype, tstart=self.tstart,
|
|
filename=self.filename_obj.basename(),
|
|
tstop=self.tstop, trigtime=self.trigtime)
|
|
# remove the keywords we don't need
|
|
del prihdr['DETNAM'], prihdr['OBJECT'], prihdr['RA_OBJ'], \
|
|
prihdr['DEC_OBJ'], prihdr['ERR_RAD'], prihdr['INFILE01']
|
|
|
|
return prihdr
|