Skip to content

Commit 0932ccc

Browse files
committed
Update xgrid and basegrid search, ravel, unravel API
1 parent 7f390bd commit 0932ccc

3 files changed

Lines changed: 125 additions & 69 deletions

File tree

parcels/basegrid.py

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1+
from __future__ import annotations
2+
13
from abc import ABC, abstractmethod
4+
from typing import TYPE_CHECKING
5+
6+
if TYPE_CHECKING:
7+
import numpy as np
28

39

410
class BaseGrid(ABC):
511
@abstractmethod
6-
def search(self, z: float, y: float, x: float, ei=None, search2D: bool = False):
12+
def search(self, z: float, y: float, x: float, ei=None) -> dict[str, tuple[int, float | np.ndarray]]:
713
"""
814
Perform a spatial (and optionally vertical) search to locate the grid element
915
that contains a given point (x, y, z).
1016
1117
This method delegates to grid-type-specific logic (e.g., structured or unstructured)
12-
to determine the appropriate indices and interpolation coordinates for evaluating a field.
18+
to determine the appropriate indices and barycentric coordinates for evaluating a field.
1319
1420
Parameters
1521
----------
@@ -28,12 +34,21 @@ def search(self, z: float, y: float, x: float, ei=None, search2D: bool = False):
2834
2935
Returns
3036
-------
31-
bcoords : np.ndarray or tuple
32-
Interpolation weights or barycentric coordinates within the containing cell/face.
33-
The interpretation of `bcoords` depends on the grid type.
34-
ei : int
35-
Encoded index of the identified grid cell or face. This value can be cached for
36-
future lookups to accelerate repeated searches.
37+
dict
38+
A dictionary mapping spatial axis names to tuples of (index, barycentric_coordinates).
39+
The returned axes depend on the grid dimensionality and type:
40+
41+
- 3D structured grid: {"X": (xi, xsi), "Y": (yi, eta), "Z": (zi, zeta)}
42+
- 2D structured grid: {"X": (xi, xsi), "Y": (yi, eta)}
43+
- 1D structured grid (depth): {"Z": (zi, zeta)}
44+
- Unstructured grid: {"Z": (zi, zeta), "FACE": (fi, bcoords)}
45+
46+
Where:
47+
- index (int): The cell position of a particle along the given axis
48+
- barycentric_coordinates (float or np.ndarray): The coordinates defining
49+
a particle's position within the grid cell. For structured grids, this
50+
is a single coordinate per axis; for unstructured grids, this can be
51+
an array of coordinates for the face polygon.
3752
3853
Raises
3954
------
@@ -43,3 +58,72 @@ def search(self, z: float, y: float, x: float, ei=None, search2D: bool = False):
4358
Raised if the search method is not implemented for the current grid type.
4459
"""
4560
...
61+
62+
@abstractmethod
63+
def ravel_index(self, axis_indices: dict[str, int]) -> int:
64+
"""
65+
Convert a dictionary of axis indices to a single encoded index (ei).
66+
67+
This method takes the individual indices for each spatial axis and combines them
68+
into a single integer that uniquely identifies a grid cell. This encoded
69+
index can be used for efficient caching and lookup operations.
70+
71+
Parameters
72+
----------
73+
axis_indices : dict[str, int]
74+
A dictionary mapping axis names to their corresponding indices.
75+
The expected keys depend on the grid dimensionality and type:
76+
77+
- 3D structured grid: {"X": xi, "Y": yi, "Z": zi}
78+
- 2D structured grid: {"X": xi, "Y": yi}
79+
- 1D structured grid: {"Z": zi}
80+
- Unstructured grid: {"Z": zi, "FACE": fi}
81+
82+
Returns
83+
-------
84+
int
85+
The encoded index (ei) representing the unique grid cell or face.
86+
87+
Raises
88+
------
89+
KeyError
90+
Raised when required axis keys are missing from axis_indices.
91+
ValueError
92+
Raised when index values are out of bounds for the grid.
93+
NotImplementedError
94+
Raised if the method is not implemented for the current grid type.
95+
"""
96+
...
97+
98+
@abstractmethod
99+
def unravel_index(self, ei: int) -> dict[str, int]:
100+
"""
101+
Convert a single encoded index (ei) back to a dictionary of axis indices.
102+
103+
This method is the inverse of ravel_index, taking an encoded index and
104+
decomposing it back into the individual indices for each spatial axis.
105+
106+
Parameters
107+
----------
108+
ei : int
109+
The encoded index representing a unique grid cell or face.
110+
111+
Returns
112+
-------
113+
dict[str, int]
114+
A dictionary mapping axis names to their corresponding indices.
115+
The returned keys depend on the grid dimensionality and type:
116+
117+
- 3D structured grid: {"X": xi, "Y": yi, "Z": zi}
118+
- 2D structured grid: {"X": xi, "Y": yi}
119+
- 1D structured grid: {"Z": zi}
120+
- Unstructured grid: {"Z": zi, "FACE": fi}
121+
122+
Raises
123+
------
124+
ValueError
125+
Raised when the encoded index is out of bounds or invalid for the grid.
126+
NotImplementedError
127+
Raised if the method is not implemented for the current grid type.
128+
"""
129+
...

parcels/xgrid.py

Lines changed: 24 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@
1010
from parcels.basegrid import BaseGrid
1111
from parcels.tools.converters import TimeConverter
1212

13-
_AXIS_DIRECTION = Literal["X", "Y", "Z", "T"]
14-
_AXIS_POSITION = Literal["center", "left", "right", "inner", "outer"]
15-
_XGCM_AXES = Mapping[_AXIS_DIRECTION, xgcm.Axis]
13+
_XGCM_AXIS_DIRECTION = Literal["X", "Y", "Z", "T"]
14+
_XGCM_AXIS_POSITION = Literal["center", "left", "right", "inner", "outer"]
15+
_AXIS_DIRECTION = Literal["X", "Y", "Z"]
16+
_XGCM_AXES = Mapping[_XGCM_AXIS_DIRECTION, xgcm.Axis]
1617

1718

1819
def get_tracer_dimensionality(axis: xgcm.Axis | None) -> int:
@@ -180,85 +181,57 @@ def _gtype(self):
180181
else:
181182
return GridType.CurvilinearSGrid
182183

183-
def search(self, z, y, x, ei=None, search2D=False):
184+
def search(self, z, y, x, ei=None):
184185
ds = self.xgcm_grid._ds
185186

186-
if search2D:
187-
zi = 0
188-
else:
189-
zi, _ = _search_1d_array(ds.depth.values, z)
187+
zi, zeta = _search_1d_array(ds.depth.values, z)
190188

191189
if ds.lon.ndim == 1:
192190
yi, eta = _search_1d_array(ds.lat.values, y)
193191
xi, xsi = _search_1d_array(ds.lon.values, x)
194-
return np.array([eta, xsi, 1 - eta, 1 - xsi]), self.ravel_index(zi, yi, xi)
192+
return {"X": (xi, xsi), "Y": (yi, eta), "Z": (zi, zeta)}
195193

196194
yi, xi = None, None
197195
if ei is not None:
198-
_, yi, xi = self.unravel_index(ei)
196+
axis_indices = self.unravel_index(ei)
197+
xi = axis_indices.get("X")
198+
yi = axis_indices.get("Y")
199199

200200
if ds.lon.ndim == 2:
201201
eta, xsi, yi, xi = _search_indices_curvilinear_2d(self, y, x, yi, xi)
202202

203-
return np.array([eta, xsi, 1 - eta, 1 - xsi]), self.ravel_index(zi, yi, xi)
203+
return {"X": (xi, xsi), "Y": (yi, eta), "Z": (zi, zeta)}
204204

205205
raise NotImplementedError("Searching in >2D lon/lat arrays is not implemented yet.")
206206

207-
def ravel_index(self, zi, yi, xi):
208-
"""
209-
Converts a z, y, and x index into a single encoded index.
210-
211-
Parameters
212-
----------
213-
zi : int
214-
Vertical index.
215-
yi : int
216-
Latitude index.
217-
xi : int
218-
Longitude index.
219-
220-
Returns
221-
-------
222-
int
223-
Encoded index.
224-
"""
207+
def ravel_index(self, axis_indices: dict[_AXIS_DIRECTION, int]) -> int:
208+
xi = axis_indices.get("X", 0)
209+
yi = axis_indices.get("Y", 0)
210+
zi = axis_indices.get("Z", 0)
225211
return xi + self.xdim * yi + self.xdim * self.ydim * zi
226212

227-
def unravel_index(self, ei):
228-
"""
229-
Converts a single encoded index back into a Z, Y, and X indices.
230-
231-
Parameters
232-
----------
233-
ei : int
234-
Encoded index to be unraveled.
235-
236-
Returns
237-
-------
238-
zi : int
239-
Vertical index.
240-
yi : int
241-
Latitude index.
242-
xi : int
243-
Longitude index.
244-
"""
213+
def unravel_index(self, ei) -> dict[_AXIS_DIRECTION, int]:
245214
zi = ei // (self.xdim * self.ydim)
246215
ei = ei % (self.xdim * self.ydim)
247216

248217
yi = ei // self.xdim
249218
xi = ei % self.xdim
250-
return zi, yi, xi
219+
return {
220+
"X": xi,
221+
"Y": yi,
222+
"Z": zi,
223+
}
251224

252225

253-
def get_axis_from_dim_name(axes: _XGCM_AXES, dim: str) -> _AXIS_DIRECTION | None:
226+
def get_axis_from_dim_name(axes: _XGCM_AXES, dim: str) -> _XGCM_AXIS_DIRECTION | None:
254227
"""For a given dimension name in a grid, returns the direction axis it is on."""
255228
for axis_name, axis in axes.items():
256229
if dim in axis.coords.values():
257230
return axis_name
258231
return None
259232

260233

261-
def get_position_from_dim_name(axes: _XGCM_AXES, dim: str) -> _AXIS_POSITION | None:
234+
def get_position_from_dim_name(axes: _XGCM_AXES, dim: str) -> _XGCM_AXIS_POSITION | None:
262235
"""For a given dimension, returns the position of the variable in the grid."""
263236
for axis in axes.values():
264237
var_to_position = {var: position for position, var in axis.coords.items()}
@@ -287,7 +260,7 @@ def assert_valid_field_array(da: xr.DataArray, axes: _XGCM_AXES):
287260
assert_all_dimensions_correspond_with_axis(da, axes)
288261

289262
dim_to_axis = {dim: get_axis_from_dim_name(axes, dim) for dim in da.dims}
290-
dim_to_axis = cast(dict[Hashable, _AXIS_DIRECTION], dim_to_axis)
263+
dim_to_axis = cast(dict[Hashable, _XGCM_AXIS_DIRECTION], dim_to_axis)
291264

292265
# Assert all dimensions are present
293266
if set(dim_to_axis.values()) != {"T", "Z", "Y", "X"}:

tests/v4/test_xgrid.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,10 @@ def test_xgrid_ravel_unravel_index():
129129
for xi in range(xdim):
130130
for yi in range(ydim):
131131
for zi in range(zdim):
132-
ei = grid.ravel_index(zi, yi, xi)
133-
zi_test, yi_test, xi_test = grid.unravel_index(ei)
134-
assert xi == xi_test, f"Expected xi {xi} but got {xi_test} for ei {ei}"
135-
assert yi == yi_test, f"Expected yi {yi} but got {yi_test} for ei {ei}"
136-
assert zi == zi_test, f"Expected zi {zi} but got {zi_test} for ei {ei}"
132+
axis_indices = {"X": xi, "Y": yi, "Z": zi}
133+
ei = grid.ravel_index(axis_indices)
134+
axis_indices_test = grid.unravel_index(ei)
135+
assert axis_indices_test == axis_indices
137136
encountered_eis.append(ei)
138137

139138
encountered_eis = sorted(encountered_eis)
@@ -156,12 +155,12 @@ def test_xgrid_search_cpoints(ds):
156155

157156
for xi in range(grid.xdim - 1):
158157
for yi in range(grid.ydim - 1):
158+
axis_indices = {"X": xi, "Y": yi, "Z": 0}
159+
159160
lat, lon = lat_array[yi, xi], lon_array[yi, xi]
160-
bcoords, ei = grid.search(0, lat, lon, ei=None, search2D=True)
161-
zi_test, yi_test, xi_test = grid.unravel_index(ei)
162-
assert xi == xi_test
163-
assert yi == yi_test
164-
assert zi_test == 0
161+
axis_indices_bcoords = grid.search(0, lat, lon, ei=None)
162+
axis_indices_test = {k: v[0] for k, v in axis_indices_bcoords.items()}
163+
assert axis_indices == axis_indices_test
165164

166165
# assert np.isclose(bcoords[0], 0.5) #? Should this not be the case with the cell center points?
167166
# assert np.isclose(bcoords[1], 0.5)

0 commit comments

Comments
 (0)