Skip to content
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
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
94 changes: 94 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,98 @@ 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.

Examples
--------
>>> import array_api_strict as xp
>>> import array_api_extra as xpx
>>> xpx.deg2rad(xp.asarray([0, 90, 180]), xp=xp)
Array([0. , 1.57079633, 3.14159265], dtype=array_api_strict.float64)
"""
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.

Examples
--------
>>> import array_api_strict as xp
>>> import array_api_extra as xpx
>>> xpx.rad2deg(xp.asarray([0.0, xp.pi / 2, xp.pi]), xp=xp)
Array([ 0., 90., 180.], dtype=array_api_strict.float64)
"""
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
71 changes: 71 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,73 @@ def test_device(self, xp: ArrayNamespace, device: Device):
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), 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)

def test_device(self, xp: ArrayNamespace, device: Device):
x = xp.asarray([0.0, 90.0, 180.0], device=device)
assert get_device(deg2rad(x)) == device


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), 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)

def test_device(self, xp: ArrayNamespace, device: Device):
x = xp.asarray([0.0, xp.pi / 2, xp.pi], device=device)
assert get_device(rad2deg(x)) == device


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