首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >雷达系列 | wradlib 多雷达组网实战

雷达系列 | wradlib 多雷达组网实战

作者头像
用户11172986
发布2026-05-13 17:27:20
发布2026-05-13 17:27:20
410
举报
文章被收录于专栏:气python风雨气python风雨

雷达系列 | wradlib 多雷达组网实战

说起来五一之前发的雷达组网教程被pycinrad的开发人员吐槽说像是五一的加班通知,唉,气python风雨太坏了

上一篇用 Py-ART 的 grid_from_radars 一行命令把三部菲律宾雷达拼了出来。好用归好用,但它把网格化、权重、合并这三步都封在了内部,想自己改插值算法、想换组合策略,就得改 Py-ART 的源码。

wradlib 走的是另一条路。它把整个组网拆成 网格化插值多雷达合并 两个独立动作,每一步清晰可见,可以自由替换。代价是比 Py-ART 多写几十行代码,但换来的是从插值核(IDW、最近邻、自定义)到合并策略(Knockout、Weighted)的完全控制。

本篇专注 wradlib 这条链路,配合 xradar 做 CF/Radial 读取。Py-ART 那条更省事的链路放在另一份单独的教程里,两份内容互不依赖,可独立运行。

本教材基于 wradlib 1.8.2 与 xradar 0.5+ 版本,wradlib 1.8 的 compose_ko / compose_weighted 是通过 xarray accessor 调用的(da.wrl.comp.xxx),1.6 之前的版本接口不一样,遇到 AttributeError 先确认版本号。

阅读路径

  1. 数据介绍
  2. wradlib comp 模块速览
  3. 用 xradar 读取 CF/Radial 并构造全局坐标 DataArray
  4. pulse_volume 质量因子 + togrid 插值
  5. compose_ko 与 compose_weighted 合并
  6. Cartopy 双面板对比绘图与附录
代码语言:javascript
复制
!pip install wradlib xradar --upgrade -i https://pypi.mirrors.ustc.edu.cn/simple/

1. 数据介绍

本教程使用 3 部菲律宾地区的雷达 CAPPI 数据,文件格式为 CF/Radial NetCDF,和 Py-ART 篇是同一份数据。

雷达站

文件名

纬度

经度

数据形状 (azimuth, range)

SBAG (Baguio)

20251103051105_SBAG_CAPPI_2km.nc

16.356°N

120.559°E

(360, 480)

SBAL (Baler)

20251103051001_SBAL_CAPPI_2km.nc

15.749°N

121.632°E

(360, 480)

SDAET (Daet)

20251103051038_SDAET_CAPPI_2km.nc

14.129°N

122.983°E

(360, 960)

CAPPI(Constant Altitude Plan Position Indicator)即等高度位显示产品,本数据为 2 km 高度的 CAPPI 反射率因子。

数据中的反射率场名称为 Reflectivity_CAPPI_2km,单位 dBZ。

wradlib 的组网思路是把每部雷达单独插值到一张公共笛卡尔网格上,再用合并函数按质量决定每个像素该取谁的数。公共网格的坐标系用 UTM Zone 51N(EPSG:32651)因为它正好覆盖菲律宾,且单位是米,距离计算不用做球面修正。

代码语言:javascript
复制
# 导入常用库
import numpy as np
import xarray as xr
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import wradlib as wrl
import xradar as xd
import pyproj
import cmaps
import warnings
warnings.filterwarnings('ignore')

import cartopy.crs as ccrs
import cartopy.feature as cfeature

file_baguio = '20251103051105_SBAG_CAPPI_2km.nc'
file_baler  = '20251103051001_SBAL_CAPPI_2km.nc'
file_daet   = '20251103051038_SDAET_CAPPI_2km.nc'

print('依赖检测完成')

2. wradlib comp 模块速览

wradlib.comp(Composition,组合)模块提供以下核心函数。wradlib 1.8 之后这些函数都做了 xarray accessor 封装,可以直接通过 da.wrl.comp.xxx 调用。

函数

功能

wradlib.comp.togrid

把单部雷达极坐标数据插值到公共笛卡尔网格

wradlib.comp.compose_ko

Knockout 组合,按质量信息选择质量最高的雷达像素

wradlib.comp.compose_weighted

加权组合,按质量权重对所有雷达像素加权平均

wradlib.comp.extract_circle

提取位于指定圆内的坐标索引

wradlib.qual.pulse_volume

计算每个采样体的脉冲体积,作为质量因子

整个 wradlib 组网流程拆成五步。

  1. 用 xradar 读 CF/Radial,得到带本地 (x, y) 坐标的极坐标 DataArray
  2. 把每部雷达的本地坐标加上雷达站 UTM 坐标,转成全局笛卡尔坐标
  3. pulse_volume 作为质量因子,同时用 togrid 把数据和质量都插值到公共网格
  4. compose_kocompose_weighted 合并
  5. 把 UTM 网格逆投影回经纬度,交给 Cartopy 绘图

看起来步骤多,但每一步都是一个独立的 xarray 操作,跑起来很顺。

3. 读取与全局坐标构造

xradar 是 Open Radar 工作组维护的雷达 IO 库,把 CF/Radial、ODIM_H5、Sigmet/IRIS 等格式都读成统一的 xradar.io.RadarDatatree 对象。调一次 get_x_y_z_tree 就能拿到每个 sweep 在本地切平面上的 x, y, z 坐标。

本地坐标的原点是雷达站本身,把它加上雷达站在 UTM Zone 51N 下的坐标,就得到三部雷达共用的全局笛卡尔坐标。这一步是 wradlib 多雷达拼接的关键。

代码语言:javascript
复制
# 步骤 1,构造 UTM 投影器
proj_utm = pyproj.CRS.from_epsg(32651)                                          # UTM Zone 51N
transformer = pyproj.Transformer.from_crs('EPSG:4326', proj_utm, always_xy=True)

# 步骤 2,逐个雷达读取 + 构造带全局 UTM 坐标的 DataArray
radar_datasets = {}
for filepath, name in [(file_baguio, 'SBAG'), (file_baler, 'SBAL'), (file_daet, 'SDAET')]:
    dtree = xd.io.open_cfradial1_datatree(filepath)
    dtree_geo = xd.georeference.get_x_y_z_tree(dtree)
    sweep = dtree_geo['/sweep_0']

    # 本地切平面坐标,原点在雷达站
    x_local = sweep['x'].values
    y_local = sweep['y'].values
    refl = sweep['Reflectivity_CAPPI_2km']

    # 雷达站经纬度 -> UTM
    radar_lon = float(sweep.coords['longitude'].values)
    radar_lat = float(sweep.coords['latitude'].values)
    radar_x, radar_y = transformer.transform(radar_lon, radar_lat)

    # 创建带全局 UTM 坐标的 DataArray
    da = xr.DataArray(
        refl.values,
        dims=['azimuth', 'range'],
        coords={
            'x': (['azimuth', 'range'], x_local + radar_x),
            'y': (['azimuth', 'range'], y_local + radar_y),
        },
        name='Reflectivity',
    )
    # 过滤 fill_value(原始数据里超大值都是无效)
    da = da.where(da < 1000)

    radar_datasets[name] = {
        'da': da,
        'site': (radar_x, radar_y),
        'site_lonlat': (radar_lon, radar_lat),
    }
    print(f'{name}, x={float(da.x.min().values):.0f}~{float(da.x.max().values):.0f}, '
          f'y={float(da.y.min().values):.0f}~{float(da.y.max().values):.0f}')

# 步骤 3,从所有雷达数据的 bbox 推导公共网格范围
xmin = min([rd['da'].x.min().values for rd in radar_datasets.values()])
xmax = max([rd['da'].x.max().values for rd in radar_datasets.values()])
ymin = min([rd['da'].y.min().values for rd in radar_datasets.values()])
ymax = max([rd['da'].y.max().values for rd in radar_datasets.values()])
print(f'数据 bbox, x={xmin:.0f}~{xmax:.0f}, y={ymin:.0f}~{ymax:.0f}')

# 目标网格 300x300,覆盖全部数据
x = np.linspace(xmin, xmax, 300)
y = np.linspace(ymin, ymax, 300)
cart = xr.Dataset(coords={'x': (['x'], x), 'y': (['y'], y)})
print(f'公共网格, {len(x)} x {len(y)}')

# 顺手算一下经纬度网格,绘图时直接用
xx_grid, yy_grid = np.meshgrid(x, y)
transformer_inv = pyproj.Transformer.from_crs(proj_utm, 'EPSG:4326', always_xy=True)
lons_grid, lats_grid = transformer_inv.transform(xx_grid, yy_grid)

4. pulse_volume 质量因子 + togrid 插值

wradlib 的合并需要两类输入。一类是 数据网格,每部雷达插值到公共网格上的反射率;另一类是 质量网格,告诉合并函数每个像素「我有多可信」。

本例用 pulse_volume 做质量因子。它的物理意义是单个采样体的体积,采样体积越小,意味着空间分辨率越高、可信度越高,所以最终的质量值取倒数。

插值核选 IDW(反距离加权)。togrid 还需要每部雷达的探测范围 radius 和中心点 center,这两个参数决定了插值时考虑哪些像素。

代码语言:javascript
复制
# pulse_volume 参数
#   range_resolution    距离分辨率(米),CF/Radial 元数据里读出来一般是 250~1000 m
#   beamwidth           波束宽度(度),常见 S 波段雷达约 1.0°
RANGE_RES = 504.0
BEAM_WIDTH = 1.0

radar_grids = []
quality_grids = []

for name, rd in radar_datasets.items():
    da = rd['da']
    site = rd['site']

    # 采样体积作为质量因子(体积越小越可靠)
    pv = da.wrl.qual.pulse_volume(RANGE_RES, BEAM_WIDTH)

    # 探测半径 = 距雷达站最远像素的距离
    max_range = float(np.nanmax(np.sqrt((da.x - site[0]) ** 2 + (da.y - site[1]) ** 2)))
    # 用数据中心做插值参考中心,注意 togrid 期望 (y, x) 顺序
    center = (float(np.nanmean(da.y.values)), float(np.nanmean(da.x.values)))

    # 插值到公共网格(IDW)
    g = da.wrl.comp.togrid(cart, radius=max_range, center=center, interpol=wrl.ipol.Idw)
    q = pv.wrl.comp.togrid(cart, radius=max_range, center=center, interpol=wrl.ipol.Idw)

    radar_grids.append(g)
    quality_grids.append(1.0 / (q + 0.001))                         # 体积越小,质量越高

    print(f'{name}, gridded={float(np.nanmin(g.values)):.2f}~{float(np.nanmax(g.values)):.2f} dBZ')

print('所有雷达网格化完成')

5. compose_ko 与 compose_weighted 合并

把每部雷达的网格沿一个新的 radar 维度堆起来,得到一个 (radar, y, x) 形状的 xarray.DataArray,质量网格同理。然后调用合并函数。

  • Knockout 在每个像素上选质量最高的那部雷达的值,结果保留各雷达的原始反射率,回波边缘干净,但相邻雷达交界处可能出现「拼接缝」
  • Weighted 按质量权重做加权平均,过渡更平滑,但也会把单点强值「平掉」一些

实际工程里一般 KO 用得更多,因为它保留了强对流的真实结构,Weighted 更适合做长期统计或者后续平滑场景。

代码语言:javascript
复制
# 沿新维度 'radar' 堆叠
radar = xr.DataArray(list(radar_datasets.keys()), dims='radar')
radargrids = xr.concat(radar_grids, dim=radar)
qualitygrids = xr.concat(quality_grids, dim=radar)

# Knockout 组合
composite_ko = radargrids.wrl.comp.compose_ko(qualitygrids)
# 加权组合
composite_weighted = radargrids.wrl.comp.compose_weighted(qualitygrids)

print(f'Knockout 组合范围, {float(np.nanmin(composite_ko.values)):.2f} ~ {float(np.nanmax(composite_ko.values)):.2f} dBZ')
print(f'加权组合范围,   {float(np.nanmin(composite_weighted.values)):.2f} ~ {float(np.nanmax(composite_weighted.values)):.2f} dBZ')

6. Cartopy 双面板对比绘图

把两种合并策略的结果画在一张图上做并排对比。前面已经把 UTM 网格 (xx_grid, yy_grid) 通过 transformer_inv 转回了经纬度网格 (lons_grid, lats_grid),直接喂给 ax.contourf 即可。

雷达站位置用 wradlib 阶段记下的 site_lonlat 标注,整张图完全不依赖 Py-ART。

代码语言:javascript
复制
# 步骤 5,将 UTM 网格转换回经纬度并绘图
cmap = cmaps.radar_1
level = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60]

plt.rcParams['font.family'] = 'serif'
plt.rcParams['font.serif'] = ['Times New Roman', 'DejaVu Serif']
plt.rcParams['mathtext.fontset'] = 'stix'
plt.rcParams['font.size'] = 11
plt.rcParams['axes.labelsize'] = 12
plt.rcParams['axes.titlesize'] = 13

fig, axes = plt.subplots(
    1, 2, figsize=(16, 7), dpi=300,
    subplot_kw={'projection': ccrs.PlateCarree()},
)

for idx, (ax, comp_data_raw, title) in enumerate(zip(
    axes,
    [composite_ko, composite_weighted],
    ['(a) Knockout composition', '(b) Weighted composition'],
)):
    ax.set_extent([120, 126, 12, 16.5], crs=ccrs.PlateCarree())

    # 把无效值(<=0)置 nan,避免显示为黑底
    comp_data = np.where(comp_data_raw.values > 0, comp_data_raw.values, np.nan)

    mesh = ax.contourf(
        lons_grid, lats_grid, comp_data,
        transform=ccrs.PlateCarree(), cmap=cmap, levels=level,
    )
    ax.coastlines(resolution='10m', linewidth=0.6, color='dimgray')

    gl = ax.gridlines(
        draw_labels=True,
        linewidth=0.3,
        color='gray',
        alpha=0.4,
        linestyle='--',
        xlocs=range(116, 129, 2),
        ylocs=range(12, 21, 2),
    )
    gl.top_labels = False
    gl.right_labels = False
    gl.xlabel_style = {'size': 10, 'color': 'black'}
    gl.ylabel_style = {'size': 10, 'color': 'black'}
    if idx == 1:                        # 右图不重复 y 轴标签
        gl.left_labels = False

    # 雷达站位置直接用 radar_datasets 里记录的经纬度
    for r_name, rd in radar_datasets.items():
        lon, lat = rd['site_lonlat']
        ax.plot(lon, lat, marker='o', markersize=5, color='black',
                markerfacecolor='white', markeredgewidth=1.0,
                transform=ccrs.PlateCarree(), zorder=10)
        ax.text(lon + 0.15, lat + 0.15, r_name, fontsize=8, color='black',
                fontweight='bold', transform=ccrs.PlateCarree(),
                bbox=dict(boxstyle='round,pad=0.15', facecolor='white',
                          alpha=0.7, edgecolor='none'))

    ax.set_title(title, fontsize=12, fontweight='bold', pad=8)

# 共享色条,放在两个子图下方中央
cbar_ax = fig.add_axes([0.35, 0.06, 0.3, 0.025])
cbar = fig.colorbar(mesh, cax=cbar_ax, orientation='horizontal', ticks=level)
cbar.set_label('Reflectivity (dBZ)', fontsize=11)
cbar.ax.tick_params(labelsize=9)

fig.suptitle('wradlib Radar Reflectivity Mosaic (2 km CAPPI)',
             fontsize=13, fontweight='bold', y=0.98)

fig.subplots_adjust(left=0.06, right=0.94, bottom=0.14, top=0.90, wspace=0.12)
plt.savefig('radar_mosaic_wradlib_cartopy.png', dpi=300, bbox_inches='tight', pad_inches=0.1)
plt.show()
print('图片已保存, radar_mosaic_wradlib_cartopy.png (300 dpi)')
image
image

image

附录,常见问题

Q1,togrid 跑得很慢

默认的 IDW 核会对网格中每个点扫描全部邻居像素。三部雷达 × 300×300 的网格 × 几万个原始像素,慢是正常的。如果只需要快速看效果,把 interpol 换成 wrl.ipol.Nearest,速度能提升一个数量级,代价是边界更糙。

Q2,质量因子怎么选

本例用 pulse_volume 是最简单的选择,本质上是 离雷达越远,采样体积越大,越不可靠。对距离敏感的应用够用了。

更精细的做法是把 SNR、波束阻挡率(wradlib.qual.beam_height_n 配合地形 DEM)也加进来做综合质量评分。wradlib 的 qual 模块里都有现成函数。

Q3,Knockout 和 Weighted 选哪个

我自己默认用 KO。它保留了原始反射率分布,强对流核心不会被周边雷达「稀释」。Weighted 更适合做长时间序列的均值场,或者下游接着上模型同化、平滑滤波这些场景。

Q4,能直接处理中国 CINRAD 数据吗

wradlib 本身对 CINRAD SA/SB 二进制格式没有原生支持,但对 ODIM_H5IRIS/Sigmet 支持很好。实际工作里推荐先用 pycwr 或者pycinrad 把 SA/SB 转成 ODIM_H5 或 CF/Radial,然后 xradar 读取、wradlib 处理,全程一气呵成。

参考资料

  • wradlib 官方文档, https://docs.wradlib.org/
  • wradlib.comp API, https://docs.wradlib.org/en/stable/comp.html
  • xradar 项目, https://docs.openradarscience.org/projects/xradar/
  • pyproj UTM 投影, https://pyproj4.github.io/pyproj/stable/
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 气python风雨 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 雷达系列 | wradlib 多雷达组网实战
    • 阅读路径
    • 1. 数据介绍
    • 2. wradlib comp 模块速览
    • 3. 读取与全局坐标构造
    • 4. pulse_volume 质量因子 + togrid 插值
    • 5. compose_ko 与 compose_weighted 合并
    • 6. Cartopy 双面板对比绘图
    • 附录,常见问题
      • Q1,togrid 跑得很慢
      • Q2,质量因子怎么选
      • Q3,Knockout 和 Weighted 选哪个
      • Q4,能直接处理中国 CINRAD 数据吗
    • 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档