Skip to content

Commit

Permalink
ENH: Adds cross2d to core and linalg (with API compatibility)
Browse files Browse the repository at this point in the history
This PR stems from the community meeting on Jun 5, 2024.
This implements a new function cross2d that accepts (a stack of)
2-element vectors, in the same manner that cross currently does.
Both np.cross2d and np.linalg.cross2d are added.
The np.linalg.cross2d is API compatible.
This PR closes numpy#13718 and closes numpy#26620.
Units tests for cross2d are included.
A new test_ValueError is added for cross.
Updated doc links for both funtions.
Added examples for cross2d. Cleaned up whitespace on both functions.
  • Loading branch information
bmwoodruff committed Jun 7, 2024
1 parent 841e3ed commit 98f1a5f
Show file tree
Hide file tree
Showing 16 changed files with 373 additions and 13 deletions.
1 change: 1 addition & 0 deletions doc/source/reference/routines.linalg.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Matrix and vector products
linalg.matrix_power
kron
linalg.cross
linalg.cross2d

Decompositions
--------------
Expand Down
1 change: 1 addition & 0 deletions doc/source/reference/routines.math.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Sums, products, differences
ediff1d
gradient
cross
cross2d

Exponents and logarithms
------------------------
Expand Down
2 changes: 2 additions & 0 deletions doc/source/user/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@ operate elementwise on an array, producing an array as output.
`corrcoef`,
`cov`,
`cross`,
`cross2d`,
`cumprod`,
`cumsum`,
`diff`,
Expand Down Expand Up @@ -1015,6 +1016,7 @@ Basic Statistics
`var`
Basic Linear Algebra
`cross`,
`cross2d`,
`dot`,
`outer`,
`linalg.svd`,
Expand Down
2 changes: 1 addition & 1 deletion numpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@
can_cast, cbrt, cdouble, ceil, character, choose, clip, clongdouble,
complex128, complex64, complexfloating, compress, concat, concatenate,
conj, conjugate, convolve, copysign, copyto, correlate, cos, cosh,
count_nonzero, cross, csingle, cumprod, cumsum,
count_nonzero, cross, cross2d, csingle, cumprod, cumsum,
datetime64, datetime_as_string, datetime_data, deg2rad, degrees,
diagonal, divide, divmod, dot, double, dtype, e, einsum, einsum_path,
empty, empty_like, equal, errstate, euler_gamma, exp, exp2, expm1,
Expand Down
1 change: 1 addition & 0 deletions numpy/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ from numpy._core.numeric import (
rollaxis as rollaxis,
moveaxis as moveaxis,
cross as cross,
cross2d as cross2d,
indices as indices,
fromfunction as fromfunction,
isscalar as isscalar,
Expand Down
144 changes: 136 additions & 8 deletions numpy/_core/numeric.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
'can_cast', 'promote_types', 'min_scalar_type',
'result_type', 'isfortran', 'empty_like', 'zeros_like', 'ones_like',
'correlate', 'convolve', 'inner', 'dot', 'outer', 'vdot', 'roll',
'rollaxis', 'moveaxis', 'cross', 'tensordot', 'little_endian',
'rollaxis', 'moveaxis', 'cross', 'cross2d', 'tensordot', 'little_endian',
'fromiter', 'array_equal', 'array_equiv', 'indices', 'fromfunction',
'isclose', 'isscalar', 'binary_repr', 'base_repr', 'ones',
'identity', 'allclose', 'putmask',
Expand Down Expand Up @@ -1555,8 +1555,9 @@ def cross(a, b, axisa=-1, axisb=-1, axisc=-1, axis=None):
--------
inner : Inner product
outer : Outer product.
linalg.cross : An Array API compatible variation of ``np.cross``,
linalg.cross : An Array API compatible variation of `numpy.cross`,
which accepts (arrays of) 3-element vectors only.
cross2d : A version for 2-element vectors only.
ix_ : Construct index arrays.
Notes
Expand Down Expand Up @@ -1590,16 +1591,16 @@ def cross(a, b, axisa=-1, axisb=-1, axisc=-1, axis=None):
Both vectors with dimension 2.
>>> x = [1,2]
>>> y = [4,5]
>>> x = [1, 2]
>>> y = [4, 5]
>>> np.cross(x, y)
array(-3)
Multiple vector cross-products. Note that the direction of the cross
product vector is defined by the *right-hand rule*.
>>> x = np.array([[1,2,3], [4,5,6]])
>>> y = np.array([[4,5,6], [1,2,3]])
>>> x = np.array([[1, 2, 3], [4, 5, 6]])
>>> y = np.array([[4, 5, 6], [1, 2, 3]])
>>> np.cross(x, y)
array([[-3, 6, -3],
[ 3, -6, 3]])
Expand All @@ -1613,8 +1614,8 @@ def cross(a, b, axisa=-1, axisb=-1, axisc=-1, axis=None):
Change the vector definition of `x` and `y` using `axisa` and `axisb`.
>>> x = np.array([[1,2,3], [4,5,6], [7, 8, 9]])
>>> y = np.array([[7, 8, 9], [4,5,6], [1,2,3]])
>>> x = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
>>> y = np.array([[7, 8, 9], [4, 5, 6], [1, 2, 3]])
>>> np.cross(x, y)
array([[ -6, 12, -6],
[ 0, 0, 0],
Expand Down Expand Up @@ -1724,6 +1725,133 @@ def cross(a, b, axisa=-1, axisb=-1, axisc=-1, axis=None):
return moveaxis(cp, -1, axisc)


def _cross2d_dispatcher(a, b, axisa=None, axisb=None, axis=None):
return (a, b)


@array_function_dispatch(_cross2d_dispatcher)
def cross2d(a, b, axisa=-1, axisb=-1, axis=None):
"""
Return the cross product of two (arrays of) vectors of dimension 2.
This is a dimension 2 version of `cross`.
As the dimension of both `a` and `b` is 2, the third component of the
input vectors are assumed to be zero.
The cross product is calculated accordingly and
the z-component of the cross product is returned as a scalar.
Parameters
----------
a : array_like
Components of the first vector(s).
b : array_like
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.
axis : int, optional
If defined, the axis of `a` and `b` that defines the vector(s).
Overrides `axisa` and `axisb`.
Returns
-------
c : ndarray
Scalar cross product(s) of 2d vectors.
Raises
------
ValueError
When the dimension of the vector(s) in `a` and/or `b` does not
equal 2.
See Also
--------
inner : Inner product
outer : Outer product.
cross : Vector cross product of 3 dimensional vectors.
linalg.cross2d : An Array API compatible variation of `numpy.cross2d`,
which accepts (arrays of) 2-element vectors only.
Notes
-----
.. versionadded:: 2.1.0
Supports full broadcasting of the inputs.
Examples
--------
Scalar cross product of 2-dimensional vectors.
>>> x = [1, 2]
>>> y = [4, 5]
>>> np.cross2d(x, y)
array(-3)
Multiple scalar cross products. Note that the direction of the cross
product vector is defined by the *right-hand rule* and
the cross product of a vector with itself is always zero.
>>> x = np.array([[1, 2], [3, 4], [5, 6]])
>>> y = np.array([[3, 4], [1, 2], [5, 6]])
>>> np.cross2d(x, y)
array([-2, 2, 0])
The choice of axis matters.
>>> u = [[1, 2], [3, 4]]
>>> v = [[3, 4], [5, 6]]
>>> np.cross2d(u, v)
array([-2, -2])
>>> np.cross2d(u, v, axisa=0)
array([-5, -8])
>>> np.cross2d(u, v, axisb=0)
array([-1, 2])
>>> np.cross2d(u, v, axis=0)
array([-4, -4])
"""
if axis is not None:
axisa, axisb = (axis,) * 2
a = asarray(a)
b = asarray(b)

if (a.ndim < 1) or (b.ndim < 1):
raise ValueError("At least one array has zero dimension")

# Check axisa and axisb are within bounds
axisa = normalize_axis_index(axisa, a.ndim, msg_prefix='axisa')
axisb = normalize_axis_index(axisb, b.ndim, msg_prefix='axisb')

# Move working axis to the end of the shape
a = moveaxis(a, axisa, -1)
b = moveaxis(b, axisb, -1)
msg = ("incompatible dimensions for cross2d product\n"
"(dimension must be 2)")
if a.shape[-1] !=2 or b.shape[-1] != 2:
raise ValueError(msg)

# Create the output array
shape = broadcast(a[..., 0], b[..., 0]).shape
dtype = promote_types(a.dtype, b.dtype)
cp = empty(shape, dtype)

# recast arrays as dtype
a = a.astype(dtype)
b = b.astype(dtype)

# create local aliases for readability
a0 = a[..., 0]
a1 = a[..., 1]
b0 = b[..., 0]
b1 = b[..., 1]

# a0 * b1 - a1 * b0
multiply(a0, b1, out=cp)
cp -= a1 * b0
return cp


little_endian = (sys.byteorder == 'little')


Expand Down
57 changes: 57 additions & 0 deletions numpy/_core/numeric.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,63 @@ def cross(
axis: None | int = ...,
) -> NDArray[object_]: ...

@overload
def cross2d(
x1: _ArrayLikeUnknown,
x2: _ArrayLikeUnknown,
axisa: int = ...,
axisb: int = ...,
axis: None | int = ...,
) -> NDArray[Any]: ...
@overload
def cross2d(
x1: _ArrayLikeBool_co,
x2: _ArrayLikeBool_co,
axisa: int = ...,
axisb: int = ...,
axis: None | int = ...,
) -> NoReturn: ...
@overload
def cross2d(
x1: _ArrayLikeUInt_co,
x2: _ArrayLikeUInt_co,
axisa: int = ...,
axisb: int = ...,
axis: None | int = ...,
) -> NDArray[unsignedinteger[Any]]: ...
@overload
def cross2d(
x1: _ArrayLikeInt_co,
x2: _ArrayLikeInt_co,
axisa: int = ...,
axisb: int = ...,
axis: None | int = ...,
) -> NDArray[signedinteger[Any]]: ...
@overload
def cross2d(
x1: _ArrayLikeFloat_co,
x2: _ArrayLikeFloat_co,
axisa: int = ...,
axisb: int = ...,
axisc: int = ...,
) -> NDArray[floating[Any]]: ...
@overload
def cross2d(
x1: _ArrayLikeComplex_co,
x2: _ArrayLikeComplex_co,
axisa: int = ...,
axisb: int = ...,
axis: None | int = ...,
) -> NDArray[complexfloating[Any, Any]]: ...
@overload
def cross2d(
x1: _ArrayLikeObject_co,
x2: _ArrayLikeObject_co,
axisa: int = ...,
axisb: int = ...,
axis: None | int = ...,
) -> NDArray[object_]: ...

@overload
def indices(
dimensions: Sequence[int],
Expand Down
53 changes: 53 additions & 0 deletions numpy/_core/tests/test_numeric.py
Original file line number Diff line number Diff line change
Expand Up @@ -3828,6 +3828,15 @@ def test_3x3(self):
cp = np.cross(v, u)
assert_equal(cp, -z)

def test_ValueError(self):
u = [0, 1, 2, 3]
v = [4, 5, 6, 4]
assert_raises(ValueError, np.cross, u, v)
u = np.arange(3)
v = np.arange(6)
assert_raises(ValueError, np.cross, u, v)
assert_raises(ValueError, np.cross, v, u)

@pytest.mark.filterwarnings(
"ignore:.*2-dimensional vectors.*:DeprecationWarning"
)
Expand Down Expand Up @@ -3897,6 +3906,50 @@ def test_zero_dimension(self, a, b):
np.cross(a, b)
assert "At least one array has zero dimension" in str(exc.value)

class TestCross2d:
def test_2x2(self):
u = [1, 2]
v = [3, 4]
z = -2
cp = np.cross2d(u, v)
assert_equal(cp, z)
cp = np.cross2d(v, u)
assert_equal(cp, -z)

def test_broadcasting(self):
# Ticket #2624 (Trac #2032)
u = np.tile([1, 2], (11, 1))
v = np.tile([3, 4], (11, 1))
z = -2
assert_equal(np.cross2d(u, v), z)
assert_equal(np.cross2d(v, u), -z)
assert_equal(np.cross2d(u, u), 0)

def test_broadcasting_shapes(self):
u = np.ones((2, 1, 2))
v = np.ones((5, 2))
assert_equal(np.cross2d(u, v).shape, (2, 5))
u = np.ones((10, 2, 5))
v = np.ones((2, 5))
assert_equal(np.cross2d(u, v, axisa=1, axisb=0).shape, (10, 5))
assert_raises(ValueError, np.cross2d, u, v, axisa=0, axisb=1)
assert_raises(ValueError, np.cross2d, u, v, axisa=1, axisb=1)
assert_raises(AxisError, np.cross2d, u, v, axisa=-5, axisb=2)
assert_raises(AxisError, np.cross2d, u, v, axisa=1, axisb=-4)

def test_uint8_int32_mixed_dtypes(self):
# regression test for gh-19138, adapted from cross
u = np.array([[195, 8]], np.uint8)
v = np.array([250, 166], np.int32)
z = np.array([-30370], dtype=np.int32)
assert_equal(np.cross2d(v, u), z)
assert_equal(np.cross2d(u, v), -z)

@pytest.mark.parametrize("a, b", [(0, [1, 2]), ([1, 2], 3)])
def test_zero_dimension(self, a, b):
with pytest.raises(ValueError) as exc:
np.cross2d(a, b)
assert "At least one array has zero dimension" in str(exc.value)

def test_outer_out_param():
arr1 = np.ones((5,))
Expand Down
1 change: 1 addition & 0 deletions numpy/linalg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
--------------------------
cross
cross2d
multi_dot
matrix_power
tensordot
Expand Down
1 change: 1 addition & 0 deletions numpy/linalg/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ from numpy.linalg._linalg import (
trace as trace,
diagonal as diagonal,
cross as cross,
cross2d as cross2d,
)

from numpy._core.fromnumeric import (
Expand Down
Loading

0 comments on commit 98f1a5f

Please sign in to comment.