Repository: chainer/chainer

Issue:
Typecasting bug
The result of multiplying a float32 cupy ndarray by a constant integer is a float64 array. This is not consistent with numpy.

```
import numpy as np
import cupy as cp
a = np.zeros((2,)).astype('float32')
b = cp.zeros((2,)).astype('float32')

(2 * a).dtype  # float32
(2 * b).dtype  # float64
```



Discussion:
Thank you for your report.
I will fix it.


Resolving patch:
diff --git a/cupy/elementwise.py b/cupy/elementwise.py
--- a/cupy/elementwise.py
+++ b/cupy/elementwise.py
@@ -56,6 +56,13 @@ def _get_simple_elementwise_kernel(
 _scalar_type = _python_scalar_type + tuple(
     t.type for t in _typenames.keys())
 
+_kind_score = {
+    'b': 0,
+    'u': 1,
+    'i': 1,
+    'f': 2,
+}
+
 
 def _get_typename(dtype):
     if dtype is None:
@@ -499,21 +506,49 @@ def _guess_routine_from_dtype(ops, dtype):
     return None
 
 
+def _check_in_args_kind(in_args):
+    all_scalars = True
+    max_array_kind = -1
+    max_scalar_kind = -1
+    for i in in_args:
+        if isinstance(i, cupy.ndarray):
+            kind = _kind_score[i.dtype.kind]
+            all_scalars = False
+            if kind > max_array_kind:
+                max_array_kind = kind
+        else:
+            if isinstance(i, _python_scalar_type):
+                dtype = numpy.dtype(type(i))
+            else:
+                dtype = i.dtype
+            kind = _kind_score[dtype.kind]
+            if kind > max_scalar_kind:
+                max_scalar_kind = kind
+    return not all_scalars and max_array_kind >= max_scalar_kind
+
+
 def _guess_routine(name, cache, ops, in_args, dtype):
     if dtype is None:
-        key = tuple([numpy.dtype(type(i)).type
-                     if isinstance(i, _python_scalar_type) else i.dtype.type
-                     for i in in_args])
-    else:
-        key = dtype
-
-    op = cache.get(key, ())
-    if op is ():
-        if dtype is None:
-            op = _guess_routine_from_in_types(ops, key)
+        use_raw_value = _check_in_args_kind(in_args)
+        if use_raw_value:
+            in_types = tuple(in_args)
+            op = ()
         else:
-            op = _guess_routine_from_dtype(ops, key)
-        cache[key] = op
+            in_types = tuple(
+                [type(i)
+                 if isinstance(i, _python_scalar_type) else i.dtype.type
+                 for i in in_args])
+            op = cache.get(in_types, ())
+
+        if op is ():
+            op = _guess_routine_from_in_types(ops, in_types)
+            if not use_raw_value:
+                cache[in_types] = op
+    else:
+        op = cache.get(dtype, ())
+        if op is ():
+            op = _guess_routine_from_dtype(ops, dtype)
+            cache[dtype] = op
 
     if op:
         return op
diff --git a/cupy/math/misc.py b/cupy/math/misc.py
--- a/cupy/math/misc.py
+++ b/cupy/math/misc.py
@@ -8,8 +8,8 @@
 
 _clip = elementwise.create_ufunc(
     'cupy_clip',
-    ('bbb->b', 'BBB->B', 'hhh->h', 'HHH->H', 'iii->i', 'III->I', 'lll->l',
-     'LLL->L', 'qqq->q', 'QQQ->Q', 'eee->e', 'fff->f', 'ddd->d'),
+    ('???->?', 'bbb->b', 'BBB->B', 'hhh->h', 'HHH->H', 'iii->i', 'III->I',
+     'lll->l', 'LLL->L', 'qqq->q', 'QQQ->Q', 'eee->e', 'fff->f', 'ddd->d'),
     'out0 = min(in2, max(in1, in0))')
 
 
diff --git a/cupy/reduction.py b/cupy/reduction.py
--- a/cupy/reduction.py
+++ b/cupy/reduction.py
@@ -494,14 +494,14 @@ def create_reduction_func(name, ops, routine=None, identity=None,
 
 amin = create_reduction_func(
     'cupy_min',
-    ('?->?', 'B->B', 'h->h', 'H->H', 'i->i', 'I->I', 'l->l', 'L->L',
+    ('?->?', 'b->b', 'B->B', 'h->h', 'H->H', 'i->i', 'I->I', 'l->l', 'L->L',
      'q->q', 'Q->Q', 'e->e', 'f->f', 'd->d'),
     ('min_max_st(in0)', 'my_min(a, b)', 'out0 = a.value', 'min_max_st'),
     None, _min_max_preamble)
 
 amax = create_reduction_func(
     'cupy_max',
-    ('?->?', 'B->B', 'h->h', 'H->H', 'i->i', 'I->I', 'l->l', 'L->L',
+    ('?->?', 'b->b', 'B->B', 'h->h', 'H->H', 'i->i', 'I->I', 'l->l', 'L->L',
      'q->q', 'Q->Q', 'e->e', 'f->f', 'd->d'),
     ('min_max_st(in0)', 'my_max(a, b)', 'out0 = a.value', 'min_max_st'),
     None, _min_max_preamble)


Test patch:
diff --git a/cupy/testing/helper.py b/cupy/testing/helper.py
--- a/cupy/testing/helper.py
+++ b/cupy/testing/helper.py
@@ -10,7 +10,7 @@
 
 
 def numpy_cupy_allclose(rtol=1e-7, atol=0, err_msg='', verbose=True,
-                        name='xp'):
+                        name='xp', type_check=True):
     def decorator(impl):
         @functools.wraps(impl)
         def test_func(self, *args, **kw):
@@ -19,12 +19,14 @@ def test_func(self, *args, **kw):
             kw[name] = numpy
             y = impl(self, *args, **kw)
             array.assert_allclose(x, y, rtol, atol, err_msg, verbose)
+            if type_check:
+                self.assertEqual(x.dtype, y.dtype)
         return test_func
     return decorator
 
 
 def numpy_cupy_array_almost_equal(decimal=6, err_msg='', verbose=True,
-                                  name='xp'):
+                                  name='xp', type_check=True):
     def decorator(impl):
         @functools.wraps(impl)
         def test_func(self, *args, **kw):
@@ -34,11 +36,13 @@ def test_func(self, *args, **kw):
             y = impl(self, *args, **kw)
             array.assert_array_almost_equal(
                 x, y, decimal, err_msg, verbose)
+            if type_check:
+                self.assertEqual(x.dtype, y.dtype)
         return test_func
     return decorator
 
 
-def numpy_cupy_arrays_almost_equal_nulp(nulp=1, name='xp'):
+def numpy_cupy_arrays_almost_equal_nulp(nulp=1, name='xp', type_check=True):
     def decorator(impl):
         @functools.wraps(impl)
         def test_func(self, *args, **kw):
@@ -47,11 +51,13 @@ def test_func(self, *args, **kw):
             kw[name] = numpy
             y = impl(self, *args, **kw)
             array.assert_arrays_almost_equal_nulp(x, y, nulp)
+            if type_check:
+                self.assertEqual(x.dtype, y.dtype)
         return test_func
     return decorator
 
 
-def numpy_cupy_array_max_ulp(maxulp=1, dtype=None, name='xp'):
+def numpy_cupy_array_max_ulp(maxulp=1, dtype=None, name='xp', type_check=True):
     def decorator(impl):
         @functools.wraps(impl)
         def test_func(self, *args, **kw):
@@ -60,11 +66,14 @@ def test_func(self, *args, **kw):
             kw[name] = numpy
             y = impl(self, *args, **kw)
             array.assert_array_max_ulp(x, y, maxulp, dtype)
+            if type_check:
+                self.assertEqual(x.dtype, y.dtype)
         return test_func
     return decorator
 
 
-def numpy_cupy_array_equal(err_msg='', verbose=True, name='xp'):
+def numpy_cupy_array_equal(err_msg='', verbose=True, name='xp',
+                           type_check=True):
     def decorator(impl):
         @functools.wraps(impl)
         def test_func(self, *args, **kw):
@@ -73,6 +82,8 @@ def test_func(self, *args, **kw):
             kw[name] = numpy
             y = impl(self, *args, **kw)
             array.assert_array_equal(x, y, err_msg, verbose)
+            if type_check:
+                self.assertEqual(x.dtype, y.dtype)
         return test_func
     return decorator
 
@@ -90,7 +101,8 @@ def test_func(self, *args, **kw):
     return decorator
 
 
-def numpy_cupy_array_less(err_msg='', verbose=True, name='xp'):
+def numpy_cupy_array_less(err_msg='', verbose=True, name='xp',
+                          type_check=True):
     def decorator(impl):
         @functools.wraps(impl)
         def test_func(self, *args, **kw):
@@ -99,6 +111,8 @@ def test_func(self, *args, **kw):
             kw[name] = numpy
             y = impl(self, *args, **kw)
             array.assert_array_less(x, y, err_msg, verbose)
+            if type_check:
+                self.assertEqual(x.dtype, y.dtype)
         return test_func
     return decorator
 
diff --git a/tests/cupy_tests/manipulation_tests/test_shape.py b/tests/cupy_tests/manipulation_tests/test_shape.py
--- a/tests/cupy_tests/manipulation_tests/test_shape.py
+++ b/tests/cupy_tests/manipulation_tests/test_shape.py
@@ -8,12 +8,12 @@ class TestShape(unittest.TestCase):
 
     _multiprocess_can_split_ = True
 
-    @testing.numpy_cupy_array_equal()
+    @testing.numpy_cupy_array_equal(type_check=False)
     def test_reshape_strides(self, xp):
         a = testing.shaped_arange((1, 1, 1, 2, 2))
         return a.strides
 
-    @testing.numpy_cupy_array_equal()
+    @testing.numpy_cupy_array_equal(type_check=False)
     def test_reshape2(self, xp):
         a = xp.zeros((8,), dtype=xp.float32)
         return a.reshape((1, 1, 1, 4, 1, 2)).strides
diff --git a/tests/cupy_tests/math_tests/test_misc.py b/tests/cupy_tests/math_tests/test_misc.py
--- a/tests/cupy_tests/math_tests/test_misc.py
+++ b/tests/cupy_tests/math_tests/test_misc.py
@@ -14,7 +14,7 @@ class TestMisc(unittest.TestCase):
     @testing.numpy_cupy_allclose(atol=1e-5)
     def check_unary(self, name, xp, dtype, no_bool=False):
         if no_bool and numpy.dtype(dtype).char == '?':
-            return 0
+            return numpy.int_(0)
         a = testing.shaped_arange((2, 3), xp, dtype)
         return getattr(xp, name)(a)
 
@@ -22,7 +22,7 @@ def check_unary(self, name, xp, dtype, no_bool=False):
     @testing.numpy_cupy_allclose(atol=1e-5)
     def check_binary(self, name, xp, dtype, no_bool=False):
         if no_bool and numpy.dtype(dtype).char == '?':
-            return 0
+            return numpy.int_(0)
         a = testing.shaped_arange((2, 3), xp, dtype)
         b = testing.shaped_reverse_arange((2, 3), xp, dtype)
         return getattr(xp, name)(a, b)
@@ -31,7 +31,7 @@ def check_binary(self, name, xp, dtype, no_bool=False):
     @testing.numpy_cupy_allclose(atol=1e-5)
     def check_unary_negative(self, name, xp, dtype, no_bool=False):
         if no_bool and numpy.dtype(dtype).char == '?':
-            return 0
+            return numpy.int_(0)
         a = xp.array([-3, -2, -1, 1, 2, 3], dtype=dtype)
         return getattr(xp, name)(a)
 
diff --git a/tests/cupy_tests/test_ndarray_elementwise_op.py b/tests/cupy_tests/test_ndarray_elementwise_op.py
--- a/tests/cupy_tests/test_ndarray_elementwise_op.py
+++ b/tests/cupy_tests/test_ndarray_elementwise_op.py
@@ -4,6 +4,7 @@
 import numpy
 import six
 
+import cupy
 from cupy import testing
 
 
@@ -264,3 +265,47 @@ def test_array_reversed_sub(self):
 
     def test_array_reversed_mul(self):
         self.check_array_reversed_op(operator.mul)
+
+    @testing.for_all_dtypes(no_bool=True)
+    def check_typecast(self, val, dtype):
+        operators = [operator.add, operator.sub, operator.mul, operator.div]
+        for op in operators:
+            err = numpy.geterr()
+            numpy.seterr(divide='ignore', invalid='ignore')
+            a = op(val, (testing.shaped_arange((5,), numpy, dtype) - 2))
+            numpy.seterr(**err)
+            b = op(val, (testing.shaped_arange((5,), cupy, dtype) - 2))
+            self.assertEqual(a.dtype, b.dtype)
+
+    def test_typecast_bool1(self):
+        self.check_typecast(True)
+
+    def test_typecast_bool2(self):
+        self.check_typecast(False)
+
+    def test_typecast_int1(self):
+        self.check_typecast(0)
+
+    def test_typecast_int2(self):
+        self.check_typecast(-127)
+
+    def test_typecast_int3(self):
+        self.check_typecast(255)
+
+    def test_typecast_int4(self):
+        self.check_typecast(-32768)
+
+    def test_typecast_int5(self):
+        self.check_typecast(65535)
+
+    def test_typecast_int6(self):
+        self.check_typecast(-2147483648)
+
+    def test_typecast_int7(self):
+        self.check_typecast(4294967295)
+
+    def test_typecast_float1(self):
+        self.check_typecast(0.0)
+
+    def test_typecast_float2(self):
+        self.check_typecast(100000.0)