diff --git a/docs/src/python/ops.rst b/docs/src/python/ops.rst index a890e7f3b..1c855b855 100644 --- a/docs/src/python/ops.rst +++ b/docs/src/python/ops.rst @@ -43,6 +43,7 @@ Operations cummin cumprod cumsum + degrees dequantize diag diagonal @@ -99,6 +100,7 @@ Operations prod quantize quantized_matmul + radians reciprocal repeat reshape diff --git a/mlx/ops.cpp b/mlx/ops.cpp index 20b445d1f..0d52af3a6 100644 --- a/mlx/ops.cpp +++ b/mlx/ops.cpp @@ -2192,6 +2192,16 @@ array arctanh(const array& a, StreamOrDevice s /* = {} */) { a.shape(), dtype, std::make_shared(to_stream(s)), {input}); } +array degrees(const array& a, StreamOrDevice s /* = {} */) { + auto dtype = at_least_float(a.dtype()); + return multiply(a, array(180.0 / M_PI, dtype), s); +} + +array radians(const array& a, StreamOrDevice s /* = {} */) { + auto dtype = at_least_float(a.dtype()); + return multiply(a, array(M_PI / 180.0, dtype), s); +} + array log(const array& a, StreamOrDevice s /* = {} */) { auto dtype = at_least_float(a.dtype()); auto input = astype(a, dtype, s); diff --git a/mlx/ops.h b/mlx/ops.h index f5d8b3f65..d3f0b8f30 100644 --- a/mlx/ops.h +++ b/mlx/ops.h @@ -851,6 +851,12 @@ array arccosh(const array& a, StreamOrDevice s = {}); /** Inverse Hyperbolic Tangent of the elements of an array */ array arctanh(const array& a, StreamOrDevice s = {}); +/** Convert the elements of an array from Radians to Degrees **/ +array degrees(const array& a, StreamOrDevice s = {}); + +/** Convert the elements of an array from Degrees to Radians **/ +array radians(const array& a, StreamOrDevice s = {}); + /** Natural logarithm of the elements of an array. */ array log(const array& a, StreamOrDevice s = {}); diff --git a/python/src/ops.cpp b/python/src/ops.cpp index 6a36faff6..22c0a97af 100644 --- a/python/src/ops.cpp +++ b/python/src/ops.cpp @@ -1032,6 +1032,40 @@ void init_ops(nb::module_& m) { Returns: array: The inverse hyperbolic tangent of ``a``. )pbdoc"); + m.def( + "degrees", + &mlx::core::degrees, + nb::arg(), + nb::kw_only(), + "stream"_a = nb::none(), + nb::sig( + "def degrees(a: array, /, *, stream: Union[None, Stream, Device] = None) -> array"), + R"pbdoc( + Convert angles from radians to degrees. + + Args: + a (array): Input array. + + Returns: + array: The angles in degrees. + )pbdoc"); + m.def( + "radians", + &mlx::core::radians, + nb::arg(), + nb::kw_only(), + "stream"_a = nb::none(), + nb::sig( + "def radians(a: array, /, *, stream: Union[None, Stream, Device] = None) -> array"), + R"pbdoc( + Convert angles from degrees to radians. + + Args: + a (array): Input array. + + Returns: + array: The angles in radians. + )pbdoc"); m.def( "log", &mlx::core::log, diff --git a/python/tests/test_ops.py b/python/tests/test_ops.py index e170c59fc..696060ccf 100644 --- a/python/tests/test_ops.py +++ b/python/tests/test_ops.py @@ -893,6 +893,22 @@ class TestOps(mlx_tests.MLXTestCase): self.assertTrue(np.allclose(result, expected)) + def test_degrees(self): + a = mx.array( + [0, math.pi / 4, math.pi / 2, math.pi, 3 * math.pi / 4, 2 * math.pi] + ) + result = mx.degrees(a) + expected = np.degrees(a, dtype=np.float32) + + self.assertTrue(np.allclose(result, expected)) + + def test_radians(self): + a = mx.array([0.0, 45.0, 90.0, 180.0, 270.0, 360.0]) + result = mx.radians(a) + expected = np.radians(a, dtype=np.float32) + + self.assertTrue(np.allclose(result, expected)) + def test_log1p(self): a = mx.array([1, 0.5, 10, 100]) result = mx.log1p(a) diff --git a/python/tests/test_vmap.py b/python/tests/test_vmap.py index 5c4690640..adba57534 100644 --- a/python/tests/test_vmap.py +++ b/python/tests/test_vmap.py @@ -49,8 +49,9 @@ class TestVmap(mlx_tests.MLXTestCase): "sin", "sqrt", "square", + "degrees", + "radians", ] - ops = ["erfinv"] for opname in ops: with self.subTest(op=opname): op = getattr(mx, opname) diff --git a/tests/ops_tests.cpp b/tests/ops_tests.cpp index ead089cec..663fc1cf7 100644 --- a/tests/ops_tests.cpp +++ b/tests/ops_tests.cpp @@ -1154,6 +1154,56 @@ TEST_CASE("test arithmetic unary ops") { CHECK(allclose(cos(x), expected).item()); } + // Test degrees + { + array x(0.0); + CHECK_EQ(degrees(x).item(), 0.0); + + x = array(M_PI_2); + CHECK(degrees(x).item() == doctest::Approx(90.0)); + + CHECK(array_equal(degrees(array({})), array({})).item()); + + // Integer input type + x = array(0); + CHECK_EQ(x.dtype(), int32); + CHECK_EQ(degrees(x).item(), 0.0); + + // Input is irregularly strided + x = broadcast_to(array(M_PI_2), {2, 2, 2}); + CHECK(allclose(degrees(x), full({2, 2, 2}, 90.0)).item()); + + float angles[] = {0.0f, M_PI_2, M_PI, 3.0f * M_PI_2}; + x = split(array(angles, {2, 2}), 2, 1)[0]; + auto expected = array({0.0f, 180.0f}, {2, 1}); + CHECK(allclose(degrees(x), expected).item()); + } + + // Test radians + { + array x(0.0); + CHECK_EQ(radians(x).item(), 0.0); + + x = array(90.0); + CHECK(radians(x).item() == doctest::Approx(M_PI_2)); + + CHECK(array_equal(radians(array({})), array({})).item()); + + // Integer input type + x = array(90); + CHECK_EQ(x.dtype(), int32); + CHECK(radians(x).item() == doctest::Approx(M_PI_2)); + + // Input is irregularly strided + x = broadcast_to(array(90.0f), {2, 2, 2}); + CHECK(allclose(radians(x), full({2, 2, 2}, M_PI_2)).item()); + + x = split(array({0.0f, 90.0f, 180.0f, 270.0f}, {2, 2}), 2, 1)[0]; + float angles[] = {0.0f, M_PI}; + auto expected = array(angles, {2, 1}); + CHECK(allclose(radians(x), expected).item()); + } + // Test log { array x(0.0);