Repository: OceanParcels/Parcels
Pull request #2096 (2025-07-18 09:44:14)

Kernelloop speedups
Some smaller speedups of the kernel loop, identified through Profiling the benchmark at https://github.com/OceanParcels/parcels-benchmarks/pull/1

These changes don't make a huge difference, but do speed up a little bit and come at no 'cost' so worthwhile to implement regardless
<!-- Feel free to remove list items that are not relevant for your changes. -->

- [x] Chose the correct base branch (`main` for v3 changes, `v4-dev` for v4 changes)
- [ ] Fixes #
- [ ] Added tests
- [ ] Added documentation



Patch:
diff --git a/parcels/kernel.py b/parcels/kernel.py
index 4b55a763..e80b6e03 100644
--- a/parcels/kernel.py
+++ b/parcels/kernel.py
@@ -75,7 +75,8 @@ class BaseKernel(abc.ABC):  # noqa # TODO v4: check if we need this BaseKernel c
         # TODO v4: need to implement ParticleFile writing of deleted particles
         # if len(indices) > 0 and self.fieldset.particlefile is not None:
         #     self.fieldset.particlefile.write(pset, None, indices=indices)
-        pset.remove_indices(indices)
+        if len(indices) > 0:
+            pset.remove_indices(indices)
 
 
 class Kernel(BaseKernel):
@@ -378,25 +379,24 @@ class Kernel(BaseKernel):
         dt :
             computational integration timestep
         """
+        sign_dt = 1 if p.dt >= 0 else -1
         while p.state in [StatusCode.Evaluate, StatusCode.Repeat]:
-            pre_dt = p.dt
-
-            sign_dt = np.sign(p.dt).astype(int)
-            if sign_dt * (endtime - p.time_nextloop) <= np.timedelta64(0, "ns"):
+            if sign_dt * (endtime - p.time_nextloop) <= 0:
                 return p
 
+            pre_dt = p.dt
             # TODO implement below later again
             # try:  # Use next_dt from AdvectionRK45 if it is set
             #     if abs(endtime - p.time_nextloop) < abs(p.next_dt) - 1e-6:
             #         p.next_dt = abs(endtime - p.time_nextloop) * sign_dt
             # except AttributeError:
-            if abs(endtime - p.time_nextloop) <= abs(p.dt):
-                p.dt = abs(endtime - p.time_nextloop) * sign_dt
+            if sign_dt * (endtime - p.time_nextloop) <= p.dt:
+                p.dt = sign_dt * (endtime - p.time_nextloop)
             res = self._pyfunc(p, self._fieldset, p.time_nextloop)
 
             if res is None:
                 if p.state == StatusCode.Success:
-                    if sign_dt * (p.time - endtime) > np.timedelta64(0, "ns"):
+                    if sign_dt * (p.time - endtime) > 0:
                         p.state = StatusCode.Evaluate
             else:
                 p.state = res
diff --git a/parcels/particleset.py b/parcels/particleset.py
index 993741d4..74a37301 100644
--- a/parcels/particleset.py
+++ b/parcels/particleset.py
@@ -797,9 +797,9 @@ class ParticleSet:
         time = start_time
         while sign_dt * (time - end_time) < 0:
             if sign_dt > 0:
-                next_time = min(time + dt, end_time)
+                next_time = end_time  # TODO update to min(next_output, end_time) when ParticleFile works
             else:
-                next_time = max(time + dt, end_time)
+                next_time = end_time  # TODO update to max(next_output, end_time) when ParticleFile works
             res = self._kernel.execute(self, endtime=next_time, dt=dt)
             if res == StatusCode.StopAllExecution:
                 return StatusCode.StopAllExecution
@@ -813,7 +813,7 @@ class ParticleSet:
                         next_output += outputdt
 
             if verbose_progress:
-                pbar.update(dt / np.timedelta64(1, "s"))
+                pbar.update((next_time - time) / np.timedelta64(1, "s"))
 
             time = next_time
 


Test patch:
diff --git a/tests/v4/test_particleset_execute.py b/tests/v4/test_particleset_execute.py
index fe97546a..f63210ad 100644
--- a/tests/v4/test_particleset_execute.py
+++ b/tests/v4/test_particleset_execute.py
@@ -113,7 +113,7 @@ def test_execution_fail_python_exception(fieldset, npart=10):
         pset.execute(PythonFail, runtime=np.timedelta64(20, "s"), dt=np.timedelta64(2, "s"))
     assert len(pset) == npart
     assert pset.time[0] == fieldset.time_interval.left + np.timedelta64(10, "s")
-    assert all([time == fieldset.time_interval.left + np.timedelta64(8, "s") for time in pset.time[1:]])
+    assert all([time == fieldset.time_interval.left + np.timedelta64(0, "s") for time in pset.time[1:]])
 
 
 @pytest.mark.parametrize("verbose_progress", [True, False])