1.000000000000001 ** float('inf') == float('inf')
while
1.0000000000000001 ** float('inf') == 1.0
What determines the exact threshold here? It seems to be an issue of float precision, where numbers below a certain threshold are considered the same as 1.0
. Is there a way to get better precision in Python?
1.000000000000001 ** float('inf') == float('inf')
while
1.0000000000000001 ** float('inf') == 1.0
What determines the exact threshold here? It seems to be an issue of float precision, where numbers below a certain threshold are considered the same as 1.0
. Is there a way to get better precision in Python?
3 Answers
Reset to default 5Floating point numbers are stored in 53 bits:
IEEE 754 binary64 values contain 53 bits of precision, so on input the computer strives to convert 0.1 to the closest fraction it can of the form
J/2**N
where J is an integer containing exactly 53 bits.
1 / 2**53
is ~1.11e-16
, so any decimal part less than that will be rounded down to 0.
It can more easily be demonstrated with:
>>> 1.000000000000001 == 1.0
False
>>> 1.0000000000000001 == 1.0
True
If you need better precision, you can use Decimal
s:
>>> from decimal import Decimal
>>> Decimal("1.0000000000000001") == Decimal("1.0")
False
As others have said, this is due to IEEE754 floats not being able to represent your constants with the precision you're expecting.
Python provides some useful tools that lets you see what's going on, specifically the math.nextafter
method and and decimal
module.
The decimal module is useful to see the decimal expansion of a float (i.e. how us humans read/write numbers), e.g.:
from decimal import Decimal as D
a = 1.000000000000001
b = 1.0000000000000001
print(D(a), D(b))
which outputs:
1.0000000000000011102230246251565404236316680908203125 1
these are the actual values seen by your computer when you use those constants. As others have said, you can see that b
is actually just 1
so it's "lost" the tailing digit you entered. You just have to know that it's going to do that as a programmer.
To see nearby values the nextafter
method is very useful, e.g.:
import math
c = math.nextafter(1.0, math.inf)
print(c, D(c))
which outputs:
1.0000000000000002 1.0000000000000002220446049250313080847263336181640625
the first number is what Python prints by default and only includes enough precision to be able to unambiguously distinguish the value, the second uses the decimal module and is exact.
Having a play with these is a great way to further understand what you're actually telling your computer to do. Chux also talked about hex representations of floats, in my experience these take a bit longer for people to understand but are another tool to know about when your code seems to be misbehaving. In Python you use
float.hex()
to see the actual bits that make up a float, using the format defined by C99, e.g.:
one = 1.0
print(one.hex(), c.hex(), math.nextafter(c, math.inf).hex())
which will output:
0x1.0000000000000p+0 0x1.0000000000001p+0 0x1.0000000000002p+0
This output is closer to how your computer represents floats. The hexadecimal fractional digits can make these values somewhat awkward to interpret, but you can get there with some more magic numbers (i.e. these come from the spec). The second is saying something like 0x10000000000001 * 2**(0-52)
, the -52
is needed to "shift" the all but the first digit right into the fraction.
Text "1.000000000000001" is converted into a binary64 encoded float
. This value is not exactly representable. The 2 closest choices are shown below in decimal and hexadecimal formats. The closer one is used and is more than 1.0 so ... ** float('inf')
has an infinite result.
// v-------------v 53 bits
1.0000000000000008882... 0x1.0000000000004p+0
"1.000000000000001"
1.0000000000000011102... 0x1.0000000000005p+0 // Closer
Text "1.0000000000000001" is converted into a binary64. This value is not exactly representable either. The 2 closest choices are shown below. The closer one is used and is exactly 1.0 so ... ** float('inf')
has a finite result of 1.0.
1.0000000000000000000 0x1.0000000000000p+0 // Closer
"1.0000000000000001"
1.0000000000000002220... 0x1.0000000000001p+0
What determines the exact threshold here?
Consider the binary64 just above and below 1.0
0.99999999999999988898... 0x1.fffffffffffffp-1
1.0000000000000000000 0x1.0000000000000p+0
1.0000000000000002220... 0x1.0000000000001p+0
... and look at the value half-way between 1.0 and that next value:
// Using an extended FP type
0.99999999999999994449... 0x1.fffffffffffff_8p-1
1.0000000000000000000 0x1.0000000000000_0p+0
1.0000000000000001110... 0x1.0000000000000_8p+0
So "text" values about the [0.99999999999999994449... to 1.0000000000000001110...] range will convert exactly to a binary64 encoded 1.0. With ... ** float('inf')
will have a finite result of 1.0.
Notice the delta above 1.0 is twice the delta below 1.0. This is due to the ULP of the values changes at 1.0.
f"{-0.1:a}"
-->'-0x1.999999999999ap-4'
. – chux Commented Mar 27 at 18:38