Skip to content

Commit 0ff3754

Browse files
committed
Add fsum to the math module
1 parent 93ec575 commit 0ff3754

File tree

2 files changed

+197
-103
lines changed

2 files changed

+197
-103
lines changed

Lib/test/test_math.py

Lines changed: 101 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -599,102 +599,101 @@ def testfrexp(name, result, expected):
599599
self.assertTrue(math.isnan(math.frexp(NAN)[0]))
600600

601601

602-
# TODO: RUSTPYTHON
603-
# @requires_IEEE_754
604-
# @unittest.skipIf(HAVE_DOUBLE_ROUNDING,
605-
# "fsum is not exact on machines with double rounding")
606-
# def testFsum(self):
607-
# # math.fsum relies on exact rounding for correct operation.
608-
# # There's a known problem with IA32 floating-point that causes
609-
# # inexact rounding in some situations, and will cause the
610-
# # math.fsum tests below to fail; see issue #2937. On non IEEE
611-
# # 754 platforms, and on IEEE 754 platforms that exhibit the
612-
# # problem described in issue #2937, we simply skip the whole
613-
# # test.
614-
615-
# # Python version of math.fsum, for comparison. Uses a
616-
# # different algorithm based on frexp, ldexp and integer
617-
# # arithmetic.
618-
# from sys import float_info
619-
# mant_dig = float_info.mant_dig
620-
# etiny = float_info.min_exp - mant_dig
621-
622-
# def msum(iterable):
623-
# """Full precision summation. Compute sum(iterable) without any
624-
# intermediate accumulation of error. Based on the 'lsum' function
625-
# at http://code.activestate.com/recipes/393090/
626-
# """
627-
# tmant, texp = 0, 0
628-
# for x in iterable:
629-
# mant, exp = math.frexp(x)
630-
# mant, exp = int(math.ldexp(mant, mant_dig)), exp - mant_dig
631-
# if texp > exp:
632-
# tmant <<= texp-exp
633-
# texp = exp
634-
# else:
635-
# mant <<= exp-texp
636-
# tmant += mant
637-
# # Round tmant * 2**texp to a float. The original recipe
638-
# # used float(str(tmant)) * 2.0**texp for this, but that's
639-
# # a little unsafe because str -> float conversion can't be
640-
# # relied upon to do correct rounding on all platforms.
641-
# tail = max(len(bin(abs(tmant)))-2 - mant_dig, etiny - texp)
642-
# if tail > 0:
643-
# h = 1 << (tail-1)
644-
# tmant = tmant // (2*h) + bool(tmant & h and tmant & 3*h-1)
645-
# texp += tail
646-
# return math.ldexp(tmant, texp)
647-
648-
# test_values = [
649-
# ([], 0.0),
650-
# ([0.0], 0.0),
651-
# ([1e100, 1.0, -1e100, 1e-100, 1e50, -1.0, -1e50], 1e-100),
652-
# ([2.0**53, -0.5, -2.0**-54], 2.0**53-1.0),
653-
# ([2.0**53, 1.0, 2.0**-100], 2.0**53+2.0),
654-
# ([2.0**53+10.0, 1.0, 2.0**-100], 2.0**53+12.0),
655-
# ([2.0**53-4.0, 0.5, 2.0**-54], 2.0**53-3.0),
656-
# ([1./n for n in range(1, 1001)],
657-
# float.fromhex('0x1.df11f45f4e61ap+2')),
658-
# ([(-1.)**n/n for n in range(1, 1001)],
659-
# float.fromhex('-0x1.62a2af1bd3624p-1')),
660-
# ([1e16, 1., 1e-16], 10000000000000002.0),
661-
# ([1e16-2., 1.-2.**-53, -(1e16-2.), -(1.-2.**-53)], 0.0),
662-
# # exercise code for resizing partials array
663-
# ([2.**n - 2.**(n+50) + 2.**(n+52) for n in range(-1074, 972, 2)] +
664-
# [-2.**1022],
665-
# float.fromhex('0x1.5555555555555p+970')),
666-
# ]
667-
668-
# # Telescoping sum, with exact differences (due to Sterbenz)
669-
# terms = [1.7**i for i in range(1001)]
670-
# test_values.append((
671-
# [terms[i+1] - terms[i] for i in range(1000)] + [-terms[1000]],
672-
# -terms[0]
673-
# ))
674-
675-
# for i, (vals, expected) in enumerate(test_values):
676-
# try:
677-
# actual = math.fsum(vals)
678-
# except OverflowError:
679-
# self.fail("test %d failed: got OverflowError, expected %r "
680-
# "for math.fsum(%.100r)" % (i, expected, vals))
681-
# except ValueError:
682-
# self.fail("test %d failed: got ValueError, expected %r "
683-
# "for math.fsum(%.100r)" % (i, expected, vals))
684-
# self.assertEqual(actual, expected)
685-
686-
# from random import random, gauss, shuffle
687-
# for j in range(1000):
688-
# vals = [7, 1e100, -7, -1e100, -9e-20, 8e-20] * 10
689-
# s = 0
690-
# for i in range(200):
691-
# v = gauss(0, random()) ** 7 - s
692-
# s += v
693-
# vals.append(v)
694-
# shuffle(vals)
695-
696-
# s = msum(vals)
697-
# self.assertEqual(msum(vals), math.fsum(vals))
602+
@requires_IEEE_754
603+
@unittest.skipIf(HAVE_DOUBLE_ROUNDING,
604+
"fsum is not exact on machines with double rounding")
605+
def testFsum(self):
606+
# math.fsum relies on exact rounding for correct operation.
607+
# There's a known problem with IA32 floating-point that causes
608+
# inexact rounding in some situations, and will cause the
609+
# math.fsum tests below to fail; see issue #2937. On non IEEE
610+
# 754 platforms, and on IEEE 754 platforms that exhibit the
611+
# problem described in issue #2937, we simply skip the whole
612+
# test.
613+
614+
# Python version of math.fsum, for comparison. Uses a
615+
# different algorithm based on frexp, ldexp and integer
616+
# arithmetic.
617+
from sys import float_info
618+
mant_dig = float_info.mant_dig
619+
etiny = float_info.min_exp - mant_dig
620+
621+
def msum(iterable):
622+
"""Full precision summation. Compute sum(iterable) without any
623+
intermediate accumulation of error. Based on the 'lsum' function
624+
at http://code.activestate.com/recipes/393090/
625+
"""
626+
tmant, texp = 0, 0
627+
for x in iterable:
628+
mant, exp = math.frexp(x)
629+
mant, exp = int(math.ldexp(mant, mant_dig)), exp - mant_dig
630+
if texp > exp:
631+
tmant <<= texp-exp
632+
texp = exp
633+
else:
634+
mant <<= exp-texp
635+
tmant += mant
636+
# Round tmant * 2**texp to a float. The original recipe
637+
# used float(str(tmant)) * 2.0**texp for this, but that's
638+
# a little unsafe because str -> float conversion can't be
639+
# relied upon to do correct rounding on all platforms.
640+
tail = max(len(bin(abs(tmant)))-2 - mant_dig, etiny - texp)
641+
if tail > 0:
642+
h = 1 << (tail-1)
643+
tmant = tmant // (2*h) + bool(tmant & h and tmant & 3*h-1)
644+
texp += tail
645+
return math.ldexp(tmant, texp)
646+
647+
test_values = [
648+
([], 0.0),
649+
([0.0], 0.0),
650+
([1e100, 1.0, -1e100, 1e-100, 1e50, -1.0, -1e50], 1e-100),
651+
([2.0**53, -0.5, -2.0**-54], 2.0**53-1.0),
652+
([2.0**53, 1.0, 2.0**-100], 2.0**53+2.0),
653+
([2.0**53+10.0, 1.0, 2.0**-100], 2.0**53+12.0),
654+
([2.0**53-4.0, 0.5, 2.0**-54], 2.0**53-3.0),
655+
([1./n for n in range(1, 1001)],
656+
float.fromhex('0x1.df11f45f4e61ap+2')),
657+
([(-1.)**n/n for n in range(1, 1001)],
658+
float.fromhex('-0x1.62a2af1bd3624p-1')),
659+
([1e16, 1., 1e-16], 10000000000000002.0),
660+
([1e16-2., 1.-2.**-53, -(1e16-2.), -(1.-2.**-53)], 0.0),
661+
# exercise code for resizing partials array
662+
([2.**n - 2.**(n+50) + 2.**(n+52) for n in range(-1074, 972, 2)] +
663+
[-2.**1022],
664+
float.fromhex('0x1.5555555555555p+970')),
665+
]
666+
667+
# Telescoping sum, with exact differences (due to Sterbenz)
668+
terms = [1.7**i for i in range(1001)]
669+
test_values.append((
670+
[terms[i+1] - terms[i] for i in range(1000)] + [-terms[1000]],
671+
-terms[0]
672+
))
673+
674+
for i, (vals, expected) in enumerate(test_values):
675+
try:
676+
actual = math.fsum(vals)
677+
except OverflowError:
678+
self.fail("test %d failed: got OverflowError, expected %r "
679+
"for math.fsum(%.100r)" % (i, expected, vals))
680+
except ValueError:
681+
self.fail("test %d failed: got ValueError, expected %r "
682+
"for math.fsum(%.100r)" % (i, expected, vals))
683+
self.assertEqual(actual, expected)
684+
685+
from random import random, gauss, shuffle
686+
for j in range(1000):
687+
vals = [7, 1e100, -7, -1e100, -9e-20, 8e-20] * 10
688+
s = 0
689+
for i in range(200):
690+
v = gauss(0, random()) ** 7 - s
691+
s += v
692+
vals.append(v)
693+
shuffle(vals)
694+
695+
s = msum(vals)
696+
self.assertEqual(msum(vals), math.fsum(vals))
698697

699698

700699
# Python 3.9
@@ -1559,12 +1558,12 @@ def testIsinf(self):
15591558
# self.assertTrue(math.isnan(math.nan))
15601559

15611560
# TODO: RUSTPYTHON
1562-
# @requires_IEEE_754
1563-
# def test_inf_constant(self):
1564-
# self.assertTrue(math.isinf(math.inf))
1565-
# self.assertGreater(math.inf, 0.0)
1566-
# self.assertEqual(math.inf, float("inf"))
1567-
# self.assertEqual(-math.inf, float("-inf"))
1561+
@requires_IEEE_754
1562+
def test_inf_constant(self):
1563+
self.assertTrue(math.isinf(math.inf))
1564+
self.assertGreater(math.inf, 0.0)
1565+
self.assertEqual(math.inf, float("inf"))
1566+
self.assertEqual(-math.inf, float("-inf"))
15681567

15691568
# RED_FLAG 16-Oct-2000 Tim
15701569
# While 2.0 is more consistent about exceptions than previous releases, it

vm/src/stdlib/math.rs

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use statrs::function::gamma::{gamma, ln_gamma};
1111
use crate::builtins::float::{self, IntoPyFloat, PyFloatRef};
1212
use crate::builtins::int::{self, PyInt, PyIntRef};
1313
use crate::function::{Args, OptionalArg};
14-
use crate::pyobject::{BorrowValue, Either, PyObjectRef, PyResult, TypeProtocol};
14+
use crate::pyobject::{BorrowValue, Either, PyIterable, PyObjectRef, PyResult, TypeProtocol};
1515
use crate::vm::VirtualMachine;
1616
use rustpython_common::float_ops;
1717

@@ -372,6 +372,100 @@ fn math_lcm(args: Args<PyIntRef>) -> BigInt {
372372
math_perf_arb_len_int_op(args, |x, y| x.lcm(y.borrow_value()), BigInt::one())
373373
}
374374

375+
fn math_fsum(iter: PyIterable<IntoPyFloat>, vm: &VirtualMachine) -> PyResult<f64> {
376+
let mut iter = iter.iter(vm)?;
377+
let mut partials = vec![];
378+
let mut special_sum = 0.0;
379+
let mut inf_sum = 0.0;
380+
381+
while let Some(obj) = iter.next() {
382+
let mut x = obj?.to_f64();
383+
384+
let xsave = x;
385+
let mut j = 0;
386+
// This inner loop applies `hi`/`lo` summation to each
387+
// partial so that the list of partial sums remains exact.
388+
for i in 0..partials.len() {
389+
let mut y: f64 = partials[i];
390+
if x.abs() < y.abs() {
391+
let t = x;
392+
x = y;
393+
y = t;
394+
}
395+
// Rounded `x+y` is stored in `hi` with round-off stored in
396+
// `lo`. Together `hi+lo` are exactly equal to `x+y`.
397+
let hi = x + y;
398+
let lo = y - (hi - x);
399+
if lo != 0.0 {
400+
partials[j] = lo;
401+
j += 1;
402+
}
403+
x = hi;
404+
}
405+
406+
if !x.is_finite() {
407+
// a nonfinite x could arise either as
408+
// a result of intermediate overflow, or
409+
// as a result of a nan or inf in the
410+
// summands
411+
if xsave.is_finite() {
412+
return Err(vm.new_overflow_error("intermediate overflow in fsum".to_owned()));
413+
}
414+
if xsave.is_infinite() {
415+
inf_sum += xsave;
416+
}
417+
special_sum += xsave;
418+
// reset partials
419+
partials.clear();
420+
}
421+
422+
if j >= partials.len() {
423+
partials.push(x);
424+
} else {
425+
partials[j] = x;
426+
partials.truncate(j + 1);
427+
}
428+
}
429+
if special_sum != 0.0 {
430+
return if inf_sum.is_nan() {
431+
Err(vm.new_overflow_error("-inf + inf in fsum".to_owned()))
432+
} else {
433+
Ok(special_sum)
434+
};
435+
}
436+
437+
let mut n = partials.len();
438+
if n > 0 {
439+
n -= 1;
440+
let mut hi = partials[n];
441+
442+
let mut lo = 0.0;
443+
while n > 0 {
444+
let x = hi;
445+
446+
n -= 1;
447+
let y = partials[n];
448+
449+
hi = x + y;
450+
lo = y - (hi - x);
451+
if lo != 0.0 {
452+
break;
453+
}
454+
}
455+
if n > 0 && ((lo < 0.0 && partials[n - 1] < 0.0) || (lo > 0.0 && partials[n - 1] > 0.0)) {
456+
let y = lo + lo;
457+
let x = hi + y;
458+
if y == x - hi {
459+
hi = x;
460+
}
461+
}
462+
463+
Ok(hi)
464+
} else {
465+
Ok(0.0)
466+
}
467+
}
468+
375469
fn math_factorial(value: PyIntRef, vm: &VirtualMachine) -> PyResult<BigInt> {
376470
let value = value.borrow_value();
377471
if value.is_negative() {
@@ -516,6 +610,7 @@ pub fn make_module(vm: &VirtualMachine) -> PyObjectRef {
516610
"ldexp" => named_function!(ctx, math, ldexp),
517611
"modf" => named_function!(ctx, math, modf),
518612
"fmod" => named_function!(ctx, math, fmod),
613+
"fsum" => named_function!(ctx, math, fsum),
519614
"remainder" => named_function!(ctx, math, remainder),
520615

521616
// Rounding functions:

0 commit comments

Comments
 (0)