Skip to content

Commit f07151d

Browse files
committed
Merge branch 'master' into fix-mitgcm-time-dimension
2 parents f0f532c + 915f6a3 commit f07151d

9 files changed

Lines changed: 133 additions & 55 deletions

parcels/examples/example_nemo_curvilinear.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ def run_nemo_curvilinear(mode, outfile):
1212
"""Function that shows how to read in curvilinear grids, in this case from NEMO"""
1313
data_path = path.join(path.dirname(__file__), 'NemoCurvilinear_data/')
1414

15-
filenames = {'U': data_path + 'U_purely_zonal-ORCA025_grid_U.nc4',
16-
'V': data_path + 'V_purely_zonal-ORCA025_grid_V.nc4',
17-
'mesh_mask': data_path + 'mesh_mask.nc4'}
15+
filenames = {'U': {'lon': data_path + 'mesh_mask.nc4',
16+
'lat': data_path + 'mesh_mask.nc4',
17+
'data': data_path + 'U_purely_zonal-ORCA025_grid_U.nc4'},
18+
'V': {'lon': data_path + 'mesh_mask.nc4',
19+
'lat': data_path + 'mesh_mask.nc4',
20+
'data': data_path + 'V_purely_zonal-ORCA025_grid_V.nc4'}}
1821
variables = {'U': 'U', 'V': 'V'}
1922
dimensions = {'lon': 'glamf', 'lat': 'gphif'}
2023
field_set = FieldSet.from_nemo(filenames, variables, dimensions)

parcels/examples/tutorial_nemo_curvilinear.ipynb

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,17 @@
3333
"cell_type": "markdown",
3434
"metadata": {},
3535
"source": [
36-
"We can create a `FieldSet` just like we do for normal grids. Note that we need to provide an extra filename for the `mesh_mask` file that holds information on the Curvilinear grid."
36+
"We can create a `FieldSet` just like we do for normal grids.\n",
37+
"Note that NEMO is discretised on a C-grid. U and V velocities are not located on the same nodes (see https://www.nemo-ocean.eu/doc/node19.html ).\n",
38+
"\n",
39+
"```\n",
40+
" __V1__\n",
41+
"| |\n",
42+
"U0 U1\n",
43+
"|__V0__|\n",
44+
"```\n",
45+
"\n",
46+
"To interpolate U, V velocities on the C-grid, Parcels needs to read the f-nodes, which are located on the corners of the cells (for indexing details: https://www.nemo-ocean.eu/doc/img360.png )."
3747
]
3848
},
3949
{
@@ -52,9 +62,13 @@
5262
}
5363
],
5464
"source": [
55-
"filenames = {'U': 'NemoCurvilinear_data/U_purely_zonal-ORCA025_grid_U.nc4',\n",
56-
" 'V': 'NemoCurvilinear_data/V_purely_zonal-ORCA025_grid_V.nc4',\n",
57-
" 'mesh_mask': 'NemoCurvilinear_data/mesh_mask.nc4'}\n",
65+
"data_path = 'NemoCurvilinear_data/'\n",
66+
"filenames = {'U': {'lon': data_path + 'mesh_mask.nc4',\n",
67+
" 'lat': data_path + 'mesh_mask.nc4',\n",
68+
" 'data': data_path + 'U_purely_zonal-ORCA025_grid_U.nc4'},\n",
69+
" 'V': {'lon': data_path + 'mesh_mask.nc4',\n",
70+
" 'lat': data_path + 'mesh_mask.nc4',\n",
71+
" 'data': data_path + 'V_purely_zonal-ORCA025_grid_V.nc4'}}\n",
5872
"variables = {'U': 'U',\n",
5973
" 'V': 'V'}\n",
6074
"dimensions = {'lon': 'glamf', 'lat': 'gphif', 'time': 'time_counter'}\n",

parcels/field.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,14 @@ def __init__(self, name, data, lon=None, lat=None, depth=None, time=None, grid=N
125125
@classmethod
126126
def from_netcdf(cls, filenames, variable, dimensions, indices=None, grid=None,
127127
mesh='spherical', allow_time_extrapolation=None, time_periodic=False,
128-
full_load=False, dimension_filename=None, **kwargs):
128+
full_load=False, **kwargs):
129129
"""Create field from netCDF file
130130
131131
:param filenames: list of filenames to read for the field.
132132
Note that wildcards ('*') are also allowed
133+
filenames can be a list [files]
134+
or a dictionary {dim:[files]} (if lon, lat, depth and/or data not stored in same files as data)
135+
time values are in filenames[data]
133136
:param variable: Name of the field to create. Note that this has to be a string
134137
:param dimensions: Dictionary mapping variable names for the relevant dimensions in the NetCDF file
135138
:param indices: dictionary mapping indices for each dimension to read from file.
@@ -155,26 +158,49 @@ def from_netcdf(cls, filenames, variable, dimensions, indices=None, grid=None,
155158

156159
if not isinstance(filenames, Iterable) or isinstance(filenames, str):
157160
filenames = [filenames]
158-
dimension_filename = dimension_filename if dimension_filename else filenames[0]
161+
162+
data_filenames = filenames['data'] if type(filenames) is dict else filenames
163+
if type(filenames) == dict:
164+
for k in filenames.keys():
165+
assert k in ['lon', 'lat', 'depth', 'data'], \
166+
'filename dimension keys must be lon, lat, depth or data'
167+
assert len(filenames['lon']) == 1
168+
if filenames['lon'] != filenames['lat']:
169+
raise NotImplementedError('longitude and latitude dimensions are currently processed together from one single file')
170+
lonlat_filename = filenames['lon'][0]
171+
if 'depth' in dimensions:
172+
depth_filename = filenames['depth'][0]
173+
if len(depth_filename) != 1:
174+
raise NotImplementedError('Vertically adaptive meshes not implemented for from_netcdf()')
175+
else:
176+
lonlat_filename = filenames[0]
177+
depth_filename = filenames[0]
178+
159179
indices = {} if indices is None else indices.copy()
160180
netcdf_engine = kwargs.pop('netcdf_engine', 'netcdf4')
161-
with NetcdfFileBuffer(dimension_filename, dimensions, indices, netcdf_engine) as filebuffer:
181+
with NetcdfFileBuffer(lonlat_filename, dimensions, indices, netcdf_engine) as filebuffer:
162182
lon, lat = filebuffer.read_lonlat
163-
depth = filebuffer.read_depth
164183
indices = filebuffer.indices
165184
# Check if parcels_mesh has been explicitly set in file
166185
if 'parcels_mesh' in filebuffer.dataset.attrs:
167186
mesh = filebuffer.dataset.attrs['parcels_mesh']
168187

169-
if len(filenames) > 1 and 'time' not in dimensions:
188+
if 'depth' in dimensions:
189+
with NetcdfFileBuffer(depth_filename, dimensions, indices, netcdf_engine) as filebuffer:
190+
depth = filebuffer.read_depth
191+
else:
192+
indices['depth'] = [0]
193+
depth = np.zeros(1)
194+
195+
if len(data_filenames) > 1 and 'time' not in dimensions:
170196
raise RuntimeError('Multiple files given but no time dimension specified')
171197

172198
if grid is None:
173199
# Concatenate time variable to determine overall dimension
174200
# across multiple files
175201
timeslices = []
176202
dataFiles = []
177-
for fname in filenames:
203+
for fname in data_filenames:
178204
with NetcdfFileBuffer(fname, dimensions, indices, netcdf_engine) as filebuffer:
179205
ftime = filebuffer.time
180206
timeslices.append(ftime)
@@ -208,7 +234,7 @@ def from_netcdf(cls, filenames, variable, dimensions, indices=None, grid=None,
208234
# Pre-allocate data before reading files into buffer
209235
data = np.empty((grid.tdim, grid.zdim, grid.ydim, grid.xdim), dtype=np.float32)
210236
ti = 0
211-
for tslice, fname in zip(grid.timeslices, filenames):
237+
for tslice, fname in zip(grid.timeslices, data_filenames):
212238
with NetcdfFileBuffer(fname, dimensions, indices, netcdf_engine) as filebuffer:
213239
# If Field.from_netcdf is called directly, it may not have a 'data' dimension
214240
# In that case, assume that 'name' is the data dimension
@@ -910,7 +936,6 @@ def __init__(self, name, U, V, W=None):
910936
assert self.U.grid is self.V.grid
911937
if W:
912938
assert self.W.interp_method == 'cgrid_linear'
913-
assert self.U.grid is self.W.grid
914939

915940
def dist(self, lon1, lon2, lat1, lat2, mesh):
916941
if mesh == 'spherical':

parcels/fieldset.py

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,18 @@ def check_complete(self):
140140
else:
141141
self.add_vector_field(VectorField('UVW', self.U, self.V, self.W))
142142

143+
@classmethod
144+
def parse_wildcards(cls, paths, filenames, var):
145+
if not isinstance(paths, list):
146+
paths = sorted(glob(str(paths)))
147+
if len(paths) == 0:
148+
notfound_paths = filenames[var] if type(filenames) is dict and var in filenames else filenames
149+
raise IOError("FieldSet files not found: %s" % str(notfound_paths))
150+
for fp in paths:
151+
if not path.exists(fp):
152+
raise IOError("FieldSet file not found: %s" % str(fp))
153+
return paths
154+
143155
@classmethod
144156
def from_netcdf(cls, filenames, variables, dimensions, indices=None,
145157
mesh='spherical', allow_time_extrapolation=None, time_periodic=False, full_load=False, **kwargs):
@@ -148,6 +160,10 @@ def from_netcdf(cls, filenames, variables, dimensions, indices=None,
148160
:param filenames: Dictionary mapping variables to file(s). The
149161
filepath may contain wildcards to indicate multiple files,
150162
or be a list of file.
163+
filenames can be a list [files], a dictionary {var:[files]},
164+
a dictionary {dim:[files]} (if lon, lat, depth and/or data not stored in same files as data),
165+
or a dictionary of dictionaries {var:{dim:[files]}}.
166+
time values are in filenames[data]
151167
:param variables: Dictionary mapping variables to variable
152168
names in the netCDF file(s).
153169
:param dimensions: Dictionary mapping data dimensions (lon,
@@ -180,15 +196,12 @@ def from_netcdf(cls, filenames, variables, dimensions, indices=None,
180196
fields = {}
181197
for var, name in variables.items():
182198
# Resolve all matching paths for the current variable
183-
paths = filenames[var] if type(filenames) is dict else filenames
184-
if not isinstance(paths, list):
185-
paths = sorted(glob(str(paths)))
186-
if len(paths) == 0:
187-
notfound_paths = filenames[var] if type(filenames) is dict else filenames
188-
raise IOError("FieldSet files not found: %s" % str(notfound_paths))
189-
for fp in paths:
190-
if not path.exists(fp):
191-
raise IOError("FieldSet file not found: %s" % str(fp))
199+
paths = filenames[var] if type(filenames) is dict and var in filenames else filenames
200+
if type(paths) is not dict:
201+
paths = cls.parse_wildcards(paths, filenames, var)
202+
else:
203+
for dim, p in paths.items():
204+
paths[dim] = cls.parse_wildcards(p, filenames, var)
192205

193206
# Use dimensions[var] and indices[var] if either of them is a dict of dicts
194207
dims = dimensions[var] if var in dimensions else dimensions
@@ -200,11 +213,19 @@ def from_netcdf(cls, filenames, variables, dimensions, indices=None,
200213
for procvar, _ in fields.items():
201214
procdims = dimensions[procvar] if procvar in dimensions else dimensions
202215
procinds = indices[procvar] if (indices and procvar in indices) else indices
203-
if (type(filenames) is not dict or filenames[procvar] == filenames[var]) \
204-
and procdims == dims and procinds == inds:
205-
grid = fields[procvar].grid
206-
kwargs['dataFiles'] = fields[procvar].dataFiles
207-
break
216+
if procdims == dims and procinds == inds:
217+
sameGrid = False
218+
if (type(filenames) is not dict or filenames[procvar] == filenames[var]):
219+
sameGrid = True
220+
elif type(filenames[procvar]) == dict:
221+
sameGrid = True
222+
for dim in ['lon', 'lat', 'depth']:
223+
if dim in dimensions:
224+
sameGrid *= filenames[procvar][dim] == filenames[var][dim]
225+
if sameGrid:
226+
grid = fields[procvar].grid
227+
kwargs['dataFiles'] = fields[procvar].dataFiles
228+
break
208229
fields[var] = Field.from_netcdf(paths, var, dims, inds, grid=grid, mesh=mesh,
209230
allow_time_extrapolation=allow_time_extrapolation,
210231
time_periodic=time_periodic, full_load=full_load, **kwargs)
@@ -217,19 +238,29 @@ def from_nemo(cls, filenames, variables, dimensions, indices=None, mesh='spheric
217238
allow_time_extrapolation=None, time_periodic=False,
218239
tracer_interp_method='linear', **kwargs):
219240
"""Initialises FieldSet object from NetCDF files of Curvilinear NEMO fields.
220-
Note that this assumes there is a variable mesh_mask that is used for the dimensions
221241
222242
:param filenames: Dictionary mapping variables to file(s). The
223243
filepath may contain wildcards to indicate multiple files,
224-
or be a list of file. At least a 'mesh_mask' needs to be present
244+
or be a list of file.
245+
filenames can be a list [files], a dictionary {var:[files]},
246+
a dictionary {dim:[files]} (if lon, lat, depth and/or data not stored in same files as data),
247+
or a dictionary of dictionaries {var:{dim:[files]}}
248+
time values are in filenames[data]
225249
:param variables: Dictionary mapping variables to variable
226-
names in the netCDF file(s). Must include a variable 'mesh_mask' that
227-
holds the dimensions
250+
names in the netCDF file(s).
228251
:param dimensions: Dictionary mapping data dimensions (lon,
229252
lat, depth, time, data) to dimensions in the netCF file(s).
230253
Note that dimensions can also be a dictionary of dictionaries if
231-
dimension names are different for each variable
232-
(e.g. dimensions['U'], dimensions['V'], etc).
254+
dimension names are different for each variable.
255+
Watch out: NEMO is discretised on a C-grid:
256+
U and V velocities are not located on the same nodes (see https://www.nemo-ocean.eu/doc/node19.html ).
257+
__V1__
258+
| |
259+
U0 U1
260+
|__V0__|
261+
To interpolate U, V velocities on the C-grid, Parcels needs to read the f-nodes,
262+
which are located on the corners of the cells.
263+
(for indexing details: https://www.nemo-ocean.eu/doc/img360.png )
233264
:param indices: Optional dictionary of indices for each dimension
234265
to read from file(s), to allow for reading of subset of data.
235266
Default is to read the full extent of each dimension.
@@ -249,7 +280,8 @@ def from_nemo(cls, filenames, variables, dimensions, indices=None, mesh='spheric
249280
250281
"""
251282

252-
dimension_filename = filenames.pop('mesh_mask') if type(filenames) is dict else filenames
283+
if 'U' in dimensions and 'V' in dimensions and dimensions['U'] != dimensions['V']:
284+
raise RuntimeError("On a c-grid discretisation like NEMO, U and V should have the same dimensions")
253285

254286
interp_method = {}
255287
for v in variables:
@@ -259,8 +291,7 @@ def from_nemo(cls, filenames, variables, dimensions, indices=None, mesh='spheric
259291
interp_method[v] = tracer_interp_method
260292

261293
return cls.from_netcdf(filenames, variables, dimensions, mesh=mesh, indices=indices, time_periodic=time_periodic,
262-
allow_time_extrapolation=allow_time_extrapolation, interp_method=interp_method,
263-
dimension_filename=dimension_filename, **kwargs)
294+
allow_time_extrapolation=allow_time_extrapolation, interp_method=interp_method, **kwargs)
264295

265296
@classmethod
266297
def from_parcels(cls, basename, uvar='vozocrtx', vvar='vomecrty', indices=None, extra_fields=None,

parcels/scripts/plottrajectoriesfile.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def plotTrajectoriesFile(filename, mode='2d', tracerfile=None, tracerfield='P',
100100
else:
101101
scat = ax.scatter(lon[b], lat[b], s=20, color='k')
102102
ttl = ax.set_title('Particles' + titlestr + ' at time ' + str(plottimes[0]))
103-
frames = np.arange(1, len(plottimes))
103+
frames = np.arange(0, len(plottimes))
104104

105105
def animate(t):
106106
b = time == plottimes[t]

tests/test_fieldset.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ class MyParticle(ptype[mode]):
296296
assert np.allclose(temp_theo, pset.particles[0].temp, atol=1e-5)
297297

298298

299-
@pytest.mark.parametrize('fail', [False, pytest.mark.xfail(strict=True)(True)])
299+
@pytest.mark.parametrize('fail', [False, pytest.param(True, marks=pytest.mark.xfail(strict=True))])
300300
def test_fieldset_defer_loading_with_diff_time_origin(tmpdir, fail, filename='test_parcels_defer_loading'):
301301
filepath = tmpdir.join(filename)
302302
data0, dims0 = generate_fieldset(10, 10, 1, 10)

tests/test_fieldset_sampling.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def test_variable_init_from_field(mode, npart=9):
118118
'P': np.zeros(dims, dtype=np.float32)}
119119
data['P'][0, 0] = 1.
120120
fieldset = FieldSet.from_data(data, dimensions, mesh='flat', transpose=True)
121-
xv, yv = np.meshgrid(np.linspace(0, 1, np.sqrt(npart)), np.linspace(0, 1, np.sqrt(npart)))
121+
xv, yv = np.meshgrid(np.linspace(0, 1, int(np.sqrt(npart))), np.linspace(0, 1, int(np.sqrt(npart))))
122122

123123
class VarParticle(pclass(mode)):
124124
a = Variable('a', dtype=np.float32, initial=fieldset.P)
@@ -161,7 +161,7 @@ def test_nearest_neighbour_interpolation2D(mode, k_sample_p, npart=81):
161161
data['P'][0, 1] = 1.
162162
fieldset = FieldSet.from_data(data, dimensions, mesh='flat', transpose=True)
163163
fieldset.P.interp_method = 'nearest'
164-
xv, yv = np.meshgrid(np.linspace(0., 1.0, np.sqrt(npart)), np.linspace(0., 1.0, np.sqrt(npart)))
164+
xv, yv = np.meshgrid(np.linspace(0., 1.0, int(np.sqrt(npart))), np.linspace(0., 1.0, int(np.sqrt(npart))))
165165
pset = ParticleSet(fieldset, pclass=pclass(mode),
166166
lon=xv.flatten(), lat=yv.flatten())
167167
pset.execute(k_sample_p, endtime=1, dt=1)
@@ -181,7 +181,7 @@ def test_nearest_neighbour_interpolation3D(mode, k_sample_p, npart=81):
181181
data['P'][0, 1, 1] = 1.
182182
fieldset = FieldSet.from_data(data, dimensions, mesh='flat', transpose=True)
183183
fieldset.P.interp_method = 'nearest'
184-
xv, yv = np.meshgrid(np.linspace(0, 1.0, np.sqrt(npart)), np.linspace(0, 1.0, np.sqrt(npart)))
184+
xv, yv = np.meshgrid(np.linspace(0, 1.0, int(np.sqrt(npart))), np.linspace(0, 1.0, int(np.sqrt(npart))))
185185
# combine a pset at 0m with pset at 1m, as meshgrid does not do 3D
186186
pset = ParticleSet(fieldset, pclass=pclass(mode), lon=xv.flatten(), lat=yv.flatten(), depth=np.zeros(npart))
187187
pset2 = ParticleSet(fieldset, pclass=pclass(mode), lon=xv.flatten(), lat=yv.flatten(), depth=np.ones(npart))

0 commit comments

Comments
 (0)