Skip to content
Open
19 changes: 15 additions & 4 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -584,13 +584,24 @@ Arcs
grid.arcs.in_between
grid.arcs.point_within_gca
grid.arcs.extreme_gca_latitude
grid.arcs.orient3d_on_sphere
grid.arcs.on_minor_arc


Accurate Computing
------------------
Compensated Arithmetic
----------------------

Numba-compiled primitives used throughout the geometry stack to avoid
catastrophic cancellation in cross-product and dot-product operations.
``two_sum`` and ``two_prod`` are true error-free transformations (EFT);
the higher-level functions are compensated algorithms built on top of them.

.. autosummary::
:toctree: generated/

utils.computing.cross_fma
utils.computing.dot_fma
utils.computing.two_sum
utils.computing.two_prod
utils.computing.diff_of_products
utils.computing.accucross
utils.computing.accucross_pair
utils.computing.acc_sqrt_re
542 changes: 542 additions & 0 deletions docs/user-guide/spherical-geometry-accuracy.ipynb

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions docs/userguide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ Supplementary Guides

These user guides provide additional details about specific features in UXarray.

`Accurate Spherical Geometry <user-guide/spherical-geometry-accuracy.ipynb>`_
How UXarray uses compensated arithmetic to avoid catastrophic cancellation in cross-product and point-in-polygon operations

`Working with HEALPix Grids <user-guide/healpix.ipynb>`_
Use UXarray with HEALPix

Expand Down Expand Up @@ -127,6 +130,7 @@ These user guides provide additional details about specific features in UXarray.
user-guide/dual-mesh.ipynb
user-guide/structured.ipynb
user-guide/from-points.ipynb
user-guide/spherical-geometry-accuracy.ipynb
user-guide/healpix.ipynb
user-guide/holoviz.ipynb
user-guide/from_file.ipynb

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
pairs_id,ref_angle_deg,a0_x_sig,a0_x_exp,a0_y_sig,a0_y_exp,a0_z_sig,a0_z_exp,a1_x_sig,a1_x_exp,a1_y_sig,a1_y_exp,a1_z_sig,a1_z_exp,b0_x_sig,b0_x_exp,b0_y_sig,b0_y_exp,b0_z_sig,b0_z_exp,b1_x_sig,b1_x_exp,b1_y_sig,b1_y_exp,b1_z_sig,b1_z_exp,baseline_x_sig,baseline_x_exp,baseline_y_sig,baseline_y_exp,baseline_z_sig,baseline_z_exp
8,5.85151e-08,-7230928401775691,-53,6413551006731969,-54,-8616241346204776,-54,8773798999433595,-53,-6000682488046241,-57,-8009426984445850,-55,-7230928402586527,-53,6413550996183423,-54,-8616241351334767,-54,8773799000690885,-53,-6000682357192868,-57,-8009426968536627,-55,-8679261493192915,-55,8181549207548938,-54,-7725742864056452,-53
14,8.70857e-08,-4607551742761558,-53,-7277539273274117,-54,-6830773080982109,-53,6068630138184276,-53,5502869841781176,-53,7488602049766939,-54,-4607551734638633,-53,-7277539289680749,-54,-6830773082091328,-53,6068630128670322,-53,5502869851389287,-53,7488602052365278,-54,5399770290349084,-55,4985477006629652,-54,-8549476488032171,-53
21,2.51618e-07,-7848987668729198,-53,7391109489544601,-54,4843926555650740,-54,-4699941368622357,-56,-8962842026919421,-53,5378199410351573,-56,-7848987668539763,-53,7391109489604955,-54,4843926556786475,-54,-4699941463474771,-56,-8962842028808165,-53,5378199126013745,-56,-7899292735369896,-53,7141700366785971,-54,4890742955135284,-54
24,4.02326e-07,-4568328231438871,-56,-5420625993444073,-54,8570749909878658,-53,-6116375928311063,-53,6025041063082346,-53,-5447371515274217,-54,-4568327936189426,-56,-5420625903420298,-54,8570749926571625,-53,-6116375934808137,-53,6025041055158319,-53,-5447371521151577,-54,-6650054535212328,-53,5934449427713896,-53,-5198035141420018,-55
27,5.38487e-07,-5234064471250982,-55,5058579375404553,-53,-7336770317160175,-53,7902251572577888,-53,-6669376776424640,-54,5500513376701818,-54,-5234064502419141,-55,5058579335254894,-53,-7336770343452975,-53,7902251586143424,-53,-6669376636627692,-54,5500513468250617,-54,7959464020805770,-54,7744128462582881,-54,-7092142844636924,-53
29,6.11719e-07,6050986485628052,-54,-5624975274774618,-54,-8004120335292124,-53,-8271075057631019,-53,-8456986651598985,-55,5744185531146434,-54,6050986468807466,-54,-5624975236079039,-54,-8004120345269573,-53,-8271075024522282,-53,-8456986956264200,-55,5744185609702737,-54,8263033716329409,-56,-7484661030342715,-54,-8127592586860685,-53
33,8.91218e-07,-6217993416889531,-53,-5375584469257136,-54,-5936494688416347,-53,8468705800669433,-53,6650362224383374,-55,-5156143178421444,-54,-6217993403770544,-53,-5375584572815451,-54,-5936494678714016,-53,8468705767162147,-53,6650362753381212,-55,-5156143227983011,-54,-6704166870089359,-54,-4705053759241793,-54,-8022393180327608,-53
34,9.48276e-07,-4962019623479789,-53,-5584302200557263,-54,6979390510927226,-53,-7402494061160030,-55,7122803430921892,-56,-8769954796524428,-53,-4962019616170019,-53,-5584302245707275,-54,6979390507092851,-53,-7402494213353407,-55,7122804370969596,-56,-8769954776565984,-53,-6025832852997469,-53,-5940435175330124,-54,5999732021037956,-53
35,9.95453e-07,-6035617260517938,-53,6276240380711044,-53,-4608587368240598,-54,7967925325362540,-54,-5818023032045022,-53,5604309762634636,-53,-6035617359078084,-53,6276240269121175,-53,-4608587459803159,-54,7967925495800519,-54,-5818022935560021,-53,5604309802218937,-53,-5796477142969564,-54,-7348950478093914,-56,8478560672644647,-53
37,1.58465e-06,7109237221237507,-53,-5391249802838377,-53,4936087698042736,-55,-7325286922064621,-54,6319459771478421,-54,-7598077937289827,-53,7109237104464673,-53,-5391249958781681,-53,4936087663801729,-55,-7325286912718776,-54,6319459783959252,-54,-7598077936947270,-53,-6932332681741704,-54,6041380115989781,-54,-7745370287181335,-53
42,6.23097e-06,-7588425694061722,-54,5300337922171494,-54,7727237007089296,-53,5408937037834848,-53,7294951169381972,-54,-6210391323775678,-53,-7588426574445265,-54,5300338281723904,-54,7727236729290293,-53,5408937802886953,-53,7294950544480860,-54,-6210390840961762,-53,-7744496150009437,-56,6740256028889544,-53,5895883564613215,-53
44,8.33965e-06,4682519649110699,-54,-7138809952082747,-53,-4968453815481641,-53,-5285473185278374,-56,4695891434211099,-53,7657789400935486,-53,4682518032116495,-54,-7138810329968187,-53,-4968453653509528,-53,-5285464430986344,-56,4695891945672347,-53,7657789181709099,-53,8009098880075897,-54,-7929304319014417,-53,5959000202527590,-55
45,9.41793e-06,8237100306028881,-53,7022501152606908,-54,-7801277242848944,-56,-7777560375867427,-54,-7705965765779950,-53,-5147861179623380,-54,8237100704763774,-53,7022499948427653,-54,-7801267641690998,-56,-7777560736256870,-54,-7705965493685948,-53,-5147862264352867,-54,-7024749570822305,-58,-8077493784355160,-53,-7958779314590154,-54
46,1.51358e-05,-8035181008987649,-55,7196128282874947,-53,-5030916197430068,-53,-7477428736703008,-57,-8558733024802324,-54,7911905879718196,-53,-8035186374763230,-55,7196127591634174,-53,-5030916650542349,-53,-7477422757881264,-57,-8558732639694402,-54,7911906005938418,-53,-5306357025023269,-55,-5877246113821485,-54,8410368237430861,-53
47,2.93867e-05,-8530315408545074,-55,-8548698092705777,-53,-7484925409163612,-55,-8505865213578574,-56,8942315233371556,-53,-5915409831234063,-58,-8530307527993322,-55,-8548697921080605,-53,-7484937526617150,-55,-8505884745109989,-56,8942315020689593,-53,-5915289701358967,-58,-7001500447725934,-53,-6239928543688813,-54,-4730164144880664,-53
51,6.56984e-05,8275974108886025,-53,5167277393821351,-54,-4883728922649398,-54,-7782304739439814,-53,-8433407601596845,-54,6675064540130152,-55,8275976163428860,-53,5167273828800138,-54,-4883718768116487,-54,-7782306117123403,-53,-8433405211054708,-54,6675050921788293,-55,-6020764263020120,-53,-6698722592085563,-53,5412348747689106,-59
55,0.000159124,4893062498398695,-53,-8738676079592702,-54,-6172233217836116,-53,-5954009323050466,-54,7366386763692950,-54,7661668758944494,-53,4893069065022559,-53,-8738652712226967,-54,-6172236283009425,-53,-5954016406967466,-54,7366374159614903,-54,7661670412262375,-53,-5356451187211240,-56,5259089675872662,-54,8588761644687132,-53
56,0.000232053,8489692534713884,-53,-8426129931190372,-56,5637513389912148,-54,-7989824472859641,-54,-8510125299767764,-55,-7787420952999829,-53,8489684044474492,-53,-8426392313840217,-56,5637540021519676,-54,-7989814084509932,-54,-8510045039288690,-55,-7787429099367662,-53,6377195679142295,-55,-7371982033656451,-54,-8062339585556965,-53
59,0.00057029,5781018189649026,-53,-7080086847675367,-55,-6676562756755131,-53,-4910678895854829,-53,5937140050511939,-54,6942799377885296,-53,5781038845985583,-53,-7080167487032430,-55,-6676539526433992,-53,-4910706842040514,-53,5937194599449982,-54,6942767949327463,-53,7224228704922008,-53,5697803458625058,-55,-5187590747906738,-53
61,0.000767608,8251478203846825,-54,7967596070464569,-53,6326215350792608,-56,-6052848968698497,-53,5425533121280526,-56,6635715023714418,-53,8251479346515285,-54,7967595719333065,-53,6326219807145638,-56,-6052926909053857,-53,5425916326945822,-56,6635639032788121,-53,8173101628292853,-54,7982689126669748,-53,6723880617402731,-56
70,0.00723011,4808962270786330,-56,6097550991378913,-57,-8979034354739469,-53,5423237434572450,-53,8165150607383119,-56,7118737270293392,-53,4807007705752224,-56,6114984972798435,-57,-8979004394930222,-53,5423388220981957,-53,8159770776237459,-56,7118718780114812,-53,8506946025451435,-53,7375198673046507,-55,4631159905674187,-54
71,0.00890077,5769443369962036,-54,-5983614074820145,-53,6083122702890450,-53,-5790623327371145,-54,6593948792725823,-53,-5409865811981166,-53,5767188889012695,-54,-5983906168917601,-53,6083369827308603,-53,-5788239974269031,-54,6594257583702071,-53,-5410127062691128,-53,5211102010518357,-55,4530259865131377,-54,8619837631472525,-53
74,0.0206073,-8035659862333313,-54,-7363807833259551,-53,-6560797284980930,-54,-7917940611768033,-56,8425555425553090,-53,6053119466146400,-54,-8035005826375428,-54,-7362865851567044,-53,-6565825221238476,-54,-7921074297809246,-56,8424427095051799,-53,6059142060728708,-54,-8880049322165750,-53,-4836184681008166,-56,-5526538576809171,-55
76,0.0462026,8127173703048925,-57,7441123885394209,-56,8944633338416150,-53,8639627923593602,-57,8409091324007294,-55,-8741768269249357,-53,8062154403355115,-57,7452323460812561,-56,8944717496730583,-53,8731602008241724,-57,8401170055227091,-55,-8741887316731750,-53,8484083315109138,-55,5760563192395373,-53,6591420238834665,-53
77,0.052799,5221222816770053,-53,-4950274785268426,-54,6909570579987614,-53,-4968381977795672,-57,5287825588365527,-54,-8604820653289657,-53,5221435213196362,-53,-4944297667067441,-54,6910479930790452,-53,-4975824390786103,-57,5274735625260514,-54,-8606812142843384,-53,7594585201434241,-53,-7582744394822946,-55,8912527867656076,-54
81,0.099423,5136209688356046,-53,-7570367369932403,-54,6357780502888556,-53,-6641817027751932,-53,4674075341242368,-54,-5617308956303604,-53,5139981731784467,-53,-7586164797614735,-54,6350019852519077,-53,-6646291071864332,-53,4692812768224429,-54,-5608104000752893,-53,-6739521340379483,-55,-6597567917735689,-53,5896008331225631,-53
82,0.173987,6554513973516502,-54,-7987770620112503,-54,-7378218293563006,-53,6401872806519504,-56,6043074187390434,-55,8843463246735159,-53,6528975833162550,-54,-8024627229178541,-54,-7373879339620572,-53,6524199114179981,-56,6131344766951964,-55,8838267408201787,-53,7404977277993908,-53,-5127941779437070,-53,6537454199100429,-62
84,0.356562,-7451089160378882,-53,6599354785688124,-56,4993036832193222,-53,8396296442464927,-53,5695352472672690,-54,6353058498534731,-55,-7441483107366564,-53,6280708453029850,-56,5013741470761668,-53,8395252491023875,-53,5704009808254162,-54,6344058074638568,-55,8125490787250087,-53,6291989587332959,-54,4564539249100196,-54
93,3.5037,8495418835663911,-53,5453966742131135,-54,4933248690565520,-55,-8570297643620917,-53,-8511367560966975,-55,-7101465318633758,-55,8572736635333399,-53,5115703395540285,-54,8372219540489569,-56,-8634536639855365,-53,-7949279712094154,-55,-6480709755306040,-55,-8040774305772088,-53,5865117273986101,-56,-7984508305031541,-54
94,4.49269,-7368955444388499,-53,4662451496900326,-53,4512063879102644,-54,-7641843075768684,-55,-7528642108591652,-53,-4560627814860603,-53,-7383904688886592,-53,4545090976327561,-53,4878416946900325,-54,-7448872215335746,-55,-7149907908367128,-53,-5151756907740391,-53,-8600420037733444,-53,5033249332855021,-54,7284015936230180,-56
99,9.5294,-7742073097949454,-55,7446055199367508,-56,-8747405273392888,-53,8198771485748285,-54,-7182485598965534,-53,7137667844998008,-54,-5584749836427226,-56,6228565086417920,-55,-8844072287110214,-53,7613143603838169,-54,-7330708031740297,-53,7183416815087230,-54,8200993966311024,-54,-7840059179852882,-53,6751633831473711,-55
208 changes: 208 additions & 0 deletions test/grid/geometry/test_accusphgeom_baseline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
"""Baseline regression tests ported from the AccuSphGeom C++ test suite.

Test data and expected results are taken directly from:
https://github.com/hongyuchen1030/AccuSphGeom

Specific C++ tests mirrored here:
tests/test_gca_gca_intersection_baseline.cpp — 31 near-tangent GCA pairs
tests/test_gca_constlat_intersection_baseline.cpp — 200 arc/latitude cases
tests/test_pip_robust.cpp — simple spherical triangle
tests/test_pip_complicated.cpp — 12-vertex concave polygon

The C++ library uses ultra-tight tolerances (3–100 ULP) backed by Shewchuk
adaptive precision and a geogram fallback. This Python port implements only
the EFT tier, so the tolerances here reflect what double-precision EFT can
achieve:

GCA-GCA intersection: 3e-8 (C++ reference: 1e-8)
GCA-const-lat intersection: 1e-13 (C++ reference: 3–100 ULP ≈ 7e-16–2e-14)
Point-in-polygon: exact location codes (same as C++)
"""

import math
import os

import numpy as np
import pytest

from uxarray.grid.intersections import gca_const_lat_intersection, gca_gca_intersection
from uxarray.grid.point_in_face import (
_LOC_INSIDE,
_LOC_ON_EDGE,
_LOC_ON_VERTEX,
_LOC_OUTSIDE,
_point_in_polygon_sphere,
)

_DATA_DIR = os.path.join(os.path.dirname(__file__), "data", "accusphgeom")
_GCA_GCA_CSV = os.path.join(
_DATA_DIR, "gca_gca_pairs_seed20251104_N100_with_baseline.csv"
)
_GCA_CONSTLAT_CSV = os.path.join(
_DATA_DIR, "gca_constlat_cases_with_baseline.csv"
)


def _sigexp(sig, exp):
return math.ldexp(int(sig), int(exp))


def _parse_vec3(fields, start):
return np.array(
[
_sigexp(fields[start], fields[start + 1]),
_sigexp(fields[start + 2], fields[start + 3]),
_sigexp(fields[start + 4], fields[start + 5]),
]
)


def _parse_scalar(fields, start):
return _sigexp(fields[start], fields[start + 1])


# ── GCA-GCA intersection ──────────────────────────────────────────────────────


def _load_gca_gca():
rows = []
with open(_GCA_GCA_CSV) as f:
next(f)
for line in f:
line = line.strip()
if not line:
continue
fields = line.split(",")
assert len(fields) == 32
pair_id = int(fields[0])
a0 = _parse_vec3(fields, 2)
a1 = _parse_vec3(fields, 8)
b0 = _parse_vec3(fields, 14)
b1 = _parse_vec3(fields, 20)
baseline = _parse_vec3(fields, 26)
rows.append((pair_id, a0, a1, b0, b1, baseline))
return rows


@pytest.fixture(scope="module")
def gca_gca_rows():
return _load_gca_gca()


def test_gca_gca_row_count(gca_gca_rows):
assert len(gca_gca_rows) == 31


@pytest.mark.parametrize("idx", range(31))
def test_gca_gca_intersection_baseline(gca_gca_rows, idx):
pair_id, a0, a1, b0, b1, baseline = gca_gca_rows[idx]
result = gca_gca_intersection(np.stack([a0, a1]), np.stack([b0, b1]))
assert result.shape[0] >= 1, f"pair_id={pair_id}: expected intersection, got none"
err = float(np.linalg.norm(result[0] - baseline))
assert err < 1e-15, f"pair_id={pair_id}: err={err:.3e} ≥ 1e-15"


# ── GCA-const-lat intersection ────────────────────────────────────────────────


def _load_gca_constlat():
rows = []
with open(_GCA_CONSTLAT_CSV) as f:
next(f)
for line in f:
line = line.strip()
if not line:
continue
fields = line.split(",")
assert len(fields) == 19
case_id = int(fields[0])
a0 = _parse_vec3(fields, 1)
a1 = _parse_vec3(fields, 7)
z0 = _parse_scalar(fields, 13)
bx = _parse_scalar(fields, 15)
by = _parse_scalar(fields, 17)
rows.append((case_id, a0, a1, z0, bx, by))
return rows


@pytest.fixture(scope="module")
def gca_constlat_rows():
return _load_gca_constlat()


def test_gca_constlat_row_count(gca_constlat_rows):
assert len(gca_constlat_rows) == 200


@pytest.mark.parametrize("idx", range(200))
def test_gca_constlat_intersection_baseline(gca_constlat_rows, idx):
case_id, a0, a1, z0, bx, by = gca_constlat_rows[idx]
result = gca_const_lat_intersection(np.stack([a0, a1]), z0)
assert not np.all(np.isnan(result[0])), f"case_id={case_id}: no intersection returned"
dx = result[0, 0] - bx
dy = result[0, 1] - by
err = math.sqrt(dx * dx + dy * dy)
assert err < 5e-15, f"case_id={case_id}: err_xy={err:.3e} ≥ 5e-15"


# ── Point-in-polygon: simple spherical triangle ───────────────────────────────
# From test_pip_robust.cpp: triangle A=(1,0,0) B=(0,1,0) C=(0,0,1)

_SIMPLE_POLY = np.array(
[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], dtype=np.float64
)


def test_pip_simple_on_vertex():
q = np.array([1.0, 0.0, 0.0])
assert _point_in_polygon_sphere(q, _SIMPLE_POLY) == _LOC_ON_VERTEX


def test_pip_simple_on_edge():
# Normalize([1,1,0]) — midpoint of edge AB
q = np.array([0.70710678118654752, 0.70710678118654752, 0.0])
assert _point_in_polygon_sphere(q, _SIMPLE_POLY) == _LOC_ON_EDGE


def test_pip_simple_inside():
q = np.array([1.0, 1.0, 1.0])
q = q / np.linalg.norm(q)
assert _point_in_polygon_sphere(q, _SIMPLE_POLY) == _LOC_INSIDE


# ── Point-in-polygon: complicated 12-vertex polygon ──────────────────────────
# From test_pip_complicated.cpp (Tier 4 / no-global-id overload)

_COMPLICATED_POLY = np.array(
[
[0.77114888623389370, -0.15726142646764130, 0.61692644537707060],
[0.45249789144681710, -0.75061357063415830, 0.48148200985709080],
[0.68946150885186746, -0.59933974587969335, 0.40673664307580021],
[0.53398361424012150, -0.82144802877974800, 0.20021147753544170],
[0.72547341102583852, -0.63064441484306173, 0.27563735581699919],
[0.90662646752004000, -0.37288916572560260, 0.19743889808393390],
[0.74736479846796566, -0.64967430761889954, 0.13917310096006544],
[0.75468084319451650, -0.65603404827296060, -0.00872653549837396],
[0.49138625363591330, -0.85368085756667700, -0.17253562867386300],
[0.86555356123625300, -0.23932615843504300, -0.43993183849315200],
[0.73819995144420940, -0.26096774566031860, -0.62205841157622660],
[0.60166139617200880, -0.05234812405382043, -0.79703402578835670],
],
dtype=np.float64,
)

_PIP_CASES = [
([0.75367527697268680, -0.65515992289232780, -0.05233595624294383], _LOC_INSIDE, "Q1 inside"),
([0.92054211727315200, -0.38498585550407840, 0.06624274592780397], _LOC_INSIDE, "Q2 inside"),
([0.53882393432914170, -0.82565565483991800, 0.16721694718218960], _LOC_OUTSIDE, "Q3 outside"),
([0.63494819288856630, -0.65761549896072850, 0.40544130015845230], _LOC_OUTSIDE, "Q4 outside"),
# Q5 is exactly vertex P8 (0-indexed)
([0.49138625363591330, -0.85368085756667700, -0.17253562867386300], _LOC_ON_VERTEX, "Q5 on vertex"),
]


@pytest.mark.parametrize("q_xyz,expected,name", _PIP_CASES)
def test_pip_complicated(q_xyz, expected, name):
q = np.array(q_xyz, dtype=np.float64)
result = _point_in_polygon_sphere(q, _COMPLICATED_POLY)
assert result == expected, f"{name}: expected {expected}, got {result}"
Loading
Loading