Skip to content

Commit 683db93

Browse files
Add test for crossing antimeridian and fix cartesian barycentric coordinate calc
1 parent 366e9d9 commit 683db93

3 files changed

Lines changed: 126 additions & 36 deletions

File tree

parcels/_datasets/unstructured/generic.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,109 @@ def _fesom2_square_delaunay_uniform_z_coordinate():
208208
return ux.UxDataset({"U": u, "V": v, "W": w, "p": p}, uxgrid=uxgrid)
209209

210210

211+
def _fesom2_square_delaunay_antimeridian():
212+
"""
213+
Delaunay grid that crosses the antimeridian with uniform z-coordinate, mimicking a FESOM2 dataset.
214+
This dataset consists of a square domain with closed boundaries, where the grid is generated using Delaunay triangulation.
215+
The bottom topography is flat and uniform, and the vertical grid spacing is constant with 10 layers spanning [0,1000.0]
216+
The lateral velocity field components are non-zero constant, and the vertical velocity component is zero.
217+
The pressure field is constant.
218+
All fields are placed on location consistent with FESOM2 variable placement conventions
219+
"""
220+
lon, lat = np.meshgrid(
221+
np.linspace(-210.0, -150.0, Nx, dtype=np.float32), np.linspace(0, 60.0, Nx, dtype=np.float32)
222+
)
223+
# wrap longitude from [-180,180]
224+
lon = np.where(lon < -180, lon + 360, lon)
225+
lon_flat = lon.ravel()
226+
lat_flat = lat.ravel()
227+
zf = np.linspace(0.0, 1000.0, 10, endpoint=True, dtype=np.float32) # Vertical element faces
228+
zc = 0.5 * (zf[:-1] + zf[1:]) # Vertical element centers
229+
nz = zf.size
230+
nz1 = zc.size
231+
232+
# mask any point on one of the boundaries
233+
mask = (
234+
np.isclose(lon_flat, 0.0) | np.isclose(lon_flat, 60.0) | np.isclose(lat_flat, 0.0) | np.isclose(lat_flat, 60.0)
235+
)
236+
237+
boundary_points = np.flatnonzero(mask)
238+
239+
uxgrid = ux.Grid.from_points(
240+
(lon_flat, lat_flat),
241+
method="regional_delaunay",
242+
boundary_points=boundary_points,
243+
)
244+
uxgrid.attrs["Conventions"] = "UGRID-1.0"
245+
246+
# Define arrays U (zonal), V (meridional) and P (sea surface height)
247+
U = np.ones(
248+
(T, nz1, uxgrid.n_face), dtype=np.float64
249+
) # Lateral velocity is on the element centers and face centers
250+
V = np.ones(
251+
(T, nz1, uxgrid.n_face), dtype=np.float64
252+
) # Lateral velocity is on the element centers and face centers
253+
W = np.zeros(
254+
(T, nz, uxgrid.n_node), dtype=np.float64
255+
) # Vertical velocity is on the element faces and face vertices
256+
P = np.ones((T, nz1, uxgrid.n_node), dtype=np.float64) # Pressure is on the element centers and face vertices
257+
258+
u = ux.UxDataArray(
259+
data=U,
260+
name="U",
261+
uxgrid=uxgrid,
262+
dims=["time", "nz1", "n_face"],
263+
coords=dict(
264+
time=(["time"], TIME),
265+
nz1=(["nz1"], zc),
266+
),
267+
attrs=dict(
268+
description="zonal velocity", units="m/s", location="face", mesh="delaunay", Conventions="UGRID-1.0"
269+
),
270+
)
271+
v = ux.UxDataArray(
272+
data=V,
273+
name="V",
274+
uxgrid=uxgrid,
275+
dims=["time", "nz1", "n_face"],
276+
coords=dict(
277+
time=(["time"], TIME),
278+
nz1=(["nz1"], zc),
279+
),
280+
attrs=dict(
281+
description="meridional velocity", units="m/s", location="face", mesh="delaunay", Conventions="UGRID-1.0"
282+
),
283+
)
284+
w = ux.UxDataArray(
285+
data=W,
286+
name="w",
287+
uxgrid=uxgrid,
288+
dims=["time", "nz", "n_node"],
289+
coords=dict(
290+
time=(["time"], TIME),
291+
nz=(["nz"], zf),
292+
),
293+
attrs=dict(
294+
description="vertical velocity", units="m/s", location="node", mesh="delaunay", Conventions="UGRID-1.0"
295+
),
296+
)
297+
p = ux.UxDataArray(
298+
data=P,
299+
name="p",
300+
uxgrid=uxgrid,
301+
dims=["time", "nz1", "n_node"],
302+
coords=dict(
303+
time=(["time"], TIME),
304+
nz1=(["nz1"], zc),
305+
),
306+
attrs=dict(description="pressure", units="N/m^2", location="node", mesh="delaunay", Conventions="UGRID-1.0"),
307+
)
308+
309+
return ux.UxDataset({"U": u, "V": v, "W": w, "p": p}, uxgrid=uxgrid)
310+
311+
211312
datasets = {
212313
"stommel_gyre_delaunay": _stommel_gyre_delaunay(),
213314
"fesom2_square_delaunay_uniform_z_coordinate": _fesom2_square_delaunay_uniform_z_coordinate(),
315+
"fesom2_square_delaunay_antimeridian": _fesom2_square_delaunay_antimeridian(),
214316
}

parcels/uxgrid.py

Lines changed: 7 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ def try_face(fid):
7474
if (bcoords >= 0).all() and (bcoords <= 1).all() and err < tol:
7575
return bcoords
7676
else:
77-
bcoords, err = self._get_barycentric_coordinates_cartesian(y, x, fid)
78-
if (bcoords >= 0).all() and (bcoords <= 1).all() and err < tol:
77+
bcoords = self._get_barycentric_coordinates_cartesian(y, x, fid)
78+
if (bcoords >= 0).all() and (bcoords <= 1).all():
7979
return bcoords
8080

8181
return None
@@ -134,27 +134,16 @@ def _get_barycentric_coordinates_cartesian(self, y, x, fi):
134134
# Second attempt to find barycentric coordinates using cartesian coordinates
135135
nodes = np.stack(
136136
(
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(),
137+
self.uxgrid.node_x[node_ids].values,
138+
self.uxgrid.node_y[node_ids].values,
139+
self.uxgrid.node_z[node_ids].values,
140140
),
141141
axis=-1,
142142
)
143143

144144
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
145+
146+
return bcoord
158147

159148

160149
def _barycentric_coordinates_cartesian(nodes, point, min_area=1e-8):
@@ -195,23 +184,6 @@ def _barycentric_coordinates_cartesian(nodes, point, min_area=1e-8):
195184
return barycentric_coords
196185

197186

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-
215187
def _triangle_area_cartesian(A, B, C):
216188
"""Compute the area of a triangle given by three points."""
217189
d1 = B - A

tests/v4/test_uxarray_fieldset.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,26 @@ def test_fesom2_square_delaunay_uniform_z_coordinate_eval():
115115
V=Field(name="V", data=ds.V, grid=UxGrid(ds.uxgrid, z=ds.coords["nz"]), interp_method=UXPiecewiseConstantFace),
116116
W=Field(name="W", data=ds.W, grid=UxGrid(ds.uxgrid, z=ds.coords["nz"]), interp_method=UXPiecewiseLinearNode),
117117
)
118-
P = Field(name="p", data=ds.p, grid=UxGrid(ds.uxgrid, z=ds.coords["nz"]), interp_method=UXPiecewiseConstantFace)
118+
P = Field(name="p", data=ds.p, grid=UxGrid(ds.uxgrid, z=ds.coords["nz"]), interp_method=UXPiecewiseLinearNode)
119119
fieldset = FieldSet([UVW, P, UVW.U, UVW.V, UVW.W])
120120

121121
assert fieldset.U.eval(time=ds.time[0].values, z=1.0, y=30.0, x=30.0, applyConversion=False) == 1.0
122122
assert fieldset.V.eval(time=ds.time[0].values, z=1.0, y=30.0, x=30.0, applyConversion=False) == 1.0
123123
assert fieldset.W.eval(time=ds.time[0].values, z=1.0, y=30.0, x=30.0, applyConversion=False) == 0.0
124124
assert fieldset.p.eval(time=ds.time[0].values, z=1.0, y=30.0, x=30.0, applyConversion=False) == 1.0
125+
126+
127+
def test_fesom2_square_delaunay_antimeridian_eval():
128+
"""
129+
Test the evaluation of a fieldset with a FESOM2 square Delaunay grid that crosses the antimeridian.
130+
Ensures that the fieldset can be created and evaluated correctly.
131+
Since the underlying data is constant, we can check that the values are as expected.
132+
"""
133+
ds = datasets_unstructured["fesom2_square_delaunay_antimeridian"]
134+
P = Field(name="p", data=ds.p, grid=UxGrid(ds.uxgrid, z=ds.coords["nz"]), interp_method=UXPiecewiseLinearNode)
135+
fieldset = FieldSet([P])
136+
137+
assert fieldset.p.eval(time=ds.time[0].values, z=1.0, y=30.0, x=-170.0, applyConversion=False) == 1.0
138+
assert fieldset.p.eval(time=ds.time[0].values, z=1.0, y=30.0, x=-180.0, applyConversion=False) == 1.0
139+
assert fieldset.p.eval(time=ds.time[0].values, z=1.0, y=30.0, x=180.0, applyConversion=False) == 1.0
140+
assert fieldset.p.eval(time=ds.time[0].values, z=1.0, y=30.0, x=170.0, applyConversion=False) == 1.0

0 commit comments

Comments
 (0)