Parsing Conventions and standards with Python

Metadata conventions, like the Climate and Forecast (CF) conventions, can be cumbersome to adhere to but it will be very handy when you or other users manipulate the data later in time.

In this notebook we will explore three Python modules that parse CF-1.6, UGRID-1.0, and SGRID-0.3

CF-1.6 with iris

There are many Python libraries to read and write CF metdata, but only iris encapsulates CF in an object with an API. From iris own docs:

Iris seeks to provide a powerful, easy to use, and community-driven Python library for analysing and visualising meteorological and oceanographic data sets.

With iris you can:

  • Use a single API to work on your data, irrespective of its original format.
  • Read and write (CF-)netCDF, GRIB, and PP files.
  • Easily produce graphs and maps via integration with matplotlib and cartopy.

In [1]:
import iris

iris.FUTURE.netcdf_promote = True

url = ('http://omgsrv1.meas.ncsu.edu:8080/thredds/dodsC/fmrc/'
       'sabgom/SABGOM_Forecast_Model_Run_Collection_best.ncd')

cubes = iris.load(url)


/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/cf.py:1059: UserWarning: Ignoring formula terms variable 'h' referenced by data variable 'v' via variable 's_rho': Dimensions ('eta_rho', 'xi_rho') do not span ('time', 's_rho', 'eta_v', 'xi_v')
  warnings.warn(msg)
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/cf.py:1059: UserWarning: Ignoring formula terms variable 'zeta' referenced by data variable 'v' via variable 's_rho': Dimensions ('time', 'eta_rho', 'xi_rho') do not span ('time', 's_rho', 'eta_v', 'xi_v')
  warnings.warn(msg)
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/cf.py:1059: UserWarning: Ignoring formula terms variable 'h' referenced by data variable 'u' via variable 's_rho': Dimensions ('eta_rho', 'xi_rho') do not span ('time', 's_rho', 'eta_u', 'xi_u')
  warnings.warn(msg)
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/cf.py:1059: UserWarning: Ignoring formula terms variable 'zeta' referenced by data variable 'u' via variable 's_rho': Dimensions ('time', 'eta_rho', 'xi_rho') do not span ('time', 's_rho', 'eta_u', 'xi_u')
  warnings.warn(msg)
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/_pyke_rules/compiled_krb/fc_rules_cf_fc.py:1686: UserWarning: Ignoring netCDF variable 'NH4' invalid units 'millimole_NH4 meter-3'
  warnings.warn(msg)
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/_pyke_rules/compiled_krb/fc_rules_cf_fc.py:1783: UserWarning: Gracefully filling 'time' dimension coordinate masked points
  warnings.warn(msg.format(str(cf_coord_var.cf_name)))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/_pyke_rules/compiled_krb/fc_rules_cf_fc.py:1686: UserWarning: Ignoring netCDF variable 'NO3' invalid units 'millimole_N03 meter-3'
  warnings.warn(msg)
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/_pyke_rules/compiled_krb/fc_rules_cf_fc.py:1783: UserWarning: Gracefully filling 'time' dimension coordinate masked points
  warnings.warn(msg.format(str(cf_coord_var.cf_name)))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/_pyke_rules/compiled_krb/fc_rules_cf_fc.py:1783: UserWarning: Gracefully filling 'time' dimension coordinate masked points
  warnings.warn(msg.format(str(cf_coord_var.cf_name)))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/_pyke_rules/compiled_krb/fc_rules_cf_fc.py:1783: UserWarning: Gracefully filling 'time' dimension coordinate masked points
  warnings.warn(msg.format(str(cf_coord_var.cf_name)))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/_pyke_rules/compiled_krb/fc_rules_cf_fc.py:1783: UserWarning: Gracefully filling 'time' dimension coordinate masked points
  warnings.warn(msg.format(str(cf_coord_var.cf_name)))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/_pyke_rules/compiled_krb/fc_rules_cf_fc.py:1783: UserWarning: Gracefully filling 'time' dimension coordinate masked points
  warnings.warn(msg.format(str(cf_coord_var.cf_name)))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/_pyke_rules/compiled_krb/fc_rules_cf_fc.py:1783: UserWarning: Gracefully filling 'time' dimension coordinate masked points
  warnings.warn(msg.format(str(cf_coord_var.cf_name)))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/_pyke_rules/compiled_krb/fc_rules_cf_fc.py:1783: UserWarning: Gracefully filling 'time' dimension coordinate masked points
  warnings.warn(msg.format(str(cf_coord_var.cf_name)))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/_pyke_rules/compiled_krb/fc_rules_cf_fc.py:1686: UserWarning: Ignoring netCDF variable 'zooplankton' invalid units 'millimole_nitrogen meter-3'
  warnings.warn(msg)
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/_pyke_rules/compiled_krb/fc_rules_cf_fc.py:1783: UserWarning: Gracefully filling 'time' dimension coordinate masked points
  warnings.warn(msg.format(str(cf_coord_var.cf_name)))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/_pyke_rules/compiled_krb/fc_rules_cf_fc.py:1686: UserWarning: Ignoring netCDF variable 'chlorophyll' invalid units 'milligrams_chlorophyll meter-3'
  warnings.warn(msg)
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/_pyke_rules/compiled_krb/fc_rules_cf_fc.py:1783: UserWarning: Gracefully filling 'time' dimension coordinate masked points
  warnings.warn(msg.format(str(cf_coord_var.cf_name)))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/_pyke_rules/compiled_krb/fc_rules_cf_fc.py:1686: UserWarning: Ignoring netCDF variable 'phytoplankton' invalid units 'millimole_nitrogen meter-3'
  warnings.warn(msg)
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/_pyke_rules/compiled_krb/fc_rules_cf_fc.py:1783: UserWarning: Gracefully filling 'time' dimension coordinate masked points
  warnings.warn(msg.format(str(cf_coord_var.cf_name)))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/_pyke_rules/compiled_krb/fc_rules_cf_fc.py:1783: UserWarning: Gracefully filling 'time' dimension coordinate masked points
  warnings.warn(msg.format(str(cf_coord_var.cf_name)))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/netcdf.py:579: UserWarning: Unable to find coordinate for variable 'zeta'
  '{!r}'.format(name))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/netcdf.py:579: UserWarning: Unable to find coordinate for variable 'h'
  '{!r}'.format(name))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/netcdf.py:696: UserWarning: Unable to construct Ocean s-coordinate, generic form 1 factory due to insufficient source coordinates.
  warnings.warn('{}'.format(e))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/_pyke_rules/compiled_krb/fc_rules_cf_fc.py:1783: UserWarning: Gracefully filling 'time' dimension coordinate masked points
  warnings.warn(msg.format(str(cf_coord_var.cf_name)))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/netcdf.py:579: UserWarning: Unable to find coordinate for variable 'zeta'
  '{!r}'.format(name))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/netcdf.py:579: UserWarning: Unable to find coordinate for variable 'h'
  '{!r}'.format(name))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/netcdf.py:696: UserWarning: Unable to construct Ocean s-coordinate, generic form 1 factory due to insufficient source coordinates.
  warnings.warn('{}'.format(e))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/fileformats/_pyke_rules/compiled_krb/fc_rules_cf_fc.py:1783: UserWarning: Gracefully filling 'time' dimension coordinate masked points
  warnings.warn(msg.format(str(cf_coord_var.cf_name)))
/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/iris/_merge.py:364: FutureWarning: elementwise comparison failed; returning scalar instead, but in the future will perform elementwise comparison
  other_defn.attributes[key])]

Aside: using iris.FUTURE.netcdf_promote = True we can promote netCDF formula terms, like sea surface height, to first class cube objects. This behavior will be default in future versions of iris and that line will not be needed after the next version of iris is released.


In [2]:
print(cubes)


0: 2D momentum inflow, nudging inverse time scale / (second-1) (-- : 4)
1: 2D momentum outflow, nudging inverse time scale / (second-1) (-- : 4)
2: nonlinear model Laplacian mixing coefficient for tracers / (meter2 second-1) (-- : 14)
3: angle between XI-axis and EAST / (radians) (-- : 320; -- : 440)
4: ammonium concentration / (unknown)  (time: 456; ocean_s_coordinate_g1: 36; -- : 320; -- : 440)
5: 3D momentum inflow, nudging inverse time scale / (second-1) (-- : 4)
6: free-surface outflow, nudging inverse time scale / (second-1) (-- : 4)
7: 3D momentum outflow, nudging inverse time scale / (second-1) (-- : 4)
8: nitrate concentration / (unknown)   (time: 456; ocean_s_coordinate_g1: 36; -- : 320; -- : 440)
9: Tracers nudging/relaxation inverse time scale / (day-1) (-- : 14)
10: free-surface inflow, nudging inverse time scale / (second-1) (-- : 4)
11: mask on RHO-points / (1)            (-- : 320; -- : 440)
12: vertically integrated u-momentum component / (meter second-1) (time: 456; -- : 320; -- : 439)
13: grid type logical switch / (1)      (-- : 64)
14: vertically integrated v-momentum component / (meter second-1) (time: 456; -- : 319; -- : 440)
15: mask on U-points / (1)              (-- : 320; -- : 439)
16: tracer point sources and simck activation switch / (1) (-- : 14)
17: tracers inflow, nudging inverse time scale / (second-1) (-- : 4; -- : 14)
18: mask on V-points / (1)              (-- : 319; -- : 440)
19: tracers outflow, nudging inverse time scale / (second-1) (-- : 4; -- : 14)
20: curvilinear coordinate metric in ETA / (meter-1) (-- : 320; -- : 440)
21: vertical momentum component / (meter second-1) (time: 456; ocean_s_coordinate_g1: 37; -- : 320; -- : 440)
22: zooplankton concentration / (unknown) (time: 456; ocean_s_coordinate_g1: 36; -- : 320; -- : 440)
23: curvilinear coordinate metric in XI / (meter-1) (-- : 320; -- : 440)
24: mask on psi-points / (1)            (-- : 319; -- : 439)
25: chlorophyll concentration / (unknown) (time: 456; ocean_s_coordinate_g1: 36; -- : 320; -- : 440)
26: background vertical mixing coefficient for tracers / (meter2 second-1) (-- : 14)
27: phytoplankton concentration / (unknown) (time: 456; ocean_s_coordinate_g1: 36; -- : 320; -- : 440)
28: Coriolis parameter at RHO-points / (second-1) (-- : 320; -- : 440)
29: bathymetry at RHO-points / (meter)  (-- : 320; -- : 440)
30: eastward_sea_water_velocity_assuming_no_tide / (meter second-1) (time: 456; ocean_s_coordinate_g1: 36; -- : 320; -- : 439)
31: forecast_period / (hours since 2016-10-01T00:00:00Z) (time: 456)
32: northward_sea_water_velocity_assuming_no_tide / (meter second-1) (time: 456; ocean_s_coordinate_g1: 36; -- : 319; -- : 440)
33: sea_surface_height / (meter)        (time: 456; -- : 320; -- : 440)
34: sea_water_potential_temperature / (Celsius) (time: 456; ocean_s_coordinate_g1: 36; -- : 320; -- : 440)
35: sea_water_salinity / (1)            (time: 456; ocean_s_coordinate_g1: 36; -- : 320; -- : 440)

The advantages of the CF data model here are:

  • high level variable access via standard_name or long_name;
  • verbose warnings when there are compliance issues (see the units warnings above);
  • raise errors for non-compliant datasets;
  • separation of each phenomena (variable) into its own cube*;
  • each cube is a fully self-described format with all the original metadata;
  • round-trip load-save to netCDF is lossless;
  • free interpretation of the formula_terms, cell_methods, and axis that helps with dimensionless coordinates, climatological variables, and plotting routines respectively.

* Most people miss the concept of a "dataset" when using iris, but that is a consequence of the CF model. Since there is no rule for unique names for the variables the dataset may contain the same phenomena with different coordinates, hence iris decides to create an individual cube for each phenomena.

Aside: note that the xarray does have a dataset concept, but it infringes the CF model in many places to do so. We recommend xarray when CF compliance is not a requirement.

For more on iris see this example.

Moving on, let's extract a single phenomena from the list of cubes above.


In [3]:
cube = cubes.extract_strict('sea_surface_height')

print(cube)


sea_surface_height / (meter)        (time: 456; -- : 320; -- : 440)
     Dimension coordinates:
          time                           x         -         -
     Auxiliary coordinates:
          forecast_reference_time        x         -         -
          latitude                       -         x         x
          longitude                      -         x         x
     Attributes:
          CPP_options: SABGOM, ADD_FSOBC, ADD_M2OBC, ANA_BPFLUX, ANA_BSFLUX, ANA_BTFLUX, ANA_SPFLUX,...
          Conventions: CF-1.4, _Coordinates
          DODS.strlen: 0
          DODS_EXTRA.Unlimited_Dimension: ocean_time
          EXTRA_DIMENSION.N: 36
          _CoordSysBuilder: ucar.nc2.dataset.conv.CF1Convention
          ana_file: ROMS/Functionals/ana_btflux.h, ROMS/Functionals/ana_nudgcoef.h, ROMS/F...
          bio_file: ROMS/Nonlinear/Biology/fennel.h
          bpar_file: /home/omg/autosabgom/bioFasham_038_Katja.in.U3C4
          bry_file: /gpfs_share/omg/omg/autosabgom/in/ncoda_bry_20161121.nc
          cdm_data_type: GRID
          clm_file: /gpfs_share/omg/omg/autosabgom/in/ncoda_clm_20161121.nc
          code_dir: /he_data/he/zxue/COAWST411
          compiler_command: /usr/local/apps/mpich/x86_64/pgi105/mx127..7/bin/mpif90
          compiler_flags: -fastsse   -Kieee -fastsse -Mipa=fast -tp k8-64 -Mfree
          compiler_system: pgi
          cpu: x86_64
          dia_file: /gpfs_share/omg/autosabgom/out/dia_20161121.nc
          featureType: GRID
          field: free-surface, scalar, series
          file: /gpfs_share/omg/autosabgom/out/his_20161121_0002.nc
          format: netCDF-3 classic file
          frc_file_01: /gpfs_share/omg/omg/autosabgom/in/nomads_forc_20161121.nc
          frc_file_02: /gpfs_share/omg/omg/autosabgom/in/SABGOM.OTIS.Ref18581117.8Cons
          frc_file_03: /gpfs_share/omg/omg/autosabgom/in/sabgom_river_79_clm_2015_2016.nc
          grd_file: /gpfs_share/omg/omg/autosabgom/in/sabgom_grd.nc.Etopo2.LP.r1_5.filled
          header_dir: /he_data/he/zxue/Projects/SABGOM_BIO
          header_file: mch_bio_nf.h
          his_base: /gpfs_share/omg/autosabgom/out/his_20161121
          history: ROMS/TOMS, Version 3.4, Monday - November 21, 2016 -  1:55:35 AM ;
FMRC...
          ini_file: /gpfs_share/omg/omg/autosabgom/in/ncoda_ini_20161121.nc
          location: Proto fmrc:SABGOM_Forecast_Model_Run_Collection
          os: Linux
          rst_file: /gpfs_share/omg/autosabgom/out/rst_20161121.nc
          script_file: /home/omg/autosabgom/sabgom_20161121.in
          spos_file: /home/omg/autosabgom/stations.in
          sta_file: /gpfs_share/omg/autosabgom/out/sta_20161121.nc
          svn_rev: 412M
          svn_url: https://www.myroms.org/svn/omlab/branches/jcwarner
          tiling: 008x004
          time: ocean_time
          title: ROMS/TOMS 3.0 - South-Atlantic Bight and Gulf of Mexico
          type: ROMS/TOMS history file

Requesting a vertical profile of temperature to see the formula_terms parsing in action. (Note that ocean_s_coordinate_g1 is actually CF-1.7 but was backported to iris because it is widely adopted and the CF conventions document evolves quite slowly.)


In [4]:
temp = cubes.extract_strict('sea_water_potential_temperature')

# Surface at the last time step.
T = temp[-1, -1, ...]

# Random profile at the last time step.
t_profile = temp[-1, :, 160, 220]

In [5]:
t_profile.coords(axis='Z')


Out[5]:
[DimCoord(array([-0.98611111, -0.95833333, -0.93055556, -0.90277778, -0.875     ,
        -0.84722222, -0.81944444, -0.79166667, -0.76388889, -0.73611111,
        -0.70833333, -0.68055556, -0.65277778, -0.625     , -0.59722222,
        -0.56944444, -0.54166667, -0.51388889, -0.48611111, -0.45833333,
        -0.43055556, -0.40277778, -0.375     , -0.34722222, -0.31944444,
        -0.29166667, -0.26388889, -0.23611111, -0.20833333, -0.18055556,
        -0.15277778, -0.125     , -0.09722222, -0.06944444, -0.04166667,
        -0.01388889]), standard_name='ocean_s_coordinate_g1', units=Unit('1'), long_name='S-coordinate at RHO-points', var_name='s_rho', attributes={'valid_max': 0.0, '_CoordinateAxes': 's_rho', '_CoordinateZisPositive': 'up', '_CoordinateTransformType': 'Vertical', 'positive': 'up', '_CoordinateAxisType': 'GeoZ', 'valid_min': -1.0, 'field': 's_rho, scalar'}),
 AuxCoord(array([-191.78458226, -177.34203908, -164.66922711, -153.51066739,
        -143.63657394, -134.83775173, -126.9209901 , -119.70506997,
        -113.01765416, -106.6935271 , -100.57486703,  -94.51438537,
         -88.38208727,  -82.07584925,  -75.53475706,  -68.75226714,
         -61.78442425,  -54.74791675,  -47.80496731,  -41.13684462,
         -34.91288897,  -29.26416149,  -24.26861252,  -19.94956019,
         -16.28448496,  -13.21893294,  -10.68077477,   -8.5919013 ,
          -6.87632075,   -5.46487914,   -4.29738849,   -3.32303343,
          -2.499781  ,   -1.79330887,   -1.17577744,   -0.62462965]), standard_name='sea_surface_height_above_reference_ellipsoid', units=Unit('meter'), attributes={'positive': 'up'})]

Iris knows about the metadata and can create fully annotated plots.


In [6]:
%matplotlib inline

import numpy.ma as ma
import iris.quickplot as qplt

T.data = ma.masked_invalid(T.data)

qplt.pcolormesh(T)


Out[6]:
<matplotlib.collections.QuadMesh at 0x7f7d56868c18>

In [7]:
qplt.plot(t_profile);


Be aware that too much automation may lead to some weird plots.

For example, the z-coord is in the x-direction and the automatic naming of the z to Sea surface height is above the reference ellipsoid in the second plot. Another example is the lack of proper coordinates in the first plot.

In these cases, manual plotting is more appropriate.


In [8]:
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(3, 7))

t = t_profile.data
z = t_profile.coord('sea_surface_height_above_reference_ellipsoid').points

ax.plot(t, z);


UGRID-1.0 with pyugrid

The Unstructured Grids convention encompasses any type of grid topology, and the details of the convention are documented in https://ugrid-conventions.github.io/ugrid-conventions. Right now pyugrid supports only triangular topologies, more will be added in the near future.

In a nutshell the pyugrid parses and exposes the underlying grid topology in a python object.


In [9]:
import pyugrid

url = 'http://crow.marine.usf.edu:8080/thredds/dodsC/FVCOM-Nowcast-Agg.nc'

ugrid = pyugrid.UGrid.from_ncfile(url)

Sometimes the topology is incomplete but, if the data is UGRID compliant, pyugrid can derive the rest for you.


In [10]:
ugrid.build_edges()

The topology can be extracted from ugrid object and used for plotting.


In [11]:
lon = ugrid.nodes[:, 0]
lat = ugrid.nodes[:, 1]
triangles = ugrid.faces[:]

In [12]:
import cartopy.crs as ccrs
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER


def make_map(projection=ccrs.PlateCarree()):
    fig, ax = plt.subplots(figsize=(8, 6),
                           subplot_kw=dict(projection=projection))
    ax.coastlines(resolution='50m')
    gl = ax.gridlines(draw_labels=True)
    gl.xlabels_top = gl.ylabels_right = False
    gl.xformatter = LONGITUDE_FORMATTER
    gl.yformatter = LATITUDE_FORMATTER
    return fig, ax


fig, ax = make_map()

kw = dict(marker='.', linestyle='-', alpha=0.25, color='darkgray')
ax.triplot(lon, lat, triangles, **kw)
ax.coastlines()
ax.set_extent([-88, -79.5, 24, 32])


There is some effort in the development community to integrate pyugrid into iris to augment the cube object to be both CF and UGRID aware. It will add convenience plotting and slicing methods.

Check a longer pyugrid example here.

SGRID-0.3 with pysgrid

The Staggered Grid conventions help users to interpret grids from models like ROMS and DELFT, where the variables are defined in different grids. The specs are detailed in https://sgrid.github.io/sgrid.

The pysgrid module is similar to pyugrid. The grid topology is parsed into a Python object with methods and attributes that translate the SGRID conventions.


In [13]:
import pysgrid

url = ('http://geoport.whoi.edu/thredds/dodsC/'
       'coawst_4/use/fmrc/coawst_4_use_best.ncd')

sgrid = pysgrid.load_grid(url)

All the raw grid information is present, like edges, dimensions, padding, grid center, and slicing.


In [14]:
sgrid.edge1_coordinates, sgrid.edge1_dimensions, sgrid.edge1_padding


Out[14]:
(('lon_u', 'lat_u'),
 'xi_u: xi_psi eta_u: eta_psi (padding: both)',
 [GridPadding(mesh_topology_var='grid', face_dim='eta_u', node_dim='eta_psi', padding='both')])

In [15]:
u_var = sgrid.u

u_var.center_axis, u_var.node_axis


Out[15]:
(1, 0)

In [16]:
v_var = sgrid.v
v_var.center_axis, v_var.node_axis


Out[16]:
(0, 1)

In [17]:
u_var.center_slicing, v_var.center_slicing


Out[17]:
((slice(None, None, None),
  slice(None, None, None),
  slice(1, -1, None),
  slice(None, None, None)),
 (slice(None, None, None),
  slice(None, None, None),
  slice(None, None, None),
  slice(1, -1, None)))

The API is "raw" but comprehensive. There is plenty of room to create convenience methods using the low level access provided by the library.

See below an example of the API and some simple convenience methods to slice, pad, average, and rotate the structure grid for plotting.


In [18]:
from netCDF4 import Dataset

nc = Dataset(url)
u_velocity = nc.variables[u_var.variable]
v_velocity = nc.variables[v_var.variable]

v_idx = 0  # Bottom.
time_idx = -1  # Last time step.

u = u_velocity[time_idx, v_idx, u_var.center_slicing[-2], u_var.center_slicing[-1]]
v = v_velocity[time_idx, v_idx, v_var.center_slicing[-2], v_var.center_slicing[-1]]

# Average at the center.
from pysgrid.processing_2d import avg_to_cell_center

u = avg_to_cell_center(u, u_var.center_axis)
v = avg_to_cell_center(v, v_var.center_axis)

# **Rotate the grid.
from pysgrid.processing_2d import rotate_vectors

angles = nc.variables[sgrid.angle.variable][sgrid.angle.center_slicing]
u, v = rotate_vectors(u, v, angles)

# Compute the speed.
from pysgrid.processing_2d import vector_sum

speed = vector_sum(u, v)


/home/filipe/miniconda3/envs/IOOS/lib/python3.5/site-packages/numpy/ma/core.py:882: RuntimeWarning: invalid value encountered in less
  return umath.less(x, self.critical_value)

** CF convention does describe the angle variable for grids that needs rotation, but there is no action expected. For example, in the formula_terms, pysgrid must be improved to abstract that action when needed via a simpler method.

<entry id="angle_of_rotation_from_east_to_x">
    <canonical_units>degree</canonical_units>
    <grib></grib>
    <amip></amip>
    <description>The quantity with standard name angle_of_rotation_from_east_to_x is the angle, anticlockwise reckoned positive, between due East and (dr/di)jk, where r(i,j,k) is the vector 3D position of the point with coordinate indices (i,j,k).  It could be used for rotating vector fields between model space and latitude-longitude space.</description>
</entry>

In [19]:
lon_var_name, lat_var_name = sgrid.face_coordinates

sg_lon = getattr(sgrid, lon_var_name)
sg_lat = getattr(sgrid, lat_var_name)

lon = sgrid.center_lon[sg_lon.center_slicing]
lat = sgrid.center_lat[sg_lat.center_slicing]

Ideally all the steps above could be performed in the background, in a high level object method call, like the iris cube plotting methods.

Let's subset and center the velocity for better visualization (not a mandatory step but recommended).


In [20]:
def is_monotonically_increasing(arr, axis=0):
    return np.all(np.diff(arr, axis=axis) > 0)


def is_monotonically_decreasing(arr, axis=0):
    return np.all(np.diff(arr, axis=axis) < 0)


def is_monotonic(arr):
    return (is_monotonically_increasing(arr) or
            is_monotonically_decreasing(arr))


def extent_bounds(arr, bound_position=0.5, axis=0):
    if not is_monotonic(arr):
        msg = "Array {!r} must be monotonic to guess bounds".format
        raise ValueError(msg(arr))

    x = arr.copy()
    x = np.c_[x[:, 0], (bound_position * (x[:, :-1] + x[:, 1:])), x[:, -1]]
    x = np.r_[x[0, :][None, ...], (bound_position * (x[:-1, :] + x[1:, :])), x[-1, :][None, ...]]

    return x

In [21]:
import numpy as np

# For plotting reasons we will subsample every 10th point here
# 100 times less data!
sub = 10

lon = lon[::sub, ::sub]
lat = lat[::sub, ::sub]
u, v = u[::sub, ::sub], v[::sub, ::sub]
speed = speed[::sub, ::sub]

x = extent_bounds(lon)
y = extent_bounds(lat)

Now we can use quiver to plot the velocity components in a single grid.


In [22]:
def make_map(projection=ccrs.PlateCarree(), figsize=(9, 9)):
    fig, ax = plt.subplots(figsize=figsize,
                           subplot_kw=dict(projection=projection))
    gl = ax.gridlines(draw_labels=True)
    gl.xlabels_top = gl.ylabels_right = False
    gl.xformatter = LONGITUDE_FORMATTER
    gl.yformatter = LATITUDE_FORMATTER
    return fig, ax

In [23]:
scale = 0.06

fig, ax = make_map()

kw = dict(scale=1.0/scale, pivot='middle', width=0.003, color='black')
q = plt.quiver(lon, lat, u, v, zorder=2, **kw)

plt.pcolormesh(x, y, speed, zorder=1, cmap=plt.cm.rainbow)

c = ax.coastlines('10m')
ax.set_extent([-73.5, -62.5, 38, 46])


For more examples on pysgrid check this post out.