Skip to content

Commit 855a2e3

Browse files
Merge pull request #1901 from OceanParcels/1898
Remove add_periodic_halo() methods on Grid, Field, FieldSet
2 parents f06f2e1 + bba3308 commit 855a2e3

File tree

13 files changed

+58
-193
lines changed

13 files changed

+58
-193
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ jobs:
7676
with:
7777
environment-file: environment.yml
7878
- name: Integration test
79-
run: |
80-
coverage run -m pytest -v -s --nbval-lax -k "not documentation" --html="${{ matrix.os }}_${{ matrix.python-version }}_integration_test_report.html" --self-contained-html docs/examples
79+
run: | # TODO v4: Re-enable `tutorial_periodic_boundaries` notebook
80+
coverage run -m pytest -v -s --nbval-lax -k "not documentation and not tutorial_periodic_boundaries" --html="${{ matrix.os }}_${{ matrix.python-version }}_integration_test_report.html" --self-contained-html docs/examples
8181
coverage xml
8282
- name: Codecov
8383
uses: codecov/[email protected]

docs/examples/example_mitgcm.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from datetime import timedelta
22
from pathlib import Path
33

4+
import pytest
5+
46
import parcels
57

68

@@ -49,6 +51,8 @@ def periodicBC(particle, fieldset, time): # pragma: no cover
4951
)
5052

5153

54+
@pytest.mark.v4alpha
55+
@pytest.mark.xfail(reason="Test uses add_periodic_halo(). Update to not use it.")
5256
def test_mitgcm_output(tmpdir):
5357
def get_path() -> Path:
5458
return tmpdir / "MIT_particles.zarr"

docs/examples/example_moving_eddies.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,10 @@ def test_moving_eddies_file(mesh, tmpdir):
250250
assert pset[1].lon < 2.0 and 48.8 < pset[1].lat < 48.85
251251

252252

253+
@pytest.mark.v4alpha
254+
@pytest.mark.xfail(
255+
reason="Calls fieldset.add_periodic_halo(). In v4, interpolation should work without adding halo."
256+
)
253257
def test_periodic_and_computeTimeChunk_eddies():
254258
data_folder = parcels.download_example_dataset("MovingEddies_data")
255259
filename = str(data_folder / "moving_eddies")

docs/examples/tutorial_analyticaladvection.ipynb

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -467,9 +467,12 @@
467467
"metadata": {},
468468
"outputs": [],
469469
"source": [
470-
"fieldsetBJ.add_constant(\"halo_west\", fieldsetBJ.U.grid.lon[0])\n",
471-
"fieldsetBJ.add_constant(\"halo_east\", fieldsetBJ.U.grid.lon[-1])\n",
472-
"fieldsetBJ.add_periodic_halo(zonal=True)\n",
470+
"fieldsetBJ.add_constant(\n",
471+
" \"halo_west\", fieldsetBJ.U.grid.lon[1]\n",
472+
") # TODO v4: Change index back to 0 when periodic interpolation is done\n",
473+
"fieldsetBJ.add_constant(\n",
474+
" \"halo_east\", fieldsetBJ.U.grid.lon[-2]\n",
475+
") # TODO v4: Change index back to -1 when periodic interpolation is done\n",
473476
"\n",
474477
"\n",
475478
"def ZonalBC(particle, fieldset, time):\n",
@@ -608,7 +611,7 @@
608611
],
609612
"metadata": {
610613
"kernelspec": {
611-
"display_name": "parcels",
614+
"display_name": "parcels-dev",
612615
"language": "python",
613616
"name": "python3"
614617
},
@@ -622,7 +625,7 @@
622625
"name": "python",
623626
"nbconvert_exporter": "python",
624627
"pygments_lexer": "ipython3",
625-
"version": "3.12.3"
628+
"version": "3.10.15"
626629
}
627630
},
628631
"nbformat": 4,

docs/v4/api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ Here, important things to note are:
3636

3737
- Interpolators (which would implement the `Interpolator` protocol) are responsible for the actual interpolation of the data, and performance considerations. There will be interpolation and indexing utilities that can be made available to the interpolators, allowing for code re-use.
3838

39+
- Interpolators of the data should handle spatial periodicity and, for the case of rectilinear structured grids, without pre-computing a halo for the FieldSet and Grid ([issue](https://github.com/OceanParcels/Parcels/issues/1898)).
40+
3941
- In the `Field` class, not all combinations of `data`, `grid`, and `interpolator` will logically make sense (e.g., a `xr.DataArray` on a `ux.Grid`, or `ux.DataArray` on a `parcels.Grid`). It's up to the `Interpolator.assert_is_compatible(Field)` to define what is and is not compatible, and raise `ValueError` / `TypeError` on incompatible data types. The `.assert_is_compatible()` method also acts as developer documentation, defining clearly for the `.interpolate()` method what assumptions it is working on. The `.assert_is_compatible()` method should be lightweight as it will be called on `Field` initialisation.
4042

4143
- The `grid` object, in the case of unstructured grids, will be the `Grid` class from UXarray. For structured `Grid`s, it will be an object similar to that of `xgcm.Grid` (note that it will be very different from the v3 `Grid` object hierarchy).

parcels/field.py

Lines changed: 8 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -596,12 +596,12 @@ def from_netcdf(
596596
if len(buffer_data.shape) == 4:
597597
errormessage = (
598598
f"Field {filebuffer.name} expecting a data shape of [tdim={grid.tdim}, zdim={grid.zdim}, "
599-
f"ydim={grid.ydim - 2 * grid.meridional_halo}, xdim={grid.xdim - 2 * grid.zonal_halo}] "
599+
f"ydim={grid.ydim}, xdim={grid.xdim }] "
600600
f"but got shape {buffer_data.shape}. Flag transpose=True could help to reorder the data."
601601
)
602602
assert buffer_data.shape[0] == grid.tdim, errormessage
603-
assert buffer_data.shape[2] == grid.ydim - 2 * grid.meridional_halo, errormessage
604-
assert buffer_data.shape[3] == grid.xdim - 2 * grid.zonal_halo, errormessage
603+
assert buffer_data.shape[2] == grid.ydim, errormessage
604+
assert buffer_data.shape[3] == grid.xdim, errormessage
605605

606606
if len(buffer_data.shape) == 2:
607607
data_list.append(buffer_data.reshape(sum(((len(tslice), 1), buffer_data.shape), ())))
@@ -723,28 +723,22 @@ def _reshape(self, data, transpose=False):
723723
"Flag transpose=True could help to reorder the data."
724724
)
725725
assert data.shape[0] == self.grid.tdim, errormessage
726-
assert data.shape[2] == self.grid.ydim - 2 * self.grid.meridional_halo, errormessage
727-
assert data.shape[3] == self.grid.xdim - 2 * self.grid.zonal_halo, errormessage
726+
assert data.shape[2] == self.grid.ydim, errormessage
727+
assert data.shape[3] == self.grid.xdim, errormessage
728728
if self.gridindexingtype == "pop":
729729
assert data.shape[1] == self.grid.zdim or data.shape[1] == self.grid.zdim - 1, errormessage
730730
else:
731731
assert data.shape[1] == self.grid.zdim, errormessage
732732
else:
733733
assert data.shape == (
734734
self.grid.tdim,
735-
self.grid.ydim - 2 * self.grid.meridional_halo,
736-
self.grid.xdim - 2 * self.grid.zonal_halo,
735+
self.grid.ydim,
736+
self.grid.xdim,
737737
), (
738738
f"Field {self.name} expecting a data shape of [tdim, ydim, xdim]. "
739739
"Flag transpose=True could help to reorder the data."
740740
)
741-
if self.grid.meridional_halo > 0 or self.grid.zonal_halo > 0:
742-
data = self.add_periodic_halo(
743-
zonal=self.grid.zonal_halo > 0,
744-
meridional=self.grid.meridional_halo > 0,
745-
halosize=max(self.grid.meridional_halo, self.grid.zonal_halo),
746-
data=data,
747-
)
741+
748742
return data
749743

750744
def set_scaling_factor(self, factor):
@@ -898,54 +892,6 @@ def eval(self, time, z, y, x, particle=None, applyConversion=True):
898892
else:
899893
return value
900894

901-
def add_periodic_halo(self, zonal, meridional, halosize=5, data=None):
902-
"""Add a 'halo' to all Fields in a FieldSet.
903-
904-
Add a 'halo' to all Fields in a FieldSet, through extending the Field (and lon/lat)
905-
by copying a small portion of the field on one side of the domain to the other.
906-
Before adding a periodic halo to the Field, it has to be added to the Grid on which the Field depends
907-
908-
See `this tutorial <../examples/tutorial_periodic_boundaries.ipynb>`__
909-
for a detailed explanation on how to set up periodic boundaries
910-
911-
Parameters
912-
----------
913-
zonal : bool
914-
Create a halo in zonal direction.
915-
meridional : bool
916-
Create a halo in meridional direction.
917-
halosize : int
918-
Size of the halo (in grid points). Default is 5 grid points
919-
data :
920-
if data is not None, the periodic halo will be achieved on data instead of self.data and data will be returned (Default value = None)
921-
"""
922-
dataNone = not isinstance(data, np.ndarray)
923-
if self.grid.defer_load and dataNone:
924-
return
925-
data = self.data if dataNone else data
926-
if zonal:
927-
if len(data.shape) == 3:
928-
data = np.concatenate((data[:, :, -halosize:], data, data[:, :, 0:halosize]), axis=len(data.shape) - 1)
929-
assert data.shape[2] == self.grid.xdim, "Third dim must be x."
930-
else:
931-
data = np.concatenate(
932-
(data[:, :, :, -halosize:], data, data[:, :, :, 0:halosize]), axis=len(data.shape) - 1
933-
)
934-
assert data.shape[3] == self.grid.xdim, "Fourth dim must be x."
935-
if meridional:
936-
if len(data.shape) == 3:
937-
data = np.concatenate((data[:, -halosize:, :], data, data[:, 0:halosize, :]), axis=len(data.shape) - 2)
938-
assert data.shape[1] == self.grid.ydim, "Second dim must be y."
939-
else:
940-
data = np.concatenate(
941-
(data[:, :, -halosize:, :], data, data[:, :, 0:halosize, :]), axis=len(data.shape) - 2
942-
)
943-
assert data.shape[2] == self.grid.ydim, "Third dim must be y."
944-
if dataNone:
945-
self.data = data
946-
else:
947-
return data
948-
949895
def write(self, filename, varname=None):
950896
"""Write a :class:`Field` to a netcdf file.
951897

parcels/fieldset.py

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1358,26 +1358,6 @@ def add_constant(self, name, value):
13581358
"""
13591359
setattr(self, name, value)
13601360

1361-
def add_periodic_halo(self, zonal=False, meridional=False, halosize=5):
1362-
"""Add a 'halo' to all :class:`parcels.field.Field` objects in a FieldSet,
1363-
through extending the Field (and lon/lat) by copying a small portion
1364-
of the field on one side of the domain to the other.
1365-
1366-
Parameters
1367-
----------
1368-
zonal : bool
1369-
Create a halo in zonal direction (Default value = False)
1370-
meridional : bool
1371-
Create a halo in meridional direction (Default value = False)
1372-
halosize : int
1373-
size of the halo (in grid points). Default is 5 grid points
1374-
"""
1375-
for grid in self.gridset.grids:
1376-
grid.add_periodic_halo(zonal, meridional, halosize)
1377-
for value in self.__dict__.values():
1378-
if isinstance(value, Field):
1379-
value.add_periodic_halo(zonal, meridional, halosize)
1380-
13811361
def write(self, filename):
13821362
"""Write FieldSet to NetCDF file using NEMO convention.
13831363
@@ -1445,9 +1425,7 @@ def computeTimeChunk(self, time=0.0, dt=1):
14451425
zd = g.zdim - 1
14461426
else:
14471427
zd = g.zdim
1448-
data = np.empty(
1449-
(g.tdim, zd, g.ydim - 2 * g.meridional_halo, g.xdim - 2 * g.zonal_halo), dtype=np.float32
1450-
)
1428+
data = np.empty((g.tdim, zd, g.ydim, g.xdim), dtype=np.float32)
14511429
f._loaded_time_indices = range(2)
14521430
for tind in f._loaded_time_indices:
14531431
for fb in f.filebuffers:
@@ -1466,9 +1444,7 @@ def computeTimeChunk(self, time=0.0, dt=1):
14661444
zd = g.zdim - 1
14671445
else:
14681446
zd = g.zdim
1469-
data = np.empty(
1470-
(g.tdim, zd, g.ydim - 2 * g.meridional_halo, g.xdim - 2 * g.zonal_halo), dtype=np.float32
1471-
)
1447+
data = np.empty((g.tdim, zd, g.ydim, g.xdim), dtype=np.float32)
14721448
if signdt >= 0:
14731449
f._loaded_time_indices = [1]
14741450
if f.filebuffers[0] is not None:

parcels/grid.py

Lines changed: 0 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,6 @@ def __init__(
6868
assert_valid_mesh(mesh)
6969
self._mesh = mesh
7070
self._zonal_periodic = False
71-
self._zonal_halo = 0
72-
self._meridional_halo = 0
7371
self._lat_flipped = False
7472
self._defer_load = False
7573
self._lonlat_minmax = np.array(
@@ -108,10 +106,6 @@ def negate_depth(self):
108106
def mesh(self):
109107
return self._mesh
110108

111-
@property
112-
def meridional_halo(self):
113-
return self._meridional_halo
114-
115109
@property
116110
def lonlat_minmax(self):
117111
return self._lonlat_minmax
@@ -124,10 +118,6 @@ def time_origin(self):
124118
def zonal_periodic(self):
125119
return self._zonal_periodic
126120

127-
@property
128-
def zonal_halo(self):
129-
return self._zonal_halo
130-
131121
@property
132122
def defer_load(self):
133123
return self._defer_load
@@ -279,50 +269,6 @@ def xdim(self):
279269
def ydim(self):
280270
return self.lat.size
281271

282-
def add_periodic_halo(self, zonal: bool, meridional: bool, halosize: int = 5):
283-
"""Add a 'halo' to the Grid, through extending the Grid (and lon/lat)
284-
similarly to the halo created for the Fields
285-
286-
Parameters
287-
----------
288-
zonal : bool
289-
Create a halo in zonal direction
290-
meridional : bool
291-
Create a halo in meridional direction
292-
halosize : int
293-
size of the halo (in grid points). Default is 5 grid points
294-
"""
295-
if zonal:
296-
lonshift = self.lon[-1] - 2 * self.lon[0] + self.lon[1]
297-
if not np.allclose(self.lon[1] - self.lon[0], self.lon[-1] - self.lon[-2]):
298-
warnings.warn(
299-
"The zonal halo is located at the east and west of current grid, "
300-
"with a dx = lon[1]-lon[0] between the last nodes of the original grid and the first ones of the halo. "
301-
"In your grid, lon[1]-lon[0] != lon[-1]-lon[-2]. Is the halo computed as you expect?",
302-
FieldSetWarning,
303-
stacklevel=2,
304-
)
305-
self._lon = np.concatenate((self.lon[-halosize:] - lonshift, self.lon, self.lon[0:halosize] + lonshift))
306-
self._zonal_periodic = True
307-
self._zonal_halo = halosize
308-
if meridional:
309-
if not np.allclose(self.lat[1] - self.lat[0], self.lat[-1] - self.lat[-2]):
310-
warnings.warn(
311-
"The meridional halo is located at the north and south of current grid, "
312-
"with a dy = lat[1]-lat[0] between the last nodes of the original grid and the first ones of the halo. "
313-
"In your grid, lat[1]-lat[0] != lat[-1]-lat[-2]. Is the halo computed as you expect?",
314-
FieldSetWarning,
315-
stacklevel=2,
316-
)
317-
latshift = self.lat[-1] - 2 * self.lat[0] + self.lat[1]
318-
self._lat = np.concatenate((self.lat[-halosize:] - latshift, self.lat, self.lat[0:halosize] + latshift))
319-
self._meridional_halo = halosize
320-
self._lonlat_minmax = np.array(
321-
[np.nanmin(self.lon), np.nanmax(self.lon), np.nanmin(self.lat), np.nanmax(self.lat)], dtype=np.float32
322-
)
323-
if isinstance(self, RectilinearSGrid):
324-
self._add_Sdepth_periodic_halo(zonal, meridional, halosize)
325-
326272

327273
class RectilinearZGrid(RectilinearGrid):
328274
"""Rectilinear Z Grid.
@@ -469,23 +415,6 @@ def xdim(self):
469415
def ydim(self):
470416
return self.lon.shape[0]
471417

472-
def add_periodic_halo(self, zonal, meridional, halosize=5):
473-
"""Add a 'halo' to the Grid, through extending the Grid (and lon/lat)
474-
similarly to the halo created for the Fields
475-
476-
Parameters
477-
----------
478-
zonal : bool
479-
Create a halo in zonal direction
480-
meridional : bool
481-
Create a halo in meridional direction
482-
halosize : int
483-
size of the halo (in grid points). Default is 5 grid points
484-
"""
485-
raise NotImplementedError(
486-
"CurvilinearGrid does not support add_periodic_halo. See https://github.com/OceanParcels/Parcels/pull/1811"
487-
)
488-
489418

490419
class CurvilinearZGrid(CurvilinearGrid):
491420
"""Curvilinear Z Grid.

tests/test_advection.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,8 @@ def periodicBC(particle, fieldset, time): # pragma: no cover
275275
particle.lat = math.fmod(particle.lat, 1)
276276

277277

278+
@pytest.mark.v4alpha
279+
@pytest.mark.xfail(reason="Calls fieldset.add_periodic_halo(). In v4, interpolation should work without adding halo.")
278280
def test_advection_periodic_zonal():
279281
xdim, ydim, halosize = 100, 100, 3
280282
fieldset = create_periodic_fieldset(xdim, ydim, uvel=1.0, vvel=0.0)
@@ -286,6 +288,8 @@ def test_advection_periodic_zonal():
286288
assert abs(pset.lon[0] - 0.15) < 0.1
287289

288290

291+
@pytest.mark.v4alpha
292+
@pytest.mark.xfail(reason="Calls fieldset.add_periodic_halo(). In v4, interpolation should work without adding halo.")
289293
def test_advection_periodic_meridional():
290294
xdim, ydim = 100, 100
291295
fieldset = create_periodic_fieldset(xdim, ydim, uvel=0.0, vvel=1.0)
@@ -297,6 +301,8 @@ def test_advection_periodic_meridional():
297301
assert abs(pset.lat[0] - 0.15) < 0.1
298302

299303

304+
@pytest.mark.v4alpha
305+
@pytest.mark.xfail(reason="Calls fieldset.add_periodic_halo(). In v4, interpolation should work without adding halo.")
300306
def test_advection_periodic_zonal_meridional():
301307
xdim, ydim = 100, 100
302308
fieldset = create_periodic_fieldset(xdim, ydim, uvel=1.0, vvel=1.0)

tests/test_fieldset.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -397,17 +397,6 @@ def test_fieldset_write_curvilinear(tmpdir):
397397
assert np.allclose(getattr(fieldset2.dx, var), getattr(fieldset.dx, var))
398398

399399

400-
def test_curv_fieldset_add_periodic_halo():
401-
fname = str(TEST_DATA / "mask_nemo_cross_180lon.nc")
402-
filenames = {"dx": fname, "dy": fname, "mesh_mask": fname}
403-
variables = {"dx": "e1u", "dy": "e1v"}
404-
dimensions = {"dx": {"lon": "glamu", "lat": "gphiu"}, "dy": {"lon": "glamu", "lat": "gphiu"}}
405-
fieldset = FieldSet.from_nemo(filenames, variables, dimensions)
406-
407-
with pytest.raises(NotImplementedError):
408-
fieldset.add_periodic_halo(zonal=3, meridional=2)
409-
410-
411400
def addConst(particle, fieldset, time): # pragma: no cover
412401
particle.lon = particle.lon + fieldset.movewest + fieldset.moveeast
413402

0 commit comments

Comments
 (0)