Skip to content

Grad-div penalty breaks POINTWISE traction recovery (28% on dynamic topography); facet-integrated is fine #633

Description

@lmoresi

The grad-div penalty corrupts the recovery of pointwise boundary traction from reaction loads, while leaving the facet-integrated value correct. This is the one thing blocking Stokes.DEFAULT_PENALTY from being turned on (PR #632, where everything else is in place).

Measured, test_1018_rotated_freeslip::test_rotated_freeslip_spherical3d_reaction_topography, spherical shell, rotated free-slip, penalty = 10:

quantity expected measured
surface (facet-integrated) 0.41920 0.4192 pass
surface_vertices (pointwise) 0.41920 0.30211 -28%
cmb (facet-integrated) 0.77060 0.7706 pass

At penalty = 0 all of them pass. That test's own docstring is "3D reaction loads must be divided by boundary mass to recover pointwise stress", which is exactly the step that stops working.

Not a p_mech bookkeeping error. The first hypothesis was that sigma_nn was being built from the Lagrange multiplier without the documented p_mech = p - lambda*mu*(div u) correction. It is not: boundary_normal_traction recovers sigma_nn from the Cartesian nodal reaction r_c = A.u - b, which already carries the penalty because the penalty is in the operator. Moving the term into the total stress (PR #632) left the measured value byte-identical at 0.30210742214244796.

Working hypothesis. lambda*mu*(div u) is non-zero cell-by-cell for P2-P0 — the discrete divergence is only weakly zero, against the pressure space — so it averages out over a facet integral but not at a vertex. The de-smearing from reaction loads to pointwise stress divides by a boundary mass that does not account for the augmented contribution.

Why it matters. Dynamic topography is the main product of this machinery, and a 28% error in it is not something to ship silently behind a default. It is also worth knowing independently of the penalty: if the pointwise recovery is sensitive to a term the integral is not, that is a property of the recovery, not of the penalty.

Suggested first step. Compare surface_vertices against surface_midpoints as lambda is swept 0, 1, 10, 100 — the test already computes both, and whether they diverge together or separately should distinguish a mass-weighting error from a sampling one.

Context: the penalty is measured to be a strong lever on the Schur count under FMG (Schur iterations per application 59 -> 18, total velocity work 546 -> 270, 21% faster) and the accuracy cost elsewhere is a consistent perturbation — same convergence rate, gap shrinking 1.102 -> 1.087 -> 1.066 over three refinements. So this is the last thing standing between that and a default.

Underworld development team with AI support from Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions