#!/usr/bin/env python
# Copyright (c) 2024, radarx developers.
# Distributed under the MIT License. See LICENSE for more info.
"""
Radarx Grid
============
This sub-module contains functions necessary to grid the radar data.
.. autosummary::
:nosignatures:
:toctree: generated/
{}
"""
__all__ = [
"stack_data",
"make_3d_grid",
"grid_radar",
]
__doc__ = __doc__.format("\n ".join(__all__))
import numpy as np
import xarray as xr
try: # pragma: no cover
from fastbarnes import interpolation
from fastbarnes.interpolation import get_half_kernel_size
FASTBARNES_AVAILABLE = True
except ImportError: # pragma: no cover
FASTBARNES_AVAILABLE = False
from ..utils import get_geocoords, find_multidim_vars # noqa
[docs]
def stack_data(dtree, data_vars=None, geo=False):
"""
Stack data from a radar DataTree into a single xarray Dataset.
Parameters
----------
dtree : xradar.DataTree
The input DataTree containing radar data.
data_vars : list of str or None, optional
A list of variables to stack. If None, all variables with more
than one dimension are included.
geo : bool, optional
If True, converts coordinates to geographic (lon, lat, alt).
Returns
-------
xarray.Dataset
A stacked dataset with the specified or all multidimensional variables.
"""
dtree = dtree.xradar.georeference()
if geo:
dtree = dtree.xradar.map_over_sweeps(get_geocoords)
x, y, z = "lon", "lat", "alt"
else:
x, y, z = "x", "y", "z"
swp_list = []
data_vars_dict = {}
# Loop through sweeps
for swp in dtree.match("sweep_*"):
ds = dtree[swp].to_dataset()
# Find variables to include if data_vars is None
if data_vars is None: # pragma
vars_to_stack = find_multidim_vars(ds, ndim=2)
else: # pragma: no cover
vars_to_stack = data_vars
# Stack coordinates
xyz = (
xr.concat(
[
ds.coords[x].reset_coords(drop=True),
ds.coords[y].reset_coords(drop=True),
ds.coords[z].reset_coords(drop=True),
],
"xyz",
)
.stack(npoints=("azimuth", "range"))
.transpose(..., "xyz")
)
swp_list.append(xyz)
# Stack data for each variable
for v in vars_to_stack:
if v not in data_vars_dict:
data_vars_dict[v] = []
data = ds[v].stack(npoints=("azimuth", "range"))
data_vars_dict[v].append(data)
# Combine stacked data
xyz = xr.concat(swp_list, "npoints")
dataset = xr.Dataset(coords={"xyz": xyz})
for v, data_list in data_vars_dict.items():
dataset[v] = xr.concat(data_list, "npoints")
return dataset
[docs]
def make_3d_grid(
ds,
x_lim=(-200e3, 200e3),
y_lim=(-200e3, 200e3),
x_step=1000,
y_step=1000,
z_lim=(0, 10e3),
z_step=250,
):
"""
Generate a 3D Cartesian grid and transform its coordinates to geographic
latitude, longitude, and altitude using the dataset's CRS.
Parameters
----------
ds : xarray.Dataset
Radar dataset with Cartesian coordinates.
x_lim : tuple of float, optional
The range of x-coords (m) in Cartesian space. Default is (-200e3, 200e3).
y_lim : tuple of float, optional
The range of y-coords (m) in Cartesian space. Default is (-200e3, 200e3).
x_step : int, optional
Step size (m) for x-coordinates. Default is 1000.
y_step : int, optional
Step size (meters) for y-coordinates. Default is 1000.
z_lim : tuple of float, optional
The range of z-coords (m) in Cartesian space. Default is (0, 10e3).
z_step : int, optional
Step size (meters) for z-coordinates. Default is 250.
Returns
-------
tuple
lat : numpy.ndarray
Latitude values of the transformed grid.
lon : numpy.ndarray
Longitude values of the transformed grid.
x : numpy.ndarray
Cartesian x-coordinates (meters).
y : numpy.ndarray
Cartesian y-coordinates (meters).
z : numpy.ndarray
Cartesian z-coordinates (meters, altitude).
trg_crs : pyproj.CRS
Target geographic coordinate reference system (CRS).
Notes
-----
- The function uses the Azimuthal Equidistant projection (AEQD)
defined in the radar dataset.
- The transformation ensures compatibility between Cartesian and
geographic coordinates.
- This function assumes the dataset is compatible with `xradar`
and has a valid CRS.
"""
# Create Cartesian grid arrays
x = np.arange(x_lim[0], x_lim[1] + x_step, x_step)
y = np.arange(y_lim[0], y_lim[1] + y_step, y_step)
z = np.arange(z_lim[0], z_lim[1] + z_step, z_step)
from pyproj import CRS, Transformer
# Convert the dataset to georeferenced coordinates
ds = ds.xradar.georeference()
# Define source and target coordinate reference systems (CRS)
src_crs = ds.xradar.get_crs()
trg_crs = CRS.from_user_input(4326) # EPSG:4326 (WGS 84)
# Create a transformer for coordinate conversion
transformer = Transformer.from_crs(src_crs, trg_crs)
# Transform x, y, z coordinates to latitude, longitude, and altitude
lat, lon = transformer.transform(x, y)
return lat, lon, x, y, z, trg_crs
[docs]
def grid_radar(
dtree,
data_vars=None,
pseudo_cappi=True,
x_lim=(-100e3, 100e3),
y_lim=(-100e3, 100e3),
z_lim=(0, 10e3),
x_step=1000,
y_step=1000,
z_step=250,
x_smth=0.2,
y_smth=0.2,
z_smth=1,
):
"""
Interpolate radar data to a 3D grid and optionally create a pseudo-CAPPI.
Parameters
----------
dtree : xradar.DataTree
Input radar DataTree containing radar sweeps.
data_vars : list of str, optional
List of variables to interpolate. If None, all variables in the dataset
are used. Defaults to None.
pseudo_cappi : bool, optional
If True, extrapolates data to lower altitudes to create a pseudo-CAPPI.
Defaults to True.
x_lim : tuple of float, optional
Range of x-coordinates (meters) for the Cartesian grid. Defaults to
(-100e3, 100e3).
y_lim : tuple of float, optional
Range of y-coordinates (meters) for the Cartesian grid. Defaults to
(-100e3, 100e3).
z_lim : tuple of float, optional
Range of z-coordinates (meters) for the Cartesian grid. Defaults to
(0, 16e3).
x_step : int, optional
Grid resolution in the x-direction (meters). Defaults to 500.
y_step : int, optional
Grid resolution in the y-direction (meters). Defaults to 500.
z_step : int, optional
Grid resolution in the z-direction (meters). Defaults to 250.
x_smth : float, optional
Smoothing factor for the x-dimension. Defaults to 0.2.
y_smth : float, optional
Smoothing factor for the y-dimension. Defaults to 0.2.
z_smth : float, optional
Smoothing factor for the z-dimension. Defaults to 1.
Returns
-------
xarray.Dataset
Interpolated dataset with the specified variables and 3D grid.
Includes longitude and latitude coordinates for the grid.
Notes
-----
- The pseudo-CAPPI is created by extrapolating data from higher altitudes
to fill missing values at lower altitudes.
- Interpolation is performed using Barnes interpolation.
"""
if not FASTBARNES_AVAILABLE: # pragma: no cover
raise ImportError(
"The 'fastbarnes' package is required for this function. "
"Install it via 'pip install fast-barnes-py'."
) # pragma: no cover
ds = stack_data(dtree, data_vars=data_vars, geo=True)
lat, lon, trgx, trgy, z, trg_crs = make_3d_grid(
dtree["sweep_0"].to_dataset(),
x_lim=x_lim,
y_lim=y_lim,
x_step=x_step,
y_step=y_step,
z_lim=z_lim,
z_step=z_step,
)
x = lon
y = lat
x0 = np.asarray([x.min(), y.min(), z.min()], dtype=np.float64)
xstep = np.diff(x).mean() # Longitude step in degrees
ystep = np.diff(y).mean() # Latitude step in degrees
size = (len(x), len(y), len(z))
# Adjust sigma to match grid steps
sigma_lon = x_smth * xstep # Smoothing across 2 longitude grid steps
sigma_lat = y_smth * ystep # Smoothing across 2 latitude grid steps
sigma_alt = z_smth * z_step # Smoothing across 2 altitude grid steps
sigma = np.array([sigma_lon, sigma_lat, sigma_alt], dtype=np.float64)
ds_out_fast = xr.Dataset({"z": ("z", z), "y": ("y", y), "x": ("x", x)})
# Perform Barnes interpolation
if data_vars is None: # pragma: no cover
data_vars = list(ds.data_vars)
for var in data_vars:
data_orig = ds[var].values.flatten()
x_flat = ds["xyz"][:, 0].values.flatten() # Longitude
y_flat = ds["xyz"][:, 1].values.flatten() # Latitude
z_flat = ds["xyz"][:, 2].values.flatten() # Altitude
mask = np.isfinite(data_orig)
if np.sum(mask) == 0:
print(f"No valid data points for variable {var}. Skipping...")
continue
xyz_data = np.vstack((x_flat[mask], y_flat[mask], z_flat[mask])).T
data_values = data_orig[mask]
# Validate kernel size
kernel_size = (
2 * get_half_kernel_size(sigma, [xstep, ystep, z_step], num_iter=4) + 1
)
if any(kernel_size > np.array(size)): # pragma: no cover
print(
f"Kernel size {kernel_size} exceeds grid size {size}. Skipping..."
) # pragma: no cover
continue
# Perform interpolation
field = interpolation.barnes(
xyz_data,
data_values,
sigma,
x0,
[xstep, ystep, z_step],
size,
max_dist=4,
)
if pseudo_cappi:
# Create pseudo-CAPPI by extrapolating to lower levels
pseudo_cappi_field = field.copy()
for z_idx in range(
len(z) - 3, -1, -1
): # Iterate from higher levels to lower levels
upper_level = pseudo_cappi_field[
z_idx + 1, :, :
] # Data from the level above
current_level = pseudo_cappi_field[z_idx, :, :] # Current level
mask = np.isnan(current_level) # Identify missing values at this level
pseudo_cappi_field[z_idx, mask] = upper_level[
mask
] # Fill missing with the level above
# Add pseudo-CAPPI field to the output dataset
ds_out_fast[var] = (("z", "y", "x"), pseudo_cappi_field)
else: # pragma: no cover
ds_out_fast[var] = (("z", "y", "x"), field)
# Assign metadata
ds_out_fast["time"] = ds.time.mean()
ds_out_fast = ds_out_fast.rename({"x": "lon", "y": "lat"})
ds_out_fast["x"] = xr.DataArray(trgx, dims="lon")
ds_out_fast["y"] = xr.DataArray(trgy, dims="lat")
ds_out_fast = ds_out_fast.set_coords(["x", "y"])
ds_out_fast = ds_out_fast.swap_dims({"lon": "x", "lat": "y"})
ds_out_fast.attrs = dtree.attrs
ds_out_fast.attrs["radar_name"] = ds_out_fast.attrs.get("instrument_name", "")
return ds_out_fast