Tutorial: matrix-free anisotropic PDE with a line preconditioner¶
This tutorial solves a two-dimensional anisotropic diffusion-reaction equation without passing an assembled matrix to FGMRES:
Point Jacobi treats the diagonal but not the strong \(x\) coupling. We construct an exact tridiagonal solve along every \(x\) line and use it as a preconditioner.
1. Matrix-free stencil¶
import jax
import jax.numpy as jnp
import solvax as sx
jax.config.update("jax_enable_x64", True)
nx, ny = 128, 32
eps_x, eps_y, reaction = 1.0, 1e-2, 0.5
hx, hy = 1.0 / (nx + 1), 1.0 / (ny + 1)
wx, wy = eps_x / hx**2, eps_y / hy**2
diagonal = 2.0 * wx + 2.0 * wy + reaction
def matvec(flat):
u = flat.reshape(nx, ny)
padded = jnp.pad(u, ((1, 1), (1, 1)))
center = diagonal * u
x_part = -wx * (padded[:-2, 1:-1] + padded[2:, 1:-1])
y_part = -wy * (padded[1:-1, :-2] + padded[1:-1, 2:])
return (center + x_part + y_part).reshape(-1)
The padding encodes homogeneous Dirichlet boundaries. The function accepts and
returns (nx * ny,), matching FGMRES.
2. Exact solves along the stiff direction¶
lower_x = jnp.full((nx, ny), -wx)
diag_x = jnp.full((nx, ny), diagonal)
upper_x = jnp.full((nx, ny), -wx)
def x_line_inverse(residual_flat):
residual = residual_flat.reshape(nx, ny)
correction = sx.tridiagonal_solve(
lower_x, diag_x, upper_x, residual, method="auto"
)
return correction.reshape(-1)
The leading nx axis is the tridiagonal system; all ny columns are solved in
one call.
3. Build the smoother/preconditioner¶
preconditioner = sx.line_smoother(
matvec,
[x_line_inverse],
omega=0.9,
sweeps=2,
)
Each sweep computes a true outer residual and applies the line inverse. Because the preconditioner is an iterative action, flexible GMRES is appropriate.
4. Solve and inspect diagnostics¶
rhs = jnp.ones(nx * ny)
solution = sx.gmres(
matvec,
rhs,
precond=preconditioner,
restart=40,
rtol=1e-9,
atol=1e-12,
max_restarts=30,
)
if not solution.converged:
raise RuntimeError(
f"FGMRES stopped after {int(solution.iterations)} steps; "
f"residual={float(solution.residual_norm):.3e}"
)
5. Compare against point Jacobi¶
point = sx.jacobi(jnp.full(nx * ny, diagonal))
point_solution = sx.gmres(
matvec, rhs, precond=point, restart=40, rtol=1e-9, max_restarts=30
)
print("point iterations:", int(point_solution.iterations))
print("line iterations:", int(solution.iterations))
The meaningful comparison is total time including the preconditioner, not only iteration count. On a GPU, the fused tridiagonal backend can make the line solve especially attractive.
6. Extensions¶
Add a \(y\)-line inverse and alternate directions when anisotropy changes spatially or both directions are stiff.
Place line smoothing inside
p_multigridto eliminate high-frequency error while a coarse grid corrects smooth error.Use PCG only if the complete preconditioner action is symmetric positive definite. The sequential relaxed line smoother above is safest with FGMRES.
Replace the stencil with spectral or finite-volume code; the outer API is unchanged as long as
matvecpreserves the flat shape.