Own asin() function (with Taylor series) not accur

2019-08-17 15:15发布

问题:

I need to write my own asin() function without math.h library with the use of Taylor series. It works fine for numbers between <-0.98;0.98> but when I am close to limits it stops with 1604 iterations and therefore is inaccurate.

I don't know how to make it more accurete. Any suggestions are very appreciated!

The code is following:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define EPS 0.000000000001

double my_arcsin(double x)
{
    long double a, an, b, bn;
    a = an = 1.0;
    b = bn = 2.0;
    long double n = 3.0;
    double xn;
    double xs = x;
    double xp = x;

    int iterace = 0;

    xn = xs + (a/b) * (my_pow(xp,n) / n);

    while (my_abs(xn - xs) >= EPS)
    {
        n += 2.0;
        an += 2.0;
        bn += 2.0;
        a = a * an;
        b = b * bn;

        xs = xn;
        xn = xs + (a/b) * (my_pow(xp,n) / n);
        iterace++;
    }

    //printf("%d\n", iterace);

    return xn;
}

int main(int argc, char* argv[])
{

    double x = 0.0;

    if (argc > 2)
        x = strtod(argv[2], NULL);
    if (strcmp(argv[1], "--asin") == 0)
    {
           if (x < -1 || x > 1)
               printf("nan\n");
           else
           {
               printf("%.10e\n", my_arcsin(x));
               //printf("%.10e\n", asin(x));
           }

        return 0;
    }
}

And also a short list of my values and expected ones:

My values              Expected values        my_asin(x)
5.2359877560e-01       5.2359877560e-01       0.5
1.5567132089e+00       1.5707963268e+00       1      //problem
1.4292568534e+00       1.4292568535e+00       0.99   //problem
1.1197695150e+00       1.1197695150e+00       0.9
1.2532358975e+00       1.2532358975e+00       0.95

回答1:

PLEASE NOTICE: In this case I strongly recommend @Bence's method, since you can't expect a slowly convergent method with low data accuracy to obtain arbitrary precision.

However I'm willing to show you how to improve the result using your current algorithm.

The main problem is that a and b grows too fast and soon become inf (after merely about 150 iterations). Another similar problem is my_pow(xp,n) grows fast when n grows, however this doesn't matter much in this very case since we could assume the input data goes inside the range of [-1, 1].

So I've just changed the method you deal with a/b by introducing ab_ratio, see my edited code:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define EPS 0.000000000001

#include <math.h>
#define my_pow powl
#define my_abs fabsl

double my_arcsin(double x)
{
    #if 0
    long double a, an, b, bn;
    a = an = 1.0;
    b = bn = 2.0;
    #endif
    unsigned long _n = 0;
    long double ab_ratio = 0.5;
    long double n = 3.0;
    long double xn;
    long double xs = x;
    long double xp = x;

    int iterace = 0;

    xn = xs + ab_ratio * (my_pow(xp,n) / n);
    long double step = EPS;

    #if 0
    while (my_abs(step) >= EPS)
    #else
    while (1) /* manually stop it */
    #endif
    {
        n += 2.0;
        #if 0
        an += 2.0;
        bn += 2.0;
        a = a * an;
        b = b * bn;
        #endif
        _n += 1;
        ab_ratio *= (1.0 + 2.0 * _n) / (2.0 + 2.0 * _n);

        xs = xn;
        step = ab_ratio * (my_pow(xp,n) / n);
        xn = xs + step;
        iterace++;
        if (_n % 10000000 == 0)
            printf("%lu %.10g %g %g %g %g\n", _n, (double)xn, (double)ab_ratio, (double)step, (double)xn, (double)my_pow(xp, n));
    }

    //printf("%d\n", iterace);

    return xn;
}

int main(int argc, char* argv[])
{

    double x = 0.0;

    if (argc > 2)
        x = strtod(argv[2], NULL);
    if (strcmp(argv[1], "--asin") == 0)
    {
           if (x < -1 || x > 1)
               printf("nan\n");
           else
           {
               printf("%.10e\n", my_arcsin(x));
               //printf("%.10e\n", asin(x));
           }

        return 0;
    }
}

For 0.99 (and even 0.9999999) it soon gives correct results with more than 10 significant digits. However it gets slow when getting near to 1.
Actually the process has been running for nearly 12 minutes on my laptop calculating --asin 1, and the current result is 1.570786871 after 3560000000 iterations.

UPDATED: It's been 1h51min now and the result 1.570792915 and iteration count is 27340000000.



回答2:

Even though the convergence radius of the series expansion you are using is 1, therefore the series will eventually converge for -1 < x < 1, convergence is indeed painfully slow close to the limits of this interval. The solution is to somehow avoid these parts of the interval.

I suggest that you

  • use your original algorithm for |x| <= 1/sqrt(2),
  • use the identity arcsin(x) = pi/2 - arcsin(sqrt(1-x^2)) for 1/sqrt(2) < x <= 1.0,
  • use the identity arcsin(x) = -pi/2 + arcsin(sqrt(1-x^2)) for -1.0 <= x < -1/sqrt(2).

This way you can transform your input x into [-1/sqrt(2),1/sqrt(2)], where convergence is relatively fast.