From 0ec328b14b3e855c2a30848d23465a0739d5bfa3 Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Date: Thu, 7 May 2026 14:16:45 +0530 Subject: [PATCH 1/2] Adding angle Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> --- src/array_api_extra/__init__.py | 2 ++ src/array_api_extra/_lib/_funcs.py | 33 ++++++++++++++++++++++++++++++ tests/test_funcs.py | 25 ++++++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/src/array_api_extra/__init__.py b/src/array_api_extra/__init__.py index 2fcdcd8e..e41deaeb 100644 --- a/src/array_api_extra/__init__.py +++ b/src/array_api_extra/__init__.py @@ -24,6 +24,7 @@ default_dtype, kron, nunique, + angle, ) from ._lib._lazy import lazy_apply @@ -54,4 +55,5 @@ "setdiff1d", "sinc", "union1d", + "angle", ] diff --git a/src/array_api_extra/_lib/_funcs.py b/src/array_api_extra/_lib/_funcs.py index 97904ddb..cce51179 100644 --- a/src/array_api_extra/_lib/_funcs.py +++ b/src/array_api_extra/_lib/_funcs.py @@ -818,3 +818,36 @@ def union1d(a: Array, b: Array, /, *, xp: ModuleType) -> Array: b = xp.reshape(b, (-1,)) # XXX: `sparse` returns NumPy arrays from `unique_values` return xp.asarray(xp.unique_values(xp.concat([a, b]))) + + +def angle(z: Array, deg: bool = False, /, *, xp: ModuleType | None = None) -> Array: + """ + Return the angle of the complex argument. + + Parameters + ---------- + z : Array + Input array. + deg : bool, optional + Return angle in degrees if True, radians if False (default). + xp : array_namespace, optional + The standard-compatible namespace for `z`. Default: infer. + + Returns + ------- + angle : ndarray or scalar + The counterclockwise angle from the positive real axis on the complex + plane in the range ``(-pi, pi]``, with dtype as float64. + """ + if xp is None: + xp = array_namespace(z) + if xp.isdtype(z.dtype, "complex floating"): + zimage = xp.imag(z) + zreal = xp.real(z) + else: + zimage = xp.zeros_like(z, dtype=xp.float64) + zreal = xp.astype(z, xp.float64) + a = xp.atan2(zimage, zreal) + if deg: + a = a * 180 / xp.pi + return a \ No newline at end of file diff --git a/tests/test_funcs.py b/tests/test_funcs.py index 6a11e059..86d0a377 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -33,6 +33,7 @@ setdiff1d, sinc, union1d, + angle, ) from array_api_extra import ( searchsorted as xpx_searchsorted, @@ -1881,3 +1882,27 @@ def test_device(self, xp: ModuleType, device: Device): a = xp.asarray([-1, 1, 0], device=device) b = xp.asarray([2, -2, 0], device=device) assert get_device(union1d(a, b)) == device + +class TestAngle: + def test_simple(self, xp: ModuleType): + a = xp.asarray([1, 0]) + expected = xp.asarray([0., 0.]) + res = angle(a) + xp_assert_equal(res, expected) + + def test_complex(self, xp: ModuleType): + a = xp.asarray([1 + 1j, 1 - 1j, -1 + 1j, -1 - 1j]) + expected = xp.asarray([np.pi / 4, -np.pi / 4, 3 * np.pi / 4, -3 * np.pi / 4]) + res = angle(a) + xp_assert_equal(res, expected) + + def test_2d(self, xp: ModuleType): + a = xp.asarray([[1 + 1j, 1 - 1j], [-1 + 1j, -1 - 1j]]) + expected = xp.asarray([[np.pi / 4, -np.pi / 4], [3 * np.pi / 4, -3 * np.pi / 4]]) + res = angle(a) + xp_assert_equal(res, expected) + + @pytest.mark.skip_xp_backend(Backend.TORCH, reason="materialize 'meta' device") + def test_device(self, xp: ModuleType, device: Device): + a = xp.asarray([1 + 1j], device=device) + assert get_device(angle(a)) == device \ No newline at end of file From 81c5651cfca0d303bf5765731782b134f71f5d5c Mon Sep 17 00:00:00 2001 From: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> Date: Thu, 7 May 2026 16:37:44 +0530 Subject: [PATCH 2/2] test correction + pre commit Signed-off-by: Pradyot Ranjan <99216956+pradyotRanjan@users.noreply.github.com> --- src/array_api_extra/__init__.py | 4 ++-- src/array_api_extra/_lib/_funcs.py | 4 ++-- tests/test_funcs.py | 11 +++++++---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/array_api_extra/__init__.py b/src/array_api_extra/__init__.py index e41deaeb..17503674 100644 --- a/src/array_api_extra/__init__.py +++ b/src/array_api_extra/__init__.py @@ -19,12 +19,12 @@ ) from ._lib._at import at from ._lib._funcs import ( + angle, apply_where, broadcast_shapes, default_dtype, kron, nunique, - angle, ) from ._lib._lazy import lazy_apply @@ -33,6 +33,7 @@ # pylint: disable=duplicate-code __all__ = [ "__version__", + "angle", "apply_where", "argpartition", "at", @@ -55,5 +56,4 @@ "setdiff1d", "sinc", "union1d", - "angle", ] diff --git a/src/array_api_extra/_lib/_funcs.py b/src/array_api_extra/_lib/_funcs.py index cce51179..b58cf3d0 100644 --- a/src/array_api_extra/_lib/_funcs.py +++ b/src/array_api_extra/_lib/_funcs.py @@ -835,7 +835,7 @@ def angle(z: Array, deg: bool = False, /, *, xp: ModuleType | None = None) -> Ar Returns ------- - angle : ndarray or scalar + ndarray or scalar The counterclockwise angle from the positive real axis on the complex plane in the range ``(-pi, pi]``, with dtype as float64. """ @@ -850,4 +850,4 @@ def angle(z: Array, deg: bool = False, /, *, xp: ModuleType | None = None) -> Ar a = xp.atan2(zimage, zreal) if deg: a = a * 180 / xp.pi - return a \ No newline at end of file + return a diff --git a/tests/test_funcs.py b/tests/test_funcs.py index 86d0a377..71d7bc57 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -13,6 +13,7 @@ from typing_extensions import override from array_api_extra import ( + angle, apply_where, argpartition, at, @@ -33,7 +34,6 @@ setdiff1d, sinc, union1d, - angle, ) from array_api_extra import ( searchsorted as xpx_searchsorted, @@ -1883,10 +1883,11 @@ def test_device(self, xp: ModuleType, device: Device): b = xp.asarray([2, -2, 0], device=device) assert get_device(union1d(a, b)) == device + class TestAngle: def test_simple(self, xp: ModuleType): a = xp.asarray([1, 0]) - expected = xp.asarray([0., 0.]) + expected = xp.asarray([0.0, 0.0], dtype=xp.float64) res = angle(a) xp_assert_equal(res, expected) @@ -1898,11 +1899,13 @@ def test_complex(self, xp: ModuleType): def test_2d(self, xp: ModuleType): a = xp.asarray([[1 + 1j, 1 - 1j], [-1 + 1j, -1 - 1j]]) - expected = xp.asarray([[np.pi / 4, -np.pi / 4], [3 * np.pi / 4, -3 * np.pi / 4]]) + expected = xp.asarray( + [[np.pi / 4, -np.pi / 4], [3 * np.pi / 4, -3 * np.pi / 4]] + ) res = angle(a) xp_assert_equal(res, expected) @pytest.mark.skip_xp_backend(Backend.TORCH, reason="materialize 'meta' device") def test_device(self, xp: ModuleType, device: Device): a = xp.asarray([1 + 1j], device=device) - assert get_device(angle(a)) == device \ No newline at end of file + assert get_device(angle(a)) == device