Boundary stress recovery & projection post-processing

Two related rules for getting accurate, affordable derived quantities (boundary tractions, radial stress, deviatoric-stress components) out of a solved Stokes field. Both came out of the spherical-benchmark post-processing work (issues #156, #157, #158).

1. Use a linear solver for projections

uw.systems.Projection (and the vector/tensor/multi-component variants) performs an L2 projection — a linear, symmetric-positive-definite mass-matrix solve (or a screened-Poisson smooth when smoothing > 0, still linear SPD). It inherits the general SNES_Scalar default stack:

snes_type = newtonls    # Newton — but the problem is linear
ksp_type  = gmres
pc_type   = gamg        # AMG setup + repartition: the expensive part

That stack is robust for general nonlinear scalar PDEs, but for projection it is unnecessarily heavy: GAMG setup/repartition dominates cost and memory, and when you project many quantities in sequence (e.g. six deviatoric-stress components at large MPI scale) it becomes the bottleneck — runs have been OOM-killed mid-sequence on Gadi.

Switch projectors used purely for output/post-processing to a lightweight linear solve with the opt-in helper:

proj = uw.systems.Projection(mesh, target)
proj.uw_function = expr
proj.linear_solver()          # ksponly + CG + jacobi, drops the GAMG options
proj.solve()

linear_solver(pc="jacobi", rtol=1e-10) sets snes_type=ksponly, ksp_type=cg, pc_type=<pc> and removes the now-unused GAMG options. It is opt-in — the global default is unchanged, so internal paths that rely on it (proxy variables, derivative evaluation, …) are unaffected. jacobi is fine for a well-conditioned mass matrix; use bjacobi or icc if CG iteration counts climb on distorted or high-degree meshes.

2. Project components, compose analytically

When recovering a boundary stress such as the radial normal stress σ_rr = nᵀ σ n, do not project the composed expression directly:

# DON'T: project the fully-composed scalar
sigma_rr = uw.systems.Projection(mesh, scalar)
sigma_rr.uw_function = n.T * stokes.stress * n      # = nᵀ(τ − pI)n

The composite mixes quantities of different FE character — the deviatoric stress τ ~ C:ε̇(u) (built from velocity gradients, one order below the P2 velocity) and the pressure p (often discontinuous P1) — so a single scalar projection cannot represent it as accurately as the parts. On the spherical Thieulot benchmark the direct path gave ≈2× the σ_rr error of the reference.

Instead, project the low-level components and compose analytically:

# DO: project the deviatoric-stress components, then form σ_rr symbolically
tau_proj = <project the τ_ij components, e.g. with a tensor/multi-component projection>
sigma_rr = n.T * tau_proj * n - p        # p used directly, not re-projected

This reproduces the stokes.tau-based benchmark values. The general principle: project the smoothest available primitives and combine them in closed form — projecting a derived composite throws away accuracy the primitives still have.

Combine the two rules — project the τ components with linear_solver() for the cheap linear solve, then compose σ_rr analytically — for accurate boundary stress recovery that also scales.

See also

  • Issues #156 (projection solver settings), #157 (projection memory), #158 (direct vs τ-based σ_rr).

  • Projection — the projection solver family.