Skip to content

Pin the spherical relaxation rate and the direction of the surface response (#496) - #660

Open
lmoresi wants to merge 3 commits into
developmentfrom
feature/pin-y20-relaxation-rate
Open

Pin the spherical relaxation rate and the direction of the surface response (#496)#660
lmoresi wants to merge 3 commits into
developmentfrom
feature/pin-y20-relaxation-rate

Conversation

@lmoresi

@lmoresi lmoresi commented Aug 27, 2026

Copy link
Copy Markdown
Member

Closes #496, both halves. The first commit pinned the direction of the 3-D surface response; the second pins the rate, and replaces the mean-free assertion this PR previously flagged as weak.

The rate

Every free-surface test in the suite starts flat and is driven by a load, so nothing exercised the decay of an imposed topography and a rate regression was invisible to CI. The new test imposes a degree-2 bump on a constant-density shell under radial gravity — the topographic self-load is the only driver — and fits the modal amplitude decay against the half-space Cathles rate ρg/(2ηk), k = √6/R = 0.204.

run ratio to Cathles floor (% of A0) fit residual
cell 0.35, 8 steps, np=1 0.5280 16.0 0.043% of A0
cell 0.35, 8 steps, np=2 0.5280 16.0 0.043% of A0
cell 0.35, 20 steps 0.5524 18.0 0.138% of A0
cell 0.25, 8 steps 0.5156 11.2 0.017% of A0

Asserted band 0.45 to 0.62. It catches a sign error, an order-of-magnitude error, and the half-space value itself being returned. It does not catch a 2× error in buoyancy_scale — measured at 0.476, still inside the band. The exponential update is only weakly sensitive to that scale, and a band tight enough to see it would sit within a few percent of the resolution spread and flake. The assertion message says this rather than implying coverage it does not have.

The fit protocol is part of the answer

The decay lands on a floor — the #431-class bias in the recovered h_inf — so the extracted rate depends entirely on how the floor is treated. On the same cell-0.35 record:

floor treatment rate ratio
no floor (2-param) 0.0643 0.315
floor free (3-param) 0.1097 0.537
floor = last sample 0.1420 0.696

So the 0.0842 / ratio 0.41 this PR previously quoted is not reproducible without also quoting its protocol. The test now fixes the protocol in code (floor free) and documents why it must not be changed casually.

This also revises the earlier reading that the rate is not converged. That came from fitted rates 8.19 (res 0.2) and 6.07 (res 0.15) under the floor-pinned fit — the most floor-sensitive of the three — on runs whose floors were 16.3% and 2.2%. With the floor free, the ratio moves only 0.528 → 0.516 from cell 0.35 to 0.25. The 26% was mostly the fit reacting to a changing floor. The floor is what converges strongly; the rate is close to converged, though two coarse resolutions do not establish that and the test says so.

The mean-free assertion

free_surface.py:901 builds _h_inf = _demean(-_demean(h)), and _demean is subtraction of _surface_mean, so the asserted quantity was zero by construction: measured 1e-18 against a 1e-8 threshold. It also could not see a wrong trace-mass weight, since both sides use the same weights.

Replaced by the FE boundary integral of the P1 carrier — an independent implementation of the same integral, which does depend on the weights being right — plus a negative control asserting the unweighted nodal mean is not zero (2.5e-4 of max|h_inf|). Without that control, "the weighted mean vanishes" says nothing about the gauge.

Two rank-local diagnostics in the end-to-end test (the h_inf and shape maxima) are now reduced globally.

A bug in the probe, worth knowing about

The measurement first showed the initial modal amplitude differing 4.6% between np=1 and np=2 on a bit-identical mesh. It was the instrument, not the library: _field_rows re-sorts whatever coordinates it is handed, so matching against _ring_coords — documented at free_surface.py:211 as live, deformed geometry — sorts by deformed x, a different permutation from the reference-x order every surface array uses, and silently permutes the carrier fill. With rows taken from _surf_coords the amplitude is identical across partitions to 11 digits.

The same bug is in ylm_relaxation.py, the probe the original workstream-C numbers came from. It biases the amplitude but not the decay rate, so those rates are not wrong for that reason.

Measurements, scripts and the negative controls: ~/+Simulations/spherical_relaxation_rate_496/README.md.

Tests: test_1072 3 passed serial and at np=2, ~55s.

Underworld development team with AI support from Claude Code


Follow-up: the test did not actually run in CI

scripts/test.sh excludes test_106*py and test_107*py pending the #504 triage, so test_1072_free_surface_spherical.py has never been executed in CI — which means both halves of #496 were still invisible there, the sign half included. Merging the first commit would have closed the issue without closing the gap it exists to close.

The second commit pulls that one file forward out of the excluded group, the same way tests/parallel/test_1069_boundary_normal_parallel.py already is. The rest of the group still awaits its triage decision.

It also names the mesh in the band-failure message. gmsh triangulates differently across platforms (serial_reference.py:202 records 1417 vs 1395 cells for a spherical shell between hosts), so the Linux runner will not build the 585-cell mesh the band was measured on. A failure looks unlikely — cell 0.35 → 0.25 is a far larger change and moved the ratio only 2.3% — but if the band does trip on another host, the message now carries the cell count, A0 and the floor, which is what separates a platform mesh difference from a real regression.

Unrelated CI failure on the first push

test_0053_hang_watchdog.py::test_watching_restores_the_previous_watchdog failed with a crashed xdist worker (worker 'gw0' crashed). Re-running the same commit unchanged passed, so it is flaky, not deterministic — and it cannot be caused by this branch in any case, since test_1072 was not being run at all at that point. Filed as #661: the failure mode is a crashed worker rather than an assertion, which is the same class as #638 and #639 in the same watchdog machinery.

test_1072's surface assertion is symmetric in sign:

    assert 0.0 < np.abs(shape).max() <= 1.5 * np.abs(h_inf).max()

Flip the 3-D recovery and |shape| is unchanged, so it passes while the surface
moves the wrong way. #496 records exactly that: CI cannot catch a 3-D sign
error.

Starting from flat, the displacement IS the shape, so it must correlate
POSITIVELY with the equilibrium it is relaxing toward. Measured +0.995; a sign
error gives about -0.995, which the magnitude bound cannot see. Asserted at
> 0.9.

PARTIAL, and the docstring now says so. The other half of #496 -- the spherical
relaxation RATE -- is not pinned. The review measured a fitted decay rate of
0.0842 against a half-space Cathles rate rho*g/(2*eta*k), k = sqrt(6)/R, of
0.204: ratio 0.41, an O(1) shell correction below the half-space value and in
the physically correct direction. Asserting that band needs an
initial-topography relaxation setup, and no test in the suite has one -- every
free-surface test starts flat and is driven by a load. Building it hastily
would risk a test that passes for the wrong reason, which is the failure #496
exists to fix, so it is left for its own session with the numbers recorded in
the docstring.

The mean-free assertion above is also weak (it applies _surface_mean to a field
_demean'ed with the same weights, so it is close to true by construction), as
#496 notes. Left alone here: replacing it properly means checking the gauge
against an independently computed surface integral, which belongs with the rate
work.

Underworld development team with AI support from Claude Code
Copilot AI lite review requested due to automatic review settings August 27, 2026 11:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Closes the second half of #496. Every other free-surface test starts flat and
is driven by a load, so nothing in the suite exercised the decay of an imposed
topography: a rate regression was invisible to CI. This adds a Y20 relaxation on
a constant-density shell under radial gravity, where the topographic self-load
is the only driver, and asserts the fitted decay rate against the half-space
Cathles rate rho*g/(2*eta*k), k = sqrt(6)/R.

Measured ratio 0.528 at cell 0.35 and 0.516 at cell 0.25; np=1 and np=2 agree to
five digits. Asserted band 0.45 to 0.62. The band catches a sign error, an
order-of-magnitude error, and the half-space value itself being returned; it
does NOT catch a 2x error in buoyancy_scale (measured 0.476), and the assertion
message says so rather than implying more coverage than it has.

The decay lands on a floor - the #431-class bias in the recovered h_inf, 16% of
the initial amplitude at cell 0.35 and 11% at 0.25 - so the extracted rate
depends entirely on how the floor is treated: the same record gives 0.064 with
no floor, 0.110 with the floor free, 0.142 with the floor pinned to the last
sample. The protocol (floor free) is therefore fixed in code and documented.
This also means the docstring's previous 0.0842 / ratio 0.41 was not
reproducible without its protocol, and that the earlier "the rate is not yet
converged" reading was mostly the naive fit reacting to a changing floor.

Also replaces this file's mean-free assertion, which had no content:
free_surface.py:901 builds _h_inf as _demean(-_demean(h)) and _demean IS
subtraction of _surface_mean, so the asserted quantity is zero by construction -
measured 1e-18 against a 1e-8 threshold - and it stays zero even if the
trace-mass weights are wrong, because both sides use the same weights. It is now
the FE boundary integral of the P1 carrier, an independent implementation of the
same integral, plus a negative control asserting the UNWEIGHTED nodal mean is
NOT zero (2.5e-4 of max|h_inf|), so the check cannot pass by construction.

The rank-local max and shape bounds in the end-to-end test are now reduced
globally; they were rank-local diagnostics that could disagree across ranks.

Measurements, negative controls, and the surface-array ordering bug that had to
be fixed in the probe first (matching _field_rows against the LIVE _ring_coords
after a deform re-sorts by deformed x and silently permutes the carrier fill;
free_surface.py:211 warns of exactly this) are in
~/+Simulations/spherical_relaxation_rate_496/README.md. That bug is also present
in the earlier ylm_relaxation.py probe the workstream-C numbers came from.

Serial and np=2: 3 passed, ~55s.

Underworld development team with AI support from Claude Code
@lmoresi lmoresi changed the title Pin the direction of the spherical surface response (#496, partial) Pin the spherical relaxation rate and the direction of the surface response (#496) Aug 28, 2026
The rate test added in the previous commit does not run in CI. scripts/test.sh
excludes test_106*py and test_107*py pending the #504 triage, so
test_1072_free_surface_spherical.py has never been executed there - which means
BOTH halves of #496 were still invisible to CI, the sign half included, and
merging the previous commit would have closed the issue without closing the gap
it exists to close.

Pulls that one file forward out of the excluded group, the same way
tests/parallel/test_1069_boundary_normal_parallel.py already is, and for the
same kind of reason. It is level_2/tier_b, ~55s serial, and passes at np=1 and
np=2. The rest of the group still awaits its triage decision.

Also names the mesh in the band-failure message. gmsh triangulates differently
across platforms - serial_reference.py:202 records 1417 vs 1395 cells for a
spherical shell between hosts - so the Linux runner will not build the 585-cell
mesh the band was measured on. The resolution study makes a failure unlikely
(cell 0.35 -> 0.25, a far larger change, moved the ratio only 2.3%), but if the
band does trip on another host, the message now carries the cell count, A0 and
the floor, which is what distinguishes a platform mesh difference from a real
regression.

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