diff --git a/gw_eccentricity/eccDefinition.py b/gw_eccentricity/eccDefinition.py index 9a0fdb1..ea117d1 100644 --- a/gw_eccentricity/eccDefinition.py +++ b/gw_eccentricity/eccDefinition.py @@ -168,18 +168,39 @@ def __init__(self, dataDict, num_orbits_to_exclude_before_merger=2, extra_kwargs: dict A dictionary of any extra kwargs to be passed. Allowed kwargs are: - spline_kwargs: dict + extrema_interp_kwargs: dict + A dictionary of dictionaries where each key corresponds to a + specific interpolation method from the available + `available_omega_gw_extrema_interpolation_methods`. Each + top-level key maps to a nested dictionary containing keyword + arguments (`kwargs`) specific to that interpolation method. The + inner dictionary is passed as arguments to the respective + `omega_gw_extrema_interpolation_method`. + + Example structure: + { + "method_1": {"param1": value1, "param2": value2}, + "method_2": {"paramA": valueA, "paramB": valueB} + }, + "method_1", "method_2" should be available methods in + `available_omega_gw_extrema_interpolation_methods`. + + currently, the available methods are: + + - "spline": default kwargs are set using + `utils.get_default_spline_kwargs` + - "rational_fit": default kwargs are set using + `utils.get_default_rational_fit_kwargs` + + See under `omega_gw_extrema_interpolation_method` for more + details on these interpolation methods. + + non_extrema_interp_kwargs: dict Dictionary of arguments to be passed to the spline interpolation routine (scipy.interpolate.InterpolatedUnivariateSpline) used to - compute quantities like omega_gw_pericenters(t) and - omega_gw_apocenters(t). - Defaults are set using utils.get_default_spline_kwargs - - rational_fit_kwargs: dict - Dictionary of arguments to be passed to the rational - fit function. Defaults are set using - `utils.get_default_rational_fit_kwargs` + interpolate data other than the omega_gw extrema. + Defaults are set using `utils.get_default_spline_kwargs` extrema_finding_kwargs: dict Dictionary of arguments to be passed to the extrema finder, @@ -281,25 +302,27 @@ def __init__(self, dataDict, num_orbits_to_exclude_before_merger=2, USE THIS WITH CAUTION! omega_gw_extrema_interpolation_method : str, default="rational_fit" - Specifies the method used to build the interpolations for - `omega_gw_pericenters_interp(t)` or `omega_gw_apocenters_interp(t)`. - The available options are: + Specifies the method used to build the interpolations for + `omega_gw_pericenters_interp(t)` or + `omega_gw_apocenters_interp(t)`. The available options are: - `spline`: Uses `scipy.interpolate.InterpolatedUnivariateSpline`. - - Best suited for cleaner data, such as when waveform modes are generated - using models like SEOB or TEOB. + - Best suited for cleaner data, such as when waveform modes + are generated using models like SEOB or TEOB. - Faster to construct and evaluate. - - Since it fits through every data point, it may exhibit oscillatory - behavior, particularly near the merger, especially for noisy NR data. + - Since it fits through every data point, it may exhibit + oscillatory behavior, particularly near the merger, + especially for noisy NR data. - `rational_fit`: Uses `polyrat.StabilizedSKRationalApproximation`. - Can handle both clean and noisy data, e.g., waveform modes from numerical simulations. - Better monotonic behaviour, particularly near the merger. - - Significantly slower compared to the `spline` method. This is because - finding optimal numerator and denominator degree needs several iterations - - Can suppress pathologies in the waveform that might be visible with - `spline`. + - Significantly slower compared to the `spline` method. + This is because finding optimal numerator and denominator + degree needs several iterations + - Can suppress pathologies in the waveform that might be + visible with `spline`. Default value: `"rational_fit"`. """ @@ -330,33 +353,47 @@ def __init__(self, dataDict, num_orbits_to_exclude_before_merger=2, extra_kwargs, self.get_default_extra_kwargs(), "extra_kwargs", "eccDefinition.get_default_extra_kwargs()") + self.debug_level = self.extra_kwargs["debug_level"] self.extrema_finding_kwargs = check_kwargs_and_set_defaults( self.extra_kwargs['extrema_finding_kwargs'], self.get_default_extrema_finding_kwargs(min_width_for_extrema), "extrema_finding_kwargs", "eccDefinition.get_default_extrema_finding_kwargs()") - self.spline_kwargs = check_kwargs_and_set_defaults( - self.extra_kwargs["spline_kwargs"], + # set omega extrema interpolation method + self.omega_gw_extrema_interpolation_method = \ + self.extra_kwargs["omega_gw_extrema_interpolation_method"] + # check extrema interpolation method + self.available_omega_gw_extrema_interpolation_methods = \ + self.get_available_omega_gw_extrema_interpolation_methods() + if self.omega_gw_extrema_interpolation_method \ + not in self.available_omega_gw_extrema_interpolation_methods: + raise Exception( + "Unknown omega_gw_extrema_interpolation_method " + f"`{self.omega_gw_extrema_interpolation_method}`. Should be " + f"of {self.available_omega_gw_extrema_interpolation_methods.keys()}") + # set extrema interpolation kwargs + self.extrema_interp_kwargs = check_kwargs_and_set_defaults( + self.extra_kwargs[ + "extrema_interp_kwargs"][ + self.omega_gw_extrema_interpolation_method], + self.get_default_extrema_interp_kwargs(), + "extrema_interp_kwargs", + "eccDefinition.get_default_extrema_interp_kwargs()") + # set other variables required for the omega_gw extrema interpolation + # method + self.set_other_variables_for_extrema_interpolation() + # set spline interpolation kwargs to be used for interpolating data + # other than the extrema. + self.non_extrema_interp_kwargs = check_kwargs_and_set_defaults( + self.extra_kwargs["non_extrema_interp_kwargs"], get_default_spline_kwargs(), - "spline_kwargs", + "non_extrema_interp_kwargs", "utils.get_default_spline_kwargs()") - self.rational_fit_kwargs = check_kwargs_and_set_defaults( - self.extra_kwargs["rational_fit_kwargs"], - get_default_rational_fit_kwargs(), - "rational_fit_kwargs", - "utils.get_default_rational_fit_kwargs()") self.available_averaging_methods \ = self.get_available_omega_gw_averaging_methods() - self.debug_level = self.extra_kwargs["debug_level"] - # set verbose to debug_level. If verbose is True, then it prints information - # of each iteration for rational fits for a given degree. - self.rational_fit_kwargs["verbose"] = self.debug_level - # keep history of rational fit degree and nonmonotonicity of the corresponding fits - self.rational_fit_nonmonotonicity_history = {"pericenters": {}, "apocenters": {}} - # Store degrees used to construct the final fits - self.rational_fit_degrees = {"pericenters": None, "apocenters": None} self.debug_plots = self.extra_kwargs["debug_plots"] - self.return_zero_if_small_ecc_failure = self.extra_kwargs["return_zero_if_small_ecc_failure"] + self.return_zero_if_small_ecc_failure \ + = self.extra_kwargs["return_zero_if_small_ecc_failure"] # check if there are unrecognized keys in the dataDict self.recognized_dataDict_keys = self.get_recognized_dataDict_keys() for kw in dataDict.keys(): @@ -870,12 +907,53 @@ def get_default_extrema_finding_kwargs(self, width): "rel_height": 0.5, "plateau_size": None} return default_extrema_finding_kwargs + + def get_default_extrema_interp_kwargs(self): + """Get default kwargs to be passed to extrema interpolating method. + + Accurate interpolation of omega_gw extrema is crucial for obtaining + monotonic eccentricity evoultion with time. Depending on the + interpolation method, this function returns the default kwargs to be + passed to the interpolating function. + """ + allowed_methods = self.extra_kwargs["extrema_interp_kwargs"].keys() + if self.omega_gw_extrema_interpolation_method == "spline": + kwargs = get_default_spline_kwargs() + elif self.omega_gw_extrema_interpolation_method == "rational_fit": + kwargs = get_default_rational_fit_kwargs() + else: + raise Exception("Unknown omega_gw extrema interpolation method " + f"`{self.omega_gw_extrema_interpolation_method}`. " + f"Allowed methods are {allowed_methods}") + return kwargs + + def set_other_variables_for_extrema_interpolation(self): + """Set other variables required for extrema interpolation. + + Depending on the omega_gw_extrema_interpolation_method, additional + varibles may be required for obtaining good interpolant and/or for + debugging purpose. These variables may vary from method to method. + """ + if self.omega_gw_extrema_interpolation_method == "rational_fit": + # set verbose to debug_level. If verbose is True, then it prints + # information of each iteration for rational fits for a given + # degree. + self.extrema_interp_kwargs["verbose"] = self.debug_level + # keep history of rational fit degree and nonmonotonicity of the + # corresponding fits + self.rational_fit_nonmonotonicity_history = { + "pericenters": {}, "apocenters": {}} + # Store degrees used to construct the final fits + self.rational_fit_degrees = { + "pericenters": None, "apocenters": None} def get_default_extra_kwargs(self): """Defaults for additional kwargs.""" default_extra_kwargs = { - "spline_kwargs": {}, - "rational_fit_kwargs": {}, + "non_extrema_interp_kwargs": {}, + "extrema_interp_kwargs": { + "spline": {}, + "rational_fit": {}}, "extrema_finding_kwargs": {}, # Gets overridden in methods like # eccDefinitionUsingAmplitude "debug_level": 0, @@ -1177,9 +1255,17 @@ def get_good_extrema(self, pericenters, apocenters, pericenters, apocenters) return pericenters, apocenters - def get_interp(self, oldX, oldY, allowExtrapolation=False, - interpolator="spline"): - """Get interpolant. + def get_spline_interpolant_for_non_extrema( + self, oldX, oldY, allowExtrapolation=False, interpolator="spline"): + """Get spline interpolant for data other than the omega_gw extrema. + + Interpolating the omega_gw extrema could be challenging, particularly + near the merger due to the rapid change in the omega_gw values and + limited number of data points. Thus, it requires special treatment. For + other data, this problem is less likely to occur. Therefore, we use + this function to interpolate data other than omega_gw extrema. The + spline kwargs for this function is provided by `extra_kwargs` using the + key "non_extrema_interp_kwargs". A wrapper of utils.get_interpolant with check_kwargs=False. This is to make sure that the checking of kwargs is not performed @@ -1188,46 +1274,33 @@ def get_interp(self, oldX, oldY, allowExtrapolation=False, function without repeating checks. """ return get_interpolant(oldX, oldY, allowExtrapolation, interpolator, - spline_kwargs=self.spline_kwargs, + spline_kwargs=self.non_extrema_interp_kwargs, check_kwargs=False) - def interp(self, newX, oldX, oldY, allowExtrapolation=False, - interpolator="spline"): - """Get interpolated values. + def get_spline_interpolant_for_extrema( + self, oldX, oldY, allowExtrapolation=False, interpolator="spline"): + """Get spline interpolant for omega_gw extrema. - A wrapper of utils.interpolate with check_kwargs=False for - reasons explained in the documentation of get_interp function. + Same as `get_spline_interpolant_for_non_extrema` but uses the + `spline_kwargs` provided via `self.extrema_interp_kwargs`. For more, + see `get_spline_interpolant_for_non_extrema`. """ - return interpolate(newX, oldX, oldY, allowExtrapolation, interpolator, - spline_kwargs=self.spline_kwargs, - check_kwargs=False) - - def interp_extrema(self, extrema_type="pericenters"): - """Build interpolant through extrema. + return get_interpolant(oldX, oldY, allowExtrapolation, interpolator, + spline_kwargs=self.extrema_interp_kwargs, + check_kwargs=False) - parameters: - ----------- - extrema_type: - Either "pericenters" or "apocenters". + def spline_interpolate_non_extrema(self, newX, oldX, oldY, allowExtrapolation=False, + interpolator="spline"): + """Get interpolated values using spline for data other than omega_gw + extrema. - returns: - ------ - Interpolant through extrema + A wrapper of utils.interpolate with check_kwargs=False for reasons + explained in the documentation of + `get_spline_interpolant_for_non_extrema` function. """ - if extrema_type == "pericenters": - extrema = self.pericenters_location - elif extrema_type == "apocenters": - extrema = self.apocenters_location - else: - raise Exception("extrema_type must be either " - "'pericenrers' or 'apocenters'.") - if len(extrema) >= 2: - return self.get_interp(self.t[extrema], - self.omega_gw[extrema]) - else: - raise Exception( - f"Sufficient number of {extrema_type} are not found." - " Can not create an interpolant.") + return interpolate(newX, oldX, oldY, allowExtrapolation, interpolator, + spline_kwargs=self.non_extrema_interp_kwargs, + check_kwargs=False) def get_rational_fit_wrapper(self, x, y): """Get rational fit. @@ -1239,10 +1312,10 @@ def get_rational_fit_wrapper(self, x, y): checks. """ return get_rational_fit(x, y, - rational_fit_kwargs=self.rational_fit_kwargs, + rational_fit_kwargs=self.extrema_interp_kwargs, check_kwargs=False) - def rational_fit(self, x, y, data_name=None): + def get_rational_fit_for_extrema(self, x, y, data_name=None): """Get rational fit with adaptive numerator and denominator degree. This function ensures that the rational fit uses the optimal degree for @@ -1254,70 +1327,81 @@ def rational_fit(self, x, y, data_name=None): the previous valid monotonic fit. The initial degrees for the rational fit can be specified through - `rational_fit_kwargs` in `extra_kwargs`. Default values are provided by - `get_default_rational_fit_kwargs`, where both degrees are set to - `None`. If both degrees remain `None`, appropriate starting values are - determined using `self.get_approximate_degree_for_rational_fit`. + `extrema_interp_kwargs` in `extra_kwargs` with the key "rational_fit". + (See `extrema_interp_kwargs` under `extra_kwargs` for more details) + Default values are provided by `get_default_rational_fit_kwargs`, where + both degrees are set to `None`. If both degrees remain `None`, + appropriate starting values are determined using + `self.get_approximate_degree_for_rational_fit`. """ # make sure that data_name is not None. A data_name is needed to # update the optimal values of the numerator and denominator degrees # used to build the final rational fit if data_name is None: - raise Exception("Please provide a data_name for which to build a " - "rational fit. `data_name` can not be None. For example, " - "it can be 'apocenters', 'pericenters' or 'omega_gw_average'.") - # Set initial degrees for the rational fit if not already specified. - # If either `num_degree` or `denom_degree` is specified, use that value - # to set the other. If both are unspecified, we use the following - # procedure to set appropriate values for `num_degree` and `denom_degree`. + raise Exception( + "Please provide a data_name for which to build a " + "rational fit. `data_name` can not be None. For example, " + "it can be 'apocenters', 'pericenters' or 'omega_gw_average'.") + # Set initial degrees for the rational fit if not already specified. If + # either `num_degree` or `denom_degree` is specified, use that value to + # set the other. If both are unspecified, we use the following + # procedure to set appropriate values for `num_degree` and + # `denom_degree`. - # The optimal degrees for the numerator and denominator can vary with - # the eccentricity and duration of the waveform. To estimate these values, - # we first count the approximate number of orbits, then use a predefined - # mapping from orbit count to optimal degrees, provided by + # The optimal degrees for the numerator and denominator can vary with + # the eccentricity and duration of the waveform. To estimate these + # values, we first count the approximate number of orbits, then use a + # predefined mapping from orbit count to optimal degrees, provided by # `get_approximate_degree_for_rational_fit`. # While this provides a good initial estimate, it may not be optimal, # as the ideal degree can change based on the eccentricity which we do # not know apriori. The true optimal values are determined through # further iterations. - if self.rational_fit_kwargs["num_degree"] is None and self.rational_fit_kwargs["denom_degree"] is None: - self.rational_fit_kwargs["num_degree"], self.rational_fit_kwargs["denom_degree"] = \ + if self.extrema_interp_kwargs["num_degree"] is None \ + and self.extrema_interp_kwargs["denom_degree"] is None: + self.extrema_interp_kwargs["num_degree"], self.extrema_interp_kwargs["denom_degree"] = \ self.get_approximate_degree_for_rational_fit() - elif self.rational_fit_kwargs["num_degree"] is None: - self.rational_fit_kwargs["num_degree"] = self.rational_fit_kwargs["denom_degree"] - elif self.rational_fit_kwargs["denom_degree"] is None: - self.rational_fit_kwargs["denom_degree"] = self.rational_fit_kwargs["num_degree"] + elif self.extrema_interp_kwargs["num_degree"] is None: + self.extrema_interp_kwargs["num_degree"] = self.extrema_interp_kwargs["denom_degree"] + elif self.extrema_interp_kwargs["denom_degree"] is None: + self.extrema_interp_kwargs["denom_degree"] = self.extrema_interp_kwargs["num_degree"] rat_fit = self.get_rational_fit_wrapper(x, y) x_test = np.arange(x[0], x[-1], self.t[1] - self.t[0]) - # save the degrees for checks at each step of iterations for finding the optimal degrees - old_num_degree = self.rational_fit_kwargs["num_degree"] - old_denom_degree = self.rational_fit_kwargs["denom_degree"] + # save the degrees for checks at each step of iterations for finding + # the optimal degrees + old_num_degree = self.extrema_interp_kwargs["num_degree"] + old_denom_degree = self.extrema_interp_kwargs["denom_degree"] # Check for nonmonotonicity and lower degrees if needed fit_is_nonmonotonic = self.check_if_first_derivative_is_not_strictly_monotonic( x_test, rat_fit(x_test), 1.0, data_name) while fit_is_nonmonotonic: - if self.rational_fit_kwargs["num_degree"] > 1: - self.rational_fit_kwargs["num_degree"] -= 1 - if self.rational_fit_kwargs["denom_degree"] > 1: - self.rational_fit_kwargs["denom_degree"] -= 1 - if self.rational_fit_kwargs["num_degree"] == 1 and self.rational_fit_kwargs["denom_degree"] == 1: - debug_message("Both numerator and denominator degrees are equal to 1 " - "and cannot be lowered further.", - debug_level=self.debug_level, important=False) + if self.extrema_interp_kwargs["num_degree"] > 1: + self.extrema_interp_kwargs["num_degree"] -= 1 + if self.extrema_interp_kwargs["denom_degree"] > 1: + self.extrema_interp_kwargs["denom_degree"] -= 1 + if self.extrema_interp_kwargs["num_degree"] == 1 \ + and self.extrema_interp_kwargs["denom_degree"] == 1: + debug_message( + "Both numerator and denominator degrees are equal to 1 " + "and cannot be lowered further.", + debug_level=self.debug_level, important=False) break - debug_message(f"Lowering degrees to num_degree={self.rational_fit_kwargs['num_degree']}, " - f"denom_degree={self.rational_fit_kwargs['denom_degree']} and retrying.", - debug_level=self.debug_level, important=False) + debug_message( + "Lowering degrees to " + f"num_degree={self.extrema_interp_kwargs['num_degree']}, " + f"denom_degree={self.extrema_interp_kwargs['denom_degree']} " + "and retrying.", debug_level=self.debug_level, important=False) # build new fit and check monotonicity rat_fit = self.get_rational_fit_wrapper(x, y) fit_is_nonmonotonic = self.check_if_first_derivative_is_not_strictly_monotonic( x_test, rat_fit(x_test), 1.0, data_name) - # If fit with initial degree is monotonic, try increasing the degree for a better fit + # If fit with initial degree is monotonic, try increasing the degree + # for a better fit if not fit_is_nonmonotonic: last_monotonic_rat_fit = rat_fit # Track last monotonic fit last_monotonic_num_degree = old_num_degree @@ -1325,22 +1409,27 @@ def rational_fit(self, x, y, data_name=None): while not fit_is_nonmonotonic: # Increase the degrees for both numerator and denominator - new_num_degree = self.rational_fit_kwargs["num_degree"] + 1 - new_denom_degree = self.rational_fit_kwargs["denom_degree"] + 1 - self.rational_fit_kwargs["num_degree"], self.rational_fit_kwargs["denom_degree"] \ + new_num_degree = self.extrema_interp_kwargs["num_degree"] + 1 + new_denom_degree = self.extrema_interp_kwargs["denom_degree"] + 1 + self.extrema_interp_kwargs["num_degree"], self.extrema_interp_kwargs["denom_degree"] \ = new_num_degree, new_denom_degree # build new fit and check monotonicity new_rat_fit = self.get_rational_fit_wrapper(x, y) - fit_is_nonmonotonic = self.check_if_first_derivative_is_not_strictly_monotonic( - x_test, new_rat_fit(x_test), 1.0, data_name) + fit_is_nonmonotonic \ + = self.check_if_first_derivative_is_not_strictly_monotonic( + x_test, new_rat_fit(x_test), 1.0, data_name) if fit_is_nonmonotonic: - # Revert to previous fit and degrees if nonmonotonicity is detected - debug_message(f"Increasing degrees caused nonmonotonicity. Reverting to " - f"last monotonic fit with num_degree={last_monotonic_num_degree} " - f"and denom_degree={last_monotonic_denom_degree}.", - debug_level=self.debug_level, important=False) - self.rational_fit_kwargs["num_degree"] = last_monotonic_num_degree - self.rational_fit_kwargs["denom_degree"] = last_monotonic_denom_degree + # Revert to previous fit and degrees if nonmonotonicity is + # detected + debug_message( + "Increasing degrees caused nonmonotonicity. Reverting to " + f"last monotonic fit with num_degree={last_monotonic_num_degree} " + f"and denom_degree={last_monotonic_denom_degree}.", + debug_level=self.debug_level, important=False) + self.extrema_interp_kwargs["num_degree"] \ + = last_monotonic_num_degree + self.extrema_interp_kwargs["denom_degree"] \ + = last_monotonic_denom_degree rat_fit = last_monotonic_rat_fit break @@ -1350,15 +1439,16 @@ def rational_fit(self, x, y, data_name=None): rat_fit = new_rat_fit # update final degrees used to build the fit - self.rational_fit_degrees[data_name] = (self.rational_fit_kwargs["num_degree"], - self.rational_fit_kwargs["denom_degree"]) + self.rational_fit_degrees[data_name] = ( + self.extrema_interp_kwargs["num_degree"], + self.extrema_interp_kwargs["denom_degree"]) return rat_fit def get_approximate_degree_for_rational_fit(self): """Get approximate degree based on the number of extrema. - Assign degree based on number of extrema found. The degree is increased as - the number of extrema increases. + Assign degree based on number of extrema found. The degree is increased + as the number of extrema increases. """ # TODO: Optimize this. # assign degree based on the number of extrema if user provided @@ -1378,11 +1468,49 @@ def get_approximate_degree_for_rational_fit(self): elif approximate_num_orbits <= 200: num_degree, denom_degree = 6, 6 else: - num_degree, denom_degree = 5 + int(np.log10(approximate_num_orbits)), 5 + int(np.log10(approximate_num_orbits)) + num_degree, denom_degree \ + = (5 + int(np.log10(approximate_num_orbits)), + 5 + int(np.log10(approximate_num_orbits))) return num_degree, denom_degree - def rational_fit_extrema(self, extrema_type="pericenters"): - """Build rational fit through extrema. + def check_if_first_derivative_is_not_strictly_monotonic( + self, x, y, tol=1.0, data_name=None): + """Check if the first derivative of data is not strictly monotonic. + """ + dy_dx = np.gradient(y, x[1] - x[0]) + is_not_strictly_monotonic = any(dy_dx[1:]/dy_dx[:-1] < tol) + # update history + if self.extra_kwargs["omega_gw_extrema_interpolation_method"] \ + == "rational_fit": + if data_name in self.rational_fit_nonmonotonicity_history: + self.rational_fit_nonmonotonicity_history[data_name].update( + {(self.extrema_interp_kwargs['num_degree'], + self.extrema_interp_kwargs['num_degree']): + is_not_strictly_monotonic}) + return is_not_strictly_monotonic + + def get_available_omega_gw_extrema_interpolation_methods(self): + """Return available omega_gw extrema interpolation methods.""" + available_methods = { + "spline": self.get_spline_interpolant_for_extrema, + "rational_fit": self.get_rational_fit_for_extrema, + } + return available_methods + + def get_omega_gw_extrema_interpolant(self, extrema_type="pericenters"): + """Build interpolant through omega_gw extrema. + + Calculating the eccentricity using the gravitational wave frequency + (`omega_gw`), requires building an interpolant through its extrema + (pericenters and apocenters). + + The method for constructing this interpolant can be specified by the + user via `omega_gw_extrema_interpolation_method` in + `extra_kwargs`. Currently, two options are supported: `"spline"` and + `"rational_fit"`. + + Based on the chosen method, an interpolation will be constructed for + `omega_gw` at the pericenter/apocenter points. parameters: ----------- @@ -1391,7 +1519,7 @@ def rational_fit_extrema(self, extrema_type="pericenters"): returns: ------ - Rational fit through extrema + Interpolant through extrema """ if extrema_type == "pericenters": extrema = self.pericenters_location @@ -1401,26 +1529,20 @@ def rational_fit_extrema(self, extrema_type="pericenters"): raise Exception("extrema_type must be either " "'pericenrers' or 'apocenters'.") if len(extrema) >= 2: - return self.rational_fit(self.t[extrema], - self.omega_gw[extrema], - data_name=extrema_type) + method = self.available_omega_gw_extrema_interpolation_methods[ + self.omega_gw_extrema_interpolation_method] + if self.omega_gw_extrema_interpolation_method == "rational_fit": + # rational fit method `get_rational_fit_for_extrema` takes an + # additional argument + return method(self.t[extrema], self.omega_gw[extrema], + extrema_type) + else: + return method(self.t[extrema], self.omega_gw[extrema]) else: raise Exception( f"Sufficient number of {extrema_type} are not found." " Can not create an interpolant.") - def check_if_first_derivative_is_not_strictly_monotonic(self, x, y, tol=1.0, data_name=None): - """Check if the first derivative of data is not strictly monotonic. - """ - dy_dx = np.gradient(y, x[1] - x[0]) - is_not_strictly_monotonic = any(dy_dx[1:]/dy_dx[:-1] < tol) - # update history - if self.extra_kwargs["omega_gw_extrema_interpolation_method"] == "rational_fit": - if data_name in self.rational_fit_nonmonotonicity_history: - self.rational_fit_nonmonotonicity_history[data_name].update({ - (self.rational_fit_kwargs['num_degree'], self.rational_fit_kwargs['num_degree']): is_not_strictly_monotonic}) - return is_not_strictly_monotonic - def check_num_extrema(self, extrema, extrema_type="extrema"): """Check number of extrema. @@ -1810,7 +1932,10 @@ def measure_ecc(self, tref_in=None, fref_in=None): raise Exception("Reference time must be within two pericenters.") # Build omega_gw extrema interpolants - self.build_omega_gw_extrema_interpolants() + self.omega_gw_pericenters_interp = self.get_omega_gw_extrema_interpolant("pericenters") + self.omega_gw_apocenters_interp = self.get_omega_gw_extrema_interpolant("apocenters") + # check monotonicity of the interpolants + self.check_omega_gw_extrema_interpolants() # compute eccentricity at self.tref_out self.eccentricity = self.compute_eccentricity(self.tref_out) # Compute mean anomaly at tref_out @@ -1833,45 +1958,24 @@ def measure_ecc(self, tref_in=None, fref_in=None): # frequency where these are measured. return self.make_return_dict_for_eccentricity_and_mean_anomaly() - def build_omega_gw_extrema_interpolants(self): - """Build interpolant through omega_gw extrema. - - Calculating the eccentricity using the gravitational wave frequency - (`omega_gw`), requires building an interpolant through its extrema - (pericenters and apocenters). - - The method for constructing this interpolant can be specified by the - user via `omega_gw_extrema_interpolation_method` in - `extra_kwargs`. Currently, two options are supported: `"spline"` and - `"rational_fit"`. - - Based on the chosen method, an interpolation will be constructed for - `omega_gw` at the pericenter and apocenter points. + def check_omega_gw_extrema_interpolants(self): """ - # Build the interpolants of omega_gw at the extrema - if self.extra_kwargs["omega_gw_extrema_interpolation_method"] == "spline": - self.omega_gw_apocenters_interp = self.interp_extrema("apocenters") - self.omega_gw_pericenters_interp = self.interp_extrema("pericenters") - elif self.extra_kwargs["omega_gw_extrema_interpolation_method"] == "rational_fit": - self.omega_gw_apocenters_interp = self.rational_fit_extrema("apocenters") - self.omega_gw_pericenters_interp = self.rational_fit_extrema("pericenters") - else: - raise Exception( - f"Unknown method {self.extra_kwargs['omega_gw_extrema_interpolation_method']} " - "for `omega_gw_extrema_interpolation_method`. " - "Must be one of `spline` or `rational_fit`.") - - # Verify the monotonicity of the first derivative of the omega_gw interpolant with spline. + check monotonicity of the omega_gw extrema interpolants. + """ + # Verify the monotonicity of the first derivative of the omega_gw + # interpolant with spline. if self.extra_kwargs["omega_gw_extrema_interpolation_method"] == "spline": - # Check if the first derivative of omega_gw at pericenters or apocenters is non-monotonic + # Check if the first derivative of omega_gw at pericenters or + # apocenters is non-monotonic if (self.check_if_first_derivative_is_not_strictly_monotonic( self.t_for_checks, self.omega_gw_pericenters_interp(self.t_for_checks)) or self.check_if_first_derivative_is_not_strictly_monotonic( self.t_for_checks, self.omega_gw_apocenters_interp(self.t_for_checks))): debug_message( - "Nonmonotonic time derivative detected in the spline interpolant through extrema. " - "Using rational fit by setting 'omega_gw_extrema_interpolation_method' to 'rational_fit' " - "may provide better result.", + "Nonmonotonic time derivative detected in the spline " + "interpolant through extrema. Using rational fit by " + "setting 'omega_gw_extrema_interpolation_method' to " + "'rational_fit' may provide better result.", debug_level=self.debug_level, important=True) def set_eccentricity_and_mean_anomaly_to_zero(self): @@ -2005,8 +2109,8 @@ def derivative_of_eccentricity(self, t, n=1): self.t_for_checks) if self.ecc_interp is None: - self.ecc_interp = self.get_interp(self.t_for_checks, - self.ecc_for_checks) + self.ecc_interp = self.get_spline_interpolant_for_non_extrema( + self.t_for_checks, self.ecc_for_checks) # Get derivative of ecc(t) using spline return self.ecc_interp.derivative(n=n)(t) @@ -2328,11 +2432,11 @@ def compute_res_amp_gw_and_res_omega_gw(self): # residual quantities can be computed. Above, we check that this # extrapolation does not happen before t_merger, which is where # eccentricity is normally measured. - self.amp_gw_zeroecc_interp = self.interp( + self.amp_gw_zeroecc_interp = self.spline_interpolate_non_extrema( self.t, self.t_zeroecc_shifted, self.amp_gw_zeroecc, allowExtrapolation=True) self.res_amp_gw = self.amp_gw - self.amp_gw_zeroecc_interp - self.omega_gw_zeroecc_interp = self.interp( + self.omega_gw_zeroecc_interp = self.spline_interpolate_non_extrema( self.t, self.t_zeroecc_shifted, self.omega_gw_zeroecc, allowExtrapolation=True) self.res_omega_gw = (self.omega_gw - self.omega_gw_zeroecc_interp) @@ -2465,7 +2569,7 @@ def compute_orbit_averaged_omega_gw_between_extrema(self, t): orbit_averaged_omega_gw, "omega_gw averaged [apocenter to apocenter] and " "[pericenter to pericenter]") - return self.interp( + return self.spline_interpolate_non_extrema( t, self.t_for_orbit_averaged_omega_gw, orbit_averaged_omega_gw) def check_monotonicity_of_omega_gw_average(self, @@ -2569,7 +2673,7 @@ def compute_mean_of_extrema_interpolants(self, t): def compute_omega_gw_zeroecc(self, t): """Find omega_gw from zeroecc data.""" - return self.interp( + return self.spline_interpolate_non_extrema( t, self.t_zeroecc_shifted, self.omega_gw_zeroecc) def get_available_omega_gw_averaging_methods(self): @@ -2721,7 +2825,7 @@ def compute_tref_in_and_fref_out_from_fref_in(self, fref_in): self.omega_gw_average, "Interpolated omega_gw_average") # Get tref_in using interpolation - tref_in = self.interp(fref_out, + tref_in = self.spline_interpolate_non_extrema(fref_out, self.omega_gw_average/(2 * np.pi), self.t_for_omega_gw_average) # check if tref_in is monotonically increasing