Skip to content

Commit 14547ae

Browse files
Merge branch 'main' into remove_constrain_dt_to_within_time_interval
2 parents 31dd2bd + eb0f6b9 commit 14547ae

10 files changed

Lines changed: 178 additions & 71 deletions

File tree

docs/user_guide/examples/tutorial_croco_3D.ipynb

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,11 @@
3737
"import matplotlib.pyplot as plt\n",
3838
"import numpy as np\n",
3939
"import polars as pl\n",
40-
"import xarray as xr\n",
4140
"\n",
4241
"import parcels\n",
4342
"import parcels.tutorial\n",
4443
"\n",
45-
"ds_fields = parcels.tutorial.open_dataset(\"CROCOidealized_data/data\")\n",
46-
"\n",
47-
"ds_fields.load(); # Preload data to speed up access"
44+
"ds_fields = parcels.tutorial.open_dataset(\"CROCOidealized_data/data\")"
4845
]
4946
},
5047
{
@@ -92,7 +89,10 @@
9289
"fieldset = parcels.FieldSet.from_sgrid_conventions(ds_fset)\n",
9390
"\n",
9491
"# Add the critical depth (`hc`) as context to the fieldset\n",
95-
"fieldset.add_context(\"hc\", ds_fields.hc.item())"
92+
"fieldset.add_context(\"hc\", ds_fields.hc.item())\n",
93+
"\n",
94+
"# Convert the FieldSet to windowed arrays for better performance\n",
95+
"fieldset = fieldset.to_windowed_arrays()"
9696
]
9797
},
9898
{
@@ -132,7 +132,7 @@
132132
")\n",
133133
"\n",
134134
"pset.execute(\n",
135-
" [parcels.kernels.AdvectionRK4_3D_CROCO, DeleteParticle],\n",
135+
" [parcels.kernels.AdvectionRK2_3D_CROCO, DeleteParticle],\n",
136136
" runtime=np.timedelta64(50_000, \"s\"),\n",
137137
" dt=np.timedelta64(100, \"s\"),\n",
138138
" output_file=outputfile,\n",
@@ -173,7 +173,7 @@
173173
"ax.set_xlabel(\"X [km]\")\n",
174174
"ax.set_xlim(30, 170)\n",
175175
"ax.set_ylabel(\"Depth [m]\")\n",
176-
"ax.set_title(\"Particles in idealized CROCO velocity field using AdvectionRK4_3D_CROCO\")\n",
176+
"ax.set_title(\"Particles in idealized CROCO velocity field using AdvectionRK2_3D_CROCO\")\n",
177177
"plt.tight_layout()\n",
178178
"plt.show()"
179179
]
@@ -189,7 +189,7 @@
189189
"cell_type": "markdown",
190190
"metadata": {},
191191
"source": [
192-
"It may be insightful to compare this 3D run with the `AdvectionRK4_3D_CROCO` kernel with a run where the vertical velocity (`W`) is set to zero. In that case, the particles will not stay on their initial depth levels but instead follow sigma-layers."
192+
"It may be insightful to compare this 3D run with the `AdvectionRK2_3D_CROCO` kernel with a run where the vertical velocity (`W`) is set to zero. In that case, the particles will not stay on their initial depth levels but instead follow sigma-layers."
193193
]
194194
},
195195
{
@@ -205,6 +205,7 @@
205205
"fieldset_noW = parcels.FieldSet.from_sgrid_conventions(ds_fset)\n",
206206
"fieldset_noW.W.data[:] = 0.0\n",
207207
"fieldset_noW.add_context(\"hc\", ds_fields.hc.item())\n",
208+
"fieldset_noW = fieldset_noW.to_windowed_arrays()\n",
208209
"\n",
209210
"X, Z = np.meshgrid(\n",
210211
" [40e3, 80e3, 120e3],\n",
@@ -221,7 +222,7 @@
221222
")\n",
222223
"\n",
223224
"pset_noW.execute(\n",
224-
" [parcels.kernels.AdvectionRK4_3D_CROCO, DeleteParticle],\n",
225+
" [parcels.kernels.AdvectionRK2_3D_CROCO, DeleteParticle],\n",
225226
" runtime=np.timedelta64(50_000, \"s\"),\n",
226227
" dt=np.timedelta64(100, \"s\"),\n",
227228
" output_file=outputfile,\n",
@@ -273,9 +274,9 @@
273274
"cell_type": "markdown",
274275
"metadata": {},
275276
"source": [
276-
"For Advection, you will need to use the `AdvectionRK4_3D_CROCO` kernel, which works slightly different from the normal 3D advection kernel because it converts the vertical velocity in sigma-units. The conversion from z to sigma is done at every time step, using the `convert_z_to_sigma_croco()` function.\n",
277+
"For Advection, you will need to use the `AdvectionRK2_3D_CROCO` kernel, which works slightly different from the normal 3D advection kernel because it converts the vertical velocity in sigma-units. The conversion from z to sigma is done at every time step, using the `convert_z_to_sigma_croco()` function.\n",
277278
"\n",
278-
"In particular, the following algorithm is used (note that the RK4 version is slightly more complex than this Euler-Forward version, but the idea is identical). Also note that the vertical velocity is linearly interpolated here, which gives much better results than the default C-grid interpolation.\n",
279+
"In particular, the following algorithm is used (note that the RK2 version is slightly more complex than this Euler-Forward version, but the idea is identical). Also note that the vertical velocity is linearly interpolated here, which gives much better results than the default C-grid interpolation.\n",
279280
"\n",
280281
"```python\n",
281282
"sigma = particles.z / fieldset.h[particles.t, 0, particles.y, particles.x]\n",
@@ -297,6 +298,11 @@
297298
"z_new = sigma_new * fieldset.h[particles.t, 0, y_new, x_new, particles]\n",
298299
"```"
299300
]
301+
},
302+
{
303+
"cell_type": "markdown",
304+
"metadata": {},
305+
"source": []
300306
}
301307
],
302308
"metadata": {

docs/user_guide/v4-migration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Version 4 of Parcels is unreleased at the moment. The information in this migrat
1515
- `math` functions should be replaced with array compatible equivalents (e.g., `math.sin` -> `np.sin`). Instead of `ParcelsRandom` you should use numpy's random functions.
1616
- `particle.depth` has been changed to `particles.z` to be consistent with the [CF conventions for trajectory data](https://cfconventions.org/cf-conventions/cf-conventions.html#trajectory-data), and to make Parcels also generalizable to atmospheric contexts.
1717
- The `InteractionKernel` class has been removed. Since normal Kernels now have access to _all_ particles, particle-particle interaction can be performed within normal Kernels.
18-
- Users need to explicitly use `convert_z_to_sigma_croco` in sampling kernels (such as the `AdvectionRK4_3D_CROCO` or `SampleOMegaCroco` kernels) when working with CROCO data, as the automatic conversion from depth to sigma grids under the hood has been removed.
18+
- Users need to explicitly use `convert_z_to_sigma_croco` in sampling kernels (such as the `AdvectionRK2_3D_CROCO` or `SampleOMegaCroco` kernels) when working with CROCO data, as the automatic conversion from depth to sigma grids under the hood has been removed.
1919
- We added a new AdvectionRK2 Kernel. The AdvectionRK4 kernel is still available, but RK2 is now the recommended default advection scheme as it is faster while the accuracy is comparable for most applications. See also the Choosing an integration method tutorial.
2020
- Functions shouldn't be converted to Kernels before adding to a pset.execute() call. Instead, simply pass the function(s) as a list to pset.execute().
2121
- Kernel variables `time`, `lat` and `lon` have been renamed to `t`, `y` and `x`, and `dlat` and `dlon` have been renamed to `dy` and `dx`. These changes are also reflected on the ParticleSet as well as the ParticleFile output.

src/parcels/_core/_windowed_array.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,6 @@ def maybe_windowed(data: xr.DataArray, max_levels: int | None = None):
108108
return data
109109
if data.dims and data.dims[0] == "time" and is_dask_collection(data.data):
110110
return WindowedArray(data, max_levels=max_levels)
111+
elif data.dims and data.dims[0] == "mockT" and is_dask_collection(data.data):
112+
return data.compute()
111113
return data

src/parcels/_core/model.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import cf_xarray # noqa: F401
88
import uxarray as ux
99
import xarray as xr
10+
from dask import is_dask_collection
1011

1112
import parcels._sgrid as sgrid
1213
import parcels._typing as ptyping
@@ -102,6 +103,12 @@ def to_windowed_arrays(self, *, max_levels: int | None = None) -> Self:
102103
and ``max_levels`` then bounds its size.
103104
"""
104105
windowed = self.__dict__.setdefault("_windowed", {})
106+
for dim in ["lon", "lat"]:
107+
# ensure lon and lat are loaded into memory for dask-backed datasets
108+
if is_dask_collection(self.data[dim]):
109+
self.data[dim].load()
110+
if "depth" in self.data.dims and is_dask_collection(self.data["depth"]):
111+
self.data["depth"].load()
105112
for name in self.scalar_field_names:
106113
current = windowed.get(name, self.data[name])
107114
windowed[name] = maybe_windowed(current, max_levels=max_levels)

src/parcels/_core/xgrid.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import numpy.typing as npt
88
import xarray as xr
99
import xgcm
10+
from dask import is_dask_collection
1011

1112
import parcels._sgrid as sgrid
1213
import parcels._typing as ptyping
@@ -207,6 +208,9 @@ def lon(self):
207208
_ = self.xgcm_grid.axes["X"]
208209
except KeyError:
209210
return np.zeros(1)
211+
# ensure lon is loaded into memory for dask-backed datasets, as it is used in the search method
212+
if is_dask_collection(self._ds["lon"].data):
213+
self._ds["lon"].load()
210214
return self._ds["lon"].values
211215

212216
@property
@@ -221,6 +225,9 @@ def lat(self):
221225
_ = self.xgcm_grid.axes["Y"]
222226
except KeyError:
223227
return np.zeros(1)
228+
# ensure lat is loaded into memory for dask-backed datasets, as it is used in the search method
229+
if is_dask_collection(self._ds["lat"].data):
230+
self._ds["lat"].load()
224231
return self._ds["lat"].values
225232

226233
@property

src/parcels/_reprs.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88

99
import numpy as np
1010
import xarray as xr
11+
import zarr
12+
from dask.base import is_dask_collection
1113

14+
from parcels._core._windowed_array import WindowedArray
1215
from parcels._python import isinstance_noimport
1316

1417
if TYPE_CHECKING:
@@ -190,13 +193,15 @@ class _FieldSetDescriptionRow:
190193
model_id: int | None
191194
name: str
192195
interp_method_or_value: str
196+
backend: str | None = None
193197

194198
def to_dict(self) -> dict[str, str]:
195199
return {
196200
"Name": self.name,
197201
"Type": self.type_,
198202
"Grid number": str(self.model_id) if self.model_id is not None else "-",
199203
"Interp method / value": self.interp_method_or_value,
204+
"Parcels backend": self.backend if self.backend is not None else "-",
200205
}
201206

202207

@@ -213,6 +218,22 @@ def _print_time_interval(time_interval: TimeInterval | None) -> str:
213218
return repr((time_interval.left, time_interval.right))
214219

215220

221+
def _field_backend(field: Field | VectorField) -> str | None:
222+
if hasattr(field, "data"):
223+
if isinstance(field.data, WindowedArray):
224+
return "WindowedArray"
225+
elif is_dask_collection(field.data.data):
226+
return "Dask"
227+
elif isinstance(field.data.variable._data, zarr.Array):
228+
return "Zarr"
229+
elif isinstance(field.data.data, np.ndarray):
230+
return "NumPy"
231+
else:
232+
return type(field.data).__name__
233+
else:
234+
return None
235+
236+
216237
def fieldset_describe(fieldset: FieldSet) -> str:
217238
rows: list[_FieldSetDescriptionRow] = []
218239
models: dict[int, int] = {} # mapping of memory ID to a human readable ID
@@ -235,6 +256,7 @@ def fieldset_describe(fieldset: FieldSet) -> str:
235256
model_id=model_id,
236257
name=field.name,
237258
interp_method_or_value=repr(field.interp_method),
259+
backend=_field_backend(field),
238260
)
239261
)
240262
for k, v in fieldset.context.items():
@@ -244,6 +266,7 @@ def fieldset_describe(fieldset: FieldSet) -> str:
244266
model_id=None,
245267
name=k,
246268
interp_method_or_value=repr(v),
269+
backend=None,
247270
)
248271
)
249272
return (

src/parcels/kernels/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
DiffusionUniformKh,
1414
)
1515
from ._sigmagrids import (
16-
AdvectionRK4_3D_CROCO,
16+
AdvectionRK2_3D_CROCO,
1717
SampleOmegaCroco,
1818
convert_z_to_sigma_croco,
1919
)
@@ -32,7 +32,7 @@
3232
"AdvectionDiffusionM1",
3333
"DiffusionUniformKh",
3434
# sigmagrids
35-
"AdvectionRK4_3D_CROCO",
35+
"AdvectionRK2_3D_CROCO",
3636
"SampleOmegaCroco",
3737
"convert_z_to_sigma_croco",
3838
]

src/parcels/kernels/_sigmagrids.py

Lines changed: 31 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import warnings
2+
13
import numpy as np
24

35

@@ -10,7 +12,7 @@ def convert_z_to_sigma_croco(fieldset, t, z, y, x, particle):
1012
h = fieldset.h.eval(t, np.zeros_like(z), y, x, particles=particle)
1113
zeta = fieldset.zeta.eval(t, np.zeros_like(z), y, x, particles=particle)
1214
sigma_levels = fieldset.U.grid.depth
13-
cs_w = fieldset.Cs_w.data[0, :, 0, 0].values
15+
cs_w = fieldset.Cs_w.data.values.flatten()
1416

1517
z0 = fieldset.hc * sigma_levels[None, :] + (h[:, None] - fieldset.hc) * cs_w[None, :]
1618
zvec = z0 + zeta[:, None] * (1.0 + (z0 / h[:, None]))
@@ -33,52 +35,38 @@ def SampleOmegaCroco(particles, fieldset):
3335
particles.omega = fieldset.omega[particles.t, sigma, particles.y, particles.x, particles]
3436

3537

36-
# TODO change to RK2 (once RK4 yields same results as v3)
37-
def AdvectionRK4_3D_CROCO(particles, fieldset): # pragma: no cover
38-
"""Advection of particles using fourth-order Runge-Kutta integration including vertical velocity.
38+
def AdvectionRK2_3D_CROCO(particles, fieldset): # pragma: no cover
39+
"""Advection of particles using second-order Runge-Kutta integration including vertical velocity.
3940
This kernel assumes the vertical velocity is the 'w' field from CROCO output and works on sigma-layers.
4041
It also uses linear interpolation of the W field, which gives much better results than the default C-grid interpolation.
4142
"""
42-
sigma = particles.z / fieldset.h[particles.t, np.zeros_like(particles.z), particles.y, particles.x]
43-
44-
sig = convert_z_to_sigma_croco(fieldset, particles.t, particles.z, particles.y, particles.x, particles)
45-
(u1, v1) = fieldset.UV[particles.t, sig, particles.y, particles.x, particles]
46-
w1 = fieldset.W[particles.t, sig, particles.y, particles.x, particles]
47-
w1 *= sigma / fieldset.h[particles.t, np.zeros_like(particles.z), particles.y, particles.x]
48-
x1 = particles.x + u1 * 0.5 * particles.dt
49-
y1 = particles.y + v1 * 0.5 * particles.dt
50-
sig_dep1 = sigma + w1 * 0.5 * particles.dt
51-
dep1 = sig_dep1 * fieldset.h[particles.t, np.zeros_like(particles.z), y1, x1]
43+
with warnings.catch_warnings():
44+
warnings.filterwarnings(
45+
"ignore",
46+
message=r"^Sampling of velocities should normally be done using fieldset\.UV or fieldset\.UVW object; tread carefully$",
47+
category=RuntimeWarning,
48+
) # Needed because of linear sampling of W with sigma conversion
5249

53-
sig1 = convert_z_to_sigma_croco(fieldset, particles.t + 0.5 * particles.dt, dep1, y1, x1, particles)
54-
(u2, v2) = fieldset.UV[particles.t + 0.5 * particles.dt, sig1, y1, x1, particles]
55-
w2 = fieldset.W[particles.t + 0.5 * particles.dt, sig1, y1, x1, particles]
56-
w2 *= sig_dep1 / fieldset.h[particles.t + 0.5 * particles.dt, np.zeros_like(particles.z), y1, x1]
57-
x2 = particles.x + u2 * 0.5 * particles.dt
58-
y2 = particles.y + v2 * 0.5 * particles.dt
59-
sig_dep2 = sigma + w2 * 0.5 * particles.dt
60-
dep2 = sig_dep2 * fieldset.h[particles.t + 0.5 * particles.dt, np.zeros_like(particles.z), y2, x2]
50+
sigma = particles.z / fieldset.h[particles.t, np.zeros_like(particles.z), particles.y, particles.x]
6151

62-
sig2 = convert_z_to_sigma_croco(fieldset, particles.t + 0.5 * particles.dt, dep2, y2, x2, particles)
63-
(u3, v3) = fieldset.UV[particles.t + 0.5 * particles.dt, sig2, y2, x2, particles]
64-
w3 = fieldset.W[particles.t + 0.5 * particles.dt, sig2, y2, x2, particles]
65-
w3 *= sig_dep2 / fieldset.h[particles.t + 0.5 * particles.dt, np.zeros_like(particles.z), y2, x2]
66-
x3 = particles.x + u3 * particles.dt
67-
y3 = particles.y + v3 * particles.dt
68-
sig_dep3 = sigma + w3 * particles.dt
69-
dep3 = sig_dep3 * fieldset.h[particles.t + particles.dt, np.zeros_like(particles.z), y3, x3]
52+
sig = convert_z_to_sigma_croco(fieldset, particles.t, particles.z, particles.y, particles.x, particles)
53+
(u1, v1) = fieldset.UV[particles.t, sig, particles.y, particles.x, particles]
54+
w1 = fieldset.W[particles.t, sig, particles.y, particles.x, particles]
55+
w1 *= sigma / fieldset.h[particles.t, np.zeros_like(particles.z), particles.y, particles.x]
56+
x1 = particles.x + u1 * 0.5 * particles.dt
57+
y1 = particles.y + v1 * 0.5 * particles.dt
58+
sig_dep1 = sigma + w1 * 0.5 * particles.dt
59+
dep1 = sig_dep1 * fieldset.h[particles.t, np.zeros_like(particles.z), y1, x1]
7060

71-
sig3 = convert_z_to_sigma_croco(fieldset, particles.t + particles.dt, dep3, y3, x3, particles)
72-
(u4, v4) = fieldset.UV[particles.t + particles.dt, sig3, y3, x3, particles]
73-
w4 = fieldset.W[particles.t + particles.dt, sig3, y3, x3, particles]
74-
w4 *= sig_dep3 / fieldset.h[particles.t + particles.dt, np.zeros_like(particles.z), y3, x3]
75-
x4 = particles.x + u4 * particles.dt
76-
y4 = particles.y + v4 * particles.dt
77-
sig_dep4 = sigma + w4 * particles.dt
61+
sig1 = convert_z_to_sigma_croco(fieldset, particles.t + 0.5 * particles.dt, dep1, y1, x1, particles)
62+
(u2, v2) = fieldset.UV[particles.t + 0.5 * particles.dt, sig1, y1, x1, particles]
63+
w2 = fieldset.W[particles.t + 0.5 * particles.dt, sig1, y1, x1, particles]
64+
w2 *= sig_dep1 / fieldset.h[particles.t + 0.5 * particles.dt, np.zeros_like(particles.z), y1, x1]
65+
x2 = particles.x + u2 * 0.5 * particles.dt
66+
y2 = particles.y + v2 * 0.5 * particles.dt
67+
sig_dep2 = sigma + w2 * 0.5 * particles.dt
68+
dep2 = sig_dep2 * fieldset.h[particles.t + 0.5 * particles.dt, np.zeros_like(particles.z), y2, x2]
7869

79-
dep4 = sig_dep4 * fieldset.h[particles.t + particles.dt, np.zeros_like(particles.z), y4, x4]
80-
particles.dx += (u1 + 2 * u2 + 2 * u3 + u4) / 6 * particles.dt
81-
particles.dy += (v1 + 2 * v2 + 2 * v3 + v4) / 6 * particles.dt
82-
particles.dz += (
83-
(dep1 - particles.z) * 2 + 2 * (dep2 - particles.z) * 2 + 2 * (dep3 - particles.z) + dep4 - particles.z
84-
) / 6
70+
particles.dx += u2 * particles.dt
71+
particles.dy += v2 * particles.dt
72+
particles.dz += (dep1 - particles.z) + (dep2 - particles.z)

0 commit comments

Comments
 (0)