Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: Add floating point error checking to (almost) all casts #21437

Merged
merged 20 commits into from Jun 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8735980
ENH: Check floating point error flags for all casts
seberg Apr 1, 2022
341d988
BUG: Avoid FPE for float NaN to datetime/timedelta casts
seberg Apr 1, 2022
84fd4a5
TST: Fixup tests that cause FPEs during casts
seberg Apr 3, 2022
35dae70
ENH: Add overflow check to float setitem
seberg Apr 4, 2022
54d6ac5
WIP,TST: Add exhaustive test for FPEs in casts
seberg Apr 5, 2022
d53fe91
WIP: Threads things into nditer for ufuncs and advanced indexing
seberg Apr 6, 2022
4c2a3d0
fixups
seberg Apr 17, 2022
a5bf28b
ENH: Add floating point error handling to advanced indexing
seberg Apr 21, 2022
96fd8ba
BUG: Fix invalid access due to `iter` already being cleaned up
seberg May 4, 2022
fc865d8
TST: Fix the weird boolean+advanced indexing test
seberg May 4, 2022
219697a
TST: Fixup test name and check for ppc64le
seberg May 4, 2022
9c74efa
BUG: Fix needs_api for avanced assignments without subspace and remov…
seberg May 4, 2022
d41f1de
BUG: Add volatile to float -> datetime casts to work around compiler …
seberg May 4, 2022
8d2d9a5
TST: Add type check to not trigger an invalid FPE within PyPy
seberg May 4, 2022
b463a7f
TST: Improve test coverage for complex FPE cast errors
seberg Jun 1, 2022
63fa74a
BUG: Avoid `NpyIter_EnableExternalLoop` as it resets the buffer
seberg Jun 2, 2022
a7e58a7
TST: Enable inteer array assignment test and add flat test
seberg Jun 2, 2022
f7b0493
TST: Add final set of cast (and FPE in cast) test to ufuncs
seberg Jun 6, 2022
7e6d8ef
DOC: Add a release note on the floating point errors in cast change
seberg Jun 6, 2022
a546ee1
TST: Remove FPE `fill` special case after rebase
seberg Jun 13, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
33 changes: 33 additions & 0 deletions doc/release/upcoming_changes/21437.improvement.rst
@@ -0,0 +1,33 @@
NumPy now gives floating point errors in casts
----------------------------------------------

In most cases, NumPy previously did not give floating point
warnings or errors when these happened during casts.
For examples, casts like::

np.array([2e300]).astype(np.float32) # overflow for float32
np.array([np.inf]).astype(np.int64)

Should now generally give floating point warnings. These warnings
should warn that floating point overflow occurred.
seberg marked this conversation as resolved.
Show resolved Hide resolved
For errors when converting floating point values to integers users
should expect invalid value warnings.
seberg marked this conversation as resolved.
Show resolved Hide resolved

Users can modify the behavior of these warnings using `np.errstate`.

Note that for float to int casts, the exact warnings that are given may
be platform dependend. For example::

arr = np.full(100, value=1000, dtype=np.float64)
arr.astype(np.int8)

May give a result equivalent to (the intermediat means no warning is given)::

arr.astype(np.int64).astype(np.int8)

May may return an undefined result, with a warning set::

RuntimeWarning: invalid value encountered in cast

seberg marked this conversation as resolved.
Show resolved Hide resolved
The precise behavior if subject to the C99 standard and its implementation
seberg marked this conversation as resolved.
Show resolved Hide resolved
in both software and hardware.
5 changes: 4 additions & 1 deletion numpy/core/include/numpy/ndarraytypes.h
Expand Up @@ -1380,7 +1380,10 @@ typedef struct {
int nd_fancy;
npy_intp fancy_dims[NPY_MAXDIMS];

/* Whether the iterator (any of the iterators) requires API */
/*
* Whether the iterator (any of the iterators) requires API. This is
* unused by NumPy itself; ArrayMethod flags are more precise.
*/
int needs_api;

/*
Expand Down
16 changes: 10 additions & 6 deletions numpy/core/src/common/lowlevel_strided_loops.h
Expand Up @@ -196,7 +196,7 @@ PyArray_GetDTypeTransferFunction(int aligned,
PyArray_Descr *src_dtype, PyArray_Descr *dst_dtype,
int move_references,
NPY_cast_info *cast_info,
int *out_needs_api);
NPY_ARRAYMETHOD_FLAGS *out_flags);

NPY_NO_EXPORT int
get_fields_transfer_function(int aligned,
Expand All @@ -205,7 +205,7 @@ get_fields_transfer_function(int aligned,
int move_references,
PyArrayMethod_StridedLoop **out_stransfer,
NpyAuxData **out_transferdata,
int *out_needs_api);
NPY_ARRAYMETHOD_FLAGS *out_flags);

NPY_NO_EXPORT int
get_subarray_transfer_function(int aligned,
Expand All @@ -214,7 +214,7 @@ get_subarray_transfer_function(int aligned,
int move_references,
PyArrayMethod_StridedLoop **out_stransfer,
NpyAuxData **out_transferdata,
int *out_needs_api);
NPY_ARRAYMETHOD_FLAGS *out_flags);

/*
* This is identical to PyArray_GetDTypeTransferFunction, but returns a
Expand All @@ -241,7 +241,7 @@ PyArray_GetMaskedDTypeTransferFunction(int aligned,
PyArray_Descr *mask_dtype,
int move_references,
NPY_cast_info *cast_info,
int *out_needs_api);
NPY_ARRAYMETHOD_FLAGS *out_flags);

/*
* Casts the specified number of elements from 'src' with data type
Expand Down Expand Up @@ -336,10 +336,14 @@ mapiter_trivial_set(PyArrayObject *self, PyArrayObject *ind,
PyArrayObject *result);

NPY_NO_EXPORT int
mapiter_get(PyArrayMapIterObject *mit);
mapiter_get(
PyArrayMapIterObject *mit, NPY_cast_info *cast_info,
NPY_ARRAYMETHOD_FLAGS flags, int is_aligned);

NPY_NO_EXPORT int
mapiter_set(PyArrayMapIterObject *mit);
mapiter_set(
PyArrayMapIterObject *mit, NPY_cast_info *cast_info,
NPY_ARRAYMETHOD_FLAGS flags, int is_aligned);

/*
* Prepares shape and strides for a simple raw array iteration.
Expand Down
6 changes: 6 additions & 0 deletions numpy/core/src/common/umathmodule.h
Expand Up @@ -7,8 +7,14 @@
NPY_NO_EXPORT PyObject *
get_sfloat_dtype(PyObject *NPY_UNUSED(mod), PyObject *NPY_UNUSED(args));

/* Defined in umath/extobj.c */
NPY_NO_EXPORT int
PyUFunc_GiveFloatingpointErrors(const char *name, int fpe_errors);

PyObject * add_newdoc_ufunc(PyObject *NPY_UNUSED(dummy), PyObject *args);
PyObject * ufunc_frompyfunc(PyObject *NPY_UNUSED(dummy), PyObject *args, PyObject *NPY_UNUSED(kwds));


int initumath(PyObject *m);

#endif /* NUMPY_CORE_SRC_COMMON_UMATHMODULE_H_ */
49 changes: 41 additions & 8 deletions numpy/core/src/multiarray/array_assign_array.c
Expand Up @@ -8,11 +8,13 @@
*/
#define NPY_NO_DEPRECATED_API NPY_API_VERSION
#define _MULTIARRAYMODULE
#define _UMATHMODULE

#define PY_SSIZE_T_CLEAN
#include <Python.h>

#include "numpy/ndarraytypes.h"
#include "numpy/npy_math.h"

#include "npy_config.h"
#include "npy_pycompat.h"
Expand All @@ -25,6 +27,8 @@
#include "array_assign.h"
#include "dtype_transfer.h"

#include "umathmodule.h"

/*
* Check that array data is both uint-aligned and true-aligned for all array
* elements, as required by the copy/casting code in lowlevel_strided_loops.c
Expand Down Expand Up @@ -83,7 +87,7 @@ raw_array_assign_array(int ndim, npy_intp const *shape,
npy_intp src_strides_it[NPY_MAXDIMS];
npy_intp coord[NPY_MAXDIMS];

int aligned, needs_api = 0;
int aligned;

NPY_BEGIN_THREADS_DEF;

Expand Down Expand Up @@ -116,15 +120,19 @@ 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;
if (PyArray_GetDTypeTransferFunction(aligned,
src_strides_it[0], dst_strides_it[0],
src_dtype, dst_dtype,
0,
&cast_info, &needs_api) != NPY_SUCCEED) {
&cast_info, &flags) != NPY_SUCCEED) {
return -1;
}

if (!needs_api) {
if (!(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) {
npy_clear_floatstatus_barrier(src_data);
}
if (!(flags & NPY_METH_REQUIRES_PYAPI)) {
NPY_BEGIN_THREADS;
}

Expand All @@ -143,6 +151,14 @@ 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)) {
int fpes = npy_get_floatstatus_barrier(src_data);
if (fpes && PyUFunc_GiveFloatingpointErrors("cast", fpes) < 0) {
return -1;
}
}

return 0;
fail:
NPY_END_THREADS;
Expand Down Expand Up @@ -170,7 +186,7 @@ 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, needs_api = 0;
int aligned;

NPY_BEGIN_THREADS_DEF;

Expand Down Expand Up @@ -207,17 +223,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;
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, &needs_api) != NPY_SUCCEED) {
&cast_info, &flags) != NPY_SUCCEED) {
return -1;
}

if (!needs_api) {
if (!(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) {
npy_clear_floatstatus_barrier(src_data);
}
if (!(flags & NPY_METH_REQUIRES_PYAPI)) {
NPY_BEGIN_THREADS;
}
npy_intp strides[2] = {src_strides_it[0], dst_strides_it[0]};
Expand All @@ -232,7 +252,7 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape,
args, &shape_it[0], strides,
(npy_bool *)wheremask_data, wheremask_strides_it[0],
cast_info.auxdata) < 0) {
break;
goto fail;
}
} NPY_RAW_ITER_THREE_NEXT(idim, ndim, coord, shape_it,
dst_data, dst_strides_it,
Expand All @@ -241,7 +261,20 @@ raw_array_wheremasked_assign_array(int ndim, npy_intp const *shape,

NPY_END_THREADS;
NPY_cast_info_xfree(&cast_info);
return (needs_api && PyErr_Occurred()) ? -1 : 0;

if (!(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) {
int fpes = npy_get_floatstatus_barrier(src_data);
if (fpes && PyUFunc_GiveFloatingpointErrors("cast", fpes) < 0) {
return -1;
}
}

return 0;

fail:
NPY_END_THREADS;
NPY_cast_info_xfree(&cast_info);
return -1;
}

/*
Expand Down
49 changes: 41 additions & 8 deletions numpy/core/src/multiarray/array_assign_scalar.c
Expand Up @@ -8,11 +8,13 @@
*/
#define NPY_NO_DEPRECATED_API NPY_API_VERSION
#define _MULTIARRAYMODULE
#define _UMATHMODULE

#define PY_SSIZE_T_CLEAN
#include <Python.h>

#include <numpy/ndarraytypes.h>
#include "numpy/npy_math.h"

#include "npy_config.h"
#include "npy_pycompat.h"
Expand All @@ -25,6 +27,8 @@
#include "array_assign.h"
#include "dtype_transfer.h"

#include "umathmodule.h"

/*
* Assigns the scalar value to every element of the destination raw array.
*
Expand All @@ -39,7 +43,7 @@ raw_array_assign_scalar(int ndim, npy_intp const *shape,
npy_intp shape_it[NPY_MAXDIMS], dst_strides_it[NPY_MAXDIMS];
npy_intp coord[NPY_MAXDIMS];

int aligned, needs_api = 0;
int aligned;

NPY_BEGIN_THREADS_DEF;

Expand All @@ -62,15 +66,19 @@ raw_array_assign_scalar(int ndim, npy_intp const *shape,

/* Get the function to do the casting */
NPY_cast_info cast_info;
NPY_ARRAYMETHOD_FLAGS flags;
if (PyArray_GetDTypeTransferFunction(aligned,
0, dst_strides_it[0],
src_dtype, dst_dtype,
0,
&cast_info, &needs_api) != NPY_SUCCEED) {
&cast_info, &flags) != NPY_SUCCEED) {
return -1;
}

if (!needs_api) {
if (!(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) {
npy_clear_floatstatus_barrier(src_data);
}
if (!(flags & NPY_METH_REQUIRES_PYAPI)) {
npy_intp nitems = 1, i;
for (i = 0; i < ndim; i++) {
nitems *= shape_it[i];
Expand All @@ -92,6 +100,14 @@ raw_array_assign_scalar(int ndim, npy_intp const *shape,

NPY_END_THREADS;
NPY_cast_info_xfree(&cast_info);

if (!(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) {
int fpes = npy_get_floatstatus_barrier(src_data);
if (fpes && PyUFunc_GiveFloatingpointErrors("cast", fpes) < 0) {
return -1;
}
}

return 0;
fail:
NPY_END_THREADS;
Expand All @@ -117,7 +133,7 @@ raw_array_wheremasked_assign_scalar(int ndim, npy_intp const *shape,
npy_intp wheremask_strides_it[NPY_MAXDIMS];
npy_intp coord[NPY_MAXDIMS];

int aligned, needs_api = 0;
int aligned;

NPY_BEGIN_THREADS_DEF;

Expand All @@ -142,15 +158,19 @@ raw_array_wheremasked_assign_scalar(int ndim, npy_intp const *shape,

/* Get the function to do the casting */
NPY_cast_info cast_info;
NPY_ARRAYMETHOD_FLAGS flags;
if (PyArray_GetMaskedDTypeTransferFunction(aligned,
0, dst_strides_it[0], wheremask_strides_it[0],
src_dtype, dst_dtype, wheremask_dtype,
0,
&cast_info, &needs_api) != NPY_SUCCEED) {
&cast_info, &flags) != NPY_SUCCEED) {
return -1;
}

if (!needs_api) {
if (!(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) {
npy_clear_floatstatus_barrier(src_data);
}
if (!(flags & NPY_METH_REQUIRES_PYAPI)) {
npy_intp nitems = 1, i;
for (i = 0; i < ndim; i++) {
nitems *= shape_it[i];
Expand All @@ -170,15 +190,28 @@ raw_array_wheremasked_assign_scalar(int ndim, npy_intp const *shape,
args, &shape_it[0], strides,
(npy_bool *)wheremask_data, wheremask_strides_it[0],
cast_info.auxdata) < 0) {
break;
goto fail;
}
} NPY_RAW_ITER_TWO_NEXT(idim, ndim, coord, shape_it,
dst_data, dst_strides_it,
wheremask_data, wheremask_strides_it);

NPY_END_THREADS;
NPY_cast_info_xfree(&cast_info);
return (needs_api && PyErr_Occurred()) ? -1 : 0;

if (!(flags & NPY_METH_NO_FLOATINGPOINT_ERRORS)) {
int fpes = npy_get_floatstatus_barrier(src_data);
if (fpes && PyUFunc_GiveFloatingpointErrors("cast", fpes) < 0) {
return -1;
}
}

return 0;

fail:
NPY_END_THREADS;
NPY_cast_info_xfree(&cast_info);
return -1;
}

/*
Expand Down