Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Linear interpolated FFT-based time-correlated signals #411

Open
wants to merge 34 commits into
base: dev
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
caef4a3
Initial FFT-based method. No sparse TNT yet
vhaasteren Feb 24, 2025
d57be32
Linting
vhaasteren Feb 26, 2025
7d7c6c9
Merge branch 'dev' into fft-interp
vhaasteren Feb 26, 2025
6f7a92e
Change some comments
vhaasteren Feb 26, 2025
6c43e1d
Removed caching for common FFT signals
vhaasteren Feb 26, 2025
9cd9899
Tweaks to interpolation basis
vhaasteren Feb 26, 2025
2baf8eb
FIXED: bug in FFT signals due to Enterprise prior functions df calcul…
vhaasteren Feb 27, 2025
01c3655
Block comment should start with '# '
vhaasteren Feb 27, 2025
357f1e9
Changed knots -> nknots, and added tests for FFT signals
vhaasteren Feb 27, 2025
bb071b4
Linting test_gp_signals
vhaasteren Feb 27, 2025
c2927c5
Removed linting of un-changed code in test_gp_signals.py
vhaasteren Feb 27, 2025
7e79acb
Removed fft tests to see whether linting goes now
vhaasteren Feb 27, 2025
5074199
Added FFT red noise test again
vhaasteren Feb 27, 2025
094c67f
Switched FFT test function, see if this one lints
vhaasteren Feb 27, 2025
9e1d1a3
Added FFT red noise and common tests
vhaasteren Feb 27, 2025
0649d1f
Used different black config
vhaasteren Feb 27, 2025
0afe73b
Yet again, linting
vhaasteren Feb 27, 2025
5d435ab
Removed debugging test statements in test_gp_signals.py
vhaasteren Feb 27, 2025
085f128
Added DMV and Chromatic noise FFT interpolation GP bases
vhaasteren Feb 27, 2025
4d84356
Added option 'basis' to FFTBasisGP for DM-variations and chromatic noise
vhaasteren Feb 27, 2025
80e3d41
Fix merge conflict with nanograv/master
vhaasteren Mar 3, 2025
c0b4b15
Added FFT documentation
vhaasteren Mar 3, 2025
78e9797
Updated documentation on FFT models
vhaasteren Mar 3, 2025
245355e
Updated FFT red signals docs to mention (...Tspan=Tspan, start_time=s…
vhaasteren Mar 3, 2025
7aaacbb
Updated docstring of knots_to_freqs
vhaasteren Mar 12, 2025
44cbff7
Updated docstring for FFTBasisGP
vhaasteren Mar 12, 2025
35a1a95
Merge branch 'master' into fft-interp
vhaasteren Mar 13, 2025
cd879c9
Added note to docs about ContitionalGP
vhaasteren Mar 13, 2025
2b20f5a
Merge branch 'dev' into fft-interp
vhaasteren Mar 13, 2025
742381c
Added higher order interpolation methods to the FFT correlated GPs
vhaasteren Mar 19, 2025
40424a0
Merge branch 'dev' into fft-interp
vhaasteren Mar 19, 2025
1eb262b
Added caching of the prior/Phi
vhaasteren Mar 21, 2025
e3c0320
Fixed caching, but untested still
vhaasteren Mar 21, 2025
e04d9d4
Added fmax_factor to FFT methods
vhaasteren Mar 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions docs/fft.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@

.. module:: enterprise
:noindex:

.. note:: This tutorial was generated from a Jupyter notebook that can be
downloaded `here <_static/notebooks/mdc.ipynb>`_.

.. _mdc:

Red noise modeling
=======================

In the beginning of Enterprise red noise modeling, the red noise prior was
always modeled as a diagonal matrix, meaning that the Fourier coefficients
were assumed to be uncorrelated. This model was introduced by Lentati et al.
(2013), and explained by van Haasteren and Vallisneri (2014). In practice this
has been a good-enough approximation, but it is not exact.

As of early 2025 we now have a more realistic implementation of red noise
priors that include correlations between the basis functions. The `FFT`
method as it is called is a rank-reduced time-domain implementation, meaning
it does not rely on Fourier modes, but on regularly sampled coarse grained
time samples. Here we briefly explain how to use it.



Red noise modeling
-------------------

The traditional old-style way of modeling was done like:

.. code:: python

rn_pl = powerlaw(log10_A=rn_log10_A, gamma=rn_gamma)
rn_phi = gp_signals.FourierBasisGP(spectrum=rn_pl, components=n_components, Tspan=Tspan)

For the FFT time-domain model, one would do:

.. code:: python

rn_pl = powerlaw(log10_A=rn_log10_A, gamma=rn_gamma)
rn_fft = gp_signals.FFTBasisGP(spectrum=rn_pl, components=n_components, oversample=3, cutoff=3, Tspan=Tspan, start_time=start_time)

The same spectral function can be used. Free spectrum is NOT supported yet.
Instead of `components`, we can also pass `knots=`, where it is understood that
`knots=2*n_components+1`. This is because `components` actually means
frequencies. In the time-domain, the number of `knots` is the number of
`modes+1`, because we cannot just omit the DC term.

The `oversample` parameter determines how densely the PSD is sampled in
frequencies. With `oversample=1` we would use frequencies at spacing of
`df=1/T`. With `oversample=3` (the default), the frequency spacing is
`df=1/(3T)`. Note that this is a way to numerically approximate the
Wiener-Khinchin integral. With oversample sufficiently large, the FFT is an
excellent approximation of the analytical integral. For powerlaw signals,
`oversample=3` seems a very reasonable choice.

The `cutoff` parameter is used to specify below which frequency `fcut = 1 /
(cutoff*Tspan)` we set the PSD equal to zero. Note that this parameterization
(which is also in Discovery) is a bit ambiguous, as fcut may not correspond to
an actual bin of the FFT: especially if oversample is not a high number this
can cause a mismatch. In case of a mismatch, `fcut` is rounded up to the next
oversampled-FFT frequency bin. Instead of `cutoff`, the parameter `cutbins` can
also be used (this overrides cutoff). With cutbins the low-frequency cutoff is
set at: `fcut = cutbins / (oversample * Tspan)`, and its interpretation is less
ambiguous: it is the number of bins of the over-sampled PSD of the FFT that is
being zeroed out.

Common signals
--------------

For common signals, instead of:

.. code:: python

gw_pl = powerlaw(log10_A=gw_log10_A, gamma=gw_gamma)
orf = utils.hd_orf()
crn_phi = gp_signals.FourierBasisCommonGP(gw_pl, orf, components=20, name='gw', Tspan=Tspan)


one would do:

.. code:: python

gw_pl = powerlaw(log10_A=gw_log10_A, gamma=gw_gamma)
orf = utils.hd_orf()
crn_fft = gp_signals.FFTBasisCommonGP(gw_pl, orf, components=20, name='gw', Tspan=Tspan, start_time=start_time)

Chromatic signals
-----------------

DM-variations and Chromatic noise can be similarly set up:

.. code:: python

nknots = 81
dm_basis = utils.create_fft_time_basis_dm(nknots=nknots)
dm_pl = powerlaw(log10_A=dm_log10_A, gamma=dm_gamma)
dm_fft = gp_signals.FFTBasisGP(dm_pl, basis=dm_basis, nknots=nknots, name='dmgp')

chrom_basis = utils.create_fft_time_basis_chromatic(nknots=nknots, idx=chrom_idx)
chrom_pl = powerlaw(log10_A=chrom_log10_A, gamma=chrom_gamma)
chrom_fft = gp_signals.FFTBasisGP(chrom_pl, basis=chrom_basis, nknots=nknots, name='chromgp')

Subtleties
----------

Enterprise allows one to combine basis functions when they are the same. This
is especially useful when analyzing common signals which have the same basis as
a single-pulsar signal, such as one would have with red noise and a correlated
GWB. This can be done with the `combine=True` option in `FFTBasisGP` and
`FFTBasisCommonGP`. Default is `combine=True`. The subtlety is that modern PTA
datasets typically have large gaps, which causes some of the time-domain basis
functions to basically be all zeros. Therefore, some basis functions that you
would not expect to be identical will be combined.

The above is not a bug. Combining such bases and the corresponding Phi matrix
does not matter, because the basis is zero, and that part of the signal has no
bearing on the data or the model. However, when doing signal reconstruction,
such as with `la_forge` or `utils.ConditionalGP`, make sure to set
`combine=False`.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -42,6 +42,7 @@ searches, and timing model analysis.

mdc
nano9
fft

.. toctree::
:maxdepth: 2
93 changes: 93 additions & 0 deletions enterprise/signals/gp_bases.py
Original file line number Diff line number Diff line change
@@ -5,19 +5,23 @@

import numpy as np
from enterprise.signals.parameter import function
import scipy.interpolate as sint

######################################
# Fourier-basis signal functions #####
######################################

__all__ = [
"createfourierdesignmatrix_red",
"create_fft_time_basis",
"createfourierdesignmatrix_dm",
"create_fft_time_basis_dm",
"createfourierdesignmatrix_dm_tn",
"createfourierdesignmatrix_env",
"createfourierdesignmatrix_ephem",
"createfourierdesignmatrix_eph",
"createfourierdesignmatrix_chromatic",
"create_fft_time_basis_chromatic",
"createfourierdesignmatrix_general",
]

@@ -91,6 +95,38 @@
return F, Ffreqs


@function
def create_fft_time_basis(toas, nknots=30, Tspan=None, start_time=None, order=1):
"""
Construct coarse time-domain design matrix from eq 11 of Chrisostomi et al., 2025
:param toas: vector of time series in seconds
:param nknots: number of coarse time samples to use (knots)
:param Tspan: option to some other Tspan
:param start_time: option to set some other start epoch of basis
:param order: order of the interpolation (1 = linear)

:return B: coarse time-domain design matrix
:return t_coarse: timestamps of coarse time grid
"""
if start_time is None:
start_time = np.min(toas)
else:
if start_time > np.min(toas):
raise ValueError("Coarse time basis start must be earlier than earliest TOA.")

Check warning on line 115 in enterprise/signals/gp_bases.py

Codecov / codecov/patch

enterprise/signals/gp_bases.py#L115

Added line #L115 was not covered by tests

if Tspan is None:
Tspan = np.max(toas) - start_time
else:
if start_time + Tspan < np.max(toas):
raise ValueError("Coarse time basis end must be later than latest TOA.")

Check warning on line 121 in enterprise/signals/gp_bases.py

Codecov / codecov/patch

enterprise/signals/gp_bases.py#L121

Added line #L121 was not covered by tests

t_fine = toas
t_coarse = np.linspace(start_time, start_time + Tspan, nknots)
Bmat = sint.interp1d(t_coarse, np.identity(nknots), kind=order)(t_fine).T

return Bmat, t_coarse


@function
def createfourierdesignmatrix_dm(
toas, freqs, nmodes=30, Tspan=None, pshift=False, fref=1400, logf=False, fmin=None, fmax=None, modes=None
@@ -127,6 +163,34 @@
return F * Dm[:, None], Ffreqs


@function
def create_fft_time_basis_dm(toas, freqs, nknots=30, Tspan=None, start_time=None, fref=1400, order=1):
"""
Construct DM-variation linear interpolation design matrix. Current
normalization expresses DM signal as a deviation [seconds]
at fref [MHz]

:param toas: vector of time series in seconds
:param freqs: radio frequencies of observations [MHz]
:param nknots: number of coarse time samples to use (knots)
:param Tspan: option to some other Tspan
:param start_time: option to set some other start epoch of basis
:param fref: reference frequency [MHz]
:param order: order of the interpolation (1 = linear)

:return B: coarse time-domain design matrix
:return t_coarse: timestamps of coarse time grid
"""

# get base course time-domain matrix and times
Bmat, t_coarse = create_fft_time_basis(toas, nknots=nknots, Tspan=Tspan, start_time=start_time, order=order)

# compute the DM-variation vectors
Dm = (fref / freqs) ** 2

return Bmat * Dm[:, None], t_coarse


@function
def createfourierdesignmatrix_dm_tn(
toas, freqs, nmodes=30, Tspan=None, pshift=False, fref=1400, logf=False, fmin=None, fmax=None, idx=2, modes=None
@@ -292,6 +356,35 @@
return F * Dm[:, None], Ffreqs


@function
def create_fft_time_basis_chromatic(toas, freqs, nknots=30, Tspan=None, start_time=None, fref=1400, idx=4, order=1):
"""
Construct scattering linear interpolation design matrix. Current
normalization expresses DM signal as a deviation [seconds]
at fref [MHz]

:param toas: vector of time series in seconds
:param freqs: radio frequencies of observations [MHz]
:param nknots: number of coarse time samples to use (knots)
:param Tspan: option to some other Tspan
:param start_time: option to set some other start epoch of basis
:param fref: reference frequency [MHz]
:param idx: Index of chromatic effects
:param order: order of the interpolation (1 = linear)

:return B: coarse time-domain design matrix
:return t_coarse: timestamps of coarse time grid
"""

# get base course time-domain matrix and times
Bmat, t_coarse = create_fft_time_basis(toas, nknots=nknots, Tspan=Tspan, start_time=start_time, order=order)

# compute the DM-variation vectors
Dm = (fref / freqs) ** idx

return Bmat * Dm[:, None], t_coarse


@function
def createfourierdesignmatrix_general(
toas,
9 changes: 9 additions & 0 deletions enterprise/signals/gp_priors.py
Original file line number Diff line number Diff line change
@@ -67,6 +67,15 @@
return powerlaw(f, log10_A=log10_A, gamma=gamma) * alpha_model


@function
def powerlaw_flat_tail(f, log10_A=-16, gamma=5, log10_kappa=-7, components=2):
"""Powerlaw with a flat tail (similar to broken powerlaw)"""
df = np.diff(np.concatenate((np.array([0]), f[::components])))
pl = (10**log10_A) ** 2 / 12.0 / np.pi**2 * const.fyr ** (gamma - 3) * f ** (-gamma) * np.repeat(df, components)
flat = 10 ** (2 * log10_kappa)
return np.maximum(pl, flat)

Check warning on line 76 in enterprise/signals/gp_priors.py

Codecov / codecov/patch

enterprise/signals/gp_priors.py#L73-L76

Added lines #L73 - L76 were not covered by tests


def InvGammaPrior(value, alpha=1, gamma=1):
"""Prior function for InvGamma parameters."""
return scipy.stats.invgamma.pdf(value, alpha, scale=gamma)
Loading