We are going to look at three different strategies for approximating a definite integral:

This is based on approximating the aforementioned area by using rectangles, as seen below.
We are in control regarding how many rectangles we want to use. It should be intuitively clear that our area is going to be well approximated by having a very large number of such rectangles.
For simplicity, we shall consider a set of equidistant points between $x=a$ and $x=b$:
$$
a = x_0 < x_1 < x_2 <\dots < x_{n-1} < x_n = b\,.
$$
'Equidistant' means that $x_{i+1}-x_i = h$ (constant) for all $i=0,1,\dots, n-1$.

Let's call the various rectangles: $R_0$, $R_1,\,\dots,,R_{n-1}$, and their corresponding areas
$I_0$, $I_1,\,\dots,,I_{n-1}$. Then
\begin{alignat*}{1}
&I_ 0 = (x_1-x_0)f(x_0) = hf(x_0)\,,\\[0.2cm]
&I_1 = (x_2-x_1)f(x_1) = hf(x_1)\,,\\[0.2cm]
&\dots\,,\\[0.2cm]
&I_k = (x_{k+1} - x_k)f(x_k) = hf(x_k)\,,\\[0.2cm]
&\dots\,,\\[0.2cm]
&I_{n-1} = (x_n-x_{n-1})f(x_{n-1}) = hf(x_{n-1})
\end{alignat*}
and hence
\begin{equation*}
\large{\int_a^bf(x)\, dx \simeq I_0 + I_1+ I_2+\dots + I_{n-1}
=h\sum_{k=0}^{n-1} f(x_k)}
\end{equation*}
The absolute error for this approximation is
$$
\large A.E. = \mathcal{O}\left(\frac{(b-a)^2}{2n}\max_{a\leq x\leq b} |f'(x)|\right)
$$
def integrateREC(fname, xmin, xmax, intervals):
""" Integrate by using the rectangle rule."""
h = (xmax - xmin) / float(intervals)
total = 0.0
# Perform the integration:
x = xmin
for interval in range(intervals):
# Add the area in the trapeziod for this slice:
total += h * fname(x)
# Move to the next slice:
x += h
return total # this returns the approx. of the integral
EXAMPLE:
a)
$$
\int_0^{\pi/2} \sin (401 x)\,dx = \frac{1}{401} \simeq 0.00249376
$$
import numpy as np
def f(x):
return np.sin(401.0*x)
intervals = [1000, 5000, 10000, 100000]
print("The integral is approximately equal to:")
print(" ")
for interval in intervals:
print(integrateREC(f, 0, 0.5*np.pi, interval), "\t (n={})".format(interval))
It should be clear from the little experiment above that the rectangle method is terrible....
In this case we divide the interval $[a, b]$ into a set of $n$ intervals, each of length $h$. The function whose definite integral we aim to find is approximated by a straight line in each sub-interval, as shown in the sketch below. Hence the area that represents our integral will be approximately equal to the sum of the areas of the trapeziums seen in that sketch.

Let's call the various trapeziums: $T_0$, $T_1,\,\dots,,T_{n-1}$, and their corresponding areas
$I_0$, $I_1,\,\dots,,I_{n-1}$. Then
\begin{alignat*}{1}
&I_ 0 = \frac{1}{2}h[f(x_1)+f(x_0)] \,,\\[0.2cm]
&I_1 = \frac{1}{2}h[f(x_1)+f(x_2)]\,,\\[0.2cm]
&\dots\,,\\[0.2cm]
&I_k = \frac{1}{2}h[f(x_k)+f(x_{k+1})]\,,\\[0.2cm]
&\dots\,,\\[0.2cm]
&I_{n-1} = \frac{1}{2}h[f(x_{n-1})+f(x_n)]
\end{alignat*}
and hence
\begin{alignat*}{1}
\large{\int_a^bf(x)\, dx }&\large{\simeq I_0 + I_1+ I_2+\dots + I_{n-1}}\\[0.1cm]
&\large =\frac{1}{2}h[f(x_0) + 2f(x_1) + 2f(x_2) +\dots + 2f(x_{n-1}) +
f(x_n)]\\[0.1cm]
&\large=\frac{h}{2}[f(x_0) + f(x_n)] + h\sum_{k=1}^{n-1}f(x_k)
\end{alignat*}
The absolute error for this approximation is
$$
\large A.E. = \mathcal{O}\left(\frac{(b-a)^3}{12n^2}\max_{a\leq x\leq b} |f''(x)|\right)
$$
def integrateTRAPZM(fname, xmin, xmax, intervals):
""" Integrate by using the trapezoid rule."""
h = (xmax - xmin) / float(intervals)
total = 0.0
# Perform the integration:
x = xmin
for interval in range(intervals):
# Add the area in the trapeziod for this slice:
total += h * (fname(x) + fname(x + h))/float(2.0)
# Move to the next slice:
x += h
return total # this returns the approx. of the integral
EXAMPLE:
a)
$$
\int_0^\pi e^x\cos x\,dx = -\frac{1}{2}(e^\pi+1)\simeq = -12.070346
$$
import numpy as np
def f(x):
return np.exp(x)*np.cos(x)
intervals = [10, 25, 50, 200, 400, 600]
print("The integral is approximately equal to:")
print(" ")
for interval in intervals:
print(integrateTRAPZM(f, 0, np.pi, interval), "\t (n={})".format(interval))
Much better than the rectangle method....
Again, we start with a set of equidistant points in the interval $[a, b]$.
The idea is to approximate the function $f(x)$ by a parabola on each sub-interval $[x_0, x_2]$,
$[x_2, x_4]$, etc. For this to be possible $n$ must be an EVEN number (any number that is divisible by 2 is an even number).

You have the details of the derivation on the slides posted on Brightspace (lots of details can be found on the internet including YouTube and Khan Academy). Here I'll mention only the final result that is needed for coding this approximation:
$$
\large{{\int_{{a}}^{{b}}}{f{{\left({x}\right)}}}{d}{x}\simeq\frac{{h}}{{3}}{\left({y}_{{0}}+{4}{y}_{{1}}+{2}{y}_{{2}}+{4}{y}_{{3}}+{2}{y}_{{4}}+\ldots+{2}{y}_{{{n}-{2}}}+{4}{y}_{{{n}-{1}}}+{y}_{{n}}\right)}}
$$
where you may recall that
$$
\large{h = (b-a)/n\,.}
$$
Note the pattern of the coefficients:
$$
1, 4, 2, 4, 2, 4, 2, \dots 4, 2, 4, 2, 4, 2, 4, 1
$$
The absolute error for this approximation is
$$
\large A.E. = \mathcal{O}\left(\frac{(b-a)^5}{180n^4}\max_{a\leq x\leq b} |f''''(x)|\right)
$$
def integrateSIMP(f, a, b, N=50):
"""
Approximate the integral of f(x) from a to b by Simpson's rule.
INPUTS:
----------
f : function
of a single variable
a , b : numbers
Interval of integration: [a,b]
N : (even) integer
Number of subintervals of [a,b]
OUTPUTS:
-------
float
Approximation of the integral of f(x) over [a, b]
using Simpson's rule with N subintervals of equal length.
"""
if N % 2 == 1:
raise ValueError("N must be an even integer.")
h = (b-a)/float(N) # the stepsize
x = np.linspace(a, b, N+1) # define the mesh
y = f(x)
var = (h/3.0) * np.sum(y[0:-1:2] + 4.0*y[1::2] + y[2::2]) # numerical approx. for integral
return var
EXAMPLES:
a).
$$
\int_0^\pi\sin(x)\,dx = 2
$$
integrateSIMP(lambda x: np.sin(x), 0.0, np.pi, 30)
b).
$$
\int_0^{\sqrt{\pi}} 2x^2\cos(x^2)\,dx
$$
def f(x):
return 2.0*(x**2)*np.cos(x**2)
integrateSIMP(f, 0, np.sqrt(np.pi), 60)
c).
$$
\int_0^{\pi/4}\ln(1+\tan x)\,dx
$$
def f(x):
return np.log(1.0+np.tan(x))
integrateSIMP(f, 0, 0.25*np.pi, 80)
If we want to approximate the value of an integral to within a specified number of significant digits we can keep repeting the process with $h\to h/2$ (which is of course equivalent to doubling the number of 'intervals'). A possible implementation of such a code is included below.
As a typical example, we shall consider the following integral
$$
\int_{\pi/6}^{\pi/2}\sin^2(x)\cos(x)\,dx = \frac{7}{24}\simeq 0.29166666666666\dots\,.
$$
(This integral can be calculated very easily by a simple change of variable.)
import numpy as np
# define the integrand; you can also use a lambda statement:
def f(x):
return (np.sin(x)**2)*np.cos(x)
#
#
# specify the integration range as well as the tolerance
# if you want 3 significant digits, TOL = 10**(-3)
# we want 6 significant digits, so TOL = 10**(-6)
TOL = 1.0e-6
a = np.pi/6.0
b = np.pi/2.0
#
# run the Trapezium integrator for intervals = 2
intervals = 2
I_old = 0.0
I_new = integrateTRAPZM(f, a, b, intervals)
#
# keep comparing the difference between the two approximations:
while abs(I_new - I_old) >= TOL:
I_old = I_new # save the I_new calculated at the previous step
intervals = 2*intervals # double the number of sub-intervals
I_new = integrateTRAPZM(f, a, b, intervals) # find the new approximation
#
# when we break out of the loop I_new will contain
# the desired result:
print("Integral =", I_new)
print("Number of intervals =", intervals)
This is a version of the Rectangle Method (more or less). It is based on approximating the area mentioned above by using a special choice of rectangles. In particular, the height of these rectangles corresponds to
$$
f(x_{i}^*)\,,
$$
where
$$
x_i^* = \frac{1}{2}(x_i+x_{i+1})\quad\mbox{is the midpoint of the interval}\qquad (x_i, x_{i+1})
$$
for $i=0,1,2\,\dots, (n-1)$.

Let's call the various rectangles: $R_0$, $R_1,\,\dots,,R_{n-1}$, and their corresponding areas
$I_0$, $I_1,\,\dots,,I_{n-1}$. Then
\begin{alignat*}{1}
&I_ 0 = (x_1-x_0)f(x_0^*) = hf\left(\frac{x_0+x_1}{2}\right)\,,\\[0.2cm]
&I_1 = (x_2-x_1)f(x_1^*) = hf\left(\frac{x_1+x_2}{2}\right)\,,\\[0.2cm]
&\dots\,,\\[0.2cm]
&I_k = (x_{k+1} - x_k)f(x_k^*) = hf\left(\frac{x_k+x_{k+1}}{2}\right)\,,\\[0.2cm]
&\dots\,,\\[0.2cm]
&I_{n-1} = (x_n-x_{n-1})f(x_{n-1}^*) = hf\left(\frac{x_{n-1}+x_n}{2}\right)
\end{alignat*}
and hence
\begin{equation*}
\large{\int_a^bf(x)\, dx \simeq I_0 + I_1+ I_2+\dots + I_{n-1}
=h\sum_{k=0}^{n-1} f(x_k^*)}
\end{equation*}
The absolute error for this approximation is
$$
\large A.E. = \mathcal{O}\left(\frac{(b-a)^3}{24n^2}\max_{a\leq x\leq b} |f''(x)|\right)
$$
HOMEWORK:
Write a Python code that implements this method. Test your code on the examples of the various integrals that apear above.