-
Notifications
You must be signed in to change notification settings - Fork 147
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
Real part of complex expMinusOne is bad when e^{-x} ~ cos y #158
Comments
Yup. This is a problem that no one has a good solution to yet. I have a few ideas, but haven't had time to sort through them. As you suggest, the cosm1 factorization is strictly better than the expm1 factorization (it at least gets an absolute error bound for this case, which is why I'm using it). This is discussed superficially in the updated comments here: #152 (which also fixed the typo you caught).
There's not even an easy proof that this suffices--how close can (x,y) be to exp(-x) = cos(y)? This is non-trivial (though there's good reason to believe you won't get much closer than random luck would provide, so using a format twice wider should suffice), and you need to know in order to bound the precision required. For any fixed precision, one could use tables to store base points (x,y) that are unusually close to the curve, and expand around them, but that doesn't work for an implementation that supports generic real types. |
I didn't think very hard about how doing it in extended precision would actually work; mostly I just wanted an excuse to say 'double-Dekker arithmetic'. |
I have some thoughts on this, not sure if it’ll help though. First, if Second, the problem curve approaches Third, when x is small, the problem curve approaches
An entirely different approach would be to look at the distance between When Ignoring that issue, if |
Well, no, because the alternative factorization ( Note that this is pretty easy to make rigorous; we're interested in comparing the magnitude of f(x) = exp(x) - 1 and g(y) = cos(y) - 1 along the curve exp(x)cos(y) = 1. If we solve for y we get: g(y(x)) = cos(acos(exp(-x)) - 1 = exp(-x) - 1 Which is always smaller in magnitude than f(x) when x > 0. To your third point, the final answer is still very nearly zero, so that expression is just going to sum the rounding errors in cosm1 and expm1, no matter how carefully you sum the terms--the fundamental problem remains that you need an extra-precise cos and exp function (and we can't even bound the number of digits you might need--there is some representable point closest to the curve, which I can compute by exhaustive testing for Float, but not for larger types). Note that even just computing the distance to the curve requires ... an extra precise cosine and exp, barring some very clever trick (because of the same cancellation issue). This is a fun research problem, but quite non-trivial. |
My point is that for large enough x, using In other places, like when Of course for Basically, what I’m suggesting is to partition the plane into some number of regions, and use whichever approach is most accurate in each. I could be mistaken, but I believe the threshold for Similarly, the threshold for |
Right, In an additive expression with cancellation, the error comes only from the rounding of the terms that are cancelling. If you look at what we use currently:
Assuming a good-quality host math library, expMinusOne, cos, and cosMinusOne are all computed with small relative error (ideally sub-ulp, a couple of ulp at worst). So we are evaluating an expression of the form a(1+δ₁) + b(1+δ₂) where a and b are the exact real values of (e^x-1)cos(y) and cos(y)-1, respectively and the deltas are bounded by a small multiple of epsilon. Because we're interested in the region close to the critical curve, the addition is computed exactly, so the result is precisely Re(exp(z)-1) + δ₁a + δ₂b, and so the absolute error is bounded by (|a|+|b|)kε for some small k. Note that this analysis holds for all of the expressions you consider, except the "dual" factorization; the way we minimize the error bound is to minimize The dual factorization is actually worse in a lot of ways, because the cancellation doesn't necessarily happen in a single shot, so you have to be careful about how you order the terms (and/or use a compensated sum in some form), and you're still running into the fundamental cancellation issue anyway. |
Forgot to address z very nearly 0 😀 In this case, expMinusOne(x) cos(y) + cosMinusOne(y) becomes x*1 + y^2/2 = x, so there's no loss of accuracy in just using the expression we have. |
I’ll trust your expertise here, but it still seems strange to me that we would intentionally calculate |
It's no less accurate than the alternatives we have (as explained), not significantly slower, and yields considerable simplicity in the implementation. What's not to like? |
For example, in binary32:
x = 1.09590280055999755859375
y = 1.230000019073486328125
The real part of e^{x + iy} - 1, computed in high precision with Sollya and rounded to binary32, is
-3.4550861727211668039672076702117919921875e-8,
but the expression
fma(expm1(x),cos(y), -versin(y))
as used atswift-numerics/Sources/ComplexModule/ElementaryFunctions.swift
Line 117 in c26e60c
-6.9330411633927724324166774749755859375e-8,
in which every digit is wrong.
The problem is catastrophic cancellation when e^x cos y ~ 1. Factoring it to compute the addition with FMA still trips over catastrophic cancellation because (e^x - 1) cos y ~ versin y if e^x cos y ~ 1. Same goes for the other obvious factorings as e^x - 1 - e^x versin y, or e^{x + log cos y} - 1 = e^{x + log(1 - versin y)} - 1, although they do worse when x >>> 1 because the low resolution of floating-point values around y ~ acos(e^{-x}) gives no more opportunity for cancellation of versin y against e^x cos y once y = fl(pi/2).
I don't see any clear way to compute the error in either term short of computing the transcendental functions in extended precision. I guess that might be easy for binary32, but in binary64 you'd presumably have to do the transcendental functions in double-Dekker arithmetic. Maybe there's a clever algebraic trick to avoid the subtraction altogether, but I haven't thought of one.
Nit: There appears to be a typo in the comment at:
swift-numerics/Sources/ComplexModule/ElementaryFunctions.swift
Line 86 in c26e60c
It reads
but it should read
The text was updated successfully, but these errors were encountered: