GBM-data-tools/simulate/tte.py

309 lines
13 KiB
Python

# 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