@@ -104,3 +104,150 @@ def _search_indices_curvilinear_2d(
104104 eta = coords [:, 1 ]
105105
106106 return (yi , eta , xi , xsi )
107+
108+
109+ def uxgrid_point_in_cell (grid , y : np .ndarray , x : np .ndarray , yi : np .ndarray , xi : np .ndarray ):
110+ """Check if points are inside the grid cells defined by the given face indices.
111+
112+ Parameters
113+ ----------
114+ grid : ux.grid.Grid
115+ The uxarray grid object containing the unstructured grid data.
116+ y : np.ndarray
117+ Array of latitudes of the points to check.
118+ x : np.ndarray
119+ Array of longitudes of the points to check.
120+ yi : np.ndarray
121+ Array of face indices corresponding to the points.
122+ xi : np.ndarray
123+ Not used, but included for compatibility with other search functions.
124+
125+ Returns
126+ -------
127+ is_in_cell : np.ndarray
128+ An array indicating whether each point is inside (1) or outside (0) the corresponding cell.
129+ coords : np.ndarray
130+ Barycentric coordinates of the points within their respective cells.
131+ """
132+ if grid ._mesh == "spherical" :
133+ # lon_rad = np.deg2rad(grid.lon.values)
134+ # lat_rad = np.deg2rad(grid.lat.values)
135+ # x_cart, y_cart, z_cart = _latlon_rad_to_xyz(lat_rad, lon_rad)
136+ # points = np.column_stack((x_cart.flatten(), y_cart.flatten(), z_cart.flatten()))
137+
138+ # Get the vertex indices for each face
139+ nids = grid .uxgrid .face_node_connectivity [yi ].values
140+ face_vertices = np .stack (
141+ (
142+ grid .uxgrid .node_x [nids .ravel ()].values .reshape (nids .shape ),
143+ grid .uxgrid .node_y [nids .ravel ()].values .reshape (nids .shape ),
144+ grid .uxgrid .node_z [nids .ravel ()].values .reshape (nids .shape ),
145+ ),
146+ axis = - 1 ,
147+ )
148+ else :
149+ nids = grid .uxgrid .face_node_connectivity [yi ].values
150+ face_vertices = np .stack (
151+ (
152+ grid .uxgrid .node_lon [nids .ravel ()].values .reshape (nids .shape ),
153+ grid .uxgrid .node_lat [nids .ravel ()].values .reshape (nids .shape ),
154+ ),
155+ axis = - 1 ,
156+ )
157+ points = np .stack ((x , y ))
158+
159+ M = len (points )
160+
161+ is_in_cell = np .zeros (M , dtype = np .int32 )
162+
163+ coords = _barycentric_coordinates (face_vertices , points )
164+ is_in_cell = np .where (np .all ((coords >= - 1e-6 ) & (coords <= 1 + 1e-6 ), axis = 1 ), 1 , 0 )
165+
166+ return is_in_cell , coords
167+
168+
169+ def _triangle_area (A , B , C ):
170+ """Compute the area of a triangle given by three points."""
171+ d1 = B - A
172+ d2 = C - A
173+ if A .shape [- 1 ] == 2 :
174+ # 2D case: cross product reduces to scalar z-component
175+ cross = d1 [..., 0 ] * d2 [..., 1 ] - d1 [..., 1 ] * d2 [..., 0 ]
176+ area = 0.5 * np .abs (cross )
177+ elif A .shape [- 1 ] == 3 :
178+ # 3D case: full vector cross product
179+ cross = np .cross (d1 , d2 )
180+ area = 0.5 * np .linalg .norm (cross , axis = - 1 )
181+ else :
182+ raise ValueError (f"Expected last dim=2 or 3, got { A .shape [- 1 ]} " )
183+
184+ return area
185+ # d3 = np.cross(d1, d2, axis=-1)
186+ # breakpoint()
187+ # return 0.5 * np.linalg.norm(d3, axis=-1)
188+
189+
190+ def _barycentric_coordinates (nodes , points , min_area = 1e-8 ):
191+ """
192+ Compute the barycentric coordinates of a point P inside a convex polygon using area-based weights.
193+ So that this method generalizes to n-sided polygons, we use the Waschpress points as the generalized
194+ barycentric coordinates, which is only valid for convex polygons.
195+
196+ Parameters
197+ ----------
198+ nodes : numpy.ndarray
199+ Polygon verties per query of shape (M, 3, 2/3) where M is the number of query points. The second dimension corresponds to the number
200+ of vertices
201+ The last dimension can be either 2 or 3, where 3 corresponds to the (z, y, x) coordinates of each vertex and 2 corresponds to the
202+ (lat, lon) coordinates of each vertex.
203+
204+ points : numpy.ndarray
205+ Spherical coordinates of the point (M,2/3) where M is the number of query points.
206+
207+ Returns
208+ -------
209+ numpy.ndarray
210+ Barycentric coordinates corresponding to each vertex.
211+
212+ """
213+ M , K = nodes .shape [:2 ]
214+
215+ # roll(-1) to get vi+1, roll(+1) to get vi-1
216+ vi = nodes # (M,K,2)
217+ vi1 = np .roll (nodes , shift = - 1 , axis = 1 ) # (M,K,2)
218+ vim1 = np .roll (nodes , shift = + 1 , axis = 1 ) # (M,K,2)
219+
220+ # a0 = area(v_{i-1}, v_i, v_{i+1})
221+ a0 = _triangle_area (vim1 , vi , vi1 ) # (M,K)
222+
223+ # a1 = area(P, v_{i-1}, v_i); a2 = area(P, v_i, v_{i+1})
224+ P = points [:, None , :] # (M,1,2) -> (M,K,2)
225+ a1 = _triangle_area (P , vim1 , vi )
226+ a2 = _triangle_area (P , vi , vi1 )
227+
228+ # clamp tiny denominators for stability
229+ a1c = np .maximum (a1 , min_area )
230+ a2c = np .maximum (a2 , min_area )
231+
232+ wi = a0 / (a1c * a2c ) # (M,K)
233+
234+ sum_wi = wi .sum (axis = 1 , keepdims = True ) # (M,1)
235+ # Avoid 0/0: if sum_wi==0 (degenerate), keep zeros
236+ with np .errstate (invalid = "ignore" , divide = "ignore" ):
237+ bcoords = wi / sum_wi
238+
239+ return bcoords
240+
241+
242+ def _latlon_rad_to_xyz (
243+ lat ,
244+ lon ,
245+ ):
246+ """Converts Spherical latitude and longitude coordinates into Cartesian x,
247+ y, z coordinates.
248+ """
249+ x = np .cos (lon ) * np .cos (lat )
250+ y = np .sin (lon ) * np .cos (lat )
251+ z = np .sin (lat )
252+
253+ return x , y , z
0 commit comments