Skip to content
Open
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
2 changes: 2 additions & 0 deletions docs/api-assorted.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
cov
create_diagonal
default_dtype
deg2rad
diag_indices
expand_dims
isclose
Expand All @@ -27,6 +28,7 @@
one_hot
pad
partition
rad2deg
searchsorted
setdiff1d
sinc
Expand Down
4 changes: 4 additions & 0 deletions src/array_api_extra/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
broadcast_shapes,
cov,
create_diagonal,
deg2rad,
diag_indices,
expand_dims,
isclose,
Expand All @@ -19,6 +20,7 @@
one_hot,
pad,
partition,
rad2deg,
searchsorted,
setdiff1d,
sinc,
Expand Down Expand Up @@ -49,6 +51,7 @@
"cov",
"create_diagonal",
"default_dtype",
"deg2rad",
"diag_indices",
"expand_dims",
"isclose",
Expand All @@ -62,6 +65,7 @@
"one_hot",
"pad",
"partition",
"rad2deg",
"searchsorted",
"setdiff1d",
"sinc",
Expand Down
80 changes: 80 additions & 0 deletions src/array_api_extra/_delegation.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"broadcast_shapes",
"cov",
"create_diagonal",
"deg2rad",
"diag_indices",
"expand_dims",
"isclose",
Expand All @@ -43,6 +44,7 @@
"one_hot",
"pad",
"partition",
"rad2deg",
"searchsorted",
"setdiff1d",
"sinc",
Expand Down Expand Up @@ -323,6 +325,84 @@ def create_diagonal(
return _funcs.create_diagonal(x, offset=offset, xp=xp)


def deg2rad(x: Array, /, *, xp: ArrayNamespace | None = None) -> Array:
"""
Convert angles from degrees to radians.

Parameters
----------
x : array
Input array in degrees. Must have an integral or floating-point dtype.
xp : array_namespace, optional
The standard-compatible namespace for `x`. Default: infer.

Returns
-------
array
The corresponding angles in radians. Integral inputs are converted to the
default floating-point dtype.
"""
if xp is None:
xp = array_namespace(x)
if xp.isdtype(x.dtype, "integral"):
x = xp.astype(x, _funcs.default_dtype(xp, device=get_device(x)))
elif not xp.isdtype(x.dtype, ("real floating", "complex floating")):
msg = "`x` must have an integral, real floating, or complex floating dtype."
raise TypeError(msg)

if is_jax_namespace(xp) or (
not xp.isdtype(x.dtype, "complex floating")
and (
is_numpy_namespace(xp)
or is_cupy_namespace(xp)
or is_torch_namespace(xp)
or is_dask_namespace(xp)
)
):
return xp.deg2rad(x)

return _funcs.deg2rad(x, xp=xp)


def rad2deg(x: Array, /, *, xp: ArrayNamespace | None = None) -> Array:
"""
Convert angles from radians to degrees.

Parameters
----------
x : array
Input array in radians. Must have an integral or floating-point dtype.
xp : array_namespace, optional
The standard-compatible namespace for `x`. Default: infer.

Returns
-------
array
The corresponding angles in degrees. Integral inputs are converted to the
default floating-point dtype.
"""
if xp is None:
xp = array_namespace(x)
if xp.isdtype(x.dtype, "integral"):
x = xp.astype(x, _funcs.default_dtype(xp, device=get_device(x)))
elif not xp.isdtype(x.dtype, ("real floating", "complex floating")):
msg = "`x` must have an integral, real floating, or complex floating dtype."
raise TypeError(msg)

if is_jax_namespace(xp) or (
not xp.isdtype(x.dtype, "complex floating")
and (
is_numpy_namespace(xp)
or is_cupy_namespace(xp)
or is_torch_namespace(xp)
or is_dask_namespace(xp)
)
):
return xp.rad2deg(x)

return _funcs.rad2deg(x, xp=xp)


def diag_indices(
n: int, /, *, ndim: int = 2, device: Device | None = None, xp: ArrayNamespace
) -> tuple[Array, ...]:
Expand Down
14 changes: 14 additions & 0 deletions src/array_api_extra/_lib/_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"cov",
"create_diagonal",
"default_dtype",
"deg2rad",
"diag_indices",
"expand_dims",
"isclose",
Expand All @@ -43,6 +44,7 @@
"one_hot",
"pad",
"partition",
"rad2deg",
"searchsorted",
"setdiff1d",
"sinc",
Expand Down Expand Up @@ -792,6 +794,18 @@ def angle(z: Array, /, *, deg: bool = False, xp: ArrayNamespace | None = None) -
return a


def deg2rad(x: Array, /, *, xp: ArrayNamespace) -> Array:
# numpydoc ignore=PR01,RT01
"""See docstring in `array_api_extra._delegation.py`."""
return x * xp.pi / 180


def rad2deg(x: Array, /, *, xp: ArrayNamespace) -> Array:
# numpydoc ignore=PR01,RT01
"""See docstring in `array_api_extra._delegation.py`."""
return x * 180 / xp.pi


def unravel_index(indices: Array, shape: tuple[int, ...], /) -> tuple[Array, ...]:
# numpydoc ignore=PR01,RT01
"""See docstring in `array_api_extra._delegation.py`."""
Expand Down
63 changes: 63 additions & 0 deletions tests/test_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
cov,
create_diagonal,
default_dtype,
deg2rad,
diag_indices,
expand_dims,
isclose,
Expand All @@ -35,6 +36,7 @@
one_hot,
pad,
partition,
rad2deg,
setdiff1d,
sinc,
tril_indices,
Expand Down Expand Up @@ -64,6 +66,7 @@
lazy_xp_function(cov)
lazy_xp_function(create_diagonal)
lazy_xp_function(default_dtype)
lazy_xp_function(deg2rad)
lazy_xp_function(diag_indices)
lazy_xp_function(expand_dims)
lazy_xp_function(isclose)
Expand All @@ -74,6 +77,7 @@
lazy_xp_function(one_hot)
lazy_xp_function(pad)
lazy_xp_function(partition)
lazy_xp_function(rad2deg)
# FIXME calls in1d which calls xp.unique_values without size
lazy_xp_function(setdiff1d, jax_jit=False)
lazy_xp_function(sinc)
Expand Down Expand Up @@ -2172,6 +2176,65 @@
assert get_device(angle(a)) == device


class TestDeg2Rad:
def test_basic(self, xp: ArrayNamespace):
x = xp.asarray([0.0, 90.0, 180.0, 270.0, 360.0])
expected = xp.asarray([0.0, xp.pi / 2, xp.pi, 3 * xp.pi / 2, 2 * xp.pi])
assert_close(deg2rad(x, xp=xp), expected)

@pytest.mark.parametrize("dtype_name", ["int32", "int64"])
def test_integral(self, xp: ArrayNamespace, dtype_name: str):
x = xp.asarray([0, 90, 180], dtype=getattr(xp, dtype_name))
actual = deg2rad(x, xp=xp)
expected = xp.asarray(
[0.0, xp.pi / 2, xp.pi], dtype=default_dtype(xp, device=get_device(x))
)
assert actual.dtype == expected.dtype
assert_close(actual, expected)

def test_complex(self, xp: ArrayNamespace):
x = xp.asarray([180 + 90j], dtype=xp.complex64)
actual = deg2rad(x, xp=xp)
expected = xp.asarray([xp.pi + xp.pi / 2 * 1j], dtype=x.dtype)
assert actual.dtype == x.dtype
assert_close(actual, expected)

def test_bool(self, xp: ArrayNamespace):
x = xp.asarray([True])
with pytest.raises(TypeError, match="integral, real floating, or complex"):
deg2rad(x, xp=xp)

Check failure on line 2205 in tests/test_funcs.py

View workflow job for this annotation

GitHub Actions / Lint

Result of call expression is of type "Array" and is not used; assign to variable "_" if this is intentional (reportUnusedCallResult)


class TestRad2Deg:
def test_basic(self, xp: ArrayNamespace):
x = xp.asarray([0.0, xp.pi / 2, xp.pi, 3 * xp.pi / 2, 2 * xp.pi])
expected = xp.asarray([0.0, 90.0, 180.0, 270.0, 360.0])
assert_close(rad2deg(x, xp=xp), expected)

@pytest.mark.parametrize("dtype_name", ["int32", "int64"])
def test_integral(self, xp: ArrayNamespace, dtype_name: str):
x = xp.asarray([0, 1, 2], dtype=getattr(xp, dtype_name))
actual = rad2deg(x, xp=xp)
expected = xp.asarray(
[0.0, 180 / xp.pi, 360 / xp.pi],
dtype=default_dtype(xp, device=get_device(x)),
)
assert actual.dtype == expected.dtype
assert_close(actual, expected)

def test_complex(self, xp: ArrayNamespace):
x = xp.asarray([xp.pi + xp.pi / 2 * 1j], dtype=xp.complex64)
actual = rad2deg(x, xp=xp)
expected = xp.asarray([180 + 90j], dtype=x.dtype)
assert actual.dtype == x.dtype
assert_close(actual, expected)

def test_bool(self, xp: ArrayNamespace):
x = xp.asarray([True])
with pytest.raises(TypeError, match="integral, real floating, or complex"):
rad2deg(x, xp=xp)

Check failure on line 2235 in tests/test_funcs.py

View workflow job for this annotation

GitHub Actions / Lint

Result of call expression is of type "Array" and is not used; assign to variable "_" if this is intentional (reportUnusedCallResult)


class TestUnravelIndex:
def test_simple(self, xp: ArrayNamespace):
indices = xp.asarray([22, 41, 37])
Expand Down
Loading