Skip to content

Commit 93a364b

Browse files
committed
Refactorings after review
- Adds a FlatMesh object - Remove assert_valid_mesh
1 parent 9ac08fc commit 93a364b

19 files changed

Lines changed: 106 additions & 97 deletions

src/parcels/_core/basegrid.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import numpy as np
1010

11-
import parcels._typing as ptyping
11+
from parcels._core.mesh import FlatMesh, SphericalMesh
1212
from parcels._core.spatialhash import SpatialHash
1313

1414
if TYPE_CHECKING:
@@ -26,7 +26,7 @@ class BaseGrid(ABC):
2626
"""Base class for parcels.XGrid and parcels.UxGrid defining common methods and properties"""
2727

2828
_spatialhash: SpatialHash | None
29-
_mesh: ptyping.Mesh
29+
_mesh: FlatMesh | SphericalMesh
3030

3131
@abstractmethod
3232
def search(self, z: float, y: float, x: float, ei=None) -> dict[str, tuple[int, float | np.ndarray]]:

src/parcels/_core/fieldset.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def add_field(self, field: Field, name: str | None = None):
152152

153153
self.fields[name] = field
154154

155-
def add_constant_field(self, name: str, value, mesh: ptyping.Mesh = "spherical"):
155+
def add_constant_field(self, name: str, value, mesh: ptyping.TMesh = "spherical"):
156156
"""Wrapper function to add a Field that is constant in space,
157157
useful e.g. when using constant horizontal diffusivity
158158
@@ -249,7 +249,7 @@ def from_ugrid_conventions(
249249
def from_sgrid_conventions(
250250
cls,
251251
ds: xr.Dataset,
252-
mesh: ptyping.Mesh | None = None,
252+
mesh: ptyping.TMesh | None = None,
253253
vector_fields: ptyping.VectorFields | NotSetType = NOTSET,
254254
): # TODO: Update mesh to be discovered from the dataset metadata
255255
"""Create a FieldSet from a dataset using SGRID convention metadata.

src/parcels/_core/index_search.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def curvilinear_point_in_cell(grid, y: np.ndarray, x: np.ndarray, yi: np.ndarray
109109
dtype=float,
110110
)
111111

112-
if grid._mesh == "spherical":
112+
if grid._mesh.is_spherical():
113113
xsi, eta = _bilinear_inverse_tangent_plane(clon, clat, x, y)
114114
is_in_cell = np.where((xsi >= 0) & (xsi <= 1) & (eta >= 0) & (eta <= 1), 1, 0)
115115
else:
@@ -318,7 +318,7 @@ def uxgrid_point_in_cell(grid, y: np.ndarray, x: np.ndarray, yi: np.ndarray, xi:
318318
coords : np.ndarray
319319
Barycentric coordinates of the points within their respective cells.
320320
"""
321-
if grid._mesh == "spherical":
321+
if grid._mesh.is_spherical():
322322
lon_rad = np.deg2rad(x)
323323
lat_rad = np.deg2rad(y)
324324
x_cart, y_cart, z_cart = _latlon_rad_to_xyz(lat_rad, lon_rad)

src/parcels/_core/kernel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def check_fieldsets_in_kernels(self, kernel): # TODO v4: this can go into anoth
141141
stacklevel=2,
142142
)
143143
self.fieldset.add_context("RK45_tol", 10)
144-
if self.fieldset.U.grid._mesh == "spherical":
144+
if self.fieldset.U.grid._mesh.is_spherical():
145145
self.fieldset.RK45_tol /= (
146146
self.fieldset.U.grid.deg2m
147147
) # TODO does not account for zonal variation in meter -> degree conversion

src/parcels/_core/mesh.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1+
from abc import ABC, abstractmethod
2+
from typing import Literal
3+
14
import numpy as np
25

36
EARTH_RADIUS = 6366707.019493707
47

58

6-
class SphericalMesh:
9+
class BaseMesh(ABC):
10+
radius: float | None
11+
12+
@abstractmethod
13+
def is_spherical(self) -> bool: ...
14+
15+
16+
class SphericalMesh(BaseMesh):
717
"""Spherical mesh object with configurable planetary radius.
818
919
Pass to FieldSet object as ``mesh=SphericalMesh(radius=...)``.
@@ -22,5 +32,41 @@ def deg2m(self) -> float:
2232
"""Meters per degree of arc."""
2333
return self.radius * np.pi / 180.0
2434

35+
def is_spherical(self):
36+
return True
37+
2538
def __repr__(self) -> str:
2639
return f"SphericalMesh(radius={self.radius})"
40+
41+
42+
class FlatMesh(BaseMesh):
43+
"""Flat mesh object."""
44+
45+
def __init__(self):
46+
self.radius = None
47+
return
48+
49+
def __repr__(self) -> str:
50+
return "FlatMesh()"
51+
52+
def is_spherical(self):
53+
return False
54+
55+
56+
TMesh = SphericalMesh | Literal["spherical", "flat"] # corresponds with `mesh`
57+
58+
59+
def get_mesh(mesh: TMesh):
60+
if isinstance(mesh, SphericalMesh):
61+
return mesh
62+
if mesh == "flat":
63+
return FlatMesh()
64+
if mesh == "spherical":
65+
return SphericalMesh(EARTH_RADIUS)
66+
raise ValueError(f"mesh must be 'flat', 'spherical', or a SphericalMesh object. Got {mesh=!r}")
67+
68+
69+
def is_spherical(mesh: FlatMesh | SphericalMesh):
70+
if isinstance(mesh, SphericalMesh):
71+
return True
72+
return False

src/parcels/_core/model.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
)
2222
from parcels._logger import logger
2323
from parcels._python import NOTSET, NotSetType
24-
from parcels._typing import Mesh
2524
from parcels.convert import _ds_rename_using_standard_names
2625
from parcels.interpolators import (
2726
CGrid_Velocity,
@@ -83,7 +82,7 @@ def preprocess_sgrid_model_data(ds: xr.Dataset) -> xr.Dataset:
8382

8483

8584
class StructuredModelData(ModelData):
86-
def __init__(self, data: xr.Dataset, mesh: Mesh, vector_field_components: ptyping.VectorFields):
85+
def __init__(self, data: xr.Dataset, mesh: ptyping.TMesh, vector_field_components: ptyping.VectorFields):
8786
if not isinstance(data, xr.Dataset):
8887
raise ValueError(f"Expected `data` to be an xarray.Dataset . Got {type(data)}")
8988

@@ -132,7 +131,7 @@ def construct_fields(self) -> list[Field | VectorField]:
132131

133132
@classmethod
134133
def from_sgrid_conventions(
135-
cls, ds: xr.Dataset, mesh: Mesh | None, vector_fields: ptyping.VectorFields | NotSetType
134+
cls, ds: xr.Dataset, mesh: ptyping.TMesh | None, vector_fields: ptyping.VectorFields | NotSetType
136135
) -> Self:
137136
ds = ds.copy()
138137
if mesh is None:
@@ -279,7 +278,9 @@ def scalar_field_names(self) -> list[str]:
279278
return list(self.data.data_vars)
280279

281280
@classmethod
282-
def from_ugrid_conventions(cls, ds: ux.UxDataset, mesh: Mesh, vector_fields: ptyping.VectorFields | NotSetType):
281+
def from_ugrid_conventions(
282+
cls, ds: ux.UxDataset, mesh: ptyping.TMesh, vector_fields: ptyping.VectorFields | NotSetType
283+
):
283284
ds_dims = list(ds.dims)
284285
if not all(dim in ds_dims for dim in ["time", "zf", "zc"]):
285286
raise ValueError(
@@ -303,7 +304,7 @@ def from_ugrid_conventions(cls, ds: ux.UxDataset, mesh: Mesh, vector_fields: pty
303304

304305

305306
# TODO: Refactor later into something like `parcels._metadata.discover(dataset)` helper that can be used to discover important metadata like this. I think this whole metadata handling should be refactored into its own module.
306-
def _get_mesh_type_from_sgrid_dataset(ds_sgrid: xr.Dataset) -> Mesh:
307+
def _get_mesh_type_from_sgrid_dataset(ds_sgrid: xr.Dataset) -> ptyping.TMesh:
307308
"""Small helper to inspect SGRID metadata and dataset metadata to determine mesh type."""
308309
sgrid_metadata = ds_sgrid.sgrid.metadata
309310

src/parcels/_core/particlefile.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import xarray as xr
1515

1616
import parcels
17+
from parcels._core.mesh import BaseMesh
1718
from parcels._core.particle import ParticleClass
1819
from parcels._core.particlesetview import ParticleSetView
1920
from parcels._core.utils.time import timedelta_to_float
@@ -119,14 +120,14 @@ def __init__(
119120
def __repr__(self) -> str:
120121
return particlefile_repr(self)
121122

122-
def set_metadata(self, parcels_grid_mesh: Literal["spherical", "flat"]):
123+
def set_metadata(self, parcels_grid_mesh: BaseMesh):
123124
self.metadata.update(
124125
{
125126
"feature_type": "trajectory",
126127
"Conventions": "CF-1.6/CF-1.7",
127128
"ncei_template_version": "NCEI_NetCDF_Trajectory_Template_v2.0",
128129
"parcels_version": parcels.__version__,
129-
"parcels_grid_mesh": parcels_grid_mesh,
130+
"parcels_grid_mesh": repr(parcels_grid_mesh),
130131
}
131132
)
132133

src/parcels/_core/spatialhash.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def __init__(
4545

4646
if isinstance_noimport(grid, "XGrid"):
4747
self._coord_dim = 2 # Number of computational coordinates is 2 (bilinear interpolation)
48-
if self._source_grid._mesh == "spherical":
48+
if self._source_grid._mesh.is_spherical():
4949
# Boundaries of the hash grid are the unit cube
5050
self._xmin = -1.0
5151
self._ymin = -1.0
@@ -148,7 +148,7 @@ def __init__(
148148

149149
elif isinstance_noimport(grid, "UxGrid"):
150150
self._coord_dim = grid.uxgrid.n_max_face_nodes # Number of barycentric coordinates
151-
if self._source_grid._mesh == "spherical":
151+
if self._source_grid._mesh.is_spherical():
152152
# Boundaries of the hash grid are the unit cube
153153
self._xmin = -1.0
154154
self._ymin = -1.0
@@ -346,7 +346,7 @@ def query(self, y, x):
346346

347347
y = np.asarray(y)
348348
x = np.asarray(x)
349-
if self._source_grid._mesh == "spherical":
349+
if self._source_grid._mesh.is_spherical():
350350
# Convert coords to Cartesian coordinates (x, y, z)
351351
lat = np.deg2rad(y)
352352
lon = np.deg2rad(x)

src/parcels/_core/utils/interpolation.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import numpy as np
55

6-
from parcels._typing import Mesh
6+
from parcels._core.mesh import BaseMesh
77

88
__all__ = []
99

@@ -61,12 +61,12 @@ def dphidxsi3D_lin(zeta: float, eta: float, xsi: float) -> tuple[list[float], li
6161

6262

6363
def dxdxsi3D_lin(
64-
hexa_z: list[float], hexa_y: list[float], hexa_x: list[float], zeta: float, eta: float, xsi: float, mesh: Mesh,
64+
hexa_z: list[float], hexa_y: list[float], hexa_x: list[float], zeta: float, eta: float, xsi: float, mesh: BaseMesh,
6565
deg2m: float = 1852 * 60.0
6666
) -> tuple[float, float, float, float, float, float, float, float, float]:
6767
dphidxsi, dphideta, dphidzet = dphidxsi3D_lin(zeta, eta, xsi)
6868

69-
if mesh == 'spherical':
69+
if mesh.is_spherical():
7070
rad = np.pi / 180.
7171
lat = (1-xsi) * (1-eta) * hexa_y[0] + \
7272
xsi * (1-eta) * hexa_y[1] + \
@@ -92,7 +92,7 @@ def dxdxsi3D_lin(
9292

9393

9494
def jacobian3D_lin(
95-
hexa_z: list[float], hexa_y: list[float], hexa_x: list[float], zeta: float, eta: float, xsi: float, mesh: Mesh,
95+
hexa_z: list[float], hexa_y: list[float], hexa_x: list[float], zeta: float, eta: float, xsi: float, mesh: BaseMesh,
9696
deg2m: float = 1852 * 60.0
9797
) -> float:
9898
dxdxsi, dxdeta, dxdzet, dydxsi, dydeta, dydzet, dzdxsi, dzdeta, dzdzet = dxdxsi3D_lin(hexa_z, hexa_y, hexa_x, zeta, eta, xsi, mesh, deg2m)
@@ -113,7 +113,7 @@ def jacobian3D_lin_face(
113113
eta: float,
114114
xsi: float,
115115
orientation: Literal["zonal", "meridional", "vertical"],
116-
mesh: Mesh,
116+
mesh: BaseMesh,
117117
) -> float:
118118
dxdxsi, dxdeta, dxdzet, dydxsi, dydeta, dydzet, dzdxsi, dzdeta, dzdzet = dxdxsi3D_lin(hexa_z, hexa_y, hexa_x, zeta, eta, xsi, mesh)
119119

@@ -176,9 +176,9 @@ def interpolate(phi: Callable[[float], list[float]], f: list[float], xsi: float)
176176

177177

178178
def _geodetic_distance(
179-
lat1: float, lat2: float, lon1: float, lon2: float, mesh: Mesh, lat: float, deg2m: float = 1852 * 60.0
179+
lat1: float, lat2: float, lon1: float, lon2: float, mesh: BaseMesh, lat: float, deg2m: float = 1852 * 60.0
180180
) -> float:
181-
if mesh == "spherical":
181+
if mesh.is_spherical():
182182
rad = np.pi / 180.0
183183
return np.sqrt(((lon2 - lon1) * deg2m * np.cos(rad * lat)) ** 2 + ((lat2 - lat1) * deg2m) ** 2)
184184
else:

src/parcels/_core/uxgrid.py

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77

88
from parcels._core.basegrid import BaseGrid
99
from parcels._core.index_search import GRID_SEARCH_ERROR, _search_1d_array, uxgrid_point_in_cell
10-
from parcels._core.mesh import EARTH_RADIUS, SphericalMesh
11-
from parcels._typing import assert_valid_mesh
10+
from parcels._core.mesh import SphericalMesh, get_mesh
1211

1312
_UXGRID_AXES = Literal["Z", "FACE"]
1413

@@ -19,7 +18,9 @@ class UxGrid(BaseGrid):
1918
for interpolation on unstructured grids.
2019
"""
2120

22-
def __init__(self, grid: ux.grid.Grid, z: ux.UxDataArray, mesh) -> None:
21+
def __init__(
22+
self, grid: ux.grid.Grid, z: ux.UxDataArray, mesh: Literal["flat", "spherical"] | SphericalMesh
23+
) -> None:
2324
"""
2425
Initializes the UxGrid with a uxarray grid and vertical coordinate array.
2526
@@ -42,16 +43,9 @@ def __init__(self, grid: ux.grid.Grid, z: ux.UxDataArray, mesh) -> None:
4243
if z.ndim != 1:
4344
raise ValueError("z must be a 1D array of vertical coordinates")
4445
self.z = z
45-
if isinstance(mesh, SphericalMesh):
46-
self._mesh = "spherical"
47-
self._radius = mesh.radius
48-
else:
49-
self._mesh = mesh
50-
self._radius = EARTH_RADIUS if mesh == "spherical" else None
46+
self._mesh = get_mesh(mesh)
5147
self._spatialhash = None
5248

53-
assert_valid_mesh(mesh)
54-
5549
@property
5650
def depth(self):
5751
"""
@@ -82,9 +76,9 @@ def get_axis_dim(self, axis: _UXGRID_AXES) -> int:
8276
@property
8377
def deg2m(self) -> float:
8478
"""Metres per arcdegree for this grid's mesh."""
85-
if self._radius is None: # flat mesh; None causes crash in advection
79+
if not self._mesh.is_spherical():
8680
return 1.0
87-
return self._radius * np.pi / 180.0
81+
return self._mesh.deg2m
8882

8983
def search(self, z, y, x, ei=None, tol=1e-6):
9084
"""

0 commit comments

Comments
 (0)