Skip to content

Nitsche penalty scale regressed with mesh.cell_size(): the Zhong spherical-shell response went from 0.2% to 2.4-5.7% off #734

Open

Description

@lmoresi

add_nitsche_bc scales its penalty as gamma*mu/h and takes h from
mesh.cell_size() by default (local_h=True). #692 and #687 changed what
cell_size() returns. The Zhong et al. (2008) spherical-shell response
benchmark in tests/test_1064_constrained_spherical_shell_response.py has
degraded by an order of magnitude as a result, and nobody saw it because that
test matched no glob in scripts/test.sh and had never run in CI (#721).

Measured

Same case, same mesh, same code — only the Nitsche h differs:

Nitsche h surface error vs Zhong CMB error
local_h=True — current default, mesh.cell_size() 2.36% 0.94%
local_h=False — legacy global get_min_radius() 0.20% 0.22%

(SphericalShell r 0.55→1.0, internal boundary 0.775, cellSize 1/8, degree-2
harmonic load, NITSCHE_GAMMA as the test sets it. Both converged,
reason=5.)

The legacy global h reproduces the published response essentially exactly. The
current default does not.

Why the scale moved

On this mesh the two measures differ by a factor of 1.745:

mesh.cell_size()   mean 0.09282   min 0.06625   max 0.11970
get_min_radius()        0.05319

so the penalty gamma*mu/h is 0.573x — 43% weaker — than it was. A weaker
Nitsche penalty enforces v.n = 0 less tightly, which is exactly the quantity
this benchmark measures.

Note the docstring on add_nitsche_bc:

Set local_h=False to restore the legacy single global-minimum scalar
(mesh.get_min_radius()); on a uniform mesh the two coincide.

They do not coincide here, on a near-uniform spherical shell — 1.745x comparing
mean to global, and still 1.25x comparing minimum to minimum. The two are
measuring different things. This is the "three live definitions of cell radius"
problem (#694) landing somewhere that has a published answer to check against.

Timeline

date event
2026-06-24 test calibrated against Zhong (#265); local_h=True becomes the Nitsche default the same day (#275)
2026-09-07 #692 changes what cell_size() returns
2026-09-08 #687 touches it again
2026-09-12 un-deferring test_106* runs the test for the first time; it fails

local_h is not the culprit — it was already the default when the reference was
calibrated. What moved is the value cell_size() returns, and the test was dark
throughout.

It is now platform-decided

CI measured 5.67% and failed the 5% tolerance; the same case on a developer
Mac gives 2.36% and passes. gmsh triangulates differently across platforms
(this repo already records it — the serial_reference skips note cells=434 vs
430). At 0.2% that difference was irrelevant; at 2-6% the platform decides the
outcome.

What this is not

It is not a tolerance to relax. add_nitsche_bc is used across the free-slip
and fault work, so the question is which penalty scale is correct in general:

  1. the old scale was right, and the new cell_size() makes Nitsche too weak
    everywhere; or
  2. the new scale is right, and the Zhong tolerance was calibrated against an
    over-stiff penalty — in which case the 0.2% agreement it used to show was
    partly luck.

The first is the more likely reading given a 0.2% match to a published result,
but it needs the resolution ladder to settle rather than one data point.

Suggested first steps

  • Run the response at cellSize 1/8, 1/12, 1/16 under both h choices: if the
    local-h error converges away it is a scale issue, if it plateaus it is a
    consistency issue.
  • Decide what h the Nitsche facet term should actually use — the adjacent
    cell's insphere radius is the usual choice for an interior-penalty method, and
    it is not obvious that either current measure is that.
  • Correct or remove the "on a uniform mesh the two coincide" claim in the
    add_nitsche_bc docstring; it is false as measured.

Found during the 2026-09 audit, by turning on tests that had never run.
Related: #721 (the dark tests), #694 (cell radius definitions).

Underworld development team with AI support from Claude Code

Activity

  1. lmoresi commented on Sep 15, 2026

    @lmoresi
    MemberAuthor

    #697 is the fix, written 2026-09-09 and not yet merged. It raises the Nitsche
    gamma from 10.0 to 12.5 for exactly this reason. Measured on development at
    347d7e6, test_1064 response case:

    gamma h surface err CMB err
    10 (current) vertex-RMS 2.36% 0.94%
    12.5 (#697) vertex-RMS 0.02% 0.47%
    14 vertex-RMS 0.10% 0.34%
    17.3 (#697's effective stiffness on this shell) vertex-RMS 0.13% 0.19%
    10 global get_min_radius 0.20% 0.22%

    Two findings beyond "the fix exists":

    gamma=10 is a cliff. The surface error falls 100x between 10 and 12.5 and
    then flattens. The default was sitting on a steep part of the curve, which is
    why a modest h change produced a 5.67% failure rather than a proportionate
    drift — and why it was platform-decided (2.36% here, 5.67% on the Linux runner).
    Anything at or above ~12.5 is in the flat region.

    #697's justification has the sign wrong in 3-D. It says cell_size() is
    "about 19% larger in the mean", measured against the kd-tree h that #692
    already replaced. On this shell the new volume**(1/dim) is 28% smaller than
    the vertex-RMS currently on development (0.070719 vs 0.097874), so the penalty
    gets 1.38x stronger from the h change alone. In 2-D on flat cells it does go
    the other way (6% larger). Detail on the PR.

    So the answer to "which scale is right" is: neither h definition is wrong, but
    gamma has to be recalibrated whenever h is redefined, and gamma=10 was
    tuned against a definition two changes ago. Closing this will follow #697.

  2. lmoresi commented on Oct 5, 2026

    @lmoresi
    MemberAuthor

    Already fixed, by #743 and the one-measure change behind it. Probed on development (1391e4aa):

    $ pytest -q tests/test_1030_analytic_zhong2008.py \
                tests/test_1064_constrained_spherical_shell_response.py
    37 passed, 2 xfailed in 675.11s
    

    The two xfails are the constrained field-split and fast grouped-Schur responses (#1064's known failures); both describe themselves against "the validated Nitsche/default velocity response", which is the response this issue says regressed, and it passes.

    The root cause is now written into the test that guards it: test_0010_cell_size_geometry.py:119-121 records the "5.67% miss on a spherical-shell benchmark (#734)" and :150 says plainly "That is how #734 happened" — the penalty scale and the cell-size definition are tied together so a redefinition cannot silently move the answer again.

    Closed under the issue-adjudication rule in #819.

  3. lmoresi commented on Oct 12, 2026

    @lmoresi
    MemberAuthor

    Reopened and labelled fixed-in-development, under the policy now in CLAUDE.md:

    While the fix is in an open PR, label the issue fixed-in-PR; when the PR merges, relabel it fixed-in-development, comment with the PR and the merge commit, and leave it open. Issues are closed when development is released to main.

    The fix stands and the evidence above is unchanged — this is a bookkeeping correction, not a reversal. Closing it on merge was wrong because development has not been released, so anyone running the release still has the defect; the label says where the fix is and the release sweep closes it.

  4. added
    fixed-in-developmentFix merged to development; the issue closes when it is released to main
    on Oct 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    fixed-in-developmentFix merged to development; the issue closes when it is released to main

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions