diff --git a/CHANGELOG.md b/CHANGELOG.md index 7548f1999b9d..d369639a8f63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Allowed input array of `uint64` dtype in `dpnp.bincount` [#2361](https://github.com/IntelPython/dpnp/pull/2361) * The vector norms `ord={None, 1, 2, inf}` and the matrix norms `ord={None, 1, 2, inf, "fro", "nuc"}` now consistently return zero for empty arrays, which are arrays with at least one axis of size zero. This change affects `dpnp.linalg.norm`, `dpnp.linalg.vector_norm`, and `dpnp.linalg.matrix_norm`. Previously, dpnp would either raise errors or return zero depending on the parameters provided [#2371](https://github.com/IntelPython/dpnp/pull/2371) * Extended `dpnp.fft.fftfreq` and `dpnp.fft.rfftfreq` functions to support `dtype` keyword per Python Array API spec 2024.12 [#2384](https://github.com/IntelPython/dpnp/pull/2384) +* Updated `dpnp.fix` to return output with the same data-type of input [#2392](https://github.com/IntelPython/dpnp/pull/2392) ### Fixed diff --git a/dpnp/dpnp_algo/dpnp_elementwise_common.py b/dpnp/dpnp_algo/dpnp_elementwise_common.py index be0e357a0ccf..0e232e0a2bcc 100644 --- a/dpnp/dpnp_algo/dpnp_elementwise_common.py +++ b/dpnp/dpnp_algo/dpnp_elementwise_common.py @@ -41,6 +41,7 @@ "DPNPI0", "DPNPAngle", "DPNPBinaryFunc", + "DPNPFix", "DPNPImag", "DPNPReal", "DPNPRound", @@ -511,6 +512,48 @@ def __call__(self, x, deg=False, out=None, order="K"): return res +class DPNPFix(DPNPUnaryFunc): + """Class that implements dpnp.fix unary element-wise functions.""" + + def __init__( + self, + name, + result_type_resolver_fn, + unary_dp_impl_fn, + docs, + ): + super().__init__( + name, + result_type_resolver_fn, + unary_dp_impl_fn, + docs, + ) + + def __call__(self, x, out=None, order="K"): + if not dpnp.is_supported_array_type(x): + pass # pass to raise error in main implementation + elif dpnp.issubdtype(x.dtype, dpnp.inexact): + pass # for inexact types, pass to calculate in the backend + elif out is not None and not dpnp.is_supported_array_type(out): + pass # pass to raise error in main implementation + elif out is not None and out.dtype != x.dtype: + # passing will raise an error but with incorrect needed dtype + raise ValueError( + f"Output array of type {x.dtype} is needed, got {out.dtype}" + ) + else: + # for exact types, return the input + if out is None: + return dpnp.copy(x, order=order) + + if isinstance(out, dpt.usm_ndarray): + out = dpnp_array._create_from_usm_ndarray(out) + out[...] = x + return out + + return super().__call__(x, out=out, order=order) + + class DPNPI0(DPNPUnaryFunc): """Class that implements dpnp.i0 unary element-wise functions.""" diff --git a/dpnp/dpnp_iface_mathematical.py b/dpnp/dpnp_iface_mathematical.py index 1d3c7789a332..8d35d62b1a96 100644 --- a/dpnp/dpnp_iface_mathematical.py +++ b/dpnp/dpnp_iface_mathematical.py @@ -63,6 +63,7 @@ DPNPI0, DPNPAngle, DPNPBinaryFunc, + DPNPFix, DPNPImag, DPNPReal, DPNPRound, @@ -1728,14 +1729,14 @@ def ediff1d(ary, to_end=None, to_begin=None): Round to nearest integer towards zero. Round an array of floats element-wise to nearest integer towards zero. -The rounded values are returned as floats. +The rounded values have the same data-type as the input. For full documentation refer to :obj:`numpy.fix`. Parameters ---------- x : {dpnp.ndarray, usm_ndarray} - An array of floats to be rounded. + Input array, expected to have a real-valued data type. out : {None, dpnp.ndarray, usm_ndarray}, optional Output array to populate. Array must have the correct shape and the expected data type. @@ -1749,19 +1750,12 @@ def ediff1d(ary, to_end=None, to_begin=None): Returns ------- out : dpnp.ndarray - An array with the rounded values and with the same dimensions as the input. - The returned array will have the default floating point data type for the - device where `a` is allocated. - If `out` is ``None`` then a float array is returned with the rounded values. + An array with the same dimensions and data-type as the input. + If `out` is ``None`` then a new array is returned + with the rounded values. Otherwise the result is stored there and the return value `out` is a reference to that array. -Limitations ------------ -Parameters `where` and `subok` are supported with their default values. -Keyword argument `kwargs` is currently unsupported. -Otherwise ``NotImplementedError`` exception will be raised. - See Also -------- :obj:`dpnp.round` : Round to given number of decimals. @@ -1782,7 +1776,7 @@ def ediff1d(ary, to_end=None, to_begin=None): array([ 2., 2., -2., -2.]) """ -fix = DPNPUnaryFunc( +fix = DPNPFix( "fix", ufi._fix_result_type, ufi._fix, @@ -1934,8 +1928,10 @@ def ediff1d(ary, to_end=None, to_begin=None): Notes ----- -Some spreadsheet programs calculate the "floor-towards-zero", in other words floor(-2.5) == -2. -DPNP instead uses the definition of floor where floor(-2.5) == -3. +Some spreadsheet programs calculate the "floor-towards-zero", where +``floor(-2.5) == -2``. DPNP instead uses the definition of :obj:`dpnp.floor` +where ``floor(-2.5) == -3``. The "floor-towards-zero" function is called +:obj:`dpnp.fix` in DPNP. Examples -------- diff --git a/dpnp/dpnp_iface_statistics.py b/dpnp/dpnp_iface_statistics.py index 57084d86a449..4cade1d9ca1e 100644 --- a/dpnp/dpnp_iface_statistics.py +++ b/dpnp/dpnp_iface_statistics.py @@ -797,8 +797,8 @@ def cov( Default: ``None``. dtype : {None, str, dtype object}, optional Data-type of the result. By default, the return data-type will have - at least floating point type based on the capabilities of the device on - which the input arrays reside. + the default floating point data-type of the device on which the input + arrays reside. Default: ``None``. diff --git a/dpnp/tests/test_binary_ufuncs.py b/dpnp/tests/test_binary_ufuncs.py index 1bc1ddd5b967..41e72b6b957d 100644 --- a/dpnp/tests/test_binary_ufuncs.py +++ b/dpnp/tests/test_binary_ufuncs.py @@ -684,6 +684,7 @@ def test_float_nan(self, dt): expected = numpy.nextafter(numpy.nan, a) assert_equal(result, expected) + @pytest.mark.skipif(not has_support_aspect16(), reason="no fp16 support") @pytest.mark.parametrize("val", [0x7C00, 0x8000], ids=["val1", "val2"]) def test_f16_strides(self, val): a = numpy.arange(val, dtype=numpy.uint16).astype(numpy.float16) @@ -702,6 +703,7 @@ def test_f16_strides(self, val): expected = numpy.nextafter(a[1:], -hinf) assert_equal(result, expected) + @pytest.mark.skipif(not has_support_aspect16(), reason="no fp16 support") @pytest.mark.parametrize("val", [0x7C00, 0x8000], ids=["val1", "val2"]) def test_f16_array_inf(self, val): a = numpy.arange(val, dtype=numpy.uint16).astype(numpy.float16) @@ -716,6 +718,7 @@ def test_f16_array_inf(self, val): expected = numpy.nextafter(-hinf, a) assert_equal(result, expected) + @pytest.mark.skipif(not has_support_aspect16(), reason="no fp16 support") @pytest.mark.parametrize( "sign1, sign2", [ @@ -734,6 +737,7 @@ def test_f16_inf(self, sign1, sign2): expected = numpy.nextafter(hinf1, hinf2) assert_equal(result, expected) + @pytest.mark.skipif(not has_support_aspect16(), reason="no fp16 support") @pytest.mark.parametrize("val", [0x7C00, 0x8000], ids=["val1", "val2"]) def test_f16_array_nan(self, val): a = numpy.arange(val, dtype=numpy.uint16).astype(numpy.float16) @@ -748,6 +752,7 @@ def test_f16_array_nan(self, val): expected = numpy.nextafter(nan, a) assert_equal(result, expected) + @pytest.mark.skipif(not has_support_aspect16(), reason="no fp16 support") @pytest.mark.parametrize( "val1, val2", [ @@ -765,6 +770,7 @@ def test_f16_inf_nan(self, val1, val2): expected = numpy.nextafter(v1, v2) assert_equal(result, expected) + @pytest.mark.skipif(not has_support_aspect16(), reason="no fp16 support") @pytest.mark.parametrize( "val, scalar", [ diff --git a/dpnp/tests/test_mathematical.py b/dpnp/tests/test_mathematical.py index ecb146b3f213..b66b65dc1c44 100644 --- a/dpnp/tests/test_mathematical.py +++ b/dpnp/tests/test_mathematical.py @@ -36,30 +36,6 @@ from .third_party.cupy import testing -def _get_output_data_type(dtype): - """Return a data type specified by input `dtype` and device capabilities.""" - dtype_float16 = any( - dpnp.issubdtype(dtype, t) for t in (dpnp.bool, dpnp.int8, dpnp.uint8) - ) - dtype_float32 = any( - dpnp.issubdtype(dtype, t) for t in (dpnp.int16, dpnp.uint16) - ) - if dtype_float16: - out_dtype = dpnp.float16 if has_support_aspect16() else dpnp.float32 - elif dtype_float32: - out_dtype = dpnp.float32 - elif dpnp.issubdtype(dtype, dpnp.complexfloating): - out_dtype = dpnp.complex64 - if has_support_aspect64() and dtype != dpnp.complex64: - out_dtype = dpnp.complex128 - else: - out_dtype = dpnp.float32 - if has_support_aspect64() and dtype != dpnp.float32: - out_dtype = dpnp.float64 - - return out_dtype - - @pytest.mark.parametrize("deg", [True, False]) class TestAngle: def test_angle_bool(self, deg): @@ -774,68 +750,6 @@ def test_errors(self): assert_raises(ExecutionPlacementError, dpnp.ediff1d, ia, to_end=to_end) -class TestFix: - @pytest.mark.parametrize( - "dt", get_all_dtypes(no_none=True, no_complex=True) - ) - def test_basic(self, dt): - a = get_abs_array([[1.0, 1.1, 1.5, 1.8], [-1.0, -1.1, -1.5, -1.8]], dt) - ia = dpnp.array(a) - - result = dpnp.fix(ia) - expected = numpy.fix(a) - assert_array_equal(result, expected) - - @pytest.mark.parametrize("xp", [numpy, dpnp]) - @pytest.mark.parametrize("dt", get_complex_dtypes()) - def test_complex(self, xp, dt): - a = xp.array([1.1, -1.1], dtype=dt) - with pytest.raises((ValueError, TypeError)): - xp.fix(a) - - @pytest.mark.parametrize( - "a_dt", get_all_dtypes(no_none=True, no_bool=True, no_complex=True) - ) - def test_out(self, a_dt): - a = get_abs_array( - [[1.0, 1.1, 1.5, 1.8], [-1.0, -1.1, -1.5, -1.8]], a_dt - ) - ia = dpnp.array(a) - - out_dt = _get_output_data_type(a.dtype) - out = numpy.zeros_like(a, dtype=out_dt) - iout = dpnp.array(out) - - result = dpnp.fix(ia, out=iout) - expected = numpy.fix(a, out=out) - assert_array_equal(result, expected) - - @pytest.mark.skipif(not has_support_aspect16(), reason="no fp16 support") - @pytest.mark.parametrize("dt", [bool, numpy.float16]) - def test_out_float16(self, dt): - a = numpy.array( - [[1.0, 1.1], [1.5, 1.8], [-1.0, -1.1], [-1.5, -1.8]], dtype=dt - ) - out = numpy.zeros_like(a, dtype=numpy.float16) - ia, iout = dpnp.array(a), dpnp.array(out) - - result = dpnp.fix(ia, out=iout) - expected = numpy.fix(a, out=out) - assert_array_equal(result, expected) - - @pytest.mark.parametrize("xp", [numpy, dpnp]) - @pytest.mark.parametrize("dt", [bool] + get_integer_dtypes()) - def test_out_invalid_dtype(self, xp, dt): - a = xp.array([[1.5, 1.8], [-1.0, -1.1]]) - out = xp.zeros_like(a, dtype=dt) - - with pytest.raises((ValueError, TypeError)): - xp.fix(a, out=out) - - def test_scalar(self): - assert_raises(TypeError, dpnp.fix, -3.4) - - class TestGradient: @pytest.mark.parametrize("dt", get_all_dtypes(no_none=True, no_bool=True)) def test_basic(self, dt): @@ -2291,50 +2205,107 @@ def test_projection(self, dtype): assert dpnp.allclose(result, expected) -@pytest.mark.parametrize("func", ["ceil", "floor", "trunc"]) +@pytest.mark.parametrize("func", ["ceil", "floor", "trunc", "fix"]) class TestRoundingFuncs: @pytest.mark.parametrize( - "dtype", get_all_dtypes(no_none=True, no_complex=True) + "dt", get_all_dtypes(no_none=True, no_complex=True) ) - def test_out(self, func, dtype): - a = generate_random_numpy_array(10, dtype) + def test_basic(self, func, dt): + a = generate_random_numpy_array((2, 4), dt) + ia = dpnp.array(a) + + result = getattr(dpnp, func)(ia) expected = getattr(numpy, func)(a) + assert_array_equal(result, expected) - ia = dpnp.array(a) - out_dt = numpy.int8 if dtype == dpnp.bool else dtype - iout = dpnp.empty(expected.shape, dtype=out_dt) - result = getattr(dpnp, func)(ia, out=iout) + @pytest.mark.parametrize("xp", [numpy, dpnp]) + @pytest.mark.parametrize("dt", get_complex_dtypes()) + def test_complex(self, func, xp, dt): + a = xp.array([1.1, -1.1], dtype=dt) + with pytest.raises((ValueError, TypeError)): + getattr(xp, func)(a) - assert result is iout - # numpy.ceil, numpy.floor, numpy.trunc always return float dtype for - # NumPy < 2.1.0 while output has the dtype of input for NumPy >= 2.1.0 - # (dpnp follows the latter behavior except for boolean dtype where it - # returns int8) - if numpy_version() < "2.1.0" or dtype == dpnp.bool: - check_type = False + @testing.with_requires("numpy>=2.1.0") + @pytest.mark.parametrize( + "dt_in", get_all_dtypes(no_none=True, no_complex=True) + ) + @pytest.mark.parametrize( + "dt_out", get_all_dtypes(no_none=True, no_complex=True) + ) + def test_out(self, func, dt_in, dt_out): + a = generate_random_numpy_array(10, dt_in) + out = numpy.empty(a.shape, dtype=dt_out) + ia, iout = dpnp.array(a), dpnp.array(out) + + if dt_in != dt_out: + if numpy.can_cast(dt_in, dt_out, casting="same_kind"): + # NumPy allows "same_kind" casting, dpnp does not + if func != "fix" and dt_in == dpnp.bool and dt_out == dpnp.int8: + # TODO: get rid of w/a when dpctl#2030 is fixed + pass + else: + assert_raises(ValueError, getattr(dpnp, func), ia, out=iout) + else: + assert_raises(ValueError, getattr(dpnp, func), ia, out=iout) + assert_raises(TypeError, getattr(numpy, func), a, out=out) else: - check_type = True - assert_dtype_allclose(result, expected, check_type=check_type) + if func != "fix" and dt_in == dpnp.bool: + # TODO: get rid of w/a when dpctl#2030 is fixed + out = out.astype(numpy.int8) + iout = iout.astype(dpnp.int8) + + expected = getattr(numpy, func)(a, out=out) + result = getattr(dpnp, func)(ia, out=iout) + assert result is iout + assert_array_equal(result, expected) + + @pytest.mark.skipif(not has_support_aspect16(), reason="no fp16 support") + def test_out_float16(self, func): + a = generate_random_numpy_array((4, 2), numpy.float16) + out = numpy.zeros_like(a, dtype=numpy.float16) + ia, iout = dpnp.array(a), dpnp.array(out) + + result = getattr(dpnp, func)(ia, out=iout) + expected = getattr(numpy, func)(a, out=out) + assert result is iout + assert_array_equal(result, expected) + @testing.with_requires("numpy>=2.1.0") @pytest.mark.parametrize( - "dtype", get_all_dtypes(no_complex=True, no_none=True)[:-1] + "dt", get_all_dtypes(no_none=True, no_complex=True) ) - def test_invalid_dtype(self, func, dtype): - dpnp_dtype = get_all_dtypes(no_complex=True, no_none=True)[-1] - ia = dpnp.arange(10, dtype=dpnp_dtype) - iout = dpnp.empty(10, dtype=dtype) + def test_out_usm_ndarray(self, func, dt): + a = generate_random_numpy_array(10, dt) + out = numpy.empty(a.shape, dtype=dt) + ia, usm_out = dpnp.array(a), dpt.asarray(out) - with pytest.raises(ValueError): - getattr(dpnp, func)(ia, out=iout) + if func != "fix" and dt == dpnp.bool: + # TODO: get rid of w/a when dpctl#2030 is fixed + out = out.astype(numpy.int8) + usm_out = dpt.asarray(usm_out, dtype=dpnp.int8) + expected = getattr(numpy, func)(a, out=out) + result = getattr(dpnp, func)(ia, out=usm_out) + assert result.get_array() is usm_out + assert_array_equal(result, expected) + + @pytest.mark.parametrize("xp", [numpy, dpnp]) @pytest.mark.parametrize( "shape", [(0,), (15,), (2, 2)], ids=["(0,)", "(15,)", "(2, 2)"] ) - def test_invalid_shape(self, func, shape): - ia = dpnp.arange(10, dtype=dpnp.float32) - iout = dpnp.empty(shape, dtype=dpnp.float32) + def test_invalid_shape(self, func, xp, shape): + a = xp.arange(10, dtype=xp.float32) + out = xp.empty(shape, dtype=xp.float32) + assert_raises(ValueError, getattr(xp, func), a, out=out) + + def test_error(self, func): + # scalar, unsupported input + assert_raises(TypeError, getattr(dpnp, func), -3.4) - assert_raises(ValueError, getattr(dpnp, func), ia, out=iout) + # unsupported out + a = dpnp.array([1, 2, 3]) + out = numpy.empty_like(3, dtype=a.dtype) + assert_raises(TypeError, getattr(dpnp, func), a, out=out) class TestHypot: