Repository: numba/numba

Issue:
arrayexpr `float32 ** const_int` yields float64 instead of float32
``` python
import numpy as np
from numba import njit


@njit
def bar(x):
     return x ** 3


x = np.arange(10, dtype=np.float32)
out = bar(x)
print(out, out.dtype)
```

prints

```
[   0.    1.    8.   27.   64.  125.  216.  343.  512.  729.] float64
```



Discussion:
The problem comes from `np.can_cast` as used in `numpy_support.ufunc_find_matching_loop`. An int32 or `int64` cannot be cast to float32, so the float64 version of the loop is chosen instead.

Numpy's ufunc resolution mechanism internally uses `PyArray_CanCastArrayTo`, which does _value-based_ analysis on scalars(!):

```
>>> a = np.float32([0])
>>> a ** np.int64(33333)
array([ 0.], dtype=float32)
>>> a ** np.int64(333333)
array([ 0.])
```

I think the main reason this is surprising is because of the mixed float-integer operation. Perhaps we should use different rules than Numpy in that case? What do you think?

> value-based analysis on scalars(!):

I didn't expect that at all.

> Perhaps we should use different rules than Numpy in that case? What do you think?

Do you mean keeping our current behavior?

> Do you mean keeping our current behavior?

No, I mean decide that a `int32` parameter is allowed to be cast to `float32` if desired. I don't really know how to express that in a generic way, though.


Resolving patch:
diff --git a/numba/numpy_support.py b/numba/numpy_support.py
--- a/numba/numpy_support.py
+++ b/numba/numpy_support.py
@@ -265,6 +265,24 @@ def numpy_outputs(self):
         return [as_dtype(x) for x in self.outputs]
 
 
+def ufunc_can_cast(from_, to, has_mixed_inputs, casting='safe'):
+    """
+    A variant of np.can_cast() that can allow casting any integer to
+    any real or complex type, in case the operation has mixed-kind
+    inputs.
+
+    For example we want `np.power(float32, int32)` to be computed using
+    SP arithmetic and return `float32`.
+    However, `np.sqrt(int32)` should use DP arithmetic and return `float64`.
+    """
+    from_ = np.dtype(from_)
+    to = np.dtype(to)
+    if has_mixed_inputs and from_.kind in 'iu' and to.kind in 'cf':
+        # Decide that all integers can cast to any real or complex type.
+        return True
+    return np.can_cast(from_, to, casting)
+
+
 def ufunc_find_matching_loop(ufunc, arg_types):
     """Find the appropriate loop to be used for a ufunc based on the types
     of the operands
@@ -290,6 +308,11 @@ def ufunc_find_matching_loop(ufunc, arg_types):
     except NotImplementedError:
         return None
 
+    # Whether the inputs are mixed integer / floating-point
+    has_mixed_inputs = (
+        any(dt.kind in 'iu' for dt in np_input_types) and
+        any(dt.kind in 'cf' for dt in np_input_types))
+
     def choose_types(numba_types, ufunc_letters):
         """
         Return a list of Numba types representing *ufunc_letters*,
@@ -327,22 +350,30 @@ def choose_types(numba_types, ufunc_letters):
                 if outer.char != inner:
                     found = False
                     break
-            elif not np.can_cast(outer.char, inner, 'safe'):
+            elif not ufunc_can_cast(outer.char, inner,
+                                    has_mixed_inputs, 'safe'):
                 found = False
                 break
         if found and strict_ufunc_typing:
             # Can we cast the inner result to the outer result type?
             for outer, inner in zip(np_output_types, ufunc_outputs):
                 if (outer.char not in 'mM' and not
-                    np.can_cast(inner, outer.char, 'same_kind')):
+                    ufunc_can_cast(inner, outer.char,
+                                   has_mixed_inputs, 'same_kind')):
                     found = False
                     break
         if found:
             # Found: determine the Numba types for the loop's inputs and
             # outputs.
-            inputs = choose_types(input_types, ufunc_inputs)
-            outputs = choose_types(output_types, ufunc_outputs)
-            return UFuncLoopSpec(inputs, outputs, candidate)
+            try:
+                inputs = choose_types(input_types, ufunc_inputs)
+                outputs = choose_types(output_types, ufunc_outputs)
+            except NotImplementedError:
+                # One of the selected dtypes isn't supported by Numba
+                # (e.g. float16), try other candidates
+                continue
+            else:
+                return UFuncLoopSpec(inputs, outputs, candidate)
 
     return None
 
diff --git a/numba/targets/numbers.py b/numba/targets/numbers.py
--- a/numba/targets/numbers.py
+++ b/numba/targets/numbers.py
@@ -500,8 +500,8 @@ def _implement_integer_operators():
         lower_builtin('<=', ty, ty)(int_ule_impl)
         lower_builtin('>', ty, ty)(int_ugt_impl)
         lower_builtin('>=', ty, ty)(int_uge_impl)
-        lower_builtin('**', types.float64, ty)(int_power_impl)
-        lower_builtin(pow, types.float64, ty)(int_power_impl)
+        lower_builtin('**', types.Float, ty)(int_power_impl)
+        lower_builtin(pow, types.Float, ty)(int_power_impl)
         lower_builtin(abs, ty)(uint_abs_impl)
 
     for ty in types.signed_domain:
@@ -509,8 +509,8 @@ def _implement_integer_operators():
         lower_builtin('<=', ty, ty)(int_sle_impl)
         lower_builtin('>', ty, ty)(int_sgt_impl)
         lower_builtin('>=', ty, ty)(int_sge_impl)
-        lower_builtin('**', types.float64, ty)(int_power_impl)
-        lower_builtin(pow, types.float64, ty)(int_power_impl)
+        lower_builtin('**', types.Float, ty)(int_power_impl)
+        lower_builtin(pow, types.Float, ty)(int_power_impl)
         lower_builtin(abs, ty)(int_abs_impl)
 
 def _implement_bitwise_operators():
diff --git a/numba/typing/builtins.py b/numba/typing/builtins.py
--- a/numba/typing/builtins.py
+++ b/numba/typing/builtins.py
@@ -217,10 +217,11 @@ class BinOpFloorDiv(ConcreteTemplate):
 class BinOpPower(ConcreteTemplate):
     key = "**"
     cases = list(integer_binop_cases)
+    # Ensure that float32 ** int doesn't go through DP computations
+    cases += [signature(types.float32, types.float32, op)
+              for op in (types.int32, types.int64, types.uint64)]
     cases += [signature(types.float64, types.float64, op)
-              for op in sorted(types.signed_domain)]
-    cases += [signature(types.float64, types.float64, op)
-              for op in sorted(types.unsigned_domain)]
+              for op in (types.int32, types.int64, types.uint64)]
     cases += [signature(op, op, op)
               for op in sorted(types.real_domain)]
     cases += [signature(op, op, op)


Test patch:
diff --git a/numba/tests/test_numpy_support.py b/numba/tests/test_numpy_support.py
--- a/numba/tests/test_numpy_support.py
+++ b/numba/tests/test_numpy_support.py
@@ -237,7 +237,7 @@ def __init__(self, types):
             assert len(in_) == self.nin
             assert len(out) == self.nout
 
-# Typical types for np.add, np.multiply
+# Typical types for np.add, np.multiply, np.isnan
 _add_types = ['??->?', 'bb->b', 'BB->B', 'hh->h', 'HH->H', 'ii->i', 'II->I',
               'll->l', 'LL->L', 'qq->q', 'QQ->Q', 'ee->e', 'ff->f', 'dd->d',
               'gg->g', 'FF->F', 'DD->D', 'GG->G', 'Mm->M', 'mm->m', 'mM->M',
@@ -248,6 +248,10 @@ def __init__(self, types):
               'gg->g', 'FF->F', 'DD->D', 'GG->G', 'mq->m', 'qm->m', 'md->m',
               'dm->m', 'OO->O']
 
+# Those ones only have floating-point loops
+_isnan_types = ['e->?', 'f->?', 'd->?', 'g->?', 'F->?', 'D->?', 'G->?']
+_sqrt_types = ['e->e', 'f->f', 'd->d', 'g->g', 'F->F', 'D->D', 'G->G', 'O->O']
+
 
 class TestUFuncs(TestCase):
     """
@@ -258,6 +262,8 @@ def test_ufunc_find_matching_loop(self):
         f = numpy_support.ufunc_find_matching_loop
         np_add = FakeUFunc(_add_types)
         np_mul = FakeUFunc(_mul_types)
+        np_isnan = FakeUFunc(_isnan_types)
+        np_sqrt = FakeUFunc(_sqrt_types)
 
         def check(ufunc, input_types, sigs, output_types=()):
             """
@@ -268,7 +274,10 @@ def check(ufunc, input_types, sigs, output_types=()):
             self.assertTrue(loop)
             if isinstance(sigs, str):
                 sigs = (sigs,)
-            self.assertIn(loop.ufunc_sig, sigs)
+            self.assertIn(loop.ufunc_sig, sigs,
+                          "inputs=%s and outputs=%s should have selected "
+                          "one of %s, got %s"
+                          % (input_types, output_types, sigs, loop.ufunc_sig))
             self.assertEqual(len(loop.numpy_inputs), len(loop.inputs))
             self.assertEqual(len(loop.numpy_outputs), len(loop.outputs))
             if not output_types:
@@ -283,6 +292,9 @@ def check(ufunc, input_types, sigs, output_types=()):
             return loop
 
         def check_exact(ufunc, input_types, sigs, output_types=()):
+            """
+            Like check(), but also ensure no casting of inputs occurred.
+            """
             loop = check(ufunc, input_types, sigs, output_types)
             self.assertEqual(loop.inputs, list(input_types))
 
@@ -320,6 +332,21 @@ def check_no_match(ufunc, input_types):
         check(np_add, (types.int16, types.uint16), 'ii->i')
         check(np_add, (types.complex64, types.float64), 'DD->D')
         check(np_add, (types.float64, types.complex64), 'DD->D')
+        # Integers, when used together with floating-point numbers,
+        # should cast to any real or complex (see #2006)
+        int_types = [types.int32, types.uint32, types.int64, types.uint64]
+        for intty in int_types:
+            check(np_add, (types.float32, intty), 'ff->f')
+            check(np_add, (types.float64, intty), 'dd->d')
+            check(np_add, (types.complex64, intty), 'FF->F')
+            check(np_add, (types.complex128, intty), 'DD->D')
+        # However, when used alone, they should cast only to
+        # floating-point types of sufficient precision
+        # (typical use case: np.sqrt(2) should give an accurate enough value)
+        for intty in int_types:
+            check(np_sqrt, (intty,), 'd->d')
+            check(np_isnan, (intty,), 'd->?')
+
         # With some timedelta64 arguments as well
         check(np_mul, (types.NPTimedelta('s'), types.int32),
               'mq->m', output_types=(types.NPTimedelta('s'),))
diff --git a/numba/tests/test_operators.py b/numba/tests/test_operators.py
--- a/numba/tests/test_operators.py
+++ b/numba/tests/test_operators.py
@@ -1432,7 +1432,7 @@ def test_int_values(self):
 
     def test_real_values(self):
         exponents = [1, 2, 3, 5, 17, 0, -1, -2, -3, 0x111111, -0x111112]
-        vals = [1.5, 3.25, -1.25, np.float32(-1.5), float('inf'), float('nan')]
+        vals = [1.5, 3.25, -1.25, np.float32(-2.0), float('inf'), float('nan')]
 
         self._check_pow(exponents, vals)
 
diff --git a/numba/tests/test_ufuncs.py b/numba/tests/test_ufuncs.py
--- a/numba/tests/test_ufuncs.py
+++ b/numba/tests/test_ufuncs.py
@@ -12,7 +12,7 @@
 from numba import types, typing, utils, typeof, numpy_support, njit
 from numba.compiler import compile_isolated, Flags, DEFAULT_FLAGS
 from numba.numpy_support import from_dtype
-from numba import vectorize
+from numba import jit, vectorize
 from numba.config import PYVERSION
 from numba.errors import LoweringError, TypingError
 from .support import TestCase, CompilationCache, MemoryLeakMixin, tag
@@ -1377,6 +1377,29 @@ def foo(x,y):
         cr = compile_isolated(foo, [types.complex128, types.complex128])
         self.assertEqual(foo(1j, 1j), cr.entry_point(1j, 1j))
 
+    def test_issue_2006(self):
+        """
+        <float32 ** int> should return float32, not float64.
+        """
+        def foo(x, y):
+            return np.power(x, y)
+        pyfunc = foo
+        cfunc = jit(nopython=True)(pyfunc)
+
+        def check(x, y):
+            got = cfunc(x, y)
+            np.testing.assert_array_almost_equal(got, pyfunc(x, y))
+            # Check the power operation conserved the input's dtype
+            # (this is different from Numpy, whose behaviour depends on
+            #  the *values* of the arguments -- see PyArray_CanCastArrayTo).
+            self.assertEqual(got.dtype, x.dtype)
+
+        xs = [np.float32([1, 2, 3]), np.complex64([1j, 2, 3-3j])]
+        for x in xs:
+            check(x, 3)
+            check(x, np.uint64(3))
+            check(x, np.int64([2, 2, 3]))
+
 
 class _LoopTypesTester(TestCase):
     """Test code generation for the different loop types defined by ufunc.