Differential Phase#

In this notebook differential phase \(\Phi_{DP}\) processing is highlighted. This includes phase unfolding as well as derivation of specific differential phase \(K_{DP}\).

import warnings

import matplotlib.pyplot as plt
import numpy as np
import wradlib as wrl
import wradlib_data
import xarray as xr

warnings.filterwarnings("ignore")
/home/docs/checkouts/readthedocs.org/user_builds/wradlib/conda/latest/lib/python3.13/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm

Synthetic \(K_{DP}\) + noise#

First we create an xarray.DataArray with a synthetic \(K_{DP}\). Finally we add some random noise to it.

# Radar grid
azimuth = np.linspace(0, 360, 360, endpoint=False)
elevation = np.ones_like(azimuth) * 1.5
range = np.linspace(0, 150000, 1000)
range_km = range / 1000.

az, r = np.meshgrid(azimuth, range_km, indexing="ij")

# Gaussian rain cell parameters
az0 = 120.0       # deg
r0 = 40.0         # km
sigma_az = 5.0    # deg
sigma_r = 8.0     # km
kdp_max = 3.0     # deg/km

kdp = kdp_max * np.exp(
    -((az - az0) ** 2) / (2 * sigma_az**2)
    -((r - r0) ** 2) / (2 * sigma_r**2)
)

kdp += 1.5 * np.exp(
    -((az - 200) ** 2) / (2 * 8**2)
    -((r - 60) ** 2) / (2 * 10**2)
)

kdp_syn = xr.DataArray(
    kdp,
    dims=("azimuth", "range"),
    coords={"azimuth": azimuth, "elevation": (["azimuth"], elevation), "range": range, "sweep_mode": "azimuth_surveillance", "latitude": 7, "longitude": 50, "altitude": 100},
    name="KDP",
    attrs={"units": "deg/km"},
).wrl.georef.georeference()

rng = np.random.default_rng(seed=42)
noise = 0.1 * rng.standard_normal(size=kdp_syn.shape)
kdp_raw = kdp_syn + noise * np.exp(-r / 80)
kdp_raw = kdp_raw.clip(min=0)

Synthetic \(\Phi_{DP}\) + noise#

From that noisy synthetic \(K_{DP}\) we derive a sythetic \(\Phi_{DP}\), adding more noise to it and remove random data.

phi_noise = kdp_raw.wrl.dp.phidp_from_kdp()

# degrees, adjust for realism
noise_std = 1.0
noise = rng.normal(loc=0.0, scale=noise_std, size=phi_noise.shape)
phi_noise += noise

missing_fraction = 0.05
mask = rng.random(phi_noise.shape) < missing_fraction
phi_noise = xr.where(mask, np.nan, phi_noise)

# system phase
phi_raw = phi_noise + 120
phi_raw = xr.where(phi_raw > 180, phi_raw - 360, phi_raw)
display(phi_raw.max())
<xarray.DataArray 'PHIDP' ()> Size: 8B
array(179.99935682)
Coordinates:
    sweep_mode  <U20 80B 'azimuth_surveillance'
    latitude    int64 8B 7
    longitude   int64 8B 50
    altitude    int64 8B 100
    crs_wkt     int64 8B 0

Overwiev Plots#

Let’s have a look how the synthetic \(\Phi_{DP}\) and \(K_{DP}\) look like

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
phi_raw.wrl.vis.plot(ax=ax1)
ax1.set_title("Raw $\Phi_{DP}$")
kdp_raw.wrl.vis.plot(ax=ax2, vmin=0, vmax=3)
ax2.set_title("Raw $K_{DP}$")
fig.tight_layout()
../../_images/a803d50a826fc5c812a737f4d7b91eb0205d145644d89b878ee0f323747802ec.png
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10))
az = 196.0
r = slice(40e3, 80e3)

phi_noise.sel(azimuth=az, range=r).plot.line(ax=ax1, label="phi_noise")
phi_raw.sel(azimuth=az, range=r).plot.line(ax=ax1, label="phi_raw")
ax1.set_title("$\Phi_{DP}$")
ax1.legend(loc="lower left")
kdp_raw.sel(azimuth=az, range=r).plot.line(ax=ax2, label="kdp_raw")
kdp_syn.sel(azimuth=az, range=r).plot.line(ax=ax2, label="kdp_syn")
ax2.set_title("$K_{DP}$")
ax2.legend(loc="upper left")
fig.tight_layout()
../../_images/38c0970ba56e45374268c1c6073198cb91fb64e9b67e6408402084ed024c13c3.png

Unfold Phase#

If your radar suffers from phase folding, eg. the phase wraps at 180° and continues to increase from -180 onwards, you would need to proeprly unfold the phase before any subsequent processing. Here we show the unfolding algorith presented in [Vulpiani et al., 2012].

We calculate a first guess \(K_{DP}\) from our \(\Phi_{DP}\) (kdp_from_phidp) and merge both together into one xarray.Dataset.

kdp_der = phi_raw.copy(deep=True).wrl.dp.kdp_from_phidp(winlen=5, method="finite_difference_vulpiani", skipna=False)
ds = xr.merge([phi_raw, kdp_der])
print(ds)
<xarray.Dataset> Size: 23MB
Dimensions:     (azimuth: 360, range: 1000)
Coordinates: (12/14)
  * azimuth     (azimuth) float64 3kB 0.0 1.0 2.0 3.0 ... 357.0 358.0 359.0
    elevation   (azimuth) float64 3kB 1.5 1.5 1.5 1.5 1.5 ... 1.5 1.5 1.5 1.5
  * range       (range) float64 8kB 0.0 150.2 300.3 ... 1.498e+05 1.5e+05
    x           (azimuth, range) float64 3MB 0.0 9.191e-15 ... -2.615e+03
    y           (azimuth, range) float64 3MB 0.0 150.1 ... 1.497e+05 1.498e+05
    z           (azimuth, range) float64 3MB 100.0 103.9 ... 5.341e+03 5.348e+03
    ...          ...
    bins        (azimuth, range) float64 3MB 0.0 150.2 ... 1.498e+05 1.5e+05
    sweep_mode  <U20 80B 'azimuth_surveillance'
    latitude    int64 8B 7
    longitude   int64 8B 50
    altitude    int64 8B 100
    crs_wkt     int64 8B 0
Data variables:
    PHIDP       (azimuth, range) float64 3MB 121.2 119.4 119.5 ... 125.9 125.1
    KDP         (azimuth, range) float64 3MB 0.0 0.8709 -0.06475 ... 0.09325 0.0

Then we apply the unfolding algorithm (unfold_phi_vulpiani). We additionally check over- and undercorrections.

phi_uf = ds.wrl.dp.unfold_phi_vulpiani(winlen=5, phidp="PHIDP", kdp="KDP")
phi_uf = xr.where(phi_uf >= 500, phi_uf - 360, phi_uf)
phi_uf = xr.where(phi_uf <= -150, phi_uf + 360, phi_uf)

plt.figure()
phi_uf.wrl.vis.plot()
plt.gca().set_title("$\Phi_{DP}$")
fig.tight_layout()
../../_images/3c59633b70ee9ec668f5409951c440cb5266038b714b2acdaa18e4df263ca00e.png
plt.figure(figsize=(12, 5))
phi_uf.sel(azimuth=slice(190., 200.)).plot.line(hue="azimuth")
plt.gca().set_title("$\Phi_{DP}$")
plt.tight_layout()
../../_images/b1b84735e71bc1b8b1b90f84add30ad32c9dda16cb15c33b3fd349454fd66ed8.png
plt.figure(figsize=(12, 5))
phi_raw.sel(azimuth=az, range=r).plot.line(label="phi_raw")
phi_uf.sel(azimuth=az, range=r).plot.line(label="phi_uf")
plt.gca().set_title("$\Phi_{DP}$")
plt.legend(loc="lower left")
fig.tight_layout()
../../_images/8fbffb9bfba7dff561111969f49adb53af1931211ff84c93540c3917e83b1637.png

Derive \(K_{DP}\) from \(\Phi_{DP}\)#

Simple derivation#

\(\omega radlib\) implements an optimized derivate algorithm which can be shaped for polar phase measurements in kdp_from_phidp.

Using the defaults the derivation algorithm uses low-noise Lanczos Differentiators [Diekema et al., 2012].

winlen should be set to a value where

kdp_der2 = phi_uf.copy(deep=True).wrl.dp.kdp_from_phidp(winlen=47,)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
phi_uf.wrl.vis.plot(ax=ax1)
ax1.set_title("$\Phi_{DP}$")
kdp_der2.wrl.vis.plot(ax=ax2, vmin=0, vmax=3)
ax2.set_title("$K_{DP}$")
fig.tight_layout()
../../_images/e0d78806a7289ab6b955e515b605689ebcc202e33bd0117e791a94a698b45f48.png

Vulpiani Algorithm#

Again in [Vulpiani et al., 2012] a full algorithm for derivation of \(K_{DP}\) from raw \(\Phi_{DP}\) as well as a preprocessed \(\Phi_{DP}\) is described (phidp_kdp_vulpiani).

phi_der, kdp_der3 = phi_uf.copy(deep=True).wrl.dp.phidp_kdp_vulpiani(winlen=47)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
phi_der.wrl.vis.plot(ax=ax1)
ax1.set_title("$\Phi_{DP}$")
kdp_der3.wrl.vis.plot(ax=ax2, vmin=0, vmax=3)
ax2.set_title("$K_{DP}$")
fig.tight_layout()
../../_images/9875b2a6ebc9f33aacc802f0decbfebd7927abf0208dfcb8ac5c1f678bf636f8.png

Comparison#

Let’s have a look at the different evolutions of \(\Phi_{DP}\) and \(K_{DP}\).

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10))

phi_noise.sel(azimuth=az, range=r).plot.line(ax=ax1, label="phi_noise")
phi_raw.sel(azimuth=az, range=r).plot.line(ax=ax1, label="phi_raw")
phi_uf.sel(azimuth=az, range=r).plot.line(ax=ax1, label="phi_uf")
phi_der.sel(azimuth=az, range=r).plot.line(ax=ax1, label="phi_der")
ax1.set_title("$\Phi_{DP}$")
ax1.legend(loc="lower left")

kdp_raw.sel(azimuth=az, range=r).plot.line(ax=ax2, label="kdp_raw")
kdp_syn.sel(azimuth=az, range=r).plot.line(ax=ax2, label="kdp_syn")
kdp_der2.sel(azimuth=az, range=r).plot.line(ax=ax2, label="kdp_der2")
kdp_der3.sel(azimuth=az, range=r).plot.line(ax=ax2, label="kdp_der3")
ax2.set_title("$K_{DP}$")
ax2.legend(loc="upper left")
fig.tight_layout()
../../_images/fa4d442163a4c274a93f585fdf4719eb671bda17b6c8ba57bd6c0a97eaecc706.png