Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
f5eb442
Typing
VeckoTheGecko May 27, 2026
735e1c0
Restructure to introduce models
VeckoTheGecko May 22, 2026
e03ccf7
Refactor from_sgrid_conventions to model
VeckoTheGecko May 22, 2026
f198994
Fix typing
VeckoTheGecko May 22, 2026
aaf6846
Refactor from_ugrid_conventions to model
VeckoTheGecko May 22, 2026
915b0cb
Fix typing
VeckoTheGecko May 22, 2026
7787c86
Add FieldSet.models
VeckoTheGecko May 26, 2026
af1dbf5
Move "time_interval" to model
VeckoTheGecko May 26, 2026
035bd3f
Update Model ABC
VeckoTheGecko May 27, 2026
9e24a14
Update Field init to take model
VeckoTheGecko May 27, 2026
b69402a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 27, 2026
915b0b6
Add XGCM adapter
VeckoTheGecko Jun 1, 2026
9685ddf
Remove xgcm constructors
VeckoTheGecko Jun 1, 2026
23bc2d4
Update _transpose_xfield_data_to_tzyx to work with SGRID metadata
VeckoTheGecko Jun 1, 2026
82f2001
Define SGRID data pre-processing
VeckoTheGecko Jun 1, 2026
f222d4b
Create grid object within StructuredModel
VeckoTheGecko Jun 1, 2026
108d3b2
Allow for time dimension size 1
VeckoTheGecko Jun 1, 2026
065c96d
Disable assert_all_field_dims_have_axis check
VeckoTheGecko Jun 1, 2026
538477d
New interpolator API
VeckoTheGecko Jun 1, 2026
f1799ac
Update interpolators to use new API
VeckoTheGecko Jun 1, 2026
1345f6e
Enable adding of fieldsets
VeckoTheGecko Jun 16, 2026
b87fae4
Add assert_compatible_fieldsets
VeckoTheGecko Jun 16, 2026
13644ea
Fix test suite
VeckoTheGecko Jun 16, 2026
5569031
Define how to set interpolators
VeckoTheGecko Jun 16, 2026
54674c6
Fix test suite
VeckoTheGecko Jun 16, 2026
f2ef7ce
Merge
VeckoTheGecko Jun 17, 2026
18e76d8
Update test suite
VeckoTheGecko Jun 18, 2026
bb8c0a5
Fix test suite
VeckoTheGecko Jun 18, 2026
a55fe97
Enable constant field tests
VeckoTheGecko Jun 18, 2026
0b2b147
Disable reprs
VeckoTheGecko Jun 18, 2026
2eaf144
Refactor constant field logic to use dedicated model
VeckoTheGecko Jun 18, 2026
cc52d2c
Fix constant field logic
VeckoTheGecko Jun 18, 2026
059f5c5
Enable unstructured tests
VeckoTheGecko Jun 18, 2026
d896ad6
Update unstructured grid interpolators
VeckoTheGecko Jun 18, 2026
73077b7
Update unstructured FieldSet ingestion in tests
VeckoTheGecko Jun 18, 2026
941c9b1
Update comments
VeckoTheGecko Jun 18, 2026
ad7eb5a
Fix test_time1D_field
VeckoTheGecko Jun 18, 2026
f278ed9
Merge remote-tracking branch 'upstream/main' into restructure
VeckoTheGecko Jun 23, 2026
da46ba7
Update test after merge with main
VeckoTheGecko Jun 23, 2026
7173dc0
Fixes after merge
VeckoTheGecko Jun 23, 2026
cc45cd1
Review feedback
VeckoTheGecko Jun 23, 2026
863328e
Update ValueError messages
VeckoTheGecko Jun 23, 2026
5646f15
Add TODO
VeckoTheGecko Jun 23, 2026
6057468
Review feedback
VeckoTheGecko Jun 23, 2026
9e4e34c
Remove zero interpolators
VeckoTheGecko Jun 23, 2026
9519503
Fix typing error
VeckoTheGecko Jun 23, 2026
4e02d1f
Merge with upstream/main
VeckoTheGecko Jun 23, 2026
b025d6d
Rename Model to ModelData
VeckoTheGecko Jun 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 48 additions & 104 deletions src/parcels/_core/field.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from __future__ import annotations

import warnings
from collections.abc import Callable, Sequence
from collections.abc import Sequence
from datetime import datetime
from typing import TYPE_CHECKING

import numpy as np
import uxarray as ux
import xarray as xr

from parcels._core.index_search import GRID_SEARCH_ERROR, LEFT_OUT_OF_BOUNDS, RIGHT_OUT_OF_BOUNDS, _search_time_index
from parcels._core.particlesetview import ParticleSetView
Expand All @@ -15,16 +14,14 @@
StatusCode,
)
from parcels._core.utils.string import _assert_str_and_python_varname
from parcels._core.utils.time import TimeInterval
from parcels._core.uxgrid import UxGrid
from parcels._core.xgrid import XGrid, _transpose_xfield_data_to_tzyx, assert_all_field_dims_have_axis
from parcels._python import assert_same_function_signature
from parcels._reprs import field_repr, vectorfield_repr
from parcels._core.xgrid import XGrid
from parcels._typing import VectorType
from parcels.interpolators import (
ZeroInterpolator,
ZeroInterpolator_Vector,
)
from parcels.interpolators._base import ScalarInterpolator, VectorInterpolator

if TYPE_CHECKING:
from parcels._core.model import ModelData


__all__ = ["Field", "VectorField"]

Expand Down Expand Up @@ -86,69 +83,51 @@ class Field:
def __init__(
self,
name: str,
data: xr.DataArray | ux.UxDataArray,
grid: UxGrid | XGrid,
interp_method: Callable,
model: ModelData,
):
if not isinstance(data, (ux.UxDataArray, xr.DataArray)):
raise ValueError(
f"Expected `data` to be a uxarray.UxDataArray or xarray.DataArray object, got {type(data)}."
)
# TODO PR: Enable isinstance check once ModelData is moved to abc.ModelData
# if not isinstance(model, "ModelData"):
# raise ValueError(
# f"Expected `model` to be a parcels ModelData object. Got {type(model)}."
# )

_assert_str_and_python_varname(name)

if not isinstance(grid, (UxGrid, XGrid)):
raise ValueError(f"Expected `grid` to be a parcels UxGrid, or parcels XGrid object, got {type(grid)}.")

_assert_compatible_combination(data, grid)

if isinstance(grid, XGrid):
assert_all_field_dims_have_axis(data, grid.xgcm_grid)
data = _transpose_xfield_data_to_tzyx(data, grid.xgcm_grid)

self.name = name
self.data = data
self.grid = grid

try:
self.time_interval = _get_time_interval(data)
except ValueError as e:
e.add_note(
f"Error getting time interval for field {name!r}. Are you sure that the time dimension on the xarray dataset is stored as timedelta, datetime or cftime datetime objects?"
)
raise e
self.model = model

try:
if isinstance(data, ux.UxDataArray):
_assert_valid_uxdataarray(data)
# TODO: For unstructured grids, validate that `data.uxgrid` is the same as `grid`
else:
pass # TODO v4: Add validation for xr.DataArray objects
except Exception as e:
e.add_note(f"Error validating field {name!r}.")
raise e
self.igrid = -1 # Default the grid index to -1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this choice of -1? Don't we run the same risk of accidentally wrapping negative indices as in #2629?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was already in main.

AFAICT the caching of ei isn't even working (searching for .igrid = gives no meaningful results). I already suspected this was the case, hence why I was mentioning before that we should have explicit tests for this as part of our release roadmap.

For another issue/pr

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.. I'll look into this grid index caching issue today.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome - thanks for picking this up @fluidnumericsJoe !


# Setting the interpolation method dynamically
assert_same_function_signature(interp_method, ref=ZeroInterpolator, context="Interpolation")
self._interp_method = interp_method
@property
def data(self):
return self.model.data[self.name]

self.igrid = -1 # Default the grid index to -1
@property
def grid(self): # TODO PR: Remove in favour of referencing model grid directly
return self.model.grid

if self.data.shape[0] > 1:
if "time" not in self.data.coords:
raise ValueError("Field data is missing a 'time' coordinate.")
@property
def time_interval(self): # TODO PR: Remove in favour of referencing model time_interval directly
return self.model.time_interval

def __repr__(self):
return field_repr(self)
return f"Field(name={self.name}, model={self.model})"

@property
def interp_method(self):
return self._interp_method
try:
return self.model.field_to_interpolator[self.name]
except KeyError as e:
raise AttributeError(
f"{type(self).__name__} doesn't have an interp_method defined for it. Use `.interp_method = ...`"
) from e

@interp_method.setter
def interp_method(self, method: Callable):
assert_same_function_signature(method, ref=ZeroInterpolator, context="Interpolation")
Comment thread
VeckoTheGecko marked this conversation as resolved.
self._interp_method = method
def interp_method(self, value):
# Setting the interpolation method dynamically
if not isinstance(value, ScalarInterpolator):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this distinction between ScalarInterpolators and VectorInterpolators!

raise ValueError(f"interp_method must be a `ScalarInterpolator` object. Got {type(value)=!r}")
self.model.field_to_interpolator[self.name] = value

def _check_velocitysampling(self):
if self.name in ["U", "V", "W"]:
Expand Down Expand Up @@ -193,7 +172,7 @@ def eval(self, time: datetime, z, y, x, particles=None):

particle_positions, grid_positions = _get_positions(self, time, z, y, x, particles, _ei)

value = self._interp_method(particle_positions, grid_positions, self)
value = self.interp_method.interp(particle_positions, grid_positions, self)

_update_particle_states_interp_value(particles, value)

Expand All @@ -219,7 +198,7 @@ def __init__(
U: Field, # noqa: N803
V: Field, # noqa: N803
W: Field | None = None, # noqa: N803
interp_method: Callable | None = None,
interp_method: VectorInterpolator | None = None,
):
if interp_method is None:
raise ValueError("interp_method must be provided for VectorField initialization.")
Expand All @@ -244,19 +223,22 @@ def __init__(
else:
self.vector_type = "2D"

assert_same_function_signature(interp_method, ref=ZeroInterpolator_Vector, context="Interpolation")
if not isinstance(interp_method, VectorInterpolator):
raise ValueError(f"interp_method must be a `VectorInterpolator` object. Got {type(interp_method)=!r}")

self._interp_method = interp_method

def __repr__(self):
return vectorfield_repr(self)
# def __repr__(self):
# return vectorfield_repr(self)

@property
def interp_method(self):
return self._interp_method

@interp_method.setter
def interp_method(self, method: Callable):
assert_same_function_signature(method, ref=ZeroInterpolator_Vector, context="Interpolation")
def interp_method(self, method: VectorInterpolator):
if not isinstance(method, VectorInterpolator):
raise ValueError(f"method must be a `VectorInterpolator` object. Got {type(method)=!r}")
self._interp_method = method

def eval(self, time: datetime, z, y, x, particles=None):
Expand Down Expand Up @@ -295,7 +277,7 @@ def eval(self, time: datetime, z, y, x, particles=None):

particle_positions, grid_positions = _get_positions(self.U, time, z, y, x, particles, _ei)

(u, v, w) = self._interp_method(particle_positions, grid_positions, self)
(u, v, w) = self._interp_method.interp(particle_positions, grid_positions, self)

for vel in (u, v, w):
_update_particle_states_interp_value(particles, vel)
Expand Down Expand Up @@ -375,44 +357,6 @@ def _update_particle_states_interp_value(particles, value):
)


def _assert_valid_uxdataarray(data: ux.UxDataArray):
"""Verifies that all the required attributes are present in the xarray.DataArray or
uxarray.UxDataArray object.
"""
# Validate dimensions
if not ("zf" in data.dims or "zc" in data.dims):
raise ValueError(
"Field is missing a 'zf' or 'zc' dimension in the field's metadata. "
"This attribute is required for xarray.DataArray objects."
)

if "time" not in data.dims:
raise ValueError(
"Field is missing a 'time' dimension in the field's metadata. "
"This attribute is required for xarray.DataArray objects."
)


def _assert_compatible_combination(data: xr.DataArray | ux.UxDataArray, grid: UxGrid | XGrid):
if isinstance(data, ux.UxDataArray):
if not isinstance(grid, UxGrid):
raise ValueError(
f"Incompatible data-grid combination. Data is a uxarray.UxDataArray, expected `grid` to be a UxGrid object, got {type(grid)}."
)
elif isinstance(data, xr.DataArray):
if not isinstance(grid, XGrid):
raise ValueError(
f"Incompatible data-grid combination. Data is a xarray.DataArray, expected `grid` to be a parcels Grid object, got {type(grid)}."
)


def _get_time_interval(data: xr.DataArray | ux.UxDataArray) -> TimeInterval | None:
if data.shape[0] == 1:
return None

return TimeInterval(data.time.values[0], data.time.values[-1])


def _assert_same_time_interval(fields: Sequence[Field]) -> None:
if len(fields) == 0:
return
Expand Down
Loading
Loading