Skip to content

Commit e4f2605

Browse files
committed
Merge branch 'master' into new_xarray
2 parents ef05e15 + 2368add commit e4f2605

6 files changed

Lines changed: 126 additions & 47 deletions

File tree

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
@@ -124,11 +124,14 @@ def __init__(self, name, data, lon=None, lat=None, depth=None, time=None, grid=N
124124
@classmethod
125125
def from_netcdf(cls, filenames, variable, dimensions, indices=None, grid=None,
126126
mesh='spherical', allow_time_extrapolation=None, time_periodic=False,
127-
full_load=False, dimension_filename=None, **kwargs):
127+
full_load=False, **kwargs):
128128
"""Create field from netCDF file
129129
130130
:param filenames: list of filenames to read for the field.
131131
Note that wildcards ('*') are also allowed
132+
filenames can be a list [files]
133+
or a dictionary {dim:[files]} (if lon, lat, depth and/or data not stored in same files as data)
134+
time values are in filenames[data]
132135
:param variable: Name of the field to create. Note that this has to be a string
133136
:param dimensions: Dictionary mapping variable names for the relevant dimensions in the NetCDF file
134137
:param indices: dictionary mapping indices for each dimension to read from file.
@@ -152,25 +155,48 @@ def from_netcdf(cls, filenames, variable, dimensions, indices=None, grid=None,
152155

153156
if not isinstance(filenames, Iterable) or isinstance(filenames, str):
154157
filenames = [filenames]
155-
dimension_filename = dimension_filename if dimension_filename else filenames[0]
158+
159+
data_filenames = filenames['data'] if type(filenames) is dict else filenames
160+
if type(filenames) == dict:
161+
for k in filenames.keys():
162+
assert k in ['lon', 'lat', 'depth', 'data'], \
163+
'filename dimension keys must be lon, lat, depth or data'
164+
assert len(filenames['lon']) == 1
165+
if filenames['lon'] != filenames['lat']:
166+
raise NotImplementedError('longitude and latitude dimensions are currently processed together from one single file')
167+
lonlat_filename = filenames['lon'][0]
168+
if 'depth' in dimensions:
169+
depth_filename = filenames['depth'][0]
170+
if len(depth_filename) != 1:
171+
raise NotImplementedError('Vertically adaptive meshes not implemented for from_netcdf()')
172+
else:
173+
lonlat_filename = filenames[0]
174+
depth_filename = filenames[0]
175+
156176
indices = {} if indices is None else indices.copy()
157-
with NetcdfFileBuffer(dimension_filename, dimensions, indices) as filebuffer:
177+
with NetcdfFileBuffer(lonlat_filename, dimensions, indices) as filebuffer:
158178
lon, lat = filebuffer.read_lonlat
159-
depth = filebuffer.read_depth
160179
indices = filebuffer.indices
161180
# Check if parcels_mesh has been explicitly set in file
162181
if 'parcels_mesh' in filebuffer.dataset.attrs:
163182
mesh = filebuffer.dataset.attrs['parcels_mesh']
164183

165-
if len(filenames) > 1 and 'time' not in dimensions:
184+
if 'depth' in dimensions:
185+
with NetcdfFileBuffer(depth_filename, dimensions, indices) as filebuffer:
186+
depth = filebuffer.read_depth
187+
else:
188+
indices['depth'] = [0]
189+
depth = np.zeros(1)
190+
191+
if len(data_filenames) > 1 and 'time' not in dimensions:
166192
raise RuntimeError('Multiple files given but no time dimension specified')
167193

168194
if grid is None:
169195
# Concatenate time variable to determine overall dimension
170196
# across multiple files
171197
timeslices = []
172198
dataFiles = []
173-
for fname in filenames:
199+
for fname in data_filenames:
174200
with NetcdfFileBuffer(fname, dimensions, indices) as filebuffer:
175201
ftime = filebuffer.time
176202
timeslices.append(ftime)
@@ -204,7 +230,7 @@ def from_netcdf(cls, filenames, variable, dimensions, indices=None, grid=None,
204230
# Pre-allocate data before reading files into buffer
205231
data = np.empty((grid.tdim, grid.zdim, grid.ydim, grid.xdim), dtype=np.float32)
206232
ti = 0
207-
for tslice, fname in zip(grid.timeslices, filenames):
233+
for tslice, fname in zip(grid.timeslices, data_filenames):
208234
with NetcdfFileBuffer(fname, dimensions, indices) as filebuffer:
209235
# If Field.from_netcdf is called directly, it may not have a 'data' dimension
210236
# In that case, assume that 'name' is the data dimension
@@ -905,7 +931,6 @@ def __init__(self, name, U, V, W=None):
905931
assert self.U.grid is self.V.grid
906932
if W:
907933
assert self.W.interp_method == 'cgrid_linear'
908-
assert self.U.grid is self.W.grid
909934

910935
def dist(self, lon1, lon2, lat1, lat2, mesh):
911936
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,
@@ -178,15 +194,12 @@ def from_netcdf(cls, filenames, variables, dimensions, indices=None,
178194
fields = {}
179195
for var, name in variables.items():
180196
# Resolve all matching paths for the current variable
181-
paths = filenames[var] if type(filenames) is dict else filenames
182-
if not isinstance(paths, list):
183-
paths = sorted(glob(str(paths)))
184-
if len(paths) == 0:
185-
notfound_paths = filenames[var] if type(filenames) is dict else filenames
186-
raise IOError("FieldSet files not found: %s" % str(notfound_paths))
187-
for fp in paths:
188-
if not path.exists(fp):
189-
raise IOError("FieldSet file not found: %s" % str(fp))
197+
paths = filenames[var] if type(filenames) is dict and var in filenames else filenames
198+
if type(paths) is not dict:
199+
paths = cls.parse_wildcards(paths, filenames, var)
200+
else:
201+
for dim, p in paths.items():
202+
paths[dim] = cls.parse_wildcards(p, filenames, var)
190203

191204
# Use dimensions[var] and indices[var] if either of them is a dict of dicts
192205
dims = dimensions[var] if var in dimensions else dimensions
@@ -198,11 +211,19 @@ def from_netcdf(cls, filenames, variables, dimensions, indices=None,
198211
for procvar, _ in fields.items():
199212
procdims = dimensions[procvar] if procvar in dimensions else dimensions
200213
procinds = indices[procvar] if (indices and procvar in indices) else indices
201-
if (type(filenames) is not dict or filenames[procvar] == filenames[var]) \
202-
and procdims == dims and procinds == inds:
203-
grid = fields[procvar].grid
204-
kwargs['dataFiles'] = fields[procvar].dataFiles
205-
break
214+
if procdims == dims and procinds == inds:
215+
sameGrid = False
216+
if (type(filenames) is not dict or filenames[procvar] == filenames[var]):
217+
sameGrid = True
218+
elif type(filenames[procvar]) == dict:
219+
sameGrid = True
220+
for dim in ['lon', 'lat', 'depth']:
221+
if dim in dimensions:
222+
sameGrid *= filenames[procvar][dim] == filenames[var][dim]
223+
if sameGrid:
224+
grid = fields[procvar].grid
225+
kwargs['dataFiles'] = fields[procvar].dataFiles
226+
break
206227
fields[var] = Field.from_netcdf(paths, var, dims, inds, grid=grid, mesh=mesh,
207228
allow_time_extrapolation=allow_time_extrapolation,
208229
time_periodic=time_periodic, full_load=full_load, **kwargs)
@@ -215,19 +236,29 @@ def from_nemo(cls, filenames, variables, dimensions, indices=None, mesh='spheric
215236
allow_time_extrapolation=None, time_periodic=False,
216237
tracer_interp_method='linear', **kwargs):
217238
"""Initialises FieldSet object from NetCDF files of Curvilinear NEMO fields.
218-
Note that this assumes there is a variable mesh_mask that is used for the dimensions
219239
220240
:param filenames: Dictionary mapping variables to file(s). The
221241
filepath may contain wildcards to indicate multiple files,
222-
or be a list of file. At least a 'mesh_mask' needs to be present
242+
or be a list of file.
243+
filenames can be a list [files], a dictionary {var:[files]},
244+
a dictionary {dim:[files]} (if lon, lat, depth and/or data not stored in same files as data),
245+
or a dictionary of dictionaries {var:{dim:[files]}}
246+
time values are in filenames[data]
223247
:param variables: Dictionary mapping variables to variable
224-
names in the netCDF file(s). Must include a variable 'mesh_mask' that
225-
holds the dimensions
248+
names in the netCDF file(s).
226249
:param dimensions: Dictionary mapping data dimensions (lon,
227250
lat, depth, time, data) to dimensions in the netCF file(s).
228251
Note that dimensions can also be a dictionary of dictionaries if
229-
dimension names are different for each variable
230-
(e.g. dimensions['U'], dimensions['V'], etc).
252+
dimension names are different for each variable.
253+
Watch out: NEMO is discretised on a C-grid:
254+
U and V velocities are not located on the same nodes (see https://www.nemo-ocean.eu/doc/node19.html ).
255+
__V1__
256+
| |
257+
U0 U1
258+
|__V0__|
259+
To interpolate U, V velocities on the C-grid, Parcels needs to read the f-nodes,
260+
which are located on the corners of the cells.
261+
(for indexing details: https://www.nemo-ocean.eu/doc/img360.png )
231262
:param indices: Optional dictionary of indices for each dimension
232263
to read from file(s), to allow for reading of subset of data.
233264
Default is to read the full extent of each dimension.
@@ -247,7 +278,8 @@ def from_nemo(cls, filenames, variables, dimensions, indices=None, mesh='spheric
247278
248279
"""
249280

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

252284
interp_method = {}
253285
for v in variables:
@@ -257,8 +289,7 @@ def from_nemo(cls, filenames, variables, dimensions, indices=None, mesh='spheric
257289
interp_method[v] = tracer_interp_method
258290

259291
return cls.from_netcdf(filenames, variables, dimensions, mesh=mesh, indices=indices, time_periodic=time_periodic,
260-
allow_time_extrapolation=allow_time_extrapolation, interp_method=interp_method,
261-
dimension_filename=dimension_filename, **kwargs)
292+
allow_time_extrapolation=allow_time_extrapolation, interp_method=interp_method, **kwargs)
262293

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

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))

tests/test_grids.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -336,9 +336,12 @@ class MyParticle(ptype[mode]):
336336
def test_nemo_grid(mode):
337337
data_path = path.join(path.dirname(__file__), 'test_data/')
338338

339-
filenames = {'U': data_path + 'Uu_eastward_nemo_cross_180lon.nc',
340-
'V': data_path + 'Vv_eastward_nemo_cross_180lon.nc',
341-
'mesh_mask': data_path + 'mask_nemo_cross_180lon.nc'}
339+
filenames = {'U': {'lon': data_path + 'mask_nemo_cross_180lon.nc',
340+
'lat': data_path + 'mask_nemo_cross_180lon.nc',
341+
'data': data_path + 'Uu_eastward_nemo_cross_180lon.nc'},
342+
'V': {'lon': data_path + 'mask_nemo_cross_180lon.nc',
343+
'lat': data_path + 'mask_nemo_cross_180lon.nc',
344+
'data': data_path + 'Vv_eastward_nemo_cross_180lon.nc'}}
342345
variables = {'U': 'U', 'V': 'V'}
343346
dimensions = {'lon': 'glamf', 'lat': 'gphif'}
344347
field_set = FieldSet.from_nemo(filenames, variables, dimensions)
@@ -367,9 +370,12 @@ class MyParticle(ptype[mode]):
367370
def test_advect_nemo(mode):
368371
data_path = path.join(path.dirname(__file__), 'test_data/')
369372

370-
filenames = {'U': data_path + 'Uu_eastward_nemo_cross_180lon.nc',
371-
'V': data_path + 'Vv_eastward_nemo_cross_180lon.nc',
372-
'mesh_mask': data_path + 'mask_nemo_cross_180lon.nc'}
373+
filenames = {'U': {'lon': data_path + 'mask_nemo_cross_180lon.nc',
374+
'lat': data_path + 'mask_nemo_cross_180lon.nc',
375+
'data': data_path + 'Uu_eastward_nemo_cross_180lon.nc'},
376+
'V': {'lon': data_path + 'mask_nemo_cross_180lon.nc',
377+
'lat': data_path + 'mask_nemo_cross_180lon.nc',
378+
'data': data_path + 'Vv_eastward_nemo_cross_180lon.nc'}}
373379
variables = {'U': 'U', 'V': 'V'}
374380
dimensions = {'lon': 'glamf', 'lat': 'gphif'}
375381
field_set = FieldSet.from_nemo(filenames, variables, dimensions)

0 commit comments

Comments
 (0)