Source code for windspharm.xarray

"""Spherical harmonic vector wind computations (`xarray` interface)."""
# Copyright (c) 2016-2018 Andrew Dawson
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from __future__ import absolute_import

try:
    import xarray as xr
except ImportError:
    import xray as xr

from . import standard
from ._common import get_apiorder, inspect_gridtype, to3d


[docs]class VectorWind(object): """Vector wind computations (`xarray` interface).""" def __init__(self, u, v, rsphere=6.3712e6, legfunc='stored'): """Initialize a VectorWind instance. **Arguments:** *u*, *v* Zonal and meridional components of the vector wind respectively. Both components should be `~xarray.DataArray` instances. The components must have the same dimension coordinates and contain no missing values. **Optional argument:** *rsphere* The radius in metres of the sphere used in the spherical harmonic computations. Default is 6371200 m, the approximate mean spherical Earth radius. *legfunc* 'stored' (default) or 'computed'. If 'stored', associated legendre functions are precomputed and stored when the class instance is created. This uses O(nlat**3) memory, but speeds up the spectral transforms. If 'computed', associated legendre functions are computed on the fly when transforms are requested. This uses O(nlat**2) memory, but slows down the spectral transforms a bit. **Example:** Initialize a `VectorWind` instance with zonal and meridional components of the vector wind:: from windspharm.xray import VectorWind w = VectorWind(u, v) """ if not isinstance(u, xr.DataArray) or not isinstance(v, xr.DataArray): raise TypeError('u and v must be xarray.DataArray instances') # Check that the dimension coordinates have the same names and values. ucoords = [u.coords[name].values for name in u.dims] vcoords = [v.coords[name].values for name in v.dims] if (u.dims != v.dims): msg = 'u and v must have the same dimension coordinates' raise ValueError(msg) if not all([(uc == vc).all() for uc, vc in zip(ucoords, vcoords)]): msg = 'u and v must have the same dimension coordinate values' raise ValueError(msg) # Find the latitude and longitude coordinates and reverse the latitude # dimension if necessary. lat, lat_dim = _find_latitude_coordinate(u) lon, lon_dim = _find_longitude_coordinate(u) if lat.values[0] < lat.values[1]: u = _reverse(u, lat_dim) v = _reverse(v, lat_dim) lat, lat_dim = _find_latitude_coordinate(u) # Determine the gridtype of the input. gridtype = inspect_gridtype(lat.values) # Determine how the DataArrays should be reordered to conform to the # windspharm.standard API. apiorder, _ = get_apiorder(u.ndim, lat_dim, lon_dim) apiorder = [u.dims[i] for i in apiorder] self._reorder = u.dims u = u.copy().transpose(*apiorder) v = v.copy().transpose(*apiorder) # Reshape the raw data and input into the API. self._ishape = u.shape self._coords = [u.coords[name] for name in u.dims] u = to3d(u.values) v = to3d(v.values) self._api = standard.VectorWind(u, v, gridtype=gridtype, rsphere=rsphere, legfunc=legfunc) def _metadata(self, var, name, **attributes): var = var.reshape(self._ishape) var = xr.DataArray(var, coords=self._coords, name=name) var = var.transpose(*self._reorder) for attr, value in attributes.items(): var.attrs[attr] = value return var
[docs] def u(self): """Zonal component of vector wind. **Returns:** *u* The zonal component of the wind. **Example:** Get the zonal component of the vector wind:: u = w.u() """ u = self._metadata(self._api.u, 'u', units='m s**-1', standard_name='eastward_wind', long_name='eastward_component_of_wind') return u
[docs] def v(self): """Meridional component of vector wind. **Returns:** *v* The meridional component of the wind. **Example:** Get the meridional component of the vector wind:: v = w.v() """ v = self._metadata(self._api.v, 'v', units='m s**-1', standard_name='northward_wind', long_name='northward_component_of_wind') return v
[docs] def magnitude(self): """Wind speed (magnitude of vector wind). **Returns:** *speed* The wind speed. **Example:** Get the magnitude of the vector wind:: spd = w.magnitude() """ m = self._api.magnitude() m = self._metadata(m, 'speed', units='m s**-1', standard_name='wind_speed', long_name='wind_speed') return m
[docs] def vrtdiv(self, truncation=None): """Relative vorticity and horizontal divergence. **Optional argument:** *truncation* Truncation limit (triangular truncation) for the spherical harmonic computation. **Returns:** *vrt*, *div* The relative vorticity and divergence respectively. **See also:** `~VectorWind.vorticity`, `~VectorWind.divergence`. **Examples:** Compute the relative vorticity and divergence:: vrt, div = w.vrtdiv() Compute the relative vorticity and divergence and apply spectral truncation at triangular T13:: vrtT13, divT13 = w.vrtdiv(truncation=13) """ vrt, div = self._api.vrtdiv(truncation=truncation) vrt = self._metadata(vrt, 'vorticity', units='s**-1', standard_name='atmosphere_relative_vorticity', long_name='relative_vorticity') div = self._metadata(div, 'divergence', units='s**-1', standard_name='divergence_of_wind', long_name='horizontal_divergence') return vrt, div
[docs] def vorticity(self, truncation=None): """Relative vorticity. **Optional argument:** *truncation* Truncation limit (triangular truncation) for the spherical harmonic computation. **Returns:** *vrt* The relative vorticity. **See also:** `~VectorWind.vrtdiv`, `~VectorWind.absolutevorticity`. **Examples:** Compute the relative vorticity:: vrt = w.vorticity() Compute the relative vorticity and apply spectral truncation at triangular T13:: vrtT13 = w.vorticity(truncation=13) """ vrt = self._api.vorticity(truncation=truncation) vrt = self._metadata(vrt, 'vorticity', units='s**-1', standard_name='atmosphere_relative_vorticity', long_name='relative_vorticity') return vrt
[docs] def divergence(self, truncation=None): """Horizontal divergence. **Optional argument:** *truncation* Truncation limit (triangular truncation) for the spherical harmonic computation. **Returns:** *div* The divergence. **See also:** `~VectorWind.vrtdiv`. **Examples:** Compute the divergence:: div = w.divergence() Compute the divergence and apply spectral truncation at triangular T13:: divT13 = w.divergence(truncation=13) """ div = self._api.divergence(truncation=truncation) div = self._metadata(div, 'divergence', units='s**-1', standard_name='divergence_of_wind', long_name='horizontal_divergence') return div
[docs] def planetaryvorticity(self, omega=None): """Planetary vorticity (Coriolis parameter). **Optional argument:** *omega* Earth's angular velocity. The default value if not specified is 7.292x10**-5 s**-1. **Returns:** *pvorticity* The planetary vorticity. **See also:** `~VectorWind.absolutevorticity`. **Example:** Compute planetary vorticity using default values:: pvrt = w.planetaryvorticity() Override the default value for Earth's angular velocity:: pvrt = w.planetaryvorticity(omega=7.2921150) """ f = self._api.planetaryvorticity(omega=omega) f = self._metadata( f, 'coriolis', units='s**-1', standard_name='coriolis_parameter', long_name='planetary_vorticity') return f
[docs] def absolutevorticity(self, omega=None, truncation=None): """Absolute vorticity (sum of relative and planetary vorticity). **Optional arguments:** *omega* Earth's angular velocity. The default value if not specified is 7.292x10**-5 s**-1. *truncation* Truncation limit (triangular truncation) for the spherical harmonic computation. **Returns:** *avorticity* The absolute (relative + planetary) vorticity. **See also:** `~VectorWind.vorticity`, `~VectorWind.planetaryvorticity`. **Examples:** Compute absolute vorticity:: avrt = w.absolutevorticity() Compute absolute vorticity and apply spectral truncation at triangular T13, also override the default value for Earth's angular velocity:: avrt = w.absolutevorticity(omega=7.2921150, truncation=13) """ avrt = self._api.absolutevorticity(omega=omega, truncation=truncation) avrt = self._metadata(avrt, 'absolute_vorticity', units='s**-1', standard_name='atmosphere_absolute_vorticity', long_name='absolute_vorticity') return avrt
[docs] def sfvp(self, truncation=None): """Streamfunction and velocity potential. **Optional argument:** *truncation* Truncation limit (triangular truncation) for the spherical harmonic computation. **Returns:** *sf*, *vp* The streamfunction and velocity potential respectively. **See also:** `~VectorWind.streamfunction`, `~VectorWind.velocitypotential`. **Examples:** Compute streamfunction and velocity potential:: sf, vp = w.sfvp() Compute streamfunction and velocity potential and apply spectral truncation at triangular T13:: sfT13, vpT13 = w.sfvp(truncation=13) """ sf, vp = self._api.sfvp(truncation=truncation) sf = self._metadata( sf, 'streamfunction', units='m**2 s**-1', standard_name='atmosphere_horizontal_streamfunction', long_name='streamfunction') vp = self._metadata( vp, 'velocity_potential', units='m**2 s**-1', standard_name='atmosphere_horizontal_velocity_potential', long_name='velocity potential') return sf, vp
[docs] def streamfunction(self, truncation=None): """Streamfunction. **Optional argument:** *truncation* Truncation limit (triangular truncation) for the spherical harmonic computation. **Returns:** *sf* The streamfunction. **See also:** `~VectorWind.sfvp`. **Examples:** Compute streamfunction:: sf = w.streamfunction() Compute streamfunction and apply spectral truncation at triangular T13:: sfT13 = w.streamfunction(truncation=13) """ sf = self._api.streamfunction(truncation=truncation) sf = self._metadata( sf, 'streamfunction', units='m**2 s**-1', standard_name='atmosphere_horizontal_streamfunction', long_name='streamfunction') return sf
[docs] def velocitypotential(self, truncation=None): """Velocity potential. **Optional argument:** *truncation* Truncation limit (triangular truncation) for the spherical harmonic computation. **Returns:** *vp* The velocity potential. **See also:** `~VectorWind.sfvp`. **Examples:** Compute velocity potential:: vp = w.velocity potential() Compute velocity potential and apply spectral truncation at triangular T13:: vpT13 = w.velocity potential(truncation=13) """ vp = self._api.velocitypotential(truncation=truncation) vp = self._metadata( vp, 'velocity_potential', units='m**2 s**-1', standard_name='atmosphere_horizontal_velocity_potential', long_name='velocity potential') return vp
[docs] def helmholtz(self, truncation=None): """Irrotational and non-divergent components of the vector wind. **Optional argument:** *truncation* Truncation limit (triangular truncation) for the spherical harmonic computation. **Returns:** *uchi*, *vchi*, *upsi*, *vpsi* Zonal and meridional components of irrotational and non-divergent wind components respectively. **See also:** `~VectorWind.irrotationalcomponent`, `~VectorWind.nondivergentcomponent`. **Examples:** Compute the irrotational and non-divergent components of the vector wind:: uchi, vchi, upsi, vpsi = w.helmholtz() Compute the irrotational and non-divergent components of the vector wind and apply spectral truncation at triangular T13:: uchiT13, vchiT13, upsiT13, vpsiT13 = w.helmholtz(truncation=13) """ uchi, vchi, upsi, vpsi = self._api.helmholtz(truncation=truncation) uchi = self._metadata(uchi, 'u_chi', units='m s**-1', long_name='irrotational_eastward_wind') vchi = self._metadata(vchi, 'v_chi', units='m s**-1', long_name='irrotational_northward_wind') upsi = self._metadata(upsi, 'u_psi', units='m s**-1', long_name='non_divergent_eastward_wind') vpsi = self._metadata(vpsi, 'v_psi', units='m s**-1', long_name='non_divergent_northward_wind') return uchi, vchi, upsi, vpsi
[docs] def irrotationalcomponent(self, truncation=None): """Irrotational (divergent) component of the vector wind. .. note:: If both the irrotational and non-divergent components are required then `~VectorWind.helmholtz` should be used instead. **Optional argument:** *truncation* Truncation limit (triangular truncation) for the spherical harmonic computation. **Returns:** *uchi*, *vchi* The zonal and meridional components of the irrotational wind respectively. **See also:** `~VectorWind.helmholtz`. **Examples:** Compute the irrotational component of the vector wind:: uchi, vchi = w.irrotationalcomponent() Compute the irrotational component of the vector wind and apply spectral truncation at triangular T13:: uchiT13, vchiT13 = w.irrotationalcomponent(truncation=13) """ uchi, vchi = self._api.irrotationalcomponent(truncation=truncation) uchi = self._metadata(uchi, 'u_chi', units='m s**-1', long_name='irrotational_eastward_wind') vchi = self._metadata(vchi, 'v_chi', units='m s**-1', long_name='irrotational_northward_wind') return uchi, vchi
[docs] def nondivergentcomponent(self, truncation=None): """Non-divergent (rotational) component of the vector wind. .. note:: If both the non-divergent and irrotational components are required then `~VectorWind.helmholtz` should be used instead. **Optional argument:** *truncation* Truncation limit (triangular truncation) for the spherical harmonic computation. **Returns:** *upsi*, *vpsi* The zonal and meridional components of the non-divergent wind respectively. **See also:** `~VectorWind.helmholtz`. **Examples:** Compute the non-divergent component of the vector wind:: upsi, vpsi = w.nondivergentcomponent() Compute the non-divergent component of the vector wind and apply spectral truncation at triangular T13:: upsiT13, vpsiT13 = w.nondivergentcomponent(truncation=13) """ upsi, vpsi = self._api.nondivergentcomponent(truncation=truncation) upsi = self._metadata(upsi, 'u_psi', units='m s**-1', long_name='non_divergent_eastward_wind') vpsi = self._metadata(vpsi, 'v_psi', units='m s**-1', long_name='non_divergent_northward_wind') return upsi, vpsi
[docs] def gradient(self, chi, truncation=None): """Computes the vector gradient of a scalar field on the sphere. **Argument:** *chi* A scalar field. It must be a `~xarray.DataArray` with the same latitude and longitude dimensions as the vector wind components that initialized the `VectorWind` instance. **Optional argument:** *truncation* Truncation limit (triangular truncation) for the spherical harmonic computation. **Returns:** *uchi*, *vchi* The zonal and meridional components of the vector gradient respectively. **Examples:** Compute the vector gradient of absolute vorticity:: avrt = w.absolutevorticity() avrt_zonal, avrt_meridional = w.gradient(avrt) Compute the vector gradient of absolute vorticity and apply spectral truncation at triangular T13:: avrt = w.absolutevorticity() avrt_zonalT13, avrt_meridionalT13 = w.gradient(avrt, truncation=13) """ if not isinstance(chi, xr.DataArray): raise TypeError('scalar field must be an xarray.DataArray') name = chi.name lat, lat_dim = _find_latitude_coordinate(chi) lon, lon_dim = _find_longitude_coordinate(chi) if (lat.values[0] < lat.values[1]): # need to reverse latitude dimension chi = _reverse(chi, lat_dim) lat, lat_dim = _find_latitude_coordinate(chi) apiorder, _ = get_apiorder(chi.ndim, lat_dim, lon_dim) apiorder = [chi.dims[i] for i in apiorder] reorder = chi.dims chi = chi.copy().transpose(*apiorder) ishape = chi.shape coords = [chi.coords[n] for n in chi.dims] chi = to3d(chi.values) uchi, vchi = self._api.gradient(chi, truncation=truncation) uchi = uchi.reshape(ishape) vchi = vchi.reshape(ishape) uchi_name = 'zonal_gradient_of_{!s}'.format(name) vchi_name = 'meridional_gradient_of_{!s}'.format(name) uchi = xr.DataArray(uchi, coords=coords, name=uchi_name, attrs={'long_name': uchi_name}) vchi = xr.DataArray(vchi, coords=coords, name=vchi_name, attrs={'long_name': vchi_name}) uchi = uchi.transpose(*reorder) vchi = vchi.transpose(*reorder) return uchi, vchi
[docs] def truncate(self, field, truncation=None): """Apply spectral truncation to a scalar field. This is useful to represent other fields in a way consistent with the output of other `VectorWind` methods. **Argument:** *field* A scalar field. It must be a `~xarray.DataArray` with the same latitude and longitude dimensions as the vector wind components that initialized the `VectorWind` instance. **Optional argument:** *truncation* Truncation limit (triangular truncation) for the spherical harmonic computation. If not specified it will default to *nlats - 1* where *nlats* is the number of latitudes. **Returns:** *truncated_field* The field with spectral truncation applied. **Examples:** Truncate a scalar field to the computational resolution of the `VectorWind` instance:: scalar_field_truncated = w.truncate(scalar_field) Truncate a scalar field to T21:: scalar_field_T21 = w.truncate(scalar_field, truncation=21) """ if not isinstance(field, xr.DataArray): raise TypeError('scalar field must be an xarray.Dataset') lat, lat_dim = _find_latitude_coordinate(field) lon, lon_dim = _find_longitude_coordinate(field) if (lat.values[0] < lat.values[1]): # need to reverse latitude dimension field = _reverse(field, lat_dim) lat, lat_dim = _find_latitude_coordinate(field) apiorder, _ = get_apiorder(field.ndim, lat_dim, lon_dim) apiorder = [field.dims[i] for i in apiorder] reorder = field.dims field = field.copy().transpose(*apiorder) ishape = field.shape fielddata = to3d(field.values) fieldtrunc = self._api.truncate(fielddata, truncation=truncation) field.values = fieldtrunc.reshape(ishape) field = field.transpose(*reorder) return field
def _reverse(array, dim): """Reverse an `xarray.DataArray` along a given dimension.""" slicers = [slice(0, None)] * array.ndim slicers[dim] = slice(-1, None, -1) return array[tuple(slicers)] def _find_coord_and_dim(array, predicate, name): """ Find a dimension coordinate in an `xarray.DataArray` that satisfies a predicate function. """ candidates = [coord for coord in [array.coords[n] for n in array.dims] if predicate(coord)] if not candidates: raise ValueError('cannot find a {!s} coordinate'.format(name)) if len(candidates) > 1: msg = 'multiple {!s} coordinates are not allowed' raise ValueError(msg.format(name)) coord = candidates[0] dim = array.dims.index(coord.name) return coord, dim def _find_latitude_coordinate(array): """Find a latitude dimension coordinate in an `xarray.DataArray`.""" return _find_coord_and_dim( array, lambda c: (c.name in ('latitude', 'lat') or c.attrs.get('units') == 'degrees_north' or c.attrs.get('axis') == 'Y'), 'latitude') def _find_longitude_coordinate(array): """Find a longitude dimension coordinate in an `xarray.DataArray`.""" return _find_coord_and_dim( array, lambda c: (c.name in ('longitude', 'lon') or c.attrs.get('units') == 'degrees_east' or c.attrs.get('axis') == 'X'), 'longitude')