Skip to content
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

Implement outer for element-wise functions #1813

Merged
merged 9 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
58 changes: 54 additions & 4 deletions dpnp/dpnp_algo/dpnp_elementwise_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,10 +341,60 @@ def __call__(
return out
return dpnp_array._create_from_usm_ndarray(res_usm)

def outer(self, x1, x2):
_x1 = x1[(Ellipsis,) + (None,) * x2.ndim]
_x2 = x2[(None,) * x1.ndim + (Ellipsis,)]
return self.__call__(_x1, _x2)
def outer(
self,
x1,
x2,
out=None,
where=True,
order="K",
dtype=None,
subok=True,
**kwargs,
npolina4 marked this conversation as resolved.
Show resolved Hide resolved
):
"""
Apply the ufunc op to all pairs (a, b) with a in A and b in B.

Parameters
----------
x1 : {dpnp.ndarray, usm_ndarray}
First input array.
x2 : {dpnp.ndarray, usm_ndarray}
Second input array.
out : {None, dpnp.ndarray}, optional
npolina4 marked this conversation as resolved.
Show resolved Hide resolved
Output array to populate.
Array must have the correct shape and the expected data type.
order : {"C", "F", "A", "K"}, optional
npolina4 marked this conversation as resolved.
Show resolved Hide resolved
Memory layout of the newly output array, if parameter `out` is ``None``.
Default: "K".

Returns
-------
out : dpnp.ndarray
Output array. The data type of the returned array is determined by
the Type Promotion Rules.

npolina4 marked this conversation as resolved.
Show resolved Hide resolved
"""

dpnp.check_supported_arrays_type(
x1, x2, scalar_type=True, all_scalars=False
)
if dpnp.isscalar(x1) or dpnp.isscalar(x2):
_x1 = x1
_x2 = x2
else:
_x1 = x1[(Ellipsis,) + (None,) * x2.ndim]
_x2 = x2[(None,) * x1.ndim + (Ellipsis,)]
return self.__call__(
_x1,
_x2,
out=out,
where=where,
order=order,
dtype=dtype,
subok=subok,
**kwargs,
)


class DPNPAngle(DPNPUnaryFunc):
Expand Down
59 changes: 25 additions & 34 deletions dpnp/dpnp_iface_linearalgebra.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,6 @@
import dpnp

# pylint: disable=no-name-in-module
npolina4 marked this conversation as resolved.
Show resolved Hide resolved
from .dpnp_utils import (
call_origin,
)
from .dpnp_utils.dpnp_utils_linearalgebra import (
dpnp_dot,
dpnp_einsum,
Expand Down Expand Up @@ -851,19 +848,25 @@ def matmul(
)


def outer(x1, x2, out=None):
def outer(a, b, out=None):
"""
Returns the outer product of two arrays.

For full documentation refer to :obj:`numpy.outer`.

Limitations
-----------
Parameters `x1` and `x2` are supported as either scalar,
:class:`dpnp.ndarray` or :class:`dpctl.tensor.usm_ndarray`, but both
`x1` and `x2` can not be scalars at the same time. Otherwise
the functions will be executed sequentially on CPU.
Input array data types are limited by supported DPNP :ref:`Data types`.
Parameters
----------
a(M,) : {dpnp.ndarray, usm_ndarray}
npolina4 marked this conversation as resolved.
Show resolved Hide resolved
First input vector. Input is flattened if not already 1-dimensional.
b(N,) : {dpnp.ndarray, usm_ndarray}
Second input vector. Input is flattened if not already 1-dimensional.
out(M, N) : {dpnp.ndarray, usm_ndarray}, optional
A location where the result is stored

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

See Also
antonwolfy marked this conversation as resolved.
Show resolved Hide resolved
--------
Expand All @@ -876,37 +879,25 @@ def outer(x1, x2, out=None):
>>> import dpnp as np
>>> a = np.array([1, 1, 1])
>>> b = np.array([1, 2, 3])
>>> result = np.outer(a, b)
>>> [x for x in result]
>>> np.outer(a, b)
array([[1, 2, 3],
[1, 2, 3],
[1, 2, 3]])

"""

x1_is_scalar = dpnp.isscalar(x1)
x2_is_scalar = dpnp.isscalar(x2)

if x1_is_scalar and x2_is_scalar:
pass
elif not dpnp.is_supported_array_or_scalar(x1):
pass
elif not dpnp.is_supported_array_or_scalar(x2):
pass
dpnp.check_supported_arrays_type(a, b, scalar_type=True, all_scalars=False)
if dpnp.isscalar(a):
x1 = a
x2 = b.flatten()[None, :]
elif dpnp.isscalar(b):
x1 = a.flatten()[:, None]
x2 = b
else:
x1_in = (
x1
if x1_is_scalar
else (x1.reshape(-1) if x1.ndim > 1 else x1)[:, None]
)
x2_in = (
x2
if x2_is_scalar
else (x2.reshape(-1) if x2.ndim > 1 else x2)[None, :]
)
return dpnp.multiply(x1_in, x2_in, out=out)
x1 = a.flatten()
x2 = b.flatten()

return call_origin(numpy.outer, x1, x2, out=out)
return dpnp.multiply.outer(x1, x2, out=out)


def tensordot(a, b, axes=2):
Expand Down
14 changes: 11 additions & 3 deletions tests/test_mathematical.py
Original file line number Diff line number Diff line change
Expand Up @@ -3173,6 +3173,14 @@ def test_elemenwise_outer(x_shape, y_shape):

assert_dtype_allclose(result, expected)

if x_shape != () and y_shape != ():
result_outer = dpnp.outer(x, y)
assert dpnp.allclose(result.flatten(), result_outer.flatten())
result_outer = dpnp.outer(x, y)
assert dpnp.allclose(result.flatten(), result_outer.flatten())


def test_elemenwise_outer_scalar():
s = 5
x = dpnp.asarray([1, 2, 3])
y = dpnp.asarray(s)
expected = dpnp.add.outer(x, y)
result = dpnp.add.outer(x, s)
assert_dtype_allclose(result, expected)
13 changes: 3 additions & 10 deletions tests/test_outer.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,32 +42,25 @@ class TestScalarOuter(unittest.TestCase):
@testing.for_all_dtypes()
@testing.numpy_cupy_allclose(type_check=False)
def test_first_is_scalar(self, xp, dtype):
scalar = xp.int64(4)
scalar = 4
a = xp.arange(5**3, dtype=dtype).reshape(5, 5, 5)
return xp.outer(scalar, a)

@testing.for_all_dtypes()
@testing.numpy_cupy_allclose(type_check=False)
def test_second_is_scalar(self, xp, dtype):
scalar = xp.int32(7)
scalar = 7
a = xp.arange(5**3, dtype=dtype).reshape(5, 5, 5)
return xp.outer(a, scalar)

@pytest.mark.usefixtures("allow_fall_back_on_numpy")
@testing.numpy_cupy_array_equal()
def test_both_inputs_as_scalar(self, xp):
a = xp.int64(4)
b = xp.int32(17)
return xp.outer(a, b)


class TestListOuter(unittest.TestCase):
def test_list(self):
a = np.arange(27).reshape(3, 3, 3)
b: list[list[list[int]]] = a.tolist()
dp_a = dp.array(a)

with assert_raises(NotImplementedError):
with assert_raises(TypeError):
dp.outer(b, dp_a)
dp.outer(dp_a, b)
dp.outer(b, b)
Loading