This approach (and much of this numerical library!) was inspired by Gerald Sussman's "Abstraction in Numerical Methods" paper.
That paper builds up to Richardson interpolation as a method of "series acceleration". The initial example concerns a series of the side lengths of an N-sided polygon inscribed in a unit circle.
The paper derives this relationship between the sidelength of an N- and 2N-sided polygon:
If we can increase the number of sides => infinity, we should reach a circle. The "semi-perimeter" of an N-sided polygon is
Loading...In code:
so as Loading..., Loading... should approach Loading..., the half-perimeter of a circle.
Let's start with a square, i.e., Loading... and Loading.... Clojure's iterate
function will let us create an infinite sequence of side lengths:
and an infinite sequence of the number of sides:
Mapping a function across two sequences at once generates a new infinite sequence, of semi-perimeter lengths in this case:
print the first 20 terms:
Unfortunately (for Archimedes, by hand!), as the paper notes, it takes 26 iterations to converge to machine precision:
Enter Sussman: "Imagine poor Archimedes doing the arithmetic by hand: square roots without even the benefit of our place value system! He would be interested in knowing that full precision can be reached on the fifth term, by forming linear combinations of the early terms that allow the limit to be seized by extrapolation." (p4, Abstraction in Numerical Methods).
Sussman does this by noting that you can also write the side length as:
Loading...Then the taylor series expansion for Loading... becomes:
Loading...A couple things to note:
The big idea is to multiply Loading... by 4 and subtract Loading... (then divide by 3 to cancel out the extra factor). This will erase the Loading... term and leave a new sequence with Loading... as the dominant error term.
Now keep going and watch the error terms drain away.
Before we write code, let's follow the paper's example and imagine instead some general sequence of Loading... (where Loading... in the example above), with a power series expansion that looks like
Loading...where the exponents Loading... are some OTHER series of error growth. (In the example above, because the taylor series expanson of $n \sin n$ only has even factors, the sequence was the even numbers.)
In that case, the general way to cancel error between successive terms is:
Loading...or:
Loading...Let's write this in code:
If we start with the original sequence, we can implement Richardson extrapolation by using Clojure's iterate
with the accelerate-sequence
function to generate successive columns in the "Richardson Tableau". (This is starting to sound familiar to the scheme for polynomial interpolation, isn't it?)
To keep things general, let's take a general sequence ps
, defaulting to the sequence of natural numbers.
All we really care about are the FIRST terms of each sequence. These approximate the sequence's final value with small and smaller error (see the paper for details).
Polynomial interpolation in polynomial.cljc
has a similar tableau structure (not by coincidence!), so we can use pi/first-terms
in the implementation below to fetch this first row.
Now we can put it all together into a sequence transforming function, with nice docs:
We can now call this function, combined with us/seq-limit
(a general-purpose tool that takes elements from a sequence until they converge), to see how much acceleration we can get:
Much faster!
Richardson extrapolation works by cancelling terms in the error terms of a function's taylor expansion about 0
. To cancel the nth error term, the nth derivative has to be defined. Non-smooth functions aren't going to play well with richardson-sequence
above.
The solution is to look at specific /columns/ of the Richardson tableau. Each column is a sequence with one further error term cancelled.
rational.cljc
and polynomial.cljc
both have this feature in their tableau-based interpolation functions. The feature here requires a different function, because the argument vector is a bit crowded already in richardson-sequence
above.
It turns out that the Richardson extrapolation is a special case of polynomial extrapolation using Neville's algorithm (as described in polynomial/neville
), evaluated at x == 0.
Neville's algorithm looks like this:
Loading...Where:
Fill in Loading... and rearrange:
Loading...In the Richardson extrapolation scheme, one of our parameters was t
, the ratio between successive elements in the sequence. Now multiply through by Loading... so that our formula contains ratios:
Because the sequence of Loading... elements looks like Loading..., every recursive step separates Loading... and Loading... by another factor of Loading.... So
Loading...Where Loading... is the difference between the positions of Loading... and Loading.... So the formula simplifies further to:
Loading...Now it looks exactly like Richardson extrapolation. The only difference is that Richardson extrapolation leaves n
general (and calls it Loading... etc), so that you can customize the jumps in the error series. (I'm sure there is some detail I'm missing here, so please feel free to make a PR and jump in!)
For the example above, we used a geometric series with Loading... to fit the archimedean Loading... sequence. Another way to think about this is that we're fitting a polynomial to the SQUARE of h
(the side length), not to the actual side length.
Let's confirm that polynomial extrapolation to 0 gives the same result, if we generate squared Loading... values:
Success!
Because Richardson extrapolation is a simplified case of polynomial interpolation, it should be possible to write the process as a functional fold, just as with [[emmy.polynomial.interpolate/neville-fold]] and friends.
The fold version works by building the tableau from the bottom up, one row at a time instead of one column at a time. Because point 0 is seen first, this has the effect of flipping the order of all input points:
Each new entry is generated by merging the entry to the left, and down the left diagonal.
Polynomial interpolation didn't care about this reversal of point order, because each point was an Loading... pair. The merge function of the fold is symmetric.
Richardson extrapolation does care, however, because the input points are the results of evaluating some function at progressively smaller values of Loading...:
Loading...The merge function inside of [[accelerate-sequence]] assumed that its first argument was Loading... and its second argument was Loading....
Flipping the order of the points requires us to /also/ flip the argument order to this merge function.
The other bit of trickiness has to do with the sequence of exponents on the error terms. Generating the tableau column by column allowed the whole column to share a p
value. Generating a row at a time requires us to generate successively longer prefixes of the p
sequence for each row.
We'll do this by preparing each point to the initial value of p
, and then take a function to produce the next element. (We could also write this to take prefixes off of an infinite sequence of p
s! If you need this, please file a ticket and we'll make it happen.)
The merge function, as noted, is the same as the merge function inside of [[accelerate-sequence]] with one change: it's now responsible for generating the next element of the p
sequence.
To "present" a full row, simply take the final element and remove the stashed "p". Since "merge" is reversed, the diagonal elements of the inverted tableau match the first row of the original tableau.