Skip to content

Commit 255d4c6

Browse files
Removing transpose option in Field creation
As not needed anymore when using xarray under the hood in Parcels v4
1 parent 4d31dbb commit 255d4c6

File tree

12 files changed

+126
-152
lines changed

12 files changed

+126
-152
lines changed

docs/examples/example_moving_eddies.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ def cosd(x):
6565
dy = (lat[1] - lat[0]) * 1852 * 60 if mesh == "spherical" else lat[1] - lat[0]
6666

6767
# Define arrays U (zonal), V (meridional), and P (sea surface height) on A-grid
68-
U = np.zeros((lon.size, lat.size, time.size), dtype=np.float32)
69-
V = np.zeros((lon.size, lat.size, time.size), dtype=np.float32)
70-
P = np.zeros((lon.size, lat.size, time.size), dtype=np.float32)
68+
U = np.zeros((time.size, lat.size, lon.size), dtype=np.float32)
69+
V = np.zeros((time.size, lat.size, lon.size), dtype=np.float32)
70+
P = np.zeros((time.size, lat.size, lon.size), dtype=np.float32)
7171

7272
# Some constants
7373
corio_0 = 1.0e-4 # Coriolis parameter
@@ -78,32 +78,32 @@ def cosd(x):
7878
dX = eddyspeed * 86400 / dx # Grid cell movement of eddy max each day
7979
dY = eddyspeed * 86400 / dy # Grid cell movement of eddy max each day
8080

81-
[x, y] = np.mgrid[: lon.size, : lat.size]
81+
[y, x] = np.mgrid[: lat.size, : lon.size]
8282
for t in range(time.size):
8383
hymax_1 = lat.size / 7.0
8484
hxmax_1 = 0.75 * lon.size - dX * t
8585
hymax_2 = 3.0 * lat.size / 7.0 + dY * t
8686
hxmax_2 = 0.75 * lon.size - dX * t
8787

88-
P[:, :, t] = h0 * np.exp(
88+
P[t, :, :] = h0 * np.exp(
8989
-((x - hxmax_1) ** 2) / (sig * lon.size / 4.0) ** 2
9090
- (y - hymax_1) ** 2 / (sig * lat.size / 7.0) ** 2
9191
)
92-
P[:, :, t] += h0 * np.exp(
92+
P[t, :, :] += h0 * np.exp(
9393
-((x - hxmax_2) ** 2) / (sig * lon.size / 4.0) ** 2
9494
- (y - hymax_2) ** 2 / (sig * lat.size / 7.0) ** 2
9595
)
9696

97-
V[:-1, :, t] = -np.diff(P[:, :, t], axis=0) / dx / corio_0 * g
98-
V[-1, :, t] = V[-2, :, t] # Fill in the last column
97+
V[t, :, :-1] = -np.diff(P[t, :, :], axis=1) / dx / corio_0 * g
98+
V[t, :, -1] = V[t, :, -2] # Fill in the last column
9999

100-
U[:, :-1, t] = np.diff(P[:, :, t], axis=1) / dy / corio_0 * g
101-
U[:, -1, t] = U[:, -2, t] # Fill in the last row
100+
U[t, :-1, :] = np.diff(P[t, :, :], axis=0) / dy / corio_0 * g
101+
U[t, -1, :] = U[t, -2, :] # Fill in the last row
102102

103103
data = {"U": U, "V": V, "P": P}
104104
dimensions = {"lon": lon, "lat": lat, "time": time}
105105

106-
fieldset = parcels.FieldSet.from_data(data, dimensions, transpose=True, mesh=mesh)
106+
fieldset = parcels.FieldSet.from_data(data, dimensions, mesh=mesh)
107107

108108
# setting some constants for AdvectionRK45 kernel
109109
fieldset.RK45_min_dt = 1e-3

parcels/field.py

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,7 @@ class Field:
108108
name : str
109109
Name of the field
110110
data : np.ndarray
111-
2D, 3D or 4D numpy array of field data.
112-
113-
1. If data shape is [xdim, ydim], [xdim, ydim, zdim], [xdim, ydim, tdim] or [xdim, ydim, zdim, tdim],
114-
whichever is relevant for the dataset, use the flag transpose=True
115-
2. If data shape is [ydim, xdim], [zdim, ydim, xdim], [tdim, ydim, xdim] or [tdim, zdim, ydim, xdim],
116-
use the flag transpose=False
117-
3. If data has any other shape, you first need to reorder it
111+
2D, 3D or 4D numpy array of field data with shape [ydim, xdim], [zdim, ydim, xdim], [tdim, ydim, xdim] or [tdim, zdim, ydim, xdim],
118112
lon : np.ndarray or list
119113
Longitude coordinates (numpy vector or array) of the field (only if grid is None)
120114
lat : np.ndarray or list
@@ -138,8 +132,6 @@ class Field:
138132
mesh and time_origin information. Can be constructed from any of the Grid objects
139133
fieldtype : str
140134
Type of Field to be used for UnitConverter (either 'U', 'V', 'Kh_zonal', 'Kh_meridional' or None)
141-
transpose : bool
142-
Transpose data to required (lon, lat) layout
143135
vmin : float
144136
Minimum allowed value on the field. Data below this value are set to zero
145137
vmax : float
@@ -183,7 +175,6 @@ def __init__(
183175
mesh: Mesh = "flat",
184176
timestamps=None,
185177
fieldtype=None,
186-
transpose: bool = False,
187178
vmin: float | None = None,
188179
vmax: float | None = None,
189180
cast_data_dtype: type[np.float32] | type[np.float64] | Literal["float32", "float64"] = "float32",
@@ -271,7 +262,7 @@ def __init__(
271262
)
272263

273264
if not self.grid.defer_load:
274-
self.data = self._reshape(self.data, transpose)
265+
self.data = self._reshape(self.data)
275266
self._loaded_time_indices = range(self.grid.tdim)
276267

277268
# Hack around the fact that NaN and ridiculously large values
@@ -668,7 +659,7 @@ def from_netcdf(
668659
errormessage = (
669660
f"Field {filebuffer.name} expecting a data shape of [tdim={grid.tdim}, zdim={grid.zdim}, "
670661
f"ydim={grid.ydim - 2 * grid.meridional_halo}, xdim={grid.xdim - 2 * grid.zonal_halo}] "
671-
f"but got shape {buffer_data.shape}. Flag transpose=True could help to reorder the data."
662+
f"but got shape {buffer_data.shape}."
672663
)
673664
assert buffer_data.shape[0] == grid.tdim, errormessage
674665
assert buffer_data.shape[2] == grid.ydim - 2 * grid.meridional_halo, errormessage
@@ -768,7 +759,7 @@ def from_xarray(
768759
**kwargs,
769760
)
770761

771-
def _reshape(self, data, transpose=False):
762+
def _reshape(self, data):
772763
# Ensure that field data is the right data type
773764
if not isinstance(data, (np.ndarray, da.core.Array)):
774765
data = np.array(data)
@@ -777,8 +768,6 @@ def _reshape(self, data, transpose=False):
777768
elif (self.cast_data_dtype == np.float64) and (data.dtype != np.float64):
778769
data = data.astype(np.float64)
779770
lib = np if isinstance(data, np.ndarray) else da
780-
if transpose:
781-
data = lib.transpose(data)
782771
if self.grid._lat_flipped:
783772
data = lib.flip(data, axis=-2)
784773

@@ -803,10 +792,7 @@ def _reshape(self, data, transpose=False):
803792
if len(data.shape) == 4:
804793
data = data.reshape(sum(((data.shape[0],), data.shape[2:]), ()))
805794
if len(data.shape) == 4:
806-
errormessage = (
807-
f"Field {self.name} expecting a data shape of [tdim, zdim, ydim, xdim]. "
808-
"Flag transpose=True could help to reorder the data."
809-
)
795+
errormessage = f"Field {self.name} expecting a data shape of [tdim, zdim, ydim, xdim]. "
810796
assert data.shape[0] == self.grid.tdim, errormessage
811797
assert data.shape[2] == self.grid.ydim - 2 * self.grid.meridional_halo, errormessage
812798
assert data.shape[3] == self.grid.xdim - 2 * self.grid.zonal_halo, errormessage
@@ -819,10 +805,7 @@ def _reshape(self, data, transpose=False):
819805
self.grid.tdim,
820806
self.grid.ydim - 2 * self.grid.meridional_halo,
821807
self.grid.xdim - 2 * self.grid.zonal_halo,
822-
), (
823-
f"Field {self.name} expecting a data shape of [tdim, ydim, xdim]. "
824-
"Flag transpose=True could help to reorder the data."
825-
)
808+
), f"Field {self.name} expecting a data shape of [tdim, ydim, xdim]. "
826809
if self.grid.meridional_halo > 0 or self.grid.zonal_halo > 0:
827810
data = self.add_periodic_halo(
828811
zonal=self.grid.zonal_halo > 0,

parcels/fieldset.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ def from_data(
7474
cls,
7575
data,
7676
dimensions,
77-
transpose=False,
7877
mesh: Mesh = "spherical",
7978
allow_time_extrapolation: bool | None = None,
8079
**kwargs,
@@ -86,21 +85,14 @@ def from_data(
8685
data :
8786
Dictionary mapping field names to numpy arrays.
8887
Note that at least a 'U' and 'V' numpy array need to be given, and that
89-
the built-in Advection kernels assume that U and V are in m/s
90-
91-
1. If data shape is [xdim, ydim], [xdim, ydim, zdim], [xdim, ydim, tdim] or [xdim, ydim, zdim, tdim],
92-
whichever is relevant for the dataset, use the flag transpose=True
93-
2. If data shape is [ydim, xdim], [zdim, ydim, xdim], [tdim, ydim, xdim] or [tdim, zdim, ydim, xdim],
94-
use the flag transpose=False (default value)
95-
3. If data has any other shape, you first need to reorder it
88+
the built-in Advection kernels assume that U and V are in m/s.
89+
Data shape is either [ydim, xdim], [zdim, ydim, xdim], [tdim, ydim, xdim] or [tdim, zdim, ydim, xdim],
9690
dimensions : dict
9791
Dictionary mapping field dimensions (lon,
9892
lat, depth, time) to numpy arrays.
9993
Note that dimensions can also be a dictionary of dictionaries if
10094
dimension names are different for each variable
10195
(e.g. dimensions['U'], dimensions['V'], etc).
102-
transpose : bool
103-
Whether to transpose data on read-in (Default value = False)
10496
mesh : str
10597
String indicating the type of mesh coordinates and
10698
units used during velocity interpolation, see also `this tutorial <../examples/tutorial_unitconverters.ipynb>`__:
@@ -154,7 +146,6 @@ def from_data(
154146
name,
155147
datafld,
156148
grid=grid,
157-
transpose=transpose,
158149
allow_time_extrapolation=allow_time_extrapolation,
159150
**kwargs,
160151
)

tests/test_advection.py

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -61,22 +61,22 @@ def test_advection_zonal(lon, lat, depth):
6161
"""Particles at high latitude move geographically faster due to the pole correction in `GeographicPolar`."""
6262
npart = 10
6363
data2D = {
64-
"U": np.ones((lon.size, lat.size), dtype=np.float32),
65-
"V": np.zeros((lon.size, lat.size), dtype=np.float32),
64+
"U": np.ones((lat.size, lon.size), dtype=np.float32),
65+
"V": np.zeros((lat.size, lon.size), dtype=np.float32),
6666
}
6767
data3D = {
68-
"U": np.ones((lon.size, lat.size, depth.size), dtype=np.float32),
69-
"V": np.zeros((lon.size, lat.size, depth.size), dtype=np.float32),
68+
"U": np.ones((depth.size, lat.size, lon.size), dtype=np.float32),
69+
"V": np.zeros((depth.size, lat.size, lon.size), dtype=np.float32),
7070
}
7171
dimensions = {"lon": lon, "lat": lat}
72-
fieldset2D = FieldSet.from_data(data2D, dimensions, mesh="spherical", transpose=True)
72+
fieldset2D = FieldSet.from_data(data2D, dimensions, mesh="spherical")
7373

7474
pset2D = ParticleSet(fieldset2D, pclass=Particle, lon=np.zeros(npart) + 20.0, lat=np.linspace(0, 80, npart))
7575
pset2D.execute(AdvectionRK4, runtime=timedelta(hours=2), dt=timedelta(seconds=30))
7676
assert (np.diff(pset2D.lon) > 1.0e-4).all()
7777

7878
dimensions["depth"] = depth
79-
fieldset3D = FieldSet.from_data(data3D, dimensions, mesh="spherical", transpose=True)
79+
fieldset3D = FieldSet.from_data(data3D, dimensions, mesh="spherical")
8080
pset3D = ParticleSet(
8181
fieldset3D,
8282
pclass=Particle,
@@ -91,9 +91,9 @@ def test_advection_zonal(lon, lat, depth):
9191
def test_advection_meridional(lon, lat):
9292
"""Particles at high latitude move geographically faster due to the pole correction in `GeographicPolar`."""
9393
npart = 10
94-
data = {"U": np.zeros((lon.size, lat.size), dtype=np.float32), "V": np.ones((lon.size, lat.size), dtype=np.float32)}
94+
data = {"U": np.zeros((lat.size, lon.size), dtype=np.float32), "V": np.ones((lat.size, lon.size), dtype=np.float32)}
9595
dimensions = {"lon": lon, "lat": lat}
96-
fieldset = FieldSet.from_data(data, dimensions, mesh="spherical", transpose=True)
96+
fieldset = FieldSet.from_data(data, dimensions, mesh="spherical")
9797

9898
pset = ParticleSet(fieldset, pclass=Particle, lon=np.linspace(-60, 60, npart), lat=np.linspace(0, 30, npart))
9999
delta_lat = np.diff(pset.lat)
@@ -110,9 +110,9 @@ def test_advection_3D():
110110
"lat": np.linspace(0.0, 1e4, ydim, dtype=np.float32),
111111
"depth": np.linspace(0.0, 1.0, zdim, dtype=np.float32),
112112
}
113-
data = {"U": np.ones((xdim, ydim, zdim), dtype=np.float32), "V": np.zeros((xdim, ydim, zdim), dtype=np.float32)}
114-
data["U"][:, :, 0] = 0.0
115-
fieldset = FieldSet.from_data(data, dimensions, mesh="flat", transpose=True)
113+
data = {"U": np.ones((zdim, ydim, xdim), dtype=np.float32), "V": np.zeros((zdim, ydim, xdim), dtype=np.float32)}
114+
data["U"][0, :, :] = 0.0
115+
fieldset = FieldSet.from_data(data, dimensions, mesh="flat")
116116

117117
pset = ParticleSet(
118118
fieldset, pclass=Particle, lon=np.zeros(npart), lat=np.zeros(npart) + 1e2, depth=np.linspace(0, 1, npart)
@@ -171,11 +171,11 @@ def SubmergeParticle(particle, fieldset, time): # pragma: no cover
171171
def test_advection_RK45(lon, lat, rk45_tol):
172172
npart = 10
173173
data2D = {
174-
"U": np.ones((lon.size, lat.size), dtype=np.float32),
175-
"V": np.zeros((lon.size, lat.size), dtype=np.float32),
174+
"U": np.ones((lat.size, lon.size), dtype=np.float32),
175+
"V": np.zeros((lat.size, lon.size), dtype=np.float32),
176176
}
177177
dimensions = {"lon": lon, "lat": lat}
178-
fieldset = FieldSet.from_data(data2D, dimensions, mesh="spherical", transpose=True)
178+
fieldset = FieldSet.from_data(data2D, dimensions, mesh="spherical")
179179
fieldset.add_constant("RK45_tol", rk45_tol)
180180

181181
dt = timedelta(seconds=30).total_seconds()
@@ -266,8 +266,8 @@ def create_periodic_fieldset(xdim, ydim, uvel, vvel):
266266
"lat": np.linspace(0.0, 1.0, ydim + 1, dtype=np.float32)[1:],
267267
}
268268

269-
data = {"U": uvel * np.ones((xdim, ydim), dtype=np.float32), "V": vvel * np.ones((xdim, ydim), dtype=np.float32)}
270-
return FieldSet.from_data(data, dimensions, mesh="spherical", transpose=True)
269+
data = {"U": uvel * np.ones((ydim, xdim), dtype=np.float32), "V": vvel * np.ones((ydim, xdim), dtype=np.float32)}
270+
return FieldSet.from_data(data, dimensions, mesh="spherical")
271271

272272

273273
def periodicBC(particle, fieldset, time): # pragma: no cover
@@ -373,10 +373,10 @@ def create_fieldset_stationary(xdim=100, ydim=100, maxtime=timedelta(hours=6)):
373373
"time": time,
374374
}
375375
data = {
376-
"U": np.ones((xdim, ydim, 1), dtype=np.float32) * u_0 * np.cos(f * time),
377-
"V": np.ones((xdim, ydim, 1), dtype=np.float32) * -u_0 * np.sin(f * time),
376+
"U": np.transpose(np.ones((xdim, ydim, 1), dtype=np.float32) * u_0 * np.cos(f * time)),
377+
"V": np.transpose(np.ones((xdim, ydim, 1), dtype=np.float32) * -u_0 * np.sin(f * time)),
378378
}
379-
fieldset = FieldSet.from_data(data, dimensions, mesh="flat", transpose=True)
379+
fieldset = FieldSet.from_data(data, dimensions, mesh="flat")
380380
# setting some constants for AdvectionRK45 kernel
381381
fieldset.RK45_min_dt = 1e-3
382382
fieldset.RK45_max_dt = 1e2
@@ -435,13 +435,13 @@ def test_stationary_eddy_vertical():
435435
lon_data = np.linspace(0, 25000, xdim, dtype=np.float32)
436436
lat_data = np.linspace(0, 25000, ydim, dtype=np.float32)
437437
time_data = np.arange(0.0, 6 * 3600 + 1e-5, 60.0, dtype=np.float64)
438-
fld1 = np.ones((xdim, ydim, 1), dtype=np.float32) * u_0 * np.cos(f * time_data)
439-
fld2 = np.ones((xdim, ydim, 1), dtype=np.float32) * -u_0 * np.sin(f * time_data)
440-
fldzero = np.zeros((xdim, ydim, 1), dtype=np.float32) * time_data
438+
fld1 = np.transpose(np.ones((xdim, ydim, 1), dtype=np.float32) * u_0 * np.cos(f * time_data))
439+
fld2 = np.transpose(np.ones((xdim, ydim, 1), dtype=np.float32) * -u_0 * np.sin(f * time_data))
440+
fldzero = np.transpose(np.zeros((xdim, ydim, 1), dtype=np.float32) * time_data)
441441

442442
dimensions = {"lon": lon_data, "lat": lat_data, "time": time_data}
443443
data = {"U": fld1, "V": fldzero, "W": fld2}
444-
fieldset = FieldSet.from_data(data, dimensions, mesh="flat", transpose=True)
444+
fieldset = FieldSet.from_data(data, dimensions, mesh="flat")
445445

446446
pset = ParticleSet(fieldset, pclass=Particle, lon=lon, lat=lat, depth=depth)
447447
pset.execute(AdvectionRK4_3D, dt=dt, endtime=endtime)
@@ -453,7 +453,7 @@ def test_stationary_eddy_vertical():
453453
assert np.allclose(pset.depth, exp_depth, rtol=1e-5)
454454

455455
data = {"U": fldzero, "V": fld2, "W": fld1}
456-
fieldset = FieldSet.from_data(data, dimensions, mesh="flat", transpose=True)
456+
fieldset = FieldSet.from_data(data, dimensions, mesh="flat")
457457

458458
pset = ParticleSet(fieldset, pclass=Particle, lon=lon, lat=lat, depth=depth)
459459
pset.execute(AdvectionRK4_3D, dt=dt, endtime=endtime)
@@ -483,10 +483,10 @@ def create_fieldset_moving(xdim=100, ydim=100, maxtime=timedelta(hours=6)):
483483
"time": time,
484484
}
485485
data = {
486-
"U": np.ones((xdim, ydim, 1), dtype=np.float32) * u_g + (u_0 - u_g) * np.cos(f * time),
487-
"V": np.ones((xdim, ydim, 1), dtype=np.float32) * -(u_0 - u_g) * np.sin(f * time),
486+
"U": np.transpose(np.ones((xdim, ydim, 1), dtype=np.float32) * u_g + (u_0 - u_g) * np.cos(f * time)),
487+
"V": np.transpose(np.ones((xdim, ydim, 1), dtype=np.float32) * -(u_0 - u_g) * np.sin(f * time)),
488488
}
489-
return FieldSet.from_data(data, dimensions, mesh="flat", transpose=True)
489+
return FieldSet.from_data(data, dimensions, mesh="flat")
490490

491491

492492
@pytest.fixture
@@ -555,11 +555,15 @@ def create_fieldset_decaying(xdim=100, ydim=100, maxtime=timedelta(hours=6)):
555555
"time": time,
556556
}
557557
data = {
558-
"U": np.ones((xdim, ydim, 1), dtype=np.float32) * u_g * np.exp(-gamma_g * time)
559-
+ (u_0 - u_g) * np.exp(-gamma * time) * np.cos(f * time),
560-
"V": np.ones((xdim, ydim, 1), dtype=np.float32) * -(u_0 - u_g) * np.exp(-gamma * time) * np.sin(f * time),
558+
"U": np.transpose(
559+
np.ones((xdim, ydim, 1), dtype=np.float32) * u_g * np.exp(-gamma_g * time)
560+
+ (u_0 - u_g) * np.exp(-gamma * time) * np.cos(f * time)
561+
),
562+
"V": np.transpose(
563+
np.ones((xdim, ydim, 1), dtype=np.float32) * -(u_0 - u_g) * np.exp(-gamma * time) * np.sin(f * time)
564+
),
561565
}
562-
return FieldSet.from_data(data, dimensions, mesh="flat", transpose=True)
566+
return FieldSet.from_data(data, dimensions, mesh="flat")
563567

564568

565569
@pytest.fixture

tests/test_data/create_testfields.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ def generate_testfieldset(xdim, ydim, zdim, tdim):
2929
lat = np.linspace(0.0, 1.0, ydim, dtype=np.float32)
3030
depth = np.linspace(0.0, 0.5, zdim, dtype=np.float32)
3131
time = np.linspace(0.0, tdim, tdim, dtype=np.float64)
32-
U = np.ones((xdim, ydim, zdim, tdim), dtype=np.float32)
33-
V = np.zeros((xdim, ydim, zdim, tdim), dtype=np.float32)
34-
P = 2.0 * np.ones((xdim, ydim, zdim, tdim), dtype=np.float32)
32+
U = np.ones((tdim, zdim, ydim, xdim), dtype=np.float32)
33+
V = np.zeros((tdim, zdim, ydim, xdim), dtype=np.float32)
34+
P = 2.0 * np.ones((tdim, zdim, ydim, xdim), dtype=np.float32)
3535
data = {"U": U, "V": V, "P": P}
3636
dimensions = {"lon": lon, "lat": lat, "depth": depth, "time": time}
37-
fieldset = FieldSet.from_data(data, dimensions, mesh="flat", transpose=True)
37+
fieldset = FieldSet.from_data(data, dimensions, mesh="flat")
3838
fieldset.write("testfields")
3939

4040

@@ -66,7 +66,7 @@ def generate_perlin_testfield():
6666
if asizeof is not None:
6767
print(f"Perlin U-field requires {U.size * U.itemsize} bytes of memory.")
6868
print(f"Perlin V-field requires {V.size * V.itemsize} bytes of memory.")
69-
fieldset = FieldSet.from_data(data, dimensions, mesh="spherical", transpose=False)
69+
fieldset = FieldSet.from_data(data, dimensions, mesh="spherical")
7070
# fieldset.write("perlinfields") # can also be used, but then has a ghost depth dimension
7171
write_simple_2Dt(fieldset.U, os.path.join(os.path.dirname(__file__), "perlinfields"), varname="vozocrtx")
7272
write_simple_2Dt(fieldset.V, os.path.join(os.path.dirname(__file__), "perlinfields"), varname="vomecrty")

0 commit comments

Comments
 (0)