2

TL;DR

F# PowerPack's BigRational type can be cast as a double to evaluate the value. However, after the numerator and denominator reach a certain size, the value returned is double.NaN.

Since BigRational keeps track of both the numerator and the denominator as System.Numerics.BigIntegers, you can use a workaround using properties of logarithms:

a / b = e^(ln(a) - ln(b))

With our BigInteger numerator and denominator, we can call

BigInteger num = myBigRational.Numerator;
BigInteger den = myBigRational.Denominator;
double value = Math.Exp(BigInteger.Log(num) - BigInteger.Log(den));

Due to the limitations of the structure of the double type for values close to 0, I would rather use a decimal. I just haven't figured out how.

Context

Just for fun, I'm writing a program that calculates pi using the Taylor series of arctan.

arctan(x) = x - x^3/3 + x^5/5 - x^7/7 +  x^9/9 - ...

If we evaluate the series at 1, we get

arctan(1) = 1 - 1/3 + 1/5 - 1/7 + 1/9 - ...

Since arctan(1) = pi/4, we can then multiply our series by 4 to calculate pi.

The goal of my program is to calculate how many terms of the series it takes to converge to pi accurate to n digits. For example, in order for the series to be accurate to one digit (3), it requires the first three terms:

1 term:  4 * (1) = 4
2 terms: 4 * (1 - 1/3) = 2.666666666666667
3 terms: 4 * (1 - 1/3 + 1/5) = 3.466666666666667

To be accurate to 2 digits (3.1), it requires the first 19 terms. Accurate to 3 digits (3.14) requires the first 119 terms, and so on.

I originally wrote my program using C#'s decimal type:

const int MaxDigits = 20;
private static void RunDecimalCalculation()
{
    decimal pi = 0m; // our current approximation of pi
    decimal denominator = 1m;
    decimal addSubtract = 1m;
    ulong terms = 0;

    for (int digits = 0; digits < MaxDigits; digits++)
    {
        decimal piToDigits, upperBound;
        GetBounds(digits, out piToDigits, out upperBound);

        while (pi >= upperBound | pi < piToDigits)
        {
            pi += addSubtract * 4m / denominator;
            denominator += 2m;
            addSubtract *= -1m;
            terms++;
        }

        PrintUpdate(terms, digits, pi);
    }
}

/// <summary>
/// Returns the convergence bounds for <paramref name="digits"/> digits of pi.
/// </summary>
/// <param name="digits">Number of accurate digits of pi.</param>
/// <param name="piToDigits">Pi to the first <paramref name="digits"/> digits of pi.</param>
/// <param name="upperBound">same as <paramref name="piToDigits"/>, but with the last digit + 1</param>
/// <example>
/// <code>GetBounds(1)</code>:
///     piToDigits = 3
///     upperBound = 4
/// 
/// <code>GetBounds(2)</code>:
///     piToDigits = 3.1
///     upperbound = 3.2
/// </example>
private static void GetBounds(int digits, out decimal piToDigits, out decimal upperBound)
{
    int pow = (int)Math.Pow(10, digits);
    piToDigits = (decimal)Math.Floor(Math.PI * pow) / pow;
    upperBound = piToDigits + 1m / pow;
}

I realized, though, that due to rounding errors in each iteration, after enough terms, the number of terms required might be off. So, I started looking into F# PowerPack's BigRational and rewrote the code:

// very minor optimization by caching common values
static readonly BigRational Minus1 = BigRational.FromInt(-1);
static readonly BigRational One = BigRational.FromInt(1);
static readonly BigRational Two = BigRational.FromInt(2);
static readonly BigRational Four = BigRational.FromInt(4);

private static void RunBigRationalCalculation()
{
    BigRational pi = BigRational.Zero;
    ulong terms = 0;
    var series = TaylorSeries().GetEnumerator();

    for (int digits = 0; digits < MaxDigits; digits++)
    {
        BigRational piToDigits, upperBound;
        GetBounds(digits, out piToDigits, out upperBound);

        while (pi >= upperBound | pi < piToDigits)
        {
            series.MoveNext();
            pi += series.Current;
            terms++;
        }

        double piDouble = Math.Exp(BigInteger.Log(pi.Numerator) - BigInteger.Log(pi.Denominator));
        PrintUpdate(terms, digits, (decimal)piDouble);
    }
}

// code adapted from http://tomasp.net/blog/powerpack-numeric.aspx
private static IEnumerable<BigRational> TaylorSeries()
{
    BigRational n = One;
    BigRational q = One;

    while (true)
    {
        yield return q * Four / n;

        n += Two;
        q *= Minus1;
    }
}

Unsurprisingly, this version runs incredibly slowly, which is fine. (The decimal version took 34 seconds to get to 9 accurate digits; the BigRational version took 17 seconds to get to 5 accurate digits, and has been running for about half an hour and hasn't reached 6 accurate digits). What's frustrating me, though, is that double is less accurate than decimal, so while the number of terms will always be correct, the value displayed from

double piDouble = Math.Exp(BigInteger.Log(pi.Numerator) - BigInteger.Log(pi.Denominator));

is inaccurate. Is there a way around this either through mathematical wizardry or some library that has decimal versions of Math.Exp() and BigInteger.Log()?

Community
  • 1
  • 1
dx_over_dt
  • 13,240
  • 17
  • 54
  • 102
  • 1
    The answer on this question seems like it might answer both your needs: http://stackoverflow.com/questions/429165/raising-a-decimal-to-a-power-of-decimal – Abion47 Jan 30 '17 at 00:52
  • @Abion47 Hmm, maybe. I'm not sure if that code falls prey to the same issue I had when doing my iterative decimal problem. Both their `DecimalExp()` and `LogN()` code does the same sort of series convergence that mine does. That is, if their code is accurate, I think my decimal version would be accurate, too--which it very well may be; I'm just not sure. – dx_over_dt Jan 30 '17 at 00:58
  • It seems like you are doing a whole bunch of casts from `double` to `int` and `double` to `decimal` in your original code. Every time you do that you risk introducing errors in to your calculations. To ensure you're getting clean results you should only compute in one type. – Enigmativity Jan 30 '17 at 03:12
  • What I don't understand is why you're doing anything in double or decimal at all. Represent pi as a BigRational out to as many decimal places you want, and use that. – Eric Lippert Jan 30 '17 at 04:24
  • @EricLippert `BigRational.ToString()` only outputs the fraction. I want to see the digits. – dx_over_dt Jan 30 '17 at 04:26
  • @Enigmativity I don't see what you're talking about. The only things that aren't decimal all the way through are `terms` and the value of pi that we're comparing our approximation to. – dx_over_dt Jan 30 '17 at 04:30
  • Don't use strings either. You've got a type dedicated to doing math; use it to do *math*. Suppose you didn't have any kind of ToString at all. Here, I'll give you two numbers: 7 / 12345 and 71 / 123456. Without converting either to string, how would you tell how many decimal digits they had in common before they diverged? No Exp, unless you write it yourself. No Log, unless you write it yourself. Can you do it? – Eric Lippert Jan 30 '17 at 04:30
  • @EricLippert First, I'm not sure. It's been a while since I've done any real math. Second, that's not as interesting to watch. The output "10 digits are accurate now" is not as interesting as "3.1415926530000001" or whatever the estimate is after 1.7 billion terms. – dx_over_dt Jan 30 '17 at 04:35
  • 1
    Hint: The first step in determining how close two things are is to take their difference. – Eric Lippert Jan 30 '17 at 04:36
  • @dfoverdx - You've got `(int)Math.Pow(10, digits);` which is casting from `int` to `double` (internally) and then casting the result back to `int`. Also `(decimal)Math.Floor(Math.PI * pow) / pow` is doing a boat load of casting. All of that casting just allows for the introduction of errors. There's a lot more than that too. You need to follow Eric Lippert's advice. – Enigmativity Jan 30 '17 at 06:14
  • @Enigmativity like I said in my previous comment, that's only getting the first n digits of Math.Pi. The double and int conversions are plenty accurate enough to get the first 10 digits. – dx_over_dt Jan 30 '17 at 19:15
  • @dfoverdx - Not really. When computing a series a small error will compound quickly until the error is magnified greatly. Eric's comment above tells you to write you own `Exp` and `Log` functions - that's the kind of level you need to go to prevent the errors you're seeing. – Enigmativity Jan 30 '17 at 21:42

1 Answers1

3

How do I evaluate the division of BigIntegers as decimal rather than double in C#?

You have BigIntegers N and D and wish to approximate the exact value N / D as a decimal. WOLOG assume both are positive.

That's easy enough. First solve this problem:

  • Given N and D, find the BigInteger M that minimizes the absolute value of N / D - M / 1028.

Second, solve this problem:

  • Given M, find the BigInteger M' and the non-negative integer E such that M / 10 28 = M' / 10 E and E is minimized.

Third, solve this problem:

  • Given M', find non-negative BigIntegers I0, I1, I2 such that M' = I0 + I1 * 232 + I2 * 264 and each is strictly smaller than 232. If there are no such I0, I1, I2, then your number cannot be represented as a decimal.

Convert I0, I1 and I2 to unsigned ints and then to signed ints.

Now you have everything you need to call

https://msdn.microsoft.com/en-us/library/bb1c1a6x(v=vs.110).aspx

And hey, you have a decimal in hand.

That said: you should not need to do this in the first place. You are doing your math in BigRationals; why ever would you want to drop out of them into decimals or doubles? Just approximate pi to whatever level you want in big rationals, and compare your slowly-converting series to that.

FYI, this series converges very slowly. Once you have empirically determined how slowly it converges, can you provide a proof of any bound on the rate of convergence?

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067