Skip to content

np.uint64 + int == ... np.float64 ?? #7126

Closed

Description

@dalexander

Is this behavior expected?

>>> print type(np.uint32(0) + int(0))
<type 'numpy.int64'>
>>> print type(np.uint64(0) + int(0))
<type 'numpy.float64'>

The latter took me by surprise!

Activity

  1. matthew-brett commented on Jan 27, 2016

    @matthew-brett
    Contributor

    It is maybe not what you'd guess at first blush, but it is deliberate, at least, because the floating point dtype is the only type that contains any possible np.uint64 value plus any possible int value.

    For example, what would you expect for this?:

    np.uint64(2**64-1) + int(2)
    
  2. dalexander commented on Jan 27, 2016

    @dalexander
    Author

    OK, that's fair.

    In your example, I probably would have preferred an overflow than an upcast to floating point, but I'm sure there are arguments for the upcast behavior.

  3. rkern commented on Jan 27, 2016

    @rkern
    Member

    It's not quite as simple as that. For example a uint64+uint64 operation results in a uint64 (with an overflow warning) although if we applied @matthew-brett's reasoning we should be returning a float64 as well.

    Rather, it's a confluence of several factors. The implementation of the numpy.add() ufunc that underlies these routines only has type-specific implementations where the two arguments are of the same type. So the ufunc system needs to cast the arguments to a common type. One of the arguments is signed, so both arguments need to be casted to a signed type. The smallest signed type that can represent values up through the maximum range of uint64 is float64 (Note! Not all uint64 values can be represented as float64 floats! Precision is lost! But it's better than int64 where the whole upper half of the values are lost.). We can't really look at the value to determine the output type because the same logic needs to apply to arrays as well. This is an unfortunate result, but the tower of integer types needs to top out somewhere.

  4. sem-geologist commented on Jan 31, 2016

    @sem-geologist

    This behaviour also buged my program. Why it couldn't simply overrun leaving responsibility for user. Casting silently to float64 is bad... If this is "normal" behaviour, it should be at least documented in official documentation.

  5. NeilGirdhar commented on Jan 24, 2017

    @NeilGirdhar
    Contributor

    When numpy integral types are added to each other, they wrap around but preserve the output type, but when you add a Python integer to them, the return type changes. So there is no consistency between numpy integers and Python integers.

    The smallest signed type that can represent values up through the maximum range of uint64 is float64…

    To be consistent with this type-promotion logic, adding two float32s should cast to float64 since you don't know whether the mantissa of a float32 can represent the whole result. Similarly, dividing a float32 by 2 should cast it to float64 since you don't know the exponent can hold the result.

    This is an unfortunate result, but the tower of integer types needs to top out somewhere.

    That's true, but I don't feel like it should be topping out on a non-integer type.

  6. rkern commented on Jan 25, 2017

    @rkern
    Member

    To be consistent with this type-promotion logic, adding two float32s should cast to float64 since you don't know whether the mantissa of a float32 can represent the whole result. Similarly, dividing a float32 by 2 should cast it to float64 since you don't know the exponent can hold the result.

    You misread me. The ability of the dtype to represent the result never factors in. The logic is based solely on the ability of the dtype to represent both of the inputs.

  7. NeilGirdhar commented on Jan 25, 2017

    @NeilGirdhar
    Contributor

    Okay, that makes sense. But as you say, even float64 can't represent all of the inputs. There has got to be a better (integer) candidate?

  8. rkern commented on Jan 25, 2017

    @rkern
    Member

    I mean, you can continue extending the integer hierarchy ad infinitum if you are willing to write software implementations of all of the ufunc operations (i.e. in such a way that they don't rely on C supporting them as native datatypes). There is no existing integer dtype candidate.

  9. NeilGirdhar commented on Jan 25, 2017

    @NeilGirdhar
    Contributor

    Two other solutions:

    • Make int64 + int return int64 and similarly uint64 + int return uint64. Wrap and warn on overflow just like it does when adding homogenous types.
    • Make int64 + int return a dtype of object and use Python's int.

    I prefer the first solution.

  10. rkern commented on Jan 25, 2017

    @rkern
    Member

    That doesn't really resolve the problem, just hide one instance of it in a special case. The root question is what does uint64 + int64 (and vice versa) do?

  11. njsmith commented on Jan 25, 2017

    @njsmith
    Member
  12. NeilGirdhar commented on Jan 25, 2017

    @NeilGirdhar
    Contributor

    I agree with at least "warning" out the same way you warn out when you run into an overflow. It's really important here to have a clear message that states:

    • that the arguments were converted to a common type and that the common type of int and uint64 is whatever, and that the whatever can result in loss of information, and
    • that the message can be suppressed by casting the arguments.

    I want to add that using float64 as the top of the integer tower essentially prioritizes protecting 52-ish higher-order bits whereas using int64 prioritizes 63-ish lower-order bits. Is there any reason to believe that higher-order bits are more important? If anything, most computations with integers are done around zero. So, in my opinion, the lower order bits are the most important.

    If you end up going with the rule that: int64 + int returns int64 and uint64 + int returns uint64, then the warning above could be selectively emitted when there is actual loss of information, just like overflow warnings are selectively emitted — they aren't emitted whenever the arguments theoretically could have overflowed.

  13. rkern commented on Jan 25, 2017

    @rkern
    Member

    I don't think it's useful to keep talking about the combination of int64 + int (or uint64 + int). The ufunc dtype logic never sees int. The int first gets converted to a numpy dtype (without knowing the context that it is in an operation with another operand), then the two numpy dtypes interact to come to a common dtype. The question is what you want uint64 + int64 to do.

  14. NeilGirdhar commented on Jan 25, 2017

    @NeilGirdhar
    Contributor

    Okay since we're blind to context, then this is my proposal:

    • int becomes int64 and warns on overflow, and
    • uint64 + int64 gives int64 and warns on overflow.
  15. eric-wieser commented on Apr 25, 2017

    @eric-wieser
    Member

    This duplicates #5745, which is older

  16. atbenmurray commented on Jul 1, 2020

    @atbenmurray

    It is maybe not what you'd guess at first blush, but it is deliberate, at least, because the floating point dtype is the only type that contains any possible np.uint64 value plus any possible int value.

    For example, what would you expect for this?:

    np.uint64(2**64-1) + int(2)
    

    I would expect overflow, personally

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions