Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce power colors #780

Merged
merged 64 commits into from Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
86d41fa
Introduce power colors
matteobachetti Nov 30, 2023
a6a66c1
Add changelog
matteobachetti Nov 30, 2023
460c90e
Fix docs for excluded frequencies and test
matteobachetti Nov 30, 2023
92c3e32
Test for power_err in power_color
matteobachetti Nov 30, 2023
e097132
Force correct format of excluded frequency intervals
matteobachetti Nov 30, 2023
0f0c70e
Be robust with array poisson_power
matteobachetti Nov 30, 2023
1f503b2
Nest inside dynamical Crosss spectrum and Power spectrum
matteobachetti Nov 30, 2023
f15b83d
Fix bug when using complex arrays
matteobachetti Nov 30, 2023
38109d7
Cast powers to real when doing power colors
matteobachetti Nov 30, 2023
c39fb77
Fix problem with input int power
matteobachetti Nov 30, 2023
3034b86
Add hue calculation and log power color
matteobachetti Dec 1, 2023
b8cf785
Add function to dynamical power spectrum to bin by number of time int…
matteobachetti Dec 1, 2023
86f4e83
Add m to docstrings
matteobachetti Dec 1, 2023
8223b06
Fix doc string
matteobachetti Dec 1, 2023
b0e6fbc
Better tests
matteobachetti Dec 1, 2023
e0d0c2e
Test average
matteobachetti Dec 1, 2023
646cdec
Fix bug that made unnorm_cs_all useless
matteobachetti Dec 1, 2023
c7bc68e
Test that the normalization is fixed once and for all
matteobachetti Dec 1, 2023
9a26a61
Add function to compute the rms (to complete the needs for power colo…
matteobachetti Dec 1, 2023
d7a55b6
Add test
matteobachetti Dec 1, 2023
7882e65
Update docs
matteobachetti Dec 2, 2023
42ad59c
Fix nbconvert version for now
matteobachetti Jan 2, 2024
a13b9f6
Improve compatibility with Numpy 2.0
matteobachetti Jan 11, 2024
8636b1d
More informative GTI failure
matteobachetti Dec 28, 2023
f01937d
Add changelog snippet
matteobachetti Dec 28, 2023
1fd421c
Make exception retrocompatible
matteobachetti Dec 28, 2023
582f17c
Hopefully less confusing warning
matteobachetti Dec 29, 2023
6ad70ec
Syntax fix
matteobachetti Dec 29, 2023
8a5b0fc
Add description of StingrayObject and StingrayTimeseries to docs
matteobachetti Dec 15, 2023
a7bcc37
Fix typo
matteobachetti Dec 15, 2023
15eaea7
Fix titles
matteobachetti Dec 15, 2023
dd1ab44
Add side parameter to find_nearest
matteobachetti Dec 8, 2023
9e23972
Add function to randomize data in small bad time intervals
matteobachetti Dec 8, 2023
4261c50
Add changelog
matteobachetti Dec 8, 2023
a60132c
Pay attention to edges
matteobachetti Dec 8, 2023
c6e7307
Rename options
matteobachetti Dec 8, 2023
c4cf72c
Fix syntax
matteobachetti Dec 8, 2023
3426b3f
Test that only the attributes we want are randomized
matteobachetti Dec 8, 2023
a57487c
Test case with no btis
matteobachetti Dec 8, 2023
6f79627
Add fake data to docs
matteobachetti Dec 9, 2023
d2084bd
Fix plotting
matteobachetti Dec 11, 2023
7dca2c6
Fix deprecation in plot
matteobachetti Dec 13, 2023
0b34459
Fix deprecation in plot
matteobachetti Dec 14, 2023
80891e6
Fix corner cases and warnings
matteobachetti Dec 14, 2023
1f6c993
Improve docstring
matteobachetti Jan 3, 2024
59d8a70
Test more keywords and more bad intervals
matteobachetti Jan 4, 2024
d1b2f0e
Top-level import when appropriate
matteobachetti Jan 11, 2024
93858a7
Add warning about applying the technique only to *very* short gaps
matteobachetti Jan 11, 2024
ba5ae1b
Change uniform to even, to avoid confusion between uniformly distribu…
matteobachetti Jan 11, 2024
c8f2595
Cleanup
matteobachetti Jan 11, 2024
dc365c3
Further warning
matteobachetti Jan 11, 2024
e809319
Fix docstring
matteobachetti Jan 11, 2024
ab966b5
Change buffer default behavior
matteobachetti Jan 11, 2024
d8c9d2b
Fix rst issue in docstring
matteobachetti Jan 11, 2024
faf9361
Fix meaning of m
matteobachetti Jan 11, 2024
dc3fd3a
Explain why rebin_by_n_intervals is different from rebin_time
matteobachetti Jan 11, 2024
c2bccea
Averaging powers, not counts
matteobachetti Jan 11, 2024
bf05e21
Default to average, not sum
matteobachetti Jan 11, 2024
12df86d
Change default behavior in rebinning: average
matteobachetti Jan 11, 2024
c7f7ba6
Add Heil citation
matteobachetti Jan 11, 2024
945a42c
frequency->freq
matteobachetti Jan 11, 2024
0469a44
Write explanation for power colors; check freq_edges dimensions and d…
matteobachetti Jan 11, 2024
17c7bf1
Additional small fix to docstring
matteobachetti Jan 11, 2024
83681ca
Fix method calls
matteobachetti Jan 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changes/780.feature.rst
@@ -0,0 +1 @@
Introduce power colors à la Heil et al. 2015
9 changes: 5 additions & 4 deletions docs/index.rst
Expand Up @@ -24,14 +24,15 @@ Current Capabilities
1. Data handling and simulation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

* loading event lists from fits files of a few missions (RXTE/PCA, NuSTAR/FPM, XMM-Newton/EPIC, NICER/XTI)
* constructing light curves from event data, various operations on light curves (e.g. addition, subtraction, joining, and truncation)
* loading event lists from fits files (and generally good handling of OGIP-compliant missions, like RXTE/PCA, NuSTAR/FPM, XMM-Newton/EPIC, NICER/XTI)
* constructing light curves and time series from event data
* various operations on time series (e.g. addition, subtraction, joining, and truncation)
* simulating a light curve with a given power spectrum
* simulating a light curve from another light curve and a 1-d (time) or 2-d (time-energy) impulse response
* simulating an event list from a given light curve _and_ with a given energy spectrum
* Good Time Interval operations

2. Fourier methods
1. Fourier methods
~~~~~~~~~~~~~~~~~~
* power spectra and cross spectra in Leahy, rms normalization, absolute rms and no normalization
* averaged power spectra and cross spectra
Expand All @@ -44,6 +45,7 @@ Current Capabilities
* bispectra; *needs testing*
* (Bayesian) quasi-periodic oscillation searches
* Lomb-Scargle periodograms and cross spectra
* Power Colors

3. Other time series methods
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -61,7 +63,6 @@ Other future additions we are currently implementing are:
* bicoherence
* phase-resolved spectroscopy of quasi-periodic oscillations
* Fourier-frequency-resolved spectroscopy
* power colours
* full HEASARC-compatible mission support
* pulsar searches with :math:`H`-test
* binary pulsar searches
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Expand Up @@ -59,6 +59,7 @@ docs =
docutils
sphinx-astropy
nbsphinx>=0.8.3,!=0.8.8
nbconvert<7.14
pandoc
ipython
towncrier<22.12.0
Expand Down
192 changes: 189 additions & 3 deletions stingray/crossspectrum.py
Expand Up @@ -21,7 +21,7 @@
from .fourier import avg_cs_from_iterables, error_on_averaged_cross_spectrum
from .fourier import avg_cs_from_events, poisson_level
from .fourier import fftfreq, fft, normalize_periodograms, raw_coherence
from .fourier import get_flux_iterable_from_segments
from .fourier import get_flux_iterable_from_segments, power_color

from scipy.special import factorial

Expand Down Expand Up @@ -2028,6 +2028,9 @@ class DynamicalCrossspectrum(AveragedCrossspectrum):
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``).

m: int
The number of averaged cross spectra.
matteobachetti marked this conversation as resolved.
Show resolved Hide resolved
"""

def __init__(self, data1, data2, segment_size, norm="frac", gti=None, sample_time=None):
Expand Down Expand Up @@ -2075,14 +2078,19 @@ def _make_matrix(self, data1, data2):
save_all=True,
)
self.dyn_ps = np.array(avg.cs_all).T
conv = avg.cs_all / avg.unnorm_cs_all
self.unnorm_conversion = np.nanmean(conv)
self.freq = avg.freq
current_gti = avg.gti
self.nphots1 = avg.nphots1
self.nphots2 = avg.nphots2

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
self.m = 1

def rebin_frequency(self, df_new, method="sum"):
"""
Expand All @@ -2108,14 +2116,16 @@ def rebin_frequency(self, df_new, method="sum"):
new_dynspec_object = copy.deepcopy(self)
dynspec_new = []
for data in self.dyn_ps.T:
freq_new, bin_counts, bin_err, _ = rebin_data(
freq_new, bin_counts, bin_err, step = 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
new_dynspec_object.m = int(step) * self.m

return new_dynspec_object

def rebin_time(self, dt_new, method="sum"):
Expand Down Expand Up @@ -2156,14 +2166,73 @@ def rebin_time(self, dt_new, method="sum"):

dynspec_new = []
for data in self.dyn_ps:
time_new, bin_counts, _, _ = rebin_data(
time_new, bin_counts, _, step = 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
new_dynspec_object.m = int(step) * self.m
return new_dynspec_object

def rebin_by_n_intervals(self, n, method="sum"):
"""
Rebin the Dynamic Power Spectrum to a new time resolution, by summing n intervals.
matteobachetti marked this conversation as resolved.
Show resolved Hide resolved

Parameters
----------
n: int
The number of intervals to be combined into one.

method: {"sum" | "mean" | "average"}, optional, default "sum"
This keyword argument sets whether the counts in the new bins
matteobachetti marked this conversation as resolved.
Show resolved Hide resolved
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 not np.issubdtype(type(n), np.integer):
warnings.warn("n must be an integer. Casting to int")

n = int(n)

if n == 1:
return copy.deepcopy(self)
if n < 1:
raise ValueError("n must be >= 1")

new_dynspec_object = copy.deepcopy(self)

dynspec_new = []
time_new = []
for i, data in enumerate(self.dyn_ps.T):
if i % n == 0:
count = 1
bin_counts = data
bin_times = self.time[i]
else:
count += 1
bin_counts += data
bin_times += self.time[i]
if count == n:
if method in ["mean", "average"]:
bin_counts /= n
dynspec_new.append(bin_counts)
bin_times /= n
time_new.append(bin_times)

new_dynspec_object.time = time_new
new_dynspec_object.dyn_ps = np.array(dynspec_new).T
new_dynspec_object.dt *= n
new_dynspec_object.m = n * self.m

return new_dynspec_object

def trace_maximum(self, min_freq=None, max_freq=None):
Expand Down Expand Up @@ -2198,6 +2267,123 @@ def trace_maximum(self, min_freq=None, max_freq=None):

return np.array(max_positions)

def power_colors(
self,
frequency_edges=[1 / 256, 1 / 32, 0.25, 2.0, 16.0],
frequencies_to_exclude=None,
matteobachetti marked this conversation as resolved.
Show resolved Hide resolved
poisson_power=0,
):
"""
Return the power colors of the dynamical power spectrum.
matteobachetti marked this conversation as resolved.
Show resolved Hide resolved

Parameters
----------
frequency_edges: iterable
The edges of the frequency bins to be used for the power colors.

frequencies_to_exclude : 1-d or 2-d iterable, optional, default None
The ranges of frequencies to exclude from the calculation of the power color.
For example, the frequencies containing strong QPOs.
A 1-d iterable should contain two values for the edges of a single range. (E.g.
``[0.1, 0.2]``). ``[[0.1, 0.2], [3, 4]]`` will exclude the ranges 0.1-0.2 Hz and
3-4 Hz.

poisson_power : float or iterable, optional, default 0
The Poisson noise level of the power spectrum. If iterable, it should have the same
length as ``frequency``. (This might apply to the case of a power spectrum with a
strong dead time distortion)

Returns
-------
pc0: np.ndarray
pc0_err: np.ndarray
pc1: np.ndarray
pc1_err: np.ndarray
The power colors for each spectrum and their respective errors
"""
power_colors = []
if np.iscomplexobj(self.dyn_ps):
warnings.warn("When using power_colors, complex powers will be cast to real.")

for ps in self.dyn_ps.T:
power_colors.append(
power_color(
self.freq,
ps.real,
frequency_edges=frequency_edges,
frequencies_to_exclude=frequencies_to_exclude,
df=self.df,
poisson_power=poisson_power,
m=self.m,
)
)

pc0, pc0_err, pc1, pc1_err = np.array(power_colors).T
return pc0, pc0_err, pc1, pc1_err

def compute_rms(self, min_freq, max_freq, poisson_noise_level=0):
"""
Compute the fractional rms amplitude in the power spectrum
between two frequencies.

Parameters
----------
min_freq: float
The lower frequency bound for the calculation.

max_freq: float
The upper frequency bound for the calculation.

Other parameters
----------------
poisson_noise_level : float, default is None
This is the Poisson noise level of the PDS with same
normalization as the PDS.

Returns
-------
rms: float
The fractional rms amplitude contained between ``min_freq`` and
``max_freq``.

rms_err: float
The error on the fractional rms amplitude.

"""
from .fourier import rms_calculation

dyn_ps_unnorm = self.dyn_ps / self.unnorm_conversion
poisson_noise_unnrom = poisson_noise_level / self.unnorm_conversion
if not hasattr(self, "nphots"):
nphots = (self.nphots1 * self.nphots2) ** 0.5
else:
nphots = self.nphots
T = self.dt * self.m
M_freqs = self.m
K_freqs = 1
minind = self.freq.searchsorted(min_freq)
maxind = self.freq.searchsorted(max_freq)
min_freq = self.freq[minind]
freq_bins = maxind - minind
rmss = []
rms_errs = []
for unnorm_powers in dyn_ps_unnorm.T:
r, re = rms_calculation(
unnorm_powers[minind:maxind],
min_freq,
max_freq,
nphots,
T,
M_freqs,
K_freqs,
freq_bins,
poisson_noise_unnrom,
deadtime=0.0,
)
rmss.append(r)
rms_errs.append(re)
return rmss, rms_errs


def crossspectrum_from_time_array(
times1,
Expand Down