import numpy as np
import parcels
import uxarray as ux
import xarray as xr
# --- Build a minimal Delaunay grid ---
NX = 10
lon, lat = np.meshgrid(
np.linspace(0, 20, NX, dtype=np.float64),
np.linspace(0, 20, NX, dtype=np.float64),
)
lon_flat, lat_flat = lon.ravel(), lat.ravel()
mask = (
np.isclose(lon_flat, 0) | np.isclose(lon_flat, 20)
| np.isclose(lat_flat, 0) | np.isclose(lat_flat, 20)
)
uxgrid = ux.Grid.from_points(
(lon_flat, lat_flat),
method="regional_delaunay",
boundary_points=np.flatnonzero(mask),
)
uxgrid.attrs["Conventions"] = "UGRID-1.0"
# --- Uniform velocity field on face centers ---
U0 = 0.001 # degrees/s
V0 = 0.0
N_TIMESTEPS = 1
TIME = xr.date_range("2000-01-01", periods=N_TIMESTEPS, freq="1h")
zf = np.array([0.0, 1.0])
zc = np.array([0.5])
U = np.full((N_TIMESTEPS, 1, uxgrid.n_face), U0)
V = np.full((N_TIMESTEPS, 1, uxgrid.n_face), V0)
W = np.zeros((N_TIMESTEPS, 2, uxgrid.n_node))
ds = ux.UxDataset({
"U": ux.UxDataArray(U, uxgrid=uxgrid, dims=["time", "zc", "n_face"],
coords=dict(time=(["time"], TIME), zc=(["zc"], zc)),
attrs=dict(location="face", mesh="delaunay", Conventions="UGRID-1.0")),
"V": ux.UxDataArray(V, uxgrid=uxgrid, dims=["time", "zc", "n_face"],
coords=dict(time=(["time"], TIME), zc=(["zc"], zc)),
attrs=dict(location="face", mesh="delaunay", Conventions="UGRID-1.0")),
"W": ux.UxDataArray(W, uxgrid=uxgrid, dims=["time", "zf", "n_node"],
coords=dict(time=(["time"], TIME), nz=(["zf"], zf)),
attrs=dict(location="node", mesh="delaunay", Conventions="UGRID-1.0")),
}, uxgrid=uxgrid)
# --- Run advection ---
T_TOTAL = np.timedelta64(3600, "s")
DT = np.timedelta64(300, "s")
dt_s = 300.0
fieldset = parcels.FieldSet.from_ugrid_conventions(ds, mesh="flat")
pset = parcels.ParticleSet(fieldset, lon=[5.0], lat=[5.0])
pfile = parcels.ParticleFile(store="test.zarr", outputdt=DT)
pset.execute(parcels.kernels.AdvectionRK4, runtime=T_TOTAL, dt=DT,
output_file=pfile, verbose_progress=False)
# --- Check ---
lon_expected = 5.0 + U0 * 3600.0 # 8.6
lon_lagged = 5.0 + U0 * 3300.0 # 8.3
print(f"pset.lon[0] = {pset.lon[0]}")
print(f"analytical(T) = {lon_expected}")
print(f"analytical(T - dt) = {lon_lagged}")
print(f"error vs T: = {abs(pset.lon[0] - lon_expected):.6e}")
print(f"error vs T - dt: = {abs(pset.lon[0] - lon_lagged):.6e}")
# Output:
# error vs T ≈ U0 * dt = 3e-1 (proportional to dt)
# error vs T - dt ≈ 0 (machine precision)
The issue reproduces with the AdvectionEE and AdvectionRK4 time integrators (I have not tested AdvectionRK45, though I don't believe the integrator choice matters). I suspect there is an issue in the particleset.execute() and file io logic.
Parcels version
afb58da
Description
When writing particle trajectories via
ParticleFile, the position recorded at each output time corresponds to the particle state onedtearlier than the time label. For a uniform velocity fieldu = U0where the analytical trajectory isx(t) = x0 + U0*t, the final output att = Tcontains the position att = T - dt. The error is exactly proportional todt.Reproducer
The issue reproduces with the AdvectionEE and AdvectionRK4 time integrators (I have not tested AdvectionRK45, though I don't believe the integrator choice matters). I suspect there is an issue in the
particleset.execute()and file io logic.