From d705094d5bbfacf3a50f822c62f2016c6118ad7a Mon Sep 17 00:00:00 2001 From: Wyatt Sieminski Date: Wed, 28 Oct 2026 18:59:58 +0000 Subject: [PATCH 1/3] move field.igrid initialization to reconstruct fields --- src/parcels/_core/field.py | 24 +++++++++++++++++++--- src/parcels/_core/fieldset.py | 3 +++ tests/test_particleset_execute.py | 34 +++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/parcels/_core/field.py b/src/parcels/_core/field.py index b629ea624..3c3b74eb2 100644 --- a/src/parcels/_core/field.py +++ b/src/parcels/_core/field.py @@ -98,8 +98,6 @@ def __init__( self.name = name self.model = model - self.igrid = -1 # Default the grid index to -1 - @property def data(self): return self.model.field_data(self.name) @@ -108,6 +106,23 @@ def data(self): def grid(self): # TODO PR: Remove in favour of referencing model grid directly return self.model.grid + @property + def igrid(self): + try: + return self._igrid + except AttributeError as e: + raise AttributeError( + f"{type(self).__name__} doesn't have an igrid defined for it. Use `.igrid = ...`" + ) from e + + @igrid.setter + def igrid(self, value): + if value < 0: + raise ValueError( + f"igrid of Field {self.name!r} must be a non-negative index into FieldSet.gridset, got {value}. " + ) + self._igrid = value + @property def time_interval(self): # TODO PR: Remove in favour of referencing model time_interval directly if "time" not in self.model.data[self.name].dims: @@ -215,7 +230,6 @@ def __init__( self.V = V self.W = W self.grid = U.grid - self.igrid = U.igrid if W is None: _assert_same_time_interval((U, V)) @@ -247,6 +261,10 @@ def interp_method(self, method: VectorInterpolator): raise ValueError(f"method must be a `VectorInterpolator` object. Got {type(method)=!r}") self._interp_method = method + @property + def igrid(self): + return self.U.igrid + def eval(self, t: datetime, z, y, x, particles=None): """Interpolate vectorfield values in space and time. diff --git a/src/parcels/_core/fieldset.py b/src/parcels/_core/fieldset.py index 9355c145e..def4f8be8 100644 --- a/src/parcels/_core/fieldset.py +++ b/src/parcels/_core/fieldset.py @@ -106,6 +106,9 @@ def reconstruct_fields(self): if self.constant_model is not None: fields += self.constant_model.construct_fields() self._fields = {f.name: f for f in fields} + for field in self._fields.values(): + if isinstance(field, Field): + field.igrid = self.gridset.index(field.grid) def __getattr__(self, name): """Get the field by name. If the field is not found, check if it's a context variable.""" diff --git a/tests/test_particleset_execute.py b/tests/test_particleset_execute.py index 9343307a8..87223a7b1 100644 --- a/tests/test_particleset_execute.py +++ b/tests/test_particleset_execute.py @@ -16,6 +16,7 @@ Variable, ) from parcels._core.mesh import FlatMesh +from parcels._core.spatialhash import SpatialHash from parcels._core.statuscodes import GridSearchingError from parcels._core.utils.time import timedelta_to_float from parcels._datasets.structured.generated import simple_UV_dataset @@ -193,6 +194,39 @@ def SampleU(particles, fieldset): # pragma: no cover pset.execute(SampleU, runtime=np.timedelta64(1, "D"), dt=np.timedelta64(1, "D")) +def test_ei_cache_skips_spatial_hash_with_two_grids(monkeypatch): + """Each grid keeps its own cached ei, so stationary particles only go through the spatial hash on the first step.""" + n_hashed = [] + query = SpatialHash.query + + def counting_query(self, y, x): + n_hashed.append(len(y)) + return query(self, y, x) + + monkeypatch.setattr(SpatialHash, "query", counting_query) + + ds_curvilinear = datasets_structured["2d_left_rotated"][["data_g", "grid"]].rename({"data_g": "curvilinear"}) + ds_rectilinear = datasets_structured["ds_2d_left"][["data_g", "grid"]].rename({"data_g": "rectilinear"}) + fieldset = FieldSet.from_sgrid_conventions(ds_curvilinear, mesh=FlatMesh()) + FieldSet.from_sgrid_conventions( + ds_rectilinear, mesh=FlatMesh() + ) + + def SampleBothGrids(particles, fieldset): # pragma: no cover + _ = fieldset.curvilinear[particles] + _ = fieldset.rectilinear[particles] + + npart = 50 + rng = np.random.default_rng(0) + pset = ParticleSet(fieldset, x=rng.uniform(1, 5, npart), y=rng.uniform(1, 5, npart), z=rng.uniform(1, 80, npart)) + dt = np.timedelta64(1, "D") + pset.execute(SampleBothGrids, runtime=dt, dt=dt) + assert sum(n_hashed) == npart + + n_hashed.clear() + pset.execute(SampleBothGrids, runtime=4 * dt, dt=dt) + assert sum(n_hashed) == 0 + + def test_particleset_run_to_endtime(fieldset): starttime = fieldset.time_interval.left endtime = fieldset.time_interval.right From a03279418ef1fa0313abbdd16403ac717a28624b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:13:39 +0000 Subject: [PATCH 2/3] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/parcels/_core/field.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parcels/_core/field.py b/src/parcels/_core/field.py index 3c3b74eb2..9d9d023d5 100644 --- a/src/parcels/_core/field.py +++ b/src/parcels/_core/field.py @@ -119,8 +119,8 @@ def igrid(self): def igrid(self, value): if value < 0: raise ValueError( - f"igrid of Field {self.name!r} must be a non-negative index into FieldSet.gridset, got {value}. " - ) + f"igrid of Field {self.name!r} must be a non-negative index into FieldSet.gridset, got {value}. " + ) self._igrid = value @property From 980ddf4e2fa0914be69a980273217d0c1445af60 Mon Sep 17 00:00:00 2001 From: Wyatt Sieminski Date: Thu, 29 Oct 2026 15:43:17 +0000 Subject: [PATCH 3/3] Remove overly defensive error messages --- src/parcels/_core/field.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/parcels/_core/field.py b/src/parcels/_core/field.py index 9d9d023d5..2ab059b28 100644 --- a/src/parcels/_core/field.py +++ b/src/parcels/_core/field.py @@ -108,19 +108,10 @@ def grid(self): # TODO PR: Remove in favour of referencing model grid directly @property def igrid(self): - try: - return self._igrid - except AttributeError as e: - raise AttributeError( - f"{type(self).__name__} doesn't have an igrid defined for it. Use `.igrid = ...`" - ) from e + return self._igrid @igrid.setter def igrid(self, value): - if value < 0: - raise ValueError( - f"igrid of Field {self.name!r} must be a non-negative index into FieldSet.gridset, got {value}. " - ) self._igrid = value @property