pycwr is a Python toolkit for operational Chinese weather radar workflows.
It covers radar base-data reading, geometry, plotting, quality control,
hydrometeor classification, single-radar wind retrieval, multi-radar
compositing, and export.
1.0.81.0.8 continues the first stable release line intended to be usable for production-style
usage and GitHub distribution.
This refresh keeps the PA reader behavior aligned while improving the
maintainability of the PA decode path and preserving the plotting colormap
compatibility update in the public release line.
Highlights:
PRD data model for sweep inspection and downstream processingVAD, VVP, VWP, and gridded horizontal wind volumesread_auto and read_PA now correctly recognize and build supported PA filesplot_ppi, plot_ppi_map, and the legacy Graph / GraphMap plotting paths
now auto-register pycwr colormap names such as CN_ref and CN_velInstall from PyPI:
python -m pip install pycwr
Install the optional full stack from PyPI:
python -m pip install "pycwr[full]"
Base install:
python -m pip install -r requirements-core.txt
python -m pip install .
Full install:
python -m pip install -r requirements-full.txt
python -m pip install ".[full]"
Notes:
pycwr 1.0.8 requires Python >=3.9python -m pip install pycwr is the recommended path for normal usersPRD, geometry, interpolation, and
NetCDF-style exportarm_pyart and xradar currently require Python >=3.10, so on
Python 3.9 the full install still covers plotting, QC, and the web
viewer, but not those two optional interop stackspandas is pinned to <3 in 1.0.8 for release stabilityRebuild the extension after editing pycwr/core/RadarGridC.pyx:
python setup.py build_ext --inplace
Build release artifacts:
python -m build
Read one radar file and inspect the returned volume:
from pycwr.io import read_auto
radar = read_auto("Z_RADR_I_Z9046_20260317065928_O_DOR_SAD_CAP_FMT.bin.bz2")
print(radar.summary())
print(radar.available_fields())
print(radar.sweep_summary()[0])
Get one field from one sweep:
dBZ0 = radar.get_sweep_field(0, "dBZ")
velocity0 = radar.get_sweep_field(0, "V")
Plot a PPI:
from pycwr.draw import plot_ppi
plot_ppi(radar, field="dBZ", sweep=0, show=True)
Plot a mapped PPI:
from pycwr.draw import plot_ppi_map
plot_ppi_map(radar, field="dBZ", sweep=0, show=True)
Extract a vertical section:
from pycwr.draw import plot_section
plot_section(radar, start=(-50, 0), end=(50, 0), field="dBZ", show=True)
Generate a simple product:
import numpy as np
x = np.arange(-150_000.0, 150_001.0, 1_000.0)
y = np.arange(-150_000.0, 150_001.0, 1_000.0)
radar.add_product_CR_xy(x, y)
print(radar.product)
| Module | What it is for | Recommended starting points |
|---|---|---|
pycwr.io |
Read and write radar base data | read_auto, read_WSR98D, read_SAB, read_CC, read_SC, read_PA |
pycwr.core |
Central volume object, geometry, export helpers | PRD, radar.summary(), radar.get_sweep_field() |
pycwr.draw |
Plotting and quick-look figures | plot_ppi, plot_ppi_map, plot_rhi, plot_section, plot_vvp, plot_wind_profile |
pycwr.qc |
Dual-pol QC | apply_dualpol_qc, run_dualpol_qc |
pycwr.retrieve |
Hydrometeor and wind retrieval | classify_hydrometeors, retrieve_vad, retrieve_vvp, retrieve_vwp, retrieve_wind_volume_xy, retrieve_wind_volume_lonlat |
pycwr.interp |
Multi-radar compositing | run_radar_network_3d |
pycwr.GraphicalInterface |
Local web viewer | create_app, launch |
All readers return pycwr.core.NRadar.PRD.
The most important parts of PRD are:
fields: one xarray.Dataset per sweepscan_info: site and scan metadataextended_fields: native sidecar fields when aligned and native ranges differproduct: computed product datasetUseful inspection helpers:
summary(): compact full-volume summaryavailable_fields(sweep=None, range_mode="aligned")sweep_summary()get_sweep_field(sweep, field_name, range_mode="aligned", sort_by_azimuth=False)get_native_sweep_field(sweep, field_name)ordered_az(inplace=False)For some low sweeps, reflectivity may exist in two forms:
Use:
range_mode="aligned" for historical compatibilityrange_mode="native" when full low-level reflectivity coverage mattersExample:
aligned = radar.get_sweep_field(0, "dBZ", range_mode="aligned")
native = radar.get_sweep_field(0, "dBZ", range_mode="native")
Recommended public plotting APIs:
from pycwr.draw import (
plot,
plot_ppi,
plot_ppi_map,
plot_rhi,
plot_section,
plot_section_lonlat,
plot_vvp,
plot_wind_profile,
)
These functions return an EasyPlotResult with fig, ax, and artist.
pycwr.draw chooses a default colormap from the plotted field when you leave
cmap=None.
dBZ uses the reflectivity palette such as CN_refV uses the velocity palette such as CN_velHCL uses a discrete hydrometeor classification paletteviridisThis default strategy is shared by plot_ppi, plot_ppi_map, plot_rhi,
plot_section, and the legacy Graph / GraphMap classes.
Recommended usage:
from pycwr.draw import plot_ppi, plot_ppi_map
plot_ppi(radar, field="dBZ", sweep=0, show=True)
plot_ppi_map(radar, field="dBZ", sweep=0, show=True)
Explicit colormap override:
plot_ppi(radar, field="dBZ", sweep=0, cmap="CN_ref", cmap_bins=16, show=True)
plot_ppi_map(radar, field="V", sweep=0, cmap="CN_vel", min_max=(-27.0, 27.0), show=True)
Legacy class-style usage is also supported:
from pycwr.draw.RadarPlot import GraphMap
from cartopy import crs as ccrs
display = GraphMap(radar, ccrs.PlateCarree())
display.plot_ppi_map(ax, 0, "dBZ", cmap="CN_ref")
Notes:
CN_ref, CN_vel, and the other pycwr colormap names are registered
automatically when plotting starts. You no longer need to "warm up" the
plotting module first.dBZ but not V.pycwr.draw.colormap.Common PRD product methods:
add_product_CR_xyadd_product_CAPPI_xyadd_product_CAPPI_3d_xyadd_product_VIL_xyadd_product_ET_xyadd_product_CR_lonlatadd_product_CAPPI_lonlatadd_product_VIL_lonlatadd_product_ET_lonlatadd_product_VWPExample:
radar.add_product_CAPPI_xy(x, y, 3000.0)
radar.add_product_VIL_xy(x, y, [1000.0, 2000.0, 3000.0])
from pycwr.qc import apply_dualpol_qc
qc_radar = apply_dualpol_qc(radar, inplace=False, clear_air_mode="mask")
Common corrected fields include Zc, ZDRc, PhiDPc, KDPc, and mask fields
such as QC_MASK and CLEAR_AIR_MASK when enabled.
hcl_radar = radar.classify_hydrometeors(
inplace=False,
band="C",
profile_height=[0.0, 2000.0, 4000.0, 8000.0],
profile_temperature=[24.0, 12.0, 2.0, -16.0],
confidence_field="HCL_CONF",
)
You can also classify directly from arrays with
pycwr.retrieve.classify_hydrometeors(...).
pycwr ships four single-radar wind workflows:
retrieve_vad: ring-wise harmonic fit on one or more sweepsretrieve_vvp: local least-squares horizontal wind retrieval on one sweepretrieve_vwp: vertical wind profile built from multiple VAD layersretrieve_wind_volume_xy / retrieve_wind_volume_lonlat: fixed-height gridded horizontal wind volume built from multiple VVP sweepsQuick example:
vad = radar.retrieve_vad(sweeps=[0, 1, 2], max_range_km=40.0, gate_step=4)
vvp = radar.retrieve_vvp(0, max_range_km=20.0, az_num=91, bin_num=5)
vwp = radar.retrieve_vwp(sweeps=[0, 1, 2], max_range_km=40.0, height_step=500.0)
wind = radar.retrieve_wind_volume_xy(
XRange=np.arange(-20_000.0, 20_001.0, 10_000.0),
YRange=np.arange(-20_000.0, 20_001.0, 10_000.0),
level_heights=np.array([500.0, 1000.0, 1500.0]),
sweeps=[0, 1, 2],
max_range_km=30.0,
)
The stored profile product path is:
radar.add_product_VWP(sweeps=[0, 1, 2], max_range_km=40.0, height_step=500.0)
Store the gridded horizontal wind volume in radar.product:
radar.add_product_WIND_VOLUME_xy(
XRange=np.arange(-20_000.0, 20_001.0, 10_000.0),
YRange=np.arange(-20_000.0, 20_001.0, 10_000.0),
level_heights=np.array([500.0, 1000.0, 1500.0]),
sweeps=[0, 1, 2],
)
retrieve_wind_volume_xy and retrieve_wind_volume_lonlat return a single-radar
fixed-height horizontal wind volume.
Treat it as:
u/v wind on multiple height levelsDo not treat it as:
u/v/w retrievalThe operational logic is:
pycwr selects the velocity field, preferring
corrected velocity when available.sweeps=None or sweeps="auto", the
strongest sweeps are selected automatically.This makes the product more stable on real radar volumes with missing velocity, weak echo regions, or sweep-to-sweep range differences.
Use:
radar.retrieve_wind_volume_xy(...) for Cartesian x/y/zradar.retrieve_wind_volume_lonlat(...) for regular lon/lat gridsradar.add_product_WIND_VOLUME_xy(...) or
radar.add_product_WIND_VOLUME_lonlat(...) to store the result in
radar.productRecommended defaults for production-style usage:
sweeps=None to enable auto sweep selectionsweeps="all" only when you explicitly want every available sweepmax_range_km instead of using the whole radar volumeworkers on multi-core machines when the target grid is not tinyExample with explicit operational settings:
wind = radar.retrieve_wind_volume_xy(
XRange=np.arange(-60_000.0, 60_001.0, 5_000.0),
YRange=np.arange(-60_000.0, 60_001.0, 5_000.0),
level_heights=np.arange(500.0, 3_500.0, 500.0),
sweeps=None,
max_range_km=60.0,
az_num=31,
bin_num=5,
azimuth_step=12,
range_step=6,
horizontal_radius_m=8_000.0,
max_horizontal_radius_m=14_000.0,
horizontal_min_neighbors=3,
vertical_tolerance_m=400.0,
max_vertical_gap_m=1_200.0,
workers=4,
)
XRange, YRange, level_heights: target output grid in metersXLon, YLat: target output grid in degrees for lon/lat modesweeps: None or "auto" enables auto selection; "all" keeps all
sweeps; a list such as [0, 1, 2] forces explicit sweepsmax_range_km: strongest first-order control for retrieval stabilityaz_num, bin_num: local VVP window sizeazimuth_step, range_step: output thinning of the local sweep VVPhorizontal_radius_m: horizontal radius used when mapping sweep VVP samples
to the target gridmax_horizontal_radius_m: optional expanded search radius when local support
is sparsevertical_tolerance_m: local height matching tolerancemax_vertical_gap_m: maximum allowed vertical interpolation gapworkers: process-based parallelism for sweep-level and grid-level workPractical tuning rules:
az_num when velocity is sparse or noisybin_num modest; too small is noisy and too large oversmoo$ claude mcp add pycwr \
-- python -m otcore.mcp_server <graph>