GH-100485: Add extended accuracy test. Switch to faster fma() based variant. GH-101383)

This commit is contained in:
Raymond Hettinger 2023-01-28 06:29:21 -06:00 committed by GitHub
parent db757f0e44
commit 84483aacc0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 100 additions and 36 deletions

View file

@ -1369,6 +1369,89 @@ class MathTests(unittest.TestCase):
args,
)
@requires_IEEE_754
@unittest.skipIf(HAVE_DOUBLE_ROUNDING,
"sumprod() accuracy not guaranteed on machines with double rounding")
@support.cpython_only # Other implementations may choose a different algorithm
@support.requires_resource('cpu')
def test_sumprod_extended_precision_accuracy(self):
import operator
from fractions import Fraction
from itertools import starmap
from collections import namedtuple
from math import log2, exp2, fabs
from random import choices, uniform, shuffle
from statistics import median
DotExample = namedtuple('DotExample', ('x', 'y', 'target_sumprod', 'condition'))
def DotExact(x, y):
vec1 = map(Fraction, x)
vec2 = map(Fraction, y)
return sum(starmap(operator.mul, zip(vec1, vec2, strict=True)))
def Condition(x, y):
return 2.0 * DotExact(map(abs, x), map(abs, y)) / abs(DotExact(x, y))
def linspace(lo, hi, n):
width = (hi - lo) / (n - 1)
return [lo + width * i for i in range(n)]
def GenDot(n, c):
""" Algorithm 6.1 (GenDot) works as follows. The condition number (5.7) of
the dot product xT y is proportional to the degree of cancellation. In
order to achieve a prescribed cancellation, we generate the first half of
the vectors x and y randomly within a large exponent range. This range is
chosen according to the anticipated condition number. The second half of x
and y is then constructed choosing xi randomly with decreasing exponent,
and calculating yi such that some cancellation occurs. Finally, we permute
the vectors x, y randomly and calculate the achieved condition number.
"""
assert n >= 6
n2 = n // 2
x = [0.0] * n
y = [0.0] * n
b = log2(c)
# First half with exponents from 0 to |_b/2_| and random ints in between
e = choices(range(int(b/2)), k=n2)
e[0] = int(b / 2) + 1
e[-1] = 0.0
x[:n2] = [uniform(-1.0, 1.0) * exp2(p) for p in e]
y[:n2] = [uniform(-1.0, 1.0) * exp2(p) for p in e]
# Second half
e = list(map(round, linspace(b/2, 0.0 , n-n2)))
for i in range(n2, n):
x[i] = uniform(-1.0, 1.0) * exp2(e[i - n2])
y[i] = (uniform(-1.0, 1.0) * exp2(e[i - n2]) - DotExact(x, y)) / x[i]
# Shuffle
pairs = list(zip(x, y))
shuffle(pairs)
x, y = zip(*pairs)
return DotExample(x, y, DotExact(x, y), Condition(x, y))
def RelativeError(res, ex):
x, y, target_sumprod, condition = ex
n = DotExact(list(x) + [-res], list(y) + [1])
return fabs(n / target_sumprod)
def Trial(dotfunc, c, n):
ex = GenDot(10, c)
res = dotfunc(ex.x, ex.y)
return RelativeError(res, ex)
times = 1000 # Number of trials
n = 20 # Length of vectors
c = 1e30 # Target condition number
relative_err = median(Trial(math.sumprod, c, n) for i in range(times))
self.assertLess(relative_err, 1e-16)
def testModf(self):
self.assertRaises(TypeError, math.modf)