diff --git a/mlair/helpers/geofunctions.py b/mlair/helpers/geofunctions.py index 2be519c50b99db7f44bdb49bb3eefd3540950812..d1a3b71236763e17b4ed5bf29d88961c29030569 100644 --- a/mlair/helpers/geofunctions.py +++ b/mlair/helpers/geofunctions.py @@ -3,12 +3,11 @@ __author__ = 'Felix Kleinert' __date__ = '2021-02-16' import dask.array as da -import numpy import numpy as np import xarray as xr from mlair.helpers.helpers import convert2xrda -from typing import Union, Tuple, List +from typing import Union, Tuple xr_int_float = Union[xr.DataArray, xr.Dataset, np.ndarray, int, float] tuple_of_2xr_int_float = Tuple[xr_int_float, xr_int_float] @@ -37,7 +36,7 @@ def deg2rad_all_points(lat1: xr_int_float, lon1: xr_int_float, def haversine_dist(lat1: xr_int_float, lon1: xr_int_float, lat2: xr_int_float, lon2: xr_int_float, - to_radians: bool = True, earth_radius: float = 6371.,) -> xr.DataArray: + to_radians: bool = True, earth_radius: float = 6371., ) -> xr.DataArray: """ Calculate the great circle distance between two points on the Earth (specified in decimal degrees or in radians) @@ -83,8 +82,8 @@ def haversine_dist(lat1: xr_int_float, lon1: xr_int_float, def bearing_angle(lat1: xr_int_float, lon1: xr_int_float, - lat2: xr_int_float, lon2: xr_int_float, - to_radians: bool = True, return_deg: bool = True) -> xr.DataArray: + lat2: xr_int_float, lon2: xr_int_float, + to_radians: bool = True, return_deg: bool = True) -> xr.DataArray: """ Calculate initial bearing angle (forward azimuth) between multiple points. Implemented by following @@ -150,7 +149,7 @@ class WindSector: deg_per_sector = 360. / number_of_sectores edges_per_sector = np.array([- deg_per_sector / 2. + n * deg_per_sector for n in range(number_of_sectores + 1)]) # convert to range 0,360 - edges_per_sector = (edges_per_sector + 360) % 360. + edges_per_sector = (edges_per_sector + 360.) % 360. wind_sector_edges = {sect: [edges_per_sector[i], edges_per_sector[i + 1]] for i, sect in enumerate(wind_sectores)} self.edges_per_sector = edges_per_sector @@ -160,7 +159,7 @@ class WindSector: def _is_value_in_sector(value, left_edge, right_edge) -> bool: if left_edge < right_edge: # all sectors which does not cross 360°/0° - return da.logical_and(left_edge <= value, value < right_edge) + return da.logical_and(left_edge <= value, value < right_edge) elif left_edge > right_edge: # (noth) sector which crosses 360°/0° return da.logical_or( da.logical_and(left_edge <= value, value < 360.), # value between left_edge and 360° @@ -174,6 +173,239 @@ class WindSector: return self._is_value_in_sector(value, left_edge, right_edge) +class VectorRotate: + RAD_PER_DEG = np.pi / 180. + DEG_PER_RAD = 180. / np.pi + + @staticmethod + def interpolate_to_grid_center(data: xr.DataArray, interpolation_dim: str, **kwargs) -> da.array: + """ + Interpolate from staged grid cells to grid cell center. + + :param data: variable on staged ("half-level") grid + :type data: + :param interpolation_dim: Dimension name of staged dimension + :type interpolation_dim: + :param constant_values: + :type constant_values: + :return: + :rtype: + """ + constant_values = kwargs.pop('constant_values', np.nan) + + # store order of orig dims + orig_dims = data.dims + + # transpose to ensure that staged dimension is located at -1 + data = data.transpose(..., interpolation_dim) + new_dims = data.dims + + # create idx list for backward transposition to original dimension order + idx_to_backward_transpose = [list(orig_dims).index(i) for i in new_dims] + + # pad data i) left, ii) right for interpolation (mean between two staged values) + d1 = data.pad({interpolation_dim: (1, 0)}, constant_values=constant_values,) + d2 = data.pad({interpolation_dim: (0, 1)}, constant_values=constant_values,) + d = (d1.data + d2.data) * .5 + + # drop padding boundary + d = d[..., 1:-1] + if orig_dims != new_dims: + d = da.transpose(d, idx_to_backward_transpose) + return d + + @staticmethod + def ull_vll2wspd(ull, vll): + """ + Convert u, v to wind speed. + + :param ull: Wind's u-component + :type ull: + :param vll: Wind's v-component + :type vll: + :return: + :rtype: + """ + return da.sqrt(ull ** 2 + vll ** 2) + + def ull_vll2wdir(self, ull, vll): + """ + Conbert u, v to wind direction. + + :param ull: Wind's u-component + :type ull: + :param vll: Wind's v-component + :type vll: + :return: + :rtype: + """ + wdir = da.arctan2(-ull, -vll) * self.DEG_PER_RAD + wdir = (wdir + 360.) % 360. + return wdir + + def ull_vll2wspd_wdir(self, ull, vll): + """ + Convert u, v to wind speed and direction (wspd, wdir). + + :param ull: Wind's u-component + :type ull: + :param vll: Wind's v-component + :type vll: + :return: + :rtype: + """ + return self.ull_vll2wspd(ull, vll), self.ull_vll2wdir(ull, vll) + + +class VectorRotateLambertConformal2latlon(VectorRotate): + CEN_LON = 12. + CEN_LAT = 52.5 + TRUELAT1 = 30. + TRUELAT2 = 60. + STAND_LON = 12. + + def __init__(self, uu, vv, xlat='XLAT', xlong='XLONG', cen_lon=CEN_LON, cen_lat=CEN_LAT, truelat1=TRUELAT1, truelat2=TRUELAT2, stand_lon=STAND_LON): + self.cen_lon = cen_lon + self.cen_lat = cen_lat + self.truelat1 = truelat1 + self.truelat2 = truelat2 + self.stand_lon = stand_lon + self.xlat = self._set_llcoords(None, xlat) + self.xlong = self._set_llcoords(None, xlong) + + @staticmethod + def _set_llcoords(wind_grd, xll): + if isinstance(xll, str): + return wind_grd[xll] + elif isinstance(xll, np.ndarray) or isinstance(xll, da.core.Array) or isinstance(xll, xr.DataArray): + return xll + else: + raise TypeError( + f"`xll' must be name of xll coord of wind_grd as str or np/da/xr array. But is of type {type(xll)}") + + @property + def cone(self): + """ + Rotation formulas taken from WRFSI's module_map_util.F. + + :return: + :rtype: + """ + # case (1) ! Lambert conformal + if np.abs(self.truelat1 - self.truelat2) > 0.1: # !secant projection + return (da.log(da.cos(self.truelat1 * self.RAD_PER_DEG)) - da.log( + da.cos(self.truelat2 * self.RAD_PER_DEG))) / ( + da.log(da.tan((90. - np.abs(self.truelat1)) * self.RAD_PER_DEG * .5)) - da.log( + da.tan((90. - np.abs(self.truelat2)) * self.RAD_PER_DEG * .5))) + else: # !tangent projection + return da.sin(np.abs(self.truelat1)*self.RAD_PER_DEG) + + @property + def _alpha(self): + return self._difflong * self.cone * self.RAD_PER_DEG + + @property + def _difflong(self): + dlong = self.xlong - self.stand_lon + dlong = da.where(dlong > +180., dlong - 360., dlong) + dlong = da.where(dlong < -180., dlong + 360., dlong) + return dlong + + def ugrd2ull(self, ugrd, vgrd): + """ + Rotate u from Lambert conformal to geographic lat/lon. + + :param ugrd: + :type ugrd: + :param vgrd: + :type vgrd: + :return: + :rtype: + """ + return vgrd * da.sin(self._alpha) + ugrd * da.cos(self._alpha) + + def vgrd2vll(self, ugrd, vgrd): + """ + Rotate v from Lambert conformal to geographic lat/lon. + + :param ugrd: + :type ugrd: + :param vgrd: + :type vgrd: + :return: + :rtype: + """ + return vgrd * da.cos(self._alpha) - ugrd * da.sin(self._alpha) + + def ugrd_vgrd2ull_vll(self, ugrd, vgrd): + """ + Rotate u, v from Lambert conformal to u, v in geographic lat/lon. + + :param ugrd: + :type ugrd: + :param vgrd: + :type vgrd: + :return: + :rtype: + """ + return self.ugrd2ull(ugrd, vgrd), self.vgrd2vll(ugrd, vgrd) + + def ustg_vstg2ull_vll(self, ustg, vstg, ustg_dim: str, vstg_dim: str): + """ + First interpolate u, v from staged grid cells to grid center, than rotate u, v from Lambert conformal to u, v + in geographic lat/lon. + :param ustg: wind's u-component on staged ("half") grid in Lambert conformal + :type ustg: + :param vstg: wind's v-component on staged ("half") grid in Lambert conformal + :type vstg: + :param ustg_dim: Dimension name of staged u-component + :type ustg_dim: + :param vstg_dim: Dimension name of staged v-component + :type vstg_dim: + :return: u, v on grid center in geographic lat/lon direction + :rtype: + """ + ugrd = self.interpolate_to_grid_center(ustg, ustg_dim) + vgrd = self.interpolate_to_grid_center(vstg, vstg_dim) + return self.ugrd_vgrd2ull_vll(ugrd, vgrd) + + def ugrd_vgrd2wspd_wdir(self, ugrd, vgrd): + """ + Convert u, v from Lambert conformal to wdspd, wdir in geographic lat/lon. + + :param ugrd: wind's u-component on grid center in Lambert conformal + :type ugrd: + :param vgrd: wind's v-component on grid center in Lambert conformal + :type vgrd: + :return: + :rtype: + """ + ull, vll = self.ugrd_vgrd2ull_vll(ugrd, vgrd) + return self.ull_vll2wspd_wdir(ull, vll) + + def ustg_vstg2wspd_wdir(self, ustg, vstg, ustg_dim: str, vstg_dim: str): + """ + First interpolate u, v from staged grid cells to grid center, than rotate u, v from Lambert conformal to + wdspd, wdir in geographic lat/lon. + + :param ustg: wind's u-component on staged ("half") grid in Lambert conformal + :type ustg: + :param vstg: wind's v-component on staged ("half") grid in Lambert conformal + :type vstg: + :param ustg_dim: Dimension name of staged u-component + :type ustg_dim: + :param vstg_dim: Dimension name of staged u-component + :type vstg_dim: + :return: + :rtype: + """ + ull, vll = self.ustg_vstg2ull_vll(ustg, vstg, ustg_dim, vstg_dim) + return self.ull_vll2wspd_wdir(ull, vll) + + + + + if __name__ == '__main__': kansas_StLouis = bearing_angle(lat1=39.099912, lon1=-94.581213, lat2=38.627089, lon2=-90.200203, @@ -187,5 +419,3 @@ if __name__ == '__main__': t1_exp = 0. t2 = bearing_angle(lat1=0., lon1=0., lat2=0., lon2=90., to_radians=True, return_deg=True) t2_exp = 90. - -