Skip to content
Merged
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
8 changes: 4 additions & 4 deletions benchmarks/geometry_kernels.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,16 +159,16 @@ def setup(self):

self._accux_gca = _accux_gca
self._try_gca_gca_intersection = _try_gca_gca_intersection
_accux_gca(*_W0, *_W1, *_V0, *_V1)
_try_gca_gca_intersection(*_W0, *_W1, *_V0, *_V1)
_accux_gca(_W0, _W1, _V0, _V1)
_try_gca_gca_intersection(_W0, _W1, _V0, _V1)

def time_accux_gca_kernel(self):
"""Layer 1: pure numerical kernel."""
self._accux_gca(*_W0, *_W1, *_V0, *_V1)
self._accux_gca(_W0, _W1, _V0, _V1)

def time_try_gca_gca_intersection(self):
"""Layer 2: batch/status layer."""
self._try_gca_gca_intersection(*_W0, *_W1, *_V0, *_V1)
self._try_gca_gca_intersection(_W0, _W1, _V0, _V1)

def time_gca_gca_intersection(self):
"""Layer 3: dispatcher (full public API)."""
Expand Down
184 changes: 75 additions & 109 deletions benchmarks/geometry_samebody.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,29 @@
import numpy as np
from numba import njit

from uxarray.grid.arcs import _on_minor_arc_xyz, on_minor_arc
from uxarray.constants import ERROR_TOLERANCE
from uxarray.grid.arcs import on_minor_arc
from uxarray.grid.intersections import (
_accux_constlat_scalar,
_snap_const_lat_endpoint_xy,
_accux_constlat,
_snap_const_lat_endpoint,
gca_const_lat_intersection,
)

# ---------------------------------------------------------------------------
# L1 (FP64 body) — direct double-precision kernel, verbatim from
# fp64_GCAconstLat.hh. Scalar in / scalar out so Numba keeps it in registers,
# mirroring _accux_constlat_scalar.
# ---------------------------------------------------------------------------
from uxarray.utils.numba_math import (
_numba_add3,
_numba_allfinite3,
_numba_mul3_scalar,
)


@njit(cache=True)
def _fp64_constlat_scalar(a0, a1, a2, b0, b1, b2, const_z):
@njit(cache=True, inline="always", error_model="numpy")
def _fp64_constlat(a, b, const_z):
"""
L1 (FP64 body) — direct double-precision kernel, compare to
fp64_GCAconstLat.hh.
Analogous to _accux_constlat, but uses FP64 math instead of AccuX.
"""
a0, a1, a2 = a
b0, b1, b2 = b
nx = a1 * b2 - a2 * b1
ny = a2 * b0 - a0 * b2
nz = a0 * b1 - a1 * b0
Expand All @@ -82,114 +89,79 @@ def _fp64_constlat_scalar(a0, a1, a2, b0, b1, b2, const_z):
py = -(const_z * ny * nz + s * nx) * inv_denom
nxo = -(const_z * nx * nz + s * ny) * inv_denom
nyo = -(const_z * ny * nz - s * nx) * inv_denom
return px, py, nxo, nyo


@njit(cache=True, inline="always")
def _fp64_constlat(x1, x2, const_z):
px, py, nxo, nyo = _fp64_constlat_scalar(
x1[0], x1[1], x1[2], x2[0], x2[1], x2[2], const_z
)
pos = np.empty(3)
pos[0] = px
pos[1] = py
pos[2] = const_z
neg = np.empty(3)
neg[0] = nxo
neg[1] = nyo
neg[2] = const_z
return pos, neg


# ---------------------------------------------------------------------------
# L2 (FP64 body) — identical logic to _try_gca_const_lat_intersection, only the
# L1 call differs. Branchless integer masks; status codes 0/1/2 as in AccuSphGeom.
# ---------------------------------------------------------------------------
return (px, py, const_z), (nxo, nyo, const_z)


@njit(cache=True)
@njit(cache=True, error_model="numpy")
def _fp64_try_gca_const_lat_intersection(gca_cart, const_z):
"""
L2 (FP64 body) — identical logic to _try_gca_const_lat_intersection, only the
L1 call differs. Branchless integer masks; status codes 0/1/2 as in AccuSphGeom.
"""
x1 = gca_cart[0]
x2 = gca_cart[1]
pos, neg = _fp64_constlat(x1, x2, const_z)

pos_fin = int(math.isfinite(pos[0]) and math.isfinite(pos[1]))
neg_fin = int(math.isfinite(neg[0]) and math.isfinite(neg[1]))
pos_on = pos_fin * int(on_minor_arc(pos, x1, x2)) if pos_fin else 0
neg_on = neg_fin * int(on_minor_arc(neg, x1, x2)) if neg_fin else 0
pos_fin = int(math.isfinite(pos[0])) * int(math.isfinite(pos[1]))
neg_fin = int(math.isfinite(neg[0])) * int(math.isfinite(neg[1]))
pos_on = pos_fin * on_minor_arc(pos, x1, x2)
neg_on = neg_fin * on_minor_arc(neg, x1, x2)

pos_valid = pos_fin * pos_on
neg_valid = neg_fin * neg_on

pos_mask = pos_valid * (1 - neg_valid)
neg_mask = neg_valid * (1 - pos_valid)

point = np.empty(3)
point[0] = pos_mask * pos[0] + neg_mask * neg[0]
point[1] = pos_mask * pos[1] + neg_mask * neg[1]
point[2] = pos_mask * pos[2] + neg_mask * neg[2]
point = _numba_add3(
_numba_mul3_scalar(pos, pos_mask), _numba_mul3_scalar(neg, neg_mask)
)

both = pos_valid * neg_valid
none = (1 - pos_valid) * (1 - neg_valid)
status = both + none * 2
return point, status, pos, neg


# ---------------------------------------------------------------------------
# L3 (FP64 body) — identical dispatcher to gca_const_lat_intersection, reusing
# the production _snap_const_lat_endpoint so only the numerical body differs.
# ---------------------------------------------------------------------------


@njit(cache=True)
@njit(cache=True, error_model="numpy")
def _fp64_gca_const_lat_intersection(gca_cart, const_z):
# Mirrors the production scalar dispatcher exactly (same allocation profile:
# one (2, 3) array), only the L1 body differs. This keeps the same-body
# comparison honest: any timing gap is the EFT math, not plumbing.
res = np.empty((2, 3))
res.fill(np.nan)

a0 = gca_cart[0, 0]
a1 = gca_cart[0, 1]
a2 = gca_cart[0, 2]
b0 = gca_cart[1, 0]
b1 = gca_cart[1, 1]
b2 = gca_cart[1, 2]

px, py, nx, ny = _fp64_constlat_scalar(a0, a1, a2, b0, b1, b2, const_z)

pos_fin = math.isfinite(px) and math.isfinite(py)
neg_fin = math.isfinite(nx) and math.isfinite(ny)
pos_valid = pos_fin and _on_minor_arc_xyz(px, py, const_z, a0, a1, a2, b0, b1, b2)
neg_valid = neg_fin and _on_minor_arc_xyz(nx, ny, const_z, a0, a1, a2, b0, b1, b2)

if pos_valid and not neg_valid:
sx, sy = _snap_const_lat_endpoint_xy(px, py, a0, a1, a2, b0, b1, b2, const_z)
res[0, 0] = sx
res[0, 1] = sy
res[0, 2] = const_z
elif neg_valid and not pos_valid:
sx, sy = _snap_const_lat_endpoint_xy(nx, ny, a0, a1, a2, b0, b1, b2, const_z)
res[0, 0] = sx
res[0, 1] = sy
res[0, 2] = const_z
"""
L3 (FP64 body) — identical dispatcher to gca_const_lat_intersection, reusing
the production _snap_const_lat_endpoint so only the numerical body differs;
the only difference is using _fp64_constlat instead of _accux_constlat.
"""
a = gca_cart[0]
b = gca_cart[1]

pos, neg = _fp64_constlat(a, b, const_z)

pos_fin = int(math.isfinite(pos[0])) * int(math.isfinite(pos[1]))
neg_fin = int(math.isfinite(neg[0])) * int(math.isfinite(neg[1]))
pos_valid = pos_fin * on_minor_arc(pos, a, b)
neg_valid = neg_fin * on_minor_arc(neg, a, b)

if pos_valid ^ neg_valid:
# exactly 1 valid intersection point
if pos_valid:
point_snapped = _snap_const_lat_endpoint(pos, a, b, const_z)
else:
point_snapped = _snap_const_lat_endpoint(neg, a, b, const_z)
result = (point_snapped, (np.nan, np.nan, np.nan))
elif pos_valid and neg_valid:
psx, psy = _snap_const_lat_endpoint_xy(px, py, a0, a1, a2, b0, b1, b2, const_z)
nsx, nsy = _snap_const_lat_endpoint_xy(nx, ny, a0, a1, a2, b0, b1, b2, const_z)
dx = psx - nsx
dy = psy - nsy
# probably 2 valid intersection points
pos_snapped = _snap_const_lat_endpoint(pos, a, b, const_z)
neg_snapped = _snap_const_lat_endpoint(neg, a, b, const_z)
dx = pos_snapped[0] - neg_snapped[0]
dy = pos_snapped[1] - neg_snapped[1]
if dx * dx + dy * dy < 1e-14:
res[0, 0] = psx
res[0, 1] = psy
res[0, 2] = const_z
# (actually, they are the same point! --> Only 1 valid point.)
result = (pos_snapped, (np.nan, np.nan, np.nan))
else:
res[0, 0] = psx
res[0, 1] = psy
res[0, 2] = const_z
res[1, 0] = nsx
res[1, 1] = nsy
res[1, 2] = const_z
return res
result = (pos_snapped, neg_snapped)
else:
# 0 valid intersection points
result = ((np.nan, np.nan, np.nan), (np.nan, np.nan, np.nan))
return result


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -241,10 +213,8 @@ def _batch_accux_kernel(A, B, Z):
"""Real AccuX L1 (EFT) kernel over a batch; accumulate to defeat DCE."""
acc = 0.0
for i in range(A.shape[0]):
px, py, nxo, nyo = _accux_constlat_scalar(
A[i, 0], A[i, 1], A[i, 2], B[i, 0], B[i, 1], B[i, 2], Z[i]
)
acc += px + py + nxo + nyo
pos, neg = _accux_constlat(A[i], B[i], Z[i])
acc += pos[0] + pos[1] + neg[0] + neg[1]
return acc


Expand All @@ -253,10 +223,8 @@ def _batch_fp64_kernel(A, B, Z):
"""Same-body FP64 L1 kernel over a batch; accumulate to defeat DCE."""
acc = 0.0
for i in range(A.shape[0]):
px, py, nxo, nyo = _fp64_constlat_scalar(
A[i, 0], A[i, 1], A[i, 2], B[i, 0], B[i, 1], B[i, 2], Z[i]
)
acc += px + py + nxo + nyo
pos, neg = _fp64_constlat(A[i], B[i], Z[i])
acc += pos[0] + pos[1] + neg[0] + neg[1]
return acc


Expand All @@ -266,7 +234,7 @@ def _batch_accux_dispatch(gcas, Z):
acc = 0.0
for i in range(gcas.shape[0]):
res = gca_const_lat_intersection(gcas[i], Z[i])
v = res[0, 0]
v = res[0][0]
if v == v: # not NaN
acc += v
return acc
Expand All @@ -278,7 +246,7 @@ def _batch_fp64_dispatch(gcas, Z):
acc = 0.0
for i in range(gcas.shape[0]):
res = _fp64_gca_const_lat_intersection(gcas[i], Z[i])
v = res[0, 0]
v = res[0][0]
if v == v: # not NaN
acc += v
return acc
Expand Down Expand Up @@ -319,15 +287,13 @@ def main():
for gca, z in base_cases:
fp64_res = _fp64_gca_const_lat_intersection(gca, z)
accux_res = gca_const_lat_intersection(gca, z)
fp64_rows = int(np.isfinite(fp64_res[0, 0])) + int(np.isfinite(fp64_res[1, 0]))
accux_rows = int(np.isfinite(accux_res[0, 0])) + int(
np.isfinite(accux_res[1, 0])
)
fp64_rows = int(np.isfinite(fp64_res[0][0])) + int(np.isfinite(fp64_res[1][0]))
accux_rows = int(np.isfinite(accux_res[0][0])) + int(np.isfinite(accux_res[1][0]))
if fp64_rows != accux_rows:
status_mismatch += 1
if fp64_rows > 0 and accux_rows > 0:
n_with_result += 1
d = np.nanmax(np.abs(fp64_res - accux_res))
d = np.nanmax(np.abs(np.asarray(fp64_res) - np.asarray(accux_res)))
if np.isfinite(d):
max_out_diff = max(max_out_diff, d)

Expand Down
Loading
Loading