Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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