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 1 commit 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 1 commit 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
Comment on lines 289 to 302
@njit(cache=True, parallel=True)
def _compute_gradients_on_faces(
data,
n_face,
face_coords,
edge_face_connectivity,
face_node_connectivity,
node_edge_connectivity,
face_lat,
face_lon,
node_coords,
normal_lon,
normal_lat,
):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This entire function looked slow to me, so I had Claude come up with something, now on cmd/gradient-dual-area. Supposedly the method it came up with is more numerically accurate as well, but I think some of the speed benefits would still apply if want to keep the same quadrature scheme.

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

2 participants