#!/usr/bin/env python
# -*- coding: utf-8 -*-
# System modules
import logging
import warnings
from functools import partial
from copy import deepcopy
# External modules
import xarray as xr
import numpy as np
# Internal modules
from pylawr.utilities.conventions import naming_convention
from pylawr.utilities.trafo import from_decibel_to_linear, zr_vars
logger = logging.getLogger(__name__)
TAGS_KEY = "tags"
TAGS_SEP = ";"
[docs]def tag_array(array, tag):
"""
Attempt to :py:meth:`~pylawr.RadarField.add_tag()` to a given array
Parameters
----------
array : array-like
the array to tag
tag : str
the tag to add to the array
Returns
-------
bool
If tagging was successful or not
"""
try:
try:
array.add_tag(tag)
except AttributeError:
array.lawr.add_tag(tag)
logger.debug("Tagged array with '{}'".format(tag))
return True
except AttributeError:
logger.warning("Could not tag array with '{}'!".format(tag))
return False
[docs]def array_has_tag(array, tag):
"""
Check if an array has a given tag
Parameters
----------
array : array-like
the array to check
tag : str
the tag to look for
Returns
-------
bool
Whether the array has the tag
"""
tags = []
try:
try:
tags = array.tags
except AttributeError:
tags = array.lawr.tags
except AttributeError:
logger.warning("This array does not seem to be able to have tags!")
return tag in tags
[docs]def untag_array(array, tag):
"""
Remove a tag from an array with :py:meth:`~pylawr.RadarField.remove_tag()`
Parameters
----------
array : array-like
the array to operate on
tag : str
the tag to remove
Returns
-------
bool
Whether the tag has been removed
"""
try:
try:
array.remove_tag(tag)
except AttributeError:
array.lawr.remove_tag(tag)
logger.debug("Removed tag '{}' from array".format(tag))
return True
except AttributeError:
logger.warning("array does not seem to be able to handle tags!")
return False
[docs]def get_verified_grid(array, grid=None):
"""
Get a verified grid from the array and given grid.
Parameters
----------
array : :py:class:`xarray.DataArray`
If no grid is given, this array will be used to determine the grid.
The grid is verified to this array.
grid : child of :py:class:`~pylawr.grid.base.BaseGrid` or None
This grid is verified within this method. If no grid is given, the
grid from the array is used.
Returns
-------
verified_grid : child of :py:class:`~pylawr.grid.base.BaseGrid`
The verified and inferred grid. If a grid is given and verified,
this will be the same grid as given.
Raises
------
AttributeError
An AttributeError is raised if no grid was specified and no grid is
set for the array.
TypeError
A TypeError is raised if the given grid is not a valid grid.
ValueError
A ValueError is raised if the given grid does not have the same
shape as the given array.
"""
if grid is None:
try:
grid = array.lawr.grid
except (TypeError, AttributeError):
raise AttributeError(
'A grid is needed, but no Grid is specified and no grid is '
'set for the array!'
)
verified_grid = array.lawr.check_grid(grid=grid)
return verified_grid
[docs]@xr.register_dataarray_accessor('lawr')
class RadarField(object):
"""
RadarField is a xarray.DataArray accessor to extend xarray for radar
processing. The accessor is called with xarray.DataArray.lawr.
Parameters
----------
data : :py:class:`xarray.DataArray`
This accessor is valid for this given data array.
Attributes
----------
grid : child of :py:class:`~pylawr.grid.base.BaseGrid`
The grid of this RadarField. The grid is used for localization,
interpolation and plotting purpose.
"""
def __init__(self, data):
self._data = data
self._grid = None
def __repr__(self):
return "{}:\n{}".format(self.__class__.__name__, repr(self.data))
@property
def data(self):
"""
Get the data for this accessor.
Returns
-------
:py:class:`xarray.DataArray`
The data for this :py:class:`xarray.DataArray` accessor.
"""
return self._data
@property
def grid(self):
if self._grid is None:
raise TypeError('The grid isn\'t set yet!')
return self._grid
@grid.setter
def grid(self, grid):
logger.debug('Got {0} as grid'.format(grid))
if grid is None:
self._grid = grid
else:
self._grid = self.check_grid(grid)
@property
def tags(self):
"""
Tags describing what has already happened to this field
:type: list
:getter: Converts the :any:`RadarField.data`
:any:`xarray.DataArray.attrs` ``"tags"`` to a :any:`list`
:setter: Sets the :any:`RadarField.data`
:any:`xarray.DataArray.attrs` ``"tags"`` key
"""
try:
self.data.attrs[TAGS_KEY]
except KeyError:
self.data.attrs[TAGS_KEY] = ""
tags = [x for x in self.data.attrs[TAGS_KEY].split(TAGS_SEP) if x != ""]
return tags
@tags.setter
def tags(self, newtags):
newtags = [x for x in newtags if x != ""]
self.data.attrs[TAGS_KEY] = TAGS_SEP.join(newtags)
[docs] def add_tag(self, newtag):
"""
Add a tag to :any:`tags`
Parameters
----------
newtag : str
The new tag to add
"""
tags = self.tags
if not newtag in tags:
tags.append(str(newtag))
self.tags = tags
[docs] def remove_tag(self, tag):
"""
Remove a tag (and any duplicates) from :any:`tags`. Don't complain if
the tag doesn't exist.
Parameters
----------
tag : str
The tag to remove
"""
self.tags = [x for x in self.tags if x != tag]
[docs] def set_variable(self, name):
"""
Set the variable name of this DataArray to the given name. The unit,
long name and short name are set according to the naming convention
defined in `utilities/conventions.py`.
Parameters
----------
name : str
The name of the variable. This name needs to be defined in the
naming convention.
Returns
-------
named_array : :py:class:`xarray.DataArray`
The DataArray with the set name, unit, long name and short name. All
other attributes will be also set. If a grid is defined for this
DataArray it will be carried over.
"""
if name not in naming_convention.keys():
raise KeyError(
'The given variable name: {0:s} cannot be found within the '
'naming convention.\nAvailable variables: {1:s}'.format(
name, ','.join(naming_convention.keys())
)
)
named_array = self.data.copy()
named_array.name = name
named_array.attrs.update(naming_convention[name])
try:
named_array = named_array.lawr.set_grid_coordinates(
self.grid
)
except TypeError:
# No grid is set
pass
return named_array
[docs] def check_grid(self, grid):
"""
Check if the given grid is a valid grid for this DataArray.
Parameters
----------
grid : child of :py:class:`~pylawr.grid.base.BaseGrid`
This grid is checked.
Returns
-------
checked_grid : child of :py:class:`~pylawr.grid.base.BaseGrid`
The grid object is returned if it is valid.
"""
if not hasattr(grid, '_calc_altitude'):
raise TypeError('The grid has to be a gridlike type!')
else:
data_coords_shape = self._data.shape[-len(grid.grid_shape):]
if grid.grid_shape != data_coords_shape:
raise ValueError(
'This grid is has not the right coordinates shape!\nactual:'
'{0}, desired: {1}'.format(grid.grid_shape,
data_coords_shape
)
)
else:
return grid
[docs] def set_grid_coordinates(self, grid):
"""
Set the grid coordinates for this DataArray. The coordinates of this
DataArray are replaced by the coordinates of the grid.
Parameters
----------
grid : child of :py:class:`~pylawr.grid.base.BaseGrid`
This grid is used to set the grid and the grid coordinates of the
returned array.
Returns
-------
gridded_array : :py:class:`xarray.DataArray`
The :py:class:`~xarray.DataArray` with the grid coordinates and
the grid.
"""
_ = self.check_grid(grid)
rename_dict = {new: old for new, old in zip(
self.data.dims[-len(grid.coord_names):], grid.coord_names)
if new != old}
gridded_array = self.data.rename(rename_dict)
new_coordinates = grid.get_coordinates()
for coord in grid.coord_names:
gridded_array.coords[coord] = new_coordinates[coord]
gridded_array.lawr._grid = grid
return gridded_array
[docs] def grid_to_array(self):
"""
Convert the array to a :py:class:`~xarray.DataArray` with the grid as
attributes and variables.
Returns
-------
combined_ds : :py:class:`xarray.DataArray`
The :py:class:`~xarray.DataArray` with the added grid.
"""
gridded_ds = self.set_grid_coordinates(self.grid).to_dataset()
lat_lon_ds = self.grid.get_lat_lon()
altitude_ds = self.grid.get_altitude().to_dataset(name='altitude')
combined_ds = xr.merge([gridded_ds, lat_lon_ds, altitude_ds],)
combined_ds = combined_ds.assign_attrs(
grid_center=self.grid.center,
grid_type=self.grid.__class__.__name__
)
return combined_ds
[docs] def filter(self, filter_class, *args, **kwargs):
"""
Method to apply a filter to this class.
Parameters
----------
filter_class : Python obj
This instance of a python class is used to filter the data. The
class needs a transform method with data as first argument and
grid as keyword argument. The return value of the transform method
should be a xarray.DataArray. A possible second return value is
interpreted as new grid.
*args :
The variable length argument list is passed to the transform method
of the filter_class.
**kwargs :
The variable keyword argument dictionary is passed to the transform
method of the filter_class. This argument could be used if another
array is necessary to transform this array with the given filter
class.
Returns
-------
filtered_data : xarray.DataArray
The filtered data array, with the set grid.
"""
filtered_data = filter_class.transform(self.data, grid=self._grid,
*args, **kwargs)
if isinstance(filtered_data, tuple) and len(filtered_data) == 2:
filtered_data = filtered_data[0].lawr.set_grid_coordinates(
filtered_data[1])
elif self._grid is not None:
filtered_data = filtered_data.lawr.set_grid_coordinates(self._grid)
return filtered_data
[docs] def _convert_field(self, conv_func, target_var='dbz'):
# Catch warnings from trafo fn and transform values
with warnings.catch_warnings():
warnings.simplefilter('ignore')
converted_field = conv_func(self.data)
converted_field = converted_field.lawr.set_metadata(self.data)
converted_field = converted_field.lawr.set_variable(target_var)
return converted_field
[docs] def zr_convert(self, a=200.0, b=1.6, inverse=False):
r"""
Convert from z to r via Z/R relationship. The Z/R relationship is
calculated with
.. math::
r = \sqrt[b]{\frac{z}{a}}
for conversion from z to r. If inverse is chosen, then the unit will be
converted from r to z with:
.. math::
z = a \cdot r^{b}
Parameters
----------
a : float, optional
Parameter a of the Z/R relationship. This is the scale factor of the
Z/R relationship. Default is 200.0 from Marshall-Palmer.
b : float, optional
Parameter b of the Z/R relationship. This is the polynomial grade
factor of the Z/R relationship. Default is 1.6 from Marshall-Palmer.
inverse : bool, optional
If the reflectivity should be calculated based on a rain rate field
(True) or if the rain rate should be calculated based on the
reflectivity (False). Default is False.
Returns
-------
converted_field : :py:class:`xarray.DataArray`
The converted rain field with r or z as unit and the same grid as
this DataArray.
Warnings
--------
UserWarning :
An UserWarning will be raised if the rain field is already the right
variable or if the field is in decibel units.
"""
warn_msg = None
# Get direction and target variable
if inverse:
conversion_func = partial(self._reflectivity, a=a, b=b)
var = 'z'
else:
conversion_func = partial(self._rainrate, a=a, b=b)
var = 'rr'
# Check if already var
if self.data.name == var:
warn_msg = 'The variable is already {0:s}, I will return the ' \
'input array!'.format(var)
# Check if convertible
not_convertible = self.data.name is not None and \
self.data.name not in zr_vars
if not_convertible:
warn_msg = '{0:s} cannot converted to {1:s}, I will return the ' \
'input array!'.format(self.data.name, var)
# If warning
if isinstance(warn_msg, str):
warnings.warn(warn_msg, UserWarning)
return self.data
# Convert the field
converted_field = self._convert_field(conversion_func, var)
return converted_field
[docs] def _rainrate(self, x, a=200.0, b=1.6):
r"""
Converts to rain rate r via Z/R relationship.
.. math::
r = \sqrt[b]{\frac{z}{a}}
Parameters
----------
x : int, float or array (radar reflectivity factor, mm**6/m**3)
a : float, optional
Parameter a of the Z/R relationship. This is the scale factor of the
Z/R relationship. Default is 200.0 from Marshall-Palmer.
b : float, optional
Parameter b of the Z/R relationship. This is the polynomial grade
factor of the Z/R relationship. Default is 1.6 from Marshall-Palmer.
Returns
-------
converted `x` in rain rate (mm/h)
"""
return (x / a) ** (1. / b)
[docs] def _reflectivity(self, x, a=200.0, b=1.6):
"""
Converts to radar reflectivity factor via Z/R relationship.
Parameters
----------
x : int, float or array (rain rate, mm/h)
a : float, optional
Parameter a of the Z/R relationship. This is the scale factor of the
Z/R relationship. Default is 200.0 from Marshall-Palmer.
b : float, optional
Parameter b of the Z/R relationship. This is the polynomial grade
factor of the Z/R relationship. Default is 1.6 from Marshall-Palmer.
Returns
-------
converted `x` in radar reflectivity factor (mm**6/mm**3)
"""
return a * x ** b
[docs] def db_to_linear(self, inverse=False):
r"""
Transform this rain field from decibel units to linear units.
.. math::
\mathrm{linear} &= 10^{\frac{\mathrm{decibel}}{10}}
\mathrm{decibel} &= 10 \cdot \log_{10}(\mathrm{linear})
.. note::
Due to conventions: Linear reflectivities smaller than or equal to
:math:`0\,\mathrm{mm^6\,m^{-3}}` will be transformed to
:math:`-32.5\,\mathrm{dBZ}` in decibel unit and vice versa.
Parameters
----------
inverse : bool, optional
Default is False.
Returns
-------
converted_field : :py:class:`xarray.DataArray`
The converted rain field with Gaussian decibel or linear non-decibel
units. The field has the same grid as this DataArray. The target
variable is determined automatically. If name is None, the target
variable will be either linear_reflectivity or reflectivity.
"""
warn_msg = None
# Get direction and target variable
if inverse:
conversion_func = self._decibel
from_linear_to_decibel = {
v: k for k, v in from_decibel_to_linear.items()
}
try:
var = from_linear_to_decibel[self.data.name]
except KeyError as e:
if isinstance(self.data.name, str):
warn_msg = 'The variable `{0:s}` cannot be converted, I ' \
'will return the input array!'.format(
self.data.name)
elif self.data.name is None:
var = 'dbz'
else:
raise e
else:
conversion_func = self._linear
try:
var = from_decibel_to_linear[self.data.name]
except KeyError as e:
if isinstance(self.data.name, str):
warn_msg = 'The variable `{0:s}` cannot be converted, I ' \
'will return the input array!'.format(
self.data.name)
elif self.data.name is None:
var = 'z'
else:
raise e
# Warning is raised
if isinstance(warn_msg, str):
warnings.warn(warn_msg, UserWarning)
return self.data
converted_field = self._convert_field(conversion_func, target_var=var)
return converted_field
[docs] def _decibel(self, x, dbzmin=-32.5):
r"""
Converts to decibel unit.
.. math::
\mathrm{decibel} = 10 \cdot \log_{10}(\mathrm{linear})
.. note::
Due to conventions: Linear reflectivities smaller than or equal to
:math:`0\,\mathrm{mm^6\,m^{-3}}` will be transformed to
:math:`-32.5\,\mathrm{dBZ}` in decibel unit.
Parameters
----------
x : :py:class:`xarray.DataArray`
The radar field with linear non-decibel unit.
Returns
-------
:py:class:`xarray.DataArray`
The radar field with decibel unit.
"""
mask = x <= 10. ** (dbzmin / 10.)
return xr.where(mask, dbzmin, 10. * np.log10(x))
[docs] def _linear(self, x, dbzmin=-32.5):
r"""
Converts to linear unit.
.. math::
\mathrm{linear} = 10^{\frac{\mathrm{decibel}}{10}}
.. note::
Due to conventions: Reflectivities in decibel unit smaller than or
equal to :math:`-32\,\mathrm{dBZ}` will be transformed to
:math:`0\,\mathrm{mm^6\,m^{-3}}` in linear unit.
Parameters
----------
x : :py:class:`xarray.DataArray`
The radar field with decibel unit.
Returns
-------
:py:class:`xarray.DataArray`
The radar field with linear non-decibel unit.
"""
return xr.where(x <= dbzmin, 0., 10. ** (x / 10.))
[docs] def _z_to_z(self):
return self._convert_field(lambda x: x, 'z')
[docs] def _dbz_to_z(self):
return self.db_to_linear()
[docs] def _r_to_z(self):
return self.zr_convert(inverse=True)
[docs] def _dbr_to_z(self):
return self.db_to_linear().lawr.zr_convert(inverse=True)
[docs] def to_z(self):
"""
Convert the rain field to linear reflectivity from any arbitrary
variable. If no variable is specified in the attributes, it is assumed
that the rain field is the reflectivity in decibel. If the variable of
this DataArray is rain_rate `zr_convert` with default values will be
used.
Returns
-------
converted_field : :py:class:`xarray.DataArray`
The converted rain field as linear reflectivity. The field has the
same grid and attributes (except for the variable unit and names) as
this DataArray.
"""
from_dict = {
None: self._dbz_to_z,
'z': self._z_to_z,
'dbz': self._dbz_to_z,
'rr': self._r_to_z,
'dbrr': self._dbr_to_z
}
converted_field = from_dict[self.data.name]()
return converted_field
[docs] def to_dbz(self):
"""
Convert the rain field to reflectivity in decibel from any arbitrary
variable. If no variable is specified in the attributes, it is assumed
that the rain field is the reflectivity in decibel. If the variable of
this DataArray is rain_rate `zr_convert` with default values will be
used.
Returns
-------
converted_field : :py:class:`xarray.DataArray`
The converted rain field as reflectivity in dB. The field has the
same grid and attributes (except for the variable unit and
names) as this DataArray.
"""
converted_field = self.to_z()
converted_field = converted_field.lawr.db_to_linear(inverse=True)
return converted_field
[docs] def to_netcdf(self, save_path):
"""
Save the RadarField to given save path as NetCDF-file. The grid is added
to the attributes such that it can be restored from the attributes.
Parameters
----------
save_path : str
Path where the file should be saved. If the path is a directory the
file name is inferred by the radar and grid.
"""
pass
[docs] def get_rain_mask(self, threshold=5):
mask = (self.to_dbz() > threshold).values
return mask
[docs] @classmethod
def load(cls, load_path):
"""
The RadarField is loaded from a NetCDF-file. This method should be only
used if the field is saved with the 'to_netcdf' method.
Parameters
----------
load_path : str
Path where the file is saved. The file need to be a NetCDF.file.
Returns
-------
loaded_field : :py:class:`xarray.DataArray`
The loaded radar field.k
"""
pass