Fix NaN arithmetic - #81
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #81 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 6 6
Lines 266 272 +6
=========================================
+ Hits 266 272 +6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
c280ca9 to
0e60f05
Compare
3d6242f to
699f9bd
Compare
|
|
||
| @inline _sb(x) = signbit(x) | ||
| @inline _sb(x::Complex) = angle(x)/π # overloading `signbit` causes type piracy | ||
| @inline _sb(x::Complex) = angle(x)/π # overloading `signbit` causes type piracy |
There was a problem hiding this comment.
Hahaha this is exactly what I just suggested!
There was a problem hiding this comment.
Btw For every θ that is "natural" in base 2 (1, -1, 1/2, 1/4, 3/4, etc.) it turns out angle(exp(im*θ)) == θ. This seems like a a very lucky coincidence that rounding goes in the right direction!
There was a problem hiding this comment.
Hahaha this is exactly what I just suggested!
Yes, but it only covers part of it, because we still have no defined interval the value is in and we still have a very strange format so store an angle when we have an integer. So this needs more thought.
There was a problem hiding this comment.
Btw For every
θthat is "natural" in base 2 (1, -1, 1/2, 1/4, 3/4, etc.) it turns outangle(exp(im*θ)) == θ. This seems like a a very lucky coincidence that rounding goes in the right direction!
You are definitely onto something here. While it is true for all your numbers, it interestingly is not true, e.g., for 1/8:
julia> angle(exp(im*π/8))/π
0.12500000000000003So although 1/8 is exact in floating points, the calculation is not exact. That not only means that our tests are incomplete here (I already added some, but have not yet pushed). It also means that we need to check whether this loss of precision is a problem anywhere. And there are quite some locations where we do the exact comparison.
But I need to do this with a fresh mind tomorrow.
Especially as this lead me to toinf and I currently think we have a bug in toinf:
julia> angle(toinf(complex(0,Inf)))
4.934802200544679
julia> angle(complex(0,Inf))
1.5707963267948966And I don't understand how we can have such a large bug here without the test suite detecting it. So I probably need to have a fresh look tomorrow to see why this is not a bug and only my current understanding is incomplete.
There was a problem hiding this comment.
It was indeed a bug and there is an explanation why the tests haven't caught it. Fix, details and off-axis tests in #85. So for all I know this conversation is resolved.
There was a problem hiding this comment.
The really important case is that for r > 0 where r is any real we always have _sb(r+0im) = zero(r), _sb(-r±0im) = one(r), _sb(r*im) = one(r)/2, _sb(-r*im) = -one(r)/2
so we want this to be correct for BigFloat and other types too. Actually, I'm not sure what the types should be (I guess for <: Real we want them to be Bool)
There was a problem hiding this comment.
I assume you mean == as comparison. What do you mean with "types"? The storage type or the return type?
I am currently trying a combination of UInt64 as a storage type and using sign with a Float64 as a possible user interface in addition to other methods for Base functions.
This seems to work well up to now (unique values, some precision everywhere, wrapping for free, usage without rounding possible), but I can't yet see whether this will work in the whole package, which is the relevant question in the end.
There was a problem hiding this comment.
This is a bit of a fringe question so I don't think any choice makes a big practical choice. But what I mean is if _sb(x::Real) = signbit(x) then the return type is a Bool, but for _sb(::Complex{<:Union{Int,Float64}) it should probably be a Float64. But then what about _sb(::Complex32) and _sb(::Complex{BigFloat})? Probably Float32 and BigFloat. (which I believe your implementation does)
There was a problem hiding this comment.
I guess the concern is: do we actually know that rounding works correctly for all BigFloat? Eg, we have
julia> setprecision(10) do
angle(one(BigFloat)im)
end
1.5703
julia> setprecision(20) do
angle(one(BigFloat)im)
end
1.570797
julia> setprecision(40) do
angle(one(BigFloat)im)
end
1.5707963267942There is no obvious guarantee that when we divide this by π we get exactly 1/2.
Except.....in tests we do up to precision 10_000:
julia> for p in 1:10_000
setprecision(p) do
@test angle(one(BigFloat)im)/π == 1/2
end
endWhich is to say: I think by luck we are fine.
There was a problem hiding this comment.
I assume you talking about #81 and not about the possible future solution.
I think in general we can't be exact with floats. The user can always have any arbitrary transformations between two points in time and when they compare then according to what should be identical with two mathematical reals it can be different in our implementation.
That being said, I think in this case it is probably more than luck. I am speculating, because I haven't followed into MPFR's implementation, but your example boils down to:
atan(one(BigFloat), zero(BigFloat))/π == 1/2which is
atan(one(BigFloat), zero(BigFloat)) == π/2and I strongly assume that this special cases such that atan will return the best rounding of π/2 according to the current precision if the first argument is positive and the second is 0. This triggers reliably because 1 and 0 are exact. π and π/2 will then have the same number in the significant and only differ in the mantissa by exactly 1 leading to 0.5 exactly in the division.
So while it would be possible to implement that in a different way, I think an implementation which cares about precision will always return the exact value here, despite being a floating point operation. That's just a special case which is exact when using binary floating points. It wouldn't for some other base in general, but it is with base 2.
|
Once the conflict is fixed I can merge this |
`Base` promotes every `Real` to `BigFloat`, so `Base._promote` handed the arithmetic two `BigFloat`s and `__add`, `__mul` and `_infpow` no longer matched: `big(2.0) + ∞`, `big(2.0) * ∞` and `(+∞)^big(2.0)` were all `MethodError`s. `BigInt` was unaffected, the `Integer` rule catching it first, and so were `ℵ₀` and `ComplexInfinity`, neither of which promotes to a float.
`NaN + ∞` gave `∞`, `NaN * ∞` gave `+∞`, `div(NaN, ∞)` gave `0.0` and `(+∞)^NaN` gave `0.0`, where the same expressions over the floats all give `NaN`. An argument that was not an infinity was treated as negligible, so a `NaN` marking a failed computation silently became a plausible infinity. Each entry point now returns its `NaN` argument unchanged, which keeps the precision as well: `NaN32 * ∞ === NaN32`. `isnan` answers for every `Number` and folds to `false` for the types that carry no `NaN`, so nothing changes for them. A float argument widens the inferred return type by one union member and still allocates nothing.
For a float parameter it returned the field itself, so `signbit(ComplexInfinity(0.5))` was `0.5` and any caller branching on it hit a `TypeError`. It now answers whether the infinity points along the negative real axis, for every parameter type, which is what the `Bool` and `Integer` methods already did and what `Base` guarantees. One method replaces the three, as `mod(signbit, 2) == 1` covers them all. The two places that wanted the whole angle rather than its sign take the field directly.
The three bugs found so far were all the same shape: an infinity behaving differently from `Inf` in a case nobody had enumerated. The table asks each comparison and each of `max` and `min` for the same answer as the matching float infinity, over a list of values that includes both zeros, both `NaN` precisions, the subnormal and the largest finite float.
699f9bd to
cfba27d
Compare
Let me please have a look at your rounding finding first. Because this might indeed not be the best way to do it. I am not sure whether this is guaranteed in all cases and whether we are missing a "not on one of the complex axes" case. |
Builds on theI recommend to review again commit-by-commit. The most recent 4 commits are new.infinity_directionbranch, so we can rebase the current PR onmasteronce #80 has landed to reduce the overall diff size (we both have the permissions).Basepromotes everyRealtoBigFloat, soBase._promoteconverted the infinity away andbig(2.0) + ∞,big(2.0) * ∞and(+∞)^big(2.0)were allMethodErrors, which also made theNaNwork below untestable at that precision.NaN + ∞gave∞,NaN * ∞gave+∞,div(NaN, ∞)gave0.0and(+∞)^NaNgave0.0, so aNaNmarking a failed computation silently became a plausible infinity; every entry point now returns itsNaNargument unchanged, precision included.signbit(ComplexInfinity(0.5))returned the field itself,0.5, so any caller branching on it hit aTypeError. The three methods are replaced by one correct method.This PR changes inference, but it was necessary to have a correct result:
x::Float64 + ∞now infersUnion{Float64, Infinity}rather thanInfinity(but no allocations)x::Int + ∞is unchanged becauseisnanfolds away for types that have noNaN.