# Elegant high precision log1p?

Sometimes it is hard to understand how numerical expressions are evaluated. I remember reading claims by Wolfram on how smart the Kernel is to evaluate expressions trees numerically by recognizing patterns, yet I don't see how it applies in very simple examples.

This question is about numerics, but to see if symbolics can help in an elegant way.

It is known that when working with finite precision, the function $\log(1+x)$ should have a special implementation for small $x$. That is why functions like log1p exists in many libraries (on top of log). For example: http://gnu.ist.utl.pt/software/gsl/manual/html_node/Elementary-Functions.html#Elementary-Functions

/*C code*/ log(1. + 1.e-15) == 1.11022e-15

/*C code*/ log1p(1.e-15) == 1.e-15

(The second version is more exact, the first is "wrong")

In Mathematica:

Log[1. + 1.*^-15] == 1.11022*10^-15

Mathematica doesn't have such Log1P function. One can say, well, that is because it doesn't need to, because of the symbolic power. In fact one can know the answer.

N[Log[1 + 1/10^15], 100] = 9.999999999999995000000000000003333333333333330833333333333335333333333333331666666666666668095238095*10^-16

But this is not general, if I want to evaluate Log[1 + x] and x has machine precision then I can't force to use something like log1p. Because 1+x will evaluate to a machine precision number.

These are my attempts:

x = 1.*^-15;
Log[1 + x]
Log[1. + x]
N[Log[1. + N[x, 20]], 20]
N[Log[1 + N[x, 20]], 20]


All evaluate to the wrong answer (1.11022*10^-15)

Finally I find this expression,

N[Log[1 + Rationalize[x, 0]], 20]
9.999999999999995000*10^-16


But really? is it that hard to get log(1+x) for small x numerically? Do I have to roll my own Log1p?

-
"I remember reading claims by Wolfram on how smart the Kernel is" reminded me of blog.stephenwolfram.com/2012/12/… –  belisarius Dec 20 '13 at 1:16
We also need the inverse of log1p, expm1, for E^x - 1 when x is small. –  Ray Koopman Dec 20 '13 at 5:34

I don't think Mathematica has that function. Seems like it should. (Same for expm1().) You should not need to resort to non-machine arithmetic to get the right answer.

Here is something that will do the trick using only machine arithmetic, if the input is a machine number:

log1p[x_] :=
If[MachineNumberQ[x],
If[x < 0.5,
If[# - 1 == 0, x,
x Divide[Log[#], # - 1]] &[1 + x],
Log[1 + x]],
Log[1 + x]]


If it's not a machine number, then it just gives you Log[1+x].

You can compile the machine number portion for much faster execution (by two orders of magnitude!):

log1px = Compile[{{x, _Real}},
If[x < 0.5,
If[# - 1 == 0, x,
x Divide[Log[#], # - 1]] &[1 + x],
Log[1 + x]], CompilationTarget -> "C"]


The cutoff for x >= 0.5 is where Log[1+x] works just fine, with only the least significant bit varying:

Plot[Log[1 + x]/x - N[N[Log[1 + Rationalize[x, 0]], 34]]/x, {x, 0.01,
1}, PlotPoints -> 100, PlotRange -> All]


-
"You should not need to resort to non-machine arithmetic to get the right answer.", that is my point, any solution I see is an overkill in my opinion. Does it help to Compile the function? –  alfC Dec 20 '13 at 6:42
Yes, it helps a lot. Without compiling, running it on a million random numbers in 0..0.1, I got about six seconds with no compilation, 0.4 seconds compiled to the MVM, and 0.07 seconds compiled to C. –  Mark Adler Dec 20 '13 at 7:30
Oh, and using arbitrary-precision numbers takes 87 seconds. –  Mark Adler Dec 20 '13 at 7:37
Thanks for the tests! Ray's answer (and maybe Rojo's) seems to be elegant how does it compare? –  alfC Dec 20 '13 at 7:43
Rojo's is nice if you don't mind slow (the 87 seconds example). Ray's uses the exact same formula as mine, but does needless operations when there is no precision loss when adding 1 and x. Also the use of With might result in calculating 1+x more than once. My use of a pure function assures that it is calculated once. –  Mark Adler Dec 20 '13 at 7:51

David Goldberg ("What every computer scientist should know about floating-point arithmetic", ACM Computing Surveys, Vol 23, No 1, March 1991, p 12, Th 4) gives pseudocode that is equivalent to

log1p[x_Real] := With[{w = 1 + x}, If[w-1 == 0, x, x*Log@w/(w-1)]]


EDIT - Following Mark Adler's comments, I checked the binary representation of the results (using RealDigits[#,2,53]) for x in Range[1.,5.,.25]*2^-52 against the value returned by setting the precision to 35, and he is right on both counts: the comparison should be w-1 == 0, not w === 1., and the division should use the a/b form, not the Divide[a,b] form. I have changed the code accordingly.

LogLogPlot[{log1p[x], Log[1 + x]}, {x, 1*^-17, 1*^-14}]


-
Very elegant solution. I hope you don't mind that I added a plot to illustrate the result. (Probably the same as most of other answers). –  alfC Dec 20 '13 at 7:34
Same answer as mine, except it does needless operations for large x. –  Mark Adler Dec 20 '13 at 7:36
@Mark Yes, I omitted a check for large x. I also used 1. instead of the 1 that would handle cases in which x has more than machine precision. As for being the same, note that w === 1. is not the same as w - 1 == 0, and Divide[a,b] is not the same as a/b for machine precision numbers. –  Ray Koopman Dec 20 '13 at 8:20
@MarkAdler according to Daniel Lichtblau recently in comments on another answer, a/b converts into a*1/b before evaluation, whereas Divide does the division directly. This could be important when we work with machine-precision numbers, but is not documented anywhere AFAIK. –  Oleksandr R. Dec 20 '13 at 8:57
FYI, I get very slightly better results with mine using Divide instead of /. The root-mean-square error goes down from 1.19*(10^-16) to 1.13*(10^-16). I have updated mine to use Divide. –  Mark Adler Dec 21 '13 at 5:04

Perhaps

SetAttributes[log1p, NumericFunction];

N[log1p[x_?MachineNumberQ], _] := N@log1p[SetPrecision[x, $MachinePrecision]]; N[log1p[x_?NumericQ], {MachinePrecision, MachinePrecision}] := N@N[log1p[x],$MachinePrecision];

N[log1p[x_?NumericQ], a_] := N[Log[1 + x], a]


EDIT This probably makes more sense

ClearAll[log1p]

log1p[x_?MachineNumberQ] := N@log1p[SetPrecision[x, \$MachinePrecision]];
log1p[x_?InexactNumberQ] := Log[1 + x];

-
Nice defination. But the underlying Precision and Accuracy can go wrong for some cases? –  Silvia Dec 20 '13 at 6:35
How is this function used? I get back the unevaluated log1p[1.*^-15. –  alfC Dec 20 '13 at 6:51
@alfC, only through N, but you are right, I should have made it evaluate automatically. I am not sure why I went this road. Probably sleep depravation –  Rojo Dec 20 '13 at 7:34

I think that is because 1. and 1.*^-15 are machine-precision numbers, and Mathematica does NOT do precision-tracking on machine-precision calculations. I suggust using the arbitrary-precision numbers instead.

Through[{Precision, Accuracy}@#] & /@ {1., 1.*^-15}


{{MachinePrecision, 15.9546}, {MachinePrecision, 30.9546}}

Now we specify a precision to the numbers to make them arbitrary-precision numbers:

res = Log[1.20 + 1.20*^-15]


1.000*10^-15

Through[{Precision, Accuracy}[res]]


{5., 20.}

And it won't be wrong even for low precision cases:

res2 = Log[1.5 + 1.5*^-15]


0.*10^-5

Through[{Precision, Accuracy}[res2]]


{0., 5.}

-