pycbc icon indicating copy to clipboard operation
pycbc copied to clipboard

Extreme discontinuities in optimized_match that are not present in match

Open bmck039 opened this issue 6 months ago • 4 comments

When calculating the mismatch between waves, discontinuities in the match value can occur when using pycbc.filter.matchedfilter.optimized_match that are not present when using match. Specifically, the research I'm doing is investigating lensed vs unlensed waves and certain combinations of chirp mass and time delay cause a spike in the mismatch (1-match). An example is shown in the following graph:

Image

This is not an isolated issue, it occurs in multiple regions of phase-space, as seen by the sharp red-orange regions in the following contour plot:

Image

Or zoomed-in:

Image

Here's a link to a pkl file containing the waveform strain for the range of waves from the first plot for testing purposes. Usage is as follows:

with open('data.pkl', 'rb') as file:
    td_list, lensed_waveform_list, unlensed_waveform_list = pickle.load(file)

Each item in lensed_waveform_list and unlensed_waveform_list is a FrequencySeries object of the strain of the wave. The PSD is calculated using the following function:

def Sn(f_arr, f_min=20, delta_f=0.25, frequencySeries=True):
    """
    Calculates the power spectral density of the aLIGO noise curve based on arXiv:0903.0338.

    Parameters
    ----------
    f_arr : np.ndarray
        The frequency array.
    f_min : float, optional
        The minimum frequency. Defaults to 20 Hz.
    delta_f : float, optional
        The frequency step size. Defaults to 0.25 Hz.
    frequencySeries : bool, optional
        If True, returns a FrequencySeries object. Defaults to True.

    Returns
    -------
    np.ndarray or FrequencySeries
        The power spectral density of the aLIGO noise curve.
    """

    Sn_val = np.zeros_like(f_arr)
    for i in range(len(f_arr)):
        if f_arr[i] < f_min:
            Sn_val[i] = np.inf
        else:
            S0 = 1e-49
            f0 = 215
            Sn_temp = (
                np.power(f_arr[i] / f0, -4.14)
                - 5 * np.power(f_arr[i] / f0, -2)
                + 111
                * (
                    (1 - np.power(f_arr[i] / f0, 2) + 0.5 * np.power(f_arr[i] / f0, 4))
                    / (1 + 0.5 * np.power(f_arr[i] / f0, 2))
                )
            )
            Sn_val[i] = Sn_temp * S0

    if frequencySeries:
        return FrequencySeries(Sn_val, delta_f=delta_f)
    return Sn_val

bmck039 avatar Jun 25 '25 22:06 bmck039

@bmck039 Thank you for reporting this. It definitely seems odd. That function uses an optimizer to try to fit to subsample precision. Perhaps that process is failing?

https://github.com/gwastro/pycbc/blob/master/pycbc/filter/matchedfilter.py#L2075

It doesn't look like we are checking the result of the minimizer to see if it has properly converged. I don't know if @jacopok would be able to look at this, but if you are comfortable modifying the code, perhaps to check the convergence result. One may need to pass in the ability to set the minimizer options to at least give some way to avoid failure modes (assuming this is the problem to begin with).

We are happy to accept contributions here if you have ideas to address this.

ahnitz avatar Jun 25 '25 22:06 ahnitz

Yeah, it's the optimizer messing up - but there's an easy solution.

Image

This is what happened in your test case - one might wonder, how is it possible that the optimizer found a local minimum outside its bounds? It turns out that the brent optimization method does not enforce its bounds, even if they are explicitly given.

So, the solution is simply to switch to the bounded method, don't know why I didn't do it from the beginning. Then your plot will look like this:

Image

Would you be willing to make the PR @bmck039 ?

Essentially you'd need to change the optimizer call to

    res = minimize_scalar(
        to_minimize,
        method="bounded",
        bounds=(-delta_t, delta_t)
    )

in the code @ahnitz linked to.

It would be nice to have an additional test in the automated test suite checking that optimized_match always returns a higher value than match (which will be true as long as the bounds are respected) - maybe hardcoding the waveforms at index 545 in your example, the ones for which the bug happens?

jacopok avatar Jun 26 '25 10:06 jacopok

Thanks for the fast reply! I'd be willing to do that!

bmck039 avatar Jun 26 '25 12:06 bmck039

Created PR #5146

bmck039 avatar Jun 26 '25 18:06 bmck039