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

Fix numerical instability #149

Merged
merged 10 commits into from
Jun 22, 2022

Conversation

iangrooms
Copy link
Member

From the beginning we've had problems with numerical stability, i.e. roundoff error, that accumulates during the iterative application of the polynomial filter to data. See, for example, #33, #124, and #135, and the things we've tried to improve this, e.g. #67, #130, and #134.

This PR uses exactly the same polynomial approximation of the target filter, but applies the polynomial to the data in a completely different way. The old way was based on factoring the polynomial in terms of its roots and then iteratively applying each factor to the data. The new method is based on an algorithm for evaluating a polynomial using its coordinates in the Chebyshev basis, the algorithm being based on the three-term recurrence for Chebyshev polynomials.

For now I have left the "Numerical Instability" example notebook in, showing that the new code can filter 1/4 degree vorticity data to 10 degrees with no problems. In fact I have not been able to break the new code in any example I've tried including the Taper filter with very large filter factors.

@review-notebook-app
Copy link

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

@NoraLoose
Copy link
Member

Thanks @iangrooms, this sounds really exciting! 🚀

It is a bit difficult for me to review this PR because there are no notes on how the new method exactly operates. Ian, it would be helpful if you could share notes (if available) and/or provide more comments in the code. That way, I could try to match up theory and code.

In addition, I am wondering if we can leverage the verification tests that @rabernat set up in #79 (where we would need verifications not only for kernels but for full filter operators). The rationale is the following: This PR swaps out the central filtering algorithm which this package is based on for another algo. Don't we want to test that the new algo leads to the same/similar filtered fields as the old one?

@iangrooms
Copy link
Member Author

It is a bit difficult for me to review this PR because there are no notes on how the new method exactly operates. Ian, it would be helpful if you could share notes (if available) and/or provide more comments in the code. That way, I could try to match up theory and code.

I think I'll update the Filter Theory section to describe how it works. In exact arithmetic it should produce exactly the same answer as the old method, but the new method is more stable to roundoff errors. Sort of like re-arranging the order of the filter steps.

In addition, I am wondering if we can leverage the verification tests that @rabernat set up in #79 (where we would need verifications not only for kernels but for full filter operators). The rationale is the following: This PR swaps out the central filtering algorithm which this package is based on for another algo. Don't we want to test that the new algo leads to the same/similar filtered fields as the old one?

I probably have to look more closely at this. I guess we could put in some kind of test to compare the result of the new algorithm to the old one, but eventually I think we should just drop the old one and use the new one.

@NoraLoose
Copy link
Member

I think I'll update the Filter Theory section to describe how it works.

Sounds great! I will wait with the review until the changes to the filter theory are pushed.

I guess we could put in some kind of test to compare the result of the new algorithm to the old one, but eventually I think we should just drop the old one and use the new one.

Yes, exactly. As outlined in #79, the idea is to have tests like these:

filtered = filter.apply(data)
np.testing.assert_allclose(filtered, expected)

where expected is a field we save explicitly into the test suite based on the current code.

@codecov-commenter
Copy link

codecov-commenter commented Apr 19, 2022

Codecov Report

Merging #149 (0cbaba3) into master (e4177da) will increase coverage by 0.33%.
The diff coverage is 95.12%.

@@            Coverage Diff             @@
##           master     #149      +/-   ##
==========================================
+ Coverage   97.98%   98.32%   +0.33%     
==========================================
  Files           9        9              
  Lines        1044     1014      -30     
==========================================
- Hits         1023      997      -26     
+ Misses         21       17       -4     
Flag Coverage Δ
unittests 98.32% <95.12%> (+0.33%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
gcm_filters/filter.py 98.01% <95.00%> (+1.55%) ⬆️
tests/test_filter.py 99.55% <100.00%> (-0.02%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update e4177da...0cbaba3. Read the comment docs.

@iangrooms
Copy link
Member Author

I added a section on the theory that underpins the new iterative algorithm for applying the filter to data. @NoraLoose can you take a look?

docs/theory.rst Outdated Show resolved Hide resolved
docs/theory.rst Outdated Show resolved Hide resolved
.. math:: \mathbf{A} = -\left(\frac{2}{s_{\text{max}}}\Delta + \mathbf{I}\right)

be the discrete Laplacian :math:`\Delta` with a rescaling and a shift.
In principle one could apply the filter to a vector of data :math:`\mathbf{f}` by computing the vectors :math:`T_i(\mathbf{A})\mathbf{f}` and then taking a linear combination with weights :math:`c_i`.
Copy link
Member

Choose a reason for hiding this comment

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

Can we reformulate to avoid confusion with the "vector" Laplacian further down?

Copy link
Member Author

Choose a reason for hiding this comment

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

Suggestions for how to do that are welcome. We don't want to be so loose with terminology that it's confusing, but at the same time we don't want to be pedantic.

docs/theory.rst Outdated Show resolved Hide resolved
docs/theory.rst Show resolved Hide resolved
Comment on lines 188 to 189
T_minus_2 = T_minus_1.copy()
T_minus_1 = T_minus_0.copy()
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
T_minus_2 = T_minus_1.copy()
T_minus_1 = T_minus_0.copy()
T_minus_2 = T_minus_1
T_minus_1 = T_minus_0

Comment on lines 264 to 267
uT_minus_2 = uT_minus_1.copy()
uT_minus_1 = uT_minus_0.copy()
vT_minus_2 = vT_minus_1.copy()
vT_minus_1 = vT_minus_0.copy()
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
uT_minus_2 = uT_minus_1.copy()
uT_minus_1 = uT_minus_0.copy()
vT_minus_2 = vT_minus_1.copy()
vT_minus_1 = vT_minus_0.copy()
uT_minus_2 = uT_minus_1
uT_minus_1 = uT_minus_0
vT_minus_2 = vT_minus_1
vT_minus_1 = vT_minus_0

Comment on lines 241 to 242
uT_minus_2 = ufield_bar.copy()
vT_minus_2 = vfield_bar.copy()
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
uT_minus_2 = ufield_bar.copy()
vT_minus_2 = vfield_bar.copy()
uT_minus_2 = ufield_bar
vT_minus_2 = vfield_bar

field_bar += (
temp_l * 2 * np.real(s_b) / np.abs(s_b) ** 2
+ temp_b * 1 / np.abs(s_b) ** 2
T_minus_2 = field_bar.copy()
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
T_minus_2 = field_bar.copy()
T_minus_2 = field_bar

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm responding here for all of the .copy() changes. I'm afraid to remove these because Python might just have T_minus_2 and field_bar point to the same thing, such that every time we update T_minus_2 it also updates field_bar, which we don't want.

Comment on lines 33 to 34
1: {"offset": 2.2, "factor": 0.6, "exponent": 2.5, "max_filter_factor": 67},
2: {"offset": 3.2, "factor": 0.7, "exponent": 2.7, "max_filter_factor": 77},
Copy link
Member

Choose a reason for hiding this comment

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

Do we still need max_filter_factor? Do we still want to throw numerical instability warnings? What are these values based on?

@NoraLoose
Copy link
Member

Looks great @iangrooms! I left some minor comments above.

Two bigger comments are:

  1. We should probably delete the numerical instability notebook because it is confusing: it says the filter becomes unstable, but then it just doesn't happen.
  2. Since this PR involves major refactoring of the code (the central filter algorithm is essentially swapped out for a new one!), I want to re-iterate what I said above: we need verification tests that the new and old algorithm give the same / similar results. I think we can do this in a similar manner as set up by @rabernat in Refactor kernel tests #79, but we need verifications not only for kernels but for full filter operators. I'm happy to hear other opinions.

iangrooms and others added 3 commits May 4, 2022 16:40
Co-authored-by: Nora Loose <NoraLoose@users.noreply.github.com>
@iangrooms
Copy link
Member Author

Thanks for the code review! In response to the two big comments:

  1. I agree that we should delete the Numerical Instability notebook before merging the PR. I'm leaving it in for now in case anyone wants to look at it and verify that indeed, the new algorithm does not suffer from numerical instability.
  2. I can work to provide some evidence that the old and new algorithms give the same results, at least when the old one is stable. But setting up a new suite of tests to verify that the filters produce the expected results (not necessarily 'the same as the old algorithm', which would be unnecessary if we believe the new algorithm is correct, not to mention being onerous to implement) seems outside the scope of this PR. I think it needs its own issue (cf Refactor kernel tests #79) and its own PR.

@NoraLoose
Copy link
Member

which would be unnecessary if we believe the new algorithm is correct, not to mention being onerous to implement

The point of the verification tests would be that we don't have to "believe". They would catch any bugs in the derivation / implementation of the new algorithm, which we may have overseen by simply looking at the code.

I think it needs its own issue (cf #79) and its own PR.

Sure, we can make the verification tests its own issue and PR. But I think those should be resolved first, before we can merge this PR. I can help with these tests.

@iangrooms
Copy link
Member Author

iangrooms commented May 5, 2022

I ran the old and new algorithms on the numerical instability notebook. I saved the result of the old algorithm (zeta_2Deg) then plotted a comparison with the result of the new algorithm. The plot is in the latest commit of the numerical instability notebook. The difference is about 7 orders of magnitude smaller than the field itself. So it looks like the old and new algorithms are giving the same results up to roundoff errors (single precision), at least in situations where the old algorithm is stable. Between the theory, the code implementing the theory, and the comparison of the old and new methods, I hope this is enough to convince that the new algorithm works as advertised.

@NoraLoose
Copy link
Member

NoraLoose commented Jun 9, 2022

Hi @iangrooms, we merged #153 so we can finalize this PR too. Here are some final minor things that have to be done / discussed:

  • Remove numerical instability notebook?
  • What to do about the "Factoring the Gaussian Filter" option (including the docs section)? Do we want to get rid off it because there is no reason to use this functionality, now that numerical instability is not a problem anymore? Or do we want to keep it, to not lose backward compatibility? (If we bump up to version v0.3, it is okay to drop the feature as long as we communicate it.) If we keep it, we should add some sentences at the top of the "Factoring the Gaussian Filter" docs section, to the effect of: "Using this feature is not recommended, and only necessary if you run into numerical instability issues, which you most likely won't."
  • Merge the master branch into your branch three_term_recurrence, and run the tests without overwriting the test data. (You won't overwrite anything if you simply run pytest and don't mess with the GCM_FILTERS_OVERWRITE_TEST_DATA variable.) If the tests pass, the filter algorithm gives the same result as the previous one.

@iangrooms
Copy link
Member Author

I'll merge the new tests. In the meantime, I do plan to remove the numerical instability notebook as well as any related comments in the docs, and the 'factoring the Gaussian.' It's a big update, so I think it makes sense to bump the version number. The old version will still be available if anyone needs it.

@NoraLoose
Copy link
Member

If we remove the "Factoring the Gaussian" section in the docs, we should probably also delete the parameter n_iterations in the code (+ functionality associated to it). I'm fine with this.

Removes discussion of the factored/iterated Gaussian filter from the docs
Removes the numerical instability notebook
Slightly updates the theory section of the docs
@iangrooms
Copy link
Member Author

I've removed the numerical instability notebook, removed the factored Gaussian from the docs and code, and merged the latest updates to testing. For some reason building the docs fails (all other tests pass); I noticed that this happened after I pulled in the latest changes from the master branch and before I made my own changes. Not sure how to fix it. Can @NoraLoose or @rabernat take a look?

@NoraLoose
Copy link
Member

I can build the docs locally for this branch. Not sure what is going on either.

@rabernat
Copy link
Contributor

I am guessing what happened is that some version has changed in the readthedocs environment, since we are not pinning specific versions. It looks a bit like this: pydata/pydata-sphinx-theme#511

I will try to dig deeper. If you want, you can merge this PR and we will fix the docs in a separate PR.

@iangrooms
Copy link
Member Author

I can also build the docs locally, so if you're both agreed then I think it's OK to merge this PR and fix the docs separately.

@iangrooms iangrooms merged commit 3505365 into ocean-eddy-cpt:master Jun 22, 2022
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.

None yet

4 participants