Improve accuracy of log1p() (#803)

Patch, ported from Tim Peters version at: https://github.com/mpmath/mpmath/issues/790#issuecomment-2084094631

Co-authored-by: Tim Peters <tim.peters@gmail.com>
This commit is contained in:
Sergey B Kirpichev
2024-08-02 07:29:15 +03:00
committed by GitHub
parent 716a750760
commit 7596816fbe
2 changed files with 81 additions and 3 deletions
+74 -3
View File
@@ -166,9 +166,80 @@ def expm1(ctx, x):
def log1p(ctx, x):
if not x:
return ctx.zero
if ctx.mag(x) < -ctx.prec:
return x - 0.5*x**2
return ctx.log(ctx.fadd(1, x, prec=2*ctx.prec))
LOG1P_EXTRAPREC = 10 # ctx._wrap_specfun()
# Note that all cases could by handled by log(1+c) provided the
# add is done exactly. Our aim here is to be much faster than that,
# especially when |c| is small.
c = ctx.convert(x)
cmag = ctx.mag(c)
a, b = c.real, c.imag
wp = ctx.prec
if cmag >= -wp:
# |c| isn't very small. We call log(1+c) instead, but
# are careful about the precision used by the add. The
# real part of the result is log(|c+1|). That's
# determined by 1 + 2*a + a**2 + b**2, and the add has
# to preserve enough info so that no important bits of
# that sum are lost. It doesn't matter to this that 2*a,
# a**2, etc, are not computed explicitly here: we're
# deducing how many bits have to be present in the sum
# for log() to "reverse engineer" the value of 2*a +
# a**2 + b**2 to `prec` good bits,
if cmag < 4:
# |c| isn't very small, or large.
if ctx.mag(a) > ctx.mag(b):
# `a` already contributes the most to c's norm.
# After adding 1, it will utterly dominate it.
# We only need enough extra precision to avoid
# losing any of a's `prec` most significant bits
# when addiog, `b**2` is too small to matter.
wp *= 2
else:
# b**2 is the larger of the square terms. The
# smallest b can be is about 2**-prec, so the
# smallest b**2 can be is about 2**(-2*prec). So
# for a bit to matter compared to b**2, it has
# to be at least about 2**(-3*prec). Bits of 2*a
# (if any) >= 2**(-3*prec) will be preserved if
# we use 3*prec bits for the add.
wp *= 3
# Else (cmag >= 4), |c+1| >= |c| - 1 is so large that
# working precision is fine (although that takes some
# careful analysis for cmag=4, given that .mag() _may_
# return a rexult too large by 2), So leave wp alone.
arg = ctx.fadd(1.0, c, prec=wp)
result = ctx.log(arg)
else:
# Else c is "very small", and we use a series expansion,
# c - c**2/2. The real part of that is a+(b*b-a*a)/2,
# and the imag part b-a*b. Given that cmag < -prec, it
# can be shown that "a*b" is numerically insignifcant in
# the imag part, and _usually_ the "a*a/2" in the real
# part. What remains is cheap to compute. In the real
# part, though, if `a` is negative, the remaining
# a+b**2/2 can suffer massive cancellation - even total.
real = a + b*b*0.5 # usually the real part of the result
if (a < 0.0
and ctx.mag(real) <= ctx.mag(a) - LOG1P_EXTRAPREC):
# The guard bits were lost to cancellation. Rare. At
# the contrived
# -1.999999873062092e-40+1.999999936531045e-20j
# _all_ bits cancel out. Since a ~= -b*b/2 in this
# case, and |b| is at largest (worst case) about
# 2**-prec, |a| is about 2**(-2*prec), and the true
# result may be as small as a**2/2, which is about
# 2**(-4*prec), of which we want the leading prec
# bits. To get the leading prec bits starting at
# 2**(-4**prec) from addends starting at
# 2**-(2*prec), we need the subtraction to handle
# 3*prec bits (the first 2*prec of which may cancel
# to exactly 0).
a2 = a*a # only need at worst prec bits
b2 = ctx.fmul(b, b, prec=2*wp)
diff = ctx.fsub(b2, a2, prec=3*wp)
real = a + ctx.ldexp(diff, -1)
result = ctx.mpc(real, b)
return result
@defun_wrapped
def powm1(ctx, x, y):
+7
View File
@@ -967,6 +967,13 @@ def test_log1p():
assert log1p(inf) == inf
assert log1p(1e-50).ae(1e-50)
assert (log1p(1e-10)*1e10).ae(0.99999999995)
# issue 790
assert log1p(1.8370676479640493e-39-4.6885882517313053e-20j).ae(2.93621063767769e-39-4.6885882517313053e-20j, 0)
assert log1p(-2.0476815825463086e-80-2.0235857941734692e-40j).ae(-2.3184935597344513e-84-2.0235857941734692e-40j, 0)
assert log1p(-6.4922176418510124e-21+1.1394926627101214e-10j).ae(-1.4201199664289643e-37+1.1394926627101214e-10j, 0)
assert log1p(-1.430796815051627e-72+1.691624553529315e-36j).ae(4.6709293580298264e-91+1.691624553529315e-36j, 0)
assert log1p(-3.1061140011623543e-21+7.881768838480807e-11j).ae(4.3173401185459216e-38+7.881768838480807e-11j, 0)
assert log1p(-1.999999873062092e-40+1.999999936531045e-20j).ae(1.9999997461241924e-80+1.999999936531045e-20j)
def test_powm1():
assert powm1(2,3) == 7