Reproject Radar Coordinates#
This notebook demonstrates the target_crs option now exposed on
.xradar.georeference() for xarray.Dataset and xarray.DataTree.
It lets you reproject radar x, y coordinates from the radar-native
Azimuthal Equidistant (AEQD) projection into any pyproj-compatible CRS
in a single accessor call:
radar = radar.xradar.georeference(target_crs=4326)
Imports#
import cartopy.crs as ccrs
import cmweather # noqa
import matplotlib.pyplot as plt
import pyproj
from open_radar_data import DATASETS
import xradar as xd
Read a sample radar file#
filename = DATASETS.fetch("cfrad.20080604_002217_000_SPOL_v36_SUR.nc")
radar = xd.io.open_cfradial1_datatree(filename, first_dim="auto")
radar
<xarray.DataTree>
Group: /
│ Dimensions: (sweep: 9)
│ Coordinates:
│ latitude float64 8B ...
│ longitude float64 8B ...
│ altitude float64 8B ...
│ Dimensions without coordinates: sweep
│ Data variables:
│ volume_number int32 4B ...
│ platform_type |S32 32B ...
│ primary_axis |S32 32B ...
│ status_str |S1 1B ...
│ instrument_type |S32 32B ...
│ time_coverage_start |S32 32B ...
│ time_coverage_end |S32 32B ...
│ sweep_group_name (sweep) <U7 252B 'sweep_0' 'sweep_1' ... 'sweep_8'
│ sweep_fixed_angle (sweep) float32 36B ...
│ Attributes: (12/13)
│ Conventions: CF/Radial instrument_parameters radar_parameters rad...
│ version: 1.2
│ title: TIMREX
│ institution:
│ references:
│ source:
│ ... ...
│ comment:
│ instrument_name: SPOLRVP8
│ site_name:
│ scan_name:
│ scan_id: 0
│ platform_is_mobile: false
├── Group: /sweep_0
│ Dimensions: (azimuth: 483, range: 996)
│ Coordinates:
│ * azimuth (azimuth) float32 2kB 0.0 0.75 ... 358.5 359.2
│ elevation (azimuth) float32 2kB ...
│ time (azimuth) datetime64[ns] 4kB 2008-06-04T00:15:...
│ * range (range) float32 4kB 150.0 300.0 ... 1.494e+05
│ Data variables: (12/18)
│ sweep_number int32 4B ...
│ sweep_mode <U20 80B 'azimuth_surveillance'
│ prt_mode |S32 32B ...
│ follow_mode |S32 32B ...
│ sweep_fixed_angle float32 4B ...
│ pulse_width (azimuth) float32 2kB ...
│ ... ...
│ r_calib_index (azimuth) int8 483B ...
│ measured_transmit_power_h (azimuth) float32 2kB ...
│ measured_transmit_power_v (azimuth) float32 2kB ...
│ scan_rate (azimuth) float32 2kB ...
│ DBZ (azimuth, range) float32 2MB ...
│ VR (azimuth, range) float32 2MB ...
├── Group: /sweep_1
│ Dimensions: (azimuth: 483, range: 996)
│ Coordinates:
│ * azimuth (azimuth) float32 2kB 0.0 0.75 ... 358.5 359.2
│ elevation (azimuth) float32 2kB ...
│ time (azimuth) datetime64[ns] 4kB 2008-06-04T00:16:...
│ * range (range) float32 4kB 150.0 300.0 ... 1.494e+05
│ Data variables: (12/18)
│ sweep_number int32 4B ...
│ sweep_mode <U20 80B 'azimuth_surveillance'
│ prt_mode |S32 32B ...
│ follow_mode |S32 32B ...
│ sweep_fixed_angle float32 4B ...
│ pulse_width (azimuth) float32 2kB ...
│ ... ...
│ r_calib_index (azimuth) int8 483B ...
│ measured_transmit_power_h (azimuth) float32 2kB ...
│ measured_transmit_power_v (azimuth) float32 2kB ...
│ scan_rate (azimuth) float32 2kB ...
│ DBZ (azimuth, range) float32 2MB ...
│ VR (azimuth, range) float32 2MB ...
├── Group: /sweep_2
│ Dimensions: (azimuth: 482, range: 996)
│ Coordinates:
│ * azimuth (azimuth) float32 2kB 0.0 0.75 ... 358.5 359.2
│ elevation (azimuth) float32 2kB ...
│ time (azimuth) datetime64[ns] 4kB 2008-06-04T00:17:...
│ * range (range) float32 4kB 150.0 300.0 ... 1.494e+05
│ Data variables: (12/18)
│ sweep_number int32 4B ...
│ sweep_mode <U20 80B 'azimuth_surveillance'
│ prt_mode |S32 32B ...
│ follow_mode |S32 32B ...
│ sweep_fixed_angle float32 4B ...
│ pulse_width (azimuth) float32 2kB ...
│ ... ...
│ r_calib_index (azimuth) int8 482B ...
│ measured_transmit_power_h (azimuth) float32 2kB ...
│ measured_transmit_power_v (azimuth) float32 2kB ...
│ scan_rate (azimuth) float32 2kB ...
│ DBZ (azimuth, range) float32 2MB ...
│ VR (azimuth, range) float32 2MB ...
├── Group: /sweep_3
│ Dimensions: (azimuth: 483, range: 996)
│ Coordinates:
│ * azimuth (azimuth) float32 2kB 0.0 0.75 ... 358.5 359.2
│ elevation (azimuth) float32 2kB ...
│ time (azimuth) datetime64[ns] 4kB 2008-06-04T00:17:...
│ * range (range) float32 4kB 150.0 300.0 ... 1.494e+05
│ Data variables: (12/18)
│ sweep_number int32 4B ...
│ sweep_mode <U20 80B 'azimuth_surveillance'
│ prt_mode |S32 32B ...
│ follow_mode |S32 32B ...
│ sweep_fixed_angle float32 4B ...
│ pulse_width (azimuth) float32 2kB ...
│ ... ...
│ r_calib_index (azimuth) int8 483B ...
│ measured_transmit_power_h (azimuth) float32 2kB ...
│ measured_transmit_power_v (azimuth) float32 2kB ...
│ scan_rate (azimuth) float32 2kB ...
│ DBZ (azimuth, range) float32 2MB ...
│ VR (azimuth, range) float32 2MB ...
├── Group: /sweep_4
│ Dimensions: (azimuth: 481, range: 996)
│ Coordinates:
│ * azimuth (azimuth) float32 2kB 0.0 0.75 ... 358.5 359.2
│ elevation (azimuth) float32 2kB ...
│ time (azimuth) datetime64[ns] 4kB 2008-06-04T00:18:...
│ * range (range) float32 4kB 150.0 300.0 ... 1.494e+05
│ Data variables: (12/18)
│ sweep_number int32 4B ...
│ sweep_mode <U20 80B 'azimuth_surveillance'
│ prt_mode |S32 32B ...
│ follow_mode |S32 32B ...
│ sweep_fixed_angle float32 4B ...
│ pulse_width (azimuth) float32 2kB ...
│ ... ...
│ r_calib_index (azimuth) int8 481B ...
│ measured_transmit_power_h (azimuth) float32 2kB ...
│ measured_transmit_power_v (azimuth) float32 2kB ...
│ scan_rate (azimuth) float32 2kB ...
│ DBZ (azimuth, range) float32 2MB ...
│ VR (azimuth, range) float32 2MB ...
├── Group: /sweep_5
│ Dimensions: (azimuth: 482, range: 996)
│ Coordinates:
│ * azimuth (azimuth) float32 2kB 0.0 0.75 ... 358.5 359.2
│ elevation (azimuth) float32 2kB ...
│ time (azimuth) datetime64[ns] 4kB 2008-06-04T00:19:...
│ * range (range) float32 4kB 150.0 300.0 ... 1.494e+05
│ Data variables: (12/18)
│ sweep_number int32 4B ...
│ sweep_mode <U20 80B 'azimuth_surveillance'
│ prt_mode |S32 32B ...
│ follow_mode |S32 32B ...
│ sweep_fixed_angle float32 4B ...
│ pulse_width (azimuth) float32 2kB ...
│ ... ...
│ r_calib_index (azimuth) int8 482B ...
│ measured_transmit_power_h (azimuth) float32 2kB ...
│ measured_transmit_power_v (azimuth) float32 2kB ...
│ scan_rate (azimuth) float32 2kB ...
│ DBZ (azimuth, range) float32 2MB ...
│ VR (azimuth, range) float32 2MB ...
├── Group: /sweep_6
│ Dimensions: (azimuth: 482, range: 996)
│ Coordinates:
│ * azimuth (azimuth) float32 2kB 0.0 0.75 ... 358.5 359.2
│ elevation (azimuth) float32 2kB ...
│ time (azimuth) datetime64[ns] 4kB 2008-06-04T00:20:...
│ * range (range) float32 4kB 150.0 300.0 ... 1.494e+05
│ Data variables: (12/18)
│ sweep_number int32 4B ...
│ sweep_mode <U20 80B 'azimuth_surveillance'
│ prt_mode |S32 32B ...
│ follow_mode |S32 32B ...
│ sweep_fixed_angle float32 4B ...
│ pulse_width (azimuth) float32 2kB ...
│ ... ...
│ r_calib_index (azimuth) int8 482B ...
│ measured_transmit_power_h (azimuth) float32 2kB ...
│ measured_transmit_power_v (azimuth) float32 2kB ...
│ scan_rate (azimuth) float32 2kB ...
│ DBZ (azimuth, range) float32 2MB ...
│ VR (azimuth, range) float32 2MB ...
├── Group: /sweep_7
│ Dimensions: (azimuth: 484, range: 996)
│ Coordinates:
│ * azimuth (azimuth) float32 2kB 0.0 0.75 ... 358.5 359.2
│ elevation (azimuth) float32 2kB ...
│ time (azimuth) datetime64[ns] 4kB 2008-06-04T00:21:...
│ * range (range) float32 4kB 150.0 300.0 ... 1.494e+05
│ Data variables: (12/18)
│ sweep_number int32 4B ...
│ sweep_mode <U20 80B 'azimuth_surveillance'
│ prt_mode |S32 32B ...
│ follow_mode |S32 32B ...
│ sweep_fixed_angle float32 4B ...
│ pulse_width (azimuth) float32 2kB ...
│ ... ...
│ r_calib_index (azimuth) int8 484B ...
│ measured_transmit_power_h (azimuth) float32 2kB ...
│ measured_transmit_power_v (azimuth) float32 2kB ...
│ scan_rate (azimuth) float32 2kB ...
│ DBZ (azimuth, range) float32 2MB ...
│ VR (azimuth, range) float32 2MB ...
└── Group: /sweep_8
Dimensions: (azimuth: 483, range: 996)
Coordinates:
* azimuth (azimuth) float32 2kB 0.0 0.75 ... 358.5 359.2
elevation (azimuth) float32 2kB ...
time (azimuth) datetime64[ns] 4kB 2008-06-04T00:21:...
* range (range) float32 4kB 150.0 300.0 ... 1.494e+05
Data variables: (12/18)
sweep_number int32 4B ...
sweep_mode <U20 80B 'azimuth_surveillance'
prt_mode |S32 32B ...
follow_mode |S32 32B ...
sweep_fixed_angle float32 4B ...
pulse_width (azimuth) float32 2kB ...
... ...
r_calib_index (azimuth) int8 483B ...
measured_transmit_power_h (azimuth) float32 2kB ...
measured_transmit_power_v (azimuth) float32 2kB ...
scan_rate (azimuth) float32 2kB ...
DBZ (azimuth, range) float32 2MB ...
VR (azimuth, range) float32 2MB ...1. Default georeferencing (AEQD, meters)#
Without target_crs, the accessor adds x, y, z in the
radar-native AEQD projection (distances in meters from the radar site).
radar_aeqd = radar.xradar.georeference()
sweep = radar_aeqd["sweep_0"].to_dataset(inherit="all_coords")
crs = sweep.xradar.get_crs()
print("projection:", crs.coordinate_operation.method_name if crs.coordinate_operation else crs.name)
print("is_projected / is_geographic:", crs.is_projected, "/", crs.is_geographic)
print("x units:", sweep.x.attrs.get("units"))
print("x range:", float(sweep.x.min()), float(sweep.x.max()))
projection: Azimuthal Equidistant
is_projected / is_geographic: True / False
x units: meters
x range: -149353.7199056801 149354.1123565658
fig, ax = plt.subplots(figsize=(7, 6))
radar_aeqd["sweep_0"]["DBZ"].plot(x="x", y="y", cmap="ChaseSpectral", ax=ax)
ax.set_title("AEQD (default) — x, y in meters from radar")
ax.set_aspect("equal")
2. Reproject to geographic lon/lat (EPSG:4326)#
Pass target_crs=4326 to get x as longitude and y as latitude.
Attributes are updated automatically (standard_name, units).
radar_geo = radar.xradar.georeference(target_crs=4326)
sweep = radar_geo["sweep_0"].to_dataset(inherit="all_coords")
crs = sweep.xradar.get_crs()
print("CRS name:", crs.name, "| EPSG:", crs.to_epsg())
print("x attrs:", dict(sweep.x.attrs))
print("y attrs:", dict(sweep.y.attrs))
CRS name: WGS 84 | EPSG: 4326
x attrs: {'standard_name': 'longitude', 'long_name': 'Geographic Longitude', 'units': 'degrees_east', 'axis': 'X'}
y attrs: {'standard_name': 'latitude', 'long_name': 'Geographic Latitude', 'units': 'degrees_north', 'axis': 'Y'}
fig, ax = plt.subplots(figsize=(8, 6))
radar_geo["sweep_0"]["DBZ"].plot(x="x", y="y", cmap="ChaseSpectral", ax=ax)
ax.set_xlabel("Longitude")
ax.set_ylabel("Latitude")
ax.set_title("EPSG:4326 — x=lon, y=lat")
Text(0.5, 1.0, 'EPSG:4326 — x=lon, y=lat')
3. Reproject to a projected CRS (UTM, Web Mercator)#
Any pyproj-compatible CRS input works: EPSG code, WKT string, or a
pyproj.CRS instance.
# Web Mercator (meters)
radar_wm = radar.xradar.georeference(target_crs="EPSG:3857")
sweep_wm = radar_wm["sweep_0"].to_dataset(inherit="all_coords")
crs = sweep_wm.xradar.get_crs()
print("CRS:", crs.name, "| EPSG:", crs.to_epsg())
CRS: WGS 84 / Pseudo-Mercator | EPSG: 3857
utm_crs = pyproj.CRS("EPSG:32633") # UTM zone 33N (tweak for your radar site)
radar_utm = radar.xradar.georeference(target_crs=utm_crs)
sweep_utm = radar_utm["sweep_0"].to_dataset(inherit="all_coords")
crs = sweep_utm.xradar.get_crs()
print("CRS:", crs.name, "| EPSG:", crs.to_epsg())
CRS: WGS 84 / UTM zone 33N | EPSG: 32633
4. Plot on a cartopy map#
Once data is in a known CRS, hand it off to cartopy for map plotting.
radar_geo = radar.xradar.georeference(target_crs=4326)
fig = plt.figure(figsize=(9, 8))
ax = fig.add_subplot(111, projection=ccrs.PlateCarree())
radar_geo["sweep_0"]["DBZ"].plot(
x="x",
y="y",
cmap="ChaseSpectral",
transform=ccrs.PlateCarree(),
ax=ax,
cbar_kwargs=dict(pad=0.05, shrink=0.7),
)
ax.coastlines()
ax.gridlines(draw_labels=True)
ax.set_title("Georeferenced PPI on PlateCarree map")
Text(0.5, 1.0, 'Georeferenced PPI on PlateCarree map')
/home/docs/checkouts/readthedocs.org/user_builds/xradar/conda/latest/lib/python3.13/site-packages/cartopy/io/__init__.py:242: DownloadWarning: Downloading: https://naturalearth.s3.amazonaws.com/10m_physical/ne_10m_coastline.zip
warnings.warn(f'Downloading: {url}', DownloadWarning)
5. Side-by-side comparison of projections#
Compare the same sweep in AEQD (meters), geographic (lon/lat), and Lambert Conformal Conic — a projection commonly used for mid-latitude weather maps.
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
# AEQD (default)
radar_aeqd = radar.xradar.georeference()
radar_aeqd["sweep_0"]["DBZ"].plot(
x="x", y="y", cmap="ChaseSpectral", ax=axes[0], add_colorbar=False,
)
axes[0].set_title("AEQD (meters)")
axes[0].set_aspect("equal")
# EPSG:4326 (lon/lat)
radar_geo = radar.xradar.georeference(target_crs=4326)
radar_geo["sweep_0"]["DBZ"].plot(
x="x", y="y", cmap="ChaseSpectral", ax=axes[1], add_colorbar=False,
)
axes[1].set_title("EPSG:4326 (lon/lat)")
axes[1].set_xlabel("Longitude")
axes[1].set_ylabel("Latitude")
# Lambert Conformal Conic
lcc_crs = pyproj.CRS.from_proj4(
"+proj=lcc +lat_1=30 +lat_2=60 +lat_0=39.5 +lon_0=-105 +datum=WGS84"
)
radar_lcc = radar.xradar.georeference(target_crs=lcc_crs)
radar_lcc["sweep_0"]["DBZ"].plot(
x="x", y="y", cmap="ChaseSpectral", ax=axes[2], add_colorbar=False,
)
axes[2].set_title("Lambert Conformal Conic")
axes[2].set_aspect("equal")
fig.tight_layout()
6. Radar data on different map projections#
The same georeferenced radar data rendered on four different cartopy map projections — coastlines and gridlines make the projection distortion visible.
import cartopy.feature as cfeature
radar_geo = radar.xradar.georeference(target_crs=4326)
lon0 = float(radar_geo.ds.coords["longitude"].values)
lat0 = float(radar_geo.ds.coords["latitude"].values)
projections = [
("PlateCarree", ccrs.PlateCarree()),
("Lambert Conformal", ccrs.LambertConformal(
central_longitude=lon0, central_latitude=lat0,
)),
("Orthographic (globe)", ccrs.Orthographic(
central_longitude=lon0, central_latitude=lat0,
)),
("Mercator", ccrs.Mercator(central_longitude=lon0)),
]
fig = plt.figure(figsize=(14, 12))
for i, (name, proj) in enumerate(projections, 1):
ax = fig.add_subplot(2, 2, i, projection=proj)
radar_geo["sweep_0"]["DBZ"].plot(
x="x", y="y", cmap="ChaseSpectral",
transform=ccrs.PlateCarree(), ax=ax,
add_colorbar=False,
)
ax.coastlines()
ax.add_feature(cfeature.BORDERS, linewidth=0.5)
ax.gridlines(draw_labels=isinstance(proj, (ccrs.PlateCarree, ccrs.Mercator)))
ax.set_title(name)
fig.tight_layout()
/home/docs/checkouts/readthedocs.org/user_builds/xradar/conda/latest/lib/python3.13/site-packages/cartopy/io/__init__.py:242: DownloadWarning: Downloading: https://naturalearth.s3.amazonaws.com/10m_cultural/ne_10m_admin_0_boundary_lines_land.zip
warnings.warn(f'Downloading: {url}', DownloadWarning)
7. Works on a single Dataset too#
The accessor is available on Dataset and DataArray as well, not just
DataTree.
ds = radar["sweep_0"].to_dataset(inherit="all_coords")
ds_geo = ds.xradar.georeference(target_crs=4326)
ds_geo[["DBZ"]]
<xarray.Dataset> Size: 13MB
Dimensions: (azimuth: 483, range: 996)
Coordinates:
* azimuth (azimuth) float32 2kB 0.0 0.75 1.5 2.25 ... 357.8 358.5 359.2
elevation (azimuth) float32 2kB 0.5164 0.5219 0.5164 ... 0.5219 0.5219
time (azimuth) datetime64[ns] 4kB 2008-06-04T00:15:34 ... 2008-06-0...
* range (range) float32 4kB 150.0 300.0 450.0 ... 1.492e+05 1.494e+05
x (azimuth, range) float64 4MB 120.4 120.4 120.4 ... 120.4 120.4
y (azimuth, range) float64 4MB 22.53 22.53 22.53 ... 23.87 23.88
z (azimuth, range) float64 4MB 46.35 47.71 ... 2.714e+03 2.718e+03
latitude float64 8B 22.53
longitude float64 8B 120.4
altitude float64 8B 45.0
crs_wkt int64 8B 0
Data variables:
DBZ (azimuth, range) float32 2MB ...Summary#
target_crsaccepts anythingpyproj.CRS(...)accepts: int EPSG codes,"EPSG:xxxx"strings, WKT, orpyproj.CRSinstances.The
spatial_ref/crs_wktcoordinate is updated to reflect the target CRS —sweep.xradar.get_crs()will return it.Currently only
xandyare reprojected;zstays as AEQD altitude above the radar site.If you want separate
lon/lat/altcoordinates (without overwritingx, y), see the Assign_GeoCoords notebook.