Translated from the author's Korean original. Read the original (한국어)
What Jacobi Cannot Kill in 10,000 Sweeps, a V-Cycle Kills in 10 — The Real Secret of Multigrid
At a weight of 2/3 the fastest mode damps by 1/3, and the 1e-2 left after 80 Jacobi sweeps is the low mode that needs a coarser grid.
Why 10,000 Jacobi sweeps refuse to finish#
Engineer B was solving a pressure Poisson on a 1024×1024 grid with point Jacobi. Two hundred seconds in, the residual was still flickering at the same order of magnitude. The CFL was tiny. The grid was orthogonal. The code was correct. Some errors Jacobi never kills.
The same error changes frequency when the grid changes. The V-cycle is that sentence turned into an algorithm.
Move Pre/Post between 1 and 5 and, on the same 64-cell grid with a zero right-hand side, only the V-cycle residual steepens, then the extra smoothing stops paying.
Both solvers attack the same homogeneous Poisson problem with random initial guess. Jacobi (orange) does 4 sweeps per step; V-cycle (cyan) does 2 pre + 2 post sweeps per level on a 6-level hierarchy.
The orange trace is Jacobi. The wiggles die and the broad hump stays. The cyan log-residual falls almost as a straight line, while orange flattens on the low-frequency shelf.
Error frequencies — who dies fast and who lives forever#
Suppose the linear system is . The error in the current guess is
one vector. The residual is .
A spectral look at weighted Jacobi says a Fourier mode in the error decays per sweep as
where is the relaxation weight, the mode number, and the spacing. The absolute damping factors:
| Mode | Damping factor () | |
|---|---|---|
| Highest oscillation | 1.0 | |
| Middle | 0.5 | |
| Lowest |
Short waves shrink to a third each sweep. Long waves barely move. What Jacobi cannot kill is the smooth hill spread over the whole grid.
On a coarser grid that low frequency looks high#
This is where multigrid splits from a plain smoother. The longest wave on cells is . Move it to cells and the same wave occupies half as many cells. It looks shorter. More precisely, the lowest fine mode is a middle mode on the coarse grid, and Jacobi there kills middle modes.
Coarsen again and the wave shortens again. After levels every mode is high-frequency somewhere. A few Jacobi sweeps on every level, and in theory every error dies. Leaving the low modes to a Krylov comparison is the point of convergence from Jacobi through BiCGSTAB.
V-cycle — the path that goes down and comes back up#
Turn the split into steps and the shape is a V.
- Pre-smoothing — Jacobi sweeps on the fine grid.
- Residual — .
- Restriction — . Carry the residual down.
- Recurse — solve with another V-cycle, or directly if the grid is small enough.
- Prolongation — . Lift the correction.
- Correct — .
- Post-smoothing — Jacobi sweeps on the fine grid.
Downward, only the residual moves. Upward, only the correction. One cycle costs : one fine sweep plus a geometric tail. One cycle drops about one digit of residual.
Restriction and prolongation — the exchange rate between two grids#
The two transfer operators have to be a pair. On a 1D vertex-centered grid the plain pair is this.
Full-weighting restriction: coarse point is a weighted average of fine points .
Linear prolongation: copy onto coarse-aligned nodes, average the two neighbors in between.
They are transposes up to a grid-ratio constant (). That pair is part of the convergence argument. An asymmetric pair can diverge.
A two-grid V-cycle in NumPy#
Solve on with . Start at 64 cells.
import numpy as np
class GridLevel:
"""State at one grid level: solution, RHS, spacing."""
def __init__(self, n):
self.n = n # number of cells
self.h = 1.0 / n
self.u = np.zeros(n + 1)
self.f = np.zeros(n + 1)
def jacobi_relax(u, f, h, iters):
"""Weighted Jacobi for 1D Poisson. Dirichlet zero at both ends."""
n = len(u) - 1
nxt = np.zeros_like(u)
for _ in range(iters):
nxt[1:n] = 0.5 * (u[0:n-1] + u[2:n+1] + h * h * f[1:n])
u[1:n] = nxt[1:n]
return u
def residual_norm(u, f, h):
n = len(u) - 1
r = np.zeros_like(u)
inv = 1.0 / (h * h)
r[1:n] = f[1:n] - (-u[0:n-1] + 2 * u[1:n] - u[2:n+1]) * inv
return float(np.sqrt(np.mean(r[1:n] ** 2)))
def restrict_full_weighting(rh):
n = len(rh) - 1
n2 = n // 2
r2 = np.zeros(n2 + 1)
r2[1:n2] = 0.25 * rh[1:n-1:2] + 0.5 * rh[2:n:2] + 0.25 * rh[3:n+1:2]
return r2
def prolong_linear(e2h):
n2 = len(e2h) - 1
n = 2 * n2
eh = np.zeros(n + 1)
eh[2:n:2] = e2h[1:n2]
eh[1:n+1:2] = 0.5 * (e2h[0:n2] + e2h[1:n2+1])
return eh
def v_cycle(u, f, h, n_pre=2, n_post=2):
n = len(u) - 1
if n <= 4:
return jacobi_relax(u, f, h, 50)
jacobi_relax(u, f, h, n_pre)
inv = 1.0 / (h * h)
r = np.zeros_like(u)
r[1:n] = f[1:n] - (-u[0:n-1] + 2 * u[1:n] - u[2:n+1]) * inv
r2 = restrict_full_weighting(r)
e2 = np.zeros_like(r2)
v_cycle(e2, r2, 2 * h, n_pre, n_post)
e = prolong_linear(e2)
u += e
jacobi_relax(u, f, h, n_post)
return u
# --- run ---
n = 64
h = 1.0 / n
x = np.linspace(0, 1, n + 1)
f = (np.pi ** 2) * np.sin(np.pi * x) # exact u = sin(pi x)
u_jac = np.zeros(n + 1)
u_mg = np.zeros(n + 1)
for k in range(20):
jacobi_relax(u_jac, f, h, 4)
v_cycle(u_mg, f, h, n_pre=2, n_post=2)
print(f"step {k:3d} | jacobi r={residual_norm(u_jac, f, h):.3e} "
f"v-cycle r={residual_norm(u_mg, f, h):.3e}")One printed step spends 4 Jacobi sweeps, and the V-cycle spends 4 fine sweeps (2 pre + 2 post). Similar work, different residuals. After 20 steps the V-cycle residual is usually below and Jacobi is stuck near .
The table closes at . The fastest mode, , has , so the factor is . The absolute value matches the table entry . Ten sweeps on that mode alone leave . The middle entry is , and . The slowest factor is . At , , so . That is the size of one chop on the long wave.
The printout lines up with the table. Four Jacobi sweeps per step, twenty steps, eighty sweeps, and the residual is still near . The fast mode is already dead at a factor . The leftover is the low mode in the table. The same twenty V-cycles, 2 pre and 2 post on the fine grid, usually sit under . Eight orders separate and .
The coarsest level is 50 Jacobi sweeps once . Sixty-four cells restrict as 64, 32, 16, 8, 4 — four transfers. Full weighting is , , . The coarse work sums as , which does not exceed . Ten V-cycles repeat that sum ten times. Ten thousand Jacobi sweeps are a different count before the constant even matters.
Put the same factor on the slowest mode. At , and , the factor is about . The listing's eighty sweeps multiply to about . One digit costs about sweeps. At the factor is about , and one digit costs about sweeps. Ten thousand sweeps still leave that mode near . The 1024×1024 run that sat in one order of magnitude for 200 seconds is the same stall. The formula is one-dimensional weighted Jacobi. The engineer's grid was two-dimensional. A V-cycle that drops one digit per cycle does in about ten cycles what those sweeps do not do to the slow mode.
The factor is not a measured residual. It is the table formula on the slowest mode. Eighty sweeps move it by about six percent. The printed Jacobi residual, near after twenty steps, is what a factor that close to 1 looks like once the fast mode — damped by — is already gone.
Why the V-cycle is not always faster — two traps#
A V-cycle is not magic. Two places break it in practice.
Rough coefficients. On anisotropic or jumping-coefficient Poisson, Jacobi is a poor smoother. In , if jumps by orders of magnitude the standard V-cycle parks the residual at some digit. The fix is line-Jacobi, a line-aligned grid, or algebraic multigrid, which builds the hierarchy from the matrix.
Non-nested grids. On unstructured meshes or AMR the fine-to-coarse map is not an index halving. Agglomeration multigrid bundles fine cells into coarse cells, restricts by volume weights, and prolongs with a cell-centered interpolant. OpenFOAM's GAMG is that pattern.
The rate is mesh-independent only when the smoother, the operator, and the coarsening ratio all match. Miss one and the iteration count creeps up with . That is when a W-cycle or an F-cycle is the next try.
Where to put the standard V-cycle down#
If jumps by orders of magnitude, plain Jacobi is not a smoother and the residual sticks at some digit. Switch to line-Jacobi or AMG. The stretched-grid version of that switch is a linelet preconditioner.
Unstructured meshes and AMR do not build a coarse level by halving an index. OpenFOAM GAMG agglomerates cells. If the iteration count starts tracking , try a W-cycle or an F-cycle. The 64-cell run, 50 Jacobi sweeps at , and the 20-step gap of versus were read on aligned 1D Poisson only.
Related
Share if you found it helpful.