Skip to content

Commit 9eceedb

Browse files
committed
[run-ex] Added method to identify and select (add standard_name attribute) surface variables (wind) from GRIB datasets. Added example_grib_cf to illustrate use with global NCAR GFS dataset.
1 parent fa931ec commit 9eceedb

5 files changed

Lines changed: 95 additions & 4 deletions

File tree

examples/example_grib_cf.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env python
2+
"""
3+
Making GRIB dataset CF-compatible
4+
=================================
5+
"""
6+
7+
from datetime import datetime, timedelta
8+
from opendrift.models.windblow import WindBlow
9+
10+
#%%
11+
# Comparing / using two different wind datasets
12+
#
13+
# The first dataset (UCAR) is a GRIB dataset served by Thredds as a netCDF dataset, but with GRIB attributes
14+
# CF standard_name are added for identified surface variables (wind) with this method:
15+
# https://opendrift.github.io/_modules/opendrift/readers.html#add_standard_name_for_surface_grib_variables
16+
# This should be used with care, making sure that only surface (10m height) variables are used
17+
#
18+
# The second dataset (PACIOOS) is a netCDF4 dataset served by Thredds with CF attributes
19+
wind_datasets = [
20+
'https://thredds.ucar.edu/thredds/dodsC/grib/NCEP/GFS/Global_0p25deg/Best',
21+
'https://pae-paha.pacioos.hawaii.edu/thredds/dodsC/ncep_global/NCEP_Global_Atmospheric_Model_best.ncd'
22+
]
23+
24+
simulations = []
25+
26+
time = datetime.now() - timedelta(days=5)
27+
for wind in wind_datasets:
28+
o = WindBlow(loglevel=20)
29+
o.add_readers_from_list(wind)
30+
o.seed_elements(lon=3, lat=60, time=time, number=10, radius=10000)
31+
o.run(duration=timedelta(hours=48))
32+
simulations.append(o)
33+
34+
#%%
35+
# Some differences are expected as datasets may origin from different runs
36+
simulations[0].plot(compare=simulations[1:], lscale='i', legend=['GRIB converted', 'netCDF CF compatible'])

opendrift/models/windblow.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class WindBlow(OpenDriftSimulation):
3535

3636
def __init__(self, *args, **kwargs):
3737
super(WindBlow, self).__init__(*args, **kwargs)
38-
self._set_config_default('drift:max_speed', 12)
38+
self._set_config_default('drift:max_speed', 25)
3939

4040
def update(self):
4141

opendrift/readers/__init__.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import logging; logger = logging.getLogger(__name__)
2626
import glob
2727
import json
28+
import numpy as np
2829
import opendrift
2930
import xarray as xr
3031
import copernicusmarine
@@ -85,6 +86,40 @@ def open_dataset_opendrift(source, zarr_storage_options=None, open_mfdataset_opt
8586

8687
return ds
8788

89+
def add_standard_name_for_surface_grib_variables(ds):
90+
"""Try to identify surface variables from GRIB datasets
91+
and add standard_name attribute"""
92+
mapping = {
93+
'wind_speed': {'Grib2_Parameter': np.array([0, 2, 1])},
94+
'x_wind': {'Grib2_Parameter': np.array([0, 2, 2])},
95+
'y_wind': {'Grib2_Parameter': np.array([0, 2, 3])},
96+
}
97+
98+
for var_name, var in ds.data_vars.items():
99+
if any(forbidden.lower() in var_name.lower() for forbidden in ('percentile', 'pctl')):
100+
logger.debug(f'Skipping percentile variable {var_name}')
101+
continue
102+
if 'standard_name' in var.attrs:
103+
continue
104+
if var.attrs.get('Grib2_Level_Type', None) != 103:
105+
continue # Not surface variable
106+
for standard_name, maps in mapping.items():
107+
if isinstance(maps, dict):
108+
maps = [maps]
109+
if any(
110+
np.all(attval == var.attrs.get(att)) # attribute value is found in map
111+
for ma in maps
112+
for att, attval in ma.items()
113+
if att in var.attrs # variable has attribute
114+
):
115+
# Must check that height is 10m or surface
116+
for coordname, coordvar in var.coords.items():
117+
if coordvar.attrs.get('_CoordinateAxisType', None) == 'Height' and 10 in coordvar.values:
118+
logger.debug(f'Selecting GRIB variable {var_name} at 10m height and adding standard_name {standard_name}')
119+
ds[var_name] = var.sel({coordname: 10})
120+
ds[var_name].attrs['standard_name'] = standard_name
121+
return ds
122+
88123
def datetime_from_variable(var):
89124
import pandas as pd
90125
try:

opendrift/readers/reader_netCDF_CF_generic.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
import pandas as pd
2525
import xarray as xr
2626
from opendrift.readers.basereader import BaseReader, StructuredReader
27-
from opendrift.readers import open_dataset_opendrift, datetime_from_variable
27+
from opendrift.readers import open_dataset_opendrift, datetime_from_variable, \
28+
add_standard_name_for_surface_grib_variables
2829

2930

3031
class Reader(StructuredReader, BaseReader):
@@ -115,6 +116,8 @@ def __init__(self, filename=None, zarr_storage_options=None, name=None, proj4=No
115116

116117
self.Dataset = open_dataset_opendrift(source=filename, zarr_storage_options=zarr_storage_options)
117118

119+
self.Dataset = add_standard_name_for_surface_grib_variables(self.Dataset)
120+
118121
if name is None:
119122
self.name = str(filename)
120123
else:
@@ -207,7 +210,12 @@ def __init__(self, filename=None, zarr_storage_options=None, name=None, proj4=No
207210
if standard_name == 'time' or axis == 'T' or var_name in ['time', 'vtime']:
208211
# Read and store time coverage (of this particular file)
209212
if len(var.dims)==1:
210-
self.dimensions['time'] = var.dims[0]
213+
if 'time' in self.dimensions:
214+
if not isinstance(self.dimensions['time'], list):
215+
self.dimensions['time'] = [self.dimensions['time']]
216+
self.dimensions['time'].append(var.dims[0])
217+
else:
218+
self.dimensions['time'] = var.dims[0]
211219

212220
self.times = datetime_from_variable(var)
213221
if len(self.times) > 1:
@@ -378,6 +386,18 @@ def __init__(self, filename=None, zarr_storage_options=None, name=None, proj4=No
378386
var = var.isel(ensemble_member=0).squeeze()
379387
self.Dataset[va] = var
380388

389+
if isinstance(self.dimensions.get('time', None), list):
390+
warning = f'Several time dimensions detected: {self.dimensions["time"]}'
391+
variable_time_dimensions = [] # Check which time dimensions are actually used
392+
for vn, va in self.variable_mapping.items():
393+
variable_time_dimensions += [x for x in self.Dataset[va].coords if x in self.dimensions['time']]
394+
variable_time_dimensions = list(set(variable_time_dimensions))
395+
if len(variable_time_dimensions) == 1:
396+
logger.debug(f'{warning}: Using single time dimension found in detected variables: {variable_time_dimensions}')
397+
self.dimensions['time'] = variable_time_dimensions[0]
398+
elif len(variable_time_dimensions) > 1:
399+
raise ValueError(f'{warning}, and also several time dimensions used by detected variables: {variable_time_dimensions}')
400+
381401
# Run constructor of parent Reader class
382402
super().__init__()
383403

tests/models/test_models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def test_windblow(self):
7070
o.seed_elements(lon, lat, radius=5000, number=1000,
7171
time=reader_arome.start_time)
7272
o.run(steps=24, time_step=3600)
73-
self.assertAlmostEqual(o.elements.lon.max(), 15.864, 2)
73+
self.assertAlmostEqual(o.elements.lon.max(), 15.881, 2)
7474

7575
def test_shipdrift(self):
7676
"""Sintef case study"""

0 commit comments

Comments
 (0)