From 3f03f4bf3c33a94c5aedf4b362910fdb92cdcc89 Mon Sep 17 00:00:00 2001 From: Peter Wolfram Date: Mon, 15 Jun 2026 14:06:25 +0200 Subject: [PATCH 01/13] test --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 72d34ef83..3e5e4f6e1 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ See [parcels-code.org](http://parcels-code.org/) for further information about [ ### Contributors + From 908310e39eb5cbef8261287892433ff5cf870b35 Mon Sep 17 00:00:00 2001 From: "peter.wolfram" Date: Tue, 16 Jun 2026 10:45:19 +0200 Subject: [PATCH 02/13] trivial replace "add_constant" -> "add_context" whole match only --- docs/user_guide/examples/tutorial_Argofloats.ipynb | 2 +- docs/user_guide/examples/tutorial_croco_3D.ipynb | 4 ++-- docs/user_guide/examples/tutorial_diffusion.ipynb | 6 +++--- .../examples_v3/documentation_stuck_particles.ipynb | 2 +- docs/user_guide/examples_v3/example_mitgcm.py | 2 +- docs/user_guide/examples_v3/example_moving_eddies.py | 8 ++++---- docs/user_guide/examples_v3/example_stommel.py | 2 +- .../examples_v3/tutorial_analyticaladvection.ipynb | 4 ++-- .../tutorial_particle_field_interaction.ipynb | 6 +++--- src/parcels/_core/fieldset.py | 2 +- src/parcels/_core/kernel.py | 12 ++++++------ tests-v3/test_fieldset.py | 4 ++-- tests-v3/test_fieldset_sampling.py | 4 ++-- tests-v3/test_kernel_language.py | 2 +- tests/test_advection.py | 12 ++++++------ tests/test_diffusion.py | 4 ++-- tests/test_fieldset.py | 6 +++--- tests/test_particlefile.py | 2 +- tests/test_sigmagrids.py | 4 ++-- 19 files changed, 44 insertions(+), 44 deletions(-) diff --git a/docs/user_guide/examples/tutorial_Argofloats.ipynb b/docs/user_guide/examples/tutorial_Argofloats.ipynb index 6704bf262..05d7fbf56 100644 --- a/docs/user_guide/examples/tutorial_Argofloats.ipynb +++ b/docs/user_guide/examples/tutorial_Argofloats.ipynb @@ -130,7 +130,7 @@ "# Convert to SGRID-compliant dataset and create FieldSet\n", "ds_fset = parcels.convert.copernicusmarine_to_sgrid(fields=fields)\n", "fieldset = parcels.FieldSet.from_sgrid_conventions(ds_fset)\n", - "fieldset.add_constant(\"mindepth\", 1.0)\n", + "fieldset.add_context(\"mindepth\", 1.0)\n", "\n", "# Define a new Particle type including extra Variables\n", "ArgoParticle = parcels.Particle.add_variable(\n", diff --git a/docs/user_guide/examples/tutorial_croco_3D.ipynb b/docs/user_guide/examples/tutorial_croco_3D.ipynb index 50917272e..a8b7d69e0 100644 --- a/docs/user_guide/examples/tutorial_croco_3D.ipynb +++ b/docs/user_guide/examples/tutorial_croco_3D.ipynb @@ -92,7 +92,7 @@ "fieldset = parcels.FieldSet.from_sgrid_conventions(ds_fset)\n", "\n", "# Add the critical depth (`hc`) as a constant to the fieldset\n", - "fieldset.add_constant(\"hc\", ds_fields.hc.item())" + "fieldset.add_context(\"hc\", ds_fields.hc.item())" ] }, { @@ -206,7 +206,7 @@ "source": [ "fieldset_noW = parcels.FieldSet.from_sgrid_conventions(ds_fset)\n", "fieldset_noW.W.data[:] = 0.0\n", - "fieldset_noW.add_constant(\"hc\", ds_fields.hc.item())\n", + "fieldset_noW.add_context(\"hc\", ds_fields.hc.item())\n", "\n", "X, Z = np.meshgrid(\n", " [40e3, 80e3, 120e3],\n", diff --git a/docs/user_guide/examples/tutorial_diffusion.ipynb b/docs/user_guide/examples/tutorial_diffusion.ipynb index 8541433d5..5aaa094c0 100644 --- a/docs/user_guide/examples/tutorial_diffusion.ipynb +++ b/docs/user_guide/examples/tutorial_diffusion.ipynb @@ -90,7 +90,7 @@ "\n", "Just like velocities, diffusivities are passed to Parcels in the form of `Field` objects. When using `DiffusionUniformKh`, they should be added to the `FieldSet` object as constant fields, e.g. `fieldset.add_constant_field(\"Kh_zonal\", 1, mesh=\"flat\")`.\n", "\n", - "To make a central difference approximation for computing the gradient in diffusivity, a resolution for this approximation `dres` is needed: _Parcels_ approximates the gradients in diffusivities by using their values at the particle's location ± `dres` (in both $x$ and $y$). A value of `dres` must be specified and added to the FieldSet by the user (e.g. `fieldset.add_constant(\"dres\", 0.01)`). Currently, it is unclear what the best value of `dres` is. From experience, the size of `dres` should be smaller than the spatial resolution of the data, but within reasonable limits of machine precision to avoid numerical errors. We are working on a method to compute gradients differently so that specifying `dres` is not necessary anymore.\n", + "To make a central difference approximation for computing the gradient in diffusivity, a resolution for this approximation `dres` is needed: _Parcels_ approximates the gradients in diffusivities by using their values at the particle's location ± `dres` (in both $x$ and $y$). A value of `dres` must be specified and added to the FieldSet by the user (e.g. `fieldset.add_context(\"dres\", 0.01)`). Currently, it is unclear what the best value of `dres` is. From experience, the size of `dres` should be smaller than the spatial resolution of the data, but within reasonable limits of machine precision to avoid numerical errors. We are working on a method to compute gradients differently so that specifying `dres` is not necessary anymore.\n", "\n", "## Example: Impermeable Diffusivity Profile\n", "\n", @@ -206,7 +206,7 @@ "source": [ "fieldset = parcels.FieldSet.from_sgrid_conventions(ds, mesh=\"flat\")\n", "fieldset.add_constant_field(\"Kh_zonal\", 1, mesh=\"flat\")\n", - "fieldset.add_constant(\"dres\", 0.00005)" + "fieldset.add_context(\"dres\", 0.00005)" ] }, { @@ -536,7 +536,7 @@ ")\n", "fieldset.add_field(cell_areas_field)\n", "\n", - "fieldset.add_constant(\"Cs\", 0.1)" + "fieldset.add_context(\"Cs\", 0.1)" ] }, { diff --git a/docs/user_guide/examples_v3/documentation_stuck_particles.ipynb b/docs/user_guide/examples_v3/documentation_stuck_particles.ipynb index 880835f96..f2a640efc 100644 --- a/docs/user_guide/examples_v3/documentation_stuck_particles.ipynb +++ b/docs/user_guide/examples_v3/documentation_stuck_particles.ipynb @@ -1158,7 +1158,7 @@ "\n", "fieldset.add_constant_field(\"Kh_zonal\", 5, mesh=\"spherical\")\n", "fieldset.add_constant_field(\"Kh_meridional\", 5, mesh=\"spherical\")\n", - "# fieldset.add_constant('dres', 0.00005)\n", + "# fieldset.add_context('dres', 0.00005)\n", "fieldset.add_field(\n", " parcels.Field(\n", " \"landmask\",\n", diff --git a/docs/user_guide/examples_v3/example_mitgcm.py b/docs/user_guide/examples_v3/example_mitgcm.py index 56d8e2beb..fbdfeeb42 100644 --- a/docs/user_guide/examples_v3/example_mitgcm.py +++ b/docs/user_guide/examples_v3/example_mitgcm.py @@ -23,7 +23,7 @@ def run_mitgcm_zonally_reentrant(path: Path): ) fieldset.add_periodic_halo(zonal=True) - fieldset.add_constant("domain_width", 1000000) + fieldset.add_context("domain_width", 1000000) def periodicBC(particle, fieldset, time): # pragma: no cover if particle.lon < 0: diff --git a/docs/user_guide/examples_v3/example_moving_eddies.py b/docs/user_guide/examples_v3/example_moving_eddies.py index 348e21b32..83cfd075a 100644 --- a/docs/user_guide/examples_v3/example_moving_eddies.py +++ b/docs/user_guide/examples_v3/example_moving_eddies.py @@ -257,10 +257,10 @@ def test_periodic_and_computeTimeChunk_eddies(): filename = str(data_folder / "moving_eddies") fieldset = parcels.FieldSet.from_parcels(filename) - fieldset.add_constant("halo_west", fieldset.U.grid.lon[0]) - fieldset.add_constant("halo_east", fieldset.U.grid.lon[-1]) - fieldset.add_constant("halo_south", fieldset.U.grid.lat[0]) - fieldset.add_constant("halo_north", fieldset.U.grid.lat[-1]) + fieldset.add_context("halo_west", fieldset.U.grid.lon[0]) + fieldset.add_context("halo_east", fieldset.U.grid.lon[-1]) + fieldset.add_context("halo_south", fieldset.U.grid.lat[0]) + fieldset.add_context("halo_north", fieldset.U.grid.lat[-1]) fieldset.add_periodic_halo(zonal=True, meridional=True) pset = parcels.ParticleSet.from_list( fieldset=fieldset, diff --git a/docs/user_guide/examples_v3/example_stommel.py b/docs/user_guide/examples_v3/example_stommel.py index 068ac5812..e0dbcee06 100755 --- a/docs/user_guide/examples_v3/example_stommel.py +++ b/docs/user_guide/examples_v3/example_stommel.py @@ -133,7 +133,7 @@ def stommel_example( print(f"Initial particle positions:\n{pset}") maxage = runtime.total_seconds() if maxage is None else maxage - fieldset.add_constant("maxage", maxage) + fieldset.add_context("maxage", maxage) print(f"Stommel: Advecting {npart} particles for {runtime}") parcels.timer.psetinit.stop() parcels.timer.psetrun = parcels.timer.Timer("Pset_run", parent=parcels.timer.pset) diff --git a/docs/user_guide/examples_v3/tutorial_analyticaladvection.ipynb b/docs/user_guide/examples_v3/tutorial_analyticaladvection.ipynb index cbf14aa22..cc82efb20 100644 --- a/docs/user_guide/examples_v3/tutorial_analyticaladvection.ipynb +++ b/docs/user_guide/examples_v3/tutorial_analyticaladvection.ipynb @@ -467,10 +467,10 @@ "metadata": {}, "outputs": [], "source": [ - "fieldsetBJ.add_constant(\n", + "fieldsetBJ.add_context(\n", " \"halo_west\", fieldsetBJ.U.grid.lon[1]\n", ") # TODO v4: Change index back to 0 when periodic interpolation is done\n", - "fieldsetBJ.add_constant(\n", + "fieldsetBJ.add_context(\n", " \"halo_east\", fieldsetBJ.U.grid.lon[-2]\n", ") # TODO v4: Change index back to -1 when periodic interpolation is done\n", "\n", diff --git a/docs/user_guide/examples_v3/tutorial_particle_field_interaction.ipynb b/docs/user_guide/examples_v3/tutorial_particle_field_interaction.ipynb index 333ad40b8..991fe2102 100644 --- a/docs/user_guide/examples_v3/tutorial_particle_field_interaction.ipynb +++ b/docs/user_guide/examples_v3/tutorial_particle_field_interaction.ipynb @@ -127,9 +127,9 @@ "metadata": {}, "outputs": [], "source": [ - "fieldset.add_constant(\"a\", 10)\n", - "fieldset.add_constant(\"b\", 0.2)\n", - "fieldset.add_constant(\"weight\", 0.01)" + "fieldset.add_context(\"a\", 10)\n", + "fieldset.add_context(\"b\", 0.2)\n", + "fieldset.add_context(\"weight\", 0.01)" ] }, { diff --git a/src/parcels/_core/fieldset.py b/src/parcels/_core/fieldset.py index 98700edd9..ebca4f3c3 100644 --- a/src/parcels/_core/fieldset.py +++ b/src/parcels/_core/fieldset.py @@ -153,7 +153,7 @@ def add_constant_field(self, name: str, value, mesh: Mesh = "spherical"): grid = XGrid(xgrid, mesh=mesh) self.add_field(Field(name, ds[name], grid, interp_method=XConstantField)) - def add_constant(self, name, value): + def add_context(self, name, value): """Add a constant to the FieldSet. Parameters diff --git a/src/parcels/_core/kernel.py b/src/parcels/_core/kernel.py index 83259228f..7853a6aec 100644 --- a/src/parcels/_core/kernel.py +++ b/src/parcels/_core/kernel.py @@ -136,29 +136,29 @@ def check_fieldsets_in_kernels(self, kernel): # TODO v4: this can go into anoth raise ValueError('ParticleClass requires a "next_dt" for AdvectionRK45 Kernel.') if not hasattr(self.fieldset, "RK45_tol"): warnings.warn( - "Setting RK45 tolerance to 10 m. Use fieldset.add_constant('RK45_tol', [distance]) to change.", + "Setting RK45 tolerance to 10 m. Use fieldset.add_context('RK45_tol', [distance]) to change.", KernelWarning, stacklevel=2, ) - self.fieldset.add_constant("RK45_tol", 10) + self.fieldset.add_context("RK45_tol", 10) if self.fieldset.U.grid._mesh == "spherical": self.fieldset.RK45_tol /= ( 1852 * 60 ) # TODO does not account for zonal variation in meter -> degree conversion if not hasattr(self.fieldset, "RK45_min_dt"): warnings.warn( - "Setting RK45 minimum timestep to 1 s. Use fieldset.add_constant('RK45_min_dt', [timestep]) to change.", + "Setting RK45 minimum timestep to 1 s. Use fieldset.add_context('RK45_min_dt', [timestep]) to change.", KernelWarning, stacklevel=2, ) - self.fieldset.add_constant("RK45_min_dt", 1) + self.fieldset.add_context("RK45_min_dt", 1) if not hasattr(self.fieldset, "RK45_max_dt"): warnings.warn( - "Setting RK45 maximum timestep to 1 day. Use fieldset.add_constant('RK45_max_dt', [timestep]) to change.", + "Setting RK45 maximum timestep to 1 day. Use fieldset.add_context('RK45_max_dt', [timestep]) to change.", KernelWarning, stacklevel=2, ) - self.fieldset.add_constant("RK45_max_dt", 60 * 60 * 24) + self.fieldset.add_context("RK45_max_dt", 60 * 60 * 24) def merge(self, kernel): if not isinstance(kernel, type(self)): diff --git a/tests-v3/test_fieldset.py b/tests-v3/test_fieldset.py index 69295cf87..49e9c518e 100644 --- a/tests-v3/test_fieldset.py +++ b/tests-v3/test_fieldset.py @@ -206,8 +206,8 @@ def test_fieldset_constant(): fieldset = FieldSet.from_data(data, dimensions) # TODO : Remove from_data westval = -0.2 eastval = 0.3 - fieldset.add_constant("movewest", westval) - fieldset.add_constant("moveeast", eastval) + fieldset.add_context("movewest", westval) + fieldset.add_context("moveeast", eastval) assert fieldset.movewest == westval pset = ParticleSet.from_line(fieldset, size=1, pclass=Particle, start=(0.5, 0.5), finish=(0.5, 0.5)) diff --git a/tests-v3/test_fieldset_sampling.py b/tests-v3/test_fieldset_sampling.py index 291c27b88..1853b2578 100644 --- a/tests-v3/test_fieldset_sampling.py +++ b/tests-v3/test_fieldset_sampling.py @@ -652,7 +652,7 @@ def test_sampling_multigrids_non_vectorfield_from_file(npart, tmpdir): dimensions = {"lon": "nav_lon", "lat": "nav_lat"} fieldset = FieldSet.from_netcdf(files, variables, dimensions, timestamps=timestamps, allow_time_extrapolation=True) - fieldset.add_constant("sample_depth", 2.5) + fieldset.add_context("sample_depth", 2.5) assert fieldset.U.grid is fieldset.V.grid assert fieldset.U.grid is not fieldset.B.grid @@ -693,7 +693,7 @@ def test_sampling_multigrids_non_vectorfield(npart): ) fieldset = FieldSet(U, V) fieldset.add_field(B, "B") - fieldset.add_constant("sample_depth", 2.5) + fieldset.add_context("sample_depth", 2.5) assert fieldset.U.grid is fieldset.V.grid assert fieldset.U.grid is not fieldset.B.grid diff --git a/tests-v3/test_kernel_language.py b/tests-v3/test_kernel_language.py index a28da2c5b..e7515819f 100644 --- a/tests-v3/test_kernel_language.py +++ b/tests-v3/test_kernel_language.py @@ -163,7 +163,7 @@ def test_varname_as_fieldname(): """Tests for error thrown if variable has same name as Field.""" fset = create_fieldset_unit_mesh(mesh="spherical") fset.add_field(Field("speed", 10, lon=0, lat=0)) - fset.add_constant("vertical_speed", 0.1) + fset.add_context("vertical_speed", 0.1) particle = Particle.add_variable("speed") pset = ParticleSet(fset, pclass=particle, lon=0, lat=0) diff --git a/tests/test_advection.py b/tests/test_advection.py index 17511e82e..3f46726d2 100644 --- a/tests/test_advection.py +++ b/tests/test_advection.py @@ -273,14 +273,14 @@ def test_moving_eddy(kernel, rtol): fieldset = FieldSet.from_sgrid_conventions(ds, mesh="flat") if kernel in [AdvectionDiffusionEM, AdvectionDiffusionM1]: - fieldset.add_constant("dres", 0.1) + fieldset.add_context("dres", 0.1) start_lon, start_lat, start_z = 12000, 12500, 12500 dt = np.timedelta64(30, "m") endtime = np.timedelta64(1, "h") if kernel == AdvectionRK45: - fieldset.add_constant("RK45_tol", rtol) + fieldset.add_context("RK45_tol", rtol) pset = ParticleSet( fieldset, pclass=DEFAULT_PARTICLES[kernel], lon=start_lon, lat=start_lat, z=start_z, time=np.timedelta64(0, "s") @@ -318,8 +318,8 @@ def test_decaying_moving_eddy(kernel, rtol): endtime = np.timedelta64(23, "h") if kernel == AdvectionRK45: - fieldset.add_constant("RK45_tol", rtol) - fieldset.add_constant("RK45_min_dt", 10 * 60) + fieldset.add_context("RK45_tol", rtol) + fieldset.add_context("RK45_min_dt", 10 * 60) pset = ParticleSet( fieldset, pclass=DEFAULT_PARTICLES[kernel], lon=start_lon, lat=start_lat, time=np.timedelta64(0, "s") @@ -368,7 +368,7 @@ def test_stommelgyre_fieldset(kernel, rtol, grid_type): ) if kernel == AdvectionRK45: - fieldset.add_constant("RK45_tol", rtol) + fieldset.add_context("RK45_tol", rtol) def UpdateP(particles, fieldset): # pragma: no cover particles.p = fieldset.P[particles.time, particles.z, particles.lat, particles.lon] @@ -403,7 +403,7 @@ def test_peninsula_fieldset(kernel, rtol, grid_type): ) if kernel == AdvectionRK45: - fieldset.add_constant("RK45_tol", rtol) + fieldset.add_context("RK45_tol", rtol) def UpdateP(particles, fieldset): # pragma: no cover particles.p = fieldset.P[particles.time, particles.z, particles.lat, particles.lon] diff --git a/tests/test_diffusion.py b/tests/test_diffusion.py index 75dd850f9..9acb5e878 100644 --- a/tests/test_diffusion.py +++ b/tests/test_diffusion.py @@ -64,7 +64,7 @@ def test_fieldKh_SpatiallyVaryingDiffusion(mesh, kernel): ds["Kh_zonal"] = (["time", "depth", "YG", "XG"], np.full((2, 1, ydim, xdim), Kh)) ds["Kh_meridional"] = (["time", "depth", "YG", "XG"], np.full((2, 1, ydim, xdim), Kh)) fieldset = FieldSet.from_sgrid_conventions(ds, mesh=mesh) - fieldset.add_constant("dres", float(ds["lon"][1] - ds["lon"][0])) + fieldset.add_context("dres", float(ds["lon"][1] - ds["lon"][0])) npart = 10000 @@ -84,7 +84,7 @@ def test_randomexponential(lambd): npart = 1000 # Rate parameter for random.expovariate - fieldset.add_constant("lambd", lambd) + fieldset.add_context("lambd", lambd) # Set random seed np.random.seed(1234) diff --git a/tests/test_fieldset.py b/tests/test_fieldset.py index 3663588fa..170fe0809 100644 --- a/tests/test_fieldset.py +++ b/tests/test_fieldset.py @@ -25,19 +25,19 @@ def test_fieldset_init_wrong_types(): def test_fieldset_add_constant(fieldset): - fieldset.add_constant("test_constant", 1.0) + fieldset.add_context("test_constant", 1.0) assert fieldset.test_constant == 1.0 def test_fieldset_add_constant_int_name(fieldset): with pytest.raises(TypeError, match="Expected a string for variable name, got int instead."): - fieldset.add_constant(123, 1.0) + fieldset.add_context(123, 1.0) @pytest.mark.parametrize("name", ["a b", "123", "while"]) def test_fieldset_add_constant_invalid_name(fieldset, name): with pytest.raises(ValueError, match=r"Received invalid Python variable name.*"): - fieldset.add_constant(name, 1.0) + fieldset.add_context(name, 1.0) def test_fieldset_add_constant_field(fieldset): diff --git a/tests/test_particlefile.py b/tests/test_particlefile.py index 9814dfe33..dc0e232c1 100755 --- a/tests/test_particlefile.py +++ b/tests/test_particlefile.py @@ -148,7 +148,7 @@ def test_write_dtypes_pfile(fieldset, tmp_parquet): def test_pset_repeated_release_delayed_adding_deleting(fieldset, tmp_parquet, dt, maxvar): """Tests that if particles are released and deleted based on age that resulting output file is correct.""" npart = 10 - fieldset.add_constant("maxvar", maxvar) + fieldset.add_context("maxvar", maxvar) MyParticle = Particle.add_variable( [Variable("sample_var", initial=0.0), Variable("v_once", dtype=np.float64, initial=0.0)] diff --git a/tests/test_sigmagrids.py b/tests/test_sigmagrids.py index 537c8c101..196ce2dd1 100644 --- a/tests/test_sigmagrids.py +++ b/tests/test_sigmagrids.py @@ -30,7 +30,7 @@ def test_conversion_3DCROCO(): ds_fset = parcels.convert.croco_to_sgrid(fields=fields, coords=ds_fields) fieldset = parcels.FieldSet.from_sgrid_conventions(ds_fset) - fieldset.add_constant("hc", ds_fields.hc.item()) + fieldset.add_context("hc", ds_fields.hc.item()) s_xroms = np.array([-1.0, -0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1, 0.0], dtype=np.float32) z_xroms = np.array([-1.26000000e02, -1.10585846e02, -9.60985413e01, -8.24131317e01, -6.94126511e01, -5.69870148e01, -4.50318756e01, -3.34476166e01, -2.21383114e01, -1.10107975e01, 2.62768921e-02,], dtype=np.float32,) # fmt: skip @@ -61,7 +61,7 @@ def test_advection_3DCROCO(): ds_fset = parcels.convert.croco_to_sgrid(fields=fields, coords=ds_fields) fieldset = parcels.FieldSet.from_sgrid_conventions(ds_fset) - fieldset.add_constant("hc", ds_fields.hc.item()) + fieldset.add_context("hc", ds_fields.hc.item()) runtime = 10_000 X, Z = np.meshgrid([40e3, 80e3, 120e3], [-10, -130]) From f707dc78e7b1d31ae0ee39399aa30872de67ba0b Mon Sep 17 00:00:00 2001 From: "peter.wolfram" Date: Tue, 16 Jun 2026 10:50:51 +0200 Subject: [PATCH 03/13] renamed test function names from "*_add_constant" -> "*_add_context" --- tests/test_fieldset.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_fieldset.py b/tests/test_fieldset.py index 170fe0809..7adb9e2b9 100644 --- a/tests/test_fieldset.py +++ b/tests/test_fieldset.py @@ -24,18 +24,18 @@ def test_fieldset_init_wrong_types(): FieldSet([1.0, 2.0, 3.0]) -def test_fieldset_add_constant(fieldset): +def test_fieldset_add_context(fieldset): fieldset.add_context("test_constant", 1.0) assert fieldset.test_constant == 1.0 -def test_fieldset_add_constant_int_name(fieldset): +def test_fieldset_add_context_int_name(fieldset): with pytest.raises(TypeError, match="Expected a string for variable name, got int instead."): fieldset.add_context(123, 1.0) @pytest.mark.parametrize("name", ["a b", "123", "while"]) -def test_fieldset_add_constant_invalid_name(fieldset, name): +def test_fieldset_add_context_invalid_name(fieldset, name): with pytest.raises(ValueError, match=r"Received invalid Python variable name.*"): fieldset.add_context(name, 1.0) From 6591d276758544f7c7dd6765824305054bfc4781 Mon Sep 17 00:00:00 2001 From: "peter.wolfram" Date: Tue, 16 Jun 2026 11:40:28 +0200 Subject: [PATCH 04/13] additional add_context changes --- docs/user_guide/examples/tutorial_croco_3D.ipynb | 2 +- tests/test_fieldset.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/user_guide/examples/tutorial_croco_3D.ipynb b/docs/user_guide/examples/tutorial_croco_3D.ipynb index a8b7d69e0..40c8501b2 100644 --- a/docs/user_guide/examples/tutorial_croco_3D.ipynb +++ b/docs/user_guide/examples/tutorial_croco_3D.ipynb @@ -91,7 +91,7 @@ "\n", "fieldset = parcels.FieldSet.from_sgrid_conventions(ds_fset)\n", "\n", - "# Add the critical depth (`hc`) as a constant to the fieldset\n", + "# Add the critical depth (`hc`) as context to the fieldset\n", "fieldset.add_context(\"hc\", ds_fields.hc.item())" ] }, diff --git a/tests/test_fieldset.py b/tests/test_fieldset.py index 7adb9e2b9..310fdf2fe 100644 --- a/tests/test_fieldset.py +++ b/tests/test_fieldset.py @@ -25,8 +25,8 @@ def test_fieldset_init_wrong_types(): def test_fieldset_add_context(fieldset): - fieldset.add_context("test_constant", 1.0) - assert fieldset.test_constant == 1.0 + fieldset.add_context("test_context", 1.0) + assert fieldset.test_context == 1.0 def test_fieldset_add_context_int_name(fieldset): From 98ad8f7947ac04ec62eddeeb46c44a081901605c Mon Sep 17 00:00:00 2001 From: "peter.wolfram" Date: Tue, 16 Jun 2026 11:41:16 +0200 Subject: [PATCH 05/13] renamed self.constants to self.context for FieldSet class --- src/parcels/_core/fieldset.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/parcels/_core/fieldset.py b/src/parcels/_core/fieldset.py index ebca4f3c3..263da4825 100644 --- a/src/parcels/_core/fieldset.py +++ b/src/parcels/_core/fieldset.py @@ -76,14 +76,14 @@ def __init__(self, fields: list[Field | VectorField]): assert_compatible_calendars(fields) self.fields = {f.name: f for f in fields} - self.constants: dict[str, float] = {} + self.context: dict[str, float] = {} def __getattr__(self, name): - """Get the field by name. If the field is not found, check if it's a constant.""" + """Get the field by name. If the field is not found, check if it's a context.""" if name in self.fields: return self.fields[name] - elif name in self.constants: - return self.constants[name] + elif name in self.context: + return self.context[name] else: raise AttributeError(f"FieldSet has no attribute '{name}'") @@ -154,23 +154,23 @@ def add_constant_field(self, name: str, value, mesh: Mesh = "spherical"): self.add_field(Field(name, ds[name], grid, interp_method=XConstantField)) def add_context(self, name, value): - """Add a constant to the FieldSet. + """Add context to the FieldSet. Parameters ---------- name : str - Name of the constant + Name of the context value : - Value of the constant + Value of the context """ _assert_str_and_python_varname(name) - if name in self.constants: - raise ValueError(f"FieldSet already has a constant with name '{name}'") + if name in self.context: + raise ValueError(f"FieldSet already has a context with name '{name}'") if not isinstance(value, (float, np.floating, int, np.integer)): - raise ValueError(f"FieldSet constants have to be of type float or int, got a {type(value)}") - self.constants[name] = value + raise ValueError(f"FieldSet contexts have to be of type float or int, got a {type(value)}") + self.context[name] = value @property def gridset(self) -> list[BaseGrid]: From 7b3bdeff02301d93271bf457e9c5a1dea9941369 Mon Sep 17 00:00:00 2001 From: "peter.wolfram" Date: Tue, 16 Jun 2026 14:04:56 +0200 Subject: [PATCH 06/13] added __setattr__ function and tests to prevent attribute access to variables from context dict --- src/parcels/_core/fieldset.py | 11 +++++++++++ tests/test_fieldset.py | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/parcels/_core/fieldset.py b/src/parcels/_core/fieldset.py index 263da4825..688ec6d20 100644 --- a/src/parcels/_core/fieldset.py +++ b/src/parcels/_core/fieldset.py @@ -78,6 +78,17 @@ def __init__(self, fields: list[Field | VectorField]): self.fields = {f.name: f for f in fields} self.context: dict[str, float] = {} + def __setattr__(self, name, value): + """Set field attribute by name. If context exists and name in context, raise error to prevent overwriting context.""" + context = self.__dict__.get("context") + + if context is not None and name in context: + raise AttributeError( + f"Cannot assign '{name}' directly. Use fieldset.context['{name}'] instead." + ) + # Handle setting of attributes not in context per default + super().__setattr__(name, value) + def __getattr__(self, name): """Get the field by name. If the field is not found, check if it's a context.""" if name in self.fields: diff --git a/tests/test_fieldset.py b/tests/test_fieldset.py index 310fdf2fe..13c13d738 100644 --- a/tests/test_fieldset.py +++ b/tests/test_fieldset.py @@ -32,6 +32,17 @@ def test_fieldset_add_context(fieldset): def test_fieldset_add_context_int_name(fieldset): with pytest.raises(TypeError, match="Expected a string for variable name, got int instead."): fieldset.add_context(123, 1.0) + + +def test_fieldset_setattr_new(fieldset): + fieldset.new_field = 1.0 + assert fieldset.new_field == 1.0 + + +def test_fieldset_setattr_context(fieldset): + fieldset.add_context("test_context", 1.0) + with pytest.raises(AttributeError, match="Tried to set attribute from context using setattr. Should use `fieldset.add_context` or fieldset[`name`] = value instead."): + fieldset.test_context = 2.0 @pytest.mark.parametrize("name", ["a b", "123", "while"]) From 19f95b36202211e28789ab2832db3c19f1f28794 Mon Sep 17 00:00:00 2001 From: "peter.wolfram" Date: Tue, 16 Jun 2026 14:48:21 +0200 Subject: [PATCH 07/13] fixed regex in test --- tests/test_fieldset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_fieldset.py b/tests/test_fieldset.py index 13c13d738..cce0bafb7 100644 --- a/tests/test_fieldset.py +++ b/tests/test_fieldset.py @@ -41,7 +41,7 @@ def test_fieldset_setattr_new(fieldset): def test_fieldset_setattr_context(fieldset): fieldset.add_context("test_context", 1.0) - with pytest.raises(AttributeError, match="Tried to set attribute from context using setattr. Should use `fieldset.add_context` or fieldset[`name`] = value instead."): + with pytest.raises(AttributeError, match=r"Cannot assign .* directly.*context"): fieldset.test_context = 2.0 From 951669a373a50b2112dee0fad2f58281bb736b37 Mon Sep 17 00:00:00 2001 From: "peter.wolfram" Date: Tue, 16 Jun 2026 15:06:09 +0200 Subject: [PATCH 08/13] deleted empty file "constants.py" --- src/parcels/_core/constants.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/parcels/_core/constants.py diff --git a/src/parcels/_core/constants.py b/src/parcels/_core/constants.py deleted file mode 100644 index e69de29bb..000000000 From 6397b936521ce970f19af40babfa62994f29022f Mon Sep 17 00:00:00 2001 From: "peter.wolfram" Date: Tue, 16 Jun 2026 15:09:40 +0200 Subject: [PATCH 09/13] changed test_fieldset_setattr_new to be more descriptive --- tests/test_fieldset.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_fieldset.py b/tests/test_fieldset.py index cce0bafb7..cacb719e9 100644 --- a/tests/test_fieldset.py +++ b/tests/test_fieldset.py @@ -35,8 +35,8 @@ def test_fieldset_add_context_int_name(fieldset): def test_fieldset_setattr_new(fieldset): - fieldset.new_field = 1.0 - assert fieldset.new_field == 1.0 + fieldset.context = {"new_field": 1.0} + assert fieldset.context == {"new_field": 1.0} def test_fieldset_setattr_context(fieldset): From 1eeb5474d9ebd950d25bffced42f62f8b535b4b0 Mon Sep 17 00:00:00 2001 From: "peter.wolfram" Date: Tue, 16 Jun 2026 15:10:45 +0200 Subject: [PATCH 10/13] reverted unwanted readme change --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 3e5e4f6e1..72d34ef83 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,6 @@ See [parcels-code.org](http://parcels-code.org/) for further information about [ ### Contributors - From 10c4143610e1ca2e262edd562707288a5ab46e05 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Jun 2026 13:16:59 +0000 Subject: [PATCH 11/13] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/parcels/_core/fieldset.py | 4 +--- tests/test_fieldset.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/parcels/_core/fieldset.py b/src/parcels/_core/fieldset.py index 688ec6d20..a24d51692 100644 --- a/src/parcels/_core/fieldset.py +++ b/src/parcels/_core/fieldset.py @@ -83,9 +83,7 @@ def __setattr__(self, name, value): context = self.__dict__.get("context") if context is not None and name in context: - raise AttributeError( - f"Cannot assign '{name}' directly. Use fieldset.context['{name}'] instead." - ) + raise AttributeError(f"Cannot assign '{name}' directly. Use fieldset.context['{name}'] instead.") # Handle setting of attributes not in context per default super().__setattr__(name, value) diff --git a/tests/test_fieldset.py b/tests/test_fieldset.py index cacb719e9..0c5a18317 100644 --- a/tests/test_fieldset.py +++ b/tests/test_fieldset.py @@ -32,7 +32,7 @@ def test_fieldset_add_context(fieldset): def test_fieldset_add_context_int_name(fieldset): with pytest.raises(TypeError, match="Expected a string for variable name, got int instead."): fieldset.add_context(123, 1.0) - + def test_fieldset_setattr_new(fieldset): fieldset.context = {"new_field": 1.0} From 8ddf817bfafb4a7a24322ef77b072231c2fd2b00 Mon Sep 17 00:00:00 2001 From: "peter.wolfram" Date: Tue, 16 Jun 2026 17:03:23 +0200 Subject: [PATCH 12/13] reverted changes on v3 files --- .../examples_v3/documentation_stuck_particles.ipynb | 2 +- docs/user_guide/examples_v3/example_mitgcm.py | 2 +- docs/user_guide/examples_v3/example_moving_eddies.py | 8 ++++---- docs/user_guide/examples_v3/example_stommel.py | 2 +- .../examples_v3/tutorial_analyticaladvection.ipynb | 4 ++-- .../examples_v3/tutorial_particle_field_interaction.ipynb | 6 +++--- tests-v3/test_fieldset.py | 4 ++-- tests-v3/test_fieldset_sampling.py | 4 ++-- tests-v3/test_kernel_language.py | 2 +- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/user_guide/examples_v3/documentation_stuck_particles.ipynb b/docs/user_guide/examples_v3/documentation_stuck_particles.ipynb index f2a640efc..880835f96 100644 --- a/docs/user_guide/examples_v3/documentation_stuck_particles.ipynb +++ b/docs/user_guide/examples_v3/documentation_stuck_particles.ipynb @@ -1158,7 +1158,7 @@ "\n", "fieldset.add_constant_field(\"Kh_zonal\", 5, mesh=\"spherical\")\n", "fieldset.add_constant_field(\"Kh_meridional\", 5, mesh=\"spherical\")\n", - "# fieldset.add_context('dres', 0.00005)\n", + "# fieldset.add_constant('dres', 0.00005)\n", "fieldset.add_field(\n", " parcels.Field(\n", " \"landmask\",\n", diff --git a/docs/user_guide/examples_v3/example_mitgcm.py b/docs/user_guide/examples_v3/example_mitgcm.py index fbdfeeb42..56d8e2beb 100644 --- a/docs/user_guide/examples_v3/example_mitgcm.py +++ b/docs/user_guide/examples_v3/example_mitgcm.py @@ -23,7 +23,7 @@ def run_mitgcm_zonally_reentrant(path: Path): ) fieldset.add_periodic_halo(zonal=True) - fieldset.add_context("domain_width", 1000000) + fieldset.add_constant("domain_width", 1000000) def periodicBC(particle, fieldset, time): # pragma: no cover if particle.lon < 0: diff --git a/docs/user_guide/examples_v3/example_moving_eddies.py b/docs/user_guide/examples_v3/example_moving_eddies.py index 83cfd075a..348e21b32 100644 --- a/docs/user_guide/examples_v3/example_moving_eddies.py +++ b/docs/user_guide/examples_v3/example_moving_eddies.py @@ -257,10 +257,10 @@ def test_periodic_and_computeTimeChunk_eddies(): filename = str(data_folder / "moving_eddies") fieldset = parcels.FieldSet.from_parcels(filename) - fieldset.add_context("halo_west", fieldset.U.grid.lon[0]) - fieldset.add_context("halo_east", fieldset.U.grid.lon[-1]) - fieldset.add_context("halo_south", fieldset.U.grid.lat[0]) - fieldset.add_context("halo_north", fieldset.U.grid.lat[-1]) + fieldset.add_constant("halo_west", fieldset.U.grid.lon[0]) + fieldset.add_constant("halo_east", fieldset.U.grid.lon[-1]) + fieldset.add_constant("halo_south", fieldset.U.grid.lat[0]) + fieldset.add_constant("halo_north", fieldset.U.grid.lat[-1]) fieldset.add_periodic_halo(zonal=True, meridional=True) pset = parcels.ParticleSet.from_list( fieldset=fieldset, diff --git a/docs/user_guide/examples_v3/example_stommel.py b/docs/user_guide/examples_v3/example_stommel.py index e0dbcee06..068ac5812 100755 --- a/docs/user_guide/examples_v3/example_stommel.py +++ b/docs/user_guide/examples_v3/example_stommel.py @@ -133,7 +133,7 @@ def stommel_example( print(f"Initial particle positions:\n{pset}") maxage = runtime.total_seconds() if maxage is None else maxage - fieldset.add_context("maxage", maxage) + fieldset.add_constant("maxage", maxage) print(f"Stommel: Advecting {npart} particles for {runtime}") parcels.timer.psetinit.stop() parcels.timer.psetrun = parcels.timer.Timer("Pset_run", parent=parcels.timer.pset) diff --git a/docs/user_guide/examples_v3/tutorial_analyticaladvection.ipynb b/docs/user_guide/examples_v3/tutorial_analyticaladvection.ipynb index cc82efb20..cbf14aa22 100644 --- a/docs/user_guide/examples_v3/tutorial_analyticaladvection.ipynb +++ b/docs/user_guide/examples_v3/tutorial_analyticaladvection.ipynb @@ -467,10 +467,10 @@ "metadata": {}, "outputs": [], "source": [ - "fieldsetBJ.add_context(\n", + "fieldsetBJ.add_constant(\n", " \"halo_west\", fieldsetBJ.U.grid.lon[1]\n", ") # TODO v4: Change index back to 0 when periodic interpolation is done\n", - "fieldsetBJ.add_context(\n", + "fieldsetBJ.add_constant(\n", " \"halo_east\", fieldsetBJ.U.grid.lon[-2]\n", ") # TODO v4: Change index back to -1 when periodic interpolation is done\n", "\n", diff --git a/docs/user_guide/examples_v3/tutorial_particle_field_interaction.ipynb b/docs/user_guide/examples_v3/tutorial_particle_field_interaction.ipynb index 991fe2102..333ad40b8 100644 --- a/docs/user_guide/examples_v3/tutorial_particle_field_interaction.ipynb +++ b/docs/user_guide/examples_v3/tutorial_particle_field_interaction.ipynb @@ -127,9 +127,9 @@ "metadata": {}, "outputs": [], "source": [ - "fieldset.add_context(\"a\", 10)\n", - "fieldset.add_context(\"b\", 0.2)\n", - "fieldset.add_context(\"weight\", 0.01)" + "fieldset.add_constant(\"a\", 10)\n", + "fieldset.add_constant(\"b\", 0.2)\n", + "fieldset.add_constant(\"weight\", 0.01)" ] }, { diff --git a/tests-v3/test_fieldset.py b/tests-v3/test_fieldset.py index 49e9c518e..69295cf87 100644 --- a/tests-v3/test_fieldset.py +++ b/tests-v3/test_fieldset.py @@ -206,8 +206,8 @@ def test_fieldset_constant(): fieldset = FieldSet.from_data(data, dimensions) # TODO : Remove from_data westval = -0.2 eastval = 0.3 - fieldset.add_context("movewest", westval) - fieldset.add_context("moveeast", eastval) + fieldset.add_constant("movewest", westval) + fieldset.add_constant("moveeast", eastval) assert fieldset.movewest == westval pset = ParticleSet.from_line(fieldset, size=1, pclass=Particle, start=(0.5, 0.5), finish=(0.5, 0.5)) diff --git a/tests-v3/test_fieldset_sampling.py b/tests-v3/test_fieldset_sampling.py index 1853b2578..291c27b88 100644 --- a/tests-v3/test_fieldset_sampling.py +++ b/tests-v3/test_fieldset_sampling.py @@ -652,7 +652,7 @@ def test_sampling_multigrids_non_vectorfield_from_file(npart, tmpdir): dimensions = {"lon": "nav_lon", "lat": "nav_lat"} fieldset = FieldSet.from_netcdf(files, variables, dimensions, timestamps=timestamps, allow_time_extrapolation=True) - fieldset.add_context("sample_depth", 2.5) + fieldset.add_constant("sample_depth", 2.5) assert fieldset.U.grid is fieldset.V.grid assert fieldset.U.grid is not fieldset.B.grid @@ -693,7 +693,7 @@ def test_sampling_multigrids_non_vectorfield(npart): ) fieldset = FieldSet(U, V) fieldset.add_field(B, "B") - fieldset.add_context("sample_depth", 2.5) + fieldset.add_constant("sample_depth", 2.5) assert fieldset.U.grid is fieldset.V.grid assert fieldset.U.grid is not fieldset.B.grid diff --git a/tests-v3/test_kernel_language.py b/tests-v3/test_kernel_language.py index e7515819f..a28da2c5b 100644 --- a/tests-v3/test_kernel_language.py +++ b/tests-v3/test_kernel_language.py @@ -163,7 +163,7 @@ def test_varname_as_fieldname(): """Tests for error thrown if variable has same name as Field.""" fset = create_fieldset_unit_mesh(mesh="spherical") fset.add_field(Field("speed", 10, lon=0, lat=0)) - fset.add_context("vertical_speed", 0.1) + fset.add_constant("vertical_speed", 0.1) particle = Particle.add_variable("speed") pset = ParticleSet(fset, pclass=particle, lon=0, lat=0) From 90f655bdb9a927d01062452a25555fcffad550e5 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Wed, 17 Jun 2026 08:58:21 +0200 Subject: [PATCH 13/13] Review feedback and update migration guide --- docs/user_guide/v4-migration.md | 1 + src/parcels/_core/fieldset.py | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/user_guide/v4-migration.md b/docs/user_guide/v4-migration.md index 53a974821..484b8dd5e 100644 --- a/docs/user_guide/v4-migration.md +++ b/docs/user_guide/v4-migration.md @@ -22,6 +22,7 @@ Version 4 of Parcels is unreleased at the moment. The information in this migrat ## FieldSet - `interp_method` has to be an Interpolation function, instead of a string. +- `.add_constant` has been renamed to `.add_context` to reflect that this value no longer has to be constant ## Particle diff --git a/src/parcels/_core/fieldset.py b/src/parcels/_core/fieldset.py index a24d51692..3a1aa449c 100644 --- a/src/parcels/_core/fieldset.py +++ b/src/parcels/_core/fieldset.py @@ -79,7 +79,7 @@ def __init__(self, fields: list[Field | VectorField]): self.context: dict[str, float] = {} def __setattr__(self, name, value): - """Set field attribute by name. If context exists and name in context, raise error to prevent overwriting context.""" + """Set field attribute by name. If context exists and name in context, raise error to prevent overwriting context variable.""" context = self.__dict__.get("context") if context is not None and name in context: @@ -88,7 +88,7 @@ def __setattr__(self, name, value): super().__setattr__(name, value) def __getattr__(self, name): - """Get the field by name. If the field is not found, check if it's a context.""" + """Get the field by name. If the field is not found, check if it's a context variable.""" if name in self.fields: return self.fields[name] elif name in self.context: @@ -163,14 +163,14 @@ def add_constant_field(self, name: str, value, mesh: Mesh = "spherical"): self.add_field(Field(name, ds[name], grid, interp_method=XConstantField)) def add_context(self, name, value): - """Add context to the FieldSet. + """Add context variable to the FieldSet. Parameters ---------- name : str - Name of the context + Name of the context variable value : - Value of the context + Value of the context variable """ _assert_str_and_python_varname(name) @@ -178,7 +178,7 @@ def add_context(self, name, value): if name in self.context: raise ValueError(f"FieldSet already has a context with name '{name}'") if not isinstance(value, (float, np.floating, int, np.integer)): - raise ValueError(f"FieldSet contexts have to be of type float or int, got a {type(value)}") + raise ValueError(f"FieldSet context variables have to be of type float or int, got a {type(value)}") self.context[name] = value @property