8000 API: Add ``outer`` to ``numpy.linalg`` [Array API] by mtsokol · Pull Request #25101 · numpy/numpy · GitHub
[go: up one dir, main page]

Skip to content

API: Add outer to numpy.linalg [Array API] #25101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions doc/release/upcoming_changes/25101.new_feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
``outer`` for `numpy.linalg`
----------------------------

`numpy.linalg.outer` has been added. It computes the outer product of two vectors.
It differs from `numpy.outer` by accepting one-dimensional arrays only.
This function is compatible with Array API.
3 changes: 0 additions & 3 deletions doc/source/reference/array_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,8 @@ Function instead of method
These functions are in the ``linalg`` sub-namespace in the array API, but are
only in the top-level namespace in NumPy:

- ``diagonal``
- ``matmul`` (*)
- ``outer``
- ``tensordot`` (*)
- ``trace``

(*): These functions are also in the top-level namespace in the array API.

Expand Down
1 change: 1 addition & 0 deletions doc/source/reference/routines.linalg.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Decompositions
:toctree: generated/

linalg.cholesky
linalg.outer
linalg.qr
linalg.svd
linalg.svdvals
Expand Down
2 changes: 2 additions & 0 deletions numpy/_core/numeric.py
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,8 @@ def outer(a, b, out=None):
ufunc.outer : A generalization to dimensions other than 1D and other
operations. ``np.multiply.outer(a.ravel(), b.ravel())``
is the equivalent.
linalg.outer : An Array API compatible variation of ``np.outer``,
which accepts 1-dimensional inputs only.
tensordot : ``np.tensordot(a.ravel(), b.ravel(), axes=((), ()))``
is the equivalent.

Expand Down
1 change: 1 addition & 0 deletions numpy/linalg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
--------------

cholesky
outer
qr
svd
svdvals
Expand Down
1 change: 1 addition & 0 deletions numpy/linalg/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ from numpy.linalg._linalg import (
tensorinv as tensorinv,
inv as inv,
cholesky as cholesky,
outer as outer,
eigvals as eigvals,
eigvalsh as eigvalsh,
pinv as pinv,
Expand Down
49 changes: 47 additions & 2 deletions numpy/linalg/_linalg.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
'cholesky', 'eigvals', 'eigvalsh', 'pinv', 'slogdet', 'det',
'svd', 'svdvals', 'eig', 'eigh', 'lstsq', 'norm', 'qr', 'cond',
'matrix_rank', 'LinAlgError', 'multi_dot', 'trace', 'diagonal',
'cross']
'cross', 'outer']

import functools
import operator
Expand All @@ -28,7 +28,7 @@
amax, prod, abs, atleast_2d, intp, asanyarray, object_, matmul,
swapaxes, divide, count_nonzero, isnan, sign, argsort, sort,
reciprocal, overrides, diagonal as _core_diagonal, trace as _core_trace,
cross as _core_cross,
cross as _core_cross, outer as _core_outer
)
from numpy.lib._twodim_base_impl import triu, eye
from numpy.lib.array_utils import normalize_axis_index
Expand Down Expand Up @@ -815,8 +815,53 @@ def cholesky(a):
return wrap(r.astype(result_t, copy=False))


# outer product


def _outer_dispatcher(x1, x2):
return (x1, x2)


@array_function_dispatch(_outer_dispatcher)
def outer(x1, x2, /):
"""
Compute the outer product of two vectors.

This function is Array API compatible. Compared to ``np.outer``
it accepts 1-dimensional inputs only.

Parameters
----------
x1 : (M,) array_like
One-dimensional input array of size ``N``.
Must have a numeric data type.
x2 : (N,) array_like
One-dimensional input array of size ``M``.
Must have a numeric data type.

Returns
-------
out : (M, N) ndarray
``out[i, j] = a[i] * b[j]``

See also
--------
outer

"""
x1 = asarray(x1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mtsokol - only seeing this now since it entered nightly and hence caused astropy to notice it is missing coverage: is there any reason this is not asanyarray? It means we have to cover this function even though we already cover np.outer, so with np.asanyarray this would just have worked. I think for new code we should just assume subclasses are OK and able to deal with standard numpy code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It indeed seems reasonable to accept subclasses in new functions. However, as long as np.matrix and masked arrays are still around, I think it'd be a lot safe to have a new validation function that does what asanyarray does plus explicitly rejects instances of those two problematic subclasses. And in order to do so, we need a faster check than isinstance calls (perhaps adding a private ._invalid_subclass and checking for that attribute?). WDYT @mhvk?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just ignore those? Concretely:

  1. Matrix is already rejected here and even if it wasn't, I wouldn't want to bother: we are trying to phase it out, don't use it with new functions.
  2. Masked arrays are a good example of a place where using asarray() is guaranteed to give bad results, while asanyarray() at least has a chance to do the right thing (and in fact, it looks like it does something pretty reasonable here).

The question is what pattern we want, which is the _invalid_subclass thought. I think the answer is: it's the subclasses problem, especially for new functions. We just always use asanyarray(), the subclass should/must implement __array_function__ and deal with it if that doesn't work out.

Using asanyarray() gives the subclass a chance at gambling it can use super().__array_function__ or call func._implementation() (and a bit of a gamble it is, as we may change the implementation; astropy is happy with that gamble).

The main thing I do not quite like about the pattern is that it would be nice if we could open this up more clearly to non-subclasses but...

PS: I actually do wonder if rather than trying to implement a proper __array_function__ for masked arrays, it may be plausible to implement one that does nothing but reject clearly broken ones.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. For non-subclasses, in this case all that is needed is the dimension of the array, so one could do np.ndim(x1) which would be fast for anything except list input. But just changing to asanyarray here would seem sensible!

x2 = asarray(x2)
if x1.ndim != 1 or x2.ndim != 1:
raise ValueError(
"Input arrays must be one-dimensional, but they are "
f"{x1.ndim=} and {x2.ndim=}."
)
return _core_outer(x1, x2, out=None)


# QR decomposition


def _qr_dispatcher(a, mode=None):
return (a,)

Expand Down
34 changes: 34 additions & 0 deletions numpy/linalg/_linalg.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ from typing import (
Generic,
)

import numpy as np
from numpy import (
generic,
floating,
complexfloating,
signedinteger,
unsignedinteger,
timedelta64,
object_,
int32,
float64,
complex128,
Expand All @@ -26,6 +29,8 @@ from numpy.linalg import LinAlgError as LinAlgError
from numpy._typing import (
NDArray,
ArrayLike,
_ArrayLikeUnknown,
_ArrayLikeBool_co,
_ArrayLikeInt_co,
_ArrayLikeUInt_co,
_ArrayLikeFloat_co,
Expand Down Expand Up @@ -140,6 +145,35 @@ def cholesky(a: _ArrayLikeFloat_co) -> NDArray[floating[Any]]: ...
@overload
def cholesky(a: _ArrayLikeComplex_co) -> NDArray[complexfloating[Any, Any]]: ...

@overload
def outer(x1: _ArrayLikeUnknown, x2: _ArrayLikeUnknown) -> NDArray[Any]: ...
@overload
def outer(x1: _ArrayLikeBool_co, x2: _ArrayLikeBool_co) -> NDArray[np.bool]: ...
@overload
def outer(x1: _ArrayLikeUInt_co, x2: _ArrayLikeUInt_co) -> NDArray[unsignedinteger[Any]]: ...
@overload
def outer(x1: _ArrayLikeInt_co, x2: _ArrayLikeInt_co) -> NDArray[signedinteger[Any]]: ...
@overload
def outer(x1: _ArrayLikeFloat_co, x2: _ArrayLikeFloat_co) -> NDArray[floating[Any]]: ...
@overload
def outer(
x1: _ArrayLikeComplex_co,
x2: _ArrayLikeComplex_co,
) -> NDArray[complexfloating[Any, Any]]: ...
@overload
def outer(
x1: _ArrayLikeTD64_co,
x2: _ArrayLikeTD64_co,
out: None = ...,
) -> NDArray[timedelta64]: ...
@overload
def outer(x1: _ArrayLikeObject_co, x2: _ArrayLikeObject_co) -> NDArray[object_]: ...
@overload
def outer(
x1: _ArrayLikeComplex_co | _ArrayLikeTD64_co | _ArrayLikeObject_co,
x2: _ArrayLikeComplex_co | _ArrayLikeTD64_co | _ArrayLikeObject_co,
) -> _ArrayType: ...

@overload
def qr(a: _ArrayLikeInt_co, mode: _ModeKind = ...) -> QRResult: ...
@overload
Expand Down
17 changes: 17 additions & 0 deletions numpy/linalg/tests/test_linalg.py
Original file line number Diff line number Diff line change
Expand Up @@ -1848,6 +1848,23 @@ class ArraySubclass(np.ndarray):
assert_(isinstance(res, np.ndarray))


class TestOuter:
arr1 = np.arange(3)
arr2 = np.arange(3)
expected = np.array(
[[0, 0, 0],
[0, 1, 2],
[0, 2, 4]]
)

assert_array_equal(np.linalg.outer(arr1, arr2), expected)

with assert_raises_regex(
ValueError, "Input arrays must be one-dimensional"
):
np.linalg.outer(arr1[:, np.newaxis], arr2)


def test_byteorder_check():
# Byte order check should pass for native order
if sys.byteorder == 'little':
Expand Down
8 changes: 8 additions & 0 deletions numpy/typing/tests/data/reveal/linalg.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ AR_c16: npt.NDArray[np.complex128]
AR_O: npt.NDArray[np.object_]
AR_m: npt.NDArray[np.timedelta64]
AR_S: npt.NDArray[np.str_]
AR_b: npt.NDArray[np.bool]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated, but @BvB93, is there a way to make the error you get from npt.NDArray[np.bool_] nicer? I bet there are downstream type stubs that are broken now. In an older version of this PR this line triggered the following error:

E   AssertionError: Reveal mismatch at line 21
    
    error:
    Name "np.bool_" is not defined  [name-defined]
        AR_b: npt.NDArray[np.bool_]
        ^~~~~~~~~~~~~~~~~~~~~~~~~~

But both np.bool and np.bool_ are defined right now in the main numpy namespace and neither are deprecated, so the typing should probably accept both or somehow warn that bool_ is deprecated.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated, but @BvB93, is there a way to make the error you get from npt.NDArray[np.bool_] nicer?

Unfortunately not yet, no. There is PEP 702: Marking deprecations using the type system in the works which might help with this in the future, but even then both numpy and mypy will have to add some support for it before seeing the actual benefits.


assert_type(np.linalg.tensorsolve(AR_i8, AR_i8), npt.NDArray[np.float64])
assert_type(np.linalg.tensorsolve(AR_i8, AR_f8), npt.NDArray[np.floating[Any]])
Expand All @@ -44,6 +45,13 @@ assert_type(np.linalg.cholesky(AR_i8), npt.NDArray[np.float64])
assert_type(np.linalg.cholesky(AR_f8), npt.NDArray[np.floating[Any]])
assert_type(np.linalg.cholesky(AR_c16), npt.NDArray[np.complexfloating[Any, Any]])

assert_type(np.linalg.outer(AR_i8, AR_i8), npt.NDArray[np.signedinteger[Any]])
assert_type(np.linalg.outer(AR_f8, AR_f8), npt.NDArray[np.floating[Any]])
assert_type(np.linalg.outer(AR_c16, AR_c16), npt.NDArray[np.complexfloating[Any, Any]])
assert_type(np.linalg.outer(AR_b, AR_b), npt.NDArray[np.bool])
assert_type(np.linalg.outer(AR_O, AR_O), npt.NDArray[np.object_])
assert_type(np.linalg.outer(AR_i8, AR_m), npt.NDArray[np.timedelta64])

assert_type(np.linalg.qr(AR_i8), QRResult)
assert_type(np.linalg.qr(AR_f8), QRResult)
assert_type(np.linalg.qr(AR_c16), QRResult)
Expand Down
3 changes: 0 additions & 3 deletions tools/ci/array-api-skips.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ array_api_tests/test_data_type_functions.py::test_astype
array_api_tests/test_has_names.py::test_has_names[linalg-matmul]
array_api_tests/test_has_names.py::test_has_names[linalg-matrix_norm]
array_api_tests/test_has_names.py::test_has_names[linalg-matrix_transpose]
array_api_tests/test_has_names.py::test_has_names[linalg-outer]
array_api_tests/test_has_names.py::test_has_names[linalg-tensordot]
array_api_tests/test_has_names.py::test_has_names[linalg-vecdot]
array_api_tests/test_has_names.py::test_has_names[linalg-vector_norm]
Expand All @@ -72,7 +71,6 @@ array_api_tests/test_has_names.py::test_has_names[array_attribute-device]
# missing linalg names
array_api_tests/test_linalg.py::test_matrix_norm
array_api_tests/test_linalg.py::test_matrix_transpose
array_api_tests/test_linalg.py::test_outer
array_api_tests/test_linalg.py::test_pinv
array_api_tests/test_linalg.py::test_vecdot

Expand Down Expand Up @@ -118,7 +116,6 @@ array_api_tests/test_signatures.py::test_extension_func_signature[linalg.cholesk
array_api_tests/test_signatures.py::test_extension_func_signature[linalg.matrix_norm]
array_api_tests/test_signatures.py::test_extension_func_signature[linalg.matrix_rank]
array_api_tests/test_signatures.py::test_extension_func_signature[linalg.matrix_transpose]
array_api_tests/test_signatures.py::test_extension_func_signature[linalg.outer]
array_api_tests/test_signatures.py::test_extension_func_signature[linalg.pinv]
array_api_tests/test_signatures.py::test_extension_func_signature[linalg.tensordot]
array_api_tests/test_signatures.py::test_extension_func_signature[linalg.vecdot]
Expand Down
0