summaryrefslogtreecommitdiff
path: root/numpy
diff options
context:
space:
mode:
authorMatti Picus <matti.picus@gmail.com>2022-03-22 03:39:45 -0600
committerGitHub <noreply@github.com>2022-03-22 03:39:45 -0600
commit982fcd381b71dc6668ad7841cd51dd6061d10c07 (patch)
treeca3267ad146211e37163217619f5a189e82b39a3 /numpy
parentd29f8b8b14bae94092dd29e8130e9545ff862f86 (diff)
parentdfaebc1e2e8ead4096bc131ef93008367ed4a93c (diff)
downloadnumpy-982fcd381b71dc6668ad7841cd51dd6061d10c07.tar.gz
Merge pull request #21218 from seberg/issue21211
BUG: Use -0. as initial value for summation (internal only)
Diffstat (limited to 'numpy')
-rw-r--r--numpy/core/src/umath/loops_utils.h.src10
-rw-r--r--numpy/core/tests/test_ufunc.py54
2 files changed, 61 insertions, 3 deletions
diff --git a/numpy/core/src/umath/loops_utils.h.src b/numpy/core/src/umath/loops_utils.h.src
index 762e9ee59..df92bc315 100644
--- a/numpy/core/src/umath/loops_utils.h.src
+++ b/numpy/core/src/umath/loops_utils.h.src
@@ -79,7 +79,11 @@ static NPY_INLINE @type@
{
if (n < 8) {
npy_intp i;
- @type@ res = 0.;
+ /*
+ * Start with -0 to preserve -0 values. The reason is that summing
+ * only -0 should return -0, but `0 + -0 == 0` while `-0 + -0 == -0`.
+ */
+ @type@ res = -0.0;
for (i = 0; i < n; i++) {
res += @trf@(*((@dtype@*)(a + i * stride)));
@@ -156,8 +160,8 @@ static NPY_INLINE void
if (n < 8) {
npy_intp i;
- *rr = 0.;
- *ri = 0.;
+ *rr = -0.0;
+ *ri = -0.0;
for (i = 0; i < n; i += 2) {
*rr += *((@ftype@ *)(a + i * stride + 0));
*ri += *((@ftype@ *)(a + i * stride + sizeof(@ftype@)));
diff --git a/numpy/core/tests/test_ufunc.py b/numpy/core/tests/test_ufunc.py
index f13c2667d..cb0f7bcbd 100644
--- a/numpy/core/tests/test_ufunc.py
+++ b/numpy/core/tests/test_ufunc.py
@@ -2507,3 +2507,57 @@ def test_ufunc_methods_floaterrors(method):
with np.errstate(all="raise"):
with pytest.raises(FloatingPointError):
method(arr)
+
+
+def _check_neg_zero(value):
+ if value != 0.0:
+ return False
+ if not np.signbit(value.real):
+ return False
+ if value.dtype.kind == "c":
+ return np.signbit(value.imag)
+ return True
+
+@pytest.mark.parametrize("dtype", np.typecodes["AllFloat"])
+def test_addition_negative_zero(dtype):
+ dtype = np.dtype(dtype)
+ if dtype.kind == "c":
+ neg_zero = dtype.type(complex(-0.0, -0.0))
+ else:
+ neg_zero = dtype.type(-0.0)
+
+ arr = np.array(neg_zero)
+ arr2 = np.array(neg_zero)
+
+ assert _check_neg_zero(arr + arr2)
+ # In-place ops may end up on a different path (reduce path) see gh-21211
+ arr += arr2
+ assert _check_neg_zero(arr)
+
+
+@pytest.mark.parametrize("dtype", np.typecodes["AllFloat"])
+@pytest.mark.parametrize("use_initial", [True, False])
+def test_addition_reduce_negative_zero(dtype, use_initial):
+ dtype = np.dtype(dtype)
+ if dtype.kind == "c":
+ neg_zero = dtype.type(complex(-0.0, -0.0))
+ else:
+ neg_zero = dtype.type(-0.0)
+
+ kwargs = {}
+ if use_initial:
+ kwargs["initial"] = neg_zero
+ else:
+ pytest.xfail("-0. propagation in sum currently requires initial")
+
+ # Test various length, in case SIMD paths or chunking play a role.
+ # 150 extends beyond the pairwise blocksize; probably not important.
+ for i in range(0, 150):
+ arr = np.array([neg_zero] * i, dtype=dtype)
+ res = np.sum(arr, **kwargs)
+ if i > 0 or use_initial:
+ assert _check_neg_zero(res)
+ else:
+ # `sum([])` should probably be 0.0 and not -0.0 like `sum([-0.0])`
+ assert not np.signbit(res.real)
+ assert not np.signbit(res.imag)