Skip to content

Time-domain likelihood (with heterodyning) - #1161

Open
neha2023sharma wants to merge 21 commits into
bilby-dev:mainfrom
neha2023sharma:td-likelihood
Open

neha2023sharma wants to merge 21 commits into
bilby-dev:mainfrom
neha2023sharma:td-likelihood

Conversation

@neha2023sharma

Copy link
Copy Markdown

What it does

  • IMR analyses in the time domain: This works with any LAL time-domain approximant. The noise in each detector is modelled by an autocovariance function computed from its PSD.
  • Inspiral and post-inspiral analyses: We can choose a cut-off time relative to the merger and analyse only the truncated segment of data before it (inspiral) or after it (post-inspiral). The analysis window is set with set_analysis_windows(duration=..., analysis_segment="inspiral" or "post_inspiral", segment_cut_time=..., cut_reference_parameters=...), where segment_cut_time is the cut-off time relative to the merger, and cut_reference_parameters gives the reference merger time and sky location.
  • Three ways of placing the waveform on the data samples, chosen with placement:
    • "nearest": the waveform is generated on its own time grid and then shifted to the data sample nearest to its arrival time in each detector. It is the simplest and fastest option, but the timing can be off by up to half a sample.
    • "fd_shift": the waveform is Fourier transformed, the exact time shift is applied as a phase in the frequency domain, and it is transformed back to the time domain.
    • "model_time": the waveform is evaluated directly at the model times of each detector, shifted by the arrival time, so no shifting or interpolation is needed afterwards. This option currently works with the IMRPhenomT/TP/THM/TPHM approximants.
  • A heterodyned version of the time-domain likelihood to make likelihood computation faster. (Works only with the IMRPhenomT family)

@ColmTalbot ColmTalbot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @neha2023sharma this looks in generally pretty good shape! Given the nature of the proposed change, there are a decent number of conceptual questions here. I haven't looked in detail through everything, but I looked at the interferometer, likelihood, and source additions which seem the most important.

There are also some things in the implementation that I think are points of tension with previous implementation choices. I appreciate you not changing existing code, but I think over the review process we may need to find a few changes where we can make changes to make the new functionality feel more natural.

Comment thread bilby/gw/detector/acf.py Outdated
Comment thread bilby/gw/detector/acf.py

>>> from bilby.gw.detector import PowerSpectralDensity
>>> psd = PowerSpectralDensity.from_aligo()
>>> acf = AutoCovarianceFunction.from_power_spectral_density_array(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This begs the question of whether the AutoCovarianceFunction should mirror the PowerSpectralDensity more closely. Or even if they should just be one class.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think one class here would be better. The ACF is just the inverse Fourier transform of the PSD, so it makes sense to keep them together. I can add a method to PowerSpectralDensity that computes the ACF whenever needed.

Does this sound good? If so, I can make these changes.

Comment thread bilby/gw/detector/acf.py
Comment on lines +102 to +128
frequency_array = np.asarray(frequency_array, dtype=float)
psd_array = np.asarray(psd_array, dtype=float)
delta_f = frequency_array[1] - frequency_array[0]
frequency_values_missing = int(round(frequency_array[0] / delta_f))
if frequency_values_missing > 0:
frequency_array = np.concatenate(
[np.arange(frequency_values_missing) * delta_f, frequency_array])
psd_array = np.concatenate([np.zeros(frequency_values_missing), psd_array])

psd_sampling_frequency = 2 * frequency_array[-1]
if sampling_frequency is not None and not np.isclose(
sampling_frequency, psd_sampling_frequency, rtol=1e-9, atol=0):
logger.warning(
f"The sampling frequency given ({sampling_frequency:g} Hz) is not equal to the one "
f"computed from the PSD ({psd_sampling_frequency:g} Hz); using "
f"{psd_sampling_frequency:g} Hz. An ACF computed with {sampling_frequency:g} Hz "
"would be wrongly scaled and its lags wrongly spaced, giving wrong likelihood "
"and SNR values.")

band = (frequency_array >= minimum_frequency) & (frequency_array <= maximum_frequency)
if fill_value is None:
fill_value = fill_multiplier * np.max(psd_array[band])
else:
fill_multiplier = None
psd_array = psd_array.copy()
psd_array[~band] = fill_value
acf_array = 0.5 * np.fft.irfft(psd_array) * psd_sampling_frequency

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels significant and large enough to just make it a standalone function to convert between PSDs and ACFs.

@neha2023sharma neha2023sharma Oct 23, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will move this to a standalone function power_spectral_density_to_autocovariance that converts PSD to ACF. The new ACF method in PowerSpectralDensity will call this function to compute the ACF.

start_time=times[0])


__all__ = ["condition_strain_data"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__all__ should live at the top of modules if we define it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I will move __all__ to the top of the modules.

parameters=parameters, injection_polarizations=injection_polarizations, placement=placement)
return injection_polarizations

def inject_signal_time_domain_from_waveform_polarizations(self, parameters, injection_polarizations,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this use the existing functionality https://github.com/bilby-dev/bilby/blob/main/bilby/gw/detector/__init__.py#L33? If not, what is missing?

polarizations_at = signal["polarizations_at"]

def rescaled(times):
polarizations = polarizations_at(times)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, this is a function?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipping this file for this pass.

Comment thread bilby/gw/__init__.py
result, source, utils, waveform_generator)
from .waveform_generator import WaveformGenerator, LALCBCWaveformGenerator
from .likelihood import GravitationalWaveTransient
from .likelihood import GravitationalWaveTransient, TimeDomainGravitationalWaveTransient

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't add this extra top level import. I'd like to reduce the number we have over time.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll remove this.

Comment thread bilby/gw/conversion.py
return output_sample


def _signal_polarizations_for_likelihood(likelihood, parameters):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a workaround for the method I suggested adding to the GravitationalWaveTransient.

Comment thread bilby/gw/source.py

Returns
=======
dict: polarizations_at, a function of the times relative to the merger (in s)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this use case yet, is it for cases where you want to apply detector-based time delays before evaluating the model?

neha2023sharma and others added 2 commits October 20, 2026 14:09

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants