Skip to content

Commit 664a1aa

Browse files
committed
Transpose Field data to 4D arrays
Needed to update the tests because the time axis information was getting lost (due to xarray behaviour with keep_attrs). Fixes #2047
1 parent ff9b433 commit 664a1aa

4 files changed

Lines changed: 66 additions & 15 deletions

File tree

parcels/field.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
_raise_field_out_of_bound_error,
3030
)
3131
from parcels.uxgrid import UxGrid
32-
from parcels.xgrid import XGrid
32+
from parcels.xgrid import XGrid, _transpose_xfield_data_to_tzyx
3333

3434
from ._index_search import _search_time_index
3535

@@ -146,6 +146,9 @@ def __init__(
146146

147147
_assert_compatible_combination(data, grid)
148148

149+
if isinstance(grid, XGrid):
150+
data = _transpose_xfield_data_to_tzyx(data, grid.xgcm_grid)
151+
149152
self.name = name
150153
self.data = data
151154
self.grid = grid
@@ -186,9 +189,6 @@ def __init__(
186189
else:
187190
raise ValueError("Unsupported mesh type in data array attributes. Choose either: 'spherical' or 'flat'")
188191

189-
if "time" not in self.data.dims:
190-
raise ValueError("Field is missing a 'time' dimension. ")
191-
192192
@property
193193
def units(self):
194194
return self._units
@@ -439,7 +439,7 @@ def _assert_compatible_combination(data: xr.DataArray | ux.UxDataArray, grid: ux
439439

440440

441441
def _get_time_interval(data: xr.DataArray | ux.UxDataArray) -> TimeInterval | None:
442-
if len(data.time) == 1:
442+
if data.shape[0] == 1:
443443
return None
444444

445445
return TimeInterval(data.time.values[0], data.time.values[-1])

parcels/fieldset.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,6 @@ def add_constant_field(self, name: str, value, mesh: Mesh = "flat"):
132132
"""
133133
da = xr.DataArray(
134134
data=np.full((1, 1, 1, 1), value),
135-
dims=["time", "ZG", "YG", "XG"],
136135
)
137136
grid = XGrid(xgcm.Grid(da))
138137
self.add_field(

parcels/xgrid.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from collections.abc import Hashable, Mapping
1+
from collections.abc import Hashable, Mapping, Sequence
22
from functools import cached_property
33
from typing import Literal, cast
44

@@ -10,12 +10,15 @@
1010
from parcels._index_search import _search_indices_curvilinear_2d
1111
from parcels.basegrid import BaseGrid
1212

13-
_XGRID_AXES_ORDERING = "ZYX"
1413
_XGRID_AXES = Literal["X", "Y", "Z"]
14+
_XGRID_AXES_ORDERING: Sequence[_XGRID_AXES] = "ZYX"
1515

1616
_XGCM_AXIS_DIRECTION = Literal["X", "Y", "Z", "T"]
1717
_XGCM_AXIS_POSITION = Literal["center", "left", "right", "inner", "outer"]
1818
_XGCM_AXES = Mapping[_XGCM_AXIS_DIRECTION, xgcm.Axis]
19+
20+
_FIELD_DATA_ORDERING: Sequence[_XGCM_AXIS_DIRECTION] = "TZYX"
21+
1922
_DEFAULT_XGCM_KWARGS = {"periodic": False}
2023

2124

@@ -35,7 +38,7 @@ def _get_xgrid_axes(grid: xgcm.Grid) -> list[_XGRID_AXES]:
3538
return sorted(spatial_axes, key=_XGRID_AXES_ORDERING.index)
3639

3740

38-
def drop_field_data(ds: xr.Dataset) -> xr.Dataset:
41+
def _drop_field_data(ds: xr.Dataset) -> xr.Dataset:
3942
"""
4043
Removes DataArrays from the dataset that are associated with field data so that
4144
when passed to the XGCM grid, the object only functions as an in memory representation
@@ -44,6 +47,39 @@ def drop_field_data(ds: xr.Dataset) -> xr.Dataset:
4447
return ds.drop_vars(ds.data_vars)
4548

4649

50+
def _transpose_xfield_data_to_tzyx(da: xr.DataArray, xgcm_grid: xgcm.Grid) -> xr.DataArray:
51+
"""
52+
Transpose a DataArray of any shape into a 4D array of order TZYX. Uses xgcm to determine
53+
the axes, and inserts dummy dimensions of size 1 for any axes not present in the DataArray.
54+
"""
55+
ax_dims = [(get_axis_from_dim_name(xgcm_grid.axes, dim), dim) for dim in da.dims]
56+
57+
if all(ax_dim[0] is None for ax_dim in ax_dims):
58+
# Assuming its a 1D constant field (hence has no axes)
59+
assert da.shape == (1, 1, 1, 1)
60+
return da.rename({old_dim: f"dummy{axis}" for old_dim, axis in zip(da.dims, _FIELD_DATA_ORDERING, strict=True)})
61+
62+
# All dimensions must be associated with an axis in the grid
63+
if any(ax_dim[0] is None for ax_dim in ax_dims):
64+
raise ValueError(
65+
f"DataArray {da.name!r} with dims {da.dims} has dimensions that are not associated with a direction on the provided grid."
66+
)
67+
68+
axes_not_in_field = set(_FIELD_DATA_ORDERING) - set(ax_dim[0] for ax_dim in ax_dims)
69+
70+
dummy_dims_to_create = {}
71+
for ax in axes_not_in_field:
72+
dummy_dims_to_create[f"dummy{ax}"] = 1
73+
ax_dims.append((ax, f"dummy{ax}"))
74+
75+
if dummy_dims_to_create:
76+
da = da.expand_dims(dummy_dims_to_create, create_index_for_new_dim=False)
77+
78+
ax_dims = sorted(ax_dims, key=lambda x: _FIELD_DATA_ORDERING.index(x[0]))
79+
80+
return da.transpose(*[ax_dim[1] for ax_dim in ax_dims])
81+
82+
4783
class XGrid(BaseGrid):
4884
"""
4985
Class to represent a structured grid in Parcels. Wraps a xgcm-like Grid object (we use a trimmed down version of the xgcm.Grid class that is vendored with Parcels).
@@ -71,7 +107,7 @@ def from_dataset(cls, ds: xr.Dataset, mesh="flat", xgcm_kwargs=None):
71107

72108
xgcm_kwargs = {**_DEFAULT_XGCM_KWARGS, **xgcm_kwargs}
73109

74-
ds = drop_field_data(ds)
110+
ds = _drop_field_data(ds)
75111
grid = xgcm.Grid(ds, **xgcm_kwargs)
76112
return cls(grid, mesh=mesh)
77113

tests/v4/test_fieldset.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def test_fieldset_time_interval():
9292
field1 = Field("field1", ds["U (A grid)"], grid1, mesh_type="flat")
9393

9494
ds2 = ds.copy()
95-
ds2["time"] = ds2["time"] + np.timedelta64(timedelta(days=1))
95+
ds2["time"] = (ds2["time"].dims, ds2["time"].data + np.timedelta64(timedelta(days=1)), ds2["time"].attrs)
9696
grid2 = XGrid.from_dataset(ds2)
9797
field2 = Field("field2", ds2["U (A grid)"], grid2, mesh_type="flat")
9898

@@ -113,15 +113,23 @@ def test_fieldset_time_interval_constant_fields():
113113

114114
def test_fieldset_init_incompatible_calendars():
115115
ds1 = ds.copy()
116-
ds1["time"] = xr.date_range("2000", "2001", T_structured, calendar="365_day", use_cftime=True)
116+
ds1["time"] = (
117+
ds1["time"].dims,
118+
xr.date_range("2000", "2001", T_structured, calendar="365_day", use_cftime=True),
119+
ds1["time"].attrs,
120+
)
117121

118122
grid = XGrid.from_dataset(ds1)
119123
U = Field("U", ds1["U (A grid)"], grid, mesh_type="flat")
120124
V = Field("V", ds1["V (A grid)"], grid, mesh_type="flat")
121125
UV = VectorField("UV", U, V)
122126

123127
ds2 = ds.copy()
124-
ds2["time"] = xr.date_range("2000", "2001", T_structured, calendar="360_day", use_cftime=True)
128+
ds2["time"] = (
129+
ds2["time"].dims,
130+
xr.date_range("2000", "2001", T_structured, calendar="360_day", use_cftime=True),
131+
ds2["time"].attrs,
132+
)
125133
grid2 = XGrid.from_dataset(ds2)
126134
incompatible_calendar = Field("test", ds2["data_g"], grid2, mesh_type="flat")
127135

@@ -131,15 +139,23 @@ def test_fieldset_init_incompatible_calendars():
131139

132140
def test_fieldset_add_field_incompatible_calendars(fieldset):
133141
ds_test = ds.copy()
134-
ds_test["time"] = xr.date_range("2000", "2001", T_structured, calendar="360_day", use_cftime=True)
142+
ds_test["time"] = (
143+
ds_test["time"].dims,
144+
xr.date_range("2000", "2001", T_structured, calendar="360_day", use_cftime=True),
145+
ds_test["time"].attrs,
146+
)
135147
grid = XGrid.from_dataset(ds_test)
136148
field = Field("test_field", ds_test["data_g"], grid, mesh_type="flat")
137149

138150
with pytest.raises(CalendarError, match="Expected field '.*' to have calendar compatible with datetime object"):
139151
fieldset.add_field(field, "test_field")
140152

141153
ds_test = ds.copy()
142-
ds_test["time"] = np.linspace(0, 100, T_structured, dtype="timedelta64[s]")
154+
ds_test["time"] = (
155+
ds_test["time"].dims,
156+
np.linspace(0, 100, T_structured, dtype="timedelta64[s]"),
157+
ds_test["time"].attrs,
158+
)
143159
grid = XGrid.from_dataset(ds_test)
144160
field = Field("test_field", ds_test["data_g"], grid, mesh_type="flat")
145161

0 commit comments

Comments
 (0)