Skip to content

Commit 7309e50

Browse files
authored
Optimize Point in Face Computation (#1249)
* initial refactor * optimize point in face * use angle function * optimize logic * update comments * remove debugging * update max face radius * add examples and clean up logic * add examples and clean up logic * clean up point util * add example * update typing * better docstring
1 parent ac525d6 commit 7309e50

File tree

7 files changed

+541
-375
lines changed

7 files changed

+541
-375
lines changed

test/test_geometry.py

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
from uxarray.grid.coordinates import _populate_node_latlon, _lonlat_rad_to_xyz, _normalize_xyz, _xyz_to_lonlat_rad, \
1212
_xyz_to_lonlat_deg, _xyz_to_lonlat_rad_scalar
1313
from uxarray.grid.arcs import extreme_gca_latitude, extreme_gca_z
14-
from uxarray.grid.utils import _get_cartesian_face_edge_nodes_array, _get_lonlat_rad_face_edge_nodes_array
14+
from uxarray.grid.utils import _get_cartesian_face_edge_nodes_array, _get_lonlat_rad_face_edge_nodes_array, _get_cartesian_face_edge_nodes
1515

1616
from uxarray.grid.geometry import _pole_point_inside_polygon_cartesian, \
17-
stereographic_projection, inverse_stereographic_projection, point_in_face, haversine_distance
17+
stereographic_projection, inverse_stereographic_projection, haversine_distance
18+
19+
from uxarray.grid.point_in_face import _face_contains_point
1820

1921

2022
from uxarray.grid.bounds import _populate_face_bounds, _construct_face_bounds_array, _construct_face_bounds
@@ -1183,24 +1185,19 @@ def test_point_inside():
11831185

11841186
# Open grid
11851187
grid = ux.open_grid(grid_mpas_2)
1188+
grid.normalize_cartesian_coordinates()
11861189

1187-
# Get the face edges of all faces in the grid
1188-
faces_edges_cartesian = _get_cartesian_face_edge_nodes_array(
1189-
grid.face_node_connectivity.values,
1190-
grid.n_face,
1191-
grid.n_max_face_edges,
1192-
grid.node_x.values,
1193-
grid.node_y.values,
1194-
grid.node_z.values,
1195-
)
11961190

11971191
# Loop through each face
11981192
for i in range(grid.n_face):
1193+
face_edges = _get_cartesian_face_edge_nodes(
1194+
i, grid.face_node_connectivity.values, grid.n_nodes_per_face.values, grid.node_x.values, grid.node_y.values, grid.node_z.values
1195+
)
1196+
11991197
# Set the point as the face center of the polygon
12001198
point_xyz = np.array([grid.face_x[i].values, grid.face_y[i].values, grid.face_z[i].values])
1201-
12021199
# Assert that the point is in the polygon
1203-
assert point_in_face(faces_edges_cartesian[i], point_xyz, inclusive=True)
1200+
assert _face_contains_point(face_edges, point_xyz)
12041201

12051202

12061203
def test_point_outside():
@@ -1223,7 +1220,7 @@ def test_point_outside():
12231220
point_xyz = np.array([grid.face_x[1].values, grid.face_y[1].values, grid.face_z[1].values])
12241221

12251222
# Assert that the point is not in the face tested
1226-
assert not point_in_face(faces_edges_cartesian[0], point_xyz, inclusive=True)
1223+
assert not _face_contains_point(faces_edges_cartesian[0], point_xyz)
12271224

12281225

12291226
def test_point_on_node():
@@ -1246,10 +1243,7 @@ def test_point_on_node():
12461243
point_xyz = np.array([*faces_edges_cartesian[0][0][0]])
12471244

12481245
# Assert that the point is in the face when inclusive is true
1249-
assert point_in_face(faces_edges_cartesian[0], point_xyz, inclusive=True)
1250-
1251-
# Assert that the point is not in the face when inclusive is false
1252-
assert not point_in_face(faces_edges_cartesian[0], point_xyz, inclusive=False)
1246+
assert _face_contains_point(faces_edges_cartesian[0], point_xyz)
12531247

12541248

12551249
def test_point_inside_close():
@@ -1274,7 +1268,7 @@ def test_point_inside_close():
12741268
)
12751269

12761270
# Use point in face to determine if the point is inside or out of the face
1277-
assert point_in_face(faces_edges_cartesian[0], point_xyz=point, inclusive=False)
1271+
assert _face_contains_point(faces_edges_cartesian[0], point)
12781272

12791273

12801274
def test_point_outside_close():
@@ -1299,7 +1293,7 @@ def test_point_outside_close():
12991293
)
13001294

13011295
# Use point in face to determine if the point is inside or out of the face
1302-
assert not point_in_face(faces_edges_cartesian[0], point_xyz=point, inclusive=False)
1296+
assert not _face_contains_point(faces_edges_cartesian[0], point)
13031297

13041298

13051299
def test_face_at_pole():
@@ -1322,7 +1316,7 @@ def test_face_at_pole():
13221316
grid.node_z.values,
13231317
)
13241318

1325-
assert point_in_face(faces_edges_cartesian[0], point_xyz=point, inclusive=True)
1319+
assert _face_contains_point(faces_edges_cartesian[0], point)
13261320

13271321

13281322
def test_face_at_antimeridian():
@@ -1344,7 +1338,7 @@ def test_face_at_antimeridian():
13441338
grid.node_z.values,
13451339
)
13461340

1347-
assert point_in_face(faces_edges_cartesian[0], point_xyz=point, inclusive=True)
1341+
assert _face_contains_point(faces_edges_cartesian[0], point)
13481342

13491343

13501344
def test_face_normal_face():
@@ -1367,7 +1361,7 @@ def test_face_normal_face():
13671361
grid.node_z.values,
13681362
)
13691363

1370-
assert point_in_face(faces_edges_cartesian[0], point_xyz=point, inclusive=True)
1364+
assert _face_contains_point(faces_edges_cartesian[0], point)
13711365

13721366

13731367
def test_stereographic_projection_stereographic_projection():

test/test_grid.py

Lines changed: 0 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -770,76 +770,10 @@ def test_normalize_existing_coordinates_norm_initial():
770770
assert _check_normalization(uxgrid)
771771

772772

773-
def test_number_of_faces_found():
774-
"""Test function for `self.get_face_containing_point`,
775-
to ensure the correct number of faces is found, depending on where the point is."""
776-
grid = ux.open_grid(gridfile_mpas)
777-
partial_grid = ux.open_grid(quad_hex_grid_path)
778773

779-
# For a face center only one face should be found
780-
point_xyz = np.array([grid.face_x[100].values, grid.face_y[100].values, grid.face_z[100].values], dtype=np.float64)
781774

782-
assert len(grid.get_faces_containing_point(point_xyz=point_xyz)) == 1
783775

784-
# For an edge two faces should be found
785-
point_xyz = np.array([grid.edge_x[100].values, grid.edge_y[100].values, grid.edge_z[100].values], dtype=np.float64)
786776

787-
assert len(grid.get_faces_containing_point(point_xyz=point_xyz)) == 2
788-
789-
# For a node three faces should be found
790-
point_xyz = np.array([grid.node_x[100].values, grid.node_y[100].values, grid.node_z[100].values], dtype=np.float64)
791-
792-
assert len(grid.get_faces_containing_point(point_xyz=point_xyz)) == 3
793-
794-
partial_grid.normalize_cartesian_coordinates()
795-
796-
# Test for a node on the edge where only 2 faces should be found
797-
point_xyz = np.array([partial_grid.node_x[1].values, partial_grid.node_y[1].values, partial_grid.node_z[1].values], dtype=np.float64)
798-
799-
assert len(partial_grid.get_faces_containing_point(point_xyz=point_xyz)) == 2
800-
801-
802-
def test_whole_grid():
803-
"""Tests `self.get_faces_containing_point`on an entire grid,
804-
checking that for each face center, one face is found to contain it"""
805-
806-
grid = ux.open_grid(gridfile_mpas_two)
807-
grid.normalize_cartesian_coordinates()
808-
809-
# Ensure a face is found on the grid for every face center
810-
for i in range(len(grid.face_x.values)):
811-
point_xyz = np.array([grid.face_x[i].values, grid.face_y[i].values, grid.face_z[i].values], dtype=np.float64)
812-
813-
assert len(grid.get_faces_containing_point(point_xyz=point_xyz)) == 1
814-
815-
def test_point_types():
816-
"""Tests that `self.get_faces_containing_point` works with cartesian and lonlat"""
817-
818-
# Open the grid
819-
grid = ux.open_grid(gridfile_mpas)
820-
821-
# Assign a cartesian point and a lon/lat point
822-
point_xyz = np.array([grid.node_x[100].values, grid.node_y[100].values, grid.node_z[100].values], dtype=np.float64)
823-
point_lonlat = np.array([grid.node_lon[100].values, grid.node_lat[100].values])
824-
825-
# Test both points find faces
826-
assert len(grid.get_faces_containing_point(point_xyz=point_xyz)) != 0
827-
assert len(grid.get_faces_containing_point(point_lonlat=point_lonlat)) !=0
828-
829-
def test_point_along_arc():
830-
node_lon = np.array([-40, -40, 40, 40])
831-
node_lat = np.array([-20, 20, 20, -20])
832-
face_node_connectivity = np.array([[0, 1, 2, 3]], dtype=np.int64)
833-
834-
uxgrid = ux.Grid.from_topology(node_lon, node_lat, face_node_connectivity)
835-
836-
# point at exactly 20 degrees latitude
837-
out1 = uxgrid.get_faces_containing_point(point_lonlat=np.array([0, 20], dtype=np.float64))
838-
839-
# point at 25.41 degrees latitude (max along the great circle arc)
840-
out2 = uxgrid.get_faces_containing_point(point_lonlat=np.array([0, 25.41], dtype=np.float64))
841-
842-
nt.assert_array_equal(out1, out2)
843777

844778
def test_from_topology():
845779
node_lon = np.array([-20.0, 0.0, 20.0, -20, -40])

test/test_point_in_face.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import os
2+
from pathlib import Path
3+
4+
import numpy as np
5+
import numpy.testing as nt
6+
import pytest
7+
8+
import uxarray as ux
9+
from uxarray.constants import INT_FILL_VALUE
10+
from .test_gradient import quad_hex_grid_path
11+
12+
current_path = Path(os.path.dirname(os.path.realpath(__file__)))
13+
gridfile_mpas = current_path / "meshfiles" / "mpas" / "QU" / "mesh.QU.1920km.151026.nc"
14+
15+
@pytest.fixture(params=["healpix", "quadhex"])
16+
def grid(request):
17+
if request.param == "healpix":
18+
g = ux.Grid.from_healpix(zoom=0)
19+
else:
20+
g = ux.open_grid(quad_hex_grid_path)
21+
g.normalize_cartesian_coordinates()
22+
return g
23+
24+
def test_face_centers(grid):
25+
"""
26+
For each face of the grid, verify that
27+
- querying its Cartesian center returns exactly that face
28+
- querying its lon/lat center returns exactly that face
29+
"""
30+
centers_xyz = np.vstack([
31+
grid.face_x.values,
32+
grid.face_y.values,
33+
grid.face_z.values,
34+
]).T
35+
36+
for fid, center in enumerate(centers_xyz):
37+
hits = grid.get_faces_containing_point(
38+
point_xyz=center,
39+
return_counts=False
40+
)
41+
assert isinstance(hits, list)
42+
assert len(hits) == 1
43+
assert hits[0] == [fid]
44+
45+
46+
centers_lonlat = np.vstack([
47+
grid.face_lon.values,
48+
grid.face_lat.values,
49+
]).T
50+
51+
for fid, (lon, lat) in enumerate(centers_lonlat):
52+
hits = grid.get_faces_containing_point(
53+
point_lonlat=(lon, lat),
54+
return_counts=False
55+
)
56+
assert hits[0] == [fid]
57+
58+
def test_node_corners(grid):
59+
"""
60+
For each corner node, verify that querying its coordinate in both
61+
Cartesian and spherical (lon/lat) returns exactly the faces sharing it.
62+
"""
63+
64+
print(grid.max_face_radius)
65+
66+
node_coords = np.vstack([
67+
grid.node_x.values,
68+
grid.node_y.values,
69+
grid.node_z.values,
70+
]).T
71+
72+
conn = grid.node_face_connectivity.values
73+
counts = np.sum(conn != INT_FILL_VALUE, axis=1)
74+
75+
# Cartesian tests
76+
for nid, (x, y, z) in enumerate(node_coords):
77+
expected = conn[nid, :counts[nid]].tolist()
78+
79+
hits_xyz = grid.get_faces_containing_point(
80+
point_xyz=(x, y, z),
81+
return_counts=False
82+
)[0]
83+
assert set(hits_xyz) == set(expected)
84+
assert len(hits_xyz) == len(expected)
85+
86+
87+
node_lonlat = np.vstack([
88+
grid.node_lon.values,
89+
grid.node_lat.values,
90+
]).T
91+
92+
# Spherical tests
93+
for nid, (lon, lat) in enumerate(node_lonlat):
94+
expected = conn[nid, :counts[nid]].tolist()
95+
96+
hits_ll = grid.get_faces_containing_point(
97+
point_lonlat=(lon, lat),
98+
return_counts=False
99+
)[0]
100+
assert set(hits_ll) == set(expected)
101+
assert len(hits_ll) == len(expected)
102+
103+
104+
def test_number_of_faces_found():
105+
"""Test function for `self.get_face_containing_point`,
106+
to ensure the correct number of faces is found, depending on where the point is."""
107+
grid = ux.open_grid(gridfile_mpas)
108+
partial_grid = ux.open_grid(quad_hex_grid_path)
109+
110+
# For a face center only one face should be found
111+
point_xyz = np.array([grid.face_x[100].values, grid.face_y[100].values, grid.face_z[100].values], dtype=np.float64)
112+
113+
assert len(grid.get_faces_containing_point(point_xyz=point_xyz, return_counts=False)[0]) == 1
114+
115+
# For an edge two faces should be found
116+
point_xyz = np.array([grid.edge_x[100].values, grid.edge_y[100].values, grid.edge_z[100].values], dtype=np.float64)
117+
118+
assert len(grid.get_faces_containing_point(point_xyz=point_xyz, return_counts=False)[0]) == 2
119+
120+
# For a node three faces should be found
121+
point_xyz = np.array([grid.node_x[100].values, grid.node_y[100].values, grid.node_z[100].values], dtype=np.float64)
122+
123+
assert len(grid.get_faces_containing_point(point_xyz=point_xyz, return_counts=False)[0]) == 3
124+
125+
partial_grid.normalize_cartesian_coordinates()
126+
127+
# Test for a node on the edge where only 2 faces should be found
128+
point_xyz = np.array([partial_grid.node_x[1].values, partial_grid.node_y[1].values, partial_grid.node_z[1].values], dtype=np.float64)
129+
130+
assert len(partial_grid.get_faces_containing_point(point_xyz=point_xyz, return_counts=False)[0]) == 2
131+
132+
def test_point_along_arc():
133+
node_lon = np.array([-40, -40, 40, 40])
134+
node_lat = np.array([-20, 20, 20, -20])
135+
face_node_connectivity = np.array([[0, 1, 2, 3]], dtype=np.int64)
136+
137+
uxgrid = ux.Grid.from_topology(node_lon, node_lat, face_node_connectivity)
138+
139+
# point at exactly 20 degrees latitude
140+
out1 = uxgrid.get_faces_containing_point(point_lonlat=np.array([0, 20], dtype=np.float64), return_counts=False)
141+
142+
# point at 25.41 degrees latitude (max along the great circle arc)
143+
out2 = uxgrid.get_faces_containing_point(point_lonlat=np.array([0, 25.41], dtype=np.float64), return_counts=False)
144+
145+
nt.assert_array_equal(out1[0], out2[0])

uxarray/grid/coordinates.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,3 +866,21 @@ def prepare_points(points, normalize):
866866
)
867867

868868
return np.vstack([x, y, z]).T
869+
870+
871+
def _prepare_points_for_kdtree(lonlat, xyz):
872+
if (lonlat is not None) and (xyz is not None):
873+
raise ValueError(
874+
"Both Cartesian (xyz) and Spherical (lonlat) coordinates were provided. One one can be "
875+
"provided at a time."
876+
)
877+
878+
# Convert to cartesian if points are spherical
879+
if xyz is None:
880+
lon, lat = map(np.deg2rad, lonlat)
881+
xyz = _lonlat_rad_to_xyz(lon, lat)
882+
pts = np.asarray(xyz, dtype=np.float64)
883+
if pts.ndim == 1:
884+
pts = pts[np.newaxis, :]
885+
886+
return pts

0 commit comments

Comments
 (0)