Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 11 additions & 25 deletions docs/examples/tutorial_kernelloop.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
"<div class=\"alert alert-info\">\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",
"</div>"
"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."
]
},
{
Expand All @@ -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:<br>\n",
"2. Within the Kernel loop, for each particle:<br>\n",
"\n",
" 1. Update `particle.lon` with `particle.lon_nextloop`<br>\n",
" 1. Update `particles.lon += particles.dlon`<br>\n",
"\n",
" 2. Update `particle.time` with `particle.time_nextloop`<br>\n",
" 2. Set variable `particles.dlon = 0`<br>\n",
"\n",
" 3. Set local variable `particle_dlon = 0`<br>\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<br>\n",
" 2. Update `particles.dlon` by adding the change in longitude, if needed<br>\n",
"\n",
" 5. Update `particle.lon_nextloop` with `particle.lon + particle_dlon`<br>\n",
" \n",
" 6. Update `particle.time_nextloop` with `particle.time + particle.dt`<br>\n",
" 5. Update `particles.time_nextloop += particles.dt`<br>\n",
"\n",
" 7. If `outputdt` is a multiple of `particle.time`, write `particle.lon` and `particle.time` to zarr output file<br>\n",
" 6. If `outputdt` is a multiple of `particle.time`, write `particle.lon` and `particle.time` to zarr output file<br>\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."
]
Expand Down Expand Up @@ -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",
Expand Down
15 changes: 7 additions & 8 deletions parcels/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()?
"""
Expand Down
3 changes: 0 additions & 3 deletions parcels/particle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions parcels/particlefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
3 changes: 0 additions & 3 deletions parcels/particleset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
Expand Down
22 changes: 11 additions & 11 deletions tests/v4/test_advection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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"])
Expand Down
2 changes: 1 addition & 1 deletion tests/v4/test_particleset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading