A 1D mesh from scratch

Finite Elements

A finite element mesh is the data structure that says "here are the points, here is how they connect." For a 1D problem on the interval with elements, that's almost a one-liner — but the way we organize the data (nodes here, element-to-node connectivity there) is the same shape we'll use in 2D, so it's worth doing carefully even when the geometry is trivial.


The data structures

Two arrays are enough:

Splitting the mesh into "node positions" and "element connectivity" is what makes the rest of FEM scale: when we assemble the stiffness matrix in the next chapter we'll iterate over elements and ask "which two global nodes do you touch?" — without ever caring how the geometry was generated. Same iteration pattern in 2D, where a triangle owns three node indices instead of two.

import numpy as np


def mesh_1d(N, a=0.0, b=1.0):
    """A uniform 1D mesh of N elements on [a, b].

    Returns
    -------
    nodes : (N+1,) ndarray of node positions x_0 < x_1 < ... < x_N.
    elements : (N, 2) ndarray of node indices (i, i+1) for each element.
    """
    nodes = np.linspace(a, b, N + 1)
    elements = np.column_stack([np.arange(N), np.arange(1, N + 1)])
    return nodes, elements

The basis functions

On this mesh we put piecewise-linear basis functions. For each interior node , the basis function is a triangle: it rises linearly from 0 at to 1 at , then falls linearly back to 0 at . Outside that two-element window, it's identically zero.

The shape gives them their nickname: hat functions. Three properties matter and follow straight from the picture:

def hat(i, x, nodes):
    """The piecewise-linear ("hat") basis function for node i.

    phi_i(x) = (x - x_{i-1}) / h     for x in [x_{i-1}, x_i]
             = (x_{i+1} - x) / h     for x in [x_i, x_{i+1}]
             = 0                     otherwise
    """
    n = len(nodes) - 1
    if i < 0 or i > n:
        return np.zeros_like(x)

    out = np.zeros_like(x, dtype=float)
    if i > 0:
        h_left = nodes[i] - nodes[i - 1]
        mask = (x >= nodes[i - 1]) & (x <= nodes[i])
        out[mask] = (x[mask] - nodes[i - 1]) / h_left
    if i < n:
        h_right = nodes[i + 1] - nodes[i]
        mask = (x >= nodes[i]) & (x <= nodes[i + 1])
        out[mask] = (nodes[i + 1] - x[mask]) / h_right
    return out


def hat_derivative(i, x, nodes):
    """The derivative of the hat function — piecewise constant: +1/h, -1/h, 0."""
    n = len(nodes) - 1
    out = np.zeros_like(x, dtype=float)
    if i > 0:
        h_left = nodes[i] - nodes[i - 1]
        mask = (x > nodes[i - 1]) & (x < nodes[i])
        out[mask] = 1.0 / h_left
    if i < n:
        h_right = nodes[i + 1] - nodes[i]
        mask = (x > nodes[i]) & (x < nodes[i + 1])
        out[mask] = -1.0 / h_right
    return out

See it

Below is the mesh and its hat-function basis for various values of . The dashed half-hats at the boundary are and , which we'll pin to zero by Dirichlet boundary conditions and never include in the linear system. The solid triangles are the interior basis functions; click any interior node (or use the prev / next buttons) to spotlight one.

01φ2x0x1x2x3x4e0e1e2e3x
elements
spotlightφ2  (node x2 = 0.500)

What to read off the picture:


Try it

Sample the basis functions on a fine grid and confirm they have the properties we just listed:

# A small mesh and its basis sketched out in code:
nodes, elements = mesh_1d(N=4)
print("nodes:   ", nodes)
print("elements:", elements.tolist())

# Sample phi_2 at a fine grid
x_fine = np.linspace(0, 1, 401)
phi_2 = hat(2, x_fine, nodes)

# phi_i(x_j) is the Kronecker delta — 1 if i==j, else 0
for j in range(len(nodes)):
    print(f"phi_2(x_{j}) = {hat(2, np.array([nodes[j]]), nodes)[0]:.0f}")
nodes:    [0.   0.25 0.5  0.75 1.  ]
elements: [[0, 1], [1, 2], [2, 3], [3, 4]]
phi_2(x_0) = 0
phi_2(x_1) = 0
phi_2(x_2) = 1
phi_2(x_3) = 0
phi_2(x_4) = 0

Five nodes, four elements, and is exactly 1 at and exactly 0 at every other node. The Kronecker-delta property is real, and it's the reason FEM coefficients have an immediate interpretation: each one is the value of the discrete solution at a specific point in space.


Where this is going

The mesh and basis are everything we need to assemble a stiffness matrix. Chapter 3 takes the integrals from chapter 1's weak form, evaluates them per element using the closed-form derivatives you'd get from this hat-function basis, assembles everything into a global tridiagonal , applies Dirichlet boundary conditions, solves , and plots against the analytic for a couple of forcing functions . Convergence as the mesh refines is the satisfying part.