Skip to content

Commit 3f8929d

Browse files
Zonal mean helpers Fix (#955)
* Initial PR Open * Finish the change of `pt_within_gcr` * Finish the change of `_pole_point_inside_polygon` * Finish the change of `gca_constlat_intersection` * fix CI fail * fix facebounds fail * fix `extreme_gca` * pre-commit * Update asv.conf.json * Update asv.conf.json * Update uxarray/grid/intersections.py Co-authored-by: Philip Chmielowiec <[email protected]> * enable `JIT_CACHE` * replace using the numba decorate function * pre-commit * Update uxarray/grid/integrate.py * Update uxarray/grid/intersections.py * Update uxarray/grid/intersections.py * Update uxarray/grid/integrate.py * add cross to intersections.py * add isclose to integrate.py * fix all() failing with params * remove local benchmark * optimize xyz_to_latlon_rad_scalar * cleanup exception handling --------- Co-authored-by: Philip Chmielowiec <[email protected]>
1 parent c66286b commit 3f8929d

File tree

14 files changed

+1191
-246
lines changed

14 files changed

+1191
-246
lines changed

benchmarks/asv.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494
"setuptools_scm": [""],
9595
"xarray": [""],
9696
"netcdf4": [""],
97-
"pip+pyfma": [""]
97+
"pip+git+https://github.com/philipc2/pyfma.git": [""]
9898
},
9999

100100

benchmarks/face_bounds.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,13 @@ def time_face_bounds(self, grid_path):
2828
def peakmem_face_bounds(self, grid_path):
2929
"""Peak memory usage obtain ``Grid.face_bounds."""
3030
face_bounds = self.uxgrid.bounds
31+
32+
class Bounds:
33+
def setup(self):
34+
self.uxgrid = ux.open_grid(r"C:\Users\chmie\PycharmProjects\ncar-uxarray\uxarray-hongyu\benchmarks\oQU480.grid.nc")
35+
36+
def teardown(self):
37+
del self.uxgrid
38+
39+
def time_bounds(self):
40+
self.uxgrid.bounds

test/test_arcs.py

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,6 @@ class TestIntersectionPoint(TestCase):
3030

3131
def test_pt_within_gcr(self):
3232
# The GCR that's eexactly 180 degrees will have Value Error raised
33-
gcr_180degree_cart = [
34-
_lonlat_rad_to_xyz(0.0, 0.0),
35-
_lonlat_rad_to_xyz(np.pi, 0.0)
36-
]
37-
pt_same_lon_in = _lonlat_rad_to_xyz(0.0, 0.0)
38-
with self.assertRaises(ValueError):
39-
point_within_gca(np.asarray(pt_same_lon_in), np.asarray(gcr_180degree_cart))
4033

4134
gcr_180degree_cart = [
4235
_lonlat_rad_to_xyz(0.0, np.pi / 2.0),
@@ -45,27 +38,27 @@ def test_pt_within_gcr(self):
4538

4639
pt_same_lon_in = _lonlat_rad_to_xyz(0.0, 0.0)
4740
with self.assertRaises(ValueError):
48-
point_within_gca(np.asarray(pt_same_lon_in), np.asarray(gcr_180degree_cart))
41+
point_within_gca(pt_same_lon_in, gcr_180degree_cart)
4942

5043
# Test when the point and the GCA all have the same longitude
5144
gcr_same_lon_cart = [
5245
_lonlat_rad_to_xyz(0.0, 1.5),
5346
_lonlat_rad_to_xyz(0.0, -1.5)
5447
]
5548
pt_same_lon_in = _lonlat_rad_to_xyz(0.0, 0.0)
56-
self.assertTrue(point_within_gca(np.asarray(pt_same_lon_in), np.asarray(gcr_same_lon_cart)))
49+
self.assertTrue(point_within_gca(pt_same_lon_in, gcr_same_lon_cart))
5750

5851
pt_same_lon_out = _lonlat_rad_to_xyz(0.0, 1.500000000000001)
59-
res = point_within_gca(np.asarray(pt_same_lon_out), np.asarray(gcr_same_lon_cart))
52+
res = point_within_gca(pt_same_lon_out, gcr_same_lon_cart)
6053
self.assertFalse(res)
6154

6255
pt_same_lon_out_2 = _lonlat_rad_to_xyz(0.1, 1.0)
63-
res = point_within_gca(np.asarray(pt_same_lon_out_2), np.asarray(gcr_same_lon_cart))
56+
res = point_within_gca(pt_same_lon_out_2, gcr_same_lon_cart)
6457
self.assertFalse(res)
6558

6659
# And if we increase the digital place by one, it should be true again
6760
pt_same_lon_out_add_one_place = _lonlat_rad_to_xyz(0.0, 1.5000000000000001)
68-
res = point_within_gca(np.asarray(pt_same_lon_out_add_one_place), np.asarray(gcr_same_lon_cart))
61+
res = point_within_gca(pt_same_lon_out_add_one_place, gcr_same_lon_cart)
6962
self.assertTrue(res)
7063

7164
# Normal case
@@ -76,7 +69,7 @@ def test_pt_within_gcr(self):
7669
-0.997]])
7770
pt_cart_within = np.array(
7871
[0.25616109352676675, 0.9246590335292105, -0.010021496695000144])
79-
self.assertTrue(point_within_gca(np.asarray(pt_cart_within), np.asarray(gcr_cart_2), True))
72+
self.assertTrue(point_within_gca(pt_cart_within, gcr_cart_2, True))
8073

8174
# Test other more complicate cases : The anti-meridian case
8275

@@ -87,16 +80,16 @@ def test_pt_within_gcr_antimeridian(self):
8780
gcr_cart = np.array([[0.351, -0.724, 0.593], [0.617, 0.672, 0.410]])
8881
pt_cart = np.array(
8982
[0.9438777657502077, 0.1193199333436068, 0.922714737029319])
90-
self.assertTrue(point_within_gca(np.asarray(pt_cart), np.asarray(gcr_cart), is_directed=True))
83+
self.assertTrue(point_within_gca(pt_cart, gcr_cart, is_directed=True))
9184
# If we swap the gcr, it should throw a value error since it's larger than 180 degree
9285
gcr_cart_flip = np.array([[0.617, 0.672, 0.410], [0.351, -0.724,
9386
0.593]])
9487
with self.assertRaises(ValueError):
95-
point_within_gca(np.asarray(pt_cart), np.asarray(gcr_cart_flip), is_directed=True)
88+
point_within_gca(pt_cart, gcr_cart_flip, is_directed=True)
9689

9790
# If we flip the gcr in the undirected mode, it should still work
9891
self.assertTrue(
99-
point_within_gca(np.asarray(pt_cart), np.asarray(gcr_cart_flip), is_directed=False))
92+
point_within_gca(pt_cart, gcr_cart_flip, is_directed=False))
10093

10194
# 2nd anti-meridian case
10295
# GCR vertex0 in radian : [4.104711496596806, 0.5352983676533828],
@@ -107,9 +100,9 @@ def test_pt_within_gcr_antimeridian(self):
107100
pt_cart_within = np.array(
108101
[0.6136726305712109, 0.28442243941920053, -0.365605190899831])
109102
self.assertFalse(
110-
point_within_gca(np.asarray(pt_cart_within), np.asarray(gcr_cart_1), is_directed=True))
103+
point_within_gca(pt_cart_within, gcr_cart_1, is_directed=True))
111104
self.assertFalse(
112-
point_within_gca(np.asarray(pt_cart_within), np.asarray(gcr_cart_1), is_directed=False))
105+
point_within_gca(pt_cart_within, gcr_cart_1, is_directed=False))
113106

114107
# The first case should not work and the second should work
115108
v1_rad = [0.1, 0.0]
@@ -119,10 +112,10 @@ def test_pt_within_gcr_antimeridian(self):
119112
gcr_cart = np.array([v1_cart, v2_cart])
120113
pt_cart = _lonlat_rad_to_xyz(0.01, 0.0)
121114
with self.assertRaises(ValueError):
122-
point_within_gca(np.asarray(pt_cart), np.asarray(gcr_cart), is_directed=True)
115+
point_within_gca(pt_cart, gcr_cart, is_directed=True)
123116
gcr_car_flipped = np.array([v2_cart, v1_cart])
124117
self.assertTrue(
125-
point_within_gca(np.asarray(pt_cart), np.asarray(gcr_car_flipped), is_directed=True))
118+
point_within_gca(pt_cart, gcr_car_flipped, is_directed=True))
126119

127120
def test_pt_within_gcr_cross_pole(self):
128121
gcr_cart = np.array([[0.351, 0.0, 0.3], [-0.351, 0.0, 0.3]])

test/test_geometry.py

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@
2727
grid_files = [gridfile_CSne8, gridfile_geoflow]
2828
data_files = [datafile_CSne30, datafile_geoflow]
2929

30+
grid_quad_hex = current_path/ "meshfiles" / "ugrid" / "quad-hexagon" / "grid.nc"
31+
grid_geoflow = current_path/ "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc"
32+
grid_mpas = current_path/ "meshfiles" / "mpas" / "QU" / "oQU480.231010.nc"
33+
34+
35+
# List of grid files to test
36+
grid_files_latlonBound = [grid_quad_hex, grid_geoflow, gridfile_CSne8, grid_mpas]
3037

3138
class TestAntimeridian(TestCase):
3239

@@ -56,6 +63,8 @@ def test_linecollection_execution(self):
5663
lines = uxgrid.to_linecollection()
5764

5865

66+
67+
5968
class TestPredicate(TestCase):
6069

6170
def test_pole_point_inside_polygon_from_vertice_north(self):
@@ -366,6 +375,19 @@ def test_extreme_gca_latitude_max(self):
366375
expected_max_latitude,
367376
delta=ERROR_TOLERANCE)
368377

378+
def test_extreme_gca_latitude_max_short(self):
379+
# Define a great circle arc in 3D space that has a small span
380+
gca_cart = np.array( [[ 0.65465367, -0.37796447, -0.65465367], [ 0.6652466, -0.33896007, -0.6652466 ]])
381+
382+
# Calculate the maximum latitude
383+
max_latitude = ux.grid.arcs.extreme_gca_latitude(gca_cart, 'max')
384+
385+
# Check if the maximum latitude is correct
386+
expected_max_latitude = self._max_latitude_rad_iterative(gca_cart)
387+
self.assertAlmostEqual(max_latitude,
388+
expected_max_latitude,
389+
delta=ERROR_TOLERANCE)
390+
369391
def test_extreme_gca_latitude_min(self):
370392
# Define a great circle arc that is symmetrical around 0 degrees longitude
371393
gca_cart = np.array([
@@ -621,6 +643,103 @@ def test_populate_bounds_antimeridian(self):
621643
bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat)
622644
nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE)
623645

646+
def test_populate_bounds_equator(self):
647+
# the face is touching the equator
648+
face_edges_cart = np.array([
649+
[[0.99726469, -0.05226443, -0.05226443], [0.99862953, 0.0, -0.05233596]],
650+
[[0.99862953, 0.0, -0.05233596], [1.0, 0.0, 0.0]],
651+
[[1.0, 0.0, 0.0], [0.99862953, -0.05233596, 0.0]],
652+
[[0.99862953, -0.05233596, 0.0], [0.99726469, -0.05226443, -0.05226443]]
653+
]
654+
)
655+
# Apply the inverse transformation to get the lat lon coordinates
656+
face_edges_lonlat = np.array(
657+
[[_xyz_to_lonlat_rad(*edge[0]), _xyz_to_lonlat_rad(*edge[1])] for edge in face_edges_cart])
658+
659+
bounds = _populate_face_latlon_bound(face_edges_cart, face_edges_lonlat)
660+
expected_bounds = np.array([[-0.05235988, 0], [6.23082543, 0]])
661+
nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE)
662+
663+
def test_populate_bounds_southSphere(self):
664+
# The face is near the south pole but doesn't contains the pole
665+
face_edges_cart = np.array([
666+
[[-1.04386773e-01, -5.20500333e-02, -9.93173799e-01], [-1.04528463e-01, -1.28010448e-17, -9.94521895e-01]],
667+
[[-1.04528463e-01, -1.28010448e-17, -9.94521895e-01], [-5.23359562e-02, -6.40930613e-18, -9.98629535e-01]],
668+
[[-5.23359562e-02, -6.40930613e-18, -9.98629535e-01], [-5.22644277e-02, -5.22644277e-02, -9.97264689e-01]],
669+
[[-5.22644277e-02, -5.22644277e-02, -9.97264689e-01], [-1.04386773e-01, -5.20500333e-02, -9.93173799e-01]]
670+
])
671+
672+
# Apply the inverse transformation to get the lat lon coordinates
673+
face_edges_lonlat = np.array(
674+
[[_xyz_to_lonlat_rad(*edge[0]), _xyz_to_lonlat_rad(*edge[1])] for edge in face_edges_cart])
675+
676+
bounds = _populate_face_latlon_bound(face_edges_cart, face_edges_lonlat)
677+
expected_bounds = np.array([[-1.51843645, -1.45388627], [3.14159265, 3.92699082]])
678+
nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE)
679+
680+
def test_populate_bounds_near_pole(self):
681+
# The face is near the south pole but doesn't contains the pole
682+
face_edges_cart = np.array([
683+
[[3.58367950e-01, 0.00000000e+00, -9.33580426e-01], [3.57939780e-01, 4.88684203e-02, -9.32465008e-01]],
684+
[[3.57939780e-01, 4.88684203e-02, -9.32465008e-01], [4.06271283e-01, 4.78221112e-02, -9.12500241e-01]],
685+
[[4.06271283e-01, 4.78221112e-02, -9.12500241e-01], [4.06736643e-01, 2.01762691e-16, -9.13545458e-01]],
686+
[[4.06736643e-01, 2.01762691e-16, -9.13545458e-01], [3.58367950e-01, 0.00000000e+00, -9.33580426e-01]]
687+
])
688+
689+
# Apply the inverse transformation to get the lat lon coordinates
690+
face_edges_lonlat = np.array(
691+
[[_xyz_to_lonlat_rad(*edge[0]), _xyz_to_lonlat_rad(*edge[1])] for edge in face_edges_cart])
692+
693+
bounds = _populate_face_latlon_bound(face_edges_cart, face_edges_lonlat)
694+
expected_bounds = np.array([[-1.20427718, -1.14935491], [0,0.13568803]])
695+
nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE)
696+
697+
def test_populate_bounds_near_pole2(self):
698+
# The face is near the south pole but doesn't contains the pole
699+
face_edges_cart = np.array([
700+
[[3.57939780e-01, -4.88684203e-02, -9.32465008e-01], [3.58367950e-01, 0.00000000e+00, -9.33580426e-01]],
701+
[[3.58367950e-01, 0.00000000e+00, -9.33580426e-01], [4.06736643e-01, 2.01762691e-16, -9.13545458e-01]],
702+
[[4.06736643e-01, 2.01762691e-16, -9.13545458e-01], [4.06271283e-01, -4.78221112e-02, -9.12500241e-01]],
703+
[[4.06271283e-01, -4.78221112e-02, -9.12500241e-01], [3.57939780e-01, -4.88684203e-02, -9.32465008e-01]]
704+
])
705+
706+
707+
# Apply the inverse transformation to get the lat lon coordinates
708+
face_edges_lonlat = np.array(
709+
[[_xyz_to_lonlat_rad(*edge[0]), _xyz_to_lonlat_rad(*edge[1])] for edge in face_edges_cart])
710+
711+
bounds = _populate_face_latlon_bound(face_edges_cart, face_edges_lonlat)
712+
expected_bounds = np.array([[-1.20427718, -1.14935491], [6.147497,4.960524e-16]])
713+
nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE)
714+
715+
def test_populate_bounds_long_face(self):
716+
"""Test case where one of the face edges is a longitude GCA."""
717+
face_edges_cart = np.array([
718+
[[9.9999946355819702e-01, -6.7040475551038980e-04, 8.0396590055897832e-04],
719+
[9.9999439716339111e-01, -3.2541253603994846e-03, -8.0110825365409255e-04]],
720+
[[9.9999439716339111e-01, -3.2541253603994846e-03, -8.0110825365409255e-04],
721+
[9.9998968839645386e-01, -3.1763643492013216e-03, -3.2474612817168236e-03]],
722+
[[9.9998968839645386e-01, -3.1763643492013216e-03, -3.2474612817168236e-03],
723+
[9.9998861551284790e-01, -8.2993711112067103e-04, -4.7004125081002712e-03]],
724+
[[9.9998861551284790e-01, -8.2993711112067103e-04, -4.7004125081002712e-03],
725+
[9.9999368190765381e-01, 1.7522916896268725e-03, -3.0944822356104851e-03]],
726+
[[9.9999368190765381e-01, 1.7522916896268725e-03, -3.0944822356104851e-03],
727+
[9.9999833106994629e-01, 1.6786820488050580e-03, -6.4892979571595788e-04]],
728+
[[9.9999833106994629e-01, 1.6786820488050580e-03, -6.4892979571595788e-04],
729+
[9.9999946355819702e-01, -6.7040475551038980e-04, 8.0396590055897832e-04]]
730+
])
731+
732+
face_edges_lonlat = np.array(
733+
[[_xyz_to_lonlat_rad(*edge[0]), _xyz_to_lonlat_rad(*edge[1])] for edge in face_edges_cart])
734+
735+
bounds = _populate_face_latlon_bound(face_edges_cart, face_edges_lonlat)
736+
737+
# The expected bounds should not contains the south pole [0,-0.5*np.pi]
738+
self.assertTrue(bounds[1][0] != 0.0)
739+
740+
741+
742+
624743
def test_populate_bounds_node_on_pole(self):
625744
# Generate a normal face that is crossing the antimeridian
626745
vertices_lonlat = [[10.0, 90.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]]
@@ -1116,6 +1235,8 @@ def test_populate_bounds_normal(self):
11161235
is_GCA_list=[True, False, True, False])
11171236
nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE)
11181237

1238+
1239+
11191240
def test_populate_bounds_antimeridian(self):
11201241
# Generate a normal face that is crossing the antimeridian
11211242
vertices_lonlat = [[350, 60.0], [350, 10.0], [50.0, 10.0], [50.0, 60.0]]
@@ -1173,7 +1294,7 @@ def test_populate_bounds_node_on_pole(self):
11731294
nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE)
11741295

11751296
def test_populate_bounds_edge_over_pole(self):
1176-
# Generate a normal face that is crossing the antimeridian
1297+
# Generate a normal face that is around the north pole
11771298
vertices_lonlat = [[210.0, 80.0], [350.0, 60.0], [10.0, 60.0], [30.0, 80.0]]
11781299
vertices_lonlat = np.array(vertices_lonlat)
11791300

@@ -1288,6 +1409,31 @@ def test_populate_bounds_GCAList_mix(self):
12881409
nt.assert_allclose(face_bounds[i], expected_bounds[i], atol=ERROR_TOLERANCE)
12891410

12901411

1412+
# Test class
1413+
class TestLatlonBoundsFiles:
1414+
1415+
def test_face_bounds(self):
1416+
"""Test to ensure ``Grid.face_bounds`` works correctly for all grid
1417+
files."""
1418+
for grid_path in grid_files_latlonBound:
1419+
try:
1420+
# Open the grid file
1421+
self.uxgrid = ux.open_grid(grid_path)
1422+
1423+
# Test: Ensure the bounds are obtained
1424+
bounds = self.uxgrid.bounds
1425+
assert bounds is not None, f"Grid.face_bounds should not be None for {grid_path}"
1426+
1427+
except Exception as e:
1428+
# Print the failing grid file and re-raise the exception
1429+
print(f"Test failed for grid file: {grid_path}")
1430+
raise e
1431+
1432+
finally:
1433+
# Clean up the grid object
1434+
del self.uxgrid
1435+
1436+
12911437
class TestGeoDataFrame(TestCase):
12921438

12931439
def test_to_gdf(self):

0 commit comments

Comments
 (0)