Repository: pytorch/pytorch

Issue:
__array__ and __array_wrap__
To improve Numpy interoperability, torch tensors could implement the array interface in a quite simple manner:
```python
def __array__(self, dtype=None):
    if dtype is None:
        return self.cpu().numpy()
    else:
        return self.cpu().numpy().astype(dtype, copy=False)


def __array_wrap__(self, array):
    if array.ndim == 0:
        if array.dtype.kind == 'b':
            return bool(array)
        elif array.dtype.kind in ('i', 'u'):
            return int(array)
        elif array.dtype.kind == 'f':
            return float(array)
        elif array.dtype.kind == 'c':
            return complex(array)
        else:
            raise RuntimeError
    else:
        if array.dtype == bool:
            # Workaround, torch has no built-in bool tensor
            cls = torch.ByteTensor
            array = array.astype('uint8')
        else:
            cls = _tensor_cls(array.dtype, use_cuda=self.is_cuda)
        return cls(array)


def _tensor_cls(dtype, use_cuda):
    mapping = {
        np.dtype('float16'): 'Half',
        np.dtype('float32'): 'Float',
        np.dtype('float64'): 'Double',
        np.dtype('int8'): 'Char',
        np.dtype('int16'): 'Short',
        np.dtype('int32'): 'Int',
        np.dtype('int64'): 'Long',
        np.dtype('uint8'): 'Byte',
    }
    try:
        name = mapping[np.dtype(dtype)]
    except KeyError:
        raise ValueError('dtype {} not supported'.format(dtype))
    if use_cuda:
        return getattr(torch.cuda, name)
    else:
        return getattr(torch, name)
```
It would enable simple things like `numpy.asarray` to turn tensors into Numpy arrays, and with that make basically all of Numpy applicable to torch tensors (with some restrictions). Is something like that of interest? 


Discussion:
Yes that looks great! Note that it's much simpler to use `torch.from_numpy`, which will create a Tensor of appropriate type for you
Alright, I'll make a PR then.

> Note that it's much simpler to use torch.from_numpy, which will create a Tensor of appropriate type for you

You're right, I don't know why I overlooked this.

Regarding placement, my suggestion is to define the generic methods like above in `torch/__init__.py` and then just set the attribute in each class.
**Question1** regarding `HalfTensor`: it doesn't implement `numpy()` so this code will crash with that tensor class. Should we let it crash or rather leave the attribute off the class? An argument for the former is that if it's left out, `numpy.asarray` will still silently work but yield a (largely useless) `object` array with 1 element (the tensor).
**Question2** regarding bools: Okay to use `ByteTensor`? Again, the alternative is to just let it crash.
1. We coud add a `numpy()` for half as well
2. Yes, that's fine

Resolving patch:
diff --git a/torch/tensor.py b/torch/tensor.py
--- a/torch/tensor.py
+++ b/torch/tensor.py
@@ -376,6 +376,35 @@ def __hash__(self):
     def data(self):
         raise RuntimeError('cannot call .data on a torch.Tensor: did you intend to use autograd.Variable?')
 
+    # Numpy array interface, to support `numpy.asarray(tensor) -> ndarray`
+    def __array__(self, dtype=None):
+        if dtype is None:
+            return self.cpu().numpy()
+        else:
+            return self.cpu().numpy().astype(dtype, copy=False)
+
+    # Wrap Numpy array again in a suitable tensor when done, to support e.g.
+    # `numpy.sin(tensor) -> tensor` or `numpy.greater(tensor, 0) -> ByteTensor`
+    def __array_wrap__(self, array):
+        if array.ndim == 0:
+            # TODO: remove this when 0-dimensional tensors are supported
+            if array.dtype.kind == 'b':
+                return bool(array)
+            elif array.dtype.kind in ('i', 'u'):
+                return int(array)
+            elif array.dtype.kind == 'f':
+                return float(array)
+            elif array.dtype.kind == 'c':
+                return complex(array)
+            else:
+                raise RuntimeError('bad scalar {!r}'.format(array))
+        else:
+            if array.dtype == bool:
+                # Workaround, torch has no built-in bool tensor
+                array = array.astype('uint8')
+
+            return torch.from_numpy(array)
+
 
 _TensorBase.type = _type
 _TensorBase.cuda = _cuda


Test patch:
diff --git a/test/test_torch.py b/test/test_torch.py
--- a/test/test_torch.py
+++ b/test/test_torch.py
@@ -4231,6 +4231,83 @@ def test_numpy_index(self):
             self.assertFalse(isinstance(idx, int))
             self.assertEqual(x[idx], x[int(idx)])
 
+    @unittest.skipIf(not TEST_NUMPY, "Numpy not found")
+    def test_numpy_array_interface(self):
+        types = [
+            torch.DoubleTensor,
+            torch.FloatTensor,
+            torch.LongTensor,
+            torch.IntTensor,
+            torch.ShortTensor,
+            torch.ByteTensor,
+        ]
+        dtypes = [
+            np.float64,
+            np.float32,
+            np.int64,
+            np.int32,
+            np.int16,
+            np.uint8,
+        ]
+        for tp, dtype in zip(types, dtypes):
+            if np.dtype(dtype).kind == 'u':
+                x = torch.Tensor([1, 2, 3, 4]).type(tp)
+                array = np.array([1, 2, 3, 4], dtype=dtype)
+            else:
+                x = torch.Tensor([1, -2, 3, -4]).type(tp)
+                array = np.array([1, -2, 3, -4], dtype=dtype)
+
+            # Test __array__ w/o dtype argument
+            asarray = np.asarray(x)
+            self.assertIsInstance(asarray, np.ndarray)
+            self.assertEqual(asarray.dtype, dtype)
+            for i in range(len(x)):
+                self.assertEqual(asarray[i], x[i])
+
+            # Test __array_wrap__, same dtype
+            abs_x = np.abs(x)
+            abs_array = np.abs(array)
+            self.assertIsInstance(abs_x, tp)
+            for i in range(len(x)):
+                self.assertEqual(abs_x[i], abs_array[i])
+
+        # Test __array__ with dtype argument
+        for dtype in dtypes:
+            x = torch.Tensor([1, -2, 3, -4])
+            asarray = np.asarray(x, dtype=dtype)
+            self.assertEqual(asarray.dtype, dtype)
+            if np.dtype(dtype).kind == 'u':
+                wrapped_x = np.array([1, -2, 3, -4], dtype=dtype)
+                for i in range(len(x)):
+                    self.assertEqual(asarray[i], wrapped_x[i])
+            else:
+                for i in range(len(x)):
+                    self.assertEqual(asarray[i], x[i])
+
+        # Test some math functions with float types
+        float_types = [torch.DoubleTensor, torch.FloatTensor]
+        float_dtypes = [np.float64, np.float32]
+        for tp, dtype in zip(float_types, float_dtypes):
+            x = torch.Tensor([1, 2, 3, 4]).type(tp)
+            array = np.array([1, 2, 3, 4], dtype=dtype)
+            for func in ['sin', 'sqrt', 'ceil']:
+                ufunc = getattr(np, func)
+                res_x = ufunc(x)
+                res_array = ufunc(array)
+                self.assertIsInstance(res_x, tp)
+                for i in range(len(x)):
+                    self.assertEqual(res_x[i], res_array[i])
+
+        # Test functions with boolean return value
+        for tp, dtype in zip(types, dtypes):
+            x = torch.Tensor([1, 2, 3, 4]).type(tp)
+            array = np.array([1, 2, 3, 4], dtype=dtype)
+            geq2_x = np.greater_equal(x, 2)
+            geq2_array = np.greater_equal(array, 2).astype('uint8')
+            self.assertIsInstance(geq2_x, torch.ByteTensor)
+            for i in range(len(x)):
+                self.assertEqual(geq2_x[i], geq2_array[i])
+
     def test_comparison_ops(self):
         x = torch.randn(5, 5)
         y = torch.randn(5, 5)