44
55import numpy as np
66import uxarray as ux
7+ from uxarray .grid .coordinates import _lonlat_rad_to_xyz
78from uxarray .grid .neighbors import _barycentric_coordinates
89
910from parcels .field import FieldOutOfBoundError # Adjust import as necessary
@@ -67,13 +68,16 @@ def get_axis_dim(self, axis: _UXGRID_AXES) -> int:
6768 elif axis == "FACE" :
6869 return self .uxgrid .n_face
6970
70- def search (self , z , y , x , ei = None ):
71- tol = 1e-6
72-
71+ def search (self , z , y , x , ei = None , tol = 1e-6 ):
7372 def try_face (fid ):
74- bcoords , err = self ._get_barycentric_coordinates (y , x , fid )
73+ bcoords , err = self ._get_barycentric_coordinates_latlon (y , x , fid )
7574 if (bcoords >= 0 ).all () and (bcoords <= 1 ).all () and err < tol :
7675 return bcoords
76+ else :
77+ bcoords , err = self ._get_barycentric_coordinates_cartesian (y , x , fid )
78+ if (bcoords >= 0 ).all () and (bcoords <= 1 ).all () and err < tol :
79+ return bcoords
80+
7781 return None
7882
7983 zi , zeta = _search_1d_array (self .z .values , z )
@@ -102,9 +106,10 @@ def try_face(fid):
102106
103107 return {"Z" : (zi , zeta ), "FACE" : (fi , bcoords )}
104108
105- def _get_barycentric_coordinates (self , y , x , fi ):
109+ def _get_barycentric_coordinates_latlon (self , y , x , fi ):
106110 """Checks if a point is inside a given face id on a UxGrid."""
107111 # Check if particle is in the same face, otherwise search again.
112+
108113 n_nodes = self .uxgrid .n_nodes_per_face [fi ].to_numpy ()
109114 node_ids = self .uxgrid .face_node_connectivity [fi , 0 :n_nodes ]
110115 nodes = np .column_stack (
@@ -118,3 +123,98 @@ def _get_barycentric_coordinates(self, y, x, fi):
118123 bcoord = np .asarray (_barycentric_coordinates (nodes , coord ))
119124 err = abs (np .dot (bcoord , nodes [:, 0 ]) - coord [0 ]) + abs (np .dot (bcoord , nodes [:, 1 ]) - coord [1 ])
120125 return bcoord , err
126+
127+ def _get_barycentric_coordinates_cartesian (self , y , x , fi ):
128+ n_nodes = self .uxgrid .n_nodes_per_face [fi ].to_numpy ()
129+ node_ids = self .uxgrid .face_node_connectivity [fi , 0 :n_nodes ]
130+
131+ coord = np .deg2rad ([x , y ])
132+ x , y , z = _lonlat_rad_to_xyz (coord [0 ], coord [1 ])
133+ cart_coord = np .array ([x , y , z ]).T
134+ # Second attempt to find barycentric coordinates using cartesian coordinates
135+ nodes = np .stack (
136+ (
137+ self ._source_grid .node_x [node_ids ].values (),
138+ self ._source_grid .node_y [node_ids ].values (),
139+ self ._source_grid .node_z [node_ids ].values (),
140+ ),
141+ axis = - 1 ,
142+ )
143+
144+ bcoord = np .asarray (_barycentric_coordinates_cartesian (nodes , cart_coord ))
145+ proj_uv = np .dot (bcoord , nodes )
146+ err = np .linalg .norm (proj_uv - coord )
147+ face_center = np .stack (
148+ (
149+ self ._source_grid .face_x [fi ].values (),
150+ self ._source_grid .face_y [fi ].values (),
151+ self ._source_grid .face_z [fi ].values (),
152+ ),
153+ axis = - 1 ,
154+ )
155+ # Compute and remove the local projection error
156+ err -= np .abs (_local_projection_error (nodes , face_center ))
157+ return bcoord , err
158+
159+
160+ def _barycentric_coordinates_cartesian (nodes , point , min_area = 1e-8 ):
161+ """
162+ Compute the barycentric coordinates of a point P inside a convex polygon using area-based weights.
163+ So that this method generalizes to n-sided polygons, we use the Waschpress points as the generalized
164+ barycentric coordinates, which is only valid for convex polygons.
165+
166+ Parameters
167+ ----------
168+ nodes : numpy.ndarray
169+ Cartesian coordinates (x,y,z) of each corner node of a face
170+ point : numpy.ndarray
171+ Cartesian coordinates (x,y,z) of the point
172+
173+ Returns
174+ -------
175+ numpy.ndarray
176+ Barycentric coordinates corresponding to each vertex.
177+
178+ """
179+ n = len (nodes )
180+ sum_wi = 0
181+ w = []
182+
183+ for i in range (0 , n ):
184+ vim1 = nodes [i - 1 ]
185+ vi = nodes [i ]
186+ vi1 = nodes [(i + 1 ) % n ]
187+ a0 = _triangle_area_cartesian (vim1 , vi , vi1 )
188+ a1 = max (_triangle_area_cartesian (point , vim1 , vi ), min_area )
189+ a2 = max (_triangle_area_cartesian (point , vi , vi1 ), min_area )
190+ sum_wi += a0 / (a1 * a2 )
191+ w .append (a0 / (a1 * a2 ))
192+
193+ barycentric_coords = [w_i / sum_wi for w_i in w ]
194+
195+ return barycentric_coords
196+
197+
198+ def _local_projection_error (nodes , point ):
199+ """
200+ Computes the size of the local projection error that arises from
201+ assuming planar faces. Effectively, a planar face on a spherical
202+ manifold is local linearization of the spherical coordinate
203+ transformation. Since query points and nodes are converted to
204+ cartesian coordinates using the full spherical coordinate transformation,
205+ the local projection error will likely be non-zero but related to the discretiztaion.
206+ """
207+ a = nodes [1 ] - nodes [0 ]
208+ b = nodes [2 ] - nodes [0 ]
209+ normal = np .cross (a , b )
210+ normal /= np .linalg .norm (normal )
211+ d = point - nodes [0 ]
212+ return abs (np .dot (d , normal ))
213+
214+
215+ def _triangle_area_cartesian (A , B , C ):
216+ """Compute the area of a triangle given by three points."""
217+ d1 = B - A
218+ d2 = C - A
219+ d3 = np .cross (d1 , d2 )
220+ return 0.5 * np .linalg .norm (d3 )
0 commit comments