From 341c6ea9d6a8843abb27fe2c6bb83771f5bda614 Mon Sep 17 00:00:00 2001 From: Lucas Colley Date: Thu, 14 Nov 2024 17:15:15 +0000 Subject: [PATCH 1/6] ENH: add `create_diagonal` Co-authored-by: Jake Bowhay --- src/array_api_extra/_funcs.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/array_api_extra/_funcs.py b/src/array_api_extra/_funcs.py index 459db63..e589242 100644 --- a/src/array_api_extra/_funcs.py +++ b/src/array_api_extra/_funcs.py @@ -140,6 +140,14 @@ def cov(m: Array, /, *, xp: ModuleType) -> Array: return xp.squeeze(c, axis=axes) +def create_diagonal(x: Array, /, *, offset: int = 0, xp: ModuleType) -> Array: + n = x.shape[0] + abs(offset) + diag = xp.zeros(n**2, dtype=x.dtype) + i = offset if offset >= 0 else abs(offset) * n + diag[i : min(n * (n - offset), diag.shape[0]) : n + 1] = x + return xp.reshape(diag, (n, n)) + + def _mean( x: Array, /, From dd1dee6f36f1219e8dc63efb3082554f953038f2 Mon Sep 17 00:00:00 2001 From: Lucas Colley Date: Thu, 14 Nov 2024 17:39:26 +0000 Subject: [PATCH 2/6] TST: `create_diagonal`: add tests --- src/array_api_extra/__init__.py | 4 ++-- src/array_api_extra/_funcs.py | 3 +++ tests/test_funcs.py | 26 +++++++++++++++++++++++++- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/array_api_extra/__init__.py b/src/array_api_extra/__init__.py index 3049274..3780b50 100644 --- a/src/array_api_extra/__init__.py +++ b/src/array_api_extra/__init__.py @@ -1,7 +1,7 @@ from __future__ import annotations -from ._funcs import atleast_nd, cov, expand_dims, kron, sinc +from ._funcs import atleast_nd, cov, create_diagonal, expand_dims, kron, sinc __version__ = "0.1.2.dev0" -__all__ = ["__version__", "atleast_nd", "cov", "expand_dims", "kron", "sinc"] +__all__ = ["__version__", "atleast_nd", "cov", "create_diagonal", "expand_dims", "kron", "sinc"] diff --git a/src/array_api_extra/_funcs.py b/src/array_api_extra/_funcs.py index e589242..c9bdab7 100644 --- a/src/array_api_extra/_funcs.py +++ b/src/array_api_extra/_funcs.py @@ -141,6 +141,9 @@ def cov(m: Array, /, *, xp: ModuleType) -> Array: def create_diagonal(x: Array, /, *, offset: int = 0, xp: ModuleType) -> Array: + if x.ndim != 1: + err_msg = "`x` must be 1-dimensional." + raise ValueError(err_msg) n = x.shape[0] + abs(offset) diag = xp.zeros(n**2, dtype=x.dtype) i = offset if offset >= 0 else abs(offset) * n diff --git a/tests/test_funcs.py b/tests/test_funcs.py index 30e71eb..5fa8a2a 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -9,7 +9,7 @@ import pytest from numpy.testing import assert_allclose, assert_array_equal, assert_equal -from array_api_extra import atleast_nd, cov, expand_dims, kron, sinc +from array_api_extra import atleast_nd, cov, create_diagonal, expand_dims, kron, sinc if TYPE_CHECKING: Array = Any # To be changed to a Protocol later (see array-api#589) @@ -112,6 +112,30 @@ def test_combination(self): assert_allclose(cov(y, xp=xp), xp.asarray(2.144133), rtol=1e-6) +class TestCreateDiagonal: + def test_1d(self): + vals = 100 * xp.arange(5, dtype=xp.float64) + b = xp.zeros((5, 5)) + for k in range(5): + b[k, k] = vals[k] + assert_array_equal(create_diagonal(vals, xp=xp), b) + b = xp.zeros((7, 7)) + c = xp.asarray(b, copy=True) + for k in range(5): + b[k, k + 2] = vals[k] + c[k + 2, k] = vals[k] + assert_array_equal(create_diagonal(vals, offset=2, xp=xp), b) + assert_array_equal(create_diagonal(vals, offset=-2, xp=xp), c) + + def test_0d(self): + with pytest.raises(ValueError, match="1-dimensional"): + print(create_diagonal(xp.asarray(1), xp=xp)) + + def test_2d(self): + with pytest.raises(ValueError, match="1-dimensional"): + print(create_diagonal(xp.asarray([[1]]), xp=xp)) + + class TestKron: def test_basic(self): # Using 0-dimensional array From 872ad8b5c46ac941763c16681955a7e7c7b0f257 Mon Sep 17 00:00:00 2001 From: Lucas Colley Date: Thu, 14 Nov 2024 19:18:03 +0000 Subject: [PATCH 3/6] DOC: `create_diagonal`: add docs --- docs/api-reference.md | 1 + src/array_api_extra/_funcs.py | 38 ++++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index 1dc3dfc..1307b6f 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -8,6 +8,7 @@ atleast_nd cov + create_diagonal expand_dims kron sinc diff --git a/src/array_api_extra/_funcs.py b/src/array_api_extra/_funcs.py index c9bdab7..b8bf73e 100644 --- a/src/array_api_extra/_funcs.py +++ b/src/array_api_extra/_funcs.py @@ -6,7 +6,7 @@ if TYPE_CHECKING: from ._typing import Array, ModuleType -__all__ = ["atleast_nd", "cov", "expand_dims", "kron", "sinc"] +__all__ = ["atleast_nd", "cov", "create_diagonal", "expand_dims", "kron", "sinc"] def atleast_nd(x: Array, /, *, ndim: int, xp: ModuleType) -> Array: @@ -141,6 +141,42 @@ def cov(m: Array, /, *, xp: ModuleType) -> Array: def create_diagonal(x: Array, /, *, offset: int = 0, xp: ModuleType) -> Array: + """ + Construct a diagonal array. + + Parameters + ---------- + x : array + A 1-D array + offset : int, optional + Offset from the leading diagonal (default is ``0``). + Use positive ints for diagonals above the leading diagonal, + and negative ints for diagonals below the leading diagonal. + + Returns + ------- + res : array + A 2-D array with `x` on the diagonal (offset by `offset`). + + Examples + -------- + >>> import array_api_strict as xp + >>> import array_api_extra as xpx + >>> x = xp.asarray([2, 4, 8]) + + >>> xpx.create_diagonal(x, xp=xp) + Array([[2, 0, 0], + [0, 4, 0], + [0, 0, 8]], dtype=array_api_strict.int64) + + >>> xpx.create_diagonal(x, offset=-2, xp=xp) + Array([[0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [2, 0, 0, 0, 0], + [0, 4, 0, 0, 0], + [0, 0, 8, 0, 0]], dtype=array_api_strict.int64) + + """ if x.ndim != 1: err_msg = "`x` must be 1-dimensional." raise ValueError(err_msg) From 254cd4a1980f340081449be993e6b395f0f9c4d2 Mon Sep 17 00:00:00 2001 From: Lucas Colley Date: Thu, 14 Nov 2024 21:36:47 +0000 Subject: [PATCH 4/6] DOC: `create_diagonal`: add `xp` param --- src/array_api_extra/_funcs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/array_api_extra/_funcs.py b/src/array_api_extra/_funcs.py index b8bf73e..ce80018 100644 --- a/src/array_api_extra/_funcs.py +++ b/src/array_api_extra/_funcs.py @@ -152,6 +152,8 @@ def create_diagonal(x: Array, /, *, offset: int = 0, xp: ModuleType) -> Array: Offset from the leading diagonal (default is ``0``). Use positive ints for diagonals above the leading diagonal, and negative ints for diagonals below the leading diagonal. + xp : array_namespace + The standard-compatible namespace for `x`. Returns ------- From 0b28fc37a66ff5dc625096374303a680a4999980 Mon Sep 17 00:00:00 2001 From: Lucas Colley Date: Mon, 18 Nov 2024 12:00:33 +0000 Subject: [PATCH 5/6] appease linter --- src/array_api_extra/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/array_api_extra/__init__.py b/src/array_api_extra/__init__.py index 3780b50..a69e939 100644 --- a/src/array_api_extra/__init__.py +++ b/src/array_api_extra/__init__.py @@ -4,4 +4,12 @@ __version__ = "0.1.2.dev0" -__all__ = ["__version__", "atleast_nd", "cov", "create_diagonal", "expand_dims", "kron", "sinc"] +__all__ = [ + "__version__", + "atleast_nd", + "cov", + "create_diagonal", + "expand_dims", + "kron", + "sinc", +] From f4a3bad36234981cd5bdf96a4a513e268dc408c0 Mon Sep 17 00:00:00 2001 From: Lucas Colley Date: Mon, 18 Nov 2024 12:04:43 +0000 Subject: [PATCH 6/6] TST: `create_diagonal`: add test from SciPy Co-authored-by: Jake Bowhay --- tests/test_funcs.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/test_funcs.py b/tests/test_funcs.py index 5fa8a2a..a8db987 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -6,6 +6,7 @@ # array-api-strict#6 import array_api_strict as xp # type: ignore[import-untyped] +import numpy as np import pytest from numpy.testing import assert_allclose, assert_array_equal, assert_equal @@ -114,6 +115,7 @@ def test_combination(self): class TestCreateDiagonal: def test_1d(self): + # from np.diag tests vals = 100 * xp.arange(5, dtype=xp.float64) b = xp.zeros((5, 5)) for k in range(5): @@ -127,13 +129,24 @@ def test_1d(self): assert_array_equal(create_diagonal(vals, offset=2, xp=xp), b) assert_array_equal(create_diagonal(vals, offset=-2, xp=xp), c) + @pytest.mark.parametrize("n", range(1, 10)) + @pytest.mark.parametrize("offset", range(1, 10)) + def test_create_diagonal(self, n, offset): + # from scipy._lib tests + rng = np.random.default_rng(2347823) + one = xp.asarray(1.0) + x = rng.random(n) + A = create_diagonal(xp.asarray(x, dtype=one.dtype), offset=offset, xp=xp) + B = xp.asarray(np.diag(x, offset), dtype=one.dtype) + assert_array_equal(A, B) + def test_0d(self): with pytest.raises(ValueError, match="1-dimensional"): - print(create_diagonal(xp.asarray(1), xp=xp)) + create_diagonal(xp.asarray(1), xp=xp) def test_2d(self): with pytest.raises(ValueError, match="1-dimensional"): - print(create_diagonal(xp.asarray([[1]]), xp=xp)) + create_diagonal(xp.asarray([[1]]), xp=xp) class TestKron: