CFD · Numerical Methods · Shock Capturing
📅 July 2026 ⏱ ≈ 12 min read 🎯 Advanced

WENO schemes — high-order shock capturing for hyperbolic equations

Hyperbolic conservation laws — the compressible Euler equations, traffic flow, the shallow-water equations — develop discontinuities (shocks, contact surfaces) even from smooth initial data. A classical high-order scheme applied blindly across a shock produces spurious oscillations (the Gibbs phenomenon — a ringing overshoot/undershoot near sharp jumps) that can crash a simulation. Weighted Essentially Non-Oscillatory (WENO) schemes solve this by adaptively blending several candidate polynomial stencils, achieving high formal order in smooth regions while automatically degrading to a robust, oscillation-free reconstruction near discontinuities.

TL;DR: WENO schemes reconstruct numerical fluxes for equations like the Euler equations by blending several candidate polynomial stencils, each weighted by how smooth it is. Smooth regions get full fifth-order accuracy; stencils crossing a shock are automatically suppressed, keeping jumps sharp without oscillation. Paired with SSP-RK3 time stepping, with a working JavaScript implementation.

1. Hyperbolic conservation laws and the Gibbs problem

A scalar hyperbolic conservation law has the form ∂u/∂t + ∂f(u)/∂x = 0. Even when the flux f(u) and the initial data are smooth, characteristics can cross in finite time and produce a genuine discontinuity — a shock. Systems such as the compressible Euler equations behave the same way: a smooth initial pulse steepens into a shock front carrying a jump in density, velocity and pressure.

Standard high-order finite-difference or finite-volume reconstructions (centred, or fixed-stencil polynomial interpolation) are designed assuming local smoothness. Near a discontinuity that assumption fails, and the reconstructed polynomial overshoots and undershoots the true solution — the numerical analogue of the Gibbs phenomenon in Fourier series. Low order but robust schemes (first-order upwind) avoid oscillation by smearing the shock over many cells, destroying resolution. WENO schemes target the middle ground: high order where the solution is smooth, first-order-like robustness where it is not — without a human-tuned shock detector.

2. From ENO to WENO — the reconstruction idea

The predecessor of WENO is ENO (Essentially Non-Oscillatory, Harten & Osher, 1987). For each cell, ENO builds several candidate polynomial stencils of the same order and picks the single smoothest one — measured by a divided-difference smoothness test — discarding the others entirely. This avoids oscillation but the reconstruction is not smooth in coefficients: a tiny change in the data can flip which stencil is "selected", hurting accuracy and steady-state convergence.

WENO (Liu, Osher & Chan, 1994; Jiang & Shu, 1996) replaces the binary choice with a convex combination of all candidate stencils, each weighted by how smooth it is. In smooth regions, all stencils contribute close to their "ideal" (linear) weights and the combination recovers the maximum possible order (2r−1 for r candidate stencils of r points each). Near a discontinuity, the weight of any stencil that crosses the jump collapses toward zero, and the scheme degrades gracefully toward the smooth, low-order stencils that avoid the jump.

i-2
i-1
i
i+1
i+2

For WENO5 (fifth-order, the most widely used variant) the 5-point stencil above is split into three overlapping 3-point candidate stencils: S₀ = {i−2, i−1, i}, S₁ = {i−1, i, i+1}, S₂ = {i, i+1, i+2}. Each gives a third-order polynomial reconstruction of the value at the cell interface i+½; WENO blends the three.

3. Smoothness indicators

The Jiang-Shu smoothness indicator β_k for stencil S_k measures the total variation of the derivatives of the local reconstruction polynomial p_k over the cell:

β_k = Σ_l ∫_{x_i−1/2}^{x_i+1/2} Δx^{2l−1} (d^l p_k / dx^l)² dx

For WENO5 (l = 1, 2), in terms of cell averages u_{i-2..i+2}:
β_0 = (13/12)(u_{i-2} − 2u_{i-1} + u_i)² + (1/4)(u_{i-2} − 4u_{i-1} + 3u_i)²
β_1 = (13/12)(u_{i-1} − 2u_i + u_{i+1})² + (1/4)(u_{i-1} − u_{i+1})²
β_2 = (13/12)(u_i − 2u_{i+1} + u_{i+2})² + (1/4)(3u_i − 4u_{i+1} + u_{i+2})²

β_k is large where the candidate stencil straddles a jump or a steep gradient, and small (ideally zero for a linear function) where the data is smooth. β_k is the single quantity that lets WENO tell the difference between "smooth" and "discontinuous" without an explicit shock-detection flag.

4. Nonlinear weights — WENO5

The three candidate reconstructions of u at the interface i+½ are:

u₀ = (1/3)u_{i-2} − (7/6)u_{i-1} + (11/6)u_i
u₁ = −(1/6)u_{i-1} + (5/6)u_i + (1/3)u_{i+1}
u₂ = (1/3)u_i + (5/6)u_{i+1} − (1/6)u_{i+2}

Combined with linear ("ideal") weights d₀ = 1/10, d₁ = 6/10, d₂ = 3/10 they give the optimal fifth-order centred scheme in smooth regions. WENO replaces d_k with nonlinear weights ω_k that depend on β_k:

α_k = d_k / (ε + β_k)²
ω_k = α_k / (α₀ + α₁ + α₂)

ε ≈ 1e-6 avoids division by zero in perfectly flat regions

Reconstructed interface value:
u_{i+1/2} = ω₀ u₀ + ω₁ u₁ + ω₂ u₂

Where the solution is smooth, β₀ ≈ β₁ ≈ β₂ and ω_k → d_k, recovering the full fifth-order accuracy. Near a shock, the β_k of any stencil crossing the discontinuity blows up, its ω_k → 0, and the reconstruction is dominated by the remaining smooth stencils — automatically and continuously, with no tunable threshold.

WENO-Z variant: Borges et al. (2008) proposed a modified weight formula using a higher-order smoothness measure τ₅ = |β₀ − β₂|, giving less numerical dissipation at critical points (local extrema) where classical WENO5 can locally drop to third order. WENO-Z is now the default in many production codes.

5. Flux reconstruction and Riemann solvers

For a system of conservation laws (e.g. the 1D Euler equations), WENO reconstructs not the raw field but the flux itself, after splitting it into left-going and right-going parts (flux splitting, e.g. Lax-Friedrichs: f± = ½(f(u) ± α u) with α = max|characteristic speed|). WENO reconstructs f⁺ using an upwind-biased (leftward) stencil and f⁻ using a mirrored stencil, and the numerical flux at the interface is F_{i+1/2} = f⁺_{i+1/2} + f⁻_{i+1/2}.

For genuinely nonlinear systems, reconstructing in characteristic variables (projecting onto local eigenvectors of the flux Jacobian) rather than conservative variables greatly reduces oscillations near strong shocks and contact discontinuities, at the cost of an extra matrix diagonalisation per cell interface.

6. Time integration — SSP-RK3

A high-order spatial reconstruction is wasted if paired with a low-order or unstable time integrator. WENO is almost always combined with a Strong Stability Preserving Runge-Kutta scheme (Shu-Osher SSP-RK3), which is a convex combination of forward-Euler substeps and therefore inherits the TVD (total variation diminishing) property of the spatial operator:

u⁽¹⁾ = uⁿ + Δt L(uⁿ)
u⁽²⁾ = (3/4)uⁿ + (1/4)u⁽¹⁾ + (1/4)Δt L(u⁽¹⁾)
uⁿ⁺¹ = (1/3)uⁿ + (2/3)u⁽²⁾ + (2/3)Δt L(u⁽²⁾)

L(u) = -∂F/∂x, the WENO spatial discretisation

The time step is limited by the usual CFL condition Δt ≤ CFL · Δx / max|λ|, with CFL typically 0.3–0.6 for WENO5 combined with SSP-RK3.

7. JavaScript implementation — 1D advection and Euler

// ── WENO5 interface reconstruction (left-biased) ─────────────────
function weno5(um2, um1, u0, up1, up2) {
  const EPS = 1e-6;

  // Candidate stencil reconstructions
  const u0r = (1/3)*um2 - (7/6)*um1 + (11/6)*u0;
  const u1r = -(1/6)*um1 + (5/6)*u0  + (1/3)*up1;
  const u2r = (1/3)*u0  + (5/6)*up1 - (1/6)*up2;

  // Smoothness indicators (Jiang-Shu)
  const b0 = (13/12)*(um2-2*um1+u0)**2 + 0.25*(um2-4*um1+3*u0)**2;
  const b1 = (13/12)*(um1-2*u0+up1)**2  + 0.25*(um1-up1)**2;
  const b2 = (13/12)*(u0-2*up1+up2)**2  + 0.25*(3*u0-4*up1+up2)**2;

  // Nonlinear weights (linear weights d = 0.1, 0.6, 0.3)
  const a0 = 0.1 / (EPS+b0)**2;
  const a1 = 0.6 / (EPS+b1)**2;
  const a2 = 0.3 / (EPS+b2)**2;
  const sum = a0 + a1 + a2;

  return (a0*u0r + a1*u1r + a2*u2r) / sum;
}

// ── Lax-Friedrichs flux splitting + WENO5 + SSP-RK3 ──────────────
function wenoRHS(u, flux, alpha, dx, N) {
  const fp = new Float64Array(N), fm = new Float64Array(N);
  for (let i = 0; i < N; i++) {
    const f = flux(u[i]);
    fp[i] = 0.5 * (f + alpha * u[i]);  // right-going part
    fm[i] = 0.5 * (f - alpha * u[i]);  // left-going part
  }
  const rhs = new Float64Array(N);
  for (let i = 2; i < N - 2; i++) {
    const Fp = weno5(fp[i-2], fp[i-1], fp[i], fp[i+1], fp[i+2]);
    const Fm = weno5(fm[i+2], fm[i+1], fm[i], fm[i-1], fm[i-2]);
    const FpL = weno5(fp[i-3]??fp[i-2], fp[i-2], fp[i-1], fp[i], fp[i+1]);
    const FmL = weno5(fm[i+1], fm[i], fm[i-1], fm[i-2], fm[i-3]??fm[i-2]);
    const F_ip = Fp + Fm;      // numerical flux at i+1/2
    const F_im = FpL + FmL;   // numerical flux at i-1/2
    rhs[i] = -(F_ip - F_im) / dx;
  }
  return rhs;
}

// ── SSP-RK3 time step ─────────────────────────────────────────────
function sspRK3Step(u, dt, rhsFn) {
  const N = u.length;
  const u1 = new Float64Array(N), u2 = new Float64Array(N), out = new Float64Array(N);
  const L0 = rhsFn(u);
  for (let i = 0; i < N; i++) u1[i] = u[i] + dt * L0[i];
  const L1 = rhsFn(u1);
  for (let i = 0; i < N; i++) u2[i] = 0.75*u[i] + 0.25*u1[i] + 0.25*dt*L1[i];
  const L2 = rhsFn(u2);
  for (let i = 0; i < N; i++) out[i] = (1/3)*u[i] + (2/3)*u2[i] + (2/3)*dt*L2[i];
  return out;
}
Sanity check: run the scheme first on scalar linear advection (flux = u) with a square-wave initial condition. A correct WENO5 implementation keeps the discontinuity sharp (2–3 cells wide) with no overshoot, unlike a naive centred fifth-order scheme which rings badly at the jump.

8. Applications and variants

🌊 Navier-Stokes Fluid

See the incompressible counterpart: pressure projection and semi-Lagrangian advection running live in WebGL.

Open Fluid Sim →