Rain radar observables
This notebook turns single-particle T-matrices into simple rain radar and brightness-temperature observables. It is adapted from the radiative-transfer prototype contributed by @xiongyuup in PR #3.
The original contribution demonstrates a larger workflow with WRF input, hydrometeor microphysics, radar moments, and microwave radiative transfer. This example keeps only the self-contained core: water refractive index, rain-drop axis ratio, spheroidal T-matrices, a Marshall-Palmer-type drop-size distribution, and integrated dual-polarization moments.
begin
import Pkg
Pkg.activate(@__DIR__)
Pkg.develop(; path = dirname(@__DIR__))
Pkg.instantiate()
using TransitionMatrices, Plots
end
Water, ice, and drop models
The dielectric fits below are compact microwave models for liquid water and ice. Rain drops are represented as oblate spheroids with diameter \(D\) and axis ratio \(c/a\).
begin
c0_m_per_s() = 2.99792458e8
c0_mm_per_s() = 1000 * c0_m_per_s()
function water_refractive_index(freq_hz, T_celsius)
λ_m = c0_m_per_s() / freq_hz
ε0 = 8.854e-12
εs = 78.54 * (1 - 4.579e-3 * (T_celsius - 25) +
1.19e-5 * (T_celsius - 25)^2 -
2.8e-8 * (T_celsius - 25)^3)
ε∞ = 5.27137 + 2.16474e-2 * T_celsius - 1.31198e-3 * T_celsius^2
α = -16.8129 / (T_celsius + 273) + 6.09265e-2
λs = 3.3836e-6 * exp(2513.98 / (T_celsius + 273))
σ = 1.1117e-4
x = (λs / λ_m)^(1 - α)
denominator = 1 + 2x * sinpi(α / 2) + (λs / λ_m)^(2 - 2α)
εreal = ε∞ + (εs - ε∞) * (1 + x * sinpi(α / 2)) / denominator
εimag = (εs - ε∞) * x * cospi(α / 2) / denominator +
σ * λ_m / (2π * c0_m_per_s() * ε0)
sqrt(complex(εreal, εimag))
end
function ice_refractive_index(freq_hz, T_celsius)
λ_m = c0_m_per_s() / freq_hz
ε0 = 8.854e-12
εs = 203.168 + 2.5 * T_celsius + 0.15T_celsius^2
ε∞ = 3.168
α = 0.288 + 5.2e-3 * T_celsius + 2.3e-4 * T_celsius^2
λs = 9.990288e-6 * exp(6643.5 / (T_celsius + 273))
σ = 1.1146e-13 * exp(-6291.2 / (T_celsius + 273))
x = (λs / λ_m)^(1 - α)
denominator = 1 + 2x * sinpi(α / 2) + (λs / λ_m)^(2 - 2α)
εreal = ε∞ + (εs - ε∞) * (1 + x * sinpi(α / 2)) / denominator
εimag = (εs - ε∞) * x * cospi(α / 2) / denominator +
σ * λ_m / (2π * c0_m_per_s() * ε0)
sqrt(complex(εreal, εimag))
end
function rain_axis_ratio(D_mm)
D_mm < 0.7 && return 1.0
D_mm < 1.5 && return 1.173 - 0.5265D_mm + 0.4698D_mm^2 -
0.1317D_mm^3 - 8.5e-3D_mm^4
1.065 - 6.25e-2D_mm - 3.99e-3D_mm^2 +
7.66e-4D_mm^3 - 4.095e-5D_mm^4
end
end
rain_axis_ratio (generic function with 1 method)
begin
round_complex(z; digits = 4) =
complex(round(real(z); digits), round(imag(z); digits))
material_summary = [
(; material = "water", freq_GHz = 9.375, T = 0.0,
m = round_complex(water_refractive_index(9.375e9, 0.0))),
(; material = "ice", freq_GHz = 9.375, T = -10.0,
m = round_complex(ice_refractive_index(9.375e9, -10.0)))
]
end
2-element Vector{@NamedTuple{material::String, freq_GHz::Float64, T::Float64, m::ComplexF64}}:
(material = "water", freq_GHz = 9.375, T = 0.0, m = 7.252 + 2.8558im)
(material = "ice", freq_GHz = 9.375, T = -10.0, m = 1.7799 + 0.0001im)
Single-particle scattering table
Each diameter is solved as a spheroid with the IITM backend. The backward amplitudes feed reflectivity and differential reflectivity; the forward amplitudes feed differential phase; extinction feeds the brightness-temperature toy calculation.
begin
function scattering_table(Ds, freq_hz; temperature_celsius = 0.0,
solver = IITM(6, 10, 16))
λ = c0_mm_per_s() / freq_hz
m = water_refractive_index(freq_hz, temperature_celsius)
map(Ds) do D
a = D / 2
axis_ratio = rain_axis_ratio(D)
c = a * axis_ratio
shape = Spheroid{Float64, ComplexF64}(a, c, ComplexF64(m))
T = transition_matrix(shape, λ, solver)
Sback = amplitude_matrix(T, π / 2, 0.0, π / 2, π; λ)
Sfwd = amplitude_matrix(T, π / 2, 0.0, π / 2, 0.0; λ)
(; D_mm = D,
axis_ratio,
Shh_back = Sback[2, 2],
Svv_back = Sback[1, 1],
Shh_forward = Sfwd[2, 2],
Svv_forward = Sfwd[1, 1],
Cext_mm2 = calc_Cext(T, λ))
end
end
spacing(table) = length(table) > 1 ? table[2].D_mm - table[1].D_mm : 1.0
end
spacing (generic function with 1 method)
begin
freq_hz = 9.375e9
temperature_celsius = 0.0
Ds = collect(0.75:0.75:3.75)
solver = IITM(6, 10, 16)
table = scattering_table(Ds, freq_hz; temperature_celsius, solver)
table_summary = [
(; D = row.D_mm,
axis_ratio = round(row.axis_ratio; digits = 3),
Cext = round(row.Cext_mm2; digits = 4))
for row in table
]
end
5-element Vector{@NamedTuple{D::Float64, axis_ratio::Float64, Cext::Float64}}:
(D = 0.75, axis_ratio = 0.984, Cext = 0.0052)
(D = 1.5, axis_ratio = 0.965, Cext = 0.0705)
(D = 2.25, axis_ratio = 0.912, Cext = 0.4337)
(D = 3.0, axis_ratio = 0.859, Cext = 1.8037)
(D = 3.75, axis_ratio = 0.807, Cext = 5.3644)
table_summary
5-element Vector{@NamedTuple{D::Float64, axis_ratio::Float64, Cext::Float64}}:
(D = 0.75, axis_ratio = 0.984, Cext = 0.0052)
(D = 1.5, axis_ratio = 0.965, Cext = 0.0705)
(D = 2.25, axis_ratio = 0.912, Cext = 0.4337)
(D = 3.0, axis_ratio = 0.859, Cext = 1.8037)
(D = 3.75, axis_ratio = 0.807, Cext = 5.3644)
Bulk rain observables
For compactness this uses \(N(D, R) = N_0 \exp[-\Lambda(R)D]\) with \(D\) in millimeters. The constants are chosen to produce a plausible monotonic example, not a complete retrieval algorithm.
begin
drop_distribution(D_mm, rain_rate_mm_h) =
rain_rate_mm_h == 0 ? 0.0 : 8.0e3 * exp(-4.1 * rain_rate_mm_h^(-0.21) * D_mm)
function dualpol_moments(table, freq_hz, rain_rate_mm_h;
temperature_celsius = 0.0)
λ = c0_mm_per_s() / freq_hz
ΔD = spacing(table)
m = water_refractive_index(freq_hz, temperature_celsius)
Kw2 = abs2((m^2 - 1) / (m^2 + 2))
weights = [
drop_distribution(row.D_mm, rain_rate_mm_h) * ΔD
for row in table
]
hh = sum(abs2(row.Shh_back) * weights[i] for (i, row) in pairs(table))
vv = sum(abs2(row.Svv_back) * weights[i] for (i, row) in pairs(table))
Zhh = hh * λ^4 / (π^5 * Kw2)
Zvv = vv * λ^4 / (π^5 * Kw2)
cov = sum(conj(row.Shh_back) * row.Svv_back * weights[i]
for (i, row) in pairs(table))
ρhv = abs(cov) / sqrt(hh * vv)
kdp = 180 / π * 1e-3 * λ *
sum(real(row.Shh_forward - row.Svv_forward) * weights[i]
for (i, row) in pairs(table))
(; R = rain_rate_mm_h,
ZH = round(10log10(Zhh); digits = 2),
ZDR = round(10log10(Zhh / Zvv); digits = 2),
ρhv = round(ρhv; digits = 4),
KDP = round(kdp; digits = 4))
end
function brightness_temperature(table, rain_rate_mm_h;
path_km = 5.0, Tatm_K = 270.0)
ΔD = spacing(table)
α = sum(drop_distribution(row.D_mm, rain_rate_mm_h) *
row.Cext_mm2 * ΔD for row in table) / 1e6
τ = α * path_km
Tatm_K * (1 - exp(-τ))
end
end
brightness_temperature (generic function with 1 method)
begin
rain_rates = [1.0, 10.0, 50.0]
radar = [
dualpol_moments(table, freq_hz, R; temperature_celsius)
for R in rain_rates
]
tb = [
(; R, TB = round(brightness_temperature(table, R); digits = 4))
for R in rain_rates
]
(; radar, tb)
end
(radar = [(R = 1.0, ZH = 13.23, ZDR = 0.56, ρhv = 0.9987, KDP = 0.0412), (R = 10.0, ZH = 27.33, ZDR = 1.23, ρhv = 0.9963, KDP = 0.5779), (R = 50.0, ZH = 36.03, ZDR = 1.66, ρhv = 0.9962, KDP = 3.1766)], tb = [(R = 1.0, TB = 0.0036), (R = 10.0, TB = 0.0418), (R = 50.0, TB = 0.2254)])
Multi-frequency brightness-temperature lookup
The larger PR #3 prototype writes lookup tables to MAT files. Here the same idea stays in memory: build one scattering table per frequency, integrate over the rain distribution, and return a compact table that Pluto renders directly.
begin
function brightness_temperature_lut(Ds, frequencies_hz, rain_rates;
temperature_celsius = 0.0, solver = IITM(6, 10, 16),
path_km = 5.0, Tatm_K = 270.0)
rows = NamedTuple[]
for f in frequencies_hz
local_table = scattering_table(Ds, f; temperature_celsius, solver)
for R in rain_rates
push!(rows, (; freq_GHz = round(f / 1e9; digits = 3), R,
TB = round(brightness_temperature(local_table, R;
path_km, Tatm_K); digits = 4)))
end
end
rows
end
lookup_frequencies = [10e9, 18e9]
tb_lut = brightness_temperature_lut(Ds, lookup_frequencies, rain_rates;
temperature_celsius, solver)
end
6-element Vector{NamedTuple}:
(freq_GHz = 10.0, R = 1.0, TB = 0.0042)
(freq_GHz = 10.0, R = 10.0, TB = 0.0496)
(freq_GHz = 10.0, R = 50.0, TB = 0.2675)
(freq_GHz = 18.0, R = 1.0, TB = 0.0167)
(freq_GHz = 18.0, R = 10.0, TB = 0.1989)
(freq_GHz = 18.0, R = 50.0, TB = 0.9967)
tb_lut
6-element Vector{NamedTuple}:
(freq_GHz = 10.0, R = 1.0, TB = 0.0042)
(freq_GHz = 10.0, R = 10.0, TB = 0.0496)
(freq_GHz = 10.0, R = 50.0, TB = 0.2675)
(freq_GHz = 18.0, R = 1.0, TB = 0.0167)
(freq_GHz = 18.0, R = 10.0, TB = 0.1989)
(freq_GHz = 18.0, R = 50.0, TB = 0.9967)
Plots
let
p1 = plot(getfield.(table_summary, :D), getfield.(table_summary, :Cext);
marker = :circle, lw = 2, label = "Cext",
xlabel = "drop diameter D (mm)", ylabel = "extinction cross section (mm²)")
p2 = plot(getfield.(radar, :R), getfield.(radar, :ZH);
marker = :circle, lw = 2, xscale = :log10, label = "ZH",
xlabel = "rain rate (mm h⁻¹)", ylabel = "reflectivity (dBZ)")
p3 = plot(getfield.(radar, :R), getfield.(radar, :ZDR);
marker = :circle, lw = 2, xscale = :log10, label = "ZDR",
xlabel = "rain rate (mm h⁻¹)", ylabel = "differential reflectivity (dB)")
p4 = plot(getfield.(tb, :R), getfield.(tb, :TB);
marker = :circle, lw = 2, xscale = :log10, label = "TB",
xlabel = "rain rate (mm h⁻¹)", ylabel = "brightness temperature (K)")
plot(p1, p2, p3, p4; layout = (2, 2), size = (820, 620))
end
let
p = plot(; xscale = :log10, xlabel = "rain rate (mm h⁻¹)",
ylabel = "brightness temperature (K)", legend = :topleft)
for f in getfield.(tb_lut, :freq_GHz) |> unique
subset = filter(row -> row.freq_GHz == f, tb_lut)
plot!(p, getfield.(subset, :R), getfield.(subset, :TB);
marker = :circle, lw = 2, label = "$(f) GHz")
end
p
end
References
H. Li, Y. Xiong, and Y. Chen, "Simulation of Complex Meteorological Target Echoes for Airborne Dual-Polarization Weather Radar Based on Invariant Imbedding T-Matrix," IEEE Transactions on Geoscience and Remote Sensing, 62, 5105817, 2024.
G. Zhang, Weather Radar Polarimetry, China Meteorological Press, 2018, pp. 39-40.
B. R. Brown, M. M. Bell, and A. J. Frambach, "Validation of Simulated Hurricane Drop Size Distributions Using Polarimetric Radar," Geophysical Research Letters, 42, 2016.