1+ import numpy as np
2+ import pytest
3+
4+ import uxarray as ux
5+ import numpy .testing as nt
6+
7+
8+ class TestQuadHex :
9+
10+ def test_divergence_output_format (self , gridpath , datasetpath ):
11+ """Tests the output format of divergence functionality"""
12+ uxds = ux .open_dataset (gridpath ("ugrid" , "quad-hexagon" , "grid.nc" ), datasetpath ("ugrid" , "quad-hexagon" , "data.nc" ))
13+
14+ # Create two components for vector field (using same data for simplicity in test)
15+ u_component = uxds ['t2m' ]
16+ v_component = uxds ['t2m' ]
17+
18+ div_da = u_component .divergence (v_component )
19+
20+ assert isinstance (div_da , ux .UxDataArray )
21+ assert div_da .name == "divergence"
22+ assert "divergence" in div_da .attrs
23+ assert u_component .sizes == div_da .sizes
24+
25+ def test_divergence_input_validation (self , gridpath , datasetpath ):
26+ """Tests input validation for divergence method"""
27+ uxds = ux .open_dataset (gridpath ("ugrid" , "quad-hexagon" , "grid.nc" ), datasetpath ("ugrid" , "quad-hexagon" , "data.nc" ))
28+
29+ u_component = uxds ['t2m' ]
30+
31+ # Test with non-UxDataArray
32+ with pytest .raises (TypeError , match = "other must be a UxDataArray" ):
33+ u_component .divergence (np .array ([1 , 2 , 3 ]))
34+
35+ # Test with different grids (create a simple test case)
36+ # This would require creating another grid, so we'll skip for now
37+
38+ # Test with different dimensions
39+ # This would require creating data with different dims, so we'll skip for now
40+
41+
42+ class TestMPASOcean :
43+
44+ def test_divergence_basic (self , gridpath , datasetpath ):
45+ """Basic test of divergence computation"""
46+ uxds = ux .open_dataset (gridpath ("mpas" , "QU" , "480" , "grid.nc" ), datasetpath ("mpas" , "QU" , "480" , "data.nc" ))
47+
48+ # Use the same field for both components (not physically meaningful but tests the method)
49+ u_component = uxds ['bottomDepth' ]
50+ v_component = uxds ['bottomDepth' ]
51+
52+ div_field = u_component .divergence (v_component )
53+
54+ # Check that we get finite values where expected
55+ assert not np .isnan (div_field .values ).all ()
56+ assert np .isfinite (div_field .values ).any ()
57+
58+
59+ class TestDyamondSubset :
60+
61+ def test_divergence_constant_field (self , gridpath , datasetpath ):
62+ """Test divergence of constant vector field (should be zero)"""
63+ uxds = ux .open_dataset (
64+ gridpath ("mpas" , "dyamond-30km" , "gradient_grid_subset.nc" ),
65+ datasetpath ("mpas" , "dyamond-30km" , "gradient_data_subset.nc" )
66+ )
67+
68+ # Create constant fields
69+ constant_u = uxds ['face_lat' ] * 0 + 1.0 # Constant field = 1
70+ constant_v = uxds ['face_lat' ] * 0 + 1.0 # Constant field = 1
71+
72+ div_field = constant_u .divergence (constant_v )
73+
74+ # Divergence of constant field should be close to zero for interior faces
75+ # Boundary faces may have NaN values (which is expected)
76+ finite_values = div_field .values [np .isfinite (div_field .values )]
77+
78+ # Check that we have some finite values (interior faces)
79+ assert len (finite_values ) > 0 , "No finite divergence values found"
80+
81+ # Divergence of constant field should be close to zero for finite values
82+ assert np .abs (finite_values ).max () < 1e-10 , f"Max divergence: { np .abs (finite_values ).max ()} "
83+
84+ def test_divergence_linear_field (self , gridpath , datasetpath ):
85+ """Test divergence of linear vector field"""
86+ uxds = ux .open_dataset (
87+ gridpath ("mpas" , "dyamond-30km" , "gradient_grid_subset.nc" ),
88+ datasetpath ("mpas" , "dyamond-30km" , "gradient_data_subset.nc" )
89+ )
90+
91+ # Create linear fields: u = x, v = y (in spherical coordinates: u = lon, v = lat)
92+ u_component = uxds ['face_lon' ] # Linear in longitude
93+ v_component = uxds ['face_lat' ] # Linear in latitude
94+
95+ div_field = u_component .divergence (v_component )
96+
97+ # Check that we have some finite values (interior faces)
98+ finite_mask = np .isfinite (div_field .values )
99+ finite_values = div_field .values [finite_mask ]
100+
101+ assert len (finite_values ) > 0 , "No finite divergence values found"
102+
103+ # For linear fields, divergence should be finite where computable
104+ # Boundary faces may have NaN values (which is expected)
105+ assert not np .isnan (finite_values ).any (), "Found NaN in finite values"
106+
107+ def test_divergence_radial_field (self , gridpath , datasetpath ):
108+ """Test divergence of radial vector field (should be positive)"""
109+ uxds = ux .open_dataset (
110+ gridpath ("mpas" , "dyamond-30km" , "gradient_grid_subset.nc" ),
111+ datasetpath ("mpas" , "dyamond-30km" , "gradient_data_subset.nc" )
112+ )
113+
114+ # Create a radial field pointing outward from center
115+ # Use the inverse gaussian as a proxy for radial distance
116+ radial_distance = uxds ['inverse_gaussian' ]
117+
118+ # Create components proportional to position (simplified radial field)
119+ u_component = radial_distance * uxds ['face_lon' ]
120+ v_component = radial_distance * uxds ['face_lat' ]
121+
122+ div_field = u_component .divergence (v_component )
123+
124+ # Check that we have some finite values (interior faces)
125+ finite_mask = np .isfinite (div_field .values )
126+ finite_values = div_field .values [finite_mask ]
127+
128+ assert len (finite_values ) > 0 , "No finite divergence values found"
129+
130+ # Boundary faces may have NaN values (which is expected)
131+ assert not np .isnan (finite_values ).any (), "Found NaN in finite values"
132+
133+ # Most finite values should be positive for an expanding field
134+ positive_values = finite_values > 0
135+ assert positive_values .sum () > len (finite_values ) * 0.3 # At least 30% positive
136+
137+ def test_divergence_curl_identity (self , gridpath , datasetpath ):
138+ """Test that divergence of curl is zero (vector calculus identity)"""
139+ uxds = ux .open_dataset (
140+ gridpath ("mpas" , "dyamond-30km" , "gradient_grid_subset.nc" ),
141+ datasetpath ("mpas" , "dyamond-30km" , "gradient_data_subset.nc" )
142+ )
143+
144+ # Create a scalar potential field
145+ potential = uxds ['gaussian' ]
146+
147+ # Compute gradient to get a conservative vector field
148+ grad = potential .gradient ()
149+ u_component = grad ['zonal_gradient' ]
150+ v_component = grad ['meridional_gradient' ]
151+
152+ # Compute divergence of this gradient (should be the Laplacian)
153+ div_grad = u_component .divergence (v_component )
154+
155+ # Check that we have some finite values (interior faces)
156+ finite_mask = np .isfinite (div_grad .values )
157+ finite_values = div_grad .values [finite_mask ]
158+
159+ assert len (finite_values ) > 0 , "No finite divergence values found"
160+
161+ # This tests the Laplacian computation via div(grad)
162+ # Boundary faces may have NaN values (which is expected)
163+ assert not np .isnan (finite_values ).any (), "Found NaN in finite values"
164+
165+ # The Laplacian of a Gaussian should have both positive and negative values
166+ assert (finite_values > 0 ).any (), "No positive Laplacian values found"
167+ assert (finite_values < 0 ).any (), "No negative Laplacian values found"
0 commit comments