Skip to content

Commit 6282b77

Browse files
Add cartesian barycentric coordinates as a backup.
1 parent 2308b52 commit 6282b77

1 file changed

Lines changed: 105 additions & 5 deletions

File tree

parcels/uxgrid.py

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import numpy as np
66
import uxarray as ux
7+
from uxarray.grid.coordinates import _lonlat_rad_to_xyz
78
from uxarray.grid.neighbors import _barycentric_coordinates
89

910
from 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

Comments
 (0)