Skip to content

Nitsche free-slip BC: inconsistent Jacobian for anisotropic (transverse-isotropic) constitutive models #239

@lmoresi

Description

@lmoresi

Summary

SNES_Stokes(_SaddlePt).add_nitsche_bc (and the SNES_Vector version) produces a boundary Jacobian that is inconsistent with its residual when the constitutive model is anisotropic (e.g. TransverseIsotropicFlowModel). The residual is correct, so solves still reach the right answer — but Newton degrades to a slow defect-correction (tens of oscillating SNES steps instead of 1–2), because the linearisation it follows is wrong by a few percent. The bulk Jacobian, derive_by_array, and the JIT are all correct; the error is confined to the Nitsche boundary terms for anisotropic stress.

Reproduce

snes_test_jacobian on a minimal TI Stokes (tilted director, η₁ ≠ η₀), varying only the boundary condition / geometry:

configuration ‖J − J_fd‖/‖J‖
box + TI 3e-11
annulus + Dirichlet + TI 2e-9
annulus + field-varying viscosity + TI 3e-11
annulus + penalty free-slip (add_natural_bc) + TI 6.7e-11
annulus + Nitsche + isotropic 2e-9
annulus + Nitsche + TI 0.069 (7%) ✗

The failure is precisely the Nitsche × anisotropy intersection. Amat and Pmat are equally wrong, and the ratio is invariant to pressure-nullspace on/off, monolithic-lu vs fieldsplit-Schur — so it is the tangent the BC contributes, not solver configuration.

A self-contained reproducer is scripts/jac_minimal_box.py (on the feature/fault-convection worktree); it sweeps GEOM/BC/VISC/NOWEAK and runs snes_test_jacobian. Happy to upstream a trimmed version as a regression test.

Mechanism (likely a small fix)

  1. Consistency term → f0 gradient dependence. Nitsche injects the gradient-dependent traction σ(u)·n into the f0 (value) boundary residual, so the boundary Jacobian g1 = ∂f0/∂L is nonzero and is exercised for the first time anywhere — the bulk f0 is just body force (∂f0/∂L = 0), so this assembly path is never tested in the bulk. The boundary assembly (cython/petsc_generic_snes_solvers.pyx ~L5665–5686) reuses the bulk (0,2,1,3) index permutation/reshape, which is not correct for the anisotropic boundary traction.

  2. Symmetry term uses scalar viscosity. The Nitsche symmetry term hardcodes mu = constitutive_model.viscosity (= shear_viscosity_0) rather than the full C : ε̇(v) response (add_nitsche_bc, vector ~L2864 / saddle ~L4557). Correct for isotropic, wrong for TI.

Impact

Latent for every anisotropic model under Nitsche — TI viscous, TI-VEP, anisotropic diffusion. For the fault-convection work it presented as "TI Stokes is intractable" (≈23 oscillating Newton steps); it is in fact a regression introduced when free-slip was switched from penalty (add_natural_bc) to add_nitsche_bc (penalty never exercises the bad path).

Workaround

Use penalty free-slip add_natural_bc(KFS · v·n̂ · n̂) instead of add_nitsche_bc for anisotropic rheologies — verified to give an exact Jacobian (6.7e-11).

Acceptance / regression gate

snes_test_jacobian on annulus + Nitsche + TI goes from 0.069 to ~1e-8, with isotropic + Nitsche unchanged. Add scripts/jac_minimal_box.py (trimmed) to the test suite.


Underworld development team with AI support from Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions