Skip to content

Commit 254090a

Browse files
Merge pull request #2315 from Parcels-code/runge-kutta2
Adding in the Runge-Kutta 2 advection schemes
2 parents 420d91e + 29b8d37 commit 254090a

File tree

3 files changed

+73
-47
lines changed

3 files changed

+73
-47
lines changed

src/parcels/kernels/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from .advection import (
22
AdvectionAnalytical,
33
AdvectionEE,
4+
AdvectionRK2,
5+
AdvectionRK2_3D,
46
AdvectionRK4,
57
AdvectionRK4_3D,
68
AdvectionRK4_3D_CROCO,
@@ -21,6 +23,8 @@
2123
# advection
2224
"AdvectionAnalytical",
2325
"AdvectionEE",
26+
"AdvectionRK2",
27+
"AdvectionRK2_3D",
2428
"AdvectionRK4_3D_CROCO",
2529
"AdvectionRK4_3D",
2630
"AdvectionRK4",

src/parcels/kernels/advection.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,38 @@
99
__all__ = [
1010
"AdvectionAnalytical",
1111
"AdvectionEE",
12+
"AdvectionRK2",
13+
"AdvectionRK2_3D",
1214
"AdvectionRK4",
1315
"AdvectionRK4_3D",
1416
"AdvectionRK4_3D_CROCO",
1517
"AdvectionRK45",
1618
]
1719

1820

21+
def AdvectionRK2(particles, fieldset): # pragma: no cover
22+
"""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
24+
(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
29+
30+
31+
def AdvectionRK2_3D(particles, fieldset): # pragma: no cover
32+
"""Advection of particles using second-order Runge-Kutta integration including vertical velocity."""
33+
dt = particles.dt / np.timedelta64(1, "s")
34+
(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
42+
43+
1944
def AdvectionRK4(particles, fieldset): # pragma: no cover
2045
"""Advection of particles using fourth-order Runge-Kutta integration."""
2146
dt = particles.dt / np.timedelta64(1, "s") # TODO: improve API for converting dt to seconds

tests/test_advection.py

Lines changed: 44 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,14 @@
1818
AdvectionDiffusionEM,
1919
AdvectionDiffusionM1,
2020
AdvectionEE,
21+
AdvectionRK2,
22+
AdvectionRK2_3D,
2123
AdvectionRK4,
2224
AdvectionRK4_3D,
2325
AdvectionRK45,
2426
)
2527
from tests.utils import round_and_hash_float_array
2628

27-
kernel = {
28-
"EE": AdvectionEE,
29-
"RK4": AdvectionRK4,
30-
"RK4_3D": AdvectionRK4_3D,
31-
"RK45": AdvectionRK45,
32-
# "AA": AdvectionAnalytical,
33-
"AdvDiffEM": AdvectionDiffusionEM,
34-
"AdvDiffM1": AdvectionDiffusionM1,
35-
}
36-
3729

3830
@pytest.mark.parametrize("mesh", ["spherical", "flat"])
3931
def test_advection_zonal(mesh, npart=10):
@@ -256,30 +248,32 @@ def test_radialrotation(npart=10):
256248

257249

258250
@pytest.mark.parametrize(
259-
"method, rtol",
251+
"kernel, rtol",
260252
[
261-
("EE", 1e-2),
262-
("AdvDiffEM", 1e-2),
263-
("AdvDiffM1", 1e-2),
264-
("RK4", 1e-5),
265-
("RK4_3D", 1e-5),
266-
("RK45", 1e-4),
253+
(AdvectionEE, 1e-2),
254+
(AdvectionDiffusionEM, 1e-2),
255+
(AdvectionDiffusionM1, 1e-2),
256+
(AdvectionRK2, 1e-4),
257+
(AdvectionRK2_3D, 1e-4),
258+
(AdvectionRK4, 1e-5),
259+
(AdvectionRK4_3D, 1e-5),
260+
(AdvectionRK45, 1e-4),
267261
],
268262
)
269-
def test_moving_eddy(method, rtol):
263+
def test_moving_eddy(kernel, rtol):
270264
ds = moving_eddy_dataset()
271265
grid = XGrid.from_dataset(ds)
272266
U = Field("U", ds["U"], grid, interp_method=XLinear)
273267
V = Field("V", ds["V"], grid, interp_method=XLinear)
274-
if method == "RK4_3D":
268+
if kernel in [AdvectionRK2_3D, AdvectionRK4_3D]:
275269
# Using W to test 3D advection (assuming same velocity as V)
276270
W = Field("W", ds["V"], grid, interp_method=XLinear)
277271
UVW = VectorField("UVW", U, V, W)
278272
fieldset = FieldSet([U, V, W, UVW])
279273
else:
280274
UV = VectorField("UV", U, V)
281275
fieldset = FieldSet([U, V, UV])
282-
if method in ["AdvDiffEM", "AdvDiffM1"]:
276+
if kernel in [AdvectionDiffusionEM, AdvectionDiffusionM1]:
283277
# Add zero diffusivity field for diffusion kernels
284278
ds["Kh"] = (["time", "depth", "YG", "XG"], np.full(ds["U"].shape, 0))
285279
fieldset.add_field(Field("Kh", ds["Kh"], grid, interp_method=XLinear), "Kh_zonal")
@@ -289,11 +283,11 @@ def test_moving_eddy(method, rtol):
289283
start_lon, start_lat, start_z = 12000, 12500, 12500
290284
dt = np.timedelta64(30, "m")
291285

292-
if method == "RK45":
286+
if kernel == AdvectionRK45:
293287
fieldset.add_constant("RK45_tol", rtol)
294288

295289
pset = ParticleSet(fieldset, lon=start_lon, lat=start_lat, z=start_z, time=np.timedelta64(0, "s"))
296-
pset.execute(kernel[method], dt=dt, endtime=np.timedelta64(1, "h"))
290+
pset.execute(kernel, dt=dt, endtime=np.timedelta64(1, "h"))
297291

298292
def truth_moving(x_0, y_0, t):
299293
t /= np.timedelta64(1, "s")
@@ -304,19 +298,20 @@ def truth_moving(x_0, y_0, t):
304298
exp_lon, exp_lat = truth_moving(start_lon, start_lat, pset.time[0])
305299
np.testing.assert_allclose(pset.lon, exp_lon, rtol=rtol)
306300
np.testing.assert_allclose(pset.lat, exp_lat, rtol=rtol)
307-
if method == "RK4_3D":
301+
if kernel == AdvectionRK4_3D:
308302
np.testing.assert_allclose(pset.z, exp_lat, rtol=rtol)
309303

310304

311305
@pytest.mark.parametrize(
312-
"method, rtol",
306+
"kernel, rtol",
313307
[
314-
("EE", 1e-1),
315-
("RK4", 1e-5),
316-
("RK45", 1e-4),
308+
(AdvectionEE, 1e-1),
309+
(AdvectionRK2, 3e-3),
310+
(AdvectionRK4, 1e-5),
311+
(AdvectionRK45, 1e-4),
317312
],
318313
)
319-
def test_decaying_moving_eddy(method, rtol):
314+
def test_decaying_moving_eddy(kernel, rtol):
320315
ds = decaying_moving_eddy_dataset()
321316
grid = XGrid.from_dataset(ds)
322317
U = Field("U", ds["U"], grid, interp_method=XLinear)
@@ -327,12 +322,12 @@ def test_decaying_moving_eddy(method, rtol):
327322
start_lon, start_lat = 10000, 10000
328323
dt = np.timedelta64(60, "m")
329324

330-
if method == "RK45":
325+
if kernel == AdvectionRK45:
331326
fieldset.add_constant("RK45_tol", rtol)
332327
fieldset.add_constant("RK45_min_dt", 10 * 60)
333328

334329
pset = ParticleSet(fieldset, lon=start_lon, lat=start_lat, time=np.timedelta64(0, "s"))
335-
pset.execute(kernel[method], dt=dt, endtime=np.timedelta64(23, "h"))
330+
pset.execute(kernel, dt=dt, endtime=np.timedelta64(23, "h"))
336331

337332
def truth_moving(x_0, y_0, t):
338333
t /= np.timedelta64(1, "s")
@@ -354,14 +349,15 @@ def truth_moving(x_0, y_0, t):
354349

355350

356351
@pytest.mark.parametrize(
357-
"method, rtol",
352+
"kernel, rtol",
358353
[
359-
("RK4", 0.1),
360-
("RK45", 0.1),
354+
(AdvectionRK2, 0.1),
355+
(AdvectionRK4, 0.1),
356+
(AdvectionRK45, 0.1),
361357
],
362358
)
363359
@pytest.mark.parametrize("grid_type", ["A", "C"])
364-
def test_stommelgyre_fieldset(method, rtol, grid_type):
360+
def test_stommelgyre_fieldset(kernel, rtol, grid_type):
365361
npart = 2
366362
ds = stommel_gyre_dataset(grid_type=grid_type)
367363
grid = XGrid.from_dataset(ds)
@@ -377,7 +373,7 @@ def test_stommelgyre_fieldset(method, rtol, grid_type):
377373
start_lon = np.linspace(10e3, 100e3, npart)
378374
start_lat = np.ones_like(start_lon) * 5000e3
379375

380-
if method == "RK45":
376+
if kernel == AdvectionRK45:
381377
fieldset.add_constant("RK45_tol", rtol)
382378

383379
SampleParticle = Particle.add_variable(
@@ -389,19 +385,20 @@ def UpdateP(particles, fieldset): # pragma: no cover
389385
particles.p_start = np.where(particles.time == 0, particles.p, particles.p_start)
390386

391387
pset = ParticleSet(fieldset, pclass=SampleParticle, lon=start_lon, lat=start_lat, time=np.timedelta64(0, "s"))
392-
pset.execute([kernel[method], UpdateP], dt=dt, runtime=runtime)
388+
pset.execute([kernel, UpdateP], dt=dt, runtime=runtime)
393389
np.testing.assert_allclose(pset.p, pset.p_start, rtol=rtol)
394390

395391

396392
@pytest.mark.parametrize(
397-
"method, rtol",
393+
"kernel, rtol",
398394
[
399-
("RK4", 5e-3),
400-
("RK45", 1e-4),
395+
(AdvectionRK2, 2e-2),
396+
(AdvectionRK4, 5e-3),
397+
(AdvectionRK45, 1e-4),
401398
],
402399
)
403400
@pytest.mark.parametrize("grid_type", ["A"]) # TODO also implement C-grid once available
404-
def test_peninsula_fieldset(method, rtol, grid_type):
401+
def test_peninsula_fieldset(kernel, rtol, grid_type):
405402
npart = 2
406403
ds = peninsula_dataset(grid_type=grid_type)
407404
grid = XGrid.from_dataset(ds)
@@ -416,7 +413,7 @@ def test_peninsula_fieldset(method, rtol, grid_type):
416413
start_lat = np.linspace(3e3, 47e3, npart)
417414
start_lon = 3e3 * np.ones_like(start_lat)
418415

419-
if method == "RK45":
416+
if kernel == AdvectionRK45:
420417
fieldset.add_constant("RK45_tol", rtol)
421418

422419
SampleParticle = Particle.add_variable(
@@ -428,7 +425,7 @@ def UpdateP(particles, fieldset): # pragma: no cover
428425
particles.p_start = np.where(particles.time == 0, particles.p, particles.p_start)
429426

430427
pset = ParticleSet(fieldset, pclass=SampleParticle, lon=start_lon, lat=start_lat, time=np.timedelta64(0, "s"))
431-
pset.execute([kernel[method], UpdateP], dt=dt, runtime=runtime)
428+
pset.execute([kernel, UpdateP], dt=dt, runtime=runtime)
432429
np.testing.assert_allclose(pset.p, pset.p_start, rtol=rtol)
433430

434431

@@ -474,8 +471,8 @@ def periodicBC(particles, fieldset): # pragma: no cover
474471
np.testing.assert_allclose(pset.lat, latp, atol=1e-1)
475472

476473

477-
@pytest.mark.parametrize("method", ["RK4", "RK4_3D"])
478-
def test_nemo_3D_curvilinear_fieldset(method):
474+
@pytest.mark.parametrize("kernel", [AdvectionRK4, AdvectionRK4_3D])
475+
def test_nemo_3D_curvilinear_fieldset(kernel):
479476
download_dir = parcels.download_example_dataset("NemoNorthSeaORCA025-N006_data")
480477
ufiles = download_dir.glob("*U.nc")
481478
dsu = xr.open_mfdataset(ufiles, decode_times=False, drop_variables=["nav_lat", "nav_lon"])
@@ -553,10 +550,10 @@ def test_nemo_3D_curvilinear_fieldset(method):
553550
lats = np.linspace(52.5, 51.6, npart)
554551
pset = parcels.ParticleSet(fieldset, lon=lons, lat=lats, z=np.ones_like(lons))
555552

556-
pset.execute(kernel[method], runtime=np.timedelta64(3, "D") + np.timedelta64(18, "h"), dt=np.timedelta64(6, "h"))
553+
pset.execute(kernel, runtime=np.timedelta64(3, "D") + np.timedelta64(18, "h"), dt=np.timedelta64(6, "h"))
557554

558-
if method == "RK4":
555+
if kernel == AdvectionRK4:
559556
np.testing.assert_equal(round_and_hash_float_array([p.lon for p in pset], decimals=5), 29977383852960156017546)
560-
elif method == "RK4_3D":
557+
elif kernel == AdvectionRK4_3D:
561558
# TODO check why decimals needs to be so low in RK4_3D (compare to v3)
562559
np.testing.assert_equal(round_and_hash_float_array([p.z for p in pset], decimals=1), 29747210774230389239432)

0 commit comments

Comments
 (0)