From cb8ea97a76ffb1ceb4b65c03439a8d3112cbd949 Mon Sep 17 00:00:00 2001 From: mattip Date: Mon, 17 Feb 2025 07:16:43 +0200 Subject: [PATCH 01/29] start implementing same_value casting --- doc/source/reference/c-api/array.rst | 5 +++++ numpy/__init__.cython-30.pxd | 1 + numpy/__init__.pxd | 1 + numpy/_core/include/numpy/ndarraytypes.h | 2 ++ numpy/_core/src/multiarray/_multiarray_tests.c.src | 1 + numpy/_core/src/multiarray/array_method.c | 1 + numpy/_core/src/multiarray/conversion_utils.c | 4 ++++ numpy/_core/src/multiarray/convert_datatype.c | 2 ++ 8 files changed, 17 insertions(+) diff --git a/doc/source/reference/c-api/array.rst b/doc/source/reference/c-api/array.rst index dc043b77f187..9996f977ad7e 100644 --- a/doc/source/reference/c-api/array.rst +++ b/doc/source/reference/c-api/array.rst @@ -4452,5 +4452,10 @@ Enumerated Types Allow any cast, no matter what kind of data loss may occur. + .. c:enumerator:: NPY_SAME_VALUE_CASTING + + Allow any cast, but error if any values change during the cast. Currently + supported only in ``ndarray.astype(... casting='same_value')`` + .. index:: pair: ndarray; C-API diff --git a/numpy/__init__.cython-30.pxd b/numpy/__init__.cython-30.pxd index 86c91cf617a5..c71898626070 100644 --- a/numpy/__init__.cython-30.pxd +++ b/numpy/__init__.cython-30.pxd @@ -156,6 +156,7 @@ cdef extern from "numpy/arrayobject.h": NPY_SAFE_CASTING NPY_SAME_KIND_CASTING NPY_UNSAFE_CASTING + NPY_SAME_VALUE_CASTING ctypedef enum NPY_CLIPMODE: NPY_CLIP diff --git a/numpy/__init__.pxd b/numpy/__init__.pxd index eb0764126116..40a24b6c7cc1 100644 --- a/numpy/__init__.pxd +++ b/numpy/__init__.pxd @@ -165,6 +165,7 @@ cdef extern from "numpy/arrayobject.h": NPY_SAFE_CASTING NPY_SAME_KIND_CASTING NPY_UNSAFE_CASTING + NPY_SAME_VALUE_CASTING ctypedef enum NPY_CLIPMODE: NPY_CLIP diff --git a/numpy/_core/include/numpy/ndarraytypes.h b/numpy/_core/include/numpy/ndarraytypes.h index baa42406ac88..972b25a7aa6e 100644 --- a/numpy/_core/include/numpy/ndarraytypes.h +++ b/numpy/_core/include/numpy/ndarraytypes.h @@ -227,6 +227,8 @@ typedef enum { NPY_SAME_KIND_CASTING=3, /* Allow any casts */ NPY_UNSAFE_CASTING=4, + /* Allow any casts, check that no values overflow/change */ + NPY_SAME_VALUE_CASTING=5, } NPY_CASTING; typedef enum { diff --git a/numpy/_core/src/multiarray/_multiarray_tests.c.src b/numpy/_core/src/multiarray/_multiarray_tests.c.src index 068fabc7fee8..1bd49df5404e 100644 --- a/numpy/_core/src/multiarray/_multiarray_tests.c.src +++ b/numpy/_core/src/multiarray/_multiarray_tests.c.src @@ -2168,6 +2168,7 @@ run_casting_converter(PyObject* NPY_UNUSED(self), PyObject *args) case NPY_SAFE_CASTING: return PyUnicode_FromString("NPY_SAFE_CASTING"); case NPY_SAME_KIND_CASTING: return PyUnicode_FromString("NPY_SAME_KIND_CASTING"); case NPY_UNSAFE_CASTING: return PyUnicode_FromString("NPY_UNSAFE_CASTING"); + case NPY_SAME_VALUE_CASTING: return PyUnicode_FromString("NPY_SAME_VALUE_CASTING"); default: return PyLong_FromLong(casting); } } diff --git a/numpy/_core/src/multiarray/array_method.c b/numpy/_core/src/multiarray/array_method.c index 5554cad5e2dd..fd4eb7a11dfc 100644 --- a/numpy/_core/src/multiarray/array_method.c +++ b/numpy/_core/src/multiarray/array_method.c @@ -190,6 +190,7 @@ validate_spec(PyArrayMethod_Spec *spec) case NPY_SAFE_CASTING: case NPY_SAME_KIND_CASTING: case NPY_UNSAFE_CASTING: + case NPY_SAME_VALUE_CASTING: break; default: if (spec->casting != -1) { diff --git a/numpy/_core/src/multiarray/conversion_utils.c b/numpy/_core/src/multiarray/conversion_utils.c index d487aa16727d..cf6c102a4c87 100644 --- a/numpy/_core/src/multiarray/conversion_utils.c +++ b/numpy/_core/src/multiarray/conversion_utils.c @@ -941,6 +941,10 @@ static int casting_parser(char const *str, Py_ssize_t length, void *data) *casting = NPY_SAME_KIND_CASTING; return 0; } + if (length == 10 && strcmp(str, "same_value") == 0) { + *casting = NPY_SAME_VALUE_CASTING; + return 0; + } break; case 's': if (length == 6 && strcmp(str, "unsafe") == 0) { diff --git a/numpy/_core/src/multiarray/convert_datatype.c b/numpy/_core/src/multiarray/convert_datatype.c index d34d852a706b..68c2cf1b809b 100644 --- a/numpy/_core/src/multiarray/convert_datatype.c +++ b/numpy/_core/src/multiarray/convert_datatype.c @@ -839,6 +839,8 @@ npy_casting_to_string(NPY_CASTING casting) return "'same_kind'"; case NPY_UNSAFE_CASTING: return "'unsafe'"; + case NPY_SAME_VALUE_CASTING: + return "'same_value'"; default: return ""; } From 3859a73c5f4583dcc237c309a63d9556e80b4cea Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Mon, 17 Feb 2025 11:22:48 +0200 Subject: [PATCH 02/29] work through more places that check 'cast', add a TODO --- TODO_same_value | 14 ++++++++++++++ numpy/_core/src/multiarray/conversion_utils.c | 2 +- numpy/_core/src/multiarray/convert_datatype.c | 8 ++++---- numpy/_core/src/multiarray/datetime.c | 8 ++++++-- numpy/_core/src/multiarray/datetime_strings.c | 2 +- .../src/multiarray/legacy_dtype_implementation.c | 8 ++++---- numpy/_core/src/multiarray/methods.c | 5 +++++ 7 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 TODO_same_value diff --git a/TODO_same_value b/TODO_same_value new file mode 100644 index 000000000000..b6ab04acb8b9 --- /dev/null +++ b/TODO_same_value @@ -0,0 +1,14 @@ +- Check where PyArray_CopyObject, PyArray_NewCopy, PyArray_CopyInto, array_datetime_as_string, PyArray_Concatenate, PyArray_where are used, do we need a 'same_value' equivalents? +- Is the comment in multiarray/common.c about NPY_DEFAULT_ASSIGN_CASTING warning still correct? +- In PyArray_FromArray(arr, newtype, flags) shoule there be a SAME_VALUE flag? +- Examine places where PyArray_CastingConverter is used and add SAME_VALUE handling + - array_astype: now errors, need to fix + - array_datetime_as_string: + - array_copyto: + - PyArray_AssignArray (called with a cast arg) + - PyArray_ConcatenateInto (called with a cast arg) + - PyArray_EinsteinSum (called with a cast arg) + - NpyIter_AdvancedNew (called with a cast arg) + +---- +latest commit: `git grep UNSAFE_CASTING` up to `numpy/_core/src/multiarray/multiarraymodule.c` diff --git a/numpy/_core/src/multiarray/conversion_utils.c b/numpy/_core/src/multiarray/conversion_utils.c index cf6c102a4c87..76b060a4fe18 100644 --- a/numpy/_core/src/multiarray/conversion_utils.c +++ b/numpy/_core/src/multiarray/conversion_utils.c @@ -965,7 +965,7 @@ PyArray_CastingConverter(PyObject *obj, NPY_CASTING *casting) return string_converter_helper( obj, (void *)casting, casting_parser, "casting", "must be one of 'no', 'equiv', 'safe', " - "'same_kind', or 'unsafe'"); + "'same_kind', "same_value", or 'unsafe'"); return 0; } diff --git a/numpy/_core/src/multiarray/convert_datatype.c b/numpy/_core/src/multiarray/convert_datatype.c index 68c2cf1b809b..63f19e8d87f0 100644 --- a/numpy/_core/src/multiarray/convert_datatype.c +++ b/numpy/_core/src/multiarray/convert_datatype.c @@ -746,13 +746,13 @@ can_cast_pyscalar_scalar_to( } else if (PyDataType_ISFLOAT(to)) { if (flags & NPY_ARRAY_WAS_PYTHON_COMPLEX) { - return casting == NPY_UNSAFE_CASTING; + return ((casting == NPY_UNSAFE_CASTING) || (casting == NPY_SAME_VALUE_CASTING)); } return 1; } else if (PyDataType_ISINTEGER(to)) { if (!(flags & NPY_ARRAY_WAS_PYTHON_INT)) { - return casting == NPY_UNSAFE_CASTING; + return ((casting == NPY_UNSAFE_CASTING) || (casting == NPY_SAME_VALUE_CASTING)); } return 1; } @@ -2465,10 +2465,10 @@ cast_to_string_resolve_descriptors( return -1; } - if (self->casting == NPY_UNSAFE_CASTING) { + if ((self->casting == NPY_UNSAFE_CASTING) || (self->casting == NPY_SAME_VALUE_CASTING)){ assert(dtypes[0]->type_num == NPY_UNICODE && dtypes[1]->type_num == NPY_STRING); - return NPY_UNSAFE_CASTING; + return self->casting; } if (loop_descrs[1]->elsize >= size) { diff --git a/numpy/_core/src/multiarray/datetime.c b/numpy/_core/src/multiarray/datetime.c index d820474532ca..66c2eca48451 100644 --- a/numpy/_core/src/multiarray/datetime.c +++ b/numpy/_core/src/multiarray/datetime.c @@ -1236,6 +1236,7 @@ can_cast_datetime64_units(NPY_DATETIMEUNIT src_unit, switch (casting) { /* Allow anything with unsafe casting */ case NPY_UNSAFE_CASTING: + case NPY_SAME_VALUE_CASTING: return 1; /* @@ -1281,6 +1282,7 @@ can_cast_timedelta64_units(NPY_DATETIMEUNIT src_unit, switch (casting) { /* Allow anything with unsafe casting */ case NPY_UNSAFE_CASTING: + case NPY_SAME_VALUE_CASTING: return 1; /* @@ -1327,6 +1329,7 @@ can_cast_datetime64_metadata(PyArray_DatetimeMetaData *src_meta, { switch (casting) { case NPY_UNSAFE_CASTING: + case NPY_SAME_VALUE_CASTING: return 1; case NPY_SAME_KIND_CASTING: @@ -1354,6 +1357,7 @@ can_cast_timedelta64_metadata(PyArray_DatetimeMetaData *src_meta, { switch (casting) { case NPY_UNSAFE_CASTING: + case NPY_SAME_VALUE_CASTING: return 1; case NPY_SAME_KIND_CASTING: @@ -2461,7 +2465,7 @@ convert_pyobject_to_datetime(PyArray_DatetimeMetaData *meta, PyObject *obj, * With unsafe casting, convert unrecognized objects into NaT * and with same_kind casting, convert None into NaT */ - if (casting == NPY_UNSAFE_CASTING || + if (casting == NPY_UNSAFE_CASTING || (casting == NPY_SAME_VALUE_CASTING) || (obj == Py_None && casting == NPY_SAME_KIND_CASTING)) { if (meta->base == NPY_FR_ERROR) { meta->base = NPY_FR_GENERIC; @@ -2729,7 +2733,7 @@ convert_pyobject_to_timedelta(PyArray_DatetimeMetaData *meta, PyObject *obj, * With unsafe casting, convert unrecognized objects into NaT * and with same_kind casting, convert None into NaT */ - if (casting == NPY_UNSAFE_CASTING || + if (casting == NPY_UNSAFE_CASTING || (casting == NPY_SAME_VALUE_CASTING) || (obj == Py_None && casting == NPY_SAME_KIND_CASTING)) { if (meta->base == NPY_FR_ERROR) { meta->base = NPY_FR_GENERIC; diff --git a/numpy/_core/src/multiarray/datetime_strings.c b/numpy/_core/src/multiarray/datetime_strings.c index f92eec3f5a59..d3011d082325 100644 --- a/numpy/_core/src/multiarray/datetime_strings.c +++ b/numpy/_core/src/multiarray/datetime_strings.c @@ -984,7 +984,7 @@ NpyDatetime_MakeISO8601Datetime( * the string representation, so ensure that the data * is being cast according to the casting rule. */ - if (casting != NPY_UNSAFE_CASTING) { + if ((casting != NPY_UNSAFE_CASTING) && (casting != NPY_SAME_VALUE_CASTING)) { /* Producing a date as a local time is always 'unsafe' */ if (base <= NPY_FR_D && local) { PyErr_SetString(PyExc_TypeError, "Cannot create a local " diff --git a/numpy/_core/src/multiarray/legacy_dtype_implementation.c b/numpy/_core/src/multiarray/legacy_dtype_implementation.c index abfc1bd0e3cd..7ae506fd7c02 100644 --- a/numpy/_core/src/multiarray/legacy_dtype_implementation.c +++ b/numpy/_core/src/multiarray/legacy_dtype_implementation.c @@ -367,7 +367,7 @@ PyArray_LegacyCanCastTypeTo(PyArray_Descr *from, PyArray_Descr *to, * field; recurse just in case the single field is itself structured. */ if (!PyDataType_HASFIELDS(to) && !PyDataType_ISOBJECT(to)) { - if (casting == NPY_UNSAFE_CASTING && + if ((casting == NPY_UNSAFE_CASTING || (casting == NPY_SAME_VALUE_CASTING)) && PyDict_Size(lfrom->fields) == 1) { Py_ssize_t ppos = 0; PyObject *tuple; @@ -399,7 +399,7 @@ PyArray_LegacyCanCastTypeTo(PyArray_Descr *from, PyArray_Descr *to, * casting; this is not correct, but needed since the treatment in can_cast * below got out of sync with astype; see gh-13667. */ - if (casting == NPY_UNSAFE_CASTING) { + if ((casting == NPY_UNSAFE_CASTING || (casting == NPY_SAME_VALUE_CASTING)) { return 1; } } @@ -408,14 +408,14 @@ PyArray_LegacyCanCastTypeTo(PyArray_Descr *from, PyArray_Descr *to, * If "from" is a simple data type and "to" has fields, then only * unsafe casting works (and that works always, even to multiple fields). */ - return casting == NPY_UNSAFE_CASTING; + return ((casting == NPY_UNSAFE_CASTING || (casting == NPY_SAME_VALUE_CASTING)); } /* * Everything else we consider castable for unsafe for now. * FIXME: ensure what we do here is consistent with "astype", * i.e., deal more correctly with subarrays and user-defined dtype. */ - else if (casting == NPY_UNSAFE_CASTING) { + else if ((casting == NPY_UNSAFE_CASTING || (casting == NPY_SAME_VALUE_CASTING)) { return 1; } /* diff --git a/numpy/_core/src/multiarray/methods.c b/numpy/_core/src/multiarray/methods.c index 50f7f5f3c73b..bc8b7b619950 100644 --- a/numpy/_core/src/multiarray/methods.c +++ b/numpy/_core/src/multiarray/methods.c @@ -841,6 +841,11 @@ array_astype(PyArrayObject *self, ((PyArrayObject_fields *)ret)->descr = dtype; } int success = PyArray_CopyInto(ret, self); + if (success >=0 && casting == NPY_SAME_VALUE_CASTING) { + /* XXX FIXME */ + PyErr_SetString(PyExc_RuntimeError, "implement same_value check in array_astype"); + success = -1; + } Py_DECREF(dtype); ((PyArrayObject_fields *)ret)->nd = out_ndim; From 9dc63a5023712383a474bda6058eca276e9a8498 Mon Sep 17 00:00:00 2001 From: mattip Date: Thu, 15 May 2025 14:06:59 +0300 Subject: [PATCH 03/29] add a test, percolate casting closer to inner loops --- .../_core/src/multiarray/array_assign_array.c | 63 ++++++++++--------- numpy/_core/src/multiarray/conversion_utils.c | 2 +- .../multiarray/legacy_dtype_implementation.c | 6 +- numpy/_core/src/multiarray/methods.c | 15 +++-- numpy/_core/tests/test_casting_unittests.py | 44 ++++++++++++- 5 files changed, 90 insertions(+), 40 deletions(-) diff --git a/numpy/_core/src/multiarray/array_assign_array.c b/numpy/_core/src/multiarray/array_assign_array.c index 8886d1cacb40..5d114640a2ca 100644 --- a/numpy/_core/src/multiarray/array_assign_array.c +++ b/numpy/_core/src/multiarray/array_assign_array.c @@ -79,7 +79,8 @@ copycast_isaligned(int ndim, npy_intp const *shape, NPY_NO_EXPORT int raw_array_assign_array(int ndim, npy_intp const *shape, PyArray_Descr *dst_dtype, char *dst_data, npy_intp const *dst_strides, - PyArray_Descr *src_dtype, char *src_data, npy_intp const *src_strides) + PyArray_Descr *src_dtype, char *src_data, npy_intp const *src_strides, + int flags) { int idim; npy_intp shape_it[NPY_MAXDIMS]; @@ -87,14 +88,11 @@ raw_array_assign_array(int ndim, npy_intp const *shape, npy_intp src_strides_it[NPY_MAXDIMS]; npy_intp coord[NPY_MAXDIMS]; - int aligned; + int aligned = flags & 0x01; + int same_value_cast = (flags & 0x02) == 0x02; NPY_BEGIN_THREADS_DEF; - aligned = - copycast_isaligned(ndim, shape, dst_dtype, dst_data, dst_strides) && - copycast_isaligned(ndim, shape, src_dtype, src_data, src_strides); - /* Use raw iteration with no heap allocation */ if (PyArray_PrepareTwoRawArrayIter( ndim, shape, @@ -120,21 +118,21 @@ raw_array_assign_array(int ndim, npy_intp const *shape, /* Get the function to do the casting */ NPY_cast_info cast_info; - NPY_ARRAYMETHOD_FLAGS flags; + NPY_ARRAYMETHOD_FLAGS method_flags; if (PyArray_GetDTypeTransferFunction(aligned, src_strides_it[0], dst_strides_it[0], src_dtype, dst_dtype, 0, - &cast_info, &flags) != NPY_SUCCEED) { + &cast_info, &method_flags) != NPY_SUCCEED) { return -1; } - if (!(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { + if (!(method_flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { npy_clear_floatstatus_barrier((char*)&src_data); } /* Ensure number of elements exceeds threshold for threading */ - if (!(flags & NPY_METH_REQUIRES_PYAPI)) { + if (!(method_flags & NPY_METH_REQUIRES_PYAPI)) { npy_intp nitems = 1, i; for (i = 0; i < ndim; i++) { nitems *= shape_it[i]; @@ -147,8 +145,10 @@ raw_array_assign_array(int ndim, npy_intp const *shape, NPY_RAW_ITER_START(idim, ndim, coord, shape_it) { /* Process the innermost dimension */ char *args[2] = {src_data, dst_data}; - if (cast_info.func(&cast_info.context, - args, &shape_it[0], strides, cast_info.auxdata) < 0) { + int result = cast_info.func(&cast_info.context, + args, &shape_it[0], strides, + cast_info.auxdata); + if (result < 0) { goto fail; } } NPY_RAW_ITER_TWO_NEXT(idim, ndim, coord, shape_it, @@ -158,7 +158,7 @@ raw_array_assign_array(int ndim, npy_intp const *shape, NPY_END_THREADS; NPY_cast_info_xfree(&cast_info); - if (!(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { + if (!(method_flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { int fpes = npy_get_floatstatus_barrier((char*)&src_data); if (fpes && PyUFunc_GiveFloatingpointErrors("cast", fpes) < 0) { return -1; @@ -183,7 +183,7 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, PyArray_Descr *dst_dtype, char *dst_data, npy_intp const *dst_strides, PyArray_Descr *src_dtype, char *src_data, npy_intp const *src_strides, PyArray_Descr *wheremask_dtype, char *wheremask_data, - npy_intp const *wheremask_strides) + npy_intp const *wheremask_strides, int flags) { int idim; npy_intp shape_it[NPY_MAXDIMS]; @@ -192,14 +192,11 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, npy_intp wheremask_strides_it[NPY_MAXDIMS]; npy_intp coord[NPY_MAXDIMS]; - int aligned; + int aligned = flags & 0x01; + int same_value_cast = (flags & 0x02) == 0x02; NPY_BEGIN_THREADS_DEF; - aligned = - copycast_isaligned(ndim, shape, dst_dtype, dst_data, dst_strides) && - copycast_isaligned(ndim, shape, src_dtype, src_data, src_strides); - /* Use raw iteration with no heap allocation */ if (PyArray_PrepareThreeRawArrayIter( ndim, shape, @@ -229,21 +226,21 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, /* Get the function to do the casting */ NPY_cast_info cast_info; - NPY_ARRAYMETHOD_FLAGS flags; + NPY_ARRAYMETHOD_FLAGS method_flags; if (PyArray_GetMaskedDTypeTransferFunction(aligned, src_strides_it[0], dst_strides_it[0], wheremask_strides_it[0], src_dtype, dst_dtype, wheremask_dtype, 0, - &cast_info, &flags) != NPY_SUCCEED) { + &cast_info, &method_flags) != NPY_SUCCEED) { return -1; } - if (!(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { + if (!(method_flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { npy_clear_floatstatus_barrier(src_data); } - if (!(flags & NPY_METH_REQUIRES_PYAPI)) { + if (!(method_flags & NPY_METH_REQUIRES_PYAPI)) { npy_intp nitems = 1, i; for (i = 0; i < ndim; i++) { nitems *= shape_it[i]; @@ -258,10 +255,11 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, /* Process the innermost dimension */ char *args[2] = {src_data, dst_data}; - if (stransfer(&cast_info.context, - args, &shape_it[0], strides, - (npy_bool *)wheremask_data, wheremask_strides_it[0], - cast_info.auxdata) < 0) { + int result = stransfer(&cast_info.context, + args, &shape_it[0], strides, + (npy_bool *)wheremask_data, wheremask_strides_it[0], + cast_info.auxdata); + if (result < 0) { goto fail; } } NPY_RAW_ITER_THREE_NEXT(idim, ndim, coord, shape_it, @@ -272,7 +270,7 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, NPY_END_THREADS; NPY_cast_info_xfree(&cast_info); - if (!(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { + if (!(method_flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { int fpes = npy_get_floatstatus_barrier(src_data); if (fpes && PyUFunc_GiveFloatingpointErrors("cast", fpes) < 0) { return -1; @@ -438,12 +436,17 @@ PyArray_AssignArray(PyArrayObject *dst, PyArrayObject *src, } } + int aligned = + copycast_isaligned(PyArray_NDIM(dst), PyArray_DIMS(dst), PyArray_DESCR(dst), PyArray_DATA(dst), PyArray_STRIDES(dst)) && + copycast_isaligned(PyArray_NDIM(dst), PyArray_DIMS(dst), PyArray_DESCR(src), PyArray_DATA(src), src_strides); + int flags = ((NPY_SAME_VALUE_CASTING == casting) << 1) | aligned; + if (wheremask == NULL) { /* A straightforward value assignment */ /* Do the assignment with raw array iteration */ if (raw_array_assign_array(PyArray_NDIM(dst), PyArray_DIMS(dst), PyArray_DESCR(dst), PyArray_DATA(dst), PyArray_STRIDES(dst), - PyArray_DESCR(src), PyArray_DATA(src), src_strides) < 0) { + PyArray_DESCR(src), PyArray_DATA(src), src_strides, flags) < 0){ goto fail; } } @@ -465,7 +468,7 @@ PyArray_AssignArray(PyArrayObject *dst, PyArrayObject *src, PyArray_DESCR(dst), PyArray_DATA(dst), PyArray_STRIDES(dst), PyArray_DESCR(src), PyArray_DATA(src), src_strides, PyArray_DESCR(wheremask), PyArray_DATA(wheremask), - wheremask_strides) < 0) { + wheremask_strides, flags) < 0) { goto fail; } } diff --git a/numpy/_core/src/multiarray/conversion_utils.c b/numpy/_core/src/multiarray/conversion_utils.c index 76b060a4fe18..e973ecb49faa 100644 --- a/numpy/_core/src/multiarray/conversion_utils.c +++ b/numpy/_core/src/multiarray/conversion_utils.c @@ -965,7 +965,7 @@ PyArray_CastingConverter(PyObject *obj, NPY_CASTING *casting) return string_converter_helper( obj, (void *)casting, casting_parser, "casting", "must be one of 'no', 'equiv', 'safe', " - "'same_kind', "same_value", or 'unsafe'"); + "'same_kind', 'unsafe', or 'same_value'"); return 0; } diff --git a/numpy/_core/src/multiarray/legacy_dtype_implementation.c b/numpy/_core/src/multiarray/legacy_dtype_implementation.c index 7ae506fd7c02..401223dd4e12 100644 --- a/numpy/_core/src/multiarray/legacy_dtype_implementation.c +++ b/numpy/_core/src/multiarray/legacy_dtype_implementation.c @@ -399,7 +399,7 @@ PyArray_LegacyCanCastTypeTo(PyArray_Descr *from, PyArray_Descr *to, * casting; this is not correct, but needed since the treatment in can_cast * below got out of sync with astype; see gh-13667. */ - if ((casting == NPY_UNSAFE_CASTING || (casting == NPY_SAME_VALUE_CASTING)) { + if (casting == NPY_UNSAFE_CASTING || casting == NPY_SAME_VALUE_CASTING) { return 1; } } @@ -408,14 +408,14 @@ PyArray_LegacyCanCastTypeTo(PyArray_Descr *from, PyArray_Descr *to, * If "from" is a simple data type and "to" has fields, then only * unsafe casting works (and that works always, even to multiple fields). */ - return ((casting == NPY_UNSAFE_CASTING || (casting == NPY_SAME_VALUE_CASTING)); + return (casting == NPY_UNSAFE_CASTING || casting == NPY_SAME_VALUE_CASTING); } /* * Everything else we consider castable for unsafe for now. * FIXME: ensure what we do here is consistent with "astype", * i.e., deal more correctly with subarrays and user-defined dtype. */ - else if ((casting == NPY_UNSAFE_CASTING || (casting == NPY_SAME_VALUE_CASTING)) { + else if (casting == NPY_UNSAFE_CASTING || casting == NPY_SAME_VALUE_CASTING) { return 1; } /* diff --git a/numpy/_core/src/multiarray/methods.c b/numpy/_core/src/multiarray/methods.c index bc8b7b619950..3574fa3a7d05 100644 --- a/numpy/_core/src/multiarray/methods.c +++ b/numpy/_core/src/multiarray/methods.c @@ -840,11 +840,16 @@ array_astype(PyArrayObject *self, ((PyArrayObject_fields *)ret)->nd = PyArray_NDIM(self); ((PyArrayObject_fields *)ret)->descr = dtype; } - int success = PyArray_CopyInto(ret, self); - if (success >=0 && casting == NPY_SAME_VALUE_CASTING) { - /* XXX FIXME */ - PyErr_SetString(PyExc_RuntimeError, "implement same_value check in array_astype"); - success = -1; + int success; + if (casting == NPY_SAME_VALUE_CASTING) { + success = PyArray_AssignArray(ret, self, NULL, casting); + if (success < 0) { + printf("error in PyArray_AssignArray\n"); + //PyErr_SetString(PyExc_RuntimeError, "error when casting"); + //npy_set_invalid_cast_error(PyArray_DESCR(self), dtype, casting, 0); + } + } else { + success = PyArray_AssignArray(ret, self, NULL, NPY_UNSAFE_CASTING); } Py_DECREF(dtype); diff --git a/numpy/_core/tests/test_casting_unittests.py b/numpy/_core/tests/test_casting_unittests.py index 91ecc0dc75b0..c44ac7a607fb 100644 --- a/numpy/_core/tests/test_casting_unittests.py +++ b/numpy/_core/tests/test_casting_unittests.py @@ -10,13 +10,14 @@ import enum import random import textwrap +import warnings import pytest import numpy as np from numpy._core._multiarray_umath import _get_castingimpl as get_castingimpl from numpy.lib.stride_tricks import as_strided -from numpy.testing import assert_array_equal +from numpy.testing import assert_array_equal, assert_equal # Simple skips object, parametric and long double (unsupported by struct) simple_dtypes = "?bhilqBHILQefdFD" @@ -76,6 +77,7 @@ class Casting(enum.IntEnum): safe = 2 same_kind = 3 unsafe = 4 + same_value = 5 def _get_cancast_table(): @@ -815,3 +817,43 @@ def test_nonstandard_bool_to_other(self, dtype): res = nonstandard_bools.astype(dtype) expected = [0, 1, 1] assert_array_equal(res, expected) + + @pytest.mark.parametrize("to_dtype", + # cast to complex (AllFloat) + np.typecodes["AllInteger"] + np.typecodes["AllFloat"]) + @pytest.mark.parametrize("from_dtype", + np.typecodes["AllInteger"] + np.typecodes["Float"]) + def test_same_value(self, from_dtype, to_dtype): + if from_dtype == to_dtype: + return + top1 = 0 + top2 = 0 + try: + top1 = np.iinfo(from_dtype).max + except ValueError: + top1 = np.finfo(from_dtype).max + try: + top2 = np.iinfo(to_dtype).max + except ValueError: + top2 = np.finfo(to_dtype).max + # No need to test if top2 > top1, since the test will also do the reverse dtype matching. + # Catch then warning if the comparison warns, i.e. np.int16(65535) < np.float16(6.55e4) + with warnings.catch_warnings(record=True): + warnings.simplefilter("always", RuntimeWarning) + if top2 >= top1: + # will be tested when the dtypes are reversed + return + # Happy path + arr1 = np.array([0] * 10, dtype=from_dtype) + arr2 = np.array([0] * 10, dtype=to_dtype) + assert_equal(arr1.astype(to_dtype, casting='same_value'), arr2, strict=True) + arr1[0] = top1 + if 1: + # with pytest.raises(ValueError): + # Casting float to float with overflow should raise RuntimeWarning (fperror) + # Casting float to int with overflow sometimes raises RuntimeWarning (fperror) + # Casting with overflow and 'same_value', should raise ValueError + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always", RuntimeWarning) + arr1.astype(to_dtype, casting='same_value') + assert len(w) < 2 From 7cad6af78867261aad42928492a446026698f888 Mon Sep 17 00:00:00 2001 From: mattip Date: Thu, 29 May 2025 06:22:33 +0300 Subject: [PATCH 04/29] use SAME_VALUE_CAST flag for one inner loop variant --- TODO_same_value | 10 ++++------ numpy/_core/include/numpy/dtype_api.h | 11 +++++++++++ numpy/_core/src/multiarray/array_assign_array.c | 9 +++++++++ numpy/_core/src/multiarray/array_method.c | 1 + numpy/_core/src/multiarray/dtype_transfer.c | 2 -- numpy/_core/src/multiarray/dtype_transfer.h | 3 +++ .../src/multiarray/lowlevel_strided_loops.c.src | 8 +++++++- numpy/_core/src/umath/legacy_array_method.c | 7 ++++--- numpy/_core/src/umath/ufunc_object.c | 12 +++++++++--- numpy/_core/tests/test_casting_unittests.py | 3 +-- 10 files changed, 49 insertions(+), 17 deletions(-) diff --git a/TODO_same_value b/TODO_same_value index b6ab04acb8b9..21afcfc310a4 100644 --- a/TODO_same_value +++ b/TODO_same_value @@ -1,14 +1,12 @@ - Check where PyArray_CopyObject, PyArray_NewCopy, PyArray_CopyInto, array_datetime_as_string, PyArray_Concatenate, PyArray_where are used, do we need a 'same_value' equivalents? -- Is the comment in multiarray/common.c about NPY_DEFAULT_ASSIGN_CASTING warning still correct? -- In PyArray_FromArray(arr, newtype, flags) shoule there be a SAME_VALUE flag? +- In PyArray_FromArray(arr, newtype, flags) should there be a SAME_VALUE flag? - Examine places where PyArray_CastingConverter is used and add SAME_VALUE handling - - array_astype: now errors, need to fix - array_datetime_as_string: - array_copyto: - - PyArray_AssignArray (called with a cast arg) + - PyArray_AssignArray with wheremask (called with a cast arg) + - PyArray_AssignRawScalar with/without wheremask - PyArray_ConcatenateInto (called with a cast arg) - PyArray_EinsteinSum (called with a cast arg) - NpyIter_AdvancedNew (called with a cast arg) - +In CanCast, make sure user defined and datetime dtypes will fail with SAME_VALUE ---- -latest commit: `git grep UNSAFE_CASTING` up to `numpy/_core/src/multiarray/multiarraymodule.c` diff --git a/numpy/_core/include/numpy/dtype_api.h b/numpy/_core/include/numpy/dtype_api.h index b37c9fbb6821..5ff9c176b7a3 100644 --- a/numpy/_core/include/numpy/dtype_api.h +++ b/numpy/_core/include/numpy/dtype_api.h @@ -107,6 +107,12 @@ typedef struct PyArrayMethod_Context_tag { /* Operand descriptors, filled in by resolve_descriptors */ PyArray_Descr *const *descriptors; + void * padding; + /* + * Optional flag to pass information into the inner loop + * If set, it will be NPY_CASTING + */ + uint64_t flags; /* Structure may grow (this is harmless for DType authors) */ } PyArrayMethod_Context; @@ -144,6 +150,11 @@ typedef struct { #define NPY_METH_contiguous_indexed_loop 9 #define _NPY_METH_static_data 10 +/* + * Constants for same_value casting + */ +#define NPY_SAME_VALUE_OVERFLOW -31 + /* * The resolve descriptors function, must be able to handle NULL values for diff --git a/numpy/_core/src/multiarray/array_assign_array.c b/numpy/_core/src/multiarray/array_assign_array.c index 5d114640a2ca..624bae2f9450 100644 --- a/numpy/_core/src/multiarray/array_assign_array.c +++ b/numpy/_core/src/multiarray/array_assign_array.c @@ -131,6 +131,10 @@ raw_array_assign_array(int ndim, npy_intp const *shape, npy_clear_floatstatus_barrier((char*)&src_data); } + if (same_value_cast) { + cast_info.context.flags |= NPY_SAME_VALUE_CASTING; + } + /* Ensure number of elements exceeds threshold for threading */ if (!(method_flags & NPY_METH_REQUIRES_PYAPI)) { npy_intp nitems = 1, i; @@ -149,6 +153,9 @@ raw_array_assign_array(int ndim, npy_intp const *shape, args, &shape_it[0], strides, cast_info.auxdata); if (result < 0) { + if (result == NPY_SAME_VALUE_OVERFLOW) { + goto same_value_overflow; + } goto fail; } } NPY_RAW_ITER_TWO_NEXT(idim, ndim, coord, shape_it, @@ -166,6 +173,8 @@ raw_array_assign_array(int ndim, npy_intp const *shape, } return 0; +same_value_overflow: + PyErr_SetString(PyExc_ValueError, "overflow in 'same_value' cast"); fail: NPY_END_THREADS; NPY_cast_info_xfree(&cast_info); diff --git a/numpy/_core/src/multiarray/array_method.c b/numpy/_core/src/multiarray/array_method.c index fd4eb7a11dfc..e0ce47dabdba 100644 --- a/numpy/_core/src/multiarray/array_method.c +++ b/numpy/_core/src/multiarray/array_method.c @@ -797,6 +797,7 @@ boundarraymethod__simple_strided_call( .caller = NULL, .method = self->method, .descriptors = descrs, + .flags = 0, }; PyArrayMethod_StridedLoop *strided_loop = NULL; NpyAuxData *loop_data = NULL; diff --git a/numpy/_core/src/multiarray/dtype_transfer.c b/numpy/_core/src/multiarray/dtype_transfer.c index 64d5bfa89e8e..66b0d686dbad 100644 --- a/numpy/_core/src/multiarray/dtype_transfer.c +++ b/numpy/_core/src/multiarray/dtype_transfer.c @@ -2910,8 +2910,6 @@ _clear_cast_info_after_get_loop_failure(NPY_cast_info *cast_info) * TODO: Expand the view functionality for general offsets, not just 0: * Partial casts could be skipped also for `view_offset != 0`. * - * The `out_needs_api` flag must be initialized. - * * NOTE: In theory casting errors here could be slightly misleading in case * of a multi-step casting scenario. It should be possible to improve * this in the future. diff --git a/numpy/_core/src/multiarray/dtype_transfer.h b/numpy/_core/src/multiarray/dtype_transfer.h index 04df5cb64c22..cd55db3a25f7 100644 --- a/numpy/_core/src/multiarray/dtype_transfer.h +++ b/numpy/_core/src/multiarray/dtype_transfer.h @@ -44,6 +44,9 @@ NPY_cast_info_init(NPY_cast_info *cast_info) // TODO: Delete this again probably maybe create a new minimal init macro cast_info->context.caller = NULL; + + cast_info->context.padding = NULL; + cast_info->context.flags = 0; } diff --git a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src index 0c4eb3dd9a8d..77265dd806a4 100644 --- a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src +++ b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src @@ -899,6 +899,7 @@ static GCC_CAST_OPT_LEVEL int #endif /*printf("@prefix@_cast_@name1@_to_@name2@\n");*/ + int same_value_casting = ((context->flags & NPY_SAME_VALUE_CASTING) == NPY_SAME_VALUE_CASTING); while (N--) { #if @aligned@ @@ -939,7 +940,12 @@ static GCC_CAST_OPT_LEVEL int # elif !@aligned@ dst_value = _CONVERT_FN(src_value); # else - *(_TYPE2 *)dst = _CONVERT_FN(*(_TYPE1 *)src); + *(_TYPE2 *)dst = _CONVERT_FN(*(_TYPE1 *)src); + if (same_value_casting) { + if (*(_TYPE2 *)dst != *(_TYPE1 *)src) { + return NPY_SAME_VALUE_OVERFLOW; + } + } # endif #endif diff --git a/numpy/_core/src/umath/legacy_array_method.c b/numpy/_core/src/umath/legacy_array_method.c index 705262fedd38..99db0134c300 100644 --- a/numpy/_core/src/umath/legacy_array_method.c +++ b/numpy/_core/src/umath/legacy_array_method.c @@ -440,9 +440,10 @@ PyArray_NewLegacyWrappingArrayMethod(PyUFuncObject *ufunc, } PyArrayMethod_Context context = { - (PyObject *)ufunc, - bound_res->method, - descrs, + .caller = (PyObject *)ufunc, + .method = bound_res->method, + .descriptors = descrs, + .flags = 0, }; int ret = get_initial_from_ufunc(&context, 0, context.method->legacy_initial); diff --git a/numpy/_core/src/umath/ufunc_object.c b/numpy/_core/src/umath/ufunc_object.c index 1d2c3edbd3b9..82a84636c30b 100644 --- a/numpy/_core/src/umath/ufunc_object.c +++ b/numpy/_core/src/umath/ufunc_object.c @@ -2088,6 +2088,7 @@ PyUFunc_GeneralizedFunctionInternal(PyUFuncObject *ufunc, .caller = (PyObject *)ufunc, .method = ufuncimpl, .descriptors = operation_descrs, + .flags = 0, }; PyArrayMethod_StridedLoop *strided_loop; NPY_ARRAYMETHOD_FLAGS flags = 0; @@ -2207,6 +2208,7 @@ PyUFunc_GenericFunctionInternal(PyUFuncObject *ufunc, .caller = (PyObject *)ufunc, .method = ufuncimpl, .descriptors = operation_descrs, + .flags = 0, }; /* Do the ufunc loop */ @@ -2557,6 +2559,7 @@ PyUFunc_Reduce(PyUFuncObject *ufunc, .caller = (PyObject *)ufunc, .method = ufuncimpl, .descriptors = descrs, + .flags = 0, }; PyArrayObject *result = PyUFunc_ReduceWrapper(&context, @@ -2633,6 +2636,7 @@ PyUFunc_Accumulate(PyUFuncObject *ufunc, PyArrayObject *arr, PyArrayObject *out, .caller = (PyObject *)ufunc, .method = ufuncimpl, .descriptors = descrs, + .flags = 0, }; ndim = PyArray_NDIM(arr); @@ -3065,6 +3069,7 @@ PyUFunc_Reduceat(PyUFuncObject *ufunc, PyArrayObject *arr, PyArrayObject *ind, .caller = (PyObject *)ufunc, .method = ufuncimpl, .descriptors = descrs, + .flags = 0, }; ndim = PyArray_NDIM(arr); @@ -5903,9 +5908,10 @@ ufunc_at(PyUFuncObject *ufunc, PyObject *args) } PyArrayMethod_Context context = { - .caller = (PyObject *)ufunc, - .method = ufuncimpl, - .descriptors = operation_descrs, + .caller = (PyObject *)ufunc, + .method = ufuncimpl, + .descriptors = operation_descrs, + .flags = 0, }; /* Use contiguous strides; if there is such a loop it may be faster */ diff --git a/numpy/_core/tests/test_casting_unittests.py b/numpy/_core/tests/test_casting_unittests.py index c44ac7a607fb..d9b183a6a62c 100644 --- a/numpy/_core/tests/test_casting_unittests.py +++ b/numpy/_core/tests/test_casting_unittests.py @@ -848,8 +848,7 @@ def test_same_value(self, from_dtype, to_dtype): arr2 = np.array([0] * 10, dtype=to_dtype) assert_equal(arr1.astype(to_dtype, casting='same_value'), arr2, strict=True) arr1[0] = top1 - if 1: - # with pytest.raises(ValueError): + with pytest.raises(ValueError): # Casting float to float with overflow should raise RuntimeWarning (fperror) # Casting float to int with overflow sometimes raises RuntimeWarning (fperror) # Casting with overflow and 'same_value', should raise ValueError From 03e9cebda7785c17a3f3600f21f313e38ecda6f9 Mon Sep 17 00:00:00 2001 From: mattip Date: Sun, 1 Jun 2025 12:19:06 +0300 Subject: [PATCH 05/29] aligned test of same_value passes. Need more tests --- .../multiarray/lowlevel_strided_loops.c.src | 72 +++++++++++++++++-- numpy/_core/src/multiarray/methods.c | 5 -- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src index 77265dd806a4..d9e05cecf063 100644 --- a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src +++ b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src @@ -17,6 +17,7 @@ #include #include #include +#include #include "lowlevel_strided_loops.h" #include "array_assign.h" @@ -742,6 +743,7 @@ NPY_NO_EXPORT PyArrayMethod_StridedLoop * * #is_float1 = 0*12, 1, 0, 0, 1, 0, 0# * #is_double1 = 0*13, 1, 0, 0, 1, 0# * #is_complex1 = 0*15, 1*3# + * #is_unsigned1 = 1*6, 0*12# */ /**begin repeat1 @@ -766,6 +768,16 @@ NPY_NO_EXPORT PyArrayMethod_StridedLoop * * npy_byte, npy_short, npy_int, npy_long, npy_longlong, * _npy_half, npy_float, npy_double, npy_longdouble, * npy_float, npy_double, npy_longdouble# + * #type2max = 0, + * UCHAR_MAX, USHRT_MAX, UINT_MAX, ULONG_MAX, ULLONG_MAX, + * CHAR_MAX, SHRT_MAX, INT_MAX, LONG_MAX, LLONG_MAX, + * 65500, FLT_MAX, DBL_MAX, LDBL_MAX, + * FLT_MAX, DBL_MAX, LDBL_MAX# + * #type2min = 0, + * 0, 0, 0, 0, 0, + * CHAR_MIN, SHRT_MIN, INT_MIN, LONG_MIN, LLONG_MIN, + * -65500, -FLT_MAX, -DBL_MAX, -LDBL_MAX, + * -FLT_MAX, -DBL_MAX, -LDBL_MAX# * #is_bool2 = 1, 0*17# * #is_emu_half2 = 0*11, EMULATED_FP16, 0*6# * #is_native_half2 = 0*11, NATIVE_FP16, 0*6# @@ -810,7 +822,7 @@ NPY_NO_EXPORT PyArrayMethod_StridedLoop * /* Determine an appropriate casting conversion function */ #if @is_emu_half1@ - +# define _TO_RTYPE1(x) npy_half_to_float(x) # if @is_float2@ # define _CONVERT_FN(x) npy_halfbits_to_floatbits(x) # elif @is_double2@ @@ -824,6 +836,7 @@ NPY_NO_EXPORT PyArrayMethod_StridedLoop * # endif #elif @is_emu_half2@ +# define _TO_RTYPE1(x) (@rtype1@)(x) # if @is_float1@ # define _CONVERT_FN(x) npy_floatbits_to_halfbits(x) @@ -838,6 +851,7 @@ NPY_NO_EXPORT PyArrayMethod_StridedLoop * # endif #else +# define _TO_RTYPE1(x) (@rtype1@)(x) # if @is_bool2@ || @is_bool1@ # define _CONVERT_FN(x) ((npy_bool)(x != 0)) @@ -899,7 +913,9 @@ static GCC_CAST_OPT_LEVEL int #endif /*printf("@prefix@_cast_@name1@_to_@name2@\n");*/ +#if !@is_bool2@ int same_value_casting = ((context->flags & NPY_SAME_VALUE_CASTING) == NPY_SAME_VALUE_CASTING); +#endif while (N--) { #if @aligned@ @@ -916,20 +932,35 @@ static GCC_CAST_OPT_LEVEL int # if @is_complex2@ dst_value[0] = _CONVERT_FN(src_value[0]); dst_value[1] = _CONVERT_FN(src_value[1]); + if (same_value_casting) { + if ((dst_value[0] != src_value[0]) || (dst_value[1] != src_value[1])){ + return NPY_SAME_VALUE_OVERFLOW; + } + } # elif !@aligned@ # if @is_bool2@ dst_value = _CONVERT_FN(src_value[0]) || _CONVERT_FN(src_value[1]); # else dst_value = _CONVERT_FN(src_value[0]); + if (same_value_casting) { + if (dst_value != src_value[0]){ + return NPY_SAME_VALUE_OVERFLOW; + } + } # endif # else # if @is_bool2@ *(_TYPE2 *)dst = _CONVERT_FN(src_value[0]) || _CONVERT_FN(src_value[1]); # else *(_TYPE2 *)dst = _CONVERT_FN(src_value[0]); + if (same_value_casting) { + if (*(_TYPE2 *)dst != src_value[0]){ + return NPY_SAME_VALUE_OVERFLOW; + } + } # endif # endif -#else +#else // @is_complex1@ # if @is_complex2@ # if !@aligned@ dst_value[0] = _CONVERT_FN(src_value); @@ -937,15 +968,41 @@ static GCC_CAST_OPT_LEVEL int dst_value[0] = _CONVERT_FN(*(_TYPE1 *)src); # endif dst_value[1] = 0; + if (same_value_casting) { +# if !@aligned@ + if ((@rtype2@)dst_value[0] != src_value){ +# else + if ((@rtype2@)dst_value[0] != *((_TYPE1 *)src)){ +# endif + return NPY_SAME_VALUE_OVERFLOW; + } + } # elif !@aligned@ dst_value = _CONVERT_FN(src_value); +# if !@is_bool2@ + if (same_value_casting) { + if (src_value > @type2max@ || src_value < @type2min@) { + return NPY_SAME_VALUE_OVERFLOW; + } + if (dst_value != src_value){ + return NPY_SAME_VALUE_OVERFLOW; + } + } +# endif // @is_bool2@ # else - *(_TYPE2 *)dst = _CONVERT_FN(*(_TYPE1 *)src); - if (same_value_casting) { - if (*(_TYPE2 *)dst != *(_TYPE1 *)src) { - return NPY_SAME_VALUE_OVERFLOW; - } + *(_TYPE2 *)dst = _CONVERT_FN(*(_TYPE1 *)src); +# if !@is_bool2@ + if (same_value_casting) { + if (_TO_RTYPE1(*((_TYPE1 *)src)) > @type2max@) { + return NPY_SAME_VALUE_OVERFLOW; + } +# if !@is_unsigned1@ + if (_TO_RTYPE1(*((_TYPE1 *)src)) < @type2min@) { + return NPY_SAME_VALUE_OVERFLOW; + } +# endif } +# endif // @is_bool2@ # endif #endif @@ -983,6 +1040,7 @@ static GCC_CAST_OPT_LEVEL int #undef _CONVERT_FN #undef _TYPE2 #undef _TYPE1 +#undef _TO_RTYPE1 #endif diff --git a/numpy/_core/src/multiarray/methods.c b/numpy/_core/src/multiarray/methods.c index 3574fa3a7d05..c5fda08041c8 100644 --- a/numpy/_core/src/multiarray/methods.c +++ b/numpy/_core/src/multiarray/methods.c @@ -843,11 +843,6 @@ array_astype(PyArrayObject *self, int success; if (casting == NPY_SAME_VALUE_CASTING) { success = PyArray_AssignArray(ret, self, NULL, casting); - if (success < 0) { - printf("error in PyArray_AssignArray\n"); - //PyErr_SetString(PyExc_RuntimeError, "error when casting"); - //npy_set_invalid_cast_error(PyArray_DESCR(self), dtype, casting, 0); - } } else { success = PyArray_AssignArray(ret, self, NULL, NPY_UNSAFE_CASTING); } From 93b8bce8505e792ccd9700b9403941ea27b5ed1c Mon Sep 17 00:00:00 2001 From: mattip Date: Sun, 1 Jun 2025 12:54:08 +0300 Subject: [PATCH 06/29] handle unaligned casting with 'same_value' --- numpy/_core/src/multiarray/lowlevel_strided_loops.c.src | 6 ++++-- numpy/_core/tests/test_casting_unittests.py | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src index d9e05cecf063..804d35f84261 100644 --- a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src +++ b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src @@ -981,12 +981,14 @@ static GCC_CAST_OPT_LEVEL int dst_value = _CONVERT_FN(src_value); # if !@is_bool2@ if (same_value_casting) { - if (src_value > @type2max@ || src_value < @type2min@) { + if (_TO_RTYPE1(src_value) > @type2max@) { return NPY_SAME_VALUE_OVERFLOW; } - if (dst_value != src_value){ +# if !@is_unsigned1@ + if (_TO_RTYPE1(src_value) < @type2min@) { return NPY_SAME_VALUE_OVERFLOW; } +# endif } # endif // @is_bool2@ # else diff --git a/numpy/_core/tests/test_casting_unittests.py b/numpy/_core/tests/test_casting_unittests.py index d9b183a6a62c..2d86022f13a0 100644 --- a/numpy/_core/tests/test_casting_unittests.py +++ b/numpy/_core/tests/test_casting_unittests.py @@ -848,6 +848,9 @@ def test_same_value(self, from_dtype, to_dtype): arr2 = np.array([0] * 10, dtype=to_dtype) assert_equal(arr1.astype(to_dtype, casting='same_value'), arr2, strict=True) arr1[0] = top1 + aligned = np.empty(arr1.itemsize * arr1.size + 1, 'uint8') + unaligned = aligned[1:].view(arr1.dtype) + unaligned[:] = arr1 with pytest.raises(ValueError): # Casting float to float with overflow should raise RuntimeWarning (fperror) # Casting float to int with overflow sometimes raises RuntimeWarning (fperror) @@ -856,3 +859,9 @@ def test_same_value(self, from_dtype, to_dtype): warnings.simplefilter("always", RuntimeWarning) arr1.astype(to_dtype, casting='same_value') assert len(w) < 2 + with pytest.raises(ValueError): + # again, unaligned + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always", RuntimeWarning) + unaligned.astype(to_dtype, casting='same_value') + assert len(w) < 2 From 87e2487743d3122f700f9bea61efa9de69e6c921 Mon Sep 17 00:00:00 2001 From: mattip Date: Sun, 1 Jun 2025 13:02:14 +0300 Subject: [PATCH 07/29] extend tests to use source-is-complex --- numpy/_core/tests/test_casting_unittests.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/numpy/_core/tests/test_casting_unittests.py b/numpy/_core/tests/test_casting_unittests.py index 2d86022f13a0..c1050b3fa015 100644 --- a/numpy/_core/tests/test_casting_unittests.py +++ b/numpy/_core/tests/test_casting_unittests.py @@ -819,10 +819,9 @@ def test_nonstandard_bool_to_other(self, dtype): assert_array_equal(res, expected) @pytest.mark.parametrize("to_dtype", - # cast to complex (AllFloat) np.typecodes["AllInteger"] + np.typecodes["AllFloat"]) @pytest.mark.parametrize("from_dtype", - np.typecodes["AllInteger"] + np.typecodes["Float"]) + np.typecodes["AllInteger"] + np.typecodes["AllFloat"]) def test_same_value(self, from_dtype, to_dtype): if from_dtype == to_dtype: return @@ -846,7 +845,12 @@ def test_same_value(self, from_dtype, to_dtype): # Happy path arr1 = np.array([0] * 10, dtype=from_dtype) arr2 = np.array([0] * 10, dtype=to_dtype) - assert_equal(arr1.astype(to_dtype, casting='same_value'), arr2, strict=True) + with warnings.catch_warnings(record=True) as w: + # complex -> non-complex will warn + warnings.simplefilter("always", RuntimeWarning) + arr1_astype = arr1.astype(to_dtype, casting='same_value') + assert_equal(arr1_astype, arr2, strict=True) + # Make it overflow, both aligned and unaligned arr1[0] = top1 aligned = np.empty(arr1.itemsize * arr1.size + 1, 'uint8') unaligned = aligned[1:].view(arr1.dtype) From 9aa1e5ac579bbafdf56b0677552832945ff6fb9c Mon Sep 17 00:00:00 2001 From: mattip Date: Mon, 2 Jun 2025 18:02:37 +0300 Subject: [PATCH 08/29] fix more interfaces to pass casting around, disallow using 'same_value' in raw_array_assign_scalar and raw_array_wheremasked_assign_scalar --- numpy/_core/src/common/array_assign.h | 4 ++-- .../_core/src/multiarray/array_assign_array.c | 11 +++++++++-- .../_core/src/multiarray/array_assign_scalar.c | 18 ++++++++++++++---- numpy/_core/src/multiarray/common.c | 9 --------- numpy/_core/src/multiarray/convert.c | 2 +- numpy/_core/src/umath/reduction.c | 2 +- 6 files changed, 27 insertions(+), 19 deletions(-) diff --git a/numpy/_core/src/common/array_assign.h b/numpy/_core/src/common/array_assign.h index 8a28ed1d3a01..cc5f044ef080 100644 --- a/numpy/_core/src/common/array_assign.h +++ b/numpy/_core/src/common/array_assign.h @@ -46,7 +46,7 @@ PyArray_AssignRawScalar(PyArrayObject *dst, NPY_NO_EXPORT int raw_array_assign_scalar(int ndim, npy_intp const *shape, PyArray_Descr *dst_dtype, char *dst_data, npy_intp const *dst_strides, - PyArray_Descr *src_dtype, char *src_data); + PyArray_Descr *src_dtype, char *src_data, NPY_CASTING casting); /* * Assigns the scalar value to every element of the destination raw array @@ -59,7 +59,7 @@ raw_array_wheremasked_assign_scalar(int ndim, npy_intp const *shape, PyArray_Descr *dst_dtype, char *dst_data, npy_intp const *dst_strides, PyArray_Descr *src_dtype, char *src_data, PyArray_Descr *wheremask_dtype, char *wheremask_data, - npy_intp const *wheremask_strides); + npy_intp const *wheremask_strides, NPY_CASTING casting); /******** LOW-LEVEL ARRAY MANIPULATION HELPERS ********/ diff --git a/numpy/_core/src/multiarray/array_assign_array.c b/numpy/_core/src/multiarray/array_assign_array.c index 624bae2f9450..6abf132fe123 100644 --- a/numpy/_core/src/multiarray/array_assign_array.c +++ b/numpy/_core/src/multiarray/array_assign_array.c @@ -174,7 +174,7 @@ raw_array_assign_array(int ndim, npy_intp const *shape, return 0; same_value_overflow: - PyErr_SetString(PyExc_ValueError, "overflow in 'same_value' cast"); + PyErr_SetString(PyExc_ValueError, "overflow in 'same_value' casting"); fail: NPY_END_THREADS; NPY_cast_info_xfree(&cast_info); @@ -256,6 +256,9 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, } NPY_BEGIN_THREADS_THRESHOLDED(nitems); } + if (same_value_cast) { + cast_info.context.flags |= NPY_SAME_VALUE_CASTING; + } npy_intp strides[2] = {src_strides_it[0], dst_strides_it[0]}; NPY_RAW_ITER_START(idim, ndim, coord, shape_it) { @@ -269,6 +272,9 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, (npy_bool *)wheremask_data, wheremask_strides_it[0], cast_info.auxdata); if (result < 0) { + if (result == NPY_SAME_VALUE_OVERFLOW) { + goto same_value_overflow; + } goto fail; } } NPY_RAW_ITER_THREE_NEXT(idim, ndim, coord, shape_it, @@ -288,6 +294,8 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, return 0; +same_value_overflow: + PyErr_SetString(PyExc_ValueError, "overflow in 'same_value' casting"); fail: NPY_END_THREADS; NPY_cast_info_xfree(&cast_info); @@ -314,7 +322,6 @@ PyArray_AssignArray(PyArrayObject *dst, PyArrayObject *src, NPY_CASTING casting) { int copied_src = 0; - npy_intp src_strides[NPY_MAXDIMS]; /* Use array_assign_scalar if 'src' NDIM is 0 */ diff --git a/numpy/_core/src/multiarray/array_assign_scalar.c b/numpy/_core/src/multiarray/array_assign_scalar.c index 0199ba969eb9..d57727162758 100644 --- a/numpy/_core/src/multiarray/array_assign_scalar.c +++ b/numpy/_core/src/multiarray/array_assign_scalar.c @@ -37,7 +37,7 @@ NPY_NO_EXPORT int raw_array_assign_scalar(int ndim, npy_intp const *shape, PyArray_Descr *dst_dtype, char *dst_data, npy_intp const *dst_strides, - PyArray_Descr *src_dtype, char *src_data) + PyArray_Descr *src_dtype, char *src_data, NPY_CASTING casting) { int idim; npy_intp shape_it[NPY_MAXDIMS], dst_strides_it[NPY_MAXDIMS]; @@ -85,6 +85,11 @@ raw_array_assign_scalar(int ndim, npy_intp const *shape, } NPY_BEGIN_THREADS_THRESHOLDED(nitems); } + if (casting == NPY_SAME_VALUE_CASTING) { + cast_info.context.flags |= NPY_SAME_VALUE_CASTING; + PyErr_SetString(PyExc_NotImplementedError, "'same_value' casting not implemented yet"); + return -1; + } npy_intp strides[2] = {0, dst_strides_it[0]}; @@ -126,7 +131,7 @@ raw_array_wheremasked_assign_scalar(int ndim, npy_intp const *shape, PyArray_Descr *dst_dtype, char *dst_data, npy_intp const *dst_strides, PyArray_Descr *src_dtype, char *src_data, PyArray_Descr *wheremask_dtype, char *wheremask_data, - npy_intp const *wheremask_strides) + npy_intp const *wheremask_strides, NPY_CASTING casting) { int idim; npy_intp shape_it[NPY_MAXDIMS], dst_strides_it[NPY_MAXDIMS]; @@ -177,6 +182,11 @@ raw_array_wheremasked_assign_scalar(int ndim, npy_intp const *shape, } NPY_BEGIN_THREADS_THRESHOLDED(nitems); } + if (casting == NPY_SAME_VALUE_CASTING) { + cast_info.context.flags |= NPY_SAME_VALUE_CASTING; + PyErr_SetString(PyExc_NotImplementedError, "'same_value' casting not implemented yet"); + return -1; + } npy_intp strides[2] = {0, dst_strides_it[0]}; @@ -298,7 +308,7 @@ PyArray_AssignRawScalar(PyArrayObject *dst, /* Do the assignment with raw array iteration */ if (raw_array_assign_scalar(PyArray_NDIM(dst), PyArray_DIMS(dst), PyArray_DESCR(dst), PyArray_DATA(dst), PyArray_STRIDES(dst), - src_dtype, src_data) < 0) { + src_dtype, src_data, casting) < 0) { goto fail; } } @@ -319,7 +329,7 @@ PyArray_AssignRawScalar(PyArrayObject *dst, PyArray_DESCR(dst), PyArray_DATA(dst), PyArray_STRIDES(dst), src_dtype, src_data, PyArray_DESCR(wheremask), PyArray_DATA(wheremask), - wheremask_strides) < 0) { + wheremask_strides, casting) < 0) { goto fail; } } diff --git a/numpy/_core/src/multiarray/common.c b/numpy/_core/src/multiarray/common.c index 8236ec5c65ae..4d1d9c238418 100644 --- a/numpy/_core/src/multiarray/common.c +++ b/numpy/_core/src/multiarray/common.c @@ -25,15 +25,6 @@ * variable is misnamed, but it's part of the public API so I'm not sure we * can just change it. Maybe someone should try and see if anyone notices. */ -/* - * In numpy 1.6 and earlier, this was NPY_UNSAFE_CASTING. In a future - * release, it will become NPY_SAME_KIND_CASTING. Right now, during the - * transitional period, we continue to follow the NPY_UNSAFE_CASTING rules (to - * avoid breaking people's code), but we also check for whether the cast would - * be allowed under the NPY_SAME_KIND_CASTING rules, and if not we issue a - * warning (that people's code will be broken in a future release.) - */ - NPY_NO_EXPORT NPY_CASTING NPY_DEFAULT_ASSIGN_CASTING = NPY_SAME_KIND_CASTING; diff --git a/numpy/_core/src/multiarray/convert.c b/numpy/_core/src/multiarray/convert.c index 8e0177616955..983d9bc19ce6 100644 --- a/numpy/_core/src/multiarray/convert.c +++ b/numpy/_core/src/multiarray/convert.c @@ -429,7 +429,7 @@ PyArray_FillWithScalar(PyArrayObject *arr, PyObject *obj) int retcode = raw_array_assign_scalar( PyArray_NDIM(arr), PyArray_DIMS(arr), descr, PyArray_BYTES(arr), PyArray_STRIDES(arr), - descr, (void *)value); + descr, (void *)value, NPY_UNSAFE_CASTING); if (PyDataType_REFCHK(descr)) { PyArray_ClearBuffer(descr, (void *)value, 0, 1, 1); diff --git a/numpy/_core/src/umath/reduction.c b/numpy/_core/src/umath/reduction.c index b376b94936bc..384ac052b226 100644 --- a/numpy/_core/src/umath/reduction.c +++ b/numpy/_core/src/umath/reduction.c @@ -372,7 +372,7 @@ PyUFunc_ReduceWrapper(PyArrayMethod_Context *context, PyArray_NDIM(result), PyArray_DIMS(result), PyArray_DESCR(result), PyArray_BYTES(result), PyArray_STRIDES(result), - op_dtypes[0], initial_buf); + op_dtypes[0], initial_buf, NPY_UNSAFE_CASTING); if (ret < 0) { goto fail; } From 953714e9eb74ef4bfdcdc160b059746edd3361d5 Mon Sep 17 00:00:00 2001 From: mattip Date: Mon, 2 Jun 2025 19:39:53 +0300 Subject: [PATCH 09/29] raise in places that have a kwarg casting, besides np.astype --- TODO_same_value | 12 ------------ numpy/_core/src/multiarray/array_assign_array.c | 10 +++++++--- numpy/_core/src/multiarray/datetime.c | 3 ++- numpy/_core/src/multiarray/einsum.c.src | 6 ++++++ numpy/_core/src/multiarray/multiarraymodule.c | 11 +++++++++++ numpy/_core/src/umath/ufunc_object.c | 4 ++++ numpy/_core/tests/test_datetime.py | 4 ++++ numpy/_core/tests/test_einsum.py | 5 +++++ numpy/_core/tests/test_shape_base.py | 5 +++++ 9 files changed, 44 insertions(+), 16 deletions(-) delete mode 100644 TODO_same_value diff --git a/TODO_same_value b/TODO_same_value deleted file mode 100644 index 21afcfc310a4..000000000000 --- a/TODO_same_value +++ /dev/null @@ -1,12 +0,0 @@ -- Check where PyArray_CopyObject, PyArray_NewCopy, PyArray_CopyInto, array_datetime_as_string, PyArray_Concatenate, PyArray_where are used, do we need a 'same_value' equivalents? -- In PyArray_FromArray(arr, newtype, flags) should there be a SAME_VALUE flag? -- Examine places where PyArray_CastingConverter is used and add SAME_VALUE handling - - array_datetime_as_string: - - array_copyto: - - PyArray_AssignArray with wheremask (called with a cast arg) - - PyArray_AssignRawScalar with/without wheremask - - PyArray_ConcatenateInto (called with a cast arg) - - PyArray_EinsteinSum (called with a cast arg) - - NpyIter_AdvancedNew (called with a cast arg) -In CanCast, make sure user defined and datetime dtypes will fail with SAME_VALUE ----- diff --git a/numpy/_core/src/multiarray/array_assign_array.c b/numpy/_core/src/multiarray/array_assign_array.c index 6abf132fe123..1e6b4cce1a70 100644 --- a/numpy/_core/src/multiarray/array_assign_array.c +++ b/numpy/_core/src/multiarray/array_assign_array.c @@ -245,6 +245,12 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, &cast_info, &method_flags) != NPY_SUCCEED) { return -1; } + if (same_value_cast) { + cast_info.context.flags |= NPY_SAME_VALUE_CASTING; + PyErr_SetString(PyExc_NotImplementedError, + "raw_array_wheremasked_assign_array with 'same_value' casting not implemented yet"); + return -1; + } if (!(method_flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { npy_clear_floatstatus_barrier(src_data); @@ -256,9 +262,7 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, } NPY_BEGIN_THREADS_THRESHOLDED(nitems); } - if (same_value_cast) { - cast_info.context.flags |= NPY_SAME_VALUE_CASTING; - } + npy_intp strides[2] = {src_strides_it[0], dst_strides_it[0]}; NPY_RAW_ITER_START(idim, ndim, coord, shape_it) { diff --git a/numpy/_core/src/multiarray/datetime.c b/numpy/_core/src/multiarray/datetime.c index 66c2eca48451..ef0502425608 100644 --- a/numpy/_core/src/multiarray/datetime.c +++ b/numpy/_core/src/multiarray/datetime.c @@ -1236,7 +1236,6 @@ can_cast_datetime64_units(NPY_DATETIMEUNIT src_unit, switch (casting) { /* Allow anything with unsafe casting */ case NPY_UNSAFE_CASTING: - case NPY_SAME_VALUE_CASTING: return 1; /* @@ -1262,6 +1261,8 @@ can_cast_datetime64_units(NPY_DATETIMEUNIT src_unit, return (src_unit <= dst_unit); } + case NPY_SAME_VALUE_CASTING: + return 0; /* Enforce equality with 'no' or 'equiv' casting */ default: return src_unit == dst_unit; diff --git a/numpy/_core/src/multiarray/einsum.c.src b/numpy/_core/src/multiarray/einsum.c.src index 3733c436cb1b..6879fe1e02bd 100644 --- a/numpy/_core/src/multiarray/einsum.c.src +++ b/numpy/_core/src/multiarray/einsum.c.src @@ -830,6 +830,12 @@ PyArray_EinsteinSum(char *subscripts, npy_intp nop, return NULL; } + if (casting == NPY_SAME_VALUE_CASTING) { + PyErr_SetString(PyExc_NotImplementedError, + "einsum with casting='same_value' not implemented yet"); + return NULL; + } + /* Parse the subscripts string into label_counts and op_labels */ memset(label_counts, 0, sizeof(label_counts)); for (iop = 0; iop < nop; ++iop) { diff --git a/numpy/_core/src/multiarray/multiarraymodule.c b/numpy/_core/src/multiarray/multiarraymodule.c index b8ade23b6d76..71f4b54caf2d 100644 --- a/numpy/_core/src/multiarray/multiarraymodule.c +++ b/numpy/_core/src/multiarray/multiarraymodule.c @@ -667,6 +667,12 @@ PyArray_ConcatenateInto(PyObject *op, "argument, but both were provided."); return NULL; } + if (casting == NPY_SAME_VALUE_CASTING) { + PyErr_SetString(PyExc_NotImplementedError, + "concatenate with casting='same_value' not implemented yet"); + return NULL; + } + /* Convert the input list into arrays */ Py_ssize_t narrays_true = PySequence_Size(op); @@ -1948,6 +1954,11 @@ array_copyto(PyObject *NPY_UNUSED(ignored), NULL, NULL, NULL) < 0) { goto fail; } + if (casting == NPY_SAME_VALUE_CASTING) { + PyErr_SetString(PyExc_NotImplementedError, + "raw_array_wheremasked_assign_array with 'same_value' casting not implemented yet"); + goto fail; + } if (!PyArray_Check(dst_obj)) { PyErr_Format(PyExc_TypeError, diff --git a/numpy/_core/src/umath/ufunc_object.c b/numpy/_core/src/umath/ufunc_object.c index 82a84636c30b..a505e33bbc35 100644 --- a/numpy/_core/src/umath/ufunc_object.c +++ b/numpy/_core/src/umath/ufunc_object.c @@ -4543,6 +4543,10 @@ ufunc_generic_fastcall(PyUFuncObject *ufunc, full_args.in, casting) < 0) { goto fail; } + if (casting == NPY_SAME_VALUE_CASTING) { + PyErr_SetString(PyExc_NotImplementedError, + "ufunc with 'same_value' casting not implemented yet"); + } /* * Do the final preparations and call the inner-loop. diff --git a/numpy/_core/tests/test_datetime.py b/numpy/_core/tests/test_datetime.py index a10ca15bc373..2fae6889e008 100644 --- a/numpy/_core/tests/test_datetime.py +++ b/numpy/_core/tests/test_datetime.py @@ -1844,6 +1844,10 @@ def test_datetime_as_string(self): '2032-07-18') assert_equal(np.datetime_as_string(a, unit='D', casting='unsafe'), '2032-07-18') + + with pytest.raises(TypeError): + np.datetime_as_string(a, unit='Y', casting='same_value') + assert_equal(np.datetime_as_string(a, unit='h'), '2032-07-18T12') assert_equal(np.datetime_as_string(a, unit='m'), '2032-07-18T12:23') diff --git a/numpy/_core/tests/test_einsum.py b/numpy/_core/tests/test_einsum.py index 84d4af1707b6..6c0253f27792 100644 --- a/numpy/_core/tests/test_einsum.py +++ b/numpy/_core/tests/test_einsum.py @@ -79,6 +79,11 @@ def test_einsum_errors(self, do_opt, einsum_fn): b = np.ones((3, 4, 5)) einsum_fn('aabcb,abc', a, b) + with pytest.raises(NotImplementedError): + a = np.arange(3) + # einsum_path does not accept kwarg 'casting' + np.einsum('ij->j', [a, a], casting='same_value') + def test_einsum_sorting_behavior(self): # Case 1: 26 dimensions (all lowercase indices) n1 = 26 diff --git a/numpy/_core/tests/test_shape_base.py b/numpy/_core/tests/test_shape_base.py index 1b9728e5c006..cc2a0f5c77a1 100644 --- a/numpy/_core/tests/test_shape_base.py +++ b/numpy/_core/tests/test_shape_base.py @@ -383,6 +383,11 @@ def test_concatenate(self): assert_(out is rout) assert_equal(res, rout) + def test_concatenate_same_value(self): + r4 = list(range(4)) + with pytest.raises(NotImplementedError): + concatenate([r4, r4], casting="same_value") + @pytest.mark.skipif( IS_PYPY, reason="PYPY handles sq_concat, nb_add differently than cpython" From cd3e1446933f5e2c6fd25c1962ccac330a09d22d Mon Sep 17 00:00:00 2001 From: mattip Date: Mon, 9 Jun 2025 19:38:38 +0300 Subject: [PATCH 10/29] refactor based on review comments --- numpy/_core/code_generators/cversions.txt | 3 +- numpy/_core/include/numpy/dtype_api.h | 10 +-- numpy/_core/include/numpy/numpyconfig.h | 5 +- numpy/_core/meson.build | 3 +- .../_core/src/multiarray/array_assign_array.c | 27 ++++---- .../src/multiarray/array_assign_scalar.c | 19 ++++-- numpy/_core/src/multiarray/array_coercion.c | 7 ++- numpy/_core/src/multiarray/array_method.c | 20 ++++-- numpy/_core/src/multiarray/array_method.h | 9 +++ numpy/_core/src/multiarray/ctors.c | 11 +++- numpy/_core/src/multiarray/datetime.c | 10 +-- numpy/_core/src/multiarray/dtype_transfer.c | 7 ++- numpy/_core/src/multiarray/dtype_transfer.h | 20 +++--- .../multiarray/lowlevel_strided_loops.c.src | 20 +++--- numpy/_core/src/multiarray/mapping.c | 7 ++- numpy/_core/src/multiarray/multiarraymodule.c | 2 +- numpy/_core/src/umath/legacy_array_method.c | 10 ++- numpy/_core/src/umath/ufunc_object.c | 62 +++++++------------ numpy/_core/tests/test_casting_unittests.py | 14 +++-- 19 files changed, 151 insertions(+), 115 deletions(-) diff --git a/numpy/_core/code_generators/cversions.txt b/numpy/_core/code_generators/cversions.txt index 0d642d760b21..a04dd784c67f 100644 --- a/numpy/_core/code_generators/cversions.txt +++ b/numpy/_core/code_generators/cversions.txt @@ -79,5 +79,6 @@ # Version 19 (NumPy 2.2.0) No change 0x00000013 = 2b8f1f4da822491ff030b2b37dff07e3 # Version 20 (NumPy 2.3.0) -# Version 20 (NumPy 2.4.0) No change 0x00000014 = e56b74d32a934d085e7c3414cb9999b8, +# Version 21 (NumPy 2.4.0) Add 'same_value' casting, header additions +0x00000015 = e56b74d32a934d085e7c3414cb9999b8, diff --git a/numpy/_core/include/numpy/dtype_api.h b/numpy/_core/include/numpy/dtype_api.h index 5ff9c176b7a3..f902a9a74d19 100644 --- a/numpy/_core/include/numpy/dtype_api.h +++ b/numpy/_core/include/numpy/dtype_api.h @@ -107,13 +107,15 @@ typedef struct PyArrayMethod_Context_tag { /* Operand descriptors, filled in by resolve_descriptors */ PyArray_Descr *const *descriptors; - void * padding; + #if NPY_FEATURE_VERSION > NPY_2_3_API_VERSION + void * _reserved; /* * Optional flag to pass information into the inner loop * If set, it will be NPY_CASTING */ uint64_t flags; /* Structure may grow (this is harmless for DType authors) */ + #endif } PyArrayMethod_Context; @@ -150,12 +152,6 @@ typedef struct { #define NPY_METH_contiguous_indexed_loop 9 #define _NPY_METH_static_data 10 -/* - * Constants for same_value casting - */ -#define NPY_SAME_VALUE_OVERFLOW -31 - - /* * The resolve descriptors function, must be able to handle NULL values for * all output (but not input) `given_descrs` and fill `loop_descrs`. diff --git a/numpy/_core/include/numpy/numpyconfig.h b/numpy/_core/include/numpy/numpyconfig.h index 52d7e2b5d7d7..c129a3aceb6d 100644 --- a/numpy/_core/include/numpy/numpyconfig.h +++ b/numpy/_core/include/numpy/numpyconfig.h @@ -84,6 +84,7 @@ #define NPY_2_1_API_VERSION 0x00000013 #define NPY_2_2_API_VERSION 0x00000013 #define NPY_2_3_API_VERSION 0x00000014 +#define NPY_2_4_API_VERSION 0x00000015 /* @@ -172,8 +173,10 @@ #define NPY_FEATURE_VERSION_STRING "2.0" #elif NPY_FEATURE_VERSION == NPY_2_1_API_VERSION #define NPY_FEATURE_VERSION_STRING "2.1" -#elif NPY_FEATURE_VERSION == NPY_2_3_API_VERSION /* also 2.4 */ +#elif NPY_FEATURE_VERSION == NPY_2_3_API_VERSION #define NPY_FEATURE_VERSION_STRING "2.3" +#elif NPY_FEATURE_VERSION == NPY_2_4_API_VERSION + #define NPY_FEATURE_VERSION_STRING "2.4" #else #error "Missing version string define for new NumPy version." #endif diff --git a/numpy/_core/meson.build b/numpy/_core/meson.build index 1b79aa39781c..4cf57212f3da 100644 --- a/numpy/_core/meson.build +++ b/numpy/_core/meson.build @@ -50,7 +50,8 @@ C_ABI_VERSION = '0x02000000' # 0x00000013 - 2.1.x # 0x00000013 - 2.2.x # 0x00000014 - 2.3.x -C_API_VERSION = '0x00000014' +# 0x00000015 - 2.4.x +C_API_VERSION = '0x00000015' # Check whether we have a mismatch between the set C API VERSION and the # actual C API VERSION. Will raise a MismatchCAPIError if so. diff --git a/numpy/_core/src/multiarray/array_assign_array.c b/numpy/_core/src/multiarray/array_assign_array.c index 1e6b4cce1a70..ec23eb6b8f10 100644 --- a/numpy/_core/src/multiarray/array_assign_array.c +++ b/numpy/_core/src/multiarray/array_assign_array.c @@ -132,7 +132,12 @@ raw_array_assign_array(int ndim, npy_intp const *shape, } if (same_value_cast) { + #if NPY_FEATURE_VERSION > NPY_2_3_API_VERSION cast_info.context.flags |= NPY_SAME_VALUE_CASTING; + #else + PyErr_SetString(PyExc_NotImplementedError, + "raw_array_assign_array with 'same_value' casting not implemented yet"); + #endif } /* Ensure number of elements exceeds threshold for threading */ @@ -146,16 +151,14 @@ raw_array_assign_array(int ndim, npy_intp const *shape, npy_intp strides[2] = {src_strides_it[0], dst_strides_it[0]}; + int result = 0; NPY_RAW_ITER_START(idim, ndim, coord, shape_it) { /* Process the innermost dimension */ char *args[2] = {src_data, dst_data}; - int result = cast_info.func(&cast_info.context, + result = cast_info.func(&cast_info.context, args, &shape_it[0], strides, cast_info.auxdata); if (result < 0) { - if (result == NPY_SAME_VALUE_OVERFLOW) { - goto same_value_overflow; - } goto fail; } } NPY_RAW_ITER_TWO_NEXT(idim, ndim, coord, shape_it, @@ -173,11 +176,10 @@ raw_array_assign_array(int ndim, npy_intp const *shape, } return 0; -same_value_overflow: - PyErr_SetString(PyExc_ValueError, "overflow in 'same_value' casting"); fail: NPY_END_THREADS; NPY_cast_info_xfree(&cast_info); + HandleArrayMethodError(result, "astype", method_flags); return -1; } @@ -246,7 +248,7 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, return -1; } if (same_value_cast) { - cast_info.context.flags |= NPY_SAME_VALUE_CASTING; + /* cast_info.context.flags |= NPY_SAME_VALUE_CASTING; */ PyErr_SetString(PyExc_NotImplementedError, "raw_array_wheremasked_assign_array with 'same_value' casting not implemented yet"); return -1; @@ -265,20 +267,18 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, npy_intp strides[2] = {src_strides_it[0], dst_strides_it[0]}; + int result = 0; NPY_RAW_ITER_START(idim, ndim, coord, shape_it) { PyArray_MaskedStridedUnaryOp *stransfer; stransfer = (PyArray_MaskedStridedUnaryOp *)cast_info.func; /* Process the innermost dimension */ char *args[2] = {src_data, dst_data}; - int result = stransfer(&cast_info.context, + result = stransfer(&cast_info.context, args, &shape_it[0], strides, (npy_bool *)wheremask_data, wheremask_strides_it[0], cast_info.auxdata); if (result < 0) { - if (result == NPY_SAME_VALUE_OVERFLOW) { - goto same_value_overflow; - } goto fail; } } NPY_RAW_ITER_THREE_NEXT(idim, ndim, coord, shape_it, @@ -295,14 +295,11 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, return -1; } } - return 0; - -same_value_overflow: - PyErr_SetString(PyExc_ValueError, "overflow in 'same_value' casting"); fail: NPY_END_THREADS; NPY_cast_info_xfree(&cast_info); + HandleArrayMethodError(result, "astype", method_flags); return -1; } diff --git a/numpy/_core/src/multiarray/array_assign_scalar.c b/numpy/_core/src/multiarray/array_assign_scalar.c index d57727162758..e4a62d3ac9e6 100644 --- a/numpy/_core/src/multiarray/array_assign_scalar.c +++ b/numpy/_core/src/multiarray/array_assign_scalar.c @@ -85,19 +85,22 @@ raw_array_assign_scalar(int ndim, npy_intp const *shape, } NPY_BEGIN_THREADS_THRESHOLDED(nitems); } + if (casting == NPY_SAME_VALUE_CASTING) { - cast_info.context.flags |= NPY_SAME_VALUE_CASTING; + /* cast_info.context.flags |= NPY_SAME_VALUE_CASTING; */ PyErr_SetString(PyExc_NotImplementedError, "'same_value' casting not implemented yet"); return -1; } npy_intp strides[2] = {0, dst_strides_it[0]}; + int result = 0; NPY_RAW_ITER_START(idim, ndim, coord, shape_it) { /* Process the innermost dimension */ char *args[2] = {src_data, dst_data}; - if (cast_info.func(&cast_info.context, - args, &shape_it[0], strides, cast_info.auxdata) < 0) { + result = cast_info.func(&cast_info.context, + args, &shape_it[0], strides, cast_info.auxdata); + if (result < 0) { goto fail; } } NPY_RAW_ITER_ONE_NEXT(idim, ndim, coord, @@ -117,6 +120,7 @@ raw_array_assign_scalar(int ndim, npy_intp const *shape, fail: NPY_END_THREADS; NPY_cast_info_xfree(&cast_info); + HandleArrayMethodError(result, "cast", flags); return -1; } @@ -183,12 +187,13 @@ raw_array_wheremasked_assign_scalar(int ndim, npy_intp const *shape, NPY_BEGIN_THREADS_THRESHOLDED(nitems); } if (casting == NPY_SAME_VALUE_CASTING) { - cast_info.context.flags |= NPY_SAME_VALUE_CASTING; + /* cast_info.context.flags |= NPY_SAME_VALUE_CASTING; */ PyErr_SetString(PyExc_NotImplementedError, "'same_value' casting not implemented yet"); return -1; } npy_intp strides[2] = {0, dst_strides_it[0]}; + int result = 0; NPY_RAW_ITER_START(idim, ndim, coord, shape_it) { /* Process the innermost dimension */ @@ -196,10 +201,11 @@ raw_array_wheremasked_assign_scalar(int ndim, npy_intp const *shape, stransfer = (PyArray_MaskedStridedUnaryOp *)cast_info.func; char *args[2] = {src_data, dst_data}; - if (stransfer(&cast_info.context, + result = stransfer(&cast_info.context, args, &shape_it[0], strides, (npy_bool *)wheremask_data, wheremask_strides_it[0], - cast_info.auxdata) < 0) { + cast_info.auxdata); + if (result < 0) { goto fail; } } NPY_RAW_ITER_TWO_NEXT(idim, ndim, coord, shape_it, @@ -221,6 +227,7 @@ raw_array_wheremasked_assign_scalar(int ndim, npy_intp const *shape, fail: NPY_END_THREADS; NPY_cast_info_xfree(&cast_info); + HandleArrayMethodError(result, "cast", flags); return -1; } diff --git a/numpy/_core/src/multiarray/array_coercion.c b/numpy/_core/src/multiarray/array_coercion.c index 8271fb6812d1..0aa41221e0e7 100644 --- a/numpy/_core/src/multiarray/array_coercion.c +++ b/numpy/_core/src/multiarray/array_coercion.c @@ -418,9 +418,12 @@ npy_cast_raw_scalar_item( char *args[2] = {from_item, to_item}; const npy_intp strides[2] = {0, 0}; const npy_intp length = 1; - if (cast_info.func(&cast_info.context, - args, &length, strides, cast_info.auxdata) < 0) { + int result = 0; + result = cast_info.func(&cast_info.context, + args, &length, strides, cast_info.auxdata); + if (result < 0) { NPY_cast_info_xfree(&cast_info); + HandleArrayMethodError(result, "cast", flags); return -1; } NPY_cast_info_xfree(&cast_info); diff --git a/numpy/_core/src/multiarray/array_method.c b/numpy/_core/src/multiarray/array_method.c index e0ce47dabdba..bf913cf5fd1b 100644 --- a/numpy/_core/src/multiarray/array_method.c +++ b/numpy/_core/src/multiarray/array_method.c @@ -39,6 +39,7 @@ #include "convert_datatype.h" #include "common.h" #include "numpy/ufuncobject.h" +#include "dtype_transfer.h" /* @@ -793,12 +794,10 @@ boundarraymethod__simple_strided_call( return NULL; } - PyArrayMethod_Context context = { - .caller = NULL, - .method = self->method, - .descriptors = descrs, - .flags = 0, - }; + PyArrayMethod_Context context; + NPY_context_init(&context, descrs); + context.method = self->method; + PyArrayMethod_StridedLoop *strided_loop = NULL; NpyAuxData *loop_data = NULL; NPY_ARRAYMETHOD_FLAGS flags = 0; @@ -986,3 +985,12 @@ NPY_NO_EXPORT PyTypeObject PyBoundArrayMethod_Type = { .tp_methods = boundarraymethod_methods, .tp_getset = boundarraymethods_getters, }; + +int HandleArrayMethodError(int result, const char * name , int method_flags) +{ + if (result == NPY_SAME_VALUE_FAILURE) { + PyErr_Format(PyExc_ValueError, "'same_value' casting failure in %s", name); + } + return result; +} + diff --git a/numpy/_core/src/multiarray/array_method.h b/numpy/_core/src/multiarray/array_method.h index bcf270899f13..3e87868f02d9 100644 --- a/numpy/_core/src/multiarray/array_method.h +++ b/numpy/_core/src/multiarray/array_method.h @@ -122,6 +122,15 @@ PyArrayMethod_FromSpec(PyArrayMethod_Spec *spec); NPY_NO_EXPORT PyBoundArrayMethodObject * PyArrayMethod_FromSpec_int(PyArrayMethod_Spec *spec, int priv); +/* + * Possible results to be handled in HandleArrayMethodError, after a call + * to a PyArrayMethod_StridedLoop + */ +#define NPY_GENERIC_LOOP_FAILURE -1 /* will have set a PyErr already */ +#define NPY_SAME_VALUE_FAILURE -31 /* same_value casting failed */ + +int HandleArrayMethodError(int result, const char * name , int method_flags); + #ifdef __cplusplus } #endif diff --git a/numpy/_core/src/multiarray/ctors.c b/numpy/_core/src/multiarray/ctors.c index 4f466677c57c..985b1d1369bb 100644 --- a/numpy/_core/src/multiarray/ctors.c +++ b/numpy/_core/src/multiarray/ctors.c @@ -2805,12 +2805,13 @@ PyArray_CopyAsFlat(PyArrayObject *dst, PyArrayObject *src, NPY_ORDER order) npy_intp strides[2] = {src_stride, dst_stride}; int res = 0; + int result = 0; for(;;) { /* Transfer the biggest amount that fits both */ count = (src_count < dst_count) ? src_count : dst_count; - if (cast_info.func(&cast_info.context, - args, &count, strides, cast_info.auxdata) < 0) { - res = -1; + result = cast_info.func(&cast_info.context, + args, &count, strides, cast_info.auxdata); + if (result < 0) { break; } @@ -2852,6 +2853,10 @@ PyArray_CopyAsFlat(PyArrayObject *dst, PyArrayObject *src, NPY_ORDER order) if (!NpyIter_Deallocate(src_iter)) { res = -1; } + if (result < 0) { + HandleArrayMethodError(result, "cast", flags); + res = result; + } if (res == 0 && !(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { int fpes = npy_get_floatstatus_barrier((char *)&src_iter); diff --git a/numpy/_core/src/multiarray/datetime.c b/numpy/_core/src/multiarray/datetime.c index ef0502425608..0adfa70b6aa5 100644 --- a/numpy/_core/src/multiarray/datetime.c +++ b/numpy/_core/src/multiarray/datetime.c @@ -1283,7 +1283,6 @@ can_cast_timedelta64_units(NPY_DATETIMEUNIT src_unit, switch (casting) { /* Allow anything with unsafe casting */ case NPY_UNSAFE_CASTING: - case NPY_SAME_VALUE_CASTING: return 1; /* @@ -1305,6 +1304,7 @@ can_cast_timedelta64_units(NPY_DATETIMEUNIT src_unit, * 'safe' casting. */ case NPY_SAFE_CASTING: + case NPY_SAME_VALUE_CASTING: if (src_unit == NPY_FR_GENERIC || dst_unit == NPY_FR_GENERIC) { return src_unit == NPY_FR_GENERIC; } @@ -1330,7 +1330,6 @@ can_cast_datetime64_metadata(PyArray_DatetimeMetaData *src_meta, { switch (casting) { case NPY_UNSAFE_CASTING: - case NPY_SAME_VALUE_CASTING: return 1; case NPY_SAME_KIND_CASTING: @@ -1338,6 +1337,7 @@ can_cast_datetime64_metadata(PyArray_DatetimeMetaData *src_meta, casting); case NPY_SAFE_CASTING: + case NPY_SAME_VALUE_CASTING: return can_cast_datetime64_units(src_meta->base, dst_meta->base, casting) && datetime_metadata_divides(src_meta, dst_meta, 0); @@ -1358,7 +1358,6 @@ can_cast_timedelta64_metadata(PyArray_DatetimeMetaData *src_meta, { switch (casting) { case NPY_UNSAFE_CASTING: - case NPY_SAME_VALUE_CASTING: return 1; case NPY_SAME_KIND_CASTING: @@ -1366,6 +1365,7 @@ can_cast_timedelta64_metadata(PyArray_DatetimeMetaData *src_meta, casting); case NPY_SAFE_CASTING: + case NPY_SAME_VALUE_CASTING: return can_cast_timedelta64_units(src_meta->base, dst_meta->base, casting) && datetime_metadata_divides(src_meta, dst_meta, 1); @@ -2466,7 +2466,7 @@ convert_pyobject_to_datetime(PyArray_DatetimeMetaData *meta, PyObject *obj, * With unsafe casting, convert unrecognized objects into NaT * and with same_kind casting, convert None into NaT */ - if (casting == NPY_UNSAFE_CASTING || (casting == NPY_SAME_VALUE_CASTING) || + if (casting == NPY_UNSAFE_CASTING || (obj == Py_None && casting == NPY_SAME_KIND_CASTING)) { if (meta->base == NPY_FR_ERROR) { meta->base = NPY_FR_GENERIC; @@ -2734,7 +2734,7 @@ convert_pyobject_to_timedelta(PyArray_DatetimeMetaData *meta, PyObject *obj, * With unsafe casting, convert unrecognized objects into NaT * and with same_kind casting, convert None into NaT */ - if (casting == NPY_UNSAFE_CASTING || (casting == NPY_SAME_VALUE_CASTING) || + if (casting == NPY_UNSAFE_CASTING || (obj == Py_None && casting == NPY_SAME_KIND_CASTING)) { if (meta->base == NPY_FR_ERROR) { meta->base = NPY_FR_GENERIC; diff --git a/numpy/_core/src/multiarray/dtype_transfer.c b/numpy/_core/src/multiarray/dtype_transfer.c index 66b0d686dbad..49764f80cbc7 100644 --- a/numpy/_core/src/multiarray/dtype_transfer.c +++ b/numpy/_core/src/multiarray/dtype_transfer.c @@ -3426,11 +3426,14 @@ PyArray_CastRawArrays(npy_intp count, /* Cast */ char *args[2] = {src, dst}; npy_intp strides[2] = {src_stride, dst_stride}; - cast_info.func(&cast_info.context, args, &count, strides, cast_info.auxdata); + int result = cast_info.func(&cast_info.context, args, &count, strides, cast_info.auxdata); /* Cleanup */ NPY_cast_info_xfree(&cast_info); - + if (result < 0) { + HandleArrayMethodError(result, "cast", flags); + return NPY_FAIL; + } if (flags & NPY_METH_REQUIRES_PYAPI && PyErr_Occurred()) { return NPY_FAIL; } diff --git a/numpy/_core/src/multiarray/dtype_transfer.h b/numpy/_core/src/multiarray/dtype_transfer.h index cd55db3a25f7..70bd6a302ef0 100644 --- a/numpy/_core/src/multiarray/dtype_transfer.h +++ b/numpy/_core/src/multiarray/dtype_transfer.h @@ -25,6 +25,17 @@ typedef struct { } NPY_cast_info; +static inline void +NPY_context_init(PyArrayMethod_Context *context, PyArray_Descr *descr[2]) +{ + context->descriptors = descr; + context->caller = NULL; + #if NPY_FEATURE_VERSION > NPY_2_3_API_VERSION + context->_reserved = NULL; + context->flags = 0; + #endif +} + /* * Create a new cast-info struct with cast_info->context.descriptors linked. * Compilers should inline this to ensure the whole struct is not actually @@ -40,16 +51,9 @@ NPY_cast_info_init(NPY_cast_info *cast_info) * a scratch space to `NPY_cast_info` and link to that instead. */ cast_info->auxdata = NULL; - cast_info->context.descriptors = cast_info->descriptors; - - // TODO: Delete this again probably maybe create a new minimal init macro - cast_info->context.caller = NULL; - - cast_info->context.padding = NULL; - cast_info->context.flags = 0; + NPY_context_init(&(cast_info->context), cast_info->descriptors); } - /* * Free's all references and data held inside the struct (not the struct). * First checks whether `cast_info.func == NULL`, and assume it is diff --git a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src index 804d35f84261..3febc8665db1 100644 --- a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src +++ b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src @@ -914,7 +914,11 @@ static GCC_CAST_OPT_LEVEL int /*printf("@prefix@_cast_@name1@_to_@name2@\n");*/ #if !@is_bool2@ + #if NPY_FEATURE_VERSION > NPY_2_3_API_VERSION int same_value_casting = ((context->flags & NPY_SAME_VALUE_CASTING) == NPY_SAME_VALUE_CASTING); + #else + int same_value_casting = 0; + #endif #endif while (N--) { @@ -934,7 +938,7 @@ static GCC_CAST_OPT_LEVEL int dst_value[1] = _CONVERT_FN(src_value[1]); if (same_value_casting) { if ((dst_value[0] != src_value[0]) || (dst_value[1] != src_value[1])){ - return NPY_SAME_VALUE_OVERFLOW; + return NPY_SAME_VALUE_FAILURE; } } # elif !@aligned@ @@ -944,7 +948,7 @@ static GCC_CAST_OPT_LEVEL int dst_value = _CONVERT_FN(src_value[0]); if (same_value_casting) { if (dst_value != src_value[0]){ - return NPY_SAME_VALUE_OVERFLOW; + return NPY_SAME_VALUE_FAILURE; } } # endif @@ -955,7 +959,7 @@ static GCC_CAST_OPT_LEVEL int *(_TYPE2 *)dst = _CONVERT_FN(src_value[0]); if (same_value_casting) { if (*(_TYPE2 *)dst != src_value[0]){ - return NPY_SAME_VALUE_OVERFLOW; + return NPY_SAME_VALUE_FAILURE; } } # endif @@ -974,7 +978,7 @@ static GCC_CAST_OPT_LEVEL int # else if ((@rtype2@)dst_value[0] != *((_TYPE1 *)src)){ # endif - return NPY_SAME_VALUE_OVERFLOW; + return NPY_SAME_VALUE_FAILURE; } } # elif !@aligned@ @@ -982,11 +986,11 @@ static GCC_CAST_OPT_LEVEL int # if !@is_bool2@ if (same_value_casting) { if (_TO_RTYPE1(src_value) > @type2max@) { - return NPY_SAME_VALUE_OVERFLOW; + return NPY_SAME_VALUE_FAILURE; } # if !@is_unsigned1@ if (_TO_RTYPE1(src_value) < @type2min@) { - return NPY_SAME_VALUE_OVERFLOW; + return NPY_SAME_VALUE_FAILURE; } # endif } @@ -996,11 +1000,11 @@ static GCC_CAST_OPT_LEVEL int # if !@is_bool2@ if (same_value_casting) { if (_TO_RTYPE1(*((_TYPE1 *)src)) > @type2max@) { - return NPY_SAME_VALUE_OVERFLOW; + return NPY_SAME_VALUE_FAILURE; } # if !@is_unsigned1@ if (_TO_RTYPE1(*((_TYPE1 *)src)) < @type2min@) { - return NPY_SAME_VALUE_OVERFLOW; + return NPY_SAME_VALUE_FAILURE; } # endif } diff --git a/numpy/_core/src/multiarray/mapping.c b/numpy/_core/src/multiarray/mapping.c index 12f25534f1d0..4c327ca7c378 100644 --- a/numpy/_core/src/multiarray/mapping.c +++ b/numpy/_core/src/multiarray/mapping.c @@ -1241,6 +1241,9 @@ array_assign_boolean_subscript(PyArrayObject *self, } NPY_cast_info_xfree(&cast_info); + if (res < 0) { + HandleArrayMethodError(res, "cast", flags); + } if (!NpyIter_Deallocate(iter)) { res = -1; } @@ -2132,7 +2135,9 @@ array_assign_subscript(PyArrayObject *self, PyObject *ind, PyObject *op) * Could add a casting check, but apparently most assignments do * not care about safe casting. */ - if (mapiter_set(mit, &cast_info, meth_flags, is_aligned) < 0) { + int result = mapiter_set(mit, &cast_info, meth_flags, is_aligned); + if (result < 0) { + HandleArrayMethodError(result, "cast", meth_flags); goto fail; } diff --git a/numpy/_core/src/multiarray/multiarraymodule.c b/numpy/_core/src/multiarray/multiarraymodule.c index 71f4b54caf2d..7d27fe5d729c 100644 --- a/numpy/_core/src/multiarray/multiarraymodule.c +++ b/numpy/_core/src/multiarray/multiarraymodule.c @@ -1956,7 +1956,7 @@ array_copyto(PyObject *NPY_UNUSED(ignored), } if (casting == NPY_SAME_VALUE_CASTING) { PyErr_SetString(PyExc_NotImplementedError, - "raw_array_wheremasked_assign_array with 'same_value' casting not implemented yet"); + "array_copyto with 'same_value' casting not implemented yet"); goto fail; } diff --git a/numpy/_core/src/umath/legacy_array_method.c b/numpy/_core/src/umath/legacy_array_method.c index 99db0134c300..7a85937fcc8f 100644 --- a/numpy/_core/src/umath/legacy_array_method.c +++ b/numpy/_core/src/umath/legacy_array_method.c @@ -439,12 +439,10 @@ PyArray_NewLegacyWrappingArrayMethod(PyUFuncObject *ufunc, descrs[i] = bound_res->dtypes[i]->singleton; } - PyArrayMethod_Context context = { - .caller = (PyObject *)ufunc, - .method = bound_res->method, - .descriptors = descrs, - .flags = 0, - }; + PyArrayMethod_Context context; + NPY_context_init(&context, descrs); + context.caller = (PyObject *)ufunc; + context.method = bound_res->method; int ret = get_initial_from_ufunc(&context, 0, context.method->legacy_initial); diff --git a/numpy/_core/src/umath/ufunc_object.c b/numpy/_core/src/umath/ufunc_object.c index a505e33bbc35..59e37e6a3e01 100644 --- a/numpy/_core/src/umath/ufunc_object.c +++ b/numpy/_core/src/umath/ufunc_object.c @@ -2084,12 +2084,10 @@ PyUFunc_GeneralizedFunctionInternal(PyUFuncObject *ufunc, NPY_SIZEOF_INTP * nop); /* Final preparation of the arraymethod call */ - PyArrayMethod_Context context = { - .caller = (PyObject *)ufunc, - .method = ufuncimpl, - .descriptors = operation_descrs, - .flags = 0, - }; + PyArrayMethod_Context context; + NPY_context_init(&context, operation_descrs); + context.caller = (PyObject *)ufunc; + context.method = ufuncimpl; PyArrayMethod_StridedLoop *strided_loop; NPY_ARRAYMETHOD_FLAGS flags = 0; @@ -2204,12 +2202,10 @@ PyUFunc_GenericFunctionInternal(PyUFuncObject *ufunc, } /* Final preparation of the arraymethod call */ - PyArrayMethod_Context context = { - .caller = (PyObject *)ufunc, - .method = ufuncimpl, - .descriptors = operation_descrs, - .flags = 0, - }; + PyArrayMethod_Context context; + NPY_context_init(&context, operation_descrs); + context.caller = (PyObject *)ufunc; + context.method = ufuncimpl; /* Do the ufunc loop */ if (wheremask != NULL) { @@ -2555,12 +2551,10 @@ PyUFunc_Reduce(PyUFuncObject *ufunc, return NULL; } - PyArrayMethod_Context context = { - .caller = (PyObject *)ufunc, - .method = ufuncimpl, - .descriptors = descrs, - .flags = 0, - }; + PyArrayMethod_Context context; + NPY_context_init(&context, descrs); + context.caller = (PyObject *)ufunc; + context.method = ufuncimpl; PyArrayObject *result = PyUFunc_ReduceWrapper(&context, arr, out, wheremask, axis_flags, keepdims, @@ -2632,13 +2626,10 @@ PyUFunc_Accumulate(PyUFuncObject *ufunc, PyArrayObject *arr, PyArrayObject *out, assert(PyArray_EquivTypes(descrs[0], descrs[1]) && PyArray_EquivTypes(descrs[0], descrs[2])); - PyArrayMethod_Context context = { - .caller = (PyObject *)ufunc, - .method = ufuncimpl, - .descriptors = descrs, - .flags = 0, - }; - + PyArrayMethod_Context context; + NPY_context_init(&context, descrs); + context.caller = (PyObject *)ufunc, + context.method = ufuncimpl, ndim = PyArray_NDIM(arr); #if NPY_UF_DBG_TRACING @@ -3065,13 +3056,10 @@ PyUFunc_Reduceat(PyUFuncObject *ufunc, PyArrayObject *arr, PyArrayObject *ind, goto fail; } - PyArrayMethod_Context context = { - .caller = (PyObject *)ufunc, - .method = ufuncimpl, - .descriptors = descrs, - .flags = 0, - }; - + PyArrayMethod_Context context; + NPY_context_init(&context, descrs); + context.caller = (PyObject *)ufunc, + context.method = ufuncimpl, ndim = PyArray_NDIM(arr); #if NPY_UF_DBG_TRACING @@ -5911,12 +5899,10 @@ ufunc_at(PyUFuncObject *ufunc, PyObject *args) } } - PyArrayMethod_Context context = { - .caller = (PyObject *)ufunc, - .method = ufuncimpl, - .descriptors = operation_descrs, - .flags = 0, - }; + PyArrayMethod_Context context; + NPY_context_init(&context, operation_descrs); + context.caller = (PyObject *)ufunc; + context.method = ufuncimpl; /* Use contiguous strides; if there is such a loop it may be faster */ npy_intp strides[3] = { diff --git a/numpy/_core/tests/test_casting_unittests.py b/numpy/_core/tests/test_casting_unittests.py index c1050b3fa015..7ce30003b4cb 100644 --- a/numpy/_core/tests/test_casting_unittests.py +++ b/numpy/_core/tests/test_casting_unittests.py @@ -306,6 +306,7 @@ def test_simple_direct_casts(self, from_dt): to_dt = to_dt.values[0] cast = get_castingimpl(type(from_dt), type(to_dt)) + print("from_dt", from_dt, "to_dt", to_dt) casting, (from_res, to_res), view_off = cast._resolve_descriptors( (from_dt, to_dt)) @@ -319,7 +320,9 @@ def test_simple_direct_casts(self, from_dt): arr1, arr2, values = self.get_data(from_dt, to_dt) + print("2", arr1, arr2, cast) cast._simple_strided_call((arr1, arr2)) + print("3") # Check via python list assert arr2.tolist() == values @@ -835,8 +838,9 @@ def test_same_value(self, from_dtype, to_dtype): top2 = np.iinfo(to_dtype).max except ValueError: top2 = np.finfo(to_dtype).max - # No need to test if top2 > top1, since the test will also do the reverse dtype matching. - # Catch then warning if the comparison warns, i.e. np.int16(65535) < np.float16(6.55e4) + # No need to test if top2 > top1, since the test will also do the + # reverse dtype matching. Catch then warning if the comparison warns, + # i.e. np.int16(65535) < np.float16(6.55e4) with warnings.catch_warnings(record=True): warnings.simplefilter("always", RuntimeWarning) if top2 >= top1: @@ -856,8 +860,10 @@ def test_same_value(self, from_dtype, to_dtype): unaligned = aligned[1:].view(arr1.dtype) unaligned[:] = arr1 with pytest.raises(ValueError): - # Casting float to float with overflow should raise RuntimeWarning (fperror) - # Casting float to int with overflow sometimes raises RuntimeWarning (fperror) + # Casting float to float with overflow should raise + # RuntimeWarning (fperror) + # Casting float to int with overflow sometimes raises + # RuntimeWarning (fperror) # Casting with overflow and 'same_value', should raise ValueError with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always", RuntimeWarning) From 6d6b045bb7a886f1f8fbb5d199194c637bfc1fdb Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Tue, 22 Jul 2025 07:37:51 +1000 Subject: [PATCH 11/29] CHAR_MAX,MIN -> SCHAR_MAX,MIN --- numpy/_core/src/multiarray/lowlevel_strided_loops.c.src | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src index 3febc8665db1..3e95b26d09dc 100644 --- a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src +++ b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src @@ -770,12 +770,12 @@ NPY_NO_EXPORT PyArrayMethod_StridedLoop * * npy_float, npy_double, npy_longdouble# * #type2max = 0, * UCHAR_MAX, USHRT_MAX, UINT_MAX, ULONG_MAX, ULLONG_MAX, - * CHAR_MAX, SHRT_MAX, INT_MAX, LONG_MAX, LLONG_MAX, + * SCHAR_MAX, SHRT_MAX, INT_MAX, LONG_MAX, LLONG_MAX, * 65500, FLT_MAX, DBL_MAX, LDBL_MAX, * FLT_MAX, DBL_MAX, LDBL_MAX# * #type2min = 0, * 0, 0, 0, 0, 0, - * CHAR_MIN, SHRT_MIN, INT_MIN, LONG_MIN, LLONG_MIN, + * SCHAR_MIN, SHRT_MIN, INT_MIN, LONG_MIN, LLONG_MIN, * -65500, -FLT_MAX, -DBL_MAX, -LDBL_MAX, * -FLT_MAX, -DBL_MAX, -LDBL_MAX# * #is_bool2 = 1, 0*17# From 1293657420ec6807639dab21645ac68b1405eb61 Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Wed, 23 Jul 2025 06:34:59 +1000 Subject: [PATCH 12/29] copy context flags --- numpy/_core/src/multiarray/dtype_transfer.h | 1 + 1 file changed, 1 insertion(+) diff --git a/numpy/_core/src/multiarray/dtype_transfer.h b/numpy/_core/src/multiarray/dtype_transfer.h index 70bd6a302ef0..0da747291514 100644 --- a/numpy/_core/src/multiarray/dtype_transfer.h +++ b/numpy/_core/src/multiarray/dtype_transfer.h @@ -107,6 +107,7 @@ NPY_cast_info_copy(NPY_cast_info *cast_info, NPY_cast_info *original) Py_XINCREF(cast_info->descriptors[1]); cast_info->context.caller = original->context.caller; Py_XINCREF(cast_info->context.caller); + cast_info->context.flags = original->context.flags; cast_info->context.method = original->context.method; Py_XINCREF(cast_info->context.method); if (original->auxdata == NULL) { From d151c91d7d699fe90bee97a350fc05cacce0ebf4 Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Thu, 24 Jul 2025 07:27:33 +1000 Subject: [PATCH 13/29] add 'same_value' to typing stubs --- numpy/__init__.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/numpy/__init__.pyi b/numpy/__init__.pyi index d8c57c87cbbe..b493e44ed7cd 100644 --- a/numpy/__init__.pyi +++ b/numpy/__init__.pyi @@ -965,7 +965,7 @@ _DTypeBuiltinKind: TypeAlias = L[0, 1, 2] _ArrayAPIVersion: TypeAlias = L["2021.12", "2022.12", "2023.12", "2024.12"] -_CastingKind: TypeAlias = L["no", "equiv", "safe", "same_kind", "unsafe"] +_CastingKind: TypeAlias = L["no", "equiv", "safe", "same_kind", "same_value", "unsafe"] _OrderKACF: TypeAlias = L["K", "A", "C", "F"] | None _OrderACF: TypeAlias = L["A", "C", "F"] | None From 6254ae5d3843f92145a272faa571c5099cf4a488 Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Fri, 25 Jul 2025 05:39:29 +1000 Subject: [PATCH 14/29] document new feature --- doc/release/upcoming_changes/29129.enhancement.rst | 7 +++++++ numpy/_core/_add_newdocs.py | 9 ++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 doc/release/upcoming_changes/29129.enhancement.rst diff --git a/doc/release/upcoming_changes/29129.enhancement.rst b/doc/release/upcoming_changes/29129.enhancement.rst new file mode 100644 index 000000000000..9a14f13c1f4a --- /dev/null +++ b/doc/release/upcoming_changes/29129.enhancement.rst @@ -0,0 +1,7 @@ +``'same_value'`` for casting by value +------------------------------------- +The ``casting`` kwarg now has a ``'same_value'`` option that checks the actual +values can be round-trip cast without changing value. Currently it is only +implemented in `ndarray.astype`. This will raise a ``ValueError`` if any of the +values in the array would change as a result of the cast, including rounding of +floats or overflowing of ints. diff --git a/numpy/_core/_add_newdocs.py b/numpy/_core/_add_newdocs.py index 90d33d4b810a..0ef76712b657 100644 --- a/numpy/_core/_add_newdocs.py +++ b/numpy/_core/_add_newdocs.py @@ -3185,7 +3185,7 @@ 'C' order otherwise, and 'K' means as close to the order the array elements appear in memory as possible. Default is 'K'. - casting : {'no', 'equiv', 'safe', 'same_kind', 'unsafe'}, optional + casting : {'no', 'equiv', 'safe', 'same_kind', 'same_value', 'unsafe'}, optional Controls what kind of data casting may occur. Defaults to 'unsafe' for backwards compatibility. @@ -3195,6 +3195,10 @@ * 'same_kind' means only safe casts or casts within a kind, like float64 to float32, are allowed. * 'unsafe' means any data conversions may be done. + * 'same_value' means any data conversions may be done, but the values + must not change, including rounding of floats or overflow of ints + .. versionadded:: 2.4 + Support for ``'same_value'`` was added. subok : bool, optional If True, then sub-classes will be passed-through (default), otherwise the returned array will be forced to be a base-class array. @@ -3217,6 +3221,9 @@ ComplexWarning When casting from complex to float or int. To avoid this, one should use ``a.real.astype(t)``. + ValueError + When casting using ``'same_value'`` and the values change or would + overflow Examples -------- From 6922b35d98e8199153a0b571a1901654025c8471 Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Fri, 25 Jul 2025 05:42:27 +1000 Subject: [PATCH 15/29] test, check exact float->int casting: refactor same_value check into a function --- .../multiarray/lowlevel_strided_loops.c.src | 112 ++++++++++++++---- numpy/_core/tests/test_casting_unittests.py | 58 ++++++++- 2 files changed, 144 insertions(+), 26 deletions(-) diff --git a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src index 3e95b26d09dc..10d1bf3d1482 100644 --- a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src +++ b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src @@ -825,14 +825,19 @@ NPY_NO_EXPORT PyArrayMethod_StridedLoop * # define _TO_RTYPE1(x) npy_half_to_float(x) # if @is_float2@ # define _CONVERT_FN(x) npy_halfbits_to_floatbits(x) +# define _ROUND_TRIP(x) npy_floatbits_to_halfbits(_CONVERT_FN(x)) # elif @is_double2@ # define _CONVERT_FN(x) npy_halfbits_to_doublebits(x) +# define _ROUND_TRIP(x) npy_doublebits_to_halfbits(_CONVERT_FN(x)) # elif @is_emu_half2@ # define _CONVERT_FN(x) (x) +# define _ROUND_TRIP(x) (x) # elif @is_bool2@ # define _CONVERT_FN(x) ((npy_bool)!npy_half_iszero(x)) +# define _ROUND_TRIP(x) npy_float_to_half((float)(!npy_half_iszero(x))) # else # define _CONVERT_FN(x) ((_TYPE2)npy_half_to_float(x)) +# define _ROUND_TRIP(x) npy_float_to_half((float)_CONVERT_FN(x)) # endif #elif @is_emu_half2@ @@ -840,24 +845,29 @@ NPY_NO_EXPORT PyArrayMethod_StridedLoop * # if @is_float1@ # define _CONVERT_FN(x) npy_floatbits_to_halfbits(x) +# define _ROUND_TRIP(x) npy_halfbits_to_floatbits(_CONVERTFN(x)) # elif @is_double1@ # define _CONVERT_FN(x) npy_doublebits_to_halfbits(x) +# define _ROUND_TRIP(x) npy_halfbits_to_doublebits(_CONVERTFN(x)) # elif @is_emu_half1@ # define _CONVERT_FN(x) (x) +# define _ROUND_TRIP(x) (x) # elif @is_bool1@ # define _CONVERT_FN(x) npy_float_to_half((float)(x!=0)) +# define _ROUND_TRIP(x) (x) # else # define _CONVERT_FN(x) npy_float_to_half((float)x) +# define _ROUND_TRIP(x) ((@rtype1@)npy_half_to_float(_CONVERT_FN(x))) # endif #else -# define _TO_RTYPE1(x) (@rtype1@)(x) - # if @is_bool2@ || @is_bool1@ # define _CONVERT_FN(x) ((npy_bool)(x != 0)) # else # define _CONVERT_FN(x) ((_TYPE2)x) # endif +# define _TO_RTYPE1(x) (@rtype1@)(x) +# define _ROUND_TRIP(x) _TO_RTYPE1(_CONVERT_FN(x)) #endif @@ -883,6 +893,58 @@ NPY_NO_EXPORT PyArrayMethod_StridedLoop * #define GCC_CAST_OPT_LEVEL NPY_GCC_OPT_3 #endif +#if !@is_bool2@ +/* + * Check various modes of failure to accurately cast src_value to dst + */ +static GCC_CAST_OPT_LEVEL int +@prefix@_check_same_value_@name1@_to_@name2@(_TYPE1 src_value) { + + int src_isnan = isnan((double)_TO_RTYPE1(src_value)); + int src_isinf = isinf((double)_TO_RTYPE1(src_value)); + /* + * 1. Check that the src does not overflow the dst. + * Ignore inf since it does not compare well + * This is complicated by clang warning that, for instance, int8 cannot + * overflow int64max + */ +#if (@is_float2@ || @is_emu_half2@ || @is_double2@ || @is_native_half2@) + if (!src_isinf) { +#endif +# if !@is_bool1@ +# if __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wtautological-constant-out-of-range-compare" +#endif + if (!src_isnan && _TO_RTYPE1(src_value) > @type2max@) { + return NPY_SAME_VALUE_FAILURE; + } +# if !@is_unsigned1@ + if (!src_isnan && _TO_RTYPE1(src_value) < @type2min@) { + return NPY_SAME_VALUE_FAILURE; + } +# endif +# if __clang__ +# pragma clang diagnostic pop +# endif +# endif +# if (@is_float2@ || @is_emu_half2@ || @is_double2@ || @is_native_half2@) + } +# endif + /* 2. Check inf, nan with a non-float dst */ +# if (!@is_float2@ && !@is_emu_half2@ && !@is_double2@ && !@is_native_half2@) + if (src_isnan || src_isinf) { + return NPY_SAME_VALUE_FAILURE; + } +# endif + /* 3. Check that the value can round trip exactly */ + if (!src_isnan && src_value != _ROUND_TRIP(src_value)) { + return NPY_SAME_VALUE_FAILURE; + } + return 0; +} +#endif + static GCC_CAST_OPT_LEVEL int @prefix@_cast_@name1@_to_@name2@( PyArrayMethod_Context *context, char *const *args, @@ -912,7 +974,7 @@ static GCC_CAST_OPT_LEVEL int assert(N == 0 || npy_is_aligned(dst, NPY_ALIGNOF(_TYPE2))); #endif - /*printf("@prefix@_cast_@name1@_to_@name2@\n");*/ + /* printf("@prefix@_cast_@name1@_to_@name2@\n"); */ #if !@is_bool2@ #if NPY_FEATURE_VERSION > NPY_2_3_API_VERSION int same_value_casting = ((context->flags & NPY_SAME_VALUE_CASTING) == NPY_SAME_VALUE_CASTING); @@ -937,7 +999,10 @@ static GCC_CAST_OPT_LEVEL int dst_value[0] = _CONVERT_FN(src_value[0]); dst_value[1] = _CONVERT_FN(src_value[1]); if (same_value_casting) { - if ((dst_value[0] != src_value[0]) || (dst_value[1] != src_value[1])){ + if (@prefix@_check_same_value_@name1@_to_@name2@(src_value[0]) == NPY_SAME_VALUE_FAILURE) { + return NPY_SAME_VALUE_FAILURE; + } + if (@prefix@_check_same_value_@name1@_to_@name2@(src_value[1]) == NPY_SAME_VALUE_FAILURE) { return NPY_SAME_VALUE_FAILURE; } } @@ -947,7 +1012,10 @@ static GCC_CAST_OPT_LEVEL int # else dst_value = _CONVERT_FN(src_value[0]); if (same_value_casting) { - if (dst_value != src_value[0]){ + if (@prefix@_check_same_value_@name1@_to_@name2@(src_value[0]) == NPY_SAME_VALUE_FAILURE) { + return NPY_SAME_VALUE_FAILURE; + } + if (src_value[1] != 0) { return NPY_SAME_VALUE_FAILURE; } } @@ -958,7 +1026,10 @@ static GCC_CAST_OPT_LEVEL int # else *(_TYPE2 *)dst = _CONVERT_FN(src_value[0]); if (same_value_casting) { - if (*(_TYPE2 *)dst != src_value[0]){ + if (@prefix@_check_same_value_@name1@_to_@name2@(src_value[0]) == NPY_SAME_VALUE_FAILURE) { + return NPY_SAME_VALUE_FAILURE; + } + if (src_value[1] != 0) { return NPY_SAME_VALUE_FAILURE; } } @@ -968,45 +1039,36 @@ static GCC_CAST_OPT_LEVEL int # if @is_complex2@ # if !@aligned@ dst_value[0] = _CONVERT_FN(src_value); + if (same_value_casting) { + if (@prefix@_check_same_value_@name1@_to_@name2@(src_value) == NPY_SAME_VALUE_FAILURE) { + return NPY_SAME_VALUE_FAILURE; + } + } # else dst_value[0] = _CONVERT_FN(*(_TYPE1 *)src); -# endif - dst_value[1] = 0; if (same_value_casting) { -# if !@aligned@ - if ((@rtype2@)dst_value[0] != src_value){ -# else - if ((@rtype2@)dst_value[0] != *((_TYPE1 *)src)){ -# endif + if (@prefix@_check_same_value_@name1@_to_@name2@(*(_TYPE1 *)src) == NPY_SAME_VALUE_FAILURE) { return NPY_SAME_VALUE_FAILURE; } } +# endif + dst_value[1] = 0; # elif !@aligned@ dst_value = _CONVERT_FN(src_value); # if !@is_bool2@ if (same_value_casting) { - if (_TO_RTYPE1(src_value) > @type2max@) { - return NPY_SAME_VALUE_FAILURE; - } -# if !@is_unsigned1@ - if (_TO_RTYPE1(src_value) < @type2min@) { + if (@prefix@_check_same_value_@name1@_to_@name2@(src_value) == NPY_SAME_VALUE_FAILURE) { return NPY_SAME_VALUE_FAILURE; } -# endif } # endif // @is_bool2@ # else *(_TYPE2 *)dst = _CONVERT_FN(*(_TYPE1 *)src); # if !@is_bool2@ if (same_value_casting) { - if (_TO_RTYPE1(*((_TYPE1 *)src)) > @type2max@) { - return NPY_SAME_VALUE_FAILURE; - } -# if !@is_unsigned1@ - if (_TO_RTYPE1(*((_TYPE1 *)src)) < @type2min@) { + if (@prefix@_check_same_value_@name1@_to_@name2@(*((_TYPE1 *)src)) == NPY_SAME_VALUE_FAILURE) { return NPY_SAME_VALUE_FAILURE; } -# endif } # endif // @is_bool2@ # endif diff --git a/numpy/_core/tests/test_casting_unittests.py b/numpy/_core/tests/test_casting_unittests.py index 7ce30003b4cb..23f29f2821cb 100644 --- a/numpy/_core/tests/test_casting_unittests.py +++ b/numpy/_core/tests/test_casting_unittests.py @@ -825,7 +825,7 @@ def test_nonstandard_bool_to_other(self, dtype): np.typecodes["AllInteger"] + np.typecodes["AllFloat"]) @pytest.mark.parametrize("from_dtype", np.typecodes["AllInteger"] + np.typecodes["AllFloat"]) - def test_same_value(self, from_dtype, to_dtype): + def test_same_value_overflow(self, from_dtype, to_dtype): if from_dtype == to_dtype: return top1 = 0 @@ -875,3 +875,59 @@ def test_same_value(self, from_dtype, to_dtype): warnings.simplefilter("always", RuntimeWarning) unaligned.astype(to_dtype, casting='same_value') assert len(w) < 2 + + @pytest.mark.parametrize("to_dtype", + np.typecodes["AllInteger"]) + @pytest.mark.parametrize("from_dtype", + np.typecodes["AllFloat"]) + @pytest.mark.filterwarnings("ignore::RuntimeWarning") + def test_same_value_float_to_int(self, from_dtype, to_dtype): + # Should not raise, since the values can round trip + arr1 = np.arange(10, dtype=from_dtype) + aligned = np.empty(arr1.itemsize * arr1.size + 1, 'uint8') + unaligned = aligned[1:].view(arr1.dtype) + unaligned[:] = arr1 + arr2 = np.arange(10, dtype=to_dtype) + assert_array_equal(arr1.astype(to_dtype, casting='same_value'), arr2) + assert_array_equal(unaligned.astype(to_dtype, casting='same_value'), arr2) + + # Should raise, since values cannot round trip + arr1_66 = arr1 + 0.666 + unaligned_66 = unaligned + 0.66 + with pytest.raises(ValueError): + arr1_66.astype(to_dtype, casting='same_value') + with pytest.raises(ValueError): + unaligned_66.astype(to_dtype, casting='same_value') + + @pytest.mark.parametrize("value", [np.nan, np.inf, -np.inf]) + @pytest.mark.filterwarnings("ignore::RuntimeWarning") + def test_same_value_naninf(self, value): + # These work + np.array([value], dtype=np.half).astype(np.cdouble, casting='same_value') + np.array([value], dtype=np.half).astype(np.double, casting='same_value') + np.array([value], dtype=np.float32).astype(np.cdouble, casting='same_value') + np.array([value], dtype=np.float32).astype(np.double, casting='same_value') + np.array([value], dtype=np.float32).astype(np.half, casting='same_value') + np.array([value], dtype=np.complex64).astype(np.half, casting='same_value') + # These fail + with pytest.raises(ValueError): + np.array([value], dtype=np.half).astype(np.int64, casting='same_value') + with pytest.raises(ValueError): + np.array([value], dtype=np.complex64).astype(np.int64, casting='same_value') + with pytest.raises(ValueError): + np.array([value], dtype=np.float32).astype(np.int64, casting='same_value') + + @pytest.mark.filterwarnings("ignore::RuntimeWarning") + def test_same_value_complex(self): + # This works + np.array([complex(1, 1)], dtype=np.cdouble).astype(np.complex64, casting='same_value') + # Casting with a non-zero imag part fails + with pytest.raises(ValueError): + np.array([complex(1, 1)], dtype=np.cdouble).astype(np.float32, casting='same_value') + + def _test_same_value_scalar(self): + # Not implemented yet + i = np.array(123, dtype=np.int64) + f = np.array(123, dtype=np.float64) + assert i.astype(np.float64, casting='same_value') == f + assert f.astype(np.int64, casting='same_value') == f From a323a4bea3805672b6f22c8abfcba1cb53e4d8ad Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Fri, 25 Jul 2025 05:47:37 +1000 Subject: [PATCH 16/29] enable astype same_value casting for scalars --- numpy/_core/src/multiarray/array_assign_scalar.c | 8 ++------ numpy/_core/tests/test_casting_unittests.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/numpy/_core/src/multiarray/array_assign_scalar.c b/numpy/_core/src/multiarray/array_assign_scalar.c index e4a62d3ac9e6..0e5a411c3653 100644 --- a/numpy/_core/src/multiarray/array_assign_scalar.c +++ b/numpy/_core/src/multiarray/array_assign_scalar.c @@ -87,9 +87,7 @@ raw_array_assign_scalar(int ndim, npy_intp const *shape, } if (casting == NPY_SAME_VALUE_CASTING) { - /* cast_info.context.flags |= NPY_SAME_VALUE_CASTING; */ - PyErr_SetString(PyExc_NotImplementedError, "'same_value' casting not implemented yet"); - return -1; + cast_info.context.flags |= NPY_SAME_VALUE_CASTING; } npy_intp strides[2] = {0, dst_strides_it[0]}; @@ -187,9 +185,7 @@ raw_array_wheremasked_assign_scalar(int ndim, npy_intp const *shape, NPY_BEGIN_THREADS_THRESHOLDED(nitems); } if (casting == NPY_SAME_VALUE_CASTING) { - /* cast_info.context.flags |= NPY_SAME_VALUE_CASTING; */ - PyErr_SetString(PyExc_NotImplementedError, "'same_value' casting not implemented yet"); - return -1; + cast_info.context.flags |= NPY_SAME_VALUE_CASTING; } npy_intp strides[2] = {0, dst_strides_it[0]}; diff --git a/numpy/_core/tests/test_casting_unittests.py b/numpy/_core/tests/test_casting_unittests.py index 23f29f2821cb..5646a1d910be 100644 --- a/numpy/_core/tests/test_casting_unittests.py +++ b/numpy/_core/tests/test_casting_unittests.py @@ -899,6 +899,21 @@ def test_same_value_float_to_int(self, from_dtype, to_dtype): with pytest.raises(ValueError): unaligned_66.astype(to_dtype, casting='same_value') + @pytest.mark.parametrize("to_dtype", + np.typecodes["AllInteger"]) + @pytest.mark.parametrize("from_dtype", + np.typecodes["AllFloat"]) + @pytest.mark.filterwarnings("ignore::RuntimeWarning") + def test_same_value_float_to_int_scalar(self, from_dtype, to_dtype): + # Should not raise, since the values can round trip + s1 = np.array(10, dtype=from_dtype) + assert s1.astype(to_dtype, casting='same_value') == 10 + + # Should raise, since values cannot round trip + s1_66 = s1 + 0.666 + with pytest.raises(ValueError): + s1_66.astype(to_dtype, casting='same_value') + @pytest.mark.parametrize("value", [np.nan, np.inf, -np.inf]) @pytest.mark.filterwarnings("ignore::RuntimeWarning") def test_same_value_naninf(self, value): From aec8ea1dc15d01497bfb78dbb760f653e910df8a Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Fri, 25 Jul 2025 06:51:09 +1000 Subject: [PATCH 17/29] typo --- numpy/_core/src/multiarray/lowlevel_strided_loops.c.src | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src index 10d1bf3d1482..d841fbd4e233 100644 --- a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src +++ b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src @@ -845,10 +845,10 @@ NPY_NO_EXPORT PyArrayMethod_StridedLoop * # if @is_float1@ # define _CONVERT_FN(x) npy_floatbits_to_halfbits(x) -# define _ROUND_TRIP(x) npy_halfbits_to_floatbits(_CONVERTFN(x)) +# define _ROUND_TRIP(x) npy_halfbits_to_floatbits(_CONVERT_FN(x)) # elif @is_double1@ # define _CONVERT_FN(x) npy_doublebits_to_halfbits(x) -# define _ROUND_TRIP(x) npy_halfbits_to_doublebits(_CONVERTFN(x)) +# define _ROUND_TRIP(x) npy_halfbits_to_doublebits(_CONVERT_FN(x)) # elif @is_emu_half1@ # define _CONVERT_FN(x) (x) # define _ROUND_TRIP(x) (x) From 26c0fa16b309530b5159d6595e24823c4556704e Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Fri, 25 Jul 2025 13:09:09 +1000 Subject: [PATCH 18/29] fix ptr-to-src_value -> value casting errors --- .../multiarray/lowlevel_strided_loops.c.src | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src index d841fbd4e233..ccc5be63fa6b 100644 --- a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src +++ b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src @@ -845,10 +845,10 @@ NPY_NO_EXPORT PyArrayMethod_StridedLoop * # if @is_float1@ # define _CONVERT_FN(x) npy_floatbits_to_halfbits(x) -# define _ROUND_TRIP(x) npy_halfbits_to_floatbits(_CONVERT_FN(x)) +# define _ROUND_TRIP(x) (@rtype1@)npy_halfbits_to_floatbits(_CONVERT_FN(x)) # elif @is_double1@ # define _CONVERT_FN(x) npy_doublebits_to_halfbits(x) -# define _ROUND_TRIP(x) npy_halfbits_to_doublebits(_CONVERT_FN(x)) +# define _ROUND_TRIP(x) (@rtype1@)npy_halfbits_to_doublebits(_CONVERT_FN(x)) # elif @is_emu_half1@ # define _CONVERT_FN(x) (x) # define _ROUND_TRIP(x) (x) @@ -898,10 +898,10 @@ NPY_NO_EXPORT PyArrayMethod_StridedLoop * * Check various modes of failure to accurately cast src_value to dst */ static GCC_CAST_OPT_LEVEL int -@prefix@_check_same_value_@name1@_to_@name2@(_TYPE1 src_value) { +@prefix@_check_same_value_@name1@_to_@name2@(@rtype1@ src_value) { - int src_isnan = isnan((double)_TO_RTYPE1(src_value)); - int src_isinf = isinf((double)_TO_RTYPE1(src_value)); + int src_isnan = isnan((long double)_TO_RTYPE1(src_value)); + int src_isinf = isinf((long double)_TO_RTYPE1(src_value)); /* * 1. Check that the src does not overflow the dst. * Ignore inf since it does not compare well @@ -917,11 +917,11 @@ static GCC_CAST_OPT_LEVEL int # pragma clang diagnostic ignored "-Wtautological-constant-out-of-range-compare" #endif if (!src_isnan && _TO_RTYPE1(src_value) > @type2max@) { - return NPY_SAME_VALUE_FAILURE; + return NPY_SAME_VALUE_FAILURE; } # if !@is_unsigned1@ if (!src_isnan && _TO_RTYPE1(src_value) < @type2min@) { - return NPY_SAME_VALUE_FAILURE; + return NPY_SAME_VALUE_FAILURE; } # endif # if __clang__ @@ -938,7 +938,7 @@ static GCC_CAST_OPT_LEVEL int } # endif /* 3. Check that the value can round trip exactly */ - if (!src_isnan && src_value != _ROUND_TRIP(src_value)) { + if (!src_isnan && !src_isinf && src_value != _ROUND_TRIP(src_value)) { return NPY_SAME_VALUE_FAILURE; } return 0; @@ -999,10 +999,10 @@ static GCC_CAST_OPT_LEVEL int dst_value[0] = _CONVERT_FN(src_value[0]); dst_value[1] = _CONVERT_FN(src_value[1]); if (same_value_casting) { - if (@prefix@_check_same_value_@name1@_to_@name2@(src_value[0]) == NPY_SAME_VALUE_FAILURE) { + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value[0]) == NPY_SAME_VALUE_FAILURE) { return NPY_SAME_VALUE_FAILURE; } - if (@prefix@_check_same_value_@name1@_to_@name2@(src_value[1]) == NPY_SAME_VALUE_FAILURE) { + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value[1]) == NPY_SAME_VALUE_FAILURE) { return NPY_SAME_VALUE_FAILURE; } } @@ -1012,7 +1012,7 @@ static GCC_CAST_OPT_LEVEL int # else dst_value = _CONVERT_FN(src_value[0]); if (same_value_casting) { - if (@prefix@_check_same_value_@name1@_to_@name2@(src_value[0]) == NPY_SAME_VALUE_FAILURE) { + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value[0]) == NPY_SAME_VALUE_FAILURE) { return NPY_SAME_VALUE_FAILURE; } if (src_value[1] != 0) { @@ -1026,7 +1026,7 @@ static GCC_CAST_OPT_LEVEL int # else *(_TYPE2 *)dst = _CONVERT_FN(src_value[0]); if (same_value_casting) { - if (@prefix@_check_same_value_@name1@_to_@name2@(src_value[0]) == NPY_SAME_VALUE_FAILURE) { + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value[0]) == NPY_SAME_VALUE_FAILURE) { return NPY_SAME_VALUE_FAILURE; } if (src_value[1] != 0) { @@ -1040,14 +1040,14 @@ static GCC_CAST_OPT_LEVEL int # if !@aligned@ dst_value[0] = _CONVERT_FN(src_value); if (same_value_casting) { - if (@prefix@_check_same_value_@name1@_to_@name2@(src_value) == NPY_SAME_VALUE_FAILURE) { + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value) == NPY_SAME_VALUE_FAILURE) { return NPY_SAME_VALUE_FAILURE; } } # else dst_value[0] = _CONVERT_FN(*(_TYPE1 *)src); if (same_value_casting) { - if (@prefix@_check_same_value_@name1@_to_@name2@(*(_TYPE1 *)src) == NPY_SAME_VALUE_FAILURE) { + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)src) == NPY_SAME_VALUE_FAILURE) { return NPY_SAME_VALUE_FAILURE; } } @@ -1057,7 +1057,7 @@ static GCC_CAST_OPT_LEVEL int dst_value = _CONVERT_FN(src_value); # if !@is_bool2@ if (same_value_casting) { - if (@prefix@_check_same_value_@name1@_to_@name2@(src_value) == NPY_SAME_VALUE_FAILURE) { + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value) == NPY_SAME_VALUE_FAILURE) { return NPY_SAME_VALUE_FAILURE; } } @@ -1066,7 +1066,7 @@ static GCC_CAST_OPT_LEVEL int *(_TYPE2 *)dst = _CONVERT_FN(*(_TYPE1 *)src); # if !@is_bool2@ if (same_value_casting) { - if (@prefix@_check_same_value_@name1@_to_@name2@(*((_TYPE1 *)src)) == NPY_SAME_VALUE_FAILURE) { + if (@prefix@_check_same_value_@name1@_to_@name2@(*((@rtype1@ *)src)) == NPY_SAME_VALUE_FAILURE) { return NPY_SAME_VALUE_FAILURE; } } @@ -1109,6 +1109,7 @@ static GCC_CAST_OPT_LEVEL int #undef _TYPE2 #undef _TYPE1 #undef _TO_RTYPE1 +#undef _ROUND_TRIP #endif From 0846081d7e959a820f88b8426862daff01ba2987 Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Fri, 25 Jul 2025 16:46:05 +1000 Subject: [PATCH 19/29] fix linting and docs, ignore warning better --- numpy/_core/_add_newdocs.py | 2 ++ .../src/multiarray/lowlevel_strided_loops.c.src | 12 ++++++------ numpy/_core/tests/test_casting_unittests.py | 5 +++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/numpy/_core/_add_newdocs.py b/numpy/_core/_add_newdocs.py index 0ef76712b657..6f2e94350d5e 100644 --- a/numpy/_core/_add_newdocs.py +++ b/numpy/_core/_add_newdocs.py @@ -3197,8 +3197,10 @@ * 'unsafe' means any data conversions may be done. * 'same_value' means any data conversions may be done, but the values must not change, including rounding of floats or overflow of ints + .. versionadded:: 2.4 Support for ``'same_value'`` was added. + subok : bool, optional If True, then sub-classes will be passed-through (default), otherwise the returned array will be forced to be a base-class array. diff --git a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src index ccc5be63fa6b..26c533d6c0d7 100644 --- a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src +++ b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src @@ -905,16 +905,16 @@ static GCC_CAST_OPT_LEVEL int /* * 1. Check that the src does not overflow the dst. * Ignore inf since it does not compare well - * This is complicated by clang warning that, for instance, int8 cannot + * This is complicated by a warning that, for instance, int8 cannot * overflow int64max */ #if (@is_float2@ || @is_emu_half2@ || @is_double2@ || @is_native_half2@) if (!src_isinf) { #endif # if !@is_bool1@ -# if __clang__ -# pragma clang diagnostic push -# pragma clang diagnostic ignored "-Wtautological-constant-out-of-range-compare" +# if defined(__clang__) || defined(__GUNC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wtautological-constant-out-of-range-compare" #endif if (!src_isnan && _TO_RTYPE1(src_value) > @type2max@) { return NPY_SAME_VALUE_FAILURE; @@ -924,8 +924,8 @@ static GCC_CAST_OPT_LEVEL int return NPY_SAME_VALUE_FAILURE; } # endif -# if __clang__ -# pragma clang diagnostic pop +# if defined(__clang__) || defined(__GUNC__) +# pragma GCC diagnostic pop # endif # endif # if (@is_float2@ || @is_emu_half2@ || @is_double2@ || @is_native_half2@) diff --git a/numpy/_core/tests/test_casting_unittests.py b/numpy/_core/tests/test_casting_unittests.py index 5646a1d910be..8dc8469921df 100644 --- a/numpy/_core/tests/test_casting_unittests.py +++ b/numpy/_core/tests/test_casting_unittests.py @@ -934,11 +934,12 @@ def test_same_value_naninf(self, value): @pytest.mark.filterwarnings("ignore::RuntimeWarning") def test_same_value_complex(self): + arr = np.array([complex(1, 1)], dtype=np.cdouble) # This works - np.array([complex(1, 1)], dtype=np.cdouble).astype(np.complex64, casting='same_value') + arr.astype(np.complex64, casting='same_value') # Casting with a non-zero imag part fails with pytest.raises(ValueError): - np.array([complex(1, 1)], dtype=np.cdouble).astype(np.float32, casting='same_value') + arr.astype(np.float32, casting='same_value') def _test_same_value_scalar(self): # Not implemented yet From 76e01c153aa6b1298970d34ff342e007af37f243 Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Fri, 25 Jul 2025 17:02:52 +1000 Subject: [PATCH 20/29] gcc warning is different --- numpy/_core/src/multiarray/lowlevel_strided_loops.c.src | 1 + 1 file changed, 1 insertion(+) diff --git a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src index 26c533d6c0d7..015c9ef16801 100644 --- a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src +++ b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src @@ -915,6 +915,7 @@ static GCC_CAST_OPT_LEVEL int # if defined(__clang__) || defined(__GUNC__) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wtautological-constant-out-of-range-compare" +# pragma GCC diagnostic ignored "-Wtautological-compare" #endif if (!src_isnan && _TO_RTYPE1(src_value) > @type2max@) { return NPY_SAME_VALUE_FAILURE; From 963ea05482648234972ad2efcd3ca0c873c7756e Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Sun, 27 Jul 2025 07:28:53 +1000 Subject: [PATCH 21/29] fixes from review, typos --- .../_core/src/multiarray/array_assign_array.c | 10 +------ numpy/_core/src/multiarray/conversion_utils.c | 27 ++++++++++++++++--- numpy/_core/src/multiarray/dtype_transfer.h | 2 -- numpy/_core/src/multiarray/einsum.c.src | 6 ----- .../multiarray/lowlevel_strided_loops.c.src | 14 ++++------ numpy/_core/src/multiarray/methods.c | 6 ++++- numpy/_core/src/multiarray/multiarraymodule.c | 11 -------- numpy/_core/src/umath/ufunc_object.c | 4 --- numpy/_core/tests/test_casting_unittests.py | 3 +-- numpy/_core/tests/test_conversion_utils.py | 4 ++- numpy/_core/tests/test_datetime.py | 2 +- numpy/_core/tests/test_einsum.py | 4 +-- numpy/_core/tests/test_shape_base.py | 2 +- 13 files changed, 43 insertions(+), 52 deletions(-) diff --git a/numpy/_core/src/multiarray/array_assign_array.c b/numpy/_core/src/multiarray/array_assign_array.c index ec23eb6b8f10..0a284b3ce9e9 100644 --- a/numpy/_core/src/multiarray/array_assign_array.c +++ b/numpy/_core/src/multiarray/array_assign_array.c @@ -132,12 +132,7 @@ raw_array_assign_array(int ndim, npy_intp const *shape, } if (same_value_cast) { - #if NPY_FEATURE_VERSION > NPY_2_3_API_VERSION cast_info.context.flags |= NPY_SAME_VALUE_CASTING; - #else - PyErr_SetString(PyExc_NotImplementedError, - "raw_array_assign_array with 'same_value' casting not implemented yet"); - #endif } /* Ensure number of elements exceeds threshold for threading */ @@ -248,10 +243,7 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, return -1; } if (same_value_cast) { - /* cast_info.context.flags |= NPY_SAME_VALUE_CASTING; */ - PyErr_SetString(PyExc_NotImplementedError, - "raw_array_wheremasked_assign_array with 'same_value' casting not implemented yet"); - return -1; + cast_info.context.flags |= NPY_SAME_VALUE_CASTING; } if (!(method_flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) { diff --git a/numpy/_core/src/multiarray/conversion_utils.c b/numpy/_core/src/multiarray/conversion_utils.c index e973ecb49faa..164aa2e4c8b4 100644 --- a/numpy/_core/src/multiarray/conversion_utils.c +++ b/numpy/_core/src/multiarray/conversion_utils.c @@ -911,7 +911,7 @@ PyArray_CorrelatemodeConverter(PyObject *object, NPY_CORRELATEMODE *val) } } -static int casting_parser(char const *str, Py_ssize_t length, void *data) +static int casting_parser_full(char const *str, Py_ssize_t length, void *data, int can_use_same_value) { NPY_CASTING *casting = (NPY_CASTING *)data; if (length < 2) { @@ -941,7 +941,7 @@ static int casting_parser(char const *str, Py_ssize_t length, void *data) *casting = NPY_SAME_KIND_CASTING; return 0; } - if (length == 10 && strcmp(str, "same_value") == 0) { + if (can_use_same_value && length == 10 && strcmp(str, "same_value") == 0) { *casting = NPY_SAME_VALUE_CASTING; return 0; } @@ -956,6 +956,11 @@ static int casting_parser(char const *str, Py_ssize_t length, void *data) return -1; } +static int casting_parser(char const *str, Py_ssize_t length, void *data) +{ + return casting_parser_full(str, length, data, 0); +} + /*NUMPY_API * Convert any Python object, *obj*, to an NPY_CASTING enum. */ @@ -965,10 +970,26 @@ PyArray_CastingConverter(PyObject *obj, NPY_CASTING *casting) return string_converter_helper( obj, (void *)casting, casting_parser, "casting", "must be one of 'no', 'equiv', 'safe', " - "'same_kind', 'unsafe', or 'same_value'"); + "'same_kind', 'unsafe'"); + return 0; +} + +static int casting_parser_same_value(char const *str, Py_ssize_t length, void *data) +{ + return casting_parser_full(str, length, data, 1); +} + +NPY_NO_EXPORT int +PyArray_CastingConverterSameValue(PyObject *obj, NPY_CASTING *casting) +{ + return string_converter_helper( + obj, (void *)casting, casting_parser_same_value, "casting", + "must be one of 'no', 'equiv', 'safe', " + "'same_kind', 'unsafe', 'same_value'"); return 0; } + /***************************** * Other conversion functions *****************************/ diff --git a/numpy/_core/src/multiarray/dtype_transfer.h b/numpy/_core/src/multiarray/dtype_transfer.h index 0da747291514..a354820e5d45 100644 --- a/numpy/_core/src/multiarray/dtype_transfer.h +++ b/numpy/_core/src/multiarray/dtype_transfer.h @@ -30,10 +30,8 @@ NPY_context_init(PyArrayMethod_Context *context, PyArray_Descr *descr[2]) { context->descriptors = descr; context->caller = NULL; - #if NPY_FEATURE_VERSION > NPY_2_3_API_VERSION context->_reserved = NULL; context->flags = 0; - #endif } /* diff --git a/numpy/_core/src/multiarray/einsum.c.src b/numpy/_core/src/multiarray/einsum.c.src index 6879fe1e02bd..3733c436cb1b 100644 --- a/numpy/_core/src/multiarray/einsum.c.src +++ b/numpy/_core/src/multiarray/einsum.c.src @@ -830,12 +830,6 @@ PyArray_EinsteinSum(char *subscripts, npy_intp nop, return NULL; } - if (casting == NPY_SAME_VALUE_CASTING) { - PyErr_SetString(PyExc_NotImplementedError, - "einsum with casting='same_value' not implemented yet"); - return NULL; - } - /* Parse the subscripts string into label_counts and op_labels */ memset(label_counts, 0, sizeof(label_counts)); for (iop = 0; iop < nop; ++iop) { diff --git a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src index 015c9ef16801..e0dc27363e1c 100644 --- a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src +++ b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src @@ -912,11 +912,11 @@ static GCC_CAST_OPT_LEVEL int if (!src_isinf) { #endif # if !@is_bool1@ -# if defined(__clang__) || defined(__GUNC__) +# ifdef __GNUC__ # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wtautological-constant-out-of-range-compare" # pragma GCC diagnostic ignored "-Wtautological-compare" -#endif +# endif if (!src_isnan && _TO_RTYPE1(src_value) > @type2max@) { return NPY_SAME_VALUE_FAILURE; } @@ -925,10 +925,10 @@ static GCC_CAST_OPT_LEVEL int return NPY_SAME_VALUE_FAILURE; } # endif -# if defined(__clang__) || defined(__GUNC__) +# ifdef __GNUC__ # pragma GCC diagnostic pop -# endif -# endif +# endif /* __GNUC__ */ +# endif /* !is_bool1 */ # if (@is_float2@ || @is_emu_half2@ || @is_double2@ || @is_native_half2@) } # endif @@ -977,11 +977,7 @@ static GCC_CAST_OPT_LEVEL int /* printf("@prefix@_cast_@name1@_to_@name2@\n"); */ #if !@is_bool2@ - #if NPY_FEATURE_VERSION > NPY_2_3_API_VERSION int same_value_casting = ((context->flags & NPY_SAME_VALUE_CASTING) == NPY_SAME_VALUE_CASTING); - #else - int same_value_casting = 0; - #endif #endif while (N--) { diff --git a/numpy/_core/src/multiarray/methods.c b/numpy/_core/src/multiarray/methods.c index c5fda08041c8..880d719bb98a 100644 --- a/numpy/_core/src/multiarray/methods.c +++ b/numpy/_core/src/multiarray/methods.c @@ -752,6 +752,10 @@ array_toscalar(PyArrayObject *self, PyObject *args) return PyArray_MultiIndexGetItem(self, multi_index); } + +NPY_NO_EXPORT int +PyArray_CastingConverterSameValue(PyObject *obj, NPY_CASTING *casting); + static PyObject * array_astype(PyArrayObject *self, PyObject *const *args, Py_ssize_t len_args, PyObject *kwnames) @@ -770,7 +774,7 @@ array_astype(PyArrayObject *self, if (npy_parse_arguments("astype", args, len_args, kwnames, "dtype", &PyArray_DTypeOrDescrConverterRequired, &dt_info, "|order", &PyArray_OrderConverter, &order, - "|casting", &PyArray_CastingConverter, &casting, + "|casting", &PyArray_CastingConverterSameValue, &casting, "|subok", &PyArray_PythonPyIntFromInt, &subok, "|copy", &PyArray_AsTypeCopyConverter, &forcecopy, NULL, NULL, NULL) < 0) { diff --git a/numpy/_core/src/multiarray/multiarraymodule.c b/numpy/_core/src/multiarray/multiarraymodule.c index 7d27fe5d729c..b8ade23b6d76 100644 --- a/numpy/_core/src/multiarray/multiarraymodule.c +++ b/numpy/_core/src/multiarray/multiarraymodule.c @@ -667,12 +667,6 @@ PyArray_ConcatenateInto(PyObject *op, "argument, but both were provided."); return NULL; } - if (casting == NPY_SAME_VALUE_CASTING) { - PyErr_SetString(PyExc_NotImplementedError, - "concatenate with casting='same_value' not implemented yet"); - return NULL; - } - /* Convert the input list into arrays */ Py_ssize_t narrays_true = PySequence_Size(op); @@ -1954,11 +1948,6 @@ array_copyto(PyObject *NPY_UNUSED(ignored), NULL, NULL, NULL) < 0) { goto fail; } - if (casting == NPY_SAME_VALUE_CASTING) { - PyErr_SetString(PyExc_NotImplementedError, - "array_copyto with 'same_value' casting not implemented yet"); - goto fail; - } if (!PyArray_Check(dst_obj)) { PyErr_Format(PyExc_TypeError, diff --git a/numpy/_core/src/umath/ufunc_object.c b/numpy/_core/src/umath/ufunc_object.c index 59e37e6a3e01..6b4ee70fec1e 100644 --- a/numpy/_core/src/umath/ufunc_object.c +++ b/numpy/_core/src/umath/ufunc_object.c @@ -4531,10 +4531,6 @@ ufunc_generic_fastcall(PyUFuncObject *ufunc, full_args.in, casting) < 0) { goto fail; } - if (casting == NPY_SAME_VALUE_CASTING) { - PyErr_SetString(PyExc_NotImplementedError, - "ufunc with 'same_value' casting not implemented yet"); - } /* * Do the final preparations and call the inner-loop. diff --git a/numpy/_core/tests/test_casting_unittests.py b/numpy/_core/tests/test_casting_unittests.py index 8dc8469921df..8613b9973fd3 100644 --- a/numpy/_core/tests/test_casting_unittests.py +++ b/numpy/_core/tests/test_casting_unittests.py @@ -941,8 +941,7 @@ def test_same_value_complex(self): with pytest.raises(ValueError): arr.astype(np.float32, casting='same_value') - def _test_same_value_scalar(self): - # Not implemented yet + def test_same_value_scalar(self): i = np.array(123, dtype=np.int64) f = np.array(123, dtype=np.float64) assert i.astype(np.float64, casting='same_value') == f diff --git a/numpy/_core/tests/test_conversion_utils.py b/numpy/_core/tests/test_conversion_utils.py index d63ca9e58df5..0ce3cdc2bc00 100644 --- a/numpy/_core/tests/test_conversion_utils.py +++ b/numpy/_core/tests/test_conversion_utils.py @@ -172,9 +172,11 @@ def test_valid(self): self._check("no", "NPY_NO_CASTING") self._check("equiv", "NPY_EQUIV_CASTING") self._check("safe", "NPY_SAFE_CASTING") - self._check("same_kind", "NPY_SAME_KIND_CASTING") self._check("unsafe", "NPY_UNSAFE_CASTING") + def test_invalid(self): + # Currently, 'same_value' is supported only in ndarray.astype + self._check_value_error("same_value") class TestIntpConverter: """ Tests of PyArray_IntpConverter """ diff --git a/numpy/_core/tests/test_datetime.py b/numpy/_core/tests/test_datetime.py index 2fae6889e008..54c569a31cae 100644 --- a/numpy/_core/tests/test_datetime.py +++ b/numpy/_core/tests/test_datetime.py @@ -1845,7 +1845,7 @@ def test_datetime_as_string(self): assert_equal(np.datetime_as_string(a, unit='D', casting='unsafe'), '2032-07-18') - with pytest.raises(TypeError): + with pytest.raises(ValueError): np.datetime_as_string(a, unit='Y', casting='same_value') assert_equal(np.datetime_as_string(a, unit='h'), '2032-07-18T12') diff --git a/numpy/_core/tests/test_einsum.py b/numpy/_core/tests/test_einsum.py index 6c0253f27792..13d147651cd7 100644 --- a/numpy/_core/tests/test_einsum.py +++ b/numpy/_core/tests/test_einsum.py @@ -79,9 +79,9 @@ def test_einsum_errors(self, do_opt, einsum_fn): b = np.ones((3, 4, 5)) einsum_fn('aabcb,abc', a, b) - with pytest.raises(NotImplementedError): + with pytest.raises(ValueError): a = np.arange(3) - # einsum_path does not accept kwarg 'casting' + # einsum_path does not yet accept kwarg 'casting' np.einsum('ij->j', [a, a], casting='same_value') def test_einsum_sorting_behavior(self): diff --git a/numpy/_core/tests/test_shape_base.py b/numpy/_core/tests/test_shape_base.py index cc2a0f5c77a1..87e2d864fc36 100644 --- a/numpy/_core/tests/test_shape_base.py +++ b/numpy/_core/tests/test_shape_base.py @@ -385,7 +385,7 @@ def test_concatenate(self): def test_concatenate_same_value(self): r4 = list(range(4)) - with pytest.raises(NotImplementedError): + with pytest.raises(ValueError): concatenate([r4, r4], casting="same_value") @pytest.mark.skipif( From 4a9a4988987e20d5da9d856b91c7cbed15b19645 Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Mon, 28 Jul 2025 09:46:59 +1000 Subject: [PATCH 22/29] fix compile warning ignore and make filter in tests more specific, disallow non-numeric 'same_value' --- .../_core/src/multiarray/array_assign_array.c | 12 ++++++++++++ .../multiarray/lowlevel_strided_loops.c.src | 18 ++++++++++-------- numpy/_core/tests/test_casting_unittests.py | 8 ++++---- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/numpy/_core/src/multiarray/array_assign_array.c b/numpy/_core/src/multiarray/array_assign_array.c index 0a284b3ce9e9..c6998c98a323 100644 --- a/numpy/_core/src/multiarray/array_assign_array.c +++ b/numpy/_core/src/multiarray/array_assign_array.c @@ -132,6 +132,12 @@ raw_array_assign_array(int ndim, npy_intp const *shape, } if (same_value_cast) { + if (!PyTypeNum_ISNUMBER(src_dtype->type_num) || !PyTypeNum_ISNUMBER(dst_dtype->type_num)) { + NPY_cast_info_xfree(&cast_info); + PyErr_SetString(PyExc_ValueError, + "'same_value' casting only supported on built-in numerical dtypes"); + return -1; + } cast_info.context.flags |= NPY_SAME_VALUE_CASTING; } @@ -243,6 +249,12 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, return -1; } if (same_value_cast) { + if (!PyTypeNum_ISNUMBER(src_dtype->type_num) || !PyTypeNum_ISNUMBER(dst_dtype->type_num)) { + NPY_cast_info_xfree(&cast_info); + PyErr_SetString(PyExc_ValueError, + "'same_value' casting only supported on built-in numerical dtypes"); + return -1; + } cast_info.context.flags |= NPY_SAME_VALUE_CASTING; } diff --git a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src index e0dc27363e1c..a6a61ce25f4b 100644 --- a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src +++ b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src @@ -908,15 +908,17 @@ static GCC_CAST_OPT_LEVEL int * This is complicated by a warning that, for instance, int8 cannot * overflow int64max */ +# ifdef __GNUC__ +# pragma GCC diagnostic push +# ifdef __clang__ +# pragma GCC diagnostic ignored "-Wtautological-constant-out-of-range-compare" +# endif +# pragma GCC diagnostic ignored "-Wtautological-compare" +# endif #if (@is_float2@ || @is_emu_half2@ || @is_double2@ || @is_native_half2@) if (!src_isinf) { #endif # if !@is_bool1@ -# ifdef __GNUC__ -# pragma GCC diagnostic push -# pragma GCC diagnostic ignored "-Wtautological-constant-out-of-range-compare" -# pragma GCC diagnostic ignored "-Wtautological-compare" -# endif if (!src_isnan && _TO_RTYPE1(src_value) > @type2max@) { return NPY_SAME_VALUE_FAILURE; } @@ -925,9 +927,6 @@ static GCC_CAST_OPT_LEVEL int return NPY_SAME_VALUE_FAILURE; } # endif -# ifdef __GNUC__ -# pragma GCC diagnostic pop -# endif /* __GNUC__ */ # endif /* !is_bool1 */ # if (@is_float2@ || @is_emu_half2@ || @is_double2@ || @is_native_half2@) } @@ -942,6 +941,9 @@ static GCC_CAST_OPT_LEVEL int if (!src_isnan && !src_isinf && src_value != _ROUND_TRIP(src_value)) { return NPY_SAME_VALUE_FAILURE; } +# ifdef __GNUC__ +# pragma GCC diagnostic pop +# endif /* __GNUC__ */ return 0; } #endif diff --git a/numpy/_core/tests/test_casting_unittests.py b/numpy/_core/tests/test_casting_unittests.py index 8613b9973fd3..813ed7ef321a 100644 --- a/numpy/_core/tests/test_casting_unittests.py +++ b/numpy/_core/tests/test_casting_unittests.py @@ -866,7 +866,7 @@ def test_same_value_overflow(self, from_dtype, to_dtype): # RuntimeWarning (fperror) # Casting with overflow and 'same_value', should raise ValueError with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always", RuntimeWarning) + warnings.simplefilter("always", ComplexWarning) arr1.astype(to_dtype, casting='same_value') assert len(w) < 2 with pytest.raises(ValueError): @@ -880,7 +880,7 @@ def test_same_value_overflow(self, from_dtype, to_dtype): np.typecodes["AllInteger"]) @pytest.mark.parametrize("from_dtype", np.typecodes["AllFloat"]) - @pytest.mark.filterwarnings("ignore::RuntimeWarning") + @pytest.mark.filterwarnings("ignore::ComplexWarning") def test_same_value_float_to_int(self, from_dtype, to_dtype): # Should not raise, since the values can round trip arr1 = np.arange(10, dtype=from_dtype) @@ -915,7 +915,7 @@ def test_same_value_float_to_int_scalar(self, from_dtype, to_dtype): s1_66.astype(to_dtype, casting='same_value') @pytest.mark.parametrize("value", [np.nan, np.inf, -np.inf]) - @pytest.mark.filterwarnings("ignore::RuntimeWarning") + @pytest.mark.filterwarnings("ignore::ComplexWarning") def test_same_value_naninf(self, value): # These work np.array([value], dtype=np.half).astype(np.cdouble, casting='same_value') @@ -932,7 +932,7 @@ def test_same_value_naninf(self, value): with pytest.raises(ValueError): np.array([value], dtype=np.float32).astype(np.int64, casting='same_value') - @pytest.mark.filterwarnings("ignore::RuntimeWarning") + @pytest.mark.filterwarnings("ignore::ComplexWarning") def test_same_value_complex(self): arr = np.array([complex(1, 1)], dtype=np.cdouble) # This works From 10c4493d4874f3f6c6bcc34f210e311e6c482a50 Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Mon, 28 Jul 2025 10:13:04 +1000 Subject: [PATCH 23/29] fix warning filters --- numpy/_core/tests/test_casting_unittests.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/numpy/_core/tests/test_casting_unittests.py b/numpy/_core/tests/test_casting_unittests.py index 813ed7ef321a..3ec0bd38e65d 100644 --- a/numpy/_core/tests/test_casting_unittests.py +++ b/numpy/_core/tests/test_casting_unittests.py @@ -825,6 +825,7 @@ def test_nonstandard_bool_to_other(self, dtype): np.typecodes["AllInteger"] + np.typecodes["AllFloat"]) @pytest.mark.parametrize("from_dtype", np.typecodes["AllInteger"] + np.typecodes["AllFloat"]) + @pytest.mark.filterwarnings("ignore::numpy.exceptions.ComplexWarning") def test_same_value_overflow(self, from_dtype, to_dtype): if from_dtype == to_dtype: return @@ -849,10 +850,7 @@ def test_same_value_overflow(self, from_dtype, to_dtype): # Happy path arr1 = np.array([0] * 10, dtype=from_dtype) arr2 = np.array([0] * 10, dtype=to_dtype) - with warnings.catch_warnings(record=True) as w: - # complex -> non-complex will warn - warnings.simplefilter("always", RuntimeWarning) - arr1_astype = arr1.astype(to_dtype, casting='same_value') + arr1_astype = arr1.astype(to_dtype, casting='same_value') assert_equal(arr1_astype, arr2, strict=True) # Make it overflow, both aligned and unaligned arr1[0] = top1 @@ -866,7 +864,7 @@ def test_same_value_overflow(self, from_dtype, to_dtype): # RuntimeWarning (fperror) # Casting with overflow and 'same_value', should raise ValueError with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always", ComplexWarning) + warnings.simplefilter("always", RuntimeWarning) arr1.astype(to_dtype, casting='same_value') assert len(w) < 2 with pytest.raises(ValueError): @@ -880,7 +878,7 @@ def test_same_value_overflow(self, from_dtype, to_dtype): np.typecodes["AllInteger"]) @pytest.mark.parametrize("from_dtype", np.typecodes["AllFloat"]) - @pytest.mark.filterwarnings("ignore::ComplexWarning") + @pytest.mark.filterwarnings("ignore::RuntimeWarning") def test_same_value_float_to_int(self, from_dtype, to_dtype): # Should not raise, since the values can round trip arr1 = np.arange(10, dtype=from_dtype) @@ -891,7 +889,8 @@ def test_same_value_float_to_int(self, from_dtype, to_dtype): assert_array_equal(arr1.astype(to_dtype, casting='same_value'), arr2) assert_array_equal(unaligned.astype(to_dtype, casting='same_value'), arr2) - # Should raise, since values cannot round trip + # Should raise, since values cannot round trip. Might warn too about + # FPE errors arr1_66 = arr1 + 0.666 unaligned_66 = unaligned + 0.66 with pytest.raises(ValueError): @@ -915,7 +914,7 @@ def test_same_value_float_to_int_scalar(self, from_dtype, to_dtype): s1_66.astype(to_dtype, casting='same_value') @pytest.mark.parametrize("value", [np.nan, np.inf, -np.inf]) - @pytest.mark.filterwarnings("ignore::ComplexWarning") + @pytest.mark.filterwarnings("ignore::numpy.exceptions.ComplexWarning") def test_same_value_naninf(self, value): # These work np.array([value], dtype=np.half).astype(np.cdouble, casting='same_value') @@ -932,7 +931,7 @@ def test_same_value_naninf(self, value): with pytest.raises(ValueError): np.array([value], dtype=np.float32).astype(np.int64, casting='same_value') - @pytest.mark.filterwarnings("ignore::ComplexWarning") + @pytest.mark.filterwarnings("ignore::numpy.exceptions.ComplexWarning") def test_same_value_complex(self): arr = np.array([complex(1, 1)], dtype=np.cdouble) # This works From 58a0a099b78a20d01cad22c9e64bd932a3663fd9 Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Thu, 31 Jul 2025 20:27:02 +1000 Subject: [PATCH 24/29] emit PyErr inside the loop --- .../_core/src/multiarray/array_assign_array.c | 2 - .../src/multiarray/array_assign_scalar.c | 2 - numpy/_core/src/multiarray/array_coercion.c | 1 - numpy/_core/src/multiarray/array_method.c | 8 --- numpy/_core/src/multiarray/array_method.h | 9 ---- numpy/_core/src/multiarray/ctors.c | 1 - numpy/_core/src/multiarray/dtype_transfer.c | 3 +- .../multiarray/lowlevel_strided_loops.c.src | 52 +++++++++++-------- numpy/_core/src/multiarray/mapping.c | 4 -- 9 files changed, 31 insertions(+), 51 deletions(-) diff --git a/numpy/_core/src/multiarray/array_assign_array.c b/numpy/_core/src/multiarray/array_assign_array.c index c6998c98a323..b21691313e01 100644 --- a/numpy/_core/src/multiarray/array_assign_array.c +++ b/numpy/_core/src/multiarray/array_assign_array.c @@ -180,7 +180,6 @@ raw_array_assign_array(int ndim, npy_intp const *shape, fail: NPY_END_THREADS; NPY_cast_info_xfree(&cast_info); - HandleArrayMethodError(result, "astype", method_flags); return -1; } @@ -303,7 +302,6 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, fail: NPY_END_THREADS; NPY_cast_info_xfree(&cast_info); - HandleArrayMethodError(result, "astype", method_flags); return -1; } diff --git a/numpy/_core/src/multiarray/array_assign_scalar.c b/numpy/_core/src/multiarray/array_assign_scalar.c index 0e5a411c3653..54d0f13c86af 100644 --- a/numpy/_core/src/multiarray/array_assign_scalar.c +++ b/numpy/_core/src/multiarray/array_assign_scalar.c @@ -118,7 +118,6 @@ raw_array_assign_scalar(int ndim, npy_intp const *shape, fail: NPY_END_THREADS; NPY_cast_info_xfree(&cast_info); - HandleArrayMethodError(result, "cast", flags); return -1; } @@ -223,7 +222,6 @@ raw_array_wheremasked_assign_scalar(int ndim, npy_intp const *shape, fail: NPY_END_THREADS; NPY_cast_info_xfree(&cast_info); - HandleArrayMethodError(result, "cast", flags); return -1; } diff --git a/numpy/_core/src/multiarray/array_coercion.c b/numpy/_core/src/multiarray/array_coercion.c index 0aa41221e0e7..854d53ab40d4 100644 --- a/numpy/_core/src/multiarray/array_coercion.c +++ b/numpy/_core/src/multiarray/array_coercion.c @@ -423,7 +423,6 @@ npy_cast_raw_scalar_item( args, &length, strides, cast_info.auxdata); if (result < 0) { NPY_cast_info_xfree(&cast_info); - HandleArrayMethodError(result, "cast", flags); return -1; } NPY_cast_info_xfree(&cast_info); diff --git a/numpy/_core/src/multiarray/array_method.c b/numpy/_core/src/multiarray/array_method.c index bf913cf5fd1b..32b32c822d51 100644 --- a/numpy/_core/src/multiarray/array_method.c +++ b/numpy/_core/src/multiarray/array_method.c @@ -986,11 +986,3 @@ NPY_NO_EXPORT PyTypeObject PyBoundArrayMethod_Type = { .tp_getset = boundarraymethods_getters, }; -int HandleArrayMethodError(int result, const char * name , int method_flags) -{ - if (result == NPY_SAME_VALUE_FAILURE) { - PyErr_Format(PyExc_ValueError, "'same_value' casting failure in %s", name); - } - return result; -} - diff --git a/numpy/_core/src/multiarray/array_method.h b/numpy/_core/src/multiarray/array_method.h index 3e87868f02d9..bcf270899f13 100644 --- a/numpy/_core/src/multiarray/array_method.h +++ b/numpy/_core/src/multiarray/array_method.h @@ -122,15 +122,6 @@ PyArrayMethod_FromSpec(PyArrayMethod_Spec *spec); NPY_NO_EXPORT PyBoundArrayMethodObject * PyArrayMethod_FromSpec_int(PyArrayMethod_Spec *spec, int priv); -/* - * Possible results to be handled in HandleArrayMethodError, after a call - * to a PyArrayMethod_StridedLoop - */ -#define NPY_GENERIC_LOOP_FAILURE -1 /* will have set a PyErr already */ -#define NPY_SAME_VALUE_FAILURE -31 /* same_value casting failed */ - -int HandleArrayMethodError(int result, const char * name , int method_flags); - #ifdef __cplusplus } #endif diff --git a/numpy/_core/src/multiarray/ctors.c b/numpy/_core/src/multiarray/ctors.c index 985b1d1369bb..c720d1f8e2d2 100644 --- a/numpy/_core/src/multiarray/ctors.c +++ b/numpy/_core/src/multiarray/ctors.c @@ -2854,7 +2854,6 @@ PyArray_CopyAsFlat(PyArrayObject *dst, PyArrayObject *src, NPY_ORDER order) res = -1; } if (result < 0) { - HandleArrayMethodError(result, "cast", flags); res = result; } diff --git a/numpy/_core/src/multiarray/dtype_transfer.c b/numpy/_core/src/multiarray/dtype_transfer.c index 49764f80cbc7..dbad10842aff 100644 --- a/numpy/_core/src/multiarray/dtype_transfer.c +++ b/numpy/_core/src/multiarray/dtype_transfer.c @@ -3431,7 +3431,6 @@ PyArray_CastRawArrays(npy_intp count, /* Cleanup */ NPY_cast_info_xfree(&cast_info); if (result < 0) { - HandleArrayMethodError(result, "cast", flags); return NPY_FAIL; } if (flags & NPY_METH_REQUIRES_PYAPI && PyErr_Occurred()) { @@ -3832,4 +3831,4 @@ PyArray_PrepareThreeRawArrayIter(int ndim, npy_intp const *shape, *out_dataC = dataC; *out_ndim = ndim; return 0; -} \ No newline at end of file +} diff --git a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src index a6a61ce25f4b..f5c126598dbd 100644 --- a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src +++ b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src @@ -25,6 +25,7 @@ #include "usertypes.h" #include "umathmodule.h" +#include "gil_utils.h" /* * x86 platform works with unaligned access but the compiler is allowed to @@ -893,6 +894,10 @@ NPY_NO_EXPORT PyArrayMethod_StridedLoop * #define GCC_CAST_OPT_LEVEL NPY_GCC_OPT_3 #endif +#define _RETURN_SAME_VALUE_FAILURE \ + npy_gil_error(PyExc_ValueError, "could not cast 'same_value' @name1@ to @name2@"); \ + return -1 + #if !@is_bool2@ /* * Check various modes of failure to accurately cast src_value to dst @@ -920,11 +925,11 @@ static GCC_CAST_OPT_LEVEL int #endif # if !@is_bool1@ if (!src_isnan && _TO_RTYPE1(src_value) > @type2max@) { - return NPY_SAME_VALUE_FAILURE; + _RETURN_SAME_VALUE_FAILURE; } # if !@is_unsigned1@ if (!src_isnan && _TO_RTYPE1(src_value) < @type2min@) { - return NPY_SAME_VALUE_FAILURE; + _RETURN_SAME_VALUE_FAILURE; } # endif # endif /* !is_bool1 */ @@ -934,12 +939,12 @@ static GCC_CAST_OPT_LEVEL int /* 2. Check inf, nan with a non-float dst */ # if (!@is_float2@ && !@is_emu_half2@ && !@is_double2@ && !@is_native_half2@) if (src_isnan || src_isinf) { - return NPY_SAME_VALUE_FAILURE; + _RETURN_SAME_VALUE_FAILURE; } # endif /* 3. Check that the value can round trip exactly */ if (!src_isnan && !src_isinf && src_value != _ROUND_TRIP(src_value)) { - return NPY_SAME_VALUE_FAILURE; + _RETURN_SAME_VALUE_FAILURE; } # ifdef __GNUC__ # pragma GCC diagnostic pop @@ -998,11 +1003,11 @@ static GCC_CAST_OPT_LEVEL int dst_value[0] = _CONVERT_FN(src_value[0]); dst_value[1] = _CONVERT_FN(src_value[1]); if (same_value_casting) { - if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value[0]) == NPY_SAME_VALUE_FAILURE) { - return NPY_SAME_VALUE_FAILURE; + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value[0]) < 0) { + return -1; } - if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value[1]) == NPY_SAME_VALUE_FAILURE) { - return NPY_SAME_VALUE_FAILURE; + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value[1]) < 0) { + return -1; } } # elif !@aligned@ @@ -1011,11 +1016,12 @@ static GCC_CAST_OPT_LEVEL int # else dst_value = _CONVERT_FN(src_value[0]); if (same_value_casting) { - if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value[0]) == NPY_SAME_VALUE_FAILURE) { - return NPY_SAME_VALUE_FAILURE; + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value[0]) < 0) { + return -1; } if (src_value[1] != 0) { - return NPY_SAME_VALUE_FAILURE; + npy_gil_error(PyExc_ValueError, "could not cast 'same_value' @name1@ to @name2@: imag is not 0"); + return -1; } } # endif @@ -1025,11 +1031,12 @@ static GCC_CAST_OPT_LEVEL int # else *(_TYPE2 *)dst = _CONVERT_FN(src_value[0]); if (same_value_casting) { - if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value[0]) == NPY_SAME_VALUE_FAILURE) { - return NPY_SAME_VALUE_FAILURE; + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value[0]) < 0) { + return -1; } if (src_value[1] != 0) { - return NPY_SAME_VALUE_FAILURE; + npy_gil_error(PyExc_ValueError, "could not cast 'same_value' @name1@ to @name2@: imag is not 0"); + return -1; } } # endif @@ -1039,15 +1046,15 @@ static GCC_CAST_OPT_LEVEL int # if !@aligned@ dst_value[0] = _CONVERT_FN(src_value); if (same_value_casting) { - if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value) == NPY_SAME_VALUE_FAILURE) { - return NPY_SAME_VALUE_FAILURE; + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value) < 0) { + return -1; } } # else dst_value[0] = _CONVERT_FN(*(_TYPE1 *)src); if (same_value_casting) { - if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)src) == NPY_SAME_VALUE_FAILURE) { - return NPY_SAME_VALUE_FAILURE; + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)src) < 0) { + return -1; } } # endif @@ -1056,8 +1063,8 @@ static GCC_CAST_OPT_LEVEL int dst_value = _CONVERT_FN(src_value); # if !@is_bool2@ if (same_value_casting) { - if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value) == NPY_SAME_VALUE_FAILURE) { - return NPY_SAME_VALUE_FAILURE; + if (@prefix@_check_same_value_@name1@_to_@name2@(*(@rtype1@ *)&src_value) < 0) { + return -1; } } # endif // @is_bool2@ @@ -1065,8 +1072,8 @@ static GCC_CAST_OPT_LEVEL int *(_TYPE2 *)dst = _CONVERT_FN(*(_TYPE1 *)src); # if !@is_bool2@ if (same_value_casting) { - if (@prefix@_check_same_value_@name1@_to_@name2@(*((@rtype1@ *)src)) == NPY_SAME_VALUE_FAILURE) { - return NPY_SAME_VALUE_FAILURE; + if (@prefix@_check_same_value_@name1@_to_@name2@(*((@rtype1@ *)src)) < 0) { + return -1; } } # endif // @is_bool2@ @@ -1109,6 +1116,7 @@ static GCC_CAST_OPT_LEVEL int #undef _TYPE1 #undef _TO_RTYPE1 #undef _ROUND_TRIP +#undef _RETURN_SAME_VALUE_FAILURE #endif diff --git a/numpy/_core/src/multiarray/mapping.c b/numpy/_core/src/multiarray/mapping.c index 4c327ca7c378..0229015f92aa 100644 --- a/numpy/_core/src/multiarray/mapping.c +++ b/numpy/_core/src/multiarray/mapping.c @@ -1241,9 +1241,6 @@ array_assign_boolean_subscript(PyArrayObject *self, } NPY_cast_info_xfree(&cast_info); - if (res < 0) { - HandleArrayMethodError(res, "cast", flags); - } if (!NpyIter_Deallocate(iter)) { res = -1; } @@ -2137,7 +2134,6 @@ array_assign_subscript(PyArrayObject *self, PyObject *ind, PyObject *op) */ int result = mapiter_set(mit, &cast_info, meth_flags, is_aligned); if (result < 0) { - HandleArrayMethodError(result, "cast", meth_flags); goto fail; } From 64b874712ca263ce26bbbc04acd4f12d35bc0a69 Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Fri, 1 Aug 2025 08:32:34 +1000 Subject: [PATCH 25/29] macOS can emit FPEs when touching NAN --- numpy/_core/tests/test_casting_unittests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/numpy/_core/tests/test_casting_unittests.py b/numpy/_core/tests/test_casting_unittests.py index 3ec0bd38e65d..8d5bc1f1aa34 100644 --- a/numpy/_core/tests/test_casting_unittests.py +++ b/numpy/_core/tests/test_casting_unittests.py @@ -915,8 +915,9 @@ def test_same_value_float_to_int_scalar(self, from_dtype, to_dtype): @pytest.mark.parametrize("value", [np.nan, np.inf, -np.inf]) @pytest.mark.filterwarnings("ignore::numpy.exceptions.ComplexWarning") + @pytest.mark.filterwarnings("ignore::RuntimeWarning") def test_same_value_naninf(self, value): - # These work + # These work, but may trigger FPE warnings on macOS np.array([value], dtype=np.half).astype(np.cdouble, casting='same_value') np.array([value], dtype=np.half).astype(np.double, casting='same_value') np.array([value], dtype=np.float32).astype(np.cdouble, casting='same_value') From 9d55847b384aaac00b248d9060ccdd95dcf863c0 Mon Sep 17 00:00:00 2001 From: Sebastian Berg Date: Fri, 13 Jun 2025 10:35:25 +0200 Subject: [PATCH 26/29] Fix can-cast logic everywhere for same-value casts (only allow numeric) --- numpy/_core/include/numpy/ndarraytypes.h | 11 +++++- numpy/_core/src/multiarray/array_method.c | 1 + numpy/_core/src/multiarray/convert_datatype.c | 39 +++++++++++++++---- numpy/_core/tests/test_casting_unittests.py | 11 +++++- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/numpy/_core/include/numpy/ndarraytypes.h b/numpy/_core/include/numpy/ndarraytypes.h index 972b25a7aa6e..445a75057b26 100644 --- a/numpy/_core/include/numpy/ndarraytypes.h +++ b/numpy/_core/include/numpy/ndarraytypes.h @@ -227,8 +227,15 @@ typedef enum { NPY_SAME_KIND_CASTING=3, /* Allow any casts */ NPY_UNSAFE_CASTING=4, - /* Allow any casts, check that no values overflow/change */ - NPY_SAME_VALUE_CASTING=5, + /* + * Allow any casts, check that no values overflow/change. For users + * we only accept same-value casting, but array methods (cast impls) + * we need to know if same-value is supported on a same-kind cast. + * Safe, equiv, and no-casts are assumed to always be same-value safe. + */ + _NPY_SAME_VALUE_CASTING_FLAG = 8, // Use one bit to indicate same-value + NPY_SAME_VALUE_SAME_KIND_CASTING=(_NPY_SAME_VALUE_CASTING_FLAG | NPY_SAME_KIND_CASTING), + NPY_SAME_VALUE_CASTING=(_NPY_SAME_VALUE_CASTING_FLAG | NPY_UNSAFE_CASTING), } NPY_CASTING; typedef enum { diff --git a/numpy/_core/src/multiarray/array_method.c b/numpy/_core/src/multiarray/array_method.c index 32b32c822d51..67d3a56aefbc 100644 --- a/numpy/_core/src/multiarray/array_method.c +++ b/numpy/_core/src/multiarray/array_method.c @@ -192,6 +192,7 @@ validate_spec(PyArrayMethod_Spec *spec) case NPY_SAME_KIND_CASTING: case NPY_UNSAFE_CASTING: case NPY_SAME_VALUE_CASTING: + case NPY_SAME_VALUE_SAME_KIND_CASTING: break; default: if (spec->casting != -1) { diff --git a/numpy/_core/src/multiarray/convert_datatype.c b/numpy/_core/src/multiarray/convert_datatype.c index 63f19e8d87f0..ca5905299a55 100644 --- a/numpy/_core/src/multiarray/convert_datatype.c +++ b/numpy/_core/src/multiarray/convert_datatype.c @@ -271,11 +271,29 @@ PyArray_MinCastSafety(NPY_CASTING casting1, NPY_CASTING casting2) if (casting1 < 0 || casting2 < 0) { return -1; } - /* larger casting values are less safe */ - if (casting1 > casting2) { + if ((casting1 & _NPY_SAME_VALUE_CASTING_FLAG) == (casting2 & _NPY_SAME_VALUE_CASTING_FLAG)) { + /* larger casting values are less safe, unless same-value mismatches */ + if (casting1 > casting2) { + return casting1; + } + return casting2; + } + else if (casting1 & _NPY_SAME_VALUE_CASTING_FLAG && casting2 <= NPY_SAFE_CASTING) { return casting1; } - return casting2; + else if (casting2 & _NPY_SAME_VALUE_CASTING_FLAG && casting1 <= NPY_SAFE_CASTING) { + return casting2; + } + else { + /* The min cast-safety isn't same-value compatible, so unset the flag. */ + casting1 &= ~_NPY_SAME_VALUE_CASTING_FLAG; + casting2 &= ~_NPY_SAME_VALUE_CASTING_FLAG; + /* with same-value casting out of the picture, use comparison */ + if (casting1 > casting2) { + return casting1; + } + return casting2; + } } @@ -2322,12 +2340,17 @@ add_numeric_cast(PyArray_DTypeMeta *from, PyArray_DTypeMeta *to) else if (_npy_can_cast_safely_table[from->type_num][to->type_num]) { spec.casting = NPY_SAFE_CASTING; } - else if (dtype_kind_to_ordering(dtypes[0]->singleton->kind) <= - dtype_kind_to_ordering(dtypes[1]->singleton->kind)) { - spec.casting = NPY_SAME_KIND_CASTING; - } else { - spec.casting = NPY_UNSAFE_CASTING; + if (dtype_kind_to_ordering(dtypes[0]->singleton->kind) <= + dtype_kind_to_ordering(dtypes[1]->singleton->kind)) { + spec.casting = NPY_SAME_KIND_CASTING; + } + else { + spec.casting = NPY_UNSAFE_CASTING; + } + if (from != &PyArray_BoolDType && to != &PyArray_BoolDType) { + spec.casting |= _NPY_SAME_VALUE_CASTING_FLAG; + } } /* Create a bound method, unbind and store it */ diff --git a/numpy/_core/tests/test_casting_unittests.py b/numpy/_core/tests/test_casting_unittests.py index 8d5bc1f1aa34..e3222c89bcb6 100644 --- a/numpy/_core/tests/test_casting_unittests.py +++ b/numpy/_core/tests/test_casting_unittests.py @@ -77,9 +77,12 @@ class Casting(enum.IntEnum): safe = 2 same_kind = 3 unsafe = 4 - same_value = 5 + same_value_same_kind = 8 | 3 + same_value = 8 | 4 +same_value_dtypes = tuple(type(np.dtype(c)) for c in "bhilqBHILQefdgFDG") + def _get_cancast_table(): table = textwrap.dedent(""" X ? b h i l q B H I L Q e f d g F D G S U V O M m @@ -119,6 +122,12 @@ def _get_cancast_table(): cancast[from_dt] = {} for to_dt, c in zip(dtypes, row[2::2]): cancast[from_dt][to_dt] = convert_cast[c] + # Of the types checked, numeric cast support same-value + if from_dt in same_value_dtypes and to_dt in same_value_dtypes: + if cancast[from_dt][to_dt] == Casting.unsafe: + cancast[from_dt][to_dt] = Casting.same_value + if cancast[from_dt][to_dt] == Casting.same_kind: + cancast[from_dt][to_dt] = Casting.same_value_same_kind return cancast From a800154b713a57af755444be5302d49c782ae555 Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Tue, 5 Aug 2025 10:09:08 +1000 Subject: [PATCH 27/29] reorder and simplify, from review --- .../multiarray/lowlevel_strided_loops.c.src | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src index f5c126598dbd..7f0373202346 100644 --- a/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src +++ b/numpy/_core/src/multiarray/lowlevel_strided_loops.c.src @@ -905,11 +905,18 @@ NPY_NO_EXPORT PyArrayMethod_StridedLoop * static GCC_CAST_OPT_LEVEL int @prefix@_check_same_value_@name1@_to_@name2@(@rtype1@ src_value) { - int src_isnan = isnan((long double)_TO_RTYPE1(src_value)); - int src_isinf = isinf((long double)_TO_RTYPE1(src_value)); + /* 1. NaN/Infs always work for float to float and otherwise never */ +#if (@is_float1@ || @is_emu_half1@ || @is_double1@ || @is_native_half1@) + if (!npy_isfinite(_TO_RTYPE1(src_value))) { +# if (@is_float2@ || @is_emu_half2@ || @is_double2@ || @is_native_half2@) + return 0; /* float to float can preserve NaN/Inf */ +# else + _RETURN_SAME_VALUE_FAILURE; /* cannot preserve NaN/Inf */ +# endif + } +#endif /* - * 1. Check that the src does not overflow the dst. - * Ignore inf since it does not compare well + * 2. Check that the src does not overflow the dst. * This is complicated by a warning that, for instance, int8 cannot * overflow int64max */ @@ -920,30 +927,18 @@ static GCC_CAST_OPT_LEVEL int # endif # pragma GCC diagnostic ignored "-Wtautological-compare" # endif -#if (@is_float2@ || @is_emu_half2@ || @is_double2@ || @is_native_half2@) - if (!src_isinf) { -#endif -# if !@is_bool1@ - if (!src_isnan && _TO_RTYPE1(src_value) > @type2max@) { - _RETURN_SAME_VALUE_FAILURE; - } -# if !@is_unsigned1@ - if (!src_isnan && _TO_RTYPE1(src_value) < @type2min@) { - _RETURN_SAME_VALUE_FAILURE; - } -# endif -# endif /* !is_bool1 */ -# if (@is_float2@ || @is_emu_half2@ || @is_double2@ || @is_native_half2@) - } -# endif - /* 2. Check inf, nan with a non-float dst */ -# if (!@is_float2@ && !@is_emu_half2@ && !@is_double2@ && !@is_native_half2@) - if (src_isnan || src_isinf) { +# if !@is_bool1@ + if (_TO_RTYPE1(src_value) > @type2max@) { _RETURN_SAME_VALUE_FAILURE; - } -# endif + } +# if !@is_unsigned1@ + if (_TO_RTYPE1(src_value) < @type2min@) { + _RETURN_SAME_VALUE_FAILURE; + } +# endif +# endif /* !is_bool1 */ /* 3. Check that the value can round trip exactly */ - if (!src_isnan && !src_isinf && src_value != _ROUND_TRIP(src_value)) { + if (src_value != _ROUND_TRIP(src_value)) { _RETURN_SAME_VALUE_FAILURE; } # ifdef __GNUC__ From 70d92bc510686d59008c7068d28682bd6b9c7d09 Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Tue, 5 Aug 2025 10:49:30 +1000 Subject: [PATCH 28/29] revert last commit and remove redundant checks --- numpy/_core/src/multiarray/array_assign_array.c | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/numpy/_core/src/multiarray/array_assign_array.c b/numpy/_core/src/multiarray/array_assign_array.c index b21691313e01..4a524558b68f 100644 --- a/numpy/_core/src/multiarray/array_assign_array.c +++ b/numpy/_core/src/multiarray/array_assign_array.c @@ -132,12 +132,6 @@ raw_array_assign_array(int ndim, npy_intp const *shape, } if (same_value_cast) { - if (!PyTypeNum_ISNUMBER(src_dtype->type_num) || !PyTypeNum_ISNUMBER(dst_dtype->type_num)) { - NPY_cast_info_xfree(&cast_info); - PyErr_SetString(PyExc_ValueError, - "'same_value' casting only supported on built-in numerical dtypes"); - return -1; - } cast_info.context.flags |= NPY_SAME_VALUE_CASTING; } @@ -248,12 +242,6 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape, return -1; } if (same_value_cast) { - if (!PyTypeNum_ISNUMBER(src_dtype->type_num) || !PyTypeNum_ISNUMBER(dst_dtype->type_num)) { - NPY_cast_info_xfree(&cast_info); - PyErr_SetString(PyExc_ValueError, - "'same_value' casting only supported on built-in numerical dtypes"); - return -1; - } cast_info.context.flags |= NPY_SAME_VALUE_CASTING; } From 3549d66949e3e188925b7e3cc3f4ba8a5c784e8a Mon Sep 17 00:00:00 2001 From: Matti Picus Date: Wed, 6 Aug 2025 10:34:13 +1000 Subject: [PATCH 29/29] gate and document SAME_VALUE_CASTING for v2.4 --- doc/source/reference/c-api/array.rst | 2 ++ numpy/_core/include/numpy/ndarraytypes.h | 2 ++ 2 files changed, 4 insertions(+) diff --git a/doc/source/reference/c-api/array.rst b/doc/source/reference/c-api/array.rst index 9996f977ad7e..13fa24960b16 100644 --- a/doc/source/reference/c-api/array.rst +++ b/doc/source/reference/c-api/array.rst @@ -4457,5 +4457,7 @@ Enumerated Types Allow any cast, but error if any values change during the cast. Currently supported only in ``ndarray.astype(... casting='same_value')`` + .. versionadded:: 2.4 + .. index:: pair: ndarray; C-API diff --git a/numpy/_core/include/numpy/ndarraytypes.h b/numpy/_core/include/numpy/ndarraytypes.h index 445a75057b26..a0cd4d454310 100644 --- a/numpy/_core/include/numpy/ndarraytypes.h +++ b/numpy/_core/include/numpy/ndarraytypes.h @@ -227,6 +227,7 @@ typedef enum { NPY_SAME_KIND_CASTING=3, /* Allow any casts */ NPY_UNSAFE_CASTING=4, +#if NPY_FEATURE_VERSION >= NPY_2_4_API_VERSION /* * Allow any casts, check that no values overflow/change. For users * we only accept same-value casting, but array methods (cast impls) @@ -236,6 +237,7 @@ typedef enum { _NPY_SAME_VALUE_CASTING_FLAG = 8, // Use one bit to indicate same-value NPY_SAME_VALUE_SAME_KIND_CASTING=(_NPY_SAME_VALUE_CASTING_FLAG | NPY_SAME_KIND_CASTING), NPY_SAME_VALUE_CASTING=(_NPY_SAME_VALUE_CASTING_FLAG | NPY_UNSAFE_CASTING), +#endif } NPY_CASTING; typedef enum { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy