16

I am trying to implement range reduction as the first step of implementing the sine function.

I am following the method described in the paper "ARGUMENT REDUCTION FOR HUGE ARGUMENTS" by K.C. NG

I am getting error as large as 0.002339146 when using the input range of x from 0 to 20000. My error obviously shouldn't be that large, and I'm not sure how I can reduce it. I noticed that the error magnitude is associated with the input theta magnitude to cosine/sine.

I was able to obtain the nearpi.c code that the paper mentions, but I'm not sure how to utilize the code for single precision floating point. If anyone is interested, the nearpi.c file can be found at this link: nearpi.c

Here is my MATLAB code:

x = 0:0.1:20000;

% Perform range reduction
% Store constant 2/pi
twooverpi = single(2/pi);

% Compute y
y = (x.*twooverpi);

% Compute k (round to nearest integer
k = round(y);

% Solve for f
f = single(y-k);

% Solve for r
r = single(f*single(pi/2));

% Find last two bits of k
n = bitand(fi(k,1,32,0),fi(3,1,32,0));
n = single(n);

% Preallocate for speed
z(length(x)) = 0;
for i = 1:length(x)

    switch(n(i))
        case 0
            z(i)=sin(r(i));
        case 1
            z(i) = single(cos(r(i)));
        case 2
            z(i) = -sin(r(i));
        case 3
            z(i) = single(-cos(r(i)));
        otherwise
    end

end

maxerror = max(abs(single(z - single(sin(single(x))))))
minerror = min(abs(single(z - single(sin(single(x))))))

I have edited the program nearpi.c so that it compiles. However I am not sure how to interpret the output. Also the file expects an input, which I had to input by hand, also I am not sure of the significance of the input.

Here is the working nearpi.c:

/*
 ============================================================================
 Name        : nearpi.c
 Author      : 
 Version     :
 Copyright   : Your copyright notice
 Description : Hello World in C, Ansi-style
 ============================================================================
 */

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


/*
 * Global macro definitions.
 */

# define hex( double )  *(1 + ((long *) &double)), *((long *) &double)
# define sgn(a)         (a >= 0 ? 1 : -1)
# define MAX_k          2500
# define D              56
# define MAX_EXP        127
# define THRESHOLD      2.22e-16

/*
 *  Global Variables
 */

int     CFlength,               /* length of CF including terminator */
        binade;
double  e,
        f;                      /* [e,f] range of D-bit unsigned int of f;
                                   form 1X...X */

// Function Prototypes
int dbleCF (double i[], double j[]);
void input (double i[]);
void nearPiOver2 (double i[]);


/*
 *  This is the start of the main program.
 */

int main (void)
{
    int     k;                  /* subscript variable */
    double  i[MAX_k],
            j[MAX_k];           /* i and j are continued fractions
                                   (coeffs) */


   // fp = fopen("/src/cfpi.txt", "r");


/*
 *  Compute global variables e and f, where
 *
 *      e = 2 ^ (D-1), i.e. the D bit number 10...0
 *  and
 *      f = 2 ^ D - 1, i.e. the D bit number 11...1  .
 */

    e = 1;
    for (k = 2; k <= D; k = k + 1)
        e = 2 * e;
    f = 2 * e - 1;

 /*
  *  Compute the continued fraction for  (2/e)/(pi/2)  , i.e.
  *  q's starting value for the first binade, given the continued
  *  fraction for  pi  as input; set the global variable CFlength
  *  to the length of the resulting continued fraction (including
  *  its negative valued terminator).  One should use as many
  *  partial coefficients of  pi  as necessary to resolve numbers
  *  of the width of the underflow plus the overflow threshold.
  *  A rule of thumb is 0.97 partial coefficients are generated
  *  for every decimal digit of  pi .
  *
  *  Note: for radix B machines, subroutine  input  should compute
  *  the continued fraction for  (B/e)/(pi/2)  where  e = B ^ (D - 1).
  */

    input (i);

/*
 *  Begin main loop over all binades:
 *  For each binade, find the nearest multiples of pi/2 in that binade.
 *
 *  [ Note: for hexadecimal machines ( B = 16 ), the rest of the main
 *  program simplifies(!) to
 *
 *                      B_ade = 1;
 *                      while (B_ade < MAX_EXP)
 *                      {
 *                          dbleCF (i, j);
 *                          dbleCF (j, i);
 *                          dbleCF (i, j);
 *                          CFlength = dbleCF (j, i);
 *                          B_ade = B_ade + 1;
 *                      }
 *                  }
 *
 *  because the alternation of source & destination are no longer necessary. ]
 */

    binade = 1;
    while (binade < MAX_EXP)
    {

/*
 *  For the current (odd) binade, find the nearest multiples of pi/2.
 */

        nearPiOver2 (i);

/*
 *  Double the continued fraction to get to the next (even) binade.
 *  To save copying arrays, i and j will alternate as the source
 *  and destination for the continued fractions.
 */

        CFlength = dbleCF (i, j);
        binade = binade + 1;

/*
 *  Check for main loop termination again because of the
 *  alternation.
 */

        if (binade >= MAX_EXP)
            break;

/*
 *  For the current (even) binade, find the nearest multiples of pi/2.
 */

        nearPiOver2 (j);

/*
 *  Double the continued fraction to get to the next (odd) binade.
 */

        CFlength = dbleCF (j, i);
        binade = binade + 1;
    }

    return 0;
}                               /* end of Main Program */

/*
 *  Subroutine  DbleCF  doubles a continued fraction whose partial
 *  coefficients are i[] into a continued fraction j[], where both
 *  arrays are of a type sufficient to do D-bit integer arithmetic.
 *
 *  In my case ( D = 56 ) , I am forced to treat integers as double
 *  precision reals because my machine does not have integers of
 *  sufficient width to handle D-bit integer arithmetic.
 *
 *  Adapted from a Basic program written by W. Kahan.
 *
 *  Algorithm based on Hurwitz's method of doubling continued
 *  fractions (see Knuth Vol. 3, p.360).
 *
 *  A negative value terminates the last partial quotient.
 *
 *  Note:  for the non-C programmers, the statement  break
 *  exits a loop and the statement  continue  skips to the next
 *  case in the same loop.
 *
 *  The call  modf ( l / 2, &l0 )  assigns the integer portion of
 *  half of L to L0.
 */

int dbleCF (double i[], double j[])
{
      double k,
                    l,
                    l0,
                    j0;
      int    n,
                    m;
    n = 1;
    m = 0;
    j0 = i[0] + i[0];
    l = i[n];
    while (1)
    {
        if (l < 0)
        {
            j[m] = j0;
            break;
        };
        modf (l / 2, &l0);
        l = l - l0 - l0;
        k = i[n + 1];
        if (l0 > 0)
        {
            j[m] = j0;
            j[m + 1] = l0;
            j0 = 0;
            m = m + 2;
        };
        if (l == 0) {
/*
 *  Even case.
 */
            if (k < 0)
            {
                m = m - 1;
                break;
            }
            else
            {
                j0 = j0 + k + k;
                n = n + 2;
                l = i[n];
                continue;
            };
        }
/*
 *  Odd case.
 */
        if (k < 0)
        {
            j[m] = j0 + 2;
            break;
        };
        if (k == 0)
        {
            n = n + 2;
            l = l + i[n];
            continue;
        };
        j[m] = j0 + 1;
        m = m + 1;
        j0 = 1;
        l = k - 1;
        n = n + 1;
        continue;
    };
    m = m + 1;
    j[m] = -99999;
    return (m);
}

/*
 *  Subroutine  input  computes the continued fraction for
 *  (2/e) / (pi/2) , where  e = 2 ^ (D-1) , given  pi 's
 *  continued fraction as input.  That is, double the continued
 *  fraction of  pi   D-3  times and place a zero at the front.
 *
 *  One should use as many partial coefficients of  pi  as
 *  necessary to resolve numbers of the width of the underflow
 *  plus the overflow threshold.  A rule of thumb is  0.97
 *  partial coefficients are generated for every decimal digit
 *  of  pi .  The last coefficient of  pi  is terminated by a
 *  negative number.
 *
 *  I'll be happy to supply anyone with the partial coefficients
 *  of  pi .  My ARPA address is  mcdonald@ucbdali.BERKELEY.ARPA .
 *
 *  I computed the partial coefficients of  pi  using a method of
 *  Bill Gosper's.  I need only compute with integers, albeit
 *  large ones.  After writing the program in  bc  and  Vaxima  ,
 *  Prof. Fateman suggested  FranzLisp .  To my surprise, FranzLisp
 *  ran the fastest!  the reason?   FranzLisp's  Bignum  package is
 *  hand coded in assembler.  Also,  FranzLisp  can be compiled.
 *
 *
 *  Note: for radix B machines, subroutine  input  should compute
 *  the continued fraction for  (B/e)/(pi/2)  where  e = B ^ (D - 1).
 *  In the case of hexadecimal ( B = 16 ), this is done by repeated
 *  doubling the appropriate number of times.
 */

void input (double i[])
{
    int     k;
    double  j[MAX_k];

/*
 *  Read in the partial coefficients of  pi  from a precalculated file
 *  until a negative value is encountered.
 */

    k = -1;
    do
    {
        k = k + 1;
        scanf ("%lE", &i[k]);
        printf("hello\n");
        printf("%d", k);
    } while (i[k] >= 0);

/*
 *  Double the continued fraction for  pi  D-3  times using
 *  i  and  j  alternately as source and destination.  On my
 *  machine  D = 56  so  D-3  is odd; hence the following code:
 *
 *  Double twice  (D-3)/2  times,
 */
    for (k = 1; k <= (D - 3) / 2; k = k + 1)
    {
        dbleCF (i, j);
        dbleCF (j, i);
    };
/*
 *  then double once more.
 */
    dbleCF (i, j);

/*
 *  Now append a zero on the front (reciprocate the continued
 *  fraction) and the return the coefficients in  i .
 */

    i[0] = 0;
    k = -1;
    do
    {
        k = k + 1;
        i[k + 1] = j[k];
    } while (j[k] >= 0);

/*
 *  Return the length of the continued fraction, including its
 *  terminator and initial zero, in the global variable CFlength.
 */

    CFlength = k;
}

/*
 *  Given a continued fraction's coefficients in an array  i ,
 *  subroutine  nearPiOver2  finds all machine representable
 *  values near a integer multiple of  pi/2  in the current binade.
 */

void nearPiOver2 (double i[])
{
    int     k,                  /* subscript for recurrences    (see
                                   handout) */
            K;                  /* like  k , but used during cancel. elim.
                                   */
    double  p[MAX_k],           /* product of the q's           (see
                                   handout) */
            q[MAX_k],           /* successive tail evals of CF  (see
                                   handout) */
            j[MAX_k],           /* like convergent numerators   (see
                                   handout) */
            tmp,                /* temporary used during cancellation
                                   elim. */
            mk0,                /* m[k - 1]                     (see
                                   handout) */
            mk,                 /* m[k] is one of the few ints  (see
                                   handout) */
            mkAbs,              /* absolute value of m sub k
                                */
            mK0,                /* like  mk0 , but used during cancel.
                                   elim. */
            mK,                 /* like  mk  , but used during cancel.
                                   elim. */
            z,                  /* the object of our quest (the argument)
                                */
            m0,                 /* the mantissa of z as a D-bit integer
                                */
            x,                  /* the reduced argument         (see
                                   handout) */
            ldexp (),           /* sys routine to multiply by a power of
                                   two  */
            fabs (),            /* sys routine to compute FP absolute
                                   value   */
            floor (),           /* sys routine to compute greatest int <=
                                   value   */
            ceil ();            /* sys routine to compute least int >=
                                   value   */

 /*
  *  Compute the q's by evaluating the continued fraction from
  *  bottom up.
  *
  *  Start evaluation with a big number in the terminator position.
  */

    q[CFlength] = 1.0 + 30;

    for (k = CFlength - 1; k >= 0; k = k - 1)
        q[k] = i[k] + 1 / q[k + 1];

/*
 *  Let  THRESHOLD  be the biggest  | x |  that we are interesed in
 *  seeing.
 *
 *  Compute the p's and j's by the recurrences from the top down.
 *
 *  Stop when
 *
 *        1                          1
 *      -----   >=  THRESHOLD  >   ------    .
 *      2 |j |                     2 |j  |
 *          k                          k+1
 */

    p[0] = 1;
    j[0] = 0;
    j[1] = 1;
    k = 0;
    do
    {
        p[k + 1] = -q[k + 1] * p[k];
        if (k > 0)
            j[1 + k] = j[k - 1] - i[k] * j[k];
        k = k + 1;
    } while (1 / (2 * fabs (j[k])) >= THRESHOLD);

/*
 *  Then  mk  runs through the integers between
 *
 *                  k        +                   k        +
 *              (-1)  e / p  -  1/2     &    (-1)  f / p  -  1/2  .
 *                         k                            k
 */

    for (mkAbs = floor (e / fabs (p[k]));
            mkAbs <= ceil (f / fabs (p[k])); mkAbs = mkAbs + 1)
    {

        mk = mkAbs * sgn (p[k]);

/*
 *  For each  mk ,  mk0  runs through integers between
 *
 *                    +
 *              m  q  -  p  THRESHOLD  .
 *               k  k     k
 */

        for (mk0 = floor (mk * q[k] - fabs (p[k]) * THRESHOLD);
                mk0 <= ceil (mk * q[k] + fabs (p[k]) * THRESHOLD);
                mk0 = mk0 + 1)
        {

/*
 *  For each pair  { mk ,  mk0 } , check that
 *
 *                             k
 *              m       =  (-1)  ( j   m  - j  m   )
 *               0                  k-1 k    k  k-1
 */
            m0 = (k & 1 ? -1 : 1) * (j[k - 1] * mk - j[k] * mk0);

/*
 *  lies between  e  and  f .
 */
            if (e <= fabs (m0) && fabs (m0) <= f)
            {

/*
 *  If so, then we have found an
 *
 *                              k
 *              x       =  ((-1)  m  / p  - m ) / j
 *                                 0    k    k     k
 *
 *                      =  ( m  q  - m   ) / p  .
 *                            k  k    k-1     k
 *
 *  But this later formula can suffer cancellation.  Therefore,
 *  run the recurrence for the  mk 's  to get  mK  with minimal
 *   | mK | + | mK0 |  in the hope  mK  is  0  .
 */
                K = k;
                mK = mk;
                mK0 = mk0;
                while (fabs (mK) > 0)
                {
                    p[K + 1] = -q[K + 1] * p[K];
                    tmp = mK0 - i[K] * mK;
                    if (fabs (tmp) > fabs (mK0))
                        break;
                    mK0 = mK;
                    mK = tmp;
                    K = K + 1;
                };

/*
 *  Then
 *              x       =  ( m  q  - m   ) / p
 *                            K  K    K-1     K
 *
 *  as accurately as one could hope.
 */
                x = (mK * q[K] - mK0) / p[K];

/*
 *  To return  z  and  m0  as positive numbers,
 *   x  must take the sign of  m0  .
 */
                x = x * sgn (m0);
                m0 = fabs (m0);

/*d
 *  Set  z = m0 * 2 ^ (binade+1-D) .
 */
                z = ldexp (m0, binade + 1 - D);

/*
 *  Print  z (hex),  z (dec),  m0 (dec),  binade+1-D,  x (hex), x (dec).
 */

                printf ("%08lx %08lx    Z=%22.16E    M=%17.17G    L+1-%d=%3d    %08lx %08lx    x=%23.16E\n", hex (z), z, m0, D, binade + 1 - D, hex (x), x);

            }
        }
    }
}
Veridian
  • 3,531
  • 12
  • 46
  • 80
  • IIRC accurate range reduction for trig functions requires a lot of accuracy both for pi and for the remainder; typically several hundred bits are used in math libraries. – janneb Feb 24 '12 at 09:53
  • 3
    +1 -- interesting paper (just had time to skim, will have to read it more carefully later) – Jason S Feb 28 '12 at 00:55
  • @starbox: I've read if before, yes. I'm not familiar with doing high-precision arithmethic with matlab, so I'll refrain from suggesting you how to do it. – janneb Feb 29 '12 at 21:11
  • The input contains the continued fraction of pi, which is necessary to calculate the everything with the mentioned accuracy, which is mentioned in section 2.3 and referenced as ref. [6]. So you might want to get your hands on that publication and write a small program which generates the input file for nearpi.c. Maybe [wolfram](http://mathworld.wolfram.com/PiContinuedFraction.html) helps here. – Bort Mar 02 '12 at 12:47
  • If I use the numbers of the given series as input, nearpi.c works as expected. [input file](http://pastebin.com/EqXThM6n), -1 indicates the end of the continued fraction input. usage: `cat confrac.txt | ./a.out`, [here](http://oeis.org/A001203/b001203.txt): you can get the first 20000 of the continued fraction series. – Bort Mar 02 '12 at 12:54
  • ups, sorry the file shouldn't be comma separated.. – Bort Mar 02 '12 at 13:03
  • doing all this on float requires quite some reworking of the code, can't you do the calculation in double precision and cast it back to float if you need to? Alternatively, you could call the calculation as a C function from matlab. – Bort Mar 02 '12 at 13:10
  • as far as I understand it. The output e.g.`bc60cbfa03602a72 400921fb54442d18 Z=3.1415926535897931E+00 M=56593902016227520 L+1-56=-54 00000000 bc60cbfa03602a72 x=-7.2844437289760273E-18` means: Hexpattern of Z, Z = M*2^L = a floating point number very close to some integer number * pi/2 here: Z = 56593902016227520*2^-54, M the mantisse in the binary system, L the corresponding exponent, Hexpattern of x (calculated only by x*B (see paper) which leads to prefixed zeros but identical trailing hexpattern as Z), x = difference between z calculated with mantissa and exponent to pi/2 – Bort Mar 02 '12 at 18:01
  • I am quite sure about M, L, and Z ... no so for x yet... – Bort Mar 02 '12 at 18:10
  • @Bort, yes that is what I was thinking, but I wasn't sure about x, because don't you want x to be as small as possible? I noticed the last printed x isn't the smallest value. – Veridian Mar 03 '12 at 00:07
  • @starbox I think you're missing the part that simulates the multi-precision procut `y=x*B`. You first have to make your code work properly for double-precision numbers. – user1071136 Mar 03 '12 at 13:31
  • @user1071136, missing a part? Did you compare the linked nearpi.c (at the top of the question) to the edited code I posted? The code is meant for double-precision numbers, I haven't changed anything to make it specific to single precision. – Veridian Mar 03 '12 at 16:28

1 Answers1

10

Theory

First let's note the difference using single-precision arithmetic makes.

  1. [Equation 8] The minimal value of f can be larger. As double-precision numbers are a super-set of the single-precision numbers, the closest single to a multiple of 2/pi can only be farther away then ~2.98e-19, therefore the number of leading zeros in fixed-arithmetic representation of f must be at most 61 leading zeros (but will probably be less). Denote this quantity fdigits.

  2. [Equation Before 9] Consequently, instead of 121 bits, y must be accurate to fdigits + 24 (non-zero significant bits in single-precision) + 7 (extra guard bits) = fdigits + 31, and at most 92.

  3. [Equation 9] "Therefore, together with the width of x's exponent, 2/pi must contain 127 (maximal exponent of single) + 31 + fdigits, or 158 + fdigits and at most 219 bits.

  4. [Subsection 2.5] The size of A is determined by the number of zeros in x before the binary point (and is unaffected by the move to single), while the size of C is determined by Equation Before 9.

    • For large x (x>=2^24), x looks like this: [24 bits, M zeros]. Multiplying it by A, whose size is the first M bits of 2/pi, will result in an integer (the zeros of x will just shift everything into the integers).
    • Choosing C to be starting from the M+d bit of 2/pi will result in the product x*C being of size at most d-24. In double precision, d is chosen to be 174 (and instead of 24, we have 53) so that the product will be of size at most 121. In single, it is enough to choose d such that d-24 <= 92, or more precisely, d-24 <= fdigits+31. That is, d can be chosen as fdigits+55, or at most 116.
    • As a result, B should be of size at most 116 bits.

We are therefore left with two problems :

  1. Computing fdigits. This involves reading ref 6 from the linked paper and understanding it. Might not be that easy. :) As far as I can see, that's the only place where nearpi.c is used.
  2. Computing B, the relevant bits of 2/pi. Since M is bounded below by 127, we can just compute the first 127+116 bits of 2/pi offline and store them in an array. See Wikipedia.
  3. Computing y=x*B. This involves multipliying x by a 116-bits number. This is where Section 3 is used. The size of the blocks is chosen to be 24 because 2*24 + 2 (multiplying two 24-bits numbers, and adding 3 such numbers) is smaller than the precision of double, 53 (and because 24 divides 96). We can use blocks of size 11 bits for single arithmetic for similar reasons.

Note - the trick with B only applies to numbers whose exponents are positive (x>=2^24).

To summarize - first, you have to solve the problem with double precision. Your Matlab code doesn't work in double precision too (try removing single and computing sin(2^53), because your twooverpi only has 53 significant bits, not 175 (and anyway, you can't directly multiply such precise numbers in Matlab). Second, the scheme should be adapted to work with single, and again, the key problem is representing 2/pi precisely enough, and supporting multiplication of highly-precise numbers. Last, when everything works, you can try and figure out a better fdigits to reduce the number of bits you have to store and multiply.

Hopefully I'm not completely off - comments and contradictions are welcome.

Example

As an example, let us compute sin(x) where x = single(2^24-1), which has no zeros after the significant bits (M = 0). This simplifies finding B, as B consists of the first 116 bits of 2/pi. Since x has precision of 24 bits and B of 116 bits, the product

y = x * B

will have 92 bits of precision, as required.

Section 3 in the linked paper describes how to perform this product with enough precision; the same algorithm can be used with blocks of size 11 to compute y in our case. Being drudgery, I hope I'm excused for not doing this explicitly, instead relying on Matlab's symbolic math toolbox. This toolbox provides us with the vpa function, which allows us to specify the precision of a number in decimal digits. So,

vpa('2/pi', ceil(116*log10(2)))

will produce an approximation of 2/pi of at least 116 bits of precision. Because vpa accepts only integers for its precision argument, we usually can't specify the binary precision of a number exactly, so we use the next-best.

The following code computes sin(x) according to the paper, in single precision :

x = single(2^24-1);
y = x *  vpa('2/pi', ceil(116*log10(2)));    % Precision = 103.075
k = round(y);
f = single(y - k);
r = f * single(pi) / 2;
switch mod(k, 4)
    case 0 
        s = sin(r);
    case 1
        s = cos(r);
    case 2
        s = -sin(r);
    case 3
        s = -cos(r);
end
sin(x) - s                                   % Expected value: exactly zero.

(The precision of y is obtained using Mathematica, which turned out to be a much better numerical tool than Matlab :) )

In libm

The other answer to this question (which has been deleted since) lead me to an implementation in libm, which although works on double-precision numbers, follows the linked paper very thoroughly.

See file s_sin.c for the wrapper (Table 2 from the linked paper appears as a switch statement at the end of the file), and e_rem_pio2.c for the argument reduction code (of particular interest is an array containing the first 396 hex-digits of 2/pi, starting at line 69).

user1071136
  • 15,636
  • 4
  • 42
  • 61
  • I understand there are problems to solve. But I still don't see the solution based on what you wrote. Can you fill in the blanks you left please? Thank you for your time. – Veridian Mar 03 '12 at 21:44
  • my inputs will be probably always be in the range +/- 20000 (small inputs will be included too), but my inputs x will always be much less than 2^24. – Veridian Mar 05 '12 at 19:06
  • The code example should work for all numbers whose magnitude is smaller than 2^24. You just need less digits in `2/pi`. How many exactly - look at Figure 2 in the linked paper. – user1071136 Mar 05 '12 at 19:28
  • I'm still now following about how to compute how many digits of 2/pi I need. I know what my input range is (+/-20000). Your example is perfect if I have inputs around 2^24, but what is the number of bits I need for 2/pi given my input range? – Veridian Mar 05 '12 at 20:24
  • I believe the answer has been resolved here: http://www.springerlink.com/content/h886863382gg4u83/fulltext.pdf (let me know if you can't view this link) – Veridian Mar 05 '12 at 22:52
  • so from that paper, I have figured out that fdigits is equal to 30. – Veridian Mar 06 '12 at 00:12