Skip to content

Normalize the gradient by the dual cell and add the spherical metric terms to curl and divergence - #1663

Open
rajeeja wants to merge 4 commits into
mainfrom
rajeeja/gradient-dual-area-metric-terms
Open

Normalize the gradient by the dual cell and add the spherical metric terms to curl and divergence#1663
rajeeja wants to merge 4 commits into
mainfrom
rajeeja/gradient-dual-area-metric-terms

Conversation

@rajeeja

@rajeeja rajeeja commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #1662.

Green-Gauss on a contour has to divide by the area that same contour encloses. _compute_gradients_on_faces walks the dual cell but _normalize_and_project_gradient divided by the primal face area, so gradients came out inflated by A_dual/A_primal — about 4 on quads, 3 on hexagons. Separately, curl and divergence used the planar formulas and dropped the u*tan(lat)/a and -v*tan(lat)/a metric terms, which costs a factor of two on solid-body rotation and partly cancelled the area inflation. Both corrections have to land together, since fixing the area alone drives curl to 0.5.

Verified against closed-form answers on the unit sphere, median ratio to exact over faces below 60 degrees:

mesh gradient curl divergence
healpix z4 0.9931 0.9965 0.9965
healpix z5 0.9981 0.9991 0.9991
healpix z6 0.9995 0.9998 0.9998
ne30pg2 (quad) 0.9994 0.9996 0.9996
QU480 (hex) 0.9959 0.9975 0.9975

All were 4.0 / 3.0 / 2.0 / 1.5 before, depending on operator and topology. The quad and hex columns now agree, which they did not previously.

Two behavior changes worth flagging for review. First, curl and divergence of a constant field are no longer zero — on a sphere they reduce to the metric term, and the new values match tan(lat)/a to machine zero, so I updated those two tests rather than the code. Second, a face whose contour is left open by a missing edge neighbor now returns NaN, because there is no enclosed area to divide by; the all-boundary quad-hexagon fixture is therefore all-NaN now. I checked that #1452 stays fixed — ne30pg2 is 1.2% NaN, not 100% — and added a regression guard for it.

There is a cost: gradient on healpix z7 goes from 0.408 s to 1.037 s, about 2.5x, since the dual-cell area is rebuilt per face inside the njit loop. It is not cached the way Grid.bounds is. Happy to add caching here or as a follow-up if you would rather not take that hit now.

Also added manufactured-solution tests with non-zero exact answers. The existing suite was all null tests and ordering comparisons, which a constant-factor error passes — that is how this shipped.

test/core is green, 137 passed, and pre-commit is clean.

…terms

Green-Gauss on a contour must divide by the area that contour encloses.
The face gradient integrates around the dual cell but divided by the
primal face area, inflating results by A_dual/A_primal: about 4 on
quadrilateral meshes and 3 on hexagonal ones. Refining healpix z3 to z6
converged to 3.9278, 3.9811, 3.9952, 3.9988, so this was a normalization
error rather than truncation.

Curl and divergence used the planar formulas and dropped the u*tan(lat)/a
and -v*tan(lat)/a metric terms. On solid-body rotation that costs exactly
a factor of two, which partly cancelled the area inflation and made curl
look like a clean 2x on quads but 1.5x on hexagons. Both corrections have
to land together: fixing the area alone drives curl to 0.5.

Faces whose contour is left open by a missing edge neighbor now return
NaN, since no enclosed area exists to divide by. Interior faces of
SCRIP-derived grids stay finite, so #1452 remains fixed.

Adds manufactured-solution tests with non-zero closed-form answers. The
previous suite relied on null tests and ordering comparisons, both of
which are satisfied by an operator that is off by a constant factor.

Fixes #1662
Comment thread uxarray/core/gradient.py
cmdupuis3 and others added 2 commits August 11, 2026 08:48
…closed form

_compute_gradients_on_faces spent half its time re-deriving the dual-cell
area with 4th-order Gaussian quadrature (16 points per fan triangle, ~580
heap allocations per face) when the area of a polygon bounded by great-circle
arcs has an exact closed form (spherical excess). The other half was lost to
per-edge np.cross/np.linalg.norm temporaries and array-based fill-value
checks; both are now scalar, following the zero-allocation convention used
elsewhere in the grid kernels.

Gradient values shift by up to 5e-6 relative to the old quadrature-based
result -- that gap is the retired quadrature's own truncation error, so the
new values are the more accurate ones. All existing test assertions hold to
6 significant digits. Added a regression test pinning scale invariance,
since the dyamond-30km test grid stores face_x/y/z in meters rather than as
unit vectors, which is the one input that distinguishes a correct closed-form
implementation from a plausible wrong one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The closed-form area reproduced the retired quadrature's unsigned accumulation
for bit-compatibility. That convention is wrong for a non-convex cell: a fan
from vertex 0 sweeps some triangles backwards and they have to cancel, but
taking abs per triangle double-counts them. About 46% of HealPix z4 dual cells
are non-convex, differing by up to 20% in area.

Measured against d/dlat sin(lat) = cos(lat), the unsigned sum leaves a
worst-case gradient error of ~19% that does not improve with resolution
(0.186 / 0.192 / 0.196 at z4 / z5 / z6). Signed converges as expected
(0.0127 / 0.0088 / 0.0051). Only the tail is affected, which is why the median
looked healthy either way; the new test bounds the worst face so a regression
to unsigned fails.
@rajeeja
rajeeja requested a review from cmdupuis3 August 11, 2026 14:24

@erogluorhan erogluorhan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks good to me as long as @cmdupuis3 's concerns are addressed. Thanks @rajeeja !

@cmdupuis3

Copy link
Copy Markdown
Collaborator

Fine with me, I just wanted a second opinion on the new code before I signed off on it

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.

gradient divides by primal face area but integrates over the dual cell; curl and divergence drop the spherical metric term

3 participants