Section 43-04.mws

Unit 8: Numerical Methods

Chapter 43: Numeric Integration

Section 43.4: adaptive quadrature

Copyright

Copyright * 2001 by Addison Wesley Longman, Inc.

All rights reserved. No part of this publication may be reproduced, stored in a retrieval system, or transmitted, in any form or by any means, electronic, mechanical, photocopying, recording, or otherwise, without the prior written permission of the publisher. Printed in the United States of America.

Initializations

> restart;

> with(linalg):
with(student):
read(`C:/Program Files/Maple 6.01/pvac.txt`):

Warning, the protected names norm and trace have been redefined and unprotected

>

Motivation

Suppose Simpson's rule requires n+1 points to evaluate the integral

Lambda = Int(f(x),x = a .. b)

to an accuracy of epsilon . It may well be that in one portion of the interval [a, b] the function f(x) varies more rapidly than in another, so that the number of function evaluations is actually being determined by only a small part of [a, b] . If so, it might be more efficient to integrate over each subsection of [a, b] , each time using just enough points appropriate for that subsection. Perhaps the total number of function evaluations can thereby be reduced.

By way of illustration, consider the function

> f := cosh(sqrt(1+x+2*x^2));

f := cosh(sqrt(1+x+2*x^2))

>

and the definite integral

> Lambda := Int(f,x=-2..3);

Lambda := Int(cosh(sqrt(1+x+2*x^2)),x = -2 .. 3)

>

which evaluates to

> lambda := evalf(Lambda);

lambda := 45.21287573

>

Using Simpson's rule to approximate Lambda with an error no greater than 10^(-4) takes 51 function-evaluations, as seen from the following computation.

> for k from 24 to 25 do
evalf(simpson(f,x=-2..3,2*k)-lambda);
od;

.10817e-3

.9189e-4

>

With n = 48 , the error is still too large, but with n = 50 , so the total number of function-evaluations is 51, the error drops under the required bound.

Suppose we split the interval [-2, 3] exactly in half, forming the two subintervals [-2, 1/2] and [1/2, 3] , then integrate separately over the two subintervals, permitting an error of at most epsilon/2 = .5*x*10^(-4) on each subinterval. On each subinterval we have the integrals

> Lambda[1] := Int(f,x=-2..1/2);
Lambda[2] := Int(f,x=1/2..3);

Lambda[1] := Int(cosh(sqrt(1+x+2*x^2)),x = -2 .. 1/...

Lambda[2] := Int(cosh(sqrt(1+x+2*x^2)),x = 1/2 .. 3...

>

whose values are, respectively,

> lambda[1] := evalf(Lambda[1]);
lambda[2] := evalf(Lambda[2]);

lambda[1] := 6.594312496

lambda[2] := 38.61856323

>

To determine the number of function-evaluations needed by Simpson's rule on each subinterval, perform the following calculations. To approximate Lambda[1] , obtain

> for k from 8 to 9 do
Left_error[2*k] := evalf(simpson(f,x=-2..1/2,2*k)-lambda[1]);
od;

Left_error[16] := .71769e-4

Left_error[18] := .44859e-4

>

so that 19 function-evaluations are needed to make the error smaller than epsilon/2 = .5*x*10^(-4) .

To approximate Lambda[2] , obtain

> for k from 14 to 15 do
Right_error[2*k] := evalf(simpson(f,x=1/2..3,2*k)-lambda[2]);
od;

Right_error[28] := .5074e-4

Right_error[30] := .3853e-4

>

so that 31 function-evaluations are needed to make the error smaller than epsilon/2 = .5*x*10^(-4) . Hence, a total of 19+31-1 = 49 function-evaluations are needed with the interval-splitting technique. (Since the two subintervals are contiguous, the function need be computed at the midpoint of [-2, 3] just once, not twice.) This is a slight savings over the original tally of 51 , but our intent was merely to provide a motivation for the interval-splitting that constitutes the adaptive quadrature strategy.

>

Error-Test Mechanism

A key ingredient of an adaptive scheme is the error-test mechanism. In Section 4.1, in the context of numeric solutions of differential equations, we derived the error-test mechanism

y[exact]-y[approx](h/2) = (y[approx](h/2)-y[approx]...

valid if the error in y[approx] is O(h^k) . Thus, the error in the more accurate approximation (i.e., the one made with stepsize h/2 ), is measured by the difference between the more accurate ``(h/2) -approximation, and the less-accurate h -approximation, all divided by 2^k-1 . This very calculation was reviewed in Section 42.2 in the context of Richardson extrapolation.

Since Simpson's composite rule is O(h^4) , the factor in the denominator of the error-test estimate would be 2^4-1 = 15 .

We will show how the interval-splitting strategy is coupled with this error-test expression to produce an adaptive, or self-directed, numeric integration scheme.

>

Naive Maple Implementation of Adaptive Quadrature

We next sketch a naive implementation of an adaptive quadrature scheme based on Simpson's rule. Since its purpose is merely to illustrate the strategy, no attempt is made to minimize the number of function-evaluations. For the moment, we ignore the efficiencies that would accrue we saved all function-evaluations which get re-computed during the execution of our algorithm.

We implement our algorithm in Maple, and use Maple's built-in simpson command, thereby ignoring the efficiencies that would be had if we stored all function-evaluations which get re-computed during the execution of our algorithm.

To evaluate the definite integral

Lambda = Int(f(x),x = a .. b)

we apply Simpson's rule with stepsize h = b-a and again, with stepsize h/2 . The first computation is done by invoking Simpson's rule with n = 2 , the second, with n = 4 . As a consequence, we have two estimates of Lambda , from which the error in the more accurate approximation can be determined by dividing the difference by 15.

If the error is small enough, we accept the more accurate ``(h/2) -approximation, and the computation is finished.

If the error is too large, the process is repeated on the interval [a, (a+b)/2] , with the interval [(a+b)/2, b] set aside in a last-in, first-out stack , for processing later. If the numeric approximation for the left-hand subinterval is acceptable, its value is stored, and the process repeated on the right-hand subinterval. The accuracy demanded of any subinterval is proportional to the ratio of the length of the subinterval to the length of the original interval. If the over-all tolerance for Lambda is epsilon , then the permitted error in any subinterval [alpha, beta] is epsilon times the ratio of the length of [alpha, beta] to the length of [a, b] .

To help follow the logic of this process, we define two functions. The first, performs two Simpson's rule integrations, one with n = 2 and one with n = 4 . The inputs to the function are f(x) , the integrand, and a list [alpha, beta] representing the interval over which Simpson's rule is to be applied. The output of the function is a list of three numbers, the more accurate of the two Simpson's rule evaluations, the estimate of the error in this value, and h = beta-alpha , the length of the interval over which these two Simpson's rule evaluations were done.

> INT := proc(f,X)
local S2,S4;
S2 := evalf(simpson(f,x=X[1]..X[2],2));
S4 := evalf(simpson(f,x=X[1]..X[2],4));
[S4,(S4-S2)/15,X[2]-X[1]];
end:

>

The second function we define performs the error-test. Its input is the list output by the first function, and its output is either true or false , depending on whether the ``(h/2) -approximation is accurate enough or not. The accuracy demanded of any subinterval is proportional to the ratio of the length of the subinterval to the length of the original interval. If the over-all tolerance for Lambda is epsilon , then the permitted error in any subinterval [alpha, beta] is epsilon times the ratio of the length of [alpha, beta] to the length of [a, b] .

> errorcheck := proc(Y)
if abs(Y[2]) < abs(evalf(Y[3]*E/H)) then true else false;fi;
end:

>

To evaluate the integral

> Lambda := Int(sqrt(3-x),x=-1..1);

Lambda := Int(sqrt(3-x),x = -1 .. 1)

>

we define the integrand as

> f := integrand(Lambda);

f := sqrt(3-x)

>

and execute the following Maple code in which [a, b] = [-1, 1] , and epsilon = 10^(-8) .

> A := -1:
B := 1:
H := B-A:
E := 10^(-8):
Sigma := 0:
L := [NULL]:
P := [A,B]:
N := 1:
while N>0 do
OUT:=INT(f,P):
if errorcheck(OUT) then Sigma := Sigma+OUT[1]:
N := nops(L):
if N>0 then P := L[N];
L := [seq(L[k],k=1..N-1)];
fi;
else
C := (P[1]+P[2])/2:
L := [op(L),[C,P[2]]]:
P := [P[1],C];
N := nops([L]):
fi;
od:
print(Sigma);

3.447715247

>

That the value produced by this code is within the prescribed error bound, we compute the difference between Lambda and the adaptive value stored in the variable Sigma .

> evalf(Lambda-Sigma);

.3e-8

>

Modifying the code to accumulate and print lists of intervals, we obtain Table 43.13.

> A := -1:
B := 1:
H := B-A:
E := 10^(-8):
Sigma := 0:
Pseq := NULL:
Lseq := NULL:
Iseq := NULL:
L := [NULL]:
P := [A,B]:
Pseq := Pseq,P:
Lseq := Lseq,L:
N := 1:
while N>0 do
OUT := INT(f,P):
Iseq := Iseq,errorcheck(OUT):
if errorcheck(OUT) then Sigma := Sigma+OUT[1]:
N := nops(L):
if N>0 then P := L[N];
L := [seq(L[k],k=1..N-1)];
Pseq := Pseq,P:
Lseq := Lseq,L:
fi;
else
C := (P[1]+P[2])/2:
L := [op(L),[C,P[2]]]:
P := [P[1],C];
Pseq := Pseq,P:
Lseq := Lseq,L:
N := nops([L]):
fi;
od:
IIseq := subs(true=`pass`,false=`fail`,[Iseq]):
M := augment([seq(k,k=1..nops([Iseq]))], vector([Pseq]), vector([Lseq]), vector(IIseq)):
stackmatrix([`row`,`Present`,`Present`,`Integration Outcome`],[`index`,`Interval`,`Stack`,`on Present Interval`],[``,``,``,``],M);
print(Sigma);

MATRIX([[row, Present, Present, `Integration Outcom...

3.447715247

>

The left-hand column indexes the "cycles" executed by the code. The second column lists the "active interval," the interval over which integration is presently taking place. The third column lists the contents of the stack, the pile of subintervals over which integration has yet to take place. The fourth column lists the outcome of the error-test applied to the integration on the "active interval." Where it says pass , the integration was accurate enough, and no further splitting of that interval need take place. Where it says fail , the integration was not accurate enough, and the "active integral" must be split in half, with the left-hand portion becoming the next "active interval" and the right-hand portion being added to the stack.

Taking the computation from the start, we begin with [-1, 1] as the active, or present, interval. Simpson's rule is applied to this interval with h = 2 , and h/2 = 1 , by invoking simpson with n = 2 and n = 4 , respectively. As seen from Table 43.13, this first integration is not accurate enough, so the interval is split into the subintervals [-1, 0] and [0, 1] .

In cycle 2, the active interval is [-1, 0] and the interval [0, 1] is placed on the stack. Again, integration over the active interval is done twice, with n = 2, 4 , respectively, in Simpson's rule. The outcome of the integrations is passed to the error-test, and again, the integration over the active interval is not accurate enough. Hence, the active interval is split again, this time into the new active interval [-1, -1/2] , and the interval [-1/2, 0] which is placed on the stack.

In cycle 3, the active integral is [-1, -1/2] , and integration over it is performed twice, with n = 2, 4 , respectively, in Simpson's rule. The outcome of the two integrations is passed to the error-test, and again, the error is too large. Once again, the active interval is split, this time into the new active interval [-1, -3/4] and the interval [-3/4, -1/2] which is placed on the stack.

In cycle 4, the active integral is [-1, -3/4] , and integration over it is performed twice, with n = 2, 4 , respectively, in Simpson's rule. The outcome of the two integrations is passed to the error-test, and finally, the error is deemed small enough. The n = 4 value produced by Simpson's rule is accepted as the value of the integral over the active interval, and this number is added to the accumulator Sigma which was initialized to 0 at the very start of the computation. No new subinterval is added to the stack. In fact, the last subinterval added to the stack at the end of cycle 3 is now removed from the stack, and becomes the new active interval for cycle 5.

We leave it to the reader to continue this exposition, cycle-by-cycle. Instead, we turn our attention to some final aspects of the logic leading to Table 43.13.

1. Each time integration over a subinterval is deemed accurate enough for that value to be "kept," it is added to the accumulator Sigma .

2. The computation ends when integration over the present interval is accepted as accurate, and the stack is empty. Table 43.13 shows the stack is empty after cycles 8, 12, 14, and 16. However, the integration in cycles 9, 13, and 15 do not pass the error test, so the integration does not terminate. Only after cycle 16 does the next integration pass the error test, and that is when the integration terminates.

3. When the integration process ends, the value accumulated in Sigma is the desired approximation of Lambda .

4. To program a "split" of the active interval [alpha, beta] , compute its midpoint C = (alpha+beta)/2 , and form the right-hand subinterval [C, beta] before forming the left-hand subinterval [alpha, C] . If beta were overwritten by C first, the subinterval [C, beta] would be unavailable.

>

Minimizing Function-Evaluations

It is highly inefficient to invoke a Simpson procedure twice for each cycle, and to invoke Simpson's rule "fresh" for each subinterval. A little thought shows that repeated calls to Simpson's rule requires re-calculating function values. If we can determine where these function values are being used, and can determine a way to store them, the number of function-evaluations can be dramatically reduced.

In the example used to generate Table 43.13, for the two initial integrations, the active interval was [-1, 1] , so when Simpson's rule was applied with n = 2 , f(x) was evaluated at the three nodes x = -1, 0, 1 . For that same interval, when Simpson's rule was applied with n = 4 , f(x) was evaluated at the five nodes x = -1, -1/2, 0, 1/2, 1 . However, only two of these nodes, namely, x = + 1/2 , are new nodes. Hence, if we had stored the values of f(x) at the first three nodes, we would have needed to evaluate f(x) at just two more nodes to complete the first cycle. The first cycle requires just five function-evaluations.

The integrations in the first cycle were not accurate enough, so the initial interval was split into the two subintervals, [-1, 0] and [0, 1] . Two new integrations were then performed in the new active interval, [-1, 0] . The n = 2 application of Simpson's rule required evaluating f(x) at the three nodes x = -1, -1/2, 0 . Had we stored all function values computed in cycle 1, we could have obtained this new Simpson's rule approximation without computing new function values. The n = 4 application of Simpson's rule required evaluating f(x) at the five nodes x = -1, -3/4, -1/2, -1/4, 0 . However, only two of these five nodes, namely, x = -3/4, -1/4 are new. It takes just two new function-evaluations to complete the integrations in cycle 2.

A similar analysis shows that after cycle 1 (which requires five function-evaluations), all subsequent cycles can be completed with just two more function-evaluations each. Thus, the 17 cycles we executed should have taken just 5+2*x*16 = 37 function-evaluations. Had we used Simpson's rule with n = 36 , we would have expended the equivalent computational energy, and would have approximated Lambda with an error of

> evalf(simpson(f,x=-1..1,36)-Lambda);

-.3e-8

>

50% more that the adaptive version's 0.2 x 10^(-8) .

However, for the function

> f := 1/x+x^2/(1+x^2);

f := 1/x+x^2/(1+x^2)

>

the integral

> Lambda := Int(f,x=1/10..5);

Lambda := Int(1/x+x^2/(1+x^2),x = 1/10 .. 5)

>

whose value is

> lambda:=evalf(Lambda);

lambda := 7.538290891

>

would require more cycles to obtain Lambda to within an error of only 10^(-4) . Indeed, modifying and re-executing our Maple code, we obtain

> A := 1/10:
B := 5:
H := B-A:
E := 10^(-4):
Sigma := 0:
m := 0:
L := [NULL]:
P := [A,B]:
Pseq := Pseq,P:
Lseq := Lseq,L:
N := 1:
while N>0 do
m := m+1:
OUT := INT(f,P):
if errorcheck(OUT) then Sigma := Sigma+OUT[1]:
N := nops(L):
if N>0 then P := L[N];
L := [seq(L[k],k=1..N-1)];
fi;
else
C := (P[1]+P[2])/2:
L := [op(L),[C,P[2]]]:
P := [P[1],C];
N := nops([L]):
fi;
od:
print(`value of integral = `,Sigma);
print(`number of cycles = `,m);

`value of integral = `, 7.538323780

`number of cycles = `, 27

>

This value for the integral is supposed to be accurate to within 10^(-4) . The actual error made is

> Sigma-lambda;

.32889e-4

>

which is within the allowed tolerance.

With 27 cycles, we would need, at most, 5+2*x*26 = 57 function-evaluations. If Simpson's rule were used with a uniform distribution of 57 nodes, we would compute Lambda with an error of

> evalf(simpson(f,x=1/10..5,56)-lambda);

.8631295e-2

>

which is considerable less accurate than the value obtained by the adaptive version of Simpson's rule. In fact, to obtain a comparable accuracy with a uniform Simpson's rule, we would need 205 function-evaluations, as verified by the following calculations.

> evalf(simpson(f,x=1/10..5,202)-lambda);
evalf(simpson(f,x=1/10..5,204)-lambda);

.102289e-3

.98541e-4

>

Adaptive Quadrature as a Procedure

The following Maple code captures the adaptive quadrature algorithm as the unsophisticated Maple procedure adapt . The input to the procedure is an expression representing the integrand of the definite integral being evaluated, the limits of integration, and an error tolerance. The output is a numeric approximation of the integral with less than the prescribed error, the number of cycles, and the number of function-evaluations.

> adapt := proc(f,A,B,E)
local H,Sigma,m,L,P,Pseq,Lseq,N,C,k,S2,S4,OUT;
H := B-A:
Sigma := 0:
m := 0:
L := [NULL]:
Pseq:=NULL:
Lseq:=NULL:
P := [A,B]:
Pseq := Pseq,P:
Lseq := Lseq,L:
N := 1:
while N>0 do
m := m+1:
S2 := evalf(simpson(f,x=P[1]..P[2],2));
S4 := evalf(simpson(f,x=P[1]..P[2],4));
OUT:=[S4,(S4-S2)/15,P[2]-P[1]];
if abs(OUT[2]) < abs(evalf(OUT[3]*E/H)) then
Sigma := Sigma+OUT[1]:N := nops(L):
if N>0 then P := L[N];
L := [seq(L[k],k=1..N-1)];
fi;
else
C := (P[1]+P[2])/2:
L := [op(L),[C,P[2]]]:
P := [P[1],C];
N := nops([L]):
fi;
od:
print(`value of integral = `,Sigma);
print(`number of cycles = `,m);
print(`number of function-evaluations = `,(m-1)*2+5);
end:

>

Applying the procedure adapt to the definite integral

> Lambda;

Int(1/x+x^2/(1+x^2),x = 1/10 .. 5)

>

via the syntax

> adapt(f,1/10,5,10^(-4));

`value of integral = `, 7.538323780

`number of cycles = `, 27

`number of function-evaluations = `, 57

>

gives the same results obtained earlier.

>