Skip to content

Constrained free-slip via recoverable Lagrange multiplier (dynamic topography)#219

Open
lmoresi wants to merge 3 commits into
developmentfrom
feature/constrained-freeslip-topography
Open

Constrained free-slip via recoverable Lagrange multiplier (dynamic topography)#219
lmoresi wants to merge 3 commits into
developmentfrom
feature/constrained-freeslip-topography

Conversation

@lmoresi
Copy link
Copy Markdown
Member

@lmoresi lmoresi commented Jun 5, 2026

Summary

Adds uw.systems.Stokes_Constrained (SNES_Stokes_Constrained): enforce u·n = g
on curved boundaries with a recoverable Lagrange multiplier instead of a tuned
penalty. The converged multiplier is the normal traction holding the boundary — a
direct dynamic-topography estimate h = λ/(Δρ g).

Motivated by the fragility of penalty/Nitsche free-slip on annulus/spherical
boundaries (the magnitude must be tuned against forcing strength and viscosity; too
weak gives spurious radial throughflow, too strong diverges).

How it works

An augmented-Lagrangian (ALG2) outer loop reusing the existing penalty BC: each
Stokes solve carries t = [λ + r(u·n − g)]·n, and λ ← λ + r(u·n − g). The penalty
term preconditions every boundary mode (fast convergence); the multiplier removes the
penalty's accuracy bias (exact constraint from a moderate, well-conditioned r).

Purely additive — the validated 2×2 saddle-point assembly and fieldsplit
configuration are untouched (honours "solver stability is paramount"). The outer loop
lives inside solve().

Hands-off solve() (no tuning)

  • Relative tolerance: RMS(u·n − g) < rtol·RMS|u| (default rtol=1e-3) — problem-independent.
  • Viscosity-weighted augmentation: default r = 10³·μ(x), so high-viscosity
    boundary regions are constrained as well as low-viscosity ones (a flat r is not).
stokes = uw.systems.Stokes_Constrained(mesh, velocityField=v, pressureField=p)
stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel
stokes.constitutive_model.Parameters.shear_viscosity_0 = mu
lam  = stokes.add_constraint_bc("Upper", g=0.0)   # free-slip, no penalty coefficient
stokes.solve()
topo = stokes.topography("Upper", buoyancy_scale=delta_rho_g)

Validation

  • Annulus (tests/test_1061_constrained_freeslip_annulus.py, 5 tests, level_2/tier_b):
    RMS(u·n)=6.5e-5, velocity within 0.3% of the penalty reference, 2 outer iterations,
    interior λ exactly 0, boundary corr(λ, −n·σ·n) = 0.9999.
  • SolCx (variable viscosity): ~2e-4 vs Dirichlet reference in 3 outer iterations across
    contrast 1 → 10⁶, no manual tuning.

Trade-offs and roadmap

The augmentation r is a speed knob, not an accuracy knob (accuracy is
r-independent — the AL property): too small just costs iterations, too large stiffens
the inner solve, the answer is never wrong. True-work is U-shaped with an efficient
basin r ∈ [300, 10⁴].

Deferred (see docs/developer/design/CONSTRAINED_FREESLIP_MULTIPLIER.md):
adaptive r₀ for very coarse meshes; parallel (mask + nodal update are serial);
rotation-null-space removal for both-boundaries free-slip; and the monolithic
P'=[p,h] fieldsplit ("arbitrary constraints in the saddle point") as a separate
feasibility spike.

Design note: docs/developer/design/CONSTRAINED_FREESLIP_MULTIPLIER.md.

Underworld development team with AI support from Claude Code

…graphy

Enforce u.n = g on curved boundaries with a recoverable Lagrange multiplier
instead of a tuned penalty. An augmented-Lagrangian (ALG2) outer loop carries
both the existing penalty BC and the multiplier; the multiplier removes the
penalty's accuracy bias, so a moderate, well-conditioned augmentation gives an
exact constraint in 2-3 Stokes solves. The converged multiplier is the normal
traction holding the boundary -- a direct dynamic-topography estimate
(h = lambda / (drho g)), recoverable as a clean MeshVariable (interior exactly
zero; boundary trace correlates with -n.sigma.n at 1.0000).

Purely additive: the validated 2x2 saddle-point assembly and fieldsplit config
are untouched. New class behind uw.systems.Stokes_Constrained.

Hands-off solve(): relative constraint tolerance (RMS(u.n-g) < rtol*RMS|u|) and
viscosity-weighted augmentation (1e3*mu(x)) by default -- no user tuning. On the
SolCx benchmark this gives ~2e-4 accuracy in 3 outer iterations across viscosity
contrast 1 -> 1e6.

Validation: tests/test_1061_constrained_freeslip_annulus.py (5 tests). Design
note and production roadmap in docs/developer/design/.

Underworld development team with AI support from Claude Code
Copilot AI review requested due to automatic review settings June 5, 2026 15:23
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Stokes solver variant that enforces curved-boundary normal-velocity constraints (u·n=g) using an augmented-Lagrangian outer loop with a recoverable Lagrange multiplier, enabling direct access to the converged boundary traction (dynamic-topography proxy) without penalty tuning.

Changes:

  • Introduces SNES_Stokes_Constrained (exported as uw.systems.Stokes_Constrained) with add_constraint_bc(), multiplier(), and topography() APIs.
  • Implements the augmented-Lagrangian outer iteration inside solve() and a boundary-only multiplier update strategy via a nodal mask.
  • Adds annulus validation tests and a developer design note documenting formulation, trade-offs, and validation results.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
src/underworld3/systems/solvers.py Adds constrained Stokes solver, constraint BC registration, multiplier/topography accessors, and augmented-Lagrangian outer loop.
src/underworld3/systems/__init__.py Exports the new solver as Stokes_Constrained.
tests/test_1061_constrained_freeslip_annulus.py Adds regression/validation coverage comparing constrained vs penalty free-slip on an annulus and checks multiplier/topography behavior.
docs/developer/design/CONSTRAINED_FREESLIP_MULTIPLIER.md Design/validation documentation for the constrained free-slip multiplier approach and roadmap.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +2125 to +2133
point_indices = petsc_dm_find_labeled_points_local(
self.mesh.dm,
"UW_Boundaries",
getattr(self.mesh.boundaries, boundary).value,
sectionIndex=False,
)
if point_indices is not None:
marker.data[point_indices] = 1.0
mask = (
Comment on lines +2271 to +2277
# Augmented-Lagrangian (ALG2) multiplier update, same r as the
# forward-problem penalty augmentation. Monotone-convergent for r>0.
# Restricted to boundary nodes so lambda stays a clean topography field.
for cbc in self._constraint_bcs:
resid = self._nodal_constraint_residual(cbc)
cbc.lam.data[cbc.mask, 0] += cbc.r_nodal[cbc.mask] * resid[cbc.mask]

Comment on lines +2224 to +2226
# Sample the augmentation r at the multiplier nodes once (it may be a
# spatial / viscosity-weighted expression). Used for the dual update.
for cbc in self._constraint_bcs:
lmoresi added 2 commits June 5, 2026 16:30
Confirmed at the PETSc level that a 3-field DM (u, p, h) with p and h grouped
yields a 2-way u | [p,h] Schur split (no nested fieldsplit), de-risking the
monolithic "arbitrary saddle-point constraints" direction. Captures the bounded
assembly touch-points and remaining caveats for a future block-Schur PR.

Underworld development team with AI support from Claude Code
- add_constraint_bc validates the boundary name and the 'UW_Boundaries' label,
  and rejects empty/None point sets (petsc_dm_find_labeled_points_local returns
  the vertex-0 sentinel np.array([0]) when the label is absent, which an
  `is not None` check would silently accept and corrupt the mask).
- The outer loop no longer applies a final, never-solved multiplier update when
  the iteration cap is hit (u/p stay consistent with lambda) and now warns
  loudly (RuntimeWarning) on non-convergence.
- Raise NotImplementedError when run on more than one MPI rank (the mask and the
  node-wise update are serial-only), instead of silently producing wrong results.

Adds a test that add_constraint_bc rejects an unknown boundary name.

Underworld development team with AI support from Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants