The Romberg Method

Revised on Monday April 7, adding some notes on presenting the results and errors nicely.

Write a function to implement the Romberg method.

  • Again, the input should include an error tolerance, and the output should include estimates of absolute error and of cost.

  • We will discuss this in class; there are lots of details to address.

  • Note that some ideas for this are first explored in the part of Centered Difference Approximation with Richardson Extrapolation.

Additions on April 5

Recall that the basic algorithm is

\(n \leftarrow 1\)
\(h \leftarrow b-a\)
\(\displaystyle R_{0,0} = \frac{f(a) + f(b)}{2} h\)
for i from 1 to M:
\(\quad\) \(R_{i,0} = \left( R_{i-1,0} + h \sum_{k=1}^n f(a + (i - 1/2)h) \right)/2\)
\(\quad\) for j from 1 to i:
\(\quad\quad \displaystyle R_{i,j} = \frac{4^j R_{i,j-1} - R_{i-1,j-1}}{4^j - 1}\)
\(\quad\) end for
\(\quad n \leftarrow 2 n\)
\(\quad h \leftarrow h/2\)
end for

Python note: The needed 2D array for \(R\) can be created with the numpy function R = numpy.array([M,M]). It has the syntactic peculiarity that the dimensions of the array are given in a list, not as successive input parameters.

Once the above basic verion is working, produce a second version that adds an error estimate, and ends once the final approximation \(R_{i,i}\) at stage \(i\) is good enough.

As usual I recommend using a for loop organization, adding an if ... break

Additions on April 7: some tips on printing, and formatting numbers.

The final array only has relevant value at positions R[i,j] with \( j \leq i\), so here is one way to print it:

import numpy as np
M = 4
R = np.zeros([M,M])
# Set up a fake array R;
# the entries are from an important example: the Hilbert matrix. We will see it again soon.
for i in range(M):
    for j in range(i+1):
        R[i, j] = 1.0/(1+i+j)
print(f"R is:")
print(R)
print(f"The relevant parts of R are:")
for i in range(M):
    print(R[i,0:i+1])
R is:
[[1.         0.         0.         0.        ]
 [0.5        0.33333333 0.         0.        ]
 [0.33333333 0.25       0.2        0.        ]
 [0.25       0.2        0.16666667 0.14285714]]
The relevant parts of R are:
[1.]
[0.5        0.33333333]
[0.33333333 0.25       0.2       ]
[0.25       0.2        0.16666667 0.14285714]

This strategy can also be used to print the errors. One can subtract a number from an array, which subtracts it from each element if the array. So pretending that the exact value is 1/8:

exact = 1.0/8.0
print(f"The fake errors are:")
for i in range(M):
    print(R[i,0:i+1] - exact)
The fake errors are:
[0.875]
[0.375      0.20833333]
[0.20833333 0.125      0.075     ]
[0.125      0.075      0.04166667 0.01785714]

An alternative is to print one entry at a time, telling the print command to not start a new line except at the end of a row:

exact = 1.0/8.0
print(f"The fake errors are:")
for i in range(M):
    for j in range(i+1):
        print(R[i,j] - exact, end=" ")  # This puts the "end" text (a space) at the end, instead of a new line.
    print()  # Start a new line only at the end of each row
The fake errors are:
0.875 
0.375 0.20833333333333331 
0.20833333333333331 0.125 0.07500000000000001 
0.125 0.07500000000000001 0.04166666666666666 0.01785714285714285 

One advantage of this “one at a time” approach is that one can specify the number of significant digits to display, using f-string formatting. Here, the “:7.4” part at the end of the braces means to use y spaces for each value (so they line up in columns) wth 4 sigificant digits:

exact = 1.0/8.0
print(f"The fake errors are:")
for i in range(M):
    for j in range(i+1):
        print(f"{R[i,j] - exact:7.4}", end=" ")  # This puts the "end" text (a space) at the end, instead of a new line.
    print()  # Start a new line only at the end of each row
The fake errors are:
  0.875 
  0.375  0.2083 
 0.2083   0.125   0.075 
  0.125   0.075 0.04167 0.01786