python4oceanographers

Turning ripples into waves

Plotting Quickscat wind velocity and Hellerman and Rosenstein wind stress climatology

As soon as classes return from a "forced" holiday (AKA Carnaval), I indent to show my Weather and Climate students some database sources to create the figures they need for their homework.

Here I'll show two simple examples: wind from scatterometer (Quickscat*) and wind Stress from the Hellerman and Rosenstein climatology.

The concepts we will discuss in class will be: Synopticity, Wind, Wind Stress, and Climatology.

* Quickscat recently provided a really nice python class to load its binary data. Check my modified version for python 3 at the bottom of the post.

In [2]:
import sys
sys.path.append('../code')

import numpy as np
import numpy.ma as ma
import matplotlib.pyplot as plt

from netCDF4 import Dataset
from mpl_toolkits.basemap import Basemap
from quickscat import get_uv, QuikScatDaily

def make_basemap(projection='robin', figsize=(10, 5), resolution='c'):
    fig, ax = plt.subplots(figsize=figsize)
    m = Basemap(projection=projection, resolution=resolution,
                lon_0=0, ax=ax)
    m.drawcoastlines()
    m.fillcontinents(color='0.95')
    parallels = np.arange(-60, 90, 30.)
    meridians = np.arange(-360, 360, 60.)
    m.drawparallels(parallels, labels=[1, 0, 0, 0])
    m.drawmeridians(meridians, labels=[0, 0, 1, 0])
    return fig, m
In [3]:
qscat = QuikScatDaily('./data/qscat_20091031v4.gz', missing=-999.)

winddir = ma.masked_equal(qscat.variables['winddir'], -999.)
windspd = ma.masked_equal(qscat.variables['windspd'], -999.)
lon = qscat.variables['longitude']
lat = qscat.variables['latitude']
lon, lat = np.meshgrid(lon, lat)

u, v = get_uv(np.ones_like(windspd), winddir)
bad = np.where(windspd < 0)
u[bad] = 0.
v[bad] = 0.

windspd = windspd[0, ...]
u = u[1, ...]
v = v[1, ...]
In [4]:
import os
filename = './data/winds_hellermanAndRosenstein.nc'

if os.path.isfile(filename):
    nc = Dataset(filename)
else:
    nc = Dataset("http://iridl.ldeo.columbia.edu/SOURCES/.HELLERMAN/dods")
    
X = nc.variables['X'][:]
Y = nc.variables['Y'][:]
time = nc.variables['T'][:]
taux = nc.variables['taux'][:].mean(axis=0) / 10
tauy = nc.variables['tauy'][:].mean(axis=0) / 10
X, Y = np.meshgrid(X, Y)
tau = np.sqrt(taux**2 + tauy**2)

vec = taux + 1j * tauy
spd = np.abs(vec)
ang = np.angle(vec, deg=True)
ang = np.mod(90 - ang, 360)  # Zero is North.
taux, tauy = get_uv(np.ones_like(spd), ang)
In [5]:
kw = dict(latlon=True, color='black', alpha=0.75)

cut = slice(0, -1, 15), slice(0, -1, 15)
fig, m = make_basemap(projection='robin', figsize=(10, 5), resolution='c')
cs = m.pcolormesh(lon, lat, windspd, cmap=plt.cm.rainbow, latlon=True)
fig.colorbar(cs, extend='both', orientation='horizontal', shrink=0.65, pad=0.05)
Q = m.quiver(lon[cut], lat[cut], u[cut], v[cut], **kw)

cut = slice(0, -1, 4), slice(0, -1, 4)
fig, m = make_basemap(projection='robin', figsize=(10, 5), resolution='c')
cs = m.pcolormesh(X, Y, tau, cmap=plt.cm.rainbow, latlon=True)
fig.colorbar(cs, extend='both', orientation='horizontal', shrink=0.65, pad=0.05)
Q = m.quiver(X[cut], Y[cut], taux[cut], tauy[cut], **kw)

All done! Now they have at least two examples to start working.

In [6]:
HTML(html)
Out[6]:

This post was written as an IPython notebook. It is available for download or as a static html.

Creative Commons License
python4oceanographers by Filipe Fernandes is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Based on a work at https://ocefpaf.github.io/.

[QuickScat] quickscat.py download
"""
Module for reading and verifying RSS gridded binary data files.

Remote Sensing Systems
444 Tenth Street, Suite 200
Santa Rosa, CA 95401, USA
FTP: ftp://ftp.ssmi.com
Web: http://www.remss.com
Support: support@remss.com
Terms of Data Use:
http://www.remss.com/terms_of_data_use/terms_of_data_use.html
"""

import sys
import copy
import gzip
import decimal
import numpy as np
from operator import mul
from functools import reduce
from collections import namedtuple
from collections import OrderedDict


class Dataset(object):
    """Base class for bytemap datasets.
    Public data:
        filename = name of data file
        missing = fill value used for missing data;
                  if None, then fill with byte codes (251-255)
        dimensions = dictionary of dimensions for each coordinate
        variables = dictionary of data for each variable

    All classes derived from Dataset must implement the following:
        _attributes() = returns list of attributes for each variable (list)
        _coordinates() = returns coordinates (tuple)
        _shape() = returns shape of raw data (tuple)
        _variables() = returns list of all variables to get (list)

    The derived class must provide "_get_" methods for the attributes.

    If the derived class provides "_get_" methods for the variables,
    those methods receive first priority.

    The "_get_" methods in this module receive second priority.

    The last priority is "_default_get", which requires:
        _get_index(var) = returns bmap index for var
        _get_scale(var) = returns bmap scale for var
        _get_offset(var) = returns bmap offset for var
    """

    def __init__(self):
        self.dimensions = self._get_dimensions()
        self.variables = self._get_variables()

    def _default_get(self, var, bmap):
        data = get_data(bmap, self._get_index(var))
        acopy = copy.deepcopy(data)
        bad = is_bad(data)
        try:
            data *= self._get_scale(var)
        except _NoValueFound:
            pass
        try:
            data += self._get_offset(var)
        except _NoValueFound:
            pass
        if not self.missing:
            data[bad] = acopy[bad]
        else:
            data[bad] = self.missing
        return data

    def _dtype(self):
        return np.uint8

    def _get(self, var):
        try:
            return _get_(var, _from_=self)
        except _NoMethodFound:
            pass
        try:
            return _get_(var, _from_=thismodule())
        except _NoMethodFound:
            pass
        return self._default_get

    def _get_avariable(self, var, data):
        variable = self._get(var)(var, data)
        return Variable(var, variable, self)

    def _get_coordinates(self, var=None):
        if not var:
            return self._coordinates()
        if var in self._coordinates():
            return (var,)
        return tuple([c for c in self._coordinates() if c != 'variable'])

    def _get_dimensions(self):
        dims = OrderedDict(list(zip(self._coordinates(), self._shape())))
        del dims['variable']
        return dims

    def _get_variables(self):
        data = OrderedDict()
        try:
            stream = readgz(self.filename)
        except:
            return data
        bmap = unpack(stream, shape=self._shape(), dtype=self._dtype())
        for var in self._variables():
            data[var] = self._get_avariable(var, bmap)
        return data


def readgz(filename):
    with gzip.open(filename, 'rb') as f:
        stream = f.read()
    return stream


def thismodule():
    return sys.modules[__name__]


def unpack(stream, shape, dtype):
    count = reduce(mul, shape)
    return np.fromstring(stream, dtype=dtype, count=count).reshape(shape)


"""Library of Methods for _get_ Functions:"""


def btest(ival, ipos):
    """Same usage as Fortran btest function."""
    return (ival & (1 << ipos)) != 0


def cosd(x):
    return np.cos(np.radians(x))


def get_data(bmap, indx, dtype=np.float64):
    """Return numpy array of dytpe for the variable in bmap given by indx."""
    return np.array(np.squeeze(bmap[..., indx, :, :]), dtype=dtype)


def get_uv(speed, direction):
    """
    Given speed and direction (degrees oceanographic),
    return u (zonal) and v (meridional) components.
    """
    u = speed * sind(direction)
    v = speed * cosd(direction)
    return u, v


def ibits(ival, ipos, ilen):
    """Same usage as Fortran ibits function."""
    ones = ((1 << ilen)-1)
    return (ival & (ones << ipos)) >> ipos


def is_bad(bmap, maxvalid=250):
    """Return mask where data are bad."""
    return bmap > maxvalid


def sind(x):
    return np.sin(np.radians(x))

where = np.where


"""Library of Named Exceptions:"""

_NoMethodFound = AttributeError

_NoValueFound = (AttributeError, KeyError)

_NotFound = AttributeError


"""Library of Named _get_ Functions:"""


def _get_(var, _from_):
    return getattr(_from_, '_get_%s' % var)


def _get_ice(var, bmap, indx=0, icevalue=252):
    return get_data(bmap, indx, dtype=bmap.dtype) == icevalue


def _get_land(var, bmap, indx=0, landvalue=255):
    return get_data(bmap, indx, dtype=bmap.dtype) == landvalue


def _get_latitude(var, bmap, nlat=720, dlat=0.25, lat0=-89.875):
    if np.shape(bmap)[-2] != nlat:
        sys.exit('Latitude mismatch')
    return np.array([dlat*ilat + lat0 for ilat in range(nlat)])


def _get_longitude(var, bmap, nlon=1440, dlon=0.25, lon0=0.125):
    if np.shape(bmap)[-1] != nlon:
        sys.exit('Longitude mismatch')
    return np.array([dlon*ilon + lon0 for ilon in range(nlon)])


def _get_nodata(var, bmap, indx=0):
    return is_bad(get_data(bmap, indx, dtype=bmap.dtype))


class Variable(np.ndarray):
    """Variable exists solely to subclass numpy array with attributes."""

    def __new__(cls, var, data, dataset):
        obj = np.asarray(data).view(cls)
        for attr in dataset._attributes():
            get = _get_(attr, _from_=dataset)
            setattr(obj, attr, get(var))
        return obj


OneOb = namedtuple('OneOb', 'lon lat asc val ndp')
"""
OneOb corresponds to one observation from verify file with:
    lon = longitude index
    lat = latitude index
    asc = ascending/descending index
    val = verify value
    ndp = number of decimal places given in verify
    The (asc,lat,lon) indices are 0-based.
"""


def places(astring):
    """
    Given a string representing a floating-point number,
    return number of decimal places of precision (note: is negative).
    """
    return decimal.Decimal(astring).as_tuple().exponent


def readtext(filename):
    with open(filename, 'r') as f:
        lines = f.readlines()
    return lines


def tokenize(line):
    return [item.strip() for item in line.split()]


def zerobased(indx):
    return indx - 1


class QuikScatDaily(Dataset):
    """Read daily QSCAT bytemaps.
    Public data:
        filename = name of data file
        missing = fill value used for missing data;
                  if None, then fill with byte codes (251-255)
        dimensions = dictionary of dimensions for each coordinate
        variables = dictionary of data for each variable

    This routine will read RSS scatterometer daily bytemap files.
    reads version-4 files released April 2011

    The routine reads the quikscat file and similar to a netCDF file provides
    the contents and variable attributes:
    'mingmt' : 'Minute of Day UTC',
    'windspd' : '10-m Surface Wind Speed in m/s',
    'winddir' : '10-m Surface Wind Direction', degrees the wind is blowing to,
                North = 0 deg
    'scatflag' : 'Scatterometer Rain Flag', 0=no rain, 1=rain
    'radrain' : 'Radiometer Rain Flag', collocated radiometer columnar rain
                rate in km*mm/hr
                this is found using an SSMI or TMI observation within 60
                minutes of the scat obs.
        radrain contains -999. where there is no collocated radiometer data
                            0. where there is radiometer data, but there is no
                               measurable rain
                           -1. where there is radiometer data, no measurable
                               rain, but nearby cells have rain
                            0.5 to 31.0  km*mm/hr radiometer columnar rain
                                         rate for that cell
    'longitude' : 'Grid Cell Center Longitude',
    'latitude' : 'Grid Cell Center Latitude',
    'land' : 'Is this land?',
    'ice' : 'Is this ice?',
    'nodata' : 'Is there no data?'

    The center of the first cell of the 1440 column and 720 row map is at
    0.125 E longitude and -89.875 latitude.
    The center of the second cell is 0.375 E longitude, -89.875 latitude.
        real lat = 0.25 * cell y position -90.125
        real lon = 0.25 * cell x position -0.125

    please read the description file on www.remss.com for information on the
    various fields, or contact RSS support:  http://www.remss.com/support

    program created for quikscat  Aug 2013
    """

    def __init__(self, filename, missing=-999.):
        """
        Required arguments:
            filename = name of data file to be read (string)

        Optional arguments:
            missing = fill value for missing data,
                      default is the value used in verify file
        """
        self.filename = filename
        self.missing = missing
        Dataset.__init__(self)

    def _attributes(self):
        return ['coordinates', 'long_name', 'units', 'valid_min', 'valid_max']

    def _coordinates(self):
        return ('orbit_segment', 'variable', 'latitude', 'longitude')

    def _shape(self):
        return (2, 4, 720, 1440)

    def _variables(self):
        return ['mingmt', 'windspd', 'winddir', 'scatflag', 'radrain',
                'longitude', 'latitude', 'land', 'ice', 'nodata']

    def _get_index(self, var):
        return {'windspd': 1,
                'winddir': 2,
                'rain': 3,
                }[var]

    def _get_scale(self, var):
        return {'mingmt': 6.0,
                'windspd': 0.2,
                'winddir': 1.5,
                }[var]

    def _get_long_name(self, var):
        return {'mingmt': 'Minute of Day UTC',
                'windspd': '10-m Surface Wind Speed',
                'winddir': '10-m Surface Wind Direction',
                'scatflag': 'Scatterometer Rain Flag',
                'radrain': 'Radiometer Rain Flag',
                'longitude': 'Grid Cell Center Longitude',
                'latitude': 'Grid Cell Center Latitude',
                'land': 'Is this land?',
                'ice': 'Is this ice?',
                'nodata': 'Is there no data?',
                }[var]

    def _get_units(self, var):
        return {'mingmt': 'minute of day',
                'windspd': 'm/s',
                'winddir': 'deg oceanographic',
                'scatflag': '0=no-rain, 1=rain',
                'radrain': '0=no-rain, -1=adjacent rain, >0=rain(km*mm/hr)',
                'longitude': 'degrees east',
                'latitude': 'degrees north',
                'land': 'True or False',
                'ice': 'True or False',
                'nodata': 'True or False',
                }[var]

    def _get_valid_min(self, var):
        return {'mingmt': 0.0,
                'windspd': 0.0,
                'winddir': 0.0,
                'scatflag': 0,
                'radrain': -1,
                'longitude': 0.0,
                'latitude': -90.0,
                'land': False,
                'ice': False,
                'nodata': False,
                }[var]

    def _get_valid_max(self, var):
        return {'mingmt': 1440.0,
                'windspd': 50.0,
                'winddir': 360.0,
                'scatflag': 1,
                'radrain': 31,
                'longitude': 360.0,
                'latitude': 90.0,
                'land': True,
                'ice': True,
                'nodata': True,
                }[var]

    def _get_mingmt(self, var, bmap):
        mingmt = get_data(bmap, indx=0)
        mingmt *= self._get_scale(var)
        return mingmt

    def _get_scatflag(self, var, bmap):
        indx = self._get_index('rain')
        scatflag = get_data(ibits(bmap, ipos=0, ilen=1), indx=indx)
        bad = is_bad(get_data(bmap, indx=0))
        scatflag[bad] = self.missing
        return scatflag

    def _get_radrain(self, var, bmap):
        indx = self._get_index('rain')
        radrain = get_data(ibits(bmap, ipos=1, ilen=1), indx=indx)
        good = (radrain == 1)
        radrain[~good] = self.missing
        intrain = get_data(ibits(bmap, ipos=2, ilen=6), indx=indx)
        nonrain = where(intrain == 0)
        adjrain = where(intrain == 1)
        hasrain = where(intrain > 1)
        intrain[nonrain] = 0.0
        intrain[adjrain] = -1.0
        intrain[hasrain] = 0.5 * (intrain[hasrain]-1)
        radrain[good] = intrain[good]
        bad = is_bad(get_data(bmap, indx=0))
        radrain[bad] = self.missing
        return radrain


class QuikScatAveraged(Dataset):
    """ Read averaged QSCAT bytemaps.
    Public data:
        filename = name of data file
        missing = fill value used for missing data;
                  if None, then fill with byte codes (251-255)
        dimensions = dictionary of dimensions for each coordinate
        variables = dictionary of data for each variable

    This subroutine will read RSS scatterometer time-averaged bytemap files.
    reads version-4 files released April 2011

    The routine reads the quikscat file and similar to a netCDF file provides
    the contents and variable attributes:
    'windspd' : '10-m Surface Wind Speed in m/s',
    'winddir' : '10-m Surface Wind Direction', degrees the wind is blowing to,
                North = 0 deg
    'scatflag' : 'Scatterometer Rain Flag', 0=no rain, 1=rain
    'radrain' : 'Radiometer Rain Flag', collocated radiometer columnar rain
                rate in km*mm/hr this is found using an SSMI or TMI
                observation within 60 minutes of the scat obs.
        radrain contains -999. where there is no collocated radiometer data
                            0. where there is radiometer data, but there is no
                               measurable rain
                           -1. where there is radiometer data, no measurable
                               rain, but nearby cells have rain
                            0.5 to 31.0  km*mm/hr radiometer columnar rain
                                         rate for that cell
    'longitude' : 'Grid Cell Center Longitude',
    'latitude' : 'Grid Cell Center Latitude',
    'land' : 'Is this land?',
    'ice' : 'Is this ice?',
    'nodata' : 'Is there no data?'

    The center of the first cell of the 1440 column and 720 row map is at
    0.125 E longitude and -89.875 latitude.  The center of the second cell is
    0.375 E longitude, -89.875 latitude.
        real lat = 0.25 * cell y position -90.125
        real lon = 0.25 * cell x position -0.125

    please read the description file on www.remss.com for information on the
    various fields, or contact RSS support:  http://www.remss.com/support

    program created for quikscat  Aug 2013
    """

    def __init__(self, filename, missing=-999.):
        """
        Required arguments:
            filename = name of data file to be read (string)

        Optional arguments:
            missing = fill value for missing data,
                      default is the value used in verify file
        """
        self.filename = filename
        self.missing = missing
        Dataset.__init__(self)

    def _attributes(self):
        return ['coordinates', 'long_name', 'units', 'valid_min', 'valid_max']

    def _coordinates(self):
        return ('variable', 'latitude', 'longitude')

    def _shape(self):
        return (3, 720, 1440)

    def _variables(self):
        return ['windspd', 'winddir', 'scatflag', 'radrain',
                'longitude', 'latitude', 'land', 'ice', 'nodata']

    # _default_get():
    def _get_index(self, var):
        return {'windspd': 0,
                'winddir': 1,
                'rain': 2}[var]

    def _get_scale(self, var):
        return {'windspd': 0.2,
                'winddir': 1.5}[var]

    def _get_long_name(self, var):
        return {'windspd': '10-m Surface Wind Speed',
                'winddir': '10-m Surface Wind Direction',
                'scatflag': 'Scatterometer Rain Flag',
                'radrain': 'Radiometer Rain Flag',
                'longitude': 'Grid Cell Center Longitude',
                'latitude': 'Grid Cell Center Latitude',
                'land': 'Is this land?',
                'ice': 'Is this ice?',
                'nodata': 'Is there no data?',
                }[var]

    def _get_units(self, var):
        return {'windspd': 'm/s',
                'winddir': 'deg oceanographic',
                'scatflag': '0=no-rain, 1=rain',
                'radrain': '0=no-rain, -1=adjacent rain, >0=rain(km*mm/hr)',
                'longitude': 'degrees east',
                'latitude': 'degrees north',
                'land': 'True or False',
                'ice': 'True or False',
                'nodata': 'True or False',
                }[var]

    def _get_valid_min(self, var):
        return {'windspd': 0.0,
                'winddir': 0.0,
                'scatflag': 0,
                'radrain': -1,
                'longitude': 0.0,
                'latitude': -90.0,
                'land': False,
                'ice': False,
                'nodata': False,
                }[var]

    def _get_valid_max(self, var):
        return {'windspd': 50.0,
                'winddir': 360.0,
                'scatflag': 1,
                'radrain': 31,
                'longitude': 360.0,
                'latitude': 90.0,
                'land': True,
                'ice': True,
                'nodata': True,
                }[var]

    def _get_scatflag(self, var, bmap):
        indx = self._get_index('rain')
        scatflag = get_data(ibits(bmap, ipos=0, ilen=1), indx=indx)
        bad = is_bad(get_data(bmap, indx=0))
        scatflag[bad] = self.missing
        return scatflag

    def _get_radrain(self, var, bmap):
        indx = self._get_index('rain')
        radrain = get_data(ibits(bmap, ipos=1, ilen=1), indx=indx)
        good = (radrain == 1)
        radrain[~good] = self.missing
        intrain = get_data(ibits(bmap, ipos=2, ilen=6), indx=indx)
        nonrain = where(intrain == 0)
        adjrain = where(intrain == 1)
        hasrain = where(intrain > 1)
        intrain[nonrain] = 0.0
        intrain[adjrain] = -1.0
        intrain[hasrain] = 0.5*(intrain[hasrain]-1)
        radrain[good] = intrain[good]
        bad = is_bad(get_data(bmap, indx=0))
        radrain[bad] = self.missing
        return radrain

Comments