Skip to content

FEAT: add option to crop whitened time series#1030

Open
ColmTalbot wants to merge 15 commits intobilby-dev:mainfrom
ColmTalbot:truncated-likelihood
Open

FEAT: add option to crop whitened time series#1030
ColmTalbot wants to merge 15 commits intobilby-dev:mainfrom
ColmTalbot:truncated-likelihood

Conversation

@ColmTalbot
Copy link
Copy Markdown
Collaborator

@ColmTalbot ColmTalbot commented Dec 23, 2025

This PR implements the proposed modification to the likelihood from https://iopscience.iop.org/article/10.1088/1361-6382/ae1ac7/meta.

The main new feature is adding a crop_duration to the InterferometerStrainData and some accompanying utility code.

I modified how the GravitationalWaveTransient and BasicGravitationalWaveTransient call the data. I did not have to change the reference likelihood tests, so there is good evidence that this doesn't impact the numerical values when not using the cropping.

I suspect there is a performance penalty due to repeatedly whitening the data. There should be a way to cache this calculation, it just requires some cleverness about when the time/frequency masks might have changed.

  • I'm not sure at this time how this should interact with accelerated likelihood, so I've left them alone.
  • I also haven't validated how this interacts with time/calibration marginalization.

@ColmTalbot ColmTalbot added enhancement New feature or request >100 lines detector likelihood to discuss To be discussed on an upcoming call labels Jan 20, 2026
@ColmTalbot ColmTalbot force-pushed the truncated-likelihood branch from f39871f to 5fbc454 Compare January 21, 2026 15:17
Comment on lines +631 to +639
@property
def frequency_mask(self):
frequencies = bilby.core.utils.series.create_frequency_series(
duration=self.ifo.cropped_duration, sampling_frequency=self.sampling_frequency
)
return (self.ifo.minimum_frequency <= frequencies) & (
frequencies <= self.ifo.maximum_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.

We should probably have a utility function that does this.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done

@ColmTalbot ColmTalbot force-pushed the truncated-likelihood branch from 0b74e2b to 0972d62 Compare January 23, 2026 16:53
@ColmTalbot ColmTalbot requested a review from mj-will January 26, 2026 08:32
Copy link
Copy Markdown
Collaborator

@GregoryAshton GregoryAshton left a comment

Choose a reason for hiding this comment

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

Overall looks good and I'm supportive, just a few things I wanted to check.

@ColmTalbot ColmTalbot added this to the 3.0.0 milestone Jan 29, 2026
@ColmTalbot ColmTalbot force-pushed the truncated-likelihood branch 2 times, most recently from b8f4b97 to c7e762a Compare February 5, 2026 14:38
else:
return self.whiten_and_crop(frequency_series=frequency_series)

def whiten_and_crop(self, frequency_series : np.array) -> np.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.

It seems like this procedure is more fundamental than suggested by its being a method of the interferometer class. The nice thing about using the gwutils SNR functions was that you could calculate SNRs using them without necessarily initializing an interferometer object. Is it possible to pull this out to the utils and just call it here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done

return np.nan_to_num(whitened) * frequency_mask * (4 / duration)**0.5


def whiten_and_crop(frequency_series, amplitude_spectral_density, frequency_mask, time_mask, duration):
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.

Should the name state what input it accepts? E.g. like 'frequency_domain_whiten' to make it clear it is acting on the frequency-domain input?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I can make this change, but it's pretty clear from the signature and docstring.

Copy link
Copy Markdown
Collaborator

@GregoryAshton GregoryAshton left a comment

Choose a reason for hiding this comment

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

A few more comments after another look.

* np.sqrt(np.sum(self.frequency_mask)) / frequency_window_factor
* np.sqrt(np.sum(self.frequency_mask))
/ frequency_window_factor
* self.time_mask.mean()**0.5
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.

Why does this use both np.sqrt and **0.5. It would probably be easier to read if it was consistent.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think I was trying to avoid explicitly using numpy, but that's unavoidable since we need an fft and sqrt is sometimes meaningfully faster.

Suggested change
* self.time_mask.mean()**0.5
* np.sqrt(self.time_mask.mean())

The frequency series, whitened by the ASD
"""
return frequency_series / (self.amplitude_spectral_density_array * np.sqrt(self.duration / 4))
if self.crop_duration == 0:
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.

What happens (or can) crop_duration be "None" or None etc? This would silently fail and then trigger the else clause, likely also erroring, but could be confusing?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This would fail in the crop_duration.setter in the InterferometerStrainData with a reasonably explicit error message.

)
_mask = interferometer.frequency_mask

if 'recalib_index' in 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.

Why does the signal no longer require masking here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Unclear, is this tested?

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

Labels

detector enhancement New feature or request likelihood to discuss To be discussed on an upcoming call >100 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants