You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Consider adding sumproduct() or dotproduct() to the math module #100485
I was reviewing the itertools recipes to see whether some were worth promoting to be builtin tools. The dotproduct() recipe was the best candidate. To non-matrix people this is known as sumproduct() and it comes up in many non-vector applications, possibly the most common being sum([price * quantity for price, quantity in zip(prices, quantities)]) and the second most common being weighted averages.
The current version of the recipe is:
def dotproduct(vec1, vec2):
"Compute a sum of products."
return sum(starmap(operator.mul, zip(vec1, vec2, strict=True)))
If we offered this as part of the math module, we could make a higher quality implementation.
For float inputs or mixed int/float inputs, we could square and sum in quad precision, making a single rounding at the end. This would make a robust building block to serve as a foundation for users to construct higher level tools. It is also something that is difficult for them to do on their own.
@rhettinger working on implementing this feature. I'm curious, how would one be expected to use quad precision in this context? Should I just write a multiplication and addition function for a custom quad precision data structure?
how would one be expected to use quad precision in this context
Raymond's use of "quad precision" was an active link to an article spelling out what he means. It's a software technique for getting twice native precision using single native precision float operations, and for that reason is usually called "doubled precision". Since Python's "float" is IEEE-754 "double precision" on most (all?) platforms, twice that leads to "quad".
If you look at the edit history of Raymond's post, at first it had a prototype implementation in pure Python.
Here's the logic I'm planning to use (and the tests to make sure it correct). If you all want something different or can suggest improvements, please let me know.
'Pure Python implementation of sumprod() with accuracy enhancements.'frommathimportfabs, fsum, frexp, isfinitefromsysimportstderrverbose=Falsedefaccumulate(total, frac, x):
iffabs(total) <fabs(x):
x, total=total, xt=total+xfrac+= (total-t) +xreturnt, fracdefsplit(x, VELTKAMP_CONSTANT=float(0x8000001)):
t=x*VELTKAMP_CONSTANThi=t- (t-x)
lo=x-hiassertnotisfinite(hi) or (frexp(hi)[0] *2**26).is_integer()
assertnotisfinite(hi) or (frexp(lo)[0] *2**26).is_integer()
assertnotisfinite(hi) orhi+lo==xreturnhi, lodefmultiply(p, q):
hp, lp=split(p)
hq, lq=split(q)
parts=hp*hq, hp*lq, hq*lp, lp*lqassertnotall(map(isfinite, parts)) orfsum(parts) ==p*qreturnpartsdefboth_int(p, q):
return {type(p), type(q)} == {int}
defone_float_one_int(p, q):
return {type(p), type(q)} == {int, float}
defboth_float(p, q):
return {type(p), type(q)} == {float}
defPyLong_AsLongAndOverflow(p):
p=int(p)
ifp.bit_length() >63:
raiseOverflowErrorreturnpdefcheck_long_mult_overflow(p, q):
"Multiply longs and raise if too large for a longlong."result=p*qifresult.bit_length() >63:
raiseOverflowErrorreturnresultdefPyFloat_AsDouble(p):
asserttype(p) in {int, float}
returnfloat(p) # This can raise OverflowErrorNULL=object()
defsumprod(p, q, verbose=False):
p_stopped=q_stopped=Falseint_path_enabled=flt_path_enabled=Trueint_total_in_use=flt_total_in_use=Falseint_total=0flt_total=flt_frac=0.0obj_total=0p_it=iter(p)
q_it=iter(q)
whileTrue:
p_i=next(p_it, NULL)
ifp_iisNULL:
p_stopped=Trueq_i=next(q_it, NULL)
ifq_iisNULL:
q_stopped=Trueifp_stopped!=q_stopped:
raiseValueError('Inputs are not the same length')
finished=p_stopped&q_stoppedifint_path_enabled:
ifnotfinishedandboth_int(p_i, q_i):
try:
ifverbose: print('I', end='', file=stderr)
int_p=PyLong_AsLongAndOverflow(p_i)
int_q=PyLong_AsLongAndOverflow(q_i)
int_total+=check_long_mult_overflow(int_p, int_q)
int_total_in_use=TruecontinueexceptOverflowError:
pass# We're finished, overflowed, or have a non-intint_path_enabled=Falseifint_total_in_use:
obj_total+=int(int_total)
int_total_in_use=Falseifflt_path_enabled:
ifnotfinishedand (both_float(p_i, q_i) orone_float_one_int(p_i, q_i)):
try:
ifverbose: print('F', end='', file=stderr)
flt_p=PyFloat_AsDouble(p_i)
flt_q=PyFloat_AsDouble(q_i)
forpartinmultiply(flt_p, flt_q):
flt_total, flt_frac=accumulate(flt_total, flt_frac, part)
flt_total_in_use=Trueifisfinite(flt_total):
continue# If a non-finite value arises, fallback to the slow pathflt_total=flt_frac=0.0exceptOverflowError:
pass# We're finished, overflowed, have a non-float, or have a non-finite valueflt_path_enabled=Falseifflt_total_in_use:
flt_total+=flt_fracobj_total+=float(flt_total)
flt_total_in_use=Falseassertnotint_total_in_useassertnotflt_total_in_useiffinished:
returnobj_totalifverbose: print('O', end='', file=stderr)
obj_total+=p_i*q_iif__name__=='__main__':
fromdecimalimportDecimalfromfractionsimportFractionfromitertoolsimportproductfrommathimportisnanfromrandomimportrandrangedefbaseline_sumprod(p, q):
"This defines the target behavior including expections and special values."total=0forp_i, q_iinzip(p, q, strict=True):
total+=p_i*q_ireturntotaldefrun(func, *args):
"Make comparing functions easier. Returns error status, type, and result."try:
result=func(*args)
exceptExceptionase:
return'Error', type(e), Noneifisinstance(result, (float, Decimal)) andisnan(result):
result='NaN'returnNone, type(result), result,
pool= [3.75, 2.5, -1.5, 0.0, 1.0,
float('inf'), -float('inf'), float('NaN'),
3, -5, 7, 0, 1, 10*2**64, 10**1000,
Fraction(7, 8), Fraction(21, 4), Fraction(-41, 2),
Fraction(0), Fraction(1),
Decimal('6.125'), Decimal('12.125'), Decimal('-2.125'),
Decimal(0), Decimal(1),
Decimal('Inf'), -Decimal('Inf'), Decimal('NaN'),
]
args= ((), ())
assertrun(baseline_sumprod, *args) ==run(sumprod, *args)
print('Done 0')
fora, binproduct(pool, repeat=2):
args= (a,), (b,)
assertrun(baseline_sumprod, *args) ==run(sumprod, *args)
print('Done 2')
form, ninproduct(range(5), repeat=2):
args= [randrange(100)/16foriinrange(m)], [randrange(100)/16foriinrange(n)]
assertrun(baseline_sumprod, *args) ==run(sumprod, *args)
print('Done uneven')
fora, b, c, dinproduct(pool, repeat=4):
args= (a, b), (c, d)
assertrun(baseline_sumprod, *args) ==run(sumprod, *args)
print('Done 4')
alt_pool= (1e200, 2e200, 5e200, 0.0, -1e200, -2e200, -5e200)
fora, b, c, d, e, finproduct(pool, repeat=6):
args= (a, b, c), (d, e, f)
assertrun(baseline_sumprod, *args) ==run(sumprod, *args)
print('Done overflow 6')
fora, b, c, d, e, finproduct(pool, repeat=6):
args= (a, b, c), (d, e, f)
assertrun(baseline_sumprod, *args) ==run(sumprod, *args)
print('Done regular 6')
I checked in the core logic so that we could have a baseline for further improvements. The beta release is still far away. Suggestions are welcome. Here are some topics that I'm mulling over:
Determine whether 2SUM would be faster than Fast2Sum with a fabs test. The disassembly from both Intel and M1 Arm builds show that Fast2Sum is compiled without branches. The x86_64 code selects between the results with cmpnlesd and andnpd. The AArch64 code uses fcmp and fcsel to the same effect.
The API is a bit awkward for some inputs. To compute the sum of squares from an iterator input, you need sumprod(*tee(it)). Inputs consisting of 2-tuples need to be regrouped with sumprod(*zip(*paired)). Generator expression inputs work fine but should be formatted on two lines for readabilty:
sumprod(((xi - xbar) for xi in x),
((yi - ybar) for yi in y))
For grins, I'm experimenting with 2-fold Neumaier summation. At a cost of an extra 2Sum call, the error bound becomes proportional to ε³·n²·Σ|aᵢ|. The extra factor of ε would only be relevant in cases where large terms mostly cancel (this is typical for some use cases; for example, summing the moments on a lever).
def twofold(data):
ce = e = b = 0.0
for n in data:
bb, ee = twosum(b, n)
eee, cce = twosum(e, ee)
b = bb
e = eee
ce += cce
return cce + b + e
It might be worthwhile to support a high accuracy code path for complex numbers.
The MS Excel implementation generalizes sumprod to handle an arbitrary number of input vectors. If there were valid use cases, we could do the same:
Example use case (as variant to the motivating example at the very top message of this thread): sum([price * quantity * (1 - discount) * (1 + tax_rate) for price, quantity, discount, tax_rate in zip(prices, quantities, discounts, tax_rates)])
which would then become: sumprod(prices, quantities, (1 - discount for discount in discounts), (1 + tax_rate for tax_rate in tax_rates))
I'd welcome this generalisation, but perhaps my comment is too late because sumproduct was added to 3.12a6 and this issue was closed.
It should not be too late for such an improvement yet. Beta 1 is the point when no new features are supposed to be added. @Dutcho, maybe you should open a new issue, though.
Activity
PurityLake commentedon Dec 24, 2022
@rhettinger working on implementing this feature. I'm curious, how would one be expected to use quad precision in this context? Should I just write a multiplication and addition function for a custom quad precision data structure?
tim-one commentedon Dec 24, 2022
Raymond's use of "quad precision" was an active link to an article spelling out what he means. It's a software technique for getting twice native precision using single native precision float operations, and for that reason is usually called "doubled precision". Since Python's "float" is IEEE-754 "double precision" on most (all?) platforms, twice that leads to "quad".
If you look at the edit history of Raymond's post, at first it had a prototype implementation in pure Python.
rhettinger commentedon Dec 24, 2022
@PurityLake Please leave that for me. I've already done substantial work on it and opened this issue to get buy in before going further.
tim-one commentedon Dec 24, 2022
Not needed - it would self-evidently be nice to have 😄
rhettinger commentedon Jan 3, 2023
Here's the logic I'm planning to use (and the tests to make sure it correct). If you all want something different or can suggest improvements, please let me know.
GH-100485: Add math.sumprod() (GH-100677)
rhettinger commentedon Jan 7, 2023
I checked in the core logic so that we could have a baseline for further improvements. The beta release is still far away. Suggestions are welcome. Here are some topics that I'm mulling over:
Determine whether
2SUMwould be faster thanFast2Sumwith afabstest. The disassembly from both Intel and M1 Arm builds show thatFast2Sumis compiled without branches. The x86_64 code selects between the results withcmpnlesdandandnpd. The AArch64 code usesfcmpandfcselto the same effect.The API is a bit awkward for some inputs. To compute the sum of squares from an iterator input, you need
sumprod(*tee(it)). Inputs consisting of 2-tuples need to be regrouped withsumprod(*zip(*paired)). Generator expression inputs work fine but should be formatted on two lines for readabilty:2Sumcall, the error bound becomes proportional toε³·n²·Σ|aᵢ|. The extra factor ofεwould only be relevant in cases where large terms mostly cancel (this is typical for some use cases; for example, summing the moments on a lever).It might be worthwhile to support a high accuracy code path for complex numbers.
The MS Excel implementation generalizes
sumprodto handle an arbitrary number of input vectors. If there were valid use cases, we could do the same:test_sumprod_stress()unittest does too much work. Runningtest_mathnow takes 15 seconds.GH-100485: Convert from Fast2Sum to 2Sum (GH-100836)
rhettinger commentedon Jan 8, 2023
2SUM was faster so I put it in.
Interestingly, this only makes the code two lines longer. The running time only increases by 12%. Worth it?
gpshead commentedon Jan 8, 2023
Please add
@support.requires_resource('cpu')totest_sumprod_stress.hugovk commentedon Jan 8, 2023
It would be good to include this in What's New.
https://docs.python.org/3.12/whatsnew/3.12.html
GH-100485: Tweaks to sumprod() (GH-100857)
rhettinger commentedon Jan 8, 2023
@gpshead Done.
@hugovk Done.
rhettinger commentedon Jan 9, 2023
Fun fact,
sumprodcan be used to make a single rounding fused-mulitply-add:TrangOul commentedon Jan 12, 2023
If we're going to have an even more efficient method, the dot product example from
itertoolsshould be replaced with something else.GH-100485: Add extended accuracy test. Switch to faster fma() based v…
pythonGH-100485: Add extended accuracy test. Switch to faster fma() b…
GH-100485: Create an alternative code path when an accurate fma() imp…
Dutcho commentedon Mar 12, 2023
@rhettinger
I'd indeed like below:
Example use case (as variant to the motivating example at the very top message of this thread):
sum([price * quantity * (1 - discount) * (1 + tax_rate) for price, quantity, discount, tax_rate in zip(prices, quantities, discounts, tax_rates)])which would then become:
sumprod(prices, quantities, (1 - discount for discount in discounts), (1 + tax_rate for tax_rate in tax_rates))I'd welcome this generalisation, but perhaps my comment is too late because
sumproductwas added to 3.12a6 and this issue was closed.TrangOul commentedon Mar 20, 2023
It should not be too late for such an improvement yet. Beta 1 is the point when no new features are supposed to be added.
@Dutcho, maybe you should open a new issue, though.