GLL points and the nodal Lagrange basis

Spectral Methods

Every SEM thing we'll do downstream — mass matrices, stiffness matrices, derivatives, quadrature, time stepping — sits on top of one decision: where to put the nodes. This chapter justifies the answer and builds the cardinal basis.


The basis on a single element

Inside one element (parameterized to for convenience), pick nodes and use the nodal Lagrange basis:

Each is a degree- polynomial that equals 1 at node and 0 at every other node. Just like FEM hat functions, but smooth and high-order. The Kronecker-delta property carries through: a function expanded in this basis, , has coefficients — values at nodes, no transformation.

def lagrange(j: int, x: float, nodes: np.ndarray) -> float:
    """The j-th cardinal Lagrange polynomial at point x."""
    xj = nodes[j]
    prod = 1.0
    for k, xk in enumerate(nodes):
        if k != j:
            prod *= (x - xk) / (xj - xk)
    return prod


# Property check: ℓ_j is 1 at its own node and 0 at every other node.
nodes = gll_points(8)
for j in range(len(nodes)):
    print(f"ℓ_{j} at nodes: {[round(lagrange(j, x, nodes), 4) for x in nodes]}")
ℓ_0 at nodes: [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
ℓ_1 at nodes: [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
ℓ_2 at nodes: [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
...
ℓ_8 at nodes: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]

Why GLL specifically

Three node distributions to consider on : equispaced (the obvious choice — and the one that will fail), Chebyshev (, used in global spectral methods), and Gauss-Lobatto-Legendre.

Equispaced nodes look natural and behave terribly. Their Lebesgue constant — a quantitative measure of how badly interpolation can amplify input error — grows like . Exponentially bad. The visible consequence is Runge's phenomenon: interpolating a smooth function at equispaced nodes produces enormous oscillations near the endpoints, growing with . The visualizer below shows it directly.

Chebyshev and GLL nodes both cluster near the endpoints, which suppresses the Runge oscillations. Their Lebesgue constants grow only like — exponentially better. Both give spectral interpolation accuracy on smooth functions.

What sets GLL apart from Chebyshev is the matched quadrature. Chebyshev nodes are the natural points for Chebyshev expansions; GLL nodes are the quadrature points of GLL quadrature. Using GLL nodes for both the basis and the quadrature gives a rare alignment that makes the mass matrix diagonal — chapter 2's punchline. Chebyshev doesn't have that property; that's why SEM picks GLL even though Chebyshev would interpolate equally well.


Computing the nodes

The N+1 GLL nodes are: plus the interior roots of the derivative of the -th Legendre polynomial, .

Newton iteration on , starting from the Chebyshev nodes as initial guesses, converges to machine precision in a few iterations. Compute via the three-term recurrence ; derive and from the standard Legendre ODE.

import numpy as np

def gll_points(N: int) -> np.ndarray:
    """Return the N+1 Gauss-Lobatto-Legendre nodes on [-1, 1].

    Endpoints are ±1; the N-1 interior nodes are roots of P'_N(x).
    Newton iteration on the Legendre polynomial derivative, starting
    from Chebyshev points (always close enough to converge fast).
    """
    if N == 1:
        return np.array([-1.0, 1.0])
    nodes = np.empty(N + 1)
    nodes[0], nodes[-1] = -1.0, 1.0

    for k in range(1, N):
        x = -np.cos(np.pi * k / N)             # initial guess
        for _ in range(60):
            p, dp, ddp = _legendre(N, x)
            dx = -dp / ddp
            x += dx
            if abs(dx) < 1e-14:
                break
        nodes[k] = x
    return nodes


def _legendre(N: int, x: float):
    """P_N(x), P'_N(x), P''_N(x) via three-term recurrence + ODE."""
    pNm2, pNm1 = 1.0, x
    for n in range(1, N):
        pN = ((2 * n + 1) * x * pNm1 - n * pNm2) / (n + 1)
        pNm2, pNm1 = pNm1, pN

    p, p_prev = (1.0, 0.0) if N == 0 else ((x, 1.0) if N == 1 else (pNm1, pNm2))
    dp = N * (p_prev - x * p) / (1 - x * x)
    ddp = (2 * x * dp - N * (N + 1) * p) / (1 - x * x)
    return p, dp, ddp

For or so this is essentially instant — a few microseconds per call. Production codes precompute and cache the nodes; we recompute on the fly because the cost is negligible at our resolution.


See it

Top plot: the nodes for the chosen distribution (GLL or equispaced) and the cardinal Lagrange basis functions living on them. Click any node, or use the prev/next buttons, to spotlight one basis function — notice it's exactly 1 at its own node and 0 at the others.

Bottom plot: Runge's function interpolated at the same nodes. The truth is the muted gray curve; the interpolant is the accent polyline through the orange nodes. Switch between GLL and equispaced and ramp up — at or higher the equispaced interpolant has visible spikes near the endpoints; the GLL one tracks the truth even at the highest order shown.

10ℓⱼ (x) — cardinal Lagrange basis20-1interpolating 1 / (1 + 25 x²)truthinterpolant−10+1
nodes
order N
spotlight2 at x = -0.6772max |interp| = 1.00

Things to read off:


What just got built

Two ingredients ready for chapter 2:

Chapter 2 builds GLL quadrature on top of these and shows how integrating the basis against itself collapses the mass matrix to a diagonal. That's the moment SEM gets cheap.