Repository: cupy/cupy

Issue:
cupy.cross function not available
I noticed that function [`numpy.cross`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.cross.html), which implements cross product for 2D and 3D vectors, is not available in cupy. Is this intentional? Would a numpy-equivalent implementation of cross product be a welcome contribution to cupy?

Let me know what you think. Thank you.


Discussion:
There is no reason for the non-existence of `cupy.cross`. Contribution is always welcome!!
I'll work on this issue

Resolving patch:
diff --git a/cupy/__init__.py b/cupy/__init__.py
--- a/cupy/__init__.py
+++ b/cupy/__init__.py
@@ -445,6 +445,7 @@ def base_repr(number, base=2, padding=0):  # NOQA (needed to avoid redefinition
 # -----------------------------------------------------------------------------
 from cupy.linalg.einsum import einsum  # NOQA
 
+from cupy.linalg.product import cross  # NOQA
 from cupy.linalg.product import dot  # NOQA
 from cupy.linalg.product import inner  # NOQA
 from cupy.linalg.product import kron  # NOQA
diff --git a/cupy/linalg/product.py b/cupy/linalg/product.py
--- a/cupy/linalg/product.py
+++ b/cupy/linalg/product.py
@@ -6,9 +6,9 @@
 from cupy import internal
 
 from cupy.linalg.solve import inv
+from cupy import util
 from cupy.util import collections_abc
 
-
 matmul = core.matmul
 
 
@@ -59,6 +59,128 @@ def vdot(a, b):
     return core.tensordot_core(a, b, None, 1, 1, a.size, ())
 
 
+def cross(a, b, axisa=-1, axisb=-1, axisc=-1, axis=None):
+    """Returns the cross product of two vectors.
+
+    The cross product of ``a`` and ``b`` in :math:`R^3` is a vector
+    perpendicular to both ``a`` and ``b``.  If ``a`` and ``b`` are arrays
+    of vectors, the vectors are defined by the last axis of ``a`` and ``b``
+    by default, and these axes can have dimensions 2 or 3.  Where the
+    dimension of either ``a`` or ``b`` is 2, the third component of the input
+    vector is assumed to be zero and the cross product calculated accordingly.
+    In cases where both input vectors have dimension 2, the z-component of
+    the cross product is returned.
+
+    Args:
+        a (cupy.ndarray): Components of the first vector(s).
+        b (cupy.ndarray): Components of the second vector(s).
+        axisa (int, optional):
+            Axis of ``a`` that defines the vector(s).
+            By default, the last axis.
+        axisb (int, optional):
+            Axis of ``b`` that defines the vector(s).
+            By default, the last axis.
+        axisc (int, optional):
+            Axis of ``c`` containing the cross product vector(s).  Ignored if
+            both input vectors have dimension 2, as the return is scalar.
+            By default, the last axis.
+        axis (int, optional):
+            If defined, the axis of ``a``, ``b`` and ``c``
+            that defines the vector(s) and cross product(s).
+            Overrides ``axisa``, ``axisb`` and ``axisc``.
+
+    Returns:
+        cupy.ndarray :
+            Vector cross product(s).
+
+    .. seealso:: :func:`numpy.cross`
+
+    """
+
+    if axis is not None:
+        axisa, axisb, axisc = (axis,) * 3
+    a = cupy.asarray(a)
+    b = cupy.asarray(b)
+    # Check axisa and axisb are within bounds
+    axisa = util._normalize_axis_index(axisa, a.ndim)
+    axisb = util._normalize_axis_index(axisb, b.ndim)
+
+    # Move working axis to the end of the shape
+    a = cupy.moveaxis(a, axisa, -1)
+    b = cupy.moveaxis(b, axisb, -1)
+    if a.shape[-1] not in (2, 3) or b.shape[-1] not in (2, 3):
+        msg = ('incompatible dimensions for cross product\n'
+               '(dimension must be 2 or 3)')
+        raise ValueError(msg)
+
+    # Create the output array
+    shape = cupy.broadcast(a[..., 0], b[..., 0]).shape
+    if a.shape[-1] == 3 or b.shape[-1] == 3:
+        shape += (3,)
+        # Check axisc is within bounds
+        axisc = util._normalize_axis_index(axisc, len(shape))
+    dtype = cupy.promote_types(a.dtype, b.dtype)
+    cp = cupy.empty(shape, dtype)
+
+    # create local aliases for readability
+    a0 = a[..., 0]
+    a1 = a[..., 1]
+    if a.shape[-1] == 3:
+        a2 = a[..., 2]
+    b0 = b[..., 0]
+    b1 = b[..., 1]
+    if b.shape[-1] == 3:
+        b2 = b[..., 2]
+    if cp.ndim != 0 and cp.shape[-1] == 3:
+        cp0 = cp[..., 0]
+        cp1 = cp[..., 1]
+        cp2 = cp[..., 2]
+
+    if a.shape[-1] == 2:
+        if b.shape[-1] == 2:
+            # a0 * b1 - a1 * b0
+            cupy.multiply(a0, b1, out=cp)
+            cp -= a1 * b0
+            return cp
+        else:
+            assert b.shape[-1] == 3
+            # cp0 = a1 * b2 - 0  (a2 = 0)
+            # cp1 = 0 - a0 * b2  (a2 = 0)
+            # cp2 = a0 * b1 - a1 * b0
+            cupy.multiply(a1, b2, out=cp0)
+            cupy.multiply(a0, b2, out=cp1)
+            cupy.negative(cp1, out=cp1)
+            cupy.multiply(a0, b1, out=cp2)
+            cp2 -= a1 * b0
+    else:
+        assert a.shape[-1] == 3
+        if b.shape[-1] == 3:
+            # cp0 = a1 * b2 - a2 * b1
+            # cp1 = a2 * b0 - a0 * b2
+            # cp2 = a0 * b1 - a1 * b0
+            cupy.multiply(a1, b2, out=cp0)
+            tmp = a2 * b1
+            cp0 -= tmp
+            cupy.multiply(a2, b0, out=cp1)
+            cupy.multiply(a0, b2, out=tmp)
+            cp1 -= tmp
+            cupy.multiply(a0, b1, out=cp2)
+            cupy.multiply(a1, b0, out=tmp)
+            cp2 -= tmp
+        else:
+            assert b.shape[-1] == 2
+            # cp0 = 0 - a2 * b1  (b2 = 0)
+            # cp1 = a2 * b0 - 0  (b2 = 0)
+            # cp2 = a0 * b1 - a1 * b0
+            cupy.multiply(a2, b1, out=cp0)
+            cupy.negative(cp0, out=cp0)
+            cupy.multiply(a2, b0, out=cp1)
+            cupy.multiply(a0, b1, out=cp2)
+            cp2 -= a1 * b0
+
+    return cupy.moveaxis(cp, -1, axisc)
+
+
 def inner(a, b):
     """Returns the inner product of two arrays.
 


Test patch:
diff --git a/tests/cupy_tests/linalg_tests/test_product.py b/tests/cupy_tests/linalg_tests/test_product.py
--- a/tests/cupy_tests/linalg_tests/test_product.py
+++ b/tests/cupy_tests/linalg_tests/test_product.py
@@ -71,6 +71,36 @@ def test_dot_with_out(self, xp, dtype_a, dtype_b, dtype_c):
         return c
 
 
+@testing.parameterize(*testing.product({
+    'params': [
+        #  Test for 0 dimension
+        ((3, ), (3, ), -1, -1, -1),
+        #  Test for basic cases
+        ((1, 2), (1, 2), -1, -1, 1),
+        ((1, 3), (1, 3), 1, -1, -1),
+        ((1, 2), (1, 3), -1, -1, 1),
+        ((2, 2), (1, 3), -1, -1, 0),
+        ((3, 3), (1, 2), 0, -1, -1),
+        ((0, 3), (0, 3), -1, -1, -1),
+        #  Test for higher dimensions
+        ((2, 0, 3), (2, 0, 3), 0, 0, 0),
+        ((2, 4, 5, 3), (2, 4, 5, 3), -1, -1, 0),
+        ((2, 4, 5, 2), (2, 4, 5, 2), 0, 0, -1),
+    ],
+}))
+@testing.gpu
+class TestCrossProduct(unittest.TestCase):
+
+    @testing.with_requires('numpy>=1.10')
+    @testing.for_all_dtypes_combination(['dtype_a', 'dtype_b'])
+    @testing.numpy_cupy_allclose()
+    def test_cross(self, xp, dtype_a, dtype_b):
+        shape_a, shape_b, axisa, axisb, axisc = self.params
+        a = testing.shaped_arange(shape_a, xp, dtype_a)
+        b = testing.shaped_arange(shape_b, xp, dtype_b)
+        return xp.cross(a, b, axisa, axisb, axisc)
+
+
 @testing.parameterize(*testing.product({
     'shape': [
         ((), ()),