diff --git a/docs/examples/tutorial_kernelloop.ipynb b/docs/examples/tutorial_kernelloop.ipynb
index 0fff52d81..ffdc47855 100644
--- a/docs/examples/tutorial_kernelloop.ipynb
+++ b/docs/examples/tutorial_kernelloop.ipynb
@@ -26,17 +26,7 @@
"\n",
"When you run a Parcels simulation (i.e. a call to `pset.execute()`), the Kernel loop is the main part of the code that is executed. This part of the code loops through all particles and executes the Kernels that are defined for each particle.\n",
"\n",
- "In order to make sure that the displacements of a particle in the different Kernels can be summed, all Kernels add to a _change_ in position (`particle_dlon`, `particle_dlat`, and `particle_ddepth`). This is important, because there are situations where movement kernels would otherwise not commute. Take the example of advecting particles by currents _and_ winds. If the particle would first be moved by the currents and then by the winds, the result could be different from first moving by the winds and then by the currents. Instead, by adding the changes in position, the ordering of the Kernels has no consequence on the particle displacement."
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "
\n",
- "\n",
- "__Note__ that the variables `particle_dlon`, `particle_dlat`, and `particle_ddepth` are defined by the wrapper function that Parcels generates for each Kernel. This is why you don't have to define these variables yourself when writing a Kernel. See [here](https://github.com/OceanParcels/parcels/blob/daa4b062ed8ae0b2be3d87367d6b45599d6f95db/parcels/kernel.py#L277-L294) for the implementation of the wrapper functions.\n",
- "
"
+ "In order to make sure that the displacements of a particle in the different Kernels can be summed, all Kernels add to a _change_ in position (`particles.dlon`, `particles.dlat`, and `particles.ddepth`). This is important, because there are situations where movement kernels would otherwise not commute. Take the example of advecting particles by currents _and_ winds. If the particle would first be moved by the currents and then by the winds, the result could be different from first moving by the winds and then by the currents. Instead, by adding the changes in position, the ordering of the Kernels has no consequence on the particle displacement."
]
},
{
@@ -52,29 +42,25 @@
"source": [
"Below is a structured overview of the Kernel loop is implemented. Note that this is for longitude only, but the same process is applied for latitude and depth.\n",
"\n",
- "1. Define an extra variable `particle.lon_nextloop` for each particle, which is the longitude at the end of the Kernel loop. Inititalise it to `particle.lon`.\n",
- "\n",
- "2. Also define an extra variable `particle.time_nextloop` for each particle, which is the time at the end of the Kernel loop. Inititalise it to `particle.time`.\n",
+ "1. Initialise an extra Variable `particles.lon=0` and `particles.time_nextloop = particles.time`\n",
"\n",
- "3. Within the Kernel loop, for each particle:
\n",
+ "2. Within the Kernel loop, for each particle:
\n",
"\n",
- " 1. Update `particle.lon` with `particle.lon_nextloop`
\n",
+ " 1. Update `particles.lon += particles.dlon`
\n",
"\n",
- " 2. Update `particle.time` with `particle.time_nextloop`
\n",
+ " 2. Set variable `particles.dlon = 0`
\n",
"\n",
- " 3. Set local variable `particle_dlon = 0`
\n",
+ " 3. Update `particles.time = particles.time_nextloop`\n",
"\n",
" 4. For each Kernel in the list of Kernels:\n",
" \n",
" 1. Execute the Kernel\n",
" \n",
- " 2. Update `particle_dlon` by adding the change in longitude, if needed
\n",
+ " 2. Update `particles.dlon` by adding the change in longitude, if needed
\n",
"\n",
- " 5. Update `particle.lon_nextloop` with `particle.lon + particle_dlon`
\n",
- " \n",
- " 6. Update `particle.time_nextloop` with `particle.time + particle.dt`
\n",
+ " 5. Update `particles.time_nextloop += particles.dt`
\n",
"\n",
- " 7. If `outputdt` is a multiple of `particle.time`, write `particle.lon` and `particle.time` to zarr output file
\n",
+ " 6. If `outputdt` is a multiple of `particle.time`, write `particle.lon` and `particle.time` to zarr output file
\n",
"\n",
"Besides having commutable Kernels, the main advantage of this implementation is that, when using Field Sampling with e.g. `particle.temp = fieldset.Temp[particle.time, particle.depth, particle.lat, particle.lon]`, the particle location stays the same throughout the entire Kernel loop. Additionally, this implementation ensures that the particle location is the same as the location of the sampled field in the output file."
]
@@ -268,12 +254,12 @@
"### 1. Avoid updating particle locations directly in Kernels\n",
"It is better not to update `particle.lon` directly in a Kernel, as it can interfere with the loop above. Assigning a value to `particle.lon` in a Kernel will throw a warning. \n",
"\n",
- "Instead, update the local variable `particle_dlon`.\n",
+ "Instead, update the local variable `particle.dlon`.\n",
"\n",
"### 2. Be careful with updating particle variables that do not depend on Fields.\n",
"While assigning the interpolated value of a `Field` to a Particle goes well in the loop above, this is not necessarily so for assigning other attributes. For example, a line like `particle.age += particle.dt` is executed directly so may result in the age being `dt` at `time = 0` in the output file. \n",
"\n",
- "A workaround is to either initialise the age to `-dt`, or to increase the `age` only when `particle.time > 0` (using an `if` statement).\n",
+ "A workaround is to either initialise the age to `-dt`, or to increase the `age` only when `particle.time > 0` (using an `np.where` statement).\n",
"\n",
"\n",
"### 3. The last time is not written to file\n",
diff --git a/parcels/kernel.py b/parcels/kernel.py
index 9b3bc9978..068b8faad 100644
--- a/parcels/kernel.py
+++ b/parcels/kernel.py
@@ -118,21 +118,20 @@ def add_positionupdate_kernels(self):
def Setcoords(particles, fieldset): # pragma: no cover
import numpy as np # noqa
+ particles.lon += particles.dlon
+ particles.lat += particles.dlat
+ particles.depth += particles.ddepth
+
particles.dlon = 0
particles.dlat = 0
particles.ddepth = 0
- particles.lon = particles.lon_nextloop
- particles.lat = particles.lat_nextloop
- particles.depth = particles.depth_nextloop
+
particles.time = particles.time_nextloop
- def Updatecoords(particles, fieldset): # pragma: no cover
- particles.lon_nextloop = particles.lon + particles.dlon
- particles.lat_nextloop = particles.lat + particles.dlat
- particles.depth_nextloop = particles.depth + particles.ddepth
+ def UpdateTime(particles, fieldset): # pragma: no cover
particles.time_nextloop = particles.time + particles.dt
- self._pyfuncs = (Setcoords + self + Updatecoords)._pyfuncs
+ self._pyfuncs = (Setcoords + self + UpdateTime)._pyfuncs
def check_fieldsets_in_kernels(self, pyfunc): # TODO v4: this can go into another method? assert_is_compatible()?
"""
diff --git a/parcels/particle.py b/parcels/particle.py
index db2feac5f..59c345708 100644
--- a/parcels/particle.py
+++ b/parcels/particle.py
@@ -170,13 +170,11 @@ def get_default_particle(spatial_dtype: np.float32 | np.float64) -> ParticleClas
dtype=spatial_dtype,
attrs={"standard_name": "longitude", "units": "degrees_east", "axis": "X"},
),
- Variable("lon_nextloop", dtype=spatial_dtype, to_write=False),
Variable(
"lat",
dtype=spatial_dtype,
attrs={"standard_name": "latitude", "units": "degrees_north", "axis": "Y"},
),
- Variable("lat_nextloop", dtype=spatial_dtype, to_write=False),
Variable(
"depth",
dtype=spatial_dtype,
@@ -185,7 +183,6 @@ def get_default_particle(spatial_dtype: np.float32 | np.float64) -> ParticleClas
Variable("dlon", dtype=spatial_dtype, to_write=False),
Variable("dlat", dtype=spatial_dtype, to_write=False),
Variable("ddepth", dtype=spatial_dtype, to_write=False),
- Variable("depth_nextloop", dtype=spatial_dtype, to_write=False),
Variable(
"time",
dtype=_SAME_AS_FIELDSET_TIME_INTERVAL.VALUE,
diff --git a/parcels/particlefile.py b/parcels/particlefile.py
index 8fd4ea25d..bcf865cee 100644
--- a/parcels/particlefile.py
+++ b/parcels/particlefile.py
@@ -262,9 +262,9 @@ def write_latest_locations(self, pset, time):
time :
Time at which to write ParticleSet. Note that typically this would be pset.time_nextloop
"""
- for var in ["lon", "lat", "depth", "time"]:
- pset._data[f"{var}"] = pset._data[f"{var}_nextloop"]
-
+ for var in ["lon", "lat", "depth"]:
+ pset._data[f"{var}"] += pset._data[f"d{var}"]
+ pset._data["time"] = pset._data["time_nextloop"]
self.write(pset, time)
diff --git a/parcels/particleset.py b/parcels/particleset.py
index 98f4f9dc3..7db2af65c 100644
--- a/parcels/particleset.py
+++ b/parcels/particleset.py
@@ -130,9 +130,6 @@ def __init__(
lat=lat,
depth=depth,
time=time,
- lon_nextloop=lon,
- lat_nextloop=lat,
- depth_nextloop=depth,
time_nextloop=time,
trajectory=trajectory_ids,
),
diff --git a/tests/v4/test_advection.py b/tests/v4/test_advection.py
index 5308fa7ed..ea4f6ea74 100644
--- a/tests/v4/test_advection.py
+++ b/tests/v4/test_advection.py
@@ -102,8 +102,8 @@ def test_advection_zonal_periodic():
pset = ParticleSet(fieldset, pclass=PeriodicParticle, lon=startlon, lat=[0.5, 0.5])
pset.execute([AdvectionEE, periodicBC], runtime=np.timedelta64(40, "s"), dt=np.timedelta64(1, "s"))
np.testing.assert_allclose(pset.total_dlon, 4, atol=1e-5)
- np.testing.assert_allclose(pset.lon_nextloop, startlon, atol=1e-5)
- np.testing.assert_allclose(pset.lat_nextloop, 0.5, atol=1e-5)
+ np.testing.assert_allclose(pset.lon + pset.dlon, startlon, atol=1e-5)
+ np.testing.assert_allclose(pset.lat, 0.5, atol=1e-5)
def test_horizontal_advection_in_3D_flow(npart=10):
@@ -246,7 +246,7 @@ def test_radialrotation(npart=10):
pset = parcels.ParticleSet(fieldset, lon=lon, lat=lat, time=starttime)
pset.execute(parcels.AdvectionRK4, endtime=np.timedelta64(10, "m"), dt=dt)
- theta = 2 * np.pi * (pset.time_nextloop - starttime) / np.timedelta64(24 * 3600, "s")
+ theta = 2 * np.pi * (pset.time - starttime) / np.timedelta64(24 * 3600, "s")
true_lon = (lon - 30.0) * np.cos(theta) + 30.0
true_lat = -(lon - 30.0) * np.sin(theta) + 30.0
@@ -300,11 +300,11 @@ def truth_moving(x_0, y_0, t):
lon = x_0 + ds.u_g * t + (ds.u_0 - ds.u_g) / ds.f * np.sin(ds.f * t)
return lon, lat
- exp_lon, exp_lat = truth_moving(start_lon, start_lat, pset.time_nextloop[0])
- np.testing.assert_allclose(pset.lon_nextloop, exp_lon, rtol=rtol)
- np.testing.assert_allclose(pset.lat_nextloop, exp_lat, rtol=rtol)
+ exp_lon, exp_lat = truth_moving(start_lon, start_lat, pset.time[0])
+ np.testing.assert_allclose(pset.lon, exp_lon, rtol=rtol)
+ np.testing.assert_allclose(pset.lat, exp_lat, rtol=rtol)
if method == "RK4_3D":
- np.testing.assert_allclose(pset.depth_nextloop, exp_lat, rtol=rtol)
+ np.testing.assert_allclose(pset.depth, exp_lat, rtol=rtol)
@pytest.mark.parametrize(
@@ -347,9 +347,9 @@ def truth_moving(x_0, y_0, t):
)
return lon, lat
- exp_lon, exp_lat = truth_moving(start_lon, start_lat, pset.time_nextloop[0])
- np.testing.assert_allclose(pset.lon_nextloop, exp_lon, rtol=rtol)
- np.testing.assert_allclose(pset.lat_nextloop, exp_lat, rtol=rtol)
+ exp_lon, exp_lat = truth_moving(start_lon, start_lat, pset.time[0])
+ np.testing.assert_allclose(pset.lon, exp_lon, rtol=rtol)
+ np.testing.assert_allclose(pset.lat, exp_lat, rtol=rtol)
@pytest.mark.parametrize(
@@ -469,7 +469,7 @@ def periodicBC(particles, fieldset): # pragma: no cover
pset = parcels.ParticleSet(fieldset, lon=lonp, lat=latp)
pset.execute([AdvectionEE, periodicBC], runtime=runtime, dt=np.timedelta64(6, "h"))
- np.testing.assert_allclose(pset.lat_nextloop, latp, atol=1e-1)
+ np.testing.assert_allclose(pset.lat, latp, atol=1e-1)
@pytest.mark.parametrize("method", ["RK4", "RK4_3D"])
diff --git a/tests/v4/test_particleset.py b/tests/v4/test_particleset.py
index e7a29e6ca..754032865 100644
--- a/tests/v4/test_particleset.py
+++ b/tests/v4/test_particleset.py
@@ -123,7 +123,7 @@ def Addlon(particles, fieldset): # pragma: no cover
particles.dlon += particles.dt / np.timedelta64(1, "s")
pset.execute(Addlon, dt=np.timedelta64(2, "s"), runtime=np.timedelta64(8, "s"), verbose_progress=False)
- assert np.allclose([p.lon_nextloop for p in pset], [8 - t for t in times])
+ assert np.allclose([p.lon + p.dlon for p in pset], [8 - t for t in times])
def test_pset_add_explicit(fieldset):