Skip to content

Commit c9546a3

Browse files
Merge branch 'v4-dev' into adding_circulation_models_datasets
2 parents 92cc994 + 8c01bf0 commit c9546a3

20 files changed

Lines changed: 688 additions & 183 deletions

docs/reference/grids.rst

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
11
Gridsets and grids
22
==================
33

4-
parcels.gridset module
5-
----------------------
6-
7-
.. automodule:: parcels.gridset
8-
:members:
9-
:show-inheritance:
10-
114
parcels.grid module
125
-------------------
136

parcels/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from parcels.field import *
77
from parcels.fieldset import *
88
from parcels.grid import *
9-
from parcels.gridset import *
109
from parcels.interaction import *
1110
from parcels.kernel import *
1211
from parcels.particle import *

parcels/_datasets/unstructured/generic.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ def _stommel_gyre_delaunay():
4444
uxgrid.attrs["Conventions"] = "UGRID-1.0"
4545

4646
# Define arrays U (zonal), V (meridional) and P (sea surface height)
47-
U = np.zeros((1, nz1, lat.size), dtype=np.float64)
48-
V = np.zeros((1, nz1, lat.size), dtype=np.float64)
47+
U = np.zeros((1, nz1, uxgrid.n_face), dtype=np.float64)
48+
V = np.zeros((1, nz1, uxgrid.n_face), dtype=np.float64)
4949
W = np.zeros((1, nz, lat.size), dtype=np.float64)
50-
P = np.zeros((1, nz1, lat.size), dtype=np.float64)
50+
P = np.zeros((1, nz1, uxgrid.n_face), dtype=np.float64)
5151

52-
for i, (x, y) in enumerate(zip(lon_flat, lat_flat, strict=False)):
52+
for i, (x, y) in enumerate(zip(uxgrid.face_lon, uxgrid.face_lat, strict=False)):
5353
xi = x / 60.0
5454
yi = y / 60.0
5555

@@ -61,10 +61,10 @@ def _stommel_gyre_delaunay():
6161
data=U,
6262
name="U",
6363
uxgrid=uxgrid,
64-
dims=["time", "nz1", "n_node"],
64+
dims=["time", "nz1", "n_face"],
6565
coords=dict(
6666
time=(["time"], [TIME[0]]),
67-
nz1=(["nz1"], [0]),
67+
nz1=(["nz1"], zc),
6868
),
6969
attrs=dict(
7070
description="zonal velocity", units="m/s", location="node", mesh="delaunay", Conventions="UGRID-1.0"
@@ -74,7 +74,7 @@ def _stommel_gyre_delaunay():
7474
data=V,
7575
name="V",
7676
uxgrid=uxgrid,
77-
dims=["time", "nz1", "n_node"],
77+
dims=["time", "nz1", "n_face"],
7878
coords=dict(
7979
time=(["time"], [TIME[0]]),
8080
nz1=(["nz1"], zc),
@@ -100,7 +100,7 @@ def _stommel_gyre_delaunay():
100100
data=P,
101101
name="p",
102102
uxgrid=uxgrid,
103-
dims=["time", "nz1", "n_node"],
103+
dims=["time", "nz1", "n_face"],
104104
coords=dict(
105105
time=(["time"], [TIME[0]]),
106106
nz1=(["nz1"], zc),

parcels/_index_search.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
from .grid import GridType
2222

2323
if TYPE_CHECKING:
24+
from parcels.xgrid import XGrid
25+
2426
from .field import Field
2527
# from .grid import Grid
2628

@@ -38,7 +40,7 @@ def _search_time_index(field: Field, time: datetime):
3840
if the sampled value is outside the time value range.
3941
"""
4042
if field.time_interval is None:
41-
return 0
43+
return 0, 0
4244

4345
if time not in field.time_interval:
4446
_raise_time_extrapolation_error(time, field=None)
@@ -270,6 +272,89 @@ def _search_indices_rectilinear(
270272
return (zeta, eta, xsi, _ei)
271273

272274

275+
def _search_indices_curvilinear_2d(
276+
grid: XGrid, y: float, x: float, yi_guess: int | None = None, xi_guess: int | None = None
277+
):
278+
yi, xi = yi_guess, xi_guess
279+
if yi is None:
280+
yi = int(grid.ydim / 2) - 1
281+
282+
if xi is None:
283+
xi = int(grid.xdim / 2) - 1
284+
285+
xsi = eta = -1.0
286+
invA = np.array(
287+
[
288+
[1, 0, 0, 0],
289+
[-1, 1, 0, 0],
290+
[-1, 0, 0, 1],
291+
[1, -1, 1, -1],
292+
]
293+
)
294+
maxIterSearch = 1e6
295+
it = 0
296+
tol = 1.0e-10
297+
298+
# # ! Error handling for out of bounds
299+
# TODO: Re-enable in some capacity
300+
# if x < field.lonlat_minmax[0] or x > field.lonlat_minmax[1]:
301+
# if grid.lon[0, 0] < grid.lon[0, -1]:
302+
# _raise_field_out_of_bound_error(y, x)
303+
# elif x < grid.lon[0, 0] and x > grid.lon[0, -1]: # This prevents from crashing in [160, -160]
304+
# _raise_field_out_of_bound_error(z, y, x)
305+
306+
# if y < field.lonlat_minmax[2] or y > field.lonlat_minmax[3]:
307+
# _raise_field_out_of_bound_error(z, y, x)
308+
309+
while xsi < -tol or xsi > 1 + tol or eta < -tol or eta > 1 + tol:
310+
px = np.array([grid.lon[yi, xi], grid.lon[yi, xi + 1], grid.lon[yi + 1, xi + 1], grid.lon[yi + 1, xi]])
311+
312+
py = np.array([grid.lat[yi, xi], grid.lat[yi, xi + 1], grid.lat[yi + 1, xi + 1], grid.lat[yi + 1, xi]])
313+
a = np.dot(invA, px)
314+
b = np.dot(invA, py)
315+
316+
aa = a[3] * b[2] - a[2] * b[3]
317+
bb = a[3] * b[0] - a[0] * b[3] + a[1] * b[2] - a[2] * b[1] + x * b[3] - y * a[3]
318+
cc = a[1] * b[0] - a[0] * b[1] + x * b[1] - y * a[1]
319+
if abs(aa) < 1e-12: # Rectilinear cell, or quasi
320+
eta = -cc / bb
321+
else:
322+
det2 = bb * bb - 4 * aa * cc
323+
if det2 > 0: # so, if det is nan we keep the xsi, eta from previous iter
324+
det = np.sqrt(det2)
325+
eta = (-bb + det) / (2 * aa)
326+
if abs(a[1] + a[3] * eta) < 1e-12: # this happens when recti cell rotated of 90deg
327+
xsi = ((y - py[0]) / (py[1] - py[0]) + (y - py[3]) / (py[2] - py[3])) * 0.5
328+
else:
329+
xsi = (x - a[0] - a[2] * eta) / (a[1] + a[3] * eta)
330+
if xsi < 0 and eta < 0 and xi == 0 and yi == 0:
331+
_raise_field_out_of_bound_error(0, y, x)
332+
if xsi > 1 and eta > 1 and xi == grid.xdim - 1 and yi == grid.ydim - 1:
333+
_raise_field_out_of_bound_error(0, y, x)
334+
if xsi < -tol:
335+
xi -= 1
336+
elif xsi > 1 + tol:
337+
xi += 1
338+
if eta < -tol:
339+
yi -= 1
340+
elif eta > 1 + tol:
341+
yi += 1
342+
(yi, xi) = _reconnect_bnd_indices(yi, xi, grid.ydim, grid.xdim, grid.mesh)
343+
it += 1
344+
if it > maxIterSearch:
345+
print(f"Correct cell not found after {maxIterSearch} iterations")
346+
_raise_field_out_of_bound_error(0, y, x)
347+
xsi = max(0.0, xsi)
348+
eta = max(0.0, eta)
349+
xsi = min(1.0, xsi)
350+
eta = min(1.0, eta)
351+
352+
if not ((0 <= xsi <= 1) and (0 <= eta <= 1)):
353+
_raise_field_sampling_error(y, x)
354+
355+
return (yi, eta, xi, xsi)
356+
357+
273358
## TODO : Still need to implement the search_indices_curvilinear
274359
def _search_indices_curvilinear(field: Field, time, z, y, x, ti, particle=None, search2D=False):
275360
if particle:

parcels/_reprs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def particleset_repr(pset: ParticleSet) -> str:
6565

6666
def fieldset_repr(fieldset: FieldSet) -> str: # TODO v4: Rework or remove entirely
6767
"""Return a pretty repr for FieldSet"""
68-
fields_repr = "\n".join([repr(f) for f in fieldset.get_fields()])
68+
fields_repr = "\n".join([repr(f) for f in fieldset.fields.values()])
6969

7070
out = f"""<{type(fieldset).__name__}>
7171
fields:

parcels/application_kernels/advection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ def AdvectionAnalytical(particle, fieldset, time): # pragma: no cover
175175
tol = 1e-10
176176
I_s = 10 # number of intermediate time steps
177177
direction = 1.0 if particle.dt > 0 else -1.0
178-
withW = True if "W" in [f.name for f in fieldset.get_fields()] else False
178+
withW = True if "W" in [f.name for f in fieldset.fields.values()] else False
179179
withTime = True if len(fieldset.U.grid.time) > 1 else False
180180
tau, zeta, eta, xsi, ti, zi, yi, xi = fieldset.U._search_indices(
181181
time, particle.depth, particle.lat, particle.lon, particle=particle

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: {"Z": (zi, zeta), "Y": (yi, eta), "X": (xi, xsi)}
42+
- 2D structured grid: {"Y": (yi, eta), "X": (xi, xsi)}
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: {"Z": zi, "Y": yi, "X": xi}
78+
- 2D structured grid: {"Y": yi, "X": xi}
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: {"Z": zi, "Y": yi, "X": xi}
118+
- 2D structured grid: {"Y": yi, "X": xi}
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+
...

0 commit comments

Comments
 (0)