Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better expm1 #117

Closed
tcaduser opened this issue Aug 4, 2024 · 7 comments
Closed

Better expm1 #117

tcaduser opened this issue Aug 4, 2024 · 7 comments

Comments

@tcaduser
Copy link

tcaduser commented Aug 4, 2024

https://github.com/j-fu/VoronoiFVM.jl/blob/975d5b1f9a2d21988dd84d61f4e58b76769d5250/src/vfvm_functions.jl#L79

To avoid precision loss, please consider using the Julia function for expm1, which offers better precision for small arguments:
https://docs.julialang.org/en/v1/base/math/#Base.expm1

@j-fu
Copy link
Member

j-fu commented Aug 4, 2024

Thanks for the hint! Not sure why I was not aware of that...

But it is not that bad now either:

julia> x=1.0e-15
1.0e-15

julia> x/expm1(x)
0.9999999999999994

julia> VoronoiFVM.bernoulli_horner(x)
0.9999999999999994

So I need to see what I can gain. Will try it out.

@tcaduser
Copy link
Author

tcaduser commented Aug 4, 2024

I checked it in python, and the two formulas match to within 1e-16 at your threshold, which is at the limits of float64.

I guess the only difference would be the speed difference in the two implementations in julia. The implementation I use in my C++ solver is equivalent to:

y = expm1(x)
// this test may only be true for x == 0
if (x != y)
{
  b = x/y;
}
else
{
  // including x is probably overkill
  b = 1 / (1 + 0.5*x);
}

I am curious though about:
https://github.com/j-fu/VoronoiFVM.jl/blob/975d5b1f9a2d21988dd84d61f4e58b76769d5250/src/vfvm_functions.jl#L82

and why not use:

bm = bp + x

as done here:
https://github.com/j-fu/VoronoiFVM.jl/blob/975d5b1f9a2d21988dd84d61f4e58b76769d5250/src/vfvm_functions.jl#L86

@j-fu
Copy link
Member

j-fu commented Aug 6, 2024

Well, I don't recall why...
I started a PR with an implentation using expm1 which indeed appears to be more robust.
However I indeed will keep the Horner scheme around zero. Bernoulli function with expm1 is good for small arguments, however the automatic derivative (which I use to create the Jacobians) fails:

julia> B(x)=x/expm1(x);

julia> B0(x)=1/(1+0.5x);

julia> B0(0.0)
1.0

julia> B0(nextfloat(0.0))
1.0

julia> ForwardDiff.derivative(B0,0)
-0.5

julia> ForwardDiff.derivative(B0,nextfloat(0.0))
-0.5

julia> ForwardDiff.derivative(B,nextfloat(0.0))
NaN

@j-fu
Copy link
Member

j-fu commented Aug 6, 2024

As for the approximations which are now in the PR:
bernoulli_posarg

bernoulli_negarg

bernoulli_derivative

... thanks for nerd sniping me!

@tcaduser
Copy link
Author

Very cool. I had to look up what Nerd Sniping was.

@tcaduser
Copy link
Author

In my code, this is what I end up using for the derivative of B(x):

    const auto ex1 = expm1(x);

    if (x != ex1)
    {
      const auto ex2 = ex1 - (x * exp(x));
      ret = ex2;
      ret *= pow(ex1, -2);
    }
    else
    {
      DoubleType num = static_cast<DoubleType>(-0.5);
      DoubleType den = static_cast<DoubleType>( 1.0);
      num -= x / static_cast<DoubleType>(3.);
      den += x;
      ret = num / den;
    }

and so it works well even with extended precision. The full implementation is here:
https://github.com/devsim/devsim/blob/main/src/MathEval/Bernoulli.cc#L102

where I have to account for numerical issues I was having with the Android version of expm1 not working well with the x != expm1(x) test.

j-fu added a commit that referenced this issue Aug 20, 2024
@j-fu
Copy link
Member

j-fu commented Aug 20, 2024

So finally, thanks for this discussion, which lead to a more concise implementation.

@j-fu j-fu closed this as completed Aug 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants