Skip to content

Commit ced2559

Browse files
Merge branch 'v4-dev' into removing_explicit_field_chunking
2 parents bfb1f62 + 4d31dbb commit ced2559

File tree

10 files changed

+58
-157
lines changed

10 files changed

+58
-157
lines changed

docs/examples/tutorial_diffusion.ipynb

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@
118118
"import trajan as ta\n",
119119
"import xarray as xr\n",
120120
"\n",
121-
"import parcels"
121+
"import parcels\n",
122+
"from parcels.grid import GridType\n",
123+
"from parcels.tools.converters import Geographic, GeographicPolar, UnitConverter"
122124
]
123125
},
124126
{
@@ -471,8 +473,40 @@
471473
"x = fieldset.U.grid.lon\n",
472474
"y = fieldset.U.grid.lat\n",
473475
"\n",
476+
"\n",
477+
"def calc_cell_edge_sizes(grid):\n",
478+
" \"\"\"Calculate cell sizes based on numpy.gradient method.\n",
479+
"\n",
480+
" Currently only works for Rectilinear Grids. Operates in place adding a `cell_edge_sizes`\n",
481+
" attribute to the grid.\n",
482+
" \"\"\"\n",
483+
" assert grid._gtype in (GridType.RectilinearZGrid, GridType.RectilinearSGrid), (\n",
484+
" f\"_cell_edge_sizes() not implemented for {grid._gtype} grids. \"\n",
485+
" \"You can provide cell_edge_sizes yourself by in, e.g., \"\n",
486+
" \"NEMO using the e1u fields etc from the mesh_mask.nc file.\"\n",
487+
" )\n",
488+
"\n",
489+
" cell_edge_sizes_x = np.zeros((grid.ydim, grid.xdim), dtype=np.float32)\n",
490+
" cell_edge_sizes_y = np.zeros((grid.ydim, grid.xdim), dtype=np.float32)\n",
491+
"\n",
492+
" x_conv = GeographicPolar() if grid.mesh == \"spherical\" else UnitConverter()\n",
493+
" y_conv = Geographic() if grid.mesh == \"spherical\" else UnitConverter()\n",
494+
" for y, (lat, dy) in enumerate(zip(grid.lat, np.gradient(grid.lat), strict=False)):\n",
495+
" for x, (lon, dx) in enumerate(\n",
496+
" zip(grid.lon, np.gradient(grid.lon), strict=False)\n",
497+
" ):\n",
498+
" cell_edge_sizes_x[y, x] = x_conv.to_source(dx, grid.depth[0], lat, lon)\n",
499+
" cell_edge_sizes_y[y, x] = y_conv.to_source(dy, grid.depth[0], lat, lon)\n",
500+
" return cell_edge_sizes_x, cell_edge_sizes_y\n",
501+
"\n",
502+
"\n",
503+
"def calc_cell_areas(grid):\n",
504+
" cell_edge_sizes_x, cell_edge_sizes_y = calc_cell_edge_sizes(grid)\n",
505+
" return cell_edge_sizes_x * cell_edge_sizes_y\n",
506+
"\n",
507+
"\n",
474508
"cell_areas = parcels.Field(\n",
475-
" name=\"cell_areas\", data=fieldset.U.cell_areas(), lon=x, lat=y\n",
509+
" name=\"cell_areas\", data=calc_cell_areas(fieldset.U.grid), lon=x, lat=y\n",
476510
")\n",
477511
"fieldset.add_field(cell_areas)\n",
478512
"\n",
@@ -580,7 +614,7 @@
580614
],
581615
"metadata": {
582616
"kernelspec": {
583-
"display_name": "parcels",
617+
"display_name": "parcels-dev",
584618
"language": "python",
585619
"name": "python3"
586620
},
@@ -594,7 +628,7 @@
594628
"name": "python",
595629
"nbconvert_exporter": "python",
596630
"pygments_lexer": "ipython3",
597-
"version": "3.12.3"
631+
"version": "3.10.15"
598632
}
599633
},
600634
"nbformat": 4,

parcels/_index_search.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def search_indices_vertical_s(
137137

138138

139139
def _search_indices_rectilinear(
140-
field: Field, time: float, z: float, y: float, x: float, ti=-1, particle=None, search2D=False
140+
field: Field, time: float, z: float, y: float, x: float, ti: int, particle=None, search2D=False
141141
):
142142
grid = field.grid
143143

@@ -223,7 +223,7 @@ def _search_indices_rectilinear(
223223
return (zeta, eta, xsi, zi, yi, xi)
224224

225225

226-
def _search_indices_curvilinear(field: Field, time, z, y, x, ti=-1, particle=None, search2D=False):
226+
def _search_indices_curvilinear(field: Field, time, z, y, x, ti, particle=None, search2D=False):
227227
if particle:
228228
zi, yi, xi = field.unravel_index(particle.ei)
229229
else:

parcels/application_kernels/advection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ def AdvectionAnalytical(particle, fieldset, time): # pragma: no cover
185185
ds_t = min(ds_t, time_i[np.where(time - fieldset.U.grid.time[ti] < time_i)[0][0]])
186186

187187
zeta, eta, xsi, zi, yi, xi = fieldset.U._search_indices(
188-
-1, particle.depth, particle.lat, particle.lon, particle=particle
188+
time, particle.depth, particle.lat, particle.lon, ti, particle=particle
189189
)
190190
if withW:
191191
if abs(xsi - 1) < tol:

parcels/field.py

Lines changed: 12 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
DeferredNetcdfFileBuffer,
4747
NetcdfFileBuffer,
4848
)
49-
from .grid import Grid, GridType, _calc_cell_areas
49+
from .grid import Grid, GridType
5050

5151
if TYPE_CHECKING:
5252
from parcels.fieldset import FieldSet
@@ -325,10 +325,6 @@ def depth(self):
325325
"""Depth defined on the Grid object"""
326326
return self.grid.depth
327327

328-
@property
329-
def cell_edge_sizes(self):
330-
return self.grid.cell_edge_sizes
331-
332328
@property
333329
def interp_method(self):
334330
return self._interp_method
@@ -837,22 +833,15 @@ def set_depth_from_field(self, field):
837833
if self.grid != field.grid:
838834
field.grid.depth_field = field
839835

840-
def cell_areas(self):
841-
"""Method to calculate cell sizes based on cell_edge_sizes.
842-
843-
Only works for Rectilinear Grids
844-
"""
845-
return _calc_cell_areas(self.grid)
846-
847-
def _search_indices(self, time, z, y, x, ti=-1, particle=None, search2D=False):
836+
def _search_indices(self, time, z, y, x, ti, particle=None, search2D=False):
848837
if self.grid._gtype in [GridType.RectilinearSGrid, GridType.RectilinearZGrid]:
849838
return _search_indices_rectilinear(self, time, z, y, x, ti, particle=particle, search2D=search2D)
850839
else:
851840
return _search_indices_curvilinear(self, time, z, y, x, ti, particle=particle, search2D=search2D)
852841

853-
def _interpolator2D(self, ti, z, y, x, particle=None):
842+
def _interpolator2D(self, time, z, y, x, ti, particle=None):
854843
"""Impelement 2D interpolation with coordinate transformations as seen in Delandmeter and Van Sebille (2019), 10.5194/gmd-12-3571-2019.."""
855-
(_, eta, xsi, _, yi, xi) = self._search_indices(-1, z, y, x, particle=particle)
844+
(_, eta, xsi, _, yi, xi) = self._search_indices(time, z, y, x, ti, particle=particle)
856845
ctx = InterpolationContext2D(self.data, eta, xsi, ti, yi, xi)
857846

858847
try:
@@ -866,7 +855,7 @@ def _interpolator2D(self, ti, z, y, x, particle=None):
866855
raise RuntimeError(self.interp_method + " is not implemented for 2D grids")
867856
return f(ctx)
868857

869-
def _interpolator3D(self, ti, z, y, x, time, particle=None):
858+
def _interpolator3D(self, time, z, y, x, ti, particle=None):
870859
"""Impelement 3D interpolation with coordinate transformations as seen in Delandmeter and Van Sebille (2019), 10.5194/gmd-12-3571-2019.."""
871860
(zeta, eta, xsi, zi, yi, xi) = self._search_indices(time, z, y, x, ti, particle=particle)
872861
ctx = InterpolationContext3D(self.data, zeta, eta, xsi, ti, zi, yi, xi, self.gridindexingtype)
@@ -877,34 +866,13 @@ def _interpolator3D(self, ti, z, y, x, time, particle=None):
877866
raise RuntimeError(self.interp_method + " is not implemented for 3D grids")
878867
return f(ctx)
879868

880-
def temporal_interpolate_fullfield(self, ti, time):
881-
"""Calculate the data of a field between two snapshots using linear interpolation.
882-
883-
Parameters
884-
----------
885-
ti :
886-
Index in time array associated with time (via :func:`time_index`)
887-
time :
888-
Time to interpolate to
889-
"""
890-
t0 = self.grid.time[ti]
891-
if time == t0:
892-
return self.data[ti, :]
893-
elif ti + 1 >= len(self.grid.time):
894-
raise TimeExtrapolationError(time, field=self)
895-
else:
896-
t1 = self.grid.time[ti + 1]
897-
f0 = self.data[ti, :]
898-
f1 = self.data[ti + 1, :]
899-
return f0 + (f1 - f0) * ((time - t0) / (t1 - t0))
900-
901-
def _spatial_interpolation(self, ti, z, y, x, time, particle=None):
902-
"""Interpolate horizontal field values using a SciPy interpolator."""
869+
def _spatial_interpolation(self, time, z, y, x, ti, particle=None):
870+
"""Interpolate spatial field values."""
903871
try:
904872
if self.grid.zdim == 1:
905-
val = self._interpolator2D(ti, z, y, x, particle=particle)
873+
val = self._interpolator2D(time, z, y, x, ti, particle=particle)
906874
else:
907-
val = self._interpolator3D(ti, z, y, x, time, particle=particle)
875+
val = self._interpolator3D(time, z, y, x, ti, particle=particle)
908876

909877
if np.isnan(val):
910878
# Detect Out-of-bounds sampling and raise exception
@@ -966,16 +934,16 @@ def eval(self, time, z, y, x, particle=None, applyConversion=True):
966934
if self.gridindexingtype == "croco" and self not in [self.fieldset.H, self.fieldset.Zeta]:
967935
z = _croco_from_z_to_sigma_scipy(self.fieldset, time, z, y, x, particle=particle)
968936
if ti < self.grid.tdim - 1 and time > self.grid.time[ti]:
969-
f0 = self._spatial_interpolation(ti, z, y, x, time, particle=particle)
970-
f1 = self._spatial_interpolation(ti + 1, z, y, x, time, particle=particle)
937+
f0 = self._spatial_interpolation(time, z, y, x, ti, particle=particle)
938+
f1 = self._spatial_interpolation(time, z, y, x, ti + 1, particle=particle)
971939
t0 = self.grid.time[ti]
972940
t1 = self.grid.time[ti + 1]
973941
value = f0 + (f1 - f0) * ((time - t0) / (t1 - t0))
974942
else:
975943
# Skip temporal interpolation if time is outside
976944
# of the defined time range or if we have hit an
977945
# exact value in the time array.
978-
value = self._spatial_interpolation(ti, z, y, x, self.grid.time[ti], particle=particle)
946+
value = self._spatial_interpolation(self.grid.time[ti], z, y, x, ti, particle=particle)
979947

980948
if applyConversion:
981949
return self.units.to_target(value, z, y, x)

parcels/grid.py

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import numpy.typing as npt
77

88
from parcels._typing import Mesh, UpdateStatus, assert_valid_mesh
9-
from parcels.tools.converters import Geographic, GeographicPolar, TimeConverter, UnitConverter
9+
from parcels.tools.converters import TimeConverter
1010
from parcels.tools.warnings import FieldSetWarning
1111

1212
__all__ = [
@@ -67,7 +67,6 @@ def __init__(
6767
assert isinstance(self.time_origin, TimeConverter), "time_origin needs to be a TimeConverter object"
6868
assert_valid_mesh(mesh)
6969
self._mesh = mesh
70-
self._cell_edge_sizes: dict[str, npt.NDArray] = {}
7170
self._zonal_periodic = False
7271
self._zonal_halo = 0
7372
self._meridional_halo = 0
@@ -133,10 +132,6 @@ def zonal_halo(self):
133132
def defer_load(self):
134133
return self._defer_load
135134

136-
@property
137-
def cell_edge_sizes(self):
138-
return self._cell_edge_sizes
139-
140135
@staticmethod
141136
def create_grid(
142137
lon: npt.ArrayLike,
@@ -610,34 +605,3 @@ def __init__(
610605
@property
611606
def zdim(self):
612607
return self.depth.shape[-3]
613-
614-
615-
def _calc_cell_edge_sizes(grid: RectilinearGrid) -> None:
616-
"""Method to calculate cell sizes based on numpy.gradient method.
617-
618-
Currently only works for Rectilinear Grids. Operates in place adding a `cell_edge_sizes`
619-
attribute to the grid.
620-
"""
621-
if not grid.cell_edge_sizes:
622-
if grid._gtype in (GridType.RectilinearZGrid, GridType.RectilinearSGrid): # type: ignore[attr-defined]
623-
grid.cell_edge_sizes["x"] = np.zeros((grid.ydim, grid.xdim), dtype=np.float32)
624-
grid.cell_edge_sizes["y"] = np.zeros((grid.ydim, grid.xdim), dtype=np.float32)
625-
626-
x_conv = GeographicPolar() if grid.mesh == "spherical" else UnitConverter()
627-
y_conv = Geographic() if grid.mesh == "spherical" else UnitConverter()
628-
for y, (lat, dy) in enumerate(zip(grid.lat, np.gradient(grid.lat), strict=False)):
629-
for x, (lon, dx) in enumerate(zip(grid.lon, np.gradient(grid.lon), strict=False)):
630-
grid.cell_edge_sizes["x"][y, x] = x_conv.to_source(dx, grid.depth[0], lat, lon)
631-
grid.cell_edge_sizes["y"][y, x] = y_conv.to_source(dy, grid.depth[0], lat, lon)
632-
else:
633-
raise ValueError(
634-
f"_cell_edge_sizes() not implemented for {grid._gtype} grids. " # type: ignore[attr-defined]
635-
"You can provide Field.grid.cell_edge_sizes yourself by in, e.g., "
636-
"NEMO using the e1u fields etc from the mesh_mask.nc file."
637-
)
638-
639-
640-
def _calc_cell_areas(grid: RectilinearGrid) -> np.ndarray:
641-
if not grid.cell_edge_sizes:
642-
_calc_cell_edge_sizes(grid)
643-
return grid.cell_edge_sizes["x"] * grid.cell_edge_sizes["y"]

parcels/particle.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,6 @@ def __init__(self, lon, lat, pid, fieldset=None, ngrids=None, depth=0.0, time=0.
149149
initial = v.initial
150150
setattr(self, v.name, v.dtype(initial))
151151

152-
def __del__(self):
153-
pass # superclass is 'object', and object itself has no destructor, hence 'pass'
154-
155152
def __repr__(self):
156153
time_string = "not_yet_set" if self.time is None or np.isnan(self.time) else f"{self.time:f}"
157154
p_string = f"P[{self.id}](lon={self.lon:f}, lat={self.lat:f}, depth={self.depth:f}, "

parcels/particledata.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ def __init__(self, pclass, lon, lat, depth, time, lonlatdepth_dtype, pid_orig, n
122122
self._ncount = len(lon)
123123

124124
for v in self.ptype.variables:
125-
if v.name in ["ei", "ti"]:
126-
self._data[v.name] = np.empty((len(lon), ngrid), dtype=v.dtype)
125+
if v.name == "ei":
126+
self._data[v.name] = np.empty((len(lon), ngrid), dtype=v.dtype) # TODO len(lon) can be self._ncount?
127127
else:
128128
self._data[v.name] = np.empty(self._ncount, dtype=v.dtype)
129129

@@ -182,9 +182,6 @@ def __init__(self, pclass, lon, lat, depth, time, lonlatdepth_dtype, pid_orig, n
182182
else:
183183
raise ValueError("Latitude and longitude required for generating ParticleSet")
184184

185-
def __del__(self):
186-
pass
187-
188185
@property
189186
def pu_indicators(self):
190187
"""

parcels/particleset.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,13 +128,11 @@ def ArrayClass_init(self, *args, **kwargs):
128128
self.ngrids = type(self).ngrids.initial
129129
if self.ngrids >= 0:
130130
self.ei = np.zeros(self.ngrids, dtype=np.int32)
131-
self.ti = -1 * np.ones(self.ngrids, dtype=np.int32)
132131
super(type(self), self).__init__(*args, **kwargs)
133132

134133
array_class_vdict = {
135134
"ngrids": Variable("ngrids", dtype=np.int32, to_write=False, initial=-1),
136135
"ei": Variable("ei", dtype=np.int32, to_write=False),
137-
"ti": Variable("ti", dtype=np.int32, to_write=False, initial=-1),
138136
"__init__": ArrayClass_init,
139137
}
140138
array_class = type(class_name, (pclass,), array_class_vdict)
@@ -719,7 +717,6 @@ def from_particlefile(
719717
v.name
720718
not in [
721719
"ei",
722-
"ti",
723720
"dt",
724721
"depth",
725722
"id",

tests/test_fieldset.py

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -407,22 +407,6 @@ def test_fieldset_samegrids_from_data():
407407
assert fieldset1.U.grid == fieldset1.B.grid
408408

409409

410-
@pytest.mark.parametrize("dx, dy", [("e1u", "e2u"), ("e1v", "e2v")])
411-
def test_fieldset_celledgesizes_curvilinear(dx, dy):
412-
fname = str(TEST_DATA / "mask_nemo_cross_180lon.nc")
413-
filenames = {"dx": fname, "dy": fname, "mesh_mask": fname}
414-
variables = {"dx": dx, "dy": dy}
415-
dimensions = {"dx": {"lon": "glamu", "lat": "gphiu"}, "dy": {"lon": "glamu", "lat": "gphiu"}}
416-
fieldset = FieldSet.from_nemo(filenames, variables, dimensions)
417-
418-
# explicitly setting cell_edge_sizes from e1u and e2u etc
419-
fieldset.dx.grid.cell_edge_sizes["x"] = fieldset.dx.data
420-
fieldset.dx.grid.cell_edge_sizes["y"] = fieldset.dy.data
421-
422-
A = fieldset.dx.cell_areas()
423-
assert np.allclose(A, fieldset.dx.data * fieldset.dy.data)
424-
425-
426410
def test_fieldset_write_curvilinear(tmpdir):
427411
fname = str(TEST_DATA / "mask_nemo_cross_180lon.nc")
428412
filenames = {"dx": fname, "mesh_mask": fname}
@@ -456,21 +440,6 @@ def test_curv_fieldset_add_periodic_halo():
456440
fieldset.add_periodic_halo(zonal=3, meridional=2)
457441

458442

459-
@pytest.mark.parametrize("mesh", ["flat", "spherical"])
460-
def test_fieldset_cellareas(mesh):
461-
data, dimensions = generate_fieldset_data(10, 7)
462-
fieldset = FieldSet.from_data(data, dimensions, mesh=mesh)
463-
cell_areas = fieldset.V.cell_areas()
464-
if mesh == "flat":
465-
assert np.allclose(cell_areas.flatten(), cell_areas[0, 0], rtol=1e-3)
466-
else:
467-
assert all(
468-
(np.gradient(cell_areas, axis=0) < 0).flatten()
469-
) # areas should decrease with latitude in spherical mesh
470-
for y in range(cell_areas.shape[0]):
471-
assert np.allclose(cell_areas[y, :], cell_areas[y, 0], rtol=1e-3)
472-
473-
474443
def addConst(particle, fieldset, time): # pragma: no cover
475444
particle.lon = particle.lon + fieldset.movewest + fieldset.moveeast
476445

@@ -555,7 +524,8 @@ def test_fieldset_write(tmp_zarrfile):
555524
def UpdateU(particle, fieldset, time): # pragma: no cover
556525
tmp1, tmp2 = fieldset.UV[particle]
557526
_, yi, xi = fieldset.U.unravel_index(particle.ei)
558-
fieldset.U.data[particle.ti, yi, xi] += 1
527+
ti = fieldset.U._time_index(time)
528+
fieldset.U.data[ti, yi, xi] += 1
559529
fieldset.U.grid.time[0] = time
560530

561531
pset = ParticleSet(fieldset, pclass=Particle, lon=5, lat=5)

0 commit comments

Comments
 (0)