|
4 | 4 |
|
5 | 5 | import numpy as np |
6 | 6 | import uxarray as ux |
| 7 | +from uxarray.grid.coordinates import _lonlat_rad_to_xyz |
7 | 8 | from uxarray.grid.neighbors import _barycentric_coordinates |
8 | 9 |
|
9 | 10 | from parcels.field import FieldOutOfBoundError # Adjust import as necessary |
@@ -67,49 +68,125 @@ def get_axis_dim(self, axis: _UXGRID_AXES) -> int: |
67 | 68 | elif axis == "FACE": |
68 | 69 | return self.uxgrid.n_face |
69 | 70 |
|
70 | | - def search(self, z, y, x, ei=None): |
71 | | - tol = 1e-10 |
72 | | - |
| 71 | + def search(self, z, y, x, ei=None, tol=1e-6): |
73 | 72 | def try_face(fid): |
74 | | - bcoords, err = self.uxgrid._get_barycentric_coordinates(y, x, fid) |
| 73 | + bcoords, err = self._get_barycentric_coordinates_latlon(y, x, fid) |
75 | 74 | if (bcoords >= 0).all() and (bcoords <= 1).all() and err < tol: |
76 | | - return bcoords, fid |
77 | | - return None, None |
| 75 | + return bcoords |
| 76 | + else: |
| 77 | + bcoords = self._get_barycentric_coordinates_cartesian(y, x, fid) |
| 78 | + if (bcoords >= 0).all() and (bcoords <= 1).all(): |
| 79 | + return bcoords |
| 80 | + |
| 81 | + return None |
78 | 82 |
|
79 | 83 | zi, zeta = _search_1d_array(self.z.values, z) |
80 | 84 |
|
81 | 85 | if ei is not None: |
82 | 86 | _, fi = self.unravel_index(ei) |
83 | | - bcoords, fi_new = try_face(fi) |
| 87 | + bcoords = try_face(fi) |
84 | 88 | if bcoords is not None: |
85 | | - return bcoords, self.ravel_index(zi, fi_new) |
| 89 | + return bcoords, self.ravel_index(zi, fi) |
86 | 90 | # Try neighbors of current face |
87 | 91 | for neighbor in self.uxgrid.face_face_connectivity[fi, :]: |
88 | 92 | if neighbor == -1: |
89 | 93 | continue |
90 | | - bcoords, fi_new = try_face(neighbor) |
| 94 | + bcoords = try_face(neighbor) |
91 | 95 | if bcoords is not None: |
92 | | - return bcoords, self.ravel_index(zi, fi_new) |
| 96 | + return bcoords, self.ravel_index(zi, neighbor) |
93 | 97 |
|
94 | | - # Global fallback using spatial hash |
95 | | - fi, bcoords = self.uxgrid.get_spatial_hash().query([[x, y]]) |
| 98 | + # Global fallback as last ditch effort |
| 99 | + face_ids = self.uxgrid.get_faces_containing_point([x, y], return_counts=False)[0] |
| 100 | + fi = face_ids[0] if len(face_ids) > 0 else -1 |
96 | 101 | if fi == -1: |
97 | 102 | raise FieldOutOfBoundError(z, y, x) |
98 | | - return {"Z": (zi, zeta), "FACE": (fi[0], bcoords[0])} |
| 103 | + bcoords = try_face(fi) |
| 104 | + if bcoords is None: |
| 105 | + raise FieldOutOfBoundError(z, y, x) |
99 | 106 |
|
100 | | - def _get_barycentric_coordinates(self, y, x, fi): |
| 107 | + return {"Z": (zi, zeta), "FACE": (fi, bcoords)} |
| 108 | + |
| 109 | + def _get_barycentric_coordinates_latlon(self, y, x, fi): |
101 | 110 | """Checks if a point is inside a given face id on a UxGrid.""" |
102 | 111 | # Check if particle is in the same face, otherwise search again. |
| 112 | + |
103 | 113 | n_nodes = self.uxgrid.n_nodes_per_face[fi].to_numpy() |
104 | 114 | node_ids = self.uxgrid.face_node_connectivity[fi, 0:n_nodes] |
105 | 115 | nodes = np.column_stack( |
106 | 116 | ( |
107 | | - np.deg2rad(self.uxgrid.grid.node_lon[node_ids].to_numpy()), |
108 | | - np.deg2rad(self.uxgrid.grid.node_lat[node_ids].to_numpy()), |
| 117 | + np.deg2rad(self.uxgrid.node_lon[node_ids].to_numpy()), |
| 118 | + np.deg2rad(self.uxgrid.node_lat[node_ids].to_numpy()), |
109 | 119 | ) |
110 | 120 | ) |
111 | 121 |
|
112 | 122 | coord = np.deg2rad([x, y]) |
113 | 123 | bcoord = np.asarray(_barycentric_coordinates(nodes, coord)) |
114 | 124 | err = abs(np.dot(bcoord, nodes[:, 0]) - coord[0]) + abs(np.dot(bcoord, nodes[:, 1]) - coord[1]) |
115 | 125 | 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.uxgrid.node_x[node_ids].values, |
| 138 | + self.uxgrid.node_y[node_ids].values, |
| 139 | + self.uxgrid.node_z[node_ids].values, |
| 140 | + ), |
| 141 | + axis=-1, |
| 142 | + ) |
| 143 | + |
| 144 | + bcoord = np.asarray(_barycentric_coordinates_cartesian(nodes, cart_coord)) |
| 145 | + |
| 146 | + return bcoord |
| 147 | + |
| 148 | + |
| 149 | +def _barycentric_coordinates_cartesian(nodes, point, min_area=1e-8): |
| 150 | + """ |
| 151 | + Compute the barycentric coordinates of a point P inside a convex polygon using area-based weights. |
| 152 | + So that this method generalizes to n-sided polygons, we use the Waschpress points as the generalized |
| 153 | + barycentric coordinates, which is only valid for convex polygons. |
| 154 | +
|
| 155 | + Parameters |
| 156 | + ---------- |
| 157 | + nodes : numpy.ndarray |
| 158 | + Cartesian coordinates (x,y,z) of each corner node of a face |
| 159 | + point : numpy.ndarray |
| 160 | + Cartesian coordinates (x,y,z) of the point |
| 161 | +
|
| 162 | + Returns |
| 163 | + ------- |
| 164 | + numpy.ndarray |
| 165 | + Barycentric coordinates corresponding to each vertex. |
| 166 | +
|
| 167 | + """ |
| 168 | + n = len(nodes) |
| 169 | + sum_wi = 0 |
| 170 | + w = [] |
| 171 | + |
| 172 | + for i in range(0, n): |
| 173 | + vim1 = nodes[i - 1] |
| 174 | + vi = nodes[i] |
| 175 | + vi1 = nodes[(i + 1) % n] |
| 176 | + a0 = _triangle_area_cartesian(vim1, vi, vi1) |
| 177 | + a1 = max(_triangle_area_cartesian(point, vim1, vi), min_area) |
| 178 | + a2 = max(_triangle_area_cartesian(point, vi, vi1), min_area) |
| 179 | + sum_wi += a0 / (a1 * a2) |
| 180 | + w.append(a0 / (a1 * a2)) |
| 181 | + |
| 182 | + barycentric_coords = [w_i / sum_wi for w_i in w] |
| 183 | + |
| 184 | + return barycentric_coords |
| 185 | + |
| 186 | + |
| 187 | +def _triangle_area_cartesian(A, B, C): |
| 188 | + """Compute the area of a triangle given by three points.""" |
| 189 | + d1 = B - A |
| 190 | + d2 = C - A |
| 191 | + d3 = np.cross(d1, d2) |
| 192 | + return 0.5 * np.linalg.norm(d3) |
0 commit comments