Function Spaces: L2, H1, and Hilbert Spaces¶
This notebook accompanies Chapter 2 (Representing Functions and Fields) and prepares its section on inner products, projection, and orthogonality.
A taut string on $[0, 1]$ is pinned at both ends and carries a load $f(x)$. Its displacement $u(x)$ solves
$$-u''(x) = f(x), \qquad u(0) = u(1) = 0.$$
The chapter measures the error of an approximate displacement with a norm, and it finds the closest approximation by projection. Both operations need a set of functions on which the norm is finite and inside which limits of approximations stay. A point load bends the string into a corner, a load can jump, and a function can grow without bound near one end. Which of these functions belong to the set, and which norm decides?
import pathlib
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import quad
_d = pathlib.Path.cwd()
for cand in (_d, *_d.parents):
if (cand / "sciml_style.mplstyle").exists():
plt.style.use(str(cand / "sciml_style.mplstyle"))
break
plt.rcParams.update({"text.usetex": False, "font.family": "sans-serif"})
BLUE, ORANGE, GREEN, RED = "#2997c8", "#ef8354", "#4f9d69", "#d95d5d"
INK, MUTED, YELLOW = "#1d252c", "#687078", "#f0c84b"
# grid spacing 5e-6; x = 1/2 is a grid point
x = np.linspace(0.0, 1.0, 200001)
def l2(f):
return np.sqrt(np.trapezoid(f**2, x))
def sine_coeff(g, k):
# b_k = 2 int_0^1 g sin(k pi x) dx, so that g = sum b_k sin(k pi x)
return 2 * np.trapezoid(g * np.sin(k * np.pi * x), x)
$L^2$, finite energy in value¶
The first measure of a function is the size of its values. The $L^2$ norm squares the values, integrates, and takes the root,
$$\|f\|_{L^2} = \left(\int_0^1 f(x)^2 \, dx\right)^{1/2}, \qquad \langle f, g \rangle = \int_0^1 f(x)\, g(x) \, dx,$$
and the inner product on the right produces the norm through $\langle f, f \rangle = \|f\|_{L^2}^2$. The space $L^2(0,1)$ contains every measurable function on $(0,1)$ whose squared integral is finite.
The unit step $H$, which jumps from $0$ to $1$ at $x = 1/2$, belongs to $L^2$ with $\|H\|_{L^2} = \sqrt{1/2} = 0.7071$. A member of $L^2$ can also be unbounded. The function $x^{-1/3}$ grows without bound near $x = 0$, yet $\int_0^1 x^{-2/3} \, dx = 3$, so $\|x^{-1/3}\|_{L^2} = \sqrt{3} = 1.7321$. The function $x^{-1/2}$ grows a little faster near zero. Its square $1/x$ integrates from $\epsilon$ to $1$ to $\ln(1/\epsilon)$, which is $4.61$ at $\epsilon = 10^{-2}$ and $18.42$ at $\epsilon = 10^{-8}$ and grows without bound as $\epsilon \to 0$. So $x^{-1/2}$ is outside $L^2$. The power $x^{-p}$ belongs to $L^2(0,1)$ when $p < 1/2$ and fails when $p \geq 1/2$.
The inner product of the step with $x^{-1/3}$ is $\int_{1/2}^1 x^{-1/3} \, dx = 0.5551$. The Cauchy-Schwarz inequality bounds it by the product of the norms, $0.7071 \times 1.7321 = 1.2247$.
Two functions that differ at one point have $L^2$ distance zero. Take $f(x) = x$, and let $g$ agree with $f$ except at $x = 1/2$, where $g(1/2) = 5$. The squared difference is zero except at one point, and its integral is zero. A trapezoid sum on a grid of spacing $h$ that contains $x = 1/2$ gives that point the weight $h$, so the sampled distance is $5\sqrt{h}$. It falls from $1.58$ at $h = 0.1$ to $0.05$ at $h = 10^{-4}$. In $L^2$, $f$ and $g$ are the same element. An element of $L^2$ therefore has no value at a point, since its value at $x = 1/2$ can change without changing the element.
step = np.where(x < 0.5, 0.0, 1.0)
print(f"||H||_L2 = {l2(step):.4f}")
I13, _ = quad(lambda t: t**(-2/3), 0, 1, limit=200)
print(f"int_0^1 x^(-2/3) dx = {I13:.4f}, ||x^(-1/3)||_L2 = {np.sqrt(I13):.4f}")
for eps in (1e-2, 1e-4, 1e-8):
I, _ = quad(lambda t: 1 / t, eps, 1, limit=200)
print(f"int_eps^1 dx/x at eps = {eps:.0e}: {I:.2f}")
ip, _ = quad(lambda t: t**(-1/3), 0.5, 1)
print(f"<H, x^(-1/3)> = {ip:.4f}, ||H|| ||x^(-1/3)|| = {l2(step) * np.sqrt(I13):.4f}")
hs, dist = [], []
for N in (10, 100, 1000, 10000):
xg = np.linspace(0, 1, N + 1)
d = np.zeros_like(xg)
d[N // 2] = 5.0 # g - f is nonzero only at x = 1/2
hs.append(1 / N)
dist.append(np.sqrt(np.trapezoid(d**2, xg)))
print(f"h = {1/N:.0e}: sampled ||f - g|| = {dist[-1]:.4f}")
fig, ax = plt.subplots(1, 3, figsize=(11.5, 3.5))
xp = np.linspace(1e-4, 1, 4000)
ax[0].plot(xp, np.where(xp < 0.5, 0.0, 1.0), color=BLUE, lw=2.5, label="unit step H")
ax[0].plot(xp, xp**(-1/3), color=GREEN, lw=2.5, label=r"$x^{-1/3}$, in $L^2$")
ax[0].plot(xp, xp**(-1/2), color=RED, lw=2.5, ls="--", label=r"$x^{-1/2}$, not in $L^2$")
ax[0].set_ylim(0, 6)
ax[0].set_xlabel("x")
ax[0].legend(frameon=False, fontsize=8)
ax[0].set_title("three functions on (0, 1)", fontsize=10)
eps = np.logspace(-10, -0.3, 60)
ax[1].semilogx(eps, [quad(lambda t: t**(-2/3), e, 1)[0] for e in eps], color=GREEN, lw=2.5,
label=r"$\int_\epsilon^1 x^{-2/3}dx \to 3$")
ax[1].semilogx(eps, [quad(lambda t: 1 / t, e, 1, limit=200)[0] for e in eps], color=RED, lw=2.5,
ls="--", label=r"$\int_\epsilon^1 x^{-1}dx = \ln(1/\epsilon)$")
ax[1].axhline(3, color=MUTED, lw=1, ls=":")
ax[1].invert_xaxis()
ax[1].set_xlabel(r"truncation $\epsilon$")
ax[1].legend(frameon=False, fontsize=8)
ax[1].set_title("squared integrals near x = 0", fontsize=10)
ax[2].loglog(hs, dist, "o-", color=BLUE, lw=2.5)
ax[2].set_xlabel("grid spacing h")
ax[2].set_ylabel("sampled distance")
ax[2].set_title("f = x against g = f except g(1/2) = 5", fontsize=10)
plt.show()
||H||_L2 = 0.7071 int_0^1 x^(-2/3) dx = 3.0000, ||x^(-1/3)||_L2 = 1.7321 int_eps^1 dx/x at eps = 1e-02: 4.61 int_eps^1 dx/x at eps = 1e-04: 9.21 int_eps^1 dx/x at eps = 1e-08: 18.42 <H, x^(-1/3)> = 0.5551, ||H|| ||x^(-1/3)|| = 1.2247 h = 1e-01: sampled ||f - g|| = 1.5811 h = 1e-02: sampled ||f - g|| = 0.5000 h = 1e-03: sampled ||f - g|| = 0.1581 h = 1e-04: sampled ||f - g|| = 0.0500
$H^1$, finite energy in value and slope¶
A displacement also has a slope, and $L^2$ measures none of it. The $H^1$ norm adds the squared slope to the squared value,
$$\|u\|_{H^1} = \left(\|u\|_{L^2}^2 + \|u'\|_{L^2}^2\right)^{1/2}, \qquad \langle u, v \rangle_{H^1} = \int_0^1 \left(u v + u' v'\right) dx.$$
Here $u'$ is the weak derivative of the chapter's section on completeness, so a function with a corner has a slope defined everywhere except at the corner. The space $H^1(0,1)$ contains the functions of $L^2$ whose weak derivative is also in $L^2$.
The pinned string's displacements form $H^1_0(0,1)$, the members of $H^1$ with $u(0) = u(1) = 0$. On $H^1_0$ the slope term alone is a norm, the seminorm $|u|_{H^1} = \|u'\|_{L^2}$. A pinned function with zero slope is a constant that vanishes at the ends, so only the zero function has $|u|_{H^1} = 0$. The Poincaré inequality $\|u\|_{L^2} \leq |u|_{H^1} / \pi$ states the same fact with a constant, and $\sin(\pi x)$ attains it. For a string of unit tension, half the square of the seminorm is the elastic energy, $E(u) = \tfrac12 \int_0^1 u'^2 \, dx$.
The point-load corner $u = \min(x, 1-x)/2$ lies in $H^1_0$ (top left panel). Its slope is a step from $+1/2$ to $-1/2$, whose square integrates to $1/4$. So $|u|_{H^1} = 0.5$, the elastic energy is $0.125$, and $\pi \|u\|_{L^2} / |u|_{H^1} = 0.9069$ sits below the Poincaré bound of $1$.
The step $H$ is outside $H^1$. Its derivative is a delta at the jump, and the delta is not a square-integrable function. Ramps approximating the step show the failure in numbers (top right panel). A ramp that climbs from $0$ to $1$ across width $w$ has slope $1/w$ on that interval, so its slope-squared integral is $1/w$. This integral is $5$ at $w = 0.2$ and $100$ at $w = 0.01$, and it grows without bound as the ramp approaches the step (bottom left panel).
In one dimension every $H^1$ function is continuous. For $x < y$, the Cauchy-Schwarz inequality gives
$$|u(y) - u(x)| = \left|\int_x^y u'(s) \, ds\right| \leq \sqrt{y - x}\, \|u'\|_{L^2},$$
so nearby points have nearby values, and every element of $H^1$ has a continuous representative with well-defined point values. The bound needs only a square-integrable slope. The function $u = x^{3/4} - x$ has the slope $\tfrac34 x^{-1/4} - 1$, which is unbounded at $x = 0$, and yet $|u|_{H^1}^2 = 0.125$. Over $20000$ random pairs of points, the largest ratio of $|u(y) - u(x)|$ to $\sqrt{|y-x|}\,|u|_{H^1}$ is $0.7028$ (bottom right panel).
The continuity bound holds in one dimension only. On the disk of radius $1/2$ in the plane, $u = \ln \ln (1/r)$ has $\int |\nabla u|^2 \, dA = 2\pi / \ln 2 = 9.0647$ and a finite $L^2$ norm, so it lies in $H^1$, and it is unbounded at the center. In two or more dimensions an $H^1$ function need not be bounded or continuous.
corner = np.minimum(x, 1 - x) / 2
dcorner = np.where(x < 0.5, 0.5, -0.5)
semi = l2(dcorner)
print(f"corner: ||u||_L2 = {l2(corner):.4f}, |u|_H1 = {semi:.4f}, ||u||_H1 = {np.sqrt(l2(corner)**2 + semi**2):.4f}")
print(f"corner: elastic energy = {0.5 * semi**2:.4f}, pi ||u|| / |u|_H1 = {np.pi * l2(corner) / semi:.4f}")
def ramp(w):
return np.clip((x - (0.5 - w / 2)) / w, 0.0, 1.0)
ws = np.array([0.2, 0.1, 0.05, 0.02, 0.01, 0.005])
energy = []
for w in ws:
r = ramp(w)
energy.append(np.sum(np.diff(r)**2 / np.diff(x))) # int (r')^2 dx for piecewise-linear r
print(f"ramp w = {w:.3f}: int (u')^2 dx = {energy[-1]:.2f} 1/w = {1/w:.2f}")
print(f"log-log slope of int (u')^2 against w: {np.polyfit(np.log(ws), np.log(energy), 1)[0]:.3f}")
u3 = lambda t: t**0.75 - t
semi3 = np.sqrt(quad(lambda t: (0.75 * t**-0.25 - 1)**2, 0, 1)[0])
rng = np.random.default_rng(1)
pairs = rng.uniform(0, 1, size=(20000, 2))
lhs = np.abs(u3(pairs[:, 1]) - u3(pairs[:, 0]))
rhs = np.sqrt(np.abs(pairs[:, 1] - pairs[:, 0])) * semi3
print(f"u = x^(3/4) - x: |u|_H1^2 = {semi3**2:.4f}, max ratio over 20000 pairs = {np.max(lhs / rhs):.4f}")
Idisk = 2 * np.pi * quad(lambda s: 1 / s**2, np.log(2), np.inf)[0] # s = ln(1/r)
print(f"u = ln ln(1/r) on r < 1/2: int |grad u|^2 dA = {Idisk:.4f}")
fig, ax = plt.subplots(2, 2, figsize=(10.5, 6.6))
ax[0, 0].plot(x, corner, color=BLUE, lw=2.5, label="u = min(x, 1 - x)/2")
ax[0, 0].plot(x[x < 0.5], dcorner[x < 0.5], color=ORANGE, lw=2.5, label="u'")
ax[0, 0].plot(x[x > 0.5], dcorner[x > 0.5], color=ORANGE, lw=2.5)
ax[0, 0].axhline(0, color=MUTED, lw=0.8)
ax[0, 0].legend(frameon=False, fontsize=8, loc="lower left")
ax[0, 0].set_title(r"the corner is in $H^1_0$, its slope is a step", fontsize=10)
for w, a in zip((0.2, 0.1, 0.05), (0.35, 0.65, 1.0)):
ax[0, 1].plot(x, ramp(w), color=BLUE, lw=2.2, alpha=a, label=f"ramp, w = {w}")
ax[0, 1].plot(x[x < 0.5], step[x < 0.5], color=INK, lw=1.8, ls="--")
ax[0, 1].plot(x[x > 0.5], step[x > 0.5], color=INK, lw=1.8, ls="--", label="step H")
ax[0, 1].set_xlim(0.3, 0.7)
ax[0, 1].legend(frameon=False, fontsize=8, loc="upper left")
ax[0, 1].set_title("ramps approaching the step", fontsize=10)
ax[1, 0].loglog(ws, energy, "o-", color=ORANGE, lw=2.5, label="measured")
ax[1, 0].loglog(ws, 1 / ws, color=INK, lw=1.2, ls=":", label="1/w")
ax[1, 0].set_xlabel("ramp width w")
ax[1, 0].set_ylabel(r"$\int_0^1 u'^2\,dx$")
ax[1, 0].legend(frameon=False, fontsize=8)
ax[1, 0].set_title("slope-squared integral grows as 1/w", fontsize=10)
ax[1, 1].scatter(rhs, lhs, s=2, color=BLUE, alpha=0.3)
lim = rhs.max()
ax[1, 1].plot([0, lim], [0, lim], color=INK, lw=1.2, ls="--", label="bound")
ax[1, 1].set_xlabel(r"$\sqrt{|y-x|}\;|u|_{H^1}$")
ax[1, 1].set_ylabel(r"$|u(y)-u(x)|$")
ax[1, 1].legend(frameon=False, fontsize=8)
ax[1, 1].set_title("continuity bound for u = x^(3/4) - x", fontsize=10)
plt.show()
corner: ||u||_L2 = 0.1443, |u|_H1 = 0.5000, ||u||_H1 = 0.5204 corner: elastic energy = 0.1250, pi ||u|| / |u|_H1 = 0.9069 ramp w = 0.200: int (u')^2 dx = 5.00 1/w = 5.00 ramp w = 0.100: int (u')^2 dx = 10.00 1/w = 10.00 ramp w = 0.050: int (u')^2 dx = 20.00 1/w = 20.00 ramp w = 0.020: int (u')^2 dx = 50.00 1/w = 50.00 ramp w = 0.010: int (u')^2 dx = 100.00 1/w = 100.00 ramp w = 0.005: int (u')^2 dx = 200.00 1/w = 200.00 log-log slope of int (u')^2 against w: -1.000 u = x^(3/4) - x: |u|_H1^2 = 0.1250, max ratio over 20000 pairs = 0.7028 u = ln ln(1/r) on r < 1/2: int |grad u|^2 dA = 9.0647
Wiggles¶
The $L^2$ norm cannot tell a slow shape from a fast wiggle of the same size. The string's modes $e_k(x) = \sqrt{2}\sin(k\pi x)$ are orthonormal in $L^2$, so every mode has $\|e_k\|_{L^2} = 1$. Their slopes are $\sqrt{2}\,k\pi\cos(k\pi x)$, and the slope norm grows with the mode number, $|e_k|_{H^1} = k\pi$. Mode $10$ has the $L^2$ norm of mode $1$, a slope norm ten times larger ($31.42$ against $3.14$), and one hundred times the elastic energy. The lab below draws the mode beside its slope.
for k in (1, 2, 3, 5, 10):
e = np.sqrt(2) * np.sin(k * np.pi * x)
de = np.sqrt(2) * k * np.pi * np.cos(k * np.pi * x)
print(f"k = {k:2d}: ||e_k||_L2 = {l2(e):.4f}, |e_k|_H1 = {l2(de):.4f}, "
f"k pi = {k*np.pi:.4f}, energy = {0.5*l2(de)**2:.2f}")
k = 1: ||e_k||_L2 = 1.0000, |e_k|_H1 = 3.1416, k pi = 3.1416, energy = 4.93 k = 2: ||e_k||_L2 = 1.0000, |e_k|_H1 = 6.2832, k pi = 6.2832, energy = 19.74 k = 3: ||e_k||_L2 = 1.0000, |e_k|_H1 = 9.4248, k pi = 9.4248, energy = 44.41 k = 5: ||e_k||_L2 = 1.0000, |e_k|_H1 = 15.7080, k pi = 15.7080, energy = 123.37 k = 10: ||e_k||_L2 = 1.0000, |e_k|_H1 = 31.4159, k pi = 31.4159, energy = 493.48
Hilbert spaces¶
Every example so far is a single function, and the chapter also needs limits of sequences of functions. Consider the continuous functions on $[0,1]$ with the $L^2$ distance. The ramp of width $w = 1/n$ differs from the step only across its transition, where the gap rises linearly to $1/2$ and falls back. The squared gap integrates to $w/12$, so the distance from ramp to step is $\sqrt{w/12}$, which is $0.2041$ at $n = 2$ and $0.0255$ at $n = 128$. By the triangle inequality, two ramps are no farther apart than the sum of their distances to the step, and the ramps of width $1/8$ and $1/32$ lie $0.0765$ apart. A sequence whose terms come arbitrarily close to one another is a Cauchy sequence. Every ramp is continuous, and the $L^2$ limit of the ramps is the step, which is not. The continuous functions with the $L^2$ distance lack the limit of one of their own Cauchy sequences.
A space is complete when every Cauchy sequence in it converges to a member of the space. A Hilbert space is an inner-product space that is complete in the norm its inner product defines. $\mathbb{R}^n$ with the dot product is a Hilbert space. So are $L^2(0,1)$ with $\int f g \, dx$, $H^1(0,1)$ with $\int (u v + u' v') \, dx$, and $H^1_0(0,1)$ with $\int u' v' \, dx$. The continuous functions with the sup norm are complete, and the sup norm comes from no inner product, so that space is complete without being a Hilbert space.
The spaces nest, $H^1_0 \subset H^1 \subset L^2$ (left panel below). The mode $\sin(\pi x)$ and the corner lie in $H^1_0$. The ramp $u = x$ lies in $H^1$ and outside $H^1_0$, because $u(1) = 1$. The step and $x^{-1/3}$ lie in $L^2$ and outside $H^1$, and $x^{-1/2}$ lies outside all three.
Projection needs completeness. Take trial shapes $v_1, v_2, \dots$ in a subspace $V$ whose distances to a target $u$ approach the smallest possible distance. The parallelogram law makes this minimizing sequence Cauchy. Completeness places its limit in the space, and when $V$ is closed the limit also stays in $V$. That limit is the closest point of $V$ to $u$, and the projection theorem needs both hypotheses to guarantee it.
from matplotlib.patches import Ellipse
for n in (2, 8, 32, 128):
w = 1 / n
print(f"n = {n:3d}, w = {w:.4f}: ||ramp - H||_L2 = {l2(ramp(w) - step):.4f} sqrt(w/12) = {np.sqrt(w/12):.4f}")
print(f"||ramp(1/8) - ramp(1/32)||_L2 = {l2(ramp(1/8) - ramp(1/32)):.4f}")
fig, (ax, ax2) = plt.subplots(1, 2, figsize=(12, 4.6), gridspec_kw={"width_ratios": [1.7, 1]})
ax.set_xlim(0, 12.2)
ax.set_ylim(0, 6.4)
ax.set_aspect("equal")
ax.axis("off")
for (cx, wd, ht, col, lab, lx) in [(5.0, 9.6, 6.0, BLUE, r"$L^2(0,1)$", 8.35),
(3.9, 6.8, 4.7, GREEN, r"$H^1(0,1)$", 5.9),
(3.05, 4.5, 3.3, ORANGE, r"$H^1_0(0,1)$", 3.05)]:
ax.add_patch(Ellipse((cx, 3.2), wd, ht, fc=col, alpha=0.10, ec=col, lw=2))
ax.text(lx, 3.2 + ht / 2 - 0.45, lab, color=col, ha="center", fontsize=11)
xt = np.linspace(1e-3, 1, 400)
thumbs = [
(1.15, 2.2, np.sin(np.pi * xt), r"$\sin(\pi x)$", INK),
(3.05, 2.2, np.minimum(xt, 1 - xt), "corner", INK),
(5.45, 2.5, xt, "u = x", INK),
(7.75, 3.55, np.where(xt < 0.5, 0.0, 1.0), "step", INK),
(7.75, 1.25, np.minimum(xt**(-1/3), 4), r"$x^{-1/3}$", INK),
(10.45, 2.5, np.minimum(xt**(-1/2), 6), r"$x^{-1/2}$", RED),
]
for (x0, y0, fy, lab, col) in thumbs:
ins = ax.inset_axes([x0, y0, 1.35, 0.95], transform=ax.transData)
ins.plot(xt, fy, color=col, lw=1.8)
ins.set_xticks([])
ins.set_yticks([])
ins.set_title(lab, fontsize=8, pad=2)
for s in ins.spines.values():
s.set_visible(True)
s.set_color(MUTED)
s.set_linewidth(0.6)
ax.text(11.12, 1.75, "outside L2", color=RED, ha="center", fontsize=8)
wn = 1 / np.array([2, 4, 8, 16, 32, 64, 128])
dn = [l2(ramp(w) - step) for w in wn]
ax2.loglog(wn, dn, "o-", color=BLUE, lw=2.5, label="measured")
ax2.loglog(wn, np.sqrt(wn / 12), color=INK, lw=1.2, ls=":", label=r"$\sqrt{w/12}$")
ax2.set_xlabel("ramp width w = 1/n")
ax2.set_ylabel("L2 distance to the step")
ax2.legend(frameon=False, fontsize=8)
ax2.set_title("continuous ramps converge to a step", fontsize=10)
plt.show()
n = 2, w = 0.5000: ||ramp - H||_L2 = 0.2041 sqrt(w/12) = 0.2041 n = 8, w = 0.1250: ||ramp - H||_L2 = 0.1021 sqrt(w/12) = 0.1021 n = 32, w = 0.0312: ||ramp - H||_L2 = 0.0510 sqrt(w/12) = 0.0510 n = 128, w = 0.0078: ||ramp - H||_L2 = 0.0255 sqrt(w/12) = 0.0255 ||ramp(1/8) - ramp(1/32)||_L2 = 0.0765
Compactness¶
A bounded sequence of real numbers has a convergent subsequence, and so does a bounded sequence in $\mathbb{R}^n$. The unit ball of $L^2(0,1)$ lacks this property. The modes $e_k$ all have norm $1$, and for $j \neq k$ orthogonality gives
$$\|e_j - e_k\|_{L^2}^2 = \|e_j\|_{L^2}^2 + \|e_k\|_{L^2}^2 = 2,$$
so every two modes lie at distance $\sqrt{2} = 1.4142$. No subsequence of the modes is Cauchy, and none converges.
A bound on the slope restores the property. Write $u = \sum_k c_k e_k$ in $H^1_0$, so that $\|u\|_{L^2}^2 = \sum_k c_k^2$ and $|u|_{H^1}^2 = \sum_k (k\pi)^2 c_k^2$. If $|u|_{H^1} \leq M$, the modes beyond $K$ satisfy
$$\sum_{k > K} c_k^2 \;\leq\; \frac{1}{\big((K+1)\pi\big)^2} \sum_{k > K} (k\pi)^2 c_k^2 \;\leq\; \frac{M^2}{\big((K+1)\pi\big)^2}.$$
The tail beyond mode $K$ has $L^2$ norm at most $M/((K+1)\pi)$, and the same bound holds for every $u$ with $|u|_{H^1} \leq M$. All energy in mode $K+1$ attains it, and at $M = 4$ and $K = 5$ that tail and the bound both equal $0.2122$. Over $500$ random coefficient lists with random $M$ and $K$, the largest ratio of tail to bound is $0.633$ and the median is $0.327$. So every function in this set lies within $M/((K+1)\pi)$ of the space of the first $K$ modes, and in that finite-dimensional space bounded sets are compact. This is Rellich's theorem for the interval. On a bounded domain $\Omega$, every sequence bounded in $H^1_0(\Omega)$ has a subsequence that converges in $L^2(\Omega)$. The same holds for $H^1(\Omega)$ when the boundary of $\Omega$ is Lipschitz, which includes every bounded interval. The modes $e_k$ fall outside the hypothesis, because $|e_k|_{H^1} = k\pi$ grows without bound.
The tail bound turns truncation into an approximation with a guaranteed error. Keeping $K$ sine modes of a displacement whose elastic energy is at most $M^2/2$ leaves an $L^2$ error of at most $M/((K+1)\pi)$. The same argument on each element of length $h$ bounds the $L^2$ error of hat interpolation by $hM/\pi$, and the same $500$ draws reach at most $0.711$ of that bound. The lab draws a random coefficient list scaled to $|u|_{H^1} = M$, its truncation to $K$ modes, and the measured tail against the bound.
modes = np.array([np.sqrt(2) * np.sin(k * np.pi * x) for k in range(1, 7)])
D = np.array([[l2(a - b) for b in modes] for a in modes])
off = D[~np.eye(6, dtype=bool)]
print(f"||e_j - e_k||_L2 for 1 <= j < k <= 6: min = {off.min():.4f}, max = {off.max():.4f}")
rng = np.random.default_rng(0)
N = 60
k = np.arange(1, N + 1)
xs = np.linspace(0, 1, 4001)
S = np.sqrt(2) * np.sin(np.outer(k, np.pi * xs)) # modes on the grid xs
tail_ratio, hat_ratio = [], []
for trial in range(500):
M = rng.uniform(0.5, 10)
K = int(rng.integers(1, 31))
c = rng.standard_normal(N) / k**1.5
c *= M / np.sqrt(np.sum((k * np.pi * c)**2))
u = c @ S
tail = np.sqrt(np.trapezoid((u - c[:K] @ S[:K])**2, xs))
tail_ratio.append(tail / (M / ((K + 1) * np.pi)))
n_el = int(rng.integers(2, 33))
nodes = np.linspace(0, 1, n_el + 1)
interp = np.interp(xs, nodes, np.interp(nodes, xs, u))
hat_ratio.append(np.sqrt(np.trapezoid((u - interp)**2, xs)) / (M / (n_el * np.pi)))
print(f"500 random draws: max tail / (M/((K+1) pi)) = {max(tail_ratio):.3f}, median = {np.median(tail_ratio):.3f}")
print(f"500 random draws: max hat error / (h M / pi) = {max(hat_ratio):.3f}")
M, K = 4.0, 5
c = np.zeros(N)
c[K] = M / ((K + 1) * np.pi) # all energy in mode K + 1
u = c @ S
print(f"M = {M}, K = {K}: |u|_H1 = {np.sqrt(np.sum((k*np.pi*c)**2)):.4f}, "
f"tail = {np.sqrt(np.trapezoid((u - c[:K] @ S[:K])**2, xs)):.4f}, bound = {M/((K+1)*np.pi):.4f}")
||e_j - e_k||_L2 for 1 <= j < k <= 6: min = 1.4142, max = 1.4142 500 random draws: max tail / (M/((K+1) pi)) = 0.633, median = 0.327 500 random draws: max hat error / (h M / pi) = 0.711 M = 4.0, K = 5: |u|_H1 = 4.0000, tail = 0.2122, bound = 0.2122
Regularity¶
The load decides which space the displacement lands in. Integrating $-u'' = f$ twice gains two derivatives, so a smoother load gives a smoother displacement.
A point load $f = \delta(x - 1/2)$ bends the string into the corner $\min(x, 1-x)/2$, whose slope jumps by $-1$ at the load. The corner lies in $H^1_0$ and outside $H^2$, since $u'' = -\delta$ is not square integrable. The point load has no square-integrable density. Its work on a displacement $v$ is $v(1/2)$, and the continuity bound with $x = 0$ gives $|v(1/2)| \leq \sqrt{1/2}\,|v|_{H^1}$. By the Riesz representation theorem in $H^1_0$, that bound is enough to place the displacement in $H^1_0$.
A step load, $f = 2$ on $(0, 1/2)$ and $f = 0$ on $(1/2, 1)$, gives
$$u(x) = \begin{cases} -x^2 + \tfrac34 x, & 0 \leq x \leq \tfrac12, \\[2pt] \tfrac14 (1 - x), & \tfrac12 \leq x \leq 1. \end{cases}$$
Both pieces equal $1/8$ at $x = 1/2$ with slope $-1/4$, so the slope is continuous. The curvature $u'' = -f$ jumps from $-2$ to $0$ there. This displacement lies in $H^2$, with $\|u''\|_{L^2} = \|f\|_{L^2} = \sqrt{2} = 1.4142$. A uniform load $f = 2$ gives the parabola $x(1-x)$, which has derivatives of every order on $[0,1]$.
The sine coefficients record the same order. Continue a piecewise-smooth function on $(0,1)$ to an odd function of period $2$. If the continuation first jumps in its $m$-th derivative, the sine coefficients fall like $1/k^{m+1}$. The uniform load's odd continuation jumps between $+2$ and $-2$ at $x = 0$, so its coefficients fall like $1/k$ (measured slope $-1.00$). The corner is continuous and its slope jumps, so its coefficients fall like $1/k^2$ (measured $-2.00$). The parabola's slope continues across $x = 0$ and its curvature jumps there, so its coefficients fall like $1/k^3$ (measured $-3.00$). Each derivative that stays continuous adds one power of $k$ to the decay. Solving the string divides the $k$-th coefficient of the load by $(k\pi)^2$ and adds two powers at once. The point load's coefficients $2\sin(k\pi/2)$ do not decay, and the corner's fall like $1/k^2$. The step load's fall like $1/k$ and its displacement's like $1/k^3$ (measured $-3.00$). The parabola is smooth inside $(0,1)$ and still decays no faster than the step-load displacement, because the sine series treats the uniform load as its odd continuation, which jumps at the ends.
The lab plots $u$, $u'$, and $u''$ for the three loads.
f_step = np.where(x < 0.5, 2.0, 0.0)
u_step = np.where(x <= 0.5, -x**2 + 0.75 * x, (1 - x) / 4)
left, right = x <= 0.5, x >= 0.5
print(f"step load: u(1/2) from the two pieces = {-0.25 + 0.375:.4f}, {0.5/4:.4f}; "
f"u'(1/2) = {-2*0.5 + 0.75:.4f}, {-0.25:.4f}")
h = x[1] - x[0]
d2 = (u_step[2:] - 2 * u_step[1:-1] + u_step[:-2]) / h**2
away = np.abs(x[1:-1] - 0.5) > 2 * h
print(f"step load: max |u'' + f| away from x = 1/2 = {np.max(np.abs(d2 + f_step[1:-1])[away]):.1e}")
print(f"step load: ||u''||_L2 = ||f||_L2 = {l2(f_step):.4f}")
v = np.minimum(x, 1 - x) / 2
print(f"corner: v(1/2) = {v[len(x)//2]:.4f}, sqrt(1/2) |v|_H1 = {np.sqrt(0.5) * 0.5:.4f}")
ks = np.arange(1, 62, 2) # odd modes; the fit uses k >= 5
fit = ks >= 5
series = {
"point load delta(x - 1/2)": (np.abs(2 * np.sin(ks * np.pi / 2)), RED, "--"),
"step load": (np.abs([sine_coeff(f_step, k) for k in ks]), ORANGE, "--"),
"uniform load f = 2": (np.abs([sine_coeff(2 + 0 * x, k) for k in ks]), GREEN, "--"),
"corner (point load)": (np.abs([sine_coeff(np.minimum(x, 1 - x) / 2, k) for k in ks]), RED, "-"),
"step-load displacement": (np.abs([sine_coeff(u_step, k) for k in ks]), ORANGE, "-"),
"parabola x(1 - x)": (np.abs([sine_coeff(x * (1 - x), k) for k in ks]), GREEN, "-"),
}
fig, ax = plt.subplots(1, 2, figsize=(11, 3.9), sharey=True)
for i, (name, (b, col, ls)) in enumerate(series.items()):
s = np.polyfit(np.log(ks[fit]), np.log(b[fit]), 1)[0]
print(f"{name:28s} decay exponent = {s:+.2f}")
ax[0 if i < 3 else 1].loglog(ks, b, ls, color=col, lw=2.2, marker="o", ms=3, label=f"{name}, slope {s:+.2f}")
ax[0].set_title("sine coefficients of the loads", fontsize=10)
ax[1].set_title("sine coefficients of the displacements", fontsize=10)
for a in ax:
a.set_xlabel("odd mode k")
a.legend(frameon=False, fontsize=8, loc="lower left")
ax[0].set_ylabel("|b_k|")
plt.show()
step load: u(1/2) from the two pieces = 0.1250, 0.1250; u'(1/2) = -0.2500, -0.2500 step load: max |u'' + f| away from x = 1/2 = 6.5e-06 step load: ||u''||_L2 = ||f||_L2 = 1.4142 corner: v(1/2) = 0.2500, sqrt(1/2) |v|_H1 = 0.3536 point load delta(x - 1/2) decay exponent = +0.00 step load decay exponent = -1.00 uniform load f = 2 decay exponent = -1.00 corner (point load) decay exponent = -2.00 step-load displacement decay exponent = -3.00 parabola x(1 - x) decay exponent = -3.00
Summary¶
| Space | Norm | What it controls | What it excludes | Where the chapter uses it |
|---|---|---|---|---|
| $L^2(0,1)$ | $\left(\int_0^1 f^2 \, dx\right)^{1/2}$ | mean-square size of the values | $x^{-1/2}$, and any meaning for a value at one point | error of a representation, projection onto modes |
| $H^1(0,1)$ | $\left(\int_0^1 (u^2 + u'^2) \, dx\right)^{1/2}$ | values and slopes, continuity in one dimension | the step, whose derivative is a delta | weak derivatives, point values |
| $H^1_0(0,1)$ | $\left(\int_0^1 u'^2 \, dx\right)^{1/2}$ | elastic energy of a pinned string | $u = x$, which is not pinned at $x = 1$ | the string's displacements, sensor readings |
| $H^2 \cap H^1_0$ | $\left(\int_0^1 u''^2 \, dx\right)^{1/2}$ | curvature | the corner, whose curvature is a delta | displacements under square-integrable loads |
The Norms notebook measures errors in the sup, $L^2$, and $H^1$ norms, and the Projection notebook finds closest approximations with the $L^2$ inner product. In Chapter 2 these spaces appear in the sections on norms and the meaning of error, on inner products, projection, and orthogonality, and on convergence, density, and completeness.