Skip to content

Commit 9790413

Browse files
Merge branch 'v4-dev' into reprs_for_v4
2 parents d5048d9 + 92fe65a commit 9790413

27 files changed

Lines changed: 246 additions & 272 deletions

docs/user_guide/examples/tutorial_diffusion.ipynb

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@
192192
"source": [
193193
"from parcels._datasets.structured.generated import simple_UV_dataset\n",
194194
"\n",
195-
"ds = simple_UV_dataset(dims=(1, 1, Ny, 1), mesh=\"flat\").isel(time=0, depth=0)\n",
195+
"ds = simple_UV_dataset(dims=(1, 1, Ny, 1), mesh=\"flat\")\n",
196196
"ds[\"lat\"][:] = np.linspace(-0.01, 1.01, Ny)\n",
197197
"ds[\"lon\"][:] = np.ones(len(ds.XG))\n",
198198
"ds[\"Kh_meridional\"] = ([\"YG\", \"XG\"], Kh_meridional[:, None])\n",
@@ -205,20 +205,8 @@
205205
"metadata": {},
206206
"outputs": [],
207207
"source": [
208-
"grid = parcels.XGrid.from_dataset(ds, mesh=\"flat\")\n",
209-
"U = parcels.Field(\"U\", ds[\"U\"], grid, interp_method=parcels.interpolators.XLinear)\n",
210-
"V = parcels.Field(\"V\", ds[\"V\"], grid, interp_method=parcels.interpolators.XLinear)\n",
211-
"UV = parcels.VectorField(\"UV\", U, V)\n",
212-
"\n",
213-
"Kh_meridional_field = parcels.Field(\n",
214-
" \"Kh_meridional\",\n",
215-
" ds[\"Kh_meridional\"],\n",
216-
" grid,\n",
217-
" interp_method=parcels.interpolators.XLinear,\n",
218-
")\n",
219-
"fieldset = parcels.FieldSet([U, V, UV, Kh_meridional_field])\n",
208+
"fieldset = parcels.FieldSet.from_sgrid_conventions(ds, mesh=\"flat\")\n",
220209
"fieldset.add_constant_field(\"Kh_zonal\", 1, mesh=\"flat\")\n",
221-
"\n",
222210
"fieldset.add_constant(\"dres\", 0.00005)"
223211
]
224212
},

docs/user_guide/examples/tutorial_interpolation.ipynb

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,20 @@
4646
"source": [
4747
"from parcels._datasets.structured.generated import simple_UV_dataset\n",
4848
"\n",
49-
"ds = simple_UV_dataset(dims=(1, 1, 5, 4), mesh=\"flat\").isel(time=0, depth=0)\n",
49+
"ds = simple_UV_dataset(dims=(1, 1, 5, 4), mesh=\"flat\")\n",
5050
"ds[\"lat\"][:] = np.linspace(0.0, 1.0, len(ds.YG))\n",
5151
"ds[\"lon\"][:] = np.linspace(0.0, 1.0, len(ds.XG))\n",
5252
"dx, dy = 1.0 / len(ds.XG), 1.0 / len(ds.YG)\n",
5353
"ds[\"P\"] = ds[\"U\"] + np.random.rand(5, 4) + 0.1\n",
54-
"ds[\"P\"][1, 1] = 0\n",
54+
"ds[\"P\"][:, :, 1, 1] = 0\n",
5555
"ds"
5656
]
5757
},
5858
{
5959
"cell_type": "markdown",
6060
"metadata": {},
6161
"source": [
62-
"From this dataset we create a {py:obj}`parcels.FieldSet`. Parcels requires an interpolation method to be set for each {py:obj}`parcels.Field`, which we will later adapt to see the effects of the different interpolators. A common interpolator for fields on structured grids is (tri)linear, implemented in {py:obj}`parcels.interpolators.XLinear`."
62+
"From this dataset we create a {py:obj}`parcels.FieldSet` using the {py:meth}`parcels.FieldSet.from_sgrid_conventions` constructor, which automatically sets up the grid and fields according to Parcels' s-grid conventions."
6363
]
6464
},
6565
{
@@ -68,12 +68,7 @@
6868
"metadata": {},
6969
"outputs": [],
7070
"source": [
71-
"grid = parcels.XGrid.from_dataset(ds, mesh=\"flat\")\n",
72-
"U = parcels.Field(\"U\", ds[\"U\"], grid, interp_method=parcels.interpolators.XLinear)\n",
73-
"V = parcels.Field(\"V\", ds[\"V\"], grid, interp_method=parcels.interpolators.XLinear)\n",
74-
"UV = parcels.VectorField(\"UV\", U, V)\n",
75-
"P = parcels.Field(\"P\", ds[\"P\"], grid, interp_method=parcels.interpolators.XLinear)\n",
76-
"fieldset = parcels.FieldSet([U, V, UV, P])"
71+
"fieldset = parcels.FieldSet.from_sgrid_conventions(ds, mesh=\"flat\")"
7772
]
7873
},
7974
{

docs/user_guide/examples/tutorial_nemo_curvilinear.ipynb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@
152152
"outputs": [],
153153
"source": [
154154
"u, v = fieldset.UV.eval(\n",
155-
" np.array([0]), np.array([0]), np.array([20]), np.array([50]), applyConversion=False\n",
155+
" np.array([0]), np.array([0]), np.array([20]), np.array([50]), apply_conversion=False\n",
156156
") # do not convert m/s to deg/s\n",
157157
"print(f\"(u, v) = ({u[0]:.3f}, {v[0]:.3f})\")\n",
158158
"assert np.isclose(u, 1.0, atol=1e-3)"
@@ -298,7 +298,7 @@
298298
],
299299
"metadata": {
300300
"kernelspec": {
301-
"display_name": "test-notebooks",
301+
"display_name": "default",
302302
"language": "python",
303303
"name": "python3"
304304
},
@@ -312,7 +312,7 @@
312312
"name": "python",
313313
"nbconvert_exporter": "python",
314314
"pygments_lexer": "ipython3",
315-
"version": "3.11.0"
315+
"version": "3.14.2"
316316
}
317317
},
318318
"nbformat": 4,

docs/user_guide/examples/tutorial_nestedgrids.ipynb

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -440,12 +440,7 @@
440440
"source": [
441441
"fields = [GridID]\n",
442442
"for i, ds in enumerate(ds_in):\n",
443-
" # TODO : use FieldSet.from_sgrid_convetion here once #2437 is merged\n",
444-
" grid = parcels.XGrid.from_dataset(ds, mesh=\"spherical\")\n",
445-
" U = parcels.Field(\"U\", ds[\"U\"], grid, interp_method=parcels.interpolators.XLinear)\n",
446-
" V = parcels.Field(\"V\", ds[\"V\"], grid, interp_method=parcels.interpolators.XLinear)\n",
447-
" UV = parcels.VectorField(\"UV\", U, V)\n",
448-
" fset = parcels.FieldSet([U, V, UV])\n",
443+
" fset = parcels.FieldSet.from_sgrid_conventions(ds, mesh=\"spherical\")\n",
449444
"\n",
450445
" for fld in fset.fields.values():\n",
451446
" fld.name = f\"{fld.name}{i}\"\n",
@@ -469,32 +464,19 @@
469464
"outputs": [],
470465
"source": [
471466
"def AdvectEE_NestedGrids(particles, fieldset):\n",
472-
" particles.gridID = fieldset.GridID[particles]\n",
473-
"\n",
474-
" # TODO because of KernelParticle bug (GH #2143), we need to copy lon/lat/time to local variables\n",
475-
" time = particles.time\n",
476-
" z = particles.z\n",
477-
" lat = particles.lat\n",
478-
" lon = particles.lon\n",
479467
" u = np.zeros_like(particles.lon)\n",
480468
" v = np.zeros_like(particles.lat)\n",
481469
"\n",
470+
" particles.gridID = fieldset.GridID[particles]\n",
482471
" unique_ids = np.unique(particles.gridID)\n",
483472
" for gid in unique_ids:\n",
484473
" mask = particles.gridID == gid\n",
485474
" UVField = getattr(fieldset, f\"UV{gid}\")\n",
486-
" (u[mask], v[mask]) = UVField[time[mask], z[mask], lat[mask], lon[mask]]\n",
475+
" (u[mask], v[mask]) = UVField[particles[mask]]\n",
487476
"\n",
488477
" particles.dlon += u * particles.dt\n",
489478
" particles.dlat += v * particles.dt\n",
490479
"\n",
491-
" # TODO particle states have to be updated manually because UVField is not called with `particles` argument (becaise of GH #2143)\n",
492-
" particles.state = np.where(\n",
493-
" np.isnan(u) | np.isnan(v),\n",
494-
" parcels.StatusCode.ErrorInterpolation,\n",
495-
" particles.state,\n",
496-
" )\n",
497-
"\n",
498480
"\n",
499481
"lat = np.linspace(-17, 35, 10)\n",
500482
"lon = np.full(len(lat), -5)\n",

docs/user_guide/examples/tutorial_statuscodes.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,19 +63,14 @@ Let's add the `KeepInOcean` Kernel to an particle simulation where particles mov
6363
import numpy as np
6464
from parcels._datasets.structured.generated import simple_UV_dataset
6565
66-
ds = simple_UV_dataset(dims=(1, 2, 5, 4), mesh="flat").isel(time=0)
66+
ds = simple_UV_dataset(dims=(1, 2, 5, 4), mesh="flat")
6767
6868
dx, dy = 1.0 / len(ds.XG), 1.0 / len(ds.YG)
6969
7070
# Add W velocity that pushes through surface
7171
ds["W"] = ds["U"] - 0.1 # 0.1 m/s towards the surface
7272
73-
grid = parcels.XGrid.from_dataset(ds, mesh="flat")
74-
U = parcels.Field("U", ds["U"], grid, interp_method=parcels.interpolators.XLinear)
75-
V = parcels.Field("V", ds["V"], grid, interp_method=parcels.interpolators.XLinear)
76-
W = parcels.Field("W", ds["W"], grid, interp_method=parcels.interpolators.XLinear)
77-
UVW = parcels.VectorField("UVW", U, V, W)
78-
fieldset = parcels.FieldSet([U, V, W, UVW])
73+
fieldset = parcels.FieldSet.from_sgrid_conventions(ds, mesh="flat")
7974
```
8075

8176
If we advect particles with the `AdvectionRK2_3D` kernel, Parcels will raise a `FieldOutOfBoundSurfaceError`:

docs/user_guide/examples/tutorial_unitconverters.ipynb

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"\n",
5050
"nlat = 10\n",
5151
"nlon = 18\n",
52-
"ds = simple_UV_dataset(dims=(1, 1, nlat, nlon), mesh=\"spherical\").isel(time=0, depth=0)\n",
52+
"ds = simple_UV_dataset(dims=(1, 1, nlat, nlon), mesh=\"spherical\")\n",
5353
"ds[\"temperature\"] = ds[\"U\"] + 20 # add temperature field of 20 deg\n",
5454
"ds[\"U\"].data[:] = 1.0 # set U to 1 m/s\n",
5555
"ds[\"V\"].data[:] = 1.0 # set V to 1 m/s\n",
@@ -61,7 +61,7 @@
6161
"cell_type": "markdown",
6262
"metadata": {},
6363
"source": [
64-
"To create a `parcels.FieldSet` object, we define the `parcels.Field`s and the structured grid (`parcels.XGrid`) the fields are defined on. We add the argument `mesh='spherical'` to the `parcels.XGrid` to signal that all longitudes and latitudes are in degrees.\n",
64+
"To create a `parcels.FieldSet` object, we use the `parcels.FieldSet.from_sgrid_conventions` constructor. We add the argument `mesh='spherical'` to signal that all longitudes and latitudes are in degrees.\n",
6565
"\n",
6666
"```{note}\n",
6767
"When using a `FieldSet` method for a specific dataset, such as `from_copernicusmarine()`, the grid information is known and parsed by Parcels, so we do not have to add the `mesh` argument.\n",
@@ -76,14 +76,7 @@
7676
"metadata": {},
7777
"outputs": [],
7878
"source": [
79-
"grid = parcels.XGrid.from_dataset(ds, mesh=\"spherical\")\n",
80-
"U = parcels.Field(\"U\", ds[\"U\"], grid, interp_method=parcels.interpolators.XLinear)\n",
81-
"V = parcels.Field(\"V\", ds[\"V\"], grid, interp_method=parcels.interpolators.XLinear)\n",
82-
"UV = parcels.VectorField(\"UV\", U, V)\n",
83-
"temperature = parcels.Field(\n",
84-
" \"temperature\", ds[\"temperature\"], grid, interp_method=parcels.interpolators.XLinear\n",
85-
")\n",
86-
"fieldset = parcels.FieldSet([U, V, UV, temperature])\n",
79+
"fieldset = parcels.FieldSet.from_sgrid_conventions(ds, mesh=\"spherical\")\n",
8780
"\n",
8881
"plt.pcolormesh(\n",
8982
" fieldset.U.grid.lon,\n",
@@ -166,7 +159,7 @@
166159
"cell_type": "markdown",
167160
"metadata": {},
168161
"source": [
169-
"Note that you can also interpolate the Field without a unit conversion, by using the `eval()` method and setting `applyConversion=False`, as below\n"
162+
"Note that you can also interpolate the Field without a unit conversion, by using the `eval()` method and setting `apply_conversion=False`, as below\n"
170163
]
171164
},
172165
{
@@ -181,7 +174,7 @@
181174
" z,\n",
182175
" lat,\n",
183176
" lon,\n",
184-
" applyConversion=False,\n",
177+
" apply_conversion=False,\n",
185178
" )\n",
186179
")"
187180
]
@@ -199,7 +192,7 @@
199192
"cell_type": "markdown",
200193
"metadata": {},
201194
"source": [
202-
"If longitudes and latitudes are given in meters, rather than degrees, simply add `mesh='flat'` when creating the XGrid object.\n"
195+
"If longitudes and latitudes are given in meters, rather than degrees, simply add `mesh='flat'` when creating the `FieldSet` object.\n"
203196
]
204197
},
205198
{
@@ -208,21 +201,11 @@
208201
"metadata": {},
209202
"outputs": [],
210203
"source": [
211-
"ds_flat = simple_UV_dataset(dims=(1, 1, nlat, nlon), mesh=\"flat\").isel(time=0, depth=0)\n",
204+
"ds_flat = simple_UV_dataset(dims=(1, 1, nlat, nlon), mesh=\"flat\")\n",
212205
"ds_flat[\"temperature\"] = ds_flat[\"U\"] + 20 # add temperature field of 20 deg\n",
213206
"ds_flat[\"U\"].data[:] = 1.0 # set U to 1 m/s\n",
214207
"ds_flat[\"V\"].data[:] = 1.0 # set V to 1 m/s\n",
215-
"grid = parcels.XGrid.from_dataset(ds_flat, mesh=\"flat\")\n",
216-
"U = parcels.Field(\"U\", ds_flat[\"U\"], grid, interp_method=parcels.interpolators.XLinear)\n",
217-
"V = parcels.Field(\"V\", ds_flat[\"V\"], grid, interp_method=parcels.interpolators.XLinear)\n",
218-
"UV = parcels.VectorField(\"UV\", U, V)\n",
219-
"temperature = parcels.Field(\n",
220-
" \"temperature\",\n",
221-
" ds_flat[\"temperature\"],\n",
222-
" grid,\n",
223-
" interp_method=parcels.interpolators.XLinear,\n",
224-
")\n",
225-
"fieldset_flat = parcels.FieldSet([U, V, UV, temperature])\n",
208+
"fieldset_flat = parcels.FieldSet.from_sgrid_conventions(ds_flat, mesh=\"flat\")\n",
226209
"\n",
227210
"plt.pcolormesh(\n",
228211
" fieldset_flat.U.grid.lon,\n",

pyproject.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,15 @@ select = [
8585
"ISC001", # single-line-implicit-string-concatenation
8686
"TID", # flake8-tidy-imports
8787
"T100", # Checks for the presence of debugger calls and imports
88+
"N", # pep8 naming conventions
89+
"PGH004", # ensures no blanket noqa's are used
8890
]
8991

92+
exclude = [
93+
"tests-v3/**",
94+
'docs/user_guide/examples_v3/**',
95+
] # TODO v4: Remove once folders are gone
96+
9097
ignore = [
9198
# line too long (82 > 79 characters)
9299
"E501",
@@ -104,6 +111,13 @@ ignore = [
104111
"RUF043",
105112
"RUF046", # Value being cast to `int` is already an integer
106113

114+
# Parcels specific deviations from PEP8 naming
115+
'N802', # We use function names that are camelcased to denote functions
116+
'N806', # We use capitalized variable names within functions in Parcels to denote fields, as well as multidimensional arrays
117+
'N811', # Well, this just seems like a bad rule O_o
118+
'N816', # TODO: Enable for `src` and consider disabling for notebooks
119+
'N818', # Not all Parcels exceptions should have an "Error" suffix
120+
107121
# TODO: Move this ignore so that it only applies in the tests folder. Do in conjunction with any doc related rules
108122
"RUF059", # Unpacked variable `coords` is never used
109123

src/parcels/_core/field.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ def _check_velocitysampling(self):
204204
stacklevel=2,
205205
)
206206

207-
def eval(self, time: datetime, z, y, x, particles=None, applyConversion=True):
207+
def eval(self, time: datetime, z, y, x, particles=None, apply_conversion=True):
208208
"""Interpolate field values in space and time.
209209
210210
We interpolate linearly in time and apply implicit unit
@@ -225,7 +225,7 @@ def eval(self, time: datetime, z, y, x, particles=None, applyConversion=True):
225225

226226
_update_particle_states_interp_value(particles, value)
227227

228-
if applyConversion:
228+
if apply_conversion:
229229
value = self.units.to_target(value, z, y, x)
230230
return value
231231

@@ -244,7 +244,12 @@ class VectorField:
244244
"""VectorField class that holds vector field data needed to execute particles."""
245245

246246
def __init__(
247-
self, name: str, U: Field, V: Field, W: Field | None = None, vector_interp_method: Callable | None = None
247+
self,
248+
name: str,
249+
U: Field, # noqa: N803
250+
V: Field, # noqa: N803
251+
W: Field | None = None, # noqa: N803
252+
vector_interp_method: Callable | None = None,
248253
):
249254
_assert_str_and_python_varname(name)
250255

@@ -286,7 +291,7 @@ def vector_interp_method(self, method: Callable):
286291
assert_same_function_signature(method, ref=ZeroInterpolator_Vector, context="Interpolation")
287292
self._vector_interp_method = method
288293

289-
def eval(self, time: datetime, z, y, x, particles=None, applyConversion=True):
294+
def eval(self, time: datetime, z, y, x, particles=None, apply_conversion=True):
290295
"""Interpolate field values in space and time.
291296
292297
We interpolate linearly in time and apply implicit unit
@@ -313,7 +318,7 @@ def eval(self, time: datetime, z, y, x, particles=None, applyConversion=True):
313318
else:
314319
(u, v, w) = self._vector_interp_method(particle_positions, grid_positions, self)
315320

316-
if applyConversion:
321+
if apply_conversion:
317322
u = self.U.units.to_target(u, z, y, x)
318323
v = self.V.units.to_target(v, z, y, x)
319324
if "3D" in self.vector_type:

src/parcels/_core/fieldset.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,8 @@ def gridset(self) -> list[BaseGrid]:
186186
grids.append(field.grid)
187187
return grids
188188

189-
def from_copernicusmarine(ds: xr.Dataset):
189+
@classmethod
190+
def from_copernicusmarine(cls, ds: xr.Dataset):
190191
"""Create a FieldSet from a Copernicus Marine Service xarray.Dataset.
191192
192193
Parameters
@@ -241,9 +242,10 @@ def from_copernicusmarine(ds: xr.Dataset):
241242
vertical_dimensions=(sgrid.DimDimPadding("z_center", "depth", sgrid.Padding.LOW),),
242243
).to_attrs(),
243244
)
244-
return FieldSet.from_sgrid_conventions(ds, mesh="spherical")
245+
return cls.from_sgrid_conventions(ds, mesh="spherical")
245246

246-
def from_fesom2(ds: ux.UxDataset):
247+
@classmethod
248+
def from_fesom2(cls, ds: ux.UxDataset):
247249
"""Create a FieldSet from a FESOM2 uxarray.UxDataset.
248250
249251
Parameters
@@ -279,10 +281,11 @@ def from_fesom2(ds: ux.UxDataset):
279281
for varname in set(ds.data_vars) - set(fields.keys()):
280282
fields[varname] = Field(varname, ds[varname], grid, _select_uxinterpolator(ds[varname]))
281283

282-
return FieldSet(list(fields.values()))
284+
return cls(list(fields.values()))
283285

286+
@classmethod
284287
def from_sgrid_conventions(
285-
ds: xr.Dataset, mesh: Mesh
288+
cls, ds: xr.Dataset, mesh: Mesh
286289
): # TODO: Update mesh to be discovered from the dataset metadata
287290
"""Create a FieldSet from a dataset using SGRID convention metadata.
288291
@@ -355,17 +358,16 @@ def from_sgrid_conventions(
355358
if "U" in ds.data_vars and "V" in ds.data_vars:
356359
fields["U"] = Field("U", ds["U"], grid, XLinear)
357360
fields["V"] = Field("V", ds["V"], grid, XLinear)
361+
fields["UV"] = VectorField("UV", fields["U"], fields["V"])
358362

359363
if "W" in ds.data_vars:
360364
fields["W"] = Field("W", ds["W"], grid, XLinear)
361365
fields["UVW"] = VectorField("UVW", fields["U"], fields["V"], fields["W"])
362-
else:
363-
fields["UV"] = VectorField("UV", fields["U"], fields["V"])
364366

365367
for varname in set(ds.data_vars) - set(fields.keys()) - skip_vars:
366368
fields[varname] = Field(varname, ds[varname], grid, XLinear)
367369

368-
return FieldSet(list(fields.values()))
370+
return cls(list(fields.values()))
369371

370372

371373
class CalendarError(Exception): # TODO: Move to a parcels errors module

src/parcels/_core/index_search.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ def uxgrid_point_in_cell(grid, y: np.ndarray, x: np.ndarray, yi: np.ndarray, xi:
276276
return is_in_cell, coords
277277

278278

279-
def _triangle_area(A, B, C):
279+
def _triangle_area(A, B, C): # noqa: N803
280280
"""Compute the area of a triangle given by three points."""
281281
d1 = B - A
282282
d2 = C - A

0 commit comments

Comments
 (0)