Hard baselines instead of drift detectors, and pin cell_size so a redefinition fails on the day - #743
Conversation
…efinition fails on the day Maintainer ruling (2026-09-14): a test asserts against a hard baseline, or it is not a test. A test loose enough to absorb a change in a default does not fail when the default changes — it drifts inside its own margin and breaks later, somewhere else, for reasons that are hard to trace back. #734 is the case study. #692 redefined `mesh.cell_size()`; nothing failed; the Nitsche penalty `gamma*mu/h` moved 43%; the Zhong spherical-shell benchmark slid from 0.2% to 2.4% — still inside its 5% tolerance — and only broke months later on one platform's triangulation, three layers from the cause. **The test that would have caught it on the day.** `cell_size` had two tests and between them they missed the case that changed: one pins an exact value (sqrt(2)/8) on a regular QUAD box, where `cell_size` and `get_min_radius` happen to agree; the other covers simplices but checks the implementation against an independent reading of the SAME DEFINITION, so it stays true if the definition moves on both sides. Added: - `cell_size` on congruent right-isosceles cells is exactly 2h/3, to 1e-12. A redefinition fails immediately, at the quantity that changed. - The two measures agree on TENSOR cells (ratio 1) and differ on uniform SIMPLICES by exactly sqrt(2) — `cell_size` 2h/3 against `get_min_radius` sqrt(2)h/3. The Nitsche penalty scales with that ratio, so pinning it means neither measure can move silently. Negative-controlled against the real 1.745x shift: both fail, and both name the quantity rather than a downstream symptom. **The docstring was false.** `add_nitsche_bc` said the two measures "coincide on a uniform mesh". True on a regular quad box — presumably where it was checked — and false on a uniform simplex mesh, which is what every free-slip and fault model uses. Corrected in both the implementation comment and the user-facing parameter docs. **Four method-comparison assertions removed rather than relocated.** #729 moved them to tier C; that was the wrong fix. In every case the hard baseline was already present and the ratio was discarding it: - `test_1070` computed an absolute error against the PRESCRIBED datum and then asserted `errors["strong"] < 0.5 * errors["penalty"]`. Now asserts the error directly. The ratio also hid which side moved — strong went 1.06e-2 to 1.73e-2 between platforms while penalty barely changed. - `test_0066` and `test_0773` had their baselines asserted in sibling tests already, so the comparisons added nothing actionable. - `test_1060` asserted that the weak constraints must STAY inaccurate (`leak > 1e-5`) — an assertion that fails when the code improves. Its exact half, an essential BC holding v.n to machine precision, is a hard baseline and is now a tier B contract; the weak-path leaks are printed, not gated. Charter §8 leads with the baseline requirement and says why: it must be tight enough to fail the moment a default changes. TESTING-RELIABILITY-SYSTEM.md records that C1 is rare by design — reach for it only after showing no baseline exists. 55 passed, 1 skipped across the five files. Underworld development team with AI support from Claude Code
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved issues affect MPI safety, CI gating, documentation consistency, and baseline tightness.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR strengthens regression coverage for mesh-size semantics, replaces comparison-based checks with direct baselines, and updates related documentation.
Changes:
- Adds closed-form
cell_size()and radius-ratio tests. - Replaces method comparisons with direct baseline assertions.
- Updates Nitsche documentation and testing guidance.
File summaries
| File | Reviewed change and outstanding notes |
|---|---|
tests/test_1070_free_surface_plume.py |
Adds a direct baseline. CI execution, metric wording, baseline tightness, and unused penalty work need correction. |
tests/test_1060_nitsche_freeslip.py |
Adds an essential-BC contract; the test is not currently included in the CI merge gate. |
tests/test_0773_surface_smoother.py |
Removes smoother comparison characterization. |
tests/test_0066_integration_point_slcn.py |
Removes the method comparison; the module description still claims the unverified comparison. |
tests/test_0010_cell_size_geometry.py |
Adds closed-form geometry tests; MPI empty-rank reductions, ratio wording, and public docstrings need correction. |
src/underworld3/cython/petsc_generic_snes_solvers.pyx |
Corrects Nitsche documentation; the simplex ratio needs qualification and other public copies need updating. |
docs/developer/UW3_STYLE_CHARTER.md |
Strengthens baseline guidance, but conflicts with the retained Tier C policy. |
docs/developer/TESTING-RELIABILITY-SYSTEM.md |
Clarifies Tier C characterization-test usage. |
Review details
Suppressed comments (9)
docs/developer/UW3_STYLE_CHARTER.md:121
- This makes every non-baseline assertion a benchmark rather than a test, but the unchanged Tier C section below still defines C1 characterisation tests—including method comparisons—as suite tests, and
TESTING-RELIABILITY-SYSTEM.md:75-100preserves that fallback. Please reconcile the absolute rule with the supported C1 policy (or remove the C1 definition), otherwise contributors have contradictory guidance.
- **Every test asserts against a HARD BASELINE.** An analytic solution, a published
value, a closed-form geometric quantity, a conservation identity, an exactness
property. If a failure cannot name what is broken, it is not a test — it is a
drift detector, and it belongs in a benchmark rather than the suite.
src/underworld3/cython/petsc_generic_snes_solvers.pyx:6762
- This implementation comment repeats the same overgeneralisation: the
sqrt(2)value is a 2-D right-isosceles example, not a universal property of uniform simplex meshes. Keep the exact fixture result, but state that other simplex geometries can have a different ratio.
# The two coincide on TENSOR cells only. On a uniform SIMPLEX mesh --
# which is what the free-slip and fault models are built on -- they
# differ by exactly sqrt(2): on congruent right-isosceles cells of legs
# h, cell_size is 2h/3 and get_min_radius is sqrt(2)h/3. The penalty
# gamma*mu/h moves with that, so the two settings are NOT interchangeable
src/underworld3/cython/petsc_generic_snes_solvers.pyx:6659
- The correction here is not propagated to the other public copies:
src/underworld3/discretisation/discretisation_mesh.py:3253-3255still says a uniform mesh reduces toget_min_radius, anddocs/docstrings/inventory.json:4272still says the two measures coincide on a uniform mesh. Both are false for the uniform simplex case documented here, so regenerate/update those user-facing docstrings with this change.
The two coincide on **tensor** cells only. On a uniform **simplex**
mesh they differ by exactly :math:`\sqrt{2}` — for congruent
right-isosceles cells of legs :math:`h`, :meth:`Mesh.cell_size` is
:math:`2h/3` while :meth:`Mesh.get_min_radius` is
:math:`\sqrt{2}h/3` — so the penalty :math:`\gamma\mu/h` differs
between the two settings on the simplex meshes the free-slip and
fault models use. See ``tests/test_0010_cell_size_geometry.py``.
tests/test_0010_cell_size_geometry.py:120
quad_localis rank-local, so.min()raises on a starved MPI rank before the other ranks can complete the collective. The module advertises MPI use, andget_min_radius()explicitly handles empty partitions; reduce a local minimum with aninfidentity across ranks instead.
quad_ratio = float(quad_local.min()) / quad.get_min_radius()
tests/test_0010_cell_size_geometry.py:133
simplex_localis rank-local, so.min()raises on a starved MPI rank before the other ranks can complete the collective. The module advertises MPI use, andget_min_radius()explicitly handles empty partitions; reduce a local minimum with aninfidentity across ranks instead.
ratio = float(simplex_local.min()) / simplex.get_min_radius()
tests/test_0010_cell_size_geometry.py:103
- This sentence is self-contradictory: the measures do not coincide by
sqrt(2); on simplices their ratio issqrt(2), while tensor cells have ratio 1. Please state the two cases explicitly so the test documents the contract it asserts.
"""The two mesh-size measures coincide on TENSOR cells only, by sqrt(2).
tests/test_0010_cell_size_geometry.py:104
- This new regression establishes that uniform simplex
cell_size()andget_min_radius()differ, but the publicMesh.cell_size()docstring still says the opposite ("On a uniform mesh ... reduces to global get_min_radius",src/underworld3/discretisation/discretisation_mesh.py:3253-3255). Please update that public docstring too; otherwise the API documentation contradicts the contract pinned here.
def test_cell_size_and_min_radius_agree_on_tensor_cells_but_not_simplices():
"""The two mesh-size measures coincide on TENSOR cells only, by sqrt(2).
tests/test_0066_integration_point_slcn.py:124
- Removing the only nodal-vs-integration-point comparison leaves the module docstring's claim that the integration-point scheme is “at least as accurate as” nodal and preserves the peak unverified. Please update that opening description to list only the exact-departure and absolute-accuracy contracts that remain.
tests/test_1070_free_surface_plume.py:330 - After removing the rival-method assertion,
errors['penalty']is no longer consumed by any assertion; the fixture still runs four penalty solves/advances and this print only reports it. That doubles this level-2 test's expensive work without gating any penalty behavior. Either remove the penalty case/diagnostics or move the comparison to an explicit tier-C test.
print(f"datum error: strong={errors['strong']:.2e} penalty={errors['penalty']:.2e}")
- Files reviewed: 8/8 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| @pytest.mark.tier_b | ||
| def test_essential_bc_holds_the_wall_normal_velocity_exactly(self, solutions): |
| @pytest.mark.level_2 | ||
| @pytest.mark.tier_c | ||
| def test_freesurface_strong_constraint_against_penalty(constraint_measurements): | ||
| r"""Characterisation: the strong constraint tracks the prescribed rate more | ||
| closely than the weak penalty. | ||
|
|
||
| This compares two METHODS, so it can fail because the penalty path improved — | ||
| which would be good news. Tier C: a failure demands an explanation, not a | ||
| revert. It is NOT the justification for ``consistent_constraint="strong"``; | ||
| the contract that justifies it is the no-net-flux test above. | ||
|
|
||
| Measured 2026-09-12 on the annulus fixture, 4 solve/advance steps: datum | ||
| error strong 1.06e-2 against penalty 3.01e-2. The 0.5 factor characterises | ||
| this fixture and is not a specification. | ||
| @pytest.mark.tier_b | ||
| def test_freesurface_strong_constraint_tracks_the_prescribed_rate(constraint_measurements): |
| f"penalty {errors['penalty']:.2e}. If the penalty path improved, " | ||
| "explain it and re-characterise; do not revert to make this pass.") | ||
| print(f"datum error: strong={errors['strong']:.2e} penalty={errors['penalty']:.2e}") | ||
| assert errors["strong"] < 5.0e-2, ( |
| The quantity here is already an absolute error against a known answer — the | ||
| datum `fs._un_target` that the constraint is asked to reproduce — so it is | ||
| asserted directly. |
…dim) The two conflicts are both places where #743's hard baselines meet this branch's new definition of h, which is exactly what those baselines exist to catch. tests/test_0010: #743 pinned cell_size against geometry so a redefinition could not pass silently again. The definition has now changed on purpose, so the numbers move with it, still written as geometry rather than as whatever the code returns: * a congruent right-isosceles cell of legs h has area h^2/2, so volume**(1/dim) is h/sqrt(2). The old vertex-RMS gave 2h/3. Verified against the implementation after the change, not read off it. * cell_size and get_min_radius no longer differ. Both now read volume**(1/dim), so on a uniform mesh they agree EXACTLY - on simplices as well as tensor cells, which is the half that used not to hold. The test that pinned their sqrt(2) ratio now pins their equality, which is the contract this branch creates: local_h is a choice between the local cell and the global minimum and nothing else. tests/test_1060: took development's side. The HEAD hunk was orphaned - it printed max_vn_nit/max_vn_pen, which the rewritten test no longer defines. Also corrected two pieces of documentation that this branch makes wrong: * the gamma justification claimed volume**(1/dim) is "about 19% larger in the mean". It is not one number and it CHANGES SIGN with the dimension. Measured as new/old per cell: 2-D simplex box +12.2%, 2-D regular simplex +6.1% (closed form 6.07%), 2-D annulus +10.0%, 3-D simplex box -29.8%, 3-D spherical shell -33.3%. So h grows by about a tenth on triangles and SHRINKS BY ABOUT A THIRD on tetrahedra, and since the penalty is gamma*mu/h no single gamma reproduces the old enforcement in both. 12.5 is the 3-D number, calibrated on the Zhong shell; a 2-D sweep has NOT been done and is now said so in the docstring rather than implied. * the local_h docstring and its implementation comment still said the two measures "coincide on TENSOR cells only" and differ by sqrt(2) on simplices. That was true before this branch and is false after it. tests/test_0010 8 passed, test_1060 + test_1065 17 passed, serial band tests/test_00[0-4]*py 141 passed. Underworld development team with AI support from Claude Code
Maintainer ruling (2026-09-14): a test asserts against a hard baseline, or it
is not a test. A test loose enough to absorb a change in a default does not
fail when the default changes — it drifts inside its own margin and breaks later,
somewhere else, for reasons that are hard to trace back.
The case study
#734. #692 redefined
mesh.cell_size(); nothing failed; the Nitsche penaltyγμ/hmoved 43%; the Zhong spherical-shell benchmark slid from 0.2% to 2.4%— still inside its 5% tolerance — and only broke months later on one platform's
triangulation, three layers from the cause.
Why the existing tests missed it
cell_sizehad two tests, and between them they missed the case that changed:test_regular_square_cell_size_keeps_global_radiustest_cell_size_matches_own_vertices_and_tracks_deform#692 changed the simplex path.
The test that would have caught it on the day
Plus the relationship, which is itself exact:
cell_size()get_min_radius()The Nitsche penalty scales with that ratio, so pinning it means neither measure
can move silently.
Negative-controlled against the real 1.745× shift — both new tests fail, and
both name the quantity rather than a downstream symptom:
The docstring was false
add_nitsche_bcsaid the two measures "coincide on a uniform mesh". True on aregular quad box — presumably where it was checked — and false on a uniform
simplex mesh, which is what every free-slip and fault model uses. Corrected
in the implementation comment and the user-facing parameter docs.
Four comparisons removed rather than relocated
#729 moved them to tier C. That was the wrong fix — in every case the hard
baseline was already present and the ratio was discarding it:
test_1070computed an absolute error against the prescribed datum, thenasserted
errors["strong"] < 0.5 * errors["penalty"]. Now asserts the errordirectly. The ratio also hid which side moved: strong went 1.06e-2 → 1.73e-2
between platforms while penalty barely changed.
test_0066,test_0773— baselines already asserted in sibling tests,so the comparisons added nothing actionable.
test_1060asserted the weak constraints must stay inaccurate(
leak > 1e-5) — it would fail if Nitsche improved. Its exact half (anessential BC holds
v·nto machine precision) is a hard baseline and is now atier B contract; the weak-path leaks are printed, not gated.
Docs
Charter §8 leads with the baseline requirement and says why it must be tight
enough to fail the moment a default changes. TESTING-RELIABILITY-SYSTEM.md
records that C1 characterisations are rare by design — reach for one only after
showing no baseline exists.
55 passed, 1 skipped across the five files.
Underworld development team with AI support from Claude Code