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
BUG: Binary operations between uint64 and intX results in float64 #20905
I expect to create a python Fraction from numpy integer types with no loss of precision
Actual Behavior:
Types are coerced to numpy.float64 and precision is lost
Reproducible code example:
importnumpyasnpfromfractionsimportFractionnums=np.array([1])
denoms=np.array([18446744073709550592], dtype='uint64')
f=Fraction(nums[0], denoms[0]) # denom has been rounded, is now a floatprint(f) # 1.0/1.8446744073709552e+19print(type(f.denominator)) # <class 'numpy.float64'>
I discovered this behavior while creating fractions from parallel arrays representing the numerators and denominators of fractions. This loss of precision is problematic because the float is rounded up so that it is out of range for uint64, despite the original value being valid. In my case I have a workaround, as converting the values to python int before inputting them to the Fraction constructor prevents this issue. However, I wanted to let you know because it is unexpected that an operation with two numpy integer types produced a value with floating point types.
I briefly looked into the Fractions constructor and I discovered that this behavior comes from lines 175-180 of fractions.py:
Inspecting the reproducible snippet above with pdb reveals that the multiplication on lines 178-179 of fractions.py is the moment when the type is changed.
@soma2000-lang I just discovered bug report #5745, which is almost identical. The only difference is that in my case the operator is * and in that case the operator is +
Sounds like it needs a decent proposal for what to do with uint64 + int64 behaviour and how to pull of that transition. To be clear, I would love a simple and easy proposal, but until we have that there is nothing to be done really. However, unless you want to think about that (which is great!), I am not sure there is anything straight forward to do here @soma2000-lang.
We could even consider just adding a warning when this type of promotion happens (that should be very simple)... But, I personally don't have a very clear gut feeling on what we should do, and I have currently more important things to lose sanity over :).
I would deeply appreciate a warning of some kind, as well as some clear documentation on when type coercions happen. I spent a full workday chasing down this behavior.
I'd be willing to write up an MR for improved documentation, if you can point me to the code where type coercions happen.
Personally I'd like int64 and uint64 operations to be coerced to int64. That would be my proposal. I'd rather have overflow than a loss of precision when I specifically chose an integer type for its discrete nature. Ideally (and this is a stretch, I don't expect it to happen any time soon) I think implementing a variable-width numpy integer type would be really handy.
Which just looks it up in another place that is a funny generated table though. In any case, that is the place where you would have to change it (by introducing a new function probably, and using that for int64 or uint64 – which ever has the "larger typenumber", just because that is how it is implemented currently). – Well, that is the path forward to add a warning at least. Plus, may need to check that nothing else directly reads that table. Nothing should, but I am not sure.
If I have time this week I'll take a look. I'm going to start with documentation so other people that experience this issue at least have a reference for what is expected
Reacted by Sebastian Berg, Somasree Majumder, Jeppe Dakin and Muhammad Yasirroni
changed the title [-]BUG: Creating a `Fraction` from numpy integer types coerces types to `numpy.float64`[/-][+]BUG: Binary operations between uint64 and intX scalars results in float64 [/+]on Feb 1, 2022
Sounds like needs a decent proposal for what to do with uint64 + int64 behaviour and how to pull of that transition.
Option 1) from seibert in #5745 seems like the obvious thing to do. Follow C/C++ and let uint64 and int64 result in int64, with the danger of overflow being present.
However, the lower-bit scalars all promote to the next higher signed integer type, e.g. uint32 and int32 combine to int64, while uint16 and int16 combine to int32.
While safe from a value perspective, this feels somewhat odd when specifically working with a "fixed size" data type.
To continue this trend, uint64 and int64 should combine to int128, which does not exist. Even if it did, this would just move the problem up one level, not remove it. Alternatively, to end this infinite regress, the rule could be that uint64 and int64 combine to the standard Python (3) int, which has arbitrary precision. To keep things "within" NumPy, this would properly require a new dtype, someting like a new np.pyint, which more or less is just an alias for the standard Python int.
The Python int looks like a reasonable fix. It would also fit in with the proposal for typed object arrays, that is, arrays that only contain one type of object.
I would object converting to int being inherently better than converting to float.
"Intuitively", uint64 + positive value should never result in a negative value.
That being said, nothing about uint + int = float is intuitive, either.
I would say, in the case of uint64 + intX:
warning would definitely be better than quietly coercing to float
obviously the same goes for the case of coercing the result to int64 or uint64
even downright refusing to go forward and raising an exception would be better than silent unexpected behaviour
I am not saying this should be the way to go, though
changed the title [-]BUG: Binary operations between uint64 and intX scalars results in float64 [/-][+]BUG: Binary operations between uint64 and intX results in float64 [/+]on May 12, 2022
We've never advertised ourselves as strictly-typed, whatever that means to you. We have types with promotion rules, most of which are fairly reasonable, but some dark corners where there aren't many obviously great alternatives (and it's not clear to me that raising an exception to be "strictly-typed" here is a better alternative, though one could certainly argue the point).
Well point is that there are certain rules about type conversion in Python. Int should not magically turn into float when added to another int, for example. Quietly coercing uint64+uint64 to float64 is not just inappropriate from the point of view of possible overflow/loss of precision, but is also the kind of loose type magic that terrible commercial tools like matlab really like. However, in this particular case numpy is clearly behind:
>>>a=np.array([2**64-1],dtype=np.uint64)
>>>int(a[0]-1) ==int((a-1)[0])
False# I would expect a True here
For comparison, the abominable Matlab gets this one correct, and preserves the type of the resulting array as expected
>> uint64(42) +1
ans =uint6443
>> a=[uint64(2)^63 ];
>> b=a-1
b =uint649223372036854775807
>> b(1) == (a(1)-1)
ans =logical1
PS: Matlab has other, more serious problems, of course:
I understand your frustration here: uint64/int64 promotion is an unfortunate corner case in NumPy's documented type promotion semantics, but it's something that has been baked-in for a quarter century now. Making that kind of backward-incompatible change is a rather arduous process, for good reason: stability in tools like NumPy is hugely important to the various communities that depend on them.
But the community of numpy developers is thinking about these things and how they can be improved.
Maybe it would be nice to have this issue a warning? This way current users can know about possible bugs (I doubt anyone is relying on this sort of promotion in production). Making this an error is not even necessary as long as there is a way to actually run the math operations in uint64 that is reliable. This would keep backwards compatibility while enabling this to get fixed eventually.
Activity
soma2000-lang commentedon Jan 27, 2022
I am working on this @paigeweber13
c3cuddles commentedon Jan 27, 2022
@soma2000-lang I just discovered bug report #5745, which is almost identical. The only difference is that in my case the operator is
*and in that case the operator is+seberg commentedon Jan 27, 2022
Sounds like it needs a decent proposal for what to do with
uint64 + int64behaviour and how to pull of that transition. To be clear, I would love a simple and easy proposal, but until we have that there is nothing to be done really. However, unless you want to think about that (which is great!), I am not sure there is anything straight forward to do here @soma2000-lang.We could even consider just adding a warning when this type of promotion happens (that should be very simple)... But, I personally don't have a very clear gut feeling on what we should do, and I have currently more important things to lose sanity over :).
c3cuddles commentedon Jan 27, 2022
I would deeply appreciate a warning of some kind, as well as some clear documentation on when type coercions happen. I spent a full workday chasing down this behavior.
I'd be willing to write up an MR for improved documentation, if you can point me to the code where type coercions happen.
Personally I'd like
int64anduint64operations to be coerced toint64. That would be my proposal. I'd rather have overflow than a loss of precision when I specifically chose an integer type for its discrete nature. Ideally (and this is a stretch, I don't expect it to happen any time soon) I think implementing a variable-width numpy integer type would be really handy.seberg commentedon Jan 27, 2022
This is the place that it is defined:
numpy/numpy/core/src/multiarray/dtypemeta.c
Lines 382 to 406 in 23dfdfc
Which just looks it up in another place that is a funny generated table though. In any case, that is the place where you would have to change it (by introducing a new function probably, and using that for
int64oruint64– which ever has the "larger typenumber", just because that is how it is implemented currently). – Well, that is the path forward to add a warning at least. Plus, may need to check that nothing else directly reads that table. Nothing should, but I am not sure.c3cuddles commentedon Jan 27, 2022
If I have time this week I'll take a look. I'm going to start with documentation so other people that experience this issue at least have a reference for what is expected
[-]BUG: Creating a `Fraction` from numpy integer types coerces types to `numpy.float64`[/-][+]BUG: Binary operations between uint64 and intX scalars results in float64 [/+]jmd-dk commentedon Feb 1, 2022
Option 1) from seibert in #5745 seems like the obvious thing to do. Follow C/C++ and let
uint64andint64result inint64, with the danger of overflow being present.However, the lower-bit scalars all promote to the next higher signed integer type, e.g.
uint32andint32combine toint64, whileuint16andint16combine toint32.uint64andint64should combine toint128, which does not exist. Even if it did, this would just move the problem up one level, not remove it. Alternatively, to end this infinite regress, the rule could be thatuint64andint64combine to the standard Python (3)int, which has arbitrary precision. To keep things "within" NumPy, this would properly require a newdtype, someting like a newnp.pyint, which more or less is just an alias for the standard Pythonint.charris commentedon Feb 2, 2022
The Python int looks like a reasonable fix. It would also fit in with the proposal for typed object arrays, that is, arrays that only contain one type of object.
joni-herttuainen commentedon Feb 9, 2022
Also found out about this recently. My two cents:
I would object converting to int being inherently better than converting to float.
"Intuitively",
uint64+ positive value should never result in a negative value.That being said, nothing about
uint + int = floatis intuitive, either.I would say, in the case of
uint64 + intX:int64oruint64[-]BUG: Binary operations between uint64 and intX scalars results in float64 [/-][+]BUG: Binary operations between uint64 and intX results in float64 [/+]Avoid type coercion due to numbe uint behaviour
Avoid type coercion due to numbe uint behaviour
Avoid type coercion due to numbe uint behaviour
burlen commentedon Mar 31, 2023
#23476 is a variation on the theme. +1 to for the proposal mentioned above to follow the C/C++ behavior.
alexpyattaev commentedon Nov 15, 2023
Until I found this issue I was promoting numpy as a strictly typed alternative to the nonsense of matlab. Please stop proving me wrong.
rkern commentedon Nov 15, 2023
We've never advertised ourselves as strictly-typed, whatever that means to you. We have types with promotion rules, most of which are fairly reasonable, but some dark corners where there aren't many obviously great alternatives (and it's not clear to me that raising an exception to be "strictly-typed" here is a better alternative, though one could certainly argue the point).
alexpyattaev commentedon Nov 16, 2023
Well point is that there are certain rules about type conversion in Python. Int should not magically turn into float when added to another int, for example. Quietly coercing uint64+uint64 to float64 is not just inappropriate from the point of view of possible overflow/loss of precision, but is also the kind of loose type magic that terrible commercial tools like matlab really like. However, in this particular case numpy is clearly behind:
For comparison, the abominable Matlab gets this one correct, and preserves the type of the resulting array as expected
PS: Matlab has other, more serious problems, of course:
jakevdp commentedon Nov 16, 2023
@alexpyattaev Just a note, I think this particular issue (adding
1to a uint64 causing promotion) would be addressed by the "weak promotion" semantics of NEP 50; see https://numpy.org/neps/nep-0050-scalar-promotion.html#proposed-weak-promotion.I understand your frustration here: uint64/int64 promotion is an unfortunate corner case in NumPy's documented type promotion semantics, but it's something that has been baked-in for a quarter century now. Making that kind of backward-incompatible change is a rather arduous process, for good reason: stability in tools like NumPy is hugely important to the various communities that depend on them.
But the community of numpy developers is thinking about these things and how they can be improved.
alexpyattaev commentedon Nov 19, 2023
Maybe it would be nice to have this issue a warning? This way current users can know about possible bugs (I doubt anyone is relying on this sort of promotion in production). Making this an error is not even necessary as long as there is a way to actually run the math operations in uint64 that is reliable. This would keep backwards compatibility while enabling this to get fixed eventually.