From e6066c5c2643f27d820b115223385085c7b77798 Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Wed, 7 Jun 2023 16:55:52 +0530 Subject: [PATCH 01/31] LSCross,Power Spectrum Classes implemented --- stingray/lombscargle.py | 638 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 638 insertions(+) create mode 100644 stingray/lombscargle.py diff --git a/stingray/lombscargle.py b/stingray/lombscargle.py new file mode 100644 index 000000000..6b2f3155e --- /dev/null +++ b/stingray/lombscargle.py @@ -0,0 +1,638 @@ +import copy +from typing import Optional, Union + +import numpy as np + +from astropy.timeseries.periodograms import LombScargle +from astropy.timeseries.periodograms.lombscargle.implementations.utils import trig_sum + +from stingray.crossspectrum import Crossspectrum +from stingray.exceptions import StingrayError +from stingray.utils import simon + +from .events import EventList # Convert to relative nomenclature +from .lightcurve import Lightcurve # Convert to relative nomenclature + +# WiP +# def lsft_fast(lc: Lightcurve, w0, df, Nf): +# # Work in Progress +# y = lc.counts +# t = lc.time +# dy = lc.counts_err +# weights = dy**-2.0 +# weights /= weights.sum() + +# Sh, Ch = trig_sum(t, weights * t, df=df, N=Nf, use_fft=True) +# S2, C2 = trig_sum(t, weights, freq_factor=2, df=df, N=Nf, use_fft=True) + + +def lsft_slow( + lc: Lightcurve, + ww: np.ndarray, + sign: Optional[int] = 1, + fullspec: Optional[bool] = False, +): + """ + Calculates the Lomb-Scargle Fourier transform of a light curve. + + Parameters + ---------- + lc : :class:`stingray.lightcurve.Lightcurve` + A light curve object. + + freqs : numpy.ndarray + An array of frequencies at which the transform is sampled. + + sign : int, optional, default: 1 + The sign of the fourier transform. 1 implies positive sign and -1 implies negative sign. + + fullspec : bool, optional, default: False + Return LSFT values for full frequency array (True) or just positive frequencies (False). + + Returns + ------- + ft_res : numpy.ndarray + An array of Fourier transformed data. + """ + if sign not in [1, -1]: + raise ValueError("sign must be 1 or -1") + + const1 = 1 / np.sqrt(2) + const2 = const1 * sign + + xx = lc.counts + sum_xx = np.sum(xx) + t = lc.time + + num_xt = len(xx) + num_ww = len(ww) + + ft_real = ft_imag = np.zeros((num_ww)) + ft_res = np.zeros((num_ww), dtype=np.complex128) + for i in range(num_ww): + if i == 0: + ft_real = sum_xx / np.sqrt(num_xt) + ft_imag = 0 + phase_this = 0 + else: + wrun = ww[i] * 2 * np.pi + + csum = np.sum(np.cos(2.0 * wrun * t)) + ssum = np.sum(np.sin(2.0 * wrun * t)) + + watan = np.arctan2(ssum, csum) + wtau = 0.5 * watan + + sumr = np.sum(np.multiply(xx, np.cos(wrun * t - wtau))) + sumi = np.sum(np.multiply(xx, np.sin(wrun * t - wtau))) + + scos2 = np.sum((np.power(np.cos(wrun * t - wtau), 2))) + ssin2 = np.sum((np.power(np.sin(wrun * t - wtau), 2))) + + ft_real = np.multiply(const1, np.divide(sumr, np.sqrt(scos2))) + ft_imag = np.multiply(const2, np.divide(sumi, np.sqrt(ssin2))) + phase_this = wtau - wrun * t[0] + work = np.multiply(np.complex128(complex(ft_real, ft_imag)), np.exp(1j * phase_this)) + ft_res[i] = work + if fullspec: + return ft_res + else: + return ft_res[ww > 0] + + +# method default will change to fast after implementation of the fast algorithm +class LombScargleCrossspectrum(Crossspectrum): + main_array_attr = "freq" + type = "crossspectrum" + """ + Make a cross spectrum from an unevenly sampled (binned) light curve. + You can also make an empty :class:`LombScargleCrossspectrum` object to populate with your + own Fourier-transformed data (this can sometimes be useful when making + binned power spectra). + + Parameters + ---------- + data1: :class:`stingray.lightcurve.Lightcurve` or :class:`stingray.events.EventList`, optional, default ``None`` + The dataset for the first channel/band of interest. + + data2: :class:`stingray.lightcurve.Lightcurve` or :class:`stingray.events.EventList`, optional, default ``None`` + The dataset for the second, or "reference", band. + + norm: {``frac``, ``abs``, ``leahy``, ``none``}, default ``none`` + The normalization of the (real part of the) cross spectrum. + + power_type: string, optional, default ``real`` + Parameter to choose among complete, real part and magnitude of the cross spectrum. + + fullspec: boolean, optional, default ``False`` + If False, keep only the positive frequencies, or if True, keep all of them . + + Other Parameters + ---------------- + dt: float + The time resolution of the light curve. Only needed when constructing + light curves in the case where ``data1``, ``data2`` are + :class:`EventList` objects + + skip_checks: bool + Skip initial checks, for speed or other reasons (you need to trust your + inputs!) + + min_freq : float + Minimum frequency to take the Lomb-Scargle Fourier Transform + + max_freq: float + Maximum frequency to take the Lomb-Scargle Fourier Transform + + df : float + The time resolution of the light curve. Only needed where ``data1``, ``data2`` are + + method : str + The method to be used by the Lomb-Scargle Fourier Transformation function. `fast` + and `slow` are the allowed values. Default is `fast`. fast uses the optimized Press + and Rybicki O(n*log(n)) + + Attributes + ---------- + freq: numpy.ndarray + The array of mid-bin frequencies that the Fourier transform samples + + power: numpy.ndarray + The array of cross spectra (complex numbers) + + power_err: numpy.ndarray + The uncertainties of ``power``. + An approximation for each bin given by ``power_err= power/sqrt(m)``. + Where ``m`` is the number of power averaged in each bin (by frequency + binning, or averaging more than one spectra). Note that for a single + realization (``m=1``) the error is equal to the power. + + df: float + The frequency resolution + + m: int + The number of averaged cross-spectra amplitudes in each bin. + + n: int + The number of data points/time bins in one segment of the light + curves. + + k: array of int + The rebinning scheme if the object has been rebinned otherwise is set to 1. + + nphots1: float + The total number of photons in light curve 1 + + nphots2: float + The total number of photons in light curve 2 + + """ + + def __init__( + self, + data1: Optional[Union[EventList, Lightcurve]] = None, + data2: Optional[Union[EventList, Lightcurve]] = None, + norm: Optional[str] = "none", + power_type: Optional[str] = "real", + dt: Optional[float] = None, + fullspec: Optional[bool] = False, + skip_checks: bool = False, + min_freq: float = 0, + max_freq: float = None, + df: float = None, + method="slow", + ): + self._type = None + good_input = data1 is not None and data2 is not None + if not skip_checks: + good_input = self.initial_checks( + data1=data1, + data2=data2, + norm=norm, + power_type=power_type, + dt=dt, + fullspec=fullspec, + min_freq=min_freq, + max_freq=max_freq, + df=df, + ) + + self.dt = dt + norm = norm.lower() + self.norm = norm + self.k = 1 + self.df = df + if not good_input: + return self._initialize_empty() + + if type(data1) not in [EventList, Lightcurve] or type(data2) not in [ + EventList, + Lightcurve, + ]: + raise TypeError("One of the arguments is not of type eventlist or lightcurve") + + if isinstance(data1, EventList): + self.lc1 = data1.to_lc(self.dt) + else: + self.lc1 = data1 + if isinstance(data2, EventList): + self.lc2 = data2.to_lc(self.dt) + else: + self.lc2 = data2 + self.power_type = power_type + self.fullspec = fullspec + self.norm = norm + + self.nphots1 = self.lc1.counts.sum() + self.nphots2 = self.lc2.counts.sum() + + self.min_freq = min_freq + self.max_freq = max_freq + self._make_crossspectrum(self.lc1, self.lc2, fullspec, method) + if self.power_type == "abs": + self.power = np.abs(self.power) + self.power_err = np.abs(self.power_err) + self.unnorm_power = np.abs(self.unnorm_power) + self.unnorm_power_err = np.abs(self.unnorm_power_err) + if self.power_type == "real": + self.power = np.real(self.power) + self.power_err = np.real(self.power) + self.unnorm_power = np.real(self.unnorm_power) + self.unnorm_power_err = np.real(self.unnorm_power_err) + self._make_auxil_pds(self.lc1, self.lc2) + + # method default will change to fast after implementation of the fast algorithm + def initial_checks( + self, + data1=None, + data2=None, + norm=None, + power_type=None, + dt=None, + min_freq=0, + max_freq=None, + fullspec=False, + df=None, + method="slow", + ): + if not isinstance(norm, str): + raise TypeError("norm must be a string") + + if not isinstance(power_type, str): + raise TypeError("power_type must be a string") + + if norm.lower() not in ["frac", "abs", "leahy", "none"]: + raise ValueError("norm must be one of ['frac','abs','leahy','none']") + + if power_type not in ["all", "absolute", "real"]: + raise ValueError("power_type must be one of ['all','absolute','real']") + + if data1 is None or data2 is None: + raise ValueError("You can't do a cross spectrum with just one lightcurve") + if min_freq < 0: + raise ValueError("min_freq must be non-negative") + if max_freq is not None: + if max_freq < min_freq or max_freq < 0: + raise ValueError("max_freq must be non-negative and greater than min_freq") + return True + + # method default will change to fast after implementation of the fast algorithm + def _make_crossspectrum(self, lc1, lc2, fullspec=False, method="fast"): + """ + Auxiliary method computing the normalized cross spectrum from two + light curves. This includes checking for the presence of and + applying Good Time Intervals, computing the unnormalized Fourier + cross-amplitude, and then renormalizing using the required + normalization. Also computes an uncertainty estimate on the cross + spectral powers. + + Parameters + ---------- + lc1, lc2 : :class:`stingray.lightcurve.Lightcurve` objects + Two light curves used for computing the cross spectrum. + + fullspec: boolean, default ``False`` + Return full frequency array (True) or just positive frequencies (False) + + method : str + The method to be used by the Lomb-Scargle Fourier Transformation function. `fast` + and `slow` are the allowed values. Default is `fast`. fast uses the optimized Press + and Rybicki O(n*log(n)) + + """ + if not isinstance(lc1, Lightcurve): + raise TypeError("lc1 must be a lightcurve.Lightcurve object") + + if not isinstance(lc2, Lightcurve): + raise TypeError("lc2 must be a lightcurve.Lightcurve object") + + if self.lc2.mjdref != self.lc1.mjdref: + raise ValueError("MJDref is different in the two light curves") + + self.meancounts1 = lc1.meancounts + self.meancounts2 = lc2.meancounts + + self.err_dist = "poisson" + if lc1.err_dist == "poisson": + self.variance1 = lc1.meancounts + else: + self.variance1 = np.mean(lc1.meancounts) ** 2 + self.err_dist = "gauss" + + if lc2.err_dist == "poisson": + self.variance2 = lc2.meancounts + else: + self.variance2 = np.mean(lc2.meancounts) ** 2 + self.err_dist = "gauss" + + if lc1.n != lc2.n: + raise StingrayError("Lightcurves do not have the same number of bins per segment.") + + # if not np.isclose(lc1.dt, lc2.dt, rtol=0.1 * lc1.dt / lc1.tseg): + # raise StingrayError("Lightcurves do not have the same time binning dt.") + + lc1.dt = lc2.dt + self.dt = lc1.dt + self.n = lc1.n + + self.df = 1.0 / lc1.tseg + + self.m = 1 + + self.freq, self.unnorm_power = self._ls_cross(self.lc1, self.lc2, fullspec=fullspec) + + self.power = self._normalize_crossspectrum(self.unnorm_power) + if lc1.err_dist.lower() != lc2.err_dist.lower(): + simon( + "Your lightcurves have different statistics.", + "The errors in the Crossspectrum will be incorrect.", + ) + + elif lc1.err_dist.lower() != "poisson": + simon( + "Looks like your lightcurve statistic is not poisson." + "The errors in the Crossspectrum will be incorrect." + ) + + if self.__class__.__name__ == "LombScarglePowerspectrum": + print("ps") + self.power_err = self.unnorm_power_err = self.power / np.sqrt(self.m) + elif self.__class__.__name__ == "LombScargleCrossspectrum": + simon( + "Errorbars on cross spectra are not thoroughly tested." + "Please report any inconsistencies." + ) + print("ls") + self.unnorm_power_err = np.sqrt(2) / np.sqrt(self.m) + self.unnorm_power_err /= np.divide(2, np.sqrt(np.abs(self.nphots1 * self.nphots2))) + self.unnorm_power_err += np.zeros_like(self.unnorm_power) + self.power_err = self._normalize_crossspectrum(self.unnorm_power_err) + else: + self.power_err = np.zeros(len(self.power)) + + def _make_auxil_pds(self, lc1, lc2): + __doc__ = super()._make_auxil_pds.__doc__ + if lc1 is not lc2 and isinstance(lc1, Lightcurve): + self.pds1 = LombScargleCrossspectrum( + lc1, + lc1, + power_type=self.power_type, + norm=self.norm, + dt=self.dt, + fullspec=self.fullspec, + min_freq=self.min_freq, + max_freq=self.max_freq, + df=self.df, + ) + self.pds2 = LombScargleCrossspectrum( + lc2, + lc2, + power_type=self.power_type, + norm=self.norm, + dt=self.dt, + fullspec=self.fullspec, + min_freq=self.min_freq, + max_freq=self.max_freq, + df=self.df, + ) + + # method default will change to fast after implementation of the fast algorithm + def _ls_cross(self, lc1, lc2, ww=None, fullspec=False, method="slow"): + """ + Lomb-Scargle Fourier transform the two light curves, then compute the cross spectrum. + Computed as CS = lc1 x lc2* (where lc2 is the one that gets + complex-conjugated). The user has the option to either get just the + positive frequencies or the full spectrum. + + Parameters + ---------- + lc1: :class:`stingray.lightcurve.Lightcurve` object + One light curve to be Lomb-Scargle Fourier transformed. Ths is the band of + interest or channel of interest. + + lc2: :class:`stingray.lightcurve.Lightcurve` object + Another light curve to be Fourier transformed. + This is the reference band. + + fullspec: boolean. Default is False. + If True, return the whole array of frequencies, or only positive frequencies (False). + + method : str + The method to be used by the Lomb-Scargle Fourier Transformation function. `fast` + and `slow` are the allowed values. Default is `fast`. fast uses the optimized Press + and Rybicki O(n*log(n)) + + Returns + ------- + freq: numpy.ndarray + The frequency grid at which the LSFT was evaluated + + cross: numpy.ndarray + The cross spectrum value at each frequency. + + """ + if not ww: + ww = ( + LombScargle( + lc1.time, + lc1.counts, + fit_mean=False, + center_data=False, + normalization="psd", + ).autofrequency(minimum_frequency=self.min_freq, maximum_frequency=self.max_freq), + )[0] + ww2 = ( + LombScargle( + lc2.time, + lc2.counts, + fit_mean=False, + center_data=False, + normalization="psd", + ).autofrequency(minimum_frequency=self.min_freq, maximum_frequency=self.max_freq), + )[0] + if max(ww2) > max(ww): + ww = ww2 + + if method == "slow": + lsft1 = lsft_slow(lc1, ww, sign=1, fullspec=fullspec) + lsft2 = lsft_slow(lc2, ww, sign=-1, fullspec=fullspec) + # elif method == "fast": + # lsft1 = lsft_fast(lc1, ww, fullspec=fullspec) + cross = np.multiply(lsft1, lsft2) + freq = ww + if not fullspec: + freq = freq[freq > 0] + cross = cross[freq > 0] + return freq, cross + + def _initialize_empty(self): + self.freq = None + self.power = None + self.power_err = None + self.unnorm_power = None + self.unnorm_power_err = None + self.df = None + self.dt = None + self.nphots1 = None + self.nphots2 = None + self.m = 1 + self.n = None + self.fullspec = None + self.k = 1 + return + + +class LombScarglePowerspectrum(LombScargleCrossspectrum): + type = "powerspectrum" + """ + Make a :class:`LombScarglePowerspectrum` (also called periodogram) from a unevenly sampled (binned) + light curve. Periodograms can be normalized by either Leahy normalization, + fractional rms normalization, absolute rms normalization, or not at all. + + You can also make an empty :class:`LombScarglePowerspectrum` object to populate with + your own fourier-transformed data (this can sometimes be useful when making + binned power spectra). + + Parameters + ---------- + data: :class:`stingray.lightcurve.Lightcurve` or :class:`stingray.events.EventList` object, optional, default ``None`` + The light curve data to be Fourier-transformed. + + norm: {"leahy" | "frac" | "abs" | "none" }, optional, default "frac" + The normaliation of the power spectrum to be used. Options are + "leahy", "frac", "abs" and "none", default is "frac". + + Other Parameters + ---------------- + dt: float + The time resolution of the light curve. Only needed when constructing + light curves in the case where ``data`` is a + :class:`EventList` object + + skip_checks: bool + Skip initial checks, for speed or other reasons (you need to trust your + inputs!). + + min_freq : float + Minimum frequency to take the Lomb-Scargle Fourier Transform + + max_freq: float + Maximum frequency to take the Lomb-Scargle Fourier Transform + + df : float + The time resolution of the light curve. Only needed where ``data`` is a :class`stingray.Eventlist` object + + method : str + The method to be used by the Lomb-Scargle Fourier Transformation function. `fast` + and `slow` are the allowed values. Default is `fast`. fast uses the optimized Press + and Rybicki O(n*log(n)) + + Attributes + ---------- + norm: {"leahy" | "frac" | "abs" | "none" } + The normalization of the power spectrum. + + freq: numpy.ndarray + The array of mid-bin frequencies that the Fourier transform samples. + + power: numpy.ndarray + The array of normalized squared absolute values of Fourier + amplitudes. + + power_err: numpy.ndarray + The uncertainties of ``power``. + An approximation for each bin given by ``power_err= power/sqrt(m)``. + Where ``m`` is the number of power averaged in each bin (by frequency + binning, or averaging power spectra of segments of a light curve). + Note that for a single realization (``m=1``) the error is equal to the + power. + + df: float + The frequency resolution. + + m: int + The number of averaged powers in each bin. + + n: int + The number of data points in the light curve. + + nphots: float + The total number of photons in the light curve. + """ + + def __init__( + self, + data: Optional[Union[Lightcurve, EventList]] = None, + norm: Optional[str] = "none", + power_type: Optional[str] = "real", + dt: Optional[float] = None, + fullspec: Optional[bool] = False, + skip_checks: Optional[bool] = False, + min_freq: Optional[float] = 0, + max_freq: Optional[float] = None, + df: Optional[float] = None, + method: Optional[str] = "fast", + ): + self._type = None + data1 = copy.deepcopy(data) + data2 = copy.deepcopy(data1) + good_input = data is not None + if not skip_checks: + good_input = self.initial_checks( + data1=data1, + data2=data2, + norm=norm, + power_type=power_type, + dt=dt, + fullspec=fullspec, + min_freq=min_freq, + max_freq=max_freq, + df=df, + method=method, + ) + if type(data) not in [EventList, Lightcurve, None]: + good_input = False + self.dt = dt + norm = norm.lower() + self.norm = norm + self.df = df + + if not good_input: + return self._initialize_empty() + + LombScargleCrossspectrum.__init__( + self, + data1=data1, + data2=data2, + norm=norm, + power_type=power_type, + dt=dt, + skip_checks=skip_checks, + min_freq=min_freq, + max_freq=max_freq, + df=df, + method=method, + ) + + self.nphots = self.nphots1 + self.dt = dt From bf459e715aef3e07ea9c25ded11d6d9c6c534538 Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Wed, 7 Jun 2023 23:08:37 +0530 Subject: [PATCH 02/31] moved lsft funcs to fourier, added lombscargle to __init__.py --- stingray/__init__.py | 2 + stingray/fourier.py | 92 ++++++++++++++++++++++++++++++++++++++++- stingray/lombscargle.py | 87 +------------------------------------- 3 files changed, 94 insertions(+), 87 deletions(-) diff --git a/stingray/__init__.py b/stingray/__init__.py index 33420352e..61288f2de 100644 --- a/stingray/__init__.py +++ b/stingray/__init__.py @@ -22,3 +22,5 @@ from stingray.stats import * from stingray.bispectrum import * from stingray.varenergyspectrum import * + from stingray.largememory import * + from stingray.lombscargle import * diff --git a/stingray/fourier.py b/stingray/fourier.py index 19dddc97e..d8b79f7ec 100644 --- a/stingray/fourier.py +++ b/stingray/fourier.py @@ -1,15 +1,19 @@ import copy import warnings from collections.abc import Iterable +from typing import Optional import numpy as np from astropy.table import Table +from astropy.timeseries.periodograms.lombscargle.implementations.utils import trig_sum +from .events import EventList from .gti import ( generate_indices_of_segment_boundaries_binned, generate_indices_of_segment_boundaries_unbinned, ) -from .utils import histogram, show_progress, sum_if_not_none_or_initialize, fft, fftfreq +from .lightcurve import Lightcurve +from .utils import fft, fftfreq, histogram, show_progress, sum_if_not_none_or_initialize def positive_fft_bins(n_bin, include_zero=False): @@ -1985,3 +1989,89 @@ def avg_cs_from_events( if results is not None: results.meta["gti"] = gti return results + + +def lsft_fast(lc: Lightcurve, w0, df, Nf): + # Work in Progress + y = lc.counts + t = lc.time + dy = lc.counts_err + weights = dy**-2.0 + weights /= weights.sum() + + Sh, Ch = trig_sum(t, weights * t, df=df, N=Nf, use_fft=True) + S2, C2 = trig_sum(t, weights, freq_factor=2, df=df, N=Nf, use_fft=True) + + +def lsft_slow( + lc: Lightcurve, + ww: np.ndarray, + sign: Optional[int] = 1, + fullspec: Optional[bool] = False, +): + """ + Calculates the Lomb-Scargle Fourier transform of a light curve. + + Parameters + ---------- + lc : :class:`stingray.lightcurve.Lightcurve` + A light curve object. + + freqs : numpy.ndarray + An array of frequencies at which the transform is sampled. + + sign : int, optional, default: 1 + The sign of the fourier transform. 1 implies positive sign and -1 implies negative sign. + + fullspec : bool, optional, default: False + Return LSFT values for full frequency array (True) or just positive frequencies (False). + + Returns + ------- + ft_res : numpy.ndarray + An array of Fourier transformed data. + """ + if sign not in [1, -1]: + raise ValueError("sign must be 1 or -1") + + const1 = 1 / np.sqrt(2) + const2 = const1 * sign + + xx = lc.counts + sum_xx = np.sum(xx) + t = lc.time + + num_xt = len(xx) + num_ww = len(ww) + + ft_real = ft_imag = np.zeros((num_ww)) + ft_res = np.zeros((num_ww), dtype=np.complex128) + for i in range(num_ww): + if i == 0: + ft_real = sum_xx / np.sqrt(num_xt) + ft_imag = 0 + phase_this = 0 + else: + wrun = ww[i] * 2 * np.pi + + csum = np.sum(np.cos(2.0 * wrun * t)) + ssum = np.sum(np.sin(2.0 * wrun * t)) + + watan = np.arctan2(ssum, csum) + wtau = 0.5 * watan + + sumr = np.sum(np.multiply(xx, np.cos(wrun * t - wtau))) + sumi = np.sum(np.multiply(xx, np.sin(wrun * t - wtau))) + + scos2 = np.sum((np.power(np.cos(wrun * t - wtau), 2))) + ssin2 = np.sum((np.power(np.sin(wrun * t - wtau), 2))) + + ft_real = np.multiply(const1, np.divide(sumr, np.sqrt(scos2))) + ft_imag = np.multiply(const2, np.divide(sumi, np.sqrt(ssin2))) + phase_this = wtau - wrun * t[0] + work = np.multiply(np.complex128(complex(ft_real, ft_imag)), np.exp(1j * phase_this)) + ft_res[i] = work + if fullspec: + return ft_res + else: + return ft_res[ww > 0] diff --git a/stingray/lombscargle.py b/stingray/lombscargle.py index 6b2f3155e..eb0ab6e4b 100644 --- a/stingray/lombscargle.py +++ b/stingray/lombscargle.py @@ -4,7 +4,6 @@ import numpy as np from astropy.timeseries.periodograms import LombScargle -from astropy.timeseries.periodograms.lombscargle.implementations.utils import trig_sum from stingray.crossspectrum import Crossspectrum from stingray.exceptions import StingrayError @@ -13,91 +12,7 @@ from .events import EventList # Convert to relative nomenclature from .lightcurve import Lightcurve # Convert to relative nomenclature -# WiP -# def lsft_fast(lc: Lightcurve, w0, df, Nf): -# # Work in Progress -# y = lc.counts -# t = lc.time -# dy = lc.counts_err -# weights = dy**-2.0 -# weights /= weights.sum() - -# Sh, Ch = trig_sum(t, weights * t, df=df, N=Nf, use_fft=True) -# S2, C2 = trig_sum(t, weights, freq_factor=2, df=df, N=Nf, use_fft=True) - - -def lsft_slow( - lc: Lightcurve, - ww: np.ndarray, - sign: Optional[int] = 1, - fullspec: Optional[bool] = False, -): - """ - Calculates the Lomb-Scargle Fourier transform of a light curve. - - Parameters - ---------- - lc : :class:`stingray.lightcurve.Lightcurve` - A light curve object. - - freqs : numpy.ndarray - An array of frequencies at which the transform is sampled. - - sign : int, optional, default: 1 - The sign of the fourier transform. 1 implies positive sign and -1 implies negative sign. - - fullspec : bool, optional, default: False - Return LSFT values for full frequency array (True) or just positive frequencies (False). - - Returns - ------- - ft_res : numpy.ndarray - An array of Fourier transformed data. - """ - if sign not in [1, -1]: - raise ValueError("sign must be 1 or -1") - - const1 = 1 / np.sqrt(2) - const2 = const1 * sign - - xx = lc.counts - sum_xx = np.sum(xx) - t = lc.time - - num_xt = len(xx) - num_ww = len(ww) - - ft_real = ft_imag = np.zeros((num_ww)) - ft_res = np.zeros((num_ww), dtype=np.complex128) - for i in range(num_ww): - if i == 0: - ft_real = sum_xx / np.sqrt(num_xt) - ft_imag = 0 - phase_this = 0 - else: - wrun = ww[i] * 2 * np.pi - - csum = np.sum(np.cos(2.0 * wrun * t)) - ssum = np.sum(np.sin(2.0 * wrun * t)) - - watan = np.arctan2(ssum, csum) - wtau = 0.5 * watan - - sumr = np.sum(np.multiply(xx, np.cos(wrun * t - wtau))) - sumi = np.sum(np.multiply(xx, np.sin(wrun * t - wtau))) - - scos2 = np.sum((np.power(np.cos(wrun * t - wtau), 2))) - ssin2 = np.sum((np.power(np.sin(wrun * t - wtau), 2))) - - ft_real = np.multiply(const1, np.divide(sumr, np.sqrt(scos2))) - ft_imag = np.multiply(const2, np.divide(sumi, np.sqrt(ssin2))) - phase_this = wtau - wrun * t[0] - work = np.multiply(np.complex128(complex(ft_real, ft_imag)), np.exp(1j * phase_this)) - ft_res[i] = work - if fullspec: - return ft_res - else: - return ft_res[ww > 0] +from .fourier import lsft_slow, lsft_fast # method default will change to fast after implementation of the fast algorithm From 1354896907fa6dcb6cb4892bcd3c8a27004ac625 Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Sat, 10 Jun 2023 19:40:25 +0530 Subject: [PATCH 03/31] Input of lsft_slow,fast changed to be non stingray specific, added changelog, --- docs/changes/737.feature.rst | 2 ++ stingray/fourier.py | 28 ++++++++++++++-------------- stingray/lombscargle.py | 26 +++++++++++--------------- 3 files changed, 27 insertions(+), 29 deletions(-) create mode 100644 docs/changes/737.feature.rst diff --git a/docs/changes/737.feature.rst b/docs/changes/737.feature.rst new file mode 100644 index 000000000..534f4c495 --- /dev/null +++ b/docs/changes/737.feature.rst @@ -0,0 +1,2 @@ +- Implemented the Lomb Scargle Fourier Transform +- Using which wrote the corresponding :class:`LombScargleCrossspectrum` and :class:`LombScarglePowerspectrum` \ No newline at end of file diff --git a/stingray/fourier.py b/stingray/fourier.py index d8b79f7ec..a0bddc52a 100644 --- a/stingray/fourier.py +++ b/stingray/fourier.py @@ -4,6 +4,7 @@ from typing import Optional import numpy as np +import numpy.typing as npt from astropy.table import Table from astropy.timeseries.periodograms.lombscargle.implementations.utils import trig_sum @@ -1991,11 +1992,8 @@ def avg_cs_from_events( return results -def lsft_fast(lc: Lightcurve, w0, df, Nf): +def lsft_fast(y, t, dy, w0, df, Nf): # Work in Progress - y = lc.counts - t = lc.time - dy = lc.counts_err weights = dy**-2.0 weights /= weights.sum() @@ -2004,8 +2002,9 @@ def lsft_fast(lc: Lightcurve, w0, df, Nf): def lsft_slow( - lc: Lightcurve, - ww: np.ndarray, + y: npt.ArrayLike, + t: npt.ArrayLike, + ww: npt.ArrayLike, sign: Optional[int] = 1, fullspec: Optional[bool] = False, ): @@ -2014,8 +2013,11 @@ def lsft_slow( Parameters ---------- - lc : :class:`stingray.lightcurve.Lightcurve` - A light curve object. + y : a `:class:numpy.array` of floats + Observations to be transformed. + + y : `:class:numpy.array` of floats + Times of the observations freqs : numpy.ndarray An array of frequencies at which the transform is sampled. @@ -2037,11 +2039,9 @@ def lsft_slow( const1 = 1 / np.sqrt(2) const2 = const1 * sign - xx = lc.counts - sum_xx = np.sum(xx) - t = lc.time + sum_xx = np.sum(y) - num_xt = len(xx) + num_xt = len(y) num_ww = len(ww) ft_real = ft_imag = np.zeros((num_ww)) @@ -2060,8 +2060,8 @@ def lsft_slow( watan = np.arctan2(ssum, csum) wtau = 0.5 * watan - sumr = np.sum(np.multiply(xx, np.cos(wrun * t - wtau))) - sumi = np.sum(np.multiply(xx, np.sin(wrun * t - wtau))) + sumr = np.sum(np.multiply(y, np.cos(wrun * t - wtau))) + sumi = np.sum(np.multiply(y, np.sin(wrun * t - wtau))) scos2 = np.sum((np.power(np.cos(wrun * t - wtau), 2))) ssin2 = np.sum((np.power(np.sin(wrun * t - wtau), 2))) diff --git a/stingray/lombscargle.py b/stingray/lombscargle.py index eb0ab6e4b..b3ed92d91 100644 --- a/stingray/lombscargle.py +++ b/stingray/lombscargle.py @@ -2,17 +2,15 @@ from typing import Optional, Union import numpy as np - +import numpy.typing as npt from astropy.timeseries.periodograms import LombScargle -from stingray.crossspectrum import Crossspectrum -from stingray.exceptions import StingrayError -from stingray.utils import simon - -from .events import EventList # Convert to relative nomenclature -from .lightcurve import Lightcurve # Convert to relative nomenclature - -from .fourier import lsft_slow, lsft_fast +from .crossspectrum import Crossspectrum +from .events import EventList +from .exceptions import StingrayError +from .fourier import lsft_fast, lsft_slow +from .lightcurve import Lightcurve +from .utils import simon # method default will change to fast after implementation of the fast algorithm @@ -263,8 +261,8 @@ def _make_crossspectrum(self, lc1, lc2, fullspec=False, method="fast"): if lc1.n != lc2.n: raise StingrayError("Lightcurves do not have the same number of bins per segment.") - # if not np.isclose(lc1.dt, lc2.dt, rtol=0.1 * lc1.dt / lc1.tseg): - # raise StingrayError("Lightcurves do not have the same time binning dt.") + if not np.isclose(lc1.dt, lc2.dt, rtol=0.1 * lc1.dt / lc1.tseg): + raise StingrayError("Lightcurves do not have the same time binning dt.") lc1.dt = lc2.dt self.dt = lc1.dt @@ -290,14 +288,12 @@ def _make_crossspectrum(self, lc1, lc2, fullspec=False, method="fast"): ) if self.__class__.__name__ == "LombScarglePowerspectrum": - print("ps") self.power_err = self.unnorm_power_err = self.power / np.sqrt(self.m) elif self.__class__.__name__ == "LombScargleCrossspectrum": simon( "Errorbars on cross spectra are not thoroughly tested." "Please report any inconsistencies." ) - print("ls") self.unnorm_power_err = np.sqrt(2) / np.sqrt(self.m) self.unnorm_power_err /= np.divide(2, np.sqrt(np.abs(self.nphots1 * self.nphots2))) self.unnorm_power_err += np.zeros_like(self.unnorm_power) @@ -389,8 +385,8 @@ def _ls_cross(self, lc1, lc2, ww=None, fullspec=False, method="slow"): ww = ww2 if method == "slow": - lsft1 = lsft_slow(lc1, ww, sign=1, fullspec=fullspec) - lsft2 = lsft_slow(lc2, ww, sign=-1, fullspec=fullspec) + lsft1 = lsft_slow(lc1.counts, lc1.time, ww, sign=1, fullspec=fullspec) + lsft2 = lsft_slow(lc2.counts, lc2.time, ww, sign=-1, fullspec=fullspec) # elif method == "fast": # lsft1 = lsft_fast(lc1, ww, fullspec=fullspec) cross = np.multiply(lsft1, lsft2) From 7a7139366c8e59474c0b17d3f0c8d55d9634fa51 Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Sun, 11 Jun 2023 02:12:14 +0530 Subject: [PATCH 04/31] Removed LC and eventlist imports in fourier.py --- stingray/fourier.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/stingray/fourier.py b/stingray/fourier.py index a0bddc52a..73363aa74 100644 --- a/stingray/fourier.py +++ b/stingray/fourier.py @@ -8,12 +8,10 @@ from astropy.table import Table from astropy.timeseries.periodograms.lombscargle.implementations.utils import trig_sum -from .events import EventList from .gti import ( generate_indices_of_segment_boundaries_binned, generate_indices_of_segment_boundaries_unbinned, ) -from .lightcurve import Lightcurve from .utils import fft, fftfreq, histogram, show_progress, sum_if_not_none_or_initialize From 315a116abf42cc3058f9e8cade110999c162bb18 Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Thu, 22 Jun 2023 01:52:05 +0530 Subject: [PATCH 05/31] Typos and added fast version of lsft to fourier.py --- stingray/fourier.py | 102 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 90 insertions(+), 12 deletions(-) diff --git a/stingray/fourier.py b/stingray/fourier.py index 73363aa74..5875fb74f 100644 --- a/stingray/fourier.py +++ b/stingray/fourier.py @@ -1990,22 +1990,100 @@ def avg_cs_from_events( return results -def lsft_fast(y, t, dy, w0, df, Nf): - # Work in Progress - weights = dy**-2.0 - weights /= weights.sum() +def lsft_fast( + y: npt.ArrayLike, + t: npt.ArrayLike, + freqs: npt.ArrayLike, + sign: Optional[int] = 1, + fullspec: Optional[bool] = False, + oversampling: Optional[int] = 5, +): + """ + Calculates the Lomb-Scargle Fourier transform of a light curve. + + Parameters + ---------- + y : a :class:`numpy.array` of floats + Observations to be transformed. + + t : :class:`numpy.array` of floats + Times of the observations + + freqs : :class:`numpy.array` + An array of frequencies at which the transform is sampled. + + sign : int, optional, default: 1 + The sign of the fourier transform. 1 implies positive sign and -1 implies negative sign. + + fullspec : bool, optional, default: False + Return LSFT values for full frequency array (True) or just positive frequencies (False). + + oversampling : float, optional, default: 5 + Interpolation Oversampling Factor + + Returns + ------- + ft_res : numpy.ndarray + An array of Fourier transformed data. + """ + if sign not in [1, -1]: + raise ValueError("sign must be 1 or -1") + + freqs = np.sort(freqs) + + f0, df, N = freqs[0], freqs[1] - freqs[0], len(freqs) + + Sh, Ch = trig_sum( + t, + y, + df, + N, + f0, + oversampling=oversampling, + ) + S2, C2 = trig_sum( + t, + np.ones_like(y), + df, + N, + f0, + freq_factor=2, + oversampling=oversampling, + ) - Sh, Ch = trig_sum(t, weights * t, df=df, N=Nf, use_fft=True) - S2, C2 = trig_sum(t, weights, freq_factor=2, df=df, N=Nf, use_fft=True) + tan_2wtau = S2 / C2 + + S2w = tan_2wtau / np.sqrt(1 + tan_2wtau**2) + C2w = 1 / np.sqrt(1 + tan_2wtau**2) + + Cw = np.sqrt(0.5) * np.sqrt(1 + C2w) + Sw = np.sqrt(0.5) * np.sign(S2w) * np.sqrt(1 - S2w) + + YC = Ch * Cw + Sh * Sw + YS = Sh * Cw - Ch * Sw + CC = 0.5 * (N + C2 * C2w + S2 * S2w) + SS = 0.5 * (N - C2 * C2w + S2 * S2w) + + ft_res = (YC**2 / CC) + 1j * (YS**2 / SS) + + ft_res *= np.exp(-1j * freqs * 2 * np.pi) + + if sign == -1: + ft_res = np.conjugate(ft_res) + + if fullspec: + return ft_res + else: + return ft_res[freqs > 0] def lsft_slow( y: npt.ArrayLike, t: npt.ArrayLike, - ww: npt.ArrayLike, + freqs: npt.ArrayLike, sign: Optional[int] = 1, fullspec: Optional[bool] = False, -): +): """ Calculates the Lomb-Scargle Fourier transform of a light curve. @@ -2014,7 +2092,7 @@ def lsft_slow( y : a `:class:numpy.array` of floats Observations to be transformed. - y : `:class:numpy.array` of floats + t : `:class:numpy.array` of floats Times of the observations freqs : numpy.ndarray @@ -2040,7 +2118,7 @@ def lsft_slow( sum_xx = np.sum(y) num_xt = len(y) - num_ww = len(ww) + num_ww = len(freqs) ft_real = ft_imag = np.zeros((num_ww)) ft_res = np.zeros((num_ww), dtype=np.complex128) @@ -2050,7 +2128,7 @@ def lsft_slow( ft_imag = 0 phase_this = 0 else: - wrun = ww[i] * 2 * np.pi + wrun = freqs[i] * 2 * np.pi csum = np.sum(np.cos(2.0 * wrun * t)) ssum = np.sum(np.sin(2.0 * wrun * t)) @@ -2072,4 +2150,4 @@ def lsft_slow( if fullspec: return ft_res else: - return ft_res[ww > 0] + return ft_res[freqs > 0] From 012c2e208aecbd8cab18876f48da0104722f5077 Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Thu, 22 Jun 2023 02:02:53 +0530 Subject: [PATCH 06/31] Integrating fast method into the cross,power spectrum classes --- stingray/lombscargle.py | 98 +++++++++++++++++++++++++++++++++-------- 1 file changed, 80 insertions(+), 18 deletions(-) diff --git a/stingray/lombscargle.py b/stingray/lombscargle.py index b3ed92d91..f706a066f 100644 --- a/stingray/lombscargle.py +++ b/stingray/lombscargle.py @@ -13,7 +13,6 @@ from .utils import simon -# method default will change to fast after implementation of the fast algorithm class LombScargleCrossspectrum(Crossspectrum): main_array_attr = "freq" type = "crossspectrum" @@ -64,6 +63,9 @@ class LombScargleCrossspectrum(Crossspectrum): The method to be used by the Lomb-Scargle Fourier Transformation function. `fast` and `slow` are the allowed values. Default is `fast`. fast uses the optimized Press and Rybicki O(n*log(n)) + + oversampling : float, optional, default: 5 + Interpolation Oversampling Factor (for the fast algorithm) Attributes ---------- @@ -113,7 +115,8 @@ def __init__( min_freq: float = 0, max_freq: float = None, df: float = None, - method="slow", + method: str = "fast", + oversampling: int = 5, ): self._type = None good_input = data1 is not None and data2 is not None @@ -128,8 +131,15 @@ def __init__( min_freq=min_freq, max_freq=max_freq, df=df, + method=method, + oversampling=oversampling, ) + if dt is None: + if isinstance(data1, Lightcurve): + dt = data1.dt + elif isinstance(data1, EventList): + raise ValueError("dt must be provided for EventLists") self.dt = dt norm = norm.lower() self.norm = norm @@ -138,12 +148,6 @@ def __init__( if not good_input: return self._initialize_empty() - if type(data1) not in [EventList, Lightcurve] or type(data2) not in [ - EventList, - Lightcurve, - ]: - raise TypeError("One of the arguments is not of type eventlist or lightcurve") - if isinstance(data1, EventList): self.lc1 = data1.to_lc(self.dt) else: @@ -161,7 +165,9 @@ def __init__( self.min_freq = min_freq self.max_freq = max_freq - self._make_crossspectrum(self.lc1, self.lc2, fullspec, method) + self._make_crossspectrum( + self.lc1, self.lc2, fullspec, method=method, oversampling=oversampling + ) if self.power_type == "abs": self.power = np.abs(self.power) self.power_err = np.abs(self.power_err) @@ -174,7 +180,6 @@ def __init__( self.unnorm_power_err = np.real(self.unnorm_power_err) self._make_auxil_pds(self.lc1, self.lc2) - # method default will change to fast after implementation of the fast algorithm def initial_checks( self, data1=None, @@ -186,7 +191,8 @@ def initial_checks( max_freq=None, fullspec=False, df=None, - method="slow", + method="fast", + oversampling=5, ): if not isinstance(norm, str): raise TypeError("norm must be a string") @@ -207,10 +213,28 @@ def initial_checks( if max_freq is not None: if max_freq < min_freq or max_freq < 0: raise ValueError("max_freq must be non-negative and greater than min_freq") + + if method not in ["fast", "slow"]: + raise ValueError("method must be one of ['fast','slow']") + + if not isinstance(oversampling, int): + raise TypeError("oversampling must be an integer") + + if not isinstance(df, float) and df is not None: + raise TypeError("df must be a float") + + if not isinstance(fullspec, bool): + raise TypeError("fullspec must be a boolean") + + if type(data1) not in [EventList, Lightcurve] or type(data2) not in [ + EventList, + Lightcurve, + ]: + raise TypeError("One of the arguments is not of type eventlist or lightcurve") + return True - # method default will change to fast after implementation of the fast algorithm - def _make_crossspectrum(self, lc1, lc2, fullspec=False, method="fast"): + def _make_crossspectrum(self, lc1, lc2, fullspec=False, method="fast", oversampling=5): """ Auxiliary method computing the normalized cross spectrum from two light curves. This includes checking for the presence of and @@ -272,7 +296,13 @@ def _make_crossspectrum(self, lc1, lc2, fullspec=False, method="fast"): self.m = 1 - self.freq, self.unnorm_power = self._ls_cross(self.lc1, self.lc2, fullspec=fullspec) + self.freq, self.unnorm_power = self._ls_cross( + self.lc1, + self.lc2, + fullspec=fullspec, + method=method, + oversampling=oversampling, + ) self.power = self._normalize_crossspectrum(self.unnorm_power) if lc1.err_dist.lower() != lc2.err_dist.lower(): @@ -327,8 +357,7 @@ def _make_auxil_pds(self, lc1, lc2): df=self.df, ) - # method default will change to fast after implementation of the fast algorithm - def _ls_cross(self, lc1, lc2, ww=None, fullspec=False, method="slow"): + def _ls_cross(self, lc1, lc2, ww=None, fullspec=False, method="fast", oversampling=5): """ Lomb-Scargle Fourier transform the two light curves, then compute the cross spectrum. Computed as CS = lc1 x lc2* (where lc2 is the one that gets @@ -387,8 +416,18 @@ def _ls_cross(self, lc1, lc2, ww=None, fullspec=False, method="slow"): if method == "slow": lsft1 = lsft_slow(lc1.counts, lc1.time, ww, sign=1, fullspec=fullspec) lsft2 = lsft_slow(lc2.counts, lc2.time, ww, sign=-1, fullspec=fullspec) - # elif method == "fast": - # lsft1 = lsft_fast(lc1, ww, fullspec=fullspec) + elif method == "fast": + lsft1 = lsft_fast( + lc1.counts, lc1.time, ww, fullspec=fullspec, oversampling=oversampling + ) + lsft2 = lsft_fast( + lc2.counts, + lc2.time, + ww, + fullspec=fullspec, + sign=-1, + oversampling=oversampling, + ) cross = np.multiply(lsft1, lsft2) freq = ww if not fullspec: @@ -412,6 +451,23 @@ def _initialize_empty(self): self.k = 1 return + def time_lag(self): + r"""Calculate the fourier time lag of the cross spectrum. + The time lag is calculated by taking the phase lag :math:`\phi` and + + ..math:: + + \tau = \frac{\phi}{\two pi \nu} + + where :math:`\nu` is the center of the frequency bins. + """ + if self.__class__ == LombScargleCrossspectrum: + ph_lag = self.phase_lag() + + return ph_lag / (2 * np.pi * self.freq) + else: + raise AttributeError("Object has no attribute named 'time_lag' !") + class LombScarglePowerspectrum(LombScargleCrossspectrum): type = "powerspectrum" @@ -458,6 +514,9 @@ class LombScarglePowerspectrum(LombScargleCrossspectrum): and `slow` are the allowed values. Default is `fast`. fast uses the optimized Press and Rybicki O(n*log(n)) + oversampling : float, optional, default: 5 + Interpolation Oversampling Factor (for the fast algorithm) + Attributes ---------- norm: {"leahy" | "frac" | "abs" | "none" } @@ -503,6 +562,7 @@ def __init__( max_freq: Optional[float] = None, df: Optional[float] = None, method: Optional[str] = "fast", + oversampling: Optional[int] = 5, ): self._type = None data1 = copy.deepcopy(data) @@ -520,6 +580,7 @@ def __init__( max_freq=max_freq, df=df, method=method, + oversampling=oversampling, ) if type(data) not in [EventList, Lightcurve, None]: good_input = False @@ -543,6 +604,7 @@ def __init__( max_freq=max_freq, df=df, method=method, + oversampling=oversampling, ) self.nphots = self.nphots1 From ed68d7570556e25994ff9f6e54368cab9acbc25c Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Thu, 29 Jun 2023 02:12:13 +0530 Subject: [PATCH 07/31] Fixed the fast LSFT implementation and refactoring variable names --- stingray/fourier.py | 94 ++++++++++++++++++++++++++--------------- stingray/lombscargle.py | 79 +++++++++++++++++++--------------- 2 files changed, 104 insertions(+), 69 deletions(-) diff --git a/stingray/fourier.py b/stingray/fourier.py index 5875fb74f..ed46d5826 100644 --- a/stingray/fourier.py +++ b/stingray/fourier.py @@ -2029,47 +2029,60 @@ def lsft_fast( if sign not in [1, -1]: raise ValueError("sign must be 1 or -1") - freqs = np.sort(freqs) + # Constants initialization + const1 = np.sqrt(0.5) + const2 = const1 * sign + + sum_xx = np.sum(y) + + num_xt = len(y) + num_ww = len(freqs) + # Arrays initialization + ft_real = ft_imag = np.zeros((num_ww)) + ft_res = np.zeros((num_ww), dtype=np.complex128) f0, df, N = freqs[0], freqs[1] - freqs[0], len(freqs) - Sh, Ch = trig_sum( - t, - y, - df, - N, - f0, - oversampling=oversampling, - ) - S2, C2 = trig_sum( - t, - np.ones_like(y), - df, - N, - f0, - freq_factor=2, - oversampling=oversampling, + # Sum (y_i * cos(wt - wtau)) + sumr, sumi = trig_sum(t, y, df, N, f0, oversampling=oversampling) + + # Summation of (cos(wt - wtau))^2 and (sin(wt - wtau))^2 + s2cos, c2cos = trig_sum( + t, np.ones_like(y) / len(y), df, N, f0, freq_factor=2, oversampling=oversampling ) + # cos^2(x) = (1 + cos(2x))/2 + # sin^2(x) = (1 - cos(2x))/2 + scos2, ssin2 = np.abs(1 + c2cos) / 2, np.abs(1 - s2cos) / 2 - tan_2wtau = S2 / C2 + freqs_new = f0 + df * np.arange(N) - S2w = tan_2wtau / np.sqrt(1 + tan_2wtau**2) - C2w = 1 / np.sqrt(1 + tan_2wtau**2) + fft_freqs = np.fft.ifft(freqs_new, N) - Cw = np.sqrt(0.5) * np.sqrt(1 + C2w) - Sw = np.sqrt(0.5) * np.sign(S2w) * np.sqrt(1 - S2w) + # Summation of cos(2wt) and sin(2wt) + csum, ssum = fft_freqs.real, fft_freqs.imag - YC = Ch * Cw + Sh * Sw - YS = Sh * Cw - Ch * Sw - CC = 0.5 * (N + C2 * C2w + S2 * S2w) - SS = 0.5 * (N - C2 * C2w + S2 * S2w) + # Looping through all the frequencies + for i in range(num_ww): + if i == 0: + ft_real = sum_xx / np.sqrt(num_xt) + ft_imag = 0 + phase_this = 0 + else: + # Angular Frequency + wrun = freqs[i] * 2 * np.pi - ft_res = (YC**2 / CC) + 1j * (YS**2 / SS) + # arctan(ssum / csum) + watan = np.arctan2(ssum[i], csum[i]) + wtau = 0.5 * watan - ft_res *= np.exp(-1j * freqs * 2 * np.pi) + # Calculating the real and imaginary components of the Fourier transform + ft_real = const1 * (sumr[i] / np.sqrt(scos2[i])) + ft_imag = const2 * (sumi[i] / np.sqrt(ssin2[i])) - if sign == -1: - ft_res = np.conjugate(ft_res) + # Phase of current angular frequency + phase_this = wtau - wrun * t[0] + # Resultant fourier transform for current angular frequency + ft_res[i] = np.multiply(np.complex128(complex(ft_real, ft_imag)), np.exp(1j * phase_this)) if fullspec: return ft_res @@ -2083,7 +2096,7 @@ def lsft_slow( freqs: npt.ArrayLike, sign: Optional[int] = 1, fullspec: Optional[bool] = False, -): +): """ Calculates the Lomb-Scargle Fourier transform of a light curve. @@ -2112,7 +2125,8 @@ def lsft_slow( if sign not in [1, -1]: raise ValueError("sign must be 1 or -1") - const1 = 1 / np.sqrt(2) + # Constants initialization + const1 = np.sqrt(0.5) const2 = const1 * sign sum_xx = np.sum(y) @@ -2120,33 +2134,45 @@ def lsft_slow( num_xt = len(y) num_ww = len(freqs) + # Arrays initialization ft_real = ft_imag = np.zeros((num_ww)) ft_res = np.zeros((num_ww), dtype=np.complex128) + + # Looping through all the frequencies for i in range(num_ww): if i == 0: ft_real = sum_xx / np.sqrt(num_xt) ft_imag = 0 phase_this = 0 else: + # Angular Frequency wrun = freqs[i] * 2 * np.pi + # Summation of cos(2wt) and sin(2wt) csum = np.sum(np.cos(2.0 * wrun * t)) ssum = np.sum(np.sin(2.0 * wrun * t)) + # arctan(ssum / csum) watan = np.arctan2(ssum, csum) wtau = 0.5 * watan + # Sum (y_i * cos(wt - wtau)) sumr = np.sum(np.multiply(y, np.cos(wrun * t - wtau))) sumi = np.sum(np.multiply(y, np.sin(wrun * t - wtau))) + # Summation of (cos(wt - wtau))^2 and (sin(wt - wtau))^2 scos2 = np.sum((np.power(np.cos(wrun * t - wtau), 2))) ssin2 = np.sum((np.power(np.sin(wrun * t - wtau), 2))) + # Calculating the real and imaginary components of the Fourier transform ft_real = np.multiply(const1, np.divide(sumr, np.sqrt(scos2))) ft_imag = np.multiply(const2, np.divide(sumi, np.sqrt(ssin2))) + + # Phase of current angular frequency phase_this = wtau - wrun * t[0] - work = np.multiply(np.complex128(complex(ft_real, ft_imag)), np.exp(1j * phase_this)) - ft_res[i] = work + # Resultant fourier transform for current angular frequency + ft_res[i] = np.multiply(np.complex128(complex(ft_real, ft_imag)), np.exp(1j * phase_this)) + if fullspec: return ft_res else: diff --git a/stingray/lombscargle.py b/stingray/lombscargle.py index f706a066f..8435b8454 100644 --- a/stingray/lombscargle.py +++ b/stingray/lombscargle.py @@ -69,7 +69,7 @@ class LombScargleCrossspectrum(Crossspectrum): Attributes ---------- - freq: numpy.ndarray + freqs: numpy.ndarray The array of mid-bin frequencies that the Fourier transform samples power: numpy.ndarray @@ -165,6 +165,8 @@ def __init__( self.min_freq = min_freq self.max_freq = max_freq + self.method = method + self.oversampling = oversampling self._make_crossspectrum( self.lc1, self.lc2, fullspec, method=method, oversampling=oversampling ) @@ -182,17 +184,17 @@ def __init__( def initial_checks( self, - data1=None, - data2=None, - norm=None, - power_type=None, - dt=None, - min_freq=0, - max_freq=None, - fullspec=False, - df=None, - method="fast", - oversampling=5, + data1, + data2, + norm, + power_type, + dt, + min_freq, + max_freq, + fullspec, + df, + method, + oversampling, ): if not isinstance(norm, str): raise TypeError("norm must be a string") @@ -220,8 +222,8 @@ def initial_checks( if not isinstance(oversampling, int): raise TypeError("oversampling must be an integer") - if not isinstance(df, float) and df is not None: - raise TypeError("df must be a float") + # if not isinstance(df, float) and df is not None: + # raise TypeError("df must be a float") if not isinstance(fullspec, bool): raise TypeError("fullspec must be a boolean") @@ -234,7 +236,7 @@ def initial_checks( return True - def _make_crossspectrum(self, lc1, lc2, fullspec=False, method="fast", oversampling=5): + def _make_crossspectrum(self, lc1, lc2, fullspec, method, oversampling): """ Auxiliary method computing the normalized cross spectrum from two light curves. This includes checking for the presence of and @@ -296,7 +298,7 @@ def _make_crossspectrum(self, lc1, lc2, fullspec=False, method="fast", oversampl self.m = 1 - self.freq, self.unnorm_power = self._ls_cross( + self.freqs, self.unnorm_power = self._ls_cross( self.lc1, self.lc2, fullspec=fullspec, @@ -344,6 +346,8 @@ def _make_auxil_pds(self, lc1, lc2): min_freq=self.min_freq, max_freq=self.max_freq, df=self.df, + method=self.method, + oversampling=self.oversampling, ) self.pds2 = LombScargleCrossspectrum( lc2, @@ -355,9 +359,11 @@ def _make_auxil_pds(self, lc1, lc2): min_freq=self.min_freq, max_freq=self.max_freq, df=self.df, + method=self.method, + oversampling=self.oversampling, ) - def _ls_cross(self, lc1, lc2, ww=None, fullspec=False, method="fast", oversampling=5): + def _ls_cross(self, lc1, lc2, freqs=None, fullspec=False, method="fast", oversampling=5): """ Lomb-Scargle Fourier transform the two light curves, then compute the cross spectrum. Computed as CS = lc1 x lc2* (where lc2 is the one that gets @@ -384,15 +390,15 @@ def _ls_cross(self, lc1, lc2, ww=None, fullspec=False, method="fast", oversampli Returns ------- - freq: numpy.ndarray + freqs: numpy.ndarray The frequency grid at which the LSFT was evaluated cross: numpy.ndarray The cross spectrum value at each frequency. """ - if not ww: - ww = ( + if not freqs: + freqs = ( LombScargle( lc1.time, lc1.counts, @@ -401,7 +407,7 @@ def _ls_cross(self, lc1, lc2, ww=None, fullspec=False, method="fast", oversampli normalization="psd", ).autofrequency(minimum_frequency=self.min_freq, maximum_frequency=self.max_freq), )[0] - ww2 = ( + freqs2 = ( LombScargle( lc2.time, lc2.counts, @@ -410,33 +416,36 @@ def _ls_cross(self, lc1, lc2, ww=None, fullspec=False, method="fast", oversampli normalization="psd", ).autofrequency(minimum_frequency=self.min_freq, maximum_frequency=self.max_freq), )[0] - if max(ww2) > max(ww): - ww = ww2 + if max(freqs2) > max(freqs): + freqs = freqs2 if method == "slow": - lsft1 = lsft_slow(lc1.counts, lc1.time, ww, sign=1, fullspec=fullspec) - lsft2 = lsft_slow(lc2.counts, lc2.time, ww, sign=-1, fullspec=fullspec) + lsft1 = lsft_slow(lc1.counts, lc1.time, freqs, sign=1, fullspec=fullspec) + lsft2 = lsft_slow(lc2.counts, lc2.time, freqs, sign=-1, fullspec=fullspec) elif method == "fast": lsft1 = lsft_fast( - lc1.counts, lc1.time, ww, fullspec=fullspec, oversampling=oversampling + lc1.counts, + lc1.time, + freqs, + fullspec=fullspec, + oversampling=oversampling, ) lsft2 = lsft_fast( lc2.counts, lc2.time, - ww, + freqs, fullspec=fullspec, sign=-1, oversampling=oversampling, ) cross = np.multiply(lsft1, lsft2) - freq = ww if not fullspec: - freq = freq[freq > 0] - cross = cross[freq > 0] - return freq, cross + freqs = freqs[freqs > 0] + cross = cross[freqs > 0] + return freqs, cross def _initialize_empty(self): - self.freq = None + self.freqs = None self.power = None self.power_err = None self.unnorm_power = None @@ -464,7 +473,7 @@ def time_lag(self): if self.__class__ == LombScargleCrossspectrum: ph_lag = self.phase_lag() - return ph_lag / (2 * np.pi * self.freq) + return ph_lag / (2 * np.pi * self.freqs) else: raise AttributeError("Object has no attribute named 'time_lag' !") @@ -522,7 +531,7 @@ class LombScarglePowerspectrum(LombScargleCrossspectrum): norm: {"leahy" | "frac" | "abs" | "none" } The normalization of the power spectrum. - freq: numpy.ndarray + freqs: numpy.ndarray The array of mid-bin frequencies that the Fourier transform samples. power: numpy.ndarray @@ -566,7 +575,7 @@ def __init__( ): self._type = None data1 = copy.deepcopy(data) - data2 = copy.deepcopy(data1) + data2 = copy.deepcopy(data) good_input = data is not None if not skip_checks: good_input = self.initial_checks( From f225487cde69efab18c8102cd463c8942f6fdbba Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Sat, 29 Jul 2023 23:53:51 +0530 Subject: [PATCH 08/31] Added basic tests, corrected algorithms wrt papers, typos in docstrings --- docs/changes/737.feature.rst | 2 +- stingray/fourier.py | 138 +++++++++++++--------------- stingray/lombscargle.py | 141 ++++++++++++++++++++--------- stingray/tests/test_lombscargle.py | 118 ++++++++++++++++++++++++ 4 files changed, 282 insertions(+), 117 deletions(-) create mode 100644 stingray/tests/test_lombscargle.py diff --git a/docs/changes/737.feature.rst b/docs/changes/737.feature.rst index 534f4c495..70e8eb97b 100644 --- a/docs/changes/737.feature.rst +++ b/docs/changes/737.feature.rst @@ -1,2 +1,2 @@ -- Implemented the Lomb Scargle Fourier Transform +- Implemented the Lomb Scargle Fourier Transform (fast and slow versions) - Using which wrote the corresponding :class:`LombScargleCrossspectrum` and :class:`LombScarglePowerspectrum` \ No newline at end of file diff --git a/stingray/fourier.py b/stingray/fourier.py index ed46d5826..6f883c154 100644 --- a/stingray/fourier.py +++ b/stingray/fourier.py @@ -2030,64 +2030,69 @@ def lsft_fast( raise ValueError("sign must be 1 or -1") # Constants initialization - const1 = np.sqrt(0.5) - const2 = const1 * sign - sum_xx = np.sum(y) - num_xt = len(y) num_ww = len(freqs) + const1 = np.sqrt(0.5) * np.sqrt(num_ww) + const2 = const1 # Arrays initialization ft_real = ft_imag = np.zeros((num_ww)) - ft_res = np.zeros((num_ww), dtype=np.complex128) f0, df, N = freqs[0], freqs[1] - freqs[0], len(freqs) # Sum (y_i * cos(wt - wtau)) - sumr, sumi = trig_sum(t, y, df, N, f0, oversampling=oversampling) + Sh, Ch = trig_sum(t, y, df, N, f0, oversampling=oversampling) # Summation of (cos(wt - wtau))^2 and (sin(wt - wtau))^2 - s2cos, c2cos = trig_sum( - t, np.ones_like(y) / len(y), df, N, f0, freq_factor=2, oversampling=oversampling + _, scos2x = trig_sum( + t, + np.ones_like(y) / len(y), + df, + N, + f0, + freq_factor=2, + oversampling=oversampling, ) # cos^2(x) = (1 + cos(2x))/2 # sin^2(x) = (1 - cos(2x))/2 - scos2, ssin2 = np.abs(1 + c2cos) / 2, np.abs(1 - s2cos) / 2 + C2, S2 = (1 + scos2x) / 2, (1 - scos2x) / 2 + # Angular Frequency + w = freqs * 2 * np.pi - freqs_new = f0 + df * np.arange(N) + # Summation of cos(2wt) and sin(2wt) + csum, ssum = trig_sum( + t, np.ones_like(y) / len(y), df, N, f0, freq_factor=2, oversampling=oversampling + ) - fft_freqs = np.fft.ifft(freqs_new, N) + wtau = 0.5 * np.arctan2(ssum, csum) - # Summation of cos(2wt) and sin(2wt) - csum, ssum = fft_freqs.real, fft_freqs.imag + coswtau = np.cos(wtau) + sinwtau = np.sin(wtau) - # Looping through all the frequencies - for i in range(num_ww): - if i == 0: - ft_real = sum_xx / np.sqrt(num_xt) - ft_imag = 0 - phase_this = 0 - else: - # Angular Frequency - wrun = freqs[i] * 2 * np.pi + sumr = Ch * coswtau + Sh * sinwtau + sumi = Sh * coswtau - Ch * sinwtau - # arctan(ssum / csum) - watan = np.arctan2(ssum[i], csum[i]) - wtau = 0.5 * watan + cos2wtau = np.cos(2 * wtau) + sin2wtau = np.sin(2 * wtau) - # Calculating the real and imaginary components of the Fourier transform - ft_real = const1 * (sumr[i] / np.sqrt(scos2[i])) - ft_imag = const2 * (sumi[i] / np.sqrt(ssin2[i])) + scos2 = 0.5 * (N + C2 * cos2wtau + S2 * sin2wtau) + ssin2 = 0.5 * (N - C2 * cos2wtau - S2 * sin2wtau) - # Phase of current angular frequency - phase_this = wtau - wrun * t[0] - # Resultant fourier transform for current angular frequency - ft_res[i] = np.multiply(np.complex128(complex(ft_real, ft_imag)), np.exp(1j * phase_this)) + # Calculating the real and imaginary components of the Fourier transform + ft_real = const1 * sumr / np.sqrt(scos2) + ft_imag = const2 * sumi / np.sqrt(ssin2) - if fullspec: - return ft_res - else: - return ft_res[freqs > 0] + # Looping through all the frequencies + ft_real[0] = sum_xx / np.sqrt(num_xt) + ft_imag[0] = 0 + + # Resultant fourier transform for current angular frequency + ft_res = np.complex128(ft_real + (1j * ft_imag)) * np.exp(-1j * w * t[0]) + + if sign == -1: + ft_res = np.conjugate(ft_res) + + return ft_res if fullspec else ft_res[freqs > 0] def lsft_slow( @@ -2122,58 +2127,43 @@ def lsft_slow( ft_res : numpy.ndarray An array of Fourier transformed data. """ - if sign not in [1, -1]: + if sign not in (1, -1): raise ValueError("sign must be 1 or -1") # Constants initialization - const1 = np.sqrt(0.5) - const2 = const1 * sign - - sum_xx = np.sum(y) - - num_xt = len(y) - num_ww = len(freqs) + const1 = np.sqrt(0.5) * np.sqrt(len(y)) + const2 = const1 - # Arrays initialization - ft_real = ft_imag = np.zeros((num_ww)) - ft_res = np.zeros((num_ww), dtype=np.complex128) + ft_real = np.zeros_like(freqs) + ft_imag = np.zeros_like(freqs) + ft_res = np.zeros_like(freqs, dtype=np.complex128) - # Looping through all the frequencies - for i in range(num_ww): + for i, freq in enumerate(freqs): + wrun = freq * 2 * np.pi if i == 0: - ft_real = sum_xx / np.sqrt(num_xt) - ft_imag = 0 - phase_this = 0 + ft_real[i] = sum(y) / np.sqrt(len(y)) + ft_imag[i] = 0 else: - # Angular Frequency - wrun = freqs[i] * 2 * np.pi - - # Summation of cos(2wt) and sin(2wt) csum = np.sum(np.cos(2.0 * wrun * t)) ssum = np.sum(np.sin(2.0 * wrun * t)) - # arctan(ssum / csum) watan = np.arctan2(ssum, csum) wtau = 0.5 * watan - # Sum (y_i * cos(wt - wtau)) - sumr = np.sum(np.multiply(y, np.cos(wrun * t - wtau))) - sumi = np.sum(np.multiply(y, np.sin(wrun * t - wtau))) + cos_wt_wtau = np.cos(wrun * t - wtau) + sin_wt_wtau = np.sin(wrun * t - wtau) - # Summation of (cos(wt - wtau))^2 and (sin(wt - wtau))^2 - scos2 = np.sum((np.power(np.cos(wrun * t - wtau), 2))) - ssin2 = np.sum((np.power(np.sin(wrun * t - wtau), 2))) + sumr = np.sum(y * cos_wt_wtau) + sumi = np.sum(y * sin_wt_wtau) - # Calculating the real and imaginary components of the Fourier transform - ft_real = np.multiply(const1, np.divide(sumr, np.sqrt(scos2))) - ft_imag = np.multiply(const2, np.divide(sumi, np.sqrt(ssin2))) + scos2 = np.sum(np.power(cos_wt_wtau, 2)) + ssin2 = np.sum(np.power(sin_wt_wtau, 2)) - # Phase of current angular frequency - phase_this = wtau - wrun * t[0] - # Resultant fourier transform for current angular frequency - ft_res[i] = np.multiply(np.complex128(complex(ft_real, ft_imag)), np.exp(1j * phase_this)) + ft_real[i] = const1 * sumr / np.sqrt(scos2) + ft_imag[i] = const2 * sumi / np.sqrt(ssin2) - if fullspec: - return ft_res - else: - return ft_res[freqs > 0] + ft_res[i] = np.complex128(ft_real[i] + (1j * ft_imag[i])) * np.exp(-1j * wrun * t[0]) + if sign == -1: + ft_res = np.conjugate(ft_res) + + return ft_res if fullspec else ft_res[freqs > 0] diff --git a/stingray/lombscargle.py b/stingray/lombscargle.py index 8435b8454..60d6e4498 100644 --- a/stingray/lombscargle.py +++ b/stingray/lombscargle.py @@ -30,10 +30,10 @@ class LombScargleCrossspectrum(Crossspectrum): data2: :class:`stingray.lightcurve.Lightcurve` or :class:`stingray.events.EventList`, optional, default ``None`` The dataset for the second, or "reference", band. - norm: {``frac``, ``abs``, ``leahy``, ``none``}, default ``none`` - The normalization of the (real part of the) cross spectrum. + norm: {``frac``, ``abs``, ``leahy``, ``none``}, string, optional, default ``none`` + The normalization of the cross spectrum. - power_type: string, optional, default ``real`` + power_type: {``real``, ``absolute``, ``all`}, string, optional, default ``real`` Parameter to choose among complete, real part and magnitude of the cross spectrum. fullspec: boolean, optional, default ``False`` @@ -69,7 +69,7 @@ class LombScargleCrossspectrum(Crossspectrum): Attributes ---------- - freqs: numpy.ndarray + freq: numpy.ndarray The array of mid-bin frequencies that the Fourier transform samples power: numpy.ndarray @@ -101,6 +101,13 @@ class LombScargleCrossspectrum(Crossspectrum): nphots2: float The total number of photons in light curve 2 + References + ---------- + .. [1] Scargle, J. D. , "Studies in astronomical time series analysis. III - Fourier + transforms, autocorrelation functions, and cross-correlation + functions of unevenly spaced data". ApJ 1:343, p874-887, 1989 + .. [2] Press W.H. and Rybicki, G.B, "Fast algorithm for spectral analysis + of unevenly sampled data". ApJ 1:338, p277, 1989 """ def __init__( @@ -120,6 +127,23 @@ def __init__( ): self._type = None good_input = data1 is not None and data2 is not None + if data1 is None and data2 is None: + if not skip_checks: + good_input = self.initial_checks( + data1=data1, + data2=data2, + norm=norm, + power_type=power_type, + dt=dt, + fullspec=fullspec, + min_freq=min_freq, + max_freq=max_freq, + df=df, + method=method, + oversampling=oversampling, + ) + self._initialize_empty() + return if not skip_checks: good_input = self.initial_checks( data1=data1, @@ -134,7 +158,6 @@ def __init__( method=method, oversampling=oversampling, ) - if dt is None: if isinstance(data1, Lightcurve): dt = data1.dt @@ -208,10 +231,12 @@ def initial_checks( if power_type not in ["all", "absolute", "real"]: raise ValueError("power_type must be one of ['all','absolute','real']") - if data1 is None or data2 is None: + if np.logical_xor(data1 is None, data2 is None): raise ValueError("You can't do a cross spectrum with just one lightcurve") + if min_freq < 0: raise ValueError("min_freq must be non-negative") + if max_freq is not None: if max_freq < min_freq or max_freq < 0: raise ValueError("max_freq must be non-negative and greater than min_freq") @@ -222,17 +247,40 @@ def initial_checks( if not isinstance(oversampling, int): raise TypeError("oversampling must be an integer") - # if not isinstance(df, float) and df is not None: - # raise TypeError("df must be a float") - if not isinstance(fullspec, bool): raise TypeError("fullspec must be a boolean") - if type(data1) not in [EventList, Lightcurve] or type(data2) not in [ - EventList, - Lightcurve, - ]: - raise TypeError("One of the arguments is not of type eventlist or lightcurve") + if np.logical_xor( + not (isinstance(data1, EventList) or isinstance(data1, Lightcurve) or data1 is None), + not (isinstance(data2, EventList) or isinstance(data2, Lightcurve) or data2 is None), + ): + raise TypeError("One of the arguments is not of type Eventlist or Lightcurve or None") + + if not ( + isinstance(data1, EventList) or isinstance(data1, Lightcurve) or data1 is None + ) and ( + not (isinstance(data2, EventList) or isinstance(data2, Lightcurve) or data2 is None), + ): + raise TypeError("Both the events are not of type Eventlist or Lightcurve or None") + + if type(data1) == type(data2): + if isinstance(data1, Lightcurve): + if len(data1.time) != len(data2.time): + raise ValueError("data1 and data2 must have the same length") + if isinstance(data1, EventList): + lc1 = data1.to_lc() + lc2 = data2.to_lc() + if len(lc1.time) != len(lc2.time): + raise ValueError("data1 and data2 must have the same length") + else: + if isinstance(data1, EventList): + lc1 = data1.to_lc() + if len(lc1.time) != len(data2.time): + raise ValueError("data1 and data2 must have the same length") + elif isinstance(data2, EventList): + lc2 = data2.to_lc() + if len(lc2.time) != len(data1.time): + raise ValueError("data1 and data2 must have the same length") return True @@ -298,7 +346,7 @@ def _make_crossspectrum(self, lc1, lc2, fullspec, method, oversampling): self.m = 1 - self.freqs, self.unnorm_power = self._ls_cross( + self.freq, self.unnorm_power = self._ls_cross( self.lc1, self.lc2, fullspec=fullspec, @@ -309,8 +357,8 @@ def _make_crossspectrum(self, lc1, lc2, fullspec, method, oversampling): self.power = self._normalize_crossspectrum(self.unnorm_power) if lc1.err_dist.lower() != lc2.err_dist.lower(): simon( - "Your lightcurves have different statistics.", - "The errors in the Crossspectrum will be incorrect.", + "Your lightcurves have different statistics." + "The errors in the Crossspectrum will be incorrect." ) elif lc1.err_dist.lower() != "poisson": @@ -363,7 +411,7 @@ def _make_auxil_pds(self, lc1, lc2): oversampling=self.oversampling, ) - def _ls_cross(self, lc1, lc2, freqs=None, fullspec=False, method="fast", oversampling=5): + def _ls_cross(self, lc1, lc2, freq=None, fullspec=False, method="fast", oversampling=5): """ Lomb-Scargle Fourier transform the two light curves, then compute the cross spectrum. Computed as CS = lc1 x lc2* (where lc2 is the one that gets @@ -390,15 +438,15 @@ def _ls_cross(self, lc1, lc2, freqs=None, fullspec=False, method="fast", oversam Returns ------- - freqs: numpy.ndarray + freq: numpy.ndarray The frequency grid at which the LSFT was evaluated cross: numpy.ndarray The cross spectrum value at each frequency. """ - if not freqs: - freqs = ( + if not freq: + freq = ( LombScargle( lc1.time, lc1.counts, @@ -416,36 +464,36 @@ def _ls_cross(self, lc1, lc2, freqs=None, fullspec=False, method="fast", oversam normalization="psd", ).autofrequency(minimum_frequency=self.min_freq, maximum_frequency=self.max_freq), )[0] - if max(freqs2) > max(freqs): - freqs = freqs2 + if max(freqs2) > max(freq): + freq = freqs2 if method == "slow": - lsft1 = lsft_slow(lc1.counts, lc1.time, freqs, sign=1, fullspec=fullspec) - lsft2 = lsft_slow(lc2.counts, lc2.time, freqs, sign=-1, fullspec=fullspec) + lsft1 = lsft_slow(lc1.counts, lc1.time, freq, sign=-1, fullspec=fullspec) + lsft2 = lsft_slow(lc2.counts, lc2.time, freq, sign=1, fullspec=fullspec) elif method == "fast": lsft1 = lsft_fast( lc1.counts, lc1.time, - freqs, + freq, + sign=-1, fullspec=fullspec, oversampling=oversampling, ) lsft2 = lsft_fast( lc2.counts, lc2.time, - freqs, + freq, fullspec=fullspec, - sign=-1, oversampling=oversampling, ) cross = np.multiply(lsft1, lsft2) if not fullspec: - freqs = freqs[freqs > 0] - cross = cross[freqs > 0] - return freqs, cross + freq = freq[freq > 0] + cross = cross[freq > 0] + return freq, cross def _initialize_empty(self): - self.freqs = None + self.freq = None self.power = None self.power_err = None self.unnorm_power = None @@ -458,6 +506,13 @@ def _initialize_empty(self): self.n = None self.fullspec = None self.k = 1 + self.err_dist = None + self.method = None + self.meancounts1 = None + self.meancounts2 = None + self.oversampling = None + self.variance1 = None + self.variance2 = None return def time_lag(self): @@ -473,7 +528,7 @@ def time_lag(self): if self.__class__ == LombScargleCrossspectrum: ph_lag = self.phase_lag() - return ph_lag / (2 * np.pi * self.freqs) + return ph_lag / (2 * np.pi * self.freq) else: raise AttributeError("Object has no attribute named 'time_lag' !") @@ -494,9 +549,14 @@ class LombScarglePowerspectrum(LombScargleCrossspectrum): data: :class:`stingray.lightcurve.Lightcurve` or :class:`stingray.events.EventList` object, optional, default ``None`` The light curve data to be Fourier-transformed. - norm: {"leahy" | "frac" | "abs" | "none" }, optional, default "frac" - The normaliation of the power spectrum to be used. Options are - "leahy", "frac", "abs" and "none", default is "frac". + norm: {``frac``, ``abs``, ``leahy``, ``none``}, string, optional, default ``none`` + The normalization of the cross spectrum. + + power_type: {``real``, ``absolute``, ``all`}, string, optional, default ``real`` + Parameter to choose among complete, real part and magnitude of the cross spectrum. + + fullspec: boolean, optional, default ``False`` + If False, keep only the positive frequencies, or if True, keep all of them . Other Parameters ---------------- @@ -528,10 +588,7 @@ class LombScarglePowerspectrum(LombScargleCrossspectrum): Attributes ---------- - norm: {"leahy" | "frac" | "abs" | "none" } - The normalization of the power spectrum. - - freqs: numpy.ndarray + freq: numpy.ndarray The array of mid-bin frequencies that the Fourier transform samples. power: numpy.ndarray @@ -562,8 +619,8 @@ class LombScarglePowerspectrum(LombScargleCrossspectrum): def __init__( self, data: Optional[Union[Lightcurve, EventList]] = None, - norm: Optional[str] = "none", - power_type: Optional[str] = "real", + norm: Optional[str] = "frac", + power_type: Optional[str] = "all", dt: Optional[float] = None, fullspec: Optional[bool] = False, skip_checks: Optional[bool] = False, diff --git a/stingray/tests/test_lombscargle.py b/stingray/tests/test_lombscargle.py new file mode 100644 index 000000000..d4d845ca1 --- /dev/null +++ b/stingray/tests/test_lombscargle.py @@ -0,0 +1,118 @@ +import numpy as np +import pytest +import copy +from stingray.lombscargle import LombScargleCrossspectrum, LombScarglePowerspectrum +from stingray.lightcurve import Lightcurve +from stingray.events import EventList +from stingray.simulator import Simulator +from stingray.exceptions import StingrayError +from scipy.interpolate import interp1d + + +class TestLombScargleCrossspectrum: + def setup_class(self): + sim = Simulator(0.0001, 10000, 100, 1, random_state=42, tstart=0) + lc1 = sim.simulate(0) + lc2 = sim.simulate(0) + self.rate1 = lc1.countrate + self.rate2 = lc2.countrate + low, high = lc1.time.min(), lc1.time.max() + s1 = lc1.counts + s2 = lc2.counts + t = lc1.time + t_new = t.copy() + t_new[1:-1] = t[1:-1] + (np.random.rand(len(t) - 2) / (high - low)) + s1_new = interp1d(t, s1, fill_value="extrapolate")(t_new) + s2_new = interp1d(t, s2, fill_value="extrapolate")(t_new) + self.lc1 = Lightcurve(t, s1_new, dt=lc1.dt) + self.lc2 = Lightcurve(t, s2_new, dt=lc2.dt) + with pytest.warns(UserWarning) as record: + self.lscs = LombScargleCrossspectrum(lc1, lc2) + + @pytest.mark.parametrize("skip_checks", [True, False]) + def test_initialize_empty(self, skip_checks): + lscs = LombScargleCrossspectrum(skip_checks=skip_checks) + assert lscs.freq is None + assert lscs.power is None + + def test_make_empty_crossspectrum(self): + lscs = LombScargleCrossspectrum() + assert lscs.freq is None + assert lscs.power is None + assert lscs.df is None + assert lscs.nphots1 is None + assert lscs.nphots2 is None + assert lscs.m == 1 + assert lscs.n is None + assert lscs.power_err is None + assert lscs.dt is None + assert lscs.err_dist is None + assert lscs.variance1 is None + assert lscs.variance2 is None + assert lscs.meancounts1 is None + assert lscs.meancounts2 is None + assert lscs.oversampling is None + assert lscs.method is None + + def test_init_with_one_lc_none(self): + with pytest.raises(ValueError): + lscs = LombScargleCrossspectrum(self.lc1) + + def test_init_with_norm_not_str(self): + with pytest.raises(TypeError): + lscs = LombScargleCrossspectrum(norm=1) + + def test_init_with_invalid_norm(self): + with pytest.raises(ValueError): + lscs = LombScargleCrossspectrum(norm="frabs") + + def test_init_with_power_type_not_str(self): + with pytest.raises(TypeError): + lscs = LombScargleCrossspectrum(power_type=3) + + def test_init_with_invalid_power_type(self): + with pytest.raises(ValueError): + lscs = LombScargleCrossspectrum(power_type="reel") + + def test_init_with_wrong_lc_instance(self): + lc1_ = {"a": 1, "b": 2} + lc2_ = {"a": 1, "b": 2} + with pytest.raises(TypeError): + lscs = LombScargleCrossspectrum(lc1_, lc2_) + + def test_init_with_wrong_lc2_instance(self): + lc_ = {"a": 1, "b": 2} + with pytest.raises(TypeError): + lscs = LombScargleCrossspectrum(self.lc1, lc_) + + def test_init_with_wrong_lc1_instance(self): + lc_ = {"a": 1, "b": 2} + with pytest.raises(TypeError): + lscs = LombScargleCrossspectrum(lc_, self.lc2) + + def test_init_with_invalid_min_freq(self): + with pytest.raises(ValueError): + lscs = LombScargleCrossspectrum(self.lc1, self.lc2, min_freq=-1) + + def test_init_with_invalid_max_freq(self): + with pytest.raises(ValueError): + lscs = LombScargleCrossspectrum(self.lc1, self.lc2, max_freq=1, min_freq=3) + + def test_make_crossspectrum_diff_lc_counts_shape(self): + lc_ = Simulator(0.0001, 10423, 100, 1, random_state=42, tstart=0).simulate(0) + with pytest.raises(ValueError): + lscs = LombScargleCrossspectrum(self.lc1, lc_) + + def test_make_crossspectrum_diff_lc_stat(self): + lc_ = copy.deepcopy(self.lc1) + lc_.err_dist = "gauss" + with pytest.warns(UserWarning) as record: + cs = LombScargleCrossspectrum(self.lc1, lc_) + assert np.any(["different statistics" in r.message.args[0] for r in record]) + + def test_make_crossspectrum_diff_dt(self): + lc_ = Simulator(0.0002, 10000, 100, 1, random_state=42, tstart=0).simulate(0) + with pytest.raises( + StingrayError, match="Lightcurves do not have the same time binning dt." + ): + lscs = LombScargleCrossspectrum(self.lc1, lc_) From 0b91e8b8dcab513eb8c34f0cb38b72224054db26 Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Mon, 21 Aug 2023 02:53:21 +0530 Subject: [PATCH 09/31] Documentation, Algorithm Fixes, API Change --- stingray/fourier.py | 150 ++++++++++++++++++++++++---------------- stingray/lombscargle.py | 92 +++++++++++++++++------- 2 files changed, 156 insertions(+), 86 deletions(-) diff --git a/stingray/fourier.py b/stingray/fourier.py index 6f883c154..3ffc40245 100644 --- a/stingray/fourier.py +++ b/stingray/fourier.py @@ -1995,11 +1995,12 @@ def lsft_fast( t: npt.ArrayLike, freqs: npt.ArrayLike, sign: Optional[int] = 1, - fullspec: Optional[bool] = False, oversampling: Optional[int] = 5, -): +) -> npt.NDArray: """ Calculates the Lomb-Scargle Fourier transform of a light curve. + Only considers non-negative frequencies. + Subtracts mean from data as it is required for the working of the algorithm. Parameters ---------- @@ -2026,46 +2027,42 @@ def lsft_fast( ft_res : numpy.ndarray An array of Fourier transformed data. """ - if sign not in [1, -1]: - raise ValueError("sign must be 1 or -1") - + y_ = copy.deepcopy(y) - np.mean(y) + freqs = freqs[freqs >= 0] # Constants initialization - sum_xx = np.sum(y) - num_xt = len(y) + sum_xx = np.sum(y_) + num_xt = len(y_) num_ww = len(freqs) - const1 = np.sqrt(0.5) * np.sqrt(num_ww) - const2 = const1 # Arrays initialization ft_real = ft_imag = np.zeros((num_ww)) f0, df, N = freqs[0], freqs[1] - freqs[0], len(freqs) # Sum (y_i * cos(wt - wtau)) - Sh, Ch = trig_sum(t, y, df, N, f0, oversampling=oversampling) + Sh, Ch = trig_sum(t, y_, df, N, f0, oversampling=oversampling) - # Summation of (cos(wt - wtau))^2 and (sin(wt - wtau))^2 - _, scos2x = trig_sum( - t, - np.ones_like(y) / len(y), - df, - N, - f0, - freq_factor=2, - oversampling=oversampling, - ) - # cos^2(x) = (1 + cos(2x))/2 - # sin^2(x) = (1 - cos(2x))/2 - C2, S2 = (1 + scos2x) / 2, (1 - scos2x) / 2 # Angular Frequency w = freqs * 2 * np.pi # Summation of cos(2wt) and sin(2wt) csum, ssum = trig_sum( - t, np.ones_like(y) / len(y), df, N, f0, freq_factor=2, oversampling=oversampling + t, np.ones_like(y_) / len(y_), df, N, f0, freq_factor=2, oversampling=oversampling ) wtau = 0.5 * np.arctan2(ssum, csum) + S2, C2 = trig_sum( + t, + np.ones_like(y_), + df, + N, + f0, + freq_factor=2, + oversampling=oversampling, + ) + + const = np.sqrt(0.5) * np.sqrt(num_ww) + coswtau = np.cos(wtau) sinwtau = np.sin(wtau) @@ -2078,21 +2075,17 @@ def lsft_fast( scos2 = 0.5 * (N + C2 * cos2wtau + S2 * sin2wtau) ssin2 = 0.5 * (N - C2 * cos2wtau - S2 * sin2wtau) - # Calculating the real and imaginary components of the Fourier transform - ft_real = const1 * sumr / np.sqrt(scos2) - ft_imag = const2 * sumi / np.sqrt(ssin2) + ft_real = const * sumr / np.sqrt(scos2) + ft_imag = const * np.sign(sign) * sumi / np.sqrt(ssin2) - # Looping through all the frequencies - ft_real[0] = sum_xx / np.sqrt(num_xt) - ft_imag[0] = 0 + ft_real[freqs == 0] = sum_xx / np.sqrt(num_xt) + ft_imag[freqs == 0] = 0 - # Resultant fourier transform for current angular frequency - ft_res = np.complex128(ft_real + (1j * ft_imag)) * np.exp(-1j * w * t[0]) + phase = wtau - w * t[0] - if sign == -1: - ft_res = np.conjugate(ft_res) + ft_res = np.complex128(ft_real + (1j * ft_imag)) * np.exp(-1j * phase) - return ft_res if fullspec else ft_res[freqs > 0] + return ft_res def lsft_slow( @@ -2100,10 +2093,11 @@ def lsft_slow( t: npt.ArrayLike, freqs: npt.ArrayLike, sign: Optional[int] = 1, - fullspec: Optional[bool] = False, -): +) -> npt.NDArray: """ Calculates the Lomb-Scargle Fourier transform of a light curve. + Only considers non-negative frequencies. + Subtracts mean from data as it is required for the working of the algorithm. Parameters ---------- @@ -2127,22 +2121,27 @@ def lsft_slow( ft_res : numpy.ndarray An array of Fourier transformed data. """ - if sign not in (1, -1): - raise ValueError("sign must be 1 or -1") - - # Constants initialization - const1 = np.sqrt(0.5) * np.sqrt(len(y)) - const2 = const1 + y_ = copy.deepcopy(y) - np.mean(y) + freqs = freqs[freqs >= 0] ft_real = np.zeros_like(freqs) ft_imag = np.zeros_like(freqs) ft_res = np.zeros_like(freqs, dtype=np.complex128) - for i, freq in enumerate(freqs): - wrun = freq * 2 * np.pi - if i == 0: - ft_real[i] = sum(y) / np.sqrt(len(y)) - ft_imag[i] = 0 + num_y = len(y_) + num_freqs = len(freqs) + sum_y = np.sum(y_) + const1 = np.sqrt(0.5) * np.sqrt(num_y) + const2 = const1 * np.sign(sign) + ft_real = ft_imag = np.zeros(num_freqs) + ft_res = np.zeros(num_freqs, dtype=np.complex128) + + for i in range(num_freqs): + wrun = freqs[i] * 2 * np.pi + if wrun == 0: + ft_real = sum_y / np.sqrt(num_y) + ft_imag = 0 + phase_this = 0 else: csum = np.sum(np.cos(2.0 * wrun * t)) ssum = np.sum(np.sin(2.0 * wrun * t)) @@ -2150,20 +2149,51 @@ def lsft_slow( watan = np.arctan2(ssum, csum) wtau = 0.5 * watan - cos_wt_wtau = np.cos(wrun * t - wtau) - sin_wt_wtau = np.sin(wrun * t - wtau) + sumr = np.sum(y_ * np.cos(wrun * t - wtau)) + sumi = np.sum(y_ * np.sin(wrun * t - wtau)) - sumr = np.sum(y * cos_wt_wtau) - sumi = np.sum(y * sin_wt_wtau) + scos2 = np.sum(np.power(np.cos(wrun * t - wtau), 2)) + ssin2 = np.sum(np.power(np.sin(wrun * t - wtau), 2)) - scos2 = np.sum(np.power(cos_wt_wtau, 2)) - ssin2 = np.sum(np.power(sin_wt_wtau, 2)) + ft_real = const1 * sumr / np.sqrt(scos2) + ft_imag = const2 * sumi / np.sqrt(ssin2) + phase_this = wtau - wrun * t[0] + ft_res[i] = np.complex128(ft_real + (1j * ft_imag)) * np.exp(-1j * phase_this) + return ft_res - ft_real[i] = const1 * sumr / np.sqrt(scos2) - ft_imag[i] = const2 * sumi / np.sqrt(ssin2) - ft_res[i] = np.complex128(ft_real[i] + (1j * ft_imag[i])) * np.exp(-1j * wrun * t[0]) - if sign == -1: - ft_res = np.conjugate(ft_res) +def impose_symmetry_lsft( + ft_res: npt.ArrayLike, + sum_y: float, + len_y: int, + freqs: npt.ArrayLike, +) -> npt.ArrayLike: + """ + Impose symmetry on the input fourier transform. - return ft_res if fullspec else ft_res[freqs > 0] + Parameters + ---------- + ft_res : np.array + The Fourier transform of the signal. + sum_y : float + The sum of the values of the signal. + len_y : int + The length of the signal. + freqs : np.array + An array of frequencies at which the transform is sampled. + + Returns + ------- + lsft_res : np.array + The Fourier transform of the signal with symmetry imposed. + freqs_new : np.array + The new frequencies + """ + zero_present = np.amy(freqs == 0) + if not zero_present: + ft_res_new = np.concatenate([np.conjugate(np.flip(ft_res)), 0, ft_res]) + freqs_new = np.concatenate([-np.flip(freqs), 0, freqs]) + else: + ft_res_new = np.concatenate([np.conjugate(np.flip(ft_res))[1:], ft_res]) + freqs_new = np.concatenate([-np.flip(freqs)[1:], freqs]) + return ft_res_new, freqs_new diff --git a/stingray/lombscargle.py b/stingray/lombscargle.py index 60d6e4498..e285e190a 100644 --- a/stingray/lombscargle.py +++ b/stingray/lombscargle.py @@ -8,7 +8,7 @@ from .crossspectrum import Crossspectrum from .events import EventList from .exceptions import StingrayError -from .fourier import lsft_fast, lsft_slow +from .fourier import lsft_fast, lsft_slow, impose_symmetry_lsft from .lightcurve import Lightcurve from .utils import simon @@ -33,7 +33,7 @@ class LombScargleCrossspectrum(Crossspectrum): norm: {``frac``, ``abs``, ``leahy``, ``none``}, string, optional, default ``none`` The normalization of the cross spectrum. - power_type: {``real``, ``absolute``, ``all`}, string, optional, default ``real`` + power_type: {``real``, ``absolute``, ``all`}, string, optional, default ``all`` Parameter to choose among complete, real part and magnitude of the cross spectrum. fullspec: boolean, optional, default ``False`` @@ -115,7 +115,7 @@ def __init__( data1: Optional[Union[EventList, Lightcurve]] = None, data2: Optional[Union[EventList, Lightcurve]] = None, norm: Optional[str] = "none", - power_type: Optional[str] = "real", + power_type: Optional[str] = "all", dt: Optional[float] = None, fullspec: Optional[bool] = False, skip_checks: bool = False, @@ -453,7 +453,9 @@ def _ls_cross(self, lc1, lc2, freq=None, fullspec=False, method="fast", oversamp fit_mean=False, center_data=False, normalization="psd", - ).autofrequency(minimum_frequency=self.min_freq, maximum_frequency=self.max_freq), + ).autofrequency( + minimum_frequency=max(self.min_freq, 0), maximum_frequency=self.max_freq + ), )[0] freqs2 = ( LombScargle( @@ -462,34 +464,43 @@ def _ls_cross(self, lc1, lc2, freq=None, fullspec=False, method="fast", oversamp fit_mean=False, center_data=False, normalization="psd", - ).autofrequency(minimum_frequency=self.min_freq, maximum_frequency=self.max_freq), + ).autofrequency( + minimum_frequency=max(self.min_freq, 0), maximum_frequency=self.max_freq + ), )[0] if max(freqs2) > max(freq): freq = freqs2 if method == "slow": - lsft1 = lsft_slow(lc1.counts, lc1.time, freq, sign=-1, fullspec=fullspec) - lsft2 = lsft_slow(lc2.counts, lc2.time, freq, sign=1, fullspec=fullspec) + lsft1 = lsft_slow(lc1.counts, lc1.time, freq) + lsft2 = lsft_slow(lc2.counts, lc2.time, freq) elif method == "fast": lsft1 = lsft_fast( lc1.counts, lc1.time, freq, - sign=-1, - fullspec=fullspec, oversampling=oversampling, ) lsft2 = lsft_fast( lc2.counts, lc2.time, freq, - fullspec=fullspec, oversampling=oversampling, ) - cross = np.multiply(lsft1, lsft2) - if not fullspec: - freq = freq[freq > 0] - cross = cross[freq > 0] + if fullspec: + lsft1, _ = impose_symmetry_lsft( + lsft1, + np.sum((lc1.counts)), + lc1.n, + freq, + ) + lsft2, freq = impose_symmetry_lsft( + lsft2, + np.sum(lc2.counts), + lc2.n, + freq, + ) + cross = np.multiply(lsft1, np.conjugate(lsft2)) return freq, cross def _initialize_empty(self): @@ -515,22 +526,51 @@ def _initialize_empty(self): self.variance2 = None return + def phase_lag(self): + """Not applicable for unevenly sampled data""" + raise AttributeError( + "Object has no attribute named 'phase_lag' ! Not applicable for unevenly sampled data" + ) + def time_lag(self): - r"""Calculate the fourier time lag of the cross spectrum. - The time lag is calculated by taking the phase lag :math:`\phi` and + """Not applicable for unevenly sampled data""" + raise AttributeError( + "Object has no attribute named 'time_lag' ! Not applicable for unevenly sampled data" + ) - ..math:: + def classical_significances(self, threshold=1, trial_correction=False): + """Not applicable for unevenly sampled data""" + raise AttributeError( + "Object has no attribute named 'classical_significances' ! Not applicable for unevenly sampled data" + ) - \tau = \frac{\phi}{\two pi \nu} + def from_time_array(): + """Not applicable for unevenly sampled data""" + raise AttributeError( + "Object has no attribute named 'from_time_array' ! Not applicable for unevenly sampled data" + ) - where :math:`\nu` is the center of the frequency bins. - """ - if self.__class__ == LombScargleCrossspectrum: - ph_lag = self.phase_lag() + def from_events(): + """Not applicable for unevenly sampled data""" + raise AttributeError( + "Object has no attribute named 'from_events' ! Not applicable for unevenly sampled data" + ) - return ph_lag / (2 * np.pi * self.freq) - else: - raise AttributeError("Object has no attribute named 'time_lag' !") + def from_lightcurve(): + """Not applicable for unevenly sampled data""" + raise AttributeError( + "Object has no attribute named 'from_lightcurve' ! Not applicable for unevenly sampled data" + ) + + def from_lc_iterable(): + """Not applicable for unevenly sampled data""" + raise AttributeError( + "Object has no attribute named 'from_lc_iterable' ! Not applicable for unevenly sampled data" + ) + + def _initialize_from_any_input(): + """Not required for unevenly sampled data""" + raise AttributeError("Object has no attribute named '_initialize_from_any_input' !") class LombScarglePowerspectrum(LombScargleCrossspectrum): @@ -552,7 +592,7 @@ class LombScarglePowerspectrum(LombScargleCrossspectrum): norm: {``frac``, ``abs``, ``leahy``, ``none``}, string, optional, default ``none`` The normalization of the cross spectrum. - power_type: {``real``, ``absolute``, ``all`}, string, optional, default ``real`` + power_type: {``real``, ``absolute``, ``all`}, string, optional, default ``all`` Parameter to choose among complete, real part and magnitude of the cross spectrum. fullspec: boolean, optional, default ``False`` From a3bd5d2a62ea922fba95b9f80c34ea45de3bff0e Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Wed, 23 Aug 2023 20:20:56 +0530 Subject: [PATCH 10/31] Edited docs(core.rst,lscs,lsps), modified function, docstrings, formatting,,new tests --- docs/core.rst | 17 ++++++++++++++ stingray/fourier.py | 9 ++------ stingray/lombscargle.py | 32 +++++--------------------- stingray/tests/test_fourier.py | 42 ++++++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 33 deletions(-) diff --git a/docs/core.rst b/docs/core.rst index e9f3353f7..56ec7088f 100644 --- a/docs/core.rst +++ b/docs/core.rst @@ -85,3 +85,20 @@ Multi-taper Periodogram :maxdepth: 2 notebooks/Multitaper/multitaper_example.ipynb + + +Lomb Scargle Crossspectrum +-------------------------- +.. toctree:: + :maxdepth: 2 + + notebooks/LombScargle/LombScargleCrossspectrum_tutorial.ipynb + + +Lomb Scargle Powerspectrum +-------------------------- + +.. toctree:: + :maxdepth: 2 + + notebooks/LombScargle/LombScarglePowerspectrum_tutorial.ipynb \ No newline at end of file diff --git a/stingray/fourier.py b/stingray/fourier.py index 3ffc40245..f9a4e6c53 100644 --- a/stingray/fourier.py +++ b/stingray/fourier.py @@ -2189,11 +2189,6 @@ def impose_symmetry_lsft( freqs_new : np.array The new frequencies """ - zero_present = np.amy(freqs == 0) - if not zero_present: - ft_res_new = np.concatenate([np.conjugate(np.flip(ft_res)), 0, ft_res]) - freqs_new = np.concatenate([-np.flip(freqs), 0, freqs]) - else: - ft_res_new = np.concatenate([np.conjugate(np.flip(ft_res))[1:], ft_res]) - freqs_new = np.concatenate([-np.flip(freqs)[1:], freqs]) + ft_res_new = np.concatenate([np.conjugate(np.flip(ft_res)), [0.0], ft_res]) + freqs_new = np.concatenate([np.flip(-freqs), [0.0], freqs]) return ft_res_new, freqs_new diff --git a/stingray/lombscargle.py b/stingray/lombscargle.py index e285e190a..22c29f20e 100644 --- a/stingray/lombscargle.py +++ b/stingray/lombscargle.py @@ -475,31 +475,11 @@ def _ls_cross(self, lc1, lc2, freq=None, fullspec=False, method="fast", oversamp lsft1 = lsft_slow(lc1.counts, lc1.time, freq) lsft2 = lsft_slow(lc2.counts, lc2.time, freq) elif method == "fast": - lsft1 = lsft_fast( - lc1.counts, - lc1.time, - freq, - oversampling=oversampling, - ) - lsft2 = lsft_fast( - lc2.counts, - lc2.time, - freq, - oversampling=oversampling, - ) + lsft1 = lsft_fast(lc1.counts, lc1.time, freq, oversampling=oversampling) + lsft2 = lsft_fast(lc2.counts, lc2.time, freq, oversampling=oversampling) if fullspec: - lsft1, _ = impose_symmetry_lsft( - lsft1, - np.sum((lc1.counts)), - lc1.n, - freq, - ) - lsft2, freq = impose_symmetry_lsft( - lsft2, - np.sum(lc2.counts), - lc2.n, - freq, - ) + lsft1, _ = impose_symmetry_lsft(lsft1, np.sum((lc1.counts)), lc1.n, freq) + lsft2, freq = impose_symmetry_lsft(lsft2, np.sum(lc2.counts), lc2.n, freq) cross = np.multiply(lsft1, np.conjugate(lsft2)) return freq, cross @@ -590,10 +570,10 @@ class LombScarglePowerspectrum(LombScargleCrossspectrum): The light curve data to be Fourier-transformed. norm: {``frac``, ``abs``, ``leahy``, ``none``}, string, optional, default ``none`` - The normalization of the cross spectrum. + The normalization of the power spectrum. power_type: {``real``, ``absolute``, ``all`}, string, optional, default ``all`` - Parameter to choose among complete, real part and magnitude of the cross spectrum. + Parameter to choose among complete, real part and magnitude of the power spectrum. fullspec: boolean, optional, default ``False`` If False, keep only the positive frequencies, or if True, keep all of them . diff --git a/stingray/tests/test_fourier.py b/stingray/tests/test_fourier.py index 480c9c2ba..31a51c022 100644 --- a/stingray/tests/test_fourier.py +++ b/stingray/tests/test_fourier.py @@ -556,3 +556,45 @@ def test_unnormalize_poisson_noise(self, norm, power_type): noise_notnorm = poisson_level("none", self.meanrate, self.nph) assert np.isclose(noise_notnorm, unnorm_noise) + + +def test_lsft_slow_fast(): + np.random.seed(0) + rand = np.random.default_rng(42) + n = 1000 + t = np.sort(rand.random(n)) * np.sqrt(n) + y = np.sin(2 * np.pi * 3.0 * t) + sub = np.min(y) + y -= sub + freqs = np.fft.fftfreq(n, np.median(np.diff(t, 1))) + freqs = freqs[freqs >= 0] + lsftslow = lsft_slow(y, t, freqs, sign=1) + lsftfast = lsft_fast(y, t, freqs, sign=1, oversampling=10) + assert np.argmax(lsftslow) == np.argmax(lsftfast) + assert round(freqs[np.argmax(lsftslow)], 1) == round(freqs[np.argmax(lsftfast)], 1) == 3.0 + assert np.all( + np.all((lsftslow * np.conjugate(lsftslow)).imag == 0) + & np.all((lsftfast * np.conjugate(lsftfast)).imag == 0) + ) + + +def test_impose_symmetry_lsft(): + np.random.seed(0) + rand = np.random.default_rng(42) + n = 1000 + t = np.sort(rand.random(n)) * np.sqrt(n) + y = np.sin(2 * np.pi * 3.0 * t) + sub = np.min(y) + y -= sub + freqs = np.fft.fftfreq(n, np.median(np.diff(t, 1))) + freqs = freqs[freqs >= 0] + lsftslow = lsft_slow(y, t, freqs, sign=1) + lsftfast = lsft_fast(y, t, freqs, sign=1, oversampling=5) + imp_sym_slow, freqs_new_slow = impose_symmetry_lsft(lsftslow, 0, n, freqs) + imp_sym_fast, freqs_new_fast = impose_symmetry_lsft(lsftfast, 0, n, freqs) + assert imp_sym_slow.shape == imp_sym_fast.shape == freqs_new_fast.shape == freqs_new_slow.shape + assert np.all((imp_sym_slow.real) == np.flip(imp_sym_slow.real)) + assert np.all((imp_sym_slow.imag) == -np.flip(imp_sym_slow.imag)) + assert np.all((imp_sym_fast.real) == np.flip(imp_sym_fast.real)) + assert np.all((imp_sym_fast.imag) == (-np.flip(imp_sym_fast.imag))) + assert np.all(freqs_new_slow == freqs_new_fast) From 4eb2be54c3a6eb6a518a33dde5e7fdb7e0808b68 Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Wed, 23 Aug 2023 23:00:54 +0530 Subject: [PATCH 11/31] Fixed Tests --- stingray/lombscargle.py | 74 ++++++-------------- stingray/tests/test_fourier.py | 5 +- stingray/tests/test_lombscargle.py | 106 ++++++++++++++++++++++++++++- 3 files changed, 126 insertions(+), 59 deletions(-) diff --git a/stingray/lombscargle.py b/stingray/lombscargle.py index 22c29f20e..19ca7ec7b 100644 --- a/stingray/lombscargle.py +++ b/stingray/lombscargle.py @@ -158,10 +158,16 @@ def __init__( method=method, oversampling=oversampling, ) + + if not good_input: + self._initialize_empty() + return if dt is None: - if isinstance(data1, Lightcurve): + if isinstance(data1, Lightcurve) or isinstance(data2, EventList): dt = data1.dt - elif isinstance(data1, EventList): + elif isinstance(data2, Lightcurve) or isinstance(data2, EventList): + dt = data2.dt + if dt is None: raise ValueError("dt must be provided for EventLists") self.dt = dt norm = norm.lower() @@ -264,22 +270,16 @@ def initial_checks( raise TypeError("Both the events are not of type Eventlist or Lightcurve or None") if type(data1) == type(data2): - if isinstance(data1, Lightcurve): + if data1 is not None: if len(data1.time) != len(data2.time): raise ValueError("data1 and data2 must have the same length") - if isinstance(data1, EventList): - lc1 = data1.to_lc() - lc2 = data2.to_lc() - if len(lc1.time) != len(lc2.time): - raise ValueError("data1 and data2 must have the same length") else: - if isinstance(data1, EventList): - lc1 = data1.to_lc() - if len(lc1.time) != len(data2.time): - raise ValueError("data1 and data2 must have the same length") - elif isinstance(data2, EventList): - lc2 = data2.to_lc() - if len(lc2.time) != len(data1.time): + if data1 is None or data2 is None: + return False + if (isinstance(data1, EventList) or isinstance(data2, EventList)) and ( + isinstance(data1, Lightcurve) or isinstance(data2, Lightcurve) + ): + if len(data1.time) != len(data2.time): raise ValueError("data1 and data2 must have the same length") return True @@ -307,15 +307,15 @@ def _make_crossspectrum(self, lc1, lc2, fullspec, method, oversampling): and Rybicki O(n*log(n)) """ - if not isinstance(lc1, Lightcurve): - raise TypeError("lc1 must be a lightcurve.Lightcurve object") - - if not isinstance(lc2, Lightcurve): - raise TypeError("lc2 must be a lightcurve.Lightcurve object") - if self.lc2.mjdref != self.lc1.mjdref: raise ValueError("MJDref is different in the two light curves") + if lc1.n != lc2.n: + raise StingrayError("Lightcurves do not have the same number of bins per segment.") + + if not np.isclose(lc1.dt, lc2.dt, rtol=0.1 * lc1.dt / lc1.tseg): + raise StingrayError("Lightcurves do not have the same time binning dt.") + self.meancounts1 = lc1.meancounts self.meancounts2 = lc2.meancounts @@ -332,12 +332,6 @@ def _make_crossspectrum(self, lc1, lc2, fullspec, method, oversampling): self.variance2 = np.mean(lc2.meancounts) ** 2 self.err_dist = "gauss" - if lc1.n != lc2.n: - raise StingrayError("Lightcurves do not have the same number of bins per segment.") - - if not np.isclose(lc1.dt, lc2.dt, rtol=0.1 * lc1.dt / lc1.tseg): - raise StingrayError("Lightcurves do not have the same time binning dt.") - lc1.dt = lc2.dt self.dt = lc1.dt self.n = lc1.n @@ -378,8 +372,6 @@ def _make_crossspectrum(self, lc1, lc2, fullspec, method, oversampling): self.unnorm_power_err /= np.divide(2, np.sqrt(np.abs(self.nphots1 * self.nphots2))) self.unnorm_power_err += np.zeros_like(self.unnorm_power) self.power_err = self._normalize_crossspectrum(self.unnorm_power_err) - else: - self.power_err = np.zeros(len(self.power)) def _make_auxil_pds(self, lc1, lc2): __doc__ = super()._make_auxil_pds.__doc__ @@ -653,30 +645,6 @@ def __init__( self._type = None data1 = copy.deepcopy(data) data2 = copy.deepcopy(data) - good_input = data is not None - if not skip_checks: - good_input = self.initial_checks( - data1=data1, - data2=data2, - norm=norm, - power_type=power_type, - dt=dt, - fullspec=fullspec, - min_freq=min_freq, - max_freq=max_freq, - df=df, - method=method, - oversampling=oversampling, - ) - if type(data) not in [EventList, Lightcurve, None]: - good_input = False - self.dt = dt - norm = norm.lower() - self.norm = norm - self.df = df - - if not good_input: - return self._initialize_empty() LombScargleCrossspectrum.__init__( self, diff --git a/stingray/tests/test_fourier.py b/stingray/tests/test_fourier.py index 31a51c022..b3602c2d9 100644 --- a/stingray/tests/test_fourier.py +++ b/stingray/tests/test_fourier.py @@ -572,9 +572,8 @@ def test_lsft_slow_fast(): lsftfast = lsft_fast(y, t, freqs, sign=1, oversampling=10) assert np.argmax(lsftslow) == np.argmax(lsftfast) assert round(freqs[np.argmax(lsftslow)], 1) == round(freqs[np.argmax(lsftfast)], 1) == 3.0 - assert np.all( - np.all((lsftslow * np.conjugate(lsftslow)).imag == 0) - & np.all((lsftfast * np.conjugate(lsftfast)).imag == 0) + assert np.allclose((lsftslow * np.conjugate(lsftslow)).imag, [0]) & np.allclose( + (lsftfast * np.conjugate(lsftfast)).imag, 0 ) diff --git a/stingray/tests/test_lombscargle.py b/stingray/tests/test_lombscargle.py index d4d845ca1..650240ef6 100644 --- a/stingray/tests/test_lombscargle.py +++ b/stingray/tests/test_lombscargle.py @@ -11,7 +11,7 @@ class TestLombScargleCrossspectrum: def setup_class(self): - sim = Simulator(0.0001, 10000, 100, 1, random_state=42, tstart=0) + sim = Simulator(0.0001, 100, 100, 1, random_state=42, tstart=0) lc1 = sim.simulate(0) lc2 = sim.simulate(0) self.rate1 = lc1.countrate @@ -99,7 +99,7 @@ def test_init_with_invalid_max_freq(self): lscs = LombScargleCrossspectrum(self.lc1, self.lc2, max_freq=1, min_freq=3) def test_make_crossspectrum_diff_lc_counts_shape(self): - lc_ = Simulator(0.0001, 10423, 100, 1, random_state=42, tstart=0).simulate(0) + lc_ = Simulator(0.0001, 103, 100, 1, random_state=42, tstart=0).simulate(0) with pytest.raises(ValueError): lscs = LombScargleCrossspectrum(self.lc1, lc_) @@ -111,8 +111,108 @@ def test_make_crossspectrum_diff_lc_stat(self): assert np.any(["different statistics" in r.message.args[0] for r in record]) def test_make_crossspectrum_diff_dt(self): - lc_ = Simulator(0.0002, 10000, 100, 1, random_state=42, tstart=0).simulate(0) + lc_ = Simulator(0.0002, 100, 100, 1, random_state=42, tstart=0).simulate(0) with pytest.raises( StingrayError, match="Lightcurves do not have the same time binning dt." ): lscs = LombScargleCrossspectrum(self.lc1, lc_) + + @pytest.mark.parametrize("power_type", ["real", "absolute", "all"]) + def test_power_type(self, power_type): + lscs = LombScargleCrossspectrum(self.lc1, self.lc2, power_type=power_type) + assert lscs.power_type == power_type + + @pytest.mark.parametrize("method", ["fft", "randommethod"]) + def test_init_with_invalid_method(self, method): + with pytest.raises(ValueError): + lscs = LombScargleCrossspectrum(self.lc1, self.lc2, method=method) + + def test_with_invalid_fullspec(self): + with pytest.raises(TypeError): + lscs = LombScargleCrossspectrum(self.lc1, self.lc2, fullspec=1) + + def test_with_invalid_oversampling(self): + with pytest.raises(TypeError): + lscs = LombScargleCrossspectrum(self.lc1, self.lc2, oversampling="invalid") + + def test_invalid_mixed_data(self): + data2 = EventList(self.lc2.time[3:], np.ones_like(self.lc2.time[3:])) + with pytest.raises(ValueError): + lscs = LombScargleCrossspectrum(self.lc1, data2) + with pytest.raises(ValueError): + lscs = LombScargleCrossspectrum(data2, self.lc1) + + def test_diff_mjdref(self): + lc3 = copy.deepcopy(self.lc1) + lc3.mjdref += 1 + with pytest.raises(ValueError): + lscs = LombScargleCrossspectrum(self.lc1, lc3) + + def test_fullspec(self): + lscs = LombScargleCrossspectrum(self.lc1, self.lc2, fullspec=True) + assert lscs.fullspec + + @pytest.mark.parametrize("method", ["slow", "fast"]) + def test_valid_method(self, method): + lscs = LombScargleCrossspectrum(self.lc1, self.lc2, method=method) + assert lscs.method == method + + @pytest.mark.parametrize( + "func", + [ + "phase_lag", + "time_lag", + "classical_significances", + "from_time_array", + "from_events", + "from_lightcurve", + "from_lc_iterable", + "_initialize_from_any_input ", + ], + ) + def test_raise_on_invalid_function(self, func): + with pytest.raises(AttributeError): + lscs = LombScargleCrossspectrum(self.lc1, self.lc2).func() + + +class TestLombScarglePowerspectrum: + def setup_class(self): + sim = Simulator(0.0001, 100, 100, 1, random_state=42, tstart=0) + lc = sim.simulate(0) + self.rate = lc.countrate + low, high = lc.time.min(), lc.time.max() + s1 = lc.counts + t = lc.time + t_new = t.copy() + t_new[1:-1] = t[1:-1] + (np.random.rand(len(t) - 2) / (high - low)) + s_new = interp1d(t, s1, fill_value="extrapolate")(t_new) + self.lc = Lightcurve(t, s_new, dt=lc.dt) + + @pytest.mark.parametrize("norm", ["leahy", "frac", "abs", "none"]) + def test_normalize_powerspectrum(self, norm): + lps = LombScarglePowerspectrum(self.lc, norm=norm) + assert lps.norm == norm + + @pytest.mark.parametrize("skip_checks", [True, False]) + def test_init_empty(self, skip_checks): + ps = LombScarglePowerspectrum(skip_checks=skip_checks) + assert ps.freq is None + assert ps.power is None + assert ps.power_err is None + assert ps.df is None + assert ps.m == 1 + + def test_make_empty_powerspectrum(self): + ps = LombScarglePowerspectrum() + assert ps.freq is None + assert ps.power is None + assert ps.power_err is None + assert ps.df is None + assert ps.m == 1 + assert ps.nphots1 is None + assert ps.nphots2 is None + assert ps.method is None + + def test_ps_real(self): + ps = LombScarglePowerspectrum(self.lc) + assert np.allclose(ps.power.imag, [0]) From e1e6bc5b1e2b1fdaa7c1ead293085ba7b0f85858 Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Fri, 1 Sep 2023 02:35:06 +0530 Subject: [PATCH 12/31] Improved Coverage --- stingray/lombscargle.py | 64 ++++++++++-------------------- stingray/tests/test_lombscargle.py | 52 +++++++++++++----------- 2 files changed, 50 insertions(+), 66 deletions(-) diff --git a/stingray/lombscargle.py b/stingray/lombscargle.py index 19ca7ec7b..7096611ef 100644 --- a/stingray/lombscargle.py +++ b/stingray/lombscargle.py @@ -126,24 +126,19 @@ def __init__( oversampling: int = 5, ): self._type = None - good_input = data1 is not None and data2 is not None + if data1 is None and data2 is None: - if not skip_checks: - good_input = self.initial_checks( - data1=data1, - data2=data2, - norm=norm, - power_type=power_type, - dt=dt, - fullspec=fullspec, - min_freq=min_freq, - max_freq=max_freq, - df=df, - method=method, - oversampling=oversampling, - ) self._initialize_empty() return + + if dt is None: + if isinstance(data1, Lightcurve) or isinstance(data2, EventList): + dt = data1.dt + elif isinstance(data2, Lightcurve) or isinstance(data2, EventList): + dt = data2.dt + if dt is None: + raise ValueError("dt must be provided for EventLists") + if not skip_checks: good_input = self.initial_checks( data1=data1, @@ -159,23 +154,15 @@ def __init__( oversampling=oversampling, ) - if not good_input: - self._initialize_empty() - return - if dt is None: - if isinstance(data1, Lightcurve) or isinstance(data2, EventList): - dt = data1.dt - elif isinstance(data2, Lightcurve) or isinstance(data2, EventList): - dt = data2.dt - if dt is None: - raise ValueError("dt must be provided for EventLists") + if not good_input: + self._initialize_empty() + return + self.dt = dt norm = norm.lower() self.norm = norm self.k = 1 self.df = df - if not good_input: - return self._initialize_empty() if isinstance(data1, EventList): self.lc1 = data1.to_lc(self.dt) @@ -199,7 +186,7 @@ def __init__( self._make_crossspectrum( self.lc1, self.lc2, fullspec, method=method, oversampling=oversampling ) - if self.power_type == "abs": + if self.power_type == "absolute": self.power = np.abs(self.power) self.power_err = np.abs(self.power_err) self.unnorm_power = np.abs(self.unnorm_power) @@ -307,15 +294,6 @@ def _make_crossspectrum(self, lc1, lc2, fullspec, method, oversampling): and Rybicki O(n*log(n)) """ - if self.lc2.mjdref != self.lc1.mjdref: - raise ValueError("MJDref is different in the two light curves") - - if lc1.n != lc2.n: - raise StingrayError("Lightcurves do not have the same number of bins per segment.") - - if not np.isclose(lc1.dt, lc2.dt, rtol=0.1 * lc1.dt / lc1.tseg): - raise StingrayError("Lightcurves do not have the same time binning dt.") - self.meancounts1 = lc1.meancounts self.meancounts2 = lc2.meancounts @@ -510,37 +488,37 @@ def time_lag(self): "Object has no attribute named 'time_lag' ! Not applicable for unevenly sampled data" ) - def classical_significances(self, threshold=1, trial_correction=False): + def classical_significances(self): """Not applicable for unevenly sampled data""" raise AttributeError( "Object has no attribute named 'classical_significances' ! Not applicable for unevenly sampled data" ) - def from_time_array(): + def from_time_array(self): """Not applicable for unevenly sampled data""" raise AttributeError( "Object has no attribute named 'from_time_array' ! Not applicable for unevenly sampled data" ) - def from_events(): + def from_events(self): """Not applicable for unevenly sampled data""" raise AttributeError( "Object has no attribute named 'from_events' ! Not applicable for unevenly sampled data" ) - def from_lightcurve(): + def from_lightcurve(self): """Not applicable for unevenly sampled data""" raise AttributeError( "Object has no attribute named 'from_lightcurve' ! Not applicable for unevenly sampled data" ) - def from_lc_iterable(): + def from_lc_iterable(self): """Not applicable for unevenly sampled data""" raise AttributeError( "Object has no attribute named 'from_lc_iterable' ! Not applicable for unevenly sampled data" ) - def _initialize_from_any_input(): + def _initialize_from_any_input(self): """Not required for unevenly sampled data""" raise AttributeError("Object has no attribute named '_initialize_from_any_input' !") diff --git a/stingray/tests/test_lombscargle.py b/stingray/tests/test_lombscargle.py index 650240ef6..5c48c2dbe 100644 --- a/stingray/tests/test_lombscargle.py +++ b/stingray/tests/test_lombscargle.py @@ -32,8 +32,8 @@ def setup_class(self): @pytest.mark.parametrize("skip_checks", [True, False]) def test_initialize_empty(self, skip_checks): lscs = LombScargleCrossspectrum(skip_checks=skip_checks) - assert lscs.freq is None - assert lscs.power is None + lscs.freq is None + lscs.power is None def test_make_empty_crossspectrum(self): lscs = LombScargleCrossspectrum() @@ -54,31 +54,35 @@ def test_make_empty_crossspectrum(self): assert lscs.oversampling is None assert lscs.method is None + def test_bad_input(self): + with pytest.raises(TypeError): + lscs = LombScargleCrossspectrum(1, self.lc1) + def test_init_with_one_lc_none(self): with pytest.raises(ValueError): - lscs = LombScargleCrossspectrum(self.lc1) + lscs = LombScargleCrossspectrum(self.lc1, None) def test_init_with_norm_not_str(self): with pytest.raises(TypeError): - lscs = LombScargleCrossspectrum(norm=1) + lscs = LombScargleCrossspectrum(self.lc1, self.lc2, norm=1) def test_init_with_invalid_norm(self): with pytest.raises(ValueError): - lscs = LombScargleCrossspectrum(norm="frabs") + lscs = LombScargleCrossspectrum(self.lc1, self.lc2, norm="frabs") def test_init_with_power_type_not_str(self): with pytest.raises(TypeError): - lscs = LombScargleCrossspectrum(power_type=3) + lscs = LombScargleCrossspectrum(self.lc1, self.lc2, power_type=3) def test_init_with_invalid_power_type(self): with pytest.raises(ValueError): - lscs = LombScargleCrossspectrum(power_type="reel") + lscs = LombScargleCrossspectrum(self.lc1, self.lc2, power_type="reel") def test_init_with_wrong_lc_instance(self): lc1_ = {"a": 1, "b": 2} lc2_ = {"a": 1, "b": 2} with pytest.raises(TypeError): - lscs = LombScargleCrossspectrum(lc1_, lc2_) + lscs = LombScargleCrossspectrum(lc1_, lc2_, dt=1) def test_init_with_wrong_lc2_instance(self): lc_ = {"a": 1, "b": 2} @@ -110,13 +114,6 @@ def test_make_crossspectrum_diff_lc_stat(self): cs = LombScargleCrossspectrum(self.lc1, lc_) assert np.any(["different statistics" in r.message.args[0] for r in record]) - def test_make_crossspectrum_diff_dt(self): - lc_ = Simulator(0.0002, 100, 100, 1, random_state=42, tstart=0).simulate(0) - with pytest.raises( - StingrayError, match="Lightcurves do not have the same time binning dt." - ): - lscs = LombScargleCrossspectrum(self.lc1, lc_) - @pytest.mark.parametrize("power_type", ["real", "absolute", "all"]) def test_power_type(self, power_type): lscs = LombScargleCrossspectrum(self.lc1, self.lc2, power_type=power_type) @@ -142,11 +139,12 @@ def test_invalid_mixed_data(self): with pytest.raises(ValueError): lscs = LombScargleCrossspectrum(data2, self.lc1) - def test_diff_mjdref(self): - lc3 = copy.deepcopy(self.lc1) - lc3.mjdref += 1 - with pytest.raises(ValueError): - lscs = LombScargleCrossspectrum(self.lc1, lc3) + def test_valid_mixed_data(self): + data2 = EventList(self.lc2.time, np.ones_like(self.lc2.time)) + lscs = LombScargleCrossspectrum(self.lc1, data2) + assert lscs.power is not None + lscs2 = LombScargleCrossspectrum(data2, self.lc1) + assert lscs2.power is not None def test_fullspec(self): lscs = LombScargleCrossspectrum(self.lc1, self.lc2, fullspec=True) @@ -158,7 +156,7 @@ def test_valid_method(self, method): assert lscs.method == method @pytest.mark.parametrize( - "func", + "func_name", [ "phase_lag", "time_lag", @@ -170,9 +168,17 @@ def test_valid_method(self, method): "_initialize_from_any_input ", ], ) - def test_raise_on_invalid_function(self, func): + def test_raise_on_invalid_function(self, func_name): with pytest.raises(AttributeError): - lscs = LombScargleCrossspectrum(self.lc1, self.lc2).func() + lscs = LombScargleCrossspectrum(self.lc1, self.lc2) + func = getattr(lscs, func_name) + func() + + def test_no_dt(self): + el1 = EventList(self.lc1.counts, self.lc1.time, dt=None) + el2 = EventList(self.lc2.counts, self.lc2.time, dt=None) + with pytest.raises(ValueError): + lscs = LombScargleCrossspectrum(el1, el2) class TestLombScarglePowerspectrum: From 9efe0099a802a85787d656dbfcf5667816109419 Mon Sep 17 00:00:00 2001 From: Matteo Bachetti Date: Tue, 5 Sep 2023 14:51:47 +0200 Subject: [PATCH 13/31] Comment code --- stingray/fourier.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/stingray/fourier.py b/stingray/fourier.py index f9a4e6c53..1b6f4ae0b 100644 --- a/stingray/fourier.py +++ b/stingray/fourier.py @@ -2143,21 +2143,33 @@ def lsft_slow( ft_imag = 0 phase_this = 0 else: + # Calculation of \omega \tau (II.5) -- csum = np.sum(np.cos(2.0 * wrun * t)) ssum = np.sum(np.sin(2.0 * wrun * t)) watan = np.arctan2(ssum, csum) wtau = 0.5 * watan + # -- + # In the following, instead of t'_n we are using \omega t'_n = \omega t - \omega\tau + # Terms of kind X_n * cos or sin (II.1) -- sumr = np.sum(y_ * np.cos(wrun * t - wtau)) sumi = np.sum(y_ * np.sin(wrun * t - wtau)) + # -- + # A and B before the square root and inversion in (II.3) -- scos2 = np.sum(np.power(np.cos(wrun * t - wtau), 2)) ssin2 = np.sum(np.power(np.sin(wrun * t - wtau), 2)) + ## -- + # const2 is const1 times the sign. + # It's the F0 in II.2 without the phase factor + # The sign decides whether we are calculating the direct or inverse transform ft_real = const1 * sumr / np.sqrt(scos2) ft_imag = const2 * sumi / np.sqrt(ssin2) + phase_this = wtau - wrun * t[0] + ft_res[i] = np.complex128(ft_real + (1j * ft_imag)) * np.exp(-1j * phase_this) return ft_res From fd91bc4a8d23ab6891593d0aeae2f77f6aca67bc Mon Sep 17 00:00:00 2001 From: Matteo Bachetti Date: Tue, 5 Sep 2023 15:37:54 +0200 Subject: [PATCH 14/31] Test time lag --- stingray/tests/test_fourier.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/stingray/tests/test_fourier.py b/stingray/tests/test_fourier.py index b3602c2d9..ea2e6370b 100644 --- a/stingray/tests/test_fourier.py +++ b/stingray/tests/test_fourier.py @@ -558,6 +558,24 @@ def test_unnormalize_poisson_noise(self, norm, power_type): assert np.isclose(noise_notnorm, unnorm_noise) +@pytest.mark.parametrize("phlag", [0.05, 0.1, 0.2, 0.4]) +def test_lag(phlag): + freq=1.1123232252 + def func(time, phase=0): + return 2 + np.sin(2 * np.pi * (time * freq - phase)) + time = np.sort(np.random.uniform(0, 100, 3000)) + ft0 = lsft_slow(func(time, 0), time, np.array([freq])) + ft1 = lsft_slow(func(time, phlag), time, np.array([freq])) + measured_lag = (np.angle(ft0) - np.angle(ft1)) / 2 / np.pi + while measured_lag > 0.5: + measured_lag -= 0.5 + while measured_lag <= -0.5: + measured_lag += 0.5 + + print(measured_lag) + assert np.isclose((np.angle(ft1) - np.angle(ft0)) / 2 / np.pi, phlag, atol=0.01) + + def test_lsft_slow_fast(): np.random.seed(0) rand = np.random.default_rng(42) From bc6541afd6de990f41519a03443909d726bfe506 Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Thu, 7 Sep 2023 01:41:21 +0530 Subject: [PATCH 15/31] Phase lag fixes, code cleanup and optimization --- stingray/fourier.py | 10 +++++----- stingray/lombscargle.py | 12 ++---------- stingray/tests/test_fourier.py | 8 +++++--- stingray/tests/test_lombscargle.py | 2 -- 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/stingray/fourier.py b/stingray/fourier.py index 1b6f4ae0b..cf6101ee2 100644 --- a/stingray/fourier.py +++ b/stingray/fourier.py @@ -2121,17 +2121,17 @@ def lsft_slow( ft_res : numpy.ndarray An array of Fourier transformed data. """ - y_ = copy.deepcopy(y) - np.mean(y) - freqs = freqs[freqs >= 0] + y_ = y - np.mean(y) + freqs = np.asarray(freqs[np.asarray(freqs) >= 0]) ft_real = np.zeros_like(freqs) ft_imag = np.zeros_like(freqs) ft_res = np.zeros_like(freqs, dtype=np.complex128) - num_y = len(y_) - num_freqs = len(freqs) + num_y = y_.shape[0] + num_freqs = freqs.shape[0] sum_y = np.sum(y_) - const1 = np.sqrt(0.5) * np.sqrt(num_y) + const1 = np.sqrt(0.5 * num_y) const2 = const1 * np.sign(sign) ft_real = ft_imag = np.zeros(num_freqs) ft_res = np.zeros(num_freqs, dtype=np.complex128) diff --git a/stingray/lombscargle.py b/stingray/lombscargle.py index 7096611ef..4756815a2 100644 --- a/stingray/lombscargle.py +++ b/stingray/lombscargle.py @@ -476,17 +476,9 @@ def _initialize_empty(self): self.variance2 = None return - def phase_lag(self): - """Not applicable for unevenly sampled data""" - raise AttributeError( - "Object has no attribute named 'phase_lag' ! Not applicable for unevenly sampled data" - ) - def time_lag(self): - """Not applicable for unevenly sampled data""" - raise AttributeError( - "Object has no attribute named 'time_lag' ! Not applicable for unevenly sampled data" - ) + super().__doc__ + return self.phase_lag() / (2 * np.pi * self.freq) def classical_significances(self): """Not applicable for unevenly sampled data""" diff --git a/stingray/tests/test_fourier.py b/stingray/tests/test_fourier.py index ea2e6370b..9f65542a9 100644 --- a/stingray/tests/test_fourier.py +++ b/stingray/tests/test_fourier.py @@ -560,9 +560,11 @@ def test_unnormalize_poisson_noise(self, norm, power_type): @pytest.mark.parametrize("phlag", [0.05, 0.1, 0.2, 0.4]) def test_lag(phlag): - freq=1.1123232252 + freq = 1.1123232252 + def func(time, phase=0): return 2 + np.sin(2 * np.pi * (time * freq - phase)) + time = np.sort(np.random.uniform(0, 100, 3000)) ft0 = lsft_slow(func(time, 0), time, np.array([freq])) ft1 = lsft_slow(func(time, phlag), time, np.array([freq])) @@ -571,9 +573,9 @@ def func(time, phase=0): measured_lag -= 0.5 while measured_lag <= -0.5: measured_lag += 0.5 - + print(measured_lag) - assert np.isclose((np.angle(ft1) - np.angle(ft0)) / 2 / np.pi, phlag, atol=0.01) + assert np.isclose((np.angle(ft1) - np.angle(ft0)) / 2 / np.pi, phlag, atol=0.02, rtol=0.02) def test_lsft_slow_fast(): diff --git a/stingray/tests/test_lombscargle.py b/stingray/tests/test_lombscargle.py index 5c48c2dbe..4672befd7 100644 --- a/stingray/tests/test_lombscargle.py +++ b/stingray/tests/test_lombscargle.py @@ -158,8 +158,6 @@ def test_valid_method(self, method): @pytest.mark.parametrize( "func_name", [ - "phase_lag", - "time_lag", "classical_significances", "from_time_array", "from_events", From ed96e46a9250fd1f028772c42869793159a1cc7b Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Thu, 7 Sep 2023 19:50:01 +0530 Subject: [PATCH 16/31] Test Coverage Improvement --- stingray/lombscargle.py | 2 -- stingray/tests/test_fourier.py | 2 +- stingray/tests/test_lombscargle.py | 4 +++- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/stingray/lombscargle.py b/stingray/lombscargle.py index 4756815a2..579774c12 100644 --- a/stingray/lombscargle.py +++ b/stingray/lombscargle.py @@ -261,8 +261,6 @@ def initial_checks( if len(data1.time) != len(data2.time): raise ValueError("data1 and data2 must have the same length") else: - if data1 is None or data2 is None: - return False if (isinstance(data1, EventList) or isinstance(data2, EventList)) and ( isinstance(data1, Lightcurve) or isinstance(data2, Lightcurve) ): diff --git a/stingray/tests/test_fourier.py b/stingray/tests/test_fourier.py index 9f65542a9..2f4c1be3b 100644 --- a/stingray/tests/test_fourier.py +++ b/stingray/tests/test_fourier.py @@ -559,7 +559,7 @@ def test_unnormalize_poisson_noise(self, norm, power_type): @pytest.mark.parametrize("phlag", [0.05, 0.1, 0.2, 0.4]) -def test_lag(phlag): +def test_lags(phlag): freq = 1.1123232252 def func(time, phase=0): diff --git a/stingray/tests/test_lombscargle.py b/stingray/tests/test_lombscargle.py index 4672befd7..b6094a00d 100644 --- a/stingray/tests/test_lombscargle.py +++ b/stingray/tests/test_lombscargle.py @@ -61,6 +61,8 @@ def test_bad_input(self): def test_init_with_one_lc_none(self): with pytest.raises(ValueError): lscs = LombScargleCrossspectrum(self.lc1, None) + with pytest.raises(ValueError): + lscs = LombScargleCrossspectrum(None, self.lc1) def test_init_with_norm_not_str(self): with pytest.raises(TypeError): @@ -163,7 +165,7 @@ def test_valid_method(self, method): "from_events", "from_lightcurve", "from_lc_iterable", - "_initialize_from_any_input ", + "_initialize_from_any_input", ], ) def test_raise_on_invalid_function(self, func_name): From f5d47b16de1633253ca4b887d95882ec823ae387 Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Sun, 17 Sep 2023 16:09:38 +0530 Subject: [PATCH 17/31] Updated docs and rewrote the classes to use modern interface from AveragedCrossspectrum --- stingray/lombscargle.py | 538 +++++++++++++++++------------ stingray/tests/test_lombscargle.py | 26 +- 2 files changed, 318 insertions(+), 246 deletions(-) diff --git a/stingray/lombscargle.py b/stingray/lombscargle.py index 579774c12..4db1c958b 100644 --- a/stingray/lombscargle.py +++ b/stingray/lombscargle.py @@ -1,6 +1,5 @@ import copy from typing import Optional, Union - import numpy as np import numpy.typing as npt from astropy.timeseries.periodograms import LombScargle @@ -131,6 +130,7 @@ def __init__( self._initialize_empty() return + good_input = True if dt is None: if isinstance(data1, Lightcurve) or isinstance(data2, EventList): dt = data1.dt @@ -158,45 +158,20 @@ def __init__( self._initialize_empty() return - self.dt = dt - norm = norm.lower() - self.norm = norm - self.k = 1 - self.df = df - - if isinstance(data1, EventList): - self.lc1 = data1.to_lc(self.dt) - else: - self.lc1 = data1 - if isinstance(data2, EventList): - self.lc2 = data2.to_lc(self.dt) - else: - self.lc2 = data2 - self.power_type = power_type - self.fullspec = fullspec - self.norm = norm - - self.nphots1 = self.lc1.counts.sum() - self.nphots2 = self.lc2.counts.sum() - - self.min_freq = min_freq - self.max_freq = max_freq - self.method = method - self.oversampling = oversampling - self._make_crossspectrum( - self.lc1, self.lc2, fullspec, method=method, oversampling=oversampling - ) - if self.power_type == "absolute": - self.power = np.abs(self.power) - self.power_err = np.abs(self.power_err) - self.unnorm_power = np.abs(self.unnorm_power) - self.unnorm_power_err = np.abs(self.unnorm_power_err) - if self.power_type == "real": - self.power = np.real(self.power) - self.power_err = np.real(self.power) - self.unnorm_power = np.real(self.unnorm_power) - self.unnorm_power_err = np.real(self.unnorm_power_err) - self._make_auxil_pds(self.lc1, self.lc2) + if data1 is not None and data2 is not None: + self._initialize_from_any_input( + data1, + data2, + dt=dt, + norm=norm, + power_type=power_type, + fullspec=fullspec, + min_freq=min_freq, + max_freq=max_freq, + df=None, + method=method, + oversampling=oversampling, + ) def initial_checks( self, @@ -224,8 +199,11 @@ def initial_checks( if power_type not in ["all", "absolute", "real"]: raise ValueError("power_type must be one of ['all','absolute','real']") - if np.logical_xor(data1 is None, data2 is None): - raise ValueError("You can't do a cross spectrum with just one lightcurve") + if data1 is None and data2 is None: + if data1 is not None and data2 is not None: + raise ValueError("You can't do a cross spectrum with just one lightcurve") + else: + return False if min_freq < 0: raise ValueError("min_freq must be non-negative") @@ -243,115 +221,79 @@ def initial_checks( if not isinstance(fullspec, bool): raise TypeError("fullspec must be a boolean") - if np.logical_xor( - not (isinstance(data1, EventList) or isinstance(data1, Lightcurve) or data1 is None), - not (isinstance(data2, EventList) or isinstance(data2, Lightcurve) or data2 is None), - ): - raise TypeError("One of the arguments is not of type Eventlist or Lightcurve or None") - - if not ( - isinstance(data1, EventList) or isinstance(data1, Lightcurve) or data1 is None - ) and ( - not (isinstance(data2, EventList) or isinstance(data2, Lightcurve) or data2 is None), - ): - raise TypeError("Both the events are not of type Eventlist or Lightcurve or None") - - if type(data1) == type(data2): - if data1 is not None: - if len(data1.time) != len(data2.time): - raise ValueError("data1 and data2 must have the same length") - else: - if (isinstance(data1, EventList) or isinstance(data2, EventList)) and ( - isinstance(data1, Lightcurve) or isinstance(data2, Lightcurve) - ): - if len(data1.time) != len(data2.time): - raise ValueError("data1 and data2 must have the same length") + dt_is_invalid = (dt is None) or (dt <= np.finfo(float).resolution) + if type(data1) != type(data2): + raise TypeError("data1 and data2 must be of the same kind") - return True - - def _make_crossspectrum(self, lc1, lc2, fullspec, method, oversampling): - """ - Auxiliary method computing the normalized cross spectrum from two - light curves. This includes checking for the presence of and - applying Good Time Intervals, computing the unnormalized Fourier - cross-amplitude, and then renormalizing using the required - normalization. Also computes an uncertainty estimate on the cross - spectral powers. - - Parameters - ---------- - lc1, lc2 : :class:`stingray.lightcurve.Lightcurve` objects - Two light curves used for computing the cross spectrum. - - fullspec: boolean, default ``False`` - Return full frequency array (True) or just positive frequencies (False) - - method : str - The method to be used by the Lomb-Scargle Fourier Transformation function. `fast` - and `slow` are the allowed values. Default is `fast`. fast uses the optimized Press - and Rybicki O(n*log(n)) - - """ - self.meancounts1 = lc1.meancounts - self.meancounts2 = lc2.meancounts - - self.err_dist = "poisson" - if lc1.err_dist == "poisson": - self.variance1 = lc1.meancounts - else: - self.variance1 = np.mean(lc1.meancounts) ** 2 - self.err_dist = "gauss" - - if lc2.err_dist == "poisson": - self.variance2 = lc2.meancounts + if isinstance(data1, EventList): + if dt_is_invalid: + raise ValueError( + "If using event lists, please specify the bin time to generate lightcurves." + ) + elif isinstance(data1, Lightcurve): + if data1.err_dist.lower() != data2.err_dist.lower(): + simon( + "Your lightcurves have different statistics." + "The errors in the Crossspectrum will be incorrect." + ) else: - self.variance2 = np.mean(lc2.meancounts) ** 2 - self.err_dist = "gauss" - - lc1.dt = lc2.dt - self.dt = lc1.dt - self.n = lc1.n - - self.df = 1.0 / lc1.tseg - - self.m = 1 - - self.freq, self.unnorm_power = self._ls_cross( - self.lc1, - self.lc2, - fullspec=fullspec, - method=method, - oversampling=oversampling, - ) - - self.power = self._normalize_crossspectrum(self.unnorm_power) - if lc1.err_dist.lower() != lc2.err_dist.lower(): - simon( - "Your lightcurves have different statistics." - "The errors in the Crossspectrum will be incorrect." - ) + raise TypeError("Input data are invalid") + return True - elif lc1.err_dist.lower() != "poisson": - simon( - "Looks like your lightcurve statistic is not poisson." - "The errors in the Crossspectrum will be incorrect." + def _initialize_from_any_input( + self, + data1, + data2, + dt, + norm, + power_type, + fullspec, + min_freq, + max_freq, + df, + method, + oversampling, + ): + """Not required for unevenly sampled data""" + if isinstance(data1, Lightcurve): + self.lc1 = data1 + self.lc2 = data2 + spec = lscrossspectrum_from_lightcurve( + data1, + data2, + norm, + power_type, + fullspec, + min_freq, + max_freq, + method, + oversampling, ) - - if self.__class__.__name__ == "LombScarglePowerspectrum": - self.power_err = self.unnorm_power_err = self.power / np.sqrt(self.m) - elif self.__class__.__name__ == "LombScargleCrossspectrum": - simon( - "Errorbars on cross spectra are not thoroughly tested." - "Please report any inconsistencies." + elif isinstance(data1, EventList): + self.lc1 = data1.to_lc(dt) + self.lc2 = data2.to_lc(dt) + spec = lscrossspectrum_from_events( + self.lc1, + self.lc2, + dt, + power_type, + norm, + fullspec, + min_freq, + max_freq, + method, + oversampling, ) - self.unnorm_power_err = np.sqrt(2) / np.sqrt(self.m) - self.unnorm_power_err /= np.divide(2, np.sqrt(np.abs(self.nphots1 * self.nphots2))) - self.unnorm_power_err += np.zeros_like(self.unnorm_power) - self.power_err = self._normalize_crossspectrum(self.unnorm_power_err) + else: + raise TypeError(f"Bad inputs to LombScargleCrossspectrum: {type(data1)}") + for key, val in spec.__dict__.items(): + setattr(self, key, val) def _make_auxil_pds(self, lc1, lc2): __doc__ = super()._make_auxil_pds.__doc__ - if lc1 is not lc2 and isinstance(lc1, Lightcurve): + is_event = isinstance(lc1, EventList) + is_lc = isinstance(lc1, Lightcurve) + if self.type != "powerspectrum" and (lc1 is not lc2) and (is_event or is_lc): self.pds1 = LombScargleCrossspectrum( lc1, lc1, @@ -379,78 +321,6 @@ def _make_auxil_pds(self, lc1, lc2): oversampling=self.oversampling, ) - def _ls_cross(self, lc1, lc2, freq=None, fullspec=False, method="fast", oversampling=5): - """ - Lomb-Scargle Fourier transform the two light curves, then compute the cross spectrum. - Computed as CS = lc1 x lc2* (where lc2 is the one that gets - complex-conjugated). The user has the option to either get just the - positive frequencies or the full spectrum. - - Parameters - ---------- - lc1: :class:`stingray.lightcurve.Lightcurve` object - One light curve to be Lomb-Scargle Fourier transformed. Ths is the band of - interest or channel of interest. - - lc2: :class:`stingray.lightcurve.Lightcurve` object - Another light curve to be Fourier transformed. - This is the reference band. - - fullspec: boolean. Default is False. - If True, return the whole array of frequencies, or only positive frequencies (False). - - method : str - The method to be used by the Lomb-Scargle Fourier Transformation function. `fast` - and `slow` are the allowed values. Default is `fast`. fast uses the optimized Press - and Rybicki O(n*log(n)) - - Returns - ------- - freq: numpy.ndarray - The frequency grid at which the LSFT was evaluated - - cross: numpy.ndarray - The cross spectrum value at each frequency. - - """ - if not freq: - freq = ( - LombScargle( - lc1.time, - lc1.counts, - fit_mean=False, - center_data=False, - normalization="psd", - ).autofrequency( - minimum_frequency=max(self.min_freq, 0), maximum_frequency=self.max_freq - ), - )[0] - freqs2 = ( - LombScargle( - lc2.time, - lc2.counts, - fit_mean=False, - center_data=False, - normalization="psd", - ).autofrequency( - minimum_frequency=max(self.min_freq, 0), maximum_frequency=self.max_freq - ), - )[0] - if max(freqs2) > max(freq): - freq = freqs2 - - if method == "slow": - lsft1 = lsft_slow(lc1.counts, lc1.time, freq) - lsft2 = lsft_slow(lc2.counts, lc2.time, freq) - elif method == "fast": - lsft1 = lsft_fast(lc1.counts, lc1.time, freq, oversampling=oversampling) - lsft2 = lsft_fast(lc2.counts, lc2.time, freq, oversampling=oversampling) - if fullspec: - lsft1, _ = impose_symmetry_lsft(lsft1, np.sum((lc1.counts)), lc1.n, freq) - lsft2, freq = impose_symmetry_lsft(lsft2, np.sum(lc2.counts), lc2.n, freq) - cross = np.multiply(lsft1, np.conjugate(lsft2)) - return freq, cross - def _initialize_empty(self): self.freq = None self.power = None @@ -508,10 +378,6 @@ def from_lc_iterable(self): "Object has no attribute named 'from_lc_iterable' ! Not applicable for unevenly sampled data" ) - def _initialize_from_any_input(self): - """Not required for unevenly sampled data""" - raise AttributeError("Object has no attribute named '_initialize_from_any_input' !") - class LombScarglePowerspectrum(LombScargleCrossspectrum): type = "powerspectrum" @@ -613,21 +479,241 @@ def __init__( self._type = None data1 = copy.deepcopy(data) data2 = copy.deepcopy(data) + if data is None: + return self._initialize_empty() + good_input = True + if not skip_checks: + good_input = self.initial_checks( + data, + data, + norm, + power_type, + dt, + min_freq, + max_freq, + fullspec, + df, + method, + oversampling, + ) + if not good_input: + return self._initialize_empty() - LombScargleCrossspectrum.__init__( - self, - data1=data1, - data2=data2, + self._initialize_from_any_input( + data1=data, + data2=data, + dt=dt, norm=norm, power_type=power_type, - dt=dt, - skip_checks=skip_checks, + fullspec=fullspec, min_freq=min_freq, max_freq=max_freq, df=df, method=method, oversampling=oversampling, ) - self.nphots = self.nphots1 self.dt = dt + + +def lscrossspectrum_from_lightcurve( + lc1, + lc2, + norm="none", + power_type="all", + fullspec=False, + min_freq=0, + max_freq=None, + method="fast", + oversampling=5, +): + """Creates a Lomb Scargle Cross Spectrum from two light curves + Parameters + ---------- + lc1: :class:`stingray.lightcurve.Lightcurve` object + Light curve from channel 1. + lc2 : :class:`stingray.lightcurve.Lightcurve` object + Light curve from channel 2. + + Other parameters + ---------------- + norm : str, default "none" + The normalization of the periodogram. "frac" is fractional rms,"abs" is absolute + rms, "leahy" is Leahy normalization, and "none" is the unnormalized periodogram + + power_type : str, default "all" + Parameter to choose among complete, real part and magnitude of the spectrum + + fullspec : bool, default False + If False, keep only the positive frequencies, or if True, keep all of them + + min_freq : float, default 0 + Minimum frequency to take the Lomb-Scargle Fourier Transform + + max_freq : float, default None + Maximum frequency to take the Lomb-Scargle Fourier Transform + + method : str, default "fast" + The method to be used by the Lomb-Scargle Fourier Transformation function. + `fast` and `slow` are the allowed values. Default is `fast`. fast uses the + optimized Press and Rybicki O(n*log(n)) algorithm, while slow uses the original + O(n^2) algorithm. + + oversampling : int, default 5 + Interpolation Oversampling Factor (for the fast algorithm) + """ + lscs = LombScargleCrossspectrum() + + freq, cross = _ls_cross( + lc1, + lc2, + freq=None, + fullspec=fullspec, + min_freq=min_freq, + max_freq=max_freq, + method=method, + oversampling=oversampling, + ) + lscs.unnorm_power = cross + lscs.freq = freq + lscs.lc1 = lc1 + lscs.lc2 = lc2 + lscs.norm = norm + lscs.power_type = power_type + lscs.fullspec = fullspec + lscs.min_freq = min_freq + lscs.max_freq = max_freq + lscs.oversampling = oversampling + lscs.nphots1 = lc1.n + lscs.nphots2 = lc2.n + lscs.dt = lc1.dt + lscs.n = lc1.n + lscs.method = method + lscs.err_dist = "poisson" + + if lc1.err_dist == "poisson": + lscs.variance1 = lc1.meancounts + else: + lscs.variance1 = np.mean(lc1.counts_err) ** 2 + lscs.err_dist = "gauss" + if lc2.err_dist == "poisson": + lscs.variance2 = lc2.meancounts + else: + lscs.variance2 = np.mean(lc2.counts_err) ** 2 + lscs.err_dist = "gauss" + + lscs.power = lscs._normalize_crossspectrum(lscs.unnorm_power) + + if power_type == "real": + lscs.power = np.real(lscs.power) + lscs.unnorm_power = np.real(lscs.power) + elif power_type == "absolute": + lscs.power = np.abs(lscs.power) + lscs.unnorm_power = np.abs(lscs.power) + return lscs + + +def lscrossspectrum_from_events( + event1, + event2, + dt=None, + norm="none", + power_type="all", + fullspec=False, + min_freq=0, + max_freq=None, + method="fast", + oversampling=5, +): + """Creates a Lomb Scargle Cross Spectrum from two event lists""" + lc1 = event1.to_lc(dt) + lc2 = event2.to_lc(dt) + return lscrossspectrum_from_lightcurve( + lc1, + lc2, + norm, + power_type, + fullspec, + min_freq, + max_freq, + method, + oversampling, + ) + + +def _ls_cross( + lc1, + lc2, + freq=None, + fullspec=False, + min_freq=0, + max_freq=None, + method="fast", + oversampling=5, +): + """ + Lomb-Scargle Fourier transform the two light curves, then compute the cross spectrum. + Computed as CS = lc1 x lc2* (where lc2 is the one that gets + complex-conjugated). The user has the option to either get just the + positive frequencies or the full spectrum. + + Parameters + ---------- + lc1: :class:`stingray.lightcurve.Lightcurve` object + One light curve to be Lomb-Scargle Fourier transformed. Ths is the band of + interest or channel of interest. + + lc2: :class:`stingray.lightcurve.Lightcurve` object + Another light curve to be Fourier transformed. + This is the reference band. + + fullspec: boolean. Default is False. + If True, return the whole array of frequencies, or only positive frequencies (False). + + method : str + The method to be used by the Lomb-Scargle Fourier Transformation function. `fast` + and `slow` are the allowed values. Default is `fast`. fast uses the optimized Press + and Rybicki O(n*log(n)) + + Returns + ------- + freq: numpy.ndarray + The frequency grid at which the LSFT was evaluated + + cross: numpy.ndarray + The cross spectrum value at each frequency. + + """ + if not freq: + freq = ( + LombScargle( + lc1.time, + lc1.counts, + fit_mean=False, + center_data=False, + normalization="psd", + ).autofrequency(minimum_frequency=max(min_freq, 0), maximum_frequency=max_freq), + )[0] + freqs2 = ( + LombScargle( + lc2.time, + lc2.counts, + fit_mean=False, + center_data=False, + normalization="psd", + ).autofrequency(minimum_frequency=max(min_freq, 0), maximum_frequency=max_freq), + )[0] + if max(freqs2) > max(freq): + freq = freqs2 + + if method == "slow": + lsft1 = lsft_slow(lc1.counts, lc1.time, freq) + lsft2 = lsft_slow(lc2.counts, lc2.time, freq) + elif method == "fast": + lsft1 = lsft_fast(lc1.counts, lc1.time, freq, oversampling=oversampling) + lsft2 = lsft_fast(lc2.counts, lc2.time, freq, oversampling=oversampling) + if fullspec: + lsft1, _ = impose_symmetry_lsft(lsft1, np.sum((lc1.counts)), lc1.n, freq) + lsft2, freq = impose_symmetry_lsft(lsft2, np.sum(lc2.counts), lc2.n, freq) + cross = np.multiply(lsft1, np.conjugate(lsft2)) + return freq, cross diff --git a/stingray/tests/test_lombscargle.py b/stingray/tests/test_lombscargle.py index b6094a00d..ed20571da 100644 --- a/stingray/tests/test_lombscargle.py +++ b/stingray/tests/test_lombscargle.py @@ -26,8 +26,7 @@ def setup_class(self): s2_new = interp1d(t, s2, fill_value="extrapolate")(t_new) self.lc1 = Lightcurve(t, s1_new, dt=lc1.dt) self.lc2 = Lightcurve(t, s2_new, dt=lc2.dt) - with pytest.warns(UserWarning) as record: - self.lscs = LombScargleCrossspectrum(lc1, lc2) + self.lscs = LombScargleCrossspectrum(lc1, lc2) @pytest.mark.parametrize("skip_checks", [True, False]) def test_initialize_empty(self, skip_checks): @@ -58,12 +57,6 @@ def test_bad_input(self): with pytest.raises(TypeError): lscs = LombScargleCrossspectrum(1, self.lc1) - def test_init_with_one_lc_none(self): - with pytest.raises(ValueError): - lscs = LombScargleCrossspectrum(self.lc1, None) - with pytest.raises(ValueError): - lscs = LombScargleCrossspectrum(None, self.lc1) - def test_init_with_norm_not_str(self): with pytest.raises(TypeError): lscs = LombScargleCrossspectrum(self.lc1, self.lc2, norm=1) @@ -106,8 +99,9 @@ def test_init_with_invalid_max_freq(self): def test_make_crossspectrum_diff_lc_counts_shape(self): lc_ = Simulator(0.0001, 103, 100, 1, random_state=42, tstart=0).simulate(0) - with pytest.raises(ValueError): + with pytest.warns(UserWarning) as record: lscs = LombScargleCrossspectrum(self.lc1, lc_) + assert np.any(["different statistics" in r.message.args[0] for r in record]) def test_make_crossspectrum_diff_lc_stat(self): lc_ = copy.deepcopy(self.lc1) @@ -136,18 +130,11 @@ def test_with_invalid_oversampling(self): def test_invalid_mixed_data(self): data2 = EventList(self.lc2.time[3:], np.ones_like(self.lc2.time[3:])) - with pytest.raises(ValueError): + with pytest.raises(TypeError): lscs = LombScargleCrossspectrum(self.lc1, data2) - with pytest.raises(ValueError): + with pytest.raises(TypeError): lscs = LombScargleCrossspectrum(data2, self.lc1) - def test_valid_mixed_data(self): - data2 = EventList(self.lc2.time, np.ones_like(self.lc2.time)) - lscs = LombScargleCrossspectrum(self.lc1, data2) - assert lscs.power is not None - lscs2 = LombScargleCrossspectrum(data2, self.lc1) - assert lscs2.power is not None - def test_fullspec(self): lscs = LombScargleCrossspectrum(self.lc1, self.lc2, fullspec=True) assert lscs.fullspec @@ -165,7 +152,6 @@ def test_valid_method(self, method): "from_events", "from_lightcurve", "from_lc_iterable", - "_initialize_from_any_input", ], ) def test_raise_on_invalid_function(self, func_name): @@ -221,4 +207,4 @@ def test_make_empty_powerspectrum(self): def test_ps_real(self): ps = LombScarglePowerspectrum(self.lc) - assert np.allclose(ps.power.imag, [0]) + assert np.allclose(ps.power.imag, [0], atol=1e-4) From 0c37abc8aff722a40e5ac68c3848f356b0d19bf3 Mon Sep 17 00:00:00 2001 From: pupper emeritus <80730927+pupperemeritus@users.noreply.github.com> Date: Mon, 18 Sep 2023 16:36:48 +0530 Subject: [PATCH 18/31] Example eventlist test Co-authored-by: Matteo Bachetti --- stingray/tests/test_lombscargle.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/stingray/tests/test_lombscargle.py b/stingray/tests/test_lombscargle.py index ed20571da..2747b19a0 100644 --- a/stingray/tests/test_lombscargle.py +++ b/stingray/tests/test_lombscargle.py @@ -28,6 +28,11 @@ def setup_class(self): self.lc2 = Lightcurve(t, s2_new, dt=lc2.dt) self.lscs = LombScargleCrossspectrum(lc1, lc2) +def test_eventlist(self): + ev1 = EventList.from_lc(self.lc1) + ev2 = EventList.from_lc(self.lc2) + ev_lscs = LombScargleCrossspectrum(ev1, ev2, dt=self.lc1.dt) + assert np.allclose(ev_lscs.power, self.lscs.power) @pytest.mark.parametrize("skip_checks", [True, False]) def test_initialize_empty(self, skip_checks): lscs = LombScargleCrossspectrum(skip_checks=skip_checks) From 68fcf8127dc1d5d836ea93a670173143abd1da8a Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Wed, 20 Sep 2023 22:44:53 +0530 Subject: [PATCH 19/31] Code Cleanup, Tests and lc warning changes --- stingray/fourier.py | 1 - stingray/lightcurve.py | 1 + stingray/lombscargle.py | 100 ++--------------------------- stingray/tests/test_fourier.py | 3 +- stingray/tests/test_lombscargle.py | 62 ++++++++++++++---- 5 files changed, 61 insertions(+), 106 deletions(-) diff --git a/stingray/fourier.py b/stingray/fourier.py index cf6101ee2..fc4842162 100644 --- a/stingray/fourier.py +++ b/stingray/fourier.py @@ -2160,7 +2160,6 @@ def lsft_slow( # A and B before the square root and inversion in (II.3) -- scos2 = np.sum(np.power(np.cos(wrun * t - wtau), 2)) ssin2 = np.sum(np.power(np.sin(wrun * t - wtau), 2)) - ## -- # const2 is const1 times the sign. # It's the F0 in II.2 without the phase factor diff --git a/stingray/lightcurve.py b/stingray/lightcurve.py index 9617e3e0a..9b76965f2 100644 --- a/stingray/lightcurve.py +++ b/stingray/lightcurve.py @@ -591,6 +591,7 @@ def check_lightcurve(self): "Bin sizes in input time array aren't equal throughout! " "This could cause problems with Fourier transforms. " "Please make the input time evenly sampled." + "Only use with LombScargleCrossspectrum, LombScarglePowerspectrum and QPO using GPResult" ) def _operation_with_other_lc(self, other, operation): diff --git a/stingray/lombscargle.py b/stingray/lombscargle.py index 4db1c958b..cb4134809 100644 --- a/stingray/lombscargle.py +++ b/stingray/lombscargle.py @@ -1,5 +1,6 @@ import copy from typing import Optional, Union + import numpy as np import numpy.typing as npt from astropy.timeseries.periodograms import LombScargle @@ -7,7 +8,7 @@ from .crossspectrum import Crossspectrum from .events import EventList from .exceptions import StingrayError -from .fourier import lsft_fast, lsft_slow, impose_symmetry_lsft +from .fourier import impose_symmetry_lsft, lsft_fast, lsft_slow from .lightcurve import Lightcurve from .utils import simon @@ -130,14 +131,11 @@ def __init__( self._initialize_empty() return - good_input = True if dt is None: if isinstance(data1, Lightcurve) or isinstance(data2, EventList): dt = data1.dt - elif isinstance(data2, Lightcurve) or isinstance(data2, EventList): + elif isinstance(data2, Lightcurve) or isinstance(data2, EventList) and dt is None: dt = data2.dt - if dt is None: - raise ValueError("dt must be provided for EventLists") if not skip_checks: good_input = self.initial_checks( @@ -154,10 +152,6 @@ def __init__( oversampling=oversampling, ) - if not good_input: - self._initialize_empty() - return - if data1 is not None and data2 is not None: self._initialize_from_any_input( data1, @@ -199,11 +193,9 @@ def initial_checks( if power_type not in ["all", "absolute", "real"]: raise ValueError("power_type must be one of ['all','absolute','real']") - if data1 is None and data2 is None: - if data1 is not None and data2 is not None: + if data1 is None or data2 is None: + if data1 is not None or data2 is not None: raise ValueError("You can't do a cross spectrum with just one lightcurve") - else: - return False if min_freq < 0: raise ValueError("min_freq must be non-negative") @@ -272,55 +264,20 @@ def _initialize_from_any_input( elif isinstance(data1, EventList): self.lc1 = data1.to_lc(dt) self.lc2 = data2.to_lc(dt) - spec = lscrossspectrum_from_events( + spec = lscrossspectrum_from_lightcurve( self.lc1, self.lc2, - dt, - power_type, norm, + power_type, fullspec, min_freq, max_freq, method, oversampling, ) - else: - raise TypeError(f"Bad inputs to LombScargleCrossspectrum: {type(data1)}") for key, val in spec.__dict__.items(): setattr(self, key, val) - def _make_auxil_pds(self, lc1, lc2): - __doc__ = super()._make_auxil_pds.__doc__ - is_event = isinstance(lc1, EventList) - is_lc = isinstance(lc1, Lightcurve) - if self.type != "powerspectrum" and (lc1 is not lc2) and (is_event or is_lc): - self.pds1 = LombScargleCrossspectrum( - lc1, - lc1, - power_type=self.power_type, - norm=self.norm, - dt=self.dt, - fullspec=self.fullspec, - min_freq=self.min_freq, - max_freq=self.max_freq, - df=self.df, - method=self.method, - oversampling=self.oversampling, - ) - self.pds2 = LombScargleCrossspectrum( - lc2, - lc2, - power_type=self.power_type, - norm=self.norm, - dt=self.dt, - fullspec=self.fullspec, - min_freq=self.min_freq, - max_freq=self.max_freq, - df=self.df, - method=self.method, - oversampling=self.oversampling, - ) - def _initialize_empty(self): self.freq = None self.power = None @@ -477,8 +434,6 @@ def __init__( oversampling: Optional[int] = 5, ): self._type = None - data1 = copy.deepcopy(data) - data2 = copy.deepcopy(data) if data is None: return self._initialize_empty() good_input = True @@ -496,8 +451,6 @@ def __init__( method, oversampling, ) - if not good_input: - return self._initialize_empty() self._initialize_from_any_input( data1=data, @@ -613,34 +566,6 @@ def lscrossspectrum_from_lightcurve( return lscs -def lscrossspectrum_from_events( - event1, - event2, - dt=None, - norm="none", - power_type="all", - fullspec=False, - min_freq=0, - max_freq=None, - method="fast", - oversampling=5, -): - """Creates a Lomb Scargle Cross Spectrum from two event lists""" - lc1 = event1.to_lc(dt) - lc2 = event2.to_lc(dt) - return lscrossspectrum_from_lightcurve( - lc1, - lc2, - norm, - power_type, - fullspec, - min_freq, - max_freq, - method, - oversampling, - ) - - def _ls_cross( lc1, lc2, @@ -694,17 +619,6 @@ def _ls_cross( normalization="psd", ).autofrequency(minimum_frequency=max(min_freq, 0), maximum_frequency=max_freq), )[0] - freqs2 = ( - LombScargle( - lc2.time, - lc2.counts, - fit_mean=False, - center_data=False, - normalization="psd", - ).autofrequency(minimum_frequency=max(min_freq, 0), maximum_frequency=max_freq), - )[0] - if max(freqs2) > max(freq): - freq = freqs2 if method == "slow": lsft1 = lsft_slow(lc1.counts, lc1.time, freq) diff --git a/stingray/tests/test_fourier.py b/stingray/tests/test_fourier.py index 2f4c1be3b..bb3deea90 100644 --- a/stingray/tests/test_fourier.py +++ b/stingray/tests/test_fourier.py @@ -1,6 +1,8 @@ import os from pickle import FALSE + import pytest + from stingray.fourier import * from stingray.utils import check_allclose_and_print @@ -574,7 +576,6 @@ def func(time, phase=0): while measured_lag <= -0.5: measured_lag += 0.5 - print(measured_lag) assert np.isclose((np.angle(ft1) - np.angle(ft0)) / 2 / np.pi, phlag, atol=0.02, rtol=0.02) diff --git a/stingray/tests/test_lombscargle.py b/stingray/tests/test_lombscargle.py index 2747b19a0..68ce37ad6 100644 --- a/stingray/tests/test_lombscargle.py +++ b/stingray/tests/test_lombscargle.py @@ -1,17 +1,19 @@ +import copy + import numpy as np import pytest -import copy -from stingray.lombscargle import LombScargleCrossspectrum, LombScarglePowerspectrum -from stingray.lightcurve import Lightcurve +from scipy.interpolate import interp1d + from stingray.events import EventList -from stingray.simulator import Simulator from stingray.exceptions import StingrayError -from scipy.interpolate import interp1d +from stingray.lightcurve import Lightcurve +from stingray.lombscargle import LombScargleCrossspectrum, LombScarglePowerspectrum +from stingray.simulator import Simulator class TestLombScargleCrossspectrum: def setup_class(self): - sim = Simulator(0.0001, 100, 100, 1, random_state=42, tstart=0) + sim = Simulator(0.0001, 50, 100, 1, random_state=42, tstart=0) lc1 = sim.simulate(0) lc2 = sim.simulate(0) self.rate1 = lc1.countrate @@ -20,6 +22,7 @@ def setup_class(self): s1 = lc1.counts s2 = lc2.counts t = lc1.time + self.time = lc1.time t_new = t.copy() t_new[1:-1] = t[1:-1] + (np.random.rand(len(t) - 2) / (high - low)) s1_new = interp1d(t, s1, fill_value="extrapolate")(t_new) @@ -28,11 +31,21 @@ def setup_class(self): self.lc2 = Lightcurve(t, s2_new, dt=lc2.dt) self.lscs = LombScargleCrossspectrum(lc1, lc2) -def test_eventlist(self): - ev1 = EventList.from_lc(self.lc1) - ev2 = EventList.from_lc(self.lc2) - ev_lscs = LombScargleCrossspectrum(ev1, ev2, dt=self.lc1.dt) - assert np.allclose(ev_lscs.power, self.lscs.power) + def test_eventlist(self): + counts = np.random.poisson(10, 1000) + times = np.arange(0, 1000, 1) + lc1 = Lightcurve(times, counts, dt=1) + lc2 = Lightcurve(times, counts, dt=1) + ev1 = EventList.from_lc(lc1) + ev2 = EventList.from_lc(lc2) + ev_lscs = LombScargleCrossspectrum(ev1, ev2, dt=1) + lc_lscs = LombScargleCrossspectrum(lc1, lc2, dt=1) + + assert np.argmax(lc_lscs) == np.argmax(ev_lscs) + assert np.all(ev_lscs.freq == lc_lscs.freq) + assert np.all(ev_lscs.power == lc_lscs.power) + assert ev_lscs.freq[np.argmax(ev_lscs.power)] == lc_lscs.freq[np.argmax(lc_lscs.power)] != 0 + @pytest.mark.parametrize("skip_checks", [True, False]) def test_initialize_empty(self, skip_checks): lscs = LombScargleCrossspectrum(skip_checks=skip_checks) @@ -61,6 +74,12 @@ def test_make_empty_crossspectrum(self): def test_bad_input(self): with pytest.raises(TypeError): lscs = LombScargleCrossspectrum(1, self.lc1) + with pytest.raises(TypeError): + lscs = LombScargleCrossspectrum("smooth", "criminal") + + def test_one_lightcurve(self): + with pytest.raises(ValueError): + lscs = LombScargleCrossspectrum(self.lc1, None) def test_init_with_norm_not_str(self): with pytest.raises(TypeError): @@ -171,6 +190,27 @@ def test_no_dt(self): with pytest.raises(ValueError): lscs = LombScargleCrossspectrum(el1, el2) + @pytest.mark.parametrize("phase_lag", [0.05, 0.1, 0.2, 0.4]) + def test_time_phase_lag(self, phase_lag): + freq = 1.112323232252 + + def func(time, phase=0): + return 2 + np.sin(2 * np.pi * (time * freq - phase)) + + time = np.sort(np.random.uniform(0, 100, 3000)) + + with pytest.warns(UserWarning): + lc1 = Lightcurve(time, func(time, 0)) + lc2 = Lightcurve(time, func(time, phase_lag)) + + lscs = LombScargleCrossspectrum(lc1, lc2) + measured_time_lag = lscs.time_lag() * 2 * np.pi * lscs.freq[lscs.freq >= 0] + measured_phase_lag = lscs.phase_lag() + measured_time_lag[np.isnan(measured_time_lag)] = measured_phase_lag[ + np.isnan(measured_time_lag) + ] + assert np.allclose(measured_phase_lag, measured_time_lag, atol=1e-1) + class TestLombScarglePowerspectrum: def setup_class(self): From d89f0f5fa42f9be92439510c2d1f8e0662e929db Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Thu, 21 Sep 2023 21:44:22 +0530 Subject: [PATCH 20/31] method test fix --- stingray/lombscargle.py | 12 ++++++------ stingray/tests/test_lombscargle.py | 14 ++++++++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/stingray/lombscargle.py b/stingray/lombscargle.py index cb4134809..34a6e688f 100644 --- a/stingray/lombscargle.py +++ b/stingray/lombscargle.py @@ -472,12 +472,12 @@ def __init__( def lscrossspectrum_from_lightcurve( lc1, lc2, - norm="none", - power_type="all", - fullspec=False, - min_freq=0, - max_freq=None, - method="fast", + norm, + power_type, + fullspec, + min_freq, + max_freq, + method, oversampling=5, ): """Creates a Lomb Scargle Cross Spectrum from two light curves diff --git a/stingray/tests/test_lombscargle.py b/stingray/tests/test_lombscargle.py index 68ce37ad6..e5a626b60 100644 --- a/stingray/tests/test_lombscargle.py +++ b/stingray/tests/test_lombscargle.py @@ -163,10 +163,16 @@ def test_fullspec(self): lscs = LombScargleCrossspectrum(self.lc1, self.lc2, fullspec=True) assert lscs.fullspec - @pytest.mark.parametrize("method", ["slow", "fast"]) - def test_valid_method(self, method): - lscs = LombScargleCrossspectrum(self.lc1, self.lc2, method=method) - assert lscs.method == method + def test_valid_method(self): + lscs_s = LombScargleCrossspectrum(self.lc1, self.lc2, method="slow") + assert lscs_s.method == "slow" + lscs_f = LombScargleCrossspectrum(self.lc1, self.lc2, method="fast", oversampling=5) + assert lscs_f.method == "fast" + assert ( + np.sum(np.isclose(lscs_f.unnorm_power, lscs_s.unnorm_power, rtol=0.1, atol=1)) + / lscs_f.power.shape[0] + > 0.9 + ) @pytest.mark.parametrize( "func_name", From ccf0af0d020a5e8e33d10338e376906d92e535cc Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Fri, 22 Sep 2023 19:45:53 +0530 Subject: [PATCH 21/31] Reintroduced ls notebooks --- docs/notebooks | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/notebooks b/docs/notebooks index fc7804914..698292742 160000 --- a/docs/notebooks +++ b/docs/notebooks @@ -1 +1 @@ -Subproject commit fc780491476f7981a1331feee6837ad047b44999 +Subproject commit 69829274224a938245030275e12698914920a9a0 From 53521ff3f7ba9cdcf2dcf0992cc3b23182fc180d Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Fri, 22 Sep 2023 19:50:31 +0530 Subject: [PATCH 22/31] Removed largememory --- stingray/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/stingray/__init__.py b/stingray/__init__.py index 61288f2de..536fa7425 100644 --- a/stingray/__init__.py +++ b/stingray/__init__.py @@ -22,5 +22,4 @@ from stingray.stats import * from stingray.bispectrum import * from stingray.varenergyspectrum import * - from stingray.largememory import * from stingray.lombscargle import * From c55c0d620989a17e73459d70fa6514e0d4ac4605 Mon Sep 17 00:00:00 2001 From: pupperemeritus Date: Fri, 22 Sep 2023 20:36:54 +0530 Subject: [PATCH 23/31] fix for python 3.11 tests --- stingray/tests/test_lombscargle.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/stingray/tests/test_lombscargle.py b/stingray/tests/test_lombscargle.py index e5a626b60..f8c820565 100644 --- a/stingray/tests/test_lombscargle.py +++ b/stingray/tests/test_lombscargle.py @@ -257,5 +257,8 @@ def test_make_empty_powerspectrum(self): assert ps.method is None def test_ps_real(self): - ps = LombScarglePowerspectrum(self.lc) - assert np.allclose(ps.power.imag, [0], atol=1e-4) + counts = np.random.poisson(10, 1000) + times = np.arange(0, 1000, 1) + lc = Lightcurve(times, counts, dt=1) + ps = LombScarglePowerspectrum(lc) + assert np.allclose(ps.power.imag, np.zeros_like(ps.power.imag), atol=1e-4) From 8d231445e79966e887fa1ad1fa589b2ae3a83be0 Mon Sep 17 00:00:00 2001 From: Matteo Bachetti Date: Tue, 26 Sep 2023 20:18:44 +0200 Subject: [PATCH 24/31] Fix the nphots parameter --- stingray/lombscargle.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stingray/lombscargle.py b/stingray/lombscargle.py index 34a6e688f..0b97112c0 100644 --- a/stingray/lombscargle.py +++ b/stingray/lombscargle.py @@ -63,7 +63,7 @@ class LombScargleCrossspectrum(Crossspectrum): The method to be used by the Lomb-Scargle Fourier Transformation function. `fast` and `slow` are the allowed values. Default is `fast`. fast uses the optimized Press and Rybicki O(n*log(n)) - + oversampling : float, optional, default: 5 Interpolation Oversampling Factor (for the fast algorithm) @@ -371,7 +371,7 @@ class LombScarglePowerspectrum(LombScargleCrossspectrum): skip_checks: bool Skip initial checks, for speed or other reasons (you need to trust your inputs!). - + min_freq : float Minimum frequency to take the Lomb-Scargle Fourier Transform @@ -537,8 +537,8 @@ def lscrossspectrum_from_lightcurve( lscs.min_freq = min_freq lscs.max_freq = max_freq lscs.oversampling = oversampling - lscs.nphots1 = lc1.n - lscs.nphots2 = lc2.n + lscs.nphots1 = lc1.counts.sum() + lscs.nphots2 = lc2.counts.sum() lscs.dt = lc1.dt lscs.n = lc1.n lscs.method = method From 1957239b298604e54dc4cd1313d67d0929829c91 Mon Sep 17 00:00:00 2001 From: Matteo Bachetti Date: Tue, 26 Sep 2023 20:29:18 +0200 Subject: [PATCH 25/31] Import all from lombscargle --- docs/notebooks | 2 +- stingray/__init__.py | 1 + stingray/lombscargle.py | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/notebooks b/docs/notebooks index 698292742..fc7804914 160000 --- a/docs/notebooks +++ b/docs/notebooks @@ -1 +1 @@ -Subproject commit 69829274224a938245030275e12698914920a9a0 +Subproject commit fc780491476f7981a1331feee6837ad047b44999 diff --git a/stingray/__init__.py b/stingray/__init__.py index 536fa7425..4131ebd45 100644 --- a/stingray/__init__.py +++ b/stingray/__init__.py @@ -12,6 +12,7 @@ from stingray.events import * from stingray.lightcurve import * from stingray.utils import * + from stingray.lombscargle import * from stingray.powerspectrum import * from stingray.crossspectrum import * from stingray.multitaper import * diff --git a/stingray/lombscargle.py b/stingray/lombscargle.py index 0b97112c0..1d6ad747c 100644 --- a/stingray/lombscargle.py +++ b/stingray/lombscargle.py @@ -13,6 +13,9 @@ from .utils import simon +__all__ = ["LombScarglePowerspectrum", "LombScargleCrossspectrum"] + + class LombScargleCrossspectrum(Crossspectrum): main_array_attr = "freq" type = "crossspectrum" From da9e4b681475da36bfbd9067c28b062f468f9651 Mon Sep 17 00:00:00 2001 From: Matteo Bachetti Date: Wed, 27 Sep 2023 12:24:53 +0200 Subject: [PATCH 26/31] Make the autofrequency calculation more consistent and controllable --- stingray/lombscargle.py | 166 +++++++++++++++++++---------- stingray/tests/test_lombscargle.py | 18 ++++ 2 files changed, 128 insertions(+), 56 deletions(-) diff --git a/stingray/lombscargle.py b/stingray/lombscargle.py index 1d6ad747c..bcb5d1e85 100644 --- a/stingray/lombscargle.py +++ b/stingray/lombscargle.py @@ -1,4 +1,5 @@ import copy +import warnings from typing import Optional, Union import numpy as np @@ -16,6 +17,63 @@ __all__ = ["LombScarglePowerspectrum", "LombScargleCrossspectrum"] +def _autofrequency(min_freq=None, max_freq=None, df=None, dt=None, length=None, nyquist_factor=1): + """Decide the frequency grid for the periodogram if not provided explicitly. + + Parameters + ---------- + min_freq : float + Minimum frequency to take the Lomb-Scargle Fourier Transform + max_freq : float + Maximum frequency to take the Lomb-Scargle Fourier Transform + df : float + The frequency resolution of the final periodogram. Defaults to 1 / length. + dt : float + The time resolution of the light curve. + length : float + The total length of the light curve. + + Returns + ------- + freq : numpy.ndarray + The array of mid-bin frequencies that the Fourier transform samples + + Examples + -------- + >>> freqs = _autofrequency(min_freq=0.1, max_freq=0.5, df=0.1) + >>> np.allclose(freqs, [0.1, 0.2, 0.3, 0.4, 0.5]) + True + >>> freqs = _autofrequency(min_freq=0.1, max_freq=0.5, length=10) + >>> np.allclose(freqs, [0.1, 0.2, 0.3, 0.4, 0.5]) + True + >>> freqs = _autofrequency(min_freq=0.1, dt=1, length=10) + >>> np.allclose(freqs, [0.1, 0.2, 0.3, 0.4, 0.5]) + True + >>> freqs = _autofrequency(max_freq=0.5, df=0.2) + >>> np.allclose(freqs, [0.1, 0.3, 0.5]) + True + """ + + if (df is None or df <= 0) and length is None: + raise ValueError("Either df or length must be specified.") + elif df is None or df <= 0: + df = 1 / length + + if max_freq is None and (dt is None or dt == 0): + raise ValueError("Either max_freq or dt must be specified.") + elif max_freq is None: + max_freq = nyquist_factor * 0.5 / dt + + if min_freq is None: + min_freq = df / 2 + elif min_freq <= 0: + warnings.warn("min_freq must be positive and >0. Setting to df / 2.") + min_freq = df / 2 + + freq = np.arange(min_freq, max_freq + df, df) + return freq + + class LombScargleCrossspectrum(Crossspectrum): main_array_attr = "freq" type = "crossspectrum" @@ -60,7 +118,7 @@ class LombScargleCrossspectrum(Crossspectrum): Maximum frequency to take the Lomb-Scargle Fourier Transform df : float - The time resolution of the light curve. Only needed where ``data1``, ``data2`` are + The frequency resolution of the final periodogram. method : str The method to be used by the Lomb-Scargle Fourier Transformation function. `fast` @@ -122,7 +180,7 @@ def __init__( dt: Optional[float] = None, fullspec: Optional[bool] = False, skip_checks: bool = False, - min_freq: float = 0, + min_freq: float = None, max_freq: float = None, df: float = None, method: str = "fast", @@ -200,13 +258,16 @@ def initial_checks( if data1 is not None or data2 is not None: raise ValueError("You can't do a cross spectrum with just one lightcurve") - if min_freq < 0: + if min_freq is not None and min_freq < 0: raise ValueError("min_freq must be non-negative") - if max_freq is not None: - if max_freq < min_freq or max_freq < 0: + if max_freq is not None and min_freq is not None: + if max_freq < min_freq: raise ValueError("max_freq must be non-negative and greater than min_freq") + if max_freq is not None and max_freq < 0: + raise ValueError("max_freq must be non-negative and greater than min_freq") + if method not in ["fast", "slow"]: raise ValueError("method must be one of ['fast','slow']") @@ -250,34 +311,27 @@ def _initialize_from_any_input( oversampling, ): """Not required for unevenly sampled data""" - if isinstance(data1, Lightcurve): - self.lc1 = data1 - self.lc2 = data2 - spec = lscrossspectrum_from_lightcurve( - data1, - data2, - norm, - power_type, - fullspec, - min_freq, - max_freq, - method, - oversampling, - ) - elif isinstance(data1, EventList): - self.lc1 = data1.to_lc(dt) - self.lc2 = data2.to_lc(dt) - spec = lscrossspectrum_from_lightcurve( - self.lc1, - self.lc2, - norm, - power_type, - fullspec, - min_freq, - max_freq, - method, - oversampling, - ) + if isinstance(data1, EventList): + data1 = data1.to_lc(dt) + if isinstance(data2, EventList): + data2 = data2.to_lc(dt) + + self.lc1 = data1.apply_gtis(inplace=False) + self.lc2 = data2.apply_gtis(inplace=False) + + spec = lscrossspectrum_from_lightcurve( + self.lc1, + self.lc2, + norm=norm, + power_type=power_type, + fullspec=fullspec, + min_freq=min_freq, + max_freq=max_freq, + df=df, + method=method, + oversampling=oversampling, + ) + for key, val in spec.__dict__.items(): setattr(self, key, val) @@ -430,7 +484,7 @@ def __init__( dt: Optional[float] = None, fullspec: Optional[bool] = False, skip_checks: Optional[bool] = False, - min_freq: Optional[float] = 0, + min_freq: Optional[float] = None, max_freq: Optional[float] = None, df: Optional[float] = None, method: Optional[str] = "fast", @@ -475,13 +529,15 @@ def __init__( def lscrossspectrum_from_lightcurve( lc1, lc2, - norm, - power_type, - fullspec, - min_freq, - max_freq, - method, + norm="frac", + power_type="all", + fullspec=False, + min_freq=None, + max_freq=None, + df=None, + method="fast", oversampling=5, + nyquist_factor=1, ): """Creates a Lomb Scargle Cross Spectrum from two light curves Parameters @@ -517,16 +573,27 @@ def lscrossspectrum_from_lightcurve( oversampling : int, default 5 Interpolation Oversampling Factor (for the fast algorithm) + nyquist_factor : int, default 1 + How many times the Nyquist frequency to use as the maximum frequency """ lscs = LombScargleCrossspectrum() + length = max(lc1.time[-1], lc2.time[-1]) - min(lc1.time[0], lc2.time[0]) + dt = np.min([lc1.dt, lc2.dt]) + + freq = _autofrequency( + min_freq=min_freq, + max_freq=max_freq, + df=df, + dt=dt, + length=length, + nyquist_factor=nyquist_factor, + ) freq, cross = _ls_cross( lc1, lc2, - freq=None, + freq=freq, fullspec=fullspec, - min_freq=min_freq, - max_freq=max_freq, method=method, oversampling=oversampling, ) @@ -574,8 +641,6 @@ def _ls_cross( lc2, freq=None, fullspec=False, - min_freq=0, - max_freq=None, method="fast", oversampling=5, ): @@ -612,17 +677,6 @@ def _ls_cross( The cross spectrum value at each frequency. """ - if not freq: - freq = ( - LombScargle( - lc1.time, - lc1.counts, - fit_mean=False, - center_data=False, - normalization="psd", - ).autofrequency(minimum_frequency=max(min_freq, 0), maximum_frequency=max_freq), - )[0] - if method == "slow": lsft1 = lsft_slow(lc1.counts, lc1.time, freq) lsft2 = lsft_slow(lc2.counts, lc2.time, freq) diff --git a/stingray/tests/test_lombscargle.py b/stingray/tests/test_lombscargle.py index f8c820565..44c09f424 100644 --- a/stingray/tests/test_lombscargle.py +++ b/stingray/tests/test_lombscargle.py @@ -8,9 +8,27 @@ from stingray.exceptions import StingrayError from stingray.lightcurve import Lightcurve from stingray.lombscargle import LombScargleCrossspectrum, LombScarglePowerspectrum +from stingray.lombscargle import _autofrequency from stingray.simulator import Simulator +def test_autofrequency(): + freqs = _autofrequency(min_freq=0.1, max_freq=0.5, df=0.1) + assert np.allclose(freqs, [0.1, 0.2, 0.3, 0.4, 0.5]) + freqs = _autofrequency(min_freq=0.1, max_freq=0.5, length=10) + assert np.allclose(freqs, [0.1, 0.2, 0.3, 0.4, 0.5]) + freqs = _autofrequency(max_freq=0.5, df=0.2) + assert np.allclose(freqs, [0.1, 0.3, 0.5]) + freqs = _autofrequency(min_freq=0.1, dt=1, length=10) + assert np.allclose(freqs, [0.1, 0.2, 0.3, 0.4, 0.5]) + with pytest.raises(ValueError, match="Either df or length must be specified."): + _autofrequency(min_freq=0.01, max_freq=0.5) + with pytest.raises(ValueError, match="Either max_freq or dt must be"): + _autofrequency(min_freq=0.01, df=1) + with pytest.warns(UserWarning, match="min_freq must be positive and >0."): + freqs = _autofrequency(min_freq=-0.1, max_freq=0.5, df=0.1) + + class TestLombScargleCrossspectrum: def setup_class(self): sim = Simulator(0.0001, 50, 100, 1, random_state=42, tstart=0) From 436db70406aea49cd3bb7bdcb4640c2f0a1f545e Mon Sep 17 00:00:00 2001 From: Matteo Bachetti Date: Wed, 27 Sep 2023 12:45:34 +0200 Subject: [PATCH 27/31] Test maxfreq <0 --- stingray/lombscargle.py | 10 +++++----- stingray/tests/test_lombscargle.py | 4 ++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/stingray/lombscargle.py b/stingray/lombscargle.py index bcb5d1e85..1018eed69 100644 --- a/stingray/lombscargle.py +++ b/stingray/lombscargle.py @@ -261,12 +261,12 @@ def initial_checks( if min_freq is not None and min_freq < 0: raise ValueError("min_freq must be non-negative") - if max_freq is not None and min_freq is not None: - if max_freq < min_freq: - raise ValueError("max_freq must be non-negative and greater than min_freq") - if max_freq is not None and max_freq < 0: - raise ValueError("max_freq must be non-negative and greater than min_freq") + raise ValueError("max_freq must be non-negative") + + if max_freq is not None and min_freq is not None: + if max_freq <= min_freq: + raise ValueError("max_freq must be greater than min_freq") if method not in ["fast", "slow"]: raise ValueError("method must be one of ['fast','slow']") diff --git a/stingray/tests/test_lombscargle.py b/stingray/tests/test_lombscargle.py index 44c09f424..d352f7076 100644 --- a/stingray/tests/test_lombscargle.py +++ b/stingray/tests/test_lombscargle.py @@ -139,6 +139,10 @@ def test_init_with_invalid_max_freq(self): with pytest.raises(ValueError): lscs = LombScargleCrossspectrum(self.lc1, self.lc2, max_freq=1, min_freq=3) + def test_init_with_negative_max_freq(self): + with pytest.raises(ValueError): + lscs = LombScargleCrossspectrum(self.lc1, self.lc2, max_freq=-1) + def test_make_crossspectrum_diff_lc_counts_shape(self): lc_ = Simulator(0.0001, 103, 100, 1, random_state=42, tstart=0).simulate(0) with pytest.warns(UserWarning) as record: From 03606c812e1da90d93c3ce125c146c1eb8929ce7 Mon Sep 17 00:00:00 2001 From: Matteo Bachetti Date: Thu, 28 Sep 2023 11:50:00 +0200 Subject: [PATCH 28/31] Update notebooks --- docs/notebooks | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/notebooks b/docs/notebooks index fc7804914..4742e8ff0 160000 --- a/docs/notebooks +++ b/docs/notebooks @@ -1 +1 @@ -Subproject commit fc780491476f7981a1331feee6837ad047b44999 +Subproject commit 4742e8ff0795fd5ea94e04d3f4581a42c9c6924c From 42087dd19be917012976474f70891bd2d1e7bd4a Mon Sep 17 00:00:00 2001 From: Matteo Bachetti Date: Thu, 28 Sep 2023 12:00:50 +0200 Subject: [PATCH 29/31] Add reference to Lomb-Scargle tutorial --- docs/dataexplo.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/dataexplo.rst b/docs/dataexplo.rst index 7183275c1..df9b98a85 100644 --- a/docs/dataexplo.rst +++ b/docs/dataexplo.rst @@ -26,3 +26,15 @@ black hole binary using NICER data. :maxdepth: 2 notebooks/Spectral Timing/Spectral Timing Exploration.ipynb + + +Studying very slow variability with the Lomb-Scargle periodogram +================================================================ + +In this Tutorial, we will show an example of how to use the Lomb-Scargle +periodogram and cross spectrum to study very slow variability in a light curve. + +.. toctree:: + :maxdepth: 2 + + notebooks/LombScargle/Very slow variability with Lomb-Scargle methods.ipynb From 8a162a50414d6023e7cf0810e3caa447a1d2516b Mon Sep 17 00:00:00 2001 From: Matteo Bachetti Date: Thu, 28 Sep 2023 12:08:30 +0200 Subject: [PATCH 30/31] Fix missing whitespace [docs only] --- docs/core.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/core.rst b/docs/core.rst index 56ec7088f..ac15d1c16 100644 --- a/docs/core.rst +++ b/docs/core.rst @@ -1,9 +1,9 @@ Core Stingray Functionality *************************** -Here we show how many of the core Stingray classes and methods -work in practice. We start with basic data constructs for -event data and light curve data, and then show how to produce +Here we show how many of the core Stingray classes and methods +work in practice. We start with basic data constructs for +event data and light curve data, and then show how to produce various Fourier products from these data sets. Working with Event Data @@ -101,4 +101,4 @@ Lomb Scargle Powerspectrum .. toctree:: :maxdepth: 2 - notebooks/LombScargle/LombScarglePowerspectrum_tutorial.ipynb \ No newline at end of file + notebooks/LombScargle/LombScarglePowerspectrum_tutorial.ipynb From 01a1533407d6d32ee1258d3c6d39b43c580b835b Mon Sep 17 00:00:00 2001 From: Matteo Bachetti Date: Thu, 28 Sep 2023 12:26:45 +0200 Subject: [PATCH 31/31] Refresh front page [docs only] --- docs/index.rst | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 57760105f..b250d1297 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,26 +21,35 @@ Features Current Capabilities -------------------- -Currently implemented functionality in this library comprises: +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) +* 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 -* power spectra in Leahy, rms normalization, absolute rms and no normalization -* averaged power spectra -* dynamical power spectra + +2. Fourier methods +~~~~~~~~~~~~~~~~~~ +* power spectra and cross spectra in Leahy, rms normalization, absolute rms and no normalization +* averaged power spectra and cross spectra +* dynamical power spectra and cross spectra * maximum likelihood fitting of periodograms/parametric models * (averaged) cross spectra * coherence, time lags -* cross correlation functions -* RMS spectra and lags (time vs energy, time vs frequency); *needs testing* +* Variability-Energy spectra, like covariance spectra and lags *needs testing* * covariance spectra; *needs testing* * bispectra; *needs testing* * (Bayesian) quasi-periodic oscillation searches -* 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 +* Lomb-Scargle periodograms and cross spectra + +3. Other time series methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * pulsar searches with Epoch Folding, :math:`Z^2_n` test +* Gaussian Processes for QPO studies +* cross correlation functions Future Plans ------------