Lecture Materials
Adaptive methods#
Error control#
It is important to control the error of numerical integration since we are often interested in the precision of the result.
Let us consider the error estimate of the rectangle/trapezoidal method. For rectangle/trapezoidal method we know that the error scales with \(h\) as \(\varepsilon = c h^2\). Let us double the number of steps. We have \(h_2 = h_1 / 2\). Then, \(\varepsilon_2 = I - I_2 = c h_2^2\), and \(\varepsilon_1 = I - I_1 = 4 c h_2^2\). Therefore, \(\varepsilon_2 \simeq (I_2 - I_1) / 3\).
More generally, the error estimate of \(k\)th iteration is
We can thus continue to double the number of subintervals until the desired precision is reached.
Let us implement this in Python.
Adaptive rectangle rule#
# Rectangle rule for numerical integration with adaptive step
def rectangle_rule_adaptive(f, a, b, nst = 1, tol = 1.e-8, max_iterations = 16):
Iprev = 0.
n = nst
Iprev = rectangle_rule(f, a, b, n)
print("Iteration: {0:5}, I = {1:20.15f}".format(1, Iprev))
for k in range(1, max_iterations):
n *= 2
Inew = rectangle_rule(f, a, b, n)
ek = (Inew - Iprev) / 3.
print("Iteration: {0:5}, I = {1:20.15f}, error estimate = {2:10.15f}".format(k+1, Inew, ek))
if (abs(ek) < tol):
return Inew
Iprev = Inew
print("Failed to achieve the desired accuracy after", max_iterations,"iterations")
return Inew
Let illustrate this with an example.
# The function example we will use
# Overwrite as applicable
flabel = 'x^4 - 2x + 2'
def f(x):
return x**4 - 2*x + 2
flimit_a = 0.
flimit_b = 2.
a = flimit_a
b = flimit_b
print("Computing the integral of",flabel, "over the interval (",a,",",b,") using adaptive rectangle rule")
res = rectangle_rule_adaptive(f,a,b)
Computing the integral of x^4 - 2x + 2 over the interval ( 0.0 , 2.0 ) using adaptive rectangle rule
Iteration: 1, I = 2.000000000000000
Iteration: 2, I = 5.125000000000000, error estimate = 1.041666666666667
Iteration: 3, I = 6.070312500000000, error estimate = 0.315104166666667
Iteration: 4, I = 6.316894531250000, error estimate = 0.082194010416667
Iteration: 5, I = 6.379180908203125, error estimate = 0.020762125651042
Iteration: 6, I = 6.394792556762695, error estimate = 0.005203882853190
Iteration: 7, I = 6.398697972297668, error estimate = 0.001301805178324
Iteration: 8, I = 6.399674482643604, error estimate = 0.000325503448645
Iteration: 9, I = 6.399918620008975, error estimate = 0.000081379121790
Iteration: 10, I = 6.399979654961498, error estimate = 0.000020344984174
Iteration: 11, I = 6.399994913737828, error estimate = 0.000005086258777
Iteration: 12, I = 6.399998728434201, error estimate = 0.000001271565458
Iteration: 13, I = 6.399999682108611, error estimate = 0.000000317891470
Iteration: 14, I = 6.399999920527143, error estimate = 0.000000079472844
Iteration: 15, I = 6.399999980131772, error estimate = 0.000000019868210
Iteration: 16, I = 6.399999995032923, error estimate = 0.000000004967050
Adaptive trapezoidal rule#
# Trapezoidal rule for numerical integration with adaptive step
def trapezoidal_rule_adaptive(f, a, b, nst = 1, tol = 1.e-8, max_iterations = 16):
Iprev = 0.
n = nst
Iprev = trapezoidal_rule(f, a, b, n)
print("Iteration: {0:5}, I = {1:20.15f}".format(1, Iprev))
for k in range(1, max_iterations):
n *= 2
Inew = trapezoidal_rule(f, a, b, n)
ek = (Inew - Iprev) / 3.
print("Iteration: {0:5}, I = {1:20.15f}, error estimate = {2:10.15f}".format(k+1, Inew, ek))
if (abs(ek) < tol):
return Inew
Iprev = Inew
print("Failed to achieve the desired accuracy after", max_iterations,"iterations")
return Inew
a = flimit_a
b = flimit_b
print("Computing the integral of",flabel, "over the interval (",a,",",b,") using adaptive trapezoidal rule")
res =trapezoidal_rule_adaptive(f,a,b)
Computing the integral of x^4 - 2x + 2 over the interval ( 0.0 , 2.0 ) using adaptive trapezoidal rule
Iteration: 1, I = 16.000000000000000
Iteration: 2, I = 9.000000000000000, error estimate = -2.333333333333333
Iteration: 3, I = 7.062500000000000, error estimate = -0.645833333333333
Iteration: 4, I = 6.566406250000000, error estimate = -0.165364583333333
Iteration: 5, I = 6.441650390625000, error estimate = -0.041585286458333
Iteration: 6, I = 6.410415649414062, error estimate = -0.010411580403646
Iteration: 7, I = 6.402604103088379, error estimate = -0.002603848775228
Iteration: 8, I = 6.400651037693024, error estimate = -0.000651021798452
Iteration: 9, I = 6.400162760168314, error estimate = -0.000162759174903
Iteration: 10, I = 6.400040690088645, error estimate = -0.000040690026556
Iteration: 11, I = 6.400010172525072, error estimate = -0.000010172521191
Iteration: 12, I = 6.400002543131352, error estimate = -0.000002543131240
Iteration: 13, I = 6.400000635782950, error estimate = -0.000000635782801
Iteration: 14, I = 6.400000158945742, error estimate = -0.000000158945736
Iteration: 15, I = 6.400000039736406, error estimate = -0.000000039736446
Iteration: 16, I = 6.400000009934106, error estimate = -0.000000009934100
Adaptive Simpson’s rule#
The error estimate of Simpson’s rule is \(\varepsilon = c h^4\). For this reason the error estimate of \(k\)th iteration is
# Simpson's rule for numerical integration with adaptive step
def simpson_rule_adaptive(f, a, b, nst = 2, tol = 1.e-8, max_iterations = 16):
Iprev = 0.
n = nst
Iprev = simpson_rule(f, a, b, n)
print("Iteration: {0:5}, I = {1:20.15f}".format(1, Iprev))
for k in range(1, max_iterations):
n *= 2
Inew = simpson_rule(f, a, b, n)
ek = (Inew - Iprev) / 15.
print("Iteration: {0:5}, I = {1:20.15f}, error estimate = {2:10.15f}".format(k+1, Inew, ek))
if (abs(ek) < tol):
return Inew
Iprev = Inew
print("Failed to achieve the desired accuracy after", max_iterations,"iterations")
return Inew
a = flimit_a
b = flimit_b
print("Computing the integral of",flabel, "over the interval (",a,",",b,") using adaptive Simpson's rule")
res = simpson_rule_adaptive(f,a,b,2)
Computing the integral of x^4 - 2x + 2 over the interval ( 0.0 , 2.0 ) using adaptive Simpson's rule
Iteration: 1, I = 6.666666666666666
Iteration: 2, I = 6.416666666666666, error estimate = -0.016666666666667
Iteration: 3, I = 6.401041666666666, error estimate = -0.001041666666667
Iteration: 4, I = 6.400065104166666, error estimate = -0.000065104166667
Iteration: 5, I = 6.400004069010416, error estimate = -0.000004069010417
Iteration: 6, I = 6.400000254313150, error estimate = -0.000000254313151
Iteration: 7, I = 6.400000015894571, error estimate = -0.000000015894572
Iteration: 8, I = 6.400000000993410, error estimate = -0.000000000993411
Example: Runge function#
Recall the problematic Runge function
Let us compute the integral of this function over the interval \([-2,2]\)
rungelabel = "Runge function"
def runge(x):
return 1./(25*x**2 + 1.)
a = -2.
b = 2.
print("Computing the integral of",rungelabel, "over the interval (",a,",",b,") using adaptive trapezoidal rule")
res = trapezoidal_rule_adaptive(runge,a,b,4,1.e-10)
Computing the integral of Runge function over the interval ( -2.0 , 2.0 ) using adaptive trapezoidal rule
Iteration: 1, I = 1.086824067022087
Iteration: 2, I = 0.698810316902099, error estimate = -0.129337916706663
Iteration: 3, I = 0.596649043819530, error estimate = -0.034053757694190
Iteration: 4, I = 0.588479663841841, error estimate = -0.002723126659230
Iteration: 5, I = 0.588444691123849, error estimate = -0.000011657572664
Iteration: 6, I = 0.588449474263155, error estimate = 0.000001594379768
Iteration: 7, I = 0.588450670842736, error estimate = 0.000000398859860
Iteration: 8, I = 0.588450970000918, error estimate = 0.000000099719394
Iteration: 9, I = 0.588451044791294, error estimate = 0.000000024930125
Iteration: 10, I = 0.588451063488940, error estimate = 0.000000006232549
Iteration: 11, I = 0.588451068163354, error estimate = 0.000000001558138
Iteration: 12, I = 0.588451069331961, error estimate = 0.000000000389535
Iteration: 13, I = 0.588451069624111, error estimate = 0.000000000097383
Romberg method#
Romberg method is a generalization of the above logic for cancelling higher-order errors. The corresponding procedure is called Richardson extrapolation and forms a quintessential method for computing integrals using equidistant grids.
Derivation#
Recall how we estimated the \(\mathcal{O}(h^2)\) error in the \(k\)th step of the trapezoidal method as
The integral can therefore be estimated to \(\mathcal{O}(h^4)\) order as
which is in fact nothing else but the Simpson rule.
We can denote \(R_{k,0} = I_k\) and \(R_{k,1} = R_{k,0} + \frac{R_{k,0} - R_{k-1,0}}{3}\). As seen above
Repeating this process to eliminate the \(\mathcal{O}(h^4)\) we get a higher-order estimate
which accurate to order \(\mathcal{O}(h^6)\).
The general formula for an integral estimate of order \(\mathcal{O}(h^{2m+2})\) reads
Implementation#
def romberg(
f,
a,
b,
accuracy=1e-8,
max_order=10
):
R = np.zeros((max_order, max_order))
h = (b - a) / 2.
R[0, 0] = h * (f(a) + f(b)) # The initial trapezoidal rule
for n in range(1, max_order):
trapezoid = 0.0
for j in range(2**(n-1)):
trapezoid += f(a + (2*j+1)*h)
R[n, 0] = 0.5 * R[n-1, 0] + h * trapezoid # The trapezoidal rule
l = 1
# The Romberg iterations
for m in range(1, n+1):
l *= 4
R[n, m] = (l * R[n, m-1] - R[n-1, m-1]) / (l-1)
print("Iteration: {0:5}, I = {1:20.15f}, error estimate = {2:10.15f}".format(n, R[n, m], abs(R[n, m] - R[n-1, m-1])))
if abs(R[n, m] - R[n-1, m-1]) < accuracy:
return R[n, m]
h /= 2.
print("Romberg method did not converge to required accuracy")
return R[-1, -1]
Illustration#
Let us compute our first integral using the Romberg method.
a = flimit_a
b = flimit_b
print("Computing the integral of",flabel, "over the interval (",a,",",b,") using Romberg method")
res = romberg(f,a,b,1e-6,18)
Computing the integral of x^4 - 2x + 2 over the interval ( 0.0 , 2.0 ) using Romberg method
Iteration: 1, I = 6.666666666666667, error estimate = 9.333333333333332
Iteration: 2, I = 6.400000000000000, error estimate = 0.266666666666667
Iteration: 3, I = 6.400000000000000, error estimate = 0.000000000000000
Now let us consider a more involved example of the Runge function
a = -2.
b = 2.
print("Computing the integral of",rungelabel, "over the interval (",a,",",b,") using Romberg method")
res =romberg(runge,a,b,1e-6,18)
Computing the integral of Runge function over the interval ( -2.0 , 2.0 ) using Romberg method
Iteration: 1, I = 2.679867986798680, error estimate = 2.640264026402640
Iteration: 2, I = 0.648895658796649, error estimate = 2.030972328002031
Iteration: 3, I = 0.554236075601252, error estimate = 0.094659583195396
Iteration: 4, I = 0.562270126297315, error estimate = 0.008034050696062
Iteration: 5, I = 0.587824850153293, error estimate = 0.025554723855978
Iteration: 6, I = 0.588636945021199, error estimate = 0.000812094867906
Iteration: 7, I = 0.588448788195693, error estimate = 0.000188156825505
Iteration: 8, I = 0.588451058525226, error estimate = 0.000002270329532
Iteration: 9, I = 0.588451069812733, error estimate = 0.000000011287507