-
-
Notifications
You must be signed in to change notification settings - Fork 10.9k
BUG: Increase accuracy of log1p for small inputs #22611
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
base: main
Are you sure you want to change the base?
Conversation
Thanks for looking into a fix so quickly! Before I (or someone else dives in), maybe you have some quick tips to help. Do you have a reference to quickly make sure this is a good implementation? Does it have a speed tradeoff? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also FWIW the test_log1p_complex
tests all pass on main, so they're not actually testing the desired behavior. I think this is expected given the precision involved.
Thanks @mfkasim1. This fixes the one case reported in gh-22609, but it breaks several other cases that are correctly handled by the existing code. The new problems are created by overflow or underflow in the expression
A correct implementation will require scaling the inputs appropriately, similar to what is done for numpy/numpy/core/src/npymath/npy_math_internal.h.src Lines 223 to 253 in 9e144f7
|
@rossbar Thanks for the comment. I've added the decimal arguments in the test to make sure it's checked with a lot of decimal points. |
numpy/core/tests/test_umath.py
Outdated
def test_log1p_complex(self): | ||
assert_almost_equal(ncu.log1p(0.2 + 0.3j), ncu.log(1.2 + 0.3j)) | ||
assert_almost_equal(ncu.log1p(1e-19 + 1e-18j), 1e-19 + 1e-18j, | ||
decimal=23) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect that assert_almost_equal_nulp
is the more convenient way of checking things here.
@WarrenWeckesser if you could want to dig into whether the implementation looks right, that would be great. I would still feel things would be much simpler if we can "steal" an implementation from elsewhere (that has a clearly BSD compatible license).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to centralise ideas (pytorch/pytorch#89214 (comment)), I agree. Now, it's not clear where is this function implemented for complex numbers really...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
I found a good implementation from scipy: https://github.com/scipy/scipy/blob/5b8490a2b80e73aec59af9ac93bca2ebd178fee1/scipy/special/_cunity.pxd#L25-L66 |
@mfkasim1, I'm testing a reformulation of the calculation that seems to work well and shouldn't require higher precision for special cases. Update: The reformulation below doesn't work as well as I originally thought. If I check the relative errors of the real and imaginary parts separately, I see that there are cases where the real parts can have errors that are large. For some inputs, the expression The idea is to use this reformulation of so In the code, this formulation would be used when Here's an implementation in Python that I've been using to test this:
So far, quite a bit of fuzz testing shows that it works well. I use
Compute the expected value of
And this is the expected double precision result:
For this
Fuzz testing shows that the relative error is generally less than 5e-16. I'm still checking this, so don't update your code--it could definitely use another set of eyes. If anyone finds a case where this doesn't work, add a comment here. |
@WarrenWeckesser Thanks for testing the reformulation! Could you give us an example where the formulation above produces large error? |
CircleCI is doing some weird thing here, I hope with the next |
It seems now the best option we have is to follow scipy's implementation. However, I can't see numpy have dependency on cephes's double double (required to handle the special case of cancellation terms) or any other cephes files. Would having the dependency on cephes of interest to numpy? Otherwise, should we just sacrifice the accuracy in that very special case (i.e. similar to calculate |
@mfkasim1, for the function
The function
The true value (as computed with
That shows that the relative error in the real part computed by However, if we look at the closeness of the complex numbers (rather than the relative error of just the real part), we get a relative error of 4.7e-17:
Even if there was interest in having a double-double library to use as a last resort for maintaining precision in special cases, I don't think the SciPy double-double code (in its current state) is a good source. Some of the transcendental functions in that code are buggy (see scipy/scipy#16227). |
As much as I like getting things fully right. @WarrenWeckesser do you have a recommendation for the best improvement that we can easily get right now? |
Ping once more since I remembered this as better than nothing. I think we should do something (maybe just copy whatever pytorch did). I am happy to base this on gut feeling and confidence that the new method is better rather than ideal in this case. But I do trust your gut feeling much more than mine @WarrenWeckesser :). |
@mfkasim1 and @seberg, sorry for not getting back to this for so long. I'm running more fuzz tests this week to compare this PR, my suggested formulation, and the version that is used in the Julia implementation at https://github.com/JuliaLang/julia/blob/c43e5a10be27b7f93b5368875aa1d2596b4d4947/base/complex.jl#L745. I'll report back in a day or so with the results. |
FYI: I ended up exploring a few rabbit holes:
I'm still working on summarizing and coming up with a recommendation for NumPy. |
After lots of experimentation with several versions of the complex log1p, here's the version that I recommend, written as a Python function that accepts scalars:
(The final version should probably have a check for either It is based on the formula for log1p(x) (real argument) given in Theorem 4 of Goldberg's paper "What every computer scientist should know about floating-point arithmetic". The only tweak that I've made is the extra check for I fuzz-tested with large samples clustered around sets such as: the circle |z + 1| = 1; specific points on that circle including 0+0j, -2+0j, -1+1j, -1-1j; the point -1+0j; and a few other scatter points. I also checked on various rays emanating from -1+0j through and beyond the circle |z + 1| = 1. I computed the relative error of the complex result The worst case relative error of the real part of the result depends the relative error of the real part of the underlying On my Mac (m2), using clang, the relative error of the real part of the result could be quite high. That's because the relative error of the real part of clang's
This point is on the circle
The only thing correct about the real part is the sign. The values are so small, however, that the overall relative error of the complex result is also very small:
The only way I could get the relative error of the real part reliably small on the Mac was to use an approach similar to that of SciPy: when the input |
I would say go for it. Anything seems like an improvement here and you covered a lot of bases it seems (enough so to fix a few bugs elsewhere!). |
I've only seen the function for real arguments (e.g. C99 has |
Well, here's another interesting wrinkle: I expected the version based on Theorem 4 to be faster than the double-double implementation. But in fact, when I measure the performance of the two functions implemented in ufunclab, the double-double version is often faster! That needs more investigation, but for now, I think the version based on Theorem 4 is very good. We could implement that while I spend more time than is reasonable on figuring out if a follow-up based on the double-double implementation is worthwhile. 😄 @mfkasim1, are you interested in updating this pull request to implement what I called |
@WarrenWeckesser please go ahead with the new PR. I'm currently on holiday. |
Rather interesting that, after so much coming and going, the final formula is simply to compute |
This PR improves the implementation of `torch.log1p` for complex inputs as mentioned in issue #107022. The new implementation is based on the insights provided in numpy/numpy#22611 (comment). Pull Request resolved: #107100 Approved by: https://github.com/lezcano
This PR improves the implementation of `torch.log1p` for complex inputs as mentioned in issue pytorch#107022. The new implementation is based on the insights provided in numpy/numpy#22611 (comment). Pull Request resolved: pytorch#107100 Approved by: https://github.com/lezcano
Fixes #22609 to increase the accuracy of
log1p
for small inputs. With this PR,log1p
can deserve its reason for existence for complex numbers.