Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
138 changes: 54 additions & 84 deletions parcels/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,45 @@
return 0


def ZeroInterpolator(
field: Field,
ti: int,
position: dict[str, tuple[int, float | np.ndarray]],
tau: np.float32 | np.float64,
t: np.float32 | np.float64,
z: np.float32 | np.float64,
y: np.float32 | np.float64,
x: np.float32 | np.float64,
) -> np.float32 | np.float64:
"""Template function used for the signature check of the lateral interpolation methods."""
return 0.0

Check warning on line 73 in parcels/field.py

View check run for this annotation

Codecov / codecov/patch

parcels/field.py#L73

Added line #L73 was not covered by tests


_DEFAULT_INTERPOLATOR_MAPPING = {
XGrid: ZeroInterpolator, # TODO v4: Update these to better defaults
UxGrid: ZeroInterpolator,
}


def _assert_same_function_signature(f: Callable, *, ref: Callable) -> None:
"""Ensures a function `f` has the same signature as the reference function `ref`."""
sig_ref = inspect.signature(ref)
sig = inspect.signature(f)

if len(sig_ref.parameters) != len(sig.parameters):
raise ValueError(

Check warning on line 88 in parcels/field.py

View check run for this annotation

Codecov / codecov/patch

parcels/field.py#L88

Added line #L88 was not covered by tests
f"Interpolation function must have {len(sig_ref.parameters)} parameters, got {len(sig.parameters)}"
)

for (_name1, param1), (_name2, param2) in zip(sig_ref.parameters.items(), sig.parameters.items(), strict=False):
if param1.kind != param2.kind:
raise ValueError(

Check warning on line 94 in parcels/field.py

View check run for this annotation

Codecov / codecov/patch

parcels/field.py#L94

Added line #L94 was not covered by tests
f"Parameter '{_name2}' has incorrect parameter kind. Expected {param1.kind}, got {param2.kind}"
)
if param1.name != param2.name:
raise ValueError(f"Parameter '{_name2}' has incorrect name. Expected '{param1.name}', got '{param2.name}'")


class Field:
"""The Field class that holds scalar field data.
The `Field` object is a wrapper around a xarray.DataArray or uxarray.UxDataArray object.
Expand Down Expand Up @@ -93,44 +132,6 @@

"""

@staticmethod
def _interp_template(
self,
ti: int,
position: dict[str, tuple[int, float | np.ndarray]],
tau: np.float32 | np.float64,
t: np.float32 | np.float64,
z: np.float32 | np.float64,
y: np.float32 | np.float64,
x: np.float32 | np.float64,
) -> np.float32 | np.float64:
"""Template function used for the signature check of the lateral interpolation methods."""
return 0.0

def _validate_interp_function(self, func: Callable) -> bool:
"""Ensures that the function has the correct signature."""
template_sig = inspect.signature(self._interp_template)
func_sig = inspect.signature(func)

if len(template_sig.parameters) != len(func_sig.parameters):
return False

for (_name1, param1), (_name2, param2) in zip(
template_sig.parameters.items(), func_sig.parameters.items(), strict=False
):
if param1.kind != param2.kind:
return False
if param1.annotation != param2.annotation:
return False

return_annotation = func_sig.return_annotation
template_return = template_sig.return_annotation

if return_annotation != template_return:
return False

return True

def __init__(
self,
name: str,
Expand Down Expand Up @@ -178,9 +179,9 @@

# Setting the interpolation method dynamically
if interp_method is None:
self._interp_method = self._interp_template # Default to method that returns 0 always
self._interp_method = _DEFAULT_INTERPOLATOR_MAPPING[type(self.grid)]
else:
self._validate_interp_function(interp_method)
_assert_same_function_signature(interp_method, ref=ZeroInterpolator)
self._interp_method = interp_method

self.igrid = -1 # Default the grid index to -1
Expand Down Expand Up @@ -272,7 +273,7 @@

@interp_method.setter
def interp_method(self, method: Callable):
self._validate_interp_function(method)
_assert_same_function_signature(method, ref=ZeroInterpolator)
self._interp_method = method

def _check_velocitysampling(self):
Expand All @@ -283,16 +284,6 @@
stacklevel=2,
)

def __getitem__(self, key):
self._check_velocitysampling()
try:
if _isParticle(key):
return self.eval(key.time, key.depth, key.lat, key.lon, key)
else:
return self.eval(*key)
except tuple(AllParcelsErrorCodes.keys()) as error:
return _deal_with_errors(error, key, vector_type=None)

def eval(self, time: datetime, z, y, x, particle=None, applyConversion=True):
"""Interpolate field values in space and time.

Expand Down Expand Up @@ -332,44 +323,23 @@
def __getattr__(self, key: str):
return getattr(self.data, key)

def __getitem__(self, key):
self._check_velocitysampling()
try:
if _isParticle(key):
return self.eval(key.time, key.depth, key.lat, key.lon, key)

Check warning on line 330 in parcels/field.py

View check run for this annotation

Codecov / codecov/patch

parcels/field.py#L327-L330

Added lines #L327 - L330 were not covered by tests
else:
return self.eval(*key)
except tuple(AllParcelsErrorCodes.keys()) as error:
return _deal_with_errors(error, key, vector_type=None)

Check warning on line 334 in parcels/field.py

View check run for this annotation

Codecov / codecov/patch

parcels/field.py#L332-L334

Added lines #L332 - L334 were not covered by tests

def __contains__(self, key: str):
return key in self.data


class VectorField:
"""VectorField class that holds vector field data needed to execute particles."""

@staticmethod
def _vector_interp_template(
self,
ti: int,
ei: int,
bcoords: np.ndarray,
t: np.float32 | np.float64,
z: np.float32 | np.float64,
y: np.float32 | np.float64,
x: np.float32 | np.float64,
) -> np.float32 | np.float64:
"""Template function used for the signature check of the lateral interpolation methods."""
return 0.0

def _validate_vector_interp_function(self, func: Callable):
"""Ensures that the function has the correct signature."""
expected_params = ["ti", "ei", "bcoords", "t", "z", "y", "x"]
expected_return_types = (np.float32, np.float64)

sig = inspect.signature(func)
params = list(sig.parameters.keys())

# Check the parameter names and count
if params != expected_params:
raise TypeError(f"Function must have parameters {expected_params}, but got {params}")

# Check return annotation if present
return_annotation = sig.return_annotation
if return_annotation not in (inspect.Signature.empty, *expected_return_types):
raise TypeError(f"Function must return a float, but got {return_annotation}")

def __init__(
self, name: str, U: Field, V: Field, W: Field | None = None, vector_interp_method: Callable | None = None
):
Expand All @@ -395,7 +365,7 @@
if vector_interp_method is None:
self._vector_interp_method = None
else:
self._validate_vector_interp_function(vector_interp_method)
_assert_same_function_signature(vector_interp_method, ref=ZeroInterpolator)
self._interp_method = vector_interp_method

def __repr__(self):
Expand All @@ -411,7 +381,7 @@

@vector_interp_method.setter
def vector_interp_method(self, method: Callable):
self._validate_vector_interp_function(method)
_assert_same_function_signature(method, ref=ZeroInterpolator)

Check warning on line 384 in parcels/field.py

View check run for this annotation

Codecov / codecov/patch

parcels/field.py#L384

Added line #L384 was not covered by tests

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.

A Vector interpolation method needs to be able to accept a VectorField in the first argument. The ZeroInterpolator expects a Field currently. I don't think this will work here.

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.

_assert_same_function_signature doesn't check the typing anymore during runtime due to #2076 . I don't think there is a way to reliably do this in Python - since type annotations were bolted on after the fact

self._vector_interp_method = method

# @staticmethod
Expand Down
32 changes: 31 additions & 1 deletion tests/v4/test_field.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from __future__ import annotations

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

from parcels import Field, UXPiecewiseConstantFace, UXPiecewiseLinearNode, xgcm
from parcels import Field, UXPiecewiseConstantFace, UXPiecewiseLinearNode, VectorField, xgcm
from parcels._datasets.structured.generic import T as T_structured
from parcels._datasets.structured.generic import datasets as datasets_structured
from parcels._datasets.unstructured.generic import datasets as datasets_unstructured
Expand Down Expand Up @@ -115,6 +117,34 @@
...


def test_field_invalid_interpolator():
ds = datasets_structured["ds_2d_left"]
grid = XGrid(xgcm.Grid(ds))

def invalid_interpolator_wrong_signature(self, ti, position, tau, t, z, y, invalid):
return 0.0

Check warning on line 125 in tests/v4/test_field.py

View check run for this annotation

Codecov / codecov/patch

tests/v4/test_field.py#L125

Added line #L125 was not covered by tests

# Test invalid interpolator with wrong signature
with pytest.raises(ValueError, match=".*incorrect name.*"):
Field(name="test", data=ds["data_g"], grid=grid, interp_method=invalid_interpolator_wrong_signature)


def test_vectorfield_invalid_interpolator():
ds = datasets_structured["ds_2d_left"]
grid = XGrid(xgcm.Grid(ds))

def invalid_interpolator_wrong_signature(self, ti, position, tau, t, z, y, invalid):
return 0.0

Check warning on line 137 in tests/v4/test_field.py

View check run for this annotation

Codecov / codecov/patch

tests/v4/test_field.py#L137

Added line #L137 was not covered by tests

# Create component fields
U = Field(name="U", data=ds["data_g"], grid=grid)
V = Field(name="V", data=ds["data_g"], grid=grid)

# Test invalid interpolator with wrong signature
with pytest.raises(ValueError, match=".*incorrect name.*"):
VectorField(name="UV", U=U, V=V, vector_interp_method=invalid_interpolator_wrong_signature)


def test_field_unstructured_z_linear():
"""Tests correctness of piecewise constant and piecewise linear interpolation methods on an unstructured grid with a vertical coordinate.
The example dataset is a FESOM2 square Delaunay grid with uniform z-coordinate. Cell centered and layer registered data are defined to be
Expand Down
Loading