Python plot curvilinear data#

Software requirements:

  • Python 3

  • numpy

  • xarray

  • matplotlib

  • cartopy

Example script#

curvilinear_grid_wrapped_lines_corrected.py

#!/usr/bin/env python
# coding: utf-8
'''
DKRZ example

Curvilinear grids

Curvilinear grids have 2-dimensional longitude lon(y,x) and latitude lat(y,x)
coordinate arrays, where x and y are just the indexes.
These kind of grids often makes it difficult to plot the data on its native
grid without remapping to a regular lonlat grid. For example, the
Matplotlib's contour and contourf functions wrap the contour lines around
most projections which looks streaky.

In this example we use the function z_masked_overlap() from htonchia at
https://github.com/SciTools/cartopy/issues/1421 to fix the wrapping problem.

The ncdump output of the input dataset:

netcdf MPI-ESM-LR_tos_20060101 {
dimensions:
  x = 256 ;
  y = 220 ;
  nv4 = 4 ;
  time = UNLIMITED ; // (1 currently)
  nb2 = 2 ;
variables:
  float lon(y, x) ;
          lon:long_name = "longitude coordinate" ;
          lon:units = "degrees_east" ;
          lon:standard_name = "longitude" ;
          lon:_CoordinateAxisType = "Lon" ;
          lon:bounds = "lon_bnds" ;
  float lon_bnds(y, x, nv4) ;
  float lat(y, x) ;
          lat:long_name = "latitude coordinate" ;
          lat:units = "degrees_north" ;
          lat:standard_name = "latitude" ;
          lat:_CoordinateAxisType = "Lat" ;
          lat:bounds = "lat_bnds" ;
  float lat_bnds(y, x, nv4) ;
  double time(time) ;
          time:bounds = "time_bnds" ;
          time:units = "days since 1850-01-01 00:00:00" ;
          time:calendar = "proleptic_gregorian" ;
  double time_bnds(time, nb2) ;
          time_bnds:units = "days since 1850-01-01 00:00:00" ;
          time_bnds:calendar = "proleptic_gregorian" ;
  float tos(time, y, x) ;
          tos:long_name = "Sea Surface Temperature" ;
          tos:standard_name = "sea_surface_temperature" ;
          tos:units = "K" ;
          tos:coordinates = "lon lat" ;
          tos:_FillValue = 1.e+20f ;


See also https://en.wikipedia.org/wiki/Curvilinear_coordinates

-------------------------------------------------------------------------------
2022 copyright DKRZ licensed under CC BY-NC-SA 4.0 <br>
               (https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en)
-------------------------------------------------------------------------------#
'''
import os
import xarray as xr
import numpy as np

import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature

# Function z_masked_overlap() to prevent the line wrapping in contour plots
# The function z_masked_overlap was taken from
# https://github.com/SciTools/cartopy/issues/1421 from htonchia.

def z_masked_overlap(axe, X, Y, Z, source_projection=None):
    """
    for data in projection axe.projection
    find and mask the overlaps (more 1/2 the axe.projection range)

    X, Y either the coordinates in axe.projection or longitudes latitudes
    Z the data
    operation one of 'pcorlor', 'pcolormesh', 'countour', 'countourf'

    if source_projection is a geodetic CRS data is in geodetic coordinates
    and should first be projected in axe.projection

    X, Y are 2D same dimension as Z for contour and contourf
    same dimension as Z or with an extra row and column for pcolor
    and pcolormesh

    return ptx, pty, Z
    """
    if not hasattr(axe, 'projection'):
        return X, Y, Z

    if not isinstance(axe.projection, ccrs.Projection):
        return X, Y, Z

    if len(X.shape) != 2 or len(Y.shape) != 2:
        return X, Y, Z

    if (source_projection is not None and
        isinstance(source_projection, ccrs.Geodetic)):
        transformed_pts = axe.projection.transform_points(
            source_projection, X, Y)
        ptx, pty = transformed_pts[..., 0], transformed_pts[..., 1]
    else:
        ptx, pty = X, Y

    with np.errstate(invalid='ignore'):
        # diagonals have one less row and one less columns
        diagonal0_lengths = np.hypot(
            ptx[1:, 1:] - ptx[:-1, :-1],
            pty[1:, 1:] - pty[:-1, :-1]
        )

        diagonal1_lengths = np.hypot(
            ptx[1:, :-1] - ptx[:-1, 1:],
            pty[1:, :-1] - pty[:-1, 1:]
        )

        to_mask = (
            (diagonal0_lengths > (
                abs(axe.projection.x_limits[1]
                    - axe.projection.x_limits[0])) / 2) |
            np.isnan(diagonal0_lengths) |
            (diagonal1_lengths > (
                abs(axe.projection.x_limits[1]
                    - axe.projection.x_limits[0])) / 2) |
            np.isnan(diagonal1_lengths)
        )

        # TODO check if we need to do something about surrounding vertices
        # add one extra colum and row for contour and contourf

        if (to_mask.shape[0] == Z.shape[0] - 1 and
                to_mask.shape[1] == Z.shape[1] - 1):
            to_mask_extended = np.zeros(Z.shape, dtype=bool)
            to_mask_extended[:-1, :-1] = to_mask
            to_mask_extended[-1, :] = to_mask_extended[-2, :]
            to_mask_extended[:, -1] = to_mask_extended[:, -2]
            to_mask = to_mask_extended

        if np.any(to_mask):
            Z_mask = getattr(Z, 'mask', None)
            to_mask = to_mask if Z_mask is None else to_mask | Z_mask
            Z = np.ma.masked_where(to_mask, Z)

        return ptx, pty, Z

def main():
    plt.switch_backend('agg')

    #-- Open curvilinear dataset
    fname = '../../data/MPI-ESM-LR_tos_20060101.nc'

    ds = xr.open_dataset(fname)
    var  = ds.tos.isel(time=0)
    lat = ds.lat
    lon = ds.lon

    #-- The coordinate arrays lat and lon are bounds, so the variable var should be
    #-- the value inside those bounds. Therefore, the last value from the var array
    #-- has to be removed.
    var = var[:-1, :-1]

    #-- Colormap
    cmap = 'RdBu_r'

    #-- Set vmin, vmax, and levels
    vmin = 260.
    vmax = 300.
    levels = 20

    #-- Create the figure
    projection = ccrs.PlateCarree()

    fig = plt.figure(figsize=(14, 8), constrained_layout=False)

    #-- Define space for the four plots
    gs = fig.add_gridspec(nrows=2, ncols=2, hspace=0, wspace=0.05)

    #-- Generate the subplots
    ax1 = plt.subplot(gs[0, 0], projection=projection)
    ax2 = plt.subplot(gs[1, 0], projection=projection)
    ax3 = plt.subplot(gs[0, 1], projection=projection)
    ax4 = plt.subplot(gs[1, 1], projection=projection)

    #-- Upper left plot
    ax1.set_title('curvilinear grid - wrapped lines', fontsize=16)
    ax1.set_global()
    ax1.add_feature(cfeature.LAND, color='gray')
    ax1.add_feature(cfeature.OCEAN, color='gray')
    ax1.add_feature(cfeature.COASTLINE)
    ax1.gridlines(draw_labels=True)

    plot = ax1.contour(lon[:-1,:-1], lat[:-1,:-1], var,
                       cmap=cmap,
                       levels=levels,
                       vmin=vmin,
                       vmax=vmax,
                       linewidths=1.0)
    plt.colorbar(plot, ax=ax1, shrink=0.6, pad=0.09)

    #-- Compute the corrected data to prevent line wrapping. Use the dataset
    #-- coordinates and variable.
    X, Y, maskedZ = z_masked_overlap(ax1,
                                     lon.data,
                                     lat.data,
                                     ds.tos.isel(time=0),
                                     source_projection=ccrs.Geodetic())
    #-- Lower left plot
    ax2.set_global()
    ax2.add_feature(cfeature.LAND, color='gray')
    ax2.add_feature(cfeature.OCEAN, color='gray')
    ax2.add_feature(cfeature.COASTLINE)
    ax2.gridlines(draw_labels=True)
    ax2.set_title('curvilinear grid - corrected', fontsize=16)

    plot = ax2.contour(X, Y, maskedZ,
                       cmap=cmap,
                       levels=levels,
                       vmin=vmin,
                       vmax=vmax,
                       linewidths=1.0)
    plt.colorbar(plot, ax=ax2, shrink=0.6, pad=0.09)

    #-- Check if it works the same for contour fill.
    #-- Display the incorrect and the corrected contour fill plot
    #-- Upper right plot
    ax3.set_title('curvilinear grid - wrapped contours', fontsize=16)
    ax3.set_global()
    ax3.add_feature(cfeature.LAND, color='gray')
    ax3.add_feature(cfeature.OCEAN, color='gray')
    ax3.add_feature(cfeature.COASTLINE)
    ax3.gridlines(draw_labels=True)

    plot = ax3.contourf(lon[:-1,:-1], lat[:-1,:-1], var,
                        cmap=cmap,
                        levels=levels,
                        vmin=vmin,
                        vmax=vmax)
    plt.colorbar(plot, ax=ax3, shrink=0.6, pad=0.09)

    #-- Use the already computed X, Y, maskedZ from above.
    #-- lower right plot
    ax4.set_global()
    ax4.add_feature(cfeature.LAND, color='gray')
    ax4.add_feature(cfeature.OCEAN, color='gray')
    ax4.add_feature(cfeature.COASTLINE)
    ax4.gridlines(draw_labels=True)
    ax4.set_title('curvilinear grid - corrected', fontsize=16)

    plot = ax4.contourf(X, Y, maskedZ,
                        cmap=cmap,
                        levels=levels,
                        vmin=vmin,
                        vmax=vmax)
    plt.colorbar(plot, ax=ax4, shrink=0.6, pad=0.09)

    plt.savefig('plot_curvilinear_contour_2x2_plots.png', bbox_inches='tight', dpi=100)


if __name__ == '__main__':
    main()
    

Plot result:

image0