Skip to content

Commit 178eebd

Browse files
authored
Merge pull request #269 from OceanParcels/brownian_diffusion
Brownian diffusion
2 parents 6f7370b + a4801f5 commit 178eebd

6 files changed

Lines changed: 149 additions & 44 deletions

File tree

docs/index.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ on the `Github Development Timeline page
3333
**Major features**
3434

3535
* Advection of particles in 2D using inbuilt kernels for Runge-Kutta4, Runge-Kutta45 and Euler Forward and in 3D using the inbuilt kernel for Runge-Kutta4_3D (see :mod:`parcels.kernels.advection`)
36+
* Simple horizontal diffusion of particles using inbuilt Brownian Motion kernel (see :mod:`parcels.kernels.diffusion`)
3637
* Ability to define and execute custom kernels (see `the Adding-a-custom-behaviour-kernel part of the Tutorial <http://nbviewer.jupyter.org/github/OceanParcels/parcels/blob/master/parcels/examples/parcels_tutorial.ipynb#Adding-a-custom-behaviour-kernel>`_)
3738
* Ability to add custom Variables to Particles (see `the Sampling-a-Field-with-Particles part of the Tutorial <http://nbviewer.jupyter.org/github/OceanParcels/parcels/blob/master/parcels/examples/parcels_tutorial.ipynb#Sampling-a-Field-with-Particles>`_)
3839
* Ability to add and remove Particles (see :func:`parcels.particleset.ParticleSet.add` and :func:`parcels.particleset.ParticleSet.remove`)
@@ -49,7 +50,7 @@ on the `Github Development Timeline page
4950

5051
**Future development goals**
5152

52-
* Diffusion of particles using suite of inbuilt kernels
53+
* More types of diffusion of particles using suite of inbuilt kernels
5354
* Support for unstructured grids
5455
* Implementation of parallel execution using tiling of the domain
5556
* Faster and more efficient code

parcels/examples/example_brownian.py

Lines changed: 31 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,59 @@
1-
from parcels import FieldSet, ParticleSet, ScipyParticle, JITParticle
1+
from parcels import FieldSet, Field, ParticleSet, ScipyParticle, JITParticle, BrownianMotion2D
22
import numpy as np
33
from datetime import timedelta as delta
4-
import math
54
from parcels import random
65
import pytest
76

87
ptype = {'scipy': ScipyParticle, 'jit': JITParticle}
98

109

11-
def two_dim_brownian_flat(particle, fieldset, time, dt):
12-
# Kernel for simple Brownian particle diffusion in zonal and meridional direction.
13-
14-
particle.lat += random.normalvariate(0, 1)*math.sqrt(2*dt*fieldset.Kh_meridional)
15-
particle.lon += random.normalvariate(0, 1)*math.sqrt(2*dt*fieldset.Kh_zonal)
16-
17-
18-
def brownian_fieldset(xdim=200, ydim=200): # Define a flat fieldset of zeros, for simplicity.
19-
dimensions = {'lon': np.linspace(0, 600000, xdim, dtype=np.float32),
20-
'lat': np.linspace(0, 600000, ydim, dtype=np.float32)}
10+
def zeros_fieldset(xdim=2, ydim=2):
11+
"""Generates a zero velocity field"""
12+
lon = np.linspace(-20, 20, xdim, dtype=np.float32)
13+
lat = np.linspace(-20, 20, ydim, dtype=np.float32)
2114

15+
dimensions = {'lon': lon, 'lat': lat}
2216
data = {'U': np.zeros((xdim, ydim), dtype=np.float32),
2317
'V': np.zeros((xdim, ydim), dtype=np.float32)}
24-
25-
return FieldSet.from_data(data, dimensions, mesh='flat')
18+
return FieldSet.from_data(data, dimensions, mesh='spherical')
2619

2720

2821
@pytest.mark.parametrize('mode', ['scipy', 'jit'])
2922
def test_brownian_example(mode, npart=3000):
30-
fieldset = brownian_fieldset()
23+
fieldset = zeros_fieldset()
3124

3225
# Set diffusion constants.
33-
fieldset.Kh_meridional = 100.
34-
fieldset.Kh_zonal = 100.
26+
kh_zonal = 100
27+
kh_meridional = 100
28+
29+
# Create field of Kh_zonal and Kh_meridional, using same grid as U
30+
grid = fieldset.U.grid
31+
fieldset.add_field(Field('Kh_zonal', kh_zonal*np.ones((2, 2)), grid=grid))
32+
fieldset.add_field(Field('Kh_meridional', kh_meridional*np.ones((2, 2)), grid=grid))
3533

3634
# Set random seed
3735
random.seed(123456)
3836

39-
ptcls_start = 300000. # Start all particles at same location in middle of grid.
40-
pset = ParticleSet.from_line(fieldset=fieldset, size=npart, pclass=ptype[mode],
41-
start=(ptcls_start, ptcls_start),
42-
finish=(ptcls_start, ptcls_start))
43-
44-
endtime = delta(days=1)
45-
dt = delta(hours=1)
46-
interval = delta(hours=1)
37+
runtime = delta(days=1)
4738

48-
k_brownian = pset.Kernel(two_dim_brownian_flat)
39+
random.seed(1234)
40+
pset = ParticleSet(fieldset=fieldset, pclass=ptype[mode],
41+
lon=np.zeros(npart), lat=np.zeros(npart))
42+
pset.execute(pset.Kernel(BrownianMotion2D),
43+
runtime=runtime, dt=delta(hours=1))
4944

50-
pset.execute(k_brownian, endtime=endtime, dt=dt, interval=interval,
51-
output_file=pset.ParticleFile(name="BrownianParticle"),
52-
show_movie=False)
45+
expected_std_x = np.sqrt(2*kh_zonal*runtime.total_seconds())
46+
expected_std_y = np.sqrt(2*kh_meridional*runtime.total_seconds())
5347

54-
lats = np.array([particle.lat for particle in pset.particles])
55-
lons = np.array([particle.lon for particle in pset.particles])
56-
expected_std_lat = np.sqrt(2*fieldset.Kh_meridional*endtime.total_seconds())
57-
expected_std_lon = np.sqrt(2*fieldset.Kh_zonal*endtime.total_seconds())
48+
conversion = (1852 * 60) # to convert from degrees to m
49+
ys = np.array([p.lat for p in pset]) * conversion
50+
xs = np.array([p.lon for p in pset]) * conversion # since near equator, we do not need to care about curvature effect
5851

59-
assert np.allclose(np.std(lats), expected_std_lat, rtol=.1)
60-
assert np.allclose(np.std(lons), expected_std_lon, rtol=.1)
61-
assert np.allclose(np.mean(lons), ptcls_start, rtol=.1)
62-
assert np.allclose(np.mean(lats), ptcls_start, rtol=.1)
52+
tol = 200 # 200m tolerance
53+
assert np.allclose(np.std(xs), expected_std_x, atol=tol)
54+
assert np.allclose(np.std(ys), expected_std_y, atol=tol)
55+
assert np.allclose(np.mean(xs), 0, atol=tol)
56+
assert np.allclose(np.mean(ys), 0, atol=tol)
6357

6458

6559
if __name__ == "__main__":

parcels/field.py

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
CurvilinearSGrid, CGrid, GridCode)
1515

1616

17-
__all__ = ['CentralDifferences', 'Field', 'Geographic', 'GeographicPolar']
17+
__all__ = ['CentralDifferences', 'Field', 'Geographic', 'GeographicPolar', 'GeographicSquare', 'GeographicPolarSquare']
1818

1919

2020
class FieldSamplingError(RuntimeError):
@@ -139,6 +139,44 @@ def ccode_to_source(self, x, y, z):
139139
return "(1000. * 1.852 * 60. * cos(%s * M_PI / 180))" % y
140140

141141

142+
class GeographicSquare(UnitConverter):
143+
""" Square distance converter from geometric to geographic coordinates (m2 to degree2) """
144+
source_unit = 'm2'
145+
target_unit = 'degree2'
146+
147+
def to_target(self, value, x, y, z):
148+
return value / pow(1000. * 1.852 * 60., 2)
149+
150+
def to_source(self, value, x, y, z):
151+
return value * pow(1000. * 1.852 * 60., 2)
152+
153+
def ccode_to_target(self, x, y, z):
154+
return "pow(1.0 / (1000.0 * 1.852 * 60.0), 2)"
155+
156+
def ccode_to_source(self, x, y, z):
157+
return "pow((1000.0 * 1.852 * 60.0), 2)"
158+
159+
160+
class GeographicPolarSquare(UnitConverter):
161+
""" Square distance converter from geometric to geographic coordinates (m2 to degree2)
162+
with a correction to account for narrower grid cells closer to the poles.
163+
"""
164+
source_unit = 'm2'
165+
target_unit = 'degree2'
166+
167+
def to_target(self, value, x, y, z):
168+
return value / pow(1000. * 1.852 * 60. * cos(y * pi / 180), 2)
169+
170+
def to_source(self, value, x, y, z):
171+
return value * pow(1000. * 1.852 * 60. * cos(y * pi / 180), 2)
172+
173+
def ccode_to_target(self, x, y, z):
174+
return "pow(1.0 / (1000. * 1.852 * 60. * cos(%s * M_PI / 180)), 2)" % y
175+
176+
def ccode_to_source(self, x, y, z):
177+
return "pow((1000. * 1.852 * 60. * cos(%s * M_PI / 180)), 2)" % y
178+
179+
142180
class Field(object):
143181
"""Class that encapsulates access to field data.
144182
@@ -168,6 +206,10 @@ class Field(object):
168206
This flag overrides the allow_time_interpolation and sets it to False
169207
"""
170208

209+
unitconverters = {'U': GeographicPolar(), 'V': Geographic(),
210+
'Kh_zonal': GeographicPolarSquare(),
211+
'Kh_meridional': GeographicSquare()}
212+
171213
def __init__(self, name, data, lon=None, lat=None, depth=None, time=None, grid=None,
172214
transpose=False, vmin=None, vmax=None, time_origin=0,
173215
interp_method='linear', allow_time_extrapolation=None, time_periodic=False, mesh='flat'):
@@ -186,12 +228,10 @@ def __init__(self, name, data, lon=None, lat=None, depth=None, time=None, grid=N
186228
self.lat = self.grid.lat
187229
self.depth = self.grid.depth
188230
self.time = self.grid.time
189-
if self.grid.mesh is 'flat' or (name is not 'U' and name is not 'V'):
231+
if self.grid.mesh is 'flat' or (name not in self.unitconverters.keys()):
190232
self.units = UnitConverter()
191-
elif self.grid.mesh is 'spherical' and name == 'U':
192-
self.units = GeographicPolar()
193-
elif self.grid.mesh is 'spherical' and name == 'V':
194-
self.units = Geographic()
233+
elif self.grid.mesh is 'spherical':
234+
self.units = self.unitconverters[name]
195235
else:
196236
raise ValueError("Unsupported mesh type. Choose either: 'spherical' or 'flat'")
197237
self.interp_method = interp_method

parcels/kernels/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from .advection import * # noqa
2+
from .diffusion import * # noqa
23
from .error import * # noqa

parcels/kernels/diffusion.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from parcels import rng as random
2+
import math
3+
4+
5+
__all__ = ['BrownianMotion2D']
6+
7+
8+
def BrownianMotion2D(particle, fieldset, time, dt):
9+
# Kernel for simple Brownian particle diffusion in zonal and meridional direction.
10+
# Assumes that fieldset has fields Kh_zonal and Kh_meridional
11+
12+
r = 1/3.
13+
kh_meridional = fieldset.Kh_meridional[time, particle.lon, particle.lat, particle.depth]
14+
particle.lat += random.uniform(-1., 1.) * math.sqrt(2*math.fabs(dt)*kh_meridional/r)
15+
kh_zonal = fieldset.Kh_zonal[time, particle.lon, particle.lat, particle.depth]
16+
particle.lon += random.uniform(-1., 1.) * math.sqrt(2*math.fabs(dt)*kh_zonal/r)

tests/test_diffusion.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from parcels import (FieldSet, Field, RectilinearZGrid, ParticleSet, BrownianMotion2D,
2+
JITParticle, ScipyParticle)
3+
from parcels import rng as random
4+
from datetime import timedelta as delta
5+
import numpy as np
6+
import pytest
7+
8+
ptype = {'scipy': ScipyParticle, 'jit': JITParticle}
9+
10+
11+
def zeros_fieldset(mesh='spherical', xdim=200, ydim=100, mesh_conversion=1):
12+
"""Generates a zero velocity field"""
13+
lon = np.linspace(-1e5*mesh_conversion, 1e5*mesh_conversion, xdim, dtype=np.float32)
14+
lat = np.linspace(-1e5*mesh_conversion, 1e5*mesh_conversion, ydim, dtype=np.float32)
15+
16+
dimensions = {'lon': lon, 'lat': lat}
17+
data = {'U': np.zeros((xdim, ydim), dtype=np.float32),
18+
'V': np.zeros((xdim, ydim), dtype=np.float32)}
19+
return FieldSet.from_data(data, dimensions, mesh=mesh)
20+
21+
22+
@pytest.mark.parametrize('mesh', ['spherical', 'flat'])
23+
@pytest.mark.parametrize('mode', ['scipy', 'jit'])
24+
def test_fieldKh_Brownian(mesh, mode, xdim=200, ydim=100, kh_zonal=100, kh_meridional=50):
25+
mesh_conversion = 1/1852./60 if mesh is 'spherical' else 1
26+
fieldset = zeros_fieldset(mesh=mesh, xdim=xdim, ydim=ydim, mesh_conversion=mesh_conversion)
27+
28+
vec = np.linspace(-1e5*mesh_conversion, 1e5*mesh_conversion, 2)
29+
grid = RectilinearZGrid('K_grid', lon=vec, lat=vec, mesh=mesh)
30+
31+
fieldset.add_field(Field('Kh_zonal', kh_zonal*np.ones((2, 2)), grid=grid))
32+
fieldset.add_field(Field('Kh_meridional', kh_meridional*np.ones((2, 2)), grid=grid))
33+
34+
npart = 1000
35+
runtime = delta(days=1)
36+
37+
random.seed(1234)
38+
pset = ParticleSet(fieldset=fieldset, pclass=ptype[mode],
39+
lon=np.zeros(npart), lat=np.zeros(npart))
40+
pset.execute(pset.Kernel(BrownianMotion2D),
41+
runtime=runtime, dt=delta(hours=1))
42+
43+
expected_std_lon = np.sqrt(2*kh_zonal*mesh_conversion**2*runtime.total_seconds())
44+
expected_std_lat = np.sqrt(2*kh_meridional*mesh_conversion**2*runtime.total_seconds())
45+
46+
lats = np.array([p.lat for p in pset])
47+
lons = np.array([p.lon for p in pset])
48+
49+
tol = 200*mesh_conversion # effectively 200 m errors
50+
assert np.allclose(np.std(lats), expected_std_lat, atol=tol)
51+
assert np.allclose(np.std(lons), expected_std_lon, atol=tol)
52+
assert np.allclose(np.mean(lons), 0, atol=tol)
53+
assert np.allclose(np.mean(lats), 0, atol=tol)

0 commit comments

Comments
 (0)