# CONTAINS TECHNICAL DATA/COMPUTER SOFTWARE DELIVERED TO THE U.S. GOVERNMENT WITH UNLIMITED RIGHTS
#
# Contract No.: CA 80MSFC17M0022
# Contractor Name: Universities Space Research Association
# Contractor Address: 7178 Columbia Gateway Drive, Columbia, MD 21046
#
# Copyright 2017-2022 by Universities Space Research Association (USRA). All rights reserved.
#
# Developed by: William Cleveland and Adam Goldstein
# Universities Space Research Association
# Science and Technology Institute
# https://sti.usra.edu
#
# Developed by: Daniel Kocevski
# National Aeronautics and Space Administration (NASA)
# Marshall Space Flight Center
# Astrophysics Branch (ST-12)
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
# in compliance with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied. See the License for the specific language governing permissions and limitations under the
# License.
#
import numpy as np
from gdt.core.data_primitives import Parameter
from gdt.core.types import set_by_dict
__all__ = ['DetectorData', 'EnergyFlux', 'EnergyFluence', 'ModelFit',
'PhotonFluence', 'PhotonFlux']
[docs]class PhotonFlux(Parameter):
"""A photon flux class.
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
"""
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):
"""(tuple): The enery range (low, high)"""
return self._energy_range
[docs]class PhotonFluence(Parameter):
"""A photon fluence class.
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
"""
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):
"""(tuple): A 2-tuple (low, high) for the energy range"""
return self._energy_range
[docs]class EnergyFlux(Parameter):
"""An energy flux class.
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
"""
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):
"""(tuple): A 2-tuple (low, high) for the energy range"""
return self._energy_range
[docs]class EnergyFluence(Parameter):
"""An energy fluence class.
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
"""
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):
"""(tuple): A 2-tuple (low, high) for the energy range"""
return self._energy_range
[docs]class ModelFit:
"""A container for the info resulting from a spectral fit.
"""
def __init__(self):
self._name = None
self._time_range = None
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
@property
def covariance(self):
"""(np.array): The covariance matrix of the fit"""
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
@property
def dof(self):
"""(int): The degrees-of-freedom of the fit"""
return self._dof
@dof.setter
def dof(self, val):
try:
int_val = int(val)
except(ValueError, TypeError):
raise TypeError('dof must be an integer')
self._dof = int_val
@property
def energy_fluence(self):
"""(:class:`EnergyFluence`): The energy fluence"""
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 energy_flux(self):
"""(:class:`EnergyFlux`): The energy flux"""
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 flux_energy_range(self):
"""(tuple): The energy range of the flux and fluence, (low, high)"""
return self._flux_energy_range
@flux_energy_range.setter
def flux_energy_range(self, val):
if not isinstance(val, (list, tuple)):
raise TypeError('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 name(self):
"""(str): The name of the model"""
return self._name
@property
def parameters(self):
"""(list of :class:`~gdt.core.data_primitives.Parameter`): A list of
model parameters"""
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_fluence(self):
"""(:class:`PhotonFluence`): The photon fluence"""
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 photon_flux(self):
"""(:class:`PhotonFlux`): The photon flux"""
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 stat_name(self):
"""(str): The name of the fit statistic"""
return self._stat_name
@stat_name.setter
def stat_name(self, val):
self._stat_name = str(val)
@property
def stat_value(self):
"""(float): The fit statistic value"""
return self._stat_value
@stat_value.setter
def stat_value(self, val):
try:
float_val = float(val)
except (ValueError, TypeError):
raise TypeError('stat_value must be a float')
self._stat_value = float_val
@property
def time_range(self):
"""(float, float): The time range of the model fit, (low, high)"""
return self._time_range
[docs] @classmethod
def from_data(cls, name, time_range, **kwargs):
"""Create a ModelFit object from data.
Args:
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
Returns:
(:class:`ModelFit`)
"""
obj = cls()
obj._name = str(name)
if not isinstance(time_range, (list, tuple)):
raise TypeError('time_range must be a 2-tuple')
else:
if len(time_range) != 2:
raise ValueError('time_range must be a 2-tuple')
obj._time_range = time_range
set_by_dict(obj, kwargs, no_private=False)
return obj
[docs] def parameter_list(self):
"""Return the list of parameter names
Returns:
(list): The parameter names
"""
return [param.name for param in self.parameters]
def __repr__(self):
s = '<{0}: {1}>'.format(self.__class__.__name__, self.name)
return s
def __str__(self):
param_str = '\n '.join([str(param) for param in self.parameters])
return '{0}\n {1}'.format(self.name, param_str)
[docs]class DetectorData:
"""A container for detector info used in a spectral fit.
"""
def __init__(self):
self._instrument = None
self._detector = None
self._datatype = None
self._filename = None
self._numchans = None
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
@property
def active(self):
"""(bool, optional): True if the detector is used in the fit"""
return self._active
@active.setter
def active(self, val):
self._active = bool(val)
@property
def channel_mask(self):
"""(np.array): A Boolean array marking the channels that were used"""
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 channel_range(self):
"""(int, int): The energy channel range of the data"""
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 datatype(self):
"""(str): The name of the datatype"""
return self._datatype
@property
def detector(self):
"""(str): The name of the detector"""
return self._detector
@property
def energy_edges(self):
"""(np.array): The edges of the energy channels"""
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 energy_range(self):
"""(float, float): The energy range of the data used"""
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 filename(self):
"""(str): The filename of the data file"""
return self._filename
@filename.setter
def filename(self, val):
self._filename = val
@property
def instrument(self):
"""(str): The name of the instrument"""
return self._instrument
@property
def num_chans(self):
"""(int): Number of energy channels used"""
return self._numchans
@property
def photon_counts(self):
"""(np.array): The deconvolved photon counts for the detector"""
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_errors(self):
"""(np.array): The deconvolved photon count errors for the detector"""
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
@property
def photon_model(self):
"""(np.array): The photon model for the detector"""
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 response(self):
"""(str): The filename of the detector response"""
return self._response
@response.setter
def response(self, val):
self._response = str(val)
@property
def time_range(self):
"""(float, float): The time range of the data used"""
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
[docs] @classmethod
def from_data(cls, instrument, detector, datatype, numchans, **kwargs):
"""Create a DetectorData object from data.
Args:
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
channel_mask (np.array, optional): A Boolean array marking the
channels that were used
channel_range (tuple, optional): The energy channel range of the
data
energy_edges (np.array, optional): The edges of the energy channels
energy_range (tuple, optional): The energy range of the data used
photon_counts (np.array, optional): The deconvolved photon counts
for the detector
photon_errors (np.array, optional): The deconvolved photon count
errors for the detector
photon_model (np.array, optional): The photon model for the detector
response (str, optional): The filename of the detector response
time_range (tuple, optional): The time range of the data used
Returns:
(:class:`DetectorData`)
"""
obj = cls()
obj._instrument = str(instrument)
obj._detector = str(detector)
obj._datatype = str(datatype)
obj._numchans = int(numchans)
set_by_dict(obj, kwargs, no_private=False)
return obj
def __repr__(self):
s = '<{0}: {1}; {2};\n'.format(self.__class__.__name__, self.detector,
self.datatype)
s += ' time range: {} s;\n'.format(self.time_range)
s += ' energy range: {} keV>'.format(self.energy_range)
return s