12.6 Variational methods in software

Software 12.6.1 (Rayleigh and Ritz). The Rayleigh quotient of \(x(1 - x)\) on \((0, 1)\), and the Ritz method with the two trial functions \(x(1 - x)\) and \(x^2(1 - x)^2\):

>>> import sympy as sp
>>> x = sp.symbols('x')
>>> def R(u):                              # Rayleigh quotient on (0, 1)
...     top = sp.integrate(u.diff(x)**2, (x, 0, 1))
...     return top / sp.integrate(u**2, (x, 0, 1))
...
>>> R(x * (1 - x)), sp.N(sp.pi**2, 8)      # one trial function
(10, 9.8696044)
>>> phi = [x * (1 - x), x**2 * (1 - x)**2]   # Ritz: best combination of two
>>> I = lambda g: sp.integrate(g, (x, 0, 1))
>>> K = sp.Matrix(2, 2, lambda i, j: I(phi[i].diff(x) * phi[j].diff(x)))
>>> M = sp.Matrix(2, 2, lambda i, j: I(phi[i] * phi[j]))
>>> lam = sp.symbols('lam')
>>> roots = sp.solve((K - lam * M).det(), lam)
>>> sorted(sp.N(r, 10) for r in roots)     # against pi^2 and 9 pi^2
[9.869749621, 102.1302504]

Reading the output. One trial function gives \(10\); two give \(9.869750\), within \(1.5 \times 10^{-5}\) of \(\pi ^2 = 9.869604\), and always above it. Both trial functions are symmetric about \(\frac 12\), so the second Ritz value, \(102.13\), is an upper bound for the next symmetric eigenvalue, \(9\pi ^2 = 88.83\), not for \(4\pi ^2\).

MATLAB/Octave: eig(K, M) for the generalised eigenvalue problem.

Software 12.6.2 (Linear finite elements). The finite element method for \(-u'' = \pi ^2\sin \pi x\), whose solution is \(\sin \pi x\), assembled element by element with five-point Gauss quadrature; the \(L^2\) error, the energy-norm error, and the largest error at the nodes, for \(N\) elements:

>>> import numpy as np
>>> gx, gw = np.polynomial.legendre.leggauss(5)  # Gauss points on [-1, 1]
>>> def fem(N, f):  # -u'' = f on (0, 1), linear elements
...     h = 1 / N; nodes = np.linspace(0, 1, N + 1)
...     K = (np.diag(2*np.ones(N - 1)) - np.diag(np.ones(N - 2), 1)
...          - np.diag(np.ones(N - 2), -1)) / h
...     b = np.zeros(N - 1)
...     for e in range(N):                 # load vector, element by element
...         xq = nodes[e] + h * (gx + 1) / 2; wq = gw * h / 2
...         for loc, node in ((1 - (xq - nodes[e]) / h, e - 1),
...                           ((xq - nodes[e]) / h, e)):
...             if 0 <= node < N - 1:
...                 b[node] += np.sum(wq * f(xq) * loc)
...     return nodes, np.r_[0, np.linalg.solve(K, b), 0]
...
>>> def errors(nodes, U, u, du):  # L2 and energy-norm errors, by Gauss
...     eL2 = eH1 = 0.0
...     for e in range(len(nodes) - 1):
...         a, h = nodes[e], nodes[e + 1] - nodes[e]
...         xq = a + h * (gx + 1) / 2; wq = gw * h / 2
...         Uq = U[e] + (U[e + 1] - U[e]) * (xq - a) / h
...         eL2 += np.sum(wq * (u(xq) - Uq)**2)
...         eH1 += np.sum(wq * (du(xq) - (U[e + 1] - U[e]) / h)**2)
...     return np.sqrt(eL2), np.sqrt(eH1)
...
>>> f = lambda s: np.pi**2 * np.sin(np.pi * s)        # exact u = sin(pi x)
>>> for N in (8, 16, 32, 64):
...     nodes, U = fem(N, f)
...     e0, e1 = errors(nodes, U, lambda s: np.sin(np.pi*s),
...                     lambda s: np.pi * np.cos(np.pi*s))
...     print(N, f"{e0:.3e}", f"{e1:.3e}",
...           f"{np.abs(U - np.sin(np.pi*nodes)).max():.1e}")
...
8 9.921e-03 2.512e-01 2.2e-16
16 2.487e-03 1.258e-01 2.8e-16
32 6.220e-04 6.295e-02 2.8e-15
64 1.555e-04 3.148e-02 9.1e-15

Reading the output. Each doubling of \(N\) divides the \(L^2\) error by \(4\) and the energy error by \(2\): orders \(2\) and \(1\), as predicted. The nodal errors are at rounding level. This is special to one dimension: the Green’s function \(G(x_i, \cdot )\) of Section 9 is piecewise linear with a corner at \(x_i\), so it lies in \(V_h\), and Galerkin orthogonality then gives \(u_h(x_i) = u(x_i)\) exactly.

MATLAB/Octave: the same assembly loop; K = (2*eye(N-1) - diag(ones(N-2,1),1) - diag(ones(N-2,1),-1))/h.

Software 12.6.3 (Weierstrass’s example). The value of \(\int _{-1}^1x^2y'^2\,dx\) for \[ y_\varepsilon (x) = \frac {\arctan (x/\varepsilon )}{\arctan (1/\varepsilon )} , \] which satisfies \(y(\pm 1) = \pm 1\):

>>> from scipy.integrate import quad
>>> def weierstrass(eps):                  # E[y] = int_{-1}^{1} x^2 y'^2 dx
...     a = np.arctan(1 / eps)             # y = arctan(x/eps)/arctan(1/eps)
...     dy = lambda s: eps / (eps**2 + s**2) / a
...     g = lambda s: s**2 * dy(s)**2
...     return quad(g, -1, 1, points=[0], limit=200)[0]
...
>>> [f"{weierstrass(e):.3e}" for e in (0.1, 0.01, 0.001, 0.0001)]
['6.340e-02', '6.366e-03', '6.366e-04', '6.366e-05']

Reading the output. The values fall like \(2\varepsilon /\pi \), so the infimum is \(0\), but it is not attained: the minimising sequence steepens into a step at the origin.

MATLAB/Octave: integral(@(x) x.^2.*dy(x).^2, -1, 1).

Questions on this section

Stuck on something here? Ask below and it stays attached to this topic.