Skip to content

Commit 79275c5

Browse files
Robust implementation of _point_within_gca_cartesian using only Cartesian coordinates (#1112)
* initial commit * initial implementation * initial testcase setup * required fix for ` _angle_of_2_vectors` * update ` _angle_of_2_vectors` to return degree in 0~360 * update `_pt_on_gca` * `test_populate_bounds_normal` failed when using njit * update _populate_face_latlon_bound * fix tests and add test case for case at pole * add test case exactly at pole * add condition to check if point is almost exactly equal to one of the end points * add general north and south pole tests * add tests for north and south pole * add extra test case for north and south pole * fix assert * add edge case test * Add error tolerance for values 0.0 * run pre-commit * remove debugging code and use njit --------- Co-authored-by: Hongyu Chen <hyvchen@ucdavis.edu>
1 parent e1d4893 commit 79275c5

File tree

10 files changed

+367
-426
lines changed

10 files changed

+367
-426
lines changed

test/test_arcs.py

Lines changed: 22 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import uxarray as ux
1111

1212
from uxarray.grid.coordinates import _lonlat_rad_to_xyz
13-
from uxarray.grid.arcs import point_within_gca, _point_within_gca_cartesian
13+
from uxarray.grid.arcs import point_within_gca
1414

1515
try:
1616
import constants
@@ -31,65 +31,46 @@ class TestIntersectionPoint(TestCase):
3131
def test_pt_within_gcr(self):
3232
# The GCR that's eexactly 180 degrees will have Value Error raised
3333

34-
gcr_180degree_cart = [
34+
gcr_180degree_cart = np.asarray([
3535
_lonlat_rad_to_xyz(0.0, np.pi / 2.0),
3636
_lonlat_rad_to_xyz(0.0, -np.pi / 2.0)
37-
]
37+
])
38+
pt_same_lon_in = np.asarray(_lonlat_rad_to_xyz(0.0, 0.0))
3839

39-
pt_same_lon_in = _lonlat_rad_to_xyz(0.0, 0.0)
4040
with self.assertRaises(ValueError):
41-
_point_within_gca_cartesian(pt_same_lon_in, gcr_180degree_cart)
42-
41+
point_within_gca(pt_same_lon_in, gcr_180degree_cart[0],gcr_180degree_cart[1] )
42+
#
4343
# Test when the point and the GCA all have the same longitude
44-
gcr_same_lon_cart = [
44+
gcr_same_lon_cart = np.asarray([
4545
_lonlat_rad_to_xyz(0.0, 1.5),
4646
_lonlat_rad_to_xyz(0.0, -1.5)
47-
]
48-
pt_same_lon_in = _lonlat_rad_to_xyz(0.0, 0.0)
49-
self.assertTrue(_point_within_gca_cartesian(pt_same_lon_in, gcr_same_lon_cart))
47+
])
48+
pt_same_lon_in = np.asarray(_lonlat_rad_to_xyz(0.0, 0.0))
49+
self.assertTrue(point_within_gca(pt_same_lon_in, gcr_same_lon_cart[0], gcr_same_lon_cart[1]))
5050

51-
pt_same_lon_out = _lonlat_rad_to_xyz(0.0, 1.500000000000001)
52-
res = _point_within_gca_cartesian(pt_same_lon_out, gcr_same_lon_cart)
51+
pt_same_lon_out = np.asarray(_lonlat_rad_to_xyz(0.0, 1.5000001))
52+
res = point_within_gca(pt_same_lon_out, gcr_same_lon_cart[0], gcr_same_lon_cart[1])
5353
self.assertFalse(res)
5454

55-
pt_same_lon_out_2 = _lonlat_rad_to_xyz(0.1, 1.0)
56-
res = _point_within_gca_cartesian(pt_same_lon_out_2, gcr_same_lon_cart)
55+
pt_same_lon_out_2 = np.asarray(_lonlat_rad_to_xyz(0.1, 1.0))
56+
res = point_within_gca(pt_same_lon_out_2, gcr_same_lon_cart[0], gcr_same_lon_cart[1])
5757
self.assertFalse(res)
5858

59-
# And if we increase the digital place by one, it should be true again
60-
pt_same_lon_out_add_one_place = _lonlat_rad_to_xyz(0.0, 1.5000000000000001)
61-
res = _point_within_gca_cartesian(pt_same_lon_out_add_one_place, gcr_same_lon_cart)
62-
self.assertTrue(res)
63-
64-
# Normal case
65-
# GCR vertex0 in radian : [1.3003315590159483, -0.007004587172323237],
66-
# GCR vertex1 in radian : [3.5997458123873827, -1.4893379576608758]
67-
# Point in radian : [1.3005410084914981, -0.010444274637648326]
68-
gcr_cart_2 = np.array([[0.267, 0.963, -0.007], [-0.073, -0.036,
69-
-0.997]])
70-
pt_cart_within = np.array(
71-
[0.25616109352676675, 0.9246590335292105, -0.010021496695000144])
72-
self.assertTrue(_point_within_gca_cartesian(pt_cart_within, gcr_cart_2, True))
73-
74-
# Test other more complicate cases : The anti-meridian case
75-
7659
def test_pt_within_gcr_antimeridian(self):
7760
# GCR vertex0 in radian : [5.163808182822441, 0.6351384888657234],
7861
# GCR vertex1 in radian : [0.8280410325693055, 0.42237025187091526]
7962
# Point in radian : [0.12574759138415173, 0.770098701904903]
8063
gcr_cart = np.array([[0.351, -0.724, 0.593], [0.617, 0.672, 0.410]])
8164
pt_cart = np.array(
8265
[0.9438777657502077, 0.1193199333436068, 0.922714737029319])
83-
self.assertTrue(_point_within_gca_cartesian(pt_cart, gcr_cart, is_directed=True))
84-
# If we swap the gcr, it should throw a value error since it's larger than 180 degree
66+
self.assertTrue(
67+
point_within_gca(pt_cart, gcr_cart[0], gcr_cart[1]))
68+
8569
gcr_cart_flip = np.array([[0.617, 0.672, 0.410], [0.351, -0.724,
8670
0.593]])
87-
with self.assertRaises(ValueError):
88-
_point_within_gca_cartesian(pt_cart, gcr_cart_flip, is_directed=True)
89-
9071
# If we flip the gcr in the undirected mode, it should still work
9172
self.assertTrue(
92-
_point_within_gca_cartesian(pt_cart, gcr_cart_flip, is_directed=False))
73+
point_within_gca(pt_cart, gcr_cart_flip[0], gcr_cart_flip[1]))
9374

9475
# 2nd anti-meridian case
9576
# GCR vertex0 in radian : [4.104711496596806, 0.5352983676533828],
@@ -100,22 +81,9 @@ def test_pt_within_gcr_antimeridian(self):
10081
pt_cart_within = np.array(
10182
[0.6136726305712109, 0.28442243941920053, -0.365605190899831])
10283
self.assertFalse(
103-
_point_within_gca_cartesian(pt_cart_within, gcr_cart_1, is_directed=True))
104-
self.assertFalse(
105-
_point_within_gca_cartesian(pt_cart_within, gcr_cart_1, is_directed=False))
106-
107-
# The first case should not work and the second should work
108-
v1_rad = [0.1, 0.0]
109-
v2_rad = [2 * np.pi - 0.1, 0.0]
110-
v1_cart = _lonlat_rad_to_xyz(v1_rad[0], v1_rad[1])
111-
v2_cart = _lonlat_rad_to_xyz(v2_rad[0], v1_rad[1])
112-
gcr_cart = np.array([v1_cart, v2_cart])
113-
pt_cart = _lonlat_rad_to_xyz(0.01, 0.0)
114-
with self.assertRaises(ValueError):
115-
_point_within_gca_cartesian(pt_cart, gcr_cart, is_directed=True)
116-
gcr_car_flipped = np.array([v2_cart, v1_cart])
117-
self.assertTrue(
118-
_point_within_gca_cartesian(pt_cart, gcr_car_flipped, is_directed=True))
84+
point_within_gca(pt_cart_within, gcr_cart_1[0], gcr_cart_1[1]))
85+
86+
11987

12088
def test_pt_within_gcr_cross_pole(self):
12189
gcr_cart = np.array([[0.351, 0.0, 0.3], [-0.351, 0.0, 0.3]])
@@ -125,15 +93,4 @@ def test_pt_within_gcr_cross_pole(self):
12593
# Normalize the point abd the GCA
12694
pt_cart = pt_cart / np.linalg.norm(pt_cart)
12795
gcr_cart = np.array([x / np.linalg.norm(x) for x in gcr_cart])
128-
self.assertTrue(_point_within_gca_cartesian(pt_cart, gcr_cart, is_directed=False))
129-
130-
gcr_cart = np.array([[0.351, 0.0, 0.3], [-0.351, 0.0, -0.6]])
131-
pt_cart = np.array(
132-
[0.10, 0.0, 0.8])
133-
134-
# When the point is not within the GCA
135-
pt_cart = pt_cart / np.linalg.norm(pt_cart)
136-
gcr_cart = np.array([x / np.linalg.norm(x) for x in gcr_cart])
137-
self.assertFalse(_point_within_gca_cartesian(pt_cart, gcr_cart, is_directed=False))
138-
with self.assertRaises(ValueError):
139-
_point_within_gca_cartesian(pt_cart, gcr_cart, is_directed=True)
96+
self.assertTrue(point_within_gca(pt_cart, gcr_cart[0], gcr_cart[1]))

test/test_helpers.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,31 @@ def test_angle_of_2_vectors(self):
238238
v2 = np.array([1.0, 0.0, 0.0])
239239
self.assertAlmostEqual(_angle_of_2_vectors(v1, v2), 0.0)
240240

241+
def test_angle_of_2_vectors_180_degree(self):
242+
GCR1_cart = np.array([
243+
_lonlat_rad_to_xyz(np.deg2rad(0.0),
244+
np.deg2rad(0.0)),
245+
_lonlat_rad_to_xyz(np.deg2rad(181.0),
246+
np.deg2rad(0.0))
247+
])
248+
249+
res = _angle_of_2_vectors( GCR1_cart[0], GCR1_cart[1])
250+
251+
# The angle between the two vectors should be 181 degree
252+
self.assertAlmostEqual(res, np.deg2rad(181.0), places=8)
253+
254+
GCR1_cart = np.array([
255+
_lonlat_rad_to_xyz(np.deg2rad(170.0),
256+
np.deg2rad(89.0)),
257+
_lonlat_rad_to_xyz(np.deg2rad(170.0),
258+
np.deg2rad(-10.0))
259+
])
260+
261+
res = _angle_of_2_vectors( GCR1_cart[0], GCR1_cart[1])
262+
263+
# The angle between the two vectors should be 181 degree
264+
self.assertAlmostEqual(res, np.deg2rad(89.0+10.0), places=8)
265+
241266

242267
class TestFaceEdgeConnectivityHelper(TestCase):
243268

test/test_integrate.py

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,9 @@ def test_get_faces_constLat_intersection_info_one_intersection(self):
8181
])
8282

8383
latitude_cart = -0.8660254037844386
84-
is_directed=False
8584
is_latlonface=False
8685
is_GCA_list=None
87-
unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface, is_directed)
86+
unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface)
8887
# The expected unique_intersections length is 1
8988
self.assertEqual(len(unique_intersections), 1)
9089

@@ -111,10 +110,10 @@ def test_get_faces_constLat_intersection_info_encompass_pole(self):
111110
latitude_rad = np.arcsin(latitude_cart)
112111
latitude_deg = np.rad2deg(latitude_rad)
113112
print(latitude_deg)
114-
is_directed=False
113+
115114
is_latlonface=False
116115
is_GCA_list=None
117-
unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface, is_directed)
116+
unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface)
118117
# The expected unique_intersections length should be no greater than 2* n_edges
119118
self.assertLessEqual(len(unique_intersections), 2*len(face_edges_cart))
120119

@@ -133,10 +132,9 @@ def test_get_faces_constLat_intersection_info_on_pole(self):
133132
[-5.2264427688714095e-02, -5.2264427688714102e-02, -9.9726468863423734e-01]]
134133
])
135134
latitude_cart = -0.9998476951563913
136-
is_directed=False
137135
is_latlonface=False
138136
is_GCA_list=None
139-
unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface, is_directed)
137+
unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface)
140138
# The expected unique_intersections length is 2
141139
self.assertEqual(len(unique_intersections), 2)
142140

@@ -156,10 +154,9 @@ def test_get_faces_constLat_intersection_info_near_pole(self):
156154
latitude_cart = -0.9876883405951378
157155
latitude_rad = np.arcsin(latitude_cart)
158156
latitude_deg = np.rad2deg(latitude_rad)
159-
is_directed=False
160157
is_latlonface=False
161158
is_GCA_list=None
162-
unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface, is_directed)
159+
unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface)
163160
# The expected unique_intersections length is 2
164161
self.assertEqual(len(unique_intersections), 1)
165162

@@ -182,10 +179,9 @@ def test_get_faces_constLat_intersection_info_2(self):
182179
[0.6546536707079771, -0.37796447300922714, -0.6546536707079772]]])
183180

184181
latitude_cart = -0.6560590289905073
185-
is_directed=False
186182
is_latlonface=False
187183
is_GCA_list=None
188-
unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface, is_directed)
184+
unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface)
189185
# The expected unique_intersections length is 2
190186
self.assertEqual(len(unique_intersections), 2)
191187

@@ -208,10 +204,9 @@ def test_get_faces_constLat_intersection_info_2(self):
208204
[0.6546536707079771, -0.37796447300922714, -0.6546536707079772]]])
209205

210206
latitude_cart = -0.6560590289905073
211-
is_directed=False
212207
is_latlonface=False
213208
is_GCA_list=None
214-
unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface, is_directed)
209+
unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface)
215210
# The expected unique_intersections length is 2
216211
self.assertEqual(len(unique_intersections), 2)
217212

@@ -240,8 +235,7 @@ def test_get_zonal_face_interval(self):
240235
# The latlon bounds for the latitude is not necessarily correct below since we don't use the latitudes bound anyway
241236
interval_df = _get_zonal_face_interval(face_edge_nodes, constZ,
242237
np.array([[-0.25 * np.pi, 0.25 * np.pi], [1.6 * np.pi,
243-
0.4 * np.pi]]),
244-
is_directed=False)
238+
0.4 * np.pi]]))
245239
expected_interval_df = pd.DataFrame({
246240
'start': [1.6 * np.pi, 0.0],
247241
'end': [2.0 * np.pi, 00.4 * np.pi]
@@ -285,7 +279,7 @@ def test_get_zonal_face_interval_empty_interval(self):
285279
[3.14159265, 3.2321175]
286280
])
287281

288-
res = _get_zonal_face_interval(face_edges_cart, latitude_cart, face_latlon_bounds, is_directed=False)
282+
res = _get_zonal_face_interval(face_edges_cart, latitude_cart, face_latlon_bounds)
289283
expected_res = pd.DataFrame({"start": [0.0], "end": [0.0]})
290284
pd.testing.assert_frame_equal(res, expected_res)
291285

@@ -322,7 +316,7 @@ def test_get_zonal_face_interval_encompass_pole(self):
322316
})
323317

324318
# Call the function to get the result
325-
res = _get_zonal_face_interval(face_edges_cart, latitude_cart, face_latlon_bounds, is_directed=False)
319+
res = _get_zonal_face_interval(face_edges_cart, latitude_cart, face_latlon_bounds)
326320

327321
# Assert the result matches the expected DataFrame
328322
pd.testing.assert_frame_equal(res, expected_df)
@@ -349,8 +343,7 @@ def test_get_zonal_face_interval_FILL_VALUE(self):
349343
# The latlon bounds for the latitude is not necessarily correct below since we don't use the latitudes bound anyway
350344
interval_df = _get_zonal_face_interval(face_edge_nodes, constZ,
351345
np.array([[-0.25 * np.pi, 0.25 * np.pi], [1.6 * np.pi,
352-
0.4 * np.pi]]),
353-
is_directed=False)
346+
0.4 * np.pi]]))
354347
expected_interval_df = pd.DataFrame({
355348
'start': [1.6 * np.pi, 0.0],
356349
'end': [2.0 * np.pi, 00.4 * np.pi]
@@ -383,7 +376,7 @@ def test_get_zonal_face_interval_GCA_constLat(self):
383376
interval_df = _get_zonal_face_interval(face_edge_nodes, constZ,
384377
np.array([[-0.25 * np.pi, 0.25 * np.pi], [1.6 * np.pi,
385378
0.4 * np.pi]]),
386-
is_directed=False, is_GCA_list=np.array([True, False, True, False]))
379+
is_GCA_list=np.array([True, False, True, False]))
387380
expected_interval_df = pd.DataFrame({
388381
'start': [1.6 * np.pi, 0.0],
389382
'end': [2.0 * np.pi, 00.4 * np.pi]
@@ -415,7 +408,7 @@ def test_get_zonal_face_interval_equator(self):
415408
interval_df = _get_zonal_face_interval(face_edge_nodes, 0.0,
416409
np.array([[-0.25 * np.pi, 0.25 * np.pi], [1.6 * np.pi,
417410
0.4 * np.pi]]),
418-
is_directed=False, is_GCA_list=np.array([True, True, True, True]))
411+
is_GCA_list=np.array([True, True, True, True]))
419412
expected_interval_df = pd.DataFrame({
420413
'start': [1.6 * np.pi, 0.0],
421414
'end': [2.0 * np.pi, 00.4 * np.pi]
@@ -434,7 +427,7 @@ def test_get_zonal_face_interval_equator(self):
434427
interval_df = _get_zonal_face_interval(face_edge_nodes, 0.0,
435428
np.array([[-0.25 * np.pi, 0.25 * np.pi], [1.6 * np.pi,
436429
0.4 * np.pi]]),
437-
is_directed=False, is_GCA_list=np.array([True, False, True, False]))
430+
is_GCA_list=np.array([True, False, True, False]))
438431
expected_interval_df = pd.DataFrame({
439432
'start': [1.6 * np.pi, 0.0],
440433
'end': [2.0 * np.pi, 00.4 * np.pi]
@@ -605,7 +598,7 @@ def test_get_zonal_faces_weight_at_constLat_equator(self):
605598
weight_df = _get_zonal_faces_weight_at_constLat(np.array([
606599
face_0_edge_nodes, face_1_edge_nodes, face_2_edge_nodes,
607600
face_3_edge_nodes
608-
]), 0.0, latlon_bounds, is_directed=False)
601+
]), 0.0, latlon_bounds)
609602

610603
nt.assert_array_almost_equal(weight_df, expected_weight_df, decimal=3)
611604

@@ -666,7 +659,7 @@ def test_get_zonal_faces_weight_at_constLat_regular(self):
666659
weight_df = _get_zonal_faces_weight_at_constLat(np.array([
667660
face_0_edge_nodes, face_1_edge_nodes, face_2_edge_nodes,
668661
face_3_edge_nodes
669-
]), np.sin(0.1 * np.pi), latlon_bounds, is_directed=False)
662+
]), np.sin(0.1 * np.pi), latlon_bounds)
670663

671664
nt.assert_array_almost_equal(weight_df, expected_weight_df, decimal=3)
672665

@@ -693,7 +686,7 @@ def test_get_zonal_faces_weight_at_constLat_on_pole_one_face(self):
693686
])
694687
constLat_cart = -1
695688

696-
weight_df = _get_zonal_faces_weight_at_constLat(face_edges_cart, constLat_cart, face_bounds, is_directed=False)
689+
weight_df = _get_zonal_faces_weight_at_constLat(face_edges_cart, constLat_cart, face_bounds)
697690
# Define the expected DataFrame
698691
expected_weight_df = pd.DataFrame({"face_index": [0], "weight": [1.0]})
699692

@@ -740,7 +733,7 @@ def test_get_zonal_faces_weight_at_constLat_on_pole_faces(self):
740733

741734
constLat_cart = 1.0
742735

743-
weight_df = _get_zonal_faces_weight_at_constLat(face_edges_cart, constLat_cart, face_bounds, is_directed=False)
736+
weight_df = _get_zonal_faces_weight_at_constLat(face_edges_cart, constLat_cart, face_bounds)
744737
# Define the expected DataFrame
745738
expected_weight_df = pd.DataFrame({
746739
'face_index': [0, 1, 2, 3],
@@ -774,7 +767,7 @@ def test_get_zonal_face_interval_pole(self):
774767
])
775768
constLat_cart = -0.9986295347545738
776769

777-
weight_df = _get_zonal_face_interval(face_edges_cart, constLat_cart, face_bounds, is_directed=False)
770+
weight_df = _get_zonal_face_interval(face_edges_cart, constLat_cart, face_bounds)
778771
# No Nan values should be present in the weight_df
779772
self.assertFalse(weight_df.isnull().values.any())
780773

@@ -827,7 +820,7 @@ def test_get_zonal_faces_weight_at_constLat_latlonface(self):
827820
# Assert the results is the same to the 3 decimal places
828821
weight_df = _get_zonal_faces_weight_at_constLat(np.array([
829822
face_0_edge_nodes, face_1_edge_nodes, face_2_edge_nodes
830-
]), np.sin(np.deg2rad(20)), latlon_bounds, is_directed=False, is_latlonface=True)
823+
]), np.sin(np.deg2rad(20)), latlon_bounds, is_latlonface=True)
831824

832825

833826
nt.assert_array_almost_equal(weight_df, expected_weight_df, decimal=3)
@@ -839,4 +832,4 @@ def test_get_zonal_faces_weight_at_constLat_latlonface(self):
839832
with self.assertRaises(ValueError):
840833
_get_zonal_faces_weight_at_constLat(np.array([
841834
face_0_edge_nodes, face_1_edge_nodes, face_2_edge_nodes
842-
]), np.deg2rad(20), latlon_bounds, is_directed=False)
835+
]), np.deg2rad(20), latlon_bounds)

0 commit comments

Comments
 (0)