Skip to content

NumPy's implementation of scalar integer ** integer has very weird casting #7449

Open

Description

@njsmith

Apropos #7447 (CC @ewmoore @charris):

I was curious about what was going on here, and it looks much uglier than we realized :-(

Little test script that tries doing 4 ** -4 using different types:

Old testing script (and results)
signed_int_types = [np.int8, np.int16, np.int32, np.int64, int]
all_int_types = signed_int_types + [np.uint8, np.uint16, np.uint32, np.uint64]
for base_type in all_int_types:
    for exponent_type in signed_int_types:
        result = base_type(4) ** exponent_type(-4)
        print("{} ** {} -> {} ({})".format(base_type.__name__,
                                           exponent_type.__name__,
                                           result.__class__.__name__,
                                           result))

On current master (after #7447 is merged):

int8 ** int8 -> float32 (0.00390625)
int8 ** int16 -> int16 (0)
int8 ** int32 -> int32 (0)
int8 ** int64 -> int64 (0)
int8 ** int -> int64 (0)
int16 ** int8 -> float32 (0.00390625)
int16 ** int16 -> float32 (0.00390625)
int16 ** int32 -> int32 (0)
int16 ** int64 -> int64 (0)
int16 ** int -> int64 (0)
int32 ** int8 -> float64 (0.00390625)
int32 ** int16 -> float64 (0.00390625)
int32 ** int32 -> float64 (0.00390625)
int32 ** int64 -> int64 (0)
int32 ** int -> int64 (0)
int64 ** int8 -> float64 (0.00390625)
int64 ** int16 -> float64 (0.00390625)
int64 ** int32 -> float64 (0.00390625)
int64 ** int64 -> float64 (0.00390625)
int64 ** int -> float64 (0.00390625)
int ** int8 -> int64 (0)
int ** int16 -> int64 (0)
int ** int32 -> int64 (0)
int ** int64 -> float64 (0.00390625)
int ** int -> float (0.00390625)
uint8 ** int8 -> int16 (0)
uint8 ** int16 -> int16 (0)
uint8 ** int32 -> int32 (0)
uint8 ** int64 -> int64 (0)
uint8 ** int -> int64 (0)
uint16 ** int8 -> int32 (0)
uint16 ** int16 -> int32 (0)
uint16 ** int32 -> int32 (0)
uint16 ** int64 -> int64 (0)
uint16 ** int -> int64 (0)
uint32 ** int8 -> int64 (0)
uint32 ** int16 -> int64 (0)
uint32 ** int32 -> int64 (0)
uint32 ** int64 -> int64 (0)
uint32 ** int -> int64 (0)
uint64 ** int8 -> float64 (0.00390625)
uint64 ** int16 -> float64 (0.00390625)
uint64 ** int32 -> float64 (0.00390625)
uint64 ** int64 -> float64 (0.00390625)
uint64 ** int -> float64 (0.00390625)

This is all extremely extremely weird and inconsistent...


Summary/edit by @seberg 2019-05-23:

On current master (and 1.16) we now error for many of these cases as found using the modified snippet:

signed_int_types = [np.int8, np.int16, np.int32, np.int64, int]
all_int_types = signed_int_types + [np.uint8, np.uint16, np.uint32, np.uint64]
for base_type in all_int_types:
    for exponent_type in signed_int_types:
        try:
            result = base_type(4) ** exponent_type(-4)
        except Exception as e:
            result = e
        print("{} ** {} -> {} ({})".format(base_type.__name__,
                                           exponent_type.__name__,
                                           result.__class__.__name__,
                                           result))

Giving (again 1.16, master shortly before 1.17):

int8 ** int8 -> ValueError (Integers to negative integer powers are not allowed.)
int8 ** int16 -> ValueError (Integers to negative integer powers are not allowed.)
int8 ** int32 -> ValueError (Integers to negative integer powers are not allowed.)
int8 ** int64 -> ValueError (Integers to negative integer powers are not allowed.)
int8 ** int -> ValueError (Integers to negative integer powers are not allowed.)
int16 ** int8 -> ValueError (Integers to negative integer powers are not allowed.)
int16 ** int16 -> ValueError (Integers to negative integer powers are not allowed.)
int16 ** int32 -> ValueError (Integers to negative integer powers are not allowed.)
int16 ** int64 -> ValueError (Integers to negative integer powers are not allowed.)
int16 ** int -> ValueError (Integers to negative integer powers are not allowed.)
int32 ** int8 -> ValueError (Integers to negative integer powers are not allowed.)
int32 ** int16 -> ValueError (Integers to negative integer powers are not allowed.)
int32 ** int32 -> ValueError (Integers to negative integer powers are not allowed.)
int32 ** int64 -> ValueError (Integers to negative integer powers are not allowed.)
int32 ** int -> ValueError (Integers to negative integer powers are not allowed.)
int64 ** int8 -> ValueError (Integers to negative integer powers are not allowed.)
int64 ** int16 -> ValueError (Integers to negative integer powers are not allowed.)
int64 ** int32 -> ValueError (Integers to negative integer powers are not allowed.)
int64 ** int64 -> ValueError (Integers to negative integer powers are not allowed.)
int64 ** int -> ValueError (Integers to negative integer powers are not allowed.)
int ** int8 -> ValueError (Integers to negative integer powers are not allowed.)
int ** int16 -> ValueError (Integers to negative integer powers are not allowed.)
int ** int32 -> ValueError (Integers to negative integer powers are not allowed.)
int ** int64 -> ValueError (Integers to negative integer powers are not allowed.)
int ** int -> float (0.00390625)
uint8 ** int8 -> ValueError (Integers to negative integer powers are not allowed.)
uint8 ** int16 -> ValueError (Integers to negative integer powers are not allowed.)
uint8 ** int32 -> ValueError (Integers to negative integer powers are not allowed.)
uint8 ** int64 -> ValueError (Integers to negative integer powers are not allowed.)
uint8 ** int -> ValueError (Integers to negative integer powers are not allowed.)
uint16 ** int8 -> ValueError (Integers to negative integer powers are not allowed.)
uint16 ** int16 -> ValueError (Integers to negative integer powers are not allowed.)
uint16 ** int32 -> ValueError (Integers to negative integer powers are not allowed.)
uint16 ** int64 -> ValueError (Integers to negative integer powers are not allowed.)
uint16 ** int -> ValueError (Integers to negative integer powers are not allowed.)
uint32 ** int8 -> ValueError (Integers to negative integer powers are not allowed.)
uint32 ** int16 -> ValueError (Integers to negative integer powers are not allowed.)
uint32 ** int32 -> ValueError (Integers to negative integer powers are not allowed.)
uint32 ** int64 -> ValueError (Integers to negative integer powers are not allowed.)
uint32 ** int -> ValueError (Integers to negative integer powers are not allowed.)
uint64 ** int8 -> float64 (0.00390625)
uint64 ** int16 -> float64 (0.00390625)
uint64 ** int32 -> float64 (0.00390625)
uint64 ** int64 -> float64 (0.00390625)
uint64 ** int -> float64 (0.00390625)

So the integer+integer which use float as a common type are a bit strange in this sense.

Activity

  1. changed the title [-]integer ** integer makes no sense for scalars[/-] [+]NumPy's implementation of scalar integer ** integer has very weird casting[/+] on Mar 22, 2016
  2. charris commented on Mar 22, 2016

    @charris
    Member

    Looks like array scalars are being used for some of the computations.

    I also want to move this file into the multiarray module at some point, starting with removing the immediate ufunc dependency and using the npymath library (which came later) instead. The file also needs other modernization.

  3. ewmoore commented on Mar 23, 2016

    @ewmoore
    Contributor

    The casting is only weird for negative exponents. For positive it's the same as the other binary operators.

    What's happening is that the scalar binary operator routines fall back to just calling the equivalent ufunc when both types cannot be cast to the type of the first. For **, this give the strange casting table because the scalar operation tries to look at the values to determine the output type, but the ufunc doesn't. The fix is probably to try and implement the effective __rop__ logic in the scalar operation without just giving up and calling the ufunc unless one of the arguments is not a scalar type.

    If we don't want to return a float for integer-integer powers, then we can just stop looking at the values to determine the output type.

    This quick fallback to calling a ufunc also mean that mixed type scalar operations can be roughly 10x slower (here at least) than the pure type scalar operation depending of the order of the types:

    In [5]: a16 = np.int16(4)
    
    In [6]: b16 = np.int16(4)
    
    In [7]: a8 = np.int8(4)
    
    In [8]: b8 = np.int8(4)
    
    In [9]: timeit a16**b16
    The slowest run took 28.07 times longer than the fastest. This could mean that an intermediate result is being cached 
    10000000 loops, best of 3: 81.2 ns per loop
    
    In [10]: timeit a8**b16
    The slowest run took 23.52 times longer than the fastest. This could mean that an intermediate result is being cached 
    1000000 loops, best of 3: 1.18 µs per loop
    
    In [11]: timeit a16**b8
    The slowest run took 76.41 times longer than the fastest. This could mean that an intermediate result is being cached 
    10000000 loops, best of 3: 144 ns per loop
    
    In [12]: timeit a16 + b16
    The slowest run took 189.67 times longer than the fastest. This could mean that an intermediate result is being cached 
    10000000 loops, best of 3: 76.2 ns per loop
    
    In [13]: timeit a8 + b16
    The slowest run took 34.06 times longer than the fastest. This could mean that an intermediate result is being cached 
    1000000 loops, best of 3: 1.05 µs per loop
    
    In [14]: timeit a16 + b8
    The slowest run took 38.54 times longer than the fastest. This could mean that an intermediate result is being cached 
    10000000 loops, best of 3: 118 ns per loop
    
  4. added a commit that references this issue on Apr 18, 2016
    223ec73
  5. added a commit that references this issue on Oct 7, 2016
    af692ce
  6. added this to the milestone on Nov 17, 2018
  7. charris commented on Nov 17, 2018

    @charris
    Member

    Pushing this issue off to 1.17. I don't know the current status of these problems, or whether #8126 is still needed. We might want to implement #8126 differently. @mattip Ping because you have been working in that area for matmul.

  8. mhvk commented on Jun 12, 2019

    @mhvk
    Contributor

    Removing the milestone - I think issues should have them only if release-critical...

  9. removed this from the milestone on Jun 12, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions