Skip to content

Commit

Permalink
Merge pull request #779 from StingraySoftware/fix_dyn_spec
Browse files Browse the repository at this point in the history
Fixes to dynamical power spectrum + Dynamical Cross spectrum
  • Loading branch information
matteobachetti authored Nov 16, 2023
2 parents 68453c6 + 36c5c0c commit 424fa59
Show file tree
Hide file tree
Showing 7 changed files with 517 additions and 232 deletions.
1 change: 1 addition & 0 deletions docs/changes/779.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Various bug fixes in DynamicalPowerspectrum, on event loading and time rebinning
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ filterwarnings =
ignore:.*is a deprecated alias for:DeprecationWarning
ignore:.*HIERARCH card will be created.*:
ignore:.*FigureCanvasAgg is non-interactive.*:UserWarning
ignore:.*jax.* deprecated. Use jax.*instead:DeprecationWarning
ignore:.*jax.* deprecated:DeprecationWarning:

;addopts = --disable-warnings

Expand Down
253 changes: 251 additions & 2 deletions stingray/crossspectrum.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from .base import StingrayObject
from .events import EventList
from .gti import cross_two_gtis, time_intervals_from_gtis
from .lightcurve import Lightcurve
from .utils import show_progress
from .fourier import avg_cs_from_iterables, error_on_averaged_cross_spectrum
Expand All @@ -28,6 +29,7 @@
__all__ = [
"Crossspectrum",
"AveragedCrossspectrum",
"DynamicalCrossspectrum",
"cospectra_pvalue",
"normalize_crossspectrum",
"time_lag",
Expand Down Expand Up @@ -377,7 +379,7 @@ def cospectra_pvalue(power, nspec):
the data.
Important: the underlying assumption that make this calculation valid
is that the powers in the power spectrum follow a Laplace distribution,
is that the powers in the cross spectrum follow a Laplace distribution,
and this requires that:
1. the co-spectrum is normalized according to [Leahy 1983]_
Expand All @@ -403,7 +405,7 @@ def cospectra_pvalue(power, nspec):
the signal-to-noise ratio, i.e. makes the statistical distributions
of the noise narrower, such that a smaller power might be very
significant in averaged spectra even though it would not be in a single
power spectrum.
cross spectrum.
Returns
-------
Expand Down Expand Up @@ -1880,6 +1882,253 @@ def time_lag(self):
return lag, lag_err


class DynamicalCrossspectrum(AveragedCrossspectrum):
type = "crossspectrum"
"""
Create a dynamical cross spectrum, also often called a *spectrogram*.
This class will divide two :class:`Lightcurve` objects into segments of
length ``segment_size``, create a cross spectrum for each segment and store
all powers in a matrix as a function of both time (using the mid-point of
each segment) and frequency.
This is often used to trace changes in period of a (quasi-)periodic signal
over time.
Parameters
----------
data1 : :class:`stingray.Lightcurve` or :class:`stingray.EventList` object
The time series or event list from the subject band, or channel, for which
the dynamical cross spectrum is to be calculated. If :class:`stingray.EventList`, ``dt``
must be specified as well.
data2 : :class:`stingray.Lightcurve` or :class:`stingray.EventList` object
The time series or event list from the reference band, or channel, of the dynamical
cross spectrum. If :class:`stingray.EventList`, ``dt`` must be specified as well.
segment_size : float, default 1
Length of the segment of light curve, default value is 1 (in whatever
units the ``time`` array in the :class:`Lightcurve`` object uses).
norm: {"leahy" | "frac" | "abs" | "none" }, optional, default "frac"
The normaliation of the periodogram to be used.
Other Parameters
----------------
gti: 2-d float array
``[[gti0_0, gti0_1], [gti1_0, gti1_1], ...]`` -- Good Time intervals.
This choice overrides the GTIs in the single light curves. Use with
care, especially if these GTIs have overlaps with the input
object GTIs! If you're getting errors regarding your GTIs, don't
use this and only give GTIs to the input object before making
the cross spectrum.
sample_time: float
Compulsory for input :class:`stingray.EventList` data. The time resolution of the
lightcurve that is created internally from the input event lists. Drives the
Nyquist frequency.
Attributes
----------
segment_size: float
The size of each segment to average. Note that if the total
duration of each input object in lc is not an integer multiple
of the ``segment_size``, then any fraction left-over at the end of the
time series will be lost.
dyn_ps : np.ndarray
The matrix of normalized squared absolute values of Fourier
amplitudes. The axis are given by the ``freq``
and ``time`` attributes.
norm: {``leahy`` | ``frac`` | ``abs`` | ``none``}
The normalization of the periodogram.
freq: numpy.ndarray
The array of mid-bin frequencies that the Fourier transform samples.
time: numpy.ndarray
The array of mid-point times of each interval used for the dynamical
power spectrum.
df: float
The frequency resolution.
dt: float
The time resolution of the dynamical spectrum. It is **not** the time resolution of the
input light curve. It is the integration time of each line of the dynamical power
spectrum (typically, an integer multiple of ``segment_size``).
"""

def __init__(self, data1, data2, segment_size, norm="frac", gti=None, sample_time=None):
if isinstance(data1, EventList) and sample_time is None:
raise ValueError("To pass input event lists, please specify sample_time")
elif isinstance(data1, Lightcurve):
sample_time = data1.dt
if segment_size > data1.tseg:
raise ValueError(
"Length of the segment is too long to create "
"any segments of the light curve!"
)
if segment_size < 2 * sample_time:
raise ValueError("Length of the segment is too short to form a light curve!")

self.segment_size = segment_size
self.sample_time = sample_time
self.gti = gti
self.norm = norm

self._make_matrix(data1, data2)

def _make_matrix(self, data1, data2):
"""
Create a matrix of powers for each time step and each frequency step.
Time increases with row index, frequency with column index.
Parameters
----------
data1 : :class:`Lightcurve` or :class:`EventList`
The object providing the subject band/channel for the dynamical
cross spectrum.
data2 : :class:`Lightcurve` or :class:`EventList`
The object providing the reference band for the dynamical
cross spectrum.
"""
avg = AveragedCrossspectrum(
data1,
data2,
dt=self.sample_time,
segment_size=self.segment_size,
norm=self.norm,
gti=self.gti,
save_all=True,
)
self.dyn_ps = np.array(avg.cs_all).T
self.freq = avg.freq
current_gti = avg.gti

tstart, _ = time_intervals_from_gtis(current_gti, self.segment_size)

self.time = tstart + 0.5 * self.segment_size
self.df = avg.df
self.dt = self.segment_size

def rebin_frequency(self, df_new, method="sum"):
"""
Rebin the Dynamic Power Spectrum to a new frequency resolution.
Rebinning is an in-place operation, i.e. will replace the existing
``dyn_ps`` attribute.
While the new resolution does not need to be an integer of the previous frequency
resolution, be aware that if this is the case, the last frequency bin will be cut
off by the fraction left over by the integer division
Parameters
----------
df_new: float
The new frequency resolution of the dynamical power spectrum.
Must be larger than the frequency resolution of the old dynamical
power spectrum!
method: {"sum" | "mean" | "average"}, optional, default "sum"
This keyword argument sets whether the counts in the new bins
should be summed or averaged.
"""
new_dynspec_object = copy.deepcopy(self)
dynspec_new = []
for data in self.dyn_ps.T:
freq_new, bin_counts, bin_err, _ = rebin_data(
self.freq, data, dx_new=df_new, method=method
)
dynspec_new.append(bin_counts)

new_dynspec_object.freq = freq_new
new_dynspec_object.dyn_ps = np.array(dynspec_new).T
new_dynspec_object.df = df_new
return new_dynspec_object

def rebin_time(self, dt_new, method="sum"):
"""
Rebin the Dynamic Power Spectrum to a new time resolution.
Note: this is *not* changing the time resolution of the input light
curve! ``dt`` is the integration time of each line of the dynamical power
spectrum (typically, an integer multiple of ``segment_size``).
While the new resolution does not need to be an integer of the previous time
resolution, be aware that if this is the case, the last time bin will be cut
off by the fraction left over by the integer division
Parameters
----------
dt_new: float
The new time resolution of the dynamical power spectrum.
Must be larger than the time resolution of the old dynamical power
spectrum!
method: {"sum" | "mean" | "average"}, optional, default "sum"
This keyword argument sets whether the counts in the new bins
should be summed or averaged.
Returns
-------
time_new: numpy.ndarray
Time axis with new rebinned time resolution.
dynspec_new: numpy.ndarray
New rebinned Dynamical Cross Spectrum.
"""
if dt_new < self.dt:
raise ValueError("New time resolution must be larger than old time resolution!")

new_dynspec_object = copy.deepcopy(self)

dynspec_new = []
for data in self.dyn_ps:
time_new, bin_counts, _, _ = rebin_data(
self.time, data, dt_new, method=method, dx=self.dt
)
dynspec_new.append(bin_counts)

new_dynspec_object.time = time_new
new_dynspec_object.dyn_ps = np.array(dynspec_new)
new_dynspec_object.dt = dt_new
return new_dynspec_object

def trace_maximum(self, min_freq=None, max_freq=None):
"""
Return the indices of the maximum powers in each segment
:class:`Powerspectrum` between specified frequencies.
Parameters
----------
min_freq: float, default ``None``
The lower frequency bound.
max_freq: float, default ``None``
The upper frequency bound.
Returns
-------
max_positions : np.array
The array of indices of the maximum power in each segment having
frequency between ``min_freq`` and ``max_freq``.
"""
if min_freq is None:
min_freq = np.min(self.freq)
if max_freq is None:
max_freq = np.max(self.freq)

max_positions = []
for ps in self.dyn_ps.T:
indices = np.logical_and(self.freq <= max_freq, min_freq <= self.freq)
max_power = np.max(ps[indices])
max_positions.append(np.where(ps == max_power)[0][0])

return np.array(max_positions)


def crossspectrum_from_time_array(
times1,
times2,
Expand Down
Loading

0 comments on commit 424fa59

Please sign in to comment.