The previous post discussed how you would plan an attempt to set a record in computing ζ(3), also known as Apéry’s constant. Specifically that post looked at how to choose your algorithm and how to anticipate the number of terms to use.
Now suppose you wanted to actually do the calculation. This post will carry out a calculation using the Unix utility bc
. I often use bc
for extended precision calculation, but mostly for simple things [1].
Calculating Apéry’s constant will require a function to compute binomial coefficients, something not built into bc
, and so this post will illustrate how to write custom functions in bc
.
(If you wanted to make a serious attempt at setting a record computing Apéry’s constant, or anything else, you would probably use an extended precision library written in C MPFR or write something even lower level; you would not use bc
.)
Apéry’s series
The series we want to evaluate is
To compute this we need to compute the binomial coefficients in the denominator, and to do that we need to compute factorials.
From calculations in the previous post we estimate that summing n terms of this series will give us 2n bits of precision, or 2n/3 decimal places. So if we carry out our calculations to n decimal places, that gives us more precision than we need: truncation error is much greater than numerical error.
bc code
Here’s the code, which I saved in the file zeta3.b
. I went for simplicity over efficiency. See [2] for a way to make the code much more efficient.
# inefficient but simple factorial define fact(x) { if (x <= 1) return (1); return (x*fact(x-1)); } # binomial coefficient n choose k define binom(n, k) { return (fact(n) / (fact(k)*fact(n-k))); } define term(n) { return ((-1)^(n-1)/(n^3 * binom(2*n, n))) } define zeta3(n) { scale = n sum = 0 for (i = 1; i <= n; ++i) sum += term(i); return (2.5*sum); }
Now say we want 100 decimal places of ζ(3). Then we should need to sum about 150 terms of the series above. Let’s sum 160 terms just to be safe. I run the code above as follows [3].
$ bc -lq zeta3.b zeta3(160)
This returns
1.202056903159594285399738161511449990764986292340498881792271555341\ 83820578631309018645587360933525814493279797483589388315482364276014\ 64239236562218818483914927
How well did we do?
I tested this by computing ζ(3) to 120 decimals in Mathematica with
N[RiemannZeta[3], 120]
and subtracting the value returned by bc
. This shows that the error in our calculation above is approximately 10−102. We wanted at least 100 decimal places of precision and we got 102.
Notes
[1] I like bc
because it’s simple. It’s a little too simple, but given that almost all software errs on the side of being too complicated, I’m OK with bc
being a little too simple. See this post where I used (coined?) the phrase controversially simple.
[2] Not only is the recursive implementation of factorial inefficient, computing factorials from scratch each time, even by a more efficient algorithm, is not optimal. The more efficient thing to do is compute each new coefficient by starting with the previous one. For example, once we’ve already computed the binomial coefficient (200, 100), then we can multiply by 202*201/101² in order to get the binomial coefficient (202, 101).
Along the same lines, computing (−1)^(n−1) is wasteful. When bootstrapping each binomial coefficient from the previous one, multiply by −1 as well.
[3] Why the options -lq
? The -l
option does two things: it loads the math library and it sets the precision variable scale
to 20. I always use the -l
flag because the default scale is zero, which lops off the decimal part of all floating point numbers. Strange default behavior! I also often need the math library. Turns out -l
wasn’t needed here because we explicitly set scale
in the function zeta3
, and we don’t use the math library.
I also use the -q
flag out of habit. It starts bc
in quiet mode, suppressing the version and copyright announcement.
Another option is to use Python’s “decimal” module. The translation is straight-forward, with the main new complexity being “with localcontext() as cxt:” / “cxt.prec = n” instead of “scale = n”, and using a Decimal(“2.5”) and Decimal(i).
Though it looks like Python’s “prec” is more like bc’s “length” than “scale”, so make that “cxt.prec = n+1”.