diff --git a/docs/user_guide/examples/tutorial_nemo_curvilinear.ipynb b/docs/user_guide/examples/tutorial_nemo_curvilinear.ipynb index 7eb6b00817..741394ec61 100644 --- a/docs/user_guide/examples/tutorial_nemo_curvilinear.ipynb +++ b/docs/user_guide/examples/tutorial_nemo_curvilinear.ipynb @@ -152,7 +152,7 @@ "outputs": [], "source": [ "u, v = fieldset.UV.eval(\n", - " np.array([0]), np.array([0]), np.array([20]), np.array([50]), applyConversion=False\n", + " np.array([0]), np.array([0]), np.array([20]), np.array([50]), apply_conversion=False\n", ") # do not convert m/s to deg/s\n", "print(f\"(u, v) = ({u[0]:.3f}, {v[0]:.3f})\")\n", "assert np.isclose(u, 1.0, atol=1e-3)" @@ -298,7 +298,7 @@ ], "metadata": { "kernelspec": { - "display_name": "test-notebooks", + "display_name": "default", "language": "python", "name": "python3" }, @@ -312,7 +312,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.0" + "version": "3.14.2" } }, "nbformat": 4, diff --git a/docs/user_guide/examples/tutorial_unitconverters.ipynb b/docs/user_guide/examples/tutorial_unitconverters.ipynb index 773c339b84..6393af839e 100644 --- a/docs/user_guide/examples/tutorial_unitconverters.ipynb +++ b/docs/user_guide/examples/tutorial_unitconverters.ipynb @@ -159,7 +159,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Note that you can also interpolate the Field without a unit conversion, by using the `eval()` method and setting `applyConversion=False`, as below\n" + "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" ] }, { @@ -174,7 +174,7 @@ " z,\n", " lat,\n", " lon,\n", - " applyConversion=False,\n", + " apply_conversion=False,\n", " )\n", ")" ] diff --git a/pyproject.toml b/pyproject.toml index 86e1420bf1..aa8ec1cce1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,8 +85,15 @@ select = [ "ISC001", # single-line-implicit-string-concatenation "TID", # flake8-tidy-imports "T100", # Checks for the presence of debugger calls and imports + "N", # pep8 naming conventions + "PGH004", # ensures no blanket noqa's are used ] +exclude = [ + "tests-v3/**", + 'docs/user_guide/examples_v3/**', +] # TODO v4: Remove once folders are gone + ignore = [ # line too long (82 > 79 characters) "E501", @@ -104,6 +111,13 @@ ignore = [ "RUF043", "RUF046", # Value being cast to `int` is already an integer + # Parcels specific deviations from PEP8 naming + 'N802', # We use function names that are camelcased to denote functions + 'N806', # We use capitalized variable names within functions in Parcels to denote fields, as well as multidimensional arrays + 'N811', # Well, this just seems like a bad rule O_o + 'N816', # TODO: Enable for `src` and consider disabling for notebooks + 'N818', # Not all Parcels exceptions should have an "Error" suffix + # TODO: Move this ignore so that it only applies in the tests folder. Do in conjunction with any doc related rules "RUF059", # Unpacked variable `coords` is never used diff --git a/src/parcels/_core/field.py b/src/parcels/_core/field.py index 988636f13d..672093a55f 100644 --- a/src/parcels/_core/field.py +++ b/src/parcels/_core/field.py @@ -201,7 +201,7 @@ def _check_velocitysampling(self): stacklevel=2, ) - def eval(self, time: datetime, z, y, x, particles=None, applyConversion=True): + def eval(self, time: datetime, z, y, x, particles=None, apply_conversion=True): """Interpolate field values in space and time. We interpolate linearly in time and apply implicit unit @@ -222,7 +222,7 @@ def eval(self, time: datetime, z, y, x, particles=None, applyConversion=True): _update_particle_states_interp_value(particles, value) - if applyConversion: + if apply_conversion: value = self.units.to_target(value, z, y, x) return value @@ -241,7 +241,12 @@ class VectorField: """VectorField class that holds vector field data needed to execute particles.""" def __init__( - self, name: str, U: Field, V: Field, W: Field | None = None, vector_interp_method: Callable | None = None + self, + name: str, + U: Field, # noqa: N803 + V: Field, # noqa: N803 + W: Field | None = None, # noqa: N803 + vector_interp_method: Callable | None = None, ): _assert_str_and_python_varname(name) @@ -287,7 +292,7 @@ def vector_interp_method(self, method: Callable): assert_same_function_signature(method, ref=ZeroInterpolator_Vector, context="Interpolation") self._vector_interp_method = method - def eval(self, time: datetime, z, y, x, particles=None, applyConversion=True): + def eval(self, time: datetime, z, y, x, particles=None, apply_conversion=True): """Interpolate field values in space and time. We interpolate linearly in time and apply implicit unit @@ -314,7 +319,7 @@ def eval(self, time: datetime, z, y, x, particles=None, applyConversion=True): else: (u, v, w) = self._vector_interp_method(particle_positions, grid_positions, self) - if applyConversion: + if apply_conversion: u = self.U.units.to_target(u, z, y, x) v = self.V.units.to_target(v, z, y, x) if "3D" in self.vector_type: diff --git a/src/parcels/_core/fieldset.py b/src/parcels/_core/fieldset.py index f9bcb795fe..7f24127a08 100644 --- a/src/parcels/_core/fieldset.py +++ b/src/parcels/_core/fieldset.py @@ -182,7 +182,8 @@ def gridset(self) -> list[BaseGrid]: grids.append(field.grid) return grids - def from_copernicusmarine(ds: xr.Dataset): + @classmethod + def from_copernicusmarine(cls, ds: xr.Dataset): """Create a FieldSet from a Copernicus Marine Service xarray.Dataset. Parameters @@ -237,9 +238,10 @@ def from_copernicusmarine(ds: xr.Dataset): vertical_dimensions=(sgrid.DimDimPadding("z_center", "depth", sgrid.Padding.LOW),), ).to_attrs(), ) - return FieldSet.from_sgrid_conventions(ds, mesh="spherical") + return cls.from_sgrid_conventions(ds, mesh="spherical") - def from_fesom2(ds: ux.UxDataset): + @classmethod + def from_fesom2(cls, ds: ux.UxDataset): """Create a FieldSet from a FESOM2 uxarray.UxDataset. Parameters @@ -275,10 +277,11 @@ def from_fesom2(ds: ux.UxDataset): for varname in set(ds.data_vars) - set(fields.keys()): fields[varname] = Field(varname, ds[varname], grid, _select_uxinterpolator(ds[varname])) - return FieldSet(list(fields.values())) + return cls(list(fields.values())) + @classmethod def from_sgrid_conventions( - ds: xr.Dataset, mesh: Mesh + cls, ds: xr.Dataset, mesh: Mesh ): # TODO: Update mesh to be discovered from the dataset metadata """Create a FieldSet from a dataset using SGRID convention metadata. @@ -360,7 +363,7 @@ def from_sgrid_conventions( for varname in set(ds.data_vars) - set(fields.keys()) - skip_vars: fields[varname] = Field(varname, ds[varname], grid, XLinear) - return FieldSet(list(fields.values())) + return cls(list(fields.values())) class CalendarError(Exception): # TODO: Move to a parcels errors module diff --git a/src/parcels/_core/index_search.py b/src/parcels/_core/index_search.py index c2dd183512..170871d92d 100644 --- a/src/parcels/_core/index_search.py +++ b/src/parcels/_core/index_search.py @@ -276,7 +276,7 @@ def uxgrid_point_in_cell(grid, y: np.ndarray, x: np.ndarray, yi: np.ndarray, xi: return is_in_cell, coords -def _triangle_area(A, B, C): +def _triangle_area(A, B, C): # noqa: N803 """Compute the area of a triangle given by three points.""" d1 = B - A d2 = C - A diff --git a/src/parcels/_core/particlefile.py b/src/parcels/_core/particlefile.py index 52f20af2e0..778e4275f5 100644 --- a/src/parcels/_core/particlefile.py +++ b/src/parcels/_core/particlefile.py @@ -130,7 +130,7 @@ def store(self): def create_new_zarrfile(self): return self._create_new_zarrfile - def _extend_zarr_dims(self, Z, store, dtype, axis): + def _extend_zarr_dims(self, Z, store, dtype, axis): # noqa: N803 if axis == 1: a = np.full((Z.shape[0], self.chunks[1]), _DATATYPES_TO_FILL_VALUES[dtype], dtype=dtype) obs = zarr.group(store=store, overwrite=False)["obs"] diff --git a/src/parcels/_core/utils/sgrid.py b/src/parcels/_core/utils/sgrid.py index ff87d50817..82ae0d727a 100644 --- a/src/parcels/_core/utils/sgrid.py +++ b/src/parcels/_core/utils/sgrid.py @@ -44,6 +44,8 @@ class Padding(enum.Enum): class AttrsSerializable(Protocol): def to_attrs(self) -> dict[str, str | int]: ... + + @classmethod def from_attrs(cls, d: dict[str, Hashable]) -> Self: ... diff --git a/src/parcels/interpolators.py b/src/parcels/interpolators.py index 8e23d49521..4d4382abe1 100644 --- a/src/parcels/interpolators.py +++ b/src/parcels/interpolators.py @@ -55,8 +55,8 @@ def _get_corner_data_Agrid( zi: int, yi: int, xi: int, - lenT: int, - lenZ: int, + lenT: int, # noqa: N803 + lenZ: int, # noqa: N803 npart: int, axis_dim: dict[str, str], ) -> np.ndarray: diff --git a/src/parcels/kernels/advection.py b/src/parcels/kernels/advection.py index 29666cba08..e88d9d99c6 100644 --- a/src/parcels/kernels/advection.py +++ b/src/parcels/kernels/advection.py @@ -316,7 +316,7 @@ def AdvectionAnalytical(particles, fieldset): # pragma: no cover V0 = V0 * (1 - tau) + tau * direction * fieldset.V.data[ti + 1, yi, xi + 1] * c1 * dz V1 = V1 * (1 - tau) + tau * direction * fieldset.V.data[ti + 1, yi + 1, xi + 1] * c3 * dz - def compute_ds(F0, F1, r, direction, tol): + def compute_ds(F0, F1, r, direction, tol): # noqa: N803 up = F0 * (1 - r) + F1 * r r_target = 1.0 if direction * up >= 0.0 else 0.0 B = F0 - F1 @@ -358,7 +358,7 @@ def compute_ds(F0, F1, r, direction, tol): s_min = min(abs(ds_x), abs(ds_y), abs(ds_z), abs(ds_t / (dxdy * dz))) # calculate end position in time s_min - def compute_rs(r, B, delta, s_min): + def compute_rs(r, B, delta, s_min): # noqa: N803 if abs(B) < tol: return -delta * s_min + r else: diff --git a/tests/test_advection.py b/tests/test_advection.py index 0a0b7484aa..91a054cbac 100644 --- a/tests/test_advection.py +++ b/tests/test_advection.py @@ -103,8 +103,8 @@ def test_horizontal_advection_in_3D_flow(npart=10): @pytest.mark.parametrize("direction", ["up", "down"]) -@pytest.mark.parametrize("wErrorThroughSurface", [True, False]) -def test_advection_3D_outofbounds(direction, wErrorThroughSurface): +@pytest.mark.parametrize("resubmerge_particle", [True, False]) +def test_advection_3D_outofbounds(direction, resubmerge_particle): ds = simple_UV_dataset(mesh="flat") ds["W"] = ds["V"].copy() # Just to have W field present ds["U"].data[:] = 0.01 # Set U to small value (to avoid horizontal out of bounds) @@ -131,14 +131,14 @@ def SubmergeParticle(particles, fieldset): # pragma: no cover particles[inds].state = StatusCode.Evaluate kernels = [AdvectionRK4_3D] - if wErrorThroughSurface: + if resubmerge_particle: kernels.append(SubmergeParticle) kernels.append(DeleteParticle) pset = ParticleSet(fieldset=fieldset, lon=0.5, lat=0.5, z=0.9) pset.execute(kernels, runtime=np.timedelta64(10, "s"), dt=np.timedelta64(1, "s")) - if direction == "up" and wErrorThroughSurface: + if direction == "up" and resubmerge_particle: np.testing.assert_allclose(pset.lon[0], 0.6, atol=1e-5) np.testing.assert_allclose(pset.z[0], 0, atol=1e-5) else: diff --git a/tests/test_field.py b/tests/test_field.py index 7795c953d2..7c2e643ddd 100644 --- a/tests/test_field.py +++ b/tests/test_field.py @@ -201,31 +201,31 @@ def test_field_unstructured_z_linear(): # Test above first cell center - for piecewise constant, should return the depth of the first cell center assert np.isclose( - P.eval(time=[0], z=[10.0], y=[30.0], x=[30.0], applyConversion=False), + P.eval(time=[0], z=[10.0], y=[30.0], x=[30.0], apply_conversion=False), 55.555557, ) # Test below first cell center, but in the first layer - for piecewise constant, should return the depth of the first cell center assert np.isclose( - P.eval(time=[0], z=[65.0], y=[30.0], x=[30.0], applyConversion=False), + P.eval(time=[0], z=[65.0], y=[30.0], x=[30.0], apply_conversion=False), 55.555557, ) # Test bottom layer - for piecewise constant, should return the depth of the of the bottom layer cell center assert np.isclose( - P.eval(time=[0], z=[900.0], y=[30.0], x=[30.0], applyConversion=False), + P.eval(time=[0], z=[900.0], y=[30.0], x=[30.0], apply_conversion=False), 944.44445801, ) W = Field(name="W", data=ds.W, grid=grid, interp_method=UxPiecewiseLinearNode) assert np.isclose( - W.eval(time=[0], z=[10.0], y=[30.0], x=[30.0], applyConversion=False), + W.eval(time=[0], z=[10.0], y=[30.0], x=[30.0], apply_conversion=False), 10.0, ) assert np.isclose( - W.eval(time=[0], z=[65.0], y=[30.0], x=[30.0], applyConversion=False), + W.eval(time=[0], z=[65.0], y=[30.0], x=[30.0], apply_conversion=False), 65.0, ) assert np.isclose( - W.eval(time=[0], z=[900.0], y=[30.0], x=[30.0], applyConversion=False), + W.eval(time=[0], z=[900.0], y=[30.0], x=[30.0], apply_conversion=False), 900.0, ) @@ -239,13 +239,13 @@ def test_field_constant_in_time(): # Assert that the field can be evaluated at any time, and returns the same value time = np.datetime64("2000-01-01T00:00:00") - P1 = P.eval(time=time, z=[10.0], y=[30.0], x=[30.0], applyConversion=False) + P1 = P.eval(time=time, z=[10.0], y=[30.0], x=[30.0], apply_conversion=False) P2 = P.eval( time=time + np.timedelta64(1, "D"), z=[10.0], y=[30.0], x=[30.0], - applyConversion=False, + apply_conversion=False, ) assert np.isclose(P1, P2) diff --git a/tests/test_interpolation.py b/tests/test_interpolation.py index 39b588f2b6..8055e954d7 100644 --- a/tests/test_interpolation.py +++ b/tests/test_interpolation.py @@ -179,7 +179,7 @@ def test_interpolation_mesh_type(mesh, npart=10): assert np.isclose(u, u_expected, atol=1e-7) assert v == 0.0 - assert fieldset.U.eval(time, 0, lat, 0, applyConversion=False) == 1.0 + assert fieldset.U.eval(time, 0, lat, 0, apply_conversion=False) == 1 interp_methods = { diff --git a/tests/test_uxarray_fieldset.py b/tests/test_uxarray_fieldset.py index 05aa3c5b24..600d40ce0d 100644 --- a/tests/test_uxarray_fieldset.py +++ b/tests/test_uxarray_fieldset.py @@ -123,25 +123,25 @@ def test_fesom2_square_delaunay_uniform_z_coordinate_eval(): fieldset = FieldSet([UVW, P, UVW.U, UVW.V, UVW.W]) assert np.isclose( - fieldset.U.eval(time=[0.0], z=[1.0], y=[30.0], x=[30.0], applyConversion=False), + fieldset.U.eval(time=[0.0], z=[1.0], y=[30.0], x=[30.0], apply_conversion=False), 1.0, rtol=1e-3, atol=1e-6, ) assert np.isclose( - fieldset.V.eval(time=[0.0], z=[1.0], y=[30.0], x=[30.0], applyConversion=False), + fieldset.V.eval(time=[0.0], z=[1.0], y=[30.0], x=[30.0], apply_conversion=False), 1.0, rtol=1e-3, atol=1e-6, ) assert np.isclose( - fieldset.W.eval(time=[0.0], z=[1.0], y=[30.0], x=[30.0], applyConversion=False), + fieldset.W.eval(time=[0.0], z=[1.0], y=[30.0], x=[30.0], apply_conversion=False), 0.0, rtol=1e-3, atol=1e-6, ) assert np.isclose( - fieldset.p.eval(time=[0.0], z=[1.0], y=[30.0], x=[30.0], applyConversion=False), + fieldset.p.eval(time=[0.0], z=[1.0], y=[30.0], x=[30.0], apply_conversion=False), 1.0, rtol=1e-3, atol=1e-6, @@ -163,7 +163,7 @@ def test_fesom2_square_delaunay_antimeridian_eval(): ) fieldset = FieldSet([P]) - assert np.isclose(fieldset.p.eval(time=[0], z=[1.0], y=[30.0], x=[-170.0], applyConversion=False), 1.0) - assert np.isclose(fieldset.p.eval(time=[0], z=[1.0], y=[30.0], x=[-180.0], applyConversion=False), 1.0) - assert np.isclose(fieldset.p.eval(time=[0], z=[1.0], y=[30.0], x=[180.0], applyConversion=False), 1.0) - assert np.isclose(fieldset.p.eval(time=[0], z=[1.0], y=[30.0], x=[170.0], applyConversion=False), 1.0) + assert np.isclose(fieldset.p.eval(time=[0], z=[1.0], y=[30.0], x=[-170.0], apply_conversion=False), 1.0) + assert np.isclose(fieldset.p.eval(time=[0], z=[1.0], y=[30.0], x=[-180.0], apply_conversion=False), 1.0) + assert np.isclose(fieldset.p.eval(time=[0], z=[1.0], y=[30.0], x=[180.0], apply_conversion=False), 1.0) + assert np.isclose(fieldset.p.eval(time=[0], z=[1.0], y=[30.0], x=[170.0], apply_conversion=False), 1.0)