#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# Copyright (c) 2011-2020, wradlib developers.
# Distributed under the MIT License. See LICENSE.txt for more info.
"""
Xarray Functions
^^^^^^^^^^^^^^^^^^^^^^^
.. autosummary::
:nosignatures:
:toctree: generated/
{}
"""
__all__ = ["as_xarray_dataarray", "create_xarray_dataarray", "georeference_dataset"]
__doc__ = __doc__.format("\n ".join(__all__))
import collections
import numpy as np
import xarray as xr
from wradlib.georef import polar
from wradlib.util import has_import, import_optional
osr = import_optional("osgeo.osr")
[docs]def as_xarray_dataarray(data, dims, coords):
"""Create Xarray DataArray from NumPy Array
.. versionadded:: 1.3
Parameters
----------
data : :class:`numpy:numpy.ndarray`
data array
dims : dict
dictionary describing xarray dimensions
coords : dict
dictionary describing xarray coordinates
Returns
-------
dataset : :py:class:`xarray:xarray.DataArray`
DataArray
"""
da = xr.DataArray(data, coords=dims.values(), dims=dims.keys())
da = da.assign_coords(**coords)
return da
[docs]def create_xarray_dataarray(
data,
r=None,
phi=None,
theta=None,
site=None,
sweep_mode="azimuth_surveillance",
rf=1.0,
**kwargs,
):
"""Create Xarray DataArray from Polar Radar Data
.. versionadded:: 1.3
Parameters
----------
data : :class:`numpy:numpy.ndarray`
The data array. It is assumed that the first dimension is over
the azimuth angles, while the second dimension is over the range bins
r : :class:`numpy:numpy.ndarray`
The ranges. Units may be chosen arbitrarily, m preferred.
phi : :class:`numpy:numpy.ndarray`
The azimuth angles in degrees.
theta : :class:`numpy:numpy.ndarray`
The elevation angles in degrees.
proj : :py:class:`gdal:osgeo.osr.SpatialReference`
Destination Spatial Reference System (Projection).
site : tuple
Tuple of coordinates of the radar site.
sweep_mode : str
Defaults to 'azimuth_surveillance'.
rf : float
factor to scale range, defaults to 1. (no scale)
Keyword Arguments
-----------------
re : float
effective earth radius
ke : float
adjustment factor to account for the refractivity gradient that
affects radar beam propagation. Defaults to 4/3.
dim0 : str
Name of the first dimension. Defaults to 'azimuth'.
dim1 : str
Name of the second dimension. Defaults to 'range'.
Returns
-------
dataset : :py:class:`xarray:xarray.DataArray`
DataArray
"""
if (r is None) or (phi is None) or (theta is None):
raise TypeError(
"wradlib: function `create_xarray_dataarray` requires "
"r, phi and theta keyword-arguments."
)
r = r.copy()
phi = phi.copy()
theta = theta.copy()
if site is None:
site = (0.0, 0.0, 0.0)
dims = collections.OrderedDict()
dim0 = kwargs.pop("dim0", "azimuth")
dim1 = kwargs.pop("dim1", "range")
dims[dim0] = np.arange(phi.shape[0])
dims[dim1] = r / rf
coords = {
"azimuth": ([dim0], phi),
"elevation": ([dim0], theta),
"longitude": (site[0]),
"latitude": (site[1]),
"altitude": (site[2]),
"sweep_mode": sweep_mode,
}
# create xarray dataarray
da = as_xarray_dataarray(data, dims=dims, coords=coords)
return da
[docs]def georeference_dataset(obj, **kwargs):
"""Georeference Dataset.
.. versionadded:: 1.5
This function adds georeference data to xarray Dataset/DataArray `obj`.
Parameters
----------
obj : :py:class:`xarray:xarray.Dataset` or :py:class:`xarray:xarray.DataArray`
Keyword Arguments
-----------------
proj : :py:class:`gdal:osgeo.osr.SpatialReference`, :py:class:`cartopy.crs.CRS` or None
If GDAL OSR SRS, output is in this projection, else AEQD.
re : float
earth's radius [m]
ke : float
adjustment factor to account for the refractivity gradient that
affects radar beam propagation. In principle this is wavelength-
dependent. The default of 4/3 is a good approximation for most
weather radar wavelengths.
Returns
----------
obj : :py:class:`xarray:xarray.Dataset` or :py:class:`xarray:xarray.DataArray`
"""
proj = kwargs.pop("proj", "None")
re = kwargs.pop("re", None)
ke = kwargs.pop("ke", 4.0 / 3.0)
# adding xyz aeqd-coordinates
site = (
obj.coords["longitude"].values,
obj.coords["latitude"].values,
obj.coords["altitude"].values,
)
if site == (0.0, 0.0, 0.0):
re = 6378137.0
# create meshgrid to overcome dimension problem with spherical_to_xyz
r, az = np.meshgrid(obj["range"], obj["azimuth"])
# GDAL OSR, convert to this proj
if has_import(osr) and isinstance(proj, osr.SpatialReference):
xyz = polar.spherical_to_proj(
r, az, obj["elevation"], site, proj=proj, re=re, ke=ke
)
# other proj, convert to aeqd
elif proj:
xyz, dst_proj = polar.spherical_to_xyz(
r, az, obj["elevation"], site, re=re, ke=ke, squeeze=True
)
# proj, convert to aeqd and add offset
else:
xyz, dst_proj = polar.spherical_to_xyz(
r, az, obj["elevation"], site, re=re, ke=ke, squeeze=True
)
xyz += np.array(site).T
# calculate center point
# use first range bins
ax = tuple(range(xyz.ndim - 2))
center = np.mean(xyz[..., 0, :], axis=ax)
# calculate ground range
gr = np.sqrt((xyz[..., 0] - center[0]) ** 2 + (xyz[..., 1] - center[1]) ** 2)
# dimension handling
dim0 = obj["azimuth"].dims[-1]
if obj["elevation"].dims:
dimlist = list(obj["elevation"].dims)
else:
dimlist = list(obj["azimuth"].dims)
# xyz is an array of cartesian coordinates for every spherical coordinate,
# so the possible dimensions are: elevation, azimuth, range, 3.
# For 2d, it either has (elevation, range, 3) or (azimuth, range, 3) dimensions.
# For 3d, the only option is the full (elevation, azimuth, range, 3) dimensions.
# Thus, adding the following two lines for the 3d case should not break other functionalities,
# and there should not be a case with more than 3 dimensions
if xyz.ndim > 3:
dimlist += ["azimuth"]
dimlist += ["range"]
# add xyz, ground range coordinates
obj.coords["x"] = (dimlist, xyz[..., 0])
obj.coords["y"] = (dimlist, xyz[..., 1])
obj.coords["z"] = (dimlist, xyz[..., 2])
obj.coords["gr"] = (dimlist, gr)
# adding rays, bins coordinates
if obj.sweep_mode == "azimuth_surveillance":
bins, rays = np.meshgrid(obj["range"], obj["azimuth"], indexing="xy")
else:
bins, rays = np.meshgrid(obj["range"], obj["elevation"], indexing="xy")
obj.coords["rays"] = ([dim0, "range"], rays)
obj.coords["bins"] = ([dim0, "range"], bins)
return obj