Skip to content

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

Description

@rajeeja

I was putting together a demo of the curl feature for a paper and the numbers came out about 2x too big, so I dug in. Two things are going on, and they partly cancel, which is why it looks like a clean factor of 2 on quad meshes but 1.5 on hexagons.

First, _compute_gradients_on_faces walks the dual cell (centroids of the faces sharing a node with the face, per its own docstring) but _normalize_and_project_gradient divides by the primal face area, so the gradient comes out inflated by A_dual/A_primal — about 4 on quads, 3 on hexagons. It isn't truncation error; refining healpix z3 through z6 converges to 3.9278, 3.9811, 3.9952, 3.9988. Second, UxDataArray.curl computes dv/dx - du/dy, which is the planar formula and is missing the u*tan(lat)/a metric term from the spherical expression, and divergence is missing the companion -v*tan(lat)/a term.

import numpy as np
import uxarray as ux
from uxarray.core.dataarray import UxDataArray

g = ux.Grid.from_healpix(zoom=5)
lat = np.deg2rad(g.face_lat.values)
m = np.abs(lat) < np.deg2rad(60)

# 1) gradient of phi = sin(lat); exact d(phi)/d(lat) = cos(lat)
phi = UxDataArray(np.sin(lat), dims=["n_face"], uxgrid=g, name="phi")
gm = phi.gradient(scale_by_radius=False)["meridional_gradient"].values
print(np.nanmedian((gm / np.cos(lat))[m]))          # 3.9952, expected 1.0

# 2) solid-body rotation u = cos(lat), v = 0; exact zeta = 2 sin(lat)
u = UxDataArray(np.cos(lat), dims=["n_face"], uxgrid=g, name="u")
v = UxDataArray(np.zeros_like(lat), dims=["n_face"], uxgrid=g, name="v")
c = u.curl(v, scale_by_radius=False).values
print(np.nanmedian((c / (2 * np.sin(lat)))[m]))     # 1.998, expected 1.0

# 3) u = 0, v = cos(lat); exact div = -2 sin(lat)
u0 = UxDataArray(np.zeros_like(lat), dims=["n_face"], uxgrid=g, name="u")
d = u0.divergence(v, scale_by_radius=False).values
print(np.nanmedian((d / (-2 * np.sin(lat)))[m]))    # 1.998, expected 1.0

Dividing out the 4.0 and then adding the metric term recovers 0.9996 on healpix z5 and 0.9976 on QU480 for both curl and divergence, so the two corrections have to land together — fixing the area alone drives curl to 0.5, and fixing the metric term alone would leave it 4x high. Tested on clean main at uxarray 2026.6.1.dev115+gf78f5ffab.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    Status
    📚 Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions