Skip to content

Commit 07f7c94

Browse files
committed
Merge branch 'v4-dev' into particle-particledata
2 parents d0a2c6a + 00c57c3 commit 07f7c94

File tree

10 files changed

+473
-56
lines changed

10 files changed

+473
-56
lines changed

parcels/_datasets/structured/generated.py

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import math
2+
13
import numpy as np
24
import xarray as xr
35

@@ -18,3 +20,267 @@ def simple_UV_dataset(dims=(360, 2, 30, 4), maxdepth=1, mesh_type="spherical"):
1820
"lon": (["XG"], np.linspace(-max_lon, max_lon, dims[3]), {"axis": "X", "c_grid_axis_shift": -0.5}),
1921
},
2022
)
23+
24+
25+
def radial_rotation_dataset(xdim=200, ydim=200): # Define 2D flat, square fieldset for testing purposes.
26+
lon = np.linspace(0, 60, xdim, dtype=np.float32)
27+
lat = np.linspace(0, 60, ydim, dtype=np.float32)
28+
29+
x0 = 30.0 # Define the origin to be the centre of the Field.
30+
y0 = 30.0
31+
32+
U = np.zeros((2, 1, ydim, xdim), dtype=np.float32)
33+
V = np.zeros((2, 1, ydim, xdim), dtype=np.float32)
34+
35+
omega = 2 * np.pi / 86400.0 # Define the rotational period as 1 day.
36+
37+
for i in range(lon.size):
38+
for j in range(lat.size):
39+
r = np.sqrt((lon[i] - x0) ** 2 + (lat[j] - y0) ** 2)
40+
assert r >= 0.0
41+
assert r <= np.sqrt(x0**2 + y0**2)
42+
43+
theta = np.arctan2((lat[j] - y0), (lon[i] - x0))
44+
assert abs(theta) <= np.pi
45+
46+
U[:, :, j, i] = r * np.sin(theta) * omega
47+
V[:, :, j, i] = -r * np.cos(theta) * omega
48+
49+
return xr.Dataset(
50+
{"U": (["time", "depth", "YG", "XG"], U), "V": (["time", "depth", "YG", "XG"], V)},
51+
coords={
52+
"time": (["time"], [np.timedelta64(0, "s"), np.timedelta64(10, "D")], {"axis": "T"}),
53+
"depth": (["depth"], np.array([0.0]), {"axis": "Z"}),
54+
"YC": (["YC"], np.arange(ydim) + 0.5, {"axis": "Y"}),
55+
"YG": (["YG"], np.arange(ydim), {"axis": "Y", "c_grid_axis_shift": -0.5}),
56+
"XC": (["XC"], np.arange(xdim) + 0.5, {"axis": "X"}),
57+
"XG": (["XG"], np.arange(xdim), {"axis": "X", "c_grid_axis_shift": -0.5}),
58+
"lat": (["YG"], lat, {"axis": "Y", "c_grid_axis_shift": 0.5}),
59+
"lon": (["XG"], lon, {"axis": "X", "c_grid_axis_shift": -0.5}),
60+
},
61+
)
62+
63+
64+
def moving_eddy_dataset(xdim=2, ydim=2): # TODO check if this also works with xdim=1, ydim=1
65+
"""Create a dataset with an eddy moving in time. Note that there is no spatial variation in the flow."""
66+
f, u_0, u_g = 1.0e-4, 0.3, 0.04 # Some constants
67+
68+
lon = np.linspace(0, 25000, xdim, dtype=np.float32)
69+
lat = np.linspace(0, 25000, ydim, dtype=np.float32)
70+
71+
time = np.arange(np.timedelta64(0, "s"), np.timedelta64(7, "h"), np.timedelta64(1, "m"))
72+
73+
U = np.zeros((len(time), 1, ydim, xdim), dtype=np.float32)
74+
V = np.zeros((len(time), 1, ydim, xdim), dtype=np.float32)
75+
76+
for t in range(len(time)):
77+
U[t, :, :, :] = u_g + (u_0 - u_g) * np.cos(f * (time[t] / np.timedelta64(1, "s")))
78+
V[t, :, :, :] = -(u_0 - u_g) * np.sin(f * (time[t] / np.timedelta64(1, "s")))
79+
80+
return xr.Dataset(
81+
{"U": (["time", "depth", "YG", "XG"], U), "V": (["time", "depth", "YG", "XG"], V)},
82+
coords={
83+
"time": (["time"], time, {"axis": "T"}),
84+
"depth": (["depth"], np.array([0.0]), {"axis": "Z"}),
85+
"YC": (["YC"], np.arange(ydim) + 0.5, {"axis": "Y"}),
86+
"YG": (["YG"], np.arange(ydim), {"axis": "Y", "c_grid_axis_shift": -0.5}),
87+
"XC": (["XC"], np.arange(xdim) + 0.5, {"axis": "X"}),
88+
"XG": (["XG"], np.arange(xdim), {"axis": "X", "c_grid_axis_shift": -0.5}),
89+
"lat": (["YG"], lat, {"axis": "Y", "c_grid_axis_shift": 0.5}),
90+
"lon": (["XG"], lon, {"axis": "X", "c_grid_axis_shift": -0.5}),
91+
},
92+
attrs={
93+
"u_0": u_0,
94+
"u_g": u_g,
95+
"f": f,
96+
},
97+
)
98+
99+
100+
def decaying_moving_eddy_dataset(xdim=2, ydim=2):
101+
"""Simulate an ocean that accelerates subject to Coriolis force
102+
and dissipative effects, upon which a geostrophic current is
103+
superimposed.
104+
105+
The original test description can be found in: N. Fabbroni, 2009,
106+
Numerical Simulation of Passive tracers dispersion in the sea,
107+
Ph.D. dissertation, University of Bologna
108+
http://amsdottorato.unibo.it/1733/1/Fabbroni_Nicoletta_Tesi.pdf
109+
"""
110+
u_g = 0.04 # Geostrophic current
111+
u_0 = 0.3 # Initial speed in x dirrection. v_0 = 0
112+
gamma = 1.0 / (2.89 * 86400) # Dissipitave effects due to viscousity.
113+
gamma_g = 1.0 / (28.9 * 86400)
114+
f = 1.0e-4 # Coriolis parameter.
115+
116+
time = np.arange(np.timedelta64(0, "s"), np.timedelta64(1, "D") + np.timedelta64(1, "h"), np.timedelta64(2, "m"))
117+
lon = np.linspace(0, 20000, xdim, dtype=np.float32)
118+
lat = np.linspace(5000, 12000, ydim, dtype=np.float32)
119+
120+
U = np.zeros((time.size, 1, lat.size, lon.size), dtype=np.float32)
121+
V = np.zeros((time.size, 1, lat.size, lon.size), dtype=np.float32)
122+
123+
for t in range(time.size):
124+
t_float = time[t] / np.timedelta64(1, "s")
125+
U[t, :, :, :] = u_g * np.exp(-gamma_g * t_float) + (u_0 - u_g) * np.exp(-gamma * t_float) * np.cos(f * t_float)
126+
V[t, :, :, :] = -(u_0 - u_g) * np.exp(-gamma * t_float) * np.sin(f * t_float)
127+
128+
return xr.Dataset(
129+
{"U": (["time", "depth", "YG", "XG"], U), "V": (["time", "depth", "YG", "XG"], V)},
130+
coords={
131+
"time": (["time"], time, {"axis": "T"}),
132+
"depth": (["depth"], np.array([0.0]), {"axis": "Z"}),
133+
"YC": (["YC"], np.arange(ydim) + 0.5, {"axis": "Y"}),
134+
"YG": (["YG"], np.arange(ydim), {"axis": "Y", "c_grid_axis_shift": -0.5}),
135+
"XC": (["XC"], np.arange(xdim) + 0.5, {"axis": "X"}),
136+
"XG": (["XG"], np.arange(xdim), {"axis": "X", "c_grid_axis_shift": -0.5}),
137+
"lat": (["YG"], lat, {"axis": "Y", "c_grid_axis_shift": 0.5}),
138+
"lon": (["XG"], lon, {"axis": "X", "c_grid_axis_shift": -0.5}),
139+
},
140+
attrs={
141+
"u_0": u_0,
142+
"u_g": u_g,
143+
"f": f,
144+
"gamma": gamma,
145+
"gamma_g": gamma_g,
146+
},
147+
)
148+
149+
150+
def peninsula_dataset(xdim=100, ydim=50, mesh="flat", grid_type="A"):
151+
"""Construct a fieldset encapsulating the flow field around an idealised peninsula.
152+
153+
Parameters
154+
----------
155+
xdim :
156+
Horizontal dimension of the generated fieldset
157+
ydim :
158+
Vertical dimension of the generated fieldset
159+
mesh : str
160+
String indicating the type of mesh coordinates and
161+
units used during velocity interpolation:
162+
163+
1. spherical: Lat and lon in degree, with a
164+
correction for zonal velocity U near the poles.
165+
2. flat (default): No conversion, lat/lon are assumed to be in m.
166+
grid_type :
167+
Option whether grid is either Arakawa A (default) or C
168+
169+
The original test description can be found in Fig. 2.2.3 in:
170+
North, E. W., Gallego, A., Petitgas, P. (Eds). 2009. Manual of
171+
recommended practices for modelling physical - biological
172+
interactions during fish early life.
173+
ICES Cooperative Research Report No. 295. 111 pp.
174+
http://archimer.ifremer.fr/doc/00157/26792/24888.pdf
175+
"""
176+
domainsizeX, domainsizeY = (1.0e5, 5.0e4)
177+
La = np.linspace(1e3, domainsizeX, xdim, dtype=np.float32)
178+
Wa = np.linspace(1e3, domainsizeY, ydim, dtype=np.float32)
179+
180+
u0 = 1
181+
x0 = domainsizeX / 2
182+
R = 0.32 * domainsizeX / 2
183+
184+
# Create the fields
185+
P = np.zeros((ydim, xdim), dtype=np.float32)
186+
U = np.zeros_like(P)
187+
V = np.zeros_like(P)
188+
x, y = np.meshgrid(La, Wa, sparse=True, indexing="xy")
189+
P[:, :] = u0 * R**2 * y / ((x - x0) ** 2 + y**2) - u0 * y
190+
191+
# Set land points to zero
192+
landpoints = P >= 0.0
193+
P[landpoints] = 0.0
194+
195+
if grid_type == "A":
196+
U[:, :] = u0 - u0 * R**2 * ((x - x0) ** 2 - y**2) / (((x - x0) ** 2 + y**2) ** 2)
197+
V[:, :] = -2 * u0 * R**2 * ((x - x0) * y) / (((x - x0) ** 2 + y**2) ** 2)
198+
U[landpoints] = 0.0
199+
V[landpoints] = 0.0
200+
Udims = ["YC", "XG"]
201+
Vdims = ["YG", "XC"]
202+
elif grid_type == "C":
203+
U = np.zeros(P.shape)
204+
V = np.zeros(P.shape)
205+
V[:, 1:] = (P[:, 1:] - P[:, :-1]) / (La[1] - La[0])
206+
U[1:, :] = -(P[1:, :] - P[:-1, :]) / (Wa[1] - Wa[0])
207+
Udims = ["YG", "XG"]
208+
Vdims = ["YG", "XG"]
209+
else:
210+
raise RuntimeError(f"Grid_type {grid_type} is not a valid option")
211+
212+
# Convert from m to lat/lon for spherical meshes
213+
lon = La / 1852.0 / 60.0 if mesh == "spherical" else La
214+
lat = Wa / 1852.0 / 60.0 if mesh == "spherical" else Wa
215+
216+
return xr.Dataset(
217+
{
218+
"U": (Udims, U),
219+
"V": (Vdims, V),
220+
"P": (["YG", "XG"], P),
221+
},
222+
coords={
223+
"YC": (["YC"], np.arange(ydim) + 0.5, {"axis": "Y"}),
224+
"YG": (["YG"], np.arange(ydim), {"axis": "Y", "c_grid_axis_shift": -0.5}),
225+
"XC": (["XC"], np.arange(xdim) + 0.5, {"axis": "X"}),
226+
"XG": (["XG"], np.arange(xdim), {"axis": "X", "c_grid_axis_shift": -0.5}),
227+
"lat": (["YG"], lat, {"axis": "Y", "c_grid_axis_shift": 0.5}),
228+
"lon": (["XG"], lon, {"axis": "X", "c_grid_axis_shift": -0.5}),
229+
},
230+
)
231+
232+
233+
def stommel_gyre_dataset(xdim=200, ydim=200, grid_type="A"):
234+
"""Simulate a periodic current along a western boundary, with significantly
235+
larger velocities along the western edge than the rest of the region
236+
237+
The original test description can be found in: N. Fabbroni, 2009,
238+
Numerical Simulation of Passive tracers dispersion in the sea,
239+
Ph.D. dissertation, University of Bologna
240+
http://amsdottorato.unibo.it/1733/1/Fabbroni_Nicoletta_Tesi.pdf
241+
"""
242+
a = b = 10000 * 1e3
243+
scalefac = 0.05 # to scale for physically meaningful velocities
244+
dx, dy = a / xdim, b / ydim
245+
246+
# Coordinates of the test fieldset (on A-grid in deg)
247+
lon = np.linspace(0, a, xdim, dtype=np.float32)
248+
lat = np.linspace(0, b, ydim, dtype=np.float32)
249+
250+
# Define arrays U (zonal), V (meridional) and P (sea surface height)
251+
U = np.zeros((lat.size, lon.size), dtype=np.float32)
252+
V = np.zeros((lat.size, lon.size), dtype=np.float32)
253+
P = np.zeros((lat.size, lon.size), dtype=np.float32)
254+
255+
beta = 2e-11
256+
r = 1 / (11.6 * 86400)
257+
es = r / (beta * a)
258+
259+
for j in range(lat.size):
260+
for i in range(lon.size):
261+
xi = lon[i] / a
262+
yi = lat[j] / b
263+
P[j, i] = (1 - math.exp(-xi / es) - xi) * math.pi * np.sin(math.pi * yi) * scalefac
264+
if grid_type == "A":
265+
U[j, i] = -(1 - math.exp(-xi / es) - xi) * math.pi**2 * np.cos(math.pi * yi) * scalefac
266+
V[j, i] = (math.exp(-xi / es) / es - 1) * math.pi * np.sin(math.pi * yi) * scalefac
267+
if grid_type == "C":
268+
V[:, 1:] = (P[:, 1:] - P[:, 0:-1]) / dx * a
269+
U[1:, :] = -(P[1:, :] - P[0:-1, :]) / dy * b
270+
Udims = ["YC", "XG"]
271+
Vdims = ["YG", "XC"]
272+
else:
273+
Udims = ["YG", "XG"]
274+
Vdims = ["YG", "XG"]
275+
276+
return xr.Dataset(
277+
{"U": (Udims, U), "V": (Vdims, V), "P": (["YG", "XG"], P)},
278+
coords={
279+
"YC": (["YC"], np.arange(ydim) + 0.5, {"axis": "Y"}),
280+
"YG": (["YG"], np.arange(ydim), {"axis": "Y", "c_grid_axis_shift": -0.5}),
281+
"XC": (["XC"], np.arange(xdim) + 0.5, {"axis": "X"}),
282+
"XG": (["XG"], np.arange(xdim), {"axis": "X", "c_grid_axis_shift": -0.5}),
283+
"lat": (["YG"], lat, {"axis": "Y", "c_grid_axis_shift": 0.5}),
284+
"lon": (["XG"], lon, {"axis": "X", "c_grid_axis_shift": -0.5}),
285+
},
286+
)

parcels/application_kernels/TEOSseawaterdensity.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77

88
def PolyTEOS10_bsq(particle, fieldset, time): # pragma: no cover
9-
"""Calculates density based on the polyTEOS10-bsq algorithm from Appendix A.2 of
9+
"""Calculates density based on the polyTEOS10-bsq algorithm from Appendix A.1 and A.2 of
1010
https://www.sciencedirect.com/science/article/pii/S1463500315000566
1111
requires fieldset.abs_salinity and fieldset.cons_temperature Fields in the fieldset
1212
and a particle.density Variable in the ParticleSet
@@ -27,10 +27,20 @@ def PolyTEOS10_bsq(particle, fieldset, time): # pragma: no cover
2727
SA = fieldset.abs_salinity[time, particle.depth, particle.lat, particle.lon]
2828
CT = fieldset.cons_temperature[time, particle.depth, particle.lat, particle.lon]
2929

30-
SAu = 40 * 35.16504 / 35
31-
CTu = 40
30+
SAu = 40.0 * 35.16504 / 35.0
31+
CTu = 40.0
3232
Zu = 1e4
33-
deltaS = 32
33+
deltaS = 32.0
34+
35+
zz = -Z / Zu
36+
R00 = 4.6494977072e01
37+
R01 = -5.2099962525e00
38+
R02 = 2.2601900708e-01
39+
R03 = 6.4326772569e-02
40+
R04 = 1.5616995503e-02
41+
R05 = -1.7243708991e-03
42+
r0 = (((((R05 * zz + R04) * zz + R03) * zz + R02) * zz + R01) * zz + R00) * zz
43+
3444
R000 = 8.0189615746e02
3545
R100 = 8.6672408165e02
3646
R200 = -1.7864682637e03
@@ -90,4 +100,6 @@ def PolyTEOS10_bsq(particle, fieldset, time): # pragma: no cover
90100
rz2 = (R022 * tt + R112 * ss + R012) * tt + (R202 * ss + R102) * ss + R002
91101
rz1 = (((R041 * tt + R131 * ss + R031) * tt + (R221 * ss + R121) * ss + R021) * tt + ((R311 * ss + R211) * ss + R111) * ss + R011) * tt + (((R401 * ss + R301) * ss + R201) * ss + R101) * ss + R001 # fmt: skip
92102
rz0 = (((((R060 * tt + R150 * ss + R050) * tt + (R240 * ss + R140) * ss + R040) * tt + ((R330 * ss + R230) * ss + R130) * ss + R030) * tt + (((R420 * ss + R320) * ss + R220) * ss + R120) * ss + R020) * tt + ((((R510 * ss + R410) * ss + R310) * ss + R210) * ss + R110) * ss + R010) * tt + (((((R600 * ss + R500) * ss + R400) * ss + R300) * ss + R200) * ss + R100) * ss + R000 # fmt: skip
93-
particle.density = ((rz3 * zz + rz2) * zz + rz1) * zz + rz0
103+
r = ((rz3 * zz + rz2) * zz + rz1) * zz + rz0
104+
105+
particle.density = r0 + r

parcels/application_kernels/advection.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,15 @@ def AdvectionRK45(particle, fieldset, time): # pragma: no cover
115115
Time-step dt is halved if error is larger than fieldset.RK45_tol,
116116
and doubled if error is smaller than 1/10th of tolerance.
117117
"""
118-
dt = min(
119-
particle.next_dt / np.timedelta64(1, "s"), fieldset.RK45_max_dt
120-
) # TODO: improve API for converting dt to seconds
118+
dt = particle.next_dt / np.timedelta64(1, "s") # TODO: improve API for converting dt to seconds
119+
if dt > fieldset.RK45_max_dt:
120+
dt = fieldset.RK45_max_dt
121+
particle.next_dt = fieldset.RK45_max_dt * np.timedelta64(1, "s")
122+
if dt < fieldset.RK45_min_dt:
123+
particle.next_dt = fieldset.RK45_min_dt * np.timedelta64(1, "s")
124+
return StatusCode.Repeat
125+
particle.dt = particle.next_dt
126+
121127
c = [1.0 / 4.0, 3.0 / 8.0, 12.0 / 13.0, 1.0, 1.0 / 2.0]
122128
A = [
123129
[1.0 / 4.0, 0.0, 0.0, 0.0, 0.0],
@@ -162,7 +168,7 @@ def AdvectionRK45(particle, fieldset, time): # pragma: no cover
162168
if (kappa <= fieldset.RK45_tol) or (math.fabs(dt) < math.fabs(fieldset.RK45_min_dt)):
163169
particle.dlon += lon_4th
164170
particle.dlat += lat_4th
165-
if (kappa <= fieldset.RK45_tol) / 10 and (math.fabs(dt * 2) <= math.fabs(fieldset.RK45_max_dt)):
171+
if (kappa <= fieldset.RK45_tol / 10) and (math.fabs(dt * 2) <= math.fabs(fieldset.RK45_max_dt)):
166172
particle.next_dt *= 2
167173
else:
168174
particle.next_dt /= 2

parcels/application_kernels/interpolation.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ def XBiLinear(
4040
zi, _ = position["Z"]
4141

4242
data = field.data.data[:, zi, yi : yi + 2, xi : xi + 2]
43-
data = (1 - tau) * data[ti, :, :] + tau * data[ti + 1, :, :]
43+
if tau > 0:
44+
data = (1 - tau) * data[ti, :, :] + tau * data[ti + 1, :, :]
45+
else:
46+
data = data[ti, :, :]
4447

4548
return (
4649
(1 - xsi) * (1 - eta) * data[0, 0]

parcels/fieldset.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ def add_constant(self, name, value):
164164
"""
165165
if name in self.constants:
166166
raise ValueError(f"FieldSet already has a constant with name '{name}'")
167-
167+
if not isinstance(value, (float, np.floating, int, np.integer)):
168+
raise ValueError(f"FieldSet constants have to be of type float or int, got a {type(value)}")
168169
self.constants[name] = np.float32(value)
169170

170171
@property

parcels/kernel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ def evaluate_particle(self, p, endtime):
303303
res_tmp = f(p, self._fieldset, p.time_nextloop)
304304
if res_tmp is not None: # TODO v4: Remove once all kernels return StatusCode
305305
res = res_tmp
306-
if res == StatusCode.StopExecution:
306+
if res in [StatusCode.StopExecution, StatusCode.Repeat]:
307307
break
308308

309309
if res is None:

parcels/xgrid.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,10 @@ def _gtype(self):
271271
def search(self, z, y, x, ei=None):
272272
ds = self.xgcm_grid._ds
273273

274-
zi, zeta = _search_1d_array(ds.depth.values, z)
274+
if "Z" in self.axes:
275+
zi, zeta = _search_1d_array(ds.depth.values, z)
276+
else:
277+
zi, zeta = 0, 0.0
275278
if zi == -1:
276279
if zeta < 0:
277280
raise FieldOutOfBoundError(

tests/test_kernel_language.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ def generate_fieldset(xdim=2, ydim=2, zdim=2, tdim=1):
336336
pset = ParticleSet(fieldset, pclass=DensParticle, lon=5, lat=5, depth=1000)
337337

338338
pset.execute(PolyTEOS10_bsq, runtime=1)
339-
assert np.allclose(pset[0].density, 1022.85377)
339+
assert np.allclose(pset[0].density, 1027.45140)
340340

341341

342342
def test_EOSseawaterproperties_kernels():

0 commit comments

Comments
 (0)