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

set failures to zero #203

Merged
merged 17 commits into from
Oct 14, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
128 changes: 102 additions & 26 deletions gw_eccentricity/eccDefinition.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"""

import numpy as np
import matplotlib.pyplot as plt
from .utils import peak_time_via_quadratic_fit, check_kwargs_and_set_defaults
from .utils import amplitude_using_all_modes
from .utils import time_deriv_4thOrder
Expand All @@ -16,7 +17,7 @@
from .utils import debug_message
from .plot_settings import use_fancy_plotsettings, colorsDict, labelsDict
from .plot_settings import figWidthsTwoColDict, figHeightsDict
import matplotlib.pyplot as plt
from .exceptions import InsufficientExtrema, NotInRange


class eccDefinition:
Expand Down Expand Up @@ -237,6 +238,12 @@ def __init__(self, dataDict, num_orbits_to_exclude_before_merger=2,
methods. See
eccDefinitionUsingFrequencyFits.get_default_kwargs_for_fits_methods
for allowed keys.

set_failures_to_zero : bool, default=False
If True and the waveform is sufficiently long then instead of
raising exception when number of extrema is insufficient to
build frequency interpolant through the extrema, eccentricity
and mean anomaly are set to zero.
vijayvarma392 marked this conversation as resolved.
Show resolved Hide resolved
"""
# Get data necessary for eccentricity measurement
vijayvarma392 marked this conversation as resolved.
Show resolved Hide resolved
self.dataDict, self.t_merger, self.amp22_merger, \
Expand Down Expand Up @@ -272,6 +279,7 @@ def __init__(self, dataDict, num_orbits_to_exclude_before_merger=2,
= self.get_available_omega22_averaging_methods()
self.debug_level = self.extra_kwargs["debug_level"]
self.debug_plots = self.extra_kwargs["debug_plots"]
self.set_failures_to_zero = self.extra_kwargs["set_failures_to_zero"]
# check if there are unrecognized keys in the dataDict
self.recognized_dataDict_keys = self.get_recognized_dataDict_keys()
for kw in dataDict.keys():
Expand Down Expand Up @@ -305,6 +313,13 @@ def __init__(self, dataDict, num_orbits_to_exclude_before_merger=2,
# called, these get set in that function.
self.t_for_omega22_average = None
self.omega22_average = None
# Approximate number of orbits derived using the phase of 22 mode
# assuming that a phase change 4pi occurs over an orbit.
vijayvarma392 marked this conversation as resolved.
Show resolved Hide resolved
self.approximate_num_orbits = ((self.phase22[-1] - self.phase22[0])
/ (4 * np.pi))
# The following is updated to True when the waveform has enough number
# of orbits but a method can not find sufficient number of extrema.
self.probably_quasicircular = False
vijayvarma392 marked this conversation as resolved.
Show resolved Hide resolved

# compute residual data
if "amplm_zeroecc" in self.dataDict and "omegalm_zeroecc" in self.dataDict:
Expand Down Expand Up @@ -716,6 +731,7 @@ def get_default_extra_kwargs(self):
"treat_mid_points_between_pericenters_as_apocenters": False,
"refine_extrema": False,
"kwargs_for_fits_methods": {}, # Gets overriden in fits methods
"set_failures_to_zero": False,
}
return default_extra_kwargs

Expand Down Expand Up @@ -1055,26 +1071,41 @@ def interp_extrema(self, extrema_type="pericenters"):
return self.get_interp(self.t[extrema],
self.omega22[extrema])
else:
raise Exception(
f"Sufficient number of {extrema_type} are not found."
" Can not create an interpolant.")
raise InsufficientExtrema(extrema_type, len(extrema))

def check_num_extrema(self, extrema, extrema_type="extrema"):
"""Check number of extrema."""
vijayvarma392 marked this conversation as resolved.
Show resolved Hide resolved
num_extrema = len(extrema)
if num_extrema < 2:
recommended_methods = ["ResidualAmplitude", "AmplitudeFits"]
if self.method not in recommended_methods:
method_message = ("It's possible that the eccentricity is too "
f"low for the {self.method} method to detect"
f" the {extrema_type}. Try one of "
f"{recommended_methods}.")
# check if the waveform is sufficiently long
if self.approximate_num_orbits > 5:
vijayvarma392 marked this conversation as resolved.
Show resolved Hide resolved
# The waveform is long but the method fails to find the extrema
# This may happen because the eccentricity too small for the
# method to detect it.
self.probably_quasicircular = True
if self.probably_quasicircular and self.set_failures_to_zero:
debug_message(
"The waveform has approximately "
f"{self.approximate_num_orbits:.2f}"
f" orbits but number of {extrema_type} found is "
f"{num_extrema}. Since `set_failures_to_zero` is set to "
f"{self.set_failures_to_zero}, no exception is raised. "
"Instead the eccentricity and mean anomaly will be set to "
"zero.",
important=True,
debug_level=0)
else:
method_message = ""
raise Exception(
f"Number of {extrema_type} found = {num_extrema}.\n"
f"Can not build frequency interpolant through the {extrema_type}.\n"
f"{method_message}")
recommended_methods = ["ResidualAmplitude", "AmplitudeFits"]
vijayvarma392 marked this conversation as resolved.
Show resolved Hide resolved
if self.method not in recommended_methods:
method_message = (
"It's possible that the eccentricity is too "
f"low for the {self.method} method to detect"
f" the {extrema_type}. Try one of "
f"{recommended_methods}.")
else:
method_message = ""
raise InsufficientExtrema(extrema_type, num_extrema,
method_message)
vijayvarma392 marked this conversation as resolved.
Show resolved Hide resolved

def check_if_dropped_too_many_extrema(self, original_extrema, new_extrema,
extrema_type="extrema",
Expand Down Expand Up @@ -1222,6 +1253,17 @@ def measure_ecc(self, tref_in=None, fref_in=None):
Measured mean anomaly at *tref_out*/*fref_out*. Same type as
*tref_out*/*fref_out*.
"""
# check that only one of tref_in or fref_in is provided
if (tref_in is not None) + (fref_in is not None) != 1:
raise KeyError("Exactly one of tref_in and fref_in"
" should be specified.")
elif tref_in is not None:
tref_in_ndim = np.ndim(tref_in)
self.tref_in = np.atleast_1d(tref_in)
else:
fref_in_ndim = np.ndim(fref_in)
tref_in_ndim = fref_in_ndim
fref_in = np.atleast_1d(fref_in)
# Get the pericenters and apocenters
pericenters = self.find_extrema("pericenters")
original_pericenters = pericenters.copy()
Expand All @@ -1237,6 +1279,17 @@ def measure_ecc(self, tref_in=None, fref_in=None):
apocenters = self.find_extrema("apocenters")
original_apocenters = apocenters.copy()
self.check_num_extrema(apocenters, "apocenters")

# If the eccentricity is too small for a method to find the extrema and
# set_failures_to_zero is set to true, then we set the eccentricity and
# mean anomaly to zero and return it.
# In this case, the rest of the code in this function is not executed and
# therefore, many variables which are used in diagnostic tests are never
# computed thus making diagnostics irrelevant.
if self.probably_quasicircular and self.set_failures_to_zero:
vijayvarma392 marked this conversation as resolved.
Show resolved Hide resolved
return self.set_eccentricity_and_mean_anomaly_to_zero(
tref_in, fref_in)

# Choose good extrema
self.pericenters_location, self.apocenters_location \
= self.get_good_extrema(pericenters, apocenters)
Expand Down Expand Up @@ -1268,17 +1321,7 @@ def measure_ecc(self, tref_in=None, fref_in=None):
self.t_apocenters = self.t[self.apocenters_location]
self.tmax = min(self.t_pericenters[-1], self.t_apocenters[-1])
self.tmin = max(self.t_pericenters[0], self.t_apocenters[0])
# check that only one of tref_in or fref_in is provided
if (tref_in is not None) + (fref_in is not None) != 1:
raise KeyError("Exactly one of tref_in and fref_in"
" should be specified.")
elif tref_in is not None:
tref_in_ndim = np.ndim(tref_in)
self.tref_in = np.atleast_1d(tref_in)
else:
fref_in_ndim = np.ndim(fref_in)
tref_in_ndim = fref_in_ndim
fref_in = np.atleast_1d(fref_in)
if tref_in is None:
# get the tref_in and fref_out from fref_in
self.tref_in, self.fref_out \
= self.compute_tref_in_and_fref_out_from_fref_in(fref_in)
Expand Down Expand Up @@ -1372,6 +1415,39 @@ def measure_ecc(self, tref_in=None, fref_in=None):
return_dict.update({"tref_out": self.tref_out})
return return_dict

def set_eccentricity_and_mean_anomaly_to_zero(
self, tref_in, fref_in):
"""Set eccentricity and mean_anomaly to zero."""
vijayvarma392 marked this conversation as resolved.
Show resolved Hide resolved
return_dict = {}
if tref_in is not None:
# check that tref_in is in the allowed range
ndim = np.ndim(tref_in)
vijayvarma392 marked this conversation as resolved.
Show resolved Hide resolved
tref_in = np.atleast_1d(tref_in)
if min(tref_in) < min(self.t) or max(tref_in) > max(self.t):
raise NotInRange("tref_in", min(self.t), max(self.t))
ref_arr = tref_in
self.tref_out = ref_arr[0] if ndim == 0 else ref_arr
return_dict.update(
{"tref_out": self.tref_out})
else:
# check that fref_in is in the allowed range
ndim = np.ndim(fref_in)
fref_in = np.atleast_1d(fref_in)
f22_min = min(self.omega22) / (2 * np.pi)
f22_max = max(self.omega22) / (2 * np.pi)
if min(fref_in) < f22_min or max(fref_in) > f22_max:
raise NotInRange("fref_in", f22_min, f22_max)
ref_arr = fref_in
self.fref_out = ref_arr[0] if ndim == 0 else ref_arr
return_dict.update({"fref_out": ref_arr})
self.eccentricity = 0 if ndim == 0 else np.zeros(len(ref_arr))
self.mean_anomaly = 0 if ndim == 0 else np.zeros(len(ref_arr))
return_dict.update({
"eccentricity": self.eccentricity,
"mean_anomaly": self.mean_anomaly
})
return return_dict

def et_from_ew22_0pn(self, ew22):
"""Get temporal eccentricity at Newtonian order.

Expand Down
58 changes: 58 additions & 0 deletions gw_eccentricity/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Custom exception classes for gw_eccentricity."""


class InsufficientExtrema(Exception):
vijayvarma392 marked this conversation as resolved.
Show resolved Hide resolved
"""Exception raised when number of extrema is not enough.

Parameters
----------
extrema_type : str
Type of extrema. Can be "pericenter" or "apocenter".
num_extrema : int
Number of extrema.
additional_message : str
Any additional message to append to the exception message.
Default is None which adds no additional message.
"""

def __init__(self, extrema_type, num_extrema, additional_message=None):
"""Init for InsufficientExtrema Class."""
self.extrema_type = extrema_type
self.num_extrema = num_extrema
self.additional_message = additional_message
self.message = (f"Number of {self.extrema_type} is {self.num_extrema}."
f" Number of {self.extrema_type} is not sufficient "
f"to build frequency interpolant.")
if self.additional_message is not None:
self.message += "\n" + self.additional_message
super().__init__(self.message)


class NotInRange(Exception):
vijayvarma392 marked this conversation as resolved.
Show resolved Hide resolved
"""Exception raised when the reference point is outside allowed range.

Parameters
----------
reference_point_type : str
Type of reference point. Can be "tref_in" or "fref_in".
lower : float
Minium allowed value, i. e., the lower boundary of allowed range.
upper : float
Maximum allowed value, i. e., the upper boundary of allowed range.
additional_message : str
Any additional message to append to the exception message.
Default is None which adds no additional message.
"""

def __init__(self, reference_point_type, lower, upper,
additional_message=None):
"""Init for NotInRange Class."""
self.reference_point_type = reference_point_type
self.lower = lower
self.upper = upper
self.additional_message = additional_message
self.message = (f"{self.reference_point_type} is outside the allowed "
f"range [{self.lower}, {self.upper}].")
if self.additional_message is not None:
self.message += "\n" + self.additional_message
super().__init__(self.message)
5 changes: 5 additions & 0 deletions gw_eccentricity/gw_eccentricity.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,11 @@ def measure_eccentricity(tref_in=None,
eccDefinitionUsingFrequencyFits.get_default_kwargs_for_fits_methods
for allowed keys.

set_failures_to_zero : bool, default=False If True and the waveform is
sufficiently long then instead of raising exception when number of
extrema is insufficient to build frequency interpolant through the
extrema, eccentricity and mean anomaly are set to zero.

Returns
-------
A dictionary containing the following keys
Expand Down
3 changes: 3 additions & 0 deletions gw_eccentricity/load_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,9 @@ def load_LAL_waveform(**kwargs):
zero_ecc_kwargs['ecc'] = 0
zero_ecc_kwargs['include_zero_ecc'] = False # to avoid infinite loops
dataDict_zero_ecc = load_waveform(**zero_ecc_kwargs)
while dataDict_zero_ecc['t'][0] > dataDict["t"][0]:
vijayvarma392 marked this conversation as resolved.
Show resolved Hide resolved
zero_ecc_kwargs["Momega0"] *= 0.5
dataDict_zero_ecc = load_waveform(**zero_ecc_kwargs)
vijayvarma392 marked this conversation as resolved.
Show resolved Hide resolved
t_zeroecc = dataDict_zero_ecc['t']
hlm_zeroecc = dataDict_zero_ecc['hlm']
dataDict.update({'t_zeroecc': t_zeroecc,
Expand Down
56 changes: 56 additions & 0 deletions test/test_set_failures_to_zero.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import gw_eccentricity
from gw_eccentricity import load_data
from gw_eccentricity import measure_eccentricity
from gw_eccentricity.exceptions import InsufficientExtrema
import numpy as np


def test_set_failures_to_zero():
""" Tests that failures handling due to insufficient extrema.

Amplitude and Frequency method usually fails to detect extrema when the
eccentricity is below 10^-3. If the waveform has enough orbits and yet
these methods fail to detect any extrema then it is probably because of the
very small eccentricity. In such cases of failures, if the user set the key
`set_failures_to_zero` in extra_kwargs, then eccentricity and mean anomaly
are set to zero.
"""
# Load test waveform
lal_kwargs = {"approximant": "EccentricTD",
"q": 3.0,
"chi1": [0.0, 0.0, 0.0],
"chi2": [0.0, 0.0, 0.0],
"Momega0": 0.01,
"ecc": 1e-4,
"mean_ano": 0,
"include_zero_ecc": True}
dataDict = load_data.load_waveform(**lal_kwargs)
available_methods = gw_eccentricity.get_available_methods()
tref_in = -8000
fref_in = 0.005
vijayvarma392 marked this conversation as resolved.
Show resolved Hide resolved

extra_kwargs = {"set_failures_to_zero": True}
for method in available_methods:
gwecc_dict = measure_eccentricity(
tref_in=tref_in,
method=method,
dataDict=dataDict,
extra_kwargs=extra_kwargs)
tref_out = gwecc_dict["tref_out"]
ecc_ref = gwecc_dict["eccentricity"]
meanano_ref = gwecc_dict["mean_anomaly"]
if method in ["Amplitude", "Frequency"]:
vijayvarma392 marked this conversation as resolved.
Show resolved Hide resolved
np.testing.assert_allclose(ecc_ref, 0.0)
np.testing.assert_allclose(meanano_ref, 0.0)

gwecc_dict = measure_eccentricity(
fref_in=fref_in,
method=method,
dataDict=dataDict,
extra_kwargs=extra_kwargs)
fref_out = gwecc_dict["fref_out"]
ecc_ref = gwecc_dict["eccentricity"]
meanano_ref = gwecc_dict["mean_anomaly"]
if method in ["Amplitude", "Frequency"]:
np.testing.assert_allclose(ecc_ref, 0.0)
np.testing.assert_allclose(meanano_ref, 0.0)