Get Global Elevation Data at the resolution of the grids used by GEOS-Chem

A recurrent problem, when analysing GEOS-Chem outputs and/or comparing them with other data sources, is to get the altitude values at the vertical levels of a GEOS-Chem grid. GEOS-Chem outputs include grid-box heights (in meters) but doesn't provide the altitude (a.s.l) of the base levels (i.e., the model topography).

The code below allows to load global elevation data (any raster format supported by the GDAL library) and then resample it to a given GEOS-Chem grid. The original, high resolution elevation data is downsampled to a GC grid using several regridding methods.

The example below uses the USGS's and NGA's GMTED2010 global elevation data. It is a high quality, multi-resolution product for global and continental applications. More info here: http://topotools.cr.usgs.gov/gmted_viewer/.

WARNING: The elevation raster must have lat/lon coordinates (no projection) ! It is also assumed that the geographic coordinate system of the elevation raster is roughly similar to the coordinate system used by the GEOS-Chem grids (GMAO Met Fields). No datum/ellipsoid/spheroid transformation is performed in the example below.

1. Dependencies / Imports


In [16]:
import numpy as np
import gdal, osr
from scipy.io import readsav

import iris
from iris.coords import DimCoord
from iris.experimental.regrid import regrid_area_weighted_rectilinear_src_and_grid
from iris.experimental.regrid_conservative import regrid_conservative_via_esmpy
import iris.quickplot as qiplot

import ESMF

import pygchem.grid as gcgrid

2. Parameters


In [2]:
# GMTED2010 product at 30 arc-seconds, aggregation method = mean elevation, ArcGRID format.
elev_src = "/home/bovy/GISData/Elevation/GMTED2010/mn30_grd"
elev_name = "GMTED2010_mn30"
elev_units = "m"

gcgrid_model = 'GEOS57'                # GEOS-Chem grid model (see the cell below for model names)
                                       # Note: many models use the same horizontal grid
gcgrid_resolution = 2.5, 2             # horizontal grid resolution (longitude, latitude) in decimal degrees

output_dir = '/home/bovy/Grids/'       # directory where regridded elevation data files will be saved
  • Get the names of GEOS-Chem grid models that are supported by default by PyGChem

In [3]:
gcgrid.CTMGrid.models


Out[3]:
['MERRA_NATIVE',
 'GEOS5_REDUCED',
 'GENERIC',
 'GEOS5_NATIVE',
 'GEOS_STRAT_46L',
 'GEOS4_30L',
 'GEOS',
 'GEOS_STRAT',
 'MERRA_47L',
 'GEOS5',
 'GEOS4',
 'GEOS3',
 'GEOS2',
 'GEOS1',
 'GEOS57_REDUCED',
 'GEOS5_47L',
 'GEOS57',
 'GEOS2_70L',
 'FVDAS',
 'MERRA',
 'GEOS57_47L',
 'GEOS3_30L',
 'GEOS3_REDUCED',
 'GEOS4_REDUCED',
 'MERRA_REDUCED',
 'GEOS57_NATIVE']

3. Import elevation data

  • Load elevation raster and get the raster values into a numpy array. Since all data is loaded into RAM, it can be very memory intensive with high resolution data!

In [4]:
elev_rst = gdal.Open(elev_src)
elev_data = elev_rst.ReadAsArray()
  • Get the elevation raster's geographic coordinate system. Will raise an error if the raster's coordinate system is not geographic. Otherwise, shows the coordinate system metadata, which is assumed to be compatible with the reference system used by the GEOS-Chem grids.

In [5]:
elev_cs = osr.SpatialReference()
elev_cs.ImportFromWkt(elev_rst.GetProjection())

if not elev_cs.IsGeographic():
    raise ValueError("The raster's coordinate system must be geographic!")

# create an iris's GeogCS object from the raster's coord system
cube_cs = iris.coord_systems.GeogCS(
    semi_major_axis=elev_cs.GetSemiMajor(),
    semi_minor_axis=elev_cs.GetSemiMinor(),
    longitude_of_prime_meridian=elev_cs.GetProjParm('PRIMEM')
)

print elev_cs


GEOGCS["WGS 84",
    DATUM["WGS_1984",
        SPHEROID["WGS 84",6378137,298.257223563,
            AUTHORITY["EPSG","7030"]],
        TOWGS84[0,0,0,0,0,0,0],
        AUTHORITY["EPSG","6326"]],
    PRIMEM["Greenwich",0,
        AUTHORITY["EPSG","8901"]],
    UNIT["degree",0.0174532925199433,
        AUTHORITY["EPSG","9108"]],
    AUTHORITY["EPSG","4326"]]
  • Get the lat, lon coordinates values of the elevation raster (grid boxes top/left corners). Get values at grid box centers and bounds.

In [6]:
elev_gt = elev_rst.GetGeoTransform()
cols = elev_rst.RasterXSize
rows = elev_rst.RasterYSize

y_ind = np.arange(0, rows + 1)
x_ind = np.arange(0, cols + 1)

elev_lon_left = elev_gt[0] + (x_ind * elev_gt[1])
elev_lat_top = elev_gt[3] + (y_ind * elev_gt[5])

elev_lon_bounds = np.column_stack((elev_lon_left[0:-1], elev_lon_left[1:]))
elev_lat_bounds = np.column_stack((elev_lat_top[0:-1], elev_lat_top[1:]))

elev_lon_centers = (elev_lon_left[0:-1] + elev_lon_left[1:]) / 2.
elev_lat_centers = (elev_lat_top[0:-1] + elev_lat_top[1:]) / 2.

In [7]:
clon = DimCoord(elev_lon_centers,
                bounds=elev_lon_bounds,
                standard_name='longitude',
                units='degrees_east',
                coord_system=cube_cs)
clat = DimCoord(elev_lat_centers,
                bounds=elev_lat_bounds,
                standard_name='latitude',
                units='degrees_north',
                coord_system=cube_cs)

elev_cube = iris.cube.Cube(elev_data.astype('d'),
                           var_name=elev_name,
                           units=elev_units,
                           dim_coords_and_dims=((clat, 0), (clon, 1)))

print elev_cube


GMTED2010_mn30 / (m)                (latitude: 20880; longitude: 43200)
     Dimension coordinates:
          latitude                           x                 -
          longitude                          -                 x

4. Define the GEOS-Chem Grid (from a given model name)

The model name (e.g., GEOS57, MERRA...) given here above is used to define the horizontal grid on which elevation data will be regridded. Change the cell below if you want to define a custom grid.

  • Get the GC grid coordinates (centers and bounds)

In [8]:
gc = gcgrid.CTMGrid.from_model(gcgrid_model, resolution=gcgrid_resolution)

gc_name = "{model}_{rlat}x{rlon}".format(model=gcgrid_model,
                                         rlat=gcgrid_resolution[1],
                                         rlon=gcgrid_resolution[0])

gc_lon_centers, gc_lat_centers = gc.lonlat_centers
elon, elat = gc.lonlat_edges
gc_lon_bounds = np.column_stack((elon[0:-1], elon[1:]))
gc_lat_bounds = np.column_stack((elat[0:-1], elat[1:]))
  • Convert the GC grid to an iris cube

In [9]:
clon = DimCoord(gc_lon_centers,
                bounds=gc_lon_bounds,
                standard_name='longitude',
                units='degrees_east',
                coord_system=cube_cs)
clat = DimCoord(gc_lat_centers,
                bounds=gc_lat_bounds,
                standard_name='latitude',
                units='degrees_north',
                coord_system=cube_cs)

dummy_data = np.empty((gc_lat_centers.size, gc_lon_centers.size))

gc_cube = iris.cube.Cube(dummy_data,
                         var_name=gc_name,
                         units=elev_units,
                         dim_coords_and_dims=((clat, 0), (clon, 1)))

print gc_cube


GEOS57_2x2.5 / (m)                  (latitude: 91; longitude: 144)
     Dimension coordinates:
          latitude                           x              -
          longitude                          -              x

5. Regrid elevation data

Uses Iris's regridding functions.

  • Area-weighted mean aggregation method. Computes area-weighted mean of elevation values regridded on the GC grid. The function used below is (very) very slow using high-resolution elevation data!! (Iris v.1.6.x)

In [13]:
gc_elev_cube_awm = regrid_area_weighted_rectilinear_src_and_grid(elev_cube, gc_cube)

gc_elev_cube_awm.var_name = "DEM_{}".format(gc_cube.name())
gc_elev_cube_awm.attributes['elevation_data_src'] = elev_cube.name()
gc_elev_cube_awm.attributes['regrid_method'] = "area-weighted mean"

print gc_elev_cube_awm


DEM_GEOS57_2x2.5 / (m)              (latitude: 91; longitude: 144)
     Dimension coordinates:
          latitude                           x              -
          longitude                          -              x
     CF Attributes:

     Attributes:
          elevation_data_src: GMTED2010_mn30
          regrid_method: area-weighted mean
  • Conservative regridding. (EXPERIMENTAL) The function below is an Iris wrapper to the conservative regridding algorithm implemented in ESMF. It should actually procude similar results than the previous function, but more efficiently? ESMPy is needed (see https://www.earthsystemcog.org/projects/esmpy/). Currently doesn't work and huge memory cunsomption.

In [39]:
gc_elev_cube_conservative = regrid_conservative_via_esmpy(elev_cube, gc_cube)

gc_elev_cube_conservative.var_name = "DEM_{}".format(gc_cube.name())
gc_elev_cube_conservative.attributes['elevation_data_src'] = elev_cube.name()
gc_elev_cube_conservative.attributes['regrid_method'] = "conservative"

print gc_elev_cube_conservative


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-39-c441f45c36c9> in <module>()
----> 1 gc_elev_cube_conservative = regrid_conservative_via_esmpy(elev_cube, gc_cube)
      2 
      3 gc_elev_cube_conservative.var_name = "DEM_{}".format(gc_cube.name())
      4 gc_elev_cube_conservative.attributes['elevation_data_src'] = elev_cube.name()
      5 gc_elev_cube_conservative.attributes['regrid_method'] = "conservative"

/home/python/PythonEnvs/pygchem_py27_0/lib/python2.7/site-packages/Iris-1.6.2_dev-py2.7.egg/iris/experimental/regrid_conservative.pyc in regrid_conservative_via_esmpy(source_cube, grid_cube)
    238         # Construct ESMF Field objects on source and destination grids.
    239         src_field = _make_esmpy_field(src_coords[0], src_coords[1],
--> 240                                       data=src_data_2d, mask=srcdata_mask)
    241         dst_field = _make_esmpy_field(dst_coords[0], dst_coords[1])
    242 

/home/python/PythonEnvs/pygchem_py27_0/lib/python2.7/site-packages/Iris-1.6.2_dev-py2.7.egg/iris/experimental/regrid_conservative.pyc in _make_esmpy_field(x_coord, y_coord, ref_name, data, mask)
     84 
     85     # Add grid 'coord' element for corners, and fill with corner values.
---> 86     grid.add_coords(staggerlocs=[ESMF.StaggerLoc.CORNER])
     87     grid_corners_x = grid.get_coords(0, ESMF.StaggerLoc.CORNER)
     88     grid_corners_x[:] = lon_bounds.T

TypeError: add_coords() got an unexpected keyword argument 'staggerlocs'
  • Bilinear regridding. Regrid using bilinear interpolation. Execution is very fast compared to the previous methods. However, it may be less relevant to use this method with very high resolution elevation data. Depending on the resolution of the elevation raster, it produces less smooth results than the previous methods (especially for high resolution elevation data and in high relief regions).

In [10]:
gc_elev_cube_bilinear = iris.analysis.interpolate.regrid(elev_cube, gc_cube, mode='bilinear')

gc_elev_cube_bilinear.var_name = "DEM_{}".format(gc_cube.name())
gc_elev_cube_bilinear.attributes['elevation_data_src'] = elev_cube.name()
gc_elev_cube_bilinear.attributes['regrid_method'] = "bilinear"

print gc_elev_cube_bilinear


DEM_GEOS57_2x2.5 / (m)              (latitude: 91; longitude: 144)
     Dimension coordinates:
          latitude                           x              -
          longitude                          -              x
     CF Attributes:

     Attributes:
          elevation_data_src: GMTED2010_mn30
          regrid_method: bilinear

6. Save regridded elevation data to disk

  • Regridded iris cubes are saved as CF-compliant netCDF files. It can be loaded again by Iris or by any other application that have netCDF / hdf5 reading capabilities.

In [14]:
iris.save(gc_elev_cube_awm, '/home/bovy/temp/dem_{gc_name}_awm.nc'.format(gc_name=gc_cube.name()))
iris.save(gc_elev_cube_bilinear, '/home/bovy/temp/dem_{gc_name}_bilinear.nc'.format(gc_name=gc_cube.name()))

7. Plot and compare results

  • A subset of the original elevation data

In [11]:
lat_selection = iris.Constraint(latitude=lambda cell: 40. <= cell <= 55.)
lon_selection = iris.Constraint(longitude=lambda cell: 0. <= cell <= 20.)

elev_cube_subset = elev_cube.extract(lat_selection & lon_selection)

qiplot.pcolormesh(elev_cube_subset, vmax=3000, cmap=cm.gist_earth)
plt.gca().coastlines()
plt.show()


  • Area-weighted mean aggregation method

In [15]:
qiplot.pcolormesh(gc_elev_cube_awm, vmax=4000, cmap=cm.gist_earth)
plt.gca().coastlines()
plt.show()


  • Bilinear method

In [12]:
qiplot.pcolormesh(gc_elev_cube_bilinear, vmax=4000, cmap=cm.gist_earth)
plt.gca().coastlines()
plt.show()



In [ ]: