"""
Tools
=====
.. autosummary::
:nosignatures:
:toctree: generated/
{}
"""
__all__ = [
"add_axis",
"concat_sweep_angle",
"create_filelist",
"create_lineplot",
"data_path",
"estimate_cmap",
"filter_data",
"filter_radar_bins",
"filter_stations",
"gauss_kernel",
"get_beamblockage",
"get_bins_from_range",
"get_discrete_cmap",
"get_discrete_norm",
"get_polar_ax",
"get_qbbf",
"get_range_step",
"get_temperature_profile",
"get_xpol_path",
"interpolate_dem",
"load_single_sweep",
"load_timeseries_sweep",
"load_volume",
"merge_radar_profile",
"noise_correction",
"noise_hist",
"noise_level",
"phase_offset",
"phase_zphi",
"plot_2by2",
"plot_beamprop",
"plot_matched_refl",
"plot_noise_hist",
"plot_unmatched_refl",
"plot_moment",
"plot_reference_colorbar",
"process_regression",
"ProgressBar",
"read_dwd_stations",
"rpol_data",
"save_dwd_station",
"set_sweep_mode_as_coords",
"smooth_data",
"timeseries_sweep_to_zarr",
"visdict14",
"volume_matching",
"xr_rolling",
"x_cumtrapz",
"zphi",
]
__doc__ = __doc__.format("\n ".join(__all__))
import datetime as dt
import fnmatch
import glob
import os
import pathlib
import re
import shutil
from contextlib import nullcontext
from functools import partial
from html.parser import HTMLParser
import certifi
import dask
import fsspec
import geopandas as gpd
import matplotlib as mpl
import matplotlib.pyplot as plt
import numcodecs
import numpy as np
import pandas as pd
import pyproj
import scipy
import shapely
import urllib3
import wradlib as wrl
import xarray as xr
import xradar as xd
import zarr
from dask.diagnostics import ProgressBar as _ProgressBar
from matplotlib.colors import LogNorm
from matplotlib.path import Path
from osgeo import osr
from scipy.spatial import KDTree
from tqdm.autonotebook import tqdm
from xhistogram.xarray import histogram
os.environ["WRADLIB_DATA"] = "/automount/ftp/radar/wradlib-data"
os.environ["CPL_DEBUG"] = "OFF"
ProgressBar = nullcontext if os.environ.get("SPHINX_BUILD") else _ProgressBar
###
## Data IO
def repo_path(start=None):
path = pathlib.Path(start or pathlib.Path.cwd()).resolve()
for parent in [path, *path.parents]:
if (parent / "pyproject.toml").exists() or (parent / ".git").exists():
return parent
# if we do not find anything, we just take the current path
return path
[docs]
def data_path(start=None):
env = os.environ.get("RP_DATA")
if env:
return pathlib.Path(env).resolve()
path = repo_path(start)
return path / "data"
os.environ["RP_DATA"] = str(data_path())
rp_mode = os.environ.get("RP_MODE", "ftp")
if rp_mode == "ftp":
target_protocol = "ftp"
target_options = {
"host": "ftp.meteo.uni-bonn.de",
"username": "anonymous",
"password": "anonymous@",
}
cache_storage = str(data_path() / ".cache/ftp.meteo.uni-bonn.de")
elif rp_mode == "automount":
target_protocol = "file"
target_options = {}
cache_storage = str(data_path() / ".cache/automount")
else:
raise ValueError("Unknown RP mode!")
rpol_data = fsspec.filesystem(
"filecache",
target_protocol=target_protocol,
target_options=target_options,
cache_storage=cache_storage,
)
class DWDHTMLParser(HTMLParser):
def handle_starttag(self, tag, attrs):
if tag != "a":
return
self.links.append(attrs[0][1])
[docs]
def get_xpol_path(path=None, start_time=None, loc="boxpol", fs=None):
"""Create path of BoXPol/JuXPol radar data files.
Parameter
---------
path : str
Path to root folder of radar data,
defaults to None (/automount/radar/scans or /automount/radar-archiv/scans)
start_time : datetime.datetime
datetime - object to select correct folder
loc : str
"boxpol" or "juxpol" (not case sensitive)
Return
------
radar_path : str
Path to radar data
"""
if start_time is None:
start_time = dt.datetime.today()
loc = "" if loc.lower()[0:2] == "bo" else "_juelich"
if fs is None:
fs = rpol_data
if path is None:
if fs.protocol == "filecache":
_fs = fs.fs
else:
_fs = fs
if _fs.protocol == ("file", "local"):
ins = "-archiv" if start_time < dt.datetime(2015, 1, 1) else ""
path = f"/automount/radar{ins}/scans{loc}"
if not fs.exists(path):
path = os.environ["RADARMET_DATA"]
else:
path = "/radar/rp-data/scans"
radar_path = os.path.join(path, "{0}/{0}-{1:02}/{0}-{1:02d}-{2:02d}")
return radar_path.format(start_time.year, start_time.month, start_time.day)
def get_file_date_regex(filename):
"""Get regex from filename."""
# regex for ""%Y-%m-%d--%H:%M:%S"
reg0 = r"\d{4}.\d{2}.\d{2}..\d{2}.\d{2}.\d{2}"
# regex for "%Y%m%d%H%M%S"
reg1 = r"\d{14}"
match = re.search(reg0, os.path.basename(filename))
return reg1 if match is None else reg0
def get_datetime_from_filename(filename, regex):
"""Get datetime from filename."""
fmt = "%Y%m%d%H%M%S"
match = re.search(regex, os.path.basename(filename))
match = "".join(re.findall(r"[0-9]+", match.group()))
return dt.datetime.strptime(match, fmt)
[docs]
def create_filelist(path_glob, starttime, endtime, fs=None):
"""Create filelist from path_glob and filename dates in a location-agnostic manner."""
if fs is None:
fs = rpol_data
file_names = sorted(fs.glob(path_glob))
regex = get_file_date_regex(file_names[0])
for fname in file_names:
time = get_datetime_from_filename(fname, regex)
if starttime <= time < endtime:
yield fname
[docs]
def load_single_sweep(start_time, scan, reindex_angle=None, fs=None):
stop_time = start_time + dt.timedelta(minutes=5)
path = get_xpol_path(start_time=start_time, fs=fs)
file_path = os.path.join(path, scan)
flist = list(
create_filelist(os.path.join(file_path, "*"), start_time, stop_time, fs=fs)
)
if reindex_angle is None:
reindex_angle = dict(start_angle=0, stop_angle=360, angle_res=1.0, direction=1)
if fs is None:
fs = rpol_data
with fs.open(flist[0], "rb") as f:
swp = xr.open_dataset(
f.name, engine="gamic", group="sweep_0", reindex_angle=reindex_angle
)
swp = swp.set_coords("sweep_mode")
swp = swp.wrl.georef.georeference()
return swp
[docs]
def timeseries_sweep_to_zarr(
start_time,
delta,
scan,
zarr_name,
compressor=None,
filters=None,
dask_scheduler=None,
reindex_angle=None,
fs=None,
):
if dask_scheduler is None:
dask.config.set(scheduler="threads")
else:
dask.config.set(scheduler=dask_scheduler)
if compressor is None:
compressor = numcodecs.Zstd(level=2)
else:
compressor = compressor
stop_time = start_time + delta
if reindex_angle is None:
reindex_angle = dict(start_angle=0, stop_angle=360, angle_res=1.0, direction=1)
if fs is None:
fs = rpol_data
path = get_xpol_path(start_time=start_time, fs=fs)
file_path = os.path.join(path, scan)
flist = list(
create_filelist(os.path.join(file_path, "*"), start_time, stop_time, fs=fs)
)
valid_keys = {"dtype", "scale_factor", "add_offset", "_FillValue"}
for i, f in enumerate(tqdm(flist, desc="Writing consolidated zarr")):
with fs.open(f, "rb") as fh:
local_path = fh.name
ds = xr.open_dataset(
local_path,
engine="gamic",
group="sweep_0",
reindex_angle=reindex_angle,
chunks=-1,
)
ds = ds.set_coords("sweep_mode")
ds = ds.reset_coords(["time", "elevation"])
ds = ds.assign_coords({"time2": ds.time.load().min()})
ds = ds.expand_dims("time2")
ds.time2.encoding = ds.time.encoding.copy()
if i == 0:
encoding = {}
for v in ds.data_vars:
enc = {k: v2 for k, v2 in ds[v].encoding.items() if k in valid_keys}
var_chunks = ds[v].chunks
if var_chunks is None:
zarr_chunks = ds[v].shape
else:
zarr_chunks = tuple(
c[0] if isinstance(c, tuple) else c for c in var_chunks
)
enc["chunks"] = zarr_chunks
enc["compressor"] = compressor
enc["filters"] = filters
encoding[v] = enc
ds.to_zarr(
zarr_name,
mode="w",
consolidated=False,
encoding=encoding,
zarr_version=2,
)
else:
ds.to_zarr(
zarr_name,
mode="a",
append_dim="time2",
consolidated=False,
zarr_version=2,
)
zarr.consolidate_metadata(zarr_name)
# shutil.rmtree(cache_dir, ignore_errors=True)
def preprocess(ds, keep_vars=None):
if keep_vars is not None:
to_drop = [v for v in ds.data_vars if v not in keep_vars]
ds = ds.drop_vars(to_drop, errors="ignore")
# ds = ds.set_coords("sweep_mode")
# ds = ds.assign_coords(time2=ds.time.min())
# dim0 = ds.wrl.util.dim0()
# dim1 = {"azimuth": "elevation", "elevation": "azimuth"}[dim0]
# angle_median = ds[dim1].load().median(dim0, skipna=True)
# angle = xr.where(ds[dim1] != angle_median, angle_median, ds[dim1])
# ds = ds.assign_coords({dim1: angle})
return ds
[docs]
def load_timeseries_sweep(
start_time, delta, scan, reindex_angle=None, keep_vars=None, fs=None
):
if fs is None:
fs = rpol_data
if reindex_angle is None:
reindex_angle = dict(start_angle=0, stop_angle=360, angle_res=1.0, direction=1)
stop_time = start_time + delta
path = get_xpol_path(start_time=start_time, fs=fs)
file_path = os.path.join(path, scan)
flist = list(
create_filelist(os.path.join(file_path, "*"), start_time, stop_time, fs=fs)
)
print(f"loading {len(flist)} files")
preprocess_func = partial(preprocess, keep_vars=keep_vars)
_flist = []
for _i, f in enumerate(tqdm(flist, desc="Claiming files")):
with fs.open(f, "rb") as fh:
_flist.append(fh.name)
sweep_ts = xr.open_mfdataset(
_flist,
concat_dim="time2",
combine="nested",
engine="gamic",
group="sweep_0",
reindex_angle=reindex_angle,
preprocess=preprocess_func,
parallel=True,
)
# sweep_ts = sweep_ts.wrl.georef.georeference()
return sweep_ts
[docs]
def load_volume(flist, reindex_angle=None, fs=None):
if fs is None:
fs = rpol_data
fnames = []
for fpath in flist:
with fs.open(fpath) as f:
fnames.append(f.name)
sweeps = [
xr.open_dataset(
fname, group="sweep_0", engine="gamic", reindex_angle=reindex_angle
)
for fname in fnames
]
sweeps.insert(0, xr.open_dataset(fnames[0], group="/"))
dtree = xr.DataTree(dataset=xd.io.backends.common._assign_root(sweeps), name="root")
dtree = xd.io.backends.common._attach_sweep_groups(dtree, sweeps[1:])
for key in list(dtree.children):
if "sweep" in key:
ds = dtree[key].to_dataset().set_coords("sweep_mode")
ds = ds.assign_coords(elevation=ds.elevation.median())
ds = ds.wrl.georef.georeference()
dtree[key].ds = ds
return dtree
[docs]
def concat_sweep_angle(dtree, rres):
output = []
for key in list(dtree.children):
if "sweep" in key:
ds = dtree[key].to_dataset()
ds = ds.reindex(
range=np.arange(rres / 2, ds.range.max() + rres / 2, rres),
method="nearest",
)
output.append(ds)
return xr.concat(output, "sweep_fixed_angle")
## Plotting
def set_title(da, ax=None):
if ax is None:
ax = plt.gca()
da_time = da.time.min().dt.strftime("%Y-%m-%dT%H:%M:%S")
ax.set_title(f"{da.name} - {da_time.values}")
[docs]
def get_polar_ax(fig=None, ax=None):
if fig is None:
fig = plt.figure(figsize=(10, 10))
if ax is None:
ax = 111
ax = fig.add_subplot(ax, projection="polar")
# set the lable go clockwise and start from the top
ax.set_theta_zero_location("N")
# clockwise
ax.set_theta_direction(-1)
# set ticks proper
ax.set_xticks(np.pi / 180.0 * np.linspace(0, 360, 36, endpoint=False))
return ax
def plot_polar(data, ax, **kwargs):
theta = np.linspace(0, 2 * np.pi, num=360, endpoint=False)
datap = data.assign_coords(
theta=(["azimuth"], theta, {"standard_name": "azimuth angle"})
)
pm = datap.plot(x="theta", ax=ax, **kwargs)
ax.set_ylabel("")
return pm
[docs]
def get_discrete_cmap(ticks, colors, bad="white", over=None, under=None):
"""Create discrete colormap.
Parameters
----------
ticks : int | sequence
number of ticks or sequence of ticks
colors : colormap | sequence
colormap or sequence of colors
bad : color
over : color
under : color
Returns
-------
matplotlib.colors.ListedColormap
"""
ticks = ticks if isinstance(ticks, int) else len(ticks)
if isinstance(colors, (str, mpl.colors.Colormap)):
cmap = mpl.colormaps.get_cmap(colors)
colors = cmap(np.linspace(0, 1, ticks + 1))
cmap = mpl.colors.ListedColormap(colors[1:-1])
if over is None:
over = colors[-1]
if under is None:
under = colors[0]
cmap.set_under(under)
cmap.set_over(over)
cmap.set_bad(color=bad)
return cmap
[docs]
def get_discrete_norm(ticks):
"""Return discrete boundary norm.
Parameters
----------
ticks : sequence
sequence of ticks
Returns
-------
matplotlib.colors.BoundaryNorm
"""
return mpl.colors.BoundaryNorm(ticks, len(ticks) - 1)
[docs]
def plot_reference_colorbar(ticks, cmap, ax, **kwargs):
"""Plot reference colorbar
Parameters
----------
ticks : sequence
sequence of ticks
cmap : cmap instance
ax : axes instance
Keyword Arguments
-----------------
Additonal Keywords for colorbar
Returns
-------
matplotlib.colorbar instance
"""
vmin = ticks[0] - np.diff(ticks)[0]
vmax = ticks[-1] + np.diff(ticks)[-1]
vmax2 = ticks[-1] + 2 * np.diff(ticks)[-1]
y = [vmin] + list(ticks) + [vmax] + [vmax2]
x = np.arange(2)
data = np.repeat(np.array(y, dtype=float), 2).reshape(-1, 2)
data[-2, :] = np.nan
norm = get_discrete_norm(ticks)
pm = ax.pcolormesh(
x,
y,
data,
cmap=cmap,
norm=norm,
)
cb = plt.colorbar(
pm,
ax=ax,
ticks=ticks,
**kwargs,
)
return cb
colors_prabhakar = np.array(
[
[0.00, 1.00, 1.00],
[0.00, 0.70, 0.93],
[0.00, 0.00, 1.00],
[0.50, 1.00, 0.00],
[0.40, 0.80, 0.00],
[0.27, 0.55, 0.00],
[1.00, 1.00, 0.00],
[0.80, 0.80, 0.00],
[1.00, 0.65, 0.00],
[1.00, 0.27, 0.00],
[0.80, 0.22, 0.00],
[0.55, 0.15, 0.00],
[1.00, 0.00, 1.00],
[0.58, 0.44, 0.86],
]
)
cmap_prabhakar = mpl.colors.ListedColormap(colors_prabhakar)
mpl.colormaps.register(cmap_prabhakar, name="miub")
#: Configuration dictionary for visualizing radar data.
visdict14 = dict(
ZH=dict(
ticks=np.arange(-10, 55, 5),
contours=[0, 5, 10, 15, 20, 25, 30, 35],
cmap=cmap_prabhakar,
name=r"Horizontal Reflectivity (dBz)",
short=r"$\mathrm{\mathsf{Z_{H}}}$",
),
ZDR=dict(
ticks=np.array(
[-1.0, -0.1, 0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1.0, 2.0, 3.0]
),
contours=np.array([-1, -0.3, 0]),
cmap=cmap_prabhakar,
name=r"Differential Reflectivity (dB)",
short=r"$\mathrm{\mathsf{Z_{DR}}}$",
),
RHO=dict(
ticks=np.array(
[
0.7,
0.8,
0.85,
0.9,
0.92,
0.94,
0.95,
0.96,
0.97,
0.98,
0.99,
0.995,
0.998,
]
),
cmap=cmap_prabhakar,
name=r"Crosscorrelation Coefficient ()",
short=r"$\mathrm{\mathsf{RHO_{HV}}}$",
),
KDP=dict(
ticks=np.array(
[-0.5, -0.1, 0.0, 0.05, 0.1, 0.2, 0.3, 0.4, 0.6, 0.8, 1.0, 2.0, 3.0]
),
cmap=cmap_prabhakar,
name=r"Specific Differental Phase (°/km)",
short=r"$\mathrm{\mathsf{K_{DP}}}$",
),
TEMP=dict(contours=[5, 0, -5, -10, -15, -20, -25, -30, -35, -40]),
PHI=dict(
ticks=np.array([0, 5, 10, 15, 20, 25, 30, 40, 50, 60, 70, 80, 90]),
cmap=cmap_prabhakar,
name=r"Differental Phase (°)",
short=r"$\mathrm{\mathsf{\Phi_{DP}}}$",
),
HMC=dict(
norm=mpl.colors.BoundaryNorm(np.arange(-0.5, 11 + 0.6, 1), 12),
ticks=np.arange(0, 11 + 1),
bounds=np.arange(-0.5, 11 + 0.6, 1),
cmap=mpl.colors.ListedColormap(
[
"LightBlue",
"Blue",
"Lime",
"Black",
"Red",
"Yellow",
"Fuchsia",
"LightPink",
"Cyan",
"Gray",
"DarkOrange",
"White",
]
),
name=r"HMC - Zrnic et al. 2001 ",
short=r"$\HMC_{Z}$",
long_name=r"Hydrometeorclass",
labels=[
"Light Rain",
"Moderate Rain",
"Heavy Rain",
"Large Drops",
"Hail",
"Rain/Hail",
"Graupel/Hail",
"Dry Snow",
"Wet Snow",
"H Crystals",
"V Crystals",
"No Rain",
],
labels_short=[
"LR",
"MR",
"HR",
"LD",
"HL",
"RH",
"GH",
"DS",
"WS",
"HC",
"VC",
"NR",
],
),
)
[docs]
def plot_moment(mom, *, ax=111, **kwargs):
sname = mom.attrs.get("standard_name", "")
if "equivalent_reflectivity_factor" in sname:
ticks = visdict14["ZH"]["ticks"]
elif "differential_reflectivity" in sname:
ticks = visdict14["ZDR"]["ticks"]
elif "correlation_coefficient" in sname:
ticks = visdict14["RHO"]["ticks"]
elif "radar_differential_phase" in sname:
ticks = visdict14["PHI"]["ticks"]
elif "radar_specific_differential_phase" in sname:
ticks = visdict14["KDP"]["ticks"]
else:
ticks = 14
cmap = None
ticks = kwargs.pop("ticks", ticks)
cmap = kwargs.pop("cmap", "HomeyerRainbow")
pm = mom.wrl.vis.plot(ax=ax, levels=ticks, cmap=cmap, **kwargs)
if ax == 111:
ax = plt.gca()
if mom["sweep_mode"] != "rhi":
maxrange = mom.range.max().values
ranges = [25e3, 50e3, 75e3, 100e3, 125e3, 150e3]
ranges = [rng for rng in ranges if rng <= maxrange + 1000]
wrl.vis.plot_ppi_crosshair((0, 0, 0), ranges=ranges, ax=ax)
if not ax.get_subplotspec().get_topmost_subplotspec().is_first_col():
ax.set_ylabel("")
if not ax.get_subplotspec().get_topmost_subplotspec().is_last_row():
ax.set_xlabel("")
dim0 = mom.wrl.util.dim0()
secs = "<M8[s]"
t0 = mom.time.load().min(dim0).values.astype(secs)
ax.set_title(f"{mom.name} - {t0}")
return pm
[docs]
def plot_2by2(swp, moments=None, **kwargs):
if moments is None:
moments = ["DBTH", "DBZH", "ZDR", "RHOHV"]
fig, axes = plt.subplots(2, 2, sharex=True, sharey=True, figsize=(13, 10))
for i, mom in enumerate(moments):
plot_moment(swp[mom], ax=axes.flat[i], **kwargs)
plt.tight_layout()
[docs]
def plot_noise_hist(hist):
fig = plt.figure(figsize=(18, 18))
g = hist.histogram_RHO_SNR.plot(
col="noise",
col_wrap=5,
levels=30,
vmin=1,
vmax=None,
cmap="turbo",
norm=LogNorm(),
)
idxmin = hist.stdev.argmin()
for i, ax in enumerate(g.axes.flat):
if not ax.has_data():
continue
h = hist.isel(noise=i).histogram_RHO_SNR
h.idxmax("RHO_bin").plot(ax=ax, c="k")
if i == idxmin:
c = "r"
else:
c = "k"
ax.set_title(
f"NL {np.round(h.noise.values, decimals=1)} \n STD {np.around(h.stdev.values, decimals=5)}",
c=c,
)
g.fig.subplots_adjust(wspace=0.35, hspace=0.8, right=0.8)
fig.tight_layout()
def make_patch_spines_invisible(ax):
ax.set_frame_on(True)
ax.patch.set_visible(False)
for _, sp in ax.spines.items():
sp.set_visible(False)
def set_spine_direction(ax, direction):
if direction in ["right", "left"]:
ax.yaxis.set_ticks_position(direction)
ax.yaxis.set_label_position(direction)
elif direction in ["top", "bottom"]:
ax.xaxis.set_ticks_position(direction)
ax.xaxis.set_label_position(direction)
else:
raise ValueError(f"Unknown Direction: {direction}")
ax.spines[direction].set_visible(True)
[docs]
def create_lineplot(fig, subplot=111, xlabel=None, ylabel=None):
if xlabel is None:
xlabel = "Range Bins"
if ylabel is None:
ylabel = ""
host = fig.add_subplot(subplot)
ax = host
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
return ax
[docs]
def add_axis(host, ylabel=None, pos=1.0):
if ylabel is None:
ylabel = ""
ax = host.twinx()
host.spines["right"].set_visible(False)
ax.spines["right"].set_position(("axes", pos))
make_patch_spines_invisible(ax)
set_spine_direction(ax, "right")
ax.set_ylabel(ylabel)
return ax
def ufunc_wrapper(data, **kwargs):
func = kwargs.pop("func")
return func(data, **kwargs)
# phase processing
[docs]
def filter_data(obj, medwin):
dim0 = obj.wrl.util.dim0()
out = xr.apply_ufunc(
ufunc_wrapper,
obj,
input_core_dims=[[dim0, "range"]],
output_core_dims=[[dim0, "range"]],
output_dtypes=["float"],
dask="parallelized",
vectorize=True,
kwargs=dict(kernel_size=[1, medwin], func=scipy.signal.medfilt2d),
dask_gufunc_kwargs=dict(allow_rechunk=True),
)
return out
[docs]
def gauss_kernel(width, sigma):
dirac = np.zeros(width)
dirac[int(width / 2)] = 1
return scipy.ndimage.gaussian_filter1d(dirac, sigma=sigma)
def convolve(data, kernel=None, mode="same"):
mask = np.isnan(data)
out = np.convolve(np.where(mask, 0, data), kernel, mode=mode) / np.convolve(
~mask, kernel, mode=mode
)
return out
[docs]
def smooth_data(obj, kernel):
out = xr.apply_ufunc(
ufunc_wrapper,
obj,
input_core_dims=[["range"]],
output_core_dims=[["range"]],
output_dtypes=["float"],
dask="parallelized",
vectorize=True,
kwargs=dict(kernel=kernel, func=convolve),
dask_gufunc_kwargs=dict(allow_rechunk=True),
)
return out
def get_peaks(da, height=0):
def process_peaks(arr, height=0):
# Apply find_peaks
arr = arr.copy()
peaks, _ = scipy.signal.find_peaks(arr, height=height)
try:
peak = peaks[0]
except IndexError:
peak = 0
return peak
return xr.apply_ufunc(
process_peaks,
da,
input_core_dims=[["PHIDP_bin"]],
output_core_dims=[[]],
dask="parallelized",
vectorize=True,
kwargs=dict(height=height),
dask_gufunc_kwargs=dict(allow_rechunk=True),
)
[docs]
def phase_offset(phioff, rng=3000.0):
"""Calculate Phase offset.
Parameter
---------
phioff : xarray.DataArray
differential phase array
Keyword Arguments
-----------------
rng : float
range in m to calculate system phase offset
Return
------
start_range : xarray.DataArray
DataArray with start range values
off : xarray.DataArray
DataArray with phase offset values
"""
range_step = np.diff(phioff.range)[0]
nprec = int(rng / range_step)
if nprec % 2:
nprec += 1
# create binary array
phib = xr.where(np.isnan(phioff), 0, 1)
# take nprec range bins and calculate sum
phib_sum = phib.rolling(range=nprec, center=True).sum(skipna=True)
# get start range of first N consecutive precip bins
start_range = phib_sum.idxmax(dim="range") - nprec // 2 * np.diff(phib_sum.range)[0]
# get range of first non-nan value per ray
# start_range = (~np.isnan(phioff)).idxmax(dim='range', skipna=True)
# add range
stop_range = start_range + rng
# get phase values in specified range
off = phioff.where(
(phioff.range >= start_range.load()) & (phioff.range <= stop_range.load()),
drop=True,
)
# calculate nan median over range
off = off.median(dim="range", skipna=True)
return xr.Dataset(
dict(PHIDP_OFFSET=off, start_range=start_range, stop_range=stop_range)
)
[docs]
def phase_zphi(phi, rng=1000.0, **kwargs):
range_step = np.diff(phi.range)[0]
nprec = int(rng / range_step)
if nprec % 2 == 0:
nprec += 1
offset = nprec // 2 * np.diff(phi.range)[0]
offset_idx = nprec // 2
# create binary array
phib = xr.where(np.isnan(phi), 0, 1)
# take nprec range bins and calculate sum
min_periods = kwargs.get("min_periods", nprec)
phib_sum = phib.rolling(range=nprec, center=True, min_periods=min_periods).sum(
skipna=True
)
start_range = phib_sum.idxmax(dim="range") - offset
start_range_idx = phib_sum.argmax(dim="range") - offset_idx
first = phi.where(
(phi.range >= start_range.load()) & (phi.range <= start_range.load() + rng),
).median("range", skipna=True)
stop_range = phib_sum[..., ::-1].idxmax(dim="range") - offset
stop_range_idx = (
len(phib_sum.range) - (phib_sum[..., ::-1].argmax(dim="range") - offset_idx) - 2
)
last = phi.where(
(phi.range >= stop_range.load() - rng) & (phi.range <= stop_range.load()),
).median("range", skipna=True)
return xr.Dataset(
dict(
phib=phib_sum,
offset=offset,
offset_idx=offset_idx,
start_range=start_range,
stop_range=stop_range,
first=first,
first_idx=start_range_idx,
last=last,
last_idx=stop_range_idx,
dphi=last - first,
)
)
# Specific attenutation attributes
spec_att = dict(
units="dB/km",
standard_name="specific_attenuation_h",
long_name="Specific attenuation H",
short_name="AH",
)
[docs]
def zphi(phi, zh, **kwargs):
rng = kwargs.get("rng", 1000.0)
cphase = phase_zphi(phi, rng=rng)
if "alpha" in cphase.coords:
cphase = cphase.reset_coords("alpha", drop=True)
dphi = cphase.dphi.where(cphase.dphi >= 0, 1e-4)
alphax = kwargs.get("alphax", np.arange(0.1, 0.35, 0.01))
alphax = xr.DataArray(alphax, coords={"alpha": alphax}, dims="alpha")
bx = kwargs.get("bx", 0.78)
# need to expand alphax to dphi-shape
fdphi = 10 ** (0.1 * bx * alphax * dphi) - 1
# zhraw = zh.copy()#where((swp.CMAP2==0) & valid & (swp.RHOHV > 0.9))
zhraw = zh.where((zh.range > cphase.start_range) & (zh.range < cphase.stop_range))
zax = zhraw.wrl.trafo.idecibel().fillna(1e-4)
za = zax**bx
# set masked to zero for integration
za_zero = za.fillna(0)
# integrate
iza_x = 0.46 * bx * za_zero.pipe(x_cumtrapz)
iza = iza_x.max("range") - iza_x
iza_fdphi = iza / fdphi
# integral from r1 to r2
iza_first = iza_fdphi[..., cphase.first_idx.compute(), :]
ah = za / (iza_first + iza)
ah.attrs = spec_att
return ah
# to wradlib
def phidp_from_kdp(da):
"""Derive PHIDP from KDP.
Parameter
---------
da : xarray.DataArray
array with specific differential phase data
winlen : int
size of window in range dimension
Return
------
phi : xarray.DataArray
DataArray with differential phase values
"""
dr = da.range.diff("range").median("range").values / 1000.0
print("range res [km]:", dr)
return (
xr.apply_ufunc(
scipy.integrate.cumulative_trapezoid,
da,
input_core_dims=[["range"]],
output_core_dims=[["range"]],
dask="parallelized",
kwargs=dict(dx=dr, initial=0.0, axis=-1),
)
* 2
)
[docs]
def xr_rolling(da, window, window2=None, method="mean", min_periods=2, **kwargs):
"""Apply rolling function `method` to 2D datasets
Parameter
---------
da : xarray.DataArray
array with data to apply rolling function
window : int
size of window in range dimension
Keyword Arguments
-----------------
window2 : int
size of window in azimuth dimension
method : str
function name to apply
min_periods : int
minimum number of valid bins
**kwargs : dict
kwargs to feed to rolling function
Return
------
da_new : xarray.DataArray
DataArray with applied rolling function
"""
prng = window // 2
srng = slice(prng, -prng)
da_new = da.pad(range=prng, mode="reflect", reflect_type="odd")
dim = dict(range=window)
isel = dict(range=srng)
dim0 = da.wrl.util.dim0()
if window2 is not None:
paz = window2 // 2
saz = slice(paz, -paz)
da_new = da_new.pad({dim0: paz}, mode="wrap")
dim.update({dim0: window2})
isel.update({dim0: saz})
rolling = da_new.rolling(dim=dim, center=True, min_periods=min_periods)
da_new = getattr(rolling, method)(**kwargs)
da_new = da_new.isel(**isel)
return da_new
# Hydrometeor Classification
# todo check with wradlib
# def msf_index_indep_xarray(msf_ds, obs):
# def wrap_digitize(data, bins=None):
# return np.digitize(data, bins)
# idp = msf_ds.idp.values
# bins = np.append(idp, idp[-1] + (idp[-1] - idp[-2]))
# idx = (
# xr.apply_ufunc(
# wrap_digitize,
# obs,
# dask="parallelized",
# kwargs=dict(bins=bins),
# output_dtypes=["i4"],
# )
# - 1
# )
# # select bins
# idx = xr.where((idx >= 0) & (idx < bins.shape[0] - 2), idx, 0)
# return msf_ds.isel(idp=idx)
# def trapezoid(msf, obs):
# ones = (obs >= msf[..., 1]) & (obs <= msf[..., 2])
# ((obs < msf[..., 0]) | (obs > msf[..., 3]))
# lower = (obs >= msf[..., 0]) & (obs < msf[..., 1])
# higher = (obs > msf[..., 2]) & (obs <= msf[..., 3])
# obs_lower = obs - msf[..., 0]
# msf_lower = msf[..., 1] - msf[..., 0]
# low = obs_lower / msf_lower
# obs_higher = obs - msf[..., 3]
# msf_higher = msf[..., 2] - msf[..., 3]
# high = obs_higher / msf_higher
# ret = xr.zeros_like(obs) # * np.nan
# # ret = ret.where(zeros, 0).where(ones, 1).where(lower, low).where(higher, high)
# ret = xr.where(ones, 1, ret)
# ret = xr.where(lower, low, ret)
# ret = xr.where(higher, high, ret)
# return ret # .where(ret == np.nan, 0)
# def fuzzify(msf_ds, hmc_ds, msf_obs_mapping):
# fuzz_ds = xr.Dataset()
# for mf, hm in msf_obs_mapping.items():
# obs = hmc_ds[hm]
# msf = msf_ds[mf]
# fuzz_ds = fuzz_ds.assign({mf: trapezoid(msf, obs)})
# return fuzz_ds.transpose("hmc", ...)
# def probability(data, weights):
# """Calculate probability of hmc-class for every data bin.
# Parameters
# ----------
# data : xarray.Dataset
# Dataset containing containing the membership probability values.
# weights : :class:`numpy:numpy.ndarray`
# Array of length (observables) containing the weights for
# each observable.
# Returns
# -------
# out : xarray.DataArray
# Array containing weighted hmc-membership probabilities.
# """
# out = data.to_array(dim="obs")
# w = weights.to_array(dim="obs")
# out = (out * w).sum("obs") / w.sum("obs")
# return out # .transpose("hmc", ...)
# def classify(data, threshold=0.0):
# """Calculate probability of hmc-class for every data bin.
# Parameters
# ----------
# data : np.ndarray
# Array which is of size (hmc-class, data.shape), containing the
# weighted hmc-membership probability values.
# Keyword Arguments
# -----------------
# threshold : float
# Threshold value where probability is considered no precip,
# defaults to 0
# Returns
# -------
# out : xr.DataArray
# DataArray containing containing probability scores.
# No precip is added on the top.
# """
# # handle no precipitation
# nop = xr.where(data.sum("hmc") / len(data.hmc) <= threshold, 1, 0)
# nop = nop.assign_coords({"hmc": "NP"}).expand_dims(dim="hmc", axis=-1)
# return xr.concat([data, nop], dim="hmc")
def calc_gpm_height(data):
# Function for height calculation from bins.....
# GPM DPR ATBD Level 2 2017 Awaka et al S. 21
def _calc_height(bin_range_number):
rangeBinSize = 125.0
binEllipsoid = 176.0
bin_range_number = data["binBBBottom"]
elipsoid_offset = data["ellipsoidBinOffset"]
localZenithAngle = data["localZenithAngle"]
calc_1 = (binEllipsoid - bin_range_number) * rangeBinSize + elipsoid_offset
calc_h = calc_1 * np.cos(np.deg2rad(localZenithAngle))
return calc_h
data = data.assign(dpr_bbb=_calc_height(data["binBBBottom"]))
data = data.assign(dpr_bbt=_calc_height(data["binBBTop"]))
data = data.assign(dpr_cfb=_calc_height(data["binClutterFreeBottom"]))
return data
def read_gpm_file(fname, proj_stereo):
sr_data = wrl.io.open_gpm_dataset(fname, group="NS")
sr_data = sr_data.set_coords(["Longitude", "Latitude"])
sr_data = xr.decode_cf(sr_data)
sr_data.attrs["platform"] = "GPM"
sr_data.attrs = {}
# calculate bb and clutter heights
sr_data = calc_gpm_height(sr_data)
# assign x,y,z coordinates in WGS84
sr_data = sr_data.assign_coords(
x=sr_data.Longitude, y=sr_data.Latitude, z=sr_data.dpr_cfb
) # .load()
# Reproject to RADOLAN
proj_wgs = osr.SpatialReference()
proj_wgs.ImportFromEPSG(4326)
sr_data = sr_data.wrl.georef.reproject(src_crs=proj_wgs, trg_crs=proj_stereo)
# parallax correction
sr_data = sr_data.wrl.georef.correct_parallax(125.0)
return sr_data
def get_radolan_path(t, pr):
return (
f"/automount/radar/dwd/{pr}/{t:%Y}/{t:%Y}-{t:%m}/"
f"{t:%Y}-{t:%m}-{t:%d}/raa01-{pr}_10000-{t:%y%m%d%H%M}-dwd---bi*"
)
def read_radolan(gpm_time):
ts = pd.Timestamp(gpm_time.values)
ts = ts.round(freq="5T")
rx_path = get_radolan_path(ts, "rx")
ry_path = get_radolan_path(ts, "ry")
ry_ds = xr.open_dataset(glob.glob(ry_path)[0], engine="radolan")
ry_ds.RY.attrs["units"] = ry_ds.RY.attrs.pop("unit")
rx_ds = xr.open_dataset(glob.glob(rx_path)[0], engine="radolan")
rx_ds.RX.attrs["units"] = rx_ds.RX.attrs.pop("unit")
n_ds = xr.open_dataset(
"/automount/ftp/radar/wradlib-data/radolan/alt/radolan_geo.nc"
)
rdata = xr.merge([ry_ds, rx_ds, n_ds])
return rdata
def get_gpm_in_radolan(gr_data, sr_data):
from skimage import measure
bingrid = xr.where(gr_data.RY >= 0, 1, 0)
bingrid = bingrid.pad(pad_width=dict(x=1, y=1), constant_values=0)
# Contouren des Bin-grids erstellen
contours = measure.find_contours(
bingrid.values, 0, positive_orientation="high", fully_connected="high"
)
contours.sort(key=lambda x: len(x), reverse=True)
radolan_contour = contours[0]
cx = radolan_contour[:, 1].astype(int)
cy = radolan_contour[:, 0].astype(int)
# pad_coords
xx = gr_data.x.pad(pad_width=dict(x=1), mode="reflect", reflect_type="odd")
yy = gr_data.y.pad(pad_width=dict(y=1), mode="reflect", reflect_type="odd")
xx1 = xx.isel(x=cx)
yy1 = yy.isel(y=cy)
# GPM Coords GeoDataFrame
points_gpm = gpd.GeoDataFrame(
geometry=gpd.points_from_xy(sr_data.x.values.ravel(), sr_data.y.values.ravel())
)
# Radolan Shapely Polygon
xy = np.vstack((xx1.values.ravel(), yy1.values.ravel())).transpose()
radolan_polygon = shapely.Polygon(xy)
# Get point index
gpm_idx = points_gpm.within(radolan_polygon)
# to 1D and select per index
sr_data_1d = sr_data.stack(onedim=("nscan", "nray")).sel(
onedim=gpm_idx.to_xarray().rename(index="onedim")
)
# add xy coords
xy = xr.concat([sr_data_1d.x, sr_data_1d.y], "xyc") # .transpose()
sr_data_1d = sr_data_1d.assign_coords(xy=xy)
# reset Multi-Index
sidx = sr_data_1d.reset_index("onedim").assign_coords(
onedim=np.arange(len(sr_data_1d.onedim))
)
return sidx.transpose("onedim", ...)
def get_radolan_in_gpm(sr_data, gr_data):
dpr_contour_x = dpr_swath_contour(sr_data.x)
dpr_contour_y = dpr_swath_contour(sr_data.y)
# create dpr polygon of dpr xy contours
dpr_xy_poly = np.vstack((dpr_contour_x.ravel(), dpr_contour_y.ravel())).transpose()
gpm_polygon = shapely.Polygon(dpr_xy_poly)
radolan_xy = xr.concat(xr.broadcast(gr_data.x, gr_data.y), dim="xy")
radolan_xy.name = "radolan_coords"
radolan_xy = radolan_xy.stack(onedim=("x", "y")).transpose(..., "xy")
points_radolan = gpd.GeoDataFrame(
geometry=gpd.points_from_xy(radolan_xy[..., 0], radolan_xy[..., 1])
)
radolan_idx = points_radolan.within(gpm_polygon)
rdata_1d = gr_data.stack(onedim=("x", "y")).sel(
onedim=radolan_idx.to_xarray().rename(index="onedim")
)
# reset Multi-Index
ridx = rdata_1d.reset_index("onedim").assign_coords(
onedim=np.arange(len(rdata_1d.onedim))
)
return ridx
def dpr_antenna_weighting(r):
"""
Funktion: explained in
Watters, D., A. Battaglia, K. Mroz, and F. Tridon, 0:
Validation of the GPM Version-5 Surface Rainfall
Products over Great Britain and Ireland.
J. Hydrometeor., 0, https://doi.org/10.1175/JHM-D-18-0051.1
and
Mroz, K., A. Battaglia, T. J. Lang, D. J. Cecil,
S. Tanelli, and F. Tridon, 2017: Hail-detection
algorithm for the GPM Core Observatory satellite sensors.
Journal of Applied Meteorology and Climatology,56 (7), 1939–1957
Input:
------
r ::: Distance to the center point of the footprint in m
Output:
-------
omega ::: Weighting for the point with the distance
"""
r = r / 1000.0
omega = np.exp(-((r / 2.5) ** 2.0) * np.log10(4.0))
return omega
def _weighted_sum(patch, weights, wsum, dim):
is_refl = patch.attrs["units"] == "dBZ" if "units" in patch.attrs else False
if is_refl:
patch = patch.wrl.trafo.idecibel()
patch = (patch * weights).sum(dim, skipna=True) / wsum
if is_refl:
patch = patch.wrl.trafo.decibel()
return patch
def weighted_sum(patch, weights, dim):
patch = patch.copy()
wsum = weights.sum(dim=dim, skipna=True)
for vname, dvar in patch.data_vars.items():
if dim in dvar.dims and dvar.dtype != object:
patch[vname] = _weighted_sum(dvar, weights, wsum, dim)
return patch
def radolan_gpm_matching(ridx, sidx, k=32, weight_gpm=True):
# build tree
tree = KDTree(np.vstack([ridx.x, ridx.y]).T, balanced_tree=False)
# query tree for 32 nearest neighbours
# 32 maximal possible ry pixel in dpr footprint
dists, ix = tree.query(sidx.xy, k=k, workers=-1)
# assign radolan data to dataset
sidx1 = sidx.assign(ridx.sel(onedim=xr.DataArray(ix, dims=("onedim", "bins"))))
# assign distances
sidx1 = sidx1.assign(dists=xr.DataArray(dists, dims=("onedim", "bins")))
# calculate weighting and summation
w = dpr_antenna_weighting(sidx1.dists)
wsum = weighted_sum(sidx1, w, "bins")
w = dpr_antenna_weighting(np.abs(wsum.rhmin - wsum.zp))
wsum = wsum.assign(weights=w)
# select gpm disc's
sr_match = wsum.where(
(wsum.zp >= (wsum.rhmin - wsum.beam_width / 2.0))
& (wsum.zp <= (wsum.rhmin + wsum.beam_width / 2.0))
)
# mean on gpm column
if weight_gpm:
res = weighted_sum(
sr_match.reset_coords(["xp", "yp", "zp"]), sr_match.weights, "nbin"
)
else:
res = sr_match.reset_coords(["xp", "yp", "zp"]).mean(dim="nbin", skipna=True)
return res
def read_gr_sweep(gr_file_path, offset_z=0, offset_phi=0, offset_zdr=0):
"""
Read and organize the GR data for one sweep.The following code reads data
in ODIM H5 format. If your GR data is in some other format
respective adaptions are needed.
# Input:
# ------
gr_file_path ::: path to graund radar sweep data
offset_z ::: Refl. offset in dB
offset_phi ::: System PHI
offset_zdr ::: Diff. Refl offst in dB
# Output:
# ------
ds0_masked ::: Sweep Data with PPB Quality Index
More description in the data itself.
"""
def noise_correction(ds, noise_level):
# noise calculations
snr = (
ds.DBZH
- 20 * np.log10(ds.range * 0.001)
- noise_level
- 0.033 * ds.range / 1000
)
rho = ds.RHOHV * np.sqrt(1.0 + 1.0 / 10.0 ** (snr * 0.1))
return ds.assign({"SNR": snr, "RHOHV": rho})
def xr_rolling(da, window, method="mean"):
da_new = da.pad(range=window // 2, mode="reflect", reflect_type="odd")
da_new = getattr(da_new.rolling(range=window, center=True), method)()
da_new = da_new.isel(range=slice(window // 2, -window // 2))
return da_new
# read one ppi data
# umstellen open_odim_dataset
ds0 = xr.open_dataset(gr_file_path, engine="gamic", group="sweep_0")
# Offsets and Noise Processing
# ------------------------------
# keep uncorrected RHOHV
ds0 = ds0.assign({"URHOHV": ds0.RHOHV})
# add the offsets
ds0["DBZH"] -= offset_z
ds0["PHIDP"] -= offset_phi
ds0["ZDR"] -= offset_zdr
# RHOHV noise correction
# ds0 = ds0.pipe(noise_correction, -22)
# ds0 = ds0.pipe(noise_correction, -22)
# georeference data
ds0 = ds0.wrl.georef.georeference()
## Phase Processing
## -----------------
window = 13
phi_ma = ds0.PHIDP.pipe(xr_rolling, window, method="median")
# mask phi
phi_masked = phi_ma.where((ds0.RHOHV >= 0.95) & (ds0.DBZH >= 0.0))
# not used for moment
# todo: check 2023-11-06
np.isfinite(phi_masked).argmax(dim="range")
phi_masked.sizes["range"] - np.isfinite(
phi_masked.sortby(["range"], ascending=False)
).argmax(dim="range")
# nur bei neuem wradlib
kdp = phi_masked.wrl.dp.kdp_from_phidp(winlen=21, min_periods=3)
phidp = kdp.pipe(phidp_from_kdp)
phi = ds0.PHIDP.where((ds0.RHOHV >= 0.95) & (ds0.DBZH >= 0.0))
phidp2, kdp2 = phi.wrl.dp.phidp_kdp_vulpiani(
winlen=15, th1=-0.5, th2=5, th3=-80, min_periods=3
)
ds0 = ds0.assign({"PHI_SMOOTH": phidp})
ds0 = ds0.assign({"PHI_MASKED": phi_masked})
ds0 = ds0.assign({"KDP1_NEW": kdp})
ds0 = ds0.assign({"PHI1_NEW": phidp})
ds0 = ds0.assign({"KDP2_NEW": kdp2})
ds0 = ds0.assign({"PHI2_NEW": phidp2})
ds0_masked = ds0.where(ds0.RHOHV >= 0.7)
ds0_masked = ds0_masked.assign({"height": ds0.z})
sitecoords = (
ds0_masked.longitude.data,
ds0_masked.latitude.data,
ds0_masked.altitude.data,
)
# BB and Q
nrays = 360 # number of rays
nbins = ds0_masked.range.shape[0] # number of range bins
el = ds0_masked.elevation.data[0] # vertical antenna pointing angle (deg)
bw = 1.0 # half power beam width (deg)
range_res = (
ds0_masked.range.data[1] - ds0_masked.range.data[0]
) # range resolution (meters)
r = np.arange(nbins) * range_res
beamradius = wrl.util.half_power_radius(r, bw)
coord = wrl.georef.sweep_centroids(nrays, range_res, nbins, el)
coords = wrl.georef.spherical_to_proj(
coord[..., 0], coord[..., 1], coord[..., 2], sitecoords
)
lon = coords[..., 0]
lat = coords[..., 1]
alt = coords[..., 2]
polcoords = coords[..., :2]
rlimits = (lon.min(), lat.min(), lon.max(), lat.max())
ds = wrl.io.open_raster(
"/automount/ags/velibor/radcal/scripts/boxpol_processing/bonn_dem.tif"
)
rastervalues, rastercoords, proj = wrl.georef.extract_raster_dataset(
ds, nodata=-32768.0
)
# Clip the region inside our bounding box
ind = wrl.util.find_bbox_indices(rastercoords, rlimits)
rastercoords = rastercoords[ind[1] : ind[3], ind[0] : ind[2], ...]
rastervalues = rastervalues[ind[1] : ind[3], ind[0] : ind[2]]
# Map rastervalues to polar grid points
polarvalues = wrl.ipol.cart_to_irregular_spline(
rastercoords, rastervalues, polcoords, order=3, prefilter=False
)
PBB = wrl.qual.beam_block_frac(polarvalues, alt, beamradius)
PBB = np.ma.masked_invalid(PBB)
CBB = wrl.qual.cum_beam_block_frac(PBB)
def QBBF(CBB):
"""
Quality Index for PBB
Crisologo 2018
"""
Qbbf = np.zeros_like(CBB) * np.nan
Qbbf[CBB < 0.1] = 1
Qbbf[CBB > 0.5] = 0
Qbbf[(CBB <= 0.5) & (CBB >= 0.1)] = (
1 - (CBB[(CBB <= 0.5) & (CBB >= 0.1)] - 0.1) / 0.4
)
return Qbbf
# ds0_masked = ds0_masked.assign({'QBBF': QBBF(CBB)})
ds0_masked = ds0_masked.assign(
{"QBBF": (["azimuth", "range"], QBBF(CBB), ds0_masked.ZDR.attrs)}
)
return ds0_masked
def get_bbox(gr_data):
"""
Calculation of bounding box around the ground radar
# Input:
# ------
gr_data ::: graund radar sweep data
# Output:
# ------
bbox ::: Coords for bounding box
"""
# number of rays in gr sweep
nray_gr = gr_data.rays.data.shape[0]
# gate length (meters)
dr_gr = gr_data.range.data[1] - gr_data.range.data[0]
# number of gates in gr beam
ngate_gr = gr_data.rays.data.shape[1]
# elevation of sweep (degree)
elev_gr = gr_data.elevation.data[0]
# Longitude of GR
lon0_gr = gr_data.longitude.data
# Latitude of GR
lat0_gr = gr_data.latitude.data
# Altitude of GR (meters)
alt0_gr = gr_data.altitude.data
# spherical to proj
coord = wrl.georef.sweep_centroids(nray_gr, dr_gr, ngate_gr, elev_gr)
coords = wrl.georef.spherical_to_proj(
coord[..., 0],
np.degrees(coord[..., 1]),
coord[..., 2],
(lon0_gr, lat0_gr, alt0_gr),
)
lon = coords[..., 0]
lat = coords[..., 1]
bbox = wrl.zonalstats.get_bbox(lon, lat)
return bbox
def get_grid_polygons(ds):
x = ds.x
for i in reversed(range(x.ndim)):
x = xr.plot.utils._infer_interval_breaks(x, axis=i)
y = ds.y
for i in reversed(range(y.ndim)):
y = xr.plot.utils._infer_interval_breaks(y, axis=i)
coords = np.stack([x, y], axis=-1)
ll = np.dstack([coords[0:-1, 0:-1], ds.z.values[..., None]])
ul = np.dstack([coords[0:-1, 1:], ds.z.values[..., None]])
ur = np.dstack([coords[1:, 1:], ds.z.values[..., None]])
lr = np.dstack([coords[1:, 0:-1], ds.z.values[..., None]])
return np.stack([ll, ul, ur, lr, ll], axis=-2)
def get_sr_cut(swp, sr_data):
# dr_sr = 125.0
# Todo: only keep one method
# # Calculate equivalent earth radius
# wgs84 = wrl.georef.get_default_projection()
# # lon0_gr = gr_data.latitude.values
# re1 = swp.wrl.georef.get_earth_radius(sr=wgs84)
# a = wgs84.GetSemiMajor()
# b = wgs84.GetSemiMinor()
# Set up aeqd-projection gr-centered
# proj_rad = wrl.georef.projstr_to_osr(
# ("+proj=aeqd +lon_0={lon:f} " + "+lat_0={lat:f} +a={a:f} " + "+b={b:f}").format(
# lon=swp.longitude.values, lat=swp.latitude.values, a=a, b=b
# )
# )
# swp.wrl.georef.get_earth_radius(sr=proj_rad)
proj_rad = osr.SpatialReference()
proj_rad.ImportFromWkt(xd.georeference.get_crs(swp).to_wkt())
sr_data = sr_data.wrl.georef.reproject(
coords=dict(x="Longitude", y="Latitude"),
src_crs=wrl.georef.get_default_projection(),
trg_crs=proj_rad,
)
# calculate range and mask srdata
r = np.sqrt(sr_data.x**2 + sr_data.y**2)
sr_cut = sr_data.where(r < swp.gr.max(), drop=True)
return sr_cut # , proj_rad, re1
def calc_sat(sr_cut, swp, dr_sr): # , proj_rad, re1):
sr_cut = sr_cut.wrl.georef.correct_parallax(dr_sr)
proj_rad = osr.SpatialReference()
proj_rad.ImportFromWkt(xd.georeference.get_crs(swp).to_wkt())
re1 = swp.wrl.georef.get_earth_radius()
sr_cut = sr_cut.wrl.georef.xyz_to_spherical(crs=proj_rad)
bw_sr = 0.71
freq = 0
sr_cut = sr_cut.wrl.georef.dist_from_orbit(bw_sr, freq, re1)
rs = sr_cut["pr_dist"]
vol_sr = rs.wrl.qual.pulse_volume(dr_sr, bw_sr)
sr_cut = sr_cut.assign(vol_sr=vol_sr)
bw_gr = 1.0
vol_gr = wrl.qual.pulse_volume(swp.range, swp.range.diff("range").median(), bw_gr)
vol_gr = vol_gr.broadcast_like(swp.DBZH)
freq = 0
alpha = sr_cut["localZenithAngle"].isel(nfreq=freq, missing_dims="ignore")
# Rs
sr_bin_radius = (
0.5 * (1 + np.cos(np.radians(alpha))) * rs * np.tan(np.radians(bw_sr / 2.0))
)
sr_bin_radius.name = "sr_bin_radius"
# Ds
sr_bin_depth = dr_sr / np.cos(np.radians(alpha))
sr_bin_depth.name = "sr_bin_depth"
sr_cut = sr_cut.assign(sr_bin_radius=sr_bin_radius, sr_bin_depth=sr_bin_depth)
return sr_cut
def calculate_transform(sr_cut):
ref_sr = sr_cut.zFactorFinal.isel(nfreq=0, missing_dims="ignore").copy()
# ref_sr = sr_data['refl'].filled(np.nan)
ref_sr_ss = xr.zeros_like(ref_sr) * np.nan
ref_sr_sh = xr.zeros_like(ref_sr) * np.nan
a_s, a_h = (wrl.trafo.KuBandToX.snow, wrl.trafo.KuBandToX.hail)
# mask bb_ratio with bb_mask
ratio = sr_cut["bb_ratio"].where(sr_cut["bb_mask"])
ia = ratio >= 1
ib = ratio <= 0
im = (ratio > 0) & (ratio < 1)
im = xr.where(im, np.round(ratio * 10), 0).astype("int")
# adding ia/ib/im in sequential order to ref_sr_ss/ref_sr/sh
# above melting layer -> no mixing only snow/hail
ref_sr_ss = xr.where(
ia, ref_sr + wrl.util.calculate_polynomial(ref_sr.copy(), a_s[:, 1]), ref_sr_ss
)
ref_sr_sh = xr.where(
ia, ref_sr + wrl.util.calculate_polynomial(ref_sr.copy(), a_h[:, 1]), ref_sr_sh
)
# below the melting layer
ref_sr_ss = xr.where(
ib, ref_sr + wrl.util.calculate_polynomial(ref_sr.copy(), a_s[:, 0]), ref_sr_ss
)
ref_sr_sh = xr.where(
ib, ref_sr + wrl.util.calculate_polynomial(ref_sr.copy(), a_h[:, 0]), ref_sr_sh
)
# ref_sr_ss = xr.where(im, ref_sr + np.nan, ref_sr_ss)
# ref_sr_sh = xr.where(im, ref_sr + np.nan, ref_sr_sh)
ref_sr_ss = xr.where(im, ref_sr + np.nan, ref_sr)
ref_sr_sh = xr.where(im, ref_sr + np.nan, ref_sr)
return ref_sr_ss, ref_sr_sh
[docs]
def volume_matching(swp, sr_data, ref="DBZH", grrefth=15):
dr_sr = 125.0
swp = swp.copy().where(swp[ref] >= grrefth)
gr_poly = get_grid_polygons(swp)
# # Todo: only keep one method
# # Calculate equivalent earth radius
# wgs84 = wrl.georef.get_default_projection()
# # lon0_gr = gr_data.latitude.values
# re1 = swp.wrl.georef.get_earth_radius(sr=wgs84)
# a = wgs84.GetSemiMajor()
# b = wgs84.GetSemiMinor()
# # Set up aeqd-projection gr-centered
# proj_rad = wrl.georef.projstr_to_osr(
# ("+proj=aeqd +lon_0={lon:f} " + "+lat_0={lat:f} +a={a:f} " + "+b={b:f}").format(
# lon=swp.longitude.values, lat=swp.latitude.values, a=a, b=b
# )
# )
# swp.wrl.georef.get_earth_radius(sr=proj_rad)
# sr_data = sr_data.wrl.georef.reproject(
# coords=dict(x="Longitude", y="Latitude"),
# src_crs=wrl.georef.get_default_projection(),
# trg_crs=proj_rad,
# )
# # calculate range and mask srdata
# r = np.sqrt(sr_data.x**2 + sr_data.y**2)
# sr_cut = sr_data.where(r < swp.gr.max(), drop=True)
sr_cut = get_sr_cut(swp, sr_data)
precip_mask = xr.where(sr_cut.flagPrecip > 0, 1, 0)
sr_cut = calc_sat(sr_cut, swp, dr_sr)
sr_cut = sr_cut.wrl.qual.get_bb_ratio()
ref_sr_ss, ref_sr_sh = calculate_transform(sr_cut)
ptype = (sr_cut.typePrecip / 1e7).astype(np.int16)
# Combined conversion
ref_sr_com = xr.zeros_like(ref_sr_ss) * np.nan
ref_sr_com = xr.where(ptype == 1, ref_sr_ss, ref_sr_com)
ref_sr_com = xr.where(ptype == 2, ref_sr_sh, ref_sr_com)
sr_cut = sr_cut.assign(
ref_sr_com=ref_sr_com,
ref_sr_ss=ref_sr_ss,
ref_sr_sh=ref_sr_sh,
precip_mask=precip_mask,
)
## matching below
bw_gr = 1.0
swp = swp.assign(sweep_fixed_angle=swp.sweep_fixed_angle.median(skipna=True))
sr_match = sr_cut.reset_coords(["xp", "yp", "zp"])
sr_match = sr_match.where(sr_cut.precip_mask).where(
(sr_match.elevation >= (swp.sweep_fixed_angle - bw_gr / 2.0))
& (sr_match.elevation <= (swp.sweep_fixed_angle + bw_gr / 2.0))
)
# calculate nbeams
sr_match_mean = sr_match.mean("nbin", skipna=True)
xyzp_sr = xr.concat(
[sr_match_mean.xp, sr_match_mean.yp, sr_match_mean.zp], dim="ncoord"
).transpose(..., "ncoord")
xyzp_sr = (
xyzp_sr.stack(nbeams=("nscan", "nray"))
.transpose("nbeams", "ncoord", ...)
.dropna(
dim="nbeams",
)
)
sr_match2 = sr_match.stack(nbeams=("nscan", "nray")).where(xyzp_sr.nbeams)
# mean along nbin
sr_match_mean = sr_match.mean(dim="nbin", skipna=True)
sr_match2_mean = sr_match2.mean(dim="nbin", skipna=True)
# max along nbin, approximate Rs
# sr_match_max = sr_match.max(dim="nbin", skipna=True)
sr_match2_max = sr_match2.max(dim="nbin", skipna=True)
# sum along nbin
# approximate Ds and Vs
sr_match.sum(dim="nbin", skipna=True)
sr_match2.sum(dim="nbin", skipna=True)
## Create Zonal Data Source of matching SR profiles
proj_rad = osr.SpatialReference()
proj_rad.ImportFromWkt(xd.georeference.get_crs(swp).to_wkt())
xyp_sr = xr.concat([sr_match2_mean.xp, sr_match2_mean.yp], dim="xy").transpose(
..., "xy"
)
zds = wrl.io.VectorSource(xyp_sr.values, trg_crs=proj_rad)
## Create SR beam polygons using sr_bin_radius Rs
tmp_trg_lyr = zds.ds.GetLayer()
trg_poly = []
for i, feat in enumerate(tmp_trg_lyr):
geom = feat.GetGeometryRef().Buffer(sr_match2_max.sr_bin_radius.values[i])
poly = wrl.georef.ogr_to_numpy(geom)
trg_poly.append(poly)
zdp = wrl.zonalstats.ZonalDataPoly(
gr_poly[..., 0:2].reshape(-1, 5, 2),
trg=trg_poly,
crs=proj_rad,
)
zdp.dump_vector("m3d_zonal_poly")
obj3 = wrl.zonalstats.ZonalStatsPoly("m3d_zonal_poly")
# converted for stratiform
ref_sr2gr_m = sr_match2.ref_sr_com.copy()
ref_sr2gr_m = wrl.trafo.idecibel(ref_sr2gr_m)
ref_sr2gr_m = ref_sr2gr_m.mean(dim="nbin", skipna=True)
ref_sr2gr_m = wrl.trafo.decibel(ref_sr2gr_m)
ref_sr2gr_m.name = "mean_sr_reflectivity"
# mean = obj3.mean(wrl.trafo.idecibel(swp.DBZH).values.ravel())
gr_mean = wrl.trafo.idecibel(swp[ref]).values.ravel()
gr_mean = np.array(
[
np.nanmean(gr_mean[obj3.ix[i]])
for i in np.arange(len(obj3.ix))[~obj3.check_empty()]
]
)
obj3.zdata.trg.geo["mean"] = wrl.trafo.decibel(gr_mean)
ref_gr_m = obj3.zdata.trg.geo["mean"].to_xarray()
ref_gr_m.name = "mean_gr_reflectivity"
ref_gr_m = ref_gr_m.rename(index="nbeams")
pbb_gr_i = swp.QBBF.values.ravel()
pbbgr1a = np.array(
[
np.nanmin(pbb_gr_i[obj3.ix[i]])
for i in np.arange(len(obj3.ix))[~obj3.check_empty()]
]
)
qbbf = xr.zeros_like(ref_gr_m)
qbbf.data = pbbgr1a
qbbf.name = "pbb_quality_index"
# qbbf = xr.zeros_like(ref_gr_m)
# qbbf.data = pbbgr1a
#########################
# pia_gr_i = swp.QPIA.values.ravel()
# piagr1a = np.array(
# [
# np.nanmax(pia_gr_i[obj3.ix[i]])
# for i in np.arange(len(obj3.ix))[~obj3.check_empty()]
# ]
# )
# qpia = xr.zeros_like(ref_gr_m)
# qpia.data = piagr1a
# return xr.merge([ref_gr_m, ref_sr2gr_m, qbbf, xyp_sr])
xy = xyp_sr.reset_index("nbeams").reset_coords(drop=True)
ds = xr.merge(
[ref_gr_m, ref_sr2gr_m.reset_index("nbeams").reset_coords(drop=True), qbbf]
)
ds = ds.assign_coords(x=xy.isel(xy=0), y=xy.isel(xy=1))
return ds
# Todo: make height level parameterizable via keyword argument
# Todo: plot gr radar domain and satellite swath outlines
[docs]
def plot_unmatched_refl(sr_data, gr_data, level=-5):
"""
Plotting unmatched SR and GR Reflectivity
# Input:
# ------
gr_data ::: ground radar sweep data
sr_data ::: satellite data
# Output:
# ------
Plot of SR Refl. and GR Refl.
"""
plt.figure(figsize=(12, 4))
ax1 = plt.subplot(1, 2, 1)
pm = sr_data.zFactorCorrected[:, :, level].plot(
x="Longitude",
y="Latitude",
cmap="HomeyerRainbow",
vmin=0,
vmax=40,
add_labels=False,
add_colorbar=False,
)
cbar = plt.colorbar(pm, ax=ax1, ticks=np.arange(0, 42, 2))
cbar.set_label("SR Reflectivity (dBz)", fontsize=12)
cbar.ax.tick_params(labelsize=12)
ax1.set_xlabel("Longitude (°)", fontsize=12)
ax1.set_ylabel("Latitude (°)", fontsize=12)
ax1.tick_params(axis="both", labelsize=12)
ax1.set_title(sr_data.attrs["platform"], fontsize=12, loc="left")
ax1.grid(lw=0.25, color="grey")
ax1.set_xlim(sr_data.Longitude.min(), sr_data.Longitude.max())
ax1.set_ylim(sr_data.Latitude.min(), sr_data.Latitude.max())
ax2 = plt.subplot(1, 2, 2)
swp = gr_data
ll = swp.wrl.georef.spherical_to_proj()
swp = swp.assign_coords(lon=ll[..., 0], lat=ll[..., 1])
pm = swp.DBZH.plot(
x="lon",
y="lat",
cmap="HomeyerRainbow",
vmin=0,
vmax=40,
ax=ax2,
add_labels=False,
add_colorbar=False,
)
cbar = plt.colorbar(pm, ax=ax2, ticks=np.arange(0, 42, 2))
cbar.set_label("GR Reflectivity (dBz)", fontsize=12)
cbar.ax.tick_params(labelsize=12)
ax2.set_xlabel("Longitude (°)", fontsize=12)
ax2.set_ylabel("Latitude (°)", fontsize=12)
ax2.tick_params(axis="both", labelsize=12)
ax2.set_title("Ground Radar", fontsize=12, loc="left")
ax2.grid(lw=0.25, color="grey")
ax2.set_xlim(sr_data.Longitude.min(), sr_data.Longitude.max())
ax2.set_ylim(sr_data.Latitude.min(), sr_data.Latitude.max())
plt.tight_layout()
[docs]
def plot_matched_refl(vmatch):
"""
Plotting unmatched SR and GR Reflectivity
# Input:
# ------
gr_data ::: graund radar sweep data
sr_data ::: satellite data
# Output:
# ------
Plot of SR Refl. and GR Refl.
"""
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.scatter(
vmatch.x,
vmatch.y,
c=vmatch.mean_sr_reflectivity,
s=10,
cmap="HomeyerRainbow",
vmin=0,
vmax=40,
)
cbar = plt.colorbar(ticks=np.arange(0, 42, 2))
cbar.set_label("SR Reflectivity (dBz)", fontsize=12)
cbar.ax.tick_params(labelsize=12)
plt.xlabel("Longitude (°)", fontsize=12)
plt.ylabel("Latitude (°)", fontsize=12)
plt.xticks(fontsize=12)
plt.yticks(fontsize=12)
plt.title("Satellite Radar", fontsize=12, loc="left")
plt.grid(lw=0.25, color="grey")
plt.subplot(1, 2, 2)
plt.scatter(
vmatch.x,
vmatch.y,
c=vmatch.mean_gr_reflectivity,
s=10,
cmap="HomeyerRainbow",
vmin=0,
vmax=40,
)
cbar = plt.colorbar(ticks=np.arange(0, 42, 2))
cbar.set_label("GR Reflectivity (dBz)", fontsize=12)
cbar.ax.tick_params(labelsize=12)
plt.xlabel("x (km)", fontsize=12)
plt.ylabel("y (km)", fontsize=12)
plt.xticks(fontsize=12)
plt.yticks(fontsize=12)
plt.title("Ground Radar", fontsize=12, loc="left")
plt.grid(lw=0.25, color="grey")
plt.tight_layout()
def dpr_swath_contour(sr_grid):
"""
Function for dpr swath grid contour
## sr_grid[:, 0] # - complete left column
## sr_grid[-1,1:-1] # - upper edge without left and right corner
## sr_grid[:, -1][::-1] # - complete right column (backward)
## sr_grid[0,0:-1] # - lower edge (without right corner)
Input:
------
sr_grid ::: Space-borne grid x,y
Output:
-------
trg ::: Contour of the space-borne grid
"""
trg = np.r_[
sr_grid[:, 0], sr_grid[-1, 1:-1], sr_grid[:, -1][::-1], sr_grid[0, 0:-1][::-1]
]
return trg
def gpm_scan_time(filename, scanswath="NS"):
from netCDF4 import Dataset
pr_data = Dataset(filename, mode="r")
year = pr_data[scanswath]["ScanTime"].variables["Year"]
month = pr_data[scanswath]["ScanTime"].variables["Month"]
dayofmonth = pr_data[scanswath]["ScanTime"].variables["DayOfMonth"]
# dayofyear = pr_data['NS']['ScanTime'].variables['DayOfYear'][mask]
hour = pr_data[scanswath]["ScanTime"].variables["Hour"]
minute = pr_data[scanswath]["ScanTime"].variables["Minute"]
second = pr_data[scanswath]["ScanTime"].variables["Second"]
# secondofday = pr_data['NS']['ScanTime'].variables['SecondOfDay'][mask]
millisecond = pr_data[scanswath]["ScanTime"].variables["MilliSecond"]
date_array = zip(
year, month, dayofmonth, hour, minute, second, millisecond, strict=True
)
pr_time = np.array(
[dt.datetime(d[0], d[1], d[2], d[3], d[4], d[5], d[6]) for d in date_array]
)
# ttt = gpm_scan_time(gpm_file)
s_n = {"NS": 49, "HS": 24, "MS": 25}
time_array = np.array(s_n[scanswath] * [pr_time]).T
pr_data.close()
return time_array
def get_time_of_gpm2(gpm_time):
"""einfache mittlere zeit"""
iii = int(len(np.array(gpm_time["Year"])) / 2)
gpm_year = np.array(gpm_time["Year"])[iii]
gpm_month = np.array(gpm_time["Month"])[iii]
gpm_day = np.array(gpm_time["DayOfMonth"])[iii]
gpm_hour = np.array(gpm_time["Hour"])[iii]
gpm_min = np.array(gpm_time["Minute"])[iii]
gpm_sek = np.array(gpm_time["Second"])[iii]
gpm_dt = dt.datetime(
gpm_year, gpm_month, gpm_day, gpm_hour, gpm_min, gpm_sek
).strftime("%Y.%m.%d -- %H:%M:%S")
return gpm_dt
def hist_2d(
A,
B,
binsx=35,
binsy=35,
mini=1,
maxi=None,
cmap="turbo",
colsteps=30,
alpha=1,
fsize=15,
colbar=True,
):
"""
# Histogram 2d Quicklooks
# ------------------------
Plotting 2d Histogramm of two varibles
# Input
# -----
A,B ::: Variables
bins1, bins2 ::: x, y bins
mini, maxi ::: min and max
cmap ::: colormap
colsteps ::: number of cmap steps
alpha ::: transperency
fsize ::: fontsize
# Output
# ------
2D Histogramm Plot
::: Hist mode:::
absolute ::: absolute numbers
relative ::: relative numbers
relative_with_y ::: relative numbers of y levels
"""
from matplotlib.colors import LogNorm
# discret cmap
cmap = mpl.colormaps.get_cmap(cmap, colsteps)
# mask array
m = ~np.isnan(A) & ~np.isnan(B)
plt.hist2d(
A[m],
B[m],
bins=(binsx, binsy),
cmap=cmap,
norm=LogNorm(vmin=mini, vmax=maxi),
alpha=alpha,
)
if colbar is True:
cb = plt.colorbar(shrink=1, pad=0.01)
cb.set_label("number of samples", fontsize=fsize)
cb.ax.tick_params(labelsize=fsize)
plt.xticks(fontsize=fsize)
plt.yticks(fontsize=fsize)
[docs]
def set_sweep_mode_as_coords(ds):
ds = ds.set_coords(["sweep_mode"])
return ds
[docs]
def x_cumtrapz(da):
"""xarray wrapper for scipy.cumtrapz"""
dr = da.range.diff("range").median("range").values / 1000.0
print("range res [km]:", dr)
return xr.apply_ufunc(
scipy.integrate.cumulative_trapezoid,
da,
input_core_dims=[["range"]],
output_core_dims=[["range"]],
dask="parallelized",
kwargs=dict(dx=dr, initial=0.0, axis=-1),
)
[docs]
def plot_beamprop(r, theta, sitecoords, er, k):
beamradius = wrl.util.half_power_radius(r, 1.0)
beam_height = wrl.georef.bin_altitude(
r, theta[:, np.newaxis], sitecoords[2], re=er, ke=k
)
arc_distance = wrl.georef.bin_distance(
r, theta[:, np.newaxis], sitecoords[2], re=er, ke=k
)
def height_formatter(x, pos):
x = (x - 6370000) / 1000
fmt_str = f"{x:g}"
return fmt_str
def range_formatter(x, pos):
x = x / 1000.0
fmt_str = f"{x:g}"
return fmt_str
fig = plt.figure(figsize=(10, 6))
cgax, caax, paax = wrl.vis.create_cg(fig=fig, rot=0, scale=1)
# azimuth angle
# fix grid_helper
gh = cgax.get_grid_helper()
gh.grid_finder.grid_locator2._nbins = 40
gh.grid_finder.grid_locator2._steps = [1, 2, 4, 5, 10]
# calculate beam_height and arc_distance for ke=1
# means line of sight
bhe = wrl.georef.bin_altitude(r, 0, sitecoords[2], re=er, ke=1.0)
ade = wrl.georef.bin_distance(r, 0, sitecoords[2], re=er, ke=1.0)
nn0 = np.zeros_like(r)
# for nice plotting we assume earth_radius = 6370000 m
ecp = nn0 + er
# theta (arc_distance sector angle)
thetap = -np.degrees(ade / er) + 90.0
# zero degree elevation with standard refraction
bh0 = wrl.georef.bin_altitude(r, 0, sitecoords[2], re=er, ke=4 / 3)
# plot (ecp is earth surface normal null)
(bes,) = paax.plot(thetap, ecp, "-k", linewidth=3, label="Earth Surface NN")
(bc0r,) = paax.plot(thetap, ecp + bh0, "-g", label="0 deg Refraction")
(bc0n,) = paax.plot(thetap, ecp + bhe, "-r", label="0 deg line of sight")
thetap_k = -np.degrees(arc_distance / er) + 90.0
for i in range(len(beam_height) - 1):
(bc,) = paax.plot(
thetap_k[i],
ecp + beam_height[i, :],
linestyle="-",
linewidth=3,
label=f"Beam Center {theta[i]}deg",
)
(b3db,) = paax.plot(
thetap_k[i],
ecp + beam_height[i, :] + beamradius,
linestyle=":",
color="grey",
)
paax.plot(
thetap_k[i],
ecp + beam_height[i, :] - beamradius,
linestyle=":",
color="grey",
)
(bc,) = paax.plot(
thetap_k[i],
ecp + beam_height[i + 1, :],
linestyle="-",
linewidth=3,
label=f"Beam Center {theta[i + 1]}deg",
)
(b3db,) = paax.plot(
thetap_k[i],
ecp + beam_height[i + 1, :] + beamradius,
linestyle=":",
color="grey",
label="+3 dB Beam width",
)
paax.plot(
thetap_k[i],
ecp + beam_height[i + 1, :] - beamradius,
linestyle=":",
color="grey",
label="-3 dB Beam width",
)
# orography
# paax.fill_between(thetap, ecp,
# ecp + polarvalues[angle, :],
# color='0.75')
# shape axes
cgax.set_xlim(0, np.max(ade))
cgax.set_ylim([ecp.min() - 1000, ecp.max() + 1000])
caax.grid(True, axis="x")
cgax.grid(True, axis="y")
cgax.axis["top"].toggle(all=False)
caax.yaxis.set_major_locator(
mpl.ticker.MaxNLocator(steps=[1, 2, 4, 5, 10], nbins=20, prune="both")
)
caax.xaxis.set_major_locator(mpl.ticker.MaxNLocator())
caax.yaxis.set_major_formatter(mpl.ticker.FuncFormatter(height_formatter))
caax.xaxis.set_major_formatter(mpl.ticker.FuncFormatter(range_formatter))
caax.set_xlabel("Range (km)")
caax.set_ylabel("Altitude (km)")
# Shrink current axis by 20%
box = cgax.get_position()
cgax.set_position([box.x0, box.y0, box.width, box.height * 0.75])
paax.legend(bbox_to_anchor=(0.5, 1.5), loc="upper center", ncol=2)
[docs]
def noise_correction(ds, dbz, rho, noise_level, gas_att=0.033):
"""Calculate SNR, apply correction to RHOHV.
Dataset needs range-coordinate in m.
Parameter
---------
ds : xarray.Dataset
Dataset with radar moments
dbz : str
name of reflectivity moment
rho : str
name of crosscorrelation coefficient moment
noise_level : float
noise level for calculation
Keyword Arguments
-----------------
gas_att : float
gaseous attenuation factor
Return
------
ds : xarray.Dataset
Dataset with added corrected moments
"""
# noise calculations
snrh = (
ds[dbz]
- 20 * np.log10(ds.range * 0.001)
- noise_level
- gas_att * ds.range / 1000
)
# set nan to 0
snrh = snrh.where(snrh >= 0).fillna(0)
attrs = xd.model.sweep_vars_mapping["SNRH"]
attrs.pop("gamic", None)
snrh = snrh.assign_attrs(attrs)
rhohv = ds[rho] * np.sqrt(1.0 + 1.0 / 10.0 ** (snrh * 0.1))
rhohv = rhohv.assign_attrs(ds[rho].attrs)
ds = ds.assign({"SNRH": snrh, rho + "_NC": rhohv})
ds = ds.assign_coords({"NOISE_LEVEL": noise_level})
return ds
def noise_correction2(dbz, rho, noise_level, gas_att=0.033):
"""Calculate SNR, apply to RHOHV.
DataArray needs range-coordinate in m.
Parameter
---------
dbz : xarray.DataArray
reflectivity DataArray
rho : xarray.DataArray
crosscorrelation coefficient DataArray
noise_level : float
noise level for calculation
Keyword Arguments
-----------------
gas_att : float
gaseous attenuation factor
Return
------
ds : xarray.Dataset
Dataset with added corrected moments
"""
# noise calculations
rng_km = dbz.range * 0.001
snrh = dbz - 20 * np.log10(rng_km) - noise_level - gas_att * rng_km
# set nan to 0
snrh = snrh.where(snrh >= 0).fillna(0)
attrs = xd.model.sweep_vars_mapping["SNRH"]
attrs.pop("gamic", None)
snrh = snrh.assign_attrs(attrs)
snrh.name = "SNR"
rho_nc = rho * np.sqrt(1.0 + 1.0 / 10.0 ** (snrh * 0.1))
rho_nc = rho_nc.assign_attrs(rho.attrs)
rho_nc.name = "RHO"
return snrh, rho_nc
def noise_rho_hist(
dbz, rho, noise=(-40, -20, 2), rho_bins=(0.9, 1.1, 0.005), snr_bins=(5.0, 30.0, 0.1)
):
"""Calculate SNR, apply to RHOHV.
DataArray needs range-coordinate in m.
Parameter
---------
dbz : xarray.DataArray
reflectivity DataArray
rho : xarray.DataArray
crosscorrelation coefficient DataArray
Keyword Arguments/RADAR_NG/
-----------------
noise : tuple
tuple to create noise-array
rho_bins : tuple
tuple to create rho-array
snr_bins : tuple
tuple to create snr-array
Return
------
hists : xarray.DataArray
DataArray with Noise Calculations
"""
noise = np.arange(*noise)
rho_bins = np.arange(*rho_bins)
snr_bins = np.arange(*snr_bins)
corr = [noise_correction2(dbz, rho, n) for n in noise]
hist = [
dask.delayed(histogram)(
rho0, snr0, bins=[rho_bins, snr_bins], block_size=rho.time.size
)
for snr0, rho0 in corr
]
with ProgressBar():
hist = dask.compute(*hist)
hists = xr.concat(hist, "noise").to_dataset()
hists = hists.assign_coords({"noise": (["noise"], noise)})
std = hists.idxmax("RHO_bin").std(dim="SNR_bin", skipna=True).histogram_RHO_SNR
hists = hists.assign_coords({"stdev": std})
return hists
# def noise_rho_hist(
# dbz, rho, noise=(-40, -10, 2), rho_bins=(0.9, 1.1, 0.005), snr_bins=(5.0, 30.0, 0.1)
# ):
# """Calculate SNR, apply to RHOHV.
#
# DataArray needs range-coordinate in m.
#
# Parameter
# ---------
# dbz : xarray.DataArray
# reflectivity DataArray
# rho : xarray.DataArray
# crosscorrelation coefficient DataArray
#
# Keyword Arguments
# -----------------
# noise : tuple
# tuple to create noise-array
# rho_bins : tuple
# tuple to create rho-array
# snr_bins : tuple
# tuple to create snr-array
#
# Return
# ------
# hists : xarray.DataArray
# DataArray with Noise Calculations
#
# """
# noise = np.arange(*noise)
# rho_bins = np.arange(*rho_bins)
# snr_bins = np.arange(*snr_bins)
# corr = [noise_correction2(dbz, rho, n) for n in noise]
# hist = [
# dask.delayed(histogram)(
# rho0, snr0, bins=[rho_bins, snr_bins], block_size=rho.time.size
# )
# for snr0, rho0 in corr
# ]
# with ProgressBar():
# hist = dask.compute(*hist)
# hists = xr.concat(hist, "noise").to_dataset()
# hists = hists.assign_coords({"noise": (["noise"], noise)})
# std = hists.idxmax("RHO_bin").std(dim="SNR_bin", skipna=True).histogram_RHO_SNR
# hists = hists.assign_coords({"stdev": std})
# return hists
[docs]
def noise_hist(dbz, rho, noise=(-40, -10, 1)):
hist = noise_rho_hist(dbz, rho, noise=noise)
nl = hist.stdev.idxmin()
noise = (nl - 2.0, nl + 2.1, 0.5)
hist = noise_rho_hist(dbz, rho, noise=noise)
nl = hist.stdev.idxmin()
noise = (nl - 0.5, nl + 0.51, 0.1)
hist = noise_rho_hist(dbz, rho, noise=noise)
return hist
[docs]
def noise_level(hist):
nl = hist.stdev.idxmin()
return nl
[docs]
def estimate_cmap(ds, dbzsum, upper_frequency=0.05, lower_frequency=0.05, name="CMAP"):
"""Retrieve Clutter and Beam blockage.
Dataset needs range-coordinate in m.
Parameter
---------
ds : xarray.Dataset
Dataset with radar moments
dbzsum : xarray.DataArray
DataArray with reflectivity sum
Return
------
ds : xarray.Dataset
Dataset with added clmap moment
"""
# clutter calculation
clmap = wrl.classify.histo_cut(
dbzsum, upper_frequency=upper_frequency, lower_frequency=lower_frequency
).compute()
attrs = xd.model.sweep_vars_mapping["CMAP"]
clmap = clmap.assign_attrs(attrs)
ds = ds.assign({name: clmap})
return ds
[docs]
def get_range_step(ds):
return np.diff(ds.range)[0]
[docs]
def get_bins_from_range(ds, rng):
range_step = get_range_step(ds)
return int(rng / range_step)
def _interpolate_dem(demc, demd, polc):
polarvalues = wrl.ipol.map_coordinates(demc, demd, polc, order=3, prefilter=False)
return polarvalues
[docs]
def interpolate_dem(swp, dem):
# swp = swp.copy()
crs_wkt = dem.coords.get("crs_wkt") or dem.coords.get("spatial_ref", False)
if crs_wkt is not False:
proj_crs = pyproj.CRS.from_cf(crs_wkt.attrs)
else:
raise ValueError("missing spatial ref")
proj_dem = osr.SpatialReference()
proj_dem.ImportFromWkt(proj_crs.to_wkt())
coords = swp.wrl.georef.reproject(trg_crs=proj_dem).reset_coords()
swp = swp.assign_coords(xd=coords.x, yd=coords.y)
polc = xr.concat([coords.x, coords.y], "xy").transpose(..., "xy")
dsign = np.sign(dem.y.diff("y")[0]).astype(int).values
dem = dem.sel(
x=slice(coords.x.min().values, coords.x.max().values),
y=slice(coords.y.min().values, coords.y.max().values, dsign),
)
demc = xr.concat(xr.broadcast(dem.x, dem.y), dim="xy").transpose("y", "x", "xy")
pol = xr.apply_ufunc(
_interpolate_dem,
demc,
dem,
polc,
input_core_dims=[["y", "x", "xy"], ["y", "x"], ["azimuth", "range", "xy"]],
output_core_dims=[["azimuth", "range"]],
)
swp = swp.assign(DEM=pol)
return swp
[docs]
def get_beamblockage(swp, bw=1.0):
bw = 1.0
beamradius = wrl.util.half_power_radius(swp.range.values, bw)
PBB = wrl.qual.beam_block_frac(swp.DEM.values, swp.z.values, beamradius)
PBB = np.ma.masked_invalid(PBB)
CBB = wrl.qual.cum_beam_block_frac(PBB)
swp = swp.assign(PBB=(["azimuth", "range"], PBB), CBB=(["azimuth", "range"], CBB))
return swp
[docs]
def get_qbbf(swp):
"""
Quality Index for PBB
Crisologo 2018
"""
Qbbf = xr.zeros_like(swp.CBB) * np.nan
Qbbf = xr.where(swp.CBB < 0.1, 1, Qbbf)
Qbbf = xr.where(swp.CBB > 0.5, 0, Qbbf)
Qbbf = xr.where(
(swp.CBB <= 0.5) & (swp.CBB >= 0.1), 1 - (swp.CBB - 0.1) / 0.4, Qbbf
)
Qbbf.name = "QBBF"
swp = swp.assign(QBBF=Qbbf)
return swp
[docs]
def save_dwd_station(station_id, start_time, stop_time, path=".", reload=False):
print(f"processing station: {int(station_id):05d}")
outfile = os.path.join(
path, f"{int(station_id):05d}_{start_time:%Y%m%d%H%M}_{stop_time:%Y%m%d%H%M}.nc"
)
# check if ncfile already exists
if os.path.isfile(outfile) and reload is False:
print(f"--- station {int(station_id):05d} already exists")
return
# check if dwd file already exists
fname = f"1minutenwerte_nieder_{station_id:05d}_{start_time:%Y%m}01*_hist.zip"
if not os.path.isfile(fname):
parser = DWDHTMLParser()
opendata_url1 = f"https://opendata.dwd.de/climate_environment/CDC/observations_germany/climate/1_minute/precipitation/historical/{start_time.year}/"
http = urllib3.PoolManager(cert_reqs="CERT_REQUIRED", ca_certs=certifi.where())
response = http.request("GET", opendata_url1).data.decode("utf-8")
parser.links = []
parser.feed(response)
filelist = parser.links[1:]
matching = fnmatch.filter(filelist, fname)
for f in matching:
with (
http.request(
"GET", os.path.join(opendata_url1, f), preload_content=False
) as r,
open(f, "wb") as out,
):
shutil.copyfileobj(r, out)
if not matching:
return
columns_to_read = ["STATIONS_ID", "MESS_DATUM_BEGINN", "RS_01"]
df = pd.read_csv(
matching[0], sep=";", parse_dates=["MESS_DATUM_BEGINN"], usecols=columns_to_read
)
df.columns = ["station_id", "time", "precipitation_height"]
rain = df.to_xarray()
rain.precipitation_height.attrs = dict(short_name="precipitation_height", unit="mm")
rain = rain.set_coords(["time", "station_id"])
rain = rain.swap_dims(index="time")
rain = rain.drop("index")
rain["station_id"] = rain["station_id"][0]
# select time range
rain = rain.sel(time=slice(start_time, stop_time))
# do not write, if no values available
if rain.sizes["time"] == 0:
return
# extract time strings
start_time_str = rain.time.dt.strftime("%Y%m%d%H%M")[0].values
stop_time_str = rain.time.dt.strftime("%Y%m%d%H%M")[-1].values
outfile = f"{int(station_id):05d}_{start_time_str}_{stop_time_str}.nc"
print("--- save to ", outfile)
rain.to_netcdf(outfile)
[docs]
def process_regression(uh, rlog):
from scipy import stats
mask = ~np.isnan(rlog) & ~np.isnan(uh)
uh = uh.where(mask, drop=True)
rl = rlog.where(mask, drop=True)
a = np.nan
b = np.nan
slope = np.nan
intercept = np.nan
if len(uh) > 0:
slope, intercept, r_value, p_value, std_err = stats.linregress(uh, rl)
print(f"m:{slope}, n:{intercept}")
b = 1.0 / (10.0 * slope)
a = 10 ** (-b * intercept)
print(f"a:{a}, b:{b}")
return a, b, slope, intercept
[docs]
def read_dwd_stations(fname):
dwd_stations = pd.read_csv(
fname, encoding="iso-8859-1", nrows=1, delimiter=" ", engine="python"
)
names = list(dwd_stations.to_xarray().data_vars)
width = [6, 9, 9, 15, 12, 10, 39]
dwd_stations = pd.read_fwf(
fname, width=width, encoding="iso-8859-1", skiprows=2, header=None
)
dwd_stations.columns = names
dwd = dwd_stations.to_xarray().rename(
Stations_id="station_id", Stationsname="station_name"
)
return dwd
[docs]
def filter_stations(dwd, swp, distance):
x = dwd.geoLaenge
y = dwd.geoBreite
points = np.squeeze(np.dstack((x, y)))
rng = swp.sel(range=distance, method="nearest")
vertices = np.squeeze(np.dstack((rng.xd, rng.yd)))
pth = Path(vertices)
dwdx = dwd.sel(index=pth.contains_points(points))
dwdx = dwdx.set_coords("station_id")
dwdx = dwdx.swap_dims(index="station_id")
return dwdx
[docs]
def filter_radar_bins(swp, dwdx, n_nearest):
ds = swp.stack(bins=["azimuth", "range"])
ds = ds.assign_coords(xyd=xr.concat([ds.xd, ds.yd], "xy").transpose(..., "xy"))
tree = KDTree(ds.xyd)
dist, ix = tree.query(
list(zip(dwdx.geoLaenge, dwdx.geoBreite, strict=True)), k=n_nearest
)
dwdx = dwdx.assign(
bins=(["station_id", "bin"], ix), dist=(["station_id", "bin"], dist)
)
ds_stations = ds.isel(bins=dwdx.bins)
return ds_stations, dwdx
def workflow(sweep_ts, scan, path, cmap2=False):
dim0 = sweep_ts.wrl.util.dim0()
# DEM Processing
# Get DEM-data
if sweep_ts.sweep_mode.values == "azimuth_surveillance":
rasterfile = wrl.util.get_wradlib_data_file("geo/bonn_dem.tif")
dem = xr.open_dataset(rasterfile, engine="rasterio")
# DEM-data on radar grid
sweep_ts = sweep_ts.pipe(interpolate_dem, dem.band_data[0])
# Beamblockage
sweep_ts = sweep_ts.pipe(get_beamblockage)
# QI(BBF)
sweep_ts = sweep_ts.pipe(get_qbbf)
# Clutter and Noise
if cmap2:
start_time = sweep_ts.time.dt.date.min().load().item()
start_time_day = dt.datetime(start_time.year, start_time.month, start_time.day)
delta = dt.timedelta(days=1)
sweep_ts_day = load_timeseries_sweep(start_time_day, delta, scan)
DBTH = sweep_ts_day.DBTH.sum("time2")
sweep_ts = sweep_ts.pipe(
estimate_cmap,
DBTH,
upper_frequency=0.05,
lower_frequency=0.01,
name="CMAP2",
)
noise = (-40, -10, 1)
hist = noise_hist(sweep_ts.DBZH, sweep_ts.RHOHV, noise=noise)
nl = noise_level(hist)
sweep_ts = sweep_ts.pipe(noise_correction, "DBZH", "RHOHV", nl)
# System PHIDP
if "CMAP" not in sweep_ts:
if "time2" in sweep_ts:
CMAP = xr.zeros_like(sweep_ts.DBTH.isel(time2=0), "uint16")
else:
CMAP = xr.zeros_like(sweep_ts.DBTH, "uint16")
# sweep_ts = sweep_ts.assign(CMAP=xr.zeros_like(sweep_ts.DBTH.isel(time2=0), "uint16"))
else:
CMAP = sweep_ts.CMAP.astype("uint16")
if "CMAP2" not in sweep_ts:
if "time2" in sweep_ts:
CMAP2 = xr.zeros_like(sweep_ts.DBTH.isel(time2=0), "uint8")
else:
CMAP2 = xr.zeros_like(sweep_ts.DBTH, "uint8")
# sweep_ts = sweep_ts.assign(CMAP=xr.zeros_like(sweep_ts.DBTH.isel(time2=0), "uint16"))
else:
CMAP2 = sweep_ts.CMAP2
bit = 0
cmp = (CMAP >> bit) & 0x1
# mask phase
no_clutter_bb = (cmp == 0) & (CMAP2 == 0)
phimask = sweep_ts.PHIDP.where(no_clutter_bb & (sweep_ts.RHOHV_NC >= 0.9))
# create binary array
phib = xr.where(np.isnan(phimask), 0, 1)
# Phase V1
rng = 2000.0
N = sweep_ts.pipe(get_bins_from_range, rng)
# take N range bins and calculate sum, for finding N consecutive bins
phib_sum = phib.rolling(range=N, center=True).sum(skipna=True)
# get maximum along range
smax = phib_sum.max(dim="range", skipna=True)
# if smax == N, we've identified the first consecutive range of valid phase values
# derive start_range and stop_range
rstep = get_range_step(phib_sum)
start_range = phib_sum.idxmax(dim="range").where(smax == N) - smax // 2 * rstep
stop_range = start_range + N * rstep
phi_1 = phimask.where(
(phimask.range >= start_range) & (phimask.range <= stop_range)
)
phi_1m = phi_1.median("range", skipna=True)
phi_1m_sort = xr.apply_ufunc(
np.sort,
phi_1m,
input_core_dims=[[dim0]],
output_core_dims=[[dim0]],
dask="parallelized",
)
sysphi_1 = phi_1m_sort.isel({dim0: slice(0, 30)}).median(dim0, skipna=True)
# Phase V2
# get the maximum along the ray
# derive start_range and stop_range
start_range = phib_sum.idxmax(dim="range") - N // 2 * rstep
stop_range = start_range + N * rstep
phi_2 = phimask.where(
(phimask.range >= start_range) & (phimask.range <= stop_range)
)
# phi_2.plot(x="azimuth")
phi_2m = phi_2.median("range", skipna=True)
phi_2m_sort = xr.apply_ufunc(
np.sort,
phi_2m,
input_core_dims=[[dim0]],
output_core_dims=[[dim0]],
dask="parallelized",
)
sysphi_2 = phi_2m_sort.isel({dim0: slice(0, 30)}).median(dim0, skipna=True)
# Phase V3
# cumulative sum, for finding first N valid bins
phib_cumsum = phib.cumsum("range", skipna=True)
# select only values until N is reached
phi_3 = phimask.where(phib_cumsum <= N)
# phi_3.plot(x="azimuth", vmin=75, vmax=95)
phi_3m = phi_3.median("range", skipna=True)
phi_3m_sort = xr.apply_ufunc(
np.sort,
phi_3m,
input_core_dims=[[dim0]],
output_core_dims=[[dim0]],
dask="parallelized",
)
sysphi_3 = phi_3m_sort.isel({dim0: slice(0, 30)}).median(dim0, skipna=True)
# Phase V4
phase_res = 0.1
bins = np.arange(-180, 180, phase_res)
phi_hist = histogram(sweep_ts.PHIDP, dim=("range",), bins=[bins])
phi_hist_smooth = phi_hist.rolling(PHIDP_bin=5, center=True).mean()
pmax = phi_hist_smooth.idxmax("PHIDP_bin").compute()
phi_hist_smooth_cut = phi_hist_smooth.where(
(phi_hist.PHIDP_bin > (pmax - 5)) & (phi_hist.PHIDP_bin < (pmax + 5)), drop=True
)
phi_hist_smooth_cut = phi_hist_smooth_cut / phi_hist_smooth_cut.max()
phi_hist_smooth_cut = phi_hist_smooth_cut.chunk(
PHIDP_bin=phi_hist_smooth_cut.sizes["PHIDP_bin"]
)
sysphi_4 = (
phi_hist_smooth_cut.differentiate("PHIDP_bin")
.rolling(PHIDP_bin=11, center=True)
.mean()
.idxmax("PHIDP_bin")
)
with xr.set_options(keep_attrs=True):
sweep_ts = sweep_ts.assign({"SYSPHI1": sysphi_1})
sweep_ts = sweep_ts.assign({"PHIDP_OC1": sweep_ts.PHIDP - sysphi_1})
sweep_ts = sweep_ts.assign({"SYSPHI2": sysphi_2})
sweep_ts = sweep_ts.assign({"PHIDP_OC2": sweep_ts.PHIDP - sysphi_2})
sweep_ts = sweep_ts.assign({"SYSPHI3": sysphi_3})
sweep_ts = sweep_ts.assign({"PHIDP_OC3": sweep_ts.PHIDP - sysphi_3})
sweep_ts = sweep_ts.assign({"SYSPHI4": sysphi_4})
sweep_ts = sweep_ts.assign({"PHIDP_OC4": sweep_ts.PHIDP - sysphi_4})
# Phase Processing
# CMAP = sweep_ts.CMAP.astype("uint16")
noise = ((CMAP >> 0) & 0x1).astype(bool)
clutter = ((CMAP >> 2) & 0x1).astype(bool)
masked = noise | clutter
valid = ~masked
# CMAP2 = sweep_ts.CMAP2
clutter2 = CMAP2 == 1
bb = CMAP == 2
masked2 = clutter2 | bb
valid2 = ~masked2
val = valid & valid2
sweep_ts_masked = (
sweep_ts.where(val).where(sweep_ts.RHOHV_NC >= 0.95).where(sweep_ts.DBZH >= 0)
)
sweep_ts = sweep_ts.assign(
RHOHV_SMOOTH=sweep_ts_masked.RHOHV_NC.rolling(
range=11, min_periods=2, center=True
).median(skipna=True)
)
medwin = 41
PHIDP_OC = "PHIDP_OC1"
# median filtering 2d
phimed = sweep_ts_masked[PHIDP_OC].copy()
with xr.set_options(keep_attrs=True):
phimed = phimed.pipe(filter_data, medwin)
# gaussian convolution 1d - smoothing
# play with sigma and kwidth
kwidth = 41
sigma = 10
gkern = gauss_kernel(kwidth, sigma)
with xr.set_options(keep_attrs=True):
phiclean = phimed.pipe(smooth_data, gkern)
window = 41
window2 = 3
dim0 = sweep_ts_masked.wrl.util.dim0()
with xr.set_options(keep_attrs=True):
phi_median = (
sweep_ts_masked[PHIDP_OC]
.copy()
.pipe(
xr_rolling,
window,
window2=window2,
method="median",
skipna=True,
min_periods=3,
)
.transpose(..., dim0, "range")
)
# gaussian convolution 1d - smoothing
# play with sigma and kwidth
kwidth = 41
sigma = 10
gkern = gauss_kernel(kwidth, sigma)
phi_clean = phi_median.pipe(smooth_data, gkern)
sweep_ts = sweep_ts.assign(
{
"PHIDP_CORR1": phimed,
"PHIDP_CORR2": phi_median,
"PHIDP_CORR3": phiclean,
"PHIDP_CORR4": phi_clean,
}
)
PHIDP_CORR = "PHIDP_CORR1"
L = 11
kdp_lanczos = sweep_ts[PHIDP_CORR].wrl.dp.kdp_from_phidp(winlen=L, min_periods=2)
phidp_v, kdp_vulpiani = sweep_ts[PHIDP_CORR].wrl.dp.phidp_kdp_vulpiani(winlen=L)
sweep_ts = sweep_ts.assign({"KDP_LANC": kdp_lanczos, "KDP_VULP": kdp_vulpiani})
# Attenuation Correction
uh = "DBTH"
zh = "DBZH"
zdr = "ZDR"
phi_list = [f"PHIDP_CORR{i}" for i in range(1, 5)]
alphax = 0.28
betax = 0.05
with xr.set_options(keep_attrs=True):
for phi in phi_list:
uhcorr = sweep_ts[uh] + alphax * sweep_ts[phi]
uhcorr.name = f"{sweep_ts[uh].name}_{sweep_ts[phi].name.split('_')[-1]}"
zhcorr = sweep_ts[zh] + alphax * sweep_ts[phi]
zhcorr.name = f"{sweep_ts[zh].name}_{sweep_ts[phi].name.split('_')[-1]}"
zdrcorr = sweep_ts[zdr] + betax * sweep_ts[phi]
zdrcorr.name = f"{sweep_ts[zdr].name}_{sweep_ts[phi].name.split('_')[-1]}"
sweep_ts = sweep_ts.assign(
{uhcorr.name: uhcorr, zhcorr.name: zhcorr, zdrcorr.name: zdrcorr}
)
# ZPHI - Method
# CMAP = sweep_ts.CMAP.astype("uint16")
noise = ((CMAP >> 0) & 0x1).astype(bool)
clutter = ((CMAP >> 2) & 0x1).astype(bool)
masked = noise | clutter
valid = ~masked
phi = sweep_ts.PHIDP.where((CMAP2 == 0) & valid & (sweep_ts.RHOHV_NC > 0.9))
rng = 2000.0
zh = sweep_ts.DBZH.where((CMAP2 == 0) & valid & (sweep_ts.RHOHV_NC > 0.9))
ah = zphi(phi, zh, rng=rng, alphax=np.array([0.28])).squeeze()
kdp_ah = ah.fillna(0) / ah.alpha
kdp_ah.attrs = sweep_ts.KDP.attrs
phical = phidp_from_kdp(kdp_ah)
phical = phical.rename("PHICAL")
phical.attrs = sweep_ts.PHIDP.attrs
sweep_ts = sweep_ts.assign(
AH=ah,
KDP_AH=kdp_ah,
PHIDP_CORR5=phical,
)
uh = "DBTH"
zh = "DBZH"
zdr = "ZDR"
phi = "PHIDP_CORR5"
with xr.set_options(keep_attrs=True):
uhcorr = sweep_ts[uh] + sweep_ts.alpha * sweep_ts[phi]
uhcorr.name = f"{sweep_ts[uh].name}_{sweep_ts[phi].name.split('_')[-1]}"
zhcorr = sweep_ts[zh] + sweep_ts.alpha * sweep_ts[phi]
zhcorr.name = f"{sweep_ts[zh].name}_{sweep_ts[phi].name.split('_')[-1]}"
zdrcorr = sweep_ts[zdr] + betax * sweep_ts[phi]
zdrcorr.name = f"{sweep_ts[zdr].name}_{sweep_ts[phi].name.split('_')[-1]}"
sweep_ts = sweep_ts.assign(
{uhcorr.name: uhcorr, zhcorr.name: zhcorr, zdrcorr.name: zdrcorr}
)
# Save data
dtime_min = sweep_ts.time.load().min().dt.strftime("%Y%m%dT%H%M").values
dtime_max = sweep_ts.time.load().max().dt.strftime("%Y%m%dT%H%M").values
fname = os.path.join(path, f"tmp_{dtime_min}_{dtime_max}_workflow.nc")
print(fname)
sweep_ts.to_netcdf(fname)
return sweep_ts
[docs]
def get_temperature_profile(rs_data, hmax=30000.0):
stemp = rs_data["TEMP"]
sheight = rs_data["HGHT"]
# remove nans
idx = np.isfinite(stemp)
stemp = stemp[idx]
sheight = sheight[idx]
stemp_da = xr.DataArray(
data=stemp,
dims=["height"],
coords=dict(height=(["height"], sheight)),
attrs=dict(description="Temperature", units="degC", long_name="Temperature"),
)
ht = np.arange(0.0, hmax)
itemp_da = stemp_da.interp({"height": ht})
itemp_da = itemp_da.bfill(dim="height")
return itemp_da
[docs]
def merge_radar_profile(rds, cds):
cds = cds.interp({"height": rds.z}, method="linear")
rds = rds.assign({"TEMP": cds})
return rds