Skip to content

Commit d4faa7b

Browse files
authored
gh-121149: improve accuracy of builtin sum() for complex inputs (gh-121176)
1 parent cecd601 commit d4faa7b

File tree

4 files changed

+120
-26
lines changed

4 files changed

+120
-26
lines changed

Doc/library/functions.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1934,6 +1934,10 @@ are always available. They are listed here in alphabetical order.
19341934
.. versionchanged:: 3.12 Summation of floats switched to an algorithm
19351935
that gives higher accuracy and better commutativity on most builds.
19361936

1937+
.. versionchanged:: 3.14
1938+
Added specialization for summation of complexes,
1939+
using same algorithm as for summation of floats.
1940+
19371941

19381942
.. class:: super()
19391943
super(type, object_or_type=None)

Lib/test/test_builtin.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1768,13 +1768,22 @@ def __getitem__(self, index):
17681768
sum(([x] for x in range(10)), empty)
17691769
self.assertEqual(empty, [])
17701770

1771+
xs = [complex(random.random() - .5, random.random() - .5)
1772+
for _ in range(10000)]
1773+
self.assertEqual(sum(xs), complex(sum(z.real for z in xs),
1774+
sum(z.imag for z in xs)))
1775+
17711776
@requires_IEEE_754
17721777
@unittest.skipIf(HAVE_DOUBLE_ROUNDING,
17731778
"sum accuracy not guaranteed on machines with double rounding")
17741779
@support.cpython_only # Other implementations may choose a different algorithm
17751780
def test_sum_accuracy(self):
17761781
self.assertEqual(sum([0.1] * 10), 1.0)
17771782
self.assertEqual(sum([1.0, 10E100, 1.0, -10E100]), 2.0)
1783+
self.assertEqual(sum([1.0, 10E100, 1.0, -10E100, 2j]), 2+2j)
1784+
self.assertEqual(sum([2+1j, 10E100j, 1j, -10E100j]), 2+2j)
1785+
self.assertEqual(sum([1j, 1, 10E100j, 1j, 1.0, -10E100j]), 2+2j)
1786+
self.assertEqual(sum([0.1j]*10 + [fractions.Fraction(1, 10)]), 0.1+1j)
17781787

17791788
def test_type(self):
17801789
self.assertEqual(type(''), type('123'))
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added specialization for summation of complexes, this also improves accuracy
2+
of builtin :func:`sum` for such inputs. Patch by Sergey B Kirpichev.

Python/bltinmodule.c

Lines changed: 105 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2516,6 +2516,49 @@ Without arguments, equivalent to locals().\n\
25162516
With an argument, equivalent to object.__dict__.");
25172517

25182518

2519+
/* Improved Kahan–Babuška algorithm by Arnold Neumaier
2520+
Neumaier, A. (1974), Rundungsfehleranalyse einiger Verfahren
2521+
zur Summation endlicher Summen. Z. angew. Math. Mech.,
2522+
54: 39-51. https://doi.org/10.1002/zamm.19740540106
2523+
https://en.wikipedia.org/wiki/Kahan_summation_algorithm#Further_enhancements
2524+
*/
2525+
2526+
typedef struct {
2527+
double hi; /* high-order bits for a running sum */
2528+
double lo; /* a running compensation for lost low-order bits */
2529+
} CompensatedSum;
2530+
2531+
static inline CompensatedSum
2532+
cs_from_double(double x)
2533+
{
2534+
return (CompensatedSum) {x};
2535+
}
2536+
2537+
static inline CompensatedSum
2538+
cs_add(CompensatedSum total, double x)
2539+
{
2540+
double t = total.hi + x;
2541+
if (fabs(total.hi) >= fabs(x)) {
2542+
total.lo += (total.hi - t) + x;
2543+
}
2544+
else {
2545+
total.lo += (x - t) + total.hi;
2546+
}
2547+
return (CompensatedSum) {t, total.lo};
2548+
}
2549+
2550+
static inline double
2551+
cs_to_double(CompensatedSum total)
2552+
{
2553+
/* Avoid losing the sign on a negative result,
2554+
and don't let adding the compensation convert
2555+
an infinite or overflowed sum to a NaN. */
2556+
if (total.lo && isfinite(total.lo)) {
2557+
return total.hi + total.lo;
2558+
}
2559+
return total.hi;
2560+
}
2561+
25192562
/*[clinic input]
25202563
sum as builtin_sum
25212564
@@ -2628,37 +2671,18 @@ builtin_sum_impl(PyObject *module, PyObject *iterable, PyObject *start)
26282671
}
26292672

26302673
if (PyFloat_CheckExact(result)) {
2631-
double f_result = PyFloat_AS_DOUBLE(result);
2632-
double c = 0.0;
2674+
CompensatedSum re_sum = cs_from_double(PyFloat_AS_DOUBLE(result));
26332675
Py_SETREF(result, NULL);
26342676
while(result == NULL) {
26352677
item = PyIter_Next(iter);
26362678
if (item == NULL) {
26372679
Py_DECREF(iter);
26382680
if (PyErr_Occurred())
26392681
return NULL;
2640-
/* Avoid losing the sign on a negative result,
2641-
and don't let adding the compensation convert
2642-
an infinite or overflowed sum to a NaN. */
2643-
if (c && isfinite(c)) {
2644-
f_result += c;
2645-
}
2646-
return PyFloat_FromDouble(f_result);
2682+
return PyFloat_FromDouble(cs_to_double(re_sum));
26472683
}
26482684
if (PyFloat_CheckExact(item)) {
2649-
// Improved Kahan–Babuška algorithm by Arnold Neumaier
2650-
// Neumaier, A. (1974), Rundungsfehleranalyse einiger Verfahren
2651-
// zur Summation endlicher Summen. Z. angew. Math. Mech.,
2652-
// 54: 39-51. https://doi.org/10.1002/zamm.19740540106
2653-
// https://en.wikipedia.org/wiki/Kahan_summation_algorithm#Further_enhancements
2654-
double x = PyFloat_AS_DOUBLE(item);
2655-
double t = f_result + x;
2656-
if (fabs(f_result) >= fabs(x)) {
2657-
c += (f_result - t) + x;
2658-
} else {
2659-
c += (x - t) + f_result;
2660-
}
2661-
f_result = t;
2685+
re_sum = cs_add(re_sum, PyFloat_AS_DOUBLE(item));
26622686
_Py_DECREF_SPECIALIZED(item, _PyFloat_ExactDealloc);
26632687
continue;
26642688
}
@@ -2667,15 +2691,70 @@ builtin_sum_impl(PyObject *module, PyObject *iterable, PyObject *start)
26672691
int overflow;
26682692
value = PyLong_AsLongAndOverflow(item, &overflow);
26692693
if (!overflow) {
2670-
f_result += (double)value;
2694+
re_sum.hi += (double)value;
2695+
Py_DECREF(item);
2696+
continue;
2697+
}
2698+
}
2699+
result = PyFloat_FromDouble(cs_to_double(re_sum));
2700+
if (result == NULL) {
2701+
Py_DECREF(item);
2702+
Py_DECREF(iter);
2703+
return NULL;
2704+
}
2705+
temp = PyNumber_Add(result, item);
2706+
Py_DECREF(result);
2707+
Py_DECREF(item);
2708+
result = temp;
2709+
if (result == NULL) {
2710+
Py_DECREF(iter);
2711+
return NULL;
2712+
}
2713+
}
2714+
}
2715+
2716+
if (PyComplex_CheckExact(result)) {
2717+
Py_complex z = PyComplex_AsCComplex(result);
2718+
CompensatedSum re_sum = cs_from_double(z.real);
2719+
CompensatedSum im_sum = cs_from_double(z.imag);
2720+
Py_SETREF(result, NULL);
2721+
while (result == NULL) {
2722+
item = PyIter_Next(iter);
2723+
if (item == NULL) {
2724+
Py_DECREF(iter);
2725+
if (PyErr_Occurred()) {
2726+
return NULL;
2727+
}
2728+
return PyComplex_FromDoubles(cs_to_double(re_sum),
2729+
cs_to_double(im_sum));
2730+
}
2731+
if (PyComplex_CheckExact(item)) {
2732+
z = PyComplex_AsCComplex(item);
2733+
re_sum = cs_add(re_sum, z.real);
2734+
im_sum = cs_add(im_sum, z.imag);
2735+
Py_DECREF(item);
2736+
continue;
2737+
}
2738+
if (PyLong_Check(item)) {
2739+
long value;
2740+
int overflow;
2741+
value = PyLong_AsLongAndOverflow(item, &overflow);
2742+
if (!overflow) {
2743+
re_sum.hi += (double)value;
2744+
im_sum.hi += 0.0;
26712745
Py_DECREF(item);
26722746
continue;
26732747
}
26742748
}
2675-
if (c && isfinite(c)) {
2676-
f_result += c;
2749+
if (PyFloat_Check(item)) {
2750+
double value = PyFloat_AS_DOUBLE(item);
2751+
re_sum.hi += value;
2752+
im_sum.hi += 0.0;
2753+
_Py_DECREF_SPECIALIZED(item, _PyFloat_ExactDealloc);
2754+
continue;
26772755
}
2678-
result = PyFloat_FromDouble(f_result);
2756+
result = PyComplex_FromDoubles(cs_to_double(re_sum),
2757+
cs_to_double(im_sum));
26792758
if (result == NULL) {
26802759
Py_DECREF(item);
26812760
Py_DECREF(iter);

0 commit comments

Comments
 (0)