Skip to content

gh-121149: improve accuracy of builtin sum() for complex inputs #121176

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

Merged
merged 10 commits into from
Jul 5, 2024
4 changes: 4 additions & 0 deletions Doc/library/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1934,6 +1934,10 @@ are always available. They are listed here in alphabetical order.
.. versionchanged:: 3.12 Summation of floats switched to an algorithm
that gives higher accuracy and better commutativity on most builds.

.. versionchanged:: 3.14
Added specialization for summation of complexes,
using same algorithm as for summation of floats.


.. class:: super()
super(type, object_or_type=None)
Expand Down
9 changes: 9 additions & 0 deletions Lib/test/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1768,13 +1768,22 @@ def __getitem__(self, index):
sum(([x] for x in range(10)), empty)
self.assertEqual(empty, [])

xs = [complex(random.random() - .5, random.random() - .5)
for _ in range(10000)]
self.assertEqual(sum(xs), complex(sum(z.real for z in xs),
sum(z.imag for z in xs)))

@requires_IEEE_754
@unittest.skipIf(HAVE_DOUBLE_ROUNDING,
"sum accuracy not guaranteed on machines with double rounding")
@support.cpython_only # Other implementations may choose a different algorithm
def test_sum_accuracy(self):
self.assertEqual(sum([0.1] * 10), 1.0)
self.assertEqual(sum([1.0, 10E100, 1.0, -10E100]), 2.0)
self.assertEqual(sum([1.0, 10E100, 1.0, -10E100, 2j]), 2+2j)
self.assertEqual(sum([2+1j, 10E100j, 1j, -10E100j]), 2+2j)
self.assertEqual(sum([1j, 1, 10E100j, 1j, 1.0, -10E100j]), 2+2j)
self.assertEqual(sum([0.1j]*10 + [fractions.Fraction(1, 10)]), 0.1+1j)

def test_type(self):
self.assertEqual(type(''), type('123'))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added specialization for summation of complexes, this also improves accuracy
of builtin :func:`sum` for such inputs. Patch by Sergey B Kirpichev.
131 changes: 105 additions & 26 deletions Python/bltinmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2516,6 +2516,49 @@ Without arguments, equivalent to locals().\n\
With an argument, equivalent to object.__dict__.");


/* Improved Kahan–Babuška algorithm by Arnold Neumaier
Neumaier, A. (1974), Rundungsfehleranalyse einiger Verfahren
zur Summation endlicher Summen. Z. angew. Math. Mech.,
54: 39-51. https://doi.org/10.1002/zamm.19740540106
https://en.wikipedia.org/wiki/Kahan_summation_algorithm#Further_enhancements
*/

typedef struct {
double hi; /* high-order bits for a running sum */
double lo; /* a running compensation for lost low-order bits */
} CompensatedSum;

static inline CompensatedSum
cs_from_double(double x)
{
return (CompensatedSum) {x};
}

static inline CompensatedSum
cs_add(CompensatedSum total, double x)
{
double t = total.hi + x;
if (fabs(total.hi) >= fabs(x)) {
total.lo += (total.hi - t) + x;
}
else {
total.lo += (x - t) + total.hi;
}
return (CompensatedSum) {t, total.lo};
}

static inline double
cs_to_double(CompensatedSum total)
{
/* Avoid losing the sign on a negative result,
and don't let adding the compensation convert
an infinite or overflowed sum to a NaN. */
if (total.lo && isfinite(total.lo)) {
return total.hi + total.lo;
}
return total.hi;
}

/*[clinic input]
sum as builtin_sum

Expand Down Expand Up @@ -2628,37 +2671,18 @@ builtin_sum_impl(PyObject *module, PyObject *iterable, PyObject *start)
}

if (PyFloat_CheckExact(result)) {
double f_result = PyFloat_AS_DOUBLE(result);
double c = 0.0;
CompensatedSum re_sum = cs_from_double(PyFloat_AS_DOUBLE(result));
Py_SETREF(result, NULL);
while(result == NULL) {
item = PyIter_Next(iter);
if (item == NULL) {
Py_DECREF(iter);
if (PyErr_Occurred())
return NULL;
/* Avoid losing the sign on a negative result,
and don't let adding the compensation convert
an infinite or overflowed sum to a NaN. */
if (c && isfinite(c)) {
f_result += c;
}
return PyFloat_FromDouble(f_result);
return PyFloat_FromDouble(cs_to_double(re_sum));
}
if (PyFloat_CheckExact(item)) {
// Improved Kahan–Babuška algorithm by Arnold Neumaier
// Neumaier, A. (1974), Rundungsfehleranalyse einiger Verfahren
// zur Summation endlicher Summen. Z. angew. Math. Mech.,
// 54: 39-51. https://doi.org/10.1002/zamm.19740540106
// https://en.wikipedia.org/wiki/Kahan_summation_algorithm#Further_enhancements
double x = PyFloat_AS_DOUBLE(item);
double t = f_result + x;
if (fabs(f_result) >= fabs(x)) {
c += (f_result - t) + x;
} else {
c += (x - t) + f_result;
}
f_result = t;
re_sum = cs_add(re_sum, PyFloat_AS_DOUBLE(item));
_Py_DECREF_SPECIALIZED(item, _PyFloat_ExactDealloc);
continue;
}
Expand All @@ -2667,15 +2691,70 @@ builtin_sum_impl(PyObject *module, PyObject *iterable, PyObject *start)
int overflow;
value = PyLong_AsLongAndOverflow(item, &overflow);
if (!overflow) {
f_result += (double)value;
re_sum.hi += (double)value;
Py_DECREF(item);
continue;
}
}
result = PyFloat_FromDouble(cs_to_double(re_sum));
if (result == NULL) {
Py_DECREF(item);
Py_DECREF(iter);
return NULL;
}
temp = PyNumber_Add(result, item);
Py_DECREF(result);
Py_DECREF(item);
result = temp;
if (result == NULL) {
Py_DECREF(iter);
return NULL;
}
}
}

if (PyComplex_CheckExact(result)) {
Py_complex z = PyComplex_AsCComplex(result);
CompensatedSum re_sum = cs_from_double(z.real);
CompensatedSum im_sum = cs_from_double(z.imag);
Py_SETREF(result, NULL);
while (result == NULL) {
item = PyIter_Next(iter);
if (item == NULL) {
Py_DECREF(iter);
if (PyErr_Occurred()) {
return NULL;
}
return PyComplex_FromDoubles(cs_to_double(re_sum),
cs_to_double(im_sum));
}
if (PyComplex_CheckExact(item)) {
z = PyComplex_AsCComplex(item);
re_sum = cs_add(re_sum, z.real);
im_sum = cs_add(im_sum, z.imag);
Py_DECREF(item);
continue;
}
if (PyLong_Check(item)) {
long value;
int overflow;
value = PyLong_AsLongAndOverflow(item, &overflow);
if (!overflow) {
re_sum.hi += (double)value;
im_sum.hi += 0.0;
Py_DECREF(item);
continue;
}
}
if (c && isfinite(c)) {
f_result += c;
if (PyFloat_Check(item)) {
double value = PyFloat_AS_DOUBLE(item);
re_sum.hi += value;
im_sum.hi += 0.0;
_Py_DECREF_SPECIALIZED(item, _PyFloat_ExactDealloc);
continue;
}
result = PyFloat_FromDouble(f_result);
result = PyComplex_FromDoubles(cs_to_double(re_sum),
cs_to_double(im_sum));
if (result == NULL) {
Py_DECREF(item);
Py_DECREF(iter);
Expand Down
Loading