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+
2135def 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
3146def 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
4460def 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
5875def 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
7997def 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 (
0 commit comments