diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc4c0d8a8..ad62ac418 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,7 +75,7 @@ jobs: - name: Upload code coverage to Codecov if: github.repository == 'UXARRAY/uxarray' - uses: codecov/codecov-action@v5.1.1 + uses: codecov/codecov-action@v5.1.2 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c8c1c5f00..05850625c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.8.3 + rev: v0.8.4 hooks: # Run the linter. - id: ruff diff --git a/benchmarks/mpas_ocean.py b/benchmarks/mpas_ocean.py index 53a67a1b6..ef2dc7692 100644 --- a/benchmarks/mpas_ocean.py +++ b/benchmarks/mpas_ocean.py @@ -138,7 +138,7 @@ def time_inverse_distance_weighted_remapping(self): class HoleEdgeIndices(DatasetBenchmark): def time_construct_hole_edge_indices(self, resolution): - ux.grid.geometry._construct_hole_edge_indices(self.uxds.uxgrid.edge_face_connectivity) + ux.grid.geometry._construct_boundary_edge_indices(self.uxds.uxgrid.edge_face_connectivity) class DualMesh(DatasetBenchmark): @@ -167,10 +167,19 @@ def time_check_norm(self, resolution): from uxarray.grid.validation import _check_normalization _check_normalization(self.uxgrid) +class CrossSection: + param_names = DatasetBenchmark.param_names + ['lat_step'] + params = DatasetBenchmark.params + [[1, 2, 4]] -class CrossSections(DatasetBenchmark): - param_names = DatasetBenchmark.param_names + ['n_lat'] - params = DatasetBenchmark.params + [[1, 2, 4, 8]] - def time_constant_lat_fast(self, resolution, n_lat): - for lat in np.linspace(-89, 89, n_lat): - self.uxds.uxgrid.constant_latitude_cross_section(lat, method='fast') + def setup(self, resolution, lat_step): + self.uxgrid = ux.open_grid(file_path_dict[resolution][0]) + self.uxgrid.normalize_cartesian_coordinates() + self.lats = np.arange(-45, 45, lat_step) + _ = self.uxgrid.bounds + + def teardown(self, resolution, lat_step): + del self.uxgrid + + def time_const_lat(self, resolution, lat_step): + for lat in self.lats: + self.uxgrid.cross_section.constant_latitude(lat) diff --git a/test/test_api.py b/test/test_api.py index 61679f1f7..4b8101d87 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -1,11 +1,9 @@ import os -from unittest import TestCase from pathlib import Path import numpy.testing as nt - import uxarray as ux -import xarray as xr import numpy as np +import pytest try: import constants @@ -14,126 +12,111 @@ current_path = Path(os.path.dirname(os.path.realpath(__file__))) +geoflow_data_path = current_path / "meshfiles" / "ugrid" / "geoflow-small" +gridfile_geoflow = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc" +geoflow_data_v1 = geoflow_data_path / "v1.nc" +geoflow_data_v2 = geoflow_data_path / "v2.nc" +geoflow_data_v3 = geoflow_data_path / "v3.nc" -class TestAPI(TestCase): - geoflow_data_path = current_path / "meshfiles" / "ugrid" / "geoflow-small" - gridfile_geoflow = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc" - geoflow_data_v1 = geoflow_data_path / "v1.nc" - geoflow_data_v2 = geoflow_data_path / "v2.nc" - geoflow_data_v3 = geoflow_data_path / "v3.nc" - - gridfile_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" - dsfile_var2_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30_var2.nc" - dsfiles_mf_ne30 = str( - current_path) + "/meshfiles/ugrid/outCSne30/outCSne30_*.nc" - - def test_open_geoflow_dataset(self): - """Loads a single dataset with its grid topology file using uxarray's - open_dataset call.""" +gridfile_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" +dsfile_var2_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30_var2.nc" +dsfiles_mf_ne30 = str(current_path) + "/meshfiles/ugrid/outCSne30/outCSne30_*.nc" - # Paths to Data Variable files - data_paths = [ - self.geoflow_data_v1, self.geoflow_data_v2, self.geoflow_data_v3 - ] +def test_open_geoflow_dataset(): + """Loads a single dataset with its grid topology file using uxarray's + open_dataset call.""" - uxds_v1 = ux.open_dataset(self.gridfile_geoflow, data_paths[0]) + # Paths to Data Variable files + data_paths = [ + geoflow_data_v1, geoflow_data_v2, geoflow_data_v3 + ] - # Ideally uxds_v1.uxgrid should NOT be None - with self.assertRaises(AssertionError): - nt.assert_equal(uxds_v1.uxgrid, None) + uxds_v1 = ux.open_dataset(gridfile_geoflow, data_paths[0]) - def test_open_dataset(self): - """Loads a single dataset with its grid topology file using uxarray's - open_dataset call.""" + # Ideally uxds_v1.uxgrid should NOT be None + nt.assert_equal(uxds_v1.uxgrid is not None, True) - uxds_var2_ne30 = ux.open_dataset(self.gridfile_ne30, - self.dsfile_var2_ne30) +def test_open_dataset(): + """Loads a single dataset with its grid topology file using uxarray's + open_dataset call.""" - nt.assert_equal(uxds_var2_ne30.uxgrid.node_lon.size, - constants.NNODES_outCSne30) - nt.assert_equal(len(uxds_var2_ne30.uxgrid._ds.data_vars), - constants.DATAVARS_outCSne30) - nt.assert_equal(uxds_var2_ne30.source_datasets, - str(self.dsfile_var2_ne30)) + uxds_var2_ne30 = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) - def test_open_mf_dataset(self): - """Loads multiple datasets with their grid topology file using - uxarray's open_dataset call.""" + nt.assert_equal(uxds_var2_ne30.uxgrid.node_lon.size, constants.NNODES_outCSne30) + nt.assert_equal(len(uxds_var2_ne30.uxgrid._ds.data_vars), constants.DATAVARS_outCSne30) + nt.assert_equal(uxds_var2_ne30.source_datasets, str(dsfile_var2_ne30)) - uxds_mf_ne30 = ux.open_mfdataset(self.gridfile_ne30, - self.dsfiles_mf_ne30) +def test_open_mf_dataset(): + """Loads multiple datasets with their grid topology file using + uxarray's open_dataset call.""" - nt.assert_equal(uxds_mf_ne30.uxgrid.node_lon.size, - constants.NNODES_outCSne30) - nt.assert_equal(len(uxds_mf_ne30.uxgrid._ds.data_vars), - constants.DATAVARS_outCSne30) + uxds_mf_ne30 = ux.open_mfdataset(gridfile_ne30, dsfiles_mf_ne30) - nt.assert_equal(uxds_mf_ne30.source_datasets, self.dsfiles_mf_ne30) + nt.assert_equal(uxds_mf_ne30.uxgrid.node_lon.size, constants.NNODES_outCSne30) + nt.assert_equal(len(uxds_mf_ne30.uxgrid._ds.data_vars), constants.DATAVARS_outCSne30) + nt.assert_equal(uxds_mf_ne30.source_datasets, dsfiles_mf_ne30) - def test_open_grid(self): - """Loads only a grid topology file using uxarray's open_grid call.""" - uxgrid = ux.open_grid(self.gridfile_geoflow) +def test_open_grid(): + """Loads only a grid topology file using uxarray's open_grid call.""" + uxgrid = ux.open_grid(gridfile_geoflow) - nt.assert_almost_equal(uxgrid.calculate_total_face_area(), - constants.MESH30_AREA, - decimal=3) + nt.assert_almost_equal(uxgrid.calculate_total_face_area(), constants.MESH30_AREA, decimal=3) - def test_copy_dataset(self): - """Loads a single dataset with its grid topology file using uxarray's - open_dataset call and make a copy of the object.""" +def test_copy_dataset(): + """Loads a single dataset with its grid topology file using uxarray's + open_dataset call and make a copy of the object.""" - uxds_var2_ne30 = ux.open_dataset(self.gridfile_ne30, - self.dsfile_var2_ne30) + uxds_var2_ne30 = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) - # make a shallow and deep copy of the dataset object - uxds_var2_ne30_copy_deep = uxds_var2_ne30.copy(deep=True) - uxds_var2_ne30_copy = uxds_var2_ne30.copy(deep=False) + # make a shallow and deep copy of the dataset object + uxds_var2_ne30_copy_deep = uxds_var2_ne30.copy(deep=True) + uxds_var2_ne30_copy = uxds_var2_ne30.copy(deep=False) - # Ideally uxds_var2_ne30_copy.uxgrid should NOT be None - with self.assertRaises(AssertionError): - nt.assert_equal(uxds_var2_ne30_copy.uxgrid, None) + # Ideally uxds_var2_ne30_copy.uxgrid should NOT be None + nt.assert_equal(uxds_var2_ne30_copy.uxgrid is not None, True) - # Check that the copy is a shallow copy - assert (uxds_var2_ne30_copy.uxgrid is uxds_var2_ne30.uxgrid) - assert (uxds_var2_ne30_copy.uxgrid == uxds_var2_ne30.uxgrid) + # Check that the copy is a shallow copy + assert uxds_var2_ne30_copy.uxgrid is uxds_var2_ne30.uxgrid + assert uxds_var2_ne30_copy.uxgrid == uxds_var2_ne30.uxgrid - # Check that the deep copy is a deep copy - assert (uxds_var2_ne30_copy_deep.uxgrid == uxds_var2_ne30.uxgrid) - assert (uxds_var2_ne30_copy_deep.uxgrid is not uxds_var2_ne30.uxgrid) + # Check that the deep copy is a deep copy + assert uxds_var2_ne30_copy_deep.uxgrid == uxds_var2_ne30.uxgrid + assert uxds_var2_ne30_copy_deep.uxgrid is not uxds_var2_ne30.uxgrid - def test_copy_dataarray(self): - """Loads an unstructured grid and data using uxarray's open_dataset - call and make a copy of the dataarray object.""" +def test_copy_dataarray(): + """Loads an unstructured grid and data using uxarray's open_dataset + call and make a copy of the dataarray object.""" - # Paths to Data Variable files - data_paths = [ - self.geoflow_data_v1, self.geoflow_data_v2, self.geoflow_data_v3 - ] + # Paths to Data Variable files + data_paths = [ + geoflow_data_v1, geoflow_data_v2, geoflow_data_v3 + ] - uxds_v1 = ux.open_dataset(self.gridfile_geoflow, data_paths[0]) + uxds_v1 = ux.open_dataset(gridfile_geoflow, data_paths[0]) - # get the uxdataarray object - v1_uxdata_array = uxds_v1['v1'] + # get the uxdataarray object + v1_uxdata_array = uxds_v1['v1'] - # make a shallow and deep copy of the dataarray object - v1_uxdata_array_copy_deep = v1_uxdata_array.copy(deep=True) - v1_uxdata_array_copy = v1_uxdata_array.copy(deep=False) + # make a shallow and deep copy of the dataarray object + v1_uxdata_array_copy_deep = v1_uxdata_array.copy(deep=True) + v1_uxdata_array_copy = v1_uxdata_array.copy(deep=False) - # Check that the copy is a shallow copy - assert (v1_uxdata_array_copy.uxgrid is v1_uxdata_array.uxgrid) - assert (v1_uxdata_array_copy.uxgrid == v1_uxdata_array.uxgrid) + # Check that the copy is a shallow copy + assert v1_uxdata_array_copy.uxgrid is v1_uxdata_array.uxgrid + assert v1_uxdata_array_copy.uxgrid == v1_uxdata_array.uxgrid - # Check that the deep copy is a deep copy - assert (v1_uxdata_array_copy_deep.uxgrid == v1_uxdata_array.uxgrid) - assert (v1_uxdata_array_copy_deep.uxgrid is not v1_uxdata_array.uxgrid) + # Check that the deep copy is a deep copy + assert v1_uxdata_array_copy_deep.uxgrid == v1_uxdata_array.uxgrid + assert v1_uxdata_array_copy_deep.uxgrid is not v1_uxdata_array.uxgrid - def test_open_dataset_grid_kwargs(self): - """Drops ``Mesh2_face_nodes`` from the inputted grid file using - ``grid_kwargs``""" +def test_open_dataset_grid_kwargs(): + """Drops ``Mesh2_face_nodes`` from the inputted grid file using + ``grid_kwargs``""" - with self.assertRaises(ValueError): - # attempt to open a dataset after dropping face nodes should raise a KeyError - uxds = ux.open_dataset( - self.gridfile_ne30, - self.dsfile_var2_ne30, - grid_kwargs={"drop_variables": "Mesh2_face_nodes"}) + with pytest.raises(ValueError): + # attempt to open a dataset after dropping face nodes should raise a KeyError + uxds = ux.open_dataset( + gridfile_ne30, + dsfile_var2_ne30, + grid_kwargs={"drop_variables": "Mesh2_face_nodes"} + ) diff --git a/test/test_arcs.py b/test/test_arcs.py index 4e9262982..8a587c8fe 100644 --- a/test/test_arcs.py +++ b/test/test_arcs.py @@ -1,14 +1,9 @@ import os import numpy as np import numpy.testing as nt -import random -import xarray as xr - -from unittest import TestCase from pathlib import Path - import uxarray as ux - +import pytest from uxarray.grid.coordinates import _lonlat_rad_to_xyz from uxarray.grid.arcs import point_within_gca @@ -25,72 +20,65 @@ gridfile_geoflowsmall_grid = current_path / 'meshfiles' / "ugrid" / "geoflow-small" / 'grid.nc' gridfile_geoflowsmall_var = current_path / 'meshfiles' / "ugrid" / "geoflow-small" / 'v1.nc' - -class TestIntersectionPoint(TestCase): - - def test_pt_within_gcr(self): - # The GCR that's eexactly 180 degrees will have Value Error raised - - gcr_180degree_cart = np.asarray([ - _lonlat_rad_to_xyz(0.0, np.pi / 2.0), - _lonlat_rad_to_xyz(0.0, -np.pi / 2.0) - ]) - pt_same_lon_in = np.asarray(_lonlat_rad_to_xyz(0.0, 0.0)) - - with self.assertRaises(ValueError): - point_within_gca(pt_same_lon_in, gcr_180degree_cart[0],gcr_180degree_cart[1] ) - # - # Test when the point and the GCA all have the same longitude - gcr_same_lon_cart = np.asarray([ - _lonlat_rad_to_xyz(0.0, 1.5), - _lonlat_rad_to_xyz(0.0, -1.5) - ]) - pt_same_lon_in = np.asarray(_lonlat_rad_to_xyz(0.0, 0.0)) - self.assertTrue(point_within_gca(pt_same_lon_in, gcr_same_lon_cart[0], gcr_same_lon_cart[1])) - - pt_same_lon_out = np.asarray(_lonlat_rad_to_xyz(0.0, 1.5000001)) - res = point_within_gca(pt_same_lon_out, gcr_same_lon_cart[0], gcr_same_lon_cart[1]) - self.assertFalse(res) - - pt_same_lon_out_2 = np.asarray(_lonlat_rad_to_xyz(0.1, 1.0)) - res = point_within_gca(pt_same_lon_out_2, gcr_same_lon_cart[0], gcr_same_lon_cart[1]) - self.assertFalse(res) - - def test_pt_within_gcr_antimeridian(self): - # GCR vertex0 in radian : [5.163808182822441, 0.6351384888657234], - # GCR vertex1 in radian : [0.8280410325693055, 0.42237025187091526] - # Point in radian : [0.12574759138415173, 0.770098701904903] - gcr_cart = np.array([[0.351, -0.724, 0.593], [0.617, 0.672, 0.410]]) - pt_cart = np.array( - [0.9438777657502077, 0.1193199333436068, 0.922714737029319]) - self.assertTrue( - point_within_gca(pt_cart, gcr_cart[0], gcr_cart[1])) - - gcr_cart_flip = np.array([[0.617, 0.672, 0.410], [0.351, -0.724, - 0.593]]) - # If we flip the gcr in the undirected mode, it should still work - self.assertTrue( - point_within_gca(pt_cart, gcr_cart_flip[0], gcr_cart_flip[1])) - - # 2nd anti-meridian case - # GCR vertex0 in radian : [4.104711496596806, 0.5352983676533828], - # GCR vertex1 in radian : [2.4269979227622533, -0.007003212877856825] - # Point in radian : [0.43400375562899113, -0.49554509841586936] - gcr_cart_1 = np.array([[-0.491, -0.706, 0.510], [-0.755, 0.655, - -0.007]]) - pt_cart_within = np.array( - [0.6136726305712109, 0.28442243941920053, -0.365605190899831]) - self.assertFalse( - point_within_gca(pt_cart_within, gcr_cart_1[0], gcr_cart_1[1])) - - - - def test_pt_within_gcr_cross_pole(self): - gcr_cart = np.array([[0.351, 0.0, 0.3], [-0.351, 0.0, 0.3]]) - pt_cart = np.array( - [0.10, 0.0, 0.8]) - - # Normalize the point abd the GCA - pt_cart = pt_cart / np.linalg.norm(pt_cart) - gcr_cart = np.array([x / np.linalg.norm(x) for x in gcr_cart]) - self.assertTrue(point_within_gca(pt_cart, gcr_cart[0], gcr_cart[1])) +def test_pt_within_gcr(): + # The GCR that's exactly 180 degrees will raise a ValueError + gcr_180degree_cart = np.asarray([ + _lonlat_rad_to_xyz(0.0, np.pi / 2.0), + _lonlat_rad_to_xyz(0.0, -np.pi / 2.0) + ]) + pt_same_lon_in = np.asarray(_lonlat_rad_to_xyz(0.0, 0.0)) + + with pytest.raises(ValueError): + point_within_gca(pt_same_lon_in, gcr_180degree_cart[0],gcr_180degree_cart[1] ) + # + # Test when the point and the GCA all have the same longitude + gcr_same_lon_cart = np.asarray([ + _lonlat_rad_to_xyz(0.0, 1.5), + _lonlat_rad_to_xyz(0.0, -1.5) + ]) + pt_same_lon_in = np.asarray(_lonlat_rad_to_xyz(0.0, 0.0)) + assert(point_within_gca(pt_same_lon_in, gcr_same_lon_cart[0], gcr_same_lon_cart[1])) + + pt_same_lon_out = np.asarray(_lonlat_rad_to_xyz(0.0, 1.5000001)) + res = point_within_gca(pt_same_lon_out, gcr_same_lon_cart[0], gcr_same_lon_cart[1]) + assert not res + + pt_same_lon_out_2 = np.asarray(_lonlat_rad_to_xyz(0.1, 1.0)) + res = point_within_gca(pt_same_lon_out_2, gcr_same_lon_cart[0], gcr_same_lon_cart[1]) + assert not res + +def test_pt_within_gcr_antimeridian(): + # GCR vertex0 in radian : [5.163808182822441, 0.6351384888657234], + # GCR vertex1 in radian : [0.8280410325693055, 0.42237025187091526] + # Point in radian : [0.12574759138415173, 0.770098701904903] + gcr_cart = np.array([[0.351, -0.724, 0.593], [0.617, 0.672, 0.410]]) + pt_cart = np.array( + [0.9438777657502077, 0.1193199333436068, 0.922714737029319]) + assert( + point_within_gca(pt_cart, gcr_cart[0], gcr_cart[1])) + + gcr_cart_flip = np.array([[0.617, 0.672, 0.410], [0.351, -0.724, + 0.593]]) + # If we flip the gcr in the undirected mode, it should still work + assert( + point_within_gca(pt_cart, gcr_cart_flip[0], gcr_cart_flip[1])) + + # 2nd anti-meridian case + # GCR vertex0 in radian : [4.104711496596806, 0.5352983676533828], + # GCR vertex1 in radian : [2.4269979227622533, -0.007003212877856825] + # Point in radian : [0.43400375562899113, -0.49554509841586936] + gcr_cart_1 = np.array([[-0.491, -0.706, 0.510], [-0.755, 0.655, + -0.007]]) + pt_cart_within = np.array( + [0.6136726305712109, 0.28442243941920053, -0.365605190899831]) + assert( not + point_within_gca(pt_cart_within, gcr_cart_1[0], gcr_cart_1[1])) + +def test_pt_within_gcr_cross_pole(): + gcr_cart = np.array([[0.351, 0.0, 0.3], [-0.351, 0.0, 0.3]]) + pt_cart = np.array([0.10, 0.0, 0.8]) + + # Normalize the point abd the GCA + pt_cart = pt_cart / np.linalg.norm(pt_cart) + gcr_cart = np.array([x / np.linalg.norm(x) for x in gcr_cart]) + assert(point_within_gca(pt_cart, gcr_cart[0], gcr_cart[1])) diff --git a/test/test_centroids.py b/test/test_centroids.py index 70c5cb208..d2f1be796 100644 --- a/test/test_centroids.py +++ b/test/test_centroids.py @@ -1,168 +1,145 @@ import os -from unittest import TestCase import numpy as np import numpy.testing as nt import uxarray as ux from pathlib import Path -from uxarray.grid.coordinates import _populate_face_centroids, _populate_edge_centroids, _populate_face_centerpoints, _is_inside_circle, _circle_from_three_points, _circle_from_two_points -from uxarray.grid.coordinates import _normalize_xyz +from uxarray.grid.coordinates import ( + _populate_face_centroids, + _populate_edge_centroids, + _populate_face_centerpoints, + _is_inside_circle, + _circle_from_three_points, + _circle_from_two_points, + _normalize_xyz, +) current_path = Path(os.path.dirname(os.path.realpath(__file__))) gridfile_CSne8 = current_path / "meshfiles" / "scrip" / "outCSne8" / "outCSne8.nc" mpasfile_QU = current_path / "meshfiles" / "mpas" / "QU" / "mesh.QU.1920km.151026.nc" +def test_centroids_from_mean_verts_triangle(): + """Test finding the centroid of a triangle.""" + test_triangle = np.array([(0, 0, 1), (0, 0, -1), (1, 0, 0)]) + expected_centroid = np.mean(test_triangle, axis=0) + norm_x, norm_y, norm_z = _normalize_xyz( + expected_centroid[0], expected_centroid[1], expected_centroid[2] + ) -class TestCentroids(TestCase): + grid = ux.open_grid(test_triangle) + _populate_face_centroids(grid) - def test_centroids_from_mean_verts_triangle(self): - """Test finding the centroid of a triangle.""" - # Create a triangle - test_triangle = np.array([(0, 0, 1), (0, 0, -1), (1, 0, 0)]) + assert norm_x == grid.face_x + assert norm_y == grid.face_y + assert norm_z == grid.face_z + +def test_centroids_from_mean_verts_pentagon(): + """Test finding the centroid of a pentagon.""" + test_polygon = np.array([(0, 0, 1), (0, 0, -1), (1, 0, 0), (0, 1, 0), (125, 125, 1)]) + expected_centroid = np.mean(test_polygon, axis=0) + norm_x, norm_y, norm_z = _normalize_xyz( + expected_centroid[0], expected_centroid[1], expected_centroid[2] + ) - # Calculate the expected centroid - expected_centroid = np.mean(test_triangle, axis=0) - norm_x, norm_y, norm_z = _normalize_xyz( - expected_centroid[0], expected_centroid[1], expected_centroid[2]) + grid = ux.open_grid(test_polygon) + _populate_face_centroids(grid) - # Open the dataset and find the centroids - grid = ux.open_grid(test_triangle) - _populate_face_centroids(grid) + assert norm_x == grid.face_x + assert norm_y == grid.face_y + assert norm_z == grid.face_z - # Test the values of the calculate centroids - self.assertEqual(norm_x, grid.face_x) - self.assertEqual(norm_y, grid.face_y) - self.assertEqual(norm_z, grid.face_z) +def test_centroids_from_mean_verts_scrip(): + """Test computed centroid values compared to values from a SCRIP dataset.""" + uxgrid = ux.open_grid(gridfile_CSne8) - def test_centroids_from_mean_verts_pentagon(self): - """Test finding the centroid of a pentagon.""" + expected_face_x = uxgrid.face_lon.values + expected_face_y = uxgrid.face_lat.values - # Create a polygon - test_triangle = np.array([(0, 0, 1), (0, 0, -1), (1, 0, 0), (0, 1, 0), - (125, 125, 1)]) + uxgrid.construct_face_centers(method="cartesian average") - # Calculate the expected centroid - expected_centroid = np.mean(test_triangle, axis=0) - norm_x, norm_y, norm_z = _normalize_xyz( - expected_centroid[0], expected_centroid[1], expected_centroid[2]) + computed_face_x = uxgrid.face_lon.values + computed_face_y = uxgrid.face_lat.values - # Open the dataset and find the centroids - grid = ux.open_grid(test_triangle) - _populate_face_centroids(grid) + nt.assert_array_almost_equal(expected_face_x, computed_face_x) + nt.assert_array_almost_equal(expected_face_y, computed_face_y) - # Test the values of the calculate centroids - self.assertEqual(norm_x, grid.face_x) - self.assertEqual(norm_y, grid.face_y) - self.assertEqual(norm_z, grid.face_z) +def test_edge_centroids_from_triangle(): + """Test finding the centroid of a triangle.""" + test_triangle = np.array([(0, 0, 0), (-1, 1, 0), (-1, -1, 0)]) + grid = ux.open_grid(test_triangle) + _populate_edge_centroids(grid) - def test_centroids_from_mean_verts_scrip(self): - """Test computed centroid values compared to values from a SCRIP - dataset.""" + centroid_x = np.mean(grid.node_x[grid.edge_node_connectivity[0][0:]]) + centroid_y = np.mean(grid.node_y[grid.edge_node_connectivity[0][0:]]) + centroid_z = np.mean(grid.node_z[grid.edge_node_connectivity[0][0:]]) - uxgrid = ux.open_grid(gridfile_CSne8) + assert centroid_x == grid.edge_x[0] + assert centroid_y == grid.edge_y[0] + assert centroid_z == grid.edge_z[0] - expected_face_x = uxgrid.face_lon.values - expected_face_y = uxgrid.face_lat.values +def test_edge_centroids_from_mpas(): + """Test computed centroid values compared to values from a MPAS dataset.""" + uxgrid = ux.open_grid(mpasfile_QU) - # _populate_face_centroids(uxgrid, repopulate=True) - uxgrid.construct_face_centers(method="cartesian average") + expected_edge_lon = uxgrid.edge_lon.values + expected_edge_lat = uxgrid.edge_lat.values - # computed_face_x = (uxgrid.face_lon.values + 180) % 360 - 180 - computed_face_x = uxgrid.face_lon.values - computed_face_y = uxgrid.face_lat.values + _populate_edge_centroids(uxgrid, repopulate=True) - nt.assert_array_almost_equal(expected_face_x, computed_face_x) - nt.assert_array_almost_equal(expected_face_y, computed_face_y) + computed_edge_lon = (uxgrid.edge_lon.values + 180) % 360 - 180 + computed_edge_lat = uxgrid.edge_lat.values - def test_edge_centroids_from_triangle(self): - """Test finding the centroid of a triangle.""" - # Create a triangle - test_triangle = np.array([(0, 0, 0), (-1, 1, 0), (-1, -1, 0)]) + nt.assert_array_almost_equal(expected_edge_lon, computed_edge_lon) + nt.assert_array_almost_equal(expected_edge_lat, computed_edge_lat) - # Open the dataset and find the centroids - grid = ux.open_grid(test_triangle) - _populate_edge_centroids(grid) +def test_circle_from_two_points(): + """Test creation of circle from 2 points.""" + p1 = (0, 0) + p2 = (0, 90) + center, radius = _circle_from_two_points(p1, p2) - # compute edge_xyz for first edge - centroid_x = np.mean(grid.node_x[grid.edge_node_connectivity[0][0:]]) - centroid_y = np.mean(grid.node_y[grid.edge_node_connectivity[0][0:]]) - centroid_z = np.mean(grid.node_z[grid.edge_node_connectivity[0][0:]]) + expected_center = (0.0, 45.0) + expected_radius = np.deg2rad(45.0) - # Test the values of computed first edge centroid and the populated one - self.assertEqual(centroid_x, grid.edge_x[0]) - self.assertEqual(centroid_y, grid.edge_y[0]) - self.assertEqual(centroid_z, grid.edge_z[0]) + assert np.allclose(center, expected_center), f"Expected center {expected_center}, but got {center}" + assert np.allclose(radius, expected_radius), f"Expected radius {expected_radius}, but got {radius}" - def test_edge_centroids_from_mpas(self): - """Test computed centroid values compared to values from a MPAS - dataset.""" +def test_circle_from_three_points(): + """Test creation of circle from 3 points.""" + p1 = (0, 0) + p2 = (0, 90) + p3 = (90, 0) + center, radius = _circle_from_three_points(p1, p2, p3) + expected_radius = np.deg2rad(45.0) + expected_center = (30.0, 30.0) - uxgrid = ux.open_grid(mpasfile_QU) + assert np.allclose(center, expected_center), f"Expected center {expected_center}, but got {center}" + assert np.allclose(radius, expected_radius), f"Expected radius {expected_radius}, but got {radius}" - expected_edge_lon = uxgrid.edge_lon.values - expected_edge_lat = uxgrid.edge_lat.values +def test_is_inside_circle(): + """Test if a points is inside the circle.""" + circle = ((0.0, 0.0), 1) # Center at lon/lat with a radius in radians + + point_inside = (30.0, 30.0) + point_outside = (90.0, 0.0) - _populate_edge_centroids(uxgrid, repopulate=True) + assert _is_inside_circle(circle, point_inside), f"Point {point_inside} should be inside the circle." + assert not _is_inside_circle(circle, point_outside), f"Point {point_outside} should be outside the circle." - computed_edge_lon = (uxgrid.edge_lon.values + 180) % 360 - 180 - computed_edge_lat = uxgrid.edge_lat.values +def test_face_centerpoint(): + """Use points from an actual spherical face and get the centerpoint.""" + points = np.array([ + (-35.26438968, -45.0), + (-36.61769496, -42.0), + (-33.78769181, -42.0), + (-32.48416571, -45.0) + ]) + uxgrid = ux.open_grid(points, latlon=True) - nt.assert_array_almost_equal(expected_edge_lon, computed_edge_lon) - nt.assert_array_almost_equal(expected_edge_lat, computed_edge_lat) + ctr_lon = uxgrid.face_lon.values[0] + ctr_lat = uxgrid.face_lat.values[0] -class TestCenterPoints(TestCase): + uxgrid.construct_face_centers(method="welzl") - def test_circle_from_two_points(self): - """Test creation of circle from 2 points.""" - p1 = (0, 0) - p2 = (0, 90) - center, radius = _circle_from_two_points(p1, p2) - - # The expected radius in radians should be half the angle between the two vectors - expected_center = (0.0, 45.0) - expected_radius = np.deg2rad(45.0) - - assert np.allclose(center, expected_center), f"Expected center {expected_center}, but got {center}" - assert np.allclose(radius, expected_radius), f"Expected radius {expected_radius}, but got {radius}" - - def test_circle_from_three_points(self): - """Test creation of circle from 3 points.""" - p1 = (0, 0) - p2 = (0, 90) - p3 = (90, 0) - center, radius = _circle_from_three_points(p1, p2, p3) - expected_radius = np.deg2rad(45.0) - expected_center = (30.0, 30.0) - - assert np.allclose(center, expected_center), f"Expected center {expected_center}, but got {center}" - assert np.allclose(radius, expected_radius), f"Expected radius {expected_radius}, but got {radius}" - - def test_is_inside_circle(self): - """Test if a points is inside the circle.""" - # Define the circle - circle = ((0.0, 0.0), 1) # Center at lon/lat with a radius in radians (angular measure of the radius) - - # Define test points - point_inside = (30.0, 30.0) # Should be inside the circle - point_outside = (90.0, 0.0) # Should be outside the circle - - # Test _is_inside_circle function - assert _is_inside_circle(circle, point_inside), f"Point {point_inside} should be inside the circle." - assert not _is_inside_circle(circle, point_outside), f"Point {point_outside} should be outside the circle." - - def test_face_centerpoint(self): - """Use points from an actual spherical face and get the centerpoint.""" - - points = np.array([(-35.26438968, -45.0), (-36.61769496, -42.0), (-33.78769181, -42.0), (-32.48416571, -45.0)]) - uxgrid = ux.open_grid(points, latlon=True) - - # Uses the @property from get face_lon/lat - default is average or centroid - ctr_lon = uxgrid.face_lon.values[0] - ctr_lat = uxgrid.face_lat.values[0] - - # now explicitly get the centerpoints stored to face_lon/lat using welzl's centerpoint algorithm - uxgrid.construct_face_centers(method = "welzl") - - # Test the values of the calculated centerpoint, giving high tolerance of two decimal place - nt.assert_array_almost_equal(ctr_lon, uxgrid.face_lon.values[0], decimal=2) - nt.assert_array_almost_equal(ctr_lat, uxgrid.face_lat.values[0], decimal=2) + nt.assert_array_almost_equal(ctr_lon, uxgrid.face_lon.values[0], decimal=2) + nt.assert_array_almost_equal(ctr_lat, uxgrid.face_lat.values[0], decimal=2) diff --git a/test/test_computing.py b/test/test_computing.py index a83d3b76c..b2a644541 100644 --- a/test/test_computing.py +++ b/test/test_computing.py @@ -1,129 +1,105 @@ -from unittest import TestCase import numpy.testing as nt import numpy as np from uxarray.grid.coordinates import _normalize_xyz import uxarray.utils.computing as ac_utils from uxarray.constants import ERROR_TOLERANCE - - -class TestCrossProduct(TestCase): - """Since we don't have the multiprecision in current release, we're just - going to test if the FMA enabled dot product is similar to the np.dot - one.""" - - def test_cross_fma(self): - v1 = np.array(_normalize_xyz(*[1.0, 2.0, 3.0])) - v2 = np.array(_normalize_xyz(*[4.0, 5.0, 6.0])) - - np_cross = np.cross(v1, v2) - fma_cross = ac_utils.cross_fma(v1, v2) - nt.assert_allclose(np_cross, fma_cross, atol=ERROR_TOLERANCE) - - -class TestDotProduct(TestCase): - """Since we don't have the multiprecision in current release, we're just - going to test if the FMA enabled dot product is similar to the np.dot - one.""" - - def test_dot_fma(self): - v1 = np.array(_normalize_xyz(*[1.0, 0.0, 0.0]), dtype=np.float64) - v2 = np.array(_normalize_xyz(*[1.0, 0.0, 0.0]), dtype=np.float64) - - np_dot = np.dot(v1, v2) - fma_dot = ac_utils.dot_fma(v1, v2) - nt.assert_allclose(np_dot, fma_dot, atol=ERROR_TOLERANCE) - - -class TestFMAOperations(TestCase): - - def test_two_sum(self): - """Test the two_sum function.""" - a = 1.0 - b = 2.0 - s, e = ac_utils._two_sum(a, b) - self.assertAlmostEqual(a + b, s + e, places=15) - - def test_fast_two_sum(self): - """Test the fase_two_sum function.""" - a = 2.0 - b = 1.0 - s, e = ac_utils._two_sum(a, b) - sf, ef = ac_utils._fast_two_sum(a, b) - self.assertEqual(s, sf) - self.assertEqual(e, ef) - - def test_two_prod_fma(self): - """Test the two_prod_fma function.""" - import pyfma - a = 1.0 - b = 2.0 - x, y = ac_utils._two_prod_fma(a, b) - self.assertEqual(x, a * b) - self.assertEqual(y, pyfma.fma(a, b, -x)) - self.assertAlmostEqual(a * b, x + y, places=15) - - def test_fast_two_mult(self): - """Test the two_prod_fma function.""" - a = 1.0 - b = 2.0 - x, y = ac_utils._two_prod_fma(a, b) - xf, yf = ac_utils._fast_two_mult(a, b) - self.assertEqual(x, xf) - self.assertEqual(y, yf) - - def test_err_fmac(self): - """Test the _err_fmac function.""" - import pyfma - a = 1.0 - b = 2.0 - c = 3.0 - x, y, z = ac_utils._err_fmac(a, b, c) - self.assertEqual(x, pyfma.fma(a, b, c)) - self.assertAlmostEqual(a * b + c, x + y + z, places=15) - - -class TestAccurateSum(TestCase): - - def test_vec_sum(self): - """Test the _vec_sum function.""" - a = np.array([1.0, 2.0, 3.0]) - res = ac_utils._vec_sum(a) - self.assertAlmostEqual(6.0, res, places=15) - import gmpy2 - a = gmpy2.mpfr('2.28888888888') - b = gmpy2.mpfr('-2.2888889999') - c = gmpy2.mpfr('0.000000000001') - d = gmpy2.mpfr('-0.000000000001') - - a_float = float(a) - b_float = float(b) - c_float = float(c) - d_float = float(d) - - res = ac_utils._vec_sum(np.array([a_float, b_float, c_float, d_float])) - res_mp = gmpy2.mpfr(a_float) + gmpy2.mpfr(b_float) + gmpy2.mpfr( - c_float) + gmpy2.mpfr(d_float) - abs_res = abs(res - res_mp) - self.assertTrue( - gmpy2.cmp(abs_res, gmpy2.mpfr(np.finfo(np.float64).eps)) == -1) - - -class TestNorm(TestCase): - - def test_norm_faithful(self): - """Test the norm_faithful function.""" - a = np.array([1.0, 2.0, 3.0]) - res = ac_utils._norm_faithful(a) - self.assertAlmostEqual(np.linalg.norm(a), res, places=15) - - def test_sqrt_faithful(self): - """Test the sqrt_faithful function.""" - a = 10.0 - res = ac_utils._acc_sqrt(a, 0.0) - self.assertAlmostEqual(np.sqrt(a), res, places=15) - - def test_two_square(self): - """Test the _two_square function.""" - a = 10.0 - res = ac_utils._two_square(a) - self.assertAlmostEqual(a * a, res[0], places=15) +import pyfma +import gmpy2 + +def test_cross_fma(): + v1 = np.array(_normalize_xyz(*[1.0, 2.0, 3.0])) + v2 = np.array(_normalize_xyz(*[4.0, 5.0, 6.0])) + + np_cross = np.cross(v1, v2) + fma_cross = ac_utils.cross_fma(v1, v2) + nt.assert_allclose(np_cross, fma_cross, atol=ERROR_TOLERANCE) + +def test_dot_fma(): + v1 = np.array(_normalize_xyz(*[1.0, 0.0, 0.0]), dtype=np.float64) + v2 = np.array(_normalize_xyz(*[1.0, 0.0, 0.0]), dtype=np.float64) + + np_dot = np.dot(v1, v2) + fma_dot = ac_utils.dot_fma(v1, v2) + nt.assert_allclose(np_dot, fma_dot, atol=ERROR_TOLERANCE) + +def test_two_sum(): + """Test the two_sum function.""" + a = 1.0 + b = 2.0 + s, e = ac_utils._two_sum(a, b) + assert np.isclose(a + b, s + e, atol=1e-15) + +def test_fast_two_sum(): + """Test the fast_two_sum function.""" + a = 2.0 + b = 1.0 + s, e = ac_utils._two_sum(a, b) + sf, ef = ac_utils._fast_two_sum(a, b) + assert s == sf + assert e == ef + +def test_two_prod_fma(): + """Test the two_prod_fma function.""" + a = 1.0 + b = 2.0 + x, y = ac_utils._two_prod_fma(a, b) + assert x == a * b + assert y == pyfma.fma(a, b, -x) + assert np.isclose(a * b, x + y, atol=1e-15) + +def test_fast_two_mult(): + """Test the fast_two_mult function.""" + a = 1.0 + b = 2.0 + x, y = ac_utils._two_prod_fma(a, b) + xf, yf = ac_utils._fast_two_mult(a, b) + assert x == xf + assert y == yf + +def test_err_fmac(): + """Test the _err_fmac function.""" + a = 1.0 + b = 2.0 + c = 3.0 + x, y, z = ac_utils._err_fmac(a, b, c) + assert x == pyfma.fma(a, b, c) + assert np.isclose(a * b + c, x + y + z, atol=1e-15) + +def test_vec_sum(): + """Test the _vec_sum function.""" + a = np.array([1.0, 2.0, 3.0]) + res = ac_utils._vec_sum(a) + assert np.isclose(6.0, res, atol=1e-15) + + a = gmpy2.mpfr('2.28888888888') + b = gmpy2.mpfr('-2.2888889999') + c = gmpy2.mpfr('0.000000000001') + d = gmpy2.mpfr('-0.000000000001') + + a_float = float(a) + b_float = float(b) + c_float = float(c) + d_float = float(d) + + res = ac_utils._vec_sum(np.array([a_float, b_float, c_float, d_float])) + res_mp = gmpy2.mpfr(a_float) + gmpy2.mpfr(b_float) + gmpy2.mpfr(c_float) + gmpy2.mpfr(d_float) + abs_res = abs(res - res_mp) + assert gmpy2.cmp(abs_res, gmpy2.mpfr(np.finfo(np.float64).eps)) == -1 + +def test_norm_faithful(): + """Test the norm_faithful function.""" + a = np.array([1.0, 2.0, 3.0]) + res = ac_utils._norm_faithful(a) + assert np.isclose(np.linalg.norm(a), res, atol=1e-15) + +def test_sqrt_faithful(): + """Test the sqrt_faithful function.""" + a = 10.0 + res = ac_utils._acc_sqrt(a, 0.0) + assert np.isclose(np.sqrt(a), res, atol=1e-15) + +def test_two_square(): + """Test the _two_square function.""" + a = 10.0 + res = ac_utils._two_square(a) + assert np.isclose(a * a, res[0], atol=1e-15) diff --git a/test/test_cross_sections.py b/test/test_cross_sections.py index 6fbe8abcf..0e249c57a 100644 --- a/test/test_cross_sections.py +++ b/test/test_cross_sections.py @@ -2,175 +2,116 @@ import pytest import numpy as np from pathlib import Path -import os - -import numpy.testing as nt # Define the current path and file paths for grid and data -current_path = Path(os.path.dirname(os.path.realpath(__file__))) +current_path = Path(__file__).resolve().parent quad_hex_grid_path = current_path / 'meshfiles' / "ugrid" / "quad-hexagon" / 'grid.nc' quad_hex_data_path = current_path / 'meshfiles' / "ugrid" / "quad-hexagon" / 'data.nc' - cube_sphere_grid = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" from uxarray.grid.intersections import constant_lat_intersections_face_bounds +def test_constant_lat_cross_section_grid(): + uxgrid = ux.open_grid(quad_hex_grid_path) + grid_top_two = uxgrid.cross_section.constant_latitude(lat=0.1) + assert grid_top_two.n_face == 2 -class TestQuadHex: - """The quad hexagon grid contains four faces. - - Top Left Face: Index 1 - - Top Right Face: Index 2 - - Bottom Left Face: Index 0 - - Bottom Right Face: Index 3 - - The top two faces intersect a constant latitude of 0.1 - - The bottom two faces intersect a constant latitude of -0.1 - - All four faces intersect a constant latitude of 0.0 - """ - - def test_constant_lat_cross_section_grid(self): - - uxgrid = ux.open_grid(quad_hex_grid_path) - - grid_top_two = uxgrid.cross_section.constant_latitude(lat=0.1, ) - - assert grid_top_two.n_face == 2 - - grid_bottom_two = uxgrid.cross_section.constant_latitude(lat=-0.1, ) - - assert grid_bottom_two.n_face == 2 - - grid_all_four = uxgrid.cross_section.constant_latitude(lat=0.0, ) - - assert grid_all_four.n_face == 4 - - with pytest.raises(ValueError): - # no intersections found at this line - uxgrid.cross_section.constant_latitude(lat=10.0, ) - - def test_constant_lon_cross_section_grid(self): - uxgrid = ux.open_grid(quad_hex_grid_path) - - grid_left_two = uxgrid.cross_section.constant_longitude(lon=-0.1, ) - - assert grid_left_two.n_face == 2 - - grid_right_two = uxgrid.cross_section.constant_longitude(lon=0.2, ) - - assert grid_right_two.n_face == 2 - - with pytest.raises(ValueError): - # no intersections found at this line - uxgrid.cross_section.constant_longitude(lon=10.0) - - def test_constant_lat_cross_section_uxds(self): - uxds = ux.open_dataset(quad_hex_grid_path, quad_hex_data_path) - uxds.uxgrid.normalize_cartesian_coordinates() - - da_top_two = uxds['t2m'].cross_section.constant_latitude(lat=0.1, ) - - nt.assert_array_equal(da_top_two.data, uxds['t2m'].isel(n_face=[1, 2]).data) - - da_bottom_two = uxds['t2m'].cross_section.constant_latitude(lat=-0.1, ) - - nt.assert_array_equal(da_bottom_two.data, uxds['t2m'].isel(n_face=[0, 3]).data) - - da_all_four = uxds['t2m'].cross_section.constant_latitude(lat=0.0, ) - - nt.assert_array_equal(da_all_four.data , uxds['t2m'].data) - - with pytest.raises(ValueError): - # no intersections found at this line - uxds['t2m'].cross_section.constant_latitude(lat=10.0, ) - - - def test_constant_lon_cross_section_uxds(self): - uxds = ux.open_dataset(quad_hex_grid_path, quad_hex_data_path) - uxds.uxgrid.normalize_cartesian_coordinates() - - da_left_two = uxds['t2m'].cross_section.constant_longitude(lon=-0.1, ) - - nt.assert_array_equal(da_left_two.data, uxds['t2m'].isel(n_face=[0, 2]).data) - - da_right_two = uxds['t2m'].cross_section.constant_longitude(lon=0.2, ) + grid_bottom_two = uxgrid.cross_section.constant_latitude(lat=-0.1) + assert grid_bottom_two.n_face == 2 - nt.assert_array_equal(da_right_two.data, uxds['t2m'].isel(n_face=[1, 3]).data) + grid_all_four = uxgrid.cross_section.constant_latitude(lat=0.0) + assert grid_all_four.n_face == 4 - with pytest.raises(ValueError): - # no intersections found at this line - uxds['t2m'].cross_section.constant_longitude(lon=10.0, ) + with pytest.raises(ValueError): + uxgrid.cross_section.constant_latitude(lat=10.0) +def test_constant_lon_cross_section_grid(): + uxgrid = ux.open_grid(quad_hex_grid_path) -class TestCubeSphere: + grid_left_two = uxgrid.cross_section.constant_longitude(lon=-0.1) + assert grid_left_two.n_face == 2 - def test_north_pole(self): - uxgrid = ux.open_grid(cube_sphere_grid) + grid_right_two = uxgrid.cross_section.constant_longitude(lon=0.2) + assert grid_right_two.n_face == 2 - lats = [89.85, 89.9, 89.95, 89.99] + with pytest.raises(ValueError): + uxgrid.cross_section.constant_longitude(lon=10.0) - for lat in lats: - cross_grid = uxgrid.cross_section.constant_latitude(lat=lat, ) - # Cube sphere grid should have 4 faces centered around the pole - assert cross_grid.n_face == 4 +def test_constant_lat_cross_section_uxds(): + uxds = ux.open_dataset(quad_hex_grid_path, quad_hex_data_path) + uxds.uxgrid.normalize_cartesian_coordinates() - def test_south_pole(self): - uxgrid = ux.open_grid(cube_sphere_grid) + da_top_two = uxds['t2m'].cross_section.constant_latitude(lat=0.1) + np.testing.assert_array_equal(da_top_two.data, uxds['t2m'].isel(n_face=[1, 2]).data) - lats = [-89.85, -89.9, -89.95, -89.99] + da_bottom_two = uxds['t2m'].cross_section.constant_latitude(lat=-0.1) + np.testing.assert_array_equal(da_bottom_two.data, uxds['t2m'].isel(n_face=[0, 3]).data) - for lat in lats: - cross_grid = uxgrid.cross_section.constant_latitude(lat=lat, ) - # Cube sphere grid should have 4 faces centered around the pole - assert cross_grid.n_face == 4 + da_all_four = uxds['t2m'].cross_section.constant_latitude(lat=0.0) + np.testing.assert_array_equal(da_all_four.data, uxds['t2m'].data) + with pytest.raises(ValueError): + uxds['t2m'].cross_section.constant_latitude(lat=10.0) +def test_constant_lon_cross_section_uxds(): + uxds = ux.open_dataset(quad_hex_grid_path, quad_hex_data_path) + uxds.uxgrid.normalize_cartesian_coordinates() -class TestCandidateFacesUsingBounds: + da_left_two = uxds['t2m'].cross_section.constant_longitude(lon=-0.1) + np.testing.assert_array_equal(da_left_two.data, uxds['t2m'].isel(n_face=[0, 2]).data) - def test_constant_lat(self): - bounds = np.array([ - [[-45, 45], [0, 360]], - [[-90, -45], [0, 360]], - [[45, 90], [0, 360]], - ]) + da_right_two = uxds['t2m'].cross_section.constant_longitude(lon=0.2) + np.testing.assert_array_equal(da_right_two.data, uxds['t2m'].isel(n_face=[1, 3]).data) - bounds_rad = np.deg2rad(bounds) + with pytest.raises(ValueError): + uxds['t2m'].cross_section.constant_longitude(lon=10.0) - const_lat = 0 +def test_north_pole(): + uxgrid = ux.open_grid(cube_sphere_grid) + lats = [89.85, 89.9, 89.95, 89.99] - candidate_faces = constant_lat_intersections_face_bounds( - lat=const_lat, - face_bounds_lat=bounds_rad[:, 0], - ) + for lat in lats: + cross_grid = uxgrid.cross_section.constant_latitude(lat=lat) + assert cross_grid.n_face == 4 - # Expected output - expected_faces = np.array([0]) +def test_south_pole(): + uxgrid = ux.open_grid(cube_sphere_grid) + lats = [-89.85, -89.9, -89.95, -89.99] - # Test the function output - nt.assert_array_equal(candidate_faces, expected_faces) + for lat in lats: + cross_grid = uxgrid.cross_section.constant_latitude(lat=lat) + assert cross_grid.n_face == 4 - def test_constant_lat_out_of_bounds(self): +def test_constant_lat(): + bounds = np.array([ + [[-45, 45], [0, 360]], + [[-90, -45], [0, 360]], + [[45, 90], [0, 360]], + ]) + bounds_rad = np.deg2rad(bounds) + const_lat = 0 - bounds = np.array([ - [[-45, 45], [0, 360]], - [[-90, -45], [0, 360]], - [[45, 90], [0, 360]], - ]) + candidate_faces = constant_lat_intersections_face_bounds( + lat=const_lat, + face_bounds_lat=bounds_rad[:, 0], + ) - bounds_rad = np.deg2rad(bounds) + expected_faces = np.array([0]) + np.testing.assert_array_equal(candidate_faces, expected_faces) - const_lat = 100 +def test_constant_lat_out_of_bounds(): + bounds = np.array([ + [[-45, 45], [0, 360]], + [[-90, -45], [0, 360]], + [[45, 90], [0, 360]], + ]) + bounds_rad = np.deg2rad(bounds) + const_lat = 100 - candidate_faces = constant_lat_intersections_face_bounds( - lat=const_lat, - face_bounds_lat=bounds_rad[:, 0], - ) + candidate_faces = constant_lat_intersections_face_bounds( + lat=const_lat, + face_bounds_lat=bounds_rad[:, 0], + ) - assert len(candidate_faces) == 0 + assert len(candidate_faces) == 0 diff --git a/test/test_dask.py b/test/test_dask.py index 5df038dc9..1286a9804 100644 --- a/test/test_dask.py +++ b/test/test_dask.py @@ -1,21 +1,16 @@ import uxarray as ux import numpy as np import dask.array as da - import pytest import os from pathlib import Path - - current_path = Path(os.path.dirname(os.path.realpath(__file__))) mpas_grid = current_path / 'meshfiles' / "mpas" / "QU" / 'oQU480.231010.nc' - csne30_grid = current_path / 'meshfiles' / "ugrid" / "outCSne30" / 'outCSne30.ug' csne30_data = current_path / 'meshfiles' / "ugrid" / "outCSne30" / 'outCSne30_var2.nc' - def test_grid_chunking(): """Tests the chunking of an entire grid.""" uxgrid = ux.open_grid(mpas_grid) @@ -44,6 +39,10 @@ def test_individual_var_chunking(): # face_node_conn should now be a dask array assert isinstance(uxgrid.face_node_connectivity.data, da.Array) - def test_uxds_chunking(): + """Tests the chunking of a dataset.""" uxds = ux.open_dataset(csne30_grid, csne30_data, chunks={"n_face": 4}) + + # Add assertions to check the correctness of chunking + for var in uxds.variables: + assert isinstance(uxds[var].data, da.Array) diff --git a/test/test_dataarray.py b/test/test_dataarray.py index 092c658af..9b6c54edc 100644 --- a/test/test_dataarray.py +++ b/test/test_dataarray.py @@ -1,123 +1,106 @@ import os - -from unittest import TestCase from pathlib import Path - import numpy as np - import uxarray as ux - from uxarray.grid.geometry import _build_polygon_shells, _build_corrected_polygon_shells - from uxarray.core.dataset import UxDataset, UxDataArray +import pytest current_path = Path(os.path.dirname(os.path.realpath(__file__))) gridfile_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" dsfile_var2_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30_vortex.nc" -dsfiles_mf_ne30 = str( - current_path) + "/meshfiles/ugrid/outCSne30/outCSne30_*.nc" - gridfile_geoflow = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc" dsfile_v1_geoflow = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "v1.nc" +def test_to_dataset(): + """Tests the conversion of UxDataArrays to a UXDataset.""" + uxds = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) + uxds_converted = uxds['psi'].to_dataset() -class TestDataArray(TestCase): - - def test_to_dataset(self): - """Tests the conversion of UxDataArrays to a UXDataset.""" - uxds = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) - uxds_converted = uxds['psi'].to_dataset() - - assert isinstance(uxds_converted, UxDataset) - assert uxds_converted.uxgrid == uxds.uxgrid - - def test_get_dual(self): - """Tests the creation of the dual mesh on a data array.""" - uxds = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) - dual = uxds['psi'].get_dual() - - assert isinstance(dual, UxDataArray) - self.assertTrue(dual._node_centered()) - + assert isinstance(uxds_converted, UxDataset) + assert uxds_converted.uxgrid == uxds.uxgrid -class TestGeometryConversions(TestCase): +def test_get_dual(): + """Tests the creation of the dual mesh on a data array.""" + uxds = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) + dual = uxds['psi'].get_dual() - def test_to_geodataframe(self): - """Tests the conversion to ``GeoDataFrame``""" - ### geoflow - uxds_geoflow = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) + assert isinstance(dual, UxDataArray) + assert dual._node_centered() - # v1 is mapped to nodes, should raise a value error - with self.assertRaises(ValueError): - uxds_geoflow['v1'].to_geodataframe() +def test_to_geodataframe(): + """Tests the conversion to ``GeoDataFrame``""" + # GeoFlow + uxds_geoflow = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) - # grid conversion - gdf_geoflow_grid = uxds_geoflow.uxgrid.to_geodataframe(periodic_elements='split') + # v1 is mapped to nodes, should raise a value error + with pytest.raises(ValueError): + uxds_geoflow['v1'].to_geodataframe() - # number of elements - assert gdf_geoflow_grid.shape == (uxds_geoflow.uxgrid.n_face, 1) + # grid conversion + gdf_geoflow_grid = uxds_geoflow.uxgrid.to_geodataframe(periodic_elements='split') - ### n30 - uxds_ne30 = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) + # number of elements + assert gdf_geoflow_grid.shape == (uxds_geoflow.uxgrid.n_face, 1) - gdf_geoflow_data = uxds_ne30['psi'].to_geodataframe(periodic_elements='split') + # NE30 + uxds_ne30 = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) - assert gdf_geoflow_data.shape == (uxds_ne30.uxgrid.n_face, 2) + gdf_geoflow_data = uxds_ne30['psi'].to_geodataframe(periodic_elements='split') - def test_to_polycollection(self): - """Tests the conversion to ``PolyCollection``""" - ### geoflow - uxds_geoflow = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) + assert gdf_geoflow_data.shape == (uxds_ne30.uxgrid.n_face, 2) - # v1 is mapped to nodes, should raise a value error - with self.assertRaises(ValueError): - uxds_geoflow['v1'].to_polycollection() +def test_to_polycollection(): + """Tests the conversion to ``PolyCollection``""" + # GeoFlow + uxds_geoflow = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) - # grid conversion - pc_geoflow_grid = uxds_geoflow.uxgrid.to_polycollection(periodic_elements='split') + # v1 is mapped to nodes, should raise a value error + with pytest.raises(ValueError): + uxds_geoflow['v1'].to_polycollection() - polygon_shells = _build_polygon_shells( - uxds_geoflow.uxgrid.node_lon.values, - uxds_geoflow.uxgrid.node_lat.values, - uxds_geoflow.uxgrid.face_node_connectivity.values, - uxds_geoflow.uxgrid.n_face, uxds_geoflow.uxgrid.n_max_face_nodes, - uxds_geoflow.uxgrid.n_nodes_per_face.values) + # grid conversion + pc_geoflow_grid = uxds_geoflow.uxgrid.to_polycollection(periodic_elements='split') - corrected_polygon_shells, _ = _build_corrected_polygon_shells( - polygon_shells) + polygon_shells = _build_polygon_shells( + uxds_geoflow.uxgrid.node_lon.values, + uxds_geoflow.uxgrid.node_lat.values, + uxds_geoflow.uxgrid.face_node_connectivity.values, + uxds_geoflow.uxgrid.n_face, uxds_geoflow.uxgrid.n_max_face_nodes, + uxds_geoflow.uxgrid.n_nodes_per_face.values) - # number of elements - assert len(pc_geoflow_grid._paths) == len(corrected_polygon_shells) + corrected_polygon_shells, _ = _build_corrected_polygon_shells(polygon_shells) - # ### n30 - uxds_ne30 = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) + # number of elements + assert len(pc_geoflow_grid._paths) == len(corrected_polygon_shells) - polygon_shells = _build_polygon_shells( - uxds_ne30.uxgrid.node_lon.values, uxds_ne30.uxgrid.node_lat.values, - uxds_ne30.uxgrid.face_node_connectivity.values, - uxds_ne30.uxgrid.n_face, uxds_ne30.uxgrid.n_max_face_nodes, - uxds_ne30.uxgrid.n_nodes_per_face.values) + # NE30 + uxds_ne30 = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) - corrected_polygon_shells, _ = _build_corrected_polygon_shells( - polygon_shells) + polygon_shells = _build_polygon_shells( + uxds_ne30.uxgrid.node_lon.values, uxds_ne30.uxgrid.node_lat.values, + uxds_ne30.uxgrid.face_node_connectivity.values, + uxds_ne30.uxgrid.n_face, uxds_ne30.uxgrid.n_max_face_nodes, + uxds_ne30.uxgrid.n_nodes_per_face.values) - pc_geoflow_data = uxds_ne30['psi'].to_polycollection(periodic_elements='split') + corrected_polygon_shells, _ = _build_corrected_polygon_shells(polygon_shells) - assert len(pc_geoflow_data._paths) == len(corrected_polygon_shells) + pc_geoflow_data = uxds_ne30['psi'].to_polycollection(periodic_elements='split') - def test_geodataframe_caching(self): - uxds = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) + assert len(pc_geoflow_data._paths) == len(corrected_polygon_shells) - gdf_start = uxds['psi'].to_geodataframe() +def test_geodataframe_caching(): + uxds = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) - gdf_next = uxds['psi'].to_geodataframe() + gdf_start = uxds['psi'].to_geodataframe() + gdf_next = uxds['psi'].to_geodataframe() - # with caching, they point to the same area in memory - assert gdf_start is gdf_next + # with caching, they point to the same area in memory + assert gdf_start is gdf_next - gdf_end = uxds['psi'].to_geodataframe(override=True) + gdf_end = uxds['psi'].to_geodataframe(override=True) - # override will recompute the grid - assert gdf_start is not gdf_end + # override will recompute the grid + assert gdf_start is not gdf_end diff --git a/test/test_dataset.py b/test/test_dataset.py index 33833bef0..afad7b5a8 100644 --- a/test/test_dataset.py +++ b/test/test_dataset.py @@ -1,11 +1,10 @@ import os -from unittest import TestCase from pathlib import Path import numpy.testing as nt import xarray as xr - import uxarray as ux from uxarray import UxDataset +import pytest try: import constants @@ -16,74 +15,59 @@ gridfile_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" dsfile_var2_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30_var2.nc" -dsfiles_mf_ne30 = str( - current_path) + "/meshfiles/ugrid/outCSne30/outCSne30_*.nc" - gridfile_geoflow = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc" dsfile_v1_geoflow = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "v1.nc" - mpas_ds_path = current_path / 'meshfiles' / "mpas" / "QU" / 'mesh.QU.1920km.151026.nc' - -class TestUxDataset(TestCase): - - def test_uxgrid_setget(self): - """Load a dataset with its grid topology file using uxarray's - open_dataset call and check its grid object.""" - - uxds_var2_ne30 = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) - - uxgrid_var2_ne30 = ux.open_grid(gridfile_ne30) - assert (uxds_var2_ne30.uxgrid == uxgrid_var2_ne30) - - def test_integrate(self): - """Load a dataset and calculate integrate().""" - - uxds_var2_ne30 = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) - - integrate_var2 = uxds_var2_ne30.integrate() - - nt.assert_almost_equal(integrate_var2, constants.VAR2_INTG, decimal=3) - - def test_info(self): - """Tests custom info containing grid information.""" - uxds_var2_geoflow = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) - - import contextlib - import io - - with contextlib.redirect_stdout(io.StringIO()): - try: - uxds_var2_geoflow.info(show_attrs=True) - except Exception as exc: - assert False, f"'uxds_var2_geoflow.info()' raised an exception: {exc}" - - def test_ugrid_dim_names(self): - """Tests the remapping of dimensions to the UGRID conventions.""" - - ugrid_dims = ["n_face", "n_node", "n_edge"] - - uxds_remap = ux.open_dataset(mpas_ds_path, mpas_ds_path) - - for dim in ugrid_dims: - assert dim in uxds_remap.dims - - def test_get_dual(self): - """Tests the creation of the dual mesh on a data set.""" - uxds = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) - dual = uxds.get_dual() - - assert isinstance(dual, UxDataset) - self.assertTrue(len(uxds.data_vars) == len(dual.data_vars)) - - # commented out due to often failures on the dataset hosting - # def test_read_from_https(self): - # """Tests reading a dataset from a HTTPS link.""" - # import requests - # - # small_file_480km = requests.get( - # "https://web.lcrc.anl.gov/public/e3sm/inputdata/share/meshes/mpas/ocean/oQU480.230422.nc" - # ).content - # - # ds_small_480km = ux.open_dataset(small_file_480km, small_file_480km) - # assert isinstance(ds_small_480km, ux.core.dataset.UxDataset) +def test_uxgrid_setget(): + """Load a dataset with its grid topology file using uxarray's + open_dataset call and check its grid object.""" + uxds_var2_ne30 = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) + uxgrid_var2_ne30 = ux.open_grid(gridfile_ne30) + assert (uxds_var2_ne30.uxgrid == uxgrid_var2_ne30) + +def test_integrate(): + """Load a dataset and calculate integrate().""" + uxds_var2_ne30 = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) + integrate_var2 = uxds_var2_ne30.integrate() + nt.assert_almost_equal(integrate_var2, constants.VAR2_INTG, decimal=3) + +def test_info(): + """Tests custom info containing grid information.""" + uxds_var2_geoflow = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) + import contextlib + import io + + with contextlib.redirect_stdout(io.StringIO()): + try: + uxds_var2_geoflow.info(show_attrs=True) + except Exception as exc: + assert False, f"'uxds_var2_geoflow.info()' raised an exception: {exc}" + +def test_ugrid_dim_names(): + """Tests the remapping of dimensions to the UGRID conventions.""" + ugrid_dims = ["n_face", "n_node", "n_edge"] + uxds_remap = ux.open_dataset(mpas_ds_path, mpas_ds_path) + + for dim in ugrid_dims: + assert dim in uxds_remap.dims + +def test_get_dual(): + """Tests the creation of the dual mesh on a data set.""" + uxds = ux.open_dataset(gridfile_ne30, dsfile_var2_ne30) + dual = uxds.get_dual() + + assert isinstance(dual, UxDataset) + assert len(uxds.data_vars) == len(dual.data_vars) + +# Uncomment the following test if you want to include it, ensuring you handle potential failures. +# def test_read_from_https(): +# """Tests reading a dataset from a HTTPS link.""" +# import requests +# +# small_file_480km = requests.get( +# "https://web.lcrc.anl.gov/public/e3sm/inputdata/share/meshes/mpas/ocean/oQU480.230422.nc" +# ).content +# +# ds_small_480km = ux.open_dataset(small_file_480km, small_file_480km) +# assert isinstance(ds_small_480km, ux.core.dataset.UxDataset) diff --git a/test/test_esmf.py b/test/test_esmf.py index 7719e0701..fac7ca58f 100644 --- a/test/test_esmf.py +++ b/test/test_esmf.py @@ -1,28 +1,20 @@ import uxarray as ux - import os from pathlib import Path - +import pytest current_path = Path(os.path.dirname(os.path.realpath(__file__))) - esmf_ne30_grid_path = current_path / 'meshfiles' / "esmf" / "ne30" / "ne30pg3.grid.nc" esmf_ne30_data_path = current_path / 'meshfiles' / "esmf" / "ne30" / "ne30pg3.data.nc" - - - def test_read_esmf(): """Tests the reading of an ESMF grid file and its encoding into the UGRID conventions.""" - uxgrid = ux.open_grid(esmf_ne30_grid_path) dims = ['n_node', 'n_face', 'n_max_face_nodes'] - coords = ['node_lon', 'node_lat', 'face_lon', 'face_lat'] - conns = ['face_node_connectivity', 'n_nodes_per_face'] for dim in dims: @@ -37,10 +29,8 @@ def test_read_esmf(): def test_read_esmf_dataset(): """Tests the constructing of a UxDataset from an ESMF Grid and Data File.""" - uxds = ux.open_dataset(esmf_ne30_grid_path, esmf_ne30_data_path) - dims = ['n_node', 'n_face'] for dim in dims: diff --git a/test/test_exodus.py b/test/test_exodus.py index 744053620..020aea3b1 100644 --- a/test/test_exodus.py +++ b/test/test_exodus.py @@ -1,49 +1,45 @@ import os import numpy as np - -from unittest import TestCase from pathlib import Path - +import pytest import uxarray as ux from uxarray.constants import INT_DTYPE, INT_FILL_VALUE current_path = Path(os.path.dirname(os.path.realpath(__file__))) - -class TestExodus(TestCase): - - exo_filename = current_path / "meshfiles" / "exodus" / "outCSne8" / "outCSne8.g" - exo2_filename = current_path / "meshfiles" / "exodus" / "mixed" / "mixed.exo" - - def test_read_exodus(self): - """Read an exodus file and writes a exodus file.""" - - uxgrid = ux.open_grid(self.exo_filename) - pass - - def test_init_verts(self): - """Create a uxarray grid from vertices and saves a 1 face exodus - file.""" - verts = [[[0, 0], [2, 0], [0, 2], [2, 2]]] - uxgrid = ux.open_grid(verts) - - def test_encode_exodus(self): - """Read a UGRID dataset and encode that as an Exodus format.""" - - def test_mixed_exodus(self): - """Read/write an exodus file with two types of faces (triangle and - quadrilaterals) and writes a ugrid file.""" - - uxgrid = ux.open_grid(self.exo2_filename) - - uxgrid.encode_as("UGRID") - uxgrid.encode_as("Exodus") - - def test_standardized_dtype_and_fill(self): - """Test to see if Mesh2_Face_Nodes uses the expected integer datatype - and expected fill value as set in constants.py.""" - - uxgrid = ux.open_grid(self.exo2_filename) - - assert uxgrid.face_node_connectivity.dtype == INT_DTYPE - assert uxgrid.face_node_connectivity._FillValue == INT_FILL_VALUE +exo_filename = current_path / "meshfiles" / "exodus" / "outCSne8" / "outCSne8.g" +exo2_filename = current_path / "meshfiles" / "exodus" / "mixed" / "mixed.exo" + +def test_read_exodus(): + """Read an exodus file and writes a exodus file.""" + uxgrid = ux.open_grid(exo_filename) + # Add assertions or checks as needed + assert uxgrid is not None # Example assertion + +def test_init_verts(): + """Create a uxarray grid from vertices and saves a 1 face exodus file.""" + verts = [[[0, 0], [2, 0], [0, 2], [2, 2]]] + uxgrid = ux.open_grid(verts) + # Add assertions or checks as needed + assert uxgrid is not None # Example assertion + +def test_encode_exodus(): + """Read a UGRID dataset and encode that as an Exodus format.""" + uxgrid = ux.open_grid(exo_filename) + # Add encoding logic and assertions as needed + pass # Placeholder for actual implementation + +def test_mixed_exodus(): + """Read/write an exodus file with two types of faces (triangle and quadrilaterals) and writes a ugrid file.""" + uxgrid = ux.open_grid(exo2_filename) + + uxgrid.encode_as("UGRID") + uxgrid.encode_as("Exodus") + # Add assertions or checks as needed + +def test_standardized_dtype_and_fill(): + """Test to see if Mesh2_Face_Nodes uses the expected integer datatype and expected fill value as set in constants.py.""" + uxgrid = ux.open_grid(exo2_filename) + + assert uxgrid.face_node_connectivity.dtype == INT_DTYPE + assert uxgrid.face_node_connectivity._FillValue == INT_FILL_VALUE diff --git a/test/test_from_points.py b/test/test_from_points.py index 7bd8d53d6..f7f42da46 100644 --- a/test/test_from_points.py +++ b/test/test_from_points.py @@ -1,16 +1,12 @@ import uxarray as ux import os - import pytest - from pathlib import Path current_path = Path(os.path.dirname(os.path.realpath(__file__))) - grid_path = current_path / 'meshfiles' / "ugrid" / "outCSne30" / 'outCSne30.ug' - def test_spherical_delaunay(): uxgrid = ux.open_grid(grid_path) points_xyz = (uxgrid.node_x.values, uxgrid.node_y.values, uxgrid.node_z.values) @@ -25,7 +21,6 @@ def test_spherical_delaunay(): assert uxgrid_dt_xyz.triangular assert uxgrid_dt_latlon.triangular - def test_regional_delaunay(): uxgrid = ux.open_grid(grid_path) @@ -41,7 +36,6 @@ def test_regional_delaunay(): assert uxgrid_dt_xyz.triangular assert uxgrid_dt_latlon.triangular - def test_spherical_voronoi(): uxgrid = ux.open_grid(grid_path) points_xyz = (uxgrid.node_x.values, uxgrid.node_y.values, uxgrid.node_z.values) diff --git a/test/test_from_topology.py b/test/test_from_topology.py index 878b94b42..2edeea636 100644 --- a/test/test_from_topology.py +++ b/test/test_from_topology.py @@ -1,12 +1,9 @@ import uxarray as ux - -from uxarray.constants import INT_FILL_VALUE -import numpy.testing as nt import os - +import numpy.testing as nt import pytest - from pathlib import Path +from uxarray.constants import INT_FILL_VALUE current_path = Path(os.path.dirname(os.path.realpath(__file__))) @@ -16,45 +13,45 @@ current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" ] - - - def test_minimal_class_method(): """Tests the minimal required variables for constructing a grid using the from topology class method.""" - for grid_path in GRID_PATHS: uxgrid = ux.open_grid(grid_path) - uxgrid_ft = ux.Grid.from_topology(node_lon=uxgrid.node_lon.values, - node_lat=uxgrid.node_lat.values, - face_node_connectivity=uxgrid.face_node_connectivity.values, - fill_value=INT_FILL_VALUE, - start_index=0) + uxgrid_ft = ux.Grid.from_topology( + node_lon=uxgrid.node_lon.values, + node_lat=uxgrid.node_lat.values, + face_node_connectivity=uxgrid.face_node_connectivity.values, + fill_value=INT_FILL_VALUE, + start_index=0 + ) nt.assert_array_equal(uxgrid.node_lon.values, uxgrid_ft.node_lon.values) nt.assert_array_equal(uxgrid.node_lat.values, uxgrid_ft.node_lat.values) nt.assert_array_equal(uxgrid.face_node_connectivity.values, uxgrid_ft.face_node_connectivity.values) - def test_minimal_api(): """Tests the minimal required variables for constructing a grid using the ``ux.open_dataset`` method.""" - for grid_path in GRID_PATHS: uxgrid = ux.open_grid(grid_path) - uxgrid_ft = ux.Grid.from_topology(node_lon=uxgrid.node_lon.values, - node_lat=uxgrid.node_lat.values, - face_node_connectivity=uxgrid.face_node_connectivity.values, - fill_value=INT_FILL_VALUE, - start_index=0) - - grid_topology = {'node_lon': uxgrid.node_lon.values, - 'node_lat': uxgrid.node_lat.values, - 'face_node_connectivity': uxgrid.face_node_connectivity.values, - 'fill_value': INT_FILL_VALUE, - 'start_index': 0} + uxgrid_ft = ux.Grid.from_topology( + node_lon=uxgrid.node_lon.values, + node_lat=uxgrid.node_lat.values, + face_node_connectivity=uxgrid.face_node_connectivity.values, + fill_value=INT_FILL_VALUE, + start_index=0 + ) + + grid_topology = { + 'node_lon': uxgrid.node_lon.values, + 'node_lat': uxgrid.node_lat.values, + 'face_node_connectivity': uxgrid.face_node_connectivity.values, + 'fill_value': INT_FILL_VALUE, + 'start_index': 0 + } uxgrid_ft = ux.open_grid(grid_topology) @@ -62,24 +59,23 @@ def test_minimal_api(): nt.assert_array_equal(uxgrid.node_lat.values, uxgrid_ft.node_lat.values) nt.assert_array_equal(uxgrid.face_node_connectivity.values, uxgrid_ft.face_node_connectivity.values) - def test_dataset(): uxds = ux.open_dataset(GRID_PATHS[0], GRID_PATHS[0]) - grid_topology = {'node_lon': uxds.uxgrid.node_lon.values, - 'node_lat': uxds.uxgrid.node_lat.values, - 'face_node_connectivity': uxds.uxgrid.face_node_connectivity.values, - 'fill_value': INT_FILL_VALUE, - 'start_index': 0, - "dims_dict" : {"nVertices": "n_node"}} - + grid_topology = { + 'node_lon': uxds.uxgrid.node_lon.values, + 'node_lat': uxds.uxgrid.node_lat.values, + 'face_node_connectivity': uxds.uxgrid.face_node_connectivity.values, + 'fill_value': INT_FILL_VALUE, + 'start_index': 0, + 'dims_dict': {"nVertices": "n_node"} + } uxds_ft = ux.open_grid(grid_topology, GRID_PATHS[1]) uxgrid = uxds.uxgrid uxgrid_ft = uxds_ft - nt.assert_array_equal(uxgrid.node_lon.values, uxgrid_ft.node_lon.values) nt.assert_array_equal(uxgrid.node_lat.values, uxgrid_ft.node_lat.values) nt.assert_array_equal(uxgrid.face_node_connectivity.values, uxgrid_ft.face_node_connectivity.values) diff --git a/test/test_geometry.py b/test/test_geometry.py index 05e0930fc..7bf21cf6b 100644 --- a/test/test_geometry.py +++ b/test/test_geometry.py @@ -2,8 +2,8 @@ import numpy as np import numpy.testing as nt import xarray as xr +import pytest -from unittest import TestCase from pathlib import Path import uxarray as ux @@ -34,1463 +34,1097 @@ # List of grid files to test grid_files_latlonBound = [grid_quad_hex, grid_geoflow, gridfile_CSne8, grid_mpas] +def test_antimeridian_crossing(): + verts = [[[-170, 40], [180, 30], [165, 25], [-170, 20]]] -class TestAntimeridian(TestCase): + uxgrid = ux.open_grid(verts, latlon=True) - def test_crossing(self): - verts = [[[-170, 40], [180, 30], [165, 25], [-170, 20]]] + gdf = uxgrid.to_geodataframe(periodic_elements='ignore') - uxgrid = ux.open_grid(verts, latlon=True) + assert len(uxgrid.antimeridian_face_indices) == 1 + assert len(gdf['geometry']) == 1 - gdf = uxgrid.to_geodataframe(periodic_elements='ignore') +def test_antimeridian_point_on(): + verts = [[[-170, 40], [180, 30], [-170, 20]]] - assert len(uxgrid.antimeridian_face_indices) == 1 + uxgrid = ux.open_grid(verts, latlon=True) - assert len(gdf['geometry']) == 1 + assert len(uxgrid.antimeridian_face_indices) == 1 - def test_point_on(self): - verts = [[[-170, 40], [180, 30], [-170, 20]]] +def test_linecollection_execution(): + uxgrid = ux.open_grid(gridfile_CSne8) + lines = uxgrid.to_linecollection() - uxgrid = ux.open_grid(verts, latlon=True) +def test_pole_point_inside_polygon_from_vertice_north(): + vertices = [[0.5, 0.5, 0.5], [-0.5, 0.5, 0.5], [-0.5, -0.5, 0.5], + [0.5, -0.5, 0.5]] - assert len(uxgrid.antimeridian_face_indices) == 1 + for i, vertex in enumerate(vertices): + float_vertex = [float(coord) for coord in vertex] + vertices[i] = _normalize_xyz(*float_vertex) + face_edge_cart = np.array([[vertices[0], vertices[1]], + [vertices[1], vertices[2]], + [vertices[2], vertices[3]], + [vertices[3], vertices[0]]]) -class TestLineCollection(TestCase): + result = _pole_point_inside_polygon_cartesian('North', face_edge_cart) + assert result, "North pole should be inside the polygon" - def test_linecollection_execution(self): - uxgrid = ux.open_grid(gridfile_CSne8) - lines = uxgrid.to_linecollection() + result = _pole_point_inside_polygon_cartesian('South', face_edge_cart) + assert not result, "South pole should not be inside the polygon" +def test_pole_point_inside_polygon_from_vertice_south(): + vertices = [[0.5, 0.5, -0.5], [-0.5, 0.5, -0.5], [0.0, 0.0, -1.0]] -class TestPredicate(TestCase): + for i, vertex in enumerate(vertices): + float_vertex = [float(coord) for coord in vertex] + vertices[i] = _normalize_xyz(*float_vertex) - def test_pole_point_inside_polygon_from_vertice_north(self): - # Define a face as a list of vertices on the unit sphere - # Here, we're defining a square like structure around the North pole - vertices = [[0.5, 0.5, 0.5], [-0.5, 0.5, 0.5], [-0.5, -0.5, 0.5], - [0.5, -0.5, 0.5]] + face_edge_cart = np.array([[vertices[0], vertices[1]], + [vertices[1], vertices[2]], + [vertices[2], vertices[0]]]) - # Normalize the vertices to ensure they lie on the unit sphere - for i, vertex in enumerate(vertices): - float_vertex = [float(coord) for coord in vertex] - vertices[i] = _normalize_xyz(*float_vertex) + result = _pole_point_inside_polygon_cartesian('North', face_edge_cart) + assert not result, "North pole should not be inside the polygon" - # Create face_edge_cart from the vertices - face_edge_cart = np.array([[vertices[0], vertices[1]], - [vertices[1], vertices[2]], - [vertices[2], vertices[3]], - [vertices[3], vertices[0]]]) + result = _pole_point_inside_polygon_cartesian('South', face_edge_cart) + assert result, "South pole should be inside the polygon" - # Check if the North pole is inside the polygon - result = _pole_point_inside_polygon_cartesian( - 'North', face_edge_cart) - self.assertTrue(result, "North pole should be inside the polygon") +def test_pole_point_inside_polygon_from_vertice_pole(): + vertices = [[0, 0, 1], [-0.5, 0.5, 0.5], [-0.5, -0.5, 0.5], + [0.5, -0.5, 0.5]] - # Check if the South pole is inside the polygon - result = _pole_point_inside_polygon_cartesian( - 'South', face_edge_cart) - self.assertFalse(result, "South pole should not be inside the polygon") + for i, vertex in enumerate(vertices): + float_vertex = [float(coord) for coord in vertex] + vertices[i] = _normalize_xyz(*float_vertex) - def test_pole_point_inside_polygon_from_vertice_south(self): - # Define a face as a list of vertices on the unit sphere - # Here, we're defining a square like structure around the south pole - vertices = [[0.5, 0.5, -0.5], [-0.5, 0.5, -0.5], [0.0, 0.0, -1.0]] + face_edge_cart = np.array([[vertices[0], vertices[1]], + [vertices[1], vertices[2]], + [vertices[2], vertices[3]], + [vertices[3], vertices[0]]]) - # Normalize the vertices to ensure they lie on the unit sphere - for i, vertex in enumerate(vertices): - float_vertex = [float(coord) for coord in vertex] - vertices[i] = _normalize_xyz(*float_vertex) + result = _pole_point_inside_polygon_cartesian('North', face_edge_cart) + assert result, "North pole should be inside the polygon" - # Create face_edge_cart from the vertices, since we are using the south pole, and want retrive the smaller face - # we need to reverse the order of the vertices - # Create face_edge_cart from the vertices - face_edge_cart = np.array([[vertices[0], vertices[1]], - [vertices[1], vertices[2]], - [vertices[2], vertices[0]]]) + result = _pole_point_inside_polygon_cartesian('South', face_edge_cart) + assert not result, "South pole should not be inside the polygon" - # Check if the North pole is inside the polygon - result = _pole_point_inside_polygon_cartesian( - 'North', face_edge_cart) - self.assertFalse(result, "North pole should not be inside the polygon") +def test_pole_point_inside_polygon_from_vertice_cross(): + vertices = [[0.6, -0.3, 0.5], [0.2, 0.2, -0.2], [-0.5, 0.1, -0.2], + [-0.1, -0.2, 0.2]] - # Check if the South pole is inside the polygon - result = _pole_point_inside_polygon_cartesian( - 'South', face_edge_cart) - self.assertTrue(result, "South pole should be inside the polygon") + for i, vertex in enumerate(vertices): + float_vertex = [float(coord) for coord in vertex] + vertices[i] = _normalize_xyz(*float_vertex) - def test_pole_point_inside_polygon_from_vertice_pole(self): - # Define a face as a list of vertices on the unit sphere - # Here, we're defining a square like structure that pole is on the edge - vertices = [[0, 0, 1], [-0.5, 0.5, 0.5], [-0.5, -0.5, 0.5], - [0.5, -0.5, 0.5]] + face_edge_cart = np.array([[vertices[0], vertices[1]], + [vertices[1], vertices[2]], + [vertices[2], vertices[3]], + [vertices[3], vertices[0]]]) - # Normalize the vertices to ensure they lie on the unit sphere - for i, vertex in enumerate(vertices): - float_vertex = [float(coord) for coord in vertex] - vertices[i] = _normalize_xyz(*float_vertex) + result = _pole_point_inside_polygon_cartesian('North', face_edge_cart) + assert result, "North pole should be inside the polygon" - # Create face_edge_cart from the vertices - face_edge_cart = np.array([[vertices[0], vertices[1]], - [vertices[1], vertices[2]], - [vertices[2], vertices[3]], - [vertices[3], vertices[0]]]) - # Check if the North pole is inside the polygon - result = _pole_point_inside_polygon_cartesian( - 'North', face_edge_cart) - self.assertTrue(result, "North pole should be inside the polygon") - - # Check if the South pole is inside the polygon - result = _pole_point_inside_polygon_cartesian( - 'South', face_edge_cart) - self.assertFalse(result, "South pole should not be inside the polygon") - - def test_pole_point_inside_polygon_from_vertice_cross(self): - # Define a face that crosses the equator and ecompasses the North pole - vertices = [[0.6, -0.3, 0.5], [0.2, 0.2, -0.2], [-0.5, 0.1, -0.2], - [-0.1, -0.2, 0.2]] - - # Normalize the vertices to ensure they lie on the unit sphere - for i, vertex in enumerate(vertices): - float_vertex = [float(coord) for coord in vertex] - vertices[i] = _normalize_xyz(*float_vertex) - - # Create face_edge_cart from the vertices - face_edge_cart = np.array([[vertices[0], vertices[1]], - [vertices[1], vertices[2]], - [vertices[2], vertices[3]], - [vertices[3], vertices[0]]]) - - # Check if the North pole is inside the polygon - result = _pole_point_inside_polygon_cartesian( - 'North', face_edge_cart) - self.assertTrue(result, "North pole should be inside the polygon") - - -class TestLatlonBoundUtils(TestCase): - - def _max_latitude_rad_iterative(self, gca_cart): - """Calculate the maximum latitude of a great circle arc defined by two - points. - - Parameters - ---------- - gca_cart : numpy.ndarray - An array containing two 3D vectors that define a great circle arc. - - Returns - ------- - float - The maximum latitude of the great circle arc in radians. - - Raises - ------ - ValueError - If the input vectors are not valid 2-element lists or arrays. - - Notes - ----- - The method divides the great circle arc into subsections, iteratively refining the subsection of interest - until the maximum latitude is found within a specified tolerance. - """ - - # Convert input vectors to radians and Cartesian coordinates - - v1_cart, v2_cart = gca_cart - b_lonlat = _xyz_to_lonlat_rad(*v1_cart.tolist()) - c_lonlat = _xyz_to_lonlat_rad(*v2_cart.tolist()) - - # Initialize variables for the iterative process - v_temp = ac_utils.cross_fma(v1_cart, v2_cart) - v0 = ac_utils.cross_fma(v_temp, v1_cart) +def _max_latitude_rad_iterative(gca_cart): + """Calculate the maximum latitude of a great circle arc defined by two + points. + + Parameters + ---------- + gca_cart : numpy.ndarray + An array containing two 3D vectors that define a great circle arc. + + Returns + ------- + float + The maximum latitude of the great circle arc in radians. + + Raises + ------ + ValueError + If the input vectors are not valid 2-element lists or arrays. + + Notes + ----- + The method divides the great circle arc into subsections, iteratively refining the subsection of interest + until the maximum latitude is found within a specified tolerance. + """ + + # Convert input vectors to radians and Cartesian coordinates + + v1_cart, v2_cart = gca_cart + b_lonlat = _xyz_to_lonlat_rad(*v1_cart.tolist()) + c_lonlat = _xyz_to_lonlat_rad(*v2_cart.tolist()) + + # Initialize variables for the iterative process + v_temp = ac_utils.cross_fma(v1_cart, v2_cart) + v0 = ac_utils.cross_fma(v_temp, v1_cart) + v0 = _normalize_xyz(*v0.tolist()) + max_section = [v1_cart, v2_cart] + + # Iteratively find the maximum latitude + while np.abs(b_lonlat[1] - c_lonlat[1]) >= ERROR_TOLERANCE or np.abs( + b_lonlat[0] - c_lonlat[0]) >= ERROR_TOLERANCE: + max_lat = -np.pi + v_b, v_c = max_section + angle_v1_v2_rad = ux.grid.arcs._angle_of_2_vectors(v_b, v_c) + v0 = ac_utils.cross_fma(v_temp, v_b) v0 = _normalize_xyz(*v0.tolist()) - max_section = [v1_cart, v2_cart] - - # Iteratively find the maximum latitude - while np.abs(b_lonlat[1] - c_lonlat[1]) >= ERROR_TOLERANCE or np.abs( - b_lonlat[0] - c_lonlat[0]) >= ERROR_TOLERANCE: - max_lat = -np.pi - v_b, v_c = max_section - angle_v1_v2_rad = ux.grid.arcs._angle_of_2_vectors(v_b, v_c) - v0 = ac_utils.cross_fma(v_temp, v_b) - v0 = _normalize_xyz(*v0.tolist()) - avg_angle_rad = angle_v1_v2_rad / 10.0 - - for i in range(10): - angle_rad_prev = avg_angle_rad * i - angle_rad_next = angle_rad_prev + avg_angle_rad if i < 9 else angle_v1_v2_rad - w1_new = np.cos(angle_rad_prev) * v_b + np.sin( - angle_rad_prev) * np.array(v0) - w2_new = np.cos(angle_rad_next) * v_b + np.sin( - angle_rad_next) * np.array(v0) - w1_lonlat = _xyz_to_lonlat_rad( - *w1_new.tolist()) - w2_lonlat = _xyz_to_lonlat_rad( - *w2_new.tolist()) - - w1_lonlat = np.asarray(w1_lonlat) - w2_lonlat = np.asarray(w2_lonlat) - - # Adjust latitude boundaries to avoid error accumulation - if i == 0: - w1_lonlat[1] = b_lonlat[1] - elif i >= 9: - w2_lonlat[1] = c_lonlat[1] - - # Update maximum latitude and section if needed - max_lat = max(max_lat, w1_lonlat[1], w2_lonlat[1]) - if np.abs(w2_lonlat[1] - - w1_lonlat[1]) <= ERROR_TOLERANCE or w1_lonlat[ - 1] == max_lat == w2_lonlat[1]: - max_section = [w1_new, w2_new] - break - if np.abs(max_lat - w1_lonlat[1]) <= ERROR_TOLERANCE: - max_section = [w1_new, w2_new] if i != 0 else [v_b, w2_new] - elif np.abs(max_lat - w2_lonlat[1]) <= ERROR_TOLERANCE: - max_section = [w1_new, w2_new] if i != 9 else [w1_new, v_c] - - # Update longitude and latitude for the next iteration - b_lonlat = _xyz_to_lonlat_rad( - *max_section[0].tolist()) - c_lonlat = _xyz_to_lonlat_rad( - *max_section[1].tolist()) - - return np.average([b_lonlat[1], c_lonlat[1]]) - - def _min_latitude_rad_iterative(self, gca_cart): - """Calculate the minimum latitude of a great circle arc defined by two - points. - - Parameters - ---------- - gca_cart : numpy.ndarray - An array containing two 3D vectors that define a great circle arc. - - Returns - ------- - float - The minimum latitude of the great circle arc in radians. - - Raises - ------ - ValueError - If the input vectors are not valid 2-element lists or arrays. - - Notes - ----- - The method divides the great circle arc into subsections, iteratively refining the subsection of interest - until the minimum latitude is found within a specified tolerance. - """ - - # Convert input vectors to radians and Cartesian coordinates - v1_cart, v2_cart = gca_cart - b_lonlat = _xyz_to_lonlat_rad(*v1_cart.tolist()) - c_lonlat = _xyz_to_lonlat_rad(*v2_cart.tolist()) - - # Initialize variables for the iterative process - v_temp = ac_utils.cross_fma(v1_cart, v2_cart) - v0 = ac_utils.cross_fma(v_temp, v1_cart) + avg_angle_rad = angle_v1_v2_rad / 10.0 + + for i in range(10): + angle_rad_prev = avg_angle_rad * i + angle_rad_next = angle_rad_prev + avg_angle_rad if i < 9 else angle_v1_v2_rad + w1_new = np.cos(angle_rad_prev) * v_b + np.sin( + angle_rad_prev) * np.array(v0) + w2_new = np.cos(angle_rad_next) * v_b + np.sin( + angle_rad_next) * np.array(v0) + w1_lonlat = _xyz_to_lonlat_rad( + *w1_new.tolist()) + w2_lonlat = _xyz_to_lonlat_rad( + *w2_new.tolist()) + + w1_lonlat = np.asarray(w1_lonlat) + w2_lonlat = np.asarray(w2_lonlat) + + # Adjust latitude boundaries to avoid error accumulation + if i == 0: + w1_lonlat[1] = b_lonlat[1] + elif i >= 9: + w2_lonlat[1] = c_lonlat[1] + + # Update maximum latitude and section if needed + max_lat = max(max_lat, w1_lonlat[1], w2_lonlat[1]) + if np.abs(w2_lonlat[1] - + w1_lonlat[1]) <= ERROR_TOLERANCE or w1_lonlat[ + 1] == max_lat == w2_lonlat[1]: + max_section = [w1_new, w2_new] + break + if np.abs(max_lat - w1_lonlat[1]) <= ERROR_TOLERANCE: + max_section = [w1_new, w2_new] if i != 0 else [v_b, w2_new] + elif np.abs(max_lat - w2_lonlat[1]) <= ERROR_TOLERANCE: + max_section = [w1_new, w2_new] if i != 9 else [w1_new, v_c] + + # Update longitude and latitude for the next iteration + b_lonlat = _xyz_to_lonlat_rad( + *max_section[0].tolist()) + c_lonlat = _xyz_to_lonlat_rad( + *max_section[1].tolist()) + + return np.average([b_lonlat[1], c_lonlat[1]]) + +def _min_latitude_rad_iterative(gca_cart): + """Calculate the minimum latitude of a great circle arc defined by two + points. + + Parameters + ---------- + gca_cart : numpy.ndarray + An array containing two 3D vectors that define a great circle arc. + + Returns + ------- + float + The minimum latitude of the great circle arc in radians. + + Raises + ------ + ValueError + If the input vectors are not valid 2-element lists or arrays. + + Notes + ----- + The method divides the great circle arc into subsections, iteratively refining the subsection of interest + until the minimum latitude is found within a specified tolerance. + """ + + # Convert input vectors to radians and Cartesian coordinates + v1_cart, v2_cart = gca_cart + b_lonlat = _xyz_to_lonlat_rad(*v1_cart.tolist()) + c_lonlat = _xyz_to_lonlat_rad(*v2_cart.tolist()) + + # Initialize variables for the iterative process + v_temp = ac_utils.cross_fma(v1_cart, v2_cart) + v0 = ac_utils.cross_fma(v_temp, v1_cart) + v0 = np.array(_normalize_xyz(*v0.tolist())) + min_section = [v1_cart, v2_cart] + + # Iteratively find the minimum latitude + while np.abs(b_lonlat[1] - c_lonlat[1]) >= ERROR_TOLERANCE or np.abs( + b_lonlat[0] - c_lonlat[0]) >= ERROR_TOLERANCE: + min_lat = np.pi + v_b, v_c = min_section + angle_v1_v2_rad = ux.grid.arcs._angle_of_2_vectors(v_b, v_c) + v0 = ac_utils.cross_fma(v_temp, v_b) v0 = np.array(_normalize_xyz(*v0.tolist())) - min_section = [v1_cart, v2_cart] - - # Iteratively find the minimum latitude - while np.abs(b_lonlat[1] - c_lonlat[1]) >= ERROR_TOLERANCE or np.abs( - b_lonlat[0] - c_lonlat[0]) >= ERROR_TOLERANCE: - min_lat = np.pi - v_b, v_c = min_section - angle_v1_v2_rad = ux.grid.arcs._angle_of_2_vectors(v_b, v_c) - v0 = ac_utils.cross_fma(v_temp, v_b) - v0 = np.array(_normalize_xyz(*v0.tolist())) - avg_angle_rad = angle_v1_v2_rad / 10.0 - - for i in range(10): - angle_rad_prev = avg_angle_rad * i - angle_rad_next = angle_rad_prev + avg_angle_rad if i < 9 else angle_v1_v2_rad - w1_new = np.cos(angle_rad_prev) * v_b + np.sin( - angle_rad_prev) * v0 - w2_new = np.cos(angle_rad_next) * v_b + np.sin( - angle_rad_next) * v0 - w1_lonlat = _xyz_to_lonlat_rad( - *w1_new.tolist()) - w2_lonlat = _xyz_to_lonlat_rad( - *w2_new.tolist()) - - w1_lonlat = np.asarray(w1_lonlat) - w2_lonlat = np.asarray(w2_lonlat) - - # Adjust latitude boundaries to avoid error accumulation - if i == 0: - w1_lonlat[1] = b_lonlat[1] - elif i >= 9: - w2_lonlat[1] = c_lonlat[1] - - # Update minimum latitude and section if needed - min_lat = min(min_lat, w1_lonlat[1], w2_lonlat[1]) - if np.abs(w2_lonlat[1] - - w1_lonlat[1]) <= ERROR_TOLERANCE or w1_lonlat[ - 1] == min_lat == w2_lonlat[1]: - min_section = [w1_new, w2_new] - break - if np.abs(min_lat - w1_lonlat[1]) <= ERROR_TOLERANCE: - min_section = [w1_new, w2_new] if i != 0 else [v_b, w2_new] - elif np.abs(min_lat - w2_lonlat[1]) <= ERROR_TOLERANCE: - min_section = [w1_new, w2_new] if i != 9 else [w1_new, v_c] - - # Update longitude and latitude for the next iteration - b_lonlat = _xyz_to_lonlat_rad( - *min_section[0].tolist()) - c_lonlat = _xyz_to_lonlat_rad( - *min_section[1].tolist()) - - return np.average([b_lonlat[1], c_lonlat[1]]) - - def test_extreme_gca_latitude_max(self): - # Define a great circle arc that is symmetrical around 0 degrees longitude - gca_cart = np.array([ - _normalize_xyz(*[0.5, 0.5, 0.5]), - _normalize_xyz(*[-0.5, 0.5, 0.5]) - ]) - - # Calculate the maximum latitude - max_latitude = _extreme_gca_latitude_cartesian(gca_cart, 'max') - - # Check if the maximum latitude is correct - expected_max_latitude = self._max_latitude_rad_iterative(gca_cart) - self.assertAlmostEqual(max_latitude, - expected_max_latitude, - delta=ERROR_TOLERANCE) - - # Define a great circle arc in 3D space - gca_cart = np.array([[0.0, 0.0, 1.0], [1.0, 0.0, 0.0]]) - - # Calculate the maximum latitude - max_latitude = _extreme_gca_latitude_cartesian(gca_cart, 'max') - - # Check if the maximum latitude is correct - expected_max_latitude = np.pi / 2 # 90 degrees in radians - self.assertAlmostEqual(max_latitude, - expected_max_latitude, - delta=ERROR_TOLERANCE) - - def test_extreme_gca_latitude_max_short(self): - # Define a great circle arc in 3D space that has a small span - gca_cart = np.array([[0.65465367, -0.37796447, -0.65465367], [0.6652466, -0.33896007, -0.6652466]]) - - # Calculate the maximum latitude - max_latitude = _extreme_gca_latitude_cartesian(gca_cart, 'max') - - # Check if the maximum latitude is correct - expected_max_latitude = self._max_latitude_rad_iterative(gca_cart) - self.assertAlmostEqual(max_latitude, - expected_max_latitude, - delta=ERROR_TOLERANCE) - - def test_extreme_gca_latitude_min(self): - # Define a great circle arc that is symmetrical around 0 degrees longitude - gca_cart = np.array([ - _normalize_xyz(*[0.5, 0.5, -0.5]), - _normalize_xyz(*[-0.5, 0.5, -0.5]) - ]) - - # Calculate the minimum latitude - min_latitude = _extreme_gca_latitude_cartesian(gca_cart, 'min') - - # Check if the minimum latitude is correct - expected_min_latitude = self._min_latitude_rad_iterative(gca_cart) - self.assertAlmostEqual(min_latitude, - expected_min_latitude, - delta=ERROR_TOLERANCE) - - # Define a great circle arc in 3D space - gca_cart = np.array([[0.0, 0.0, -1.0], [1.0, 0.0, 0.0]]) - - # Calculate the minimum latitude - min_latitude = _extreme_gca_latitude_cartesian(gca_cart, 'min') - - # Check if the minimum latitude is correct - expected_min_latitude = -np.pi / 2 # 90 degrees in radians - self.assertAlmostEqual(min_latitude, - expected_min_latitude, - delta=ERROR_TOLERANCE) - - def test_get_latlonbox_width(self): - # Define a great circle arc that is not wrapping around the meridian - gca_latlon = np.array([[0.0, 0.0], [0.0, 3.0]]) - - # Calculate the width of the latlonbox - width = ux.grid.geometry._get_latlonbox_width(gca_latlon) - self.assertEqual(width, 3.0) - - # Define a great circle arc that is not wrapping around the meridian - gca_latlon = np.array([[0.0, 0.0], [2 * np.pi - 1.0, 1.0]]) - width = ux.grid.geometry._get_latlonbox_width(gca_latlon) - self.assertEqual(width, 2.0) - - def test_insert_pt_in_latlonbox_non_periodic(self): - old_box = np.array([[0.1, 0.2], [0.3, 0.4]]) # Radians - new_pt = np.array([0.15, 0.35]) - expected = np.array([[0.1, 0.2], [0.3, 0.4]]) - result = ux.grid.geometry.insert_pt_in_latlonbox( - old_box, new_pt, False) - np.testing.assert_array_equal(result, expected) - - def test_insert_pt_in_latlonbox_periodic(self): - old_box = np.array([[0.1, 0.2], [6.0, 0.1]]) # Radians, periodic - new_pt = np.array([0.15, 6.2]) - expected = np.array([[0.1, 0.2], [6.0, 0.1]]) - result = ux.grid.geometry.insert_pt_in_latlonbox(old_box, new_pt, True) - np.testing.assert_array_equal(result, expected) - - def test_insert_pt_in_latlonbox_pole(self): - old_box = np.array([[0.1, 0.2], [0.3, 0.4]]) - new_pt = np.array([np.pi / 2, np.nan]) # Pole point - expected = np.array([[0.1, np.pi / 2], [0.3, 0.4]]) - result = ux.grid.geometry.insert_pt_in_latlonbox(old_box, new_pt) - np.testing.assert_array_equal(result, expected) - - def test_insert_pt_in_empty_state(self): - old_box = np.array([[np.nan, np.nan], - [np.nan, np.nan]]) # Empty state - new_pt = np.array([0.15, 0.35]) - expected = np.array([[0.15, 0.15], [0.35, 0.35]]) - result = ux.grid.geometry.insert_pt_in_latlonbox(old_box, new_pt) - np.testing.assert_array_equal(result, expected) - - -class TestLatlonBoundsGCA(TestCase): - - def _get_cartesian_face_edge_nodes_testcase_helper( - self, face_nodes_ind, face_edges_ind, edge_nodes_grid, node_x, node_y, node_z - ): - """This function is only used to help generating the testcase and - should not be used in the actual implementation. Construct an array to - hold the edge Cartesian coordinates connectivity for a face in a grid. - - Parameters - ---------- - face_nodes_ind : np.ndarray, shape (n_nodes,) - The ith entry of Grid.face_node_connectivity, where n_nodes is the number of nodes in the face. - face_edges_ind : np.ndarray, shape (n_edges,) - The ith entry of Grid.face_edge_connectivity, where n_edges is the number of edges in the face. - edge_nodes_grid : np.ndarray, shape (n_edges, 2) - The entire Grid.edge_node_connectivity, where n_edges is the total number of edges in the grid. - node_x : np.ndarray, shape (n_nodes,) - The values of Grid.node_x, where n_nodes_total is the total number of nodes in the grid. - node_y : np.ndarray, shape (n_nodes,) - The values of Grid.node_y. - node_z : np.ndarray, shape (n_nodes,) - The values of Grid.node_z. - - Returns - ------- - face_edges : np.ndarray, shape (n_edges, 2, 3) - Face edge connectivity in Cartesian coordinates, where n_edges is the number of edges for the specific face. - - Notes - ----- - - The function assumes that the inputs are well-formed and correspond to the same face. - - The output array contains the Cartesian coordinates for each edge of the face. - """ - - # Create a mask that is True for all values not equal to INT_FILL_VALUE - mask = face_edges_ind != INT_FILL_VALUE - - # Use the mask to select only the elements not equal to INT_FILL_VALUE - valid_edges = face_edges_ind[mask] - face_edges = edge_nodes_grid[valid_edges] - - # Ensure counter-clockwise order of edge nodes - # Start with the first two nodes - face_edges[0] = [face_nodes_ind[0], face_nodes_ind[1]] - - for idx in range(1, len(face_edges)): - if face_edges[idx][0] != face_edges[idx - 1][1]: - # Swap the node index in this edge if not in counter-clockwise order - face_edges[idx] = face_edges[idx][::-1] - - # Fetch coordinates for each node in the face edges - cartesian_coordinates = np.array( - [ - [[node_x[node], node_y[node], node_z[node]] for node in edge] - for edge in face_edges - ] - ) - - return cartesian_coordinates - - def _get_lonlat_rad_face_edge_nodes_testcase_helper( - self, face_nodes_ind, face_edges_ind, edge_nodes_grid, node_lon, node_lat - ): - """This function is only used to help generating the testcase and - should not be used in the actual implementation. Construct an array to - hold the edge lat lon in radian connectivity for a face in a grid. - - Parameters - ---------- - face_nodes_ind : np.ndarray, shape (n_nodes,) - The ith entry of Grid.face_node_connectivity, where n_nodes is the number of nodes in the face. - face_edges_ind : np.ndarray, shape (n_edges,) - The ith entry of Grid.face_edge_connectivity, where n_edges is the number of edges in the face. - edge_nodes_grid : np.ndarray, shape (n_edges, 2) - The entire Grid.edge_node_connectivity, where n_edges is the total number of edges in the grid. - node_lon : np.ndarray, shape (n_nodes,) - The values of Grid.node_lon. - node_lat : np.ndarray, shape (n_nodes,) - The values of Grid.node_lat, where n_nodes_total is the total number of nodes in the grid. - Returns - ------- - face_edges : np.ndarray, shape (n_edges, 2, 3) - Face edge connectivity in Cartesian coordinates, where n_edges is the number of edges for the specific face. - - Notes - ----- - - The function assumes that the inputs are well-formed and correspond to the same face. - - The output array contains the Latitude and longitude coordinates in radian for each edge of the face. - """ - - # Create a mask that is True for all values not equal to INT_FILL_VALUE - mask = face_edges_ind != INT_FILL_VALUE - - # Use the mask to select only the elements not equal to INT_FILL_VALUE - valid_edges = face_edges_ind[mask] - face_edges = edge_nodes_grid[valid_edges] - - # Ensure counter-clockwise order of edge nodes - # Start with the first two nodes - face_edges[0] = [face_nodes_ind[0], face_nodes_ind[1]] - - for idx in range(1, len(face_edges)): - if face_edges[idx][0] != face_edges[idx - 1][1]: - # Swap the node index in this edge if not in counter-clockwise order - face_edges[idx] = face_edges[idx][::-1] - - # Fetch coordinates for each node in the face edges - lonlat_coordinates = np.array( - [ - [ - [ - np.mod(np.deg2rad(node_lon[node]), 2 * np.pi), - np.deg2rad(node_lat[node]), - ] - for node in edge - ] - for edge in face_edges - ] - ) - - return lonlat_coordinates - - def test_populate_bounds_normal(self): - # Generate a normal face that is not crossing the antimeridian or the poles - vertices_lonlat = [[10.0, 60.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] - vertices_lonlat = np.array(vertices_lonlat) - - # Convert everything into radians - vertices_rad = np.radians(vertices_lonlat) - - vertices_cart = np.vstack([_lonlat_rad_to_xyz(vertices_rad[:, 0], vertices_rad[:, 1])]).T - # vertices_cart = [_lonlat_rad_to_xyz(vertices_rad[0], vertices_rad[1])] - lat_max = max(np.deg2rad(60.0), - _extreme_gca_latitude_cartesian(np.array([vertices_cart[0], vertices_cart[3]]), extreme_type="max")) - lat_min = min(np.deg2rad(10.0), - _extreme_gca_latitude_cartesian(np.array([vertices_cart[1], vertices_cart[2]]), extreme_type="min")) - lon_min = np.deg2rad(10.0) - lon_max = np.deg2rad(50.0) - grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) - face_edges_connectivity_cartesian = self._get_cartesian_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_x.values, - grid.node_y.values, grid.node_z.values) - face_edges_connectivity_lonlat = self._get_lonlat_rad_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_lon.values, - grid.node_lat.values) - expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) - bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat) - nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) - - def test_populate_bounds_antimeridian(self): - # Generate a normal face that is crossing the antimeridian - vertices_lonlat = [[350, 60.0], [350, 10.0], [50.0, 10.0], [50.0, 60.0]] - vertices_lonlat = np.array(vertices_lonlat) - - # Convert everything into radians - vertices_rad = np.radians(vertices_lonlat) - vertices_cart = np.vstack([_lonlat_rad_to_xyz(vertices_rad[:, 0], vertices_rad[:, 1])]).T - lat_max = max(np.deg2rad(60.0), - _extreme_gca_latitude_cartesian(np.array([vertices_cart[0], vertices_cart[3]]), extreme_type="max")) - lat_min = min(np.deg2rad(10.0), - _extreme_gca_latitude_cartesian(np.array([vertices_cart[1], vertices_cart[2]]), extreme_type="min")) - lon_min = np.deg2rad(350.0) - lon_max = np.deg2rad(50.0) - grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) - face_edges_connectivity_cartesian = self._get_cartesian_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_x.values, - grid.node_y.values, grid.node_z.values) - face_edges_connectivity_lonlat = self._get_lonlat_rad_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_lon.values, - grid.node_lat.values) - expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) - bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat) - nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) - - def test_populate_bounds_equator(self): - # the face is touching the equator - face_edges_cart = np.array([ - [[0.99726469, -0.05226443, -0.05226443], [0.99862953, 0.0, -0.05233596]], - [[0.99862953, 0.0, -0.05233596], [1.0, 0.0, 0.0]], - [[1.0, 0.0, 0.0], [0.99862953, -0.05233596, 0.0]], - [[0.99862953, -0.05233596, 0.0], [0.99726469, -0.05226443, -0.05226443]] + avg_angle_rad = angle_v1_v2_rad / 10.0 + + for i in range(10): + angle_rad_prev = avg_angle_rad * i + angle_rad_next = angle_rad_prev + avg_angle_rad if i < 9 else angle_v1_v2_rad + w1_new = np.cos(angle_rad_prev) * v_b + np.sin( + angle_rad_prev) * v0 + w2_new = np.cos(angle_rad_next) * v_b + np.sin( + angle_rad_next) * v0 + w1_lonlat = _xyz_to_lonlat_rad( + *w1_new.tolist()) + w2_lonlat = _xyz_to_lonlat_rad( + *w2_new.tolist()) + + w1_lonlat = np.asarray(w1_lonlat) + w2_lonlat = np.asarray(w2_lonlat) + + # Adjust latitude boundaries to avoid error accumulation + if i == 0: + w1_lonlat[1] = b_lonlat[1] + elif i >= 9: + w2_lonlat[1] = c_lonlat[1] + + # Update minimum latitude and section if needed + min_lat = min(min_lat, w1_lonlat[1], w2_lonlat[1]) + if np.abs(w2_lonlat[1] - + w1_lonlat[1]) <= ERROR_TOLERANCE or w1_lonlat[ + 1] == min_lat == w2_lonlat[1]: + min_section = [w1_new, w2_new] + break + if np.abs(min_lat - w1_lonlat[1]) <= ERROR_TOLERANCE: + min_section = [w1_new, w2_new] if i != 0 else [v_b, w2_new] + elif np.abs(min_lat - w2_lonlat[1]) <= ERROR_TOLERANCE: + min_section = [w1_new, w2_new] if i != 9 else [w1_new, v_c] + + # Update longitude and latitude for the next iteration + b_lonlat = _xyz_to_lonlat_rad( + *min_section[0].tolist()) + c_lonlat = _xyz_to_lonlat_rad( + *min_section[1].tolist()) + + return np.average([b_lonlat[1], c_lonlat[1]]) + +def test_extreme_gca_latitude_max(): + gca_cart = np.array([ + _normalize_xyz(*[0.5, 0.5, 0.5]), + _normalize_xyz(*[-0.5, 0.5, 0.5]) + ]) + + max_latitude = _extreme_gca_latitude_cartesian(gca_cart, 'max') + expected_max_latitude = _max_latitude_rad_iterative(gca_cart) + assert np.isclose(max_latitude, expected_max_latitude, atol=ERROR_TOLERANCE) + + gca_cart = np.array([[0.0, 0.0, 1.0], [1.0, 0.0, 0.0]]) + max_latitude = _extreme_gca_latitude_cartesian(gca_cart, 'max') + expected_max_latitude = np.pi / 2 # 90 degrees in radians + assert np.isclose(max_latitude, expected_max_latitude, atol=ERROR_TOLERANCE) + +def test_extreme_gca_latitude_max_short(): + # Define a great circle arc in 3D space that has a small span + gca_cart = np.array([[0.65465367, -0.37796447, -0.65465367], [0.6652466, -0.33896007, -0.6652466]]) + + # Calculate the maximum latitude + max_latitude = _extreme_gca_latitude_cartesian(gca_cart, 'max') + + # Check if the maximum latitude is correct + expected_max_latitude = _max_latitude_rad_iterative(gca_cart) + assert np.isclose(max_latitude, + expected_max_latitude, + atol=ERROR_TOLERANCE) + + +def test_extreme_gca_latitude_min(): + gca_cart = np.array([ + _normalize_xyz(*[0.5, 0.5, -0.5]), + _normalize_xyz(*[-0.5, 0.5, -0.5]) + ]) + + min_latitude = _extreme_gca_latitude_cartesian(gca_cart, 'min') + expected_min_latitude = _min_latitude_rad_iterative(gca_cart) + assert np.isclose(min_latitude, expected_min_latitude, atol=ERROR_TOLERANCE) + + gca_cart = np.array([[0.0, 0.0, -1.0], [1.0, 0.0, 0.0]]) + min_latitude = _extreme_gca_latitude_cartesian(gca_cart, 'min') + expected_min_latitude = -np.pi / 2 # 90 degrees in radians + assert np.isclose(min_latitude, expected_min_latitude, atol=ERROR_TOLERANCE) + +def test_get_latlonbox_width(): + gca_latlon = np.array([[0.0, 0.0], [0.0, 3.0]]) + width = ux.grid.geometry._get_latlonbox_width(gca_latlon) + assert width == 3.0 + + gca_latlon = np.array([[0.0, 0.0], [2 * np.pi - 1.0, 1.0]]) + width = ux.grid.geometry._get_latlonbox_width(gca_latlon) + assert width == 2.0 + +def test_insert_pt_in_latlonbox_non_periodic(): + old_box = np.array([[0.1, 0.2], [0.3, 0.4]]) # Radians + new_pt = np.array([0.15, 0.35]) + expected = np.array([[0.1, 0.2], [0.3, 0.4]]) + result = ux.grid.geometry.insert_pt_in_latlonbox(old_box, new_pt, False) + np.testing.assert_array_equal(result, expected) + +def test_insert_pt_in_latlonbox_periodic(): + old_box = np.array([[0.1, 0.2], [6.0, 0.1]]) # Radians, periodic + new_pt = np.array([0.15, 6.2]) + expected = np.array([[0.1, 0.2], [6.0, 0.1]]) + result = ux.grid.geometry.insert_pt_in_latlonbox(old_box, new_pt, True) + np.testing.assert_array_equal(result, expected) + +def test_insert_pt_in_latlonbox_pole(): + old_box = np.array([[0.1, 0.2], [0.3, 0.4]]) + new_pt = np.array([np.pi / 2, np.nan]) # Pole point + expected = np.array([[0.1, np.pi / 2], [0.3, 0.4]]) + result = ux.grid.geometry.insert_pt_in_latlonbox(old_box, new_pt) + np.testing.assert_array_equal(result, expected) + +def test_insert_pt_in_empty_state(): + old_box = np.array([[np.nan, np.nan], + [np.nan, np.nan]]) # Empty state + new_pt = np.array([0.15, 0.35]) + expected = np.array([[0.15, 0.15], [0.35, 0.35]]) + result = ux.grid.geometry.insert_pt_in_latlonbox(old_box, new_pt) + np.testing.assert_array_equal(result, expected) + + +def _get_cartesian_face_edge_nodes_testcase_helper_latlon_bounds_gca(face_nodes_ind, face_edges_ind, edge_nodes_grid, node_x, node_y, node_z): + """Construct an array to hold the edge Cartesian coordinates connectivity for a face in a grid.""" + mask = face_edges_ind != INT_FILL_VALUE + valid_edges = face_edges_ind[mask] + face_edges = edge_nodes_grid[valid_edges] + + face_edges[0] = [face_nodes_ind[0], face_nodes_ind[1]] + + for idx in range(1, len(face_edges)): + if face_edges[idx][0] != face_edges[idx - 1][1]: + face_edges[idx] = face_edges[idx][::-1] + + cartesian_coordinates = np.array( + [ + [[node_x[node], node_y[node], node_z[node]] for node in edge] + for edge in face_edges ] - ) - # Apply the inverse transformation to get the lat lon coordinates - face_edges_lonlat = np.array( - [[_xyz_to_lonlat_rad(*edge[0]), _xyz_to_lonlat_rad(*edge[1])] for edge in face_edges_cart]) - - bounds = _populate_face_latlon_bound(face_edges_cart, face_edges_lonlat) - expected_bounds = np.array([[-0.05235988, 0], [6.23082543, 0]]) - nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) - - def test_populate_bounds_southSphere(self): - # The face is near the south pole but doesn't contains the pole - face_edges_cart = np.array([ - [[-1.04386773e-01, -5.20500333e-02, -9.93173799e-01], [-1.04528463e-01, -1.28010448e-17, -9.94521895e-01]], - [[-1.04528463e-01, -1.28010448e-17, -9.94521895e-01], [-5.23359562e-02, -6.40930613e-18, -9.98629535e-01]], - [[-5.23359562e-02, -6.40930613e-18, -9.98629535e-01], [-5.22644277e-02, -5.22644277e-02, -9.97264689e-01]], - [[-5.22644277e-02, -5.22644277e-02, -9.97264689e-01], [-1.04386773e-01, -5.20500333e-02, -9.93173799e-01]] - ]) - - # Apply the inverse transformation to get the lat lon coordinates - face_edges_lonlat = np.array( - [[_xyz_to_lonlat_rad(*edge[0]), _xyz_to_lonlat_rad(*edge[1])] for edge in face_edges_cart]) - - bounds = _populate_face_latlon_bound(face_edges_cart, face_edges_lonlat) - expected_bounds = np.array([[-1.51843645, -1.45388627], [3.14159265, 3.92699082]]) - nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) - - def test_populate_bounds_near_pole(self): - # The face is near the south pole but doesn't contains the pole - face_edges_cart = np.array([ - [[3.58367950e-01, 0.00000000e+00, -9.33580426e-01], [3.57939780e-01, 4.88684203e-02, -9.32465008e-01]], - [[3.57939780e-01, 4.88684203e-02, -9.32465008e-01], [4.06271283e-01, 4.78221112e-02, -9.12500241e-01]], - [[4.06271283e-01, 4.78221112e-02, -9.12500241e-01], [4.06736643e-01, 2.01762691e-16, -9.13545458e-01]], - [[4.06736643e-01, 2.01762691e-16, -9.13545458e-01], [3.58367950e-01, 0.00000000e+00, -9.33580426e-01]] - ]) - - # Apply the inverse transformation to get the lat lon coordinates - face_edges_lonlat = np.array( - [[_xyz_to_lonlat_rad(*edge[0]), _xyz_to_lonlat_rad(*edge[1])] for edge in face_edges_cart]) - - bounds = _populate_face_latlon_bound(face_edges_cart, face_edges_lonlat) - expected_bounds = np.array([[-1.20427718, -1.14935491], [0, 0.13568803]]) - nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) - - def test_populate_bounds_near_pole2(self): - # The face is near the south pole but doesn't contains the pole - face_edges_cart = np.array([ - [[3.57939780e-01, -4.88684203e-02, -9.32465008e-01], [3.58367950e-01, 0.00000000e+00, -9.33580426e-01]], - [[3.58367950e-01, 0.00000000e+00, -9.33580426e-01], [4.06736643e-01, 2.01762691e-16, -9.13545458e-01]], - [[4.06736643e-01, 2.01762691e-16, -9.13545458e-01], [4.06271283e-01, -4.78221112e-02, -9.12500241e-01]], - [[4.06271283e-01, -4.78221112e-02, -9.12500241e-01], [3.57939780e-01, -4.88684203e-02, -9.32465008e-01]] - ]) - - # Apply the inverse transformation to get the lat lon coordinates - face_edges_lonlat = np.array( - [[_xyz_to_lonlat_rad(*edge[0]), _xyz_to_lonlat_rad(*edge[1])] for edge in face_edges_cart]) - - bounds = _populate_face_latlon_bound(face_edges_cart, face_edges_lonlat) - expected_bounds = np.array([[-1.20427718, -1.14935491], [6.147497, 4.960524e-16]]) - nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) - - def test_populate_bounds_long_face(self): - """Test case where one of the face edges is a longitude GCA.""" - face_edges_cart = np.array([ - [[9.9999946355819702e-01, -6.7040475551038980e-04, 8.0396590055897832e-04], - [9.9999439716339111e-01, -3.2541253603994846e-03, -8.0110825365409255e-04]], - [[9.9999439716339111e-01, -3.2541253603994846e-03, -8.0110825365409255e-04], - [9.9998968839645386e-01, -3.1763643492013216e-03, -3.2474612817168236e-03]], - [[9.9998968839645386e-01, -3.1763643492013216e-03, -3.2474612817168236e-03], - [9.9998861551284790e-01, -8.2993711112067103e-04, -4.7004125081002712e-03]], - [[9.9998861551284790e-01, -8.2993711112067103e-04, -4.7004125081002712e-03], - [9.9999368190765381e-01, 1.7522916896268725e-03, -3.0944822356104851e-03]], - [[9.9999368190765381e-01, 1.7522916896268725e-03, -3.0944822356104851e-03], - [9.9999833106994629e-01, 1.6786820488050580e-03, -6.4892979571595788e-04]], - [[9.9999833106994629e-01, 1.6786820488050580e-03, -6.4892979571595788e-04], - [9.9999946355819702e-01, -6.7040475551038980e-04, 8.0396590055897832e-04]] - ]) - - face_edges_lonlat = np.array( - [[_xyz_to_lonlat_rad(*edge[0]), _xyz_to_lonlat_rad(*edge[1])] for edge in face_edges_cart]) - - bounds = _populate_face_latlon_bound(face_edges_cart, face_edges_lonlat) - - # The expected bounds should not contains the south pole [0,-0.5*np.pi] - self.assertTrue(bounds[1][0] != 0.0) - - def test_populate_bounds_node_on_pole(self): - # Generate a normal face that is crossing the antimeridian - vertices_lonlat = [[10.0, 90.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] - vertices_lonlat = np.array(vertices_lonlat) - - # Convert everything into radians - vertices_rad = np.radians(vertices_lonlat) - vertices_cart = np.vstack([_lonlat_rad_to_xyz(vertices_rad[:, 0], vertices_rad[:, 1])]).T - lat_max = np.pi / 2 - lat_min = min(np.deg2rad(10.0), - _extreme_gca_latitude_cartesian(np.array([vertices_cart[1], vertices_cart[2]]), extreme_type="min")) - lon_min = np.deg2rad(10.0) - lon_max = np.deg2rad(50.0) - grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) - face_edges_connectivity_cartesian = self._get_cartesian_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_x.values, - grid.node_y.values, grid.node_z.values) - face_edges_connectivity_lonlat = self._get_lonlat_rad_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_lon.values, - grid.node_lat.values) - expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) - bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat) - nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) - - def test_populate_bounds_edge_over_pole(self): - # Generate a normal face that is crossing the antimeridian - vertices_lonlat = [[210.0, 80.0], [350.0, 60.0], [10.0, 60.0], [30.0, 80.0]] - vertices_lonlat = np.array(vertices_lonlat) - - # Convert everything into radians - vertices_rad = np.radians(vertices_lonlat) - vertices_cart = np.vstack([_lonlat_rad_to_xyz(vertices_rad[:, 0], vertices_rad[:, 1])]).T - lat_max = np.pi / 2 - lat_min = min(np.deg2rad(60.0), - _extreme_gca_latitude_cartesian(np.array([vertices_cart[1], vertices_cart[2]]), extreme_type="min")) - lon_min = np.deg2rad(210.0) - lon_max = np.deg2rad(30.0) - grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) - face_edges_connectivity_cartesian = self._get_cartesian_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_x.values, - grid.node_y.values, grid.node_z.values) - face_edges_connectivity_lonlat = self._get_lonlat_rad_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_lon.values, - grid.node_lat.values) - expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) - bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat) - nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) - - def test_populate_bounds_pole_inside(self): - # Generate a normal face that is crossing the antimeridian - vertices_lonlat = [[200.0, 80.0], [350.0, 60.0], [10.0, 60.0], [40.0, 80.0]] - vertices_lonlat = np.array(vertices_lonlat) - - # Convert everything into radians - vertices_rad = np.radians(vertices_lonlat) - vertices_cart = np.vstack([_lonlat_rad_to_xyz(vertices_rad[:, 0], vertices_rad[:, 1])]).T - lat_max = np.pi / 2 - lat_min = min(np.deg2rad(60.0), - _extreme_gca_latitude_cartesian(np.array([vertices_cart[1], vertices_cart[2]]), extreme_type="min")) - lon_min = 0 - lon_max = 2 * np.pi - grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) - face_edges_connectivity_cartesian = self._get_cartesian_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_x.values, - grid.node_y.values, grid.node_z.values) - face_edges_connectivity_lonlat = self._get_lonlat_rad_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_lon.values, - grid.node_lat.values) - expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) - bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat) - nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) - - -class TestLatlonBoundsLatLonFace(TestCase): - - def _get_cartesian_face_edge_nodes_testcase_helper( - self, face_nodes_ind, face_edges_ind, edge_nodes_grid, node_x, node_y, node_z - ): - """This function is only used to help generating the testcase and - should not be used in the actual implementation. Construct an array to - hold the edge Cartesian coordinates connectivity for a face in a grid. - - Parameters - ---------- - face_nodes_ind : np.ndarray, shape (n_nodes,) - The ith entry of Grid.face_node_connectivity, where n_nodes is the number of nodes in the face. - face_edges_ind : np.ndarray, shape (n_edges,) - The ith entry of Grid.face_edge_connectivity, where n_edges is the number of edges in the face. - edge_nodes_grid : np.ndarray, shape (n_edges, 2) - The entire Grid.edge_node_connectivity, where n_edges is the total number of edges in the grid. - node_x : np.ndarray, shape (n_nodes,) - The values of Grid.node_x, where n_nodes_total is the total number of nodes in the grid. - node_y : np.ndarray, shape (n_nodes,) - The values of Grid.node_y. - node_z : np.ndarray, shape (n_nodes,) - The values of Grid.node_z. - - Returns - ------- - face_edges : np.ndarray, shape (n_edges, 2, 3) - Face edge connectivity in Cartesian coordinates, where n_edges is the number of edges for the specific face. - - Notes - ----- - - The function assumes that the inputs are well-formed and correspond to the same face. - - The output array contains the Cartesian coordinates for each edge of the face. - """ - - # Create a mask that is True for all values not equal to INT_FILL_VALUE - mask = face_edges_ind != INT_FILL_VALUE - - # Use the mask to select only the elements not equal to INT_FILL_VALUE - valid_edges = face_edges_ind[mask] - face_edges = edge_nodes_grid[valid_edges] - - # Ensure counter-clockwise order of edge nodes - # Start with the first two nodes - face_edges[0] = [face_nodes_ind[0], face_nodes_ind[1]] - - for idx in range(1, len(face_edges)): - if face_edges[idx][0] != face_edges[idx - 1][1]: - # Swap the node index in this edge if not in counter-clockwise order - face_edges[idx] = face_edges[idx][::-1] - - # Fetch coordinates for each node in the face edges - cartesian_coordinates = np.array( - [ - [[node_x[node], node_y[node], node_z[node]] for node in edge] - for edge in face_edges - ] - ) - - return cartesian_coordinates - - def _get_lonlat_rad_face_edge_nodes_testcase_helper( - self, face_nodes_ind, face_edges_ind, edge_nodes_grid, node_lon, node_lat - ): - """This function is only used to help generating the testcase and - should not be used in the actual implementation. Construct an array to - hold the edge lat lon in radian connectivity for a face in a grid. - - Parameters - ---------- - face_nodes_ind : np.ndarray, shape (n_nodes,) - The ith entry of Grid.face_node_connectivity, where n_nodes is the number of nodes in the face. - face_edges_ind : np.ndarray, shape (n_edges,) - The ith entry of Grid.face_edge_connectivity, where n_edges is the number of edges in the face. - edge_nodes_grid : np.ndarray, shape (n_edges, 2) - The entire Grid.edge_node_connectivity, where n_edges is the total number of edges in the grid. - node_lon : np.ndarray, shape (n_nodes,) - The values of Grid.node_lon. - node_lat : np.ndarray, shape (n_nodes,) - The values of Grid.node_lat, where n_nodes_total is the total number of nodes in the grid. - Returns - ------- - face_edges : np.ndarray, shape (n_edges, 2, 3) - Face edge connectivity in Cartesian coordinates, where n_edges is the number of edges for the specific face. - - Notes - ----- - - The function assumes that the inputs are well-formed and correspond to the same face. - - The output array contains the Latitude and longitude coordinates in radian for each edge of the face. - """ - - # Create a mask that is True for all values not equal to INT_FILL_VALUE - mask = face_edges_ind != INT_FILL_VALUE - - # Use the mask to select only the elements not equal to INT_FILL_VALUE - valid_edges = face_edges_ind[mask] - face_edges = edge_nodes_grid[valid_edges] - - # Ensure counter-clockwise order of edge nodes - # Start with the first two nodes - face_edges[0] = [face_nodes_ind[0], face_nodes_ind[1]] - - for idx in range(1, len(face_edges)): - if face_edges[idx][0] != face_edges[idx - 1][1]: - # Swap the node index in this edge if not in counter-clockwise order - face_edges[idx] = face_edges[idx][::-1] - - # Fetch coordinates for each node in the face edges - lonlat_coordinates = np.array( + ) + + return cartesian_coordinates + +def _get_lonlat_rad_face_edge_nodes_testcase_helper_latlon_bounds_gca(face_nodes_ind, face_edges_ind, edge_nodes_grid, node_lon, node_lat): + """Construct an array to hold the edge lat lon in radian connectivity for a face in a grid.""" + mask = face_edges_ind != INT_FILL_VALUE + valid_edges = face_edges_ind[mask] + face_edges = edge_nodes_grid[valid_edges] + + face_edges[0] = [face_nodes_ind[0], face_nodes_ind[1]] + + for idx in range(1, len(face_edges)): + if face_edges[idx][0] != face_edges[idx - 1][1]: + face_edges[idx] = face_edges[idx][::-1] + + lonlat_coordinates = np.array( + [ [ [ - [ - np.mod(np.deg2rad(node_lon[node]), 2 * np.pi), - np.deg2rad(node_lat[node]), - ] - for node in edge + np.mod(np.deg2rad(node_lon[node]), 2 * np.pi), + np.deg2rad(node_lat[node]), ] - for edge in face_edges + for node in edge ] - ) - - return lonlat_coordinates - - def test_populate_bounds_normal(self): - # Generate a normal face that is not crossing the antimeridian or the poles - vertices_lonlat = [[10.0, 60.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] - vertices_lonlat = np.array(vertices_lonlat) - - # Convert everything into radians - vertices_rad = np.radians(vertices_lonlat) - lat_max = np.deg2rad(60.0) - lat_min = np.deg2rad(10.0) - lon_min = np.deg2rad(10.0) - lon_max = np.deg2rad(50.0) - grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) - face_edges_connectivity_cartesian = self._get_cartesian_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_x.values, - grid.node_y.values, grid.node_z.values) - face_edges_connectivity_lonlat = self._get_lonlat_rad_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_lon.values, - grid.node_lat.values) - expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) - bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, - is_latlonface=True) - nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) - - def test_populate_bounds_antimeridian(self): - # Generate a normal face that is crossing the antimeridian - vertices_lonlat = [[350, 60.0], [350, 10.0], [50.0, 10.0], [50.0, 60.0]] - vertices_lonlat = np.array(vertices_lonlat) - - # Convert everything into radians - vertices_rad = np.radians(vertices_lonlat) - lat_max = np.deg2rad(60.0) - lat_min = np.deg2rad(10.0) - lon_min = np.deg2rad(350.0) - lon_max = np.deg2rad(50.0) - grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) - face_edges_connectivity_cartesian = self._get_cartesian_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_x.values, - grid.node_y.values, grid.node_z.values) - face_edges_connectivity_lonlat = self._get_lonlat_rad_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_lon.values, - grid.node_lat.values) - expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) - bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, - is_latlonface=True) - nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) - - def test_populate_bounds_node_on_pole(self): - # Generate a normal face that is crossing the antimeridian - vertices_lonlat = [[10.0, 90.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] - vertices_lonlat = np.array(vertices_lonlat) - - # Convert everything into radians - vertices_rad = np.radians(vertices_lonlat) - lat_max = np.pi / 2 - lat_min = np.deg2rad(10.0) - lon_min = np.deg2rad(10.0) - lon_max = np.deg2rad(50.0) - grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) - face_edges_connectivity_cartesian = self._get_cartesian_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_x.values, - grid.node_y.values, grid.node_z.values) - face_edges_connectivity_lonlat = self._get_lonlat_rad_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_lon.values, - grid.node_lat.values) - expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) - bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, - is_latlonface=True) - nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) - - def test_populate_bounds_edge_over_pole(self): - # Generate a normal face that is crossing the antimeridian - vertices_lonlat = [[210.0, 80.0], [350.0, 60.0], [10.0, 60.0], [30.0, 80.0]] - vertices_lonlat = np.array(vertices_lonlat) - - # Convert everything into radians - vertices_rad = np.radians(vertices_lonlat) - vertices_cart = np.vstack([_lonlat_rad_to_xyz(vertices_rad[:, 0], vertices_rad[:, 1])]).T - lat_max = np.pi / 2 - lat_min = np.deg2rad(60.0) - lon_min = np.deg2rad(210.0) - lon_max = np.deg2rad(30.0) - grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) - face_edges_connectivity_cartesian = self._get_cartesian_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_x.values, - grid.node_y.values, grid.node_z.values) - face_edges_connectivity_lonlat = self._get_lonlat_rad_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_lon.values, - grid.node_lat.values) - expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) - bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, - is_latlonface=True) - nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) - - def test_populate_bounds_pole_inside(self): - # Generate a normal face that is crossing the antimeridian - vertices_lonlat = [[200.0, 80.0], [350.0, 60.0], [10.0, 60.0], [40.0, 80.0]] - vertices_lonlat = np.array(vertices_lonlat) - - # Convert everything into radians - vertices_rad = np.radians(vertices_lonlat) - vertices_cart = np.vstack([_lonlat_rad_to_xyz(vertices_rad[:, 0], vertices_rad[:, 1])]).T - lat_max = np.pi / 2 - lat_min = np.deg2rad(60.0) - lon_min = 0 - lon_max = 2 * np.pi - grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) - face_edges_connectivity_cartesian = self._get_cartesian_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_x.values, - grid.node_y.values, grid.node_z.values) - face_edges_connectivity_lonlat = self._get_lonlat_rad_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_lon.values, - grid.node_lat.values) - expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) - bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, - is_latlonface=True) - nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) - - -class TestLatlonBoundsGCAList(TestCase): - def _get_cartesian_face_edge_nodes_testcase_helper( - self, face_nodes_ind, face_edges_ind, edge_nodes_grid, node_x, node_y, node_z - ): - """This function is only used to help generating the testcase and - should not be used in the actual implementation. Construct an array to - hold the edge Cartesian coordinates connectivity for a face in a grid. - - Parameters - ---------- - face_nodes_ind : np.ndarray, shape (n_nodes,) - The ith entry of Grid.face_node_connectivity, where n_nodes is the number of nodes in the face. - face_edges_ind : np.ndarray, shape (n_edges,) - The ith entry of Grid.face_edge_connectivity, where n_edges is the number of edges in the face. - edge_nodes_grid : np.ndarray, shape (n_edges, 2) - The entire Grid.edge_node_connectivity, where n_edges is the total number of edges in the grid. - node_x : np.ndarray, shape (n_nodes,) - The values of Grid.node_x, where n_nodes_total is the total number of nodes in the grid. - node_y : np.ndarray, shape (n_nodes,) - The values of Grid.node_y. - node_z : np.ndarray, shape (n_nodes,) - The values of Grid.node_z. - - Returns - ------- - face_edges : np.ndarray, shape (n_edges, 2, 3) - Face edge connectivity in Cartesian coordinates, where n_edges is the number of edges for the specific face. - - Notes - ----- - - The function assumes that the inputs are well-formed and correspond to the same face. - - The output array contains the Cartesian coordinates for each edge of the face. - """ - - # Create a mask that is True for all values not equal to INT_FILL_VALUE - mask = face_edges_ind != INT_FILL_VALUE - - # Use the mask to select only the elements not equal to INT_FILL_VALUE - valid_edges = face_edges_ind[mask] - face_edges = edge_nodes_grid[valid_edges] - - # Ensure counter-clockwise order of edge nodes - # Start with the first two nodes - face_edges[0] = [face_nodes_ind[0], face_nodes_ind[1]] - - for idx in range(1, len(face_edges)): - if face_edges[idx][0] != face_edges[idx - 1][1]: - # Swap the node index in this edge if not in counter-clockwise order - face_edges[idx] = face_edges[idx][::-1] - - # Fetch coordinates for each node in the face edges - cartesian_coordinates = np.array( - [ - [[node_x[node], node_y[node], node_z[node]] for node in edge] - for edge in face_edges - ] - ) - - return cartesian_coordinates - - def _get_lonlat_rad_face_edge_nodes_testcase_helper( - self, face_nodes_ind, face_edges_ind, edge_nodes_grid, node_lon, node_lat - ): - """This function is only used to help generating the testcase and - should not be used in the actual implementation. Construct an array to - hold the edge lat lon in radian connectivity for a face in a grid. - - Parameters - ---------- - face_nodes_ind : np.ndarray, shape (n_nodes,) - The ith entry of Grid.face_node_connectivity, where n_nodes is the number of nodes in the face. - face_edges_ind : np.ndarray, shape (n_edges,) - The ith entry of Grid.face_edge_connectivity, where n_edges is the number of edges in the face. - edge_nodes_grid : np.ndarray, shape (n_edges, 2) - The entire Grid.edge_node_connectivity, where n_edges is the total number of edges in the grid. - node_lon : np.ndarray, shape (n_nodes,) - The values of Grid.node_lon. - node_lat : np.ndarray, shape (n_nodes,) - The values of Grid.node_lat, where n_nodes_total is the total number of nodes in the grid. - Returns - ------- - face_edges : np.ndarray, shape (n_edges, 2, 3) - Face edge connectivity in Cartesian coordinates, where n_edges is the number of edges for the specific face. - - Notes - ----- - - The function assumes that the inputs are well-formed and correspond to the same face. - - The output array contains the Latitude and longitude coordinates in radian for each edge of the face. - """ - - # Create a mask that is True for all values not equal to INT_FILL_VALUE - mask = face_edges_ind != INT_FILL_VALUE - - # Use the mask to select only the elements not equal to INT_FILL_VALUE - valid_edges = face_edges_ind[mask] - face_edges = edge_nodes_grid[valid_edges] - - # Ensure counter-clockwise order of edge nodes - # Start with the first two nodes - face_edges[0] = [face_nodes_ind[0], face_nodes_ind[1]] - - for idx in range(1, len(face_edges)): - if face_edges[idx][0] != face_edges[idx - 1][1]: - # Swap the node index in this edge if not in counter-clockwise order - face_edges[idx] = face_edges[idx][::-1] - - # Fetch coordinates for each node in the face edges - lonlat_coordinates = np.array( + for edge in face_edges + ] + ) + + return lonlat_coordinates + +def test_populate_bounds_normal_latlon_bounds_gca(): + vertices_lonlat = [[10.0, 60.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] + vertices_lonlat = np.array(vertices_lonlat) + + vertices_rad = np.radians(vertices_lonlat) + vertices_cart = np.vstack([_lonlat_rad_to_xyz(vertices_rad[:, 0], vertices_rad[:, 1])]).T + lat_max = max(np.deg2rad(60.0), + _extreme_gca_latitude_cartesian(np.array([vertices_cart[0], vertices_cart[3]]), extreme_type="max")) + lat_min = min(np.deg2rad(10.0), + _extreme_gca_latitude_cartesian(np.array([vertices_cart[1], vertices_cart[2]]), extreme_type="min")) + lon_min = np.deg2rad(10.0) + lon_max = np.deg2rad(50.0) + grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) + face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes_testcase_helper_latlon_bounds_gca( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_x.values, + grid.node_y.values, grid.node_z.values) + face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes_testcase_helper_latlon_bounds_gca( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_lon.values, + grid.node_lat.values) + expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) + bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat) + np.testing.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) + +def test_populate_bounds_antimeridian_latlon_bounds_gca(): + vertices_lonlat = [[350, 60.0], [350, 10.0], [50.0, 10.0], [50.0, 60.0]] + vertices_lonlat = np.array(vertices_lonlat) + + vertices_rad = np.radians(vertices_lonlat) + vertices_cart = np.vstack([_lonlat_rad_to_xyz(vertices_rad[:, 0], vertices_rad[:, 1])]).T + lat_max = max(np.deg2rad(60.0), + _extreme_gca_latitude_cartesian(np.array([vertices_cart[0], vertices_cart[3]]), extreme_type="max")) + lat_min = min(np.deg2rad(10.0), + _extreme_gca_latitude_cartesian(np.array([vertices_cart[1], vertices_cart[2]]), extreme_type="min")) + lon_min = np.deg2rad(350.0) + lon_max = np.deg2rad(50.0) + grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) + face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes_testcase_helper_latlon_bounds_gca( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_x.values, + grid.node_y.values, grid.node_z.values) + face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes_testcase_helper_latlon_bounds_gca( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_lon.values, + grid.node_lat.values) + expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) + bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat) + np.testing.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) + +def test_populate_bounds_equator_latlon_bounds_gca(): + face_edges_cart = np.array([ + [[0.99726469, -0.05226443, -0.05226443], [0.99862953, 0.0, -0.05233596]], + [[0.99862953, 0.0, -0.05233596], [1.0, 0.0, 0.0]], + [[1.0, 0.0, 0.0], [0.99862953, -0.05233596, 0.0]], + [[0.99862953, -0.05233596, 0.0], [0.99726469, -0.05226443, -0.05226443]] + ]) + face_edges_lonlat = np.array( + [[_xyz_to_lonlat_rad(*edge[0]), _xyz_to_lonlat_rad(*edge[1])] for edge in face_edges_cart]) + + bounds = _populate_face_latlon_bound(face_edges_cart, face_edges_lonlat) + expected_bounds = np.array([[-0.05235988, 0], [6.23082543, 0]]) + np.testing.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) + +def test_populate_bounds_south_sphere_latlon_bounds_gca(): + face_edges_cart = np.array([ + [[-1.04386773e-01, -5.20500333e-02, -9.93173799e-01], [-1.04528463e-01, -1.28010448e-17, -9.94521895e-01]], + [[-1.04528463e-01, -1.28010448e-17, -9.94521895e-01], [-5.23359562e-02, -6.40930613e-18, -9.98629535e-01]], + [[-5.23359562e-02, -6.40930613e-18, -9.98629535e-01], [-5.22644277e-02, -5.22644277e-02, -9.97264689e-01]], + [[-5.22644277e-02, -5.22644277e-02, -9.97264689e-01], [-1.04386773e-01, -5.20500333e-02, -9.93173799e-01]] + ]) + + face_edges_lonlat = np.array( + [[_xyz_to_lonlat_rad(*edge[0]), _xyz_to_lonlat_rad(*edge[1])] for edge in face_edges_cart]) + + bounds = _populate_face_latlon_bound(face_edges_cart, face_edges_lonlat) + expected_bounds = np.array([[-1.51843645, -1.45388627], [3.14159265, 3.92699082]]) + np.testing.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) + +def test_populate_bounds_near_pole_latlon_bounds_gca(): + face_edges_cart = np.array([ + [[3.58367950e-01, 0.00000000e+00, -9.33580426e-01], [3.57939780e-01, 4.88684203e-02, -9.32465008e-01]], + [[3.57939780e-01, 4.88684203e-02, -9.32465008e-01], [4.06271283e-01, 4.78221112e-02, -9.12500241e-01]], + [[4.06271283e-01, 4.78221112e-02, -9.12500241e-01], [4.06736643e-01, 2.01762691e-16, -9.13545458e-01]], + [[4.06736643e-01, 2.01762691e-16, -9.13545458e-01], [3.58367950e-01, 0.00000000e+00, -9.33580426e-01]] + ]) + + face_edges_lonlat = np.array( + [[_xyz_to_lonlat_rad(*edge[0]), _xyz_to_lonlat_rad(*edge[1])] for edge in face_edges_cart]) + + bounds = _populate_face_latlon_bound(face_edges_cart, face_edges_lonlat) + expected_bounds = np.array([[-1.20427718, -1.14935491], [0, 0.13568803]]) + np.testing.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) + +def test_populate_bounds_near_pole2_latlon_bounds_gca(): + face_edges_cart = np.array([ + [[3.57939780e-01, -4.88684203e-02, -9.32465008e-01], [3.58367950e-01, 0.00000000e+00, -9.33580426e-01]], + [[3.58367950e-01, 0.00000000e+00, -9.33580426e-01], [4.06736643e-01, 2.01762691e-16, -9.13545458e-01]], + [[4.06736643e-01, 2.01762691e-16, -9.13545458e-01], [4.06271283e-01, -4.78221112e-02, -9.12500241e-01]], + [[4.06271283e-01, -4.78221112e-02, -9.12500241e-01], [3.57939780e-01, -4.88684203e-02, -9.32465008e-01]] + ]) + + face_edges_lonlat = np.array( + [[_xyz_to_lonlat_rad(*edge[0]), _xyz_to_lonlat_rad(*edge[1])] for edge in face_edges_cart]) + + bounds = _populate_face_latlon_bound(face_edges_cart, face_edges_lonlat) + expected_bounds = np.array([[-1.20427718, -1.14935491], [6.147497, 4.960524e-16]]) + np.testing.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) + +def test_populate_bounds_long_face_latlon_bounds_gca(): + face_edges_cart = np.array([ + [[9.9999946355819702e-01, -6.7040475551038980e-04, 8.0396590055897832e-04], + [9.9999439716339111e-01, -3.2541253603994846e-03, -8.0110825365409255e-04]], + [[9.9999439716339111e-01, -3.2541253603994846e-03, -8.0110825365409255e-04], + [9.9998968839645386e-01, -3.1763643492013216e-03, -3.2474612817168236e-03]], + [[9.9998968839645386e-01, -3.1763643492013216e-03, -3.2474612817168236e-03], + [9.9998861551284790e-01, -8.2993711112067103e-04, -4.7004125081002712e-03]], + [[9.9998861551284790e-01, -8.2993711112067103e-04, -4.7004125081002712e-03], + [9.9999368190765381e-01, 1.7522916896268725e-03, -3.0944822356104851e-03]], + [[9.9999368190765381e-01, 1.7522916896268725e-03, -3.0944822356104851e-03], + [9.9999833106994629e-01, 1.6786820488050580e-03, -6.4892979571595788e-04]], + [[9.9999833106994629e-01, 1.6786820488050580e-03, -6.4892979571595788e-04], + [9.9999946355819702e-01, -6.7040475551038980e-04, 8.0396590055897832e-04]] + ]) + + face_edges_lonlat = np.array( + [[_xyz_to_lonlat_rad(*edge[0]), _xyz_to_lonlat_rad(*edge[1])] for edge in face_edges_cart]) + + bounds = _populate_face_latlon_bound(face_edges_cart, face_edges_lonlat) + + # The expected bounds should not contain the south pole [0,-0.5*np.pi] + assert bounds[1][0] != 0.0 + +def test_populate_bounds_node_on_pole_latlon_bounds_gca(): + vertices_lonlat = [[10.0, 90.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] + vertices_lonlat = np.array(vertices_lonlat) + + vertices_rad = np.radians(vertices_lonlat) + vertices_cart = np.vstack([_lonlat_rad_to_xyz(vertices_rad[:, 0], vertices_rad[:, 1])]).T + lat_max = np.pi / 2 + lat_min = min(np.deg2rad(10.0), + _extreme_gca_latitude_cartesian(np.array([vertices_cart[1], vertices_cart[2]]), extreme_type="min")) + lon_min = np.deg2rad(10.0) + lon_max = np.deg2rad(50.0) + grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) + face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes_testcase_helper_latlon_bounds_gca( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_x.values, + grid.node_y.values, grid.node_z.values) + face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes_testcase_helper_latlon_bounds_gca( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_lon.values, + grid.node_lat.values) + expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) + bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat) + np.testing.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) + +def test_populate_bounds_edge_over_pole_latlon_bounds_gca(): + vertices_lonlat = [[210.0, 80.0], [350.0, 60.0], [10.0, 60.0], [30.0, 80.0]] + vertices_lonlat = np.array(vertices_lonlat) + + vertices_rad = np.radians(vertices_lonlat) + vertices_cart = np.vstack([_lonlat_rad_to_xyz(vertices_rad[:, 0], vertices_rad[:, 1])]).T + lat_max = np.pi / 2 + lat_min = min(np.deg2rad(60.0), + _extreme_gca_latitude_cartesian(np.array([vertices_cart[1], vertices_cart[2]]), extreme_type="min")) + lon_min = np.deg2rad(210.0) + lon_max = np.deg2rad(30.0) + grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) + face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes_testcase_helper_latlon_bounds_gca( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_x.values, + grid.node_y.values, grid.node_z.values) + face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes_testcase_helper_latlon_bounds_gca( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_lon.values, + grid.node_lat.values) + expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) + bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat) + np.testing.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) + +def test_populate_bounds_pole_inside_latlon_bounds_gca(): + vertices_lonlat = [[200.0, 80.0], [350.0, 60.0], [10.0, 60.0], [40.0, 80.0]] + vertices_lonlat = np.array(vertices_lonlat) + + vertices_rad = np.radians(vertices_lonlat) + vertices_cart = np.vstack([_lonlat_rad_to_xyz(vertices_rad[:, 0], vertices_rad[:, 1])]).T + lat_max = np.pi / 2 + lat_min = min(np.deg2rad(60.0), + _extreme_gca_latitude_cartesian(np.array([vertices_cart[1], vertices_cart[2]]), extreme_type="min")) + lon_min = 0 + lon_max = 2 * np.pi + grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) + face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes_testcase_helper_latlon_bounds_gca( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_x.values, + grid.node_y.values, grid.node_z.values) + face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes_testcase_helper_latlon_bounds_gca( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_lon.values, + grid.node_lat.values) + expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) + bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat) + np.testing.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) + + +def _get_cartesian_face_edge_nodes_testcase_helper_latlon_bounds_latlonface(face_nodes_ind, face_edges_ind, edge_nodes_grid, node_x, node_y, node_z): + """Construct an array to hold the edge Cartesian coordinates connectivity for a face in a grid.""" + mask = face_edges_ind != INT_FILL_VALUE + valid_edges = face_edges_ind[mask] + face_edges = edge_nodes_grid[valid_edges] + + face_edges[0] = [face_nodes_ind[0], face_nodes_ind[1]] + + for idx in range(1, len(face_edges)): + if face_edges[idx][0] != face_edges[idx - 1][1]: + face_edges[idx] = face_edges[idx][::-1] + + cartesian_coordinates = np.array( + [ + [[node_x[node], node_y[node], node_z[node]] for node in edge] + for edge in face_edges + ] + ) + + return cartesian_coordinates + +def _get_lonlat_rad_face_edge_nodes_testcase_helper_latlon_bounds_latlonface(face_nodes_ind, face_edges_ind, edge_nodes_grid, node_lon, node_lat): + """Construct an array to hold the edge lat lon in radian connectivity for a face in a grid.""" + mask = face_edges_ind != INT_FILL_VALUE + valid_edges = face_edges_ind[mask] + face_edges = edge_nodes_grid[valid_edges] + + face_edges[0] = [face_nodes_ind[0], face_nodes_ind[1]] + + for idx in range(1, len(face_edges)): + if face_edges[idx][0] != face_edges[idx - 1][1]: + face_edges[idx] = face_edges[idx][::-1] + + lonlat_coordinates = np.array( + [ [ [ - [ - np.mod(np.deg2rad(node_lon[node]), 2 * np.pi), - np.deg2rad(node_lat[node]), - ] - for node in edge + np.mod(np.deg2rad(node_lon[node]), 2 * np.pi), + np.deg2rad(node_lat[node]), ] - for edge in face_edges + for node in edge ] - ) - - return lonlat_coordinates - - def test_populate_bounds_normal(self): - # Generate a normal face that is not crossing the antimeridian or the poles - vertices_lonlat = [[10.0, 60.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] - vertices_lonlat = np.array(vertices_lonlat) - - # Convert everything into radians - vertices_rad = np.radians(vertices_lonlat) - lat_max = np.deg2rad(60.0) - lat_min = np.deg2rad(10.0) - lon_min = np.deg2rad(10.0) - lon_max = np.deg2rad(50.0) - grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) - face_edges_connectivity_cartesian = self._get_cartesian_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_x.values, - grid.node_y.values, grid.node_z.values) - face_edges_connectivity_lonlat = self._get_lonlat_rad_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_lon.values, - grid.node_lat.values) - expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) - bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, - is_GCA_list=[True, False, True, False]) - nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) - - def test_populate_bounds_antimeridian(self): - # Generate a normal face that is crossing the antimeridian - vertices_lonlat = [[350, 60.0], [350, 10.0], [50.0, 10.0], [50.0, 60.0]] - vertices_lonlat = np.array(vertices_lonlat) - - # Convert everything into radians - vertices_rad = np.radians(vertices_lonlat) - vertices_cart = np.vstack([_lonlat_rad_to_xyz(vertices_rad[:, 0], vertices_rad[:, 1])]).T - lat_max = np.deg2rad(60.0) - lat_min = np.deg2rad(10.0) - lon_min = np.deg2rad(350.0) - lon_max = np.deg2rad(50.0) - grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) - face_edges_connectivity_cartesian = self._get_cartesian_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_x.values, - grid.node_y.values, grid.node_z.values) - face_edges_connectivity_lonlat = self._get_lonlat_rad_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_lon.values, - grid.node_lat.values) - expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) - bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, - is_GCA_list=[True, False, True, False]) - nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) - - def test_populate_bounds_node_on_pole(self): - # Generate a normal face that is crossing the antimeridian - vertices_lonlat = [[10.0, 90.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] - vertices_lonlat = np.array(vertices_lonlat) - - # Convert everything into radians - vertices_rad = np.radians(vertices_lonlat) - vertices_cart = np.vstack([_lonlat_rad_to_xyz(vertices_rad[:, 0], vertices_rad[:, 1])]).T - lat_max = np.pi / 2 - lat_min = np.deg2rad(10.0) - lon_min = np.deg2rad(10.0) - lon_max = np.deg2rad(50.0) - grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) - face_edges_connectivity_cartesian = self._get_cartesian_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_x.values, - grid.node_y.values, grid.node_z.values) - face_edges_connectivity_lonlat = self._get_lonlat_rad_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_lon.values, - grid.node_lat.values) - expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) - bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, - is_GCA_list=[True, False, True, False]) - nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) - - def test_populate_bounds_edge_over_pole(self): - # Generate a normal face that is around the north pole - vertices_lonlat = [[210.0, 80.0], [350.0, 60.0], [10.0, 60.0], [30.0, 80.0]] - vertices_lonlat = np.array(vertices_lonlat) - - # Convert everything into radians - vertices_rad = np.radians(vertices_lonlat) - vertices_cart = np.vstack([_lonlat_rad_to_xyz(vertices_rad[:, 0], vertices_rad[:, 1])]).T - lat_max = np.pi / 2 - lat_min = np.deg2rad(60.0) - lon_min = np.deg2rad(210.0) - lon_max = np.deg2rad(30.0) - grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) - face_edges_connectivity_cartesian = self._get_cartesian_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_x.values, - grid.node_y.values, grid.node_z.values) - face_edges_connectivity_lonlat = self._get_lonlat_rad_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_lon.values, - grid.node_lat.values) - expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) - bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, - is_GCA_list=[True, False, True, False]) - nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) - - def test_populate_bounds_pole_inside(self): - # Generate a normal face that is crossing the antimeridian - vertices_lonlat = [[200.0, 80.0], [350.0, 60.0], [10.0, 60.0], [40.0, 80.0]] - vertices_lonlat = np.array(vertices_lonlat) - - # Convert everything into radians - vertices_rad = np.radians(vertices_lonlat) - vertices_cart = np.vstack([_lonlat_rad_to_xyz(vertices_rad[:, 0], vertices_rad[:, 1])]).T - lat_max = np.pi / 2 - lat_min = np.deg2rad(60.0) - lon_min = 0 - lon_max = 2 * np.pi - grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) - face_edges_connectivity_cartesian = self._get_cartesian_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_x.values, - grid.node_y.values, grid.node_z.values) - face_edges_connectivity_lonlat = self._get_lonlat_rad_face_edge_nodes_testcase_helper( - grid.face_node_connectivity.values[0], - grid.face_edge_connectivity.values[0], - grid.edge_node_connectivity.values, grid.node_lon.values, - grid.node_lat.values) - expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) - bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, - is_GCA_list=[True, False, True, False]) - nt.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) - - -class TestLatlonBoundsMix(TestCase): - def test_populate_bounds_GCA_mix(self): - face_1 = [[10.0, 60.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] - face_2 = [[350, 60.0], [350, 10.0], [50.0, 10.0], [50.0, 60.0]] - face_3 = [[210.0, 80.0], [350.0, 60.0], [10.0, 60.0], [30.0, 80.0]] - face_4 = [[200.0, 80.0], [350.0, 60.0], [10.0, 60.0], [40.0, 80.0]] - - faces = [face_1, face_2, face_3, face_4] - - # Hand calculated bounds for the above faces in radians - expected_bounds = [[[0.17453293, 1.07370494], [0.17453293, 0.87266463]], - [[0.17453293, 1.10714872], [6.10865238, 0.87266463]], - [[1.04719755, 1.57079633], [3.66519143, 0.52359878]], - [[1.04719755, 1.57079633], [0., 6.28318531]]] - - grid = ux.Grid.from_face_vertices(faces, latlon=True) - face_bounds = grid.bounds.values - for i in range(len(faces)): - nt.assert_allclose(face_bounds[i], expected_bounds[i], atol=ERROR_TOLERANCE) - - def test_populate_bounds_LatlonFace_mix(self): - face_1 = [[10.0, 60.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] - face_2 = [[350, 60.0], [350, 10.0], [50.0, 10.0], [50.0, 60.0]] - face_3 = [[210.0, 80.0], [350.0, 60.0], [10.0, 60.0], [30.0, 80.0]] - face_4 = [[200.0, 80.0], [350.0, 60.0], [10.0, 60.0], [40.0, 80.0]] - - faces = [face_1, face_2, face_3, face_4] - - expected_bounds = [[[np.deg2rad(10.0), np.deg2rad(60.0)], [np.deg2rad(10.0), np.deg2rad(50.0)]], - [[np.deg2rad(10.0), np.deg2rad(60.0)], [np.deg2rad(350.0), np.deg2rad(50.0)]], - [[np.deg2rad(60.0), np.pi / 2], [np.deg2rad(210.0), np.deg2rad(30.0)]], - [[np.deg2rad(60.0), np.pi / 2], [0., 2 * np.pi]]] - - grid = ux.Grid.from_face_vertices(faces, latlon=True) - bounds_xarray = _populate_bounds(grid, is_latlonface=True, return_array=True) - face_bounds = bounds_xarray.values - for i in range(len(faces)): - nt.assert_allclose(face_bounds[i], expected_bounds[i], atol=ERROR_TOLERANCE) - - def test_populate_bounds_GCAList_mix(self): - face_1 = [[10.0, 60.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] - face_2 = [[350, 60.0], [350, 10.0], [50.0, 10.0], [50.0, 60.0]] - face_3 = [[210.0, 80.0], [350.0, 60.0], [10.0, 60.0], [30.0, 80.0]] - face_4 = [[200.0, 80.0], [350.0, 60.0], [10.0, 60.0], [40.0, 80.0]] - - faces = [face_1, face_2, face_3, face_4] - - expected_bounds = [[[np.deg2rad(10.0), np.deg2rad(60.0)], [np.deg2rad(10.0), np.deg2rad(50.0)]], - [[np.deg2rad(10.0), np.deg2rad(60.0)], [np.deg2rad(350.0), np.deg2rad(50.0)]], - [[np.deg2rad(60.0), np.pi / 2], [np.deg2rad(210.0), np.deg2rad(30.0)]], - [[np.deg2rad(60.0), np.pi / 2], [0., 2 * np.pi]]] - - grid = ux.Grid.from_face_vertices(faces, latlon=True) - bounds_xarray = _populate_bounds(grid, is_face_GCA_list=np.array([[True, False, True, False], - [True, False, True, False], - [True, False, True, False], - [True, False, True, False]]) , return_array=True) - face_bounds = bounds_xarray.values - for i in range(len(faces)): - nt.assert_allclose(face_bounds[i], expected_bounds[i], atol=ERROR_TOLERANCE) - - -# Test class -class TestLatlonBoundsFiles: - - def test_face_bounds(self): - """Test to ensure ``Grid.face_bounds`` works correctly for all grid - files.""" - for grid_path in grid_files_latlonBound: - try: - # Open the grid file - self.uxgrid = ux.open_grid(grid_path) + for edge in face_edges + ] + ) + + return lonlat_coordinates + +def test_populate_bounds_normal_latlon_bounds_latlonface(): + vertices_lonlat = [[10.0, 60.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] + vertices_lonlat = np.array(vertices_lonlat) + + vertices_rad = np.radians(vertices_lonlat) + lat_max = np.deg2rad(60.0) + lat_min = np.deg2rad(10.0) + lon_min = np.deg2rad(10.0) + lon_max = np.deg2rad(50.0) + grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) + face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes_testcase_helper_latlon_bounds_latlonface( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_x.values, + grid.node_y.values, grid.node_z.values) + face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes_testcase_helper_latlon_bounds_latlonface( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_lon.values, + grid.node_lat.values) + expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) + bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, is_latlonface=True) + np.testing.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) + +def test_populate_bounds_antimeridian_latlon_bounds_latlonface(): + vertices_lonlat = [[350, 60.0], [350, 10.0], [50.0, 10.0], [50.0, 60.0]] + vertices_lonlat = np.array(vertices_lonlat) + + vertices_rad = np.radians(vertices_lonlat) + lat_max = np.deg2rad(60.0) + lat_min = np.deg2rad(10.0) + lon_min = np.deg2rad(350.0) + lon_max = np.deg2rad(50.0) + grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) + face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes_testcase_helper_latlon_bounds_latlonface( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_x.values, + grid.node_y.values, grid.node_z.values) + face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes_testcase_helper_latlon_bounds_latlonface( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_lon.values, + grid.node_lat.values) + expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) + bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, is_latlonface=True) + np.testing.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) + +def test_populate_bounds_node_on_pole_latlon_bounds_latlonface(): + vertices_lonlat = [[10.0, 90.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] + vertices_lonlat = np.array(vertices_lonlat) + + vertices_rad = np.radians(vertices_lonlat) + lat_max = np.pi / 2 + lat_min = np.deg2rad(10.0) + lon_min = np.deg2rad(10.0) + lon_max = np.deg2rad(50.0) + grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) + face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes_testcase_helper_latlon_bounds_latlonface( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_x.values, + grid.node_y.values, grid.node_z.values) + face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes_testcase_helper_latlon_bounds_latlonface( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_lon.values, + grid.node_lat.values) + expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) + bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, is_latlonface=True) + np.testing.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) + +def test_populate_bounds_edge_over_pole_latlon_bounds_latlonface(): + vertices_lonlat = [[210.0, 80.0], [350.0, 60.0], [10.0, 60.0], [30.0, 80.0]] + vertices_lonlat = np.array(vertices_lonlat) + + vertices_rad = np.radians(vertices_lonlat) + lat_max = np.pi / 2 + lat_min = np.deg2rad(60.0) + lon_min = np.deg2rad(210.0) + lon_max = np.deg2rad(30.0) + grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) + face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes_testcase_helper_latlon_bounds_latlonface( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_x.values, + grid.node_y.values, grid.node_z.values) + face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes_testcase_helper_latlon_bounds_latlonface( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_lon.values, + grid.node_lat.values) + expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) + bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, is_latlonface=True) + np.testing.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) + +def test_populate_bounds_pole_inside_latlon_bounds_latlonface(): + vertices_lonlat = [[200.0, 80.0], [350.0, 60.0], [10.0, 60.0], [40.0, 80.0]] + vertices_lonlat = np.array(vertices_lonlat) + + vertices_rad = np.radians(vertices_lonlat) + lat_max = np.pi / 2 + lat_min = np.deg2rad(60.0) + lon_min = 0 + lon_max = 2 * np.pi + grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) + face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes_testcase_helper_latlon_bounds_latlonface( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_x.values, + grid.node_y.values, grid.node_z.values) + face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes_testcase_helper_latlon_bounds_latlonface( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_lon.values, + grid.node_lat.values) + expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) + bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, is_latlonface=True) + np.testing.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) + + +def _get_cartesian_face_edge_nodes_testcase_helper_latlon_bounds_gca_list(face_nodes_ind, face_edges_ind, edge_nodes_grid, node_x, node_y, node_z): + """Construct an array to hold the edge Cartesian coordinates connectivity for a face in a grid.""" + mask = face_edges_ind != INT_FILL_VALUE + valid_edges = face_edges_ind[mask] + face_edges = edge_nodes_grid[valid_edges] + + face_edges[0] = [face_nodes_ind[0], face_nodes_ind[1]] + + for idx in range(1, len(face_edges)): + if face_edges[idx][0] != face_edges[idx - 1][1]: + face_edges[idx] = face_edges[idx][::-1] + + cartesian_coordinates = np.array( + [ + [[node_x[node], node_y[node], node_z[node]] for node in edge] + for edge in face_edges + ] + ) - # Test: Ensure the bounds are obtained - bounds = self.uxgrid.bounds - assert bounds is not None, f"Grid.face_bounds should not be None for {grid_path}" + return cartesian_coordinates - except Exception as e: - # Print the failing grid file and re-raise the exception - print(f"Test failed for grid file: {grid_path}") - raise e +def _get_lonlat_rad_face_edge_nodes_testcase_helper_latlon_bounds_gca_list(face_nodes_ind, face_edges_ind, edge_nodes_grid, node_lon, node_lat): + """Construct an array to hold the edge lat lon in radian connectivity for a face in a grid.""" + mask = face_edges_ind != INT_FILL_VALUE + valid_edges = face_edges_ind[mask] + face_edges = edge_nodes_grid[valid_edges] - finally: - # Clean up the grid object - del self.uxgrid + face_edges[0] = [face_nodes_ind[0], face_nodes_ind[1]] + for idx in range(1, len(face_edges)): + if face_edges[idx][0] != face_edges[idx - 1][1]: + face_edges[idx] = face_edges[idx][::-1] -class TestGeoDataFrame(TestCase): + lonlat_coordinates = np.array( + [ + [ + [ + np.mod(np.deg2rad(node_lon[node]), 2 * np.pi), + np.deg2rad(node_lat[node]), + ] + for node in edge + ] + for edge in face_edges + ] + ) + + return lonlat_coordinates + +def test_populate_bounds_normal_latlon_bounds_gca_list(): + vertices_lonlat = [[10.0, 60.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] + vertices_lonlat = np.array(vertices_lonlat) + + vertices_rad = np.radians(vertices_lonlat) + lat_max = np.deg2rad(60.0) + lat_min = np.deg2rad(10.0) + lon_min = np.deg2rad(10.0) + lon_max = np.deg2rad(50.0) + grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) + face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes_testcase_helper_latlon_bounds_gca_list( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_x.values, + grid.node_y.values, grid.node_z.values) + face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes_testcase_helper_latlon_bounds_gca_list( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_lon.values, + grid.node_lat.values) + expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) + bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, + is_GCA_list=[True, False, True, False]) + np.testing.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) + +def test_populate_bounds_antimeridian_latlon_bounds_gca_list(): + vertices_lonlat = [[350, 60.0], [350, 10.0], [50.0, 10.0], [50.0, 60.0]] + vertices_lonlat = np.array(vertices_lonlat) + + vertices_rad = np.radians(vertices_lonlat) + lat_max = np.deg2rad(60.0) + lat_min = np.deg2rad(10.0) + lon_min = np.deg2rad(350.0) + lon_max = np.deg2rad(50.0) + grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) + face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes_testcase_helper_latlon_bounds_gca_list( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_x.values, + grid.node_y.values, grid.node_z.values) + face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes_testcase_helper_latlon_bounds_gca_list( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_lon.values, + grid.node_lat.values) + expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) + bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, + is_GCA_list=[True, False, True, False]) + np.testing.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) + +def test_populate_bounds_node_on_pole_latlon_bounds_gca_list(): + vertices_lonlat = [[10.0, 90.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] + vertices_lonlat = np.array(vertices_lonlat) + + vertices_rad = np.radians(vertices_lonlat) + lat_max = np.pi / 2 + lat_min = np.deg2rad(10.0) + lon_min = np.deg2rad(10.0) + lon_max = np.deg2rad(50.0) + grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) + face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes_testcase_helper_latlon_bounds_gca_list( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_x.values, + grid.node_y.values, grid.node_z.values) + face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes_testcase_helper_latlon_bounds_gca_list( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_lon.values, + grid.node_lat.values) + expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) + bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, + is_GCA_list=[True, False, True, False]) + np.testing.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) + +def test_populate_bounds_edge_over_pole_latlon_bounds_gca_list(): + vertices_lonlat = [[210.0, 80.0], [350.0, 60.0], [10.0, 60.0], [30.0, 80.0]] + vertices_lonlat = np.array(vertices_lonlat) + + vertices_rad = np.radians(vertices_lonlat) + lat_max = np.pi / 2 + lat_min = np.deg2rad(60.0) + lon_min = np.deg2rad(210.0) + lon_max = np.deg2rad(30.0) + grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) + face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes_testcase_helper_latlon_bounds_gca_list( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_x.values, + grid.node_y.values, grid.node_z.values) + face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes_testcase_helper_latlon_bounds_gca_list( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_lon.values, + grid.node_lat.values) + expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) + bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, + is_GCA_list=[True, False, True, False]) + np.testing.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) + +def test_populate_bounds_pole_inside_latlon_bounds_gca_list(): + vertices_lonlat = [[200.0, 80.0], [350.0, 60.0], [10.0, 60.0], [40.0, 80.0]] + vertices_lonlat = np.array(vertices_lonlat) + + vertices_rad = np.radians(vertices_lonlat) + lat_max = np.pi / 2 + lat_min = np.deg2rad(60.0) + lon_min = 0 + lon_max = 2 * np.pi + grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) + face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes_testcase_helper_latlon_bounds_gca_list( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_x.values, + grid.node_y.values, grid.node_z.values) + face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes_testcase_helper_latlon_bounds_gca_list( + grid.face_node_connectivity.values[0], + grid.face_edge_connectivity.values[0], + grid.edge_node_connectivity.values, grid.node_lon.values, + grid.node_lat.values) + expected_bounds = np.array([[lat_min, lat_max], [lon_min, lon_max]]) + bounds = _populate_face_latlon_bound(face_edges_connectivity_cartesian, face_edges_connectivity_lonlat, + is_GCA_list=[True, False, True, False]) + np.testing.assert_allclose(bounds, expected_bounds, atol=ERROR_TOLERANCE) + + +def test_populate_bounds_GCA_mix_latlon_bounds_mix(): + face_1 = [[10.0, 60.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] + face_2 = [[350, 60.0], [350, 10.0], [50.0, 10.0], [50.0, 60.0]] + face_3 = [[210.0, 80.0], [350.0, 60.0], [10.0, 60.0], [30.0, 80.0]] + face_4 = [[200.0, 80.0], [350.0, 60.0], [10.0, 60.0], [40.0, 80.0]] + + faces = [face_1, face_2, face_3, face_4] + + expected_bounds = [[[0.17453293, 1.07370494], [0.17453293, 0.87266463]], + [[0.17453293, 1.10714872], [6.10865238, 0.87266463]], + [[1.04719755, 1.57079633], [3.66519143, 0.52359878]], + [[1.04719755, 1.57079633], [0., 6.28318531]]] + + grid = ux.Grid.from_face_vertices(faces, latlon=True) + face_bounds = grid.bounds.values + for i in range(len(faces)): + np.testing.assert_allclose(face_bounds[i], expected_bounds[i], atol=ERROR_TOLERANCE) + +def test_populate_bounds_LatlonFace_mix_latlon_bounds_mix(): + face_1 = [[10.0, 60.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] + face_2 = [[350, 60.0], [350, 10.0], [50.0, 10.0], [50.0, 60.0]] + face_3 = [[210.0, 80.0], [350.0, 60.0], [10.0, 60.0], [30.0, 80.0]] + face_4 = [[200.0, 80.0], [350.0, 60.0], [10.0, 60.0], [40.0, 80.0]] + + faces = [face_1, face_2, face_3, face_4] + + expected_bounds = [[[np.deg2rad(10.0), np.deg2rad(60.0)], [np.deg2rad(10.0), np.deg2rad(50.0)]], + [[np.deg2rad(10.0), np.deg2rad(60.0)], [np.deg2rad(350.0), np.deg2rad(50.0)]], + [[np.deg2rad(60.0), np.pi / 2], [np.deg2rad(210.0), np.deg2rad(30.0)]], + [[np.deg2rad(60.0), np.pi / 2], [0., 2 * np.pi]]] + + grid = ux.Grid.from_face_vertices(faces, latlon=True) + bounds_xarray = _populate_bounds(grid, is_latlonface=True, return_array=True) + face_bounds = bounds_xarray.values + for i in range(len(faces)): + np.testing.assert_allclose(face_bounds[i], expected_bounds[i], atol=ERROR_TOLERANCE) + +def test_populate_bounds_GCAList_mix_latlon_bounds_mix(): + face_1 = [[10.0, 60.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] + face_2 = [[350, 60.0], [350, 10.0], [50.0, 10.0], [50.0, 60.0]] + face_3 = [[210.0, 80.0], [350.0, 60.0], [10.0, 60.0], [30.0, 80.0]] + face_4 = [[200.0, 80.0], [350.0, 60.0], [10.0, 60.0], [40.0, 80.0]] + + faces = [face_1, face_2, face_3, face_4] + + expected_bounds = [[[np.deg2rad(10.0), np.deg2rad(60.0)], [np.deg2rad(10.0), np.deg2rad(50.0)]], + [[np.deg2rad(10.0), np.deg2rad(60.0)], [np.deg2rad(350.0), np.deg2rad(50.0)]], + [[np.deg2rad(60.0), np.pi / 2], [np.deg2rad(210.0), np.deg2rad(30.0)]], + [[np.deg2rad(60.0), np.pi / 2], [0., 2 * np.pi]]] + + grid = ux.Grid.from_face_vertices(faces, latlon=True) + bounds_xarray = _populate_bounds(grid, is_face_GCA_list=np.array([[True, False, True, False], + [True, False, True, False], + [True, False, True, False], + [True, False, True, False]]), return_array=True) + face_bounds = bounds_xarray.values + for i in range(len(faces)): + np.testing.assert_allclose(face_bounds[i], expected_bounds[i], atol=ERROR_TOLERANCE) + +def test_face_bounds_latlon_bounds_files(): + """Test to ensure ``Grid.face_bounds`` works correctly for all grid files.""" + for grid_path in grid_files_latlonBound: + try: + # Open the grid file + uxgrid = ux.open_grid(grid_path) + + # Test: Ensure the bounds are obtained + bounds = uxgrid.bounds + assert bounds is not None, f"Grid.face_bounds should not be None for {grid_path}" - def test_engine(self): - uxgrid = ux.open_grid(gridfile_geoflow) - for engine in ['geopandas', 'spatialpandas']: - gdf = uxgrid.to_geodataframe(engine=engine) + except Exception as e: + # Print the failing grid file and re-raise the exception + print(f"Test failed for grid file: {grid_path}") + raise e - def test_periodic_elements(self): - uxgrid = ux.open_grid(gridfile_geoflow) - for periodic_elements in ['ignore', 'exclude', 'split']: - gdf = uxgrid.to_geodataframe(periodic_elements=periodic_elements) + finally: + # Clean up the grid object + del uxgrid - def test_to_gdf(self): - uxgrid = ux.open_grid(gridfile_geoflow) +def test_engine_geodataframe(): + uxgrid = ux.open_grid(gridfile_geoflow) + for engine in ['geopandas', 'spatialpandas']: + gdf = uxgrid.to_geodataframe(engine=engine) - gdf_with_am = uxgrid.to_geodataframe(exclude_antimeridian=False) +def test_periodic_elements_geodataframe(): + uxgrid = ux.open_grid(gridfile_geoflow) + for periodic_elements in ['ignore', 'exclude', 'split']: + gdf = uxgrid.to_geodataframe(periodic_elements=periodic_elements) - gdf_without_am = uxgrid.to_geodataframe(exclude_antimeridian=True) +def test_to_gdf_geodataframe(): + uxgrid = ux.open_grid(gridfile_geoflow) - def test_cache_and_override(self): - """Tests the cache and override functionality for GeoDataFrame - conversion.""" + gdf_with_am = uxgrid.to_geodataframe(exclude_antimeridian=False) - uxgrid = ux.open_grid(gridfile_geoflow) + gdf_without_am = uxgrid.to_geodataframe(exclude_antimeridian=True) - gdf_a = uxgrid.to_geodataframe(exclude_antimeridian=False) +def test_cache_and_override_geodataframe(): + """Tests the cache and override functionality for GeoDataFrame conversion.""" + uxgrid = ux.open_grid(gridfile_geoflow) - gdf_b = uxgrid.to_geodataframe(exclude_antimeridian=False) + gdf_a = uxgrid.to_geodataframe(exclude_antimeridian=False) - assert gdf_a is gdf_b + gdf_b = uxgrid.to_geodataframe(exclude_antimeridian=False) - gdf_c = uxgrid.to_geodataframe(exclude_antimeridian=True) + assert gdf_a is gdf_b - assert gdf_a is not gdf_c + gdf_c = uxgrid.to_geodataframe(exclude_antimeridian=True) - gdf_d = uxgrid.to_geodataframe(exclude_antimeridian=True) + assert gdf_a is not gdf_c - assert gdf_d is gdf_c + gdf_d = uxgrid.to_geodataframe(exclude_antimeridian=True) - gdf_e = uxgrid.to_geodataframe(exclude_antimeridian=True, - override=True, - cache=False) + assert gdf_d is gdf_c - assert gdf_d is not gdf_e + gdf_e = uxgrid.to_geodataframe(exclude_antimeridian=True, override=True, cache=False) - gdf_f = uxgrid.to_geodataframe(exclude_antimeridian=True) + assert gdf_d is not gdf_e - assert gdf_f is not gdf_e + gdf_f = uxgrid.to_geodataframe(exclude_antimeridian=True) + assert gdf_f is not gdf_e -class TestStereographicProjection(TestCase): - def test_stereographic_projection(self): - lon = np.array(0) - lat = np.array(0) +def test_stereographic_projection_stereographic_projection(): + lon = np.array(0) + lat = np.array(0) - central_lon = np.array(0) - central_lat = np.array(0) + central_lon = np.array(0) + central_lat = np.array(0) - x, y = stereographic_projection(lon, lat, central_lon, central_lat) + x, y = stereographic_projection(lon, lat, central_lon, central_lat) - new_lon, new_lat = inverse_stereographic_projection(x, y, central_lon, central_lat) + new_lon, new_lat = inverse_stereographic_projection(x, y, central_lon, central_lat) - self.assertTrue(lon == new_lon) - self.assertTrue(lat == new_lat) - self.assertTrue(x == y == 0) + assert np.array_equal(lon, new_lon) + assert np.array_equal(lat, new_lat) + assert np.array_equal(x, y) and x == 0 diff --git a/test/test_geopandas.py b/test/test_geopandas.py index 3653fc052..84d790019 100644 --- a/test/test_geopandas.py +++ b/test/test_geopandas.py @@ -1,60 +1,45 @@ import os import numpy as np - -from unittest import TestCase from pathlib import Path - import uxarray as ux -import matplotlib.pyplot as plt current_path = Path(os.path.dirname(os.path.realpath(__file__))) - - - - -class TestGeopandas(TestCase): - - shp_filename = current_path / "meshfiles" / "shp" / "cb_2018_us_nation_20m" / "cb_2018_us_nation_20m.shp" - shp_filename_5poly = current_path / "meshfiles" / "shp" / "5poly/5poly.shp" - shp_filename_multi = current_path / "meshfiles" / "shp" / "multipoly/multipoly.shp" - geojson_filename = current_path / "meshfiles"/ "geojson"/ "sample_chicago_buildings.geojson" - - def test_read_shpfile(self): - """Read a shapefile.""" - - uxgrid = ux.Grid.from_file(self.shp_filename) - assert (uxgrid.validate()) - - def test_read_shpfile_multi(self): - """Read a shapefile, that consists of multipolygons.""" - - uxgrid = ux.Grid.from_file(self.shp_filename_multi) - assert (uxgrid.validate()) - - def test_read_shpfile_5poly(self): - """Read a shapefile, that consists of 5 polygons of different - shapes.""" - - uxgrid = ux.Grid.from_file(self.shp_filename_5poly) - assert (uxgrid.validate()) - - def test_read_geojson(self): - """Read a geojson file with a few of Chicago buildings. - - Number of polygons: 10 - Polygon 1: 26 sides - Polygon 2: 36 sides - Polygon 3: 29 sides - Polygon 4: 10 sides - Polygon 5: 30 sides - Polygon 6: 8 sides - Polygon 7: 7 sides - Polygon 8: 9 sides - Polygon 9: 7 sides - Polygon 10: 19 sides - """ - - uxgrid = ux.Grid.from_file(self.geojson_filename) - assert (uxgrid.n_face == 10) - assert (uxgrid.n_max_face_nodes == 36) +shp_filename = current_path / "meshfiles" / "shp" / "cb_2018_us_nation_20m" / "cb_2018_us_nation_20m.shp" +shp_filename_5poly = current_path / "meshfiles" / "shp" / "5poly/5poly.shp" +shp_filename_multi = current_path / "meshfiles" / "shp" / "multipoly/multipoly.shp" +geojson_filename = current_path / "meshfiles" / "geojson" / "sample_chicago_buildings.geojson" + +def test_read_shpfile(): + """Read a shapefile.""" + uxgrid = ux.Grid.from_file(shp_filename) + assert uxgrid.validate() + +def test_read_shpfile_multi(): + """Read a shapefile that consists of multipolygons.""" + uxgrid = ux.Grid.from_file(shp_filename_multi) + assert uxgrid.validate() + +def test_read_shpfile_5poly(): + """Read a shapefile that consists of 5 polygons of different shapes.""" + uxgrid = ux.Grid.from_file(shp_filename_5poly) + assert uxgrid.validate() + +def test_read_geojson(): + """Read a geojson file with a few of Chicago buildings. + + Number of polygons: 10 + Polygon 1: 26 sides + Polygon 2: 36 sides + Polygon 3: 29 sides + Polygon 4: 10 sides + Polygon 5: 30 sides + Polygon 6: 8 sides + Polygon 7: 7 sides + Polygon 8: 9 sides + Polygon 9: 7 sides + Polygon 10: 19 sides + """ + uxgrid = ux.Grid.from_file(geojson_filename) + assert uxgrid.n_face == 10 + assert uxgrid.n_max_face_nodes == 36 diff --git a/test/test_geos.py b/test/test_geos.py index 95ef721d5..e79a2c8ed 100644 --- a/test/test_geos.py +++ b/test/test_geos.py @@ -1,20 +1,17 @@ import uxarray as ux - import os from pathlib import Path +import pytest current_path = Path(os.path.dirname(os.path.realpath(__file__))) gridfile_geos_cs = current_path / "meshfiles" / "geos-cs" / "c12" / "test-c12.native.nc4" - - def test_read_geos_cs_grid(): """Tests the conversion of a CS12 GEOS-CS Grid to the UGRID conventions. A CS12 grid has 6 faces, each with 12x12 faces and 13x13 nodes each. """ - uxgrid = ux.open_grid(gridfile_geos_cs) n_face = 6 * 12 * 12 @@ -23,7 +20,6 @@ def test_read_geos_cs_grid(): assert uxgrid.n_face == n_face assert uxgrid.n_node == n_node - def test_read_geos_cs_uxds(): """Tests the creating of a UxDataset from a CS12 GEOS-CS Grid.""" uxds = ux.open_dataset(gridfile_geos_cs, gridfile_geos_cs) diff --git a/test/test_gradient.py b/test/test_gradient.py index 29b728cd3..9ed65d956 100644 --- a/test/test_gradient.py +++ b/test/test_gradient.py @@ -1,170 +1,97 @@ from uxarray.io._mpas import _replace_padding, _replace_zeros, _to_zero_index from uxarray.io._mpas import _read_mpas import uxarray as ux -import xarray as xr -from unittest import TestCase import numpy as np import numpy.testing as nt import os from pathlib import Path - -from uxarray.constants import INT_DTYPE, INT_FILL_VALUE +from uxarray.constants import INT_FILL_VALUE current_path = Path(os.path.dirname(os.path.realpath(__file__))) +mpas_atmo_path = current_path / 'meshfiles' / "mpas" / "QU" / 'mesh.QU.1920km.151026.nc' +mpas_ocean_path = current_path / 'meshfiles' / "mpas" / "QU" / 'oQU480.231010.nc' +CSne30_grid_path = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" +CSne30_data_path = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30_vortex.nc" +quad_hex_grid_path = current_path / "meshfiles" / "ugrid" / "quad-hexagon" / "grid.nc" +quad_hex_data_path = current_path / "meshfiles" / "ugrid" / "quad-hexagon" / "data.nc" +geoflow_grid_path = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc" +geoflow_data_path = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "v1.nc" +def test_uniform_data(): + """Computes the gradient on meshes with uniform data, with the expected + gradient being zero on all edges.""" + for grid_path in [ + mpas_atmo_path, mpas_ocean_path, CSne30_grid_path, quad_hex_grid_path, + ]: + uxgrid = ux.open_grid(grid_path) + uxda_zeros = ux.UxDataArray(data=np.zeros(uxgrid.n_face), + uxgrid=uxgrid, + name="zeros", + dims=['n_face']) -class TestGrad(TestCase): - mpas_atmo_path = current_path / 'meshfiles' / "mpas" / "QU" / 'mesh.QU.1920km.151026.nc' - mpas_ocean_path = current_path / 'meshfiles' / "mpas" / "QU" / 'oQU480.231010.nc' - - CSne30_grid_path = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" - CSne30_data_path = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30_vortex.nc" - - quad_hex_grid_path = current_path / "meshfiles" / "ugrid" / "quad-hexagon" / "grid.nc" - quad_hex_data_path = current_path / "meshfiles" / "ugrid" / "quad-hexagon" / "data.nc" - - geoflow_grid_path = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc" - geoflow_data_path = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "v1.nc" - - def test_uniform_data(self): - """Computes the gradient on meshes with uniform data, with the expected - gradient being zero on all edges.""" - for grid_path in [ - self.mpas_atmo_path, self.mpas_ocean_path, self.CSne30_grid_path, self.quad_hex_grid_path, - ]: - uxgrid = ux.open_grid(grid_path) - - uxda_zeros = ux.UxDataArray(data=np.zeros(uxgrid.n_face), - uxgrid=uxgrid, - name="zeros", - dims=['n_face']) - - zero_grad = uxda_zeros.gradient() - - nt.assert_array_equal(zero_grad.values, np.zeros(uxgrid.n_edge)) - - uxda_ones = ux.UxDataArray(data=np.ones(uxgrid.n_face), - uxgrid=uxgrid, - name="zeros", - dims=['n_face']) - - one_grad = uxda_ones.gradient() - - nt.assert_array_equal(one_grad.values, np.zeros(uxgrid.n_edge)) - - - def test_quad_hex(self): - """Computes the gradient on a mesh of 4 hexagons. - - Computations - ------------ - - [0, 1, 2, 3] [BotLeft, TopRight, TopLeft, BotRight] - - 0.00475918 [0, 2] - 0.00302971 [0, 1] - 0.00241687 [1, 2] - 0.00549181 [0, 3] - 0.00458049 [1, 3] - - |297.716 - 297.646| / 0.00241687 Top Two 28.96 - |297.583 - 297.250| / 0.00549181 Bot Two 60.64 - |297.716 - 297.583| / 0.00475918 Lft Two 27.95 - |297.646 - 297.250| / 0.00458049 Rht Two 86.45 - |297.583 - 297.646| / 0.00302971 Bl Tr 20.79 - """ - - uxds = ux.open_dataset(self.quad_hex_grid_path, self.quad_hex_data_path) - - grad = uxds['t2m'].gradient() - - for i, edge in enumerate(uxds.uxgrid.edge_face_connectivity.values): - - if INT_FILL_VALUE in edge: - # an edge not saddled by two faces has a grad of 0 - assert grad.values[i] == 0 - else: - # a non zero gradient for edges sadled by two faces - assert np.nonzero(grad.values[i]) - - # expected grad values computed by hand - expected_values = np.array([27.95, 20.79, 28.96, 0, 0, 0, 0, 60.64, 0, 86.45, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - - nt.assert_almost_equal(grad.values, expected_values, 1e-2) - - def test_normalization(self): - """Tests the normalization gradient values.""" - - uxds = ux.open_dataset(self.quad_hex_grid_path, self.quad_hex_data_path) - - grad_l2_norm = uxds['t2m'].gradient(normalize=True) - - assert np.isclose(np.sum(grad_l2_norm.values**2), 1) - - - def test_grad_multi_dim(self): - - uxgrid = ux.open_grid(self.quad_hex_grid_path) - - sample_data = np.random.randint(-10, 10, (5, 5, 4)) - - uxda = ux.UxDataArray(uxgrid=uxgrid, - data=sample_data, - dims=["time", "lev", "n_face"]) - - grad = uxda.gradient(normalize=True) - - assert grad.shape[:-1] == uxda.shape[:-1] - - - - -class TestDifference(TestCase): - - quad_hex_grid_path = current_path / "meshfiles" / "ugrid" / "quad-hexagon" / "grid.nc" - quad_hex_data_path = current_path / "meshfiles" / "ugrid" / "quad-hexagon" / "data.nc" - - geoflow_grid_path = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc" - geoflow_data_path = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "v1.nc" - - mpas_atmo_path = current_path / 'meshfiles' / "mpas" / "QU" / 'mesh.QU.1920km.151026.nc' - mpas_ocean_path = current_path / 'meshfiles' / "mpas" / "QU" / 'oQU480.231010.nc' - - CSne30_grid_path = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" - CSne30_data_path = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30_vortex.nc" - + zero_grad = uxda_zeros.gradient() + nt.assert_array_equal(zero_grad.values, np.zeros(uxgrid.n_edge)) - def test_face_centered_difference(self): + uxda_ones = ux.UxDataArray(data=np.ones(uxgrid.n_face), + uxgrid=uxgrid, + name="ones", + dims=['n_face']) - uxds = ux.open_dataset(self.CSne30_grid_path, self.CSne30_data_path) + one_grad = uxda_ones.gradient() + nt.assert_array_equal(one_grad.values, np.zeros(uxgrid.n_edge)) - uxda_diff = uxds['psi'].difference(destination='edge') +def test_quad_hex(): + """Computes the gradient on a mesh of 4 hexagons.""" + uxds = ux.open_dataset(quad_hex_grid_path, quad_hex_data_path) + grad = uxds['t2m'].gradient() - assert uxda_diff._edge_centered() + for i, edge in enumerate(uxds.uxgrid.edge_face_connectivity.values): + if INT_FILL_VALUE in edge: + assert grad.values[i] == 0 + else: + assert np.nonzero(grad.values[i]) - uxds = ux.open_dataset(self.mpas_atmo_path, self.mpas_atmo_path) + expected_values = np.array([27.95, 20.79, 28.96, 0, 0, 0, 0, 60.64, 0, 86.45, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + nt.assert_almost_equal(grad.values, expected_values, 1e-2) - uxda_diff = uxds['areaCell'].difference(destination='edge') +def test_normalization(): + """Tests the normalization gradient values.""" + uxds = ux.open_dataset(quad_hex_grid_path, quad_hex_data_path) + grad_l2_norm = uxds['t2m'].gradient(normalize=True) - assert uxda_diff._edge_centered() + assert np.isclose(np.sum(grad_l2_norm.values**2), 1) +def test_grad_multi_dim(): + uxgrid = ux.open_grid(quad_hex_grid_path) + sample_data = np.random.randint(-10, 10, (5, 5, 4)) + uxda = ux.UxDataArray(uxgrid=uxgrid, + data=sample_data, + dims=["time", "lev", "n_face"]) + grad = uxda.gradient(normalize=True) + assert grad.shape[:-1] == uxda.shape[:-1] - def test_node_centered_difference(self): - uxds = ux.open_dataset(self.geoflow_grid_path, self.geoflow_data_path) +def test_face_centered_difference(): + uxds = ux.open_dataset(CSne30_grid_path, CSne30_data_path) + uxda_diff = uxds['psi'].difference(destination='edge') - uxda_diff = uxds['v1'][0][0].difference(destination='edge') + assert uxda_diff._edge_centered() - assert uxda_diff._edge_centered() + uxds = ux.open_dataset(mpas_atmo_path, mpas_atmo_path) + uxda_diff = uxds['areaCell'].difference(destination='edge') + assert uxda_diff._edge_centered() - def test_hexagon(self): - uxds = ux.open_dataset(self.quad_hex_grid_path, self.quad_hex_data_path) +def test_node_centered_difference(): + uxds = ux.open_dataset(geoflow_grid_path, geoflow_data_path) + uxda_diff = uxds['v1'][0][0].difference(destination='edge') - uxda_diff = uxds['t2m'].difference(destination='edge') + assert uxda_diff._edge_centered() +def test_hexagon(): + uxds = ux.open_dataset(quad_hex_grid_path, quad_hex_data_path) + uxda_diff = uxds['t2m'].difference(destination='edge') - # expected number of edges is n_face + 1, since we have 4 polygons - assert len(np.nonzero(uxda_diff.values)[0]) == uxds.uxgrid.n_face + 1 + assert len(np.nonzero(uxda_diff.values)[0]) == uxds.uxgrid.n_face + 1 diff --git a/test/test_grid.py b/test/test_grid.py index 81db37193..4c8d98c76 100644 --- a/test/test_grid.py +++ b/test/test_grid.py @@ -8,6 +8,7 @@ import uxarray import uxarray as ux +import pytest from uxarray.grid.connectivity import _populate_face_edge_connectivity, _build_edge_face_connectivity, \ _build_edge_node_connectivity, _build_face_face_connectivity, _populate_face_face_connectivity @@ -44,963 +45,690 @@ shp_filename = current_path / "meshfiles" / "shp" / "grid_fire.shp" -class TestGrid(TestCase): - grid_CSne30 = ux.open_grid(gridfile_CSne30) - grid_RLL1deg = ux.open_grid(gridfile_RLL1deg) - grid_RLL10deg_CSne4 = ux.open_grid(gridfile_RLL10deg_CSne4) - - def test_validate(self): - """Test to check the validate function.""" - grid_mpas = ux.open_grid(gridfile_mpas) - assert (grid_mpas.validate()) - - def test_grid_with_holes(self): - """Test _holes_in_mesh function.""" - grid_without_holes = ux.open_grid(gridfile_mpas) - grid_with_holes = ux.open_grid(gridfile_mpas_holes) - - self.assertTrue(grid_with_holes.partial_sphere_coverage) - self.assertTrue(grid_without_holes.global_sphere_coverage) - - def test_encode_as(self): - """Reads a ugrid file and encodes it as `xarray.Dataset` in various - types.""" - - self.grid_CSne30.encode_as("UGRID") - self.grid_RLL1deg.encode_as("UGRID") - self.grid_RLL10deg_CSne4.encode_as("UGRID") - - self.grid_CSne30.encode_as("Exodus") - self.grid_RLL1deg.encode_as("Exodus") - self.grid_RLL10deg_CSne4.encode_as("Exodus") - - - def test_init_verts(self): - """Create a uxarray grid from multiple face vertices with duplicate - nodes and saves a ugrid file. - - Also, test kwargs for grid initialization - - The input cartesian coordinates represents 8 vertices on a cube - 7---------6 /| /| / | / | 3---------2 | | - | | | | 4------|--5 | / | / |/ |/ 0 - ---------1 - """ - cart_x = [ - 0.577340924821405, 0.577340924821405, 0.577340924821405, - 0.577340924821405, -0.577345166204668, -0.577345166204668, - -0.577345166204668, -0.577345166204668 - ] - cart_y = [ - 0.577343045516932, 0.577343045516932, -0.577343045516932, - -0.577343045516932, 0.577338804118089, 0.577338804118089, - -0.577338804118089, -0.577338804118089 - ] - cart_z = [ - 0.577366836872017, -0.577366836872017, 0.577366836872017, - -0.577366836872017, 0.577366836872017, -0.577366836872017, - 0.577366836872017, -0.577366836872017 - ] - - # The order of the vertexes is irrelevant, the following indexing is just for forming a face matrix - face_vertices = [ - [0, 1, 2, 3], # front face - [1, 5, 6, 2], # right face - [5, 4, 7, 6], # back face - [4, 0, 3, 7], # left face - [3, 2, 6, 7], # top face - [4, 5, 1, 0] # bottom face - ] - - # Pack the cart_x/y/z into the face matrix using the index from face_vertices - faces_coords = [] - for face in face_vertices: - face_coords = [] - for vertex_index in face: - x, y, z = cart_x[vertex_index], cart_y[vertex_index], cart_z[ - vertex_index] - face_coords.append([x, y, z]) - faces_coords.append(face_coords) - - # Now consturct the grid using the faces_coords - verts_cart = np.array(faces_coords) - vgrid = ux.open_grid(verts_cart, latlon=False) - - assert (vgrid.n_face == 6) - assert (vgrid.n_node == 8) - vgrid.encode_as("UGRID") - - # Test the case when user created a nested one-face grid - faces_verts_one = np.array([ - np.array([[150, 10], [160, 20], [150, 30], [135, 30], [125, 20], - [135, 10]]) - ]) - vgrid = ux.open_grid(faces_verts_one, latlon=True) - assert (vgrid.n_face == 1) - assert (vgrid.n_node == 6) - vgrid.encode_as("UGRID") - - # Test the case when user created a one-face grid - faces_verts_single_face = np.array([[150, 10], [160, 20], [150, 30], - [135, 30], [125, 20], [135, 10]]) - - vgrid = ux.open_grid(faces_verts_single_face, latlon=True) - assert (vgrid.n_face == 1) - assert (vgrid.n_node == 6) - vgrid.encode_as("UGRID") - - def test_init_verts_different_input_datatype(self): - """Create a uxarray grid from multiple face vertices with different - datatypes(ndarray, list, tuple) and saves a ugrid file. - - Also, test kwargs for grid initialization - """ - - # Test initializing Grid from ndarray - faces_verts_ndarray = np.array([ - np.array([[150, 10], [160, 20], [150, 30], [135, 30], [125, 20], - [135, 10]]), - np.array([[125, 20], [135, 30], [125, 60], [110, 60], [100, 30], - [105, 20]]), - np.array([[95, 10], [105, 20], [100, 30], [85, 30], [75, 20], - [85, 10]]), - ]) - vgrid = ux.open_grid(faces_verts_ndarray, latlon=True) - assert (vgrid.n_face == 3) - assert (vgrid.n_node == 14) - vgrid.encode_as("UGRID") - print(vgrid._ds) - - # Test initializing Grid from list - faces_verts_list = [[[150, 10], [160, 20], [150, 30], [135, 30], - [125, 20], [135, 10]], - [[125, 20], [135, 30], [125, 60], [110, 60], - [100, 30], [105, 20]], - [[95, 10], [105, 20], [100, 30], [85, 30], [75, 20], - [85, 10]]] - vgrid = ux.open_grid(faces_verts_list, latlon=True) - assert (vgrid.n_face == 3) - assert (vgrid.n_node == 14) - - # validate the grid - assert (vgrid.validate()) - - vgrid.encode_as("UGRID") - - # Test initializing Grid from tuples - faces_verts_tuples = [ - ((150, 10), (160, 20), (150, 30), (135, 30), (125, 20), (135, 10)), - ((125, 20), (135, 30), (125, 60), (110, 60), (100, 30), (105, 20)), - ((95, 10), (105, 20), (100, 30), (85, 30), (75, 20), (85, 10)) - ] - vgrid = ux.open_grid(faces_verts_tuples, latlon=True) - assert (vgrid.n_face == 3) - assert (vgrid.n_node == 14) - - # validate the grid - assert (vgrid.validate()) - - vgrid.encode_as("UGRID") - - def test_init_verts_fill_values(self): - faces_verts_filled_values = [[[150, 10], [160, 20], [150, 30], - [135, 30], [125, 20], [135, 10]], - [[125, 20], [135, 30], [125, 60], - [110, 60], [100, 30], - [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE]], - [[95, 10], [105, 20], [100, 30], [85, 30], - [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE], - [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE]]] - vgrid = ux.open_grid( - faces_verts_filled_values, - latlon=False, - ) - assert (vgrid.n_face == 3) - assert (vgrid.n_node == 12) - - def test_grid_properties(self): - """Tests to see if accessing variables through set properties is equal - to using the dict.""" - - # Dataset with standard UGRID variable names - # Coordinates - xr.testing.assert_equal(self.grid_CSne30.node_lon, - self.grid_CSne30._ds["node_lon"]) - xr.testing.assert_equal(self.grid_CSne30.node_lat, - self.grid_CSne30._ds["node_lat"]) - # Variables - xr.testing.assert_equal(self.grid_CSne30.face_node_connectivity, - self.grid_CSne30._ds["face_node_connectivity"]) - - # Dimensions - n_nodes = self.grid_CSne30.node_lon.shape[0] - n_faces, n_face_nodes = self.grid_CSne30.face_node_connectivity.shape - - self.assertEqual(n_nodes, self.grid_CSne30.n_node) - self.assertEqual(n_faces, self.grid_CSne30.n_face) - self.assertEqual(n_face_nodes, self.grid_CSne30.n_max_face_nodes) - - # Dataset with non-standard UGRID variable names - grid_geoflow = ux.open_grid(gridfile_geoflow) - - xr.testing.assert_equal(grid_geoflow.node_lon, - grid_geoflow._ds["node_lon"]) - xr.testing.assert_equal(grid_geoflow.node_lat, - grid_geoflow._ds["node_lat"]) - # Variables - xr.testing.assert_equal(grid_geoflow.face_node_connectivity, - grid_geoflow._ds["face_node_connectivity"]) - # Dimensions - n_nodes = grid_geoflow.node_lon.shape[0] - n_faces, n_face_nodes = grid_geoflow.face_node_connectivity.shape - - self.assertEqual(n_nodes, grid_geoflow.n_node) - self.assertEqual(n_faces, grid_geoflow.n_face) - self.assertEqual(n_face_nodes, grid_geoflow.n_max_face_nodes) - - def test_read_shpfile(self): - """Reads a shape file and write ugrid file.""" - with self.assertRaises(ValueError): - grid_shp = ux.open_grid(shp_filename) - - def test_read_scrip(self): - """Reads a scrip file.""" - - # Test read from scrip and from ugrid for grid class - grid_CSne8 = ux.open_grid(gridfile_CSne8) # tests from scrip - - -class TestOperators(TestCase): + +grid_CSne30 = ux.open_grid(gridfile_CSne30) +grid_RLL1deg = ux.open_grid(gridfile_RLL1deg) +grid_RLL10deg_CSne4 = ux.open_grid(gridfile_RLL10deg_CSne4) + + +mpas_filepath = current_path / "meshfiles" / "mpas" / "QU" / "mesh.QU.1920km.151026.nc" +exodus_filepath = current_path / "meshfiles" / "exodus" / "outCSne8" / "outCSne8.g" +ugrid_filepath_01 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" +ugrid_filepath_02 = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug" +ugrid_filepath_03 = current_path / "meshfiles" / "ugrid" / "ov_RLL10deg_CSne4" / "ov_RLL10deg_CSne4.ug" + +grid_mpas = ux.open_grid(mpas_filepath) +grid_exodus = ux.open_grid(exodus_filepath) +grid_ugrid = ux.open_grid(ugrid_filepath_01) + +f0_deg = [[120, -20], [130, -10], [120, 0], [105, 0], [95, -10], [105, -20]] +f1_deg = [[120, 0], [120, 10], [115, 0], + [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE], + [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE], + [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE]] +f2_deg = [[115, 0], [120, 10], [100, 10], [105, 0], + [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE], + [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE]] +f3_deg = [[95, -10], [105, 0], [95, 30], [80, 30], [70, 0], [75, -10]] +f4_deg = [[65, -20], [75, -10], [70, 0], [55, 0], [45, -10], [55, -20]] +f5_deg = [[70, 0], [80, 30], [70, 30], [60, 0], + [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE], + [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE]] +f6_deg = [[60, 0], [70, 30], [40, 30], [45, 0], + [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE], + [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE]] + +gridfile_ugrid = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc" +gridfile_mpas = current_path / "meshfiles" / "mpas" / "QU" / "mesh.QU.1920km.151026.nc" +gridfile_exodus = current_path / "meshfiles" / "exodus" / "outCSne8" / "outCSne8.g" +gridfile_scrip = current_path / "meshfiles" / "scrip" / "outCSne8" / "outCSne8.nc" + + +def test_grid_validate(): + """Test to check the validate function.""" + grid_mpas = ux.open_grid(gridfile_mpas) + assert grid_mpas.validate() + +def test_grid_with_holes(): + """Test _holes_in_mesh function.""" + grid_without_holes = ux.open_grid(gridfile_mpas) + grid_with_holes = ux.open_grid(gridfile_mpas_holes) + + assert grid_with_holes.partial_sphere_coverage + assert grid_without_holes.global_sphere_coverage + +def test_grid_encode_as(): + """Reads a ugrid file and encodes it as `xarray.Dataset` in various types.""" + grid_CSne30.encode_as("UGRID") + grid_RLL1deg.encode_as("UGRID") + grid_RLL10deg_CSne4.encode_as("UGRID") + + grid_CSne30.encode_as("Exodus") + grid_RLL1deg.encode_as("Exodus") + grid_RLL10deg_CSne4.encode_as("Exodus") + +def test_grid_init_verts(): + """Create a uxarray grid from multiple face vertices with duplicate nodes and saves a ugrid file.""" + cart_x = [ + 0.577340924821405, 0.577340924821405, 0.577340924821405, + 0.577340924821405, -0.577345166204668, -0.577345166204668, + -0.577345166204668, -0.577345166204668 + ] + cart_y = [ + 0.577343045516932, 0.577343045516932, -0.577343045516932, + -0.577343045516932, 0.577338804118089, 0.577338804118089, + -0.577338804118089, -0.577338804118089 + ] + cart_z = [ + 0.577366836872017, -0.577366836872017, 0.577366836872017, + -0.577366836872017, 0.577366836872017, -0.577366836872017, + 0.577366836872017, -0.577366836872017 + ] + + face_vertices = [ + [0, 1, 2, 3], # front face + [1, 5, 6, 2], # right face + [5, 4, 7, 6], # back face + [4, 0, 3, 7], # left face + [3, 2, 6, 7], # top face + [4, 5, 1, 0] # bottom face + ] + + faces_coords = [] + for face in face_vertices: + face_coords = [] + for vertex_index in face: + x, y, z = cart_x[vertex_index], cart_y[vertex_index], cart_z[vertex_index] + face_coords.append([x, y, z]) + faces_coords.append(face_coords) + + verts_cart = np.array(faces_coords) + vgrid = ux.open_grid(verts_cart, latlon=False) + + assert vgrid.n_face == 6 + assert vgrid.n_node == 8 + vgrid.encode_as("UGRID") + + faces_verts_one = np.array([ + np.array([[150, 10], [160, 20], [150, 30], [135, 30], [125, 20], [135, 10]]) + ]) + vgrid = ux.open_grid(faces_verts_one, latlon=True) + assert vgrid.n_face == 1 + assert vgrid.n_node == 6 + vgrid.encode_as("UGRID") + + faces_verts_single_face = np.array([[150, 10], [160, 20], [150, 30], [135, 30], [125, 20], [135, 10]]) + vgrid = ux.open_grid(faces_verts_single_face, latlon=True) + assert vgrid.n_face == 1 + assert vgrid.n_node == 6 + vgrid.encode_as("UGRID") + +def test_grid_init_verts_different_input_datatype(): + """Create a uxarray grid from multiple face vertices with different datatypes (ndarray, list, tuple) and saves a ugrid file.""" + faces_verts_ndarray = np.array([ + np.array([[150, 10], [160, 20], [150, 30], [135, 30], [125, 20], [135, 10]]), + np.array([[125, 20], [135, 30], [125, 60], [110, 60], [100, 30], [105, 20]]), + np.array([[95, 10], [105, 20], [100, 30], [85, 30], [75, 20], [85, 10]]), + ]) + vgrid = ux.open_grid(faces_verts_ndarray, latlon=True) + assert vgrid.n_face == 3 + assert vgrid.n_node == 14 + vgrid.encode_as("UGRID") + + faces_verts_list = [[[150, 10], [160, 20], [150, 30], [135, 30], [125, 20], [135, 10]], + [[125, 20], [135, 30], [125, 60], [110, 60], [100, 30], [105, 20]], + [[95, 10], [105, 20], [100, 30], [85, 30], [75, 20], [85, 10]]] + vgrid = ux.open_grid(faces_verts_list, latlon=True) + assert vgrid.n_face == 3 + assert vgrid.n_node == 14 + assert vgrid.validate() + vgrid.encode_as("UGRID") + + faces_verts_tuples = [ + ((150, 10), (160, 20), (150, 30), (135, 30), (125, 20), (135, 10)), + ((125, 20), (135, 30), (125, 60), (110, 60), (100, 30), (105, 20)), + ((95, 10), (105, 20), (100, 30), (85, 30), (75, 20), (85, 10)) + ] + vgrid = ux.open_grid(faces_verts_tuples, latlon=True) + assert vgrid.n_face == 3 + assert vgrid.n_node == 14 + assert vgrid.validate() + vgrid.encode_as("UGRID") + +def test_grid_init_verts_fill_values(): + faces_verts_filled_values = [[[150, 10], [160, 20], [150, 30], + [135, 30], [125, 20], [135, 10]], + [[125, 20], [135, 30], [125, 60], + [110, 60], [100, 30], + [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE]], + [[95, 10], [105, 20], [100, 30], [85, 30], + [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE], + [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE]]] + vgrid = ux.open_grid(faces_verts_filled_values, latlon=False) + assert vgrid.n_face == 3 + assert vgrid.n_node == 12 + +def test_grid_properties(): + """Tests to see if accessing variables through set properties is equal to using the dict.""" + xr.testing.assert_equal(grid_CSne30.node_lon, grid_CSne30._ds["node_lon"]) + xr.testing.assert_equal(grid_CSne30.node_lat, grid_CSne30._ds["node_lat"]) + xr.testing.assert_equal(grid_CSne30.face_node_connectivity, grid_CSne30._ds["face_node_connectivity"]) + + n_nodes = grid_CSne30.node_lon.shape[0] + n_faces, n_face_nodes = grid_CSne30.face_node_connectivity.shape + + assert n_nodes == grid_CSne30.n_node + assert n_faces == grid_CSne30.n_face + assert n_face_nodes == grid_CSne30.n_max_face_nodes + + grid_geoflow = ux.open_grid(gridfile_geoflow) + + xr.testing.assert_equal(grid_geoflow.node_lon, grid_geoflow._ds["node_lon"]) + xr.testing.assert_equal(grid_geoflow.node_lat, grid_geoflow._ds["node_lat"]) + xr.testing.assert_equal(grid_geoflow.face_node_connectivity, grid_geoflow._ds["face_node_connectivity"]) + + n_nodes = grid_geoflow.node_lon.shape[0] + n_faces, n_face_nodes = grid_geoflow.face_node_connectivity.shape + + assert n_nodes == grid_geoflow.n_node + assert n_faces == grid_geoflow.n_face + assert n_face_nodes == grid_geoflow.n_max_face_nodes + +def test_read_shpfile(): + """Reads a shape file and write ugrid file.""" + with pytest.raises(ValueError): + grid_shp = ux.open_grid(shp_filename) + +def test_read_scrip(): + """Reads a scrip file.""" + grid_CSne8 = ux.open_grid(gridfile_CSne8) # tests from scrip + +def test_operators_eq(): + """Test Equals ('==') operator.""" grid_CSne30_01 = ux.open_grid(gridfile_CSne30) grid_CSne30_02 = ux.open_grid(gridfile_CSne30) - grid_RLL1deg = ux.open_grid(gridfile_RLL1deg) - - def test_eq(self): - """Test Equals ('==') operator.""" - assert self.grid_CSne30_01 == self.grid_CSne30_02 - - def test_ne(self): - """Test Not Equals ('!=') operator.""" - assert self.grid_CSne30_01 != self.grid_RLL1deg - + assert grid_CSne30_01 == grid_CSne30_02 -class TestFaceAreas(TestCase): - grid_CSne30 = ux.open_grid(gridfile_CSne30) - - def test_calculate_total_face_area_triangle(self): - """Create a uxarray grid from vertices and saves an exodus file.""" - - verts = [[[0.57735027, -5.77350269e-01, -0.57735027], - [0.57735027, 5.77350269e-01, -0.57735027], - [-0.57735027, 5.77350269e-01, -0.57735027]]] - - grid_verts = ux.open_grid(verts, latlon=False) - - # validate the grid - assert (grid_verts.validate()) - - # validate the grid - assert (grid_verts.validate()) - - # calculate area - area_gaussian = grid_verts.calculate_total_face_area( - quadrature_rule="gaussian", order=5) - nt.assert_almost_equal(area_gaussian, constants.TRI_AREA, decimal=3) - - area_triangular = grid_verts.calculate_total_face_area( - quadrature_rule="triangular", order=4) - nt.assert_almost_equal(area_triangular, constants.TRI_AREA, decimal=1) - - def test_calculate_total_face_area_file(self): - """Create a uxarray grid from vertices and saves an exodus file.""" +def test_operators_ne(): + """Test Not Equals ('!=') operator.""" + grid_CSne30_01 = ux.open_grid(gridfile_CSne30) + grid_RLL1deg = ux.open_grid(gridfile_RLL1deg) + assert grid_CSne30_01 != grid_RLL1deg + +def test_face_areas_calculate_total_face_area_triangle(): + """Create a uxarray grid from vertices and saves an exodus file.""" + verts = [[[0.57735027, -5.77350269e-01, -0.57735027], + [0.57735027, 5.77350269e-01, -0.57735027], + [-0.57735027, 5.77350269e-01, -0.57735027]]] + + grid_verts = ux.open_grid(verts, latlon=False) + + # validate the grid + assert grid_verts.validate() + + # calculate area + area_gaussian = grid_verts.calculate_total_face_area( + quadrature_rule="gaussian", order=5) + nt.assert_almost_equal(area_gaussian, constants.TRI_AREA, decimal=3) + + area_triangular = grid_verts.calculate_total_face_area( + quadrature_rule="triangular", order=4) + nt.assert_almost_equal(area_triangular, constants.TRI_AREA, decimal=1) + +def test_face_areas_calculate_total_face_area_file(): + """Create a uxarray grid from vertices and saves an exodus file.""" + area = ux.open_grid(gridfile_CSne30).calculate_total_face_area() + nt.assert_almost_equal(area, constants.MESH30_AREA, decimal=3) + +def test_face_areas_calculate_total_face_area_sphere(): + """Computes the total face area of an MPAS mesh that lies on a unit sphere, with an expected total face area of 4pi.""" + mpas_grid_path = current_path / 'meshfiles' / "mpas" / "QU" / 'mesh.QU.1920km.151026.nc' + + primal_grid = ux.open_grid(mpas_grid_path, use_dual=False) + dual_grid = ux.open_grid(mpas_grid_path, use_dual=True) + + primal_face_area = primal_grid.calculate_total_face_area() + dual_face_area = dual_grid.calculate_total_face_area() + + nt.assert_almost_equal(primal_face_area, constants.UNIT_SPHERE_AREA, decimal=3) + nt.assert_almost_equal(dual_face_area, constants.UNIT_SPHERE_AREA, decimal=3) + +def test_face_areas_compute_face_areas_geoflow_small(): + """Checks if the GeoFlow Small can generate a face areas output.""" + grid_geoflow = ux.open_grid(gridfile_geoflow) + grid_geoflow.compute_face_areas() + +def test_face_areas_verts_calc_area(): + faces_verts_ndarray = np.array([ + np.array([[150, 10, 0], [160, 20, 0], [150, 30, 0], [135, 30, 0], + [125, 20, 0], [135, 10, 0]]), + np.array([[125, 20, 0], [135, 30, 0], [125, 60, 0], [110, 60, 0], + [100, 30, 0], [105, 20, 0]]), + np.array([[95, 10, 0], [105, 20, 0], [100, 30, 0], [85, 30, 0], + [75, 20, 0], [85, 10, 0]]), + ]) + verts_grid = ux.open_grid(faces_verts_ndarray, latlon=True) + face_verts_areas = verts_grid.face_areas + nt.assert_almost_equal(face_verts_areas.sum(), constants.FACE_VERTS_AREA, decimal=3) + +def test_populate_coordinates_populate_cartesian_xyz_coord(): + # The following testcases are generated through the matlab cart2sph/sph2cart functions + lon_deg = [ + 45.0001052295749, 45.0001052295749, 360 - 45.0001052295749, + 360 - 45.0001052295749 + ] + lat_deg = [ + 35.2655522903022, -35.2655522903022, 35.2655522903022, + -35.2655522903022 + ] + cart_x = [ + 0.577340924821405, 0.577340924821405, 0.577340924821405, + 0.577340924821405 + ] + cart_y = [ + 0.577343045516932, 0.577343045516932, -0.577343045516932, + -0.577343045516932 + ] + cart_z = [ + -0.577366836872017, 0.577366836872017, -0.577366836872017, + 0.577366836872017 + ] + + verts_degree = np.stack((lon_deg, lat_deg), axis=1) + vgrid = ux.open_grid(verts_degree, latlon=True) + + for i in range(0, vgrid.n_node): + nt.assert_almost_equal(vgrid.node_x.values[i], cart_x[i], decimal=12) + nt.assert_almost_equal(vgrid.node_y.values[i], cart_y[i], decimal=12) + nt.assert_almost_equal(vgrid.node_z.values[i], cart_z[i], decimal=12) + +def test_populate_coordinates_populate_lonlat_coord(): + lon_deg = [ + 45.0001052295749, 45.0001052295749, 360 - 45.0001052295749, + 360 - 45.0001052295749 + ] + lat_deg = [ + 35.2655522903022, -35.2655522903022, 35.2655522903022, + -35.2655522903022 + ] + cart_x = [ + 0.577340924821405, 0.577340924821405, 0.577340924821405, + 0.577340924821405 + ] + cart_y = [ + 0.577343045516932, 0.577343045516932, -0.577343045516932, + -0.577343045516932 + ] + cart_z = [ + 0.577366836872017, -0.577366836872017, 0.577366836872017, + -0.577366836872017 + ] + + verts_cart = np.stack((cart_x, cart_y, cart_z), axis=1) + vgrid = ux.open_grid(verts_cart, latlon=False) + _populate_node_latlon(vgrid) + lon_deg, lat_deg = zip(*reversed(list(zip(lon_deg, lat_deg)))) + for i in range(0, vgrid.n_node): + nt.assert_almost_equal(vgrid._ds["node_lon"].values[i], lon_deg[i], decimal=12) + nt.assert_almost_equal(vgrid._ds["node_lat"].values[i], lat_deg[i], decimal=12) + + +def _revert_edges_conn_to_face_nodes_conn(edge_nodes_connectivity: np.ndarray, + face_edges_connectivity: np.ndarray, + original_face_nodes_connectivity: np.ndarray): + """Utilize the edge_nodes_connectivity and face_edges_connectivity to + generate the res_face_nodes_connectivity in the counter-clockwise + order. The counter-clockwise order will be enforced by the passed in + original_face_edges_connectivity. We will only use the first two nodes + in the original_face_edges_connectivity. The order of these two nodes + will provide a correct counter-clockwise order to build our + res_face_nodes_connectivity. A ValueError will be raised if the first + two nodes in the res_face_nodes_connectivity and the + original_face_nodes_connectivity are not the same elements (The order + doesn't matter here). + """ + # Create a dictionary to store the face indices for each edge + face_nodes_dict = {} + + # Loop through each face and edge to build the dictionary + for face_idx, face_edges in enumerate(face_edges_connectivity): + for edge_idx in face_edges: + if edge_idx != ux.INT_FILL_VALUE: + edge = edge_nodes_connectivity[edge_idx] + if face_idx not in face_nodes_dict: + face_nodes_dict[face_idx] = [] + face_nodes_dict[face_idx].append(edge[0]) + face_nodes_dict[face_idx].append(edge[1]) + + # Make sure the face_nodes_dict is in the counter-clockwise order and remove duplicate nodes + for face_idx, face_nodes in face_nodes_dict.items(): + first_edge_correct = np.array([ + original_face_nodes_connectivity[face_idx][0], + original_face_nodes_connectivity[face_idx][1] + ]) + first_edge = np.array([face_nodes[0], face_nodes[1]]) - # = self.grid_CSne30.calculate_total_face_area() - area = ux.open_grid(gridfile_CSne30).calculate_total_face_area() + first_edge_correct_copy = first_edge_correct.copy() + first_edge_copy = first_edge.copy() + assert np.array_equal(np.sort(first_edge_correct_copy), np.sort(first_edge_copy)) + face_nodes[0] = first_edge_correct[0] + face_nodes[1] = first_edge_correct[1] - nt.assert_almost_equal(area, constants.MESH30_AREA, decimal=3) + i = 2 + while i < len(face_nodes): + if face_nodes[i] != face_nodes[i - 1]: + old = face_nodes[i] + face_nodes[i] = face_nodes[i - 1] + face_nodes[i + 1] = old + i += 2 - def test_calculate_total_face_area_sphere(self): - """Computes the total face area of an MPAS mesh that lies on a unit - sphere, with an expected total face area of 4pi.""" - mpas_grid_path = current_path / 'meshfiles' / "mpas" / "QU" / 'mesh.QU.1920km.151026.nc' + after_swapped = face_nodes + after_swapped_remove = [after_swapped[0]] - primal_grid = ux.open_grid(mpas_grid_path, use_dual=False) - dual_grid = ux.open_grid(mpas_grid_path, use_dual=True) + for i in range(1, len(after_swapped) - 1): + if after_swapped[i] != after_swapped[i - 1]: + after_swapped_remove.append(after_swapped[i]) - primal_face_area = primal_grid.calculate_total_face_area() - dual_face_area = dual_grid.calculate_total_face_area() + face_nodes_dict[face_idx] = after_swapped_remove - nt.assert_almost_equal(primal_face_area, - constants.UNIT_SPHERE_AREA, - decimal=3) + # Convert the dictionary to a list + res_face_nodes_connectivity = [] + for face_idx in range(len(face_edges_connectivity)): + res_face_nodes_connectivity.append(face_nodes_dict[face_idx]) + while len(res_face_nodes_connectivity[face_idx]) < original_face_nodes_connectivity.shape[1]: + res_face_nodes_connectivity[face_idx].append(ux.INT_FILL_VALUE) - nt.assert_almost_equal(dual_face_area, - constants.UNIT_SPHERE_AREA, - decimal=3) + return np.array(res_face_nodes_connectivity) - # TODO: Will depend on the decision for whether to provide integrate function - # from within `Grid` as well as UxDataset - # def test_integrate(self): - # xr_psi = xr.open_dataset(dsfile_vortex_CSne30) - # xr_v2 = xr.open_dataset(dsfile_var2_CSne30) - # - # integral_psi = self.grid_CSne30.integrate(xr_psi) - # integral_var2 = self.grid_CSne30.integrate(xr_v2) - # - # nt.assert_almost_equal(integral_psi, constants.PSI_INTG, decimal=3) - # nt.assert_almost_equal(integral_var2, constants.VAR2_INTG, decimal=3) +def test_connectivity_build_n_nodes_per_face(): + """Tests the construction of the ``n_nodes_per_face`` variable.""" + grids = [grid_mpas, grid_exodus, grid_ugrid] - def test_compute_face_areas_geoflow_small(self): - """Checks if the GeoFlow Small can generate a face areas output.""" - grid_geoflow = ux.open_grid(gridfile_geoflow) + for grid in grids: + max_dimension = grid.n_max_face_nodes + min_dimension = 3 - grid_geoflow.compute_face_areas() + assert grid.n_nodes_per_face.min() >= min_dimension + assert grid.n_nodes_per_face.max() <= max_dimension - # TODO: Add this test after fix to tranposed face nodes - # def test_compute_face_areas_fesom(self): - # """Checks if the FESOM PI-Grid Output can generate a face areas - # output.""" - # grid_fesom = ux.open_grid(gridfile_fesom) - # - # grid_fesom.compute_face_areas() + verts = [f0_deg, f1_deg, f2_deg, f3_deg, f4_deg, f5_deg, f6_deg] + grid_from_verts = ux.open_grid(verts) - def test_verts_calc_area(self): - faces_verts_ndarray = np.array([ - np.array([[150, 10, 0], [160, 20, 0], [150, 30, 0], [135, 30, 0], - [125, 20, 0], [135, 10, 0]]), - np.array([[125, 20, 0], [135, 30, 0], [125, 60, 0], [110, 60, 0], - [100, 30, 0], [105, 20, 0]]), - np.array([[95, 10, 0], [105, 20, 0], [100, 30, 0], [85, 30, 0], - [75, 20, 0], [85, 10, 0]]), - ]) - # load our vertices into a UXarray Grid object - verts_grid = ux.open_grid(faces_verts_ndarray, latlon=True) - - face_verts_areas = verts_grid.face_areas - - nt.assert_almost_equal(face_verts_areas.sum(), - constants.FACE_VERTS_AREA, - decimal=3) - - -class TestPopulateCoordinates(TestCase): - - def test_populate_cartesian_xyz_coord(self): - # The following testcases are generated through the matlab cart2sph/sph2cart functions - # These points correspond to the eight vertices of a cube. - lon_deg = [ - 45.0001052295749, 45.0001052295749, 360 - 45.0001052295749, - 360 - 45.0001052295749 - ] - lat_deg = [ - 35.2655522903022, -35.2655522903022, 35.2655522903022, - -35.2655522903022 - ] - cart_x = [ - 0.577340924821405, 0.577340924821405, 0.577340924821405, - 0.577340924821405 - ] - - cart_y = [ - 0.577343045516932, 0.577343045516932, -0.577343045516932, - -0.577343045516932 - ] - - cart_z = [ - -0.577366836872017, 0.577366836872017, -0.577366836872017, - 0.577366836872017 - ] - - verts_degree = np.stack((lon_deg, lat_deg), axis=1) - - vgrid = ux.open_grid(verts_degree, latlon=True) - # _populate_cartesian_xyz_coord(vgrid) - - for i in range(0, vgrid.n_node): - nt.assert_almost_equal(vgrid.node_x.values[i], - cart_x[i], - decimal=12) - nt.assert_almost_equal(vgrid.node_y.values[i], - cart_y[i], - decimal=12) - nt.assert_almost_equal(vgrid.node_z.values[i], - cart_z[i], - decimal=12) - - def test_populate_lonlat_coord(self): - # The following testcases are generated through the matlab cart2sph/sph2cart functions - # These points correspond to the 4 vertexes on a cube. - - lon_deg = [ - 45.0001052295749, 45.0001052295749, 360 - 45.0001052295749, - 360 - 45.0001052295749 - ] - lat_deg = [ - 35.2655522903022, -35.2655522903022, 35.2655522903022, - -35.2655522903022 - ] - cart_x = [ - 0.577340924821405, 0.577340924821405, 0.577340924821405, - 0.577340924821405 - ] - cart_y = [ - 0.577343045516932, 0.577343045516932, -0.577343045516932, - -0.577343045516932 - ] - cart_z = [ - 0.577366836872017, -0.577366836872017, 0.577366836872017, - -0.577366836872017 - ] - - verts_cart = np.stack((cart_x, cart_y, cart_z), axis=1) - - vgrid = ux.open_grid(verts_cart, latlon=False) - _populate_node_latlon(vgrid) - # The connectivity in `__from_vert__()` will be formed in a reverse order - lon_deg, lat_deg = zip(*reversed(list(zip(lon_deg, lat_deg)))) - for i in range(0, vgrid.n_node): - nt.assert_almost_equal(vgrid._ds["node_lon"].values[i], - lon_deg[i], - decimal=12) - nt.assert_almost_equal(vgrid._ds["node_lat"].values[i], - lat_deg[i], - decimal=12) - - -class TestConnectivity(TestCase): - mpas_filepath = current_path / "meshfiles" / "mpas" / "QU" / "mesh.QU.1920km.151026.nc" - exodus_filepath = current_path / "meshfiles" / "exodus" / "outCSne8" / "outCSne8.g" - ugrid_filepath_01 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" - ugrid_filepath_02 = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug" - ugrid_filepath_03 = current_path / "meshfiles" / "ugrid" / "ov_RLL10deg_CSne4" / "ov_RLL10deg_CSne4.ug" - - grid_mpas = ux.open_grid(mpas_filepath) - grid_exodus = ux.open_grid(exodus_filepath) - grid_ugrid = ux.open_grid(ugrid_filepath_01) - - # used from constructing vertices - f0_deg = [[120, -20], [130, -10], [120, 0], [105, 0], [95, -10], [105, -20]] - f1_deg = [[120, 0], [120, 10], [115, 0], - [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE], - [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE], - [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE]] - f2_deg = [[115, 0], [120, 10], [100, 10], [105, 0], - [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE], - [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE]] - f3_deg = [[95, -10], [105, 0], [95, 30], [80, 30], [70, 0], [75, -10]] - f4_deg = [[65, -20], [75, -10], [70, 0], [55, 0], [45, -10], [55, -20]] - f5_deg = [[70, 0], [80, 30], [70, 30], [60, 0], - [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE], - [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE]] - f6_deg = [[60, 0], [70, 30], [40, 30], [45, 0], - [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE], - [ux.INT_FILL_VALUE, ux.INT_FILL_VALUE]] - - # Helper function - def _revert_edges_conn_to_face_nodes_conn( - self, edge_nodes_connectivity: np.ndarray, - face_edges_connectivity: np.ndarray, - original_face_nodes_connectivity: np.ndarray): - """Utilize the edge_nodes_connectivity and face_edges_connectivity to - generate the res_face_nodes_connectivity in the counter-clockwise - order. The counter-clockwise order will be enforced by the passed in - original_face_edges_connectivity. We will only use the first two nodes - in the original_face_edges_connectivity. The order of these two nodes - will provide a correct counter-clockwise order to build our - res_face_nodes_connectivity. A ValueError will be raised if the first - two nodes in the res_face_nodes_connectivity and the - original_face_nodes_connectivity are not the same elements (The order - doesn't matter here). - - Parameters - ---------- - edge_nodes_connectivity : np.ndarray - The edge_nodes_connectivity array - face_edges_connectivity : np.ndarray - The face_edges_connectivity array - original_face_nodes_connectivity : np.ndarray - The original face_nodes_connectivity array - - Returns - ------- - res_face_nodes_connectivity : np.ndarray - The face_nodes_connectivity array in the counter-clockwise order - - Raises - ------ - ValueError - if the first two nodes in the res_face_nodes_connectivity are not the same as the first two nodes in the - original_face_nodes_connectivity - """ - - # Create a dictionary to store the face indices for each edge - face_nodes_dict = {} - - # Loop through each face and edge to build the dictionary - for face_idx, face_edges in enumerate(face_edges_connectivity): - for edge_idx in face_edges: - if edge_idx != ux.INT_FILL_VALUE: - edge = edge_nodes_connectivity[edge_idx] - if face_idx not in face_nodes_dict: - face_nodes_dict[face_idx] = [] - face_nodes_dict[face_idx].append(edge[0]) - face_nodes_dict[face_idx].append(edge[1]) - - # Make sure the face_nodes_dict is in the counter-clockwise order and remove duplicate nodes - for face_idx, face_nodes in face_nodes_dict.items(): - # First need to re-position the first two nodes position according to the original face_nodes_connectivity - first_edge_correct = np.array([ - original_face_nodes_connectivity[face_idx][0], - original_face_nodes_connectivity[face_idx][1] - ]) - first_edge = np.array([face_nodes[0], face_nodes[1]]) - - first_edge_correct_copy = first_edge_correct.copy() - first_edge_copy = first_edge.copy() - self.assertTrue( - np.array_equal(first_edge_correct_copy.sort(), - first_edge_copy.sort())) - face_nodes[0] = first_edge_correct[0] - face_nodes[1] = first_edge_correct[1] - - i = 2 - while i < len(face_nodes): - if face_nodes[i] != face_nodes[i - 1]: - # swap the order - old = face_nodes[i] - face_nodes[i] = face_nodes[i - 1] - face_nodes[i + 1] = old - i += 2 - - after_swapped = face_nodes - - after_swapped_remove = [after_swapped[0]] - - for i in range(1, len(after_swapped) - 1): - if after_swapped[i] != after_swapped[i - 1]: - after_swapped_remove.append(after_swapped[i]) - - face_nodes_dict[face_idx] = after_swapped_remove - - # Convert the dictionary to a list - res_face_nodes_connectivity = [] - for face_idx in range(len(face_edges_connectivity)): - res_face_nodes_connectivity.append(face_nodes_dict[face_idx]) - while len(res_face_nodes_connectivity[face_idx] - ) < original_face_nodes_connectivity.shape[1]: - res_face_nodes_connectivity[face_idx].append(ux.INT_FILL_VALUE) - - return np.array(res_face_nodes_connectivity) - - def test_build_n_nodes_per_face(self): - """Tests the construction of the ``n_nodes_per_face`` variable.""" - - # test on grid constructed from sample datasets - grids = [self.grid_mpas, self.grid_exodus, self.grid_ugrid] - - for grid in grids: - # highest possible dimension dimension for a face - max_dimension = grid.n_max_face_nodes - - # face must be at least a triangle - min_dimension = 3 - - assert grid.n_nodes_per_face.min() >= min_dimension - assert grid.n_nodes_per_face.max() <= max_dimension - - # test on grid constructed from vertices - verts = [ - self.f0_deg, self.f1_deg, self.f2_deg, self.f3_deg, self.f4_deg, - self.f5_deg, self.f6_deg - ] - grid_from_verts = ux.open_grid(verts) - - # number of non-fill-value nodes per face - expected_nodes_per_face = np.array([6, 3, 4, 6, 6, 4, 4], dtype=int) - nt.assert_equal(grid_from_verts.n_nodes_per_face.values, - expected_nodes_per_face) - - def test_edge_nodes_euler(self): - """Verifies that (``n_edge``) follows euler's formula.""" - grid_paths = [ - self.exodus_filepath, self.ugrid_filepath_01, - self.ugrid_filepath_02, self.ugrid_filepath_03 - ] - - for grid_path in grid_paths: - grid_ux = ux.open_grid(grid_path) - - n_face = grid_ux.n_face - n_node = grid_ux.n_node - n_edge = grid_ux.n_edge - - # euler's formula (n_face = n_edges - n_nodes + 2) - assert (n_face == n_edge - n_node + 2) - - def test_build_face_edges_connectivity_mpas(self): - """Tests the construction of (``Mesh2_edge_nodes``) on an MPAS grid - with known edge nodes.""" - - from uxarray.grid.connectivity import _build_edge_node_connectivity - - # grid with known edge node connectivity - mpas_grid_ux = ux.open_grid(self.mpas_filepath) - edge_nodes_expected = mpas_grid_ux._ds['edge_node_connectivity'].values - - # arrange edge nodes in the same manner as Grid._build_edge_node_connectivity - edge_nodes_expected.sort(axis=1) - edge_nodes_expected = np.unique(edge_nodes_expected, axis=0) - - # construct edge nodes - edge_nodes_output, _, _ = _build_edge_node_connectivity(mpas_grid_ux.face_node_connectivity.values, - mpas_grid_ux.n_face, - mpas_grid_ux.n_max_face_nodes) + expected_nodes_per_face = np.array([6, 3, 4, 6, 6, 4, 4], dtype=int) + nt.assert_equal(grid_from_verts.n_nodes_per_face.values, expected_nodes_per_face) - # _populate_face_edge_connectivity(mpas_grid_ux) - # edge_nodes_output = mpas_grid_ux._ds['edge_node_connectivity'].values +def test_connectivity_edge_nodes_euler(): + """Verifies that (``n_edge``) follows euler's formula.""" + grid_paths = [exodus_filepath, ugrid_filepath_01, ugrid_filepath_02, ugrid_filepath_03] - self.assertTrue(np.array_equal(edge_nodes_expected, edge_nodes_output)) + for grid_path in grid_paths: + grid_ux = ux.open_grid(grid_path) - # euler's formula (n_face = n_edges - n_nodes + 2) - n_face = mpas_grid_ux.n_node - n_node = mpas_grid_ux.n_face - n_edge = edge_nodes_output.shape[0] + n_face = grid_ux.n_face + n_node = grid_ux.n_node + n_edge = grid_ux.n_edge assert (n_face == n_edge - n_node + 2) - def test_build_face_edges_connectivity(self): - """Generates Grid.Mesh2_edge_nodes from Grid.face_node_connectivity.""" - ug_filename_list = [ - self.ugrid_filepath_01, self.ugrid_filepath_02, - self.ugrid_filepath_03 - ] - for ug_file_name in ug_filename_list: - tgrid = ux.open_grid(ug_file_name) - - face_node_connectivity = tgrid._ds["face_node_connectivity"] - - _populate_face_edge_connectivity(tgrid) - face_edge_connectivity = tgrid._ds.face_edge_connectivity - edge_node_connectivity = tgrid._ds.edge_node_connectivity - - # Assert if the mesh2_face_edges sizes are correct. - self.assertEqual(face_edge_connectivity.sizes["n_face"], - face_node_connectivity.sizes["n_face"]) - self.assertEqual(face_edge_connectivity.sizes["n_max_face_edges"], - face_node_connectivity.sizes["n_max_face_nodes"]) - - # Assert if the mesh2_edge_nodes sizes are correct. - # Euler formular for determining the edge numbers: n_face = n_edges - n_nodes + 2 - num_edges = face_edge_connectivity.sizes["n_face"] + tgrid._ds[ - "node_lon"].sizes["n_node"] - 2 - size = edge_node_connectivity.sizes["n_edge"] - self.assertEqual(edge_node_connectivity.sizes["n_edge"], num_edges) - - original_face_nodes_connectivity = tgrid._ds.face_node_connectivity.values - - reverted_mesh2_edge_nodes = self._revert_edges_conn_to_face_nodes_conn( - edge_nodes_connectivity=edge_node_connectivity.values, - face_edges_connectivity=face_edge_connectivity.values, - original_face_nodes_connectivity=original_face_nodes_connectivity - ) - - for i in range(len(reverted_mesh2_edge_nodes)): - self.assertTrue( - np.array_equal(reverted_mesh2_edge_nodes[i], - original_face_nodes_connectivity[i])) - - def test_build_face_edges_connectivity_fillvalues(self): - verts = [ - self.f0_deg, self.f1_deg, self.f2_deg, self.f3_deg, self.f4_deg, - self.f5_deg, self.f6_deg - ] - uds = ux.open_grid(verts) - _populate_face_edge_connectivity(uds) - n_face = len(uds._ds["face_edge_connectivity"].values) - n_node = uds.n_node - n_edge = len(uds._ds["edge_node_connectivity"].values) - - self.assertEqual(7, n_face) - self.assertEqual(21, n_node) - self.assertEqual(28, n_edge) - - # We will utilize the edge_nodes_connectivity and face_edges_connectivity to generate the - # res_face_nodes_connectivity and compare it with the uds._ds["face_node_connectivity"].values - edge_nodes_connectivity = uds._ds["edge_node_connectivity"].values - face_edges_connectivity = uds._ds["face_edge_connectivity"].values - face_nodes_connectivity = uds._ds["face_node_connectivity"].values - - res_face_nodes_connectivity = self._revert_edges_conn_to_face_nodes_conn( - edge_nodes_connectivity, face_edges_connectivity, - face_nodes_connectivity) - - # Compare the res_face_nodes_connectivity with the uds._ds["face_node_connectivity"].values - self.assertTrue( - np.array_equal(res_face_nodes_connectivity, - uds._ds["face_node_connectivity"].values)) - - def test_node_face_connectivity_from_verts(self): - """Test generating Grid.Mesh2_node_faces from array input.""" - - # We used the following codes to generate the testing face_nodes_connectivity in lonlat, - # The index of the nodes here is just for generation purpose and ensure the topology. - # This nodes list is only for vertices creation purposes and the nodes' order will not be used the - # same in the Grid object; i.e. the Grid object instantiation will instead use the below - # `face_nodes_conn_lonlat_degree` connectivity variable and determine the actual node orders by itself. - face_nodes_conn_lonlat_degree = [[162., 30], [216., 30], [70., 30], - [162., -30], [216., -30], [70., -30]] - - # This index variable will only be used to determine the face-node lon-lat values that are - # represented by `face_nodes_conn_lonlat` below, which is the actual data that is used - # by `Grid.__from_vert__()` during the creation of the grid topology. - face_nodes_conn_index = np.array([[3, 4, 5, ux.INT_FILL_VALUE], - [3, 0, 2, 5], [3, 4, 1, 0], - [0, 1, 2, ux.INT_FILL_VALUE]]) - face_nodes_conn_lonlat = np.full( - (face_nodes_conn_index.shape[0], face_nodes_conn_index.shape[1], 2), - ux.INT_FILL_VALUE) - - for i, face_nodes_conn_index_row in enumerate(face_nodes_conn_index): - for j, node_index in enumerate(face_nodes_conn_index_row): - if node_index != ux.INT_FILL_VALUE: - face_nodes_conn_lonlat[ - i, j] = face_nodes_conn_lonlat_degree[node_index] - - # Now we don't need the face_nodes_conn_index anymore. - del face_nodes_conn_index - - vgrid = ux.Grid.from_face_vertices( - face_nodes_conn_lonlat, - latlon=True, - ) +def test_connectivity_build_face_edges_connectivity_mpas(): + """Tests the construction of (``Mesh2_edge_nodes``) on an MPAS grid with known edge nodes.""" + from uxarray.grid.connectivity import _build_edge_node_connectivity - # We eyeballed the `Grid._face_nodes_connectivity` and wrote the following expected result - expected = np.array([ - np.array([0, 1, ux.INT_FILL_VALUE]), - np.array([1, 3, ux.INT_FILL_VALUE]), - np.array([0, 1, 2]), - np.array([1, 2, 3]), - np.array([0, 2, ux.INT_FILL_VALUE]), - np.array([2, 3, ux.INT_FILL_VALUE]) - ]) + mpas_grid_ux = ux.open_grid(mpas_filepath) + edge_nodes_expected = mpas_grid_ux._ds['edge_node_connectivity'].values - self.assertTrue( - np.array_equal(vgrid.node_face_connectivity.values, expected)) - - def test_node_face_connectivity_from_files(self): - """Test generating Grid.Mesh2_node_faces from file input.""" - grid_paths = [ - self.exodus_filepath, self.ugrid_filepath_01, - self.ugrid_filepath_02, self.ugrid_filepath_03 - ] - - for grid_path in grid_paths: - grid_xr = xr.open_dataset(grid_path) - grid_ux = ux.Grid.from_dataset(grid_xr) - - # use the dictionary method to build the node_face_connectivity - node_face_connectivity = {} - n_nodes_per_face = grid_ux.n_nodes_per_face.values - face_nodes = grid_ux._ds["face_node_connectivity"].values - for face_idx, max_nodes in enumerate(n_nodes_per_face): - cur_face_nodes = face_nodes[face_idx, 0:max_nodes] - for j in cur_face_nodes: - if j not in node_face_connectivity: - node_face_connectivity[j] = [] - node_face_connectivity[j].append(face_idx) - - # compare the two methods - for i in range(grid_ux.n_node): - face_index_from_sparse_matrix = grid_ux.node_face_connectivity.values[ - i] - valid_face_index_from_sparse_matrix = face_index_from_sparse_matrix[ - face_index_from_sparse_matrix != - grid_ux.node_face_connectivity.attrs["_FillValue"]] - valid_face_index_from_sparse_matrix.sort() - face_index_from_dict = node_face_connectivity[i] - face_index_from_dict.sort() - self.assertTrue( - np.array_equal(valid_face_index_from_sparse_matrix, - face_index_from_dict)) - - def test_edge_face_connectivity_mpas(self): - """Tests the construction of ``Mesh2_face_edges`` to the expected - results of an MPAS grid.""" - uxgrid = ux.open_grid(self.mpas_filepath) - - edge_faces_gold = uxgrid.edge_face_connectivity.values - - edge_faces_output = _build_edge_face_connectivity( - uxgrid.face_edge_connectivity.values, - uxgrid.n_nodes_per_face.values, uxgrid.n_edge) - - nt.assert_array_equal(edge_faces_output, edge_faces_gold) - - def test_edge_face_connectivity_sample(self): - """Tests the construction of ``Mesh2_face_edges`` on an example with - one shared edge, and the remaining edges only being part of one - face.""" - # single triangle with point on antimeridian - verts = [[(0.0, -90.0), (180, 0.0), (0.0, 90)], - [(-180, 0.0), (0, 90.0), (0.0, -90)]] - - uxgrid = ux.open_grid(verts) - - n_shared = 0 - n_solo = 0 - n_invalid = 0 - for edge_face in uxgrid.edge_face_connectivity.values: - if edge_face[0] != INT_FILL_VALUE and edge_face[1] != INT_FILL_VALUE: - # shared edge - n_shared += 1 - elif edge_face[0] != INT_FILL_VALUE and edge_face[ - 1] == INT_FILL_VALUE: - # edge borders one face - n_solo += 1 - else: - # invalid edge, if any - n_invalid += 1 - - # example has only 1 shared edge - assert n_shared == 1 - - # remaining edges only saddle one face - assert n_solo == uxgrid.n_edge - n_shared - - # no invalid entries should occur - assert n_invalid == 0 - - def test_face_face_connectivity_construction(self): - """Tests the construction of face-face connectivity.""" - - # Open MPAS grid and read in face_face_connectivity - grid = ux.open_grid(gridfile_mpas) - face_face_conn_old = grid.face_face_connectivity.values - - # Construct new face_face_connectivity using UXarray - face_face_conn_new = _build_face_face_connectivity(grid) - - # Sort the arrays before comparison - face_face_conn_old_sorted = np.sort(face_face_conn_old, axis=None) - face_face_conn_new_sorted = np.sort(face_face_conn_new, axis=None) - - # Assert the new and old face_face_connectivity contains the same faces - nt.assert_array_equal(face_face_conn_new_sorted, face_face_conn_old_sorted) - - -class TestClassMethods(TestCase): - gridfile_ugrid = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc" - gridfile_mpas = current_path / "meshfiles" / "mpas" / "QU" / "mesh.QU.1920km.151026.nc" - gridfile_exodus = current_path / "meshfiles" / "exodus" / "outCSne8" / "outCSne8.g" - gridfile_scrip = current_path / "meshfiles" / "scrip" / "outCSne8" / "outCSne8.nc" - - def test_from_dataset(self): - # UGRID - xrds = xr.open_dataset(self.gridfile_ugrid) - uxgrid = ux.Grid.from_dataset(xrds) + edge_nodes_expected.sort(axis=1) + edge_nodes_expected = np.unique(edge_nodes_expected, axis=0) - # MPAS - xrds = xr.open_dataset(self.gridfile_mpas) - uxgrid = ux.Grid.from_dataset(xrds, use_dual=False) - uxgrid = ux.Grid.from_dataset(xrds, use_dual=True) + edge_nodes_output, _, _ = _build_edge_node_connectivity(mpas_grid_ux.face_node_connectivity.values, + mpas_grid_ux.n_face, + mpas_grid_ux.n_max_face_nodes) - # Exodus - xrds = xr.open_dataset(self.gridfile_exodus) - uxgrid = ux.Grid.from_dataset(xrds) + assert np.array_equal(edge_nodes_expected, edge_nodes_output) - # SCRIP - xrds = xr.open_dataset(self.gridfile_scrip) - uxgrid = ux.Grid.from_dataset(xrds) + n_face = mpas_grid_ux.n_node + n_node = mpas_grid_ux.n_face + n_edge = edge_nodes_output.shape[0] + assert (n_face == n_edge - n_node + 2) - def test_from_face_vertices(self): - single_face_latlon = [(0.0, 90.0), (-180, 0.0), (0.0, -90)] - uxgrid = ux.Grid.from_face_vertices(single_face_latlon, latlon=True) +def test_connectivity_build_face_edges_connectivity(): + """Generates Grid.Mesh2_edge_nodes from Grid.face_node_connectivity.""" + ug_filename_list = [ugrid_filepath_01, ugrid_filepath_02, ugrid_filepath_03] + for ug_file_name in ug_filename_list: + tgrid = ux.open_grid(ug_file_name) - multi_face_latlon = [[(0.0, 90.0), (-180, 0.0), (0.0, -90)], - [(0.0, 90.0), (180, 0.0), (0.0, -90)]] - uxgrid = ux.Grid.from_face_vertices(multi_face_latlon, latlon=True) + face_node_connectivity = tgrid._ds["face_node_connectivity"] - single_face_cart = [(0.0,)] + _populate_face_edge_connectivity(tgrid) + face_edge_connectivity = tgrid._ds.face_edge_connectivity + edge_node_connectivity = tgrid._ds.edge_node_connectivity + assert face_edge_connectivity.sizes["n_face"] == face_node_connectivity.sizes["n_face"] + assert face_edge_connectivity.sizes["n_max_face_edges"] == face_node_connectivity.sizes["n_max_face_nodes"] -class TestLatlonBounds(TestCase): - gridfile_mpas = current_path / "meshfiles" / "mpas" / "QU" / "oQU480.231010.nc" - - def test_populate_bounds_GCA_mix(self): - face_1 = [[10.0, 60.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] - face_2 = [[350, 60.0], [350, 10.0], [50.0, 10.0], [50.0, 60.0]] - face_3 = [[210.0, 80.0], [350.0, 60.0], [10.0, 60.0], [30.0, 80.0]] - face_4 = [[200.0, 80.0], [350.0, 60.0], [10.0, 60.0], [40.0, 80.0]] + num_edges = face_edge_connectivity.sizes["n_face"] + tgrid._ds["node_lon"].sizes["n_node"] - 2 + size = edge_node_connectivity.sizes["n_edge"] + assert edge_node_connectivity.sizes["n_edge"] == num_edges - faces = [face_1, face_2, face_3, face_4] + original_face_nodes_connectivity = tgrid._ds.face_node_connectivity.values - # Hand calculated bounds for the above faces in radians - expected_bounds = [[[0.17453293, 1.07370494], [0.17453293, 0.87266463]], - [[0.17453293, 1.10714872], [6.10865238, 0.87266463]], - [[1.04719755, 1.57079633], [3.66519143, 0.52359878]], - [[1.04719755, 1.57079633], [0., 6.28318531]]] - - grid = ux.Grid.from_face_vertices(faces, latlon=True) - bounds_xarray = grid.bounds - face_bounds = bounds_xarray.values - nt.assert_allclose(grid.bounds.values, expected_bounds, atol=ERROR_TOLERANCE) + reverted_mesh2_edge_nodes = _revert_edges_conn_to_face_nodes_conn( + edge_nodes_connectivity=edge_node_connectivity.values, + face_edges_connectivity=face_edge_connectivity.values, + original_face_nodes_connectivity=original_face_nodes_connectivity + ) - def test_populate_bounds_MPAS(self): - uxgrid = ux.open_grid(self.gridfile_mpas) - bounds_xarray = uxgrid.bounds + for i in range(len(reverted_mesh2_edge_nodes)): + assert np.array_equal(reverted_mesh2_edge_nodes[i], original_face_nodes_connectivity[i]) + +def test_connectivity_build_face_edges_connectivity_fillvalues(): + verts = [f0_deg, f1_deg, f2_deg, f3_deg, f4_deg, f5_deg, f6_deg] + uds = ux.open_grid(verts) + _populate_face_edge_connectivity(uds) + n_face = len(uds._ds["face_edge_connectivity"].values) + n_node = uds.n_node + n_edge = len(uds._ds["edge_node_connectivity"].values) + + assert n_face == 7 + assert n_node == 21 + assert n_edge == 28 + + edge_nodes_connectivity = uds._ds["edge_node_connectivity"].values + face_edges_connectivity = uds._ds["face_edge_connectivity"].values + face_nodes_connectivity = uds._ds["face_node_connectivity"].values + + res_face_nodes_connectivity = _revert_edges_conn_to_face_nodes_conn( + edge_nodes_connectivity, face_edges_connectivity, face_nodes_connectivity) + + assert np.array_equal(res_face_nodes_connectivity, uds._ds["face_node_connectivity"].values) + +def test_connectivity_node_face_connectivity_from_verts(): + """Test generating Grid.Mesh2_node_faces from array input.""" + face_nodes_conn_lonlat_degree = [[162., 30], [216., 30], [70., 30], + [162., -30], [216., -30], [70., -30]] + + face_nodes_conn_index = np.array([[3, 4, 5, ux.INT_FILL_VALUE], + [3, 0, 2, 5], [3, 4, 1, 0], + [0, 1, 2, ux.INT_FILL_VALUE]]) + face_nodes_conn_lonlat = np.full( + (face_nodes_conn_index.shape[0], face_nodes_conn_index.shape[1], 2), + ux.INT_FILL_VALUE) + + for i, face_nodes_conn_index_row in enumerate(face_nodes_conn_index): + for j, node_index in enumerate(face_nodes_conn_index_row): + if node_index != ux.INT_FILL_VALUE: + face_nodes_conn_lonlat[i, j] = face_nodes_conn_lonlat_degree[node_index] + + vgrid = ux.Grid.from_face_vertices(face_nodes_conn_lonlat, latlon=True) + + expected = np.array([ + np.array([0, 1, ux.INT_FILL_VALUE]), + np.array([1, 3, ux.INT_FILL_VALUE]), + np.array([0, 1, 2]), + np.array([1, 2, 3]), + np.array([0, 2, ux.INT_FILL_VALUE]), + np.array([2, 3, ux.INT_FILL_VALUE]) + ]) + + assert np.array_equal(vgrid.node_face_connectivity.values, expected) + +def test_connectivity_node_face_connectivity_from_files(): + """Test generating Grid.Mesh2_node_faces from file input.""" + grid_paths = [exodus_filepath, ugrid_filepath_01, ugrid_filepath_02, ugrid_filepath_03] + + for grid_path in grid_paths: + grid_xr = xr.open_dataset(grid_path) + grid_ux = ux.Grid.from_dataset(grid_xr) + + node_face_connectivity = {} + n_nodes_per_face = grid_ux.n_nodes_per_face.values + face_nodes = grid_ux._ds["face_node_connectivity"].values + for face_idx, max_nodes in enumerate(n_nodes_per_face): + cur_face_nodes = face_nodes[face_idx, 0:max_nodes] + for j in cur_face_nodes: + if j not in node_face_connectivity: + node_face_connectivity[j] = [] + node_face_connectivity[j].append(face_idx) + + for i in range(grid_ux.n_node): + face_index_from_sparse_matrix = grid_ux.node_face_connectivity.values[i] + valid_face_index_from_sparse_matrix = face_index_from_sparse_matrix[face_index_from_sparse_matrix != grid_ux.node_face_connectivity.attrs["_FillValue"]] + valid_face_index_from_sparse_matrix.sort() + face_index_from_dict = node_face_connectivity[i] + face_index_from_dict.sort() + assert np.array_equal(valid_face_index_from_sparse_matrix, face_index_from_dict) + +def test_connectivity_edge_face_connectivity_mpas(): + """Tests the construction of ``Mesh2_face_edges`` to the expected results of an MPAS grid.""" + uxgrid = ux.open_grid(mpas_filepath) + + edge_faces_gold = uxgrid.edge_face_connectivity.values + + edge_faces_output = _build_edge_face_connectivity( + uxgrid.face_edge_connectivity.values, + uxgrid.n_nodes_per_face.values, uxgrid.n_edge) + + nt.assert_array_equal(edge_faces_output, edge_faces_gold) + +def test_connectivity_edge_face_connectivity_sample(): + """Tests the construction of ``Mesh2_face_edges`` on an example with one shared edge, and the remaining edges only being part of one face.""" + verts = [[(0.0, -90.0), (180, 0.0), (0.0, 90)], + [(-180, 0.0), (0, 90.0), (0.0, -90)]] + + uxgrid = ux.open_grid(verts) + + n_shared = 0 + n_solo = 0 + n_invalid = 0 + for edge_face in uxgrid.edge_face_connectivity.values: + if edge_face[0] != INT_FILL_VALUE and edge_face[1] != INT_FILL_VALUE: + n_shared += 1 + elif edge_face[0] != INT_FILL_VALUE and edge_face[1] == INT_FILL_VALUE: + n_solo += 1 + else: + n_invalid += 1 + + assert n_shared == 1 + assert n_solo == uxgrid.n_edge - n_shared + assert n_invalid == 0 + +def test_connectivity_face_face_connectivity_construction(): + """Tests the construction of face-face connectivity.""" + grid = ux.open_grid(mpas_filepath) + face_face_conn_old = grid.face_face_connectivity.values + + face_face_conn_new = _build_face_face_connectivity(grid) + + face_face_conn_old_sorted = np.sort(face_face_conn_old, axis=None) + face_face_conn_new_sorted = np.sort(face_face_conn_new, axis=None) + + nt.assert_array_equal(face_face_conn_new_sorted, face_face_conn_old_sorted) + +def test_class_methods_from_dataset(): + # UGRID + xrds = xr.open_dataset(gridfile_ugrid) + uxgrid = ux.Grid.from_dataset(xrds) + + # MPAS + xrds = xr.open_dataset(gridfile_mpas) + uxgrid = ux.Grid.from_dataset(xrds, use_dual=False) + uxgrid = ux.Grid.from_dataset(xrds, use_dual=True) + + # Exodus + xrds = xr.open_dataset(gridfile_exodus) + uxgrid = ux.Grid.from_dataset(xrds) + + # SCRIP + xrds = xr.open_dataset(gridfile_scrip) + uxgrid = ux.Grid.from_dataset(xrds) + +def test_class_methods_from_face_vertices(): + single_face_latlon = [(0.0, 90.0), (-180, 0.0), (0.0, -90)] + uxgrid = ux.Grid.from_face_vertices(single_face_latlon, latlon=True) + + multi_face_latlon = [[(0.0, 90.0), (-180, 0.0), (0.0, -90)], + [(0.0, 90.0), (180, 0.0), (0.0, -90)]] + uxgrid = ux.Grid.from_face_vertices(multi_face_latlon, latlon=True) + + single_face_cart = [(0.0,)] + +def test_latlon_bounds_populate_bounds_GCA_mix(): + gridfile_mpas = current_path / "meshfiles" / "mpas" / "QU" / "oQU480.231010.nc" + face_1 = [[10.0, 60.0], [10.0, 10.0], [50.0, 10.0], [50.0, 60.0]] + face_2 = [[350, 60.0], [350, 10.0], [50.0, 10.0], [50.0, 60.0]] + face_3 = [[210.0, 80.0], [350.0, 60.0], [10.0, 60.0], [30.0, 80.0]] + face_4 = [[200.0, 80.0], [350.0, 60.0], [10.0, 60.0], [40.0, 80.0]] + faces = [face_1, face_2, face_3, face_4] -class TestDualMesh(TestCase): - """Test Dual Mesh Construction.""" + expected_bounds = [[[0.17453293, 1.07370494], [0.17453293, 0.87266463]], + [[0.17453293, 1.10714872], [6.10865238, 0.87266463]], + [[1.04719755, 1.57079633], [3.66519143, 0.52359878]], + [[1.04719755, 1.57079633], [0., 6.28318531]]] - def test_dual_mesh_mpas(self): - # Open a grid with and without dual - grid = ux.open_grid(gridfile_mpas, use_dual=False) - mpas_dual = ux.open_grid(gridfile_mpas, use_dual=True) + grid = ux.Grid.from_face_vertices(faces, latlon=True) + bounds_xarray = grid.bounds + nt.assert_allclose(bounds_xarray.values, expected_bounds, atol=ERROR_TOLERANCE) - # Construct Dual - dual = grid.get_dual() +def test_latlon_bounds_populate_bounds_MPAS(): + gridfile_mpas = current_path / "meshfiles" / "mpas" / "QU" / "oQU480.231010.nc" + uxgrid = ux.open_grid(gridfile_mpas) + bounds_xarray = uxgrid.bounds - # Assert the dimensions are the same - assert dual.n_face == mpas_dual.n_face - assert dual.n_node == mpas_dual.n_node - assert dual.n_max_face_nodes == mpas_dual.n_max_face_nodes +def test_dual_mesh_mpas(): + grid = ux.open_grid(gridfile_mpas, use_dual=False) + mpas_dual = ux.open_grid(gridfile_mpas, use_dual=True) - # Assert the faces are the same - nt.assert_equal(dual.face_node_connectivity.values, mpas_dual.face_node_connectivity.values) + dual = grid.get_dual() - def test_dual_duplicate(self): - # Test that dual mesh throws an exception if duplicate nodes exist - dataset = ux.open_dataset(gridfile_geoflow, gridfile_geoflow) + assert dual.n_face == mpas_dual.n_face + assert dual.n_node == mpas_dual.n_node + assert dual.n_max_face_nodes == mpas_dual.n_max_face_nodes - nt.assert_raises(RuntimeError, dataset.get_dual) + nt.assert_equal(dual.face_node_connectivity.values, mpas_dual.face_node_connectivity.values) +def test_dual_duplicate(): + dataset = ux.open_dataset(gridfile_geoflow, gridfile_geoflow) + with pytest.raises(RuntimeError): + dataset.get_dual() -class TestNormalizeExistingCoordinates(TestCase): +def test_normalize_existing_coordinates_non_norm_initial(): gridfile_mpas = current_path / "meshfiles" / "mpas" / "QU" / "mesh.QU.1920km.151026.nc" - gridfile_CSne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" - - def test_non_norm_initial(self): - """Check the normalization of coordinates that were initially parsed as - non-normalized.""" - from uxarray.grid.validation import _check_normalization - uxgrid = ux.open_grid(self.gridfile_mpas) + from uxarray.grid.validation import _check_normalization + uxgrid = ux.open_grid(gridfile_mpas) - # Make the coordinates not normalized - uxgrid.node_x.data = 5 * uxgrid.node_x.data - uxgrid.node_y.data = 5 * uxgrid.node_y.data - uxgrid.node_z.data = 5 * uxgrid.node_z.data - assert not _check_normalization(uxgrid) + uxgrid.node_x.data = 5 * uxgrid.node_x.data + uxgrid.node_y.data = 5 * uxgrid.node_y.data + uxgrid.node_z.data = 5 * uxgrid.node_z.data + assert not _check_normalization(uxgrid) - uxgrid.normalize_cartesian_coordinates() + uxgrid.normalize_cartesian_coordinates() + assert _check_normalization(uxgrid) - assert _check_normalization(uxgrid) - - def test_norm_initial(self): - """Coordinates should be normalized for grids that we construct - them.""" - from uxarray.grid.validation import _check_normalization - uxgrid = ux.open_grid(self.gridfile_CSne30) +def test_normalize_existing_coordinates_norm_initial(): + gridfile_CSne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" + from uxarray.grid.validation import _check_normalization + uxgrid = ux.open_grid(gridfile_CSne30) - assert _check_normalization(uxgrid) + assert _check_normalization(uxgrid) diff --git a/test/test_helpers.py b/test/test_helpers.py index 491351e14..3713228bf 100644 --- a/test/test_helpers.py +++ b/test/test_helpers.py @@ -3,15 +3,12 @@ import numpy.testing as nt import random import xarray as xr - -from unittest import TestCase from pathlib import Path +import pytest import uxarray as ux - from uxarray.grid.connectivity import _replace_fill_values from uxarray.constants import INT_DTYPE, INT_FILL_VALUE - from uxarray.grid.coordinates import _lonlat_rad_to_xyz, _normalize_xyz, _xyz_to_lonlat_rad from uxarray.grid.arcs import point_within_gca, _angle_of_2_vectors, in_between from uxarray.grid.utils import _get_cartesian_face_edge_nodes, _get_lonlat_rad_face_edge_nodes @@ -32,537 +29,406 @@ err_tolerance = 1.0e-12 +def test_face_area_coords(): + """Test function for helper function get_all_face_area_from_coords.""" + # Note: currently only testing one face, but this can be used to get area of multiple faces + x = np.array([0.57735027, 0.57735027, -0.57735027]) + y = np.array([-5.77350269e-01, 5.77350269e-01, 5.77350269e-01]) + z = np.array([-0.57735027, -0.57735027, -0.57735027]) -class TestIntegrate(TestCase): - - def test_face_area_coords(self): - """Test function for helper function get_all_face_area_from_coords.""" - # Note: currently only testing one face, but this can be used to get area of multiple faces - x = np.array([0.57735027, 0.57735027, -0.57735027]) - y = np.array([-5.77350269e-01, 5.77350269e-01, 5.77350269e-01]) - z = np.array([-0.57735027, -0.57735027, -0.57735027]) - - face_nodes = np.array([[0, 1, 2]]).astype(INT_DTYPE) - face_dimension = np.array([3], dtype=INT_DTYPE) - - area, jacobian = ux.grid.area.get_all_face_area_from_coords( - x, y, z, face_nodes, face_dimension, 3, coords_type="cartesian") - - nt.assert_almost_equal(area, constants.TRI_AREA, decimal=1) - - def test_calculate_face_area(self): - """Test function for helper function calculate_face_area - only one face.""" - # Note: currently only testing one face, but this can be used to get area of multiple faces - # Also note, this does not need face_nodes, assumes nodes are in counterclockwise orientation - x = np.array([0.57735027, 0.57735027, -0.57735027]) - y = np.array([-5.77350269e-01, 5.77350269e-01, 5.77350269e-01]) - z = np.array([-0.57735027, -0.57735027, -0.57735027]) - - area, jacobian = ux.grid.area.calculate_face_area( - x, y, z, "gaussian", 5, "cartesian") - - nt.assert_almost_equal(area, constants.TRI_AREA, decimal=3) - - def test_quadrature(self): - order = 1 - dG, dW = ux.grid.area.get_tri_quadratureDG(order) - G = np.array([[0.33333333, 0.33333333, 0.33333333]]) - W = np.array([1.0]) - - np.testing.assert_array_almost_equal(G, dG) - np.testing.assert_array_almost_equal(W, dW) - - dG, dW = ux.grid.area.get_gauss_quadratureDG(order) - - G = np.array([[0.5]]) - W = np.array([1.0]) - - np.testing.assert_array_almost_equal(G, dG) - np.testing.assert_array_almost_equal(W, dW) - - -class TestGridCenter(TestCase): - - def test_grid_center(self): - """Calculates if the calculated center point of a grid box is the same - as a given value for the same dataset.""" - ds_scrip_CSne8 = xr.open_dataset(gridfile_scrip_CSne8) - - # select actual center_lat/lon - scrip_center_lon = ds_scrip_CSne8['grid_center_lon'] - scrip_center_lat = ds_scrip_CSne8['grid_center_lat'] - - # Calculate the center_lat/lon using same dataset's corner_lat/lon - calc_center = ux.io._scrip.grid_center_lat_lon(ds_scrip_CSne8) - calc_lat = calc_center[0] - calc_lon = calc_center[1] - - # Test that calculated center_lat/lon is the same as actual center_lat/lon - np.testing.assert_array_almost_equal(scrip_center_lat, calc_lat) - np.testing.assert_array_almost_equal(scrip_center_lon, calc_lon) - - -class TestCoordinatesConversion(TestCase): - - def test_normalize_in_place(self): - x, y, z = _normalize_xyz( - random.random(), random.random(), - random.random()) - - self.assertLessEqual(np.absolute(np.sqrt(x * x + y * y + z * z) - 1), - err_tolerance) - - def test_node_xyz_to_lonlat_rad(self): - x, y, z = _normalize_xyz(*[ - random.uniform(-1, 1), - random.uniform(-1, 1), - random.uniform(-1, 1) - ]) - - lon, lat = _xyz_to_lonlat_rad(x, y, z) - new_x, new_y, new_z =_lonlat_rad_to_xyz(lon, lat) - - self.assertLessEqual(np.absolute(new_x - x), err_tolerance) - self.assertLessEqual(np.absolute(new_y - y), err_tolerance) - self.assertLessEqual(np.absolute(new_z - z), err_tolerance) - - def test_node_latlon_rad_to_xyz(self): - [lon, lat] = [ - random.uniform(0, 2 * np.pi), - random.uniform(-0.5 * np.pi, 0.5 * np.pi) - ] - - x, y, z = _lonlat_rad_to_xyz(lon, lat) - - new_lon, new_lat = _xyz_to_lonlat_rad(x, y, z) - - self.assertLessEqual(np.absolute(new_lon - lon), err_tolerance) - self.assertLessEqual(np.absolute(new_lat - lat), err_tolerance) - - -class TestConstants(TestCase): - # DTYPE as set in constants.py - expected_int_dtype = INT_DTYPE - - # INT_FILL_VALUE as set in constants.py - fv = INT_FILL_VALUE - - def test_invalid_indexing(self): - """Tests if the current INT_DTYPE and INT_FILL_VALUE throw the correct - errors when indexing.""" - dummy_data = np.array([1, 2, 3, 4]) - - invalid_indices = np.array([self.fv, self.fv], dtype=INT_DTYPE) - invalid_index = self.fv - - # invalid index/indices should throw an Index Error - with self.assertRaises(IndexError): - dummy_data[invalid_indices] - dummy_data[invalid_index] - - def test_replace_fill_values(self): - """Tests _replace_fill_values() helper function across multiple - different dtype arrays used as face_nodes.""" - - # expected output from _replace_fill_values() - face_nodes_gold = np.array( - [[1, 2, self.fv], [self.fv, self.fv, self.fv]], dtype=INT_DTYPE) - - # test different datatypes for face_nodes - dtypes = [np.int32, np.int64, np.float32, np.float64] - for dtype in dtypes: - # test face nodes with set dtype - face_nodes = np.array([[1, 2, -1], [-1, -1, -1]], dtype=dtype) - - # output of _replace_fill_values() - face_nodes_test = _replace_fill_values(grid_var=face_nodes, - original_fill=-1, - new_fill=INT_FILL_VALUE, - new_dtype=INT_DTYPE) - - assert np.array_equal(face_nodes_test, face_nodes_gold) - - def test_replace_fill_values_invalid(self): - """Tests _replace_fill_values() helper function attempting to use a - fill value that is not representable by the current dtype.""" - - face_nodes = np.array([[1, 2, -1], [-1, -1, -1]], dtype=np.int32) - # invalid fill value with dtype should raise a valueError - with self.assertRaises(ValueError): - # INT_FILL_VALUE (max(uint32) not representable by int16) - face_nodes_test = _replace_fill_values(grid_var=face_nodes, - original_fill=-1, - new_fill=INT_FILL_VALUE, - new_dtype=np.int16) - + face_nodes = np.array([[0, 1, 2]]).astype(INT_DTYPE) + face_dimension = np.array([3], dtype=INT_DTYPE) -class TestSparseMatrix(TestCase): + area, jacobian = ux.grid.area.get_all_face_area_from_coords( + x, y, z, face_nodes, face_dimension, 3, coords_type="cartesian") - def test_convert_face_node_conn_to_sparse_matrix(self): - """Tests _face_nodes_to_sparse_matrix() helper function to see if can - generate sparse matrix from face_nodes_conn that has Fill Values.""" - face_nodes_conn = np.array([[3, 4, 5, INT_FILL_VALUE], [3, 0, 2, 5], - [3, 4, 1, 0], [0, 1, 2, INT_FILL_VALUE]]) + nt.assert_almost_equal(area, constants.TRI_AREA, decimal=1) - face_indices, nodes_indices, non_zero_flag = ux.grid.connectivity._face_nodes_to_sparse_matrix( - face_nodes_conn) - expected_non_zero_flag = np.array( - [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) - expected_face_indices = np.array( - [0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3]) - expected_nodes_indices = np.array( - [3, 4, 5, 3, 0, 2, 5, 3, 4, 1, 0, 0, 1, 2]) +def test_calculate_face_area(): + """Test function for helper function calculate_face_area - only one face.""" + # Note: currently only testing one face, but this can be used to get area of multiple faces + # Also note, this does not need face_nodes, assumes nodes are in counterclockwise orientation + x = np.array([0.57735027, 0.57735027, -0.57735027]) + y = np.array([-5.77350269e-01, 5.77350269e-01, 5.77350269e-01]) + z = np.array([-0.57735027, -0.57735027, -0.57735027]) + + area, jacobian = ux.grid.area.calculate_face_area( + x, y, z, "gaussian", 5, "cartesian") - nt.assert_array_equal(non_zero_flag, expected_non_zero_flag) - nt.assert_array_equal(face_indices, expected_face_indices) - nt.assert_array_equal(nodes_indices, expected_nodes_indices) - - -class TestOperators(TestCase): - - def test_in_between(self): - # Test the in_between operator - self.assertTrue(in_between(0, 1, 2)) - self.assertTrue(in_between(-1, -1.5, -2)) - - -class TestVectorsAngel(TestCase): - - def test_angle_of_2_vectors(self): - # Test the angle between two vectors - v1 = np.array([1.0, 0.0, 0.0]) - v2 = np.array([0.0, 1.0, 0.0]) - self.assertAlmostEqual(_angle_of_2_vectors(v1, v2), np.pi / 2.0) - - v1 = np.array([1.0, 0.0, 0.0]) - v2 = np.array([1.0, 0.0, 0.0]) - self.assertAlmostEqual(_angle_of_2_vectors(v1, v2), 0.0) - - def test_angle_of_2_vectors_180_degree(self): - GCR1_cart = np.array([ - _lonlat_rad_to_xyz(np.deg2rad(0.0), - np.deg2rad(0.0)), - _lonlat_rad_to_xyz(np.deg2rad(181.0), - np.deg2rad(0.0)) - ]) - - res = _angle_of_2_vectors( GCR1_cart[0], GCR1_cart[1]) - - # The angle between the two vectors should be 181 degree - self.assertAlmostEqual(res, np.deg2rad(181.0), places=8) - - GCR1_cart = np.array([ - _lonlat_rad_to_xyz(np.deg2rad(170.0), - np.deg2rad(89.0)), - _lonlat_rad_to_xyz(np.deg2rad(170.0), - np.deg2rad(-10.0)) - ]) - - res = _angle_of_2_vectors( GCR1_cart[0], GCR1_cart[1]) - - # The angle between the two vectors should be 181 degree - self.assertAlmostEqual(res, np.deg2rad(89.0+10.0), places=8) - - -class TestFaceEdgeConnectivityHelper(TestCase): - - def test_get_cartesian_face_edge_nodes_pipeline(self): - # Create the vertices for the grid, based around the North Pole - vertices = [[0.5, 0.5, 0.5], [-0.5, 0.5, 0.5], [-0.5, -0.5, 0.5], [0.5, -0.5, 0.5]] - - # Normalize the vertices - vertices = [x / np.linalg.norm(x) for x in vertices] - - # Construct the grid from the vertices - grid = ux.Grid.from_face_vertices(vertices, latlon=False) - - # Extract the necessary grid data - face_node_conn = grid.face_node_connectivity.values - n_nodes_per_face = np.array([len(face) for face in face_node_conn]) - n_face = len(face_node_conn) - - n_max_face_edges = max(n_nodes_per_face) - node_x = grid.node_x.values - node_y = grid.node_y.values - node_z = grid.node_z.values - - # Call the function to test - face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes( - face_node_conn, n_face, n_max_face_edges, node_x, node_y, node_z + nt.assert_almost_equal(area, constants.TRI_AREA, decimal=3) + +def test_quadrature(): + order = 1 + dG, dW = ux.grid.area.get_tri_quadratureDG(order) + G = np.array([[0.33333333, 0.33333333, 0.33333333]]) + W = np.array([1.0]) + + np.testing.assert_array_almost_equal(G, dG) + np.testing.assert_array_almost_equal(W, dW) + + dG, dW = ux.grid.area.get_gauss_quadratureDG(order) + + G = np.array([[0.5]]) + W = np.array([1.0]) + + np.testing.assert_array_almost_equal(G, dG) + np.testing.assert_array_almost_equal(W, dW) + +def test_grid_center(): + """Calculates if the calculated center point of a grid box is the same + as a given value for the same dataset.""" + ds_scrip_CSne8 = xr.open_dataset(gridfile_scrip_CSne8) + + # select actual center_lat/lon + scrip_center_lon = ds_scrip_CSne8['grid_center_lon'] + scrip_center_lat = ds_scrip_CSne8['grid_center_lat'] + + # Calculate the center_lat/lon using same dataset's corner_lat/lon + calc_center = ux.io._scrip.grid_center_lat_lon(ds_scrip_CSne8) + calc_lat = calc_center[0] + calc_lon = calc_center[1] + + # Test that calculated center_lat/lon is the same as actual center_lat/lon + np.testing.assert_array_almost_equal(scrip_center_lat, calc_lat) + np.testing.assert_array_almost_equal(scrip_center_lon, calc_lon) + +def test_normalize_in_place(): + x, y, z = _normalize_xyz( + random.random(), random.random(), + random.random()) + + assert np.absolute(np.sqrt(x * x + y * y + z * z) - 1) <= err_tolerance + +def test_node_xyz_to_lonlat_rad(): + x, y, z = _normalize_xyz(*[ + random.uniform(-1, 1), + random.uniform(-1, 1), + random.uniform(-1, 1) + ]) + + lon, lat = _xyz_to_lonlat_rad(x, y, z) + new_x, new_y, new_z = _lonlat_rad_to_xyz(lon, lat) + + assert np.absolute(new_x - x) <= err_tolerance + assert np.absolute(new_y - y) <= err_tolerance + assert np.absolute(new_z - z) <= err_tolerance + +def test_node_latlon_rad_to_xyz(): + lon, lat = [ + random.uniform(0, 2 * np.pi), + random.uniform(-0.5 * np.pi, 0.5 * np.pi) + ] + + x, y, z = _lonlat_rad_to_xyz(lon, lat) + new_lon, new_lat = _xyz_to_lonlat_rad(x, y, z) + + assert np.absolute(new_lon - lon) <= err_tolerance + assert np.absolute(new_lat - lat) <= err_tolerance + +def test_invalid_indexing(): + """Tests if the current INT_DTYPE and INT_FILL_VALUE throw the correct + errors when indexing.""" + dummy_data = np.array([1, 2, 3, 4]) + + invalid_indices = np.array([INT_FILL_VALUE, INT_FILL_VALUE], dtype=INT_DTYPE) + invalid_index = INT_FILL_VALUE + + # invalid index/indices should throw an Index Error + with pytest.raises(IndexError): + dummy_data[invalid_indices] + dummy_data[invalid_index] + +def test_replace_fill_values(): + """Tests _replace_fill_values() helper function across multiple + different dtype arrays used as face_nodes.""" + + # expected output from _replace_fill_values() + face_nodes_gold = np.array( + [[1, 2, INT_FILL_VALUE], [INT_FILL_VALUE, INT_FILL_VALUE, INT_FILL_VALUE]], + dtype=INT_DTYPE) + + # test different datatypes for face_nodes + dtypes = [np.int32, np.int64, np.float32, np.float64] + for dtype in dtypes: + # test face nodes with set dtype + face_nodes = np.array([[1, 2, -1], [-1, -1, -1]], dtype=dtype) + + # output of _replace_fill_values() + face_nodes_test = _replace_fill_values( + grid_var=face_nodes, + original_fill=-1, + new_fill=INT_FILL_VALUE, + new_dtype=INT_DTYPE ) - # Check that the face_edges_connectivity_cartesian works as an input to _pole_point_inside_polygon - result = _pole_point_inside_polygon_cartesian( - 'North', face_edges_connectivity_cartesian[0] - ) - - # Assert that the result is True - self.assertTrue(result) - - def test_get_cartesian_face_edge_nodes_filled_value(self): - # Create the vertices for the grid, based around the North Pole - vertices = [[0.5, 0.5, 0.5], [-0.5, 0.5, 0.5], [-0.5, -0.5, 0.5], [0.5, -0.5, 0.5]] + assert np.array_equal(face_nodes_test, face_nodes_gold) - # Normalize the vertices - vertices = [x / np.linalg.norm(x) for x in vertices] - vertices.append([INT_FILL_VALUE, INT_FILL_VALUE, INT_FILL_VALUE]) +def test_replace_fill_values_invalid(): + """Tests _replace_fill_values() helper function attempting to use a + fill value that is not representable by the current dtype.""" - # Construct the grid from the vertices - grid = ux.Grid.from_face_vertices(vertices, latlon=False) + face_nodes = np.array([[1, 2, -1], [-1, -1, -1]], dtype=np.int32) - # Extract the necessary grid data - face_node_conn = grid.face_node_connectivity.values - n_nodes_per_face = np.array([len(face) for face in face_node_conn]) - n_face = len(face_node_conn) - n_max_face_edges = max(n_nodes_per_face) - node_x = grid.node_x.values - node_y = grid.node_y.values - node_z = grid.node_z.values - - # Call the function to test - face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes( - face_node_conn, n_face, n_max_face_edges, node_x, node_y, node_z - ) - - # Check that the face_edges_connectivity_cartesian works as an input to _pole_point_inside_polygon - result = _pole_point_inside_polygon_cartesian( - 'North', face_edges_connectivity_cartesian[0] - ) - - # Assert that the result is True - self.assertTrue(result) - - def test_get_cartesian_face_edge_nodes_filled_value2(self): - # The face vertices order in counter-clockwise - # face_conn = [[0,1,2],[1,3,4,2]] - - #Each vertex is a 2D vector represent the longitude and latitude in degree. Call the node_lonlat_to_xyz to convert it to 3D vector - v0_deg = [10,10] - v1_deg = [15,15] - v2_deg = [5,15] - v3_deg = [15,45] - v4_deg = [5,45] - - # First convert them into radians - v0_rad = np.deg2rad(v0_deg) - v1_rad = np.deg2rad(v1_deg) - v2_rad = np.deg2rad(v2_deg) - v3_rad = np.deg2rad(v3_deg) - v4_rad = np.deg2rad(v4_deg) - - # It should look like following when passing in the _get_cartesian_face_edge_nodes - # [[v0_cart,v1_cart,v2_cart, [Fill_Value,Fill_Value,Fill_Value]],[v1_cart,v3_cart,v4_cart,v2_cart]] - v0_cart = _lonlat_rad_to_xyz(v0_rad[0],v0_rad[1]) - v1_cart = _lonlat_rad_to_xyz(v1_rad[0],v1_rad[1]) - v2_cart = _lonlat_rad_to_xyz(v2_rad[0],v2_rad[1]) - v3_cart = _lonlat_rad_to_xyz(v3_rad[0],v3_rad[1]) - v4_cart = _lonlat_rad_to_xyz(v4_rad[0],v4_rad[1]) - - face_node_conn = np.array([[0, 1, 2, INT_FILL_VALUE],[1, 3, 4, 2]]) - n_face = 2 - n_max_face_edges = 4 - n_nodes_per_face = np.array([len(face) for face in face_node_conn]) - node_x = np.array([v0_cart[0],v1_cart[0],v2_cart[0],v3_cart[0],v4_cart[0]]) - node_y = np.array([v0_cart[1],v1_cart[1],v2_cart[1],v3_cart[1],v4_cart[1]]) - node_z = np.array([v0_cart[2],v1_cart[2],v2_cart[2],v3_cart[2],v4_cart[2]]) - - # call the function to test - face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes( - face_node_conn, n_face, n_max_face_edges, node_x, node_y, node_z - ) - - # Define correct result - correct_result = np.array([ - [ - [ - [v0_cart[0], v0_cart[1], v0_cart[2]], - [v1_cart[0], v1_cart[1], v1_cart[2]] - ], - [ - [v1_cart[0], v1_cart[1], v1_cart[2]], - [v2_cart[0], v2_cart[1], v2_cart[2]] - ], - [ - [v2_cart[0], v2_cart[1], v2_cart[2]], - [v0_cart[0], v0_cart[1], v0_cart[2]] - ], - [ - [INT_FILL_VALUE, INT_FILL_VALUE, INT_FILL_VALUE], - [INT_FILL_VALUE, INT_FILL_VALUE, INT_FILL_VALUE] - ] - ], - [ - [ - [v1_cart[0], v1_cart[1], v1_cart[2]], - [v3_cart[0], v3_cart[1], v3_cart[2]] - ], - [ - [v3_cart[0], v3_cart[1], v3_cart[2]], - [v4_cart[0], v4_cart[1], v4_cart[2]] - ], - [ - [v4_cart[0], v4_cart[1], v4_cart[2]], - [v2_cart[0], v2_cart[1], v2_cart[2]] - ], - [ - [v2_cart[0], v2_cart[1], v2_cart[2]], - [v1_cart[0], v1_cart[1], v1_cart[2]] - ] - ] - ]) - - - # Assert that the result is correct - self.assertEqual(face_edges_connectivity_cartesian.shape, correct_result.shape) - - - def test_get_lonlat_face_edge_nodes_pipeline(self): - # Create the vertices for the grid, based around the North Pole - vertices = [[0.5, 0.5, 0.5], [-0.5, 0.5, 0.5], [-0.5, -0.5, 0.5], [0.5, -0.5, 0.5]] - - # Normalize the vertices - vertices = [x / np.linalg.norm(x) for x in vertices] - - # Construct the grid from the vertices - grid = ux.Grid.from_face_vertices(vertices, latlon=False) - - # Extract the necessary grid data - face_node_conn = grid.face_node_connectivity.values - n_nodes_per_face = np.array([len(face) for face in face_node_conn]) - n_face = len(face_node_conn) - n_max_face_edges = max(n_nodes_per_face) - node_lon = grid.node_lon.values - node_lat = grid.node_lat.values - - # Call the function to test - face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes( - face_node_conn, n_face, n_max_face_edges, node_lon, node_lat - ) - - # Convert the first face's edges to Cartesian coordinates - face_edges_connectivity_lonlat = face_edges_connectivity_lonlat[0] - face_edges_connectivity_cartesian = [] - for edge in face_edges_connectivity_lonlat: - edge_cart = [_lonlat_rad_to_xyz(*node) for node in edge] - face_edges_connectivity_cartesian.append(edge_cart) - - # Check that the face_edges_connectivity_cartesian works as an input to _pole_point_inside_polygon - result = _pole_point_inside_polygon_cartesian( - 'North', np.array(face_edges_connectivity_cartesian) + # invalid fill value with dtype should raise a valueError + with pytest.raises(ValueError): + # INT_FILL_VALUE (max(uint32) not representable by int16) + face_nodes_test = _replace_fill_values( + grid_var=face_nodes, + original_fill=-1, + new_fill=INT_FILL_VALUE, + new_dtype=np.int16 ) - # Assert that the result is True - self.assertTrue(result) - - def test_get_lonlat_face_edge_nodes_filled_value(self): - # Create the vertices for the grid, based around the North Pole - vertices = [[0.5, 0.5, 0.5], [-0.5, 0.5, 0.5], [-0.5, -0.5, 0.5], [0.5, -0.5, 0.5]] - - # Normalize the vertices - vertices = [x / np.linalg.norm(x) for x in vertices] - vertices.append([INT_FILL_VALUE, INT_FILL_VALUE, INT_FILL_VALUE]) - - # Construct the grid from the vertices - grid = ux.Grid.from_face_vertices(vertices, latlon=False) - - # Extract the necessary grid data - face_node_conn = grid.face_node_connectivity.values - n_nodes_per_face = np.array([len(face) for face in face_node_conn]) - n_face = len(face_node_conn) - n_max_face_edges = max(n_nodes_per_face) - node_lon = grid.node_lon.values - node_lat = grid.node_lat.values - - # Call the function to test - face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes( - face_node_conn, n_face, n_max_face_edges, node_lon, node_lat - ) - - # Convert the first face's edges to Cartesian coordinates - face_edges_connectivity_lonlat = face_edges_connectivity_lonlat[0] - face_edges_connectivity_cartesian = [] - for edge in face_edges_connectivity_lonlat: - edge_cart = [_lonlat_rad_to_xyz(*node) for node in edge] - face_edges_connectivity_cartesian.append(edge_cart) - - # Check that the face_edges_connectivity_cartesian works as an input to _pole_point_inside_polygon - result = _pole_point_inside_polygon_cartesian( - 'North', np.array(face_edges_connectivity_cartesian) - ) - - # Assert that the result is True - self.assertTrue(result) - - - def test_get_lonlat_face_edge_nodes_filled_value2(self): - # The face vertices order in counter-clockwise - # face_conn = [[0,1,2],[1,3,4,2]] - - #Each vertex is a 2D vector represent the longitude and latitude in degree. Call the node_lonlat_to_xyz to convert it to 3D vector - v0_deg = [10,10] - v1_deg = [15,15] - v2_deg = [5,15] - v3_deg = [15,45] - v4_deg = [5,45] - - # First convert them into radians - v0_rad = np.deg2rad(v0_deg) - v1_rad = np.deg2rad(v1_deg) - v2_rad = np.deg2rad(v2_deg) - v3_rad = np.deg2rad(v3_deg) - v4_rad = np.deg2rad(v4_deg) - - face_node_conn = np.array([[0, 1, 2, INT_FILL_VALUE],[1, 3, 4, 2]]) - n_face = 2 - n_max_face_edges = 4 - n_nodes_per_face = np.array([len(face) for face in face_node_conn]) - node_lon = np.array([v0_rad[0],v1_rad[0],v2_rad[0],v3_rad[0],v4_rad[0]]) - node_lat = np.array([v0_rad[1],v1_rad[1],v2_rad[1],v3_rad[1],v4_rad[1]]) - - # call the function to test - face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes( - face_node_conn, n_face, n_max_face_edges, node_lon, node_lat - ) +def test_convert_face_node_conn_to_sparse_matrix(): + """Tests _face_nodes_to_sparse_matrix() helper function to see if can + generate sparse matrix from face_nodes_conn that has Fill Values.""" + face_nodes_conn = np.array([[3, 4, 5, INT_FILL_VALUE], [3, 0, 2, 5], + [3, 4, 1, 0], [0, 1, 2, INT_FILL_VALUE]]) + + face_indices, nodes_indices, non_zero_flag = ux.grid.connectivity._face_nodes_to_sparse_matrix( + face_nodes_conn) + expected_non_zero_flag = np.array( + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + expected_face_indices = np.array( + [0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3]) + expected_nodes_indices = np.array( + [3, 4, 5, 3, 0, 2, 5, 3, 4, 1, 0, 0, 1, 2]) + + nt.assert_array_equal(non_zero_flag, expected_non_zero_flag) + nt.assert_array_equal(face_indices, expected_face_indices) + nt.assert_array_equal(nodes_indices, expected_nodes_indices) + +def test_in_between(): + # Test the in_between operator + assert in_between(0, 1, 2) + assert in_between(-1, -1.5, -2) + +def test_angle_of_2_vectors(): + # Test the angle between two vectors + v1 = np.array([1.0, 0.0, 0.0]) + v2 = np.array([0.0, 1.0, 0.0]) + assert pytest.approx(_angle_of_2_vectors(v1, v2)) == np.pi / 2.0 + + v1 = np.array([1.0, 0.0, 0.0]) + v2 = np.array([1.0, 0.0, 0.0]) + assert pytest.approx(_angle_of_2_vectors(v1, v2)) == 0.0 + +def test_angle_of_2_vectors_180_degree(): + GCR1_cart = np.array([ + _lonlat_rad_to_xyz(np.deg2rad(0.0), + np.deg2rad(0.0)), + _lonlat_rad_to_xyz(np.deg2rad(181.0), + np.deg2rad(0.0)) + ]) + + res = _angle_of_2_vectors(GCR1_cart[0], GCR1_cart[1]) + + # The angle between the two vectors should be 181 degree + assert pytest.approx(res, abs=1e-8) == np.deg2rad(181.0) + + GCR1_cart = np.array([ + _lonlat_rad_to_xyz(np.deg2rad(170.0), + np.deg2rad(89.0)), + _lonlat_rad_to_xyz(np.deg2rad(170.0), + np.deg2rad(-10.0)) + ]) + + res = _angle_of_2_vectors(GCR1_cart[0], GCR1_cart[1]) + + # The angle between the two vectors should be 99 degrees + assert pytest.approx(res, abs=1e-8) == np.deg2rad(89.0+10.0) + +def test_get_cartesian_face_edge_nodes_pipeline(): + vertices = [[0.5, 0.5, 0.5], [-0.5, 0.5, 0.5], [-0.5, -0.5, 0.5], [0.5, -0.5, 0.5]] + vertices = [x / np.linalg.norm(x) for x in vertices] + grid = ux.Grid.from_face_vertices(vertices, latlon=False) + + face_node_conn = grid.face_node_connectivity.values + n_nodes_per_face = np.array([len(face) for face in face_node_conn]) + n_face = len(face_node_conn) + n_max_face_edges = max(n_nodes_per_face) + node_x = grid.node_x.values + node_y = grid.node_y.values + node_z = grid.node_z.values + + face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes( + face_node_conn, n_face, n_max_face_edges, node_x, node_y, node_z + ) + + result = _pole_point_inside_polygon_cartesian( + 'North', face_edges_connectivity_cartesian[0] + ) + + assert result is True + +def test_get_cartesian_face_edge_nodes_filled_value(): + vertices = [[0.5, 0.5, 0.5], [-0.5, 0.5, 0.5], [-0.5, -0.5, 0.5], [0.5, -0.5, 0.5]] + vertices = [x / np.linalg.norm(x) for x in vertices] + vertices.append([INT_FILL_VALUE, INT_FILL_VALUE, INT_FILL_VALUE]) + + grid = ux.Grid.from_face_vertices(vertices, latlon=False) + + face_node_conn = grid.face_node_connectivity.values + n_nodes_per_face = np.array([len(face) for face in face_node_conn]) + n_face = len(face_node_conn) + n_max_face_edges = max(n_nodes_per_face) + node_x = grid.node_x.values + node_y = grid.node_y.values + node_z = grid.node_z.values + + face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes( + face_node_conn, n_face, n_max_face_edges, node_x, node_y, node_z + ) + + result = _pole_point_inside_polygon_cartesian( + 'North', face_edges_connectivity_cartesian[0] + ) + + assert result is True + +def test_get_cartesian_face_edge_nodes_filled_value2(): + v0_deg = [10,10] + v1_deg = [15,15] + v2_deg = [5,15] + v3_deg = [15,45] + v4_deg = [5,45] + + v0_rad = np.deg2rad(v0_deg) + v1_rad = np.deg2rad(v1_deg) + v2_rad = np.deg2rad(v2_deg) + v3_rad = np.deg2rad(v3_deg) + v4_rad = np.deg2rad(v4_deg) + + v0_cart = _lonlat_rad_to_xyz(v0_rad[0],v0_rad[1]) + v1_cart = _lonlat_rad_to_xyz(v1_rad[0],v1_rad[1]) + v2_cart = _lonlat_rad_to_xyz(v2_rad[0],v2_rad[1]) + v3_cart = _lonlat_rad_to_xyz(v3_rad[0],v3_rad[1]) + v4_cart = _lonlat_rad_to_xyz(v4_rad[0],v4_rad[1]) + + face_node_conn = np.array([[0, 1, 2, INT_FILL_VALUE],[1, 3, 4, 2]]) + n_face = 2 + n_max_face_edges = 4 + n_nodes_per_face = np.array([len(face) for face in face_node_conn]) + node_x = np.array([v0_cart[0],v1_cart[0],v2_cart[0],v3_cart[0],v4_cart[0]]) + node_y = np.array([v0_cart[1],v1_cart[1],v2_cart[1],v3_cart[1],v4_cart[1]]) + node_z = np.array([v0_cart[2],v1_cart[2],v2_cart[2],v3_cart[2],v4_cart[2]]) + + face_edges_connectivity_cartesian = _get_cartesian_face_edge_nodes( + face_node_conn, n_face, n_max_face_edges, node_x, node_y, node_z + ) + + correct_result = np.array([ + [ + [[v0_cart[0], v0_cart[1], v0_cart[2]], [v1_cart[0], v1_cart[1], v1_cart[2]]], + [[v1_cart[0], v1_cart[1], v1_cart[2]], [v2_cart[0], v2_cart[1], v2_cart[2]]], + [[v2_cart[0], v2_cart[1], v2_cart[2]], [v0_cart[0], v0_cart[1], v0_cart[2]]], + [[INT_FILL_VALUE, INT_FILL_VALUE, INT_FILL_VALUE], [INT_FILL_VALUE, INT_FILL_VALUE, INT_FILL_VALUE]] + ], + [ + [[v1_cart[0], v1_cart[1], v1_cart[2]], [v3_cart[0], v3_cart[1], v3_cart[2]]], + [[v3_cart[0], v3_cart[1], v3_cart[2]], [v4_cart[0], v4_cart[1], v4_cart[2]]], + [[v4_cart[0], v4_cart[1], v4_cart[2]], [v2_cart[0], v2_cart[1], v2_cart[2]]], + [[v2_cart[0], v2_cart[1], v2_cart[2]], [v1_cart[0], v1_cart[1], v1_cart[2]]] + ] + ]) + + assert face_edges_connectivity_cartesian.shape == correct_result.shape + +def test_get_lonlat_face_edge_nodes_pipeline(): + vertices = [[0.5, 0.5, 0.5], [-0.5, 0.5, 0.5], [-0.5, -0.5, 0.5], [0.5, -0.5, 0.5]] + vertices = [x / np.linalg.norm(x) for x in vertices] + grid = ux.Grid.from_face_vertices(vertices, latlon=False) + + face_node_conn = grid.face_node_connectivity.values + n_nodes_per_face = np.array([len(face) for face in face_node_conn]) + n_face = len(face_node_conn) + n_max_face_edges = max(n_nodes_per_face) + node_lon = grid.node_lon.values + node_lat = grid.node_lat.values + + face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes( + face_node_conn, n_face, n_max_face_edges, node_lon, node_lat + ) + + face_edges_connectivity_lonlat = face_edges_connectivity_lonlat[0] + face_edges_connectivity_cartesian = [] + for edge in face_edges_connectivity_lonlat: + edge_cart = [_lonlat_rad_to_xyz(*node) for node in edge] + face_edges_connectivity_cartesian.append(edge_cart) + + result = _pole_point_inside_polygon_cartesian( + 'North', np.array(face_edges_connectivity_cartesian) + ) + + assert result is True + +def test_get_lonlat_face_edge_nodes_filled_value(): + vertices = [[0.5, 0.5, 0.5], [-0.5, 0.5, 0.5], [-0.5, -0.5, 0.5], [0.5, -0.5, 0.5]] + vertices = [x / np.linalg.norm(x) for x in vertices] + vertices.append([INT_FILL_VALUE, INT_FILL_VALUE, INT_FILL_VALUE]) + + grid = ux.Grid.from_face_vertices(vertices, latlon=False) + + face_node_conn = grid.face_node_connectivity.values + n_nodes_per_face = np.array([len(face) for face in face_node_conn]) + n_face = len(face_node_conn) + n_max_face_edges = max(n_nodes_per_face) + node_lon = grid.node_lon.values + node_lat = grid.node_lat.values + + face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes( + face_node_conn, n_face, n_max_face_edges, node_lon, node_lat + ) + + face_edges_connectivity_lonlat = face_edges_connectivity_lonlat[0] + face_edges_connectivity_cartesian = [] + for edge in face_edges_connectivity_lonlat: + edge_cart = [_lonlat_rad_to_xyz(*node) for node in edge] + face_edges_connectivity_cartesian.append(edge_cart) + + result = _pole_point_inside_polygon_cartesian( + 'North', np.array(face_edges_connectivity_cartesian) + ) + + assert result is True + +def test_get_lonlat_face_edge_nodes_filled_value2(): + v0_deg = [10,10] + v1_deg = [15,15] + v2_deg = [5,15] + v3_deg = [15,45] + v4_deg = [5,45] + + v0_rad = np.deg2rad(v0_deg) + v1_rad = np.deg2rad(v1_deg) + v2_rad = np.deg2rad(v2_deg) + v3_rad = np.deg2rad(v3_deg) + v4_rad = np.deg2rad(v4_deg) + + face_node_conn = np.array([[0, 1, 2, INT_FILL_VALUE],[1, 3, 4, 2]]) + n_face = 2 + n_max_face_edges = 4 + n_nodes_per_face = np.array([len(face) for face in face_node_conn]) + node_lon = np.array([v0_rad[0],v1_rad[0],v2_rad[0],v3_rad[0],v4_rad[0]]) + node_lat = np.array([v0_rad[1],v1_rad[1],v2_rad[1],v3_rad[1],v4_rad[1]]) + + face_edges_connectivity_lonlat = _get_lonlat_rad_face_edge_nodes( + face_node_conn, n_face, n_max_face_edges, node_lon, node_lat + ) + + correct_result = np.array([ + [ + [[v0_rad[0], v0_rad[1]], [v1_rad[0], v1_rad[1]]], + [[v1_rad[0], v1_rad[1]], [v2_rad[0], v2_rad[1]]], + [[v2_rad[0], v2_rad[1]], [v0_rad[0], v0_rad[1]]], + [[INT_FILL_VALUE, INT_FILL_VALUE], [INT_FILL_VALUE, INT_FILL_VALUE]] + ], + [ + [[v1_rad[0], v1_rad[1]], [v3_rad[0], v3_rad[1]]], + [[v3_rad[0], v3_rad[1]], [v4_rad[0], v4_rad[1]]], + [[v4_rad[0], v4_rad[1]], [v2_rad[0], v2_rad[1]]], + [[v2_rad[0], v2_rad[1]], [v1_rad[0], v1_rad[1]]] + ] + ]) - # Define correct result - correct_result = np.array([ - [ - [ - [v0_rad[0], v0_rad[1]], - [v1_rad[0], v1_rad[1]] - ], - [ - [v1_rad[0], v1_rad[1]], - [v2_rad[0], v2_rad[1]] - ], - [ - [v2_rad[0], v2_rad[1]], - [v0_rad[0], v0_rad[1]] - ], - [ - [INT_FILL_VALUE, INT_FILL_VALUE], - [INT_FILL_VALUE, INT_FILL_VALUE] - ] - ], - [ - [ - [v1_rad[0], v1_rad[1]], - [v3_rad[0], v3_rad[1]] - ], - [ - [v3_rad[0], v3_rad[1]], - [v4_rad[0], v4_rad[1]] - ], - [ - [v4_rad[0], v4_rad[1]], - [v2_rad[0], v2_rad[1]] - ], - [ - [v2_rad[0], v2_rad[1]], - [v1_rad[0], v1_rad[1]] - ] - ] - ]) - - # Assert that the result is correct - self.assertEqual(face_edges_connectivity_lonlat.shape, correct_result.shape) + assert face_edges_connectivity_lonlat.shape == correct_result.shape diff --git a/test/test_icon.py b/test/test_icon.py index 67bc03a91..75bab973b 100644 --- a/test/test_icon.py +++ b/test/test_icon.py @@ -1,17 +1,12 @@ import uxarray as ux import os - import pytest - from pathlib import Path current_path = Path(os.path.dirname(os.path.realpath(__file__))) - grid_path = current_path / 'meshfiles' / "icon" / "R02B04" / 'icon_grid_0010_R02B04_G.nc' - - def test_read_icon_grid(): uxgrid = ux.open_grid(grid_path) diff --git a/test/test_integrate.py b/test/test_integrate.py index db327959e..f2566189f 100644 --- a/test/test_integrate.py +++ b/test/test_integrate.py @@ -1,6 +1,5 @@ import uxarray as ux import os -from unittest import TestCase from pathlib import Path import numpy as np import pandas as pd @@ -8,828 +7,704 @@ import numpy.testing as nt import uxarray as ux +import pytest from uxarray.constants import ERROR_TOLERANCE, INT_FILL_VALUE from uxarray.grid.coordinates import _lonlat_rad_to_xyz from uxarray.grid.integrate import _get_zonal_face_interval, _process_overlapped_intervals, _get_zonal_faces_weight_at_constLat,_get_faces_constLat_intersection_info - current_path = Path(os.path.dirname(os.path.realpath(__file__))) - -class TestIntegrate(TestCase): - gridfile_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" - dsfile_var2_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30_var2.nc" - - def test_single_dim(self): - """Integral with 1D data mapped to each face.""" - uxgrid = ux.open_grid(self.gridfile_ne30) - - test_data = np.ones(uxgrid.n_face) - - dims = {"n_face": uxgrid.n_face} - - uxda = ux.UxDataArray(data=test_data, - dims=dims, - uxgrid=uxgrid, - name='var2') - - integral = uxda.integrate() - - # integration reduces the dimension by 1 - assert integral.ndim == len(dims) - 1 - - nt.assert_almost_equal(integral, 4 * np.pi) - - def test_multi_dim(self): - """Integral with 3D data mapped to each face.""" - uxgrid = ux.open_grid(self.gridfile_ne30) - - test_data = np.ones((5, 5, uxgrid.n_face)) - - dims = {"a": 5, "b": 5, "n_face": uxgrid.n_face} - - uxda = ux.UxDataArray(data=test_data, - dims=dims, - uxgrid=uxgrid, - name='var2') - - integral = uxda.integrate() - - # integration reduces the dimension by 1 - assert integral.ndim == len(dims) - 1 - - nt.assert_almost_equal(integral, np.ones((5, 5)) * 4 * np.pi) - - -class TestFaceWeights(TestCase): - gridfile_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" - dsfile_var2_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30_var2.nc" - - def test_get_faces_constLat_intersection_info_one_intersection(self): - face_edges_cart = np.array([ - [[-5.4411371445381629e-01, -4.3910468172333759e-02, -8.3786164521844386e-01], - [-5.4463903501502697e-01, -6.6699045092185599e-17, -8.3867056794542405e-01]], - - [[-5.4463903501502697e-01, -6.6699045092185599e-17, -8.3867056794542405e-01], - [-4.9999999999999994e-01, -6.1232339957367648e-17, -8.6602540378443871e-01]], - - [[-4.9999999999999994e-01, -6.1232339957367648e-17, -8.6602540378443871e-01], - [-4.9948581138450826e-01, -4.5339793804534498e-02, -8.6513480297773349e-01]], - - [[-4.9948581138450826e-01, -4.5339793804534498e-02, -8.6513480297773349e-01], - [-5.4411371445381629e-01, -4.3910468172333759e-02, -8.3786164521844386e-01]] - ]) - - latitude_cart = -0.8660254037844386 - is_latlonface=False - is_GCA_list=None - unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface) - # The expected unique_intersections length is 1 - self.assertEqual(len(unique_intersections), 1) - - def test_get_faces_constLat_intersection_info_encompass_pole(self): - face_edges_cart = np.array([ - [[0.03982285692494229, 0.00351700770436231, 0.9992005658140627], - [0.00896106681877875, 0.03896060263227105, 0.9992005658144913]], - - [[0.00896106681877875, 0.03896060263227105, 0.9992005658144913], - [-0.03428461218295055, 0.02056197086916728, 0.9992005658132106]], - - [[-0.03428461218295055, 0.02056197086916728, 0.9992005658132106], - [-0.03015012448894485, -0.02625260499902213, 0.9992005658145248]], - - [[-0.03015012448894485, -0.02625260499902213, 0.9992005658145248], - [0.01565081128889155, -0.03678697293262131, 0.9992005658167203]], - - [[0.01565081128889155, -0.03678697293262131, 0.9992005658167203], - [0.03982285692494229, 0.00351700770436231, 0.9992005658140627]] - ]) - - latitude_cart = 0.9993908270190958 - # Convert the latitude to degrees - latitude_rad = np.arcsin(latitude_cart) - latitude_deg = np.rad2deg(latitude_rad) - print(latitude_deg) - - is_latlonface=False - is_GCA_list=None - unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface) - # The expected unique_intersections length should be no greater than 2* n_edges - self.assertLessEqual(len(unique_intersections), 2*len(face_edges_cart)) - - def test_get_faces_constLat_intersection_info_on_pole(self): - face_edges_cart = np.array([ - [[-5.2264427688714095e-02, -5.2264427688714102e-02, -9.9726468863423734e-01], - [-5.2335956242942412e-02, -6.4093061293235361e-18, -9.9862953475457394e-01]], - - [[-5.2335956242942412e-02, -6.4093061293235361e-18, -9.9862953475457394e-01], - [6.1232339957367660e-17, 0.0000000000000000e+00, -1.0000000000000000e+00]], - - [[6.1232339957367660e-17, 0.0000000000000000e+00, -1.0000000000000000e+00], - [3.2046530646617680e-18, -5.2335956242942412e-02, -9.9862953475457394e-01]], - - [[3.2046530646617680e-18, -5.2335956242942412e-02, -9.9862953475457394e-01], - [-5.2264427688714095e-02, -5.2264427688714102e-02, -9.9726468863423734e-01]] - ]) - latitude_cart = -0.9998476951563913 - is_latlonface=False - is_GCA_list=None - unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface) - # The expected unique_intersections length is 2 - self.assertEqual(len(unique_intersections), 2) - - - def test_get_faces_constLat_intersection_info_near_pole(self): - face_edges_cart = np.array([ - [[-5.1693346290592648e-02, 1.5622531297347531e-01, -9.8636780641686628e-01], - [-5.1195320928843470e-02, 2.0763904784932552e-01, -9.7686491641537532e-01]], - [[-5.1195320928843470e-02, 2.0763904784932552e-01, -9.7686491641537532e-01], - [1.2730919333264125e-17, 2.0791169081775882e-01, -9.7814760073380580e-01]], - [[1.2730919333264125e-17, 2.0791169081775882e-01, -9.7814760073380580e-01], - [9.5788483443923397e-18, 1.5643446504023048e-01, -9.8768834059513777e-01]], - [[9.5788483443923397e-18, 1.5643446504023048e-01, -9.8768834059513777e-01], - [-5.1693346290592648e-02, 1.5622531297347531e-01, -9.8636780641686628e-01]] - ]) - - latitude_cart = -0.9876883405951378 - latitude_rad = np.arcsin(latitude_cart) - latitude_deg = np.rad2deg(latitude_rad) - is_latlonface=False - is_GCA_list=None - unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface) - # The expected unique_intersections length is 2 - self.assertEqual(len(unique_intersections), 1) - - - def test_get_faces_constLat_intersection_info_2(self): - """This might test the case where the calculated intersection points - might suffer from floating point errors If not handled properly, the - function might return more than 2 unique intersections.""" - - face_edges_cart = np.array([[[0.6546536707079771, -0.37796447300922714, -0.6546536707079772], - [0.6652465971273088, -0.33896007142593115, -0.6652465971273087]], - - [[0.6652465971273088, -0.33896007142593115, -0.6652465971273087], - [0.6949903639307233, -0.3541152775760984, -0.6257721344312508]], - - [[0.6949903639307233, -0.3541152775760984, -0.6257721344312508], - [0.6829382762718700, -0.39429459764546304, -0.6149203859609873]], - - [[0.6829382762718700, -0.39429459764546304, -0.6149203859609873], - [0.6546536707079771, -0.37796447300922714, -0.6546536707079772]]]) - - latitude_cart = -0.6560590289905073 - is_latlonface=False - is_GCA_list=None - unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface) - # The expected unique_intersections length is 2 - self.assertEqual(len(unique_intersections), 2) - - - def test_get_faces_constLat_intersection_info_2(self): - """This might test the case where the calculated intersection points - might suffer from floating point errors If not handled properly, the - function might return more than 2 unique intersections.""" - - face_edges_cart = np.array([[[0.6546536707079771, -0.37796447300922714, -0.6546536707079772], - [0.6652465971273088, -0.33896007142593115, -0.6652465971273087]], - - [[0.6652465971273088, -0.33896007142593115, -0.6652465971273087], - [0.6949903639307233, -0.3541152775760984, -0.6257721344312508]], - - [[0.6949903639307233, -0.3541152775760984, -0.6257721344312508], - [0.6829382762718700, -0.39429459764546304, -0.6149203859609873]], - - [[0.6829382762718700, -0.39429459764546304, -0.6149203859609873], - [0.6546536707079771, -0.37796447300922714, -0.6546536707079772]]]) - - latitude_cart = -0.6560590289905073 - is_latlonface=False - is_GCA_list=None - unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface) - # The expected unique_intersections length is 2 - self.assertEqual(len(unique_intersections), 2) - - def test_get_zonal_face_interval(self): - """Test the _get_zonal_face_interval function for correct interval - computation. - - This test verifies that the _get_zonal_face_interval function - accurately computes the zonal face intervals given a set of face - edge nodes and a constant latitude value (constZ). The expected - intervals are compared against the calculated intervals to - ensure correctness. - """ - vertices_lonlat = [[1.6 * np.pi, 0.25 * np.pi], - [1.6 * np.pi, -0.25 * np.pi], - [0.4 * np.pi, -0.25 * np.pi], - [0.4 * np.pi, 0.25 * np.pi]] - vertices = [_lonlat_rad_to_xyz(*v) for v in vertices_lonlat] - - face_edge_nodes = np.array([[vertices[0], vertices[1]], - [vertices[1], vertices[2]], - [vertices[2], vertices[3]], - [vertices[3], vertices[0]]]) - - constZ = np.sin(0.20) - # The latlon bounds for the latitude is not necessarily correct below since we don't use the latitudes bound anyway - interval_df = _get_zonal_face_interval(face_edge_nodes, constZ, - np.array([[-0.25 * np.pi, 0.25 * np.pi], [1.6 * np.pi, - 0.4 * np.pi]])) - expected_interval_df = pd.DataFrame({ - 'start': [1.6 * np.pi, 0.0], - 'end': [2.0 * np.pi, 00.4 * np.pi] - }) - # Sort both DataFrames by 'start' column before comparison - expected_interval_df_sorted = expected_interval_df.sort_values(by='start').reset_index(drop=True) - - # Converting the sorted DataFrames to NumPy arrays - actual_values_sorted = interval_df[['start', 'end']].to_numpy() - expected_values_sorted = expected_interval_df_sorted[['start', 'end']].to_numpy() - - # Asserting almost equal arrays - nt.assert_array_almost_equal(actual_values_sorted, expected_values_sorted, decimal=13) - - - def test_get_zonal_face_interval_empty_interval(self): - """Test the _get_zonal_face_interval function for cases where the - interval is empty. - - This test verifies that the _get_zonal_face_interval function - correctly returns an empty interval when the latitude only - touches the face but does not intersect it. - """ - face_edges_cart = np.array([ - [[-5.4411371445381629e-01, -4.3910468172333759e-02, -8.3786164521844386e-01], - [-5.4463903501502697e-01, -6.6699045092185599e-17, -8.3867056794542405e-01]], - - [[-5.4463903501502697e-01, -6.6699045092185599e-17, -8.3867056794542405e-01], - [-4.9999999999999994e-01, -6.1232339957367648e-17, -8.6602540378443871e-01]], - - [[-4.9999999999999994e-01, -6.1232339957367648e-17, -8.6602540378443871e-01], - [-4.9948581138450826e-01, -4.5339793804534498e-02, -8.6513480297773349e-01]], - - [[-4.9948581138450826e-01, -4.5339793804534498e-02, -8.6513480297773349e-01], - [-5.4411371445381629e-01, -4.3910468172333759e-02, -8.3786164521844386e-01]] - ]) - - latitude_cart = -0.8660254037844386 - face_latlon_bounds = np.array([ - [-1.04719755, -0.99335412], - [3.14159265, 3.2321175] - ]) - - res = _get_zonal_face_interval(face_edges_cart, latitude_cart, face_latlon_bounds) - expected_res = pd.DataFrame({"start": [0.0], "end": [0.0]}) - pd.testing.assert_frame_equal(res, expected_res) - - def test_get_zonal_face_interval_encompass_pole(self): - """Test the _get_zonal_face_interval function for cases where the face - encompasses the pole inside.""" - face_edges_cart = np.array([ - [[0.03982285692494229, 0.00351700770436231, 0.9992005658140627], - [0.00896106681877875, 0.03896060263227105, 0.9992005658144913]], - - [[0.00896106681877875, 0.03896060263227105, 0.9992005658144913], - [-0.03428461218295055, 0.02056197086916728, 0.9992005658132106]], - - [[-0.03428461218295055, 0.02056197086916728, 0.9992005658132106], - [-0.03015012448894485, -0.02625260499902213, 0.9992005658145248]], - - [[-0.03015012448894485, -0.02625260499902213, 0.9992005658145248], - [0.01565081128889155, -0.03678697293262131, 0.9992005658167203]], - - [[0.01565081128889155, -0.03678697293262131, 0.9992005658167203], - [0.03982285692494229, 0.00351700770436231, 0.9992005658140627]] - ]) - - latitude_cart = 0.9993908270190958 - - face_latlon_bounds = np.array([ - [np.arcsin(0.9992005658145248), 0.5*np.pi], - [0, 2*np.pi] - ]) - # Expected result DataFrame - expected_df = pd.DataFrame({ - 'start': [0.000000, 1.101091, 2.357728, 3.614365, 4.871002, 6.127640], - 'end': [0.331721, 1.588358, 2.844995, 4.101632, 5.358270, 6.283185] - }) - - # Call the function to get the result - res = _get_zonal_face_interval(face_edges_cart, latitude_cart, face_latlon_bounds) - - # Assert the result matches the expected DataFrame - pd.testing.assert_frame_equal(res, expected_df) - - - - def test_get_zonal_face_interval_FILL_VALUE(self): - """Test the _get_zonal_face_interval function for cases where there are - dummy nodes.""" - dummy_node = [INT_FILL_VALUE, INT_FILL_VALUE, INT_FILL_VALUE] - vertices_lonlat = [[1.6 * np.pi, 0.25 * np.pi], - [1.6 * np.pi, -0.25 * np.pi], - [0.4 * np.pi, -0.25 * np.pi], - [0.4 * np.pi, 0.25 * np.pi]] - vertices = [_lonlat_rad_to_xyz(*v) for v in vertices_lonlat] - - face_edge_nodes = np.array([[vertices[0], vertices[1]], - [vertices[1], vertices[2]], - [vertices[2], vertices[3]], - [vertices[3], vertices[0]], - [dummy_node,dummy_node]]) - - constZ = np.sin(0.20) - # The latlon bounds for the latitude is not necessarily correct below since we don't use the latitudes bound anyway - interval_df = _get_zonal_face_interval(face_edge_nodes, constZ, - np.array([[-0.25 * np.pi, 0.25 * np.pi], [1.6 * np.pi, - 0.4 * np.pi]])) - expected_interval_df = pd.DataFrame({ - 'start': [1.6 * np.pi, 0.0], - 'end': [2.0 * np.pi, 00.4 * np.pi] - }) - # Sort both DataFrames by 'start' column before comparison - expected_interval_df_sorted = expected_interval_df.sort_values(by='start').reset_index(drop=True) - - # Converting the sorted DataFrames to NumPy arrays - actual_values_sorted = interval_df[['start', 'end']].to_numpy() - expected_values_sorted = expected_interval_df_sorted[['start', 'end']].to_numpy() - - # Asserting almost equal arrays - nt.assert_array_almost_equal(actual_values_sorted, expected_values_sorted, decimal=13) - - - def test_get_zonal_face_interval_GCA_constLat(self): - vertices_lonlat = [[-0.4 * np.pi, 0.25 * np.pi], - [-0.4 * np.pi, -0.25 * np.pi], - [0.4 * np.pi, -0.25 * np.pi], - [0.4 * np.pi, 0.25 * np.pi]] - - vertices = [_lonlat_rad_to_xyz(*v) for v in vertices_lonlat] - - face_edge_nodes = np.array([[vertices[0], vertices[1]], - [vertices[1], vertices[2]], - [vertices[2], vertices[3]], - [vertices[3], vertices[0]]]) - - constZ = np.sin(0.20 * np.pi) - interval_df = _get_zonal_face_interval(face_edge_nodes, constZ, - np.array([[-0.25 * np.pi, 0.25 * np.pi], [1.6 * np.pi, - 0.4 * np.pi]]), - is_GCA_list=np.array([True, False, True, False])) - expected_interval_df = pd.DataFrame({ - 'start': [1.6 * np.pi, 0.0], - 'end': [2.0 * np.pi, 00.4 * np.pi] - }) - # Sort both DataFrames by 'start' column before comparison - expected_interval_df_sorted = expected_interval_df.sort_values(by='start').reset_index(drop=True) - - # Converting the sorted DataFrames to NumPy arrays - actual_values_sorted = interval_df[['start', 'end']].to_numpy() - expected_values_sorted = expected_interval_df_sorted[['start', 'end']].to_numpy() - - # Asserting almost equal arrays - nt.assert_array_almost_equal(actual_values_sorted, expected_values_sorted, decimal=13) - - - def test_get_zonal_face_interval_equator(self): - """Test that the face interval is correctly computed when the latitude - is at the equator.""" - vertices_lonlat = [[-0.4 * np.pi, 0.25 * np.pi], [-0.4 * np.pi, 0.0], - [0.4 * np.pi, 0.0], [0.4 * np.pi, 0.25 * np.pi]] - - vertices = [_lonlat_rad_to_xyz(*v) for v in vertices_lonlat] - - face_edge_nodes = np.array([[vertices[0], vertices[1]], - [vertices[1], vertices[2]], - [vertices[2], vertices[3]], - [vertices[3], vertices[0]]]) - - interval_df = _get_zonal_face_interval(face_edge_nodes, 0.0, - np.array([[-0.25 * np.pi, 0.25 * np.pi], [1.6 * np.pi, - 0.4 * np.pi]]), - is_GCA_list=np.array([True, True, True, True])) - expected_interval_df = pd.DataFrame({ - 'start': [1.6 * np.pi, 0.0], - 'end': [2.0 * np.pi, 00.4 * np.pi] - }) - # Sort both DataFrames by 'start' column before comparison - expected_interval_df_sorted = expected_interval_df.sort_values(by='start').reset_index(drop=True) - - # Converting the sorted DataFrames to NumPy arrays - actual_values_sorted = interval_df[['start', 'end']].to_numpy() - expected_values_sorted = expected_interval_df_sorted[['start', 'end']].to_numpy() - - # Asserting almost equal arrays - nt.assert_array_almost_equal(actual_values_sorted, expected_values_sorted, decimal=13) - - # Even if we change the is_GCA_list to False, the result should be the same - interval_df = _get_zonal_face_interval(face_edge_nodes, 0.0, - np.array([[-0.25 * np.pi, 0.25 * np.pi], [1.6 * np.pi, - 0.4 * np.pi]]), - is_GCA_list=np.array([True, False, True, False])) - expected_interval_df = pd.DataFrame({ - 'start': [1.6 * np.pi, 0.0], - 'end': [2.0 * np.pi, 00.4 * np.pi] - }) - - # Sort both DataFrames by 'start' column before comparison - expected_interval_df_sorted = expected_interval_df.sort_values(by='start').reset_index(drop=True) - - # Converting the sorted DataFrames to NumPy arrays - actual_values_sorted = interval_df[['start', 'end']].to_numpy() - expected_values_sorted = expected_interval_df_sorted[['start', 'end']].to_numpy() - - # Asserting almost equal arrays - nt.assert_array_almost_equal(actual_values_sorted, expected_values_sorted, decimal=13) - - - def test_process_overlapped_intervals_overlap_and_gap(self): - # Test intervals data that has overlapping intervals and gap - intervals_data = [ - { - 'start': 0.0, - 'end': 100.0, - 'face_index': 0 - }, - { - 'start': 50.0, - 'end': 150.0, - 'face_index': 1 - }, - { - 'start': 140.0, - 'end': 150.0, - 'face_index': 2 - }, - { - 'start': 150.0, - 'end': 250.0, - 'face_index': 3 - }, - { - 'start': 260.0, - 'end': 350.0, - 'face_index': 4 - }, - ] - - df = pd.DataFrame(intervals_data) - df['interval'] = df.apply(lambda row: pd.Interval( - left=row['start'], right=row['end'], closed='both'), - axis=1) - df['interval'] = df['interval'].astype('interval[float64]') - - # Expected result - expected_overlap_contributions = np.array({ - 0: 75.0, - 1: 70.0, - 2: 5.0, - 3: 100.0, - 4: 90.0 - }) - overlap_contributions, total_length = _process_overlapped_intervals(df) - self.assertEqual(total_length, 340.0) - nt.assert_array_equal(overlap_contributions, - expected_overlap_contributions) - - - def test_process_overlapped_intervals_antimerdian(self): - intervals_data = [ - { - 'start': 350.0, - 'end': 360.0, - 'face_index': 0 - }, - { - 'start': 0.0, - 'end': 100.0, - 'face_index': 0 - }, - { - 'start': 100.0, - 'end': 150.0, - 'face_index': 1 - }, - { - 'start': 100.0, - 'end': 300.0, - 'face_index': 2 - }, - { - 'start': 310.0, - 'end': 360.0, - 'face_index': 3 - }, - ] - - df = pd.DataFrame(intervals_data) - df['interval'] = df.apply(lambda row: pd.Interval( - left=row['start'], right=row['end'], closed='both'), - axis=1) - df['interval'] = df['interval'].astype('interval[float64]') - - # Expected result - expected_overlap_contributions = np.array({ - 0: 105.0, - 1: 25.0, - 2: 175.0, - 3: 45.0 - }) - overlap_contributions, total_length = _process_overlapped_intervals(df) - self.assertEqual(total_length, 350.0) - nt.assert_array_equal(overlap_contributions, - expected_overlap_contributions) - - - def test_get_zonal_faces_weight_at_constLat_equator(self): - face_0 = [[1.7 * np.pi, 0.25 * np.pi], [1.7 * np.pi, 0.0], - [0.3 * np.pi, 0.0], [0.3 * np.pi, 0.25 * np.pi]] - face_1 = [[0.3 * np.pi, 0.0], [0.3 * np.pi, -0.25 * np.pi], - [0.6 * np.pi, -0.25 * np.pi], [0.6 * np.pi, 0.0]] - face_2 = [[0.3 * np.pi, 0.25 * np.pi], [0.3 * np.pi, 0.0], [np.pi, 0.0], - [np.pi, 0.25 * np.pi]] - face_3 = [[0.7 * np.pi, 0.0], [0.7 * np.pi, -0.25 * np.pi], - [np.pi, -0.25 * np.pi], [np.pi, 0.0]] - - # Convert the face vertices to xyz coordinates - face_0 = [_lonlat_rad_to_xyz(*v) for v in face_0] - face_1 = [_lonlat_rad_to_xyz(*v) for v in face_1] - face_2 = [_lonlat_rad_to_xyz(*v) for v in face_2] - face_3 = [_lonlat_rad_to_xyz(*v) for v in face_3] - - face_0_edge_nodes = np.array([[face_0[0], face_0[1]], - [face_0[1], face_0[2]], - [face_0[2], face_0[3]], - [face_0[3], face_0[0]]]) - face_1_edge_nodes = np.array([[face_1[0], face_1[1]], - [face_1[1], face_1[2]], - [face_1[2], face_1[3]], - [face_1[3], face_1[0]]]) - face_2_edge_nodes = np.array([[face_2[0], face_2[1]], - [face_2[1], face_2[2]], - [face_2[2], face_2[3]], - [face_2[3], face_2[0]]]) - face_3_edge_nodes = np.array([[face_3[0], face_3[1]], - [face_3[1], face_3[2]], - [face_3[2], face_3[3]], - [face_3[3], face_3[0]]]) - - face_0_latlon_bound = np.array([[0.0, 0.25 * np.pi], - [1.7 * np.pi, 0.3 * np.pi]]) - face_1_latlon_bound = np.array([[-0.25 * np.pi, 0.0], - [0.3 * np.pi, 0.6 * np.pi]]) - face_2_latlon_bound = np.array([[0.0, 0.25 * np.pi], - [0.3 * np.pi, np.pi]]) - face_3_latlon_bound = np.array([[-0.25 * np.pi, 0.0], - [0.7 * np.pi, np.pi]]) - - latlon_bounds = np.array([ - face_0_latlon_bound, face_1_latlon_bound, face_2_latlon_bound, - face_3_latlon_bound - ]) - - expected_weight_df = pd.DataFrame({ - 'face_index': [0, 1, 2, 3], - 'weight': [0.46153, 0.11538, 0.30769, 0.11538] - }) - - # Assert the results is the same to the 3 decimal places - weight_df = _get_zonal_faces_weight_at_constLat(np.array([ - face_0_edge_nodes, face_1_edge_nodes, face_2_edge_nodes, - face_3_edge_nodes - ]), 0.0, latlon_bounds) - - nt.assert_array_almost_equal(weight_df, expected_weight_df, decimal=3) - - - def test_get_zonal_faces_weight_at_constLat_regular(self): - face_0 = [[1.7 * np.pi, 0.25 * np.pi], [1.7 * np.pi, 0.0], - [0.3 * np.pi, 0.0], [0.3 * np.pi, 0.25 * np.pi]] - face_1 = [[0.4 * np.pi, 0.3 * np.pi], [0.4 * np.pi, 0.0], - [0.5 * np.pi, 0.0], [0.5 * np.pi, 0.3 * np.pi]] - face_2 = [[0.5 * np.pi, 0.25 * np.pi], [0.5 * np.pi, 0.0], [np.pi, 0.0], - [np.pi, 0.25 * np.pi]] - face_3 = [[1.2 * np.pi, 0.25 * np.pi], [1.2 * np.pi, 0.0], - [1.6 * np.pi, -0.01 * np.pi], [1.6 * np.pi, 0.25 * np.pi]] - - # Convert the face vertices to xyz coordinates - face_0 = [_lonlat_rad_to_xyz(*v) for v in face_0] - face_1 = [_lonlat_rad_to_xyz(*v) for v in face_1] - face_2 = [_lonlat_rad_to_xyz(*v) for v in face_2] - face_3 = [_lonlat_rad_to_xyz(*v) for v in face_3] - - face_0_edge_nodes = np.array([[face_0[0], face_0[1]], - [face_0[1], face_0[2]], - [face_0[2], face_0[3]], - [face_0[3], face_0[0]]]) - face_1_edge_nodes = np.array([[face_1[0], face_1[1]], - [face_1[1], face_1[2]], - [face_1[2], face_1[3]], - [face_1[3], face_1[0]]]) - face_2_edge_nodes = np.array([[face_2[0], face_2[1]], - [face_2[1], face_2[2]], - [face_2[2], face_2[3]], - [face_2[3], face_2[0]]]) - face_3_edge_nodes = np.array([[face_3[0], face_3[1]], - [face_3[1], face_3[2]], - [face_3[2], face_3[3]], - [face_3[3], face_3[0]]]) - - face_0_latlon_bound = np.array([[0.0, 0.25 * np.pi], - [1.7 * np.pi, 0.3 * np.pi]]) - face_1_latlon_bound = np.array([[0, 0.3 * np.pi], - [0.4 * np.pi, 0.5 * np.pi]]) - face_2_latlon_bound = np.array([[0.0, 0.25 * np.pi], - [0.5 * np.pi, np.pi]]) - face_3_latlon_bound = np.array([[-0.01 * np.pi, 0.25 * np.pi], - [1.2 * np.pi, 1.6 * np.pi]]) - - latlon_bounds = np.array([ - face_0_latlon_bound, face_1_latlon_bound, face_2_latlon_bound, - face_3_latlon_bound - ]) - - expected_weight_df = pd.DataFrame({ - 'face_index': [0, 1, 2, 3], - 'weight': [0.375, 0.0625, 0.3125, 0.25] - }) - - # Assert the results is the same to the 3 decimal places - weight_df = _get_zonal_faces_weight_at_constLat(np.array([ - face_0_edge_nodes, face_1_edge_nodes, face_2_edge_nodes, - face_3_edge_nodes - ]), np.sin(0.1 * np.pi), latlon_bounds) - - nt.assert_array_almost_equal(weight_df, expected_weight_df, decimal=3) - - def test_get_zonal_faces_weight_at_constLat_on_pole_one_face(self): - #The face is touching the pole, so the weight should be 1.0 since there's only 1 face - face_edges_cart = np.array([[ - [[-5.22644277e-02, -5.22644277e-02, -9.97264689e-01], - [-5.23359562e-02, -6.40930613e-18, -9.98629535e-01]], - - [[-5.23359562e-02, -6.40930613e-18, -9.98629535e-01], - [6.12323400e-17, 0.00000000e+00, -1.00000000e+00]], - - [[6.12323400e-17, 0.00000000e+00, -1.00000000e+00], - [3.20465306e-18, -5.23359562e-02, -9.98629535e-01]], - - [[3.20465306e-18, -5.23359562e-02, -9.98629535e-01], - [-5.22644277e-02, -5.22644277e-02, -9.97264689e-01]] - ]]) - - # Corrected face_bounds - face_bounds = np.array([ - [-1.57079633, -1.4968158], - [3.14159265, 0.] - ]) - constLat_cart = -1 - - weight_df = _get_zonal_faces_weight_at_constLat(face_edges_cart, constLat_cart, face_bounds) - # Define the expected DataFrame - expected_weight_df = pd.DataFrame({"face_index": [0], "weight": [1.0]}) - - # Assert that the resulting should have weight is 1.0 - pd.testing.assert_frame_equal(weight_df, expected_weight_df) - - - def test_get_zonal_faces_weight_at_constLat_on_pole_faces(self): - #there will be 4 faces touching the pole, so the weight should be 0.25 for each face - face_edges_cart = np.array([ - [ - [[5.22644277e-02, -5.22644277e-02, 9.97264689e-01], [5.23359562e-02, 0.00000000e+00, 9.98629535e-01]], - [[5.23359562e-02, 0.00000000e+00, 9.98629535e-01], [6.12323400e-17, 0.00000000e+00, 1.00000000e+00]], - [[6.12323400e-17, 0.00000000e+00, 1.00000000e+00], [3.20465306e-18, -5.23359562e-02, 9.98629535e-01]], - [[3.20465306e-18, -5.23359562e-02, 9.98629535e-01], [5.22644277e-02, -5.22644277e-02, 9.97264689e-01]] - ], - [ - [[5.23359562e-02, 0.00000000e+00, 9.98629535e-01], [5.22644277e-02, 5.22644277e-02, 9.97264689e-01]], - [[5.22644277e-02, 5.22644277e-02, 9.97264689e-01], [3.20465306e-18, 5.23359562e-02, 9.98629535e-01]], - [[3.20465306e-18, 5.23359562e-02, 9.98629535e-01], [6.12323400e-17, 0.00000000e+00, 1.00000000e+00]], - [[6.12323400e-17, 0.00000000e+00, 1.00000000e+00], [5.23359562e-02, 0.00000000e+00, 9.98629535e-01]] - ], - [ - [[3.20465306e-18, -5.23359562e-02, 9.98629535e-01], [6.12323400e-17, 0.00000000e+00, 1.00000000e+00]], - [[6.12323400e-17, 0.00000000e+00, 1.00000000e+00], [-5.23359562e-02, -6.40930613e-18, 9.98629535e-01]], - [[-5.23359562e-02, -6.40930613e-18, 9.98629535e-01], - [-5.22644277e-02, -5.22644277e-02, 9.97264689e-01]], - [[-5.22644277e-02, -5.22644277e-02, 9.97264689e-01], [3.20465306e-18, -5.23359562e-02, 9.98629535e-01]] - ], - [ - [[6.12323400e-17, 0.00000000e+00, 1.00000000e+00], [3.20465306e-18, 5.23359562e-02, 9.98629535e-01]], - [[3.20465306e-18, 5.23359562e-02, 9.98629535e-01], [-5.22644277e-02, 5.22644277e-02, 9.97264689e-01]], - [[-5.22644277e-02, 5.22644277e-02, 9.97264689e-01], [-5.23359562e-02, -6.40930613e-18, 9.98629535e-01]], - [[-5.23359562e-02, -6.40930613e-18, 9.98629535e-01], [6.12323400e-17, 0.00000000e+00, 1.00000000e+00]] - ] - ]) - - face_bounds = np.array([ - [[1.4968158, 1.57079633], [4.71238898, 0.0]], - [[1.4968158, 1.57079633], [0.0, 1.57079633]], - [[1.4968158, 1.57079633], [3.14159265, 0.0]], - [[1.4968158, 1.57079633], [0.0, 3.14159265]] - ]) - - constLat_cart = 1.0 - - weight_df = _get_zonal_faces_weight_at_constLat(face_edges_cart, constLat_cart, face_bounds) - # Define the expected DataFrame - expected_weight_df = pd.DataFrame({ - 'face_index': [0, 1, 2, 3], - 'weight': [0.25, 0.25, 0.25, 0.25] - }) - - # Assert that the DataFrame matches the expected DataFrame - pd.testing.assert_frame_equal(weight_df, expected_weight_df) - - - def test_get_zonal_face_interval_pole(self): - #The face is touching the pole - face_edges_cart = np.array([ - [[-5.22644277e-02, -5.22644277e-02, -9.97264689e-01], - [-5.23359562e-02, -6.40930613e-18, -9.98629535e-01]], - - [[-5.23359562e-02, -6.40930613e-18, -9.98629535e-01], - [6.12323400e-17, 0.00000000e+00, -1.00000000e+00]], - - [[6.12323400e-17, 0.00000000e+00, -1.00000000e+00], - [3.20465306e-18, -5.23359562e-02, -9.98629535e-01]], - - [[3.20465306e-18, -5.23359562e-02, -9.98629535e-01], - [-5.22644277e-02, -5.22644277e-02, -9.97264689e-01]] - ]) - - # Corrected face_bounds - face_bounds = np.array([ - [-1.57079633, -1.4968158], - [3.14159265, 0.] - ]) - constLat_cart = -0.9986295347545738 - - weight_df = _get_zonal_face_interval(face_edges_cart, constLat_cart, face_bounds) - # No Nan values should be present in the weight_df - self.assertFalse(weight_df.isnull().values.any()) - - - def test_get_zonal_faces_weight_at_constLat_latlonface(self): - face_0 = [[np.deg2rad(350), np.deg2rad(40)], [np.deg2rad(350), np.deg2rad(20)], +gridfile_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" +dsfile_var2_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30_var2.nc" + +def test_single_dim(): + """Integral with 1D data mapped to each face.""" + uxgrid = ux.open_grid(gridfile_ne30) + test_data = np.ones(uxgrid.n_face) + dims = {"n_face": uxgrid.n_face} + uxda = ux.UxDataArray(data=test_data, dims=dims, uxgrid=uxgrid, name='var2') + integral = uxda.integrate() + assert integral.ndim == len(dims) - 1 + nt.assert_almost_equal(integral, 4 * np.pi) + +def test_multi_dim(): + """Integral with 3D data mapped to each face.""" + uxgrid = ux.open_grid(gridfile_ne30) + test_data = np.ones((5, 5, uxgrid.n_face)) + dims = {"a": 5, "b": 5, "n_face": uxgrid.n_face} + uxda = ux.UxDataArray(data=test_data, dims=dims, uxgrid=uxgrid, name='var2') + integral = uxda.integrate() + assert integral.ndim == len(dims) - 1 + nt.assert_almost_equal(integral, np.ones((5, 5)) * 4 * np.pi) + +def test_get_faces_constLat_intersection_info_one_intersection(): + face_edges_cart = np.array([ + [[-5.4411371445381629e-01, -4.3910468172333759e-02, -8.3786164521844386e-01], + [-5.4463903501502697e-01, -6.6699045092185599e-17, -8.3867056794542405e-01]], + + [[-5.4463903501502697e-01, -6.6699045092185599e-17, -8.3867056794542405e-01], + [-4.9999999999999994e-01, -6.1232339957367648e-17, -8.6602540378443871e-01]], + + [[-4.9999999999999994e-01, -6.1232339957367648e-17, -8.6602540378443871e-01], + [-4.9948581138450826e-01, -4.5339793804534498e-02, -8.6513480297773349e-01]], + + [[-4.9948581138450826e-01, -4.5339793804534498e-02, -8.6513480297773349e-01], + [-5.4411371445381629e-01, -4.3910468172333759e-02, -8.3786164521844386e-01]] + ]) + + latitude_cart = -0.8660254037844386 + is_latlonface = False + is_GCA_list = None + unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface) + assert len(unique_intersections) == 1 + +def test_get_faces_constLat_intersection_info_encompass_pole(): + face_edges_cart = np.array([ + [[0.03982285692494229, 0.00351700770436231, 0.9992005658140627], + [0.00896106681877875, 0.03896060263227105, 0.9992005658144913]], + + [[0.00896106681877875, 0.03896060263227105, 0.9992005658144913], + [-0.03428461218295055, 0.02056197086916728, 0.9992005658132106]], + + [[-0.03428461218295055, 0.02056197086916728, 0.9992005658132106], + [-0.03015012448894485, -0.02625260499902213, 0.9992005658145248]], + + [[-0.03015012448894485, -0.02625260499902213, 0.9992005658145248], + [0.01565081128889155, -0.03678697293262131, 0.9992005658167203]], + + [[0.01565081128889155, -0.03678697293262131, 0.9992005658167203], + [0.03982285692494229, 0.00351700770436231, 0.9992005658140627]] + ]) + + latitude_cart = 0.9993908270190958 + latitude_rad = np.arcsin(latitude_cart) + latitude_deg = np.rad2deg(latitude_rad) + print(latitude_deg) + + is_latlonface = False + is_GCA_list = None + unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface) + assert len(unique_intersections) <= 2 * len(face_edges_cart) + +def test_get_faces_constLat_intersection_info_on_pole(): + face_edges_cart = np.array([ + [[-5.2264427688714095e-02, -5.2264427688714102e-02, -9.9726468863423734e-01], + [-5.2335956242942412e-02, -6.4093061293235361e-18, -9.9862953475457394e-01]], + + [[-5.2335956242942412e-02, -6.4093061293235361e-18, -9.9862953475457394e-01], + [6.1232339957367660e-17, 0.0000000000000000e+00, -1.0000000000000000e+00]], + + [[6.1232339957367660e-17, 0.0000000000000000e+00, -1.0000000000000000e+00], + [3.2046530646617680e-18, -5.2335956242942412e-02, -9.9862953475457394e-01]], + + [[3.2046530646617680e-18, -5.2335956242942412e-02, -9.9862953475457394e-01], + [-5.2264427688714095e-02, -5.2264427688714102e-02, -9.9726468863423734e-01]] + ]) + latitude_cart = -0.9998476951563913 + is_latlonface = False + is_GCA_list = None + unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface) + assert len(unique_intersections) == 2 + +def test_get_faces_constLat_intersection_info_near_pole(): + face_edges_cart = np.array([ + [[-5.1693346290592648e-02, 1.5622531297347531e-01, -9.8636780641686628e-01], + [-5.1195320928843470e-02, 2.0763904784932552e-01, -9.7686491641537532e-01]], + [[-5.1195320928843470e-02, 2.0763904784932552e-01, -9.7686491641537532e-01], + [1.2730919333264125e-17, 2.0791169081775882e-01, -9.7814760073380580e-01]], + [[1.2730919333264125e-17, 2.0791169081775882e-01, -9.7814760073380580e-01], + [9.5788483443923397e-18, 1.5643446504023048e-01, -9.8768834059513777e-01]], + [[9.5788483443923397e-18, 1.5643446504023048e-01, -9.8768834059513777e-01], + [-5.1693346290592648e-02, 1.5622531297347531e-01, -9.8636780641686628e-01]] + ]) + + latitude_cart = -0.9876883405951378 + latitude_rad = np.arcsin(latitude_cart) + latitude_deg = np.rad2deg(latitude_rad) + is_latlonface = False + is_GCA_list = None + unique_intersections, pt_lon_min, pt_lon_max = _get_faces_constLat_intersection_info(face_edges_cart, latitude_cart, is_GCA_list, is_latlonface) + assert len(unique_intersections) == 1 + +def test_get_zonal_face_interval(): + """Test the _get_zonal_face_interval function for correct interval computation.""" + vertices_lonlat = [[1.6 * np.pi, 0.25 * np.pi], + [1.6 * np.pi, -0.25 * np.pi], + [0.4 * np.pi, -0.25 * np.pi], + [0.4 * np.pi, 0.25 * np.pi]] + vertices = [_lonlat_rad_to_xyz(*v) for v in vertices_lonlat] + + face_edge_nodes = np.array([[vertices[0], vertices[1]], + [vertices[1], vertices[2]], + [vertices[2], vertices[3]], + [vertices[3], vertices[0]]]) + + constZ = np.sin(0.20) + interval_df = _get_zonal_face_interval(face_edge_nodes, constZ, + np.array([[-0.25 * np.pi, 0.25 * np.pi], [1.6 * np.pi, + 0.4 * np.pi]])) + expected_interval_df = pd.DataFrame({ + 'start': [1.6 * np.pi, 0.0], + 'end': [2.0 * np.pi, 0.4 * np.pi] + }) + expected_interval_df_sorted = expected_interval_df.sort_values(by='start').reset_index(drop=True) + + actual_values_sorted = interval_df[['start', 'end']].to_numpy() + expected_values_sorted = expected_interval_df_sorted[['start', 'end']].to_numpy() + + nt.assert_array_almost_equal(actual_values_sorted, expected_values_sorted, decimal=13) + +def test_get_zonal_face_interval_empty_interval(): + """Test the _get_zonal_face_interval function for cases where the interval is empty.""" + face_edges_cart = np.array([ + [[-5.4411371445381629e-01, -4.3910468172333759e-02, -8.3786164521844386e-01], + [-5.4463903501502697e-01, -6.6699045092185599e-17, -8.3867056794542405e-01]], + + [[-5.4463903501502697e-01, -6.6699045092185599e-17, -8.3867056794542405e-01], + [-4.9999999999999994e-01, -6.1232339957367648e-17, -8.6602540378443871e-01]], + + [[-4.9999999999999994e-01, -6.1232339957367648e-17, -8.6602540378443871e-01], + [-4.9948581138450826e-01, -4.5339793804534498e-02, -8.6513480297773349e-01]], + + [[-4.9948581138450826e-01, -4.5339793804534498e-02, -8.6513480297773349e-01], + [-5.4411371445381629e-01, -4.3910468172333759e-02, -8.3786164521844386e-01]] + ]) + + latitude_cart = -0.8660254037844386 + face_latlon_bounds = np.array([ + [-1.04719755, -0.99335412], + [3.14159265, 3.2321175] + ]) + + res = _get_zonal_face_interval(face_edges_cart, latitude_cart, face_latlon_bounds) + expected_res = pd.DataFrame({"start": [0.0], "end": [0.0]}) + pd.testing.assert_frame_equal(res, expected_res) + +def test_get_zonal_face_interval_encompass_pole(): + """Test the _get_zonal_face_interval function for cases where the face encompasses the pole inside.""" + face_edges_cart = np.array([ + [[0.03982285692494229, 0.00351700770436231, 0.9992005658140627], + [0.00896106681877875, 0.03896060263227105, 0.9992005658144913]], + + [[0.00896106681877875, 0.03896060263227105, 0.9992005658144913], + [-0.03428461218295055, 0.02056197086916728, 0.9992005658132106]], + + [[-0.03428461218295055, 0.02056197086916728, 0.9992005658132106], + [-0.03015012448894485, -0.02625260499902213, 0.9992005658145248]], + + [[-0.03015012448894485, -0.02625260499902213, 0.9992005658145248], + [0.01565081128889155, -0.03678697293262131, 0.9992005658167203]], + + [[0.01565081128889155, -0.03678697293262131, 0.9992005658167203], + [0.03982285692494229, 0.00351700770436231, 0.9992005658140627]] + ]) + + latitude_cart = 0.9993908270190958 + + face_latlon_bounds = np.array([ + [np.arcsin(0.9992005658145248), 0.5 * np.pi], + [0, 2 * np.pi] + ]) + expected_df = pd.DataFrame({ + 'start': [0.000000, 1.101091, 2.357728, 3.614365, 4.871002, 6.127640], + 'end': [0.331721, 1.588358, 2.844995, 4.101632, 5.358270, 6.283185] + }) + + res = _get_zonal_face_interval(face_edges_cart, latitude_cart, face_latlon_bounds) + + pd.testing.assert_frame_equal(res, expected_df) + +def test_get_zonal_face_interval_FILL_VALUE(): + """Test the _get_zonal_face_interval function for cases where there are dummy nodes.""" + dummy_node = [INT_FILL_VALUE, INT_FILL_VALUE, INT_FILL_VALUE] + vertices_lonlat = [[1.6 * np.pi, 0.25 * np.pi], + [1.6 * np.pi, -0.25 * np.pi], + [0.4 * np.pi, -0.25 * np.pi], + [0.4 * np.pi, 0.25 * np.pi]] + vertices = [_lonlat_rad_to_xyz(*v) for v in vertices_lonlat] + + face_edge_nodes = np.array([[vertices[0], vertices[1]], + [vertices[1], vertices[2]], + [vertices[2], vertices[3]], + [vertices[3], vertices[0]], + [dummy_node, dummy_node]]) + + constZ = np.sin(0.20) + interval_df = _get_zonal_face_interval(face_edge_nodes, constZ, + np.array([[-0.25 * np.pi, 0.25 * np.pi], [1.6 * np.pi, + 0.4 * np.pi]])) + expected_interval_df = pd.DataFrame({ + 'start': [1.6 * np.pi, 0.0], + 'end': [2.0 * np.pi, 0.4 * np.pi] + }) + expected_interval_df_sorted = expected_interval_df.sort_values(by='start').reset_index(drop=True) + + actual_values_sorted = interval_df[['start', 'end']].to_numpy() + expected_values_sorted = expected_interval_df_sorted[['start', 'end']].to_numpy() + + nt.assert_array_almost_equal(actual_values_sorted, expected_values_sorted, decimal=13) + +def test_get_zonal_face_interval_GCA_constLat(): + vertices_lonlat = [[-0.4 * np.pi, 0.25 * np.pi], + [-0.4 * np.pi, -0.25 * np.pi], + [0.4 * np.pi, -0.25 * np.pi], + [0.4 * np.pi, 0.25 * np.pi]] + + vertices = [_lonlat_rad_to_xyz(*v) for v in vertices_lonlat] + + face_edge_nodes = np.array([[vertices[0], vertices[1]], + [vertices[1], vertices[2]], + [vertices[2], vertices[3]], + [vertices[3], vertices[0]]]) + + constZ = np.sin(0.20 * np.pi) + interval_df = _get_zonal_face_interval(face_edge_nodes, constZ, + np.array([[-0.25 * np.pi, 0.25 * np.pi], [1.6 * np.pi, + 0.4 * np.pi]]), + is_GCA_list=np.array([True, False, True, False])) + expected_interval_df = pd.DataFrame({ + 'start': [1.6 * np.pi, 0.0], + 'end': [2.0 * np.pi, 0.4 * np.pi] + }) + expected_interval_df_sorted = expected_interval_df.sort_values(by='start').reset_index(drop=True) + + actual_values_sorted = interval_df[['start', 'end']].to_numpy() + expected_values_sorted = expected_interval_df_sorted[['start', 'end']].to_numpy() + + nt.assert_array_almost_equal(actual_values_sorted, expected_values_sorted, decimal=13) + +def test_get_zonal_face_interval_equator(): + """Test that the face interval is correctly computed when the latitude + is at the equator.""" + vertices_lonlat = [[-0.4 * np.pi, 0.25 * np.pi], [-0.4 * np.pi, 0.0], + [0.4 * np.pi, 0.0], [0.4 * np.pi, 0.25 * np.pi]] + + vertices = [_lonlat_rad_to_xyz(*v) for v in vertices_lonlat] + + face_edge_nodes = np.array([[vertices[0], vertices[1]], + [vertices[1], vertices[2]], + [vertices[2], vertices[3]], + [vertices[3], vertices[0]]]) + + interval_df = _get_zonal_face_interval(face_edge_nodes, 0.0, + np.array([[-0.25 * np.pi, 0.25 * np.pi], [1.6 * np.pi, + 0.4 * np.pi]]), + is_GCA_list=np.array([True, True, True, True])) + expected_interval_df = pd.DataFrame({ + 'start': [1.6 * np.pi, 0.0], + 'end': [2.0 * np.pi, 00.4 * np.pi] + }) + # Sort both DataFrames by 'start' column before comparison + expected_interval_df_sorted = expected_interval_df.sort_values(by='start').reset_index(drop=True) + + # Converting the sorted DataFrames to NumPy arrays + actual_values_sorted = interval_df[['start', 'end']].to_numpy() + expected_values_sorted = expected_interval_df_sorted[['start', 'end']].to_numpy() + + # Asserting almost equal arrays + nt.assert_array_almost_equal(actual_values_sorted, expected_values_sorted, decimal=13) + + # Even if we change the is_GCA_list to False, the result should be the same + interval_df = _get_zonal_face_interval(face_edge_nodes, 0.0, + np.array([[-0.25 * np.pi, 0.25 * np.pi], [1.6 * np.pi, + 0.4 * np.pi]]), + is_GCA_list=np.array([True, False, True, False])) + expected_interval_df = pd.DataFrame({ + 'start': [1.6 * np.pi, 0.0], + 'end': [2.0 * np.pi, 00.4 * np.pi] + }) + + # Sort both DataFrames by 'start' column before comparison + expected_interval_df_sorted = expected_interval_df.sort_values(by='start').reset_index(drop=True) + + # Converting the sorted DataFrames to NumPy arrays + actual_values_sorted = interval_df[['start', 'end']].to_numpy() + expected_values_sorted = expected_interval_df_sorted[['start', 'end']].to_numpy() + + # Asserting almost equal arrays + nt.assert_array_almost_equal(actual_values_sorted, expected_values_sorted, decimal=10) + +def test_process_overlapped_intervals_overlap_and_gap(): + intervals_data = [ + { + 'start': 0.0, + 'end': 100.0, + 'face_index': 0 + }, + { + 'start': 50.0, + 'end': 150.0, + 'face_index': 1 + }, + { + 'start': 140.0, + 'end': 150.0, + 'face_index': 2 + }, + { + 'start': 150.0, + 'end': 250.0, + 'face_index': 3 + }, + { + 'start': 260.0, + 'end': 350.0, + 'face_index': 4 + }, + ] + + df = pd.DataFrame(intervals_data) + df['interval'] = df.apply(lambda row: pd.Interval( + left=row['start'], right=row['end'], closed='both'), + axis=1) + df['interval'] = df['interval'].astype('interval[float64]') + + expected_overlap_contributions = np.array({ + 0: 75.0, + 1: 70.0, + 2: 5.0, + 3: 100.0, + 4: 90.0 + }) + overlap_contributions, total_length = _process_overlapped_intervals(df) + assert total_length == 340.0 + nt.assert_array_equal(overlap_contributions, expected_overlap_contributions) + +def test_process_overlapped_intervals_antimeridian(): + intervals_data = [ + { + 'start': 350.0, + 'end': 360.0, + 'face_index': 0 + }, + { + 'start': 0.0, + 'end': 100.0, + 'face_index': 0 + }, + { + 'start': 100.0, + 'end': 150.0, + 'face_index': 1 + }, + { + 'start': 100.0, + 'end': 300.0, + 'face_index': 2 + }, + { + 'start': 310.0, + 'end': 360.0, + 'face_index': 3 + }, + ] + + df = pd.DataFrame(intervals_data) + df['interval'] = df.apply(lambda row: pd.Interval( + left=row['start'], right=row['end'], closed='both'), + axis=1) + df['interval'] = df['interval'].astype('interval[float64]') + + expected_overlap_contributions = np.array({ + 0: 105.0, + 1: 25.0, + 2: 175.0, + 3: 45.0 + }) + overlap_contributions, total_length = _process_overlapped_intervals(df) + assert total_length == 350.0 + nt.assert_array_equal(overlap_contributions, expected_overlap_contributions) + +def test_get_zonal_faces_weight_at_constLat_equator(): + face_0 = [[np.deg2rad(350), np.deg2rad(40)], [np.deg2rad(350), np.deg2rad(20)], [np.deg2rad(10), np.deg2rad(20)], [np.deg2rad(10), np.deg2rad(40)]] - face_1 = [[np.deg2rad(5), np.deg2rad(20)], [np.deg2rad(5), np.deg2rad(10)], - [np.deg2rad(25), np.deg2rad(10)], [np.deg2rad(25), np.deg2rad(20)]] - face_2 = [[np.deg2rad(30), np.deg2rad(40)], [np.deg2rad(30), np.deg2rad(20)], - [np.deg2rad(40), np.deg2rad(20)], [np.deg2rad(40), np.deg2rad(40)]] - - # Convert the face vertices to xyz coordinates - face_0 = [_lonlat_rad_to_xyz(*v) for v in face_0] - face_1 = [_lonlat_rad_to_xyz(*v) for v in face_1] - face_2 = [_lonlat_rad_to_xyz(*v) for v in face_2] - - - face_0_edge_nodes = np.array([[face_0[0], face_0[1]], - [face_0[1], face_0[2]], - [face_0[2], face_0[3]], - [face_0[3], face_0[0]]]) - face_1_edge_nodes = np.array([[face_1[0], face_1[1]], - [face_1[1], face_1[2]], - [face_1[2], face_1[3]], - [face_1[3], face_1[0]]]) - face_2_edge_nodes = np.array([[face_2[0], face_2[1]], - [face_2[1], face_2[2]], - [face_2[2], face_2[3]], - [face_2[3], face_2[0]]]) - - face_0_latlon_bound = np.array([[np.deg2rad(20), np.deg2rad(40)], - [np.deg2rad(350), np.deg2rad(10)]]) - face_1_latlon_bound = np.array([[np.deg2rad(10), np.deg2rad(20)], - [np.deg2rad(5), np.deg2rad(25)]]) - face_2_latlon_bound = np.array([[np.deg2rad(20), np.deg2rad(40)], - [np.deg2rad(30), np.deg2rad(40)]]) - - - latlon_bounds = np.array([ - face_0_latlon_bound, face_1_latlon_bound, face_2_latlon_bound - ]) - - sum = 17.5 + 17.5 + 10 - expected_weight_df = pd.DataFrame({ - 'face_index': [0, 1, 2], - 'weight': [17.5 / sum, 17.5/sum, 10/sum] - }) - - # Assert the results is the same to the 3 decimal places - weight_df = _get_zonal_faces_weight_at_constLat(np.array([ + face_1 = [[np.deg2rad(5), np.deg2rad(20)], [np.deg2rad(5), np.deg2rad(10)], + [np.deg2rad(25), np.deg2rad(10)], [np.deg2rad(25), np.deg2rad(20)]] + face_2 = [[np.deg2rad(30), np.deg2rad(40)], [np.deg2rad(30), np.deg2rad(20)], + [np.deg2rad(40), np.deg2rad(20)], [np.deg2rad(40), np.deg2rad(40)]] + + # Convert the face vertices to xyz coordinates + face_0 = [_lonlat_rad_to_xyz(*v) for v in face_0] + face_1 = [_lonlat_rad_to_xyz(*v) for v in face_1] + face_2 = [_lonlat_rad_to_xyz(*v) for v in face_2] + + + face_0_edge_nodes = np.array([[face_0[0], face_0[1]], + [face_0[1], face_0[2]], + [face_0[2], face_0[3]], + [face_0[3], face_0[0]]]) + face_1_edge_nodes = np.array([[face_1[0], face_1[1]], + [face_1[1], face_1[2]], + [face_1[2], face_1[3]], + [face_1[3], face_1[0]]]) + face_2_edge_nodes = np.array([[face_2[0], face_2[1]], + [face_2[1], face_2[2]], + [face_2[2], face_2[3]], + [face_2[3], face_2[0]]]) + + face_0_latlon_bound = np.array([[np.deg2rad(20), np.deg2rad(40)], + [np.deg2rad(350), np.deg2rad(10)]]) + face_1_latlon_bound = np.array([[np.deg2rad(10), np.deg2rad(20)], + [np.deg2rad(5), np.deg2rad(25)]]) + face_2_latlon_bound = np.array([[np.deg2rad(20), np.deg2rad(40)], + [np.deg2rad(30), np.deg2rad(40)]]) + + + latlon_bounds = np.array([ + face_0_latlon_bound, face_1_latlon_bound, face_2_latlon_bound + ]) + + sum = 17.5 + 17.5 + 10 + expected_weight_df = pd.DataFrame({ + 'face_index': [0, 1, 2], + 'weight': [17.5 / sum, 17.5/sum, 10/sum] + }) + + # Assert the results is the same to the 3 decimal places + weight_df = _get_zonal_faces_weight_at_constLat(np.array([ + face_0_edge_nodes, face_1_edge_nodes, face_2_edge_nodes + ]), np.sin(np.deg2rad(20)), latlon_bounds, is_latlonface=True) + + + nt.assert_array_almost_equal(weight_df, expected_weight_df, decimal=3) + + + + # A error will be raise if we don't set is_latlonface=True since the face_2 will be concave if + # It's edges are all GCA + with pytest.raises(ValueError): + _get_zonal_faces_weight_at_constLat(np.array([ face_0_edge_nodes, face_1_edge_nodes, face_2_edge_nodes - ]), np.sin(np.deg2rad(20)), latlon_bounds, is_latlonface=True) - - - nt.assert_array_almost_equal(weight_df, expected_weight_df, decimal=3) - - - - # A error will be raise if we don't set is_latlonface=True since the face_2 will be concave if - # It's edges are all GCA - with self.assertRaises(ValueError): - _get_zonal_faces_weight_at_constLat(np.array([ - face_0_edge_nodes, face_1_edge_nodes, face_2_edge_nodes - ]), np.deg2rad(20), latlon_bounds) + ]), np.deg2rad(20), latlon_bounds) + + +def test_get_zonal_faces_weight_at_constLat_regular(): + face_0 = [[1.7 * np.pi, 0.25 * np.pi], [1.7 * np.pi, 0.0], + [0.3 * np.pi, 0.0], [0.3 * np.pi, 0.25 * np.pi]] + face_1 = [[0.4 * np.pi, 0.3 * np.pi], [0.4 * np.pi, 0.0], + [0.5 * np.pi, 0.0], [0.5 * np.pi, 0.3 * np.pi]] + face_2 = [[0.5 * np.pi, 0.25 * np.pi], [0.5 * np.pi, 0.0], [np.pi, 0.0], + [np.pi, 0.25 * np.pi]] + face_3 = [[1.2 * np.pi, 0.25 * np.pi], [1.2 * np.pi, 0.0], + [1.6 * np.pi, -0.01 * np.pi], [1.6 * np.pi, 0.25 * np.pi]] + + # Convert the face vertices to xyz coordinates + face_0 = [_lonlat_rad_to_xyz(*v) for v in face_0] + face_1 = [_lonlat_rad_to_xyz(*v) for v in face_1] + face_2 = [_lonlat_rad_to_xyz(*v) for v in face_2] + face_3 = [_lonlat_rad_to_xyz(*v) for v in face_3] + + face_0_edge_nodes = np.array([[face_0[0], face_0[1]], + [face_0[1], face_0[2]], + [face_0[2], face_0[3]], + [face_0[3], face_0[0]]]) + face_1_edge_nodes = np.array([[face_1[0], face_1[1]], + [face_1[1], face_1[2]], + [face_1[2], face_1[3]], + [face_1[3], face_1[0]]]) + face_2_edge_nodes = np.array([[face_2[0], face_2[1]], + [face_2[1], face_2[2]], + [face_2[2], face_2[3]], + [face_2[3], face_2[0]]]) + face_3_edge_nodes = np.array([[face_3[0], face_3[1]], + [face_3[1], face_3[2]], + [face_3[2], face_3[3]], + [face_3[3], face_3[0]]]) + + face_0_latlon_bound = np.array([[0.0, 0.25 * np.pi], + [1.7 * np.pi, 0.3 * np.pi]]) + face_1_latlon_bound = np.array([[0, 0.3 * np.pi], + [0.4 * np.pi, 0.5 * np.pi]]) + face_2_latlon_bound = np.array([[0.0, 0.25 * np.pi], + [0.5 * np.pi, np.pi]]) + face_3_latlon_bound = np.array([[-0.01 * np.pi, 0.25 * np.pi], + [1.2 * np.pi, 1.6 * np.pi]]) + + latlon_bounds = np.array([ + face_0_latlon_bound, face_1_latlon_bound, face_2_latlon_bound, + face_3_latlon_bound + ]) + + expected_weight_df = pd.DataFrame({ + 'face_index': [0, 1, 2, 3], + 'weight': [0.375, 0.0625, 0.3125, 0.25] + }) + + # Assert the results is the same to the 3 decimal places + weight_df = _get_zonal_faces_weight_at_constLat(np.array([ + face_0_edge_nodes, face_1_edge_nodes, face_2_edge_nodes, + face_3_edge_nodes + ]), np.sin(0.1 * np.pi), latlon_bounds) + + nt.assert_array_almost_equal(weight_df, expected_weight_df, decimal=3) + +def test_get_zonal_faces_weight_at_constLat_on_pole_one_face(): + #The face is touching the pole, so the weight should be 1.0 since there's only 1 face + face_edges_cart = np.array([[ + [[-5.22644277e-02, -5.22644277e-02, -9.97264689e-01], + [-5.23359562e-02, -6.40930613e-18, -9.98629535e-01]], + + [[-5.23359562e-02, -6.40930613e-18, -9.98629535e-01], + [6.12323400e-17, 0.00000000e+00, -1.00000000e+00]], + + [[6.12323400e-17, 0.00000000e+00, -1.00000000e+00], + [3.20465306e-18, -5.23359562e-02, -9.98629535e-01]], + + [[3.20465306e-18, -5.23359562e-02, -9.98629535e-01], + [-5.22644277e-02, -5.22644277e-02, -9.97264689e-01]] + ]]) + + # Corrected face_bounds + face_bounds = np.array([ + [-1.57079633, -1.4968158], + [3.14159265, 0.] + ]) + constLat_cart = -1 + + weight_df = _get_zonal_faces_weight_at_constLat(face_edges_cart, constLat_cart, face_bounds) + # Define the expected DataFrame + expected_weight_df = pd.DataFrame({"face_index": [0], "weight": [1.0]}) + + # Assert that the resulting should have weight is 1.0 + pd.testing.assert_frame_equal(weight_df, expected_weight_df) + + +def test_get_zonal_faces_weight_at_constLat_on_pole_faces(): + #there will be 4 faces touching the pole, so the weight should be 0.25 for each face + face_edges_cart = np.array([ + [ + [[5.22644277e-02, -5.22644277e-02, 9.97264689e-01], [5.23359562e-02, 0.00000000e+00, 9.98629535e-01]], + [[5.23359562e-02, 0.00000000e+00, 9.98629535e-01], [6.12323400e-17, 0.00000000e+00, 1.00000000e+00]], + [[6.12323400e-17, 0.00000000e+00, 1.00000000e+00], [3.20465306e-18, -5.23359562e-02, 9.98629535e-01]], + [[3.20465306e-18, -5.23359562e-02, 9.98629535e-01], [5.22644277e-02, -5.22644277e-02, 9.97264689e-01]] + ], + [ + [[5.23359562e-02, 0.00000000e+00, 9.98629535e-01], [5.22644277e-02, 5.22644277e-02, 9.97264689e-01]], + [[5.22644277e-02, 5.22644277e-02, 9.97264689e-01], [3.20465306e-18, 5.23359562e-02, 9.98629535e-01]], + [[3.20465306e-18, 5.23359562e-02, 9.98629535e-01], [6.12323400e-17, 0.00000000e+00, 1.00000000e+00]], + [[6.12323400e-17, 0.00000000e+00, 1.00000000e+00], [5.23359562e-02, 0.00000000e+00, 9.98629535e-01]] + ], + [ + [[3.20465306e-18, -5.23359562e-02, 9.98629535e-01], [6.12323400e-17, 0.00000000e+00, 1.00000000e+00]], + [[6.12323400e-17, 0.00000000e+00, 1.00000000e+00], [-5.23359562e-02, -6.40930613e-18, 9.98629535e-01]], + [[-5.23359562e-02, -6.40930613e-18, 9.98629535e-01], + [-5.22644277e-02, -5.22644277e-02, 9.97264689e-01]], + [[-5.22644277e-02, -5.22644277e-02, 9.97264689e-01], [3.20465306e-18, -5.23359562e-02, 9.98629535e-01]] + ], + [ + [[6.12323400e-17, 0.00000000e+00, 1.00000000e+00], [3.20465306e-18, 5.23359562e-02, 9.98629535e-01]], + [[3.20465306e-18, 5.23359562e-02, 9.98629535e-01], [-5.22644277e-02, 5.22644277e-02, 9.97264689e-01]], + [[-5.22644277e-02, 5.22644277e-02, 9.97264689e-01], [-5.23359562e-02, -6.40930613e-18, 9.98629535e-01]], + [[-5.23359562e-02, -6.40930613e-18, 9.98629535e-01], [6.12323400e-17, 0.00000000e+00, 1.00000000e+00]] + ] + ]) + + face_bounds = np.array([ + [[1.4968158, 1.57079633], [4.71238898, 0.0]], + [[1.4968158, 1.57079633], [0.0, 1.57079633]], + [[1.4968158, 1.57079633], [3.14159265, 0.0]], + [[1.4968158, 1.57079633], [0.0, 3.14159265]] + ]) + + constLat_cart = 1.0 + + weight_df = _get_zonal_faces_weight_at_constLat(face_edges_cart, constLat_cart, face_bounds) + # Define the expected DataFrame + expected_weight_df = pd.DataFrame({ + 'face_index': [0, 1, 2, 3], + 'weight': [0.25, 0.25, 0.25, 0.25] + }) + + # Assert that the DataFrame matches the expected DataFrame + pd.testing.assert_frame_equal(weight_df, expected_weight_df) + + +def test_get_zonal_face_interval_pole(): + #The face is touching the pole + face_edges_cart = np.array([ + [[-5.22644277e-02, -5.22644277e-02, -9.97264689e-01], + [-5.23359562e-02, -6.40930613e-18, -9.98629535e-01]], + + [[-5.23359562e-02, -6.40930613e-18, -9.98629535e-01], + [6.12323400e-17, 0.00000000e+00, -1.00000000e+00]], + + [[6.12323400e-17, 0.00000000e+00, -1.00000000e+00], + [3.20465306e-18, -5.23359562e-02, -9.98629535e-01]], + + [[3.20465306e-18, -5.23359562e-02, -9.98629535e-01], + [-5.22644277e-02, -5.22644277e-02, -9.97264689e-01]] + ]) + + # Corrected face_bounds + face_bounds = np.array([ + [-1.57079633, -1.4968158], + [3.14159265, 0.] + ]) + constLat_cart = -0.9986295347545738 + + weight_df = _get_zonal_face_interval(face_edges_cart, constLat_cart, face_bounds) + # No Nan values should be present in the weight_df + assert (not weight_df.isnull().values.any()) + + +def test_get_zonal_faces_weight_at_constLat_latlonface(): + face_0 = [[np.deg2rad(350), np.deg2rad(40)], [np.deg2rad(350), np.deg2rad(20)], + [np.deg2rad(10), np.deg2rad(20)], [np.deg2rad(10), np.deg2rad(40)]] + face_1 = [[np.deg2rad(5), np.deg2rad(20)], [np.deg2rad(5), np.deg2rad(10)], + [np.deg2rad(25), np.deg2rad(10)], [np.deg2rad(25), np.deg2rad(20)]] + face_2 = [[np.deg2rad(30), np.deg2rad(40)], [np.deg2rad(30), np.deg2rad(20)], + [np.deg2rad(40), np.deg2rad(20)], [np.deg2rad(40), np.deg2rad(40)]] + + # Convert the face vertices to xyz coordinates + face_0 = [_lonlat_rad_to_xyz(*v) for v in face_0] + face_1 = [_lonlat_rad_to_xyz(*v) for v in face_1] + face_2 = [_lonlat_rad_to_xyz(*v) for v in face_2] + + + face_0_edge_nodes = np.array([[face_0[0], face_0[1]], + [face_0[1], face_0[2]], + [face_0[2], face_0[3]], + [face_0[3], face_0[0]]]) + face_1_edge_nodes = np.array([[face_1[0], face_1[1]], + [face_1[1], face_1[2]], + [face_1[2], face_1[3]], + [face_1[3], face_1[0]]]) + face_2_edge_nodes = np.array([[face_2[0], face_2[1]], + [face_2[1], face_2[2]], + [face_2[2], face_2[3]], + [face_2[3], face_2[0]]]) + + face_0_latlon_bound = np.array([[np.deg2rad(20), np.deg2rad(40)], + [np.deg2rad(350), np.deg2rad(10)]]) + face_1_latlon_bound = np.array([[np.deg2rad(10), np.deg2rad(20)], + [np.deg2rad(5), np.deg2rad(25)]]) + face_2_latlon_bound = np.array([[np.deg2rad(20), np.deg2rad(40)], + [np.deg2rad(30), np.deg2rad(40)]]) + + + latlon_bounds = np.array([ + face_0_latlon_bound, face_1_latlon_bound, face_2_latlon_bound + ]) + + sum = 17.5 + 17.5 + 10 + expected_weight_df = pd.DataFrame({ + 'face_index': [0, 1, 2], + 'weight': [17.5 / sum, 17.5/sum, 10/sum] + }) + + # Assert the results is the same to the 3 decimal places + weight_df = _get_zonal_faces_weight_at_constLat(np.array([ + face_0_edge_nodes, face_1_edge_nodes, face_2_edge_nodes + ]), np.sin(np.deg2rad(20)), latlon_bounds, is_latlonface=True) + + + nt.assert_array_almost_equal(weight_df, expected_weight_df, decimal=3) + + + + # A error will be raise if we don't set is_latlonface=True since the face_2 will be concave if + # It's edges are all GCA + with pytest.raises(ValueError): + _get_zonal_faces_weight_at_constLat(np.array([ + face_0_edge_nodes, face_1_edge_nodes, face_2_edge_nodes + ]), np.deg2rad(20), latlon_bounds) diff --git a/test/test_intersections.py b/test/test_intersections.py index a9051d466..efe516dce 100644 --- a/test/test_intersections.py +++ b/test/test_intersections.py @@ -1,95 +1,75 @@ import numpy as np -from unittest import TestCase +import pytest import uxarray as ux from uxarray.constants import ERROR_TOLERANCE - -# from uxarray.grid.coordinates import node_lonlat_rad_to_xyz, node_xyz_to_lonlat_rad - from uxarray.grid.arcs import _extreme_gca_latitude_cartesian from uxarray.grid.coordinates import _lonlat_rad_to_xyz, _xyz_to_lonlat_rad,_xyz_to_lonlat_rad_scalar from uxarray.grid.intersections import gca_gca_intersection, gca_const_lat_intersection, _gca_gca_intersection_cartesian - -class TestGCAGCAIntersection(TestCase): - - def test_get_GCA_GCA_intersections_antimeridian(self): - # Test the case where the two GCAs are on the antimeridian - GCR1_cart = np.array([ - _lonlat_rad_to_xyz(np.deg2rad(170.0), - np.deg2rad(89.99)), - _lonlat_rad_to_xyz(np.deg2rad(170.0), - np.deg2rad(10.0)) - ]) - GCR2_cart = np.array([ - _lonlat_rad_to_xyz(np.deg2rad(70.0), 0.0), - _lonlat_rad_to_xyz(np.deg2rad(179.0), 0.0) - ]) - res_cart = _gca_gca_intersection_cartesian(GCR1_cart, GCR2_cart) - - # res_cart should be empty since these two GCRs are not intersecting - self.assertTrue(len(res_cart) == 0) - - GCR1_cart = np.array([ - _lonlat_rad_to_xyz(np.deg2rad(170.0), - np.deg2rad(89.0)), - _lonlat_rad_to_xyz(np.deg2rad(170.0), - np.deg2rad(-10.0)) - ]) - - - GCR2_cart = np.array([ - _lonlat_rad_to_xyz(np.deg2rad(70.0), 0.0), - _lonlat_rad_to_xyz(np.deg2rad(175.0), 0.0) - ]) - - res_cart = _gca_gca_intersection_cartesian(GCR1_cart, GCR2_cart) - res_cart = res_cart[0] - - # Test if the result is normalized - self.assertTrue( - np.allclose(np.linalg.norm(res_cart, axis=0), - 1.0, - atol=ERROR_TOLERANCE)) - res_lonlat_rad = _xyz_to_lonlat_rad(res_cart[0], res_cart[1], res_cart[2]) - - # res_cart should be [170, 0] - self.assertTrue( - np.array_equal(res_lonlat_rad, - np.array([np.deg2rad(170.0), - np.deg2rad(0.0)]))) - - def test_get_GCA_GCA_intersections_parallel(self): - # Test the case where the two GCAs are parallel - GCR1_cart = np.array([ - _lonlat_rad_to_xyz(0.3 * np.pi, 0.0), - _lonlat_rad_to_xyz(0.5 * np.pi, 0.0) - ]) - GCR2_cart = np.array([ - _lonlat_rad_to_xyz(0.5 * np.pi, 0.0), - _lonlat_rad_to_xyz(-0.5 * np.pi - 0.01, 0.0) - ]) - res_cart = _gca_gca_intersection_cartesian(GCR1_cart, GCR2_cart) - res_cart = res_cart[0] - expected_res = np.array(_lonlat_rad_to_xyz(0.5 * np.pi, 0.0)) - # Test if two results are equal within the error tolerance - self.assertAlmostEqual(np.linalg.norm(res_cart - expected_res), 0.0, delta=ERROR_TOLERANCE) - - def test_get_GCA_GCA_intersections_perpendicular(self): - # Test the case where the two GCAs are perpendicular to each other - GCR1_cart = np.array([ - _lonlat_rad_to_xyz(np.deg2rad(170.0), - np.deg2rad(0.0)), - _lonlat_rad_to_xyz(np.deg2rad(170.0), - np.deg2rad(10.0)) - ]) - GCR2_cart = np.array([ - _lonlat_rad_to_xyz(*[0.5 * np.pi - 0.01, 0.0]), - _lonlat_rad_to_xyz(*[-0.5 * np.pi + 0.01, 0.0]) - ]) - res_cart = _gca_gca_intersection_cartesian(GCR1_cart, GCR2_cart) - - # rest_cart should be empty since these two GCAs are not intersecting - self.assertTrue(len(res_cart) == 0) +def test_get_GCA_GCA_intersections_antimeridian(): + GCA1 = _lonlat_rad_to_xyz(np.deg2rad(170.0), np.deg2rad(89.99)) + GCR1_cart = np.array([ + _lonlat_rad_to_xyz(np.deg2rad(170.0), np.deg2rad(89.99)), + _lonlat_rad_to_xyz(np.deg2rad(170.0), np.deg2rad(10.0)) + ]) + GCR2_cart = np.array([ + _lonlat_rad_to_xyz(np.deg2rad(70.0), 0.0), + _lonlat_rad_to_xyz(np.deg2rad(179.0), 0.0) + ]) + res_cart = _gca_gca_intersection_cartesian(GCR1_cart, GCR2_cart) + + assert len(res_cart) == 0 + + GCR1_cart = np.array([ + _lonlat_rad_to_xyz(np.deg2rad(170.0), np.deg2rad(89.0)), + _lonlat_rad_to_xyz(np.deg2rad(170.0), np.deg2rad(-10.0)) + ]) + + + GCR2_cart = np.array([ + _lonlat_rad_to_xyz(np.deg2rad(70.0), 0.0), + _lonlat_rad_to_xyz(np.deg2rad(175.0), 0.0) + ]) + + res_cart = _gca_gca_intersection_cartesian(GCR1_cart, GCR2_cart) + res_cart = res_cart[0] + + assert np.allclose(np.linalg.norm(res_cart, axis=0), 1.0, atol=ERROR_TOLERANCE) + res_lonlat_rad = _xyz_to_lonlat_rad(res_cart[0], res_cart[1], res_cart[2]) + + assert np.array_equal(res_lonlat_rad, np.array([np.deg2rad(170.0), np.deg2rad(0.0)])) + +def test_get_GCA_GCA_intersections_parallel(): + GCR1_cart = np.array([ + _lonlat_rad_to_xyz(0.3 * np.pi, 0.0), + _lonlat_rad_to_xyz(0.5 * np.pi, 0.0) + ]) + GCR2_cart = np.array([ + _lonlat_rad_to_xyz(0.5 * np.pi, 0.0), + _lonlat_rad_to_xyz(-0.5 * np.pi - 0.01, 0.0) + ]) + res_cart = _gca_gca_intersection_cartesian(GCR1_cart, GCR2_cart) + res_cart = res_cart[0] + expected_res = np.array(_lonlat_rad_to_xyz(0.5 * np.pi, 0.0)) + + assert np.isclose(np.linalg.norm(res_cart - expected_res), 0.0, atol=ERROR_TOLERANCE) + +def test_get_GCA_GCA_intersections_perpendicular(): + # Test the case where the two GCAs are perpendicular to each other + GCR1_cart = np.array([ + _lonlat_rad_to_xyz(np.deg2rad(170.0), + np.deg2rad(0.0)), + _lonlat_rad_to_xyz(np.deg2rad(170.0), + np.deg2rad(10.0)) + ]) + GCR2_cart = np.array([ + _lonlat_rad_to_xyz(*[0.5 * np.pi - 0.01, 0.0]), + _lonlat_rad_to_xyz(*[-0.5 * np.pi + 0.01, 0.0]) + ]) + res_cart = _gca_gca_intersection_cartesian(GCR1_cart, GCR2_cart) + + # rest_cart should be empty since these two GCAs are not intersecting + assert(len(res_cart) == 0) # def test_GCA_GCA_single_edge_to_pole(self): @@ -115,192 +95,175 @@ def test_get_GCA_GCA_intersections_perpendicular(self): # # The edge should intersect # self.assertTrue(len(gca_gca_intersection(gca_a_xyz, gca_b_xyz))) - def test_GCA_GCA_south_pole(self): - - # GCA_a - Face Center connected to South Pole - # Point A - South Pole - ref_point_lonlat = np.deg2rad(np.array([0.0, -90.0])) - ref_point_xyz = np.array(_lonlat_rad_to_xyz(*ref_point_lonlat)) - # Point B - Face Center - face_lonlat = np.deg2rad(np.array([0.0, 0.0])) - face_xyz = np.array(_lonlat_rad_to_xyz(*face_lonlat)) - gca_a_xyz = np.array([face_xyz, ref_point_xyz]) - - # GCA_b - Single Face Edge - # Point A - First Edge Point - edge_a_lonlat = np.deg2rad(np.array((-45, -1.0))) - edge_b_lonlat = np.deg2rad(np.array((45, -1.0))) - - # Point B - Second Edge Point - edge_a_xyz = np.array(_lonlat_rad_to_xyz(*edge_a_lonlat)) - edge_b_xyz = np.array(_lonlat_rad_to_xyz(*edge_b_lonlat)) - gca_b_xyz = np.array([edge_a_xyz, edge_b_xyz]) - - # The edge should intersect - self.assertTrue(len(gca_gca_intersection(gca_a_xyz, gca_b_xyz))) - - def test_GCA_GCA_north_pole(self): - # GCA_a - Face Center connected to South Pole - ref_point_lonlat = np.deg2rad(np.array([0.0, 90.0])) - ref_point_xyz = np.array(_lonlat_rad_to_xyz(*ref_point_lonlat)) - face_lonlat = np.deg2rad(np.array([0.0, 0.0])) - face_xyz = np.array(_lonlat_rad_to_xyz(*face_lonlat)) - gca_a_xyz = np.array([face_xyz, ref_point_xyz]) - - # GCA_b - Single Face Edge - edge_a_lonlat = np.deg2rad(np.array((-45, 1.0))) - edge_b_lonlat = np.deg2rad(np.array((45, 1.0))) - - edge_a_xyz = np.array(_lonlat_rad_to_xyz(*edge_a_lonlat)) - edge_b_xyz = np.array(_lonlat_rad_to_xyz(*edge_b_lonlat)) - gca_b_xyz = np.array([edge_a_xyz, edge_b_xyz]) - - # The edge should intersect - self.assertTrue(len(gca_gca_intersection(gca_a_xyz, gca_b_xyz))) - - def test_GCA_GCA_north_pole_angled(self): - # GCA_a - ref_point_lonlat = np.deg2rad(np.array([0.0, 90.0])) - ref_point_xyz = np.array(_lonlat_rad_to_xyz(*ref_point_lonlat)) - face_lonlat = np.deg2rad(np.array([-45.0, 45.0])) - face_xyz = np.array(_lonlat_rad_to_xyz(*face_lonlat)) - gca_a_xyz = np.array([face_xyz, ref_point_xyz]) - - # GCA_b - edge_a_lonlat = np.deg2rad(np.array((-45.0, 50.0))) - edge_b_lonlat = np.deg2rad(np.array((-40.0, 45.0))) - - # Point B - Second Edge Point - edge_a_xyz = np.array(_lonlat_rad_to_xyz(*edge_a_lonlat)) - edge_b_xyz = np.array(_lonlat_rad_to_xyz(*edge_b_lonlat)) - gca_b_xyz = np.array([edge_a_xyz, edge_b_xyz]) - - # The edge should intersect - self.assertTrue(len(gca_gca_intersection(gca_a_xyz, gca_b_xyz))) - - - def test_GCA_edge_intersection_count(self): - - from uxarray.grid.utils import _get_cartesian_face_edge_nodes - - # Generate a normal face that is not crossing the antimeridian or the poles - vertices_lonlat = [[29.5, 11.0], [29.5, 10.0], [30.5, 10.0], [30.5, 11.0]] - vertices_lonlat = np.array(vertices_lonlat) - - grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) - face_edge_nodes_cartesian = _get_cartesian_face_edge_nodes( - grid.face_node_connectivity.values, - grid.n_face, - grid.n_max_face_edges, - grid.node_x.values, - grid.node_y.values, - grid.node_z.values) - - face_center_xyz = np.array([grid.face_x.values[0], grid.face_y.values[0], grid.face_z.values[0]], dtype=np.float64) - north_pole_xyz = np.array([0.0, 0.0, 1.0], dtype=np.float64) - south_pole_xyz = np.array([0.0, 0.0, -1.0], dtype=np.float64) - - gca_face_center_north_pole = np.array([face_center_xyz, north_pole_xyz], dtype=np.float64) - gca_face_center_south_pole = np.array([face_center_xyz, south_pole_xyz], dtype=np.float64) - - intersect_north_pole_count = 0 - intersect_south_pole_count = 0 - - for edge in face_edge_nodes_cartesian[0]: - res1 = gca_gca_intersection(edge, gca_face_center_north_pole) - res2 = gca_gca_intersection(edge, gca_face_center_south_pole) - - if len(res1): - intersect_north_pole_count += 1 - if len(res2): - intersect_south_pole_count += 1 - - print(intersect_north_pole_count, intersect_south_pole_count) - self.assertTrue(intersect_north_pole_count == 1) - self.assertTrue(intersect_south_pole_count == 1) - - def test_GCA_GCA_single_edge_to_pole(self): - # GCA_a - Face Center connected to South Pole - # Point A - South Pole - ref_point_lonlat_exact = np.deg2rad(np.array([0.0, -90.0])) - ref_point_lonlat_close = np.deg2rad(np.array([0.0, -89.99999])) - ref_point_xyz_exact = np.array(_lonlat_rad_to_xyz(*ref_point_lonlat_exact)) - ref_point_xyz_close = np.array(_lonlat_rad_to_xyz(*ref_point_lonlat_close)) - - # Point B - Face Center - face_lonlat = np.deg2rad(np.array([-175.0, 26.5])) - face_xyz = np.array(_lonlat_rad_to_xyz(*face_lonlat)) - gca_a_xyz_close = np.array([face_xyz, ref_point_xyz_close]) - gca_a_xyz_exact = np.array([face_xyz, ref_point_xyz_exact]) - - # GCA_b - Single Face Edge - # Point A - First Edge Point - edge_a_lonlat = np.deg2rad(np.array((-175.0, -24.5))) - edge_b_lonlat = np.deg2rad(np.array((-173.0, 28.7))) - - # Point B - Second Edge Point - edge_a_xyz = np.array(_lonlat_rad_to_xyz(*edge_a_lonlat)) - edge_b_xyz = np.array(_lonlat_rad_to_xyz(*edge_b_lonlat)) - gca_b_xyz = np.array([edge_a_xyz, edge_b_xyz]) - - # The edge should intersect - self.assertTrue(len(gca_gca_intersection(gca_a_xyz_close, gca_b_xyz))) - self.assertTrue(len(gca_gca_intersection(gca_a_xyz_exact, gca_b_xyz))) - - - - - -class TestGCAconstLatIntersection(TestCase): - - def test_GCA_constLat_intersections_antimeridian(self): - GCR1_cart = np.array([ - _lonlat_rad_to_xyz(np.deg2rad(170.0), - np.deg2rad(89.99)), - _lonlat_rad_to_xyz(np.deg2rad(170.0), - np.deg2rad(10.0)) - ]) - - res = gca_const_lat_intersection(GCR1_cart, np.sin(np.deg2rad(60.0)), verbose=True) - res_lonlat_rad = _xyz_to_lonlat_rad(*(res[0].tolist())) - self.assertTrue( - np.allclose(res_lonlat_rad, - np.array([np.deg2rad(170.0), - np.deg2rad(60.0)]))) - - def test_GCA_constLat_intersections_empty(self): - GCR1_cart = np.array([ - _lonlat_rad_to_xyz(np.deg2rad(170.0), - np.deg2rad(89.99)), - _lonlat_rad_to_xyz(np.deg2rad(170.0), - np.deg2rad(10.0)) - ]) - - res = gca_const_lat_intersection(GCR1_cart, np.sin(np.deg2rad(-10.0)), verbose=False) - self.assertTrue(res.size == 0) - - def test_GCA_constLat_intersections_two_pts(self): - GCR1_cart = np.array([ - _lonlat_rad_to_xyz(np.deg2rad(10.0), - np.deg2rad(10)), - _lonlat_rad_to_xyz(np.deg2rad(170.0), - np.deg2rad(10.0)) - ]) - max_lat = _extreme_gca_latitude_cartesian(GCR1_cart, 'max') - - query_lat = (np.deg2rad(10.0) + max_lat) / 2.0 - - res = gca_const_lat_intersection(GCR1_cart, np.sin(query_lat), verbose=False) - self.assertTrue(res.shape[0] == 2) - - - def test_GCA_constLat_intersections_no_convege(self): - # It should return an one single point and a warning about unable to be converged should be raised - GCR1_cart = np.array([[-0.59647278, 0.59647278, -0.53706651], - [-0.61362973, 0.61362973, -0.49690755]]) - - constZ = -0.5150380749100542 - - with self.assertWarns(UserWarning): - res = gca_const_lat_intersection(GCR1_cart, constZ, verbose=False) - self.assertTrue(res.shape[0] == 1) +def test_GCA_GCA_south_pole(): + + # GCA_a - Face Center connected to South Pole + # Point A - South Pole + ref_point_lonlat = np.deg2rad(np.array([0.0, -90.0])) + ref_point_xyz = np.array(_lonlat_rad_to_xyz(*ref_point_lonlat)) + # Point B - Face Center + face_lonlat = np.deg2rad(np.array([0.0, 0.0])) + face_xyz = np.array(_lonlat_rad_to_xyz(*face_lonlat)) + gca_a_xyz = np.array([face_xyz, ref_point_xyz]) + + # GCA_b - Single Face Edge + # Point A - First Edge Point + edge_a_lonlat = np.deg2rad(np.array((-45, -1.0))) + edge_b_lonlat = np.deg2rad(np.array((45, -1.0))) + + # Point B - Second Edge Point + edge_a_xyz = np.array(_lonlat_rad_to_xyz(*edge_a_lonlat)) + edge_b_xyz = np.array(_lonlat_rad_to_xyz(*edge_b_lonlat)) + gca_b_xyz = np.array([edge_a_xyz, edge_b_xyz]) + + # The edge should intersect + assert(len(gca_gca_intersection(gca_a_xyz, gca_b_xyz))) + +def test_GCA_GCA_north_pole(): + # GCA_a - Face Center connected to South Pole + ref_point_lonlat = np.deg2rad(np.array([0.0, 90.0])) + ref_point_xyz = np.array(_lonlat_rad_to_xyz(*ref_point_lonlat)) + face_lonlat = np.deg2rad(np.array([0.0, 0.0])) + face_xyz = np.array(_lonlat_rad_to_xyz(*face_lonlat)) + gca_a_xyz = np.array([face_xyz, ref_point_xyz]) + + # GCA_b - Single Face Edge + edge_a_lonlat = np.deg2rad(np.array((-45, 1.0))) + edge_b_lonlat = np.deg2rad(np.array((45, 1.0))) + + edge_a_xyz = np.array(_lonlat_rad_to_xyz(*edge_a_lonlat)) + edge_b_xyz = np.array(_lonlat_rad_to_xyz(*edge_b_lonlat)) + gca_b_xyz = np.array([edge_a_xyz, edge_b_xyz]) + + # The edge should intersect + assert(len(gca_gca_intersection(gca_a_xyz, gca_b_xyz))) + +def test_GCA_GCA_north_pole_angled(): + # GCA_a + ref_point_lonlat = np.deg2rad(np.array([0.0, 90.0])) + ref_point_xyz = np.array(_lonlat_rad_to_xyz(*ref_point_lonlat)) + face_lonlat = np.deg2rad(np.array([-45.0, 45.0])) + face_xyz = np.array(_lonlat_rad_to_xyz(*face_lonlat)) + gca_a_xyz = np.array([face_xyz, ref_point_xyz]) + + # GCA_b + edge_a_lonlat = np.deg2rad(np.array((-45.0, 50.0))) + edge_b_lonlat = np.deg2rad(np.array((-40.0, 45.0))) + + # Point B - Second Edge Point + edge_a_xyz = np.array(_lonlat_rad_to_xyz(*edge_a_lonlat)) + edge_b_xyz = np.array(_lonlat_rad_to_xyz(*edge_b_lonlat)) + gca_b_xyz = np.array([edge_a_xyz, edge_b_xyz]) + + # The edge should intersect + assert(len(gca_gca_intersection(gca_a_xyz, gca_b_xyz))) + + +def test_GCA_edge_intersection_count(): + + from uxarray.grid.utils import _get_cartesian_face_edge_nodes + + # Generate a normal face that is not crossing the antimeridian or the poles + vertices_lonlat = [[29.5, 11.0], [29.5, 10.0], [30.5, 10.0], [30.5, 11.0]] + vertices_lonlat = np.array(vertices_lonlat) + + grid = ux.Grid.from_face_vertices(vertices_lonlat, latlon=True) + face_edge_nodes_cartesian = _get_cartesian_face_edge_nodes( + grid.face_node_connectivity.values, + grid.n_face, + grid.n_max_face_edges, + grid.node_x.values, + grid.node_y.values, + grid.node_z.values) + + face_center_xyz = np.array([grid.face_x.values[0], grid.face_y.values[0], grid.face_z.values[0]], dtype=np.float64) + north_pole_xyz = np.array([0.0, 0.0, 1.0], dtype=np.float64) + south_pole_xyz = np.array([0.0, 0.0, -1.0], dtype=np.float64) + + gca_face_center_north_pole = np.array([face_center_xyz, north_pole_xyz], dtype=np.float64) + gca_face_center_south_pole = np.array([face_center_xyz, south_pole_xyz], dtype=np.float64) + + intersect_north_pole_count = 0 + intersect_south_pole_count = 0 + + for edge in face_edge_nodes_cartesian[0]: + res1 = gca_gca_intersection(edge, gca_face_center_north_pole) + res2 = gca_gca_intersection(edge, gca_face_center_south_pole) + + if len(res1): + intersect_north_pole_count += 1 + if len(res2): + intersect_south_pole_count += 1 + + print(intersect_north_pole_count, intersect_south_pole_count) + assert(intersect_north_pole_count == 1) + assert(intersect_south_pole_count == 1) + +def test_GCA_GCA_single_edge_to_pole(): + # GCA_a - Face Center connected to South Pole + # Point A - South Pole + ref_point_lonlat_exact = np.deg2rad(np.array([0.0, -90.0])) + ref_point_lonlat_close = np.deg2rad(np.array([0.0, -89.99999])) + ref_point_xyz_exact = np.array(_lonlat_rad_to_xyz(*ref_point_lonlat_exact)) + ref_point_xyz_close = np.array(_lonlat_rad_to_xyz(*ref_point_lonlat_close)) + + # Point B - Face Center + face_lonlat = np.deg2rad(np.array([-175.0, 26.5])) + face_xyz = np.array(_lonlat_rad_to_xyz(*face_lonlat)) + gca_a_xyz_close = np.array([face_xyz, ref_point_xyz_close]) + gca_a_xyz_exact = np.array([face_xyz, ref_point_xyz_exact]) + + # GCA_b - Single Face Edge + # Point A - First Edge Point + edge_a_lonlat = np.deg2rad(np.array((-175.0, -24.5))) + edge_b_lonlat = np.deg2rad(np.array((-173.0, 28.7))) + + # Point B - Second Edge Point + edge_a_xyz = np.array(_lonlat_rad_to_xyz(*edge_a_lonlat)) + edge_b_xyz = np.array(_lonlat_rad_to_xyz(*edge_b_lonlat)) + gca_b_xyz = np.array([edge_a_xyz, edge_b_xyz]) + + # The edge should intersect + assert(len(gca_gca_intersection(gca_a_xyz_close, gca_b_xyz))) + assert(len(gca_gca_intersection(gca_a_xyz_exact, gca_b_xyz))) + +def test_GCA_constLat_intersections_antimeridian(): + GCR1_cart = np.array([ + _lonlat_rad_to_xyz(np.deg2rad(170.0), np.deg2rad(89.99)), + _lonlat_rad_to_xyz(np.deg2rad(170.0), np.deg2rad(10.0)) + ]) + + res = gca_const_lat_intersection(GCR1_cart, np.sin(np.deg2rad(60.0)), verbose=True) + res_lonlat_rad = _xyz_to_lonlat_rad(*(res[0].tolist())) + assert np.allclose(res_lonlat_rad, np.array([np.deg2rad(170.0), np.deg2rad(60.0)])) + +def test_GCA_constLat_intersections_empty(): + GCR1_cart = np.array([ + _lonlat_rad_to_xyz(np.deg2rad(170.0), np.deg2rad(89.99)), + _lonlat_rad_to_xyz(np.deg2rad(170.0), np.deg2rad(10.0)) + ]) + + res = gca_const_lat_intersection(GCR1_cart, np.sin(np.deg2rad(-10.0)), verbose=False) + assert res.size == 0 + +def test_GCA_constLat_intersections_two_pts(): + GCR1_cart = np.array([ + _lonlat_rad_to_xyz(np.deg2rad(10.0), np.deg2rad(10)), + _lonlat_rad_to_xyz(np.deg2rad(170.0), np.deg2rad(10.0)) + ]) + max_lat = _extreme_gca_latitude_cartesian(GCR1_cart, 'max') + + query_lat = (np.deg2rad(10.0) + max_lat) / 2.0 + + res = gca_const_lat_intersection(GCR1_cart, np.sin(query_lat), verbose=False) + assert res.shape[0] == 2 + +def test_GCA_constLat_intersections_no_converge(): + GCR1_cart = np.array([[-0.59647278, 0.59647278, -0.53706651], + [-0.61362973, 0.61362973, -0.49690755]]) + + constZ = -0.5150380749100542 + + with pytest.warns(UserWarning): + res = gca_const_lat_intersection(GCR1_cart, constZ, verbose=False) + assert res.shape[0] == 1 diff --git a/test/test_mpas.py b/test/test_mpas.py index 5833e0f85..7e6dc6cfb 100644 --- a/test/test_mpas.py +++ b/test/test_mpas.py @@ -1,142 +1,109 @@ -from uxarray.io._mpas import _replace_padding, _replace_zeros, _to_zero_index -from uxarray.io._mpas import _read_mpas -import uxarray as ux -import xarray as xr -from unittest import TestCase import numpy as np import os +import pytest +import uxarray as ux +import xarray as xr from pathlib import Path - from uxarray.constants import INT_DTYPE, INT_FILL_VALUE +from uxarray.io._mpas import _replace_padding, _replace_zeros, _to_zero_index, _read_mpas current_path = Path(os.path.dirname(os.path.realpath(__file__))) - -class TestMPAS(TestCase): - """Test suite for Read MPAS functionality.""" - - # sample mpas dataset - mpas_grid_path = current_path / 'meshfiles' / "mpas" / "QU" / 'mesh.QU.1920km.151026.nc' - mpas_xr_ds = xr.open_dataset(mpas_grid_path) - - mpas_ocean_mesh = current_path / 'meshfiles' / "mpas" / "QU" / 'oQU480.231010.nc' - - # fill value (remove once there is a unified approach in uxarray) - fv = INT_FILL_VALUE - - def test_read_mpas(self): - """Tests execution of _read_mpas()""" - mpas_primal_ugrid, _ = _read_mpas(self.mpas_xr_ds, use_dual=False) - mpas_dual_ugrid, _ = _read_mpas(self.mpas_xr_ds, use_dual=True) - - def test_mpas_to_grid(self): - """Tests creation of Grid object from converted MPAS dataset.""" - mpas_uxgrid_primal = ux.open_grid(self.mpas_grid_path, use_dual=False) - mpas_uxgrid_dual = ux.open_grid(self.mpas_grid_path, use_dual=True) - mpas_uxgrid_dual.__repr__() - pass - - def test_primal_to_ugrid_conversion(self): - """Verifies that the Primal-Mesh was converted properly.""" - - for path in [self.mpas_grid_path, self.mpas_ocean_mesh]: - # dual-mesh encoded in the UGRID conventions - uxgrid = ux.open_grid(path, use_dual=False) - ds = uxgrid._ds - - # check for correct dimensions - expected_ugrid_dims = ['n_node', "n_face", "n_max_face_nodes"] - for dim in expected_ugrid_dims: - assert dim in ds.sizes - - # check for correct length of coordinates - assert len(ds['node_lon']) == len(ds['node_lat']) - assert len(ds['face_lon']) == len(ds['face_lat']) - - # check for correct shape of face nodes - n_face = ds.sizes['n_face'] - n_max_face_nodes = ds.sizes['n_max_face_nodes'] - assert ds['face_node_connectivity'].shape == (n_face, - n_max_face_nodes) - - pass - - def test_dual_to_ugrid_conversion(self): - """Verifies that the Dual-Mesh was converted properly.""" - - for path in [self.mpas_grid_path, self.mpas_ocean_mesh]: - - # dual-mesh encoded in the UGRID conventions - uxgrid = ux.open_grid(path, use_dual=True) - ds = uxgrid._ds - - # check for correct dimensions - expected_ugrid_dims = ['n_node', "n_face", "n_max_face_nodes"] - for dim in expected_ugrid_dims: - assert dim in ds.sizes - - # check for correct length of coordinates - assert len(ds['node_lon']) == len(ds['node_lat']) - assert len(ds['face_lon']) == len(ds['face_lat']) - - # check for correct shape of face nodes - nMesh2_face = ds.sizes['n_face'] - assert ds['face_node_connectivity'].shape == (nMesh2_face, 3) - - def test_add_fill_values(self): - """Test _add_fill_values() implementation, output should be both be - zero-indexed and padded values should be replaced with fill values.""" - - # two cells with 2, 3 and 2 padded faces respectively - verticesOnCell = np.array([[1, 2, 1, 1], [3, 4, 5, 3], [6, 7, 0, 0]], - dtype=INT_DTYPE) - - # cell has 2, 3 and 2 nodes respectively - nEdgesOnCell = np.array([2, 3, 2]) - - # expected output of _add_fill_values() - gold_output = np.array([[0, 1, self.fv, self.fv], [2, 3, 4, self.fv], - [5, 6, self.fv, self.fv]], - dtype=INT_DTYPE) - - # test data output - verticesOnCell = _replace_padding(verticesOnCell, nEdgesOnCell) - verticesOnCell = _replace_zeros(verticesOnCell) - verticesOnCell = _to_zero_index(verticesOnCell) - - assert np.array_equal(verticesOnCell, gold_output) - - def test_set_attrs(self): - """Tests the execution of ``_set_global_attrs``, checking for - attributes being correctly stored in ``Grid._ds``""" - - # full set of expected mpas attributes - expected_attrs = [ - 'sphere_radius', 'mesh_spec', 'on_a_sphere', 'mesh_id', - 'is_periodic', 'x_period', 'y_period' - ] - - # included attrs: 'sphere_radius', 'mesh_spec' 'on_a_sphere' - ds, _ = _read_mpas(self.mpas_xr_ds) - - # set dummy attrs to test execution - ds.attrs['mesh_id'] = "12345678" - ds.attrs['is_periodic'] = "YES" - ds.attrs['x_period'] = 1.0 - ds.attrs['y_period'] = 1.0 - - # create a grid - uxgrid = ux.Grid(ds) - - # check if all expected attributes are set - for mpas_attr in expected_attrs: - assert mpas_attr in uxgrid._ds.attrs - - def test_face_area(self): - """Tests the parsing of face areas for MPAS grids.""" - - uxgrid_primal = ux.open_grid(self.mpas_grid_path, use_dual=False) - uxgrid_dual = ux.open_grid(self.mpas_grid_path, use_dual=True) - - assert "face_areas" in uxgrid_primal._ds - assert "face_areas" in uxgrid_dual._ds +# Sample MPAS dataset paths +mpas_grid_path = current_path / 'meshfiles' / "mpas" / "QU" / 'mesh.QU.1920km.151026.nc' +mpas_xr_ds = xr.open_dataset(mpas_grid_path) +mpas_ocean_mesh = current_path / 'meshfiles' / "mpas" / "QU" / 'oQU480.231010.nc' + +# Fill value +fv = INT_FILL_VALUE + +def test_read_mpas(): + """Tests execution of _read_mpas()""" + mpas_primal_ugrid, _ = _read_mpas(mpas_xr_ds, use_dual=False) + mpas_dual_ugrid, _ = _read_mpas(mpas_xr_ds, use_dual=True) + +def test_mpas_to_grid(): + """Tests creation of Grid object from converted MPAS dataset.""" + mpas_uxgrid_primal = ux.open_grid(mpas_grid_path, use_dual=False) + mpas_uxgrid_dual = ux.open_grid(mpas_grid_path, use_dual=True) + mpas_uxgrid_dual.__repr__() + +def test_primal_to_ugrid_conversion(): + """Verifies that the Primal-Mesh was converted properly.""" + for path in [mpas_grid_path, mpas_ocean_mesh]: + uxgrid = ux.open_grid(path, use_dual=False) + ds = uxgrid._ds + + # Check for correct dimensions + expected_ugrid_dims = ['n_node', "n_face", "n_max_face_nodes"] + for dim in expected_ugrid_dims: + assert dim in ds.sizes + + # Check for correct length of coordinates + assert len(ds['node_lon']) == len(ds['node_lat']) + assert len(ds['face_lon']) == len(ds['face_lat']) + + # Check for correct shape of face nodes + n_face = ds.sizes['n_face'] + n_max_face_nodes = ds.sizes['n_max_face_nodes'] + assert ds['face_node_connectivity'].shape == (n_face, n_max_face_nodes) + +def test_dual_to_ugrid_conversion(): + """Verifies that the Dual-Mesh was converted properly.""" + for path in [mpas_grid_path, mpas_ocean_mesh]: + uxgrid = ux.open_grid(path, use_dual=True) + ds = uxgrid._ds + + # Check for correct dimensions + expected_ugrid_dims = ['n_node', "n_face", "n_max_face_nodes"] + for dim in expected_ugrid_dims: + assert dim in ds.sizes + + # Check for correct length of coordinates + assert len(ds['node_lon']) == len(ds['node_lat']) + assert len(ds['face_lon']) == len(ds['face_lat']) + + # Check for correct shape of face nodes + nMesh2_face = ds.sizes['n_face'] + assert ds['face_node_connectivity'].shape == (nMesh2_face, 3) + +def test_add_fill_values(): + """Test _add_fill_values() implementation.""" + verticesOnCell = np.array([[1, 2, 1, 1], [3, 4, 5, 3], [6, 7, 0, 0]], dtype=INT_DTYPE) + nEdgesOnCell = np.array([2, 3, 2]) + gold_output = np.array([[0, 1, fv, fv], [2, 3, 4, fv], [5, 6, fv, fv]], dtype=INT_DTYPE) + + verticesOnCell = _replace_padding(verticesOnCell, nEdgesOnCell) + verticesOnCell = _replace_zeros(verticesOnCell) + verticesOnCell = _to_zero_index(verticesOnCell) + + assert np.array_equal(verticesOnCell, gold_output) + +def test_set_attrs(): + """Tests the execution of _set_global_attrs.""" + expected_attrs = [ + 'sphere_radius', 'mesh_spec', 'on_a_sphere', 'mesh_id', + 'is_periodic', 'x_period', 'y_period' + ] + + ds, _ = _read_mpas(mpas_xr_ds) + + # Set dummy attrs to test execution + ds.attrs['mesh_id'] = "12345678" + ds.attrs['is_periodic'] = "YES" + ds.attrs['x_period'] = 1.0 + ds.attrs['y_period'] = 1.0 + + uxgrid = ux.Grid(ds) + + # Check if all expected attributes are set + for mpas_attr in expected_attrs: + assert mpas_attr in uxgrid._ds.attrs + +def test_face_area(): + """Tests the parsing of face areas for MPAS grids.""" + uxgrid_primal = ux.open_grid(mpas_grid_path, use_dual=False) + uxgrid_dual = ux.open_grid(mpas_grid_path, use_dual=True) + + assert "face_areas" in uxgrid_primal._ds + assert "face_areas" in uxgrid_dual._ds diff --git a/test/test_neighbors.py b/test/test_neighbors.py index 74321c247..d06975c1c 100644 --- a/test/test_neighbors.py +++ b/test/test_neighbors.py @@ -1,24 +1,13 @@ import os import numpy as np -import numpy.testing as nt +import pytest import xarray as xr - -from unittest import TestCase from pathlib import Path - import uxarray as ux -from uxarray.grid.connectivity import _populate_face_edge_connectivity, _build_edge_face_connectivity - -from uxarray.constants import INT_FILL_VALUE - -try: - import constants -except ImportError: - from . import constants - current_path = Path(os.path.dirname(os.path.realpath(__file__))) +# Sample grid file paths gridfile_CSne8 = current_path / "meshfiles" / "scrip" / "outCSne8" / "outCSne8.nc" gridfile_RLL1deg = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug" gridfile_RLL10deg_CSne4 = current_path / "meshfiles" / "ugrid" / "ov_RLL10deg_CSne4" / "ov_RLL10deg_CSne4.ug" @@ -27,312 +16,144 @@ gridfile_geoflow = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc" gridfile_mpas = current_path / 'meshfiles' / "mpas" / "QU" / 'mesh.QU.1920km.151026.nc' -dsfile_vortex_CSne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30_vortex.nc" -dsfile_var2_CSne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30_var2.nc" - -shp_filename = current_path / "meshfiles" / "shp" / "grid_fire.shp" - - - -class TestBallTree(TestCase): - corner_grid_files = [gridfile_CSne30, gridfile_mpas] - center_grid_files = [gridfile_mpas] - - def test_construction_from_nodes(self): - """Tests the construction of the ball tree on nodes and performs a - sample query.""" - - for grid_file in self.corner_grid_files: - uxgrid = ux.open_grid(grid_file) - - # performs a sample query - d, ind = uxgrid.get_ball_tree(coordinates="nodes").query([3.0, 3.0], - k=3) - - # assert it returns the correct k neighbors and is not empty - self.assertEqual(len(d), len(ind)) - self.assertTrue(len(d) and len(ind) != 0) - - def test_construction_from_face_centers(self): - """Tests the construction of the ball tree on center nodes and performs - a sample query.""" - - for grid_file in self.center_grid_files: - uxgrid = ux.open_grid(grid_file) - - # performs a sample query - d, ind = uxgrid.get_ball_tree(coordinates="face centers").query( - [3.0, 3.0], k=3) - - # assert it returns the correct k neighbors and is not empty - self.assertEqual(len(d), len(ind)) - self.assertTrue(len(d) and len(ind) != 0) - - def test_construction_from_edge_centers(self): - """Tests the construction of the ball tree on edge_centers and performs - a sample query.""" - - for grid_file in self.center_grid_files: - uxgrid = ux.open_grid(grid_file) - - # performs a sample query - d, ind = uxgrid.get_ball_tree(coordinates="edge centers").query( - [3.0, 3.0], k=3) - - # assert it returns the correct k neighbors and is not empty - self.assertEqual(len(d), len(ind)) - self.assertTrue(len(d) and len(ind) != 0) - - def test_construction_from_both_sequentially(self): - """Tests the construction of the ball tree on center nodes and performs - a sample query.""" - - for grid_file in self.center_grid_files: - uxgrid = ux.open_grid(grid_file) - - # performs a sample query - d, ind = uxgrid.get_ball_tree(coordinates="nodes").query([3.0, 3.0], - k=3) - d_centers, ind_centers = uxgrid.get_ball_tree( - coordinates="face centers").query([3.0, 3.0], k=3) - - def test_antimeridian_distance_nodes(self): - """Verifies nearest neighbor search across Antimeridian.""" - - # single triangle with point on antimeridian - verts = [(0.0, 90.0), (-180, 0.0), (0.0, -90)] - - uxgrid = ux.open_grid(verts, latlon=True) - - # point on antimeridian, other side of grid - d, ind = uxgrid.get_ball_tree(coordinates="nodes").query([180.0, 0.0], - k=1) - - # distance across antimeridian is approx zero - assert np.isclose(d, 0.0) - - # index should point to the 0th (x, y) pair (-180, 0.0) - assert ind == 0 - - # point on antimeridian, other side of grid, slightly larger than 90 due to floating point calcs - d, ind = uxgrid.get_ball_tree(coordinates="nodes").query_radius( - [-180, 0.0], r=90.01, return_distance=True) - - expected_d = np.array([0.0, 90.0, 90.0]) - - assert np.allclose(a=d, b=expected_d, atol=1e-03) - - def test_antimeridian_distance_face_centers(self): - """TODO: Write addition tests once construction and representation of face centers is implemented.""" - pass - - def test_construction_using_cartesian_coords(self): - """Test the BallTree creation and query function using cartesian - coordinates.""" - - for grid_file in self.corner_grid_files: - uxgrid = ux.open_grid(grid_file) - d, ind = uxgrid.get_ball_tree(coordinates="nodes", - coordinate_system="cartesian", - distance_metric="minkowski").query( - [1.0, 0.0, 0.0], k=2) - - # assert it returns the correct k neighbors and is not empty - self.assertEqual(len(d), len(ind)) - self.assertTrue(len(d) and len(ind) != 0) - - def test_query_radius(self): - """Test the BallTree creation and query_radius function using the grids - face centers.""" - for grid_file in self.center_grid_files: - uxgrid = ux.open_grid(grid_file) - - # Return just index without distance on a spherical grid - ind = uxgrid.get_ball_tree(coordinates="face centers", - coordinate_system="spherical", - distance_metric="haversine", - reconstruct=True).query_radius( - [3.0, 3.0], - r=15, - return_distance=False) - - # assert the indexes have been populated - self.assertTrue(len(ind) != 0) - - # Return index and distance on a cartesian grid - d, ind = uxgrid.get_ball_tree(coordinates="face centers", - coordinate_system="cartesian", - distance_metric="minkowski", - reconstruct=True).query_radius( - [0.0, 0.0, 1.0], - r=15, - return_distance=True) - - # assert the distance and indexes have been populated - self.assertTrue(len(d) and len(ind) != 0) - - def test_query(self): - """Test the creation and querying function of the BallTree - structure.""" - for grid_file in self.center_grid_files: - uxgrid = ux.open_grid(grid_file) - - # Test querying with distance and indexes - d, ind = uxgrid.get_ball_tree(coordinates="face centers", - coordinate_system="spherical").query( - [0.0, 0.0], return_distance=True, k=3) - - # assert the distance and indexes have been populated - self.assertTrue(len(d) and len(ind) != 0) - - # Test querying with just indexes - ind = uxgrid.get_ball_tree(coordinates="face centers", - coordinate_system="spherical").query( - [0.0, 0.0], return_distance=False, k=3) - - # assert the indexes have been populated - self.assertTrue(len(ind) != 0) - - - def test_multi_point_query(self): - """Tests a query on multiple points.""" - - for grid_file in self.center_grid_files: - uxgrid = ux.open_grid(grid_file) - - c = [ - [-100, 40], - [-101, 38], - [-102, 38], - ] - - multi_ind = uxgrid.get_ball_tree(coordinates="nodes").query_radius(c, 45) - - for i, cur_c in enumerate(c): - single_ind = ind = uxgrid.get_ball_tree(coordinates="nodes").query_radius(cur_c, 45) - - assert np.array_equal(single_ind, multi_ind[i]) - - - - - -class TestKDTree(TestCase): - corner_grid_files = [gridfile_CSne30, gridfile_mpas] - center_grid_files = [gridfile_mpas] - - def test_construction_from_nodes(self): - """Test the KDTree creation and query function using the grids - nodes.""" - - for grid_file in self.corner_grid_files: - uxgrid = ux.open_grid(grid_file) - d, ind = uxgrid.get_kd_tree(coordinates="nodes").query( - [0.0, 0.0, 1.0], k=5) - - # assert it returns the correct k neighbors and is not empty - self.assertEqual(len(d), len(ind)) - self.assertTrue(len(d) and len(ind) != 0) - - def test_construction_using_spherical_coords(self): - """Test the KDTree creation and query function using spherical - coordinates.""" - - for grid_file in self.corner_grid_files: - uxgrid = ux.open_grid(grid_file) - d, ind = uxgrid.get_kd_tree(coordinates="nodes", - coordinate_system="spherical").query( - [3.0, 3.0], k=5) - - # assert it returns the correct k neighbors and is not empty - self.assertEqual(len(d), len(ind)) - self.assertTrue(len(d) and len(ind) != 0) - - def test_construction_from_face_centers(self): - """Test the KDTree creation and query function using the grids face - centers.""" - - uxgrid = ux.open_grid(self.center_grid_files[0]) - d, ind = uxgrid.get_kd_tree(coordinates="face centers").query( - [1.0, 0.0, 0.0], k=5, return_distance=True) - - # assert it returns the correct k neighbors and is not empty - self.assertEqual(len(d), len(ind)) - self.assertTrue(len(d) and len(ind) != 0) - - def test_construction_from_edge_centers(self): - """Tests the construction of the KDTree with cartesian coordinates on - edge_centers and performs a sample query.""" - - uxgrid = ux.open_grid(self.center_grid_files[0]) - - # Performs a sample query - d, ind = uxgrid.get_kd_tree(coordinates="edge centers").query( - [1.0, 0.0, 1.0], k=2, return_distance=True) - - # assert it returns the correct k neighbors and is not empty - self.assertEqual(len(d), len(ind)) - self.assertTrue(len(d) and len(ind) != 0) - - def test_query_radius(self): - """Test the KDTree creation and query_radius function using the grids - face centers.""" - - uxgrid = ux.open_grid(self.center_grid_files[0]) - - # Test returning distance and indexes - d, ind = uxgrid.get_kd_tree(coordinates="face centers", - coordinate_system="spherical", - reconstruct=True).query_radius( - [3.0, 3.0], r=5, return_distance=True) - - # assert the distance and indexes have been populated - self.assertTrue(len(d), len(ind)) - # Test returning just the indexes - ind = uxgrid.get_kd_tree(coordinates="face centers", - coordinate_system="spherical", - reconstruct=True).query_radius( - [3.0, 3.0], r=5, return_distance=False) - - # assert the indexes have been populated - self.assertTrue(len(ind)) - - def test_query(self): - """Test the creation and querying function of the KDTree structure.""" - uxgrid = ux.open_grid(self.center_grid_files[0]) - - # Test querying with distance and indexes with spherical coordinates - d, ind = uxgrid.get_kd_tree(coordinates="face centers", - coordinate_system="spherical").query( - [0.0, 0.0], return_distance=True) - - # assert the distance and indexes have been populated - assert d, ind - - # Test querying with just indexes with cartesian coordinates - ind = uxgrid.get_kd_tree(coordinates="face centers", - coordinate_system="cartesian", - reconstruct=True).query([0.0, 0.0, 1.0], - return_distance=False) - - # assert the indexes have been populated - assert ind - - def test_multi_point_query(self): - """Tests a query on multiple points.""" - - for grid_file in self.center_grid_files: - uxgrid = ux.open_grid(grid_file) - - c = [ - [0, 0, 0], - [0, 1, 0], - [0, 0, 1], - ] - - multi_ind = uxgrid.get_kd_tree(coordinates="nodes").query_radius(c, 45) - - for i, cur_c in enumerate(c): - single_ind = uxgrid.get_kd_tree(coordinates="nodes").query_radius(cur_c, 45) - - assert np.array_equal(single_ind, multi_ind[i]) +corner_grid_files = [gridfile_CSne30, gridfile_mpas] +center_grid_files = [gridfile_mpas] + +def test_construction_from_nodes(): + """Tests the construction of the ball tree on nodes and performs a sample query.""" + for grid_file in corner_grid_files: + uxgrid = ux.open_grid(grid_file) + d, ind = uxgrid.get_ball_tree(coordinates="nodes").query([3.0, 3.0], k=3) + assert len(d) == len(ind) + assert len(d) > 0 and len(ind) > 0 + +def test_construction_from_face_centers(): + """Tests the construction of the ball tree on center nodes and performs a sample query.""" + for grid_file in center_grid_files: + uxgrid = ux.open_grid(grid_file) + d, ind = uxgrid.get_ball_tree(coordinates="face centers").query([3.0, 3.0], k=3) + assert len(d) == len(ind) + assert len(d) > 0 and len(ind) > 0 + +def test_construction_from_edge_centers(): + """Tests the construction of the ball tree on edge_centers and performs a sample query.""" + for grid_file in center_grid_files: + uxgrid = ux.open_grid(grid_file) + d, ind = uxgrid.get_ball_tree(coordinates="edge centers").query([3.0, 3.0], k=3) + assert len(d) == len(ind) + assert len(d) > 0 and len(ind) > 0 + +def test_construction_from_both_sequentially(): + """Tests the construction of the ball tree on center nodes and performs a sample query.""" + for grid_file in center_grid_files: + uxgrid = ux.open_grid(grid_file) + d, ind = uxgrid.get_ball_tree(coordinates="nodes").query([3.0, 3.0], k=3) + d_centers, ind_centers = uxgrid.get_ball_tree(coordinates="face centers").query([3.0, 3.0], k=3) + +def test_antimeridian_distance_nodes(): + """Verifies nearest neighbor search across Antimeridian.""" + verts = [(0.0, 90.0), (-180, 0.0), (0.0, -90)] + uxgrid = ux.open_grid(verts, latlon=True) + d, ind = uxgrid.get_ball_tree(coordinates="nodes").query([180.0, 0.0], k=1) + assert np.isclose(d, 0.0) + assert ind == 0 + d, ind = uxgrid.get_ball_tree(coordinates="nodes").query_radius([-180, 0.0], r=90.01, return_distance=True) + expected_d = np.array([0.0, 90.0, 90.0]) + assert np.allclose(a=d, b=expected_d, atol=1e-03) + +def test_antimeridian_distance_face_centers(): + """TODO: Write addition tests once construction and representation of face centers is implemented.""" + pass + +def test_construction_using_cartesian_coords(): + """Test the BallTree creation and query function using cartesian coordinates.""" + for grid_file in corner_grid_files: + uxgrid = ux.open_grid(grid_file) + d, ind = uxgrid.get_ball_tree(coordinates="nodes", coordinate_system="cartesian", distance_metric="minkowski").query([1.0, 0.0, 0.0], k=2) + assert len(d) == len(ind) + assert len(d) > 0 and len(ind) > 0 + +def test_query_radius(): + """Test the BallTree creation and query_radius function using the grids face centers.""" + for grid_file in center_grid_files: + uxgrid = ux.open_grid(grid_file) + ind = uxgrid.get_ball_tree(coordinates="face centers", coordinate_system="spherical", distance_metric="haversine", reconstruct=True).query_radius([3.0, 3.0], r=15, return_distance=False) + assert len(ind) > 0 + d, ind = uxgrid.get_ball_tree(coordinates="face centers", coordinate_system="cartesian", distance_metric="minkowski", reconstruct=True).query_radius([0.0, 0.0, 1.0], r=15, return_distance=True) + assert len(d) > 0 and len(ind) > 0 + +def test_query(): + """Test the creation and querying function of the BallTree structure.""" + for grid_file in center_grid_files: + uxgrid = ux.open_grid(grid_file) + d, ind = uxgrid.get_ball_tree(coordinates="face centers", coordinate_system="spherical").query([0.0, 0.0], return_distance=True, k=3) + assert len(d) > 0 and len(ind) > 0 + ind = uxgrid.get_ball_tree(coordinates="face centers", coordinate_system="spherical").query([0.0, 0.0], return_distance=False, k=3) + assert len(ind) > 0 + +def test_multi_point_query(): + """Tests a query on multiple points.""" + for grid_file in center_grid_files: + uxgrid = ux.open_grid(grid_file) + c = [[-100, 40], [-101, 38], [-102, 38]] + multi_ind = uxgrid.get_ball_tree(coordinates="nodes").query_radius(c, 45) + for i, cur_c in enumerate(c): + single_ind = uxgrid.get_ball_tree(coordinates="nodes").query_radius(cur_c, 45) + assert np.array_equal(single_ind, multi_ind[i]) + +# KDTree tests +def test_kdtree_construction_from_nodes(): + """Test the KDTree creation and query function using the grids nodes.""" + for grid_file in corner_grid_files: + uxgrid = ux.open_grid(grid_file) + d, ind = uxgrid.get_kd_tree(coordinates="nodes").query([0.0, 0.0, 1.0], k=5) + assert len(d) == len(ind) + assert len(d) > 0 and len(ind) > 0 + +def test_kdtree_construction_using_spherical_coords(): + """Test the KDTree creation and query function using spherical coordinates.""" + for grid_file in corner_grid_files: + uxgrid = ux.open_grid(grid_file) + d, ind = uxgrid.get_kd_tree(coordinates="nodes", coordinate_system="spherical").query([3.0, 3.0], k=5) + assert len(d) == len(ind) + assert len(d) > 0 and len(ind) > 0 + +def test_kdtree_construction_from_face_centers(): + """Test the KDTree creation and query function using the grids face centers.""" + uxgrid = ux.open_grid(center_grid_files[0]) + d, ind = uxgrid.get_kd_tree(coordinates="face centers").query([1.0, 0.0, 0.0], k=5, return_distance=True) + assert len(d) == len(ind) + assert len(d) > 0 and len(ind) > 0 + +def test_kdtree_construction_from_edge_centers(): + """Tests the construction of the KDTree with cartesian coordinates on edge_centers and performs a sample query.""" + uxgrid = ux.open_grid(center_grid_files[0]) + d, ind = uxgrid.get_kd_tree(coordinates="edge centers").query([1.0, 0.0, 1.0], k=2, return_distance=True) + assert len(d) == len(ind) + assert len(d) > 0 and len(ind) > 0 + +def test_kdtree_query_radius(): + """Test the KDTree creation and query_radius function using the grids face centers.""" + uxgrid = ux.open_grid(center_grid_files[0]) + d, ind = uxgrid.get_kd_tree(coordinates="face centers", coordinate_system="spherical", reconstruct=True).query_radius([3.0, 3.0], r=5, return_distance=True) + assert len(d) > 0 and len(ind) > 0 + ind = uxgrid.get_kd_tree(coordinates="face centers", coordinate_system="spherical", reconstruct=True).query_radius([3.0, 3.0], r=5, return_distance=False) + assert len(ind) > 0 + +def test_kdtree_query(): + """Test the creation and querying function of the KDTree structure.""" + uxgrid = ux.open_grid(center_grid_files[0]) + d, ind = uxgrid.get_kd_tree(coordinates="face centers", coordinate_system="spherical").query([0.0, 0.0], return_distance=True) + assert d, ind + ind = uxgrid.get_kd_tree(coordinates="face centers", coordinate_system="cartesian", reconstruct=True).query([0.0, 0.0, 1.0], return_distance=False) + assert ind + +def test_kdtree_multi_point_query(): + """Tests a query on multiple points.""" + for grid_file in center_grid_files: + uxgrid = ux.open_grid(grid_file) + c = [[0, 0, 0], [0, 1, 0], [0, 0, 1]] + multi_ind = uxgrid.get_kd_tree(coordinates="nodes").query_radius(c, 45) + for i, cur_c in enumerate(c): + single_ind = uxgrid.get_kd_tree(coordinates="nodes").query_radius(cur_c, 45) + assert np.array_equal(single_ind, multi_ind[i]) diff --git a/test/test_placeholder.py b/test/test_placeholder.py index 59b0acc4b..24e677f02 100644 --- a/test/test_placeholder.py +++ b/test/test_placeholder.py @@ -1,17 +1,12 @@ import sys -from unittest import TestCase - import xarray as xr -# Import from directory structure if coverage test, or from installed -# packages otherwise +# Import from directory structure if coverage test, or from installed packages otherwise if "--cov" in str(sys.argv): import uxarray else: import uxarray - -class test_placeholder(TestCase): - - def test_placeholder(self): - pass +def test_placeholder(): + """Placeholder test that currently does nothing.""" + pass diff --git a/test/test_plot.py b/test/test_plot.py index 62bcf08f6..24fa081a7 100644 --- a/test/test_plot.py +++ b/test/test_plot.py @@ -1,112 +1,89 @@ import os import uxarray as ux import holoviews as hv - - -from unittest import TestCase +import pytest from pathlib import Path current_path = Path(os.path.dirname(os.path.realpath(__file__))) gridfile_geoflow = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "grid.nc" datafile_geoflow = current_path / "meshfiles" / "ugrid" / "geoflow-small" / "v1.nc" - gridfile_mpas = current_path / "meshfiles" / "mpas" / "QU" / "oQU480.231010.nc" - gridfile_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" - datafile_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30_vortex.nc" grid_files = [gridfile_geoflow, gridfile_mpas] - grid_plot_routines = ['points', 'nodes', 'node_coords', ''] - -class TestPlot(TestCase): - - def test_topology(self): - """Tests execution on Grid elements.""" - uxgrid = ux.open_grid(gridfile_mpas) - - for backend in ['matplotlib', 'bokeh']: - uxgrid.plot(backend=backend) - uxgrid.plot.mesh(backend=backend) - uxgrid.plot.edges(backend=backend) - uxgrid.plot.nodes(backend=backend) - uxgrid.plot.node_coords(backend=backend) - uxgrid.plot.corner_nodes(backend=backend) - uxgrid.plot.face_centers(backend=backend) - uxgrid.plot.face_coords(backend=backend) - uxgrid.plot.edge_centers(backend=backend) - uxgrid.plot.edge_coords(backend=backend) - - def test_face_centered_data(self): - """Tests execution of plotting methods on face-centered data.""" - - uxds = ux.open_dataset(gridfile_mpas, gridfile_mpas) - - for backend in ['matplotlib', 'bokeh']: - assert(isinstance(uxds['bottomDepth'].plot(backend=backend, dynamic=True), hv.DynamicMap)) - assert(isinstance(uxds['bottomDepth'].plot.polygons(backend=backend, dynamic=True), hv.DynamicMap)) - assert(isinstance(uxds['bottomDepth'].plot.points(backend=backend), hv.Points)) - - def test_face_centered_remapped_dim(self): - """Tests execution of plotting method on a data variable whose - dimension needed to be re-mapped.""" - uxds = ux.open_dataset(gridfile_ne30, datafile_ne30) - - for backend in ['matplotlib', 'bokeh']: - assert(isinstance(uxds['psi'].plot(backend=backend, dynamic=True), hv.DynamicMap)) - assert(isinstance(uxds['psi'].plot.polygons(backend=backend, dynamic=True), hv.DynamicMap)) - assert(isinstance(uxds['psi'].plot.points(backend=backend), hv.Points)) - - - def test_node_centered_data(self): - """Tests execution of plotting methods on node-centered data.""" - - uxds = ux.open_dataset(gridfile_geoflow, datafile_geoflow) - - for backend in ['matplotlib', 'bokeh']: - assert(isinstance(uxds['v1'][0][0].plot(backend=backend), hv.Points)) - - assert(isinstance(uxds['v1'][0][0].plot.points(backend=backend), hv.Points)) - - assert(isinstance(uxds['v1'][0][0].topological_mean(destination='face').plot.polygons(backend=backend, dynamic=True), hv.DynamicMap)) - - - - def test_clabel(self): - """Tests the execution of passing in a custom clabel.""" - - uxds = ux.open_dataset(gridfile_geoflow, datafile_geoflow) - - raster_no_clabel = uxds['v1'][0][0].plot.rasterize(method='point') - raster_with_clabel = uxds['v1'][0][0].plot.rasterize(method='point', clabel='Foo') - - def test_engine(self): - uxds = ux.open_dataset(gridfile_mpas, gridfile_mpas) - _plot_sp = uxds['bottomDepth'].plot.polygons(rasterize=True, dynamic=True, engine='spatialpandas') - _plot_gp = uxds['bottomDepth'].plot.polygons(rasterize=True, dynamic=True, engine='geopandas') - - assert isinstance(_plot_sp, hv.DynamicMap) - assert isinstance(_plot_gp, hv.DynamicMap) - - - -class TestXarrayMethods(TestCase): - - def test_dataset(self): - """Tests whether a Xarray DataArray method can be called through the - UxDataArray plotting accessor.""" - uxds = ux.open_dataset(gridfile_geoflow, datafile_geoflow) - - # plot.hist() is an xarray method - assert hasattr(uxds['v1'].plot, 'hist') - - def test_dataarray(self): - """Tests whether a Xarray Dataset method can be called through the - UxDataset plotting accessor.""" - uxds = ux.open_dataset(gridfile_geoflow, datafile_geoflow) - - # plot.scatter() is an xarray method - assert hasattr(uxds.plot, 'scatter') +def test_topology(): + """Tests execution on Grid elements.""" + uxgrid = ux.open_grid(gridfile_mpas) + + for backend in ['matplotlib', 'bokeh']: + uxgrid.plot(backend=backend) + uxgrid.plot.mesh(backend=backend) + uxgrid.plot.edges(backend=backend) + uxgrid.plot.nodes(backend=backend) + uxgrid.plot.node_coords(backend=backend) + uxgrid.plot.corner_nodes(backend=backend) + uxgrid.plot.face_centers(backend=backend) + uxgrid.plot.face_coords(backend=backend) + uxgrid.plot.edge_centers(backend=backend) + uxgrid.plot.edge_coords(backend=backend) + +def test_face_centered_data(): + """Tests execution of plotting methods on face-centered data.""" + uxds = ux.open_dataset(gridfile_mpas, gridfile_mpas) + + for backend in ['matplotlib', 'bokeh']: + assert isinstance(uxds['bottomDepth'].plot(backend=backend, dynamic=True), hv.DynamicMap) + assert isinstance(uxds['bottomDepth'].plot.polygons(backend=backend, dynamic=True), hv.DynamicMap) + assert isinstance(uxds['bottomDepth'].plot.points(backend=backend), hv.Points) + +def test_face_centered_remapped_dim(): + """Tests execution of plotting method on a data variable whose dimension needed to be re-mapped.""" + uxds = ux.open_dataset(gridfile_ne30, datafile_ne30) + + for backend in ['matplotlib', 'bokeh']: + assert isinstance(uxds['psi'].plot(backend=backend, dynamic=True), hv.DynamicMap) + assert isinstance(uxds['psi'].plot.polygons(backend=backend, dynamic=True), hv.DynamicMap) + assert isinstance(uxds['psi'].plot.points(backend=backend), hv.Points) + +def test_node_centered_data(): + """Tests execution of plotting methods on node-centered data.""" + uxds = ux.open_dataset(gridfile_geoflow, datafile_geoflow) + + for backend in ['matplotlib', 'bokeh']: + assert isinstance(uxds['v1'][0][0].plot(backend=backend), hv.Points) + assert isinstance(uxds['v1'][0][0].plot.points(backend=backend), hv.Points) + assert isinstance(uxds['v1'][0][0].topological_mean(destination='face').plot.polygons(backend=backend, dynamic=True), hv.DynamicMap) + +def test_clabel(): + """Tests the execution of passing in a custom clabel.""" + uxds = ux.open_dataset(gridfile_geoflow, datafile_geoflow) + + raster_no_clabel = uxds['v1'][0][0].plot.rasterize(method='point') + raster_with_clabel = uxds['v1'][0][0].plot.rasterize(method='point', clabel='Foo') + +def test_engine(): + """Tests different plotting engines.""" + uxds = ux.open_dataset(gridfile_mpas, gridfile_mpas) + _plot_sp = uxds['bottomDepth'].plot.polygons(rasterize=True, dynamic=True, engine='spatialpandas') + _plot_gp = uxds['bottomDepth'].plot.polygons(rasterize=True, dynamic=True, engine='geopandas') + + assert isinstance(_plot_sp, hv.DynamicMap) + assert isinstance(_plot_gp, hv.DynamicMap) + +def test_dataset_methods(): + """Tests whether a Xarray DataArray method can be called through the UxDataArray plotting accessor.""" + uxds = ux.open_dataset(gridfile_geoflow, datafile_geoflow) + + # plot.hist() is an xarray method + assert hasattr(uxds['v1'].plot, 'hist') + +def test_dataarray_methods(): + """Tests whether a Xarray Dataset method can be called through the UxDataset plotting accessor.""" + uxds = ux.open_dataset(gridfile_geoflow, datafile_geoflow) + + # plot.scatter() is an xarray method + assert hasattr(uxds.plot, 'scatter') diff --git a/test/test_projection.py b/test/test_projection.py index 9eb79b3c5..85940ba8b 100644 --- a/test/test_projection.py +++ b/test/test_projection.py @@ -1,6 +1,5 @@ import uxarray as ux import cartopy.crs as ccrs - import os from pathlib import Path @@ -8,9 +7,10 @@ gridfile_geos_cs = current_path / "meshfiles" / "geos-cs" / "c12" / "test-c12.native.nc4" - - def test_geodataframe_projection(): + """Test the projection of a GeoDataFrame.""" uxgrid = ux.open_grid(gridfile_geos_cs) - gdf = uxgrid.to_geodataframe(projection=ccrs.Robinson(), periodic_elements='exclude') + + # Example assertion to check if gdf is not None + assert gdf is not None diff --git a/test/test_remap.py b/test/test_remap.py index 6fb9b7535..9c375f770 100644 --- a/test/test_remap.py +++ b/test/test_remap.py @@ -1,15 +1,12 @@ import os import numpy as np - -from unittest import TestCase -from pathlib import Path import numpy.testing as nt +import pytest +from pathlib import Path import uxarray as ux - from uxarray.core.dataset import UxDataset from uxarray.core.dataarray import UxDataArray - from uxarray.remap.inverse_distance_weighted import _inverse_distance_weighted_remap from uxarray.remap.nearest_neighbor import _nearest_neighbor @@ -24,408 +21,246 @@ mpasfile_QU = current_path / "meshfiles" / "mpas" / "QU" / "mesh.QU.1920km.151026.nc" -class TestNearestNeighborRemap(TestCase): - """Tests for nearest neighbor remapping.""" +def test_remap_to_same_grid_corner_nodes(): + """Test remapping to the same dummy 3-vertex grid. Corner nodes case.""" + source_verts = np.array([(0.0, 90.0), (-180, 0.0), (0.0, -90)]) + source_data_single_dim = [1.0, 2.0, 3.0] + source_grid = ux.open_grid(source_verts) + destination_grid = ux.open_grid(source_verts) + + destination_single_data = _nearest_neighbor(source_grid, + destination_grid, + source_data_single_dim, + remap_to="nodes") - def test_remap_to_same_grid_corner_nodes(self): - """Test remapping to the same dummy 3-vertex grid. + source_data_multi_dim = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0]]) - Corner nodes case. - """ - # single triangle with point on antimeridian - source_verts = np.array([(0.0, 90.0), (-180, 0.0), (0.0, -90)]) - source_data_single_dim = [1.0, 2.0, 3.0] - source_grid = ux.open_grid(source_verts) - destination_grid = ux.open_grid(source_verts) + destination_multi_data = _nearest_neighbor(source_grid, + destination_grid, + source_data_multi_dim, + remap_to="nodes") - destination_single_data = _nearest_neighbor(source_grid, - destination_grid, - source_data_single_dim, - remap_to="nodes") + nt.assert_array_equal(source_data_single_dim, destination_single_data) + nt.assert_array_equal(source_data_multi_dim, destination_multi_data) - source_data_multi_dim = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], - [7.0, 8.0, 9.0]]) - destination_multi_data = _nearest_neighbor(source_grid, - destination_grid, - source_data_multi_dim, - remap_to="nodes") +def test_remap_to_corner_nodes_cartesian(): + """Test remapping to the same dummy 3-vertex grid, using cartesian coordinates. Corner nodes case.""" + source_verts = np.array([(0.0, 0.0, 1.0), (0.0, 1.0, 0.0), (1.0, 0.0, 0.0)]) + source_data_single_dim = [1.0, 2.0, 3.0] + source_grid = ux.open_grid(source_verts) + destination_grid = ux.open_grid(source_verts) - assert np.array_equal(source_data_single_dim, destination_single_data) - assert np.array_equal(source_data_multi_dim, destination_multi_data) + destination_data = _nearest_neighbor(source_grid, + destination_grid, + source_data_single_dim, + remap_to="nodes", + coord_type="cartesian") - def test_remap_to_corner_nodes_cartesian(self): - """Test remapping to the same dummy 3-vertex grid, using cartesian - coordinates. + nt.assert_array_equal(source_data_single_dim, destination_data) - Corner nodes case. - """ - # single triangle - source_verts = np.array([(0.0, 0.0, 1.0), (0.0, 1.0, 0.0), - (1.0, 0.0, 0.0)]) - source_data_single_dim = [1.0, 2.0, 3.0] +def test_nn_remap(): + """Test nearest neighbor remapping.""" + uxds = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) + uxgrid = ux.open_grid(gridfile_ne30) + uxda = uxds['v1'] + out_da = uxda.remap.nearest_neighbor(destination_grid=uxgrid, remap_to="nodes") - # open the source and destination grids - source_grid = ux.open_grid(source_verts) - destination_grid = ux.open_grid(source_verts) + assert len(out_da) != 0 - # create the destination data using the nearest neighbor function - destination_data = _nearest_neighbor(source_grid, - destination_grid, - source_data_single_dim, - remap_to="nodes", - coord_type="cartesian") - # assert that the source and destination data are the same - assert np.array_equal(source_data_single_dim, destination_data) +def test_remap_return_types(): + """Tests the return type of the `UxDataset` and `UxDataArray` implementations of Nearest Neighbor Remapping.""" + source_data_paths = [dsfile_v1_geoflow, dsfile_v2_geoflow, dsfile_v3_geoflow] + source_uxds = ux.open_mfdataset(gridfile_geoflow, source_data_paths) + destination_grid = ux.open_grid(gridfile_CSne30) - def test_nn_remap(self): - """Test nearest neighbor remapping. + remap_uxda_to_grid = source_uxds['v1'].remap.nearest_neighbor(destination_grid) - Steps: - 1. Open a grid and a dataset, - 2. Open the grid to remap dataset in 1 - 3. Remap the dataset in 1 to the grid in 2 - """ - uxds = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) + assert isinstance(remap_uxda_to_grid, UxDataArray) - uxgrid = ux.open_grid(gridfile_ne30) + remap_uxds_to_grid = source_uxds.remap.nearest_neighbor(destination_grid) - uxda = uxds['v1'] - out_da = uxda.remap.nearest_neighbor(destination_grid=uxgrid, remap_to="nodes") + assert isinstance(remap_uxds_to_grid, UxDataset) + assert len(remap_uxds_to_grid.data_vars) == 3 - # Assert the remapping was successful and the variable is populated - self.assertTrue(len(out_da) != 0) - def test_remap_return_types(self): - """Tests the return type of the `UxDataset` and `UxDataArray` - implementations of Nearest Neighbor Remapping.""" - source_data_paths = [ - dsfile_v1_geoflow, dsfile_v2_geoflow, dsfile_v3_geoflow - ] - source_uxds = ux.open_mfdataset(gridfile_geoflow, source_data_paths) - destination_grid = ux.open_grid(gridfile_CSne30) +def test_edge_centers_remapping(): + """Tests the ability to remap on edge centers using Nearest Neighbor Remapping.""" + source_grid = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) + destination_grid = ux.open_grid(mpasfile_QU) - remap_uxda_to_grid = source_uxds['v1'].remap.nearest_neighbor( - destination_grid) + remap_to_edge_centers_spherical = source_grid['v1'].remap.nearest_neighbor(destination_grid=destination_grid, + remap_to="edge centers", coord_type='spherical') - assert isinstance(remap_uxda_to_grid, UxDataArray) + remap_to_edge_centers_cartesian = source_grid['v1'].remap.nearest_neighbor(destination_grid=destination_grid, + remap_to="edge centers", coord_type='cartesian') - remap_uxds_to_grid = source_uxds.remap.nearest_neighbor( - destination_grid) + assert remap_to_edge_centers_spherical._edge_centered() + assert remap_to_edge_centers_cartesian._edge_centered() - # Dataset with three vars: remapped "v1, v2, v3" - assert isinstance(remap_uxds_to_grid, UxDataset) - assert len(remap_uxds_to_grid.data_vars) == 3 - def test_edge_centers_remapping(self): - """Tests the ability to remap on edge centers using Nearest Neighbor - Remapping.""" +def test_overwrite(): + """Tests that the remapping no longer overwrites the dataset.""" + source_grid = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) + destination_dataset = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) - # Open source and destination datasets to remap to - source_grid = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) - destination_grid = ux.open_grid(mpasfile_QU) + remap_to_edge_centers = source_grid['v1'].remap.nearest_neighbor(destination_grid=destination_dataset.uxgrid, + remap_to="face centers", coord_type='cartesian') - remap_to_edge_centers_spherical = source_grid['v1'].remap.nearest_neighbor(destination_grid=destination_grid, - remap_to="edge centers", coord_type='spherical') + assert not np.array_equal(destination_dataset['v1'], remap_to_edge_centers) - remap_to_edge_centers_cartesian = source_grid['v1'].remap.nearest_neighbor(destination_grid=destination_grid, - remap_to="edge centers", coord_type='cartesian') - # Assert the data variable lies on the "edge centers" - self.assertTrue(remap_to_edge_centers_spherical._edge_centered()) - self.assertTrue(remap_to_edge_centers_cartesian._edge_centered()) - - def test_overwrite(self): - """Tests that the remapping no longer overwrites the dataset.""" +def test_source_data_remap(): + """Test the remapping of all source data positions.""" + source_uxds = ux.open_dataset(mpasfile_QU, mpasfile_QU) + destination_grid = ux.open_grid(gridfile_geoflow) + + face_centers = source_uxds['latCell'].remap.nearest_neighbor(destination_grid=destination_grid, remap_to="nodes") + nodes = source_uxds['latVertex'].remap.nearest_neighbor(destination_grid=destination_grid, remap_to="nodes") + edges = source_uxds['angleEdge'].remap.nearest_neighbor(destination_grid=destination_grid, remap_to="nodes") + + assert len(face_centers.values) != 0 + assert len(nodes.values) != 0 + assert len(edges.values) != 0 + + +def test_value_errors(): + """Tests the raising of value errors and warnings in the function.""" + source_uxds = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) + source_uxds_2 = ux.open_dataset(mpasfile_QU, mpasfile_QU) + destination_grid = ux.open_grid(gridfile_geoflow) + + with nt.assert_raises(ValueError): + source_uxds['v1'].remap.nearest_neighbor(destination_grid=destination_grid, remap_to="test", coord_type='spherical') + with nt.assert_raises(ValueError): + source_uxds['v1'].remap.nearest_neighbor(destination_grid=destination_grid, remap_to="test", coord_type="cartesian") + with nt.assert_raises(ValueError): + source_uxds['v1'].remap.nearest_neighbor(destination_grid=destination_grid, remap_to="nodes", coord_type="test") + with nt.assert_raises(ValueError): + source_uxds_2['cellsOnCell'].remap.nearest_neighbor(destination_grid=destination_grid, remap_to="nodes") + +def test_preserve_coordinates(): + """Tests if coordinates are preserved after remapping.""" + source_uxds = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) + destination_grid = ux.open_grid(mpasfile_QU) + + res = source_uxds.remap.nearest_neighbor(destination_grid=destination_grid) + + assert "time" in res.coords + + +# Inverse Distance Weighted Remapping Tests +def test_remap_center_nodes(): + """Test remapping to center nodes.""" + dataset = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) + destination_grid = ux.open_grid(gridfile_geoflow) + + data_on_face_centers = dataset['v1'].remap.inverse_distance_weighted(destination_grid, remap_to="face centers", power=6) + + assert not np.array_equal(dataset['v1'], data_on_face_centers) + + +def test_remap_nodes(): + """Test remapping to nodes.""" + dataset = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) + destination_grid = ux.open_grid(gridfile_geoflow) + + data_on_nodes = dataset['v1'].remap.inverse_distance_weighted(destination_grid, remap_to="nodes") + + assert not np.array_equal(dataset['v1'], data_on_nodes) + + +def test_cartesian_remap_to_nodes(): + """Test remapping using cartesian coordinates.""" + source_verts = np.array([(0.0, 0.0, 1.0), (0.0, 1.0, 0.0), (1.0, 0.0, 0.0)]) + source_data = [1.0, 2.0, 3.0] + source_grid = ux.open_grid(source_verts) + destination_grid = ux.open_grid(source_verts) + + destination_data_neighbors_2 = _inverse_distance_weighted_remap(source_grid, destination_grid, source_data, remap_to="nodes", coord_type="cartesian", k=3) + destination_data_neighbors_1 = _inverse_distance_weighted_remap(source_grid, destination_grid, source_data, remap_to="nodes", coord_type="cartesian", k=2) + + assert not np.array_equal(destination_data_neighbors_1, destination_data_neighbors_2) + + +def test_remap_return_types_idw(): + """Tests the return type of the `UxDataset` and `UxDataArray` implementations of Inverse Distance Weighted.""" + source_data_paths = [dsfile_v1_geoflow, dsfile_v2_geoflow, dsfile_v3_geoflow] + source_uxds = ux.open_mfdataset(gridfile_geoflow, source_data_paths) + destination_grid = ux.open_grid(gridfile_CSne30) + + remap_uxda_to_grid = source_uxds['v1'].remap.inverse_distance_weighted(destination_grid, power=3, k=10) + + assert isinstance(remap_uxda_to_grid, UxDataArray) + assert len(remap_uxda_to_grid) == 1 + + remap_uxds_to_grid = source_uxds.remap.inverse_distance_weighted(destination_grid) + + assert isinstance(remap_uxds_to_grid, UxDataset) + assert len(remap_uxds_to_grid.data_vars) == 3 + + +def test_edge_remapping(): + """Tests the ability to remap on edge centers using Inverse Distance Weighted Remapping.""" + source_grid = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) + destination_grid = ux.open_grid(mpasfile_QU) + + remap_to_edge_centers_spherical = source_grid['v1'].remap.inverse_distance_weighted(destination_grid=destination_grid, remap_to="edge centers", coord_type='spherical') + remap_to_edge_centers_cartesian = source_grid['v1'].remap.inverse_distance_weighted(destination_grid=destination_grid, remap_to="edge centers", coord_type='cartesian') + + assert remap_to_edge_centers_spherical._edge_centered() + assert remap_to_edge_centers_cartesian._edge_centered() + + +def test_overwrite_idw(): + """Tests that the remapping no longer overwrites the dataset.""" + source_grid = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) + destination_dataset = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) + + remap_to_edge_centers = source_grid['v1'].remap.inverse_distance_weighted(destination_grid=destination_dataset.uxgrid, remap_to="face centers", coord_type='cartesian') + + assert not np.array_equal(destination_dataset['v1'], remap_to_edge_centers) + - # Open source and destination datasets to remap to - source_grid = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) - destination_dataset = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) +def test_source_data_remap_idw(): + """Test the remapping of all source data positions.""" + source_uxds = ux.open_dataset(mpasfile_QU, mpasfile_QU) + destination_grid = ux.open_grid(gridfile_geoflow) - # Perform remapping - remap_to_edge_centers = source_grid['v1'].remap.nearest_neighbor(destination_grid=destination_dataset.uxgrid, - remap_to="face centers", coord_type='cartesian') + face_centers = source_uxds['latCell'].remap.inverse_distance_weighted(destination_grid=destination_grid, remap_to="nodes") + nodes = source_uxds['latVertex'].remap.inverse_distance_weighted(destination_grid=destination_grid, remap_to="nodes") + edges = source_uxds['angleEdge'].remap.inverse_distance_weighted(destination_grid=destination_grid, remap_to="nodes") - # Assert the remapped data is different from the original data - assert not np.array_equal(destination_dataset['v1'], remap_to_edge_centers) + assert len(face_centers.values) != 0 + assert len(nodes.values) != 0 + assert len(edges.values) != 0 - def test_source_data_remap(self): - """Test the remapping of all source data positions.""" - # Open source and destination datasets to remap to - source_uxds = ux.open_dataset(mpasfile_QU, mpasfile_QU) - destination_grid = ux.open_grid(gridfile_geoflow) +def test_value_errors_idw(): + """Tests the raising of value errors and warnings in the function.""" + source_uxds = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) + source_uxds_2 = ux.open_dataset(mpasfile_QU, mpasfile_QU) + destination_grid = ux.open_grid(gridfile_geoflow) - # Remap from `face_centers` - face_centers = source_uxds['latCell'].remap.nearest_neighbor( - destination_grid=destination_grid, - remap_to="nodes" - ) + with nt.assert_raises(ValueError): + source_uxds['v1'].remap.inverse_distance_weighted(destination_grid=destination_grid, remap_to="nodes", k=1) - # Remap from `nodes` - nodes = source_uxds['latVertex'].remap.nearest_neighbor( - destination_grid=destination_grid, - remap_to="nodes" - ) + with nt.assert_raises(ValueError): + source_uxds['v1'].remap.inverse_distance_weighted(destination_grid=destination_grid, remap_to="nodes", k=source_uxds.uxgrid.n_node + 1) - # Remap from `edges` - edges = source_uxds['angleEdge'].remap.nearest_neighbor( - destination_grid=destination_grid, - remap_to="nodes" - ) - - self.assertTrue(len(face_centers.values) != 0) - self.assertTrue(len(nodes.values) != 0) - self.assertTrue(len(edges.values) != 0) - - def test_value_errors(self): - """Tests the raising of value errors and warnings in the function.""" - - # Open source and destination datasets to remap to - source_uxds = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) - source_uxds_2 = ux.open_dataset(mpasfile_QU, mpasfile_QU) - destination_grid = ux.open_grid(gridfile_geoflow) - - # Raise ValueError when `remap_to` is invalid - with nt.assert_raises(ValueError): - source_uxds['v1'].remap.nearest_neighbor( - destination_grid=destination_grid, - remap_to="test", coord_type='spherical' - ) - with nt.assert_raises(ValueError): - source_uxds['v1'].remap.nearest_neighbor( - destination_grid=destination_grid, - remap_to="test", coord_type="cartesian" - ) - - # Raise ValueError when `coord_type` is invalid - with nt.assert_raises(ValueError): - source_uxds['v1'].remap.nearest_neighbor( - destination_grid=destination_grid, - remap_to="nodes", coord_type="test" - ) - - # Raise ValueError when the source data is invalid - with nt.assert_raises(ValueError): - source_uxds_2['cellsOnCell'].remap.nearest_neighbor( - destination_grid=destination_grid, - remap_to="nodes" - ) - - def test_preserve_coordinates(self): - # Dataset with 'time' coordinates - source_uxds = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) - destination_grid = ux.open_grid(mpasfile_QU) + with nt.assert_raises(ValueError): + source_uxds['v1'].remap.inverse_distance_weighted(destination_grid=destination_grid, remap_to="test", k=2, coord_type='spherical') + with nt.assert_raises(ValueError): + source_uxds['v1'].remap.inverse_distance_weighted(destination_grid=destination_grid, remap_to="test", k=2, coord_type="cartesian") - res = source_uxds.remap.nearest_neighbor(destination_grid=destination_grid) - - assert "time" in res.coords + with nt.assert_raises(ValueError): + source_uxds['v1'].remap.inverse_distance_weighted(destination_grid=destination_grid, remap_to="nodes", k=2, coord_type="test") + with nt.assert_raises(ValueError): + source_uxds_2['cellsOnCell'].remap.inverse_distance_weighted(destination_grid=destination_grid, remap_to="nodes") - -class TestInverseDistanceWeightedRemapping(TestCase): - """Testing for inverse distance weighted remapping.""" - - def test_remap_center_nodes(self): - """Test remapping to center nodes.""" - - # datasets to use for remap - dataset = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) - destination_grid = ux.open_grid(gridfile_geoflow) - - data_on_face_centers = dataset['v1'].remap.inverse_distance_weighted( - destination_grid, remap_to="face centers", power=6) - - assert not np.array_equal(dataset['v1'], data_on_face_centers) - - def test_remap_nodes(self): - """Test remapping to nodes.""" - - # datasets to use for remap - dataset = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) - destination_grid = ux.open_grid(gridfile_geoflow) - - data_on_nodes = dataset['v1'].remap.inverse_distance_weighted( - destination_grid, remap_to="nodes") - - assert not np.array_equal(dataset['v1'], data_on_nodes) - - def test_cartesian_remap_to_nodes(self): - """Test remapping using cartesian coordinates using nodes.""" - - # triangle with data on nodes - source_verts = np.array([(0.0, 0.0, 1.0), (0.0, 1.0, 0.0), - (1.0, 0.0, 0.0)]) - source_data = [1.0, 2.0, 3.0] - - # open the source and destination grids - source_grid = ux.open_grid(source_verts) - destination_grid = ux.open_grid(source_verts) - - # create the first destination data using two k neighbors - destination_data_neighbors_2 = _inverse_distance_weighted_remap( - source_grid, - destination_grid, - source_data, - remap_to="nodes", - coord_type="cartesian", - k=3) - - # create the second destination data using one k neighbor - destination_data_neighbors_1 = _inverse_distance_weighted_remap( - source_grid, - destination_grid, - source_data, - remap_to="nodes", - coord_type="cartesian", - k=2) - - # two different k remaps are different - assert not np.array_equal(destination_data_neighbors_1, - destination_data_neighbors_2) - - def test_remap_return_types(self): - """Tests the return type of the `UxDataset` and `UxDataArray` - implementations of Inverse Distance Weighted.""" - - source_data_paths = [ - dsfile_v1_geoflow, dsfile_v2_geoflow, dsfile_v3_geoflow - ] - source_uxds = ux.open_mfdataset(gridfile_geoflow, source_data_paths) - destination_grid = ux.open_grid(gridfile_CSne30) - - remap_uxda_to_grid = source_uxds['v1'].remap.inverse_distance_weighted( - destination_grid, power=3, k=10) - - assert isinstance(remap_uxda_to_grid, UxDataArray) - assert len(remap_uxda_to_grid) == 1 - - remap_uxds_to_grid = source_uxds.remap.inverse_distance_weighted( - destination_grid) - - # Dataset with three vars: remapped "v1, v2, v3" - assert isinstance(remap_uxds_to_grid, UxDataset) - assert len(remap_uxds_to_grid.data_vars) == 3 - - def test_edge_remapping(self): - """Tests the ability to remap on edge centers using Inverse Distance - Weighted Remapping.""" - - # Open source and destination datasets to remap to - source_grid = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) - destination_grid = ux.open_grid(mpasfile_QU) - - # Perform remapping to the edge centers of the dataset - - remap_to_edge_centers_spherical = source_grid['v1'].remap.inverse_distance_weighted( - destination_grid=destination_grid, - remap_to="edge centers", coord_type='spherical') - - remap_to_edge_centers_cartesian = source_grid['v1'].remap.inverse_distance_weighted( - destination_grid=destination_grid, - remap_to="edge centers", coord_type='cartesian') - - # Assert the data variable lies on the "edge centers" - self.assertTrue(remap_to_edge_centers_spherical._edge_centered()) - self.assertTrue(remap_to_edge_centers_cartesian._edge_centered()) - - def test_overwrite(self): - """Tests that the remapping no longer overwrites the dataset.""" - - # Open source and destination datasets to remap to - source_grid = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) - destination_dataset = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) - - # Perform Remapping - remap_to_edge_centers = source_grid['v1'].remap.inverse_distance_weighted( - destination_grid=destination_dataset.uxgrid, - remap_to="face centers", coord_type='cartesian') - - # Assert the remapped data is different from the original data - assert not np.array_equal(destination_dataset['v1'], remap_to_edge_centers) - - def test_source_data_remap(self): - """Test the remapping of all source data positions.""" - - # Open source and destination datasets to remap to - source_uxds = ux.open_dataset(mpasfile_QU, mpasfile_QU) - destination_grid = ux.open_grid(gridfile_geoflow) - - # Remap from `face_centers` - face_centers = source_uxds['latCell'].remap.inverse_distance_weighted( - destination_grid=destination_grid, - remap_to="nodes" - ) - - # Remap from `nodes` - nodes = source_uxds['latVertex'].remap.inverse_distance_weighted( - destination_grid=destination_grid, - remap_to="nodes" - ) - - # Remap from `edges` - edges = source_uxds['angleEdge'].remap.inverse_distance_weighted( - destination_grid=destination_grid, - remap_to="nodes" - ) - - self.assertTrue(len(face_centers.values) != 0) - self.assertTrue(len(nodes.values) != 0) - self.assertTrue(len(edges.values) != 0) - - def test_value_errors(self): - """Tests the raising of value errors and warnings in the function.""" - - # Open source and destination datasets to remap to - source_uxds = ux.open_dataset(gridfile_geoflow, dsfile_v1_geoflow) - source_uxds_2 = ux.open_dataset(mpasfile_QU, mpasfile_QU) - destination_grid = ux.open_grid(gridfile_geoflow) - - # Raise ValueError when `k` =< 1 - with nt.assert_raises(ValueError): - source_uxds['v1'].remap.inverse_distance_weighted( - destination_grid=destination_grid, - remap_to="nodes", k=1 - ) - - # Raise ValueError when k is larger than `n_node` - with nt.assert_raises(ValueError): - source_uxds['v1'].remap.inverse_distance_weighted( - destination_grid=destination_grid, - remap_to="nodes", k=source_uxds.uxgrid.n_node + 1 - ) - - # Raise ValueError when `remap_to` is invalid - with nt.assert_raises(ValueError): - source_uxds['v1'].remap.inverse_distance_weighted( - destination_grid=destination_grid, - remap_to="test", k=2, coord_type='spherical' - ) - with nt.assert_raises(ValueError): - source_uxds['v1'].remap.inverse_distance_weighted( - destination_grid=destination_grid, - remap_to="test", k=2, coord_type="cartesian" - ) - - # Raise ValueError when `coord_type` is invalid - with nt.assert_raises(ValueError): - source_uxds['v1'].remap.inverse_distance_weighted( - destination_grid=destination_grid, - remap_to="nodes", k=2, coord_type="test" - ) - - # Raise ValueError when the source data is invalid - with nt.assert_raises(ValueError): - source_uxds_2['cellsOnCell'].remap.inverse_distance_weighted( - destination_grid=destination_grid, - remap_to="nodes" - ) - - # Raise UserWarning when `power` > 5 - with nt.assert_warns(UserWarning): - source_uxds['v1'].remap.inverse_distance_weighted( - destination_grid=destination_grid, - remap_to="nodes", power=6 - ) + with nt.assert_warns(UserWarning): + source_uxds['v1'].remap.inverse_distance_weighted(destination_grid=destination_grid, remap_to="nodes", power=6) diff --git a/test/test_scrip.py b/test/test_scrip.py index 9f234f6f7..a7dd5a61b 100644 --- a/test/test_scrip.py +++ b/test/test_scrip.py @@ -1,10 +1,9 @@ import os import xarray as xr - -from unittest import TestCase -from pathlib import Path import warnings import numpy.testing as nt +import pytest +from pathlib import Path import uxarray as ux from uxarray.constants import INT_DTYPE, INT_FILL_VALUE @@ -16,88 +15,70 @@ current_path = Path(os.path.dirname(os.path.realpath(__file__))) +# Define grid file paths gridfile_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" gridfile_RLL1deg = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug" gridfile_RLL10deg_ne4 = current_path / "meshfiles" / "ugrid" / "ov_RLL10deg_CSne4" / "ov_RLL10deg_CSne4.ug" - gridfile_exo_ne8 = current_path / "meshfiles" / "exodus" / "outCSne8" / "outCSne8.g" - -class TestUgrid(TestCase): - - def test_read_ugrid(self): - """Reads a ugrid file."""\ - - uxgrid_ne30 = ux.open_grid(str(gridfile_ne30)) - uxgrid_RLL1deg = ux.open_grid(str(gridfile_RLL1deg)) - uxgrid_RLL10deg_ne4 = ux.open_grid(str(gridfile_RLL10deg_ne4)) - - nt.assert_equal(uxgrid_ne30.node_lon.size, constants.NNODES_outCSne30) - nt.assert_equal(uxgrid_RLL1deg.node_lon.size, - constants.NNODES_outRLL1deg) - nt.assert_equal(uxgrid_RLL10deg_ne4.node_lon.size, - constants.NNODES_ov_RLL10deg_CSne4) - - # TODO: UNCOMMENT - # def test_read_ugrid_opendap(self): - # """Read an ugrid model from an OPeNDAP URL.""" - # - # try: - # # make sure we can read the ugrid file from the OPeNDAP URL - # url = "http://test.opendap.org:8080/opendap/ugrid/NECOFS_GOM3_FORECAST.nc" - # uxgrid_url = ux.open_grid(url, drop_variables="siglay") - # - # except OSError: - # # print warning and pass if we can't connect to the OPeNDAP server - # warnings.warn(f'Could not connect to OPeNDAP server: {url}') - # pass - # - # else: - # - # assert isinstance(getattr(uxgrid_url, "node_lon"), xr.DataArray) - # assert isinstance(getattr(uxgrid_url, "node_lat"), xr.DataArray) - # assert isinstance(getattr(uxgrid_url, "face_node_connectivity"), - # xr.DataArray) - - def test_encode_ugrid(self): - """Read an Exodus dataset and encode that as a UGRID format.""" - - ux_grid = ux.open_grid(gridfile_exo_ne8) - ux_grid.encode_as("UGRID") - - def test_standardized_dtype_and_fill(self): - """Test to see if Mesh2_Face_Nodes uses the expected integer datatype - and expected fill value as set in constants.py.""" - - ug_filename1 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" - ug_filename2 = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug" - ug_filename3 = current_path / "meshfiles" / "ugrid" / "ov_RLL10deg_CSne4" / "ov_RLL10deg_CSne4.ug" - - ux_grid1 = ux.open_grid(ug_filename1) - ux_grid2 = ux.open_grid(ug_filename2) - ux_grid3 = ux.open_grid(ug_filename3) - - # check for correct dtype and fill value - grids_with_fill = [ux_grid2] - for grid in grids_with_fill: - assert grid.face_node_connectivity.dtype == INT_DTYPE - assert grid.face_node_connectivity._FillValue == INT_FILL_VALUE - assert INT_FILL_VALUE in grid.face_node_connectivity.values - - grids_without_fill = [ux_grid1, ux_grid3] - for grid in grids_without_fill: - assert grid.face_node_connectivity.dtype == INT_DTYPE - assert grid.face_node_connectivity._FillValue == INT_FILL_VALUE - - def test_standardized_dtype_and_fill_dask(self): - """Test to see if Mesh2_Face_Nodes uses the expected integer datatype - and expected fill value as set in constants.py. - - with dask chunking - """ - ug_filename = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug" - ux_grid = ux.open_grid(ug_filename) - - assert ux_grid.face_node_connectivity.dtype == INT_DTYPE - assert ux_grid.face_node_connectivity._FillValue == INT_FILL_VALUE - assert INT_FILL_VALUE in ux_grid.face_node_connectivity.values +def test_read_ugrid(): + """Reads a ugrid file.""" + uxgrid_ne30 = ux.open_grid(str(gridfile_ne30)) + uxgrid_RLL1deg = ux.open_grid(str(gridfile_RLL1deg)) + uxgrid_RLL10deg_ne4 = ux.open_grid(str(gridfile_RLL10deg_ne4)) + + nt.assert_equal(uxgrid_ne30.node_lon.size, constants.NNODES_outCSne30) + nt.assert_equal(uxgrid_RLL1deg.node_lon.size, constants.NNODES_outRLL1deg) + nt.assert_equal(uxgrid_RLL10deg_ne4.node_lon.size, constants.NNODES_ov_RLL10deg_CSne4) + +# TODO: UNCOMMENT +# def test_read_ugrid_opendap(): +# """Read an ugrid model from an OPeNDAP URL.""" +# try: +# url = "http://test.opendap.org:8080/opendap/ugrid/NECOFS_GOM3_FORECAST.nc" +# uxgrid_url = ux.open_grid(url, drop_variables="siglay") +# except OSError: +# warnings.warn(f'Could not connect to OPeNDAP server: {url}') +# pass +# else: +# assert isinstance(getattr(uxgrid_url, "node_lon"), xr.DataArray) +# assert isinstance(getattr(uxgrid_url, "node_lat"), xr.DataArray) +# assert isinstance(getattr(uxgrid_url, "face_node_connectivity"), xr.DataArray) + +def test_encode_ugrid(): + """Read an Exodus dataset and encode that as a UGRID format.""" + ux_grid = ux.open_grid(gridfile_exo_ne8) + ux_grid.encode_as("UGRID") + +def test_standardized_dtype_and_fill(): + """Test to see if Mesh2_Face_Nodes uses the expected integer datatype + and expected fill value as set in constants.py.""" + ug_filename1 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" + ug_filename2 = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug" + ug_filename3 = current_path / "meshfiles" / "ugrid" / "ov_RLL10deg_CSne4" / "ov_RLL10deg_CSne4.ug" + + ux_grid1 = ux.open_grid(ug_filename1) + ux_grid2 = ux.open_grid(ug_filename2) + ux_grid3 = ux.open_grid(ug_filename3) + + # Check for correct dtype and fill value + grids_with_fill = [ux_grid2] + for grid in grids_with_fill: + assert grid.face_node_connectivity.dtype == INT_DTYPE + assert grid.face_node_connectivity._FillValue == INT_FILL_VALUE + assert INT_FILL_VALUE in grid.face_node_connectivity.values + + grids_without_fill = [ux_grid1, ux_grid3] + for grid in grids_without_fill: + assert grid.face_node_connectivity.dtype == INT_DTYPE + assert grid.face_node_connectivity._FillValue == INT_FILL_VALUE + +def test_standardized_dtype_and_fill_dask(): + """Test to see if Mesh2_Face_Nodes uses the expected integer datatype + and expected fill value as set in constants.py with dask chunking.""" + ug_filename = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug" + ux_grid = ux.open_grid(ug_filename) + + assert ux_grid.face_node_connectivity.dtype == INT_DTYPE + assert ux_grid.face_node_connectivity._FillValue == INT_FILL_VALUE + assert INT_FILL_VALUE in ux_grid.face_node_connectivity.values diff --git a/test/test_ugrid.py b/test/test_ugrid.py index 9f7576ce2..9f4087217 100644 --- a/test/test_ugrid.py +++ b/test/test_ugrid.py @@ -1,10 +1,9 @@ import os -import xarray as xr - -from unittest import TestCase -from pathlib import Path import warnings +from pathlib import Path import numpy.testing as nt +import pytest +import xarray as xr import uxarray as ux from uxarray.constants import INT_DTYPE, INT_FILL_VALUE @@ -19,84 +18,60 @@ gridfile_ne30 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" gridfile_RLL1deg = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug" gridfile_RLL10deg_ne4 = current_path / "meshfiles" / "ugrid" / "ov_RLL10deg_CSne4" / "ov_RLL10deg_CSne4.ug" - gridfile_exo_ne8 = current_path / "meshfiles" / "exodus" / "outCSne8" / "outCSne8.g" - -class TestUgrid(TestCase): - - def test_read_ugrid(self): - """Reads a ugrid file."""\ - - uxgrid_ne30 = ux.open_grid(str(gridfile_ne30)) - uxgrid_RLL1deg = ux.open_grid(str(gridfile_RLL1deg)) - uxgrid_RLL10deg_ne4 = ux.open_grid(str(gridfile_RLL10deg_ne4)) - - nt.assert_equal(uxgrid_ne30.node_lon.size, constants.NNODES_outCSne30) - nt.assert_equal(uxgrid_RLL1deg.node_lon.size, - constants.NNODES_outRLL1deg) - nt.assert_equal(uxgrid_RLL10deg_ne4.node_lon.size, - constants.NNODES_ov_RLL10deg_CSne4) - - # def test_read_ugrid_opendap(self): - # """Read an ugrid model from an OPeNDAP URL.""" - # - # try: - # # make sure we can read the ugrid file from the OPeNDAP URL - # url = "http://test.opendap.org:8080/opendap/ugrid/NECOFS_GOM3_FORECAST.nc" - # uxgrid_url = ux.open_grid(url, drop_variables="siglay") - # - # except OSError: - # # print warning and pass if we can't connect to the OPeNDAP server - # warnings.warn(f'Could not connect to OPeNDAP server: {url}') - # pass - # - # else: - # - # assert isinstance(getattr(uxgrid_url, "node_lon"), xr.DataArray) - # assert isinstance(getattr(uxgrid_url, "node_lat"), xr.DataArray) - # assert isinstance(getattr(uxgrid_url, "face_node_connectivity"), - # xr.DataArray) - - def test_encode_ugrid(self): - """Read an Exodus dataset and encode that as a UGRID format.""" - - ux_grid = ux.open_grid(gridfile_exo_ne8) - ux_grid.encode_as("UGRID") - - def test_standardized_dtype_and_fill(self): - """Test to see if Mesh2_Face_Nodes uses the expected integer datatype - and expected fill value as set in constants.py.""" - - ug_filename1 = current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug" - ug_filename2 = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug" - ug_filename3 = current_path / "meshfiles" / "ugrid" / "ov_RLL10deg_CSne4" / "ov_RLL10deg_CSne4.ug" - - ux_grid1 = ux.open_grid(ug_filename1) - ux_grid2 = ux.open_grid(ug_filename2) - ux_grid3 = ux.open_grid(ug_filename3) - - # check for correct dtype and fill value - grids_with_fill = [ux_grid2] - for grid in grids_with_fill: - assert grid.face_node_connectivity.dtype == INT_DTYPE - assert grid.face_node_connectivity._FillValue == INT_FILL_VALUE - assert INT_FILL_VALUE in grid.face_node_connectivity.values - - grids_without_fill = [ux_grid1, ux_grid3] - for grid in grids_without_fill: - assert grid.face_node_connectivity.dtype == INT_DTYPE - assert grid.face_node_connectivity._FillValue == INT_FILL_VALUE - - def test_standardized_dtype_and_fill_dask(self): - """Test to see if Mesh2_Face_Nodes uses the expected integer datatype - and expected fill value as set in constants.py. - - with dask chunking - """ - ug_filename = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug" - ux_grid = ux.open_grid(ug_filename) - - assert ux_grid.face_node_connectivity.dtype == INT_DTYPE - assert ux_grid.face_node_connectivity._FillValue == INT_FILL_VALUE - assert INT_FILL_VALUE in ux_grid.face_node_connectivity.values +def test_read_ugrid(): + """Reads a ugrid file.""" + uxgrid_ne30 = ux.open_grid(str(gridfile_ne30)) + uxgrid_RLL1deg = ux.open_grid(str(gridfile_RLL1deg)) + uxgrid_RLL10deg_ne4 = ux.open_grid(str(gridfile_RLL10deg_ne4)) + + nt.assert_equal(uxgrid_ne30.node_lon.size, constants.NNODES_outCSne30) + nt.assert_equal(uxgrid_RLL1deg.node_lon.size, constants.NNODES_outRLL1deg) + nt.assert_equal(uxgrid_RLL10deg_ne4.node_lon.size, constants.NNODES_ov_RLL10deg_CSne4) + +# Uncomment this test if you want to test OPeNDAP functionality +# def test_read_ugrid_opendap(): +# """Read an ugrid model from an OPeNDAP URL.""" +# url = "http://test.opendap.org:8080/opendap/ugrid/NECOFS_GOM3_FORECAST.nc" +# try: +# uxgrid_url = ux.open_grid(url, drop_variables="siglay") +# except OSError: +# warnings.warn(f'Could not connect to OPeNDAP server: {url}') +# else: +# assert isinstance(getattr(uxgrid_url, "node_lon"), xr.DataArray) +# assert isinstance(getattr(uxgrid_url, "node_lat"), xr.DataArray) +# assert isinstance(getattr(uxgrid_url, "face_node_connectivity"), xr.DataArray) + +def test_encode_ugrid(): + """Read an Exodus dataset and encode that as a UGRID format.""" + ux_grid = ux.open_grid(gridfile_exo_ne8) + ux_grid.encode_as("UGRID") + +def test_standardized_dtype_and_fill(): + """Test to see if Mesh2_Face_Nodes uses the expected integer datatype and expected fill value.""" + ug_filenames = [ + current_path / "meshfiles" / "ugrid" / "outCSne30" / "outCSne30.ug", + current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug", + current_path / "meshfiles" / "ugrid" / "ov_RLL10deg_CSne4" / "ov_RLL10deg_CSne4.ug" + ] + + grids_with_fill = [ux.open_grid(ug_filenames[1])] + for grid in grids_with_fill: + assert grid.face_node_connectivity.dtype == INT_DTYPE + assert grid.face_node_connectivity._FillValue == INT_FILL_VALUE + assert INT_FILL_VALUE in grid.face_node_connectivity.values + + grids_without_fill = [ux.open_grid(ug_filenames[0]), ux.open_grid(ug_filenames[2])] + for grid in grids_without_fill: + assert grid.face_node_connectivity.dtype == INT_DTYPE + assert grid.face_node_connectivity._FillValue == INT_FILL_VALUE + +def test_standardized_dtype_and_fill_dask(): + """Test to see if Mesh2_Face_Nodes uses the expected integer datatype with dask chunking.""" + ug_filename = current_path / "meshfiles" / "ugrid" / "outRLL1deg" / "outRLL1deg.ug" + ux_grid = ux.open_grid(ug_filename) + + assert ux_grid.face_node_connectivity.dtype == INT_DTYPE + assert ux_grid.face_node_connectivity._FillValue == INT_FILL_VALUE + assert INT_FILL_VALUE in ux_grid.face_node_connectivity.values diff --git a/uxarray/cross_sections/dataarray_accessor.py b/uxarray/cross_sections/dataarray_accessor.py index d8c357e55..6f82a8f2e 100644 --- a/uxarray/cross_sections/dataarray_accessor.py +++ b/uxarray/cross_sections/dataarray_accessor.py @@ -46,7 +46,7 @@ def constant_latitude(self, lat: float): Examples -------- >>> # Extract data at 15.5°S latitude - >>> cross_section = uxda.constant_latitude(lat=-15.5) + >>> cross_section = uxda.cross_section.constant_latitude(lat=-15.5) Notes ----- @@ -86,7 +86,7 @@ def constant_longitude(self, lon: float): Examples -------- >>> # Extract data at 0° longitude - >>> cross_section = uxda.constant_latitude(lon=0.0) + >>> cross_section = uxda.cross_section.constant_latitude(lon=0.0) Notes ----- diff --git a/uxarray/cross_sections/grid_accessor.py b/uxarray/cross_sections/grid_accessor.py index 146da10af..ee30bd913 100644 --- a/uxarray/cross_sections/grid_accessor.py +++ b/uxarray/cross_sections/grid_accessor.py @@ -51,7 +51,7 @@ def constant_latitude( Examples -------- >>> # Extract data at 15.5°S latitude - >>> cross_section = grid.constant_latitude(lat=-15.5) + >>> cross_section = grid.cross_section.constant_latitude(lat=-15.5) Notes ----- @@ -103,7 +103,7 @@ def constant_longitude( Examples -------- >>> # Extract data at 0° longitude - >>> cross_section = grid.constant_latitude(lon=0.0) + >>> cross_section = grid.cross_section.constant_latitude(lon=0.0) Notes -----