"# try different numbers of points, generated as 2^n + 1 so the number is odd\n",
"n_vals = [2**(i+1) + 1 for i in range(11)]\n",
"err1=np.array([])\n",
"err2=np.array([])\n",
"err3=np.array([])\n",
"for n in n_vals:\n",
" e1, e2, e3 = do_int(n)\n",
" print(n, e1, e2, e3)\n",
" err1 = np.append(err1, e1)\n",
" err2 = np.append(err2, e2)\n",
" err3 = np.append(err3, e3)\n",
" \n",
"plt.plot(n_vals, err1, label='Rectangular')\n",
"plt.plot(n_vals, err2, label='Trapezoidal')\n",
"plt.plot(n_vals, err3, label='Simpson')\n",
"\n",
"n = np.array(n_vals)\n",
"dx = (np.pi/2)/(n-1)\n",
"# Plot the Euler Maclaurin error formulas\n",
"plt.plot(n_vals, dx/2, \":\")\n",
"plt.plot(n_vals, dx**2/12, \":\")\n",
"plt.plot(n_vals, dx**4/180, \":\")\n",
"\n",
"plt.yscale('log')\n",
"plt.xscale('log')\n",
"plt.legend()\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "e0bb8f63",
"metadata": {},
"source": [
"Notes:\n",
"- The scalings with the number of points are $1/N$ for rectangular, $1/N^2$ for trapezoidal, and $1/N^4$ for Simpson's rule. (You might have been expecting $1/N^3$ for Simpson's rule but because of the way it was constructed using double intervals, the third order term is antisymmetric over the double interval and cancels.)\n",
"- If you try integrating polynomials, you should find that the error goes to zero (machine precision) for cubic polynomials and below (Simpson), linear or below (trapezoid) or constant (rectangular).\n",
"- Some special cases can give surprising results. For example, for $\\int \\sin^2(x) dx$, all the methods can give exact results. To see why, you can rewrite $\\sin^2 x=(1-\\cos(2x))/2$. If you have an odd number of sample points, the cos term averages to zero and the remaining term is a constant, which all three methods will fit exactly. \n",
"- It is possible to derive analytic expressions for the errors in the different approximations (these are known as [Euler Maclaurin](https://en.wikipedia.org/wiki/Euler–Maclaurin_formula) formulae). For the trapezoidal rule, the leading term is \n",
"These are plotted as dotted lines in the Figure, you can see that they agree remarkably well with the numerical results. (Note that when plotting we use the fact that the derivative terms are equal to unity for $\\cos(x)$). "
]
},
{
"cell_type": "markdown",
"id": "a5381b56",
"metadata": {},
"source": [
"## Gaussian quadrature"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "7eb969c8",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"quad gives I = 1.493648265624854 1.6582826951881447e-14\n",
"# then use Gaussian quadrature with different numbers of points\n",
"n_vec = np.array([])\n",
"err_vec = np.array([])\n",
"\n",
"for npoints in range(1,10):\n",
" I = 0.0\n",
" x, w = np.polynomial.legendre.leggauss(npoints)\n",
" for i in range(npoints):\n",
" I += w[i] * func(x[i])\n",
" err = (I-I0)/I0\n",
" print(npoints, 2*npoints-1, I, err)\n",
" n_vec = np.append(n_vec, npoints)\n",
" err_vec = np.append(err_vec, abs(err))\n",
" \n",
"plt.plot(n_vec, err_vec)\n",
"plt.yscale('log')\n",
"plt.xscale('linear')\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "22c1aadf",
"metadata": {},
"source": [
"- You can use the code above to check that the answer is exact for polynomials of degree $2n-1$ and smaller\n",
"- The scaling of the error with number of points depends on the function you are integrating, but decreases approximately exponentially (rather than power law with the Newton-Cotes methods)."
]
},
{
"cell_type": "markdown",
"id": "1650be55",
"metadata": {},
"source": [
"Let's try the Gauss-Hermite quadrature which has weighting function $W(x)=e^{-x^2}$ for integrals from $-\\infty$ to $+\\infty$. In the next example I try $x^4 e^{-x^2}$ which becomes exact once we go to 3 terms:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "38475475",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"quad gives I = 1.3293403881791366 1.5859180523983767e-08\n",
"$$\\langle v\\rangle = \\int_0^\\infty d^3v\\ v f(v).$$"
]
},
{
"cell_type": "markdown",
"id": "563e6fa9",
"metadata": {},
"source": [
"To get this into a simpler form, change integration variable to $x=(m/2k_BT)^{1/2} v$ and use the spherical volume element $d^3 v= 4\\pi v^2 dv$. This gives\n",
"We can evaluate the integral $I$ numerically (aiming for an error of 0.1%). (Spoiler: the analytic answer is 1/2).\n",
"\n",
"For Gaussian quadrature, you might think of using the Gauss-Hermite polynomials since we have an $e^{-x^2}$ in the integral, but the limits are from zero to infinity, not -infinity to infinity. One way to do it is to write the integral as\n",
"quad gives 0.5 with error 3.36729e-05; number of evaluations=75\n",
"Simpson gives 0.499296 with error 0.00140896; number of points=21\n",
"Gauss-Hermite gives 0.500992 with error 0.0019831; number of terms=15\n",
"Gauss-Laguerre gives 0.5 with error 0; number of terms=1\n"
]
}
],
"source": [
"import numpy as np\n",
"import scipy.integrate\n",
"\n",
"func = lambda x: x**3 * np.exp(-x**2)\n",
"I0 = 0.5 # the analytic result\n",
"\n",
"# First use quad\n",
"# We'll set the relative error we are looking for to 1e-3 and also ask for \n",
"# full output which gives us information such as how many function evaluations were done\n",
"I1, err1, info = scipy.integrate.quad(func,0.0,np.inf, full_output=True, epsrel=1e-3)\n",
"print(\"quad gives %lg with error %lg; number of evaluations=%d\" % (I1, err1, info['neval']))\n",
"\n",
"# Now Simpson's rule\n",
"# Sample the function first\n",
"npoints = 21\n",
"xp = np.linspace(0.0,7.0,npoints) # integrate to x=7\n",
"fp = func(xp)\n",
"I2 = scipy.integrate.simpson(fp, xp)\n",
"print(\"Simpson gives %lg with error %lg; number of points=%d\" %(I2, abs(I2-I0)/I0, npoints))\n",
"\n",
"# Now use Gaussian quadrature\n",
"npoints = 15\n",
"x, w = np.polynomial.hermite.hermgauss(npoints)\n",
"I3 = 0.5 * np.sum(w * abs(x)**3)\n",
"print(\"Gauss-Hermite gives %lg with error %lg; number of terms=%d\" %(I3, abs(I3-I0)/I0, npoints))\n",
"\n",
"# Gauss-Laguerre\n",
"npoints = 1\n",
"x, w = np.polynomial.laguerre.laggauss(npoints)\n",
"I4 = 0.5 * np.sum(w * x)\n",
"print(\"Gauss-Laguerre gives %lg with error %lg; number of terms=%d\" %(I4, abs(I4-I0)/I0, npoints))\n",
"\n"
]
},
{
"cell_type": "markdown",
"id": "4f9b0ea2",
"metadata": {},
"source": [
"In the Simpson's method, we integrate to $x=7$. You can experiment with this and see how much it changes the answer. At $x=7$, the integrand becomes comparable to the machine precision, so it seems a reasonable place to stop."