Introduction¶
In this chapter, for a given function we will compute the definite integral:
where and are the limits of integration, and are the function values obtained either from a table of values or from an analytical function.
In numerical integration, the integral is estimated by , and the following holds:
where is the error of the integral estimate.
We will compute the numerical integral based on a discrete sum:
where are the weights, are the nodes on the interval , and is the number of nodes.
We will look at two different approaches to numerical integration:
the Newton-Cotes approach, which is based on equidistant nodes (a constant integration step), and
the Gaussian integration approach, where the nodes are placed so as to achieve exactness for polynomials.
Motivational example¶
For numerical integration, we will use a concrete example to guide us:
Let us prepare the nodes. Let the basic step be ; in this case we have four subintervals and five nodes, at step there are three nodes, and at step only two (the endpoints):
import numpy as np
xg, hg = np.linspace(1, 2, 100, retstep=True) # dense points (for plotting)
x2v, h2v = np.linspace(1, 2, 2, retstep=True) # step h2v = 1 (2 nodes)
x3v, h3v = np.linspace(1, 2, 3, retstep=True) # step h3v = 0.5 (3 nodes)
x4v, h4v = np.linspace(1, 2, 4, retstep=True) # step h4v = 0.33.. (4 nodes)
x5v, h5v = np.linspace(1, 2, 5, retstep=True) # step h5v = 0.25 (5 nodes)Let us also prepare the function values:
yg = xg * np.sin(xg)
y2v = x2v * np.sin(x2v)
y3v = x3v * np.sin(x3v)
y4v = x4v * np.sin(x4v)
y5v = x5v * np.sin(x5v)Let us prepare a plot of the data:
import matplotlib.pyplot as plt
%matplotlib inline
def fig_motivation():
plt.fill_between(xg, yg, alpha=0.25, facecolor='r')
plt.annotate(r'$\int_1^2\,x\,\sin(x)\,\mathrm{d}x$', (1.3, 0.5), fontsize=22)
plt.plot(xg, yg, lw=3, alpha=0.5, label=r'$x\,\sin(x)$')
plt.plot(x2v, y2v, 's', alpha=0.5, label=f'$h={h2v}$', markersize=14)
plt.plot(x3v, y3v, 'o', alpha=0.5, label=f'$h={h3v}$', markersize=10)
plt.legend(loc=(1.01, 0))
plt.ylim(0, 2)
plt.show()Let us display the data:
fig_motivation()
Let us compute the exact result analytically:
import sympy as sym
sym.init_printing()
x = sym.symbols('x')
I_exact = float(sym.integrate(x*sym.sin(x), (x, 1, 2)).evalf())
I_exactNewton-Cotes approach¶
The figure shows a general case in which the distance between the nodes is equal to (this is an equidistant partition).

In this chapter we will first look at the trapezoidal and the composite trapezoidal rule, and later the Simpson’s and Romberg methods will follow.
Trapezoidal rule¶
The trapezoidal rule interpolates the values of the integrand on the (sub)interval with a linear function. For two nodes, this means that we approximate the area under the graph of the function as:
And the weights are:
Numerical implementation¶
The numerical implementation is:
def trapezoidal(y, h):
"""
Trapezoidal rule of integration.
:param y: function values
:param h: integration step
"""
return (y[0] + y[-1])*h/2Numerical example¶
In our concrete case this means that we weight the first and last function value by . In our example :
I_trapezoidal = trapezoidal(y2v, h=h2v)
I_trapezoidalLet us prepare a figure:
def fig_trapezoidal():
plt.fill_between(x2v, y2v, alpha=0.25, facecolor='r')
plt.vlines(x2v, 0, y2v, color='r', linestyles='dashed', lw=1)
plt.annotate('$I_{\\mathrm{trapezno}}$', (1.4, 0.5), fontsize=22)
plt.annotate('Error', fontsize=20, xy=(1.5, 1.4), xytext=(1.6, 1.8),
arrowprops=dict(facecolor='gray', shrink=0.05))
plt.plot(xg, yg, lw=3, alpha=0.5, label='$x\\,\\sin(x)$')
plt.plot(x2v, y2v, 'o', alpha=0.5, label=f'$h={h2v}$')
plt.legend(loc=(1.01, 0))
plt.ylim(0, 2)
plt.show()We display:
fig_trapezoidal()
Error of the trapezoidal rule¶
The difference between the analytical value of the integral and the numerical approximation is the error of the method:
If the function is at least twice differentiable, one can derive (see, e.g., the reference: Burden, Faires, Burden: Numerical Analysis 10th Ed) an error estimate that applies only to the trapezoidal approximation over the entire integration interval:
where and is an unknown value on the interval .
Composite trapezoidal rule¶
If we divide the interval into subintervals and apply the trapezoidal rule of integration on each of them, we speak of the composite trapezoidal rule.
In this case, for each subinterval we apply the trapezoidal rule, and thus for the bounds of the subinterval and we use the weights . Since the interior nodes are doubled, it follows that:
Here we have assumed subintervals of equal width:
It therefore follows that:
Numerical implementation¶
The numerical implementation is:
def trapezoidal_comp(y, h):
"""
Composite trapezoidal rule of integration.
:param y: function values
:param h: integration step
"""
return (np.sum(y) - 0.5*y[0] - 0.5*y[-1])*hNumerical example¶
Above we already prepared the data for two subintervals (three nodes):
x3varray([1. , 1.5, 2. ])h3vLet us compute the estimate of the integral with the composite trapezoidal rule:
I_trapezoidal_comp = trapezoidal_comp(y3v, h=h3v)
I_trapezoidal_compLet us prepare a figure:
def fig_trapezoidal_comp():
plt.fill_between(x3v, y3v, alpha=0.25, facecolor='r')
plt.vlines(x3v, 0, y3v, color='r', linestyles='dashed', lw=1)
plt.annotate('$I_{\\mathrm{trapezno\\;sestavljeno}}$', (1.2, 0.5), fontsize=22)
plt.annotate('Error', fontsize=20, xy=(1.75, 1.68), xytext=(1.4, 1.8),
arrowprops=dict(facecolor='gray', shrink=0.05))
plt.annotate('Error', fontsize=20, xy=(1.2, 1.1), xytext=(1., 1.4),
arrowprops=dict(facecolor='gray', shrink=0.05))
plt.plot(xg, yg, lw=3, alpha=0.5, label='$x\\,\\sin(x)$')
plt.plot(x3v, y3v, 'o', alpha=0.5, label=f'$h={h3v}$')
plt.legend(loc=(1.01, 0))
plt.ylim(0, 2)
plt.show()We display:
fig_trapezoidal_comp()
Error of the composite trapezoidal rule¶
The error of the composite trapezoidal rule stems from the error of the trapezoidal rule; here we make such an error times.
Since holds, we derive the error of the composite trapezoidal rule as:
is a value on the interval . The error is of second order .
A better approximation of the integral¶
Next we will look at the so-called Richardson extrapolation, in which we compute a better approximation from the integral estimates with step and .
If we compute the integral numerically at two different steps and , the following holds:
where and are the approximations of the integral with step and , and and are the error estimates at step and . We derive
Next we write:
Under the assumption that is the same at step and ( is actually different at step and ), we write:
Numerical example¶
We previously computed the integral with the trapezoidal rule at step and at step already; the results were:
[I_trapezoidal, I_trapezoidal_comp]Using the formula above, we compute a better approximation:
I_trapezoidal_better = 4/3*I_trapezoidal_comp - 1/3*I_trapezoidal
print(f'Exact result: {I_exact}\nBetter approximation: {I_trapezoidal_better}')Exact result: 1.4404224209802097
Better approximation: 1.4408392930139313
The composite trapezoidal rule is also implemented in the numpy package, via the numpy.trapezoid function (numpy.trapz in older versions):
trapezoid(y, x=None, dx=1.0, axis=-1)yrepresents the table of function values,xis an optional parameter and defines the nodes; if the parameter is not defined, equidistant nodes spaced bydxare assumed,dxdefines the (constant) integration step, with a default value of 1,axisdefines the axis along which to integrate (in caseyis a multidimensional numeric array).
The function returns the computed integral according to the composite trapezoidal rule. More information can be found in the documentation.
numpy implementation of the composite trapezoidal rule¶
Let us look at an example:
#%%timeit
I_trapezoidal_np = np.trapezoid(y3v, dx=h3v)
I_trapezoidal_npSimpson’s and other methods¶
Above we looked at the trapezoidal rule, which is based on a linear interpolating function on an individual subinterval. With higher-order interpolation we can derive further integration methods.
We need to compute:
We tabulate the integrand and interpolate the table with the Lagrange interpolating polynomial of degree :
where are the function values at the nodes and the Lagrange polynomial is defined as:
For the numerical computation of the integral (the limits are: , ), instead of the function we substitute the interpolating polynomial into the integral:
Since integration is a linear operation, we can swap the integration and the sum:
We integrate the Lagrange polynomial and obtain the weights :
Derivation of the trapezoidal rule using Lagrange polynomials¶
Let us look at how we can computationally derive the weights for the case of the trapezoidal rule using a first-degree Lagrange interpolating polynomial.
First, let us define the variable x, the nodes x0 and x1, and the step h in symbolic form:
x, x0, x1, h = sym.symbols('x x0, x1, h')Let us prepare a Python function that returns, in symbolic form, a list of coefficients of the Lagrange polynomials of degree :
def lagrange(n, x, nodes_prefix='x'):
if isinstance(nodes_prefix, str):
nodes = sym.symbols(f'{nodes_prefix}:{n}')
coeffs = []
for i in range(0, n):
numer = []
denom = []
for j in range(0, n):
if i == j:
continue
numer.append(x - nodes[j])
denom.append(nodes[i] - nodes[j])
numer = sym.Mul(*numer)
denom = sym.Mul(*denom)
coeffs.append(numer/denom)
return coeffsFirst, let us look at the Lagrange polynomials for linear interpolation ():
lag = lagrange(2, x)
lagNow we integrate the Lagrange polynomial over the entire interval:
int0 = sym.integrate(lag[0], (x, x0, x1))
int0We simplify the expression and obtain:
int1 = int0.factor()
int1Since the width of the subinterval is constant, , we perform the substitution:
substitutions = {x1: x0+h}
int1.subs(substitutions)The steps above for the Lagrange polynomial can be generalized to a list of Lagrange polynomials:
x, x0, x1, h = sym.symbols('x, x0, x1, h')
substitutions = {x1: x0+h}
A_trapezoid = [sym.integrate(li, (x, x0, x1)).factor().subs(substitutions)
for li in lagrange(2, x)] # for each Lagrange polynomial `li` in the list lagrange(2,x)
A_trapezoidWe derived the weights that we used in the trapezoidal method:
The trapezoidal rule is:
The error estimate is (reference: Burden, Faires, Burden: Numerical Analysis 10th Ed):
Computing the weights for Simpson’s rule¶
After showing the concise computation for the trapezoidal rule above, we can proceed similarly for quadratic interpolation over three points ().
The computation of the weights is analogous to the one above:
x, x0, x1, x2, h = sym.symbols('x, x0, x1, x2, h')
substitutions = {x1: x0+h, x2: x0+2*h}
A_Simpson1_3 = [sym.integrate(li, (x, x0, x2)).factor().subs(substitutions).factor()
for li in lagrange(3, x)]
A_Simpson1_3Simpson’s rule (this rule is also called Simpson’s 1/3 rule) is:
The error estimate is (reference: Burden, Faires, Burden: Numerical Analysis 10th Ed):
It should be pointed out that the error is locally of fifth order and is defined by the unknown value of the fourth derivative ; consequently, this rule is exact for polynomials of degree 3 or less.
Example of use:
I_Simps = h3v/3 * np.sum(y3v * [1, 4, 1])
I_SimpsLet us prepare a figure. Since Simpson’s rule is based on quadratic interpolation, we must first prepare the interpolating polynomial (we use scipy.interpolate):
from numpy.polynomial import Polynomial # interpolating polynomial through the nodesdef fig_simpson():
y_interpolate = Polynomial.fit(x3v, y3v, deg=len(x3v)-1)
plt.fill_between(xg, y_interpolate(xg), alpha=0.25, facecolor='r')
plt.vlines(x3v, 0, y3v, color='r', linestyles='dashed', lw=1)
plt.annotate('$I_{\\mathrm{Simpsonovo}}$', (1.2, 0.5), fontsize=22)
plt.annotate('Error', fontsize=20, xy=(1.75, 1.7), xytext=(1.4, 1.8),
arrowprops=dict(facecolor='gray', shrink=0.05))
plt.annotate('Error', fontsize=20, xy=(1.2, 1.1), xytext=(1., 1.4),
arrowprops=dict(facecolor='gray', shrink=0.05))
plt.plot(xg, yg, lw=3, alpha=0.5, label='$x\\,\\sin(x)$')
plt.plot(x3v, y3v, 'o', alpha=0.5, label=f'$h={h3v}$')
plt.legend(loc=(1.01, 0))
plt.ylim(0, 2)
plt.show()We display:
fig_simpson()
scipy.integrate.newton_cotes¶
The coefficients of the Newton-Cotes integration approach can also be obtained using scipy.integrate.newton_cotes() (documentation):
newton_cotes(rn, equal=0)where the parameters are:
rn, which defines the number of subintervals (a non-constant step is also possible, see the documentation),equal, which defines whether equally wide subintervals are required.
The function returns a tuple, in which the first element represents the numeric array of weights and the second element the error estimate.
Let us look at an example:
from scipy import integrate
integrate.newton_cotes(2)(array([0.33333333, 1.33333333, 0.33333333]), -0.011111111111111112)Computing the weights for Simpson’s 3/8 rule¶
We can continue with cubic interpolation over four points ():
x, x0, x1, x2, x3, h = sym.symbols('x, x0, x1, x2, x3, h')
substitutions = {x1: x0+h, x2: x0+2*h, x3: x0+3*h}
A_Simpson3_8 = [sym.integrate(li, (x, x0, x3)).factor().subs(substitutions).factor()
for li in lagrange(4, x)]
A_Simpson3_8Simpson’s 3/8 rule is:
The error estimate is (reference: Burden, Faires, Burden: Numerical Analysis 10th Ed):
Let us look at an example of use. We use the previously prepared table of function values at four points:
y4varray([0.84147098, 1.2959172 , 1.65901326, 1.81859485])I_Simps38 = 3*h4v/8 * np.sum(y4v * [1, 3, 3, 1])
I_Simps38Let us also prepare a plot:
def fig_simpson38():
y_interpolate = Polynomial.fit(x4v, y4v, deg=len(x4v)-1)
plt.fill_between(xg, y_interpolate(xg), alpha=0.25, facecolor='r')
plt.vlines(x4v, 0, y4v, color='r', linestyles='dashed', lw=1)
plt.annotate('$I_{\\mathrm{Simpsonovo\\;3/8}}$', (1.2, 0.5), fontsize=22)
plt.annotate('Error', fontsize=20, xy=(1.75, 1.7), xytext=(1.4, 1.8),
arrowprops=dict(facecolor='gray', shrink=0.05))
plt.annotate('Error', fontsize=20, xy=(1.5, 1.47), xytext=(1.1, 1.6),
arrowprops=dict(facecolor='gray', shrink=0.05))
plt.annotate('Error', fontsize=20, xy=(1.2, 1.1), xytext=(1., 1.4),
arrowprops=dict(facecolor='gray', shrink=0.05))
plt.plot(xg, yg, lw=3, alpha=0.5, label='$x\\,\\sin(x)$')
plt.plot(x4v, y4v, 'o', alpha=0.5, label=f'$h={h4v:.6f}$')
plt.legend(loc=(1.01, 0))
plt.ylim(0, 2)
plt.show()We display:
fig_simpson38()
Composite Simpson’s rule¶
We divide the interval into an even number of subintervals of equal width , which defines the nodes for . In this case we write the composite Simpson’s rule:
where is an unknown value on the interval . The error is of fourth order .
Numerical implementation:
def simpson_comp(y, h):
"""
Composite Simpson's rule of integration.
:param y: function values
:param h: integration step
"""
return h/3 * (y[0] + 4*np.sum(y[1:-1:2]) + 2*np.sum(y[2:-1:2]) + y[-1])I_Simps_comp = simpson_comp(y5v, h=h5v)
I_Simps_compLet us prepare a figure:
def fig_simpson_comp():
y_interpolate = Polynomial.fit(x5v, y5v, deg=len(x5v)-1)
plt.fill_between(xg, y_interpolate(xg), alpha=0.25, facecolor='r')
plt.vlines(x5v, 0, y5v, color='r', linestyles='dashed', lw=1)
plt.annotate('$I_{\\mathrm{Simpsonovo\\;sestavljeno}}$', (1.2, 0.5), fontsize=22)
plt.annotate('Error', fontsize=20, xy=(1.70, 1.68), xytext=(1.4, 1.8),
arrowprops=dict(facecolor='gray', shrink=0.05))
plt.plot(xg, yg, lw=3, alpha=0.5, label='$x\\,\\sin(x)$')
plt.plot(x5v, y5v, 'o', alpha=0.5, label=f'$h={h5v}$')
plt.legend(loc=(1.01, 0))
plt.ylim(0, 2)
plt.show()We display:
fig_simpson_comp()
A better estimate of the integral¶
The error of the composite Simpson’s method is defined by:
where is an unknown value on the interval .
We determine an improved estimate of the integral in a manner similar to the composite trapezoidal method; we estimate the integral at two different steps and , and the following holds exactly:
where is the approximation of the integral with step and is the error estimate at step ; the same holds analogously for and .
If we assume that is the same in both cases, we can determine the difference .
Next we write:
Under the assumption that is the same at step and ( is actually different at step and ), we write:
Numerical example¶
We previously computed the integral with Simpson’s rule at step and at step already; the results were:
[I_Simps, I_Simps_comp]Using the formula above, we compute a better approximation:
I_Simps_better = 16/15*I_Simps_comp - 1/15*I_Simps
print(f'Exact result: {I_exact}\nBetter approximation: {I_Simps_better}')Exact result: 1.4404224209802097
Better approximation: 1.4404218714139077
We obtain a better numerical approximation, but we lose the error estimate!
Simpson’s method in scipy.integrate.simpson¶
The composite Simpson’s rule is implemented in scipy in scipy.integrate.simpson() (documentation):
simpson(y, x=None, *, dx=1.0, axis=-1)where the parameters are:
ythe table of function values that we integrate,xthe nodes, an optional parameter; ifx=None, equidistant subintervals of widthdxare assumed,dxthe width of the equidistant subintervals, i.e., the integration step,axisthe integration axis (important in the case of a multidimensional numeric array).
In older SciPy versions the function was called simps and also had the parameter even; both were removed in SciPy 1.14.
Let us look at an example; first we import the simpson function:
from scipy.integrate import simpson#%%timeit
simpson(y5v, dx=h5v)Romberg method¶
The Romberg method is based on Richardson extrapolation. Let us assume that we integrate the integral on the interval , which we divide into subintervals ().
We denote the result of the trapezoidal rule at by , where denotes the accuracy of the obtained result .
If we apply the trapezoidal integration rule at subintervals, we compute:
, step , order of accuracy ,
, step , order of accuracy ,
, step , order of accuracy ,
, step , order of accuracy ,
, step , order of accuracy .
Based on Richardson extrapolation we compute a better approximation
, step , order of accuracy ,
, step , order of accuracy ,
, step , order of accuracy .
Then we continue with Richardson extrapolation:
, step , order of accuracy ,
, step , order of accuracy .
Richardson extrapolation can be generalized:
, step , order of accuracy
Here, the better approximation at step is equal to the result obtained by Simpson’s method at step . Similarly, the better approximation at step is equal to the numerical integral of Simpson’s method at step . And further, is equal to the corrected approximation of Simpson’s method at step .
Let us prepare the numerical data:
x9v, h9v = np.linspace(1, 2, 9, retstep=True) # step h9v = 0.125 (9 nodes)
y9v = x9v * np.sin(x9v)Let us look at the example from above. First, we compute the integral with the composite trapezoidal method at different steps (second-order error):
## O(h^2)
R1_1 = trapezoidal_comp(y9v[::8], 8*h9v) # h=1.0
R2_1 = trapezoidal_comp(y9v[::4], 4*h9v) # h=0.5
R3_1 = trapezoidal_comp(y9v[::2], 2*h9v) # h=0.25
R4_1 = trapezoidal_comp(y9v, h9v) # h=0.125
[R1_1, R2_1, R3_1, R4_1]Then we compute better approximations (we obtain a fourth-order error):
## O(h^4)
R2_2 = R2_1 + 1/3 * (R2_1 - R1_1)
R3_2 = R3_1 + 1/3 * (R3_1 - R2_1)
R4_2 = R4_1 + 1/3 * (R4_1 - R3_1)
[R2_2, R3_2, R4_2]The results represent the result of Simpson’s method at step , and :
[simpson_comp(y9v[::4], 4*h9v), simpson_comp(y9v[::2], 2*h9v), simpson_comp(y9v, h9v)]We again compute better approximations (we obtain a sixth-order error):
## O(h6)
R3_3 = R3_2 + 1/15 * (R3_2 - R2_2)
R4_3 = R4_2 + 1/15 * (R4_2 - R3_2)
[R3_3, R4_3]The result represents the better result of Simpson’s method at step and :
a4h, a2h, ah = [simpson_comp(y9v[::4], 4*h9v), simpson_comp(y9v[::2], 2*h9v), simpson_comp(y9v, h9v)]
[16/15*a2h-1/15*a4h, 16/15*ah-1/15*a2h]We again compute better approximations (we obtain an eighth-order error):
## O(h8)
R4_4 = R4_3 + 1/63 * (R4_3 - R3_3)
R4_4The Romberg method thus offers high result accuracy at a relatively low numerical cost. The error is estimated by:
Romberg method in scipy.integrate.romb¶
The Romberg method is implemented in scipy in scipy.integrate.romb() (documentation):
romb(y, dx=1.0, axis=-1, show=False)where the parameters are:
ythe table of function values that we integrate,dxthe width of the equidistant subintervals, i.e., the integration step,axisthe integration axis (important in the case of a multidimensional numeric array),showifTrue, displays the elements of the Richardson extrapolation.
Let us look at the example from above:
from scipy.integrate import romby9varray([0.84147098, 1.01505104, 1.18623077, 1.34872795, 1.49624248,
1.62261343, 1.72197541, 1.78891084, 1.81859485])romb(y9v, dx=h9v, show=True)Richardson Extrapolation Table for Romberg Integration
======================================================
1.33003
1.41314 1.44084
1.43362 1.44045 1.44042
1.43872 1.44042 1.44042 1.44042
======================================================
Gaussian integration approach¶
The Newton-Cotes approach is based on a polynomial of degree , and the error is of degree . This means that the integration yields an exact result if the integrated function is a polynomial of degree or less; for even , owing to symmetry, even of degree (Simpson’s rule, , is e.g. exact also for cubic polynomials).
The Gaussian integration approach is fundamentally different: the goal is to replace the integral of the function with a weighted sum of function values at discrete values :
Here both the weight and the location of the node are unknown. For polynomials of degree we will also need more points .
In what follows, we will learn that we can compute a numerically exact integral very efficiently. An additional advantage of Gaussian integration is that it can compute the integral of functions with singularities (e.g.: ).
Gaussian quadrature with one node¶
Let us assume that we want to integrate a polynomial of degree (a linear function):
Let us compute:
On the other hand, we want to compute the integral from the appropriately weighted value of the function at an unknown node (only one node!):
By equating the expressions above, we derive:
and are the coefficients of the linear function, which can take arbitrary values, so it holds that:
This is a system of linear equations with the solution:
If the integrated function is linear, we will compute the true value based only on the value at a single point!
So that the Gaussian integration approach is independent of the integration limits , , we introduce standard limits.
In the case of standard limits, at one Gauss point the weight is and is the value at which we must evaluate the function .
Computational derivation of the weights and the Gauss point¶
Let us define the symbols and set up the equation:
a_0, a_1, x, x_L, x_D, A_0, x_0 = sym.symbols('a_0, a_1, x, x_L, x_D, A_0, x_0') # symbols
P1 = a_0 + a_1*x # linear polynomial
eq = sym.Eq(P1.integrate((x, x_L, x_D)).expand(), A_0*P1.subs(x, x_0)) # theoretical integral = estimate with weights
eqLet us prepare two equations (for the first we assume , for the second we assume ) and solve the system for A_0 and x_0:
sym.solve([eq.subs(a_0, 0).subs(a_1, 1), eq.subs(a_1, 0).subs(a_0, 1)], [A_0, x_0])For additional explanation, I recommend this video.
Gaussian integration method with multiple nodes¶
We want to derive a Gaussian integration method that will take into account nodes on the interval and will exactly compute the integral of polynomials up to degree . The following must hold:
In the derivation we will restrict ourselves to the standard limits , ,
where the polynomial of degree is defined as:
With two Gauss points/nodes we exactly compute the integral of a polynomial up to third order, and with three Gauss nodes we exactly compute the integral of a polynomial up to fifth order!
Computational derivation¶
Let us first prepare the symbolic notation of the polynomial and the corresponding variables:
def P_etc(n=1, a='a', x='x'): # n is the degree of the polynomial
ai = sym.symbols(f'{a}:{n+1}') # list of a_i
x = sym.symbols(x) # variable x
return ai, x, sum([ai[i]*x**i for i in range(n+1)])Now let us find the weights and the nodes for the case of two Gauss nodes; the polynomial is therefore of third degree.
v = 2 # number of nodes
ai, x, P = P_etc(n=2*v-1)
xi = sym.symbols(f'x:{v}') # list of x_i
Ai = sym.symbols(f'A:{v}') # list of A_i
print(f'Nodes: {xi}\nWeights: {Ai}')Nodes: (x0, x1)
Weights: (A0, A1)
The polynomial:
PSimilarly to the one node above, here let us define the equations:
eqs = [sym.Eq(P.integrate((x, -1, 1)).coeff(a_),\
sum([Ai[i]*P.subs(x, xi[i]) \
for i in range(v)]).expand().coeff(a_)) \
for a_ in ai]
eqsWe solve them for the unknown and :
sol = sym.solve(eqs, sym.flatten((xi, Ai)))
solWe determined a list of two (equal) solutions.
First, the nodes are defined: and , with the corresponding weights .
The code above is derived in general - you can increase the number of nodes v and compute the nodes and the corresponding weights.
Below is a table of nodes and weights for one, two, and three nodes (for the limits , ):
Number of points 1:
| Node | Weight | |
|---|---|---|
| 0 | 0 | 2 |
Number of points 2:
| Node | Weight | |
|---|---|---|
| 0 | 1 | |
| 1 | 1 |
Number of points 3:
| Node | Weight | |
|---|---|---|
| 0 | ||
| 1 | 0 | |
| 2 |
For more nodes and also the error estimate, see Mathworld Legendre-Gauss Quadrature.
For the case of three Gauss points, we compute the numerical integral (standard limits):
Numerical implementation¶
The numerical implementation (including the transformation of the limits) for one, two, or three nodes:
def gaussian(fun, a, b, nodes=1):
"""
Gaussian integration method.
:param fun: object of the function to integrate
:param a: lower limit
:param b: upper limit
:param nodes: number of nodes (1, 2, or 3)
"""
def g(xi): # function for transforming the limits
return (b-a)/2 * fun((b+a +xi * (b-a))/2)
if nodes == 1:
return 2*g(0.)
elif nodes == 2:
return 1. * g(-np.sqrt(3)/3) + 1. * g(np.sqrt(3)/3)
elif nodes == 3:
return 5/9 * g(-np.sqrt(15)/5) +8/9 * g(0.) + 5/9 * g(np.sqrt(15)/5)Let us look at an example. First, let us define the function we want to integrate:
def f(x):
return x*np.sin(x)Now we pass the function (in this concrete case f) and not the value (e.g., f(0.)) to the gaussian function. First for one node, then two and three:
gaussian(fun=f, a=1., b=2., nodes=1)gaussian(fun=f, a=1., b=2., nodes=2)gaussian(fun=f, a=1., b=2., nodes=3)scipy.integrate.fixed_quad¶
Within scipy, the Gaussian integration method is implemented in the scipy.integrate.fixed_quad() function (documentation):
fixed_quad(func, a, b, args=(), n=5)where the parameters are:
funcis the name of the function we call,ais the lower limit,bis the upper limit,argsis a tuple of any additional arguments of the functionfunc,nis the number of nodes of the Gaussian integration, defaultn=5.
The function returns a tuple with the integration result val and the value None: (val, None)
Let us look at the example from above:
from scipy.integrate import fixed_quad
fixed_quad(f, a=1, b=2, n=3)[0]The result is equal to the previous one:
gaussian(fun=f, a=1., b=2., nodes=3)scipy.integrate¶
scipy.integrate is a powerful tool for numerical integration (see the documentation). In what follows, we will look at some of the functions.
Integration functions that require a defining function:¶
quad(func, a, b[, args, full_output, ...])computes the definite integral offunc(x)over the limits [a,b],dblquad(func, a, b, gfun, hfun[, args, ...])computes the definite integral offunc(x,y),tplquad(func, a, b, gfun, hfun, qfun, rfun)computes the definite integral offunc(x,y,z),nquad(func, ranges[, args, opts, full_output])computes the definite integral of the -dimensional functionfunc( ...),
scipy.integrate.quad¶
Let us look at a very useful function for integration, scipy.integrate.quad() (documentation):
quad(func, a, b, args=(), full_output=0, epsabs=1.49e-08, epsrel=1.49e-08, limit=50,
points=None, weight=None, wvar=None, wopts=None, maxp1=50, limlst=50)The selected parameters are:
functhe Python function we integrate,athe lower limit of integration (np.infcan be used for an infinite limit),bthe upper limit of integration (np.infcan be used for an infinite limit),full_outputto display all results, default 0,epsabsthe allowed absolute error,epsrelthe allowed relative error.
Let us look at the example from above:
from scipy import integrateintegrate.quad(f, a=1, b=2)scipy.integrate.dblquad¶
This is a function similar to quad, but for integration over two variables (documentation).
The function scipy.integrate.dblquad() computes the double integral of func(y, x) over the limits from x = [a, b] and y = [gfun(x), hfun(x)].
dblquad(func, a, b, gfun, hfun, args=(), epsabs=1.49e-08, epsrel=1.49e-08)The selected parameters are:
funcis the Python function we integrate,ais the lower limit of integration ofx(np.infcan be used for an infinite limit),bis the upper limit of integration ofx(np.infcan be used for an infinite limit),gfunis the Python function that defines the lower limit ofyas a function ofx,hfunis the Python function that defines the upper limit ofyas a function ofx.
Let us look at an example of computing the area of a half-circle with radius 1:
Let us define the appropriate Python functions and compute the result:
def func(y, x): # the integration function is simple, a constant = 1!
return 1.
def gfun(x): # lower limit = 0
return 0.
def hfun(x): # upper limit
return np.sqrt(1-x**2)
integrate.dblquad(func=func, a=-1, b=1, gfun=gfun, hfun=hfun)Integration functions that require a table of values:¶
trapezoid(y[, x, dx, axis])composite trapezoidal rule,cumulative_trapezoid(y[, x, dx, axis, initial])cumulative integral of the integrand (returns the result at each node),simpson(y[, x, dx, axis])Simpson’s method,romb(y[, dx, axis, show])Romberg method.
Here we will look at the cumulative_trapezoid function on an example. Consider the case where a mass (we omit the units) acted upon by a force . From Newton’s 2nd law it follows that: . We integrate the acceleration to compute the velocity; then we integrate once more for the displacement. Let us first define the function for the acceleration:
def acceleration(t):
F = np.sin(t)
m = 1
return F/mWe are interested in what happens over a time of 3 seconds:
t, h = np.linspace(0, 3, 100, retstep=True)Let us compute the table of accelerations and then integrate for the velocity (here it is important to define the initial value):
a = acceleration(t)
v = integrate.cumulative_trapezoid(y=a, dx=h, initial=0)Now let us integrate the velocity once more to compute the path:
s = integrate.cumulative_trapezoid(y=v, dx=h, initial=0)Let us display the result:
plt.plot(t, a, label='Acceleration')
plt.plot(t, v, label='Velocity')
plt.plot(t, s, label='Path')
plt.xlabel('$t$ [s]')
plt.legend();