Skip to content

Particle positions in output are lagged by one dt relative to time labels #2568

@fluidnumerics-joe

Description

@fluidnumerics-joe

Parcels version

afb58da

Description

When writing particle trajectories via ParticleFile, the position recorded at each output time corresponds to the particle state one dt earlier than the time label. For a uniform velocity field u = U0 where the analytical trajectory is x(t) = x0 + U0*t, the final output at t = T contains the position at t = T - dt. The error is exactly proportional to dt.

Reproducer

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.

Metadata

Metadata

Labels

Type

No type

Projects

Status

Backlog

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions