Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add rational fits #223

Open
wants to merge 38 commits into
base: main
Choose a base branch
from
Open

add rational fits #223

wants to merge 38 commits into from

Conversation

md-arif-shaikh
Copy link
Collaborator

@md-arif-shaikh md-arif-shaikh commented Sep 25, 2024

Add rational_fit as an alternative method to interpolate the omega at the extrema.
This PR introduces a new option in extra_kwargs for omega_gw_extrema_interpolation_method which defaults to the spline-based interpolation.

omega_gw_extrema_interpolation_method can be set to either of the following values:

  • spline: Uses InterpolatedUnivariateSpline with k=3 (as default).
  • rational_fit: Uses rational approximation as implemented in polyrat.StabilizedSKRationalApproximation

rational_fit improves the fit to the omega_gw at pericenter and apocenters, particularly near the merger where spline struggles to remain monotonic.

@md-arif-shaikh md-arif-shaikh marked this pull request as draft September 25, 2024 09:38
@md-arif-shaikh
Copy link
Collaborator Author

This PR is quite long, so I want to highlight the main changes

  • utils.py: general rational fit functions added here
  • eccDefinition.py: rational fit functions for fit to the omega_gw_extrema added. This is the most important function to be reviewed.

Other changes including the tests are straightforward. I Had to change a few things to make the tests pass because we have changed the default to rational_fit. More changes are to be made to the tests but that can be done later in a separate PR.

@md-arif-shaikh md-arif-shaikh marked this pull request as ready for review October 27, 2024 06:18
@md-arif-shaikh
Copy link
Collaborator Author

Hi @vijayvarma392, this PR is now ready to be reviewed. I have summarized the main changes here. I think at this point, I would like to have your comments for making further changes.

Dictionary of arguments to be passed to the rational
fit function. Defaults are set using
`utils.get_default_rational_fit_kwargs`

Copy link
Owner

Choose a reason for hiding this comment

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

You should indicate here already that only one of spline_kwargs or rational_fit_kwargs are allowed.
Question: Would it be better to have interp_kwargs, which has a key 'interp_type' that can be 'spline' or 'rational' fit? In the future, if we add extra options like Toni's PN inspired fits, that would make it easier.
This is more of a question for you rather than a suggestion because I don't know if that will make the code more complicated than it needs to be. Either way, needs to be clear that only one is allowed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We use spline kwargs for things other than interpolating the omega_gw extrema. For example, we interpolate zeroecc data using spline. So providing just rational_fit kwargs won't work because rational_fit is only good for interpolating monotonic trends.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Basically we will need to make all interpolation thing work with a single method if we want the user to provide only one of them. Here, we wanted to use rational fit for the omega envelope mainly.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What do you suggest?

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.
Copy link
Owner

Choose a reason for hiding this comment

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

near the merger, especially for noisy NR data.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

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
Copy link
Owner

Choose a reason for hiding this comment

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

Can you say a rough number instead of "Significantly slower"?

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": {}}
# Update the degrees used to construct the final fits
Copy link
Owner

Choose a reason for hiding this comment

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

Should the comment be: This is used to keep track and update the degrees used to construct the final fits?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Okay, so I am using rational_fit_nonmonotonicity_history to keep track of what degrees are being iterated over and the corresponding behaviour, i.e., is it nonmonotonic? And using rational_fit_degrees to store the final degree used to build the fits. Instead of using "update the degrees", I changed it to "store degrees" to better convey the purpose.

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
Copy link
Owner

Choose a reason for hiding this comment

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

Why not just call it debug_level?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

"verbose" is the keyword used in the routine for rational fit, so I have kept that the same and instead assign value to it using debug_level

@@ -1184,6 +1228,180 @@ def interp_extrema(self, extrema_type="pericenters"):
raise Exception(
f"Sufficient number of {extrema_type} are not found."
" Can not create an interpolant.")

def get_rat_fit(self, x, y):
Copy link
Owner

Choose a reason for hiding this comment

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

hmm, don't like the name...come up with a way to not call the two things get_rat_fit and get_rational_fit? Easily confused.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

changed get_rat_fit to get_rational_fit_wrapper

This function ensures that the rational fit uses the optimal degree for
the numerator and denominator. It first checks for nonmonotonicity in the
fit's derivative and lowers the degrees if necessary. Only if the initial
degree was monotonic (no reduction needed), it attempts to increase the
Copy link
Owner

Choose a reason for hiding this comment

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

"Only if the initial degree was monotonic" --> "Only if the fit for the initial degree was monotonic"

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

# make sure that description is not None. A description is needed to
# update the optimal values of the numerator and denominator degrees
# used to build the final rational fit
if description is None:
Copy link
Owner

Choose a reason for hiding this comment

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

description --> data_name?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

fit's derivative and lowers the degrees if necessary. Only if the initial
degree was monotonic (no reduction needed), it attempts to increase the
degrees to find a higher-degree fit. If increasing the degrees causes
nonmonotonicity, it reverts to the previous valid monotonic fit.
Copy link
Owner

Choose a reason for hiding this comment

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

Say something about how the initial degrees should be set? Through self.rational_fit_kwargs?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

# value is found by doing some iterations.
if not (self.rational_fit_kwargs["num_degree"] and self.rational_fit_kwargs["denom_degree"]):
self.rational_fit_kwargs["num_degree"], self.rational_fit_kwargs["denom_degree"] \
= self.get_approximate_degree_for_rational_fit()
Copy link
Owner

Choose a reason for hiding this comment

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

Does this still work if the user gives only one of num_degree and denom_degree?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No, I have changed this part of the code so that it does the following: If both are None, i.e., unspecified only then use the function to set the approximate degree to both num and denom. If any one of the degrees is given, use it to set the other.

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))
return num_degree, denom_degree
Copy link
Owner

Choose a reason for hiding this comment

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

If you check for approximate_num_orbits <= 5 once, you don't need to check for approximate_num_orbits > 5 again in elif. Something like:

if orbits <= 5:
    bla
elif orbits <= 20:

Copy link
Owner

Choose a reason for hiding this comment

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

How did you arrive at these choices? Add some comments about how you got there and how important these particular choices are.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

corrected. These are based on limited investigations. It needs a lot more tests to set more optimal values which I intend to do once this is merged. I have noted this using TODO at the top

= self.get_approximate_degree_for_rational_fit()

rat_fit = self.get_rat_fit(x, y)
t = np.arange(x[0], x[-1], self.t[1] - self.t[0])
Copy link
Owner

Choose a reason for hiding this comment

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

best not to call this t. There is already a self.t, and the natural thing to do is to assume they are the same.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

degrees_were_reduced = False

# Check for nonmonotonicity and lower degrees if needed
while self.check_if_domega_dt_is_nonmonotonic(t, rat_fit(t), 1.0, description):
Copy link
Owner

Choose a reason for hiding this comment

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

Should the function be called something other than check_if_domega_dt_is_nonmonotonic? Isn't the data allowed to be things other than omega?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, I have changed it to check_if_first_derivative_is_not_strictly_monotonic

debug_level=self.debug_level, important=False)

rat_fit = self.get_rat_fit(x, y)
# If no degrees were reduced, try increasing the degree for better fit
Copy link
Owner

Choose a reason for hiding this comment

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

for a better fit

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

self.rational_fit_kwargs["num_degree"], self.rational_fit_kwargs["denom_degree"] \
= new_num_degree, new_denom_degree
new_rat_fit = self.get_rat_fit(x, y)
if self.check_if_domega_dt_is_nonmonotonic(t, new_rat_fit(t), 1.0, description):
Copy link
Owner

Choose a reason for hiding this comment

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

Looks like this check happens twice for the same fit in some cases? Maybe rework the code so that that doesn't happen?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

@@ -1597,6 +1814,33 @@ def measure_ecc(self, tref_in=None, fref_in=None):
# 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 build_omega_gw_extrema_interpolants(self):
Copy link
Owner

Choose a reason for hiding this comment

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

Needs a docstring explaining what it's doing and why.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants