From a66fb0395d291f4084e956c99ffc047f3a29aed7 Mon Sep 17 00:00:00 2001 From: md-arif-shaikh Date: Thu, 24 Aug 2023 22:22:59 +0900 Subject: [PATCH 01/17] set failures to zero --- gw_eccentricity/eccDefinition.py | 128 +++++++++++++++++++++++------ gw_eccentricity/exceptions.py | 58 +++++++++++++ gw_eccentricity/gw_eccentricity.py | 5 ++ gw_eccentricity/load_data.py | 3 + test/test_set_failures_to_zero.py | 56 +++++++++++++ 5 files changed, 224 insertions(+), 26 deletions(-) create mode 100644 gw_eccentricity/exceptions.py create mode 100644 test/test_set_failures_to_zero.py diff --git a/gw_eccentricity/eccDefinition.py b/gw_eccentricity/eccDefinition.py index e072f1a0..7f6eebc7 100644 --- a/gw_eccentricity/eccDefinition.py +++ b/gw_eccentricity/eccDefinition.py @@ -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 @@ -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: @@ -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. """ # Get data necessary for eccentricity measurement self.dataDict, self.t_merger, self.amp22_merger, \ @@ -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(): @@ -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. + 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 # compute residual data if "amplm_zeroecc" in self.dataDict and "omegalm_zeroecc" in self.dataDict: @@ -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 @@ -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.""" 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: + # 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"] + 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) def check_if_dropped_too_many_extrema(self, original_extrema, new_extrema, extrema_type="extrema", @@ -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() @@ -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: + 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) @@ -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) @@ -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.""" + return_dict = {} + if tref_in is not None: + # check that tref_in is in the allowed range + ndim = np.ndim(tref_in) + 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. diff --git a/gw_eccentricity/exceptions.py b/gw_eccentricity/exceptions.py new file mode 100644 index 00000000..36d15a63 --- /dev/null +++ b/gw_eccentricity/exceptions.py @@ -0,0 +1,58 @@ +"""Custom exception classes for gw_eccentricity.""" + + +class InsufficientExtrema(Exception): + """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): + """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) diff --git a/gw_eccentricity/gw_eccentricity.py b/gw_eccentricity/gw_eccentricity.py index 5bd0b525..705fade6 100644 --- a/gw_eccentricity/gw_eccentricity.py +++ b/gw_eccentricity/gw_eccentricity.py @@ -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 diff --git a/gw_eccentricity/load_data.py b/gw_eccentricity/load_data.py index cfa737b2..e29c8c20 100644 --- a/gw_eccentricity/load_data.py +++ b/gw_eccentricity/load_data.py @@ -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]: + zero_ecc_kwargs["Momega0"] *= 0.5 + dataDict_zero_ecc = load_waveform(**zero_ecc_kwargs) t_zeroecc = dataDict_zero_ecc['t'] hlm_zeroecc = dataDict_zero_ecc['hlm'] dataDict.update({'t_zeroecc': t_zeroecc, diff --git a/test/test_set_failures_to_zero.py b/test/test_set_failures_to_zero.py new file mode 100644 index 00000000..d0b16af7 --- /dev/null +++ b/test/test_set_failures_to_zero.py @@ -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 + + 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"]: + 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) From c5b98c9a4583a724f6220c735d60432e36bd87d4 Mon Sep 17 00:00:00 2001 From: md-arif-shaikh Date: Tue, 12 Sep 2023 10:47:04 +0900 Subject: [PATCH 02/17] address comments --- gw_eccentricity/eccDefinition.py | 48 +++++++++++++++++++----------- gw_eccentricity/exceptions.py | 17 +++++++++-- gw_eccentricity/gw_eccentricity.py | 13 +++++--- gw_eccentricity/load_data.py | 3 ++ 4 files changed, 57 insertions(+), 24 deletions(-) diff --git a/gw_eccentricity/eccDefinition.py b/gw_eccentricity/eccDefinition.py index 7f6eebc7..0c3e615a 100644 --- a/gw_eccentricity/eccDefinition.py +++ b/gw_eccentricity/eccDefinition.py @@ -17,7 +17,7 @@ from .utils import debug_message from .plot_settings import use_fancy_plotsettings, colorsDict, labelsDict from .plot_settings import figWidthsTwoColDict, figHeightsDict -from .exceptions import InsufficientExtrema, NotInRange +from .exceptions import InsufficientExtrema, NotInAllowedInputRange class eccDefinition: @@ -240,10 +240,14 @@ def __init__(self, dataDict, num_orbits_to_exclude_before_merger=2, 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. + The code normally raises an exception if sufficient number of + extrema are not found. This can happen for various reasons + including when the eccentricity is too small for some methods + (like the Amplitude method) to measure. See e.g. Fig.4 of + arxiv.2302.11257. If `set_failures_to_zero` is set to True, we + assume that small eccentricity is the cause, and set the + returned eccentricity and mean anomaly to zero when sufficient + extrema are not found. USE THIS WITH CAUTION! """ # Get data necessary for eccentricity measurement self.dataDict, self.t_merger, self.amp22_merger, \ @@ -317,10 +321,6 @@ def __init__(self, dataDict, num_orbits_to_exclude_before_merger=2, # assuming that a phase change 4pi occurs over an orbit. 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 - # compute residual data if "amplm_zeroecc" in self.dataDict and "omegalm_zeroecc" in self.dataDict: self.compute_res_amp22_and_res_omega22() @@ -1082,8 +1082,10 @@ def check_num_extrema(self, extrema, extrema_type="extrema"): # 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: + probably_quasicircular = True + else: + probably_quasicircular = False + if probably_quasicircular and self.set_failures_to_zero: debug_message( "The waveform has approximately " f"{self.approximate_num_orbits:.2f}" @@ -1104,8 +1106,9 @@ def check_num_extrema(self, extrema, extrema_type="extrema"): f"{recommended_methods}.") else: method_message = "" - raise InsufficientExtrema(extrema_type, num_extrema, - method_message) + raise InsufficientExtrema(extrema_type, num_extrema, + method_message) + return probably_quasicircular def check_if_dropped_too_many_extrema(self, original_extrema, new_extrema, extrema_type="extrema", @@ -1267,7 +1270,13 @@ def measure_ecc(self, tref_in=None, fref_in=None): # Get the pericenters and apocenters pericenters = self.find_extrema("pericenters") original_pericenters = pericenters.copy() - self.check_num_extrema(pericenters, "pericenters") + # Check if there are sufficient number of extrema. In case the waveform + # is long enough but do not have any extrema detected, it might be that + # the eccentricity is too small for the current method to detect + # it. See Fig.4 in arxiv.2302.11257. In such case we assume that the + # waveform is probably quasicircular. + probably_quasicircular_pericenter = self.check_num_extrema( + pericenters, "pericenters") # In some cases it is easier to find the pericenters than finding the # apocenters. For such cases, one can only find the pericenters and use # the mid points between two consecutive pericenters as the location of @@ -1278,7 +1287,8 @@ def measure_ecc(self, tref_in=None, fref_in=None): else: apocenters = self.find_extrema("apocenters") original_apocenters = apocenters.copy() - self.check_num_extrema(apocenters, "apocenters") + probably_quasicircular_apocenter = 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 @@ -1286,7 +1296,9 @@ def measure_ecc(self, tref_in=None, fref_in=None): # 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: + if any([probably_quasicircular_pericenter, + probably_quasicircular_apocenter]) \ + and self.set_failures_to_zero: return self.set_eccentricity_and_mean_anomaly_to_zero( tref_in, fref_in) @@ -1424,7 +1436,7 @@ def set_eccentricity_and_mean_anomaly_to_zero( ndim = np.ndim(tref_in) 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)) + raise NotInAllowedInputRange("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( @@ -1436,7 +1448,7 @@ def set_eccentricity_and_mean_anomaly_to_zero( 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) + raise NotInAllowedInputRange("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}) diff --git a/gw_eccentricity/exceptions.py b/gw_eccentricity/exceptions.py index 36d15a63..c24f451b 100644 --- a/gw_eccentricity/exceptions.py +++ b/gw_eccentricity/exceptions.py @@ -4,6 +4,14 @@ class InsufficientExtrema(Exception): """Exception raised when number of extrema is not enough. + While measuring eccentricity, one common failure that may occur is due to + insufficient number of extrema. Applying gw_eccentricity on a large number + of waveforms, for example, when reconstructing PE posterior by measuring + the eccentricity at the samples, one may need to loop over all the + samples. In such cases, one may want to avoid failures that are due to + insufficient extrema. Having a specific exception class helps in such + scenario instead of using generic exceptions. + Parameters ---------- extrema_type : str @@ -28,9 +36,14 @@ def __init__(self, extrema_type, num_extrema, additional_message=None): super().__init__(self.message) -class NotInRange(Exception): +class NotInAllowedInputRange(Exception): """Exception raised when the reference point is outside allowed range. + Due to the nature of the eccentricity definition, one can measure the + eccentricity only in an allowed range of time/frequency. If the failure + during eccentricity measurement is due to an input time/frequency that lies + outside the allowed range this exception helps in identifying that. + Parameters ---------- reference_point_type : str @@ -46,7 +59,7 @@ class NotInRange(Exception): def __init__(self, reference_point_type, lower, upper, additional_message=None): - """Init for NotInRange Class.""" + """Init for NotInAllowedRange Class.""" self.reference_point_type = reference_point_type self.lower = lower self.upper = upper diff --git a/gw_eccentricity/gw_eccentricity.py b/gw_eccentricity/gw_eccentricity.py index 705fade6..0a2110d0 100644 --- a/gw_eccentricity/gw_eccentricity.py +++ b/gw_eccentricity/gw_eccentricity.py @@ -360,10 +360,15 @@ 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. + set_failures_to_zero : bool, default=False + The code normally raises an exception if sufficient number of + extrema are not found. This can happen for various reasons + including when the eccentricity is too small for some methods (like + the Amplitude method) to measure. See e.g. Fig.4 of + arxiv.2302.11257. If `set_failures_to_zero` is set to True, we + assume that small eccentricity is the cause, and set the returned + eccentricity and mean anomaly to zero when sufficient extrema are + not found. USE THIS WITH CAUTION! Returns ------- diff --git a/gw_eccentricity/load_data.py b/gw_eccentricity/load_data.py index e29c8c20..cece5fc6 100644 --- a/gw_eccentricity/load_data.py +++ b/gw_eccentricity/load_data.py @@ -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) + # To make sure that we can compute the residual amplitude/frequency, we + # need the zeroecc data to be longer than the ecc data so that we can + # intepolate the zeroecc data on the same times as the ecc data. while dataDict_zero_ecc['t'][0] > dataDict["t"][0]: zero_ecc_kwargs["Momega0"] *= 0.5 dataDict_zero_ecc = load_waveform(**zero_ecc_kwargs) From 7ae0c32b5e0c78ec8e67f2c317223ff765d90c6a Mon Sep 17 00:00:00 2001 From: md-arif-shaikh Date: Sun, 8 Oct 2023 01:40:44 +0900 Subject: [PATCH 03/17] improve func to set ecc and mean ano to zero --- gw_eccentricity/eccDefinition.py | 172 +++++++++++++++++++------------ gw_eccentricity/exceptions.py | 3 +- 2 files changed, 107 insertions(+), 68 deletions(-) diff --git a/gw_eccentricity/eccDefinition.py b/gw_eccentricity/eccDefinition.py index 0c3e615a..a00ceb3c 100644 --- a/gw_eccentricity/eccDefinition.py +++ b/gw_eccentricity/eccDefinition.py @@ -1261,12 +1261,12 @@ def measure_ecc(self, tref_in=None, fref_in=None): 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_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) + self.fref_in_ndim = np.ndim(fref_in) + self.tref_in_ndim = self.fref_in_ndim + self.fref_in = np.atleast_1d(fref_in) # Get the pericenters and apocenters pericenters = self.find_extrema("pericenters") original_pericenters = pericenters.copy() @@ -1275,7 +1275,7 @@ def measure_ecc(self, tref_in=None, fref_in=None): # the eccentricity is too small for the current method to detect # it. See Fig.4 in arxiv.2302.11257. In such case we assume that the # waveform is probably quasicircular. - probably_quasicircular_pericenter = self.check_num_extrema( + self.probably_quasicircular_pericenter = self.check_num_extrema( pericenters, "pericenters") # In some cases it is easier to find the pericenters than finding the # apocenters. For such cases, one can only find the pericenters and use @@ -1287,20 +1287,20 @@ def measure_ecc(self, tref_in=None, fref_in=None): else: apocenters = self.find_extrema("apocenters") original_apocenters = apocenters.copy() - probably_quasicircular_apocenter = self.check_num_extrema( + self.probably_quasicircular_apocenter = 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 any([probably_quasicircular_pericenter, - probably_quasicircular_apocenter]) \ + # 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 any([self.probably_quasicircular_pericenter, + self.probably_quasicircular_apocenter]) \ and self.set_failures_to_zero: return self.set_eccentricity_and_mean_anomaly_to_zero( - tref_in, fref_in) + tref_in) # Choose good extrema self.pericenters_location, self.apocenters_location \ @@ -1336,7 +1336,7 @@ def measure_ecc(self, tref_in=None, fref_in=None): 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) + = self.compute_tref_in_and_fref_out_from_fref_in(self.fref_in) # We measure eccentricity and mean anomaly from tmin to tmax. self.tref_out = self.tref_in[ np.logical_and(self.tref_in <= self.tmax, @@ -1357,18 +1357,7 @@ def measure_ecc(self, tref_in=None, fref_in=None): # Check if tref_out is reasonable if len(self.tref_out) == 0: - if self.tref_in[-1] > self.tmax: - raise Exception( - f"tref_in {self.tref_in} is later than tmax=" - f"{self.tmax}, " - "which corresponds to min(last pericenter " - "time, last apocenter time).") - if self.tref_in[0] < self.tmin: - raise Exception( - f"tref_in {self.tref_in} is earlier than tmin=" - f"{self.tmin}, " - "which corresponds to max(first pericenter " - "time, first apocenter time).") + self.check_input_limits(self.tref_in, self.tmin, self.tmax, "time") raise Exception( "tref_out is empty. This can happen if the " "waveform has insufficient identifiable " @@ -1406,12 +1395,12 @@ def measure_ecc(self, tref_in=None, fref_in=None): self.check_monotonicity_and_convexity() # If tref_in is a scalar, return a scalar - if tref_in_ndim == 0: + if self.tref_in_ndim == 0: self.mean_anomaly = self.mean_anomaly[0] self.eccentricity = self.eccentricity[0] self.tref_out = self.tref_out[0] - if fref_in is not None and fref_in_ndim == 0: + if fref_in is not None and self.fref_in_ndim == 0: self.fref_out = self.fref_out[0] if self.debug_plots: @@ -1427,38 +1416,53 @@ 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): + def set_eccentricity_and_mean_anomaly_to_zero(self, tref_in): """Set eccentricity and mean_anomaly to zero.""" - return_dict = {} if tref_in is not None: - # check that tref_in is in the allowed range - ndim = np.ndim(tref_in) - tref_in = np.atleast_1d(tref_in) - if min(tref_in) < min(self.t) or max(tref_in) > max(self.t): - raise NotInAllowedInputRange("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}) + # This function sets eccentricity and mean anomaly to zero + # when a method fails to detect any apoceneters or pericenrers, + # and therefore in such cases, we can set the tref_out to be + # the times that falls within the range of self.t. + ref_arr = self.tref_in[ + np.logical_and(self.tref_in >= min(self.t), + self.tref_in <= max(self.t))] + if len(ref_arr) == 0: + # check that tref_in is in the allowed range + self.check_input_limits( + self.tref_in, min(self.t), max(self.t), "time") + # To match the type of tref_in + ref_arr = ref_arr[0] if self.tref_in_ndim == 0 else ref_arr + # Finally make tref_out available to self + self.tref_out = ref_arr else: - # check that fref_in is in the allowed range - ndim = np.ndim(fref_in) - fref_in = np.atleast_1d(fref_in) + # Since we don't have the maximum and minimum allowed reference + # frequencies computed from the frequencies at the pericenetrs and + # apoceneters, we simply set the maximum and minimum value to be + # the maximum and minimum of instantaneous f22, respectively. 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 NotInAllowedInputRange("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({ + ref_arr = self.fref_in[ + np.logical_and(self.fref_in >= f22_min, + self.fref_in <= f22_max)] + # check that fref_in is in the allowed range + if len(ref_arr) == 0: + self.check_input_limits( + self.fref_in, f22_min, f22_max, "frequency") + # To match the type of the fref_in. + ref_arr = ref_arr[0] if self.fref_in_ndim == 0 else ref_arr + self.fref_out = ref_arr + # At the top of measure_ecc we set tref_in_ndim and fref_in_ndim, and + # even in case of fref_in, we set tref_in_ndim to the same as + # fref_in_ndim, therefore, below we can just use tref_in_ndim. + self.eccentricity \ + = 0 if self.tref_in_ndim == 0 else np.zeros(len(ref_arr)) + self.mean_anomaly \ + = 0 if self.tref_in_ndim == 0 else np.zeros(len(ref_arr)) + return { + "tref_out" if tref_in is not None else "fref_out": ref_arr, "eccentricity": self.eccentricity, "mean_anomaly": self.mean_anomaly - }) - return return_dict + } def et_from_ew22_0pn(self, ew22): """Get temporal eccentricity at Newtonian order. @@ -1496,7 +1500,7 @@ def compute_eccentricity(self, t): Eccentricity at t. """ # Check that t is within tmin and tmax to avoid extrapolation - self.check_time_limits(t) + self.check_input_limits(t, self.tmin, self.tmax, "time") omega22_pericenter_at_t = self.omega22_pericenters_interp(t) omega22_apocenter_at_t = self.omega22_apocenters_interp(t) @@ -1523,7 +1527,7 @@ def derivative_of_eccentricity(self, t, n=1): nth order time derivative of eccentricity. """ # Check that t is within tmin and tmax to avoid extrapolation - self.check_time_limits(t) + self.check_input_limits(t, self.tmin, self.tmax, "time") if self.ecc_for_checks is None: self.ecc_for_checks = self.compute_eccentricity( @@ -1562,7 +1566,7 @@ def compute_mean_anomaly(self, t): Mean anomaly at t. """ # Check that t is within tmin and tmax to avoid extrapolation - self.check_time_limits(t) + self.check_input_limits(t, self.tmin, self.tmax, "time") # Get the mean anomaly at the pericenters mean_ano_pericenters = np.arange(len(self.t_pericenters)) * 2 * np.pi @@ -1572,21 +1576,55 @@ def compute_mean_anomaly(self, t): # Modulo 2pi to make the mean anomaly vary between 0 and 2pi return mean_ano % (2 * np.pi) - def check_time_limits(self, t): - """Check that time t is within tmin and tmax. + def check_input_limits(self, input_vals, min_allowed_val, max_allowed_val, + input_type): + """Check that the input time/frequency is within allowed range. + + To avoid any extrapolation, check that the times or frequencies are + always greater than or equal to the minimum allowed value and always + less than the maximum allowed value. + + Parameters + ---------- + input_vals: float or array-like + Input times or frequencies where eccentricity/mean anomaly are to + be measured. + + min_allowed_val: float + Minimum allowed time or frequency where eccentricity/mean anomaly + can be measured. + + max_allowed_val: float + Maximum allowed time or frequency where eccentricity/mean anomaly + can be measured. - To avoid any extrapolation, check that the times t are - always greater than or equal to tmin and always less than tmax. + input_type: str + Description of the input. Can be tref_in or fref_in """ - t = np.atleast_1d(t) - if any(t > self.tmax): - raise Exception(f"Found times later than tmax={self.tmax}, " - "which corresponds to min(last pericenter " + if input_type not in ["time", "frequency"]: + raise ValueError("Input type must be `time` or `frequency`.") + input_vals = np.atleast_1d(input_vals) + add_extra_info = (input_type == "time" and + not any([self.probably_quasicircular_apocenter, + self.probably_quasicircular_pericenter])) + if any(input_vals > max_allowed_val): + message = (f"Found reference {input_type} later than maximum " + f"allowed {input_type}={max_allowed_val}") + if add_extra_info: + message += (" which corresponds to min(last pericenter " "time, last apocenter time).") - if any(t < self.tmin): - raise Exception(f"Found times earlier than tmin= {self.tmin}, " - "which corresponds to max(first pericenter " + raise NotInAllowedInputRange( + "Reference " + input_type, min_allowed_val, max_allowed_val, + message) + if any(input_vals < min_allowed_val): + message = (f"Found reference {input_type} earlier than minimum " + f"allowed {input_type}={min_allowed_val}") + if add_extra_info: + message += (" which corresponds to max(first pericenter " "time, first apocenter time).") + raise NotInAllowedInputRange( + "Reference " + input_type, min_allowed_val, max_allowed_val, + message) def check_extrema_separation(self, extrema_location, extrema_type="extrema", diff --git a/gw_eccentricity/exceptions.py b/gw_eccentricity/exceptions.py index c24f451b..536e1095 100644 --- a/gw_eccentricity/exceptions.py +++ b/gw_eccentricity/exceptions.py @@ -64,7 +64,8 @@ def __init__(self, reference_point_type, lower, upper, self.lower = lower self.upper = upper self.additional_message = additional_message - self.message = (f"{self.reference_point_type} is outside the allowed " + 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 From 597c120db2d93e1a972ededcd488d19e316d7688 Mon Sep 17 00:00:00 2001 From: md-arif-shaikh Date: Sun, 8 Oct 2023 11:29:13 +0900 Subject: [PATCH 04/17] fix import error in readthedocs --- docs/source/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 1497dcf8..667ee36c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -4,7 +4,6 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- -import sphinx_rtd_theme import pathlib import sys import os From 8f382ae46896e53f8c29098780b2979da8e8792d Mon Sep 17 00:00:00 2001 From: md-arif-shaikh Date: Sun, 8 Oct 2023 11:36:54 +0900 Subject: [PATCH 05/17] add sphinx_rtd_theme in docs/requirements --- docs/requirements.txt | 2 +- docs/source/conf.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index cb5b4953..aad45e84 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ +sphinx-rtd-theme=1.3.0 numpydoc==1.5.0 nbsphinx==0.9.1 sphinx-tabs==3.4.1 @@ -6,5 +7,4 @@ scipy==1.10.1 matplotlib==3.7.1 lalsuite==7.15 h5py==3.8.0 -# furo==2023.3.27 myst-parser==1.0.0 diff --git a/docs/source/conf.py b/docs/source/conf.py index 667ee36c..1497dcf8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -4,6 +4,7 @@ # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Path setup -------------------------------------------------------------- +import sphinx_rtd_theme import pathlib import sys import os From 665aa8e7644ab8dc742ecc43c8ebebade9033a22 Mon Sep 17 00:00:00 2001 From: md-arif-shaikh Date: Sun, 8 Oct 2023 11:47:40 +0900 Subject: [PATCH 06/17] fix typo in docs/requirements --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index aad45e84..13af0cab 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,4 @@ -sphinx-rtd-theme=1.3.0 +sphinx-rtd-theme==1.3.0 numpydoc==1.5.0 nbsphinx==0.9.1 sphinx-tabs==3.4.1 From bf3d9c52a80b240acfb84784286e9d6aeed419e4 Mon Sep 17 00:00:00 2001 From: md-arif-shaikh Date: Sun, 8 Oct 2023 14:46:47 +0900 Subject: [PATCH 07/17] prepare return dict in a separate function --- gw_eccentricity/eccDefinition.py | 144 +++++++++++++++---------------- 1 file changed, 70 insertions(+), 74 deletions(-) diff --git a/gw_eccentricity/eccDefinition.py b/gw_eccentricity/eccDefinition.py index a00ceb3c..4264931b 100644 --- a/gw_eccentricity/eccDefinition.py +++ b/gw_eccentricity/eccDefinition.py @@ -1261,11 +1261,12 @@ def measure_ecc(self, tref_in=None, fref_in=None): raise KeyError("Exactly one of tref_in and fref_in" " should be specified.") elif tref_in is not None: - self.tref_in_ndim = np.ndim(tref_in) + self.domain = "time" + self.ref_ndim = np.ndim(tref_in) self.tref_in = np.atleast_1d(tref_in) else: - self.fref_in_ndim = np.ndim(fref_in) - self.tref_in_ndim = self.fref_in_ndim + self.domain = "frequency" + self.ref_ndim = np.ndim(fref_in) self.fref_in = np.atleast_1d(fref_in) # Get the pericenters and apocenters pericenters = self.find_extrema("pericenters") @@ -1299,8 +1300,7 @@ def measure_ecc(self, tref_in=None, fref_in=None): if any([self.probably_quasicircular_pericenter, self.probably_quasicircular_apocenter]) \ and self.set_failures_to_zero: - return self.set_eccentricity_and_mean_anomaly_to_zero( - tref_in) + return self.set_eccentricity_and_mean_anomaly_to_zero() # Choose good extrema self.pericenters_location, self.apocenters_location \ @@ -1357,7 +1357,7 @@ def measure_ecc(self, tref_in=None, fref_in=None): # Check if tref_out is reasonable if len(self.tref_out) == 0: - self.check_input_limits(self.tref_in, self.tmin, self.tmax, "time") + self.check_input_limits(self.tref_in, self.tmin, self.tmax) raise Exception( "tref_out is empty. This can happen if the " "waveform has insufficient identifiable " @@ -1394,46 +1394,30 @@ def measure_ecc(self, tref_in=None, fref_in=None): # check if eccentricity is monotonic and convex self.check_monotonicity_and_convexity() - # If tref_in is a scalar, return a scalar - if self.tref_in_ndim == 0: - self.mean_anomaly = self.mean_anomaly[0] - self.eccentricity = self.eccentricity[0] - self.tref_out = self.tref_out[0] - - if fref_in is not None and self.fref_in_ndim == 0: - self.fref_out = self.fref_out[0] - if self.debug_plots: # make a plot for diagnostics fig, axes = self.make_diagnostic_plots() self.save_debug_fig(fig, f"gwecc_{self.method}_diagnostics.pdf") plt.close(fig) - return_dict = {"eccentricity": self.eccentricity, - "mean_anomaly": self.mean_anomaly} - if fref_in is not None: - return_dict.update({"fref_out": self.fref_out}) - else: - return_dict.update({"tref_out": self.tref_out}) - return return_dict + # return measured eccentricity, mean anomaly and reference time or + # frequency where these are measured. + return self.make_return_dict_for_eccentricity_and_mean_anomaly() - def set_eccentricity_and_mean_anomaly_to_zero(self, tref_in): + def set_eccentricity_and_mean_anomaly_to_zero(self): """Set eccentricity and mean_anomaly to zero.""" - if tref_in is not None: + if self.domain == "time": # This function sets eccentricity and mean anomaly to zero # when a method fails to detect any apoceneters or pericenrers, # and therefore in such cases, we can set the tref_out to be # the times that falls within the range of self.t. - ref_arr = self.tref_in[ + self.tref_out = self.tref_in[ np.logical_and(self.tref_in >= min(self.t), self.tref_in <= max(self.t))] - if len(ref_arr) == 0: + out_len = len(self.tref_out) + if out_len == 0: # check that tref_in is in the allowed range self.check_input_limits( - self.tref_in, min(self.t), max(self.t), "time") - # To match the type of tref_in - ref_arr = ref_arr[0] if self.tref_in_ndim == 0 else ref_arr - # Finally make tref_out available to self - self.tref_out = ref_arr + self.tref_in, min(self.t), max(self.t)) else: # Since we don't have the maximum and minimum allowed reference # frequencies computed from the frequencies at the pericenetrs and @@ -1441,28 +1425,52 @@ def set_eccentricity_and_mean_anomaly_to_zero(self, tref_in): # the maximum and minimum of instantaneous f22, respectively. f22_min = min(self.omega22) / (2 * np.pi) f22_max = max(self.omega22) / (2 * np.pi) - ref_arr = self.fref_in[ + self.fref_out = self.fref_in[ np.logical_and(self.fref_in >= f22_min, self.fref_in <= f22_max)] - # check that fref_in is in the allowed range - if len(ref_arr) == 0: + out_len = len(self.fref_out) + if out_len == 0: + # check that fref_in is in the allowed range self.check_input_limits( - self.fref_in, f22_min, f22_max, "frequency") - # To match the type of the fref_in. - ref_arr = ref_arr[0] if self.fref_in_ndim == 0 else ref_arr - self.fref_out = ref_arr - # At the top of measure_ecc we set tref_in_ndim and fref_in_ndim, and - # even in case of fref_in, we set tref_in_ndim to the same as - # fref_in_ndim, therefore, below we can just use tref_in_ndim. - self.eccentricity \ - = 0 if self.tref_in_ndim == 0 else np.zeros(len(ref_arr)) - self.mean_anomaly \ - = 0 if self.tref_in_ndim == 0 else np.zeros(len(ref_arr)) - return { - "tref_out" if tref_in is not None else "fref_out": ref_arr, + self.fref_in, f22_min, f22_max) + self.eccentricity = np.zeros(out_len) + self.mean_anomaly = np.zeros(out_len) + return self.make_return_dict_for_eccentricity_and_mean_anomaly() + + def make_return_dict_for_eccentricity_and_mean_anomaly(self): + """Prepare a dictionary with reference time/freq, ecc and mean ano. + + In this function, we prepare a dictionary containing the measured + eccentricity, mean anomaly and the reference time or frequency where + these are measured at. + + We also make sure that if the input reference time/frequency is scalar + then the returned eccentricity and mean anomaly is also a scalar. To do + this, we use the information about the tref_in/fref_in that is provided + by the user. At the top of measure_ecc we set ref_ndim to identify + whether the original input was scalar or array-like and use that here. + """ + if self.ref_ndim == 0: + self.eccentricity = self.eccentricity[0] + self.mean_anomaly = self.mean_anomaly[0] + if self.domain == "time": + self.tref_out = self.tref_out[0] + else: + self.fref_out = self.fref_out[0] + + return_dict = { "eccentricity": self.eccentricity, "mean_anomaly": self.mean_anomaly } + if self.domain == "time": + return_dict.update({ + "tref_out": self.tref_out + }) + else: + return_dict.update({ + "fref_out": self.fref_out + }) + return return_dict def et_from_ew22_0pn(self, ew22): """Get temporal eccentricity at Newtonian order. @@ -1500,7 +1508,7 @@ def compute_eccentricity(self, t): Eccentricity at t. """ # Check that t is within tmin and tmax to avoid extrapolation - self.check_input_limits(t, self.tmin, self.tmax, "time") + self.check_input_limits(t, self.tmin, self.tmax) omega22_pericenter_at_t = self.omega22_pericenters_interp(t) omega22_apocenter_at_t = self.omega22_apocenters_interp(t) @@ -1527,7 +1535,7 @@ def derivative_of_eccentricity(self, t, n=1): nth order time derivative of eccentricity. """ # Check that t is within tmin and tmax to avoid extrapolation - self.check_input_limits(t, self.tmin, self.tmax, "time") + self.check_input_limits(t, self.tmin, self.tmax) if self.ecc_for_checks is None: self.ecc_for_checks = self.compute_eccentricity( @@ -1566,7 +1574,7 @@ def compute_mean_anomaly(self, t): Mean anomaly at t. """ # Check that t is within tmin and tmax to avoid extrapolation - self.check_input_limits(t, self.tmin, self.tmax, "time") + self.check_input_limits(t, self.tmin, self.tmax) # Get the mean anomaly at the pericenters mean_ano_pericenters = np.arange(len(self.t_pericenters)) * 2 * np.pi @@ -1576,8 +1584,7 @@ def compute_mean_anomaly(self, t): # Modulo 2pi to make the mean anomaly vary between 0 and 2pi return mean_ano % (2 * np.pi) - def check_input_limits(self, input_vals, min_allowed_val, max_allowed_val, - input_type): + def check_input_limits(self, input_vals, min_allowed_val, max_allowed_val): """Check that the input time/frequency is within allowed range. To avoid any extrapolation, check that the times or frequencies are @@ -1597,33 +1604,28 @@ def check_input_limits(self, input_vals, min_allowed_val, max_allowed_val, max_allowed_val: float Maximum allowed time or frequency where eccentricity/mean anomaly can be measured. - - input_type: str - Description of the input. Can be tref_in or fref_in """ - if input_type not in ["time", "frequency"]: - raise ValueError("Input type must be `time` or `frequency`.") input_vals = np.atleast_1d(input_vals) - add_extra_info = (input_type == "time" and + add_extra_info = (self.domain == "time" and not any([self.probably_quasicircular_apocenter, self.probably_quasicircular_pericenter])) if any(input_vals > max_allowed_val): - message = (f"Found reference {input_type} later than maximum " - f"allowed {input_type}={max_allowed_val}") + message = (f"Found reference {self.domain} later than maximum " + f"allowed {self.domain}={max_allowed_val}") if add_extra_info: message += (" which corresponds to min(last pericenter " "time, last apocenter time).") raise NotInAllowedInputRange( - "Reference " + input_type, min_allowed_val, max_allowed_val, + f"Reference {self.domain}", min_allowed_val, max_allowed_val, message) if any(input_vals < min_allowed_val): - message = (f"Found reference {input_type} earlier than minimum " - f"allowed {input_type}={min_allowed_val}") + message = (f"Found reference {self.domain} earlier than minimum " + f"allowed {self.domain}={min_allowed_val}") if add_extra_info: message += (" which corresponds to max(first pericenter " "time, first apocenter time).") raise NotInAllowedInputRange( - "Reference " + input_type, min_allowed_val, max_allowed_val, + f"Reference {self.domain}", min_allowed_val, max_allowed_val, message) def check_extrema_separation(self, extrema_location, @@ -2318,16 +2320,10 @@ def get_fref_out(self, fref_in, method): np.logical_and(fref_in >= fref_min, fref_in < fref_max)] if len(fref_out) == 0: - if fref_in[0] < fref_min: - raise Exception("fref_in is earlier than minimum available " - f"frequency {fref_min}") - if fref_in[-1] > fref_max: - raise Exception("fref_in is later than maximum available " - f"frequency {fref_max}") - else: - raise Exception("fref_out is empty. This can happen if the " - "waveform has insufficient identifiable " - "pericenters/apocenters.") + self.check_input_limits(fref_in, fref_min, fref_max) + raise Exception("fref_out is empty. This can happen if the " + "waveform has insufficient identifiable " + "pericenters/apocenters.") return fref_out def make_diagnostic_plots( From 0a8c96c088b3d43d8dc61f794952cf1632152f91 Mon Sep 17 00:00:00 2001 From: md-arif-shaikh Date: Mon, 9 Oct 2023 12:21:47 +0900 Subject: [PATCH 08/17] polishing docs --- gw_eccentricity/eccDefinition.py | 85 ++++++++++++++++++-------------- gw_eccentricity/exceptions.py | 30 +++++------ 2 files changed, 62 insertions(+), 53 deletions(-) diff --git a/gw_eccentricity/eccDefinition.py b/gw_eccentricity/eccDefinition.py index 4264931b..0b6f3490 100644 --- a/gw_eccentricity/eccDefinition.py +++ b/gw_eccentricity/eccDefinition.py @@ -1079,9 +1079,12 @@ def check_num_extrema(self, extrema, extrema_type="extrema"): if num_extrema < 2: # check if the waveform is sufficiently long if self.approximate_num_orbits > 5: - # 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. + # The waveform is sufficiently long but the extrema finding + # method fails to find enough number of extrema. This may + # happen if the eccentricity is too small and, therefore, the + # modulations in the amplitude/frequency is too small for the + # method to detect them. In such cases, the waveform can be + # assumed to be quasicircular. probably_quasicircular = True else: probably_quasicircular = False @@ -1100,10 +1103,11 @@ def check_num_extrema(self, extrema, extrema_type="extrema"): 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}.") + "It's possible that the eccentricity is too small for " + f"the {self.method} method to detect the " + f"{extrema_type}. Try one of {recommended_methods} " + "which should work even for a very small eccentricity." + ) else: method_message = "" raise InsufficientExtrema(extrema_type, num_extrema, @@ -1261,7 +1265,9 @@ def measure_ecc(self, tref_in=None, fref_in=None): raise KeyError("Exactly one of tref_in and fref_in" " should be specified.") elif tref_in is not None: + # Identify whether the reference point is in time or frequency self.domain = "time" + # Identify whether the reference point is scalar or array-like self.ref_ndim = np.ndim(tref_in) self.tref_in = np.atleast_1d(tref_in) else: @@ -1271,11 +1277,11 @@ def measure_ecc(self, tref_in=None, fref_in=None): # Get the pericenters and apocenters pericenters = self.find_extrema("pericenters") original_pericenters = pericenters.copy() - # Check if there are sufficient number of extrema. In case the waveform - # is long enough but do not have any extrema detected, it might be that - # the eccentricity is too small for the current method to detect - # it. See Fig.4 in arxiv.2302.11257. In such case we assume that the - # waveform is probably quasicircular. + # Check if there are a sufficient number of extrema. In cases where the + # waveform is long enough but the method fails to detect any extrema, + # it might be that the eccentricity is too small for the current method + # to detect it. See Fig.4 in arxiv.2302.11257. In such cases, we assume + # that the waveform is probably quasicircular. self.probably_quasicircular_pericenter = self.check_num_extrema( pericenters, "pericenters") # In some cases it is easier to find the pericenters than finding the @@ -1291,11 +1297,11 @@ def measure_ecc(self, tref_in=None, fref_in=None): self.probably_quasicircular_apocenter = 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 + # 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 them. In this case, + # the rest of the code in this function is not executed, and therefore, + # many variables used in diagnostic tests are never computed, making # diagnostics irrelevant. if any([self.probably_quasicircular_pericenter, self.probably_quasicircular_apocenter]) \ @@ -1406,10 +1412,10 @@ def measure_ecc(self, tref_in=None, fref_in=None): def set_eccentricity_and_mean_anomaly_to_zero(self): """Set eccentricity and mean_anomaly to zero.""" if self.domain == "time": - # This function sets eccentricity and mean anomaly to zero - # when a method fails to detect any apoceneters or pericenrers, - # and therefore in such cases, we can set the tref_out to be - # the times that falls within the range of self.t. + # This function sets eccentricity and mean anomaly to zero when a + # method fails to detect any extrema, and therefore, in such cases, + # we can set tref_out to be the times that fall within the range of + # self.t. self.tref_out = self.tref_in[ np.logical_and(self.tref_in >= min(self.t), self.tref_in <= max(self.t))] @@ -1420,8 +1426,8 @@ def set_eccentricity_and_mean_anomaly_to_zero(self): self.tref_in, min(self.t), max(self.t)) else: # Since we don't have the maximum and minimum allowed reference - # frequencies computed from the frequencies at the pericenetrs and - # apoceneters, we simply set the maximum and minimum value to be + # frequencies computed from the frequencies at the pericenters and + # apocenters, we simply set the maximum and minimum values to be # the maximum and minimum of instantaneous f22, respectively. f22_min = min(self.omega22) / (2 * np.pi) f22_max = max(self.omega22) / (2 * np.pi) @@ -1438,18 +1444,21 @@ def set_eccentricity_and_mean_anomaly_to_zero(self): return self.make_return_dict_for_eccentricity_and_mean_anomaly() def make_return_dict_for_eccentricity_and_mean_anomaly(self): - """Prepare a dictionary with reference time/freq, ecc and mean ano. + """Prepare a dictionary with reference time/freq, ecc, and mean anomaly. In this function, we prepare a dictionary containing the measured - eccentricity, mean anomaly and the reference time or frequency where - these are measured at. - - We also make sure that if the input reference time/frequency is scalar - then the returned eccentricity and mean anomaly is also a scalar. To do - this, we use the information about the tref_in/fref_in that is provided - by the user. At the top of measure_ecc we set ref_ndim to identify - whether the original input was scalar or array-like and use that here. + eccentricity, mean anomaly, and the reference time or frequency where + these are measured. + + We also make sure that if the input reference time/frequency is scalar, + then the returned eccentricity and mean anomaly are also scalars. To do + this, we use the information about tref_in/fref_in that is provided by + the user. At the top of the measure_ecc function, we set ref_ndim to + identify whether the original input was scalar or array-like and use + that here. """ + # If the original input was scalar, convert the measured eccentricity, + # mean anomaly, etc., to scalar. if self.ref_ndim == 0: self.eccentricity = self.eccentricity[0] self.mean_anomaly = self.mean_anomaly[0] @@ -1462,14 +1471,14 @@ def make_return_dict_for_eccentricity_and_mean_anomaly(self): "eccentricity": self.eccentricity, "mean_anomaly": self.mean_anomaly } + # Return either tref_out or fref_out, depending on whether the input + # reference point was in time or frequency, respectively. if self.domain == "time": return_dict.update({ - "tref_out": self.tref_out - }) + "tref_out": self.tref_out}) else: return_dict.update({ - "fref_out": self.fref_out - }) + "fref_out": self.fref_out}) return return_dict def et_from_ew22_0pn(self, ew22): @@ -1585,7 +1594,7 @@ def compute_mean_anomaly(self, t): return mean_ano % (2 * np.pi) def check_input_limits(self, input_vals, min_allowed_val, max_allowed_val): - """Check that the input time/frequency is within allowed range. + """Check that the input time/frequency is within the allowed range. To avoid any extrapolation, check that the times or frequencies are always greater than or equal to the minimum allowed value and always @@ -1594,7 +1603,7 @@ def check_input_limits(self, input_vals, min_allowed_val, max_allowed_val): Parameters ---------- input_vals: float or array-like - Input times or frequencies where eccentricity/mean anomaly are to + Input times or frequencies where eccentricity/mean anomaly is to be measured. min_allowed_val: float diff --git a/gw_eccentricity/exceptions.py b/gw_eccentricity/exceptions.py index 536e1095..f8b9abae 100644 --- a/gw_eccentricity/exceptions.py +++ b/gw_eccentricity/exceptions.py @@ -2,15 +2,15 @@ class InsufficientExtrema(Exception): - """Exception raised when number of extrema is not enough. + """Exception raised when the number of extrema is not enough. While measuring eccentricity, one common failure that may occur is due to - insufficient number of extrema. Applying gw_eccentricity on a large number - of waveforms, for example, when reconstructing PE posterior by measuring - the eccentricity at the samples, one may need to loop over all the - samples. In such cases, one may want to avoid failures that are due to - insufficient extrema. Having a specific exception class helps in such - scenario instead of using generic exceptions. + an insufficient number of extrema. Applying gw_eccentricity to a large + number of waveforms, for example, when reconstructing the PE posterior by + measuring eccentricity at the samples, one may need to loop over all the + samples. In such cases, one may want to avoid failures that are due to an + insufficient number of extrema. Having a specific exception class helps in + such scenarios instead of using a generic exception. Parameters ---------- @@ -20,7 +20,7 @@ class InsufficientExtrema(Exception): Number of extrema. additional_message : str Any additional message to append to the exception message. - Default is None which adds no additional message. + Default is None, which adds no additional message. """ def __init__(self, extrema_type, num_extrema, additional_message=None): @@ -37,24 +37,24 @@ def __init__(self, extrema_type, num_extrema, additional_message=None): class NotInAllowedInputRange(Exception): - """Exception raised when the reference point is outside allowed range. + """Exception raised when the reference point is outside the allowed range. Due to the nature of the eccentricity definition, one can measure the - eccentricity only in an allowed range of time/frequency. If the failure + eccentricity only within an allowed range of time/frequency. If the failure during eccentricity measurement is due to an input time/frequency that lies - outside the allowed range this exception helps in identifying that. + outside the allowed range, this exception helps in identifying that. Parameters ---------- reference_point_type : str - Type of reference point. Can be "tref_in" or "fref_in". + Type of reference point. Can be "time" or "frequency". lower : float - Minium allowed value, i. e., the lower boundary of allowed range. + Minimum allowed value, i.e., the lower boundary of the allowed range. upper : float - Maximum allowed value, i. e., the upper boundary of allowed range. + Maximum allowed value, i.e., the upper boundary of the allowed range. additional_message : str Any additional message to append to the exception message. - Default is None which adds no additional message. + Default is None, which adds no additional message. """ def __init__(self, reference_point_type, lower, upper, From c74403e03736fd8dd6205da3c2a9bf9e8081b787 Mon Sep 17 00:00:00 2001 From: md-arif-shaikh Date: Wed, 11 Oct 2023 15:51:23 +0900 Subject: [PATCH 09/17] improve docs and address suggestions --- gw_eccentricity/eccDefinition.py | 61 ++++++++++++++++++++++++-------- gw_eccentricity/load_data.py | 2 +- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/gw_eccentricity/eccDefinition.py b/gw_eccentricity/eccDefinition.py index 0b6f3490..6c8dd1a3 100644 --- a/gw_eccentricity/eccDefinition.py +++ b/gw_eccentricity/eccDefinition.py @@ -317,10 +317,6 @@ 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. - self.approximate_num_orbits = ((self.phase22[-1] - self.phase22[0]) - / (4 * np.pi)) # compute residual data if "amplm_zeroecc" in self.dataDict and "omegalm_zeroecc" in self.dataDict: self.compute_res_amp22_and_res_omega22() @@ -1074,24 +1070,61 @@ def interp_extrema(self, extrema_type="pericenters"): raise InsufficientExtrema(extrema_type, len(extrema)) def check_num_extrema(self, extrema, extrema_type="extrema"): - """Check number of extrema.""" + """Check number of extrema. + + Check the number of extrema to determine if there are enough for + building the interpolants through the pericenters and apocenters. In + cases where the number of extrema is insufficient, i.e., less than 2, + we further verify if the provided waveform is long enough to have a + sufficient number of extrema. + + If the waveform is long enough, but the chosen method fails to detect + any extrema, it is possible that the eccentricity is too small. If + `set_failures_to_zero` is set to True, then we set + `insufficient_extrema_but_long_waveform` to True and return it. + + Parameters + ---------- + extrema : array-like + 1d array of extrema to determine if the length is sufficient for + building interpolants of omega22 values at these extrema. We + require the length to be greater than or equal to two. + extrema_type: str, default="extrema" + String to indicate whether the extrema corresponds to pericenters + or the apocenters. + + Returns + ------- + insufficient_extrema_but_long_waveform : bool + True if the waveform has more than approximately 5 orbits but the + number of extrema is less than two. False otherwise. + """ num_extrema = len(extrema) if num_extrema < 2: - # check if the waveform is sufficiently long - if self.approximate_num_orbits > 5: + # Check if the waveform is sufficiently long by estimating the + # approximate number of orbits contained in the waveform data using + # the phase of the (2, 2) mode, assuming that a phase change of + # 4*pi occurs over one orbit. + # NOTE: Since we truncate the waveform data by removing + # `num_orbits_to_remove_before_merger` orbits before the merger, + # phase22[-1] corresponds to the phase of the (2, 2) mode + # `num_orbits_to_remove_before_merger` orbits before the merger. + approximate_num_orbits = ((self.phase22[-1] - self.phase22[0]) + / (4 * np.pi)) + if approximate_num_orbits > 5: # The waveform is sufficiently long but the extrema finding # method fails to find enough number of extrema. This may # happen if the eccentricity is too small and, therefore, the # modulations in the amplitude/frequency is too small for the - # method to detect them. In such cases, the waveform can be - # assumed to be quasicircular. - probably_quasicircular = True + # method to detect them. + insufficient_extrema_but_long_waveform = True else: - probably_quasicircular = False - if probably_quasicircular and self.set_failures_to_zero: + insufficient_extrema_but_long_waveform = False + if insufficient_extrema_but_long_waveform \ + and self.set_failures_to_zero: debug_message( "The waveform has approximately " - f"{self.approximate_num_orbits:.2f}" + f"{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. " @@ -1112,7 +1145,7 @@ def check_num_extrema(self, extrema, extrema_type="extrema"): method_message = "" raise InsufficientExtrema(extrema_type, num_extrema, method_message) - return probably_quasicircular + return insufficient_extrema_but_long_waveform def check_if_dropped_too_many_extrema(self, original_extrema, new_extrema, extrema_type="extrema", diff --git a/gw_eccentricity/load_data.py b/gw_eccentricity/load_data.py index cece5fc6..92efbe78 100644 --- a/gw_eccentricity/load_data.py +++ b/gw_eccentricity/load_data.py @@ -265,7 +265,7 @@ def load_LAL_waveform(**kwargs): # need the zeroecc data to be longer than the ecc data so that we can # intepolate the zeroecc data on the same times as the ecc data. while dataDict_zero_ecc['t'][0] > dataDict["t"][0]: - zero_ecc_kwargs["Momega0"] *= 0.5 + zero_ecc_kwargs["Momega0"] *= 0.9 dataDict_zero_ecc = load_waveform(**zero_ecc_kwargs) t_zeroecc = dataDict_zero_ecc['t'] hlm_zeroecc = dataDict_zero_ecc['hlm'] From f7bae291bfe2ee66f71f4487c105b7509563669c Mon Sep 17 00:00:00 2001 From: md-arif-shaikh Date: Wed, 11 Oct 2023 16:19:04 +0900 Subject: [PATCH 10/17] minor changes --- gw_eccentricity/eccDefinition.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/gw_eccentricity/eccDefinition.py b/gw_eccentricity/eccDefinition.py index 6c8dd1a3..98705987 100644 --- a/gw_eccentricity/eccDefinition.py +++ b/gw_eccentricity/eccDefinition.py @@ -1315,8 +1315,8 @@ def measure_ecc(self, tref_in=None, fref_in=None): # it might be that the eccentricity is too small for the current method # to detect it. See Fig.4 in arxiv.2302.11257. In such cases, we assume # that the waveform is probably quasicircular. - self.probably_quasicircular_pericenter = self.check_num_extrema( - pericenters, "pericenters") + self.insufficient_extrema_but_long_waveform_pericenter \ + = self.check_num_extrema(pericenters, "pericenters") # In some cases it is easier to find the pericenters than finding the # apocenters. For such cases, one can only find the pericenters and use # the mid points between two consecutive pericenters as the location of @@ -1327,8 +1327,8 @@ def measure_ecc(self, tref_in=None, fref_in=None): else: apocenters = self.find_extrema("apocenters") original_apocenters = apocenters.copy() - self.probably_quasicircular_apocenter = self.check_num_extrema( - apocenters, "apocenters") + self.insufficient_extrema_but_long_waveform_apocenter \ + = 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 @@ -1336,8 +1336,8 @@ def measure_ecc(self, tref_in=None, fref_in=None): # the rest of the code in this function is not executed, and therefore, # many variables used in diagnostic tests are never computed, making # diagnostics irrelevant. - if any([self.probably_quasicircular_pericenter, - self.probably_quasicircular_apocenter]) \ + if any([self.insufficient_extrema_but_long_waveform_pericenter, + self.insufficient_extrema_but_long_waveform_apocenter]) \ and self.set_failures_to_zero: return self.set_eccentricity_and_mean_anomaly_to_zero() @@ -1387,7 +1387,7 @@ def measure_ecc(self, tref_in=None, fref_in=None): # Sanity checks # check that fref_out and tref_out are of the same length - if fref_in is not None: + if self.domain == "frequency": if len(self.fref_out) != len(self.tref_out): raise Exception( "length of fref_out and tref_out do not match." @@ -1648,9 +1648,10 @@ def check_input_limits(self, input_vals, min_allowed_val, max_allowed_val): can be measured. """ input_vals = np.atleast_1d(input_vals) - add_extra_info = (self.domain == "time" and - not any([self.probably_quasicircular_apocenter, - self.probably_quasicircular_pericenter])) + add_extra_info = ( + self.domain == "time" and + not any([self.insufficient_extrema_but_long_waveform_apocenter, + self.insufficient_extrema_but_long_waveform_pericenter])) if any(input_vals > max_allowed_val): message = (f"Found reference {self.domain} later than maximum " f"allowed {self.domain}={max_allowed_val}") From 21e10410ba6afd6ce2a2aab377411e96f76b8f6b Mon Sep 17 00:00:00 2001 From: md-arif-shaikh Date: Wed, 11 Oct 2023 16:23:19 +0900 Subject: [PATCH 11/17] better names --- gw_eccentricity/eccDefinition.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gw_eccentricity/eccDefinition.py b/gw_eccentricity/eccDefinition.py index 98705987..1bfefe23 100644 --- a/gw_eccentricity/eccDefinition.py +++ b/gw_eccentricity/eccDefinition.py @@ -1315,7 +1315,7 @@ def measure_ecc(self, tref_in=None, fref_in=None): # it might be that the eccentricity is too small for the current method # to detect it. See Fig.4 in arxiv.2302.11257. In such cases, we assume # that the waveform is probably quasicircular. - self.insufficient_extrema_but_long_waveform_pericenter \ + self.insufficient_pericenters_but_long_waveform \ = self.check_num_extrema(pericenters, "pericenters") # In some cases it is easier to find the pericenters than finding the # apocenters. For such cases, one can only find the pericenters and use @@ -1327,7 +1327,7 @@ def measure_ecc(self, tref_in=None, fref_in=None): else: apocenters = self.find_extrema("apocenters") original_apocenters = apocenters.copy() - self.insufficient_extrema_but_long_waveform_apocenter \ + self.insufficient_apocenters_but_long_waveform \ = self.check_num_extrema(apocenters, "apocenters") # If the eccentricity is too small for a method to find the extrema, @@ -1336,8 +1336,8 @@ def measure_ecc(self, tref_in=None, fref_in=None): # the rest of the code in this function is not executed, and therefore, # many variables used in diagnostic tests are never computed, making # diagnostics irrelevant. - if any([self.insufficient_extrema_but_long_waveform_pericenter, - self.insufficient_extrema_but_long_waveform_apocenter]) \ + if any([self.insufficient_pericenters_but_long_waveform, + self.insufficient_apocenters_but_long_waveform]) \ and self.set_failures_to_zero: return self.set_eccentricity_and_mean_anomaly_to_zero() @@ -1650,8 +1650,8 @@ def check_input_limits(self, input_vals, min_allowed_val, max_allowed_val): input_vals = np.atleast_1d(input_vals) add_extra_info = ( self.domain == "time" and - not any([self.insufficient_extrema_but_long_waveform_apocenter, - self.insufficient_extrema_but_long_waveform_pericenter])) + not any([self.insufficient_apocenters_but_long_waveform, + self.insufficient_pericenters_but_long_waveform])) if any(input_vals > max_allowed_val): message = (f"Found reference {self.domain} later than maximum " f"allowed {self.domain}={max_allowed_val}") From 2d581c70d9a087b32d7da40e432bfccffbe19013 Mon Sep 17 00:00:00 2001 From: md-arif-shaikh Date: Thu, 12 Oct 2023 15:39:11 +0900 Subject: [PATCH 12/17] remove custom exceptions --- gw_eccentricity/eccDefinition.py | 77 ++++++++++++++++++------------- gw_eccentricity/exceptions.py | 72 ----------------------------- test/test_set_failures_to_zero.py | 29 ++++++------ 3 files changed, 60 insertions(+), 118 deletions(-) delete mode 100644 gw_eccentricity/exceptions.py diff --git a/gw_eccentricity/eccDefinition.py b/gw_eccentricity/eccDefinition.py index 1bfefe23..d3755766 100644 --- a/gw_eccentricity/eccDefinition.py +++ b/gw_eccentricity/eccDefinition.py @@ -17,7 +17,6 @@ from .utils import debug_message from .plot_settings import use_fancy_plotsettings, colorsDict, labelsDict from .plot_settings import figWidthsTwoColDict, figHeightsDict -from .exceptions import InsufficientExtrema, NotInAllowedInputRange class eccDefinition: @@ -1067,7 +1066,9 @@ def interp_extrema(self, extrema_type="pericenters"): return self.get_interp(self.t[extrema], self.omega22[extrema]) else: - raise InsufficientExtrema(extrema_type, len(extrema)) + raise Exception( + f"Sufficient number of {extrema_type} are not found." + " Can not create an interpolant.") def check_num_extrema(self, extrema, extrema_type="extrema"): """Check number of extrema. @@ -1143,8 +1144,11 @@ def check_num_extrema(self, extrema, extrema_type="extrema"): ) else: method_message = "" - raise InsufficientExtrema(extrema_type, num_extrema, - method_message) + raise Exception( + f"Number of {extrema_type} found = {num_extrema}.\n" + "Can not build frequency interpolant through the " + f"{extrema_type}.\n" + f"{method_message}") return insufficient_extrema_but_long_waveform def check_if_dropped_too_many_extrema(self, original_extrema, new_extrema, @@ -1315,7 +1319,7 @@ def measure_ecc(self, tref_in=None, fref_in=None): # it might be that the eccentricity is too small for the current method # to detect it. See Fig.4 in arxiv.2302.11257. In such cases, we assume # that the waveform is probably quasicircular. - self.insufficient_pericenters_but_long_waveform \ + insufficient_pericenters_but_long_waveform \ = self.check_num_extrema(pericenters, "pericenters") # In some cases it is easier to find the pericenters than finding the # apocenters. For such cases, one can only find the pericenters and use @@ -1327,19 +1331,24 @@ def measure_ecc(self, tref_in=None, fref_in=None): else: apocenters = self.find_extrema("apocenters") original_apocenters = apocenters.copy() - self.insufficient_apocenters_but_long_waveform \ + insufficient_apocenters_but_long_waveform \ = 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 them. In this case, - # the rest of the code in this function is not executed, and therefore, - # many variables used in diagnostic tests are never computed, making - # diagnostics irrelevant. - if any([self.insufficient_pericenters_but_long_waveform, - self.insufficient_apocenters_but_long_waveform]) \ + # and `set_failures_to_zero` is true, then we set the eccentricity and + # mean anomaly to zero and return them. In this case, the rest of the + # code in this function is not executed, and therefore, many variables + # that are needed for making diagnostic plots are not computed. Thus, + # in such cases, the diagnostic plots may not work. + if any([insufficient_pericenters_but_long_waveform, + insufficient_apocenters_but_long_waveform]) \ and self.set_failures_to_zero: + # store this information that ecc and mean have been set to zero + # to use it in other places + self.eccentricity_and_mean_anomaly_have_been_set_to_zero = True return self.set_eccentricity_and_mean_anomaly_to_zero() + else: + self.eccentricity_and_mean_anomaly_have_been_set_to_zero = False # Choose good extrema self.pericenters_location, self.apocenters_location \ @@ -1372,7 +1381,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]) - if tref_in is None: + if self.domain == "frequency": # 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(self.fref_in) @@ -1648,28 +1657,35 @@ def check_input_limits(self, input_vals, min_allowed_val, max_allowed_val): can be measured. """ input_vals = np.atleast_1d(input_vals) - add_extra_info = ( - self.domain == "time" and - not any([self.insufficient_apocenters_but_long_waveform, - self.insufficient_pericenters_but_long_waveform])) if any(input_vals > max_allowed_val): message = (f"Found reference {self.domain} later than maximum " f"allowed {self.domain}={max_allowed_val}") - if add_extra_info: - message += (" which corresponds to min(last pericenter " - "time, last apocenter time).") - raise NotInAllowedInputRange( - f"Reference {self.domain}", min_allowed_val, max_allowed_val, - message) + if self.domain == "time": + # Add information about the maximum allowed time + message += " which corresponds to " + if self.eccentricity_and_mean_anomaly_have_been_set_to_zero: + message += ("time at `num_orbits_to_exclude_before_merger`" + " orbits before the merger.") + else: + message += "min(last pericenter time, last apocenter time)." + raise Exception( + f"Reference {self.domain} is outside the allowed " + f"range [{min_allowed_val}, {max_allowed_val}]." + f"\n{message}") if any(input_vals < min_allowed_val): message = (f"Found reference {self.domain} earlier than minimum " f"allowed {self.domain}={min_allowed_val}") - if add_extra_info: - message += (" which corresponds to max(first pericenter " - "time, first apocenter time).") - raise NotInAllowedInputRange( - f"Reference {self.domain}", min_allowed_val, max_allowed_val, - message) + if self.domain == "time": + # Add information about the minimum allowed time + message += " which corresponds to " + if self.eccentricity_and_mean_anomaly_have_been_set_to_zero: + message += "the starting time in the time array." + else: + message += "max(first pericenter time, first apocenter time)." + raise Exception( + f"Reference {self.domain} is outside the allowed " + f"range [{min_allowed_val}, {max_allowed_val}]." + f"\n{message}") def check_extrema_separation(self, extrema_location, extrema_type="extrema", @@ -1696,7 +1712,6 @@ def check_extrema_separation(self, extrema_location, return values regardless of debug_level. However, the warnings will still be suppressed for debug_level < 1. """ - # This function only has checks with the flag important=False, which # means that warnings are suppressed when debug_level < 1. # We return without running the rest of the body to avoid unnecessary diff --git a/gw_eccentricity/exceptions.py b/gw_eccentricity/exceptions.py deleted file mode 100644 index f8b9abae..00000000 --- a/gw_eccentricity/exceptions.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Custom exception classes for gw_eccentricity.""" - - -class InsufficientExtrema(Exception): - """Exception raised when the number of extrema is not enough. - - While measuring eccentricity, one common failure that may occur is due to - an insufficient number of extrema. Applying gw_eccentricity to a large - number of waveforms, for example, when reconstructing the PE posterior by - measuring eccentricity at the samples, one may need to loop over all the - samples. In such cases, one may want to avoid failures that are due to an - insufficient number of extrema. Having a specific exception class helps in - such scenarios instead of using a generic exception. - - 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 NotInAllowedInputRange(Exception): - """Exception raised when the reference point is outside the allowed range. - - Due to the nature of the eccentricity definition, one can measure the - eccentricity only within an allowed range of time/frequency. If the failure - during eccentricity measurement is due to an input time/frequency that lies - outside the allowed range, this exception helps in identifying that. - - Parameters - ---------- - reference_point_type : str - Type of reference point. Can be "time" or "frequency". - lower : float - Minimum allowed value, i.e., the lower boundary of the allowed range. - upper : float - Maximum allowed value, i.e., the upper boundary of the 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 NotInAllowedRange 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) diff --git a/test/test_set_failures_to_zero.py b/test/test_set_failures_to_zero.py index d0b16af7..0681faef 100644 --- a/test/test_set_failures_to_zero.py +++ b/test/test_set_failures_to_zero.py @@ -1,27 +1,28 @@ 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. + In certain situations, the waveform may have zero eccentricity or a very + small eccentricity, making it difficult for the given method to identify + any extrema. In cases where such a situation occurs, and if the user has + configured the 'set_failures_to_zero' to `True` in the 'extra_kwargs' + parameter, both the eccentricity and mean anomaly will be forcibly set to + zero. """ # Load test waveform - lal_kwargs = {"approximant": "EccentricTD", + # We use a quasicircular waveform model to test if the eccentricity and + # mean anomaly are correctly set to zero. + lal_kwargs = {"approximant": "IMRPhenomT", "q": 3.0, "chi1": [0.0, 0.0, 0.0], "chi2": [0.0, 0.0, 0.0], "Momega0": 0.01, - "ecc": 1e-4, + "ecc": 0, "mean_ano": 0, "include_zero_ecc": True} dataDict = load_data.load_waveform(**lal_kwargs) @@ -39,9 +40,8 @@ def test_set_failures_to_zero(): tref_out = gwecc_dict["tref_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) + np.testing.assert_allclose(ecc_ref, 0.0) + np.testing.assert_allclose(meanano_ref, 0.0) gwecc_dict = measure_eccentricity( fref_in=fref_in, @@ -51,6 +51,5 @@ def test_set_failures_to_zero(): 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) + np.testing.assert_allclose(ecc_ref, 0.0) + np.testing.assert_allclose(meanano_ref, 0.0) From b7745a0e2516267819008f2480e958698d918c94 Mon Sep 17 00:00:00 2001 From: md-arif-shaikh Date: Thu, 12 Oct 2023 22:52:09 +0900 Subject: [PATCH 13/17] improve test function --- gw_eccentricity/eccDefinition.py | 12 ++++----- test/test_set_failures_to_zero.py | 44 ++++++++++++++++++++----------- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/gw_eccentricity/eccDefinition.py b/gw_eccentricity/eccDefinition.py index d3755766..830d8a68 100644 --- a/gw_eccentricity/eccDefinition.py +++ b/gw_eccentricity/eccDefinition.py @@ -1343,12 +1343,12 @@ def measure_ecc(self, tref_in=None, fref_in=None): if any([insufficient_pericenters_but_long_waveform, insufficient_apocenters_but_long_waveform]) \ and self.set_failures_to_zero: - # store this information that ecc and mean have been set to zero - # to use it in other places - self.eccentricity_and_mean_anomaly_have_been_set_to_zero = True + # Store this information that we are setting ecc and mean anomaly + # to zero to use it in other places + self.setting_ecc_to_zero = True return self.set_eccentricity_and_mean_anomaly_to_zero() else: - self.eccentricity_and_mean_anomaly_have_been_set_to_zero = False + self.setting_ecc_to_zero = False # Choose good extrema self.pericenters_location, self.apocenters_location \ @@ -1663,7 +1663,7 @@ def check_input_limits(self, input_vals, min_allowed_val, max_allowed_val): if self.domain == "time": # Add information about the maximum allowed time message += " which corresponds to " - if self.eccentricity_and_mean_anomaly_have_been_set_to_zero: + if self.setting_ecc_to_zero: message += ("time at `num_orbits_to_exclude_before_merger`" " orbits before the merger.") else: @@ -1678,7 +1678,7 @@ def check_input_limits(self, input_vals, min_allowed_val, max_allowed_val): if self.domain == "time": # Add information about the minimum allowed time message += " which corresponds to " - if self.eccentricity_and_mean_anomaly_have_been_set_to_zero: + if self.setting_ecc_to_zero: message += "the starting time in the time array." else: message += "max(first pericenter time, first apocenter time)." diff --git a/test/test_set_failures_to_zero.py b/test/test_set_failures_to_zero.py index 0681faef..3f99abe9 100644 --- a/test/test_set_failures_to_zero.py +++ b/test/test_set_failures_to_zero.py @@ -11,27 +11,41 @@ def test_set_failures_to_zero(): small eccentricity, making it difficult for the given method to identify any extrema. In cases where such a situation occurs, and if the user has configured the 'set_failures_to_zero' to `True` in the 'extra_kwargs' - parameter, both the eccentricity and mean anomaly will be forcibly set to - zero. + parameter, both the eccentricity and mean anomaly will be set to zero. """ - # Load test waveform - # We use a quasicircular waveform model to test if the eccentricity and - # mean anomaly are correctly set to zero. - lal_kwargs = {"approximant": "IMRPhenomT", - "q": 3.0, - "chi1": [0.0, 0.0, 0.0], - "chi2": [0.0, 0.0, 0.0], - "Momega0": 0.01, - "ecc": 0, - "mean_ano": 0, - "include_zero_ecc": True} - dataDict = load_data.load_waveform(**lal_kwargs) + # The Amplitude and Frequency methods usually fail to detect any extrema + # for eccentricities less than about 1e-3. Therefore, to test whether we + # are setting eccentricity to zero when using these two methods, we use an + # EccentricTD waveform with an initial eccentricity of 1e-4. However, since + # the Residual and the Fits methods can detect extrema for the same + # eccentricity, we use a quasicircular waveform to test them with these + # methods. + lal_kwargs_ecc = {"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.0, + "include_zero_ecc": True} + lal_kwargs_qc = lal_kwargs_ecc.copy() + lal_kwargs_qc.update({"approximant": "IMRPhenomT", + "ecc": 0.0}) + # Create the dataDict for Amplitude and Frequency method + dataDict_ecc = load_data.load_waveform(**lal_kwargs_ecc) + # Create the dataDict for Residual and Fits method + dataDict_qc = load_data.load_waveform(**lal_kwargs_qc) available_methods = gw_eccentricity.get_available_methods() tref_in = -8000 fref_in = 0.005 - + # The following will set ecc and mean ano to zero + # if no extrema are found. extra_kwargs = {"set_failures_to_zero": True} for method in available_methods: + if method in ["Amplitude", "Frequency"]: + dataDict = dataDict_ecc + else: + dataDict = dataDict_qc gwecc_dict = measure_eccentricity( tref_in=tref_in, method=method, From 97a705664b73a750dafeff4e59c23e0190d00b63 Mon Sep 17 00:00:00 2001 From: md-arif-shaikh Date: Thu, 12 Oct 2023 22:55:21 +0900 Subject: [PATCH 14/17] minor polishing --- test/test_set_failures_to_zero.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_set_failures_to_zero.py b/test/test_set_failures_to_zero.py index 3f99abe9..65b895eb 100644 --- a/test/test_set_failures_to_zero.py +++ b/test/test_set_failures_to_zero.py @@ -5,7 +5,7 @@ def test_set_failures_to_zero(): - """ Tests that failures handling due to insufficient extrema. + """Tests handling of failures due to insufficient extrema. In certain situations, the waveform may have zero eccentricity or a very small eccentricity, making it difficult for the given method to identify From 9b15b2e32f0be4f2e25cf73a95478b83a7817a89 Mon Sep 17 00:00:00 2001 From: md-arif-shaikh Date: Thu, 12 Oct 2023 23:00:37 +0900 Subject: [PATCH 15/17] more polishing --- test/test_set_failures_to_zero.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_set_failures_to_zero.py b/test/test_set_failures_to_zero.py index 65b895eb..b855267f 100644 --- a/test/test_set_failures_to_zero.py +++ b/test/test_set_failures_to_zero.py @@ -5,21 +5,21 @@ def test_set_failures_to_zero(): - """Tests handling of failures due to insufficient extrema. + """Test handling of failures due to insufficient extrema. In certain situations, the waveform may have zero eccentricity or a very small eccentricity, making it difficult for the given method to identify any extrema. In cases where such a situation occurs, and if the user has - configured the 'set_failures_to_zero' to `True` in the 'extra_kwargs' - parameter, both the eccentricity and mean anomaly will be set to zero. + set 'set_failures_to_zero' to `True` in the 'extra_kwargs' parameter, both + the eccentricity and mean anomaly will be set to zero. """ # The Amplitude and Frequency methods usually fail to detect any extrema # for eccentricities less than about 1e-3. Therefore, to test whether we # are setting eccentricity to zero when using these two methods, we use an # EccentricTD waveform with an initial eccentricity of 1e-4. However, since # the Residual and the Fits methods can detect extrema for the same - # eccentricity, we use a quasicircular waveform to test them with these - # methods. + # eccentricity, we instead use a quasicircular waveform to test the + # handling of failures due to insufficient extrema for these methods. lal_kwargs_ecc = {"approximant": "EccentricTD", "q": 3.0, "chi1": [0.0, 0.0, 0.0], From db8735ae7cfb70755ebf0a7e8c0d623a6f715a09 Mon Sep 17 00:00:00 2001 From: md-arif-shaikh Date: Fri, 13 Oct 2023 13:02:47 +0900 Subject: [PATCH 16/17] more improvements --- gw_eccentricity/eccDefinition.py | 80 +++++++++++++++++------------- gw_eccentricity/gw_eccentricity.py | 19 +++++-- test/test_set_failures_to_zero.py | 65 +++++++++++++++--------- 3 files changed, 101 insertions(+), 63 deletions(-) diff --git a/gw_eccentricity/eccDefinition.py b/gw_eccentricity/eccDefinition.py index 830d8a68..59d3bba4 100644 --- a/gw_eccentricity/eccDefinition.py +++ b/gw_eccentricity/eccDefinition.py @@ -243,10 +243,21 @@ def __init__(self, dataDict, num_orbits_to_exclude_before_merger=2, extrema are not found. This can happen for various reasons including when the eccentricity is too small for some methods (like the Amplitude method) to measure. See e.g. Fig.4 of - arxiv.2302.11257. If `set_failures_to_zero` is set to True, we - assume that small eccentricity is the cause, and set the - returned eccentricity and mean anomaly to zero when sufficient - extrema are not found. USE THIS WITH CAUTION! + arxiv.2302.11257. If no extrema are found but the following two + conditions are met: + + 1. `set_failures_to_zero` is set to `True`. + 2. The length of the waveform is required to be at least (5 + + `num_obrits_to_exclude_before_merger`) orbits long. By default, + `num_obrits_to_exclude_before_merger` is set to 2, meaning that + 2 orbits are removed from the waveform before it is used by the + extrema finding routine. Consequently, in the default + configuration, the original waveform in the input `dataDict` + must have a minimum length of 7 orbits. + + we assume that small eccentricity is the cause, and set the + returned eccentricity and mean anomaly to zero. + USE THIS WITH CAUTION! """ # Get data necessary for eccentricity measurement self.dataDict, self.t_merger, self.amp22_merger, \ @@ -1076,11 +1087,19 @@ def check_num_extrema(self, extrema, extrema_type="extrema"): Check the number of extrema to determine if there are enough for building the interpolants through the pericenters and apocenters. In cases where the number of extrema is insufficient, i.e., less than 2, - we further verify if the provided waveform is long enough to have a - sufficient number of extrema. - - If the waveform is long enough, but the chosen method fails to detect - any extrema, it is possible that the eccentricity is too small. If + we further verify if the waveform is long enough to have a sufficient + number of extrema. + + We verify that the waveform sent to the peak finding routine is a + minimum of 5 orbits long. By default, + `num_orbits_to_exclude_before_merger` is set to 2, which means that 2 + orbits are subtracted from the original waveform within the input + dataDict. Consequently, in the default configuration, the original + waveform must be at least 7 orbits in length to be considered as + sufficiently long. + + If it is sufficiently long, but the chosen method fails to detect any + extrema, it is possible that the eccentricity is too small. If `set_failures_to_zero` is set to True, then we set `insufficient_extrema_but_long_waveform` to True and return it. @@ -1315,10 +1334,12 @@ def measure_ecc(self, tref_in=None, fref_in=None): pericenters = self.find_extrema("pericenters") original_pericenters = pericenters.copy() # Check if there are a sufficient number of extrema. In cases where the - # waveform is long enough but the method fails to detect any extrema, - # it might be that the eccentricity is too small for the current method - # to detect it. See Fig.4 in arxiv.2302.11257. In such cases, we assume - # that the waveform is probably quasicircular. + # waveform is long enough (at least 5 + + # `num_orbits_to_exclude_before_merger`, i.e., 7 orbits long with + # default settings) but the method fails to detect any extrema, it + # might be that the eccentricity is too small for the current method to + # detect it. See Fig.4 in arxiv.2302.11257. In such cases, the + # following variable will be true. insufficient_pericenters_but_long_waveform \ = self.check_num_extrema(pericenters, "pericenters") # In some cases it is easier to find the pericenters than finding the @@ -1456,31 +1477,13 @@ def set_eccentricity_and_mean_anomaly_to_zero(self): if self.domain == "time": # This function sets eccentricity and mean anomaly to zero when a # method fails to detect any extrema, and therefore, in such cases, - # we can set tref_out to be the times that fall within the range of - # self.t. - self.tref_out = self.tref_in[ - np.logical_and(self.tref_in >= min(self.t), - self.tref_in <= max(self.t))] + # we can set tref_out to be the same as tref_in. + self.tref_out = self.tref_in out_len = len(self.tref_out) - if out_len == 0: - # check that tref_in is in the allowed range - self.check_input_limits( - self.tref_in, min(self.t), max(self.t)) else: - # Since we don't have the maximum and minimum allowed reference - # frequencies computed from the frequencies at the pericenters and - # apocenters, we simply set the maximum and minimum values to be - # the maximum and minimum of instantaneous f22, respectively. - f22_min = min(self.omega22) / (2 * np.pi) - f22_max = max(self.omega22) / (2 * np.pi) - self.fref_out = self.fref_in[ - np.logical_and(self.fref_in >= f22_min, - self.fref_in <= f22_max)] + # similarly we can set fref_out to be the same as fref_in + self.fref_out = self.fref_in out_len = len(self.fref_out) - if out_len == 0: - # check that fref_in is in the allowed range - self.check_input_limits( - self.fref_in, f22_min, f22_max) self.eccentricity = np.zeros(out_len) self.mean_anomaly = np.zeros(out_len) return self.make_return_dict_for_eccentricity_and_mean_anomaly() @@ -1502,6 +1505,13 @@ def make_return_dict_for_eccentricity_and_mean_anomaly(self): # If the original input was scalar, convert the measured eccentricity, # mean anomaly, etc., to scalar. if self.ref_ndim == 0: + # check if ecc, mean ano have more than one elements + for var, arr in zip(["eccentricity", "mean_anomaly"], + [self.eccentricity, self.mean_anomaly]): + if len(arr) != 1: + raise Exception(f"The reference {self.domain} is scalar " + f"but measured {var} does not have " + "exactly one element.") self.eccentricity = self.eccentricity[0] self.mean_anomaly = self.mean_anomaly[0] if self.domain == "time": diff --git a/gw_eccentricity/gw_eccentricity.py b/gw_eccentricity/gw_eccentricity.py index 0a2110d0..054a670c 100644 --- a/gw_eccentricity/gw_eccentricity.py +++ b/gw_eccentricity/gw_eccentricity.py @@ -365,10 +365,21 @@ def measure_eccentricity(tref_in=None, extrema are not found. This can happen for various reasons including when the eccentricity is too small for some methods (like the Amplitude method) to measure. See e.g. Fig.4 of - arxiv.2302.11257. If `set_failures_to_zero` is set to True, we - assume that small eccentricity is the cause, and set the returned - eccentricity and mean anomaly to zero when sufficient extrema are - not found. USE THIS WITH CAUTION! + arxiv.2302.11257. If no extrema are found but the following two + conditions are met: + + 1. `set_failures_to_zero` is set to `True`. + 2. The length of the waveform is required to be at least (5 + + `num_obrits_to_exclude_before_merger`) orbits long. By default, + `num_obrits_to_exclude_before_merger` is set to 2, meaning that 2 + orbits are removed from the waveform before it is used by the + extrema finding routine. Consequently, in the default + configuration, the original waveform in the input `dataDict` must + have a minimum length of 7 orbits. + + we assume that small eccentricity is the cause, and set the + returned eccentricity and mean anomaly to zero. USE THIS WITH + CAUTION! Returns ------- diff --git a/test/test_set_failures_to_zero.py b/test/test_set_failures_to_zero.py index b855267f..afd3208d 100644 --- a/test/test_set_failures_to_zero.py +++ b/test/test_set_failures_to_zero.py @@ -5,7 +5,8 @@ def test_set_failures_to_zero(): - """Test handling of failures due to insufficient extrema. + """Test that the interface works with set_failures_to_zero for waveforms + with small or zero ecc. In certain situations, the waveform may have zero eccentricity or a very small eccentricity, making it difficult for the given method to identify @@ -36,34 +37,50 @@ def test_set_failures_to_zero(): # Create the dataDict for Residual and Fits method dataDict_qc = load_data.load_waveform(**lal_kwargs_qc) available_methods = gw_eccentricity.get_available_methods() - tref_in = -8000 - fref_in = 0.005 # The following will set ecc and mean ano to zero # if no extrema are found. extra_kwargs = {"set_failures_to_zero": True} + + # We want to test it with both a single reference point + # as well as an array of reference points + tref_in = {"scalar": -8000.0, + "array": np.arange(-8000.0, 0.)} + fref_in = {"scalar": 0.05, + "array": np.arange(0.02, 0.05, 0.005)} for method in available_methods: if method in ["Amplitude", "Frequency"]: dataDict = dataDict_ecc else: dataDict = dataDict_qc - 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"] - 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"] - np.testing.assert_allclose(ecc_ref, 0.0) - np.testing.assert_allclose(meanano_ref, 0.0) + # test for reference time + for ref in tref_in: + gwecc_dict = measure_eccentricity( + tref_in=tref_in[ref], + 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"] + np.testing.assert_allclose( + ecc_ref, 0.0 if ref == "scalar" + else np.zeros(len(tref_in[ref]))) + np.testing.assert_allclose( + meanano_ref, 0.0 if ref == "scalar" + else np.zeros(len(tref_in[ref]))) + # test for reference frequency + for ref in fref_in: + gwecc_dict = measure_eccentricity( + fref_in=fref_in[ref], + 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"] + np.testing.assert_allclose( + ecc_ref, 0.0 if ref == "scalar" + else np.zeros(len(fref_in[ref]))) + np.testing.assert_allclose( + meanano_ref, 0.0 if ref == "scalar" + else np.zeros(len(fref_in[ref]))) From 3aa771bb4f35bbec72b701723558a80fccb0367d Mon Sep 17 00:00:00 2001 From: md-arif-shaikh Date: Sat, 14 Oct 2023 11:48:33 +0900 Subject: [PATCH 17/17] shorten doc --- gw_eccentricity/eccDefinition.py | 25 +++++++++++++------------ gw_eccentricity/gw_eccentricity.py | 27 ++++++++++++++------------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/gw_eccentricity/eccDefinition.py b/gw_eccentricity/eccDefinition.py index 59d3bba4..16d80d2d 100644 --- a/gw_eccentricity/eccDefinition.py +++ b/gw_eccentricity/eccDefinition.py @@ -243,20 +243,21 @@ def __init__(self, dataDict, num_orbits_to_exclude_before_merger=2, extrema are not found. This can happen for various reasons including when the eccentricity is too small for some methods (like the Amplitude method) to measure. See e.g. Fig.4 of - arxiv.2302.11257. If no extrema are found but the following two - conditions are met: + arxiv.2302.11257. If no extrema are found, we check whether the + following two conditions are satisfied. 1. `set_failures_to_zero` is set to `True`. - 2. The length of the waveform is required to be at least (5 + - `num_obrits_to_exclude_before_merger`) orbits long. By default, - `num_obrits_to_exclude_before_merger` is set to 2, meaning that - 2 orbits are removed from the waveform before it is used by the - extrema finding routine. Consequently, in the default - configuration, the original waveform in the input `dataDict` - must have a minimum length of 7 orbits. - - we assume that small eccentricity is the cause, and set the - returned eccentricity and mean anomaly to zero. + 2. The waveform is at least + (5 + `num_obrits_to_exclude_before_merger`) orbits long. By + default, `num_obrits_to_exclude_before_merger` is set to 2, + meaning that 2 orbits are removed from the waveform before it + is used by the extrema finding routine. Consequently, in the + default configuration, the original waveform in the input + `dataDict` must have a minimum length of 7 orbits. + + If both of these conditions are met, we assume that small + eccentricity is the cause, and set the returned eccentricity + and mean anomaly to zero. USE THIS WITH CAUTION! """ # Get data necessary for eccentricity measurement diff --git a/gw_eccentricity/gw_eccentricity.py b/gw_eccentricity/gw_eccentricity.py index 054a670c..8079c8f3 100644 --- a/gw_eccentricity/gw_eccentricity.py +++ b/gw_eccentricity/gw_eccentricity.py @@ -365,21 +365,22 @@ def measure_eccentricity(tref_in=None, extrema are not found. This can happen for various reasons including when the eccentricity is too small for some methods (like the Amplitude method) to measure. See e.g. Fig.4 of - arxiv.2302.11257. If no extrema are found but the following two - conditions are met: + arxiv.2302.11257. If no extrema are found, we check whether the + following two conditions are satisfied. 1. `set_failures_to_zero` is set to `True`. - 2. The length of the waveform is required to be at least (5 + - `num_obrits_to_exclude_before_merger`) orbits long. By default, - `num_obrits_to_exclude_before_merger` is set to 2, meaning that 2 - orbits are removed from the waveform before it is used by the - extrema finding routine. Consequently, in the default - configuration, the original waveform in the input `dataDict` must - have a minimum length of 7 orbits. - - we assume that small eccentricity is the cause, and set the - returned eccentricity and mean anomaly to zero. USE THIS WITH - CAUTION! + 2. The waveform is at least (5 + + `num_obrits_to_exclude_before_merger`) orbits long. By default, + `num_obrits_to_exclude_before_merger` is set to 2, meaning that 2 + orbits are removed from the waveform before it is used by the + extrema finding routine. Consequently, in the default + configuration, the original waveform in the input `dataDict` must + have a minimum length of 7 orbits. + + If both of these conditions are met, we assume that small + eccentricity is the cause, and set the returned eccentricity and + mean anomaly to zero. + USE THIS WITH CAUTION! Returns -------