Skip to content
cfd-lab:~/en/posts/2026-08-28-self-adjointn…● online

Translated from the author's Korean original. Read the original (한국어)

NOTE #142DAY FRI CFD기법DATE 2026.08.28UPDATED 2026.10.19READ 8 min read#SUPG#Weighted-Residual#FEM#Convection-Diffusion#Numerical-Analysis

Pushing the cell Péclet number to 2 drove the solution to -0.33 — where the energy to minimize disappeared

Stabilization does not restore symmetry. It inflates the symmetric part and buys the discrete maximum principle instead.

Four methods gave the same answer on one bar#

Chapter 1 of a finite element textbook solves the same tapered-bar problem four times: direct stiffness, minimum total potential energy, weighted residuals, and Galerkin. All four produce the identical 5×5 stiffness matrix. The lecture notes put it plainly — whichever method you pick, you land on essentially the same accurate answer.

That sentence carries a condition, and the condition hides inside the bar problem where nobody trips over it. Flow equations do not satisfy it. The measurement is which guarantee disappears when the condition breaks, and which part stabilization can hand back. Raise advection a from zero and the pink dot's lap work leaves 0.000 for exactly 2πa2\pi a.

Set advection a to 0: the pink dot goes all the way around and brings back 0.000 — the field is a gradient, and the amber ball slides straight down the ellipses. Push a up and the last lap returns 6.283, exactly 2πa. Now drag diffusion s across its whole range: the lap total does not budge. No amount of added diffusion buys back a potential.

The amber ball that used to slide inward without cutting the grey level curves starts spiraling up them once a grows. Push diffusion s to the stop and the lap total stays put.

A minimum needs a symmetric matrix#

The minimum total potential energy formulation differentiates the total potential with respect to the nodal unknowns and sets the result to zero. In discrete form:

Π(ϕ)=12 ϕTK ϕ−fTϕ\Pi(\boldsymbol{\phi}) = \frac{1}{2}\,\boldsymbol{\phi}^{T}\mathbf{K}\,\boldsymbol{\phi} - \mathbf{f}^{T}\boldsymbol{\phi}

Here ϕ\boldsymbol{\phi} is the vector of nodal unknowns, K\mathbf{K} the stiffness matrix, f\mathbf{f} the load vector. Now differentiate with respect to component ii:

∂Π∂ϕi=12∑j(Kij+Kji)ϕj−fi\frac{\partial \Pi}{\partial \phi_i} = \frac{1}{2}\sum_{j}\left(K_{ij} + K_{ji}\right)\phi_j - f_i

Note what appeared: Kij+KjiK_{ij} + K_{ji}. For this to reduce to Kϕ=f\mathbf{K}\boldsymbol{\phi} = \mathbf{f}, you need Kij=KjiK_{ij} = K_{ji}. Without symmetry, minimization actually solves the symmetrized matrix 12(K+KT)\tfrac{1}{2}(\mathbf{K}+\mathbf{K}^{T}), which is a different equation from the one you wrote down.

There is a deeper version of the same statement. For the residual field r(ϕ)=f−Kϕ\mathbf{r}(\boldsymbol{\phi}) = \mathbf{f} - \mathbf{K}\boldsymbol{\phi} to be the gradient of some scalar function, its Jacobian must be symmetric. If it is not, no potential function exists at all. The fastest way to test existence is to walk a closed loop: a gradient field always returns zero work after a full lap. The climb over the level curves, once a leaves zero, is that lap failing to close at zero.

Advection breaks it by exactly uu#

Write the weak form of the one-dimensional advection-diffusion equation u ϕ′=ϵ ϕ′′u\,\phi' = \epsilon\,\phi'' and the two terms show their different characters:

a(w,ϕ)=∫0L(ϵ dwdxdϕdx+w u dϕdx)dxa(w, \phi) = \int_0^L \left( \epsilon\,\frac{dw}{dx}\frac{d\phi}{dx} + w\,u\,\frac{d\phi}{dx} \right) dx

Here ww is the test function, ϵ\epsilon the diffusivity, uu the advection velocity. The first term is unchanged when you swap ww and ϕ\phi — it is symmetric. The advection term flips sign under integration by parts. For test functions vanishing at the boundaries, ∫w u ϕ′ dx=−∫ϕ u w′ dx\int w\,u\,\phi'\,dx = -\int \phi\,u\,w'\,dx, so advection is a purely skew-symmetric contribution.

Assemble with linear elements and that structure survives in the coefficients. With element size hh, an interior row reads:

Ki,i−1=−ϵh−u2,Ki,i=2ϵh,Ki,i+1=−ϵh+u2K_{i,i-1} = -\frac{\epsilon}{h} - \frac{u}{2}, \qquad K_{i,i} = \frac{2\epsilon}{h}, \qquad K_{i,i+1} = -\frac{\epsilon}{h} + \frac{u}{2}

Diffusion deposits the same value on both neighbors; advection adds +u/2+u/2 on one side and −u/2-u/2 on the other. So the departure from symmetry is exactly one number:

Ki,i+1−Ki+1,i=uK_{i,i+1} - K_{i+1,i} = u

Refine the mesh as much as you like and this stays uu, because hh never entered it. As long as there is flow, the minimum total potential energy principle is not coming back. Folding a stiffness into a 6×6 Voigt matrix only worked while the matrix stayed symmetric. When advection makes a second-order scheme ring near a step, the fix lives on the TVD advection side, with a flux limiter.

Three schemes on the same mesh in Python#

Ten elements, u=1u = 1, boundary conditions ϕ(0)=0\phi(0)=0 and ϕ(1)=1\phi(1)=1. A single artificial-diffusion coefficient β\beta generates all three schemes through ϵeff=ϵ+β u h/2\epsilon_{\text{eff}} = \epsilon + \beta\,u\,h/2: β=0\beta = 0 is pure Galerkin, β=1\beta = 1 is full upwind, and β=coth⁡(Peh)−1/Peh\beta = \coth(Pe_h) - 1/Pe_h is SUPG.

import numpy as np
 
def assemble_ad(n, vel, eps, beta):
    """1D advection-diffusion on linear elements. beta = artificial diffusion."""
    h = 1.0 / n
    eps_eff = eps + beta * vel * h / 2.0
    kd = (eps_eff / h) * np.array([[1.0, -1.0], [-1.0, 1.0]])   # diffusion: symmetric
    ka = (vel / 2.0) * np.array([[-1.0, 1.0], [-1.0, 1.0]])     # advection: skew
    K = np.zeros((n + 1, n + 1))
    for e in range(n):
        K[e:e + 2, e:e + 2] += kd + ka
    return K
 
def solve_bvp(K):
    n = K.shape[0] - 1
    A, b = K.copy(), np.zeros(n + 1)
    A[0, :], A[0, 0], b[0] = 0.0, 1.0, 0.0
    A[n, :], A[n, n], b[n] = 0.0, 1.0, 1.0
    return np.linalg.solve(A, b)
 
def exact_ad(x, pe):
    return (np.exp(pe * (x - 1.0)) - np.exp(-pe)) / (1.0 - np.exp(-pe))
 
def skew_ratio(K):
    return np.linalg.norm(K - K.T) / np.linalg.norm(K + K.T)
 
def loop_work(K, m=20000):
    """Work of the residual field -K.phi once around the unit circle in DOF space."""
    t = np.linspace(0.0, 2.0 * np.pi, m, endpoint=False)
    path = np.stack([np.cos(t), np.sin(t)])          # position
    tang = np.stack([-np.sin(t), np.cos(t)])         # dl / dt
    return float(np.sum(np.sum(-(K @ path) * tang, axis=0)) * (2.0 * np.pi / m))
 
n, vel = 10, 1.0
x = np.linspace(0.0, 1.0, n + 1)
print(f"{'Pe_h':>5} {'scheme':>9} {'max|err|%':>10} {'min phi':>9} {'skew/sym':>9} {'loop W':>8}")
for pe_h in [0.5, 1.0, 2.0, 5.0]:
    eps = vel / (2.0 * n * pe_h)
    ex = exact_ad(x, vel / eps)
    for name, beta in [("Galerkin", 0.0), ("upwind", 1.0),
                       ("SUPG", 1.0 / np.tanh(pe_h) - 1.0 / pe_h)]:
        K = assemble_ad(n, vel, eps, beta)
        phi = solve_bvp(K)
        print(f"{pe_h:5.1f} {name:>9} {100 * np.max(np.abs(phi - ex)):10.2f} "
              f"{phi.min():9.4f} {skew_ratio(K):9.4f} {loop_work(K[4:6, 4:6]):8.4f}")
 
K0 = assemble_ad(n, 0.0, 0.1, 0.0)
print(f"\nvel = 0 : skew/sym = {skew_ratio(K0):.2e},  loop W = {loop_work(K0[4:6, 4:6]):.2e}")
print(f"pi * u  = {np.pi * vel:.4f}")
 Pe_h    scheme  max|err|%   min phi  skew/sym   loop W
  0.5  Galerkin       3.45    0.0000    0.2924   3.1416
  0.5    upwind      13.17    0.0000    0.1954   3.1416
  0.5      SUPG       0.00   -0.0000    0.2704   3.1416
  1.0  Galerkin      13.53    0.0000    0.5774   3.1416
  1.0    upwind      19.80    0.0000    0.2924   3.1416
  1.0      SUPG       0.00   -0.0000    0.4428   3.1416
  2.0  Galerkin      35.17   -0.3334    1.1010   3.1416
  2.0    upwind      18.17   -0.0000    0.3885   3.1416
  2.0      SUPG       0.00    0.0000    0.5572   3.1416
  5.0  Galerkin      69.61   -0.6961    2.1517   3.1416
  5.0    upwind       9.09    0.0000    0.4836   3.1416
  5.0      SUPG       0.00    0.0000    0.5773   3.1416
 
vel = 0 : skew/sym = 0.00e+00,  loop W = -4.29e-16
pi * u  = 3.1416

Push the Pe_h slider past 1 and the a_E bar crosses red while the nodes sink below zero.

Leave beta at 0 and drag Pe_h past 1: a_E flips sign, and the marching profile starts ringing — at Pe_h = 2 the node next to the outlet dives to 0.000 with the boundary values still pinned at 0 and 1. Hit SUPG and the error goes to 0.00 %. The pink line at bottom right is the skew part of the matrix: none of the three buttons moves it.

Leave beta at zero and set Pe_h to 2. The nodal minimum lands near the table's -0.3334. Hit SUPG and the error sticks at 0 %, while the pink skew line stays on 3.1416.

The boundary values are pinned at 0 and 1, yet the Galerkin solution at Peh=2Pe_h = 2 dips to −0.3334-0.3334. At Peh=5Pe_h = 5 it reaches −0.6961-0.6961. Neither value can exist physically.

One coefficient flips sign at Peh=1Pe_h = 1#

Define the cell Péclet number as Peh=uh/(2ϵ)Pe_h = u h / (2\epsilon) and the right-hand coefficient of that row rewrites itself:

Ki,i+1=ϵh(Peh−1)K_{i,i+1} = \frac{\epsilon}{h}\left(Pe_h - 1\right)

Once Peh>1Pe_h > 1 the off-diagonal entry turns positive. At that instant the matrix stops being an M-matrix, and the discrete maximum principle — the guarantee that interior values stay inside the range set by the boundaries — leaves with it. That is why min phi in the table holds at zero through Peh=1.0Pe_h = 1.0 and goes negative at 2.0. The moment a_E crossed red above is this sign change.

Stabilization does not restore symmetry#

A common misreading shows up here. When upwinding or SUPG is said to "restore stability", that does not mean symmetry and the minimization principle come back. Look at the loop W column: three schemes across four values of PehPe_h, and all twelve rows read 3.1416. That number is πu\pi u, with neither ϵ\epsilon nor β\beta anywhere in it.

The reason is simple. All β\beta does is inflate ϵeff\epsilon_{\text{eff}}, which lives in the symmetric part of the matrix. The skew part, uu, is untouched. The skew/sym ratio dropping from 2.1517 to 0.4836 at Peh=5Pe_h = 5 is not the skew part shrinking; it is the symmetric denominator growing.

At Peh=0.5Pe_h = 0.5 Galerkin prints max|err|% 3.45, min phi 0.0000, and skew/sym 0.2924. At Peh=1.0Pe_h = 1.0 the error rises to 13.53 while the minimum is still 0.0000. From Ki,i+1=(ϵ/h)(Peh−1)K_{i,i+1} = (\epsilon/h)(Pe_h - 1), the right-hand coefficient is zero at Peh=1Pe_h = 1 and turns positive the moment the cell Péclet crosses 1. At Peh=2Pe_h = 2 Galerkin's minimum is -0.3334, the error is 35.17 %, and skew/sym is 1.1010. On the same row upwind has minimum -0.0000, error 18.17 %, and skew/sym 0.3885. SUPG has error 0.00 and skew/sym 0.5572. All three loop W entries read 3.1416. With u=1u = 1, πu\pi u is 3.1416, so the lap work is the same whether β\beta is 0, 1, or coth⁡(Peh)−1/Peh\coth(Pe_h) - 1/Pe_h. At Peh=5Pe_h = 5 Galerkin reaches minimum -0.6961, error 69.61 %, and skew/sym 2.1517. Upwind cuts the error to 9.09 % and skew/sym to 0.4836. What shrank is the symmetric denominator. The zero-velocity check prints skew/sym 0.00e+00 and loop W -4.29e-16. Ten elements and u=1u = 1 give h=0.1h = 0.1, and Peh=uh/(2ϵ)Pe_h = uh/(2\epsilon) then puts ϵ=0.025\epsilon = 0.025 at Peh=2Pe_h = 2. Inflating that ϵ\epsilon with β\beta still leaves Ki,i+1−Ki+1,i=uK_{i,i+1} - K_{i+1,i} = u equal to 1.

So what stabilization actually buys is a weaker guarantee. You gave up the minimization principle — optimality in the energy norm — and in exchange you get the M-matrix property and the discrete maximum principle. You pay in accuracy. At Peh=2Pe_h = 2, upwinding killed the oscillation but carries 18.17 % error. SUPG adds only as much β\beta as needed and stays exact at the nodes.

βopt=coth⁡(Peh)−1Peh\beta_{\text{opt}} = \coth(Pe_h) - \frac{1}{Pe_h}

This goes to 0 as Peh→0Pe_h \to 0 and to 1 as Peh→∞Pe_h \to \infty: turn stabilization off when diffusion dominates, go full upwind when advection does. Nodal exactness, though, is a privilege of the one-dimensional constant-coefficient problem. In two dimensions you need the original SUPG form, which adds artificial diffusion only along the streamline, and even then "exact" is gone.

The name finite volume uses for the same place#

The calculation above spoke finite element, but the conclusion is independent of the discretization. In finite volume, a central-differenced convection term produces exactly the same stencil and flips sign at exactly the same Peh=1Pe_h = 1. That is what first-order upwinding answers, and the numerical diffusion it introduces equals the uh/2u h / 2 you get at β=1\beta = 1.

Where the two worlds part company is conservation. Finite volume upwinding modifies the face flux, so the global balance survives intact. It is the same kind of split as conservative versus primitive forms disagreeing on shock speed. Finite element artificial diffusion adds a term to the stiffness matrix, so what it still conserves has to be checked on its own.

Choosing a different member of the weighted-residual family costs something else. Least squares minimizes the residual norm, so it always produces a symmetric positive definite matrix — the minimization principle returns. In exchange the condition number is squared, and on linear elements the second derivative vanishes inside each element, so the diffusion term drops out entirely. The basis and the integration rule quietly change the character of the formulation.

At Peh=2Pe_h = 2, buy a bound before buying a finer mesh#

The textbook line about all methods agreeing holds only for self-adjoint operators. Diffusion, elasticity, and potential flow sit inside that region. Advection brings Ki,i+1−Ki+1,i=uK_{i,i+1} - K_{i+1,i} = u, the symmetry breaks, and loop W stays at 3.1416 — πu\pi u — no matter what ϵ\epsilon does. The energy-norm optimum that CG promised is gone. A nonsymmetric matrix belongs with the Krylov side, from Jacobi through BiCGSTAB.

A negative min phi means the cell Péclet has passed 1. At Peh=2Pe_h = 2 Galerkin reaches -0.3334 with 35.17 % error. Upwind kills the oscillation and leaves 18.17 % error. SUPG is exact at the nodes, yet skew/sym only falls from 1.1010 to 0.5572. The skew piece itself did not move. In three dimensions, taking PehPe_h from 5 down to 1 multiplies the cell count by 125. When that factor is unaffordable, what gets bought is boundedness, not accuracy.

Share if you found it helpful.