Skip to content

Commit e16ae5c

Browse files
Merge branch 'v4-dev' into kernelloop-explanation
2 parents b4be3a3 + cecfb2a commit e16ae5c

File tree

4 files changed

+120
-81
lines changed

4 files changed

+120
-81
lines changed

docs/getting_started/tutorial_quickstart.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ TODO: link to a list of included kernels
9999
```
100100

101101
```{code-cell}
102-
kernels = [parcels.kernels.AdvectionEE]
102+
kernels = [parcels.kernels.AdvectionRK2]
103103
```
104104

105105
## Prepare output: `ParticleFile`

src/parcels/kernels/advection.py

Lines changed: 98 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -18,101 +18,120 @@
1818
]
1919

2020

21+
def _constrain_dt_to_within_time_interval(time_interval, time, dt):
22+
"""Helper function to make sure dt does not go outside time_interval.
23+
24+
This is especially relevant for higher-order RK methods (RK2, RK4, RK45),
25+
which require interpolations at time + dt. If time is at the edges of the
26+
time_interval (typically the last integration step), such an operation would
27+
lead to an OutofTimeError.
28+
"""
29+
if time_interval:
30+
dt = np.where(time + dt <= time_interval.right, dt, time_interval.right - time)
31+
dt = np.where(time + dt >= time_interval.left, dt, time - time_interval.left)
32+
return dt.astype("timedelta64[s]")
33+
34+
2135
def AdvectionRK2(particles, fieldset): # pragma: no cover
2236
"""Advection of particles using second-order Runge-Kutta integration."""
23-
dt = particles.dt / np.timedelta64(1, "s") # TODO: improve API for converting dt to seconds
37+
dt = _constrain_dt_to_within_time_interval(fieldset.time_interval, particles.time, particles.dt)
38+
dt_flt = dt / np.timedelta64(1, "s") # TODO: improve API for converting dt to seconds
2439
(u1, v1) = fieldset.UV[particles]
25-
lon1, lat1 = (particles.lon + u1 * 0.5 * dt, particles.lat + v1 * 0.5 * dt)
26-
(u2, v2) = fieldset.UV[particles.time + 0.5 * particles.dt, particles.z, lat1, lon1, particles]
27-
particles.dlon += u2 * dt
28-
particles.dlat += v2 * dt
40+
lon1, lat1 = (particles.lon + u1 * 0.5 * dt_flt, particles.lat + v1 * 0.5 * dt_flt)
41+
(u2, v2) = fieldset.UV[particles.time + 0.5 * dt, particles.z, lat1, lon1, particles]
42+
particles.dlon += u2 * dt_flt
43+
particles.dlat += v2 * dt_flt
2944

3045

3146
def AdvectionRK2_3D(particles, fieldset): # pragma: no cover
3247
"""Advection of particles using second-order Runge-Kutta integration including vertical velocity."""
33-
dt = particles.dt / np.timedelta64(1, "s")
48+
dt = _constrain_dt_to_within_time_interval(fieldset.time_interval, particles.time, particles.dt)
49+
dt_flt = dt / np.timedelta64(1, "s")
3450
(u1, v1, w1) = fieldset.UVW[particles]
35-
lon1 = particles.lon + u1 * 0.5 * dt
36-
lat1 = particles.lat + v1 * 0.5 * dt
37-
z1 = particles.z + w1 * 0.5 * dt
38-
(u2, v2, w2) = fieldset.UVW[particles.time + 0.5 * particles.dt, z1, lat1, lon1, particles]
39-
particles.dlon += u2 * dt
40-
particles.dlat += v2 * dt
41-
particles.dz += w2 * dt
51+
lon1 = particles.lon + u1 * 0.5 * dt_flt
52+
lat1 = particles.lat + v1 * 0.5 * dt_flt
53+
z1 = particles.z + w1 * 0.5 * dt_flt
54+
(u2, v2, w2) = fieldset.UVW[particles.time + 0.5 * dt, z1, lat1, lon1, particles]
55+
particles.dlon += u2 * dt_flt
56+
particles.dlat += v2 * dt_flt
57+
particles.dz += w2 * dt_flt
4258

4359

4460
def AdvectionRK4(particles, fieldset): # pragma: no cover
4561
"""Advection of particles using fourth-order Runge-Kutta integration."""
46-
dt = particles.dt / np.timedelta64(1, "s") # TODO: improve API for converting dt to seconds
62+
dt = _constrain_dt_to_within_time_interval(fieldset.time_interval, particles.time, particles.dt)
63+
dt_flt = dt / np.timedelta64(1, "s") # TODO: improve API for converting dt to seconds
4764
(u1, v1) = fieldset.UV[particles]
48-
lon1, lat1 = (particles.lon + u1 * 0.5 * dt, particles.lat + v1 * 0.5 * dt)
49-
(u2, v2) = fieldset.UV[particles.time + 0.5 * particles.dt, particles.z, lat1, lon1, particles]
50-
lon2, lat2 = (particles.lon + u2 * 0.5 * dt, particles.lat + v2 * 0.5 * dt)
51-
(u3, v3) = fieldset.UV[particles.time + 0.5 * particles.dt, particles.z, lat2, lon2, particles]
52-
lon3, lat3 = (particles.lon + u3 * dt, particles.lat + v3 * dt)
53-
(u4, v4) = fieldset.UV[particles.time + particles.dt, particles.z, lat3, lon3, particles]
54-
particles.dlon += (u1 + 2 * u2 + 2 * u3 + u4) / 6.0 * dt
55-
particles.dlat += (v1 + 2 * v2 + 2 * v3 + v4) / 6.0 * dt
65+
lon1, lat1 = (particles.lon + u1 * 0.5 * dt_flt, particles.lat + v1 * 0.5 * dt_flt)
66+
(u2, v2) = fieldset.UV[particles.time + 0.5 * dt, particles.z, lat1, lon1, particles]
67+
lon2, lat2 = (particles.lon + u2 * 0.5 * dt_flt, particles.lat + v2 * 0.5 * dt_flt)
68+
(u3, v3) = fieldset.UV[particles.time + 0.5 * dt, particles.z, lat2, lon2, particles]
69+
lon3, lat3 = (particles.lon + u3 * dt_flt, particles.lat + v3 * dt_flt)
70+
(u4, v4) = fieldset.UV[particles.time + dt, particles.z, lat3, lon3, particles]
71+
particles.dlon += (u1 + 2 * u2 + 2 * u3 + u4) / 6.0 * dt_flt
72+
particles.dlat += (v1 + 2 * v2 + 2 * v3 + v4) / 6.0 * dt_flt
5673

5774

5875
def AdvectionRK4_3D(particles, fieldset): # pragma: no cover
5976
"""Advection of particles using fourth-order Runge-Kutta integration including vertical velocity."""
60-
dt = particles.dt / np.timedelta64(1, "s")
77+
dt = _constrain_dt_to_within_time_interval(fieldset.time_interval, particles.time, particles.dt)
78+
dt_flt = dt / np.timedelta64(1, "s")
6179
(u1, v1, w1) = fieldset.UVW[particles]
62-
lon1 = particles.lon + u1 * 0.5 * dt
63-
lat1 = particles.lat + v1 * 0.5 * dt
64-
z1 = particles.z + w1 * 0.5 * dt
65-
(u2, v2, w2) = fieldset.UVW[particles.time + 0.5 * particles.dt, z1, lat1, lon1, particles]
66-
lon2 = particles.lon + u2 * 0.5 * dt
67-
lat2 = particles.lat + v2 * 0.5 * dt
68-
z2 = particles.z + w2 * 0.5 * dt
69-
(u3, v3, w3) = fieldset.UVW[particles.time + 0.5 * particles.dt, z2, lat2, lon2, particles]
70-
lon3 = particles.lon + u3 * dt
71-
lat3 = particles.lat + v3 * dt
72-
z3 = particles.z + w3 * dt
73-
(u4, v4, w4) = fieldset.UVW[particles.time + particles.dt, z3, lat3, lon3, particles]
74-
particles.dlon += (u1 + 2 * u2 + 2 * u3 + u4) / 6 * dt
75-
particles.dlat += (v1 + 2 * v2 + 2 * v3 + v4) / 6 * dt
76-
particles.dz += (w1 + 2 * w2 + 2 * w3 + w4) / 6 * dt
80+
lon1 = particles.lon + u1 * 0.5 * dt_flt
81+
lat1 = particles.lat + v1 * 0.5 * dt_flt
82+
z1 = particles.z + w1 * 0.5 * dt_flt
83+
(u2, v2, w2) = fieldset.UVW[particles.time + 0.5 * dt, z1, lat1, lon1, particles]
84+
lon2 = particles.lon + u2 * 0.5 * dt_flt
85+
lat2 = particles.lat + v2 * 0.5 * dt_flt
86+
z2 = particles.z + w2 * 0.5 * dt_flt
87+
(u3, v3, w3) = fieldset.UVW[particles.time + 0.5 * dt, z2, lat2, lon2, particles]
88+
lon3 = particles.lon + u3 * dt_flt
89+
lat3 = particles.lat + v3 * dt_flt
90+
z3 = particles.z + w3 * dt_flt
91+
(u4, v4, w4) = fieldset.UVW[particles.time + dt, z3, lat3, lon3, particles]
92+
particles.dlon += (u1 + 2 * u2 + 2 * u3 + u4) / 6 * dt_flt
93+
particles.dlat += (v1 + 2 * v2 + 2 * v3 + v4) / 6 * dt_flt
94+
particles.dz += (w1 + 2 * w2 + 2 * w3 + w4) / 6 * dt_flt
7795

7896

7997
def AdvectionRK4_3D_CROCO(particles, fieldset): # pragma: no cover
8098
"""Advection of particles using fourth-order Runge-Kutta integration including vertical velocity.
8199
This kernel assumes the vertical velocity is the 'w' field from CROCO output and works on sigma-layers.
82100
"""
83-
dt = particles.dt / np.timedelta64(1, "s") # TODO: improve API for converting dt to seconds
101+
dt = _constrain_dt_to_within_time_interval(fieldset.time_interval, particles.time, particles.dt)
102+
dt_flt = dt / np.timedelta64(1, "s") # TODO: improve API for converting dt to seconds
84103
sig_dep = particles.z / fieldset.H[particles.time, 0, particles.lat, particles.lon]
85104

86105
(u1, v1, w1) = fieldset.UVW[particles.time, particles.z, particles.lat, particles.lon, particles]
87106
w1 *= sig_dep / fieldset.H[particles.time, 0, particles.lat, particles.lon]
88-
lon1 = particles.lon + u1 * 0.5 * dt
89-
lat1 = particles.lat + v1 * 0.5 * dt
90-
sig_dep1 = sig_dep + w1 * 0.5 * dt
107+
lon1 = particles.lon + u1 * 0.5 * dt_flt
108+
lat1 = particles.lat + v1 * 0.5 * dt_flt
109+
sig_dep1 = sig_dep + w1 * 0.5 * dt_flt
91110
dep1 = sig_dep1 * fieldset.H[particles.time, 0, lat1, lon1]
92111

93-
(u2, v2, w2) = fieldset.UVW[particles.time + 0.5 * particles.dt, dep1, lat1, lon1, particles]
112+
(u2, v2, w2) = fieldset.UVW[particles.time + 0.5 * dt, dep1, lat1, lon1, particles]
94113
w2 *= sig_dep1 / fieldset.H[particles.time, 0, lat1, lon1]
95-
lon2 = particles.lon + u2 * 0.5 * dt
96-
lat2 = particles.lat + v2 * 0.5 * dt
97-
sig_dep2 = sig_dep + w2 * 0.5 * dt
114+
lon2 = particles.lon + u2 * 0.5 * dt_flt
115+
lat2 = particles.lat + v2 * 0.5 * dt_flt
116+
sig_dep2 = sig_dep + w2 * 0.5 * dt_flt
98117
dep2 = sig_dep2 * fieldset.H[particles.time, 0, lat2, lon2]
99118

100-
(u3, v3, w3) = fieldset.UVW[particles.time + 0.5 * particles.dt, dep2, lat2, lon2, particles]
119+
(u3, v3, w3) = fieldset.UVW[particles.time + 0.5 * dt, dep2, lat2, lon2, particles]
101120
w3 *= sig_dep2 / fieldset.H[particles.time, 0, lat2, lon2]
102-
lon3 = particles.lon + u3 * dt
103-
lat3 = particles.lat + v3 * dt
104-
sig_dep3 = sig_dep + w3 * dt
121+
lon3 = particles.lon + u3 * dt_flt
122+
lat3 = particles.lat + v3 * dt_flt
123+
sig_dep3 = sig_dep + w3 * dt_flt
105124
dep3 = sig_dep3 * fieldset.H[particles.time, 0, lat3, lon3]
106125

107-
(u4, v4, w4) = fieldset.UVW[particles.time + particles.dt, dep3, lat3, lon3, particles]
126+
(u4, v4, w4) = fieldset.UVW[particles.time + dt, dep3, lat3, lon3, particles]
108127
w4 *= sig_dep3 / fieldset.H[particles.time, 0, lat3, lon3]
109-
lon4 = particles.lon + u4 * dt
110-
lat4 = particles.lat + v4 * dt
111-
sig_dep4 = sig_dep + w4 * dt
128+
lon4 = particles.lon + u4 * dt_flt
129+
lat4 = particles.lat + v4 * dt_flt
130+
sig_dep4 = sig_dep + w4 * dt_flt
112131
dep4 = sig_dep4 * fieldset.H[particles.time, 0, lat4, lon4]
113132

114-
particles.dlon += (u1 + 2 * u2 + 2 * u3 + u4) / 6 * dt
115-
particles.dlat += (v1 + 2 * v2 + 2 * v3 + v4) / 6 * dt
133+
particles.dlon += (u1 + 2 * u2 + 2 * u3 + u4) / 6 * dt_flt
134+
particles.dlat += (v1 + 2 * v2 + 2 * v3 + v4) / 6 * dt_flt
116135
particles.dz += (
117136
(dep1 - particles.z) * 2 + 2 * (dep2 - particles.z) * 2 + 2 * (dep3 - particles.z) + dep4 - particles.z
118137
) / 6
@@ -135,8 +154,9 @@ def AdvectionRK45(particles, fieldset): # pragma: no cover
135154
Time-step dt is halved if error is larger than fieldset.RK45_tol,
136155
and doubled if error is smaller than 1/10th of tolerance.
137156
"""
138-
dt = particles.dt / np.timedelta64(1, "s") # TODO: improve API for converting dt to seconds
139-
sign_dt = np.sign(dt)
157+
dt = _constrain_dt_to_within_time_interval(fieldset.time_interval, particles.time, particles.dt)
158+
dt_flt = dt / np.timedelta64(1, "s") # TODO: improve API for converting dt to seconds
159+
sign_dt = np.sign(dt_flt)
140160

141161
c = [1.0 / 4.0, 3.0 / 8.0, 12.0 / 13.0, 1.0, 1.0 / 2.0]
142162
A = [
@@ -150,42 +170,42 @@ def AdvectionRK45(particles, fieldset): # pragma: no cover
150170
b5 = [16.0 / 135.0, 0.0, 6656.0 / 12825.0, 28561.0 / 56430.0, -9.0 / 50.0, 2.0 / 55.0]
151171

152172
(u1, v1) = fieldset.UV[particles]
153-
lon1, lat1 = (particles.lon + u1 * A[0][0] * dt, particles.lat + v1 * A[0][0] * dt)
154-
(u2, v2) = fieldset.UV[particles.time + c[0] * particles.dt, particles.z, lat1, lon1, particles]
173+
lon1, lat1 = (particles.lon + u1 * A[0][0] * dt_flt, particles.lat + v1 * A[0][0] * dt_flt)
174+
(u2, v2) = fieldset.UV[particles.time + c[0] * dt, particles.z, lat1, lon1, particles]
155175
lon2, lat2 = (
156-
particles.lon + (u1 * A[1][0] + u2 * A[1][1]) * dt,
157-
particles.lat + (v1 * A[1][0] + v2 * A[1][1]) * dt,
176+
particles.lon + (u1 * A[1][0] + u2 * A[1][1]) * dt_flt,
177+
particles.lat + (v1 * A[1][0] + v2 * A[1][1]) * dt_flt,
158178
)
159-
(u3, v3) = fieldset.UV[particles.time + c[1] * particles.dt, particles.z, lat2, lon2, particles]
179+
(u3, v3) = fieldset.UV[particles.time + c[1] * dt, particles.z, lat2, lon2, particles]
160180
lon3, lat3 = (
161-
particles.lon + (u1 * A[2][0] + u2 * A[2][1] + u3 * A[2][2]) * dt,
162-
particles.lat + (v1 * A[2][0] + v2 * A[2][1] + v3 * A[2][2]) * dt,
181+
particles.lon + (u1 * A[2][0] + u2 * A[2][1] + u3 * A[2][2]) * dt_flt,
182+
particles.lat + (v1 * A[2][0] + v2 * A[2][1] + v3 * A[2][2]) * dt_flt,
163183
)
164-
(u4, v4) = fieldset.UV[particles.time + c[2] * particles.dt, particles.z, lat3, lon3, particles]
184+
(u4, v4) = fieldset.UV[particles.time + c[2] * dt, particles.z, lat3, lon3, particles]
165185
lon4, lat4 = (
166-
particles.lon + (u1 * A[3][0] + u2 * A[3][1] + u3 * A[3][2] + u4 * A[3][3]) * dt,
167-
particles.lat + (v1 * A[3][0] + v2 * A[3][1] + v3 * A[3][2] + v4 * A[3][3]) * dt,
186+
particles.lon + (u1 * A[3][0] + u2 * A[3][1] + u3 * A[3][2] + u4 * A[3][3]) * dt_flt,
187+
particles.lat + (v1 * A[3][0] + v2 * A[3][1] + v3 * A[3][2] + v4 * A[3][3]) * dt_flt,
168188
)
169-
(u5, v5) = fieldset.UV[particles.time + c[3] * particles.dt, particles.z, lat4, lon4, particles]
189+
(u5, v5) = fieldset.UV[particles.time + c[3] * dt, particles.z, lat4, lon4, particles]
170190
lon5, lat5 = (
171-
particles.lon + (u1 * A[4][0] + u2 * A[4][1] + u3 * A[4][2] + u4 * A[4][3] + u5 * A[4][4]) * dt,
172-
particles.lat + (v1 * A[4][0] + v2 * A[4][1] + v3 * A[4][2] + v4 * A[4][3] + v5 * A[4][4]) * dt,
191+
particles.lon + (u1 * A[4][0] + u2 * A[4][1] + u3 * A[4][2] + u4 * A[4][3] + u5 * A[4][4]) * dt_flt,
192+
particles.lat + (v1 * A[4][0] + v2 * A[4][1] + v3 * A[4][2] + v4 * A[4][3] + v5 * A[4][4]) * dt_flt,
173193
)
174-
(u6, v6) = fieldset.UV[particles.time + c[4] * particles.dt, particles.z, lat5, lon5, particles]
194+
(u6, v6) = fieldset.UV[particles.time + c[4] * dt, particles.z, lat5, lon5, particles]
175195

176-
lon_4th = (u1 * b4[0] + u2 * b4[1] + u3 * b4[2] + u4 * b4[3] + u5 * b4[4]) * dt
177-
lat_4th = (v1 * b4[0] + v2 * b4[1] + v3 * b4[2] + v4 * b4[3] + v5 * b4[4]) * dt
178-
lon_5th = (u1 * b5[0] + u2 * b5[1] + u3 * b5[2] + u4 * b5[3] + u5 * b5[4] + u6 * b5[5]) * dt
179-
lat_5th = (v1 * b5[0] + v2 * b5[1] + v3 * b5[2] + v4 * b5[3] + v5 * b5[4] + v6 * b5[5]) * dt
196+
lon_4th = (u1 * b4[0] + u2 * b4[1] + u3 * b4[2] + u4 * b4[3] + u5 * b4[4]) * dt_flt
197+
lat_4th = (v1 * b4[0] + v2 * b4[1] + v3 * b4[2] + v4 * b4[3] + v5 * b4[4]) * dt_flt
198+
lon_5th = (u1 * b5[0] + u2 * b5[1] + u3 * b5[2] + u4 * b5[3] + u5 * b5[4] + u6 * b5[5]) * dt_flt
199+
lat_5th = (v1 * b5[0] + v2 * b5[1] + v3 * b5[2] + v4 * b5[3] + v5 * b5[4] + v6 * b5[5]) * dt_flt
180200

181201
kappa = np.sqrt(np.pow(lon_5th - lon_4th, 2) + np.pow(lat_5th - lat_4th, 2))
182202

183-
good_particles = (kappa <= fieldset.RK45_tol) | (np.fabs(dt) <= np.fabs(fieldset.RK45_min_dt))
203+
good_particles = (kappa <= fieldset.RK45_tol) | (np.fabs(dt_flt) <= np.fabs(fieldset.RK45_min_dt))
184204
particles.dlon += np.where(good_particles, lon_5th, 0)
185205
particles.dlat += np.where(good_particles, lat_5th, 0)
186206

187207
increase_dt_particles = (
188-
good_particles & (kappa <= fieldset.RK45_tol / 10) & (np.fabs(dt * 2) <= np.fabs(fieldset.RK45_max_dt))
208+
good_particles & (kappa <= fieldset.RK45_tol / 10) & (np.fabs(dt_flt * 2) <= np.fabs(fieldset.RK45_max_dt))
189209
)
190210
particles.dt = np.where(increase_dt_particles, particles.dt * 2, particles.dt)
191211
particles.dt = np.where(

tests/test_advection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ def test_nemo_curvilinear_fieldset():
467467
runtime = np.timedelta64(160, "D")
468468

469469
pset = parcels.ParticleSet(fieldset, lon=lonp, lat=latp)
470-
pset.execute(AdvectionEE, runtime=runtime, dt=np.timedelta64(6, "h"))
470+
pset.execute(AdvectionEE, runtime=runtime, dt=np.timedelta64(10, "D"))
471471
np.testing.assert_allclose(pset.lat, latp, atol=1e-1)
472472

473473

tests/test_particleset_execute.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from parcels._datasets.structured.generic import datasets as datasets_structured
2424
from parcels._datasets.unstructured.generic import datasets as datasets_unstructured
2525
from parcels.interpolators import UXPiecewiseConstantFace, UXPiecewiseLinearNode, XLinear
26-
from parcels.kernels import AdvectionEE, AdvectionRK4, AdvectionRK4_3D
26+
from parcels.kernels import AdvectionEE, AdvectionRK2, AdvectionRK4, AdvectionRK4_3D, AdvectionRK45
2727
from tests.common_kernels import DoNothing
2828

2929

@@ -162,6 +162,25 @@ def SampleU(particles, fieldset): # pragma: no cover
162162
assert pset[0].time == endtime
163163

164164

165+
@pytest.mark.parametrize("kernel", [AdvectionEE, AdvectionRK2, AdvectionRK4, AdvectionRK45])
166+
@pytest.mark.parametrize("dt", [np.timedelta64(10, "D"), np.timedelta64(1, "D")])
167+
def test_particleset_run_RK_to_endtime_fwd_bwd(fieldset, kernel, dt):
168+
"""Test that RK kernels can be run to the endtime of a fieldset (and not throw OutsideTimeInterval)"""
169+
starttime = fieldset.time_interval.left
170+
endtime = fieldset.time_interval.right
171+
172+
# Setting zero velocities to avoid OutofBoundsErrors
173+
fieldset.U.data[:] = 0.0
174+
fieldset.V.data[:] = 0.0
175+
176+
pset = ParticleSet(fieldset, lon=[0.2], lat=[5.0], time=[starttime])
177+
pset.execute(kernel, endtime=endtime, dt=dt)
178+
assert pset[0].time == endtime
179+
180+
pset.execute(kernel, endtime=starttime, dt=-dt)
181+
assert pset[0].time == starttime
182+
183+
165184
def test_particleset_interpolate_on_domainedge(zonal_flow_fieldset):
166185
fieldset = zonal_flow_fieldset
167186

0 commit comments

Comments
 (0)