Source code for wradlib.georef.rect

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# Copyright (c) 2011-2023, wradlib developers.
# Distributed under the MIT License. See LICENSE.txt for more info.

"""
Rectangular Grid Functions
^^^^^^^^^^^^^^^^^^^^^^^^^^

.. autosummary::
   :nosignatures:
   :toctree: generated/

   {}
"""
__all__ = [
    "get_radolan_coords",
    "get_radolan_coordinates",
    "get_radolan_grid",
    "xyz_to_spherical",
    "grid_to_polyvert",
    "GeorefRectMethods",
]
__doc__ = __doc__.format("\n   ".join(__all__))
__doctest_requires__ = {"get_radolan_grid": ["osgeo"]}

from functools import singledispatch

import numpy as np
from xarray import DataArray, Dataset, concat

from wradlib.georef import projection
from wradlib.util import docstring, has_import, import_optional


[docs]def get_radolan_coords(lon, lat, **kwargs): """ Calculates x,y coordinates of radolan grid from lon, lat Parameters ---------- lon : float or :class:`numpy:numpy.ndarray` longitude lat : float, :class:`numpy:numpy.ndarray` latitude Keyword Arguments ----------------- crs : :py:class:`gdal:osgeo.osr.SpatialReference` | str projection of the DWD grid with spheroid model or string `trig` to use trigonometric formulas for calculation (only for earth model - `sphere`). Defaults to None (earth model - sphere). """ osr = import_optional("osgeo.osr") crs = kwargs.get("crs", None) # use trig if osgeo.osr is not available if crs is None and not has_import(osr): crs = "trig" if crs == "trig": # calculation of x_0 and y_0 coordinates of radolan grid # as described in the format description phi_0 = np.radians(60) phi_m = np.radians(lat) lam_0 = 10 lam_m = lon lam = np.radians(lam_m - lam_0) er = 6370.040 m_phi = (1 + np.sin(phi_0)) / (1 + np.sin(phi_m)) x = er * m_phi * np.cos(phi_m) * np.sin(lam) y = -er * m_phi * np.cos(phi_m) * np.cos(lam) else: # create radolan projection osr object if crs is None: crs = projection.create_osr("dwd-radolan") # create wgs84 projection osr object crs_wgs = projection.get_default_projection() x, y = projection.reproject(lon, lat, src_crs=crs_wgs, trg_crs=crs) return x, y
[docs]def get_radolan_coordinates(nrows=None, ncols=None, **kwargs): """Calculates x/y coordinates of radolan projection of the German Weather Service Returns the 1D x,y coordinates of the radolan projection for the given grid layout. Parameters ---------- nrows : int number of rows (460, 900 by default, 1100, 1500) ncols : int number of columns (460, 900 by default, 1400) Keyword Arguments ----------------- wgs84 : bool if True, output coordinates are in wgs84 lonlat format (default: False) mode : str 'radolan' - lower left pixel coordinates 'center' - pixel center coordinates 'edge' - pixel edge coordinates crs : :py:class:`gdal:osgeo.osr.SpatialReference` | str projection of the DWD grid with spheroid model or string `trig` to use trigonometric formulas for calculation (only for earth model - `sphere`). Defaults to None (earth model - sphere). Returns ------- radolan_ccords : tuple x and y 1D coordinate :class:`numpy:numpy.ndarray` shape is (nrows, ) and (ncols, ) if `mode='radolan'` shape is (nrows, ) and (ncols, ) if `mode='center'` shape is (nrows+1, ) and (ncols+1, ) if `mode='edge'` """ # setup default parameters in dicts tiny = {"j_0": 450, "i_0": 450, "res": 2} small = {"j_0": 460, "i_0": 460, "res": 2} rx = {"j_0": 450, "i_0": 450, "res": 1} normal_wx = {"j_0": 370, "i_0": 550, "res": 1} de1200 = {"j_0": 470, "i_0": 600, "res": 1} extended = {"j_0": 600, "i_0": 800, "res": 1} de4800 = {"j_0": 470, "i_0": 600, "res": 0.25} griddefs = { (450, 450): tiny, (460, 460): small, (900, 900): rx, (1100, 900): normal_wx, (1200, 1100): de1200, (1500, 1400): extended, (4800, 4400): de4800, } mode = kwargs.get("mode", "radolan") crs = kwargs.get("crs", None) if nrows and ncols: if not (isinstance(nrows, int) and isinstance(ncols, int)): raise TypeError("Parameter `nrows` and `ncols` need to be of integer type.") if (nrows, ncols) not in griddefs.keys(): raise ValueError( f"Parameter `nrows` ({nrows}) and `ncols` ({ncols}) are no " f"valid RADOLAN grid size numbers." ) else: # fallback for call without parameters nrows = 900 ncols = 900 # tiny, small, normal or extended grid check # reference point changes according to radolan composit format j_0 = griddefs[(nrows, ncols)]["j_0"] i_0 = griddefs[(nrows, ncols)]["i_0"] res = griddefs[(nrows, ncols)]["res"] x_0, y_0 = get_radolan_coords(9.0, 51.0, crs=crs) if mode == "edge": ncols += 1 nrows += 1 # get from km to meter for meter-base projections if crs is not None and crs != "trig": lin = crs.GetLinearUnits() if lin == 1.0: res *= 1000 j_0 *= 1000 i_0 *= 1000 x_arr = np.arange(x_0 - j_0, x_0 - j_0 + ncols * res, res) y_arr = np.arange(y_0 - i_0, y_0 - i_0 + nrows * res, res) if mode == "center": x_arr += res / 2.0 y_arr += res / 2.0 return x_arr, y_arr
[docs]def get_radolan_grid(nrows=None, ncols=None, **kwargs): """Calculates x/y coordinates of radolan grid of the German Weather Service Returns the x,y coordinates of the radolan grid positions (lower left corner of every pixel). The radolan grid is a polarstereographic projection, the projection information was taken from RADOLAN-RADVOR-OP Kompositformat_2.2.2 :cite:`DWD2009` .. table:: Coordinates for 900km x 900km grid +------------+-----------+------------+-----------+-----------+ | Coordinate | lon | lat | x | y | +============+===========+============+===========+===========+ | LowerLeft | 3.5889E | 46.9526N | -523.4622 | -4658.645 | +------------+-----------+------------+-----------+-----------+ | LowerRight | 14.6209E | 47.0705N | 376.5378 | -4658.645 | +------------+-----------+------------+-----------+-----------+ | UpperRight | 15.7208E | 54.7405N | 376.5378 | -3758.645 | +------------+-----------+------------+-----------+-----------+ | UpperLeft | 2.0715E | 54.5877N | -523.4622 | -3758.645 | +------------+-----------+------------+-----------+-----------+ .. table:: Coordinates for 1100km x 900km grid +------------+-----------+------------+-----------+-----------+ | Coordinate | lon | lat | x | y | +============+===========+============+===========+===========+ | LowerLeft | 4.6759E | 46.1929N | -443.4622 | -4758.645 | +------------+-----------+------------+-----------+-----------+ | LowerRight | 15.4801E | 46.1827N | 456.5378 | -4758.645 | +------------+-----------+------------+-----------+-----------+ | UpperRight | 17.1128E | 55.5342N | 456.5378 | -3658.645 | +------------+-----------+------------+-----------+-----------+ | UpperLeft | 3.0889E | 55.5482N | -443.4622 | -3658.645 | +------------+-----------+------------+-----------+-----------+ .. table:: Coordinates for 1500km x 1400km grid +------------+-----------+------------+-----------+-----------+ | Coordinate | lon | lat | x | y | +============+===========+============+===========+===========+ | LowerLeft | 2.3419E | 43.9336N | -673.4622 | -5008.645 | +------------+-----------+------------+-----------+-----------+ Parameters ---------- nrows : int number of rows (460, 900 by default, 1100, 1500) ncols : int number of columns (460, 900 by default, 1400) Keyword Arguments ----------------- wgs84 : bool if True, output coordinates are in wgs84 lonlat format (default: False) mode : str 'radolan' - lower left pixel coordinates 'center' - pixel center coordinates 'edge' - pixel edge coordinates crs : :py:class:`gdal:osgeo.osr.SpatialReference` | str projection of the DWD grid with spheroid model or string `trig` to use trigonometric formulas for calculation (only for earth model - `sphere`). Defaults to None (earth model - sphere). Returns ------- radolan_grid : :class:`numpy:numpy.ndarray` Array of xy- or lonlat-grid. shape is (nrows, ncols, 2) if `mode='radolan'` shape is (nrows, ncols, 2) if `mode='center'` shape is (nrows+1, ncols+1, 2) if `mode='edge'` Examples -------- >>> # using osr spatial reference transformation >>> import wradlib.georef as georef # noqa >>> radolan_grid = georef.get_radolan_grid() >>> print("{0}, ({1:.4f}, {2:.4f})".format(radolan_grid.shape, *radolan_grid[0,0,:])) # noqa (900, 900, 2), (-523.4622, -4658.6447) >>> # using pure trigonometric transformations >>> import wradlib.georef as georef >>> radolan_grid = georef.get_radolan_grid(crs="trig") >>> print("{0}, ({1:.4f}, {2:.4f})".format(radolan_grid.shape, *radolan_grid[0,0,:])) # noqa (900, 900, 2), (-523.4622, -4658.6447) >>> # using osr spatial reference transformation >>> import wradlib.georef as georef >>> radolan_grid = georef.get_radolan_grid(1500, 1400) >>> print("{0}, ({1:.4f}, {2:.4f})".format(radolan_grid.shape, *radolan_grid[0,0,:])) # noqa (1500, 1400, 2), (-673.4622, -5008.6447) >>> # using osr spatial reference transformation >>> import wradlib.georef as georef >>> radolan_grid = georef.get_radolan_grid(900, 900, wgs84=True) >>> print("{0}, ({1:.4f}, {2:.4f})".format(radolan_grid.shape, *radolan_grid[0,0,:])) # noqa (900, 900, 2), (3.5889, 46.9526) See :ref:`/notebooks/fileio/radolan/radolan_grid.ipynb#Polar-Stereographic-Projection`. Raises ------ TypeError, ValueError """ wgs84 = kwargs.get("wgs84", False) mode = kwargs.get("mode", "radolan") crs = kwargs.get("crs", None) x_arr, y_arr = get_radolan_coordinates(nrows=nrows, ncols=ncols, mode=mode, crs=crs) x, y = np.meshgrid(x_arr, y_arr) radolan_grid = np.dstack((x, y)) if wgs84: if crs == "trig": # inverse projection lon0 = 10.0 # central meridian of projection lat0 = 60.0 # standard parallel of projection sinlat0 = np.sin(np.radians(lat0)) fac = (6370.040**2.0) * ((1.0 + sinlat0) ** 2.0) lon = np.degrees(np.arctan(-x / y)) + lon0 lat = np.degrees( np.arcsin((fac - (x**2.0 + y**2.0)) / (fac + (x**2.0 + y**2.0))) ) radolan_grid = np.dstack((lon, lat)) else: # create radolan projection osr object if crs is None: crs = projection.create_osr("dwd-radolan") # create wgs84 projection osr object crs_wgs84 = projection.get_default_projection() radolan_grid = projection.reproject( radolan_grid, src_crs=crs, trg_crs=crs_wgs84 ) return radolan_grid
[docs]@singledispatch def xyz_to_spherical(*args, **kwargs): pass
@xyz_to_spherical.register(np.ndarray) def _xyz_to_spherical_numpy(xyz, *, altitude=0, crs=None, ke=4.0 / 3.0): """Returns spherical representation (r, theta, phi) of given cartesian coordinates (x, y, z) with respect to the reference altitude (asl) considering earth's geometry (crs). Parameters ---------- xyz : :class:`numpy:numpy.ndarray` Array of shape (..., 3). Contains cartesian coordinates. altitude : float Altitude (in meters) defaults to 0. crs : :py:class:`gdal:osgeo.osr.SpatialReference` projection of the source coordinates (aeqd) with spheroid model defaults to None. ke : float Adjustment factor to account for the refractivity gradient that affects radar beam propagation. In principle this is wavelength- dependend. The default of 4/3 is a good approximation for most weather radar wavelengths Returns ------- r : :class:`numpy:numpy.ndarray` Array of xyz.shape. Contains the radial distances. phi : :class:`numpy:numpy.ndarray` Array of xyz.shape. Contains the azimuthal angles. theta: :class:`numpy:numpy.ndarray` Array of xyz.shape. Contains the elevation angles. """ # get the approximate radius of the projection's ellipsoid # for the latitude_of_center, if no projection is given assume # spherical earth try: lat0 = crs.GetProjParm("latitude_of_center") re = projection.get_earth_radius(lat0, crs) except Exception: re = 6371000.0 # calculate xy-distance s = np.sqrt(np.sum(xyz[..., 0:2] ** 2, axis=-1)) # calculate earth's arc angle gamma = s / (re * ke) # calculate elevation angle theta numer = np.cos(gamma) - (re * ke + altitude) / (re * ke + xyz[..., 2]) denom = np.sin(gamma) theta = np.arctan(numer / denom) # calculate radial distance r r = (re * ke + xyz[..., 2]) * denom / np.cos(theta) # calculate azimuth angle phi phi = np.degrees(np.arctan2(xyz[..., 0], xyz[..., 1])) phi = np.fmod(phi + 360, 360) return r, phi, np.degrees(theta) @xyz_to_spherical.register(Dataset) @xyz_to_spherical.register(DataArray) def _xyz_to_spherical_xarray(obj, **kwargs): """Returns spherical representation (r, theta, phi) of given cartesian coordinates (x, y, z) with respect to the reference altitude (asl) considering earth's geometry (crs). Parameters ---------- obj : :py:class:`xarray:xarray.DataArray` | :py:class:`xarray:xarray.Dataset` Keyword Arguments ----------------- crs : :py:class:`gdal:osgeo.osr.SpatialReference` projection of the source coordinates (aeqd) with spheroid model defaults to None. ke : float Adjustment factor to account for the refractivity gradient that affects radar beam propagation. In principle this is wavelength- dependend. The default of 4/3 is a good approximation for most weather radar wavelengths Returns ------- obj : :py:class:`xarray:xarray.Dataset` obj with added spherical coordinates. """ # transform xp,yp,zp to ncoord xyzp = concat([obj.xp, obj.yp, obj.zp], dim="ncoord").transpose(..., "ncoord") r_sr, az_sr, elev_sr = _xyz_to_spherical_numpy( xyzp, altitude=xyzp.altitude, **kwargs ) obj = obj.assign_coords( { "range": r_sr, "azimuth": az_sr, "elevation": elev_sr, } ) return obj
[docs]def grid_to_polyvert(grid, *, ravel=False): """Get polygonal vertices from rectangular grid coordinates. Parameters ---------- grid : :class:`numpy:numpy.ndarray` grid edge coordinates ravel : bool, optional option to flatten the grid, defaults to False. Returns ------- polyvert : :class:`numpy:numpy.ndarray` A 3-d array of polygon vertices with shape (..., 5, 2). """ v1 = grid[:-1, :-1] v2 = grid[:-1, 1:] v3 = grid[1:, 1:] v4 = grid[1:, :-1] polyvert = np.stack((v1, v2, v3, v4, v1), axis=-2) if ravel: polyvert = polyvert.reshape((-1, 5, 2)) return polyvert
[docs]class GeorefRectMethods: """wradlib xarray SubAccessor methods for Georef Rect Methods."""
[docs] @docstring(_xyz_to_spherical_xarray) def xyz_to_spherical(self, *args, **kwargs): if not isinstance(self, GeorefRectMethods): return xyz_to_spherical(self, *args, **kwargs) else: return xyz_to_spherical(self._obj, *args, **kwargs)