You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
np.around uses an inexact algorithm, not mentioned in docstring #14391
I came across this from this discussion of how CPython rounds floating point values to decimal places in round(). The two examples below show what CPython does (correct), and the last two what numpy does with a fast but inexact strategy:
I don't see any past issue for this, and the around code and docstring seems to have been last touched over 11 years ago, since the last commit is the copy-n-paste from numeric.
The np.around docstring does contain the cryptic remark that "Results may also be surprising due
to the inexact representation of decimal fractions in the IEEE floating point standard [1]_ and errors introduced when scaling by powers of ten."
If it is indeed the case that there is no "fast" algorithm for decimal rounding, I think the docstring should at least be elaborated to make clear what approximation is being made. Eg, that we compute np.true_divide(x*10**d, 10**d), which has floating point error, instead of a more accurate BigInt computation.
Some brainstorming on if/how np.around might actually be fixed, rather than just changing the docstring. The goal is to get np.around to return the nearest floating-point value to the correctly rounded float.
Instead of falling back to the (slow) string-formatting code like CPython, maybe there is a simpler computation to do: As noted in the link, the basic computation we need is done in decimal string-printing code (eg our dragon4 code) which uses BigInt manipulation to do decimal arithmetic of the binary float mantissa/exponent. That involves a large number of BigInt multiplies and divides.
It feels like we can greatly simplify that if all we want is decimal rounding.
Instead of so many mul/div in dragon4, it seems like we should be able to get away with one of each, as the basic computation we want to do is rint(x * (10**d))/10**d which has 1 mul and 1 div. Lets unpack a float64 number into integer exponent e and mantissa m, so x = m/2^(52-e) (lets ignore sign for now). Then x*10**d in the formula becomes (m*10**d)/2^(52-e). Then the rint involves flooring the numerator to the nearest multiple of 2^(52-e), which means just masking the lower bits of the numerator since the denom is a power of 2. Then we divide by 10**d, which I think we can do on the numerator only since any lower bits would get lost by renormalization anyway. So the computation is something like m' = maskbits(m * 10**d, 52-e)/10**d using integer arithmetic only, and we leave e alone.
Here's a toy implementation using Python's builtin BigInts. It also has some extra steps to get rounding right.
defround(x, d):
# use Python's BigInt implementation to round mantissas, m, e=unpack_sme(x)
m*=10**dm+=2**(52-e-1) # add 0.5 for correct rounding (can optimize: lower bits unchanged)m&=~(2**(52-e)-1) # rint (round to integer, ie multiple of 2**(52-e))m+= (10**d)//2# add another 0.5 prec to get right // rounding (?)m//=10**d# Uses: one MulPow10, one DivPow10, one shift/add op, one mask op, # and one AddPow10/2.# may need some extra steps here to normalize float, especially for# negative d (this function designed for positive d so far)returnpack_sme(s, m, e)
defunpack_sme(x):
"return sign, mantissa, exponent"x=np.float64(x).view(np.int64)
m= (x&0xfffffffffffff)|(1<<52)
e= ((x>>52)&0x7ff) -1023s=x>>63returnint(s), int(m), int(e)
defpack_sme(s, m, e):
x=np.int64(0)
x|=s<<63x|= (e+1023)<<52x|=m&~(1<<52)
returnx.view(float64)
round(12.34567, 2) # 12.35round(56294995342131.5, 3) # 56294995342131.5
I haven't worked through it rigorously to make sure it is mathematically correct, this is just to sketch the idea. We might be able to re-use the BigInt_MultiplyPow10 and the like from dragon4, or at least all the PowerOf10 tables of values.
Or, if we limit the allowed values of decimals, we might get away with a very simple BigInt implementation.
Activity
ahaldane commentedon Aug 29, 2019
Some brainstorming on if/how
np.aroundmight actually be fixed, rather than just changing the docstring. The goal is to getnp.aroundto return the nearest floating-point value to the correctly rounded float.Instead of falling back to the (slow) string-formatting code like CPython, maybe there is a simpler computation to do: As noted in the link, the basic computation we need is done in decimal string-printing code (eg our dragon4 code) which uses BigInt manipulation to do decimal arithmetic of the binary float mantissa/exponent. That involves a large number of BigInt multiplies and divides.
It feels like we can greatly simplify that if all we want is decimal rounding.
Instead of so many mul/div in dragon4, it seems like we should be able to get away with one of each, as the basic computation we want to do is
rint(x * (10**d))/10**dwhich has 1 mul and 1 div. Lets unpack a float64 number into integer exponenteand mantissam, sox = m/2^(52-e)(lets ignore sign for now). Thenx*10**din the formula becomes(m*10**d)/2^(52-e). Then the rint involves flooring the numerator to the nearest multiple of2^(52-e), which means just masking the lower bits of the numerator since the denom is a power of 2. Then we divide by10**d, which I think we can do on the numerator only since any lower bits would get lost by renormalization anyway. So the computation is something likem' = maskbits(m * 10**d, 52-e)/10**dusing integer arithmetic only, and we leaveealone.Here's a toy implementation using Python's builtin BigInts. It also has some extra steps to get rounding right.
I haven't worked through it rigorously to make sure it is mathematically correct, this is just to sketch the idea. We might be able to re-use the
BigInt_MultiplyPow10and the like from dragon4, or at least all the PowerOf10 tables of values.Or, if we limit the allowed values of
decimals, we might get away with a very simple BigInt implementation.DOC: update np.around docstring with note about floating-point error