diff --git a/CHANGELOG.md b/CHANGELOG.md index ed4779cdb96d..758e9076ad13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ This release is compatible with NumPy 2.5. * Fixed a crash in boolean-mask advanced indexing (`dpnp.ndarray` get/set item) when the selection is empty (e.g. a scalar `False` index that injects a length-0 axis) [#3019](https://github.com/IntelPython/dpnp/pull/3019) * Released the GIL before the remaining blocking OneMKL BLAS and LAPACK calls to prevent host tasks contention, completing the work started in [#2850](https://github.com/IntelPython/dpnp/pull/2850) [#3027](https://github.com/IntelPython/dpnp/pull/3027) * Fixed `dpnp.repeat` raising an unclear `TypeError` for a nested sequence of `repeats` [#3024](https://github.com/IntelPython/dpnp/pull/3024) +* Fixed `astype` casting an out-of-range floating point value to a signed narrow integer type saturating to the destination min/max instead of wrapping like NumPy, generalizing the earlier unsigned-only fix [#3033](https://github.com/IntelPython/dpnp/pull/3033) ### Security diff --git a/dpnp/tensor/libtensor/include/utils/type_utils.hpp b/dpnp/tensor/libtensor/include/utils/type_utils.hpp index bb83c210b9fa..b62bfb38a2e6 100644 --- a/dpnp/tensor/libtensor/include/utils/type_utils.hpp +++ b/dpnp/tensor/libtensor/include/utils/type_utils.hpp @@ -98,15 +98,24 @@ dstTy convert_impl(const srcTy &v) } else if constexpr (!std::is_integral_v && !std::is_same_v && - std::is_integral_v && std::is_unsigned_v) { - // for negative values, cast through signed integer to get two's - // complement wrapping - using intermediateT = - std::conditional_t; - return (v < srcTy{0}) - ? static_cast(static_cast(v)) - : static_cast(v); + std::is_integral_v) { + // Out-of-range float-to-int casts are UB; SYCL saturates while NumPy + // wraps. Funnel through a wider signed integer so the well-defined + // integer narrowing reproduces NumPy's wrapping, e.g. f32(128) -> + // i8(-128). + if constexpr (sizeof(dstTy) < sizeof(std::int64_t)) { + return static_cast(static_cast(v)); + } + else if constexpr (std::is_unsigned_v) { + // uint64: no wider signed type, so only negatives need int64 + return (v < srcTy{0}) + ? static_cast(static_cast(v)) + : static_cast(v); + } + else { + // int64: nothing wider to funnel through + return static_cast(v); + } } else { return static_cast(v); diff --git a/dpnp/tests/tensor/test_usm_ndarray_ctor.py b/dpnp/tests/tensor/test_usm_ndarray_ctor.py index 8a791524546c..9abf09e56cbf 100644 --- a/dpnp/tests/tensor/test_usm_ndarray_ctor.py +++ b/dpnp/tests/tensor/test_usm_ndarray_ctor.py @@ -39,28 +39,12 @@ import dpnp.tensor as dpt from dpnp.tensor import Device +from .elementwise.utils import _all_dtypes, _integral_dtypes, _real_fp_dtypes from .helper import ( get_queue_or_skip, skip_if_dtype_not_supported, ) -_all_dtypes = [ - "b1", - "i1", - "u1", - "i2", - "u2", - "i4", - "u4", - "i8", - "u8", - "f2", - "f4", - "f8", - "c8", - "c16", -] - @pytest.mark.parametrize( "shape", @@ -1039,6 +1023,22 @@ def test_astype_gh_2882(): assert dpt.all(r == expected) +@pytest.mark.usefixtures("suppress_overflow_encountered_in_cast_numpy_warnings") +@pytest.mark.parametrize("dst_dtype", _integral_dtypes) +@pytest.mark.parametrize("src_dtype", _real_fp_dtypes) +def test_astype_out_of_range_float_to_int(src_dtype, dst_dtype): + q = get_queue_or_skip() + skip_if_dtype_not_supported(src_dtype, q) + + values = [0, 1, -1, 127, 128, -129, 255, 256, -256, 300, 60000, -60000] + x_np = np.asarray(values, dtype=src_dtype) + x = dpt.asarray(x_np, sycl_queue=q) + + expected = x_np.astype(dst_dtype) + res = dpt.astype(x, dst_dtype) + assert dpt.all(res == dpt.asarray(expected, sycl_queue=q)) + + def test_copy(): try: X = dpt.usm_ndarray((5, 5), "i4")[2:4, 1:4] @@ -1350,7 +1350,7 @@ def test_full_dtype_inference(): assert np.issubdtype(dpt.full(10, 0.3 - 2j, dtype=rdt).dtype, np.floating) -@pytest.mark.parametrize("dt", ["f2", "f4", "f8"]) +@pytest.mark.parametrize("dt", _real_fp_dtypes) def test_full_special_fp(dt): """See gh-1314""" q = get_queue_or_skip() @@ -1434,7 +1434,7 @@ def test_full_strides(): assert np.array_equal(dpt.asnumpy(X), Xnp) -@pytest.mark.parametrize("dt", ["i1", "u1", "i2", "u2", "i4", "u4", "i8", "u8"]) +@pytest.mark.parametrize("dt", _integral_dtypes) def test_full_gh_1230(dt): get_queue_or_skip() dtype = dpt.dtype(dt) @@ -1551,7 +1551,7 @@ def test_linspace_fp(): assert X.strides == (1,) -@pytest.mark.parametrize("dtype", ["f2", "f4", "f8"]) +@pytest.mark.parametrize("dtype", _real_fp_dtypes) def test_linspace_fp_max(dtype): q = get_queue_or_skip() skip_if_dtype_not_supported(dtype, q)