This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: Make fsum usable incrementally.
Type: enhancement Stage:
Components: Library (Lib) Versions: Python 3.4, Python 3.5
process
Status: closed Resolution: rejected
Dependencies: Superseder:
Assigned To: rhettinger Nosy List: mark.dickinson, oscarbenjamin, rhettinger, tim.peters
Priority: low Keywords:

Created on 2013-09-25 10:46 by oscarbenjamin, last changed 2022-04-11 14:57 by admin. This issue is now closed.

Messages (8)
msg198381 - (view) Author: Oscar Benjamin (oscarbenjamin) * Date: 2013-09-25 10:46
I would like to be able use fsum incrementally however it is not currently possible.

With sum() you can do:

subtotal = sum(nums)
subtotal = sum(othernums, subtotal)

This wouldn't work for fsum() because the returned float is not the same as the state maintained internally which is a list of floats. I propose instead that a fsum() could take an optional second argument which is a list of floats and in this case return the updated list of floats.

I have modified Raymond Hettinger's msum() recipe to do what I want:

def fsum(iterable, carry=None):
    "Full precision summation using multiple floats for intermediate values"
    partials = list(carry) if carry else []
    for x in iterable:
        i = 0
        for y in partials:
            if abs(x) < abs(y):
                x, y = y, x
            hi = x + y
            lo = y - (hi - x)
            if lo:
                partials[i] = lo
                i += 1
            x = hi
        partials[i:] = [x]
    if carry is None:
        return sum(partials, 0.0)
    else:
        return partials


Here's an interactive session showing how you might use it:

>>> from fsum import fsum
>>> fsum([1e20, 1, -1e20])
1.0
>>> fsum([1e20, 1, -1e20], [])
[1.0, 0.0]
>>> fsum([1e20, 1, -1e20], [])
[1.0, 0.0]
>>> fsum([1e20, 1, -1e20], [])
[1.0, 0.0]
>>> fsum([1e20, 1], [])
[1.0, 1e+20]
>>> carry = fsum([1e20, 1], [])
>>> fsum([-1e20], carry)
[1.0, 0.0]
>>> nums = [7, 1e100, -7, -1e100, -9e-20, 8e-20] * 10
>>> subtotal = []
>>> for n in nums:
...     subtotal = fsum([n], subtotal)
...
>>> subtotal
[-1.0000000000000007e-19]
>>> fsum(subtotal)
-1.0000000000000007e-19


My motivation for this is that I want to be able to write an efficient sum function that is accurate for a mix of ints and floats while being as fast as possible in the case that I have a list of only floats (or only ints). What I have so far looks a bit like:

def sum(numbers):
    exact_total = 0
    float_total = 0.0
    for T, nums in groupby(numbers):
        if T is int:
            exact_total = sum(nums, exact_total)
        elif T is float:
            # This doesn't really work:
            float_total += fsum(float_total)
        # ...


However fsum is only exact if it adds all the numbers in a single pass. The above would have order-dependent results given a mixed list of ints and floats e.g.:

[1e20, -1e20, 1, -1, 1.0, -1.0]
  vs
[1e20, 1.0, -1e20, 1, -1, -1.0]

Although fsum is internally very accurate it always discards information on output. Even if I build a list of all floats and fsum those at the end it can still be inaccurate if the exact_total cannot be exactly coerced to float and passed to fsum.

If I could get fsum to return the exact float expansion then I could use that in a number of different ways. Given the partials list I can combine all of the different subtotals with Fraction arithmetic and coerce to float only at the very end. If I can also get fsum to accept a float expansion on input then I can use it incrementally and there is no need to build a list of all floats.

I am prepared to write a patch for this if the idea is deemed acceptable.
msg198497 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2013-09-27 18:40
FWIW, the reason for __builtin__.sum() having a start argument was so that it could change the base datatype:   sum(iterable, start=Fraction(0)).

[Oscar Benjamin]
> Although fsum is internally very accurate 
> it always discards information on output.

A "start" argument won't help you, because you will discard information on input.  A sequence like [1E100, 0.1, -1E100, 0.1] wouldn't work when split into subtotal=fsum([1E100, 0.1]) and fsum([-1E100, 0.1], start=subtotal).

> My motivation for this is that I want to be able to write 
> an efficient sum function that is accurate for a mix of ints
> and floats

FWIW, fsum() already works well with integers as long as they don't exceed 53bits of precision.  For such exotic use cases, the decimal module would be a better alternative.  Now that the decimal module has a C implementation, there is no reason not to use it for high precision applications.

It is possible to extent the current fsum() implementation to separately track integers and combine it with the float results at the end.   That said, the probability that any user (other than yourself) will need this can only be expressed as a denormal number ;-)

I'm not rejecting the request; rather, I'm questioning the need for it.
msg198520 - (view) Author: Oscar Benjamin (oscarbenjamin) * Date: 2013-09-28 14:46
Thanks for responding Raymond.

Raymond Hettinger wrote:
> A "start" argument won't help you, because you will discard information
on input.  A sequence like [1E100, 0.1, -1E100, 0.1] wouldn't work when
split into subtotal=fsum([1E100, 0.1]) and fsum([-1E100, 0.1],
start=subtotal).

I'm not sure if you've fully understood the proposal.

The algorithm underlying fsum is (I think) called distillation. It
compresses a list of floats into a shorter list of non-overlapping floats
having the same exact sum. Where fsum deviates from this idea is that it
doesn't return a list of floats, rather it returns only the largest float.
My proposal is that there be a way to tell fsum to return the list of
floats whose sum is exact and not rounded. Specifically the subtotal that
is returned and fed back into fsum would be a list of floats and no
information would be discarded. So fsum(numbers, []) would return a list of
floats and that list can be passed back in again.

> > My motivation for this is that I want to be able to write
> > an efficient sum function that is accurate for a mix of ints
> > and floats
>
> FWIW, fsum() already works well with integers as long as they don't
exceed 53bits of precision.

I was simplifying the use-case somewhat. I would also like this sum
function to work with fractions and decimals neither of which coerces
exactly to float (and potentially I want some non-stdlib types also).

>  For such exotic use cases, the decimal module would be a better
alternative.  Now that the decimal module has a C implementation, there is
no reason not to use it for high precision applications.

It is possible to coerce to Decimal and exactly sum a list of ints, floats
and decimals (by trapping inexact and increasing the context precision). I
have tried this under CPython 3.3 and it was 20-50x slower than fsum
depending on how it manages the arithmetic context and whether it can be
used safely with a generator that also manipulates the context - the safe
version that doesn't build a list is 50x slower. It is also not possible to
use Decimal exactly with Fractions.

I believe that there are other use-cases for having fsum be usable
incrementally. This would make it usable for accurate summation in
incremental, parallel and distributed computation also. Unfortunately fsum
itself already isn't used as much as it should be and I agree that probably
all the use cases for this extension would be relatively obscure.
msg198677 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2013-09-30 05:52
Conceptually, I support the idea of having some way to avoid information loss by returning uncollapsed partial sums.  As you say, it would provide an essential primitive for parallel fsums and for cumulative subtotals.

If something along those lines were to be accepted, it would likely take one of two forms:
   
    fsum(iterable, detail=True) --> list_of_floats
    fsum_detailed(iterable) --> list_of_floats

Note, there is no need for a "carry" argument because the previous result can simply be prepended to the new data (for a running total) or combined in a final summation at the end (for parallel computation):

    detail = [0.0]
    for group in groups:
        detail = math.fsum_detailed(itertools.chain(detail, group))
        print('Running total', math.fsum(detail))

On the negative side, I think the use case is too exotic and no one will care about or learn to use this function.  Also, it usually isn't wise to expose the internals (we may want a completely different implementation someday).  Lastly, I think there is some virtue in keeping the API simple; otherwise, math.fsum() may ward-off some its potential users, resulting in even lower adoption than we have now.

With respect to the concerns about speed, I'm curious whether you've run your msum() edits under pypy or cython.  I would expect both to give excellent performance.
msg198687 - (view) Author: Oscar Benjamin (oscarbenjamin) * Date: 2013-09-30 10:27
I should be clearer about my intentions. I'm hoping to create an
efficient and accurate sum() function for the new stdlib statistics
module:
http://bugs.python.org/issue18606
http://www.python.org/dev/peps/pep-0450/

The sum() function currently proposed can be seen in this patch:
http://bugs.python.org/file31680/statistics_combined.patch

It uses Fractions to compute an exact sum and separately tries to keep
track of type coercion to convert the end result. It is accurate and
returns a result of the appropriate type for any mix of ints,
Fractions, Decimals and floats with the caveat that mixing Fractions
and Decimals is disallowed. I believe the most common use-cases will
be for ints/floats and for these cases it is 100x slower than
sum/fsum.

The discussion here:
http://bugs.python.org/issue18606#msg195630
resulted in the sum function that you can see in the above patch.
Following that I've had off-list discussions with the author of the
module about improving the speed of the sum function (which is a
bottleneck for half of the functions exposed by the module). He is
very much interested in speed optimisations but doesn't want to
compromise on accuracy.

My own interest is that I would like an efficient and accurate (for
all types) sum function *somewhere* in the stdlib. The addition of the
statistics module with its new sum() function is a good opportunity to
achieve this. If this were purely for myself I wouldn't bother this
much with speed optimisation since I've probably already spent more of
my own time thinking about this function than I ever would running it!
(If I did want to specifically speed this up in my own work I would,
as you say, use cython).

If fsum() were modified in the way that I describe that it would be
usable as a primitive for the statistics.sum() function and also for
parallel etc. computation. I agree that the carry argument is
unnecessary and that the main is just the exactness of the return
value: it seems a shame that fsum could so easily give me an exact
result but doesn't.

As for exposing the internals of fsum, is it not the case that a
*non-overlapping* set of floats having a given exact sum is a uniquely
defined set? For msum I believe it is only necessary to strip out
zeros in order to normalise the output so that it is uniquely defined
by the true value of the sum in question (the list is already in
ascending order once the zeros are removed).

Uniqueness: If a many-digit float is given by something like
(+-)abcdefghijkl * 2 ** x and we want to break it into non-overlapping
2-digit floats of the form ab*2**x. Since the first digit of each
2-digit float must be non-zero (consider denormals as a separate case)
the largest magnitude float is uniquely determined. Subtract that from
the many-digit total and then the next largest float is uniquely
determined and so on. There can be at most 1 denormal number in the
result and this is also uniquely determined once the larger numbers
are extracted. So the only way that the partials list can be
non-unique is by the inclusion of zeros (unless I've missed something
:).
msg198715 - (view) Author: Tim Peters (tim.peters) * (Python committer) Date: 2013-09-30 18:33
The possible use cases are so varied & fuzzy it seems better to use an object for this, rather than funk-ify `fsum`.  Say, class Summer.  Then methods could be invented as needed, with whatever state is required belonging to Summer objects rather than passed around in funky calling-sequence/return conventions.  Like,

s = Summer() # new object
s.add(3.1)   # add one number to running sum
s.sum()      # returns sum so far as float: 3.1
s.update(iterable_returning_numbers)  # add a bunch of numbers to sum
s.combine(another_Summer_object)  # add Summer's
s.sum_as_decimal(precision=None) # sum so far as Decimal
s.sum_as_fraction()  # exact sum so far as Fraction

Blah blah blah ;-)
msg198725 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2013-09-30 19:55
Let's close this one.  As Uncle Timmy says, it would be a mistake to "funkify" the signature for math.fsum.

If something like a Summer() class is born, it should start it life outside the standard library, gain a following, and let both the API and implementation mature.

I'm sympathic about the loss of information in the final step of math.fsum() but don't think the standard library is the place for this to be born.
msg198729 - (view) Author: Oscar Benjamin (oscarbenjamin) * Date: 2013-09-30 20:16
Fair enough.

Thanks again for taking the time to look at this.
History
Date User Action Args
2022-04-11 14:57:51adminsetgithub: 63286
2013-09-30 20:16:22oscarbenjaminsetmessages: + msg198729
2013-09-30 19:55:36rhettingersetstatus: open -> closed
resolution: rejected
messages: + msg198725
2013-09-30 18:33:58tim.peterssetnosy: + tim.peters
messages: + msg198715
2013-09-30 10:27:13oscarbenjaminsetmessages: + msg198687
2013-09-30 05:52:05rhettingersetmessages: + msg198677
2013-09-28 14:46:27oscarbenjaminsetmessages: + msg198520
2013-09-27 18:40:39rhettingersetmessages: + msg198497
2013-09-27 18:23:58rhettingersetpriority: normal -> low
assignee: rhettinger
2013-09-25 10:46:10oscarbenjamincreate