raddb package

Contents

raddb package#

Subpackages#

Submodules#

raddb.aoi module#

raddb/aoi.py#

Area-of-Interest (AOI) selection internals.

These are the private building blocks behind the public RadDB.crop_* methods. The design is LUT-first: an AOI geometry is intersected once with the static per-radar LUT centroids (in Swiss LV95 / EPSG:2056) to resolve a set of gate_id values, and that set then filters any number of dynamic volume DataFrames — cheaply and identically across timesteps.

Because the intersection is done on gate centroids over all sweeps, a single horizontal footprint selects the whole vertical column above it (every elevation).

Coordinate convention: AOI geometries are handled in EPSG:2056 throughout; input in another CRS is reprojected via _reproject_to_2056() before intersection.

No pyart / geocube dependency — only numpy, polars, pandas, shapely, pyproj.

The centroid tables are polars frames (the package-wide default); the spatial predicate itself runs on plain numpy arrays via shapely, so no geometry objects are ever materialised for the ~1.7M gates of a full LUT.

raddb.discovery module#

raddb/discovery.py#

File discovery for RadDB — both sides of the archive:

  • DataTree file discovery (input side): locate DataTree files (NetCDF / Zarr) on disk, optionally filtered by a filename timestamp, ready to load with raddb.io_core.open_any_datatree() and archive with raddb.RadDB.archive_from_datatrees().

  • Archived POL search (output side): locate *_POL.parquet files in a time range inside an existing RadDB archive.

Everything here is pure filesystem + pandas — no pyart / radar_api dependency — so discovery works in any environment. (Raw METRANET scanning lives in the private raddb.mch.discovery module.)

raddb.discovery.find_datatree_files(directory: str | Path, recursive: bool = True, extensions: tuple[str, ...] = ('.nc', '.nc4', '.cdf', '.zarr'), start_time: str | Timestamp | None = None, end_time: str | Timestamp | None = None, strict_time: bool = False) list[Path][source][source]#

Find DataTree files (NetCDF files / Zarr stores) under directory.

Zarr stores are directories — they are matched as leaves and never descended into. Results are sorted by (filename timestamp, path); files whose stem has no parseable timestamp sort last.

Parameters:
  • directory (str or Path) – Directory to scan.

  • recursive (bool) – Recurse into subdirectories (default True).

  • extensions (tuple of str) – File/store suffixes to match (case-insensitive).

  • start_time (str or Timestamp, optional) – Keep only files whose filename timestamp (see _parse_datatree_file_time()) falls in this range.

  • end_time (str or Timestamp, optional) – Keep only files whose filename timestamp (see _parse_datatree_file_time()) falls in this range.

  • strict_time (bool) – When a time range is given, drop files whose timestamp cannot be parsed from the filename (default False — they are kept).

Returns:

Matching files/stores, time-sorted.

Return type:

list of Path

raddb.hc_mapping module#

raddb/hc_mapping.py#

Canonical hydrometeor class constants for the MCH operational encoding.

Both HC_MCH and HC_PYART are stored in parquet on the same 1-based scale:

parquet integer k → HC_MAP_DICT[k - 1]

HC_MCH raw (HYM file, after /25) is already 0-based (0-8). HC_PYART (PyART native 1-9) is remapped to 0-based operational via PYART_TO_OPE before archiving, then shifted +1 to match HC_MCH.

Import these constants wherever HC label/color information is needed.

raddb.helper module#

raddb/helper.py#

Shared utilities and configuration for RadDB.

class raddb.helper.StageTimer[source][source]#

Bases: object

Accumulates per-stage timing records for pipeline profiling.

Example

>>> timer = StageTimer()
>>> with timer.time_stage("my_stage", volume="vol_001", sweep=2):
...     do_work()
>>> timer.print_summary()
>>> plot_profiling_dashboard(timer)
print_summary()[source][source]#

Print a formatted profiling table to stdout.

record(stage: str, duration: float, volume: str | None = None, sweep: int | None = None, t_start: float | None = None)[source][source]#

Manually append a pre-measured timing entry.

summary() DataFrame[source][source]#

Aggregate by stage — returns (sum, mean, min, max, count) sorted by total time.

time_stage(stage: str, volume: str | None = None, sweep: int | None = None)[source][source]#

Context manager — records elapsed wall-clock time for stage.

to_dataframe() DataFrame[source][source]#

Return all records as a DataFrame with columns [volume, sweep, stage, duration].

raddb.helper.check_dataframe(df: DataFrame) None[source][source]#
raddb.helper.ensure_utc(dt_input)[source][source]#

Ensure a datetime-like input is timezone-aware and in UTC.

raddb.helper.filter_df(df: DataFrame, feature: str = 'DBZH', threshold: float = 0.0, logic: str = '>') DataFrame[source][source]#

Filter a DataFrame, keeping rows where feature [logic] threshold.

Parameters:
  • df (pd.DataFrame) – Input DataFrame.

  • feature (str) – Column name to filter on (default "DBZH").

  • threshold (float) – Comparison value.

  • logic (str) – Comparison operator: '>', '>=', '<', '<=', '==', '!='. Default '>'.

Returns:

Filtered DataFrame with non-matching rows dropped (index reset).

Return type:

pd.DataFrame

Raises:
  • KeyError – If feature is not a column of df.

  • ValueError – If logic is not one of the supported operators.

raddb.helper.filter_dt(dt: DataTree, feature: str = 'DBZH', threshold: float = 0.0, logic: str = '>') DataTree[source][source]#

Filter a DataTree, masking gates where feature [logic] threshold is False.

Gates that do not satisfy the condition are set to NaN across all data variables in each sweep. Gates that do satisfy the condition keep their original values unchanged — including legitimate zero values.

Note

This operates on the multidimensional DataTree structure via xr.Dataset.where(). For tabular (row-level) filtering use filter_df() instead, which drops non-matching rows entirely.

Parameters:
  • dt (xr.DataTree) – Input DataTree with sweep_N groups.

  • feature (str) – Variable name to use as the filter criterion (default "DBZH").

  • threshold (float) – Comparison value.

  • logic (str) – Comparison operator: '>', '>=', '<', '<=', '==', '!='. Default '>'.

Returns:

New DataTree where non-matching gates have NaN for all variables. Matching gates are left entirely unchanged (zeros remain zeros).

Return type:

xr.DataTree

Raises:

ValueError – If logic is not a supported operator.

raddb.helper.list_sweep_names(dt: DataTree) list[str][source][source]#

Return sorted sweep group names from a DataTree.

raddb.helper.normalize_radar_name(radar: str) str[source][source]#

Normalize radar name to use only the single letter identifier.

Handles both formats: - “MLA” -> “A” - “A” -> “A” - “MLW” -> “W”

Parameters:

radar (str) – Radar name (can be “ML*” format or just the letter)

Returns:

Normalized single-letter radar name (uppercase)

Return type:

str

Examples

>>> normalize_radar_name("MLA")
'A'
>>> normalize_radar_name("A")
'A'
>>> normalize_radar_name("MLW")
'W'
raddb.helper.read_parquet_files(base_path: str | Path, pattern: str = '**/*POL.parquet', columns: list[str] | None = None, verbose: bool = True) DataFrame[source][source]#
raddb.helper.resolve_filter_logic(logic: str)[source][source]#

Return the comparison function for logic.

Raises:

ValueError – If logic is not one of the supported operators.

raddb.io_core module#

raddb/io_core.py#

Core I/O conversion functions for radar data.

This module provides generic conversions between xarray DataTree, pandas DataFrame, and Parquet files. It does not depend on pyart or radar_api — all MCH-specific I/O lives in the private raddb.mch subpackage.

raddb.io_core.add_feature_to_df(df: DataFrame, feature_name: str, compute_fn: callable) DataFrame[source][source]#

Add a new column to a DataFrame computed from existing columns.

Parameters:
  • df (pd.DataFrame) – Input DataFrame (e.g. from parquet_to_dataframe()).

  • feature_name (str) – Name of the new column to add.

  • compute_fn (callable) –

    Function that takes df and returns a Series or array of the same length. Example:

    def my_feature(df):
        return df["ZDR"] + df["DBZH"] * 0.1
    

Returns:

Copy of df with the new column appended.

Return type:

pd.DataFrame

raddb.io_core.add_feature_to_dt(dt: DataTree, feature_name: str, compute_fn: callable) DataTree[source][source]#

Add a new variable to every sweep in a DataTree.

Parameters:
  • dt (xr.DataTree) – Input DataTree with sweep_N groups.

  • feature_name (str) – Name of the new variable to add to each sweep Dataset.

  • compute_fn (callable) –

    Function that takes an xr.Dataset (one sweep) and returns an xr.DataArray with matching dimensions. Example:

    def kdp_proxy(ds):
        return (ds["PHIDP"].diff("range") / 0.250).clip(0)
    

Returns:

New DataTree with the computed variable added to every sweep.

Return type:

xr.DataTree

raddb.io_core.archive_multiple_volumes(volumes: list[DataTree] | dict[str, DataTree], radar: str, base_output_path: str, filter_feature: str = 'DBZH', filter_threshold: float = 0.0, filter_logic: str = '>', verbose: bool = True, timer: StageTimer | None = None) list[dict][source][source]#

Archive multiple DataTree volumes sequentially.

Parameters:
  • volumes (list or dict) – If a list, each element is a DataTree. If a dict, keys are volume labels and values are DataTrees.

  • radar (str) – Radar identifier.

  • base_output_path (str) – Base output directory.

  • filter_feature (str) – Column to use for gate filtering (default "DBZH").

  • filter_threshold (float) – Threshold value (default 0.0).

  • filter_logic (str) – Comparison operator (default ">").

  • verbose (bool) – Print progress.

  • timer (StageTimer, optional) – Profiling timer.

Returns:

Results with keys: label, success, error, polar_path, n_gates.

Return type:

list of dict

raddb.io_core.archive_volume(dt: DataTree, radar: str, base_output_path: str, filter_feature: str = 'DBZH', filter_threshold: float = 0.0, filter_logic: str = '>', timer=None, volume: str | None = None) str[source][source]#

Archive a single DataTree volume to Parquet format.

Converts the DataTree to a DataFrame, generates a gate_id for each gate (linking back to the LUT), drops gates that do not satisfy filter_feature [filter_logic] filter_threshold, and saves the result as a POL parquet file.

Non-matching gates are dropped (row removal), so zero values in surviving gates are never converted to NaN. NaN values in the reconstruction come only from gates absent in the parquet (i.e. gates that were filtered out or had no data in the original DataTree).

HZT availability check: if "HZT" is not present in the DataTree, "HC_PYART" is also skipped because it requires HZT to be meaningful.

Parameters:
  • dt (xr.DataTree) – Processed volume (any radar network).

  • radar (str) – Radar identifier (single letter, e.g. "A").

  • base_output_path (str) – Base output directory for parquet files.

  • filter_feature (str) – Column to use for gate filtering (default "DBZH").

  • filter_threshold (float) – Threshold value for the filter (default 0.0).

  • filter_logic (str) – Comparison operator (default ">").

  • timer (StageTimer, optional) – Profiling timer.

  • volume (str, optional) – Volume label for timer records.

Returns:

Path to the saved POL parquet file.

Return type:

str

raddb.io_core.archive_volumes_multi_radar(volumes_by_radar: dict[str, list[DataTree] | dict[str, DataTree]], base_output_path: str, filter_feature: str = 'DBZH', filter_threshold: float = 0.0, filter_logic: str = '>', verbose: bool = True, timer: StageTimer | None = None) dict[str, list[dict]][source][source]#

Archive volumes for multiple radars sequentially.

Parameters:
  • volumes_by_radar (dict) – Keys are radar names, values are lists or dicts of DataTrees. Example: {"A": [dt1, dt2], "D": [dt3, dt4]}

  • base_output_path (str) – Base output directory.

  • filter_feature (str) – Column to use for gate filtering (default "DBZH").

  • filter_threshold (float) – Threshold value (default 0.0).

  • filter_logic (str) – Comparison operator (default ">").

  • verbose (bool) – Print progress.

  • timer (StageTimer, optional) – Profiling timer.

Returns:

Keys are radar names, values are lists of result dicts.

Return type:

dict

raddb.io_core.dataframe_to_datatree(df: DataFrame, radar: str, base_path: str | Path, label_column: str = 'DBZH', max_workers: int = 1) DataTree[source][source]#

Reconstruct a DataTree from an in-memory per-gate DataFrame.

The df→DataTree core shared by parquet_to_datatree() and by DataFrame plotting: it joins df with the radar LUT on gate_id to recover geometry (sweep/azimuth/range + lat/lon/alt/x/y/z and any projection cols), fills the full (azimuth × range) grid, and NaN-fills gates absent from df — so a cropped/filtered DataFrame reconstructs to a DataTree that carries the correct geometry but only the rows present in df, with the DataFrame’s own values (honouring crops or added feature columns).

df must be a single radar and a single volume already (see raddb.RadDB.datatree_from_df() for the radar/volume selection helper). Any of the LUT’s geometry columns already on df are dropped and taken from the LUT instead, so a crop_bbox frame (which carries sweep/x_2056/y_2056/z/altitude) reconstructs cleanly.

Parameters:
  • df (pd.DataFrame) – Per-gate rows with a gate_id column (+ measurement columns).

  • radar (str) – Radar identifier whose LUT to join against.

  • base_path (str or Path) – RadDB archive base directory.

  • label_column (str) – Feature used for reconstruction (default "DBZH").

  • max_workers (int) – Parallel workers for sweep reconstruction.

Return type:

xr.DataTree

Raises:
raddb.io_core.datatree_to_dataframe(dt: DataTree, max_workers: int = 1) DataFrame[source][source]#

Flatten a DataTree into a single pandas DataFrame.

Each sweep is converted independently and concatenated, with a sweep column indicating the source sweep number.

raddb.io_core.datatree_to_dataset(dt: DataTree, sweep: str | int) Dataset[source][source]#

Extract a single sweep Dataset from a DataTree.

raddb.io_core.datatree_to_parquet(dt: DataTree, radar: str, base_output_path: str, filter_feature: str = 'DBZH', filter_threshold: float = 0.0, filter_logic: str = '>', max_workers: int = 1) str[source][source]#

Convert a DataTree to a filtered POL parquet file.

Gates that do not satisfy filter_feature [filter_logic] filter_threshold are dropped (row removal) before saving. Zero values in surviving gates are preserved as-is.

If "HZT" is absent from the DataTree, "HC_PYART" is also skipped because it requires HZT to be meaningful.

raddb.io_core.join_labels_with_lut(df_labels: DataFrame, lut_path: str | Path) DataFrame[source][source]#

Join label data with the LUT to recover spatial coordinates.

raddb.io_core.labels_to_dataframe(labels: ndarray, gate_ids, extra_columns: dict | None = None) DataFrame[source][source]#

Create a DataFrame from prediction labels and gate IDs.

raddb.io_core.open_any_datatree(path: str | Path, engine: str | None = None, **open_kwargs) DataTree[source][source]#

Open a DataTree from disk — NetCDF file or Zarr store.

Engine resolution: an explicit engine wins; a *.zarr suffix or a directory containing .zgroup / zarr.json selects "zarr"; otherwise xarray auto-detects the NetCDF backend (netCDF4 / h5netcdf).

Parameters:
  • path (str or Path) – Path to a NetCDF file or Zarr store.

  • engine (str, optional) – xarray backend override (e.g. "h5netcdf", "zarr").

  • **open_kwargs – Forwarded to xarray.open_datatree().

Return type:

xr.DataTree

raddb.io_core.parquet_to_dataframe(radar: str, base_path: str | Path, start_time: str | Timestamp | None = None, end_time: str | Timestamp | None = None, columns: list[str] | None = None, merge_lut: bool = False) DataFrame[source][source]#

Load archived POLAR parquet files as a single DataFrame.

Parameters:
  • radar (str) – Single-letter radar identifier.

  • base_path (str or Path) – RadDB base directory.

  • start_time (optional) – Filter by volume timestamp.

  • end_time (optional) – Filter by volume timestamp.

  • columns (list of str, optional) – Columns to load from parquet files.

  • merge_lut (bool) – If True, merge with the LUT to add spatial coordinates (azimuth, range, latitude, longitude, altitude, x, y, z).

Returns:

Includes a volume_time column (the volume timestamp parsed from each source filename) so a multi-volume frame can be split back into single volumes — used by dataframe_to_datatree() / PPI plotting.

Return type:

pd.DataFrame

raddb.io_core.parquet_to_datatree(radar: str, base_path: str | Path, start_time: str | Timestamp | None = None, end_time: str | Timestamp | None = None, label_column: str = 'DBZH', max_workers: int = 1) DataTree[source][source]#

Load archived POLAR parquet files and reconstruct a DataTree.

Loads all volumes in the given time range, joins with the LUT to recover azimuth/range coordinates, and reconstructs an xarray DataTree.

Parameters:
  • radar (str) – Single-letter radar identifier.

  • base_path (str or Path) – RadDB base directory.

  • start_time (optional) – Filter by volume timestamp.

  • end_time (optional) – Filter by volume timestamp.

  • label_column (str) – Column to use for reconstruction (default "DBZH").

  • max_workers (int) – Parallel workers for sweep reconstruction.

Return type:

xr.DataTree

Raises:
raddb.io_core.reconstruct_datatree(df_joined: DataFrame, lut_path: str | Path, radar_info_path: str | Path, label_column: str = 'hydrometeor_class', max_workers: int = 1) DataTree[source][source]#

Reconstruct a full DataTree from joined data + LUT + radar info.

raddb.io_core.reconstruct_sweep_dataset(df_joined: DataFrame, sweep: int, lut_df: DataFrame, radar_info: dict, label_column: str = 'hydrometeor_class', sweep_corners: dict | None = None) Dataset[source][source]#

Reconstruct a single sweep Dataset from joined data.

Data variables come from the filtered POL parquet (NaN where gates were dropped). Per-gate spatial coords (lat/lon/alt/x/y/z and any x_<epsg>/ y_<epsg>) come from the LUT directly so every gate has a valid geometry regardless of filtering.

If sweep_corners is provided, per-gate edge arrays (x_edges, y_edges, z_edges, lon_edges, lat_edges) of shape (n_az+1, n_range+1) are attached as data_vars for pcolormesh rendering with shading="flat".

raddb.io_core.scan_polar_parquet(radar: str, base_path: str | Path, start_time: str | pd.Timestamp | None = None, end_time: str | pd.Timestamp | None = None, columns: list[str] | None = None) pl.LazyFrame | None[source][source]#

Scan archived POLAR parquet files as a single polars LazyFrame.

The polars counterpart of parquet_to_dataframe(), used by raddb.RadDB.open(). Scanning (rather than reading) lets polars push the column projection into the parquet reader, and avoids the pandas intermediate frames and the pd.concat copy of the full result.

Unlike parquet_to_dataframe() this never merges the LUT: the static geometry stays in its own table and is joined only by the to_* converters.

Parameters:
  • radar (str) – Single-letter radar identifier.

  • base_path (str or Path) – RadDB base directory.

  • start_time (optional) – Filter by volume timestamp.

  • end_time (optional) – Filter by volume timestamp.

  • columns (list of str, optional) – Columns to project. volume_time / radar are added here rather than read, so they are dropped from the parquet projection.

Returns:

None when no volume matches — callers decide what an empty result means. The frame carries a volume_time and a radar column.

Return type:

pl.LazyFrame or None

raddb.lut module#

raddb/lut.py#

Look-Up Table (LUT) generation and loading utilities.

The LUT stores one record per radar gate (azimuth x range x sweep) with static spatial information (Cartesian coordinates, lat/lon, elevation). It is generated once per radar and reused for every subsequent volume.

This module is generic — it works with any xarray DataTree that has azimuth, range, and elevation coordinates per sweep. No pyart or radar_api dependency.

raddb.lut.add_lut_projection(lut_df: DataFrame | DataFrame, epsg: int | None = None, crs=None) DataFrame | DataFrame[source][source]#

Add projected coordinates to a LUT DataFrame.

Converts the latitude / longitude columns to the target CRS and appends x_{suffix} / y_{suffix} columns, where suffix is the EPSG code (if available) or "custom".

Accepts either a polars or a pandas frame and returns the same kind — LUT loading is polars, but LUT generation still builds pandas frames.

Requires pyproj (pip install pyproj).

Parameters:
  • lut_df (pl.DataFrame or pd.DataFrame) – LUT DataFrame with latitude and longitude columns (degrees, WGS-84 / EPSG:4326).

  • epsg (int, optional) – EPSG code for the target CRS. Example: 2056 for CH1903+ / LV95 (Swiss national grid).

  • crs (pyproj.CRS or any CRS-coercible object, optional) – Alternative to epsg. Used when epsg is None. Can be a pyproj CRS object, a WKT string, or any value accepted by pyproj.CRS().

Returns:

Copy of lut_df (same kind) with two new columns: x_{suffix} (easting / metres) and y_{suffix} (northing / metres).

Return type:

pl.DataFrame or pd.DataFrame

Raises:

Examples

Add Swiss LV95 (CH1903+) coordinates to a LUT:

>>> lut_ch = add_lut_projection(lut_df, epsg=2056)
>>> lut_ch[["x_2056", "y_2056"]].head()

Use a custom pyproj CRS:

>>> import pyproj
>>> my_crs = pyproj.CRS.from_epsg(32632)   # UTM zone 32N
>>> lut_utm = add_lut_projection(lut_df, crs=my_crs)
raddb.lut.antenna_vectors_to_cartesian(ranges: ndarray, azimuths: ndarray, elevations: ndarray, ke: float = 1.3333333333333333, edges: bool = False) tuple[ndarray, ndarray, ndarray][source][source]#

Convert radar antenna coordinates to Cartesian (x, y, z).

Replicates the standard radar beam propagation formula used by PyART (pyart.core.transforms.antenna_vectors_to_cartesian) using only numpy. The 4/3 effective-Earth-radius model is used by default.

Parameters:
  • ranges (1-D array, shape (n_gates,)) – Range to each gate in meters.

  • azimuths (1-D array, shape (n_rays,)) – Azimuth angle of each ray in degrees (0 = North, clockwise).

  • elevations (1-D array, shape (n_rays,)) – Elevation angle of each ray in degrees.

  • ke (float) – Effective Earth radius scale factor (default 4/3).

  • edges (bool) – If True, interpolate ranges/azimuths/elevations to gate edges (size N+1) before computing Cartesian coords. Used for pcolormesh(shading="flat") rendering.

Returns:

x, y, z – Cartesian coordinates in meters relative to the radar.

Return type:

arrays, shape (n_rays, n_gates) — or (n_rays+1, n_gates+1) if edges.

raddb.lut.cartesian_to_geographic(x: ndarray, y: ndarray, z: ndarray, radar_lat: float, radar_lon: float, radar_alt: float) tuple[ndarray, ndarray, ndarray][source][source]#

Convert gate Cartesian offsets to geographic coordinates.

Uses an equirectangular approximation which is accurate for typical radar ranges (< 250 km).

Parameters:
  • x (arrays) – Cartesian offsets from the radar in meters (x = east, y = north, z = height above radar).

  • y (arrays) – Cartesian offsets from the radar in meters (x = east, y = north, z = height above radar).

  • z (arrays) – Cartesian offsets from the radar in meters (x = east, y = north, z = height above radar).

  • radar_lat (float) – Radar site latitude and longitude in degrees.

  • radar_lon (float) – Radar site latitude and longitude in degrees.

  • radar_alt (float) – Radar site altitude in meters above sea level.

Returns:

lat, lon, alt – Geographic coordinates of each gate.

Return type:

arrays (same shape as input)

raddb.lut.compute_corners_from_lut(radar: str, lut_base_path: str | Path, ke: float = 1.3333333333333333) str[source][source]#

Rebuild {radar}_corners.npz from an existing LUT parquet + info YAML.

Useful when the corners file is missing (e.g. for LUTs generated before corners were introduced). Reads the per-sweep unique (azimuth, range, elevation) triples from the LUT, computes corner arrays via compute_sweep_corners(), and saves them.

Returns:

str

Return type:

path to the written .npz file.

raddb.lut.compute_gate_xyz(ranges: ndarray, azimuths: ndarray, elevations: ndarray, ke: float = 1.25) tuple[ndarray, ndarray, ndarray][source][source]#

Compute gate Cartesian coordinates from polar coordinates.

Thin wrapper around antenna_vectors_to_cartesian() for backwards compatibility.

raddb.lut.compute_sweep_corners(ranges: ndarray, azimuths: ndarray, elevations: ndarray, radar_lat: float, radar_lon: float, radar_alt: float, ke: float = 1.3333333333333333) dict[source][source]#

Compute per-sweep gate corner arrays for pcolormesh rendering.

Uses PyART’s edge-interpolation (complex-plane for azimuth wrap-around) plus the standard 4/3 Earth beam propagation.

Returns:

  • dict with keys x_edges, y_edges, z_edges, lon_edges,

  • lat_edges, each shape (n_az+1, n_range+1).

raddb.lut.decode_gate_ids(gate_ids) tuple[ndarray, ndarray, ndarray][source][source]#

Inverse of encode_gate_ids() (radar index excluded).

Returns:

(sweeps, azimuths, ranges) – Sweep number (int64), azimuth in degrees (float64, 1 decimal) and range in metres (float64), one entry per gate_id.

Return type:

np.ndarray

raddb.lut.encode_gate_ids(radar: str, sweeps: int | ndarray, azimuths: ndarray, ranges: ndarray) ndarray[source][source]#

Vectorised 64-bit gate identifier encoding.

Encoding: radar_idx * 10^12 + sweep * 10^10 + az_int * 10^6 + range_int

where az_int = round(azimuth * 10) (1 decimal place precision) and range_int = int(range_m) (integer metres). This is the single canonical implementation, used by LUT generation and volume archiving.

Parameters:
  • radar (str) – Radar identifier (single letter, e.g. "A").

  • sweeps (int or array) – Sweep number(s) — a scalar (applied to all gates) or an array aligned with azimuths / ranges.

  • azimuths (arrays (aligned per gate)) – Azimuth [deg] and range [m] of each gate.

  • ranges (arrays (aligned per gate)) – Azimuth [deg] and range [m] of each gate.

Return type:

np.ndarray of int64

raddb.lut.gate_polygons_geoarrow(radar: str, lut_base_path: str | Path, gate_ids, frame: str = 'geographic')[source][source]#

Build the gate wedge polygons for gate_ids as a GeoArrow array.

Each gate becomes a 4-corner ring (closed, 5 vertices) taken from the per-sweep edge arrays produced by compute_sweep_corners() — the exact wedge, not a rectangle. The corners file is rebuilt via compute_corners_from_lut() if it does not exist yet.

Parameters:
  • radar (str) – Single-letter radar identifier.

  • lut_base_path (str or Path) – RadDB archive base directory.

  • gate_ids (array-like of int64) – Gates to build polygons for, in the order they should appear.

  • frame ({"geographic", "cartesian"}) – "geographic" uses lon_edges/lat_edges (EPSG:4326, the frame web maps expect); "cartesian" uses x_edges/y_edges (metres from the radar).

Returns:

A geoarrow.polygon extension array, one polygon per gate_id. Gates whose (azimuth, range) is absent from the LUT grid yield null.

Return type:

pyarrow.Array

raddb.lut.generate_gate_id(radar: str, sweep: int, azimuth: float, range_m: float) int[source][source]#

Create a unique gate identifier as a 64-bit integer.

Scalar convenience wrapper around encode_gate_ids() — see there for the encoding definition.

raddb.lut.generate_lut_from_datatree(dt: DataTree, radar: str, output_base_path: str, ke: float = 1.25, network: str = '', projection_epsg: int | None = None, projection_crs=None) str[source][source]#

Generate a LUT from an xarray DataTree.

This is the generic LUT generator — it works with any DataTree that has the standard xradar coordinate layout (azimuth, range, elevation per sweep).

For MCH-specific LUT generation from raw METRANET files, use raddb.mch.generate_mch_lut() instead.

Parameters:
  • dt (xr.DataTree) – DataTree with sweep_N groups, each containing azimuth, range, and elevation coordinates.

  • radar (str) – Radar identifier (single letter, e.g. "A").

  • output_base_path (str) – Base directory for LUT storage.

  • ke (float) – Effective Earth radius scale factor (default 1.25 for Switzerland).

  • network (str, optional) – Network identifier stored in radar info YAML.

  • projection_epsg (int, optional) – EPSG code for an additional projected coordinate system to include in the LUT (e.g. 2056 for CH1903+ / LV95). Adds x_{epsg} / y_{epsg} columns via add_lut_projection().

  • projection_crs (pyproj.CRS or CRS-coercible, optional) – Alternative to projection_epsg.

Returns:

Path to the saved LUT parquet file.

Return type:

str

raddb.lut.geoarrow_field(name: str, dtype, kind: str, crs: str | None = None)[source][source]#

Build a pyarrow.Field tagged as a GeoArrow extension type.

GeoArrow identifies geometry through field metadata, so the tag can only be attached where the array is placed into a table/schema.

Parameters:
  • name (str) – Column name.

  • dtype (pyarrow.DataType) – Type of the geometry array (e.g. polygons.type).

  • kind (str) – GeoArrow geometry kind, e.g. "point" or "polygon".

  • crs (str, optional) – CRS identifier such as "EPSG:4326".

raddb.lut.get_full_sweep_index(lut_df: DataFrame, sweep: int) MultiIndex[source][source]#

Get the full (azimuth, range) MultiIndex for a sweep from the LUT.

raddb.lut.load_radar_info(radar: str, lut_base_path: str | Path) dict[source][source]#

Load the radar info YAML for a radar.

raddb.lut.load_radar_lut(radar: str, lut_base_path: str | Path) DataFrame[source][source]#

Load the LUT parquet for a radar as a polars DataFrame.

Call polars.DataFrame.to_pandas() on the result if you need pandas.

raddb.lut.load_sweep_corners(radar: str, lut_base_path: str | Path) dict[int, dict][source][source]#

Load per-sweep corner arrays from {radar}_corners.npz.

Returns:

  • dict {sweep_number ({“x_edges”, “y_edges”, …}} — empty dict if the)

  • corners file does not exist (backwards-compatible).

raddb.lut.save_sweep_corners(corners_by_sweep: dict[int, dict], corners_path: str | Path) str[source][source]#

Save per-sweep corner arrays to a single .npz file.

Parameters:
  • corners_by_sweep (dict {sweep_number: {"x_edges", "y_edges", ...}}) –

  • corners_path (str or Path) – Target .npz file path.

raddb.main module#

raddb/main.py#

High-level interface for RadDB — a generic radar data archiving library.

RadDB is a single, dual-role class:

  • Archive-bounddb = RadDB(archive_dir, crs=2056). Use it to archive() DataTree volumes and to open() archived data. archive_dir / crs are the shared defaults for every task.

  • Data-carrying — the object returned by open() (and by filter / crop_* `` / ``extract_cross_section). It holds the loaded data as a polars DataFrame (rdf.data) and exposes the query, conversion, area-of-interest, cross-section and plotting methods. These return a new RadDB (fluent open filter crop plot).

Note: network-specific constants such as a list of radar identifiers (e.g. Swiss radars A, D, L, P, W) belong in the user script or in the network-specific pipeline (e.g. the private raddb.mch subpackage), not here.

class raddb.main.RadDB(archive_dir: str | None = None, crs: int | str | None = None, network: str = '', *, _data: DataFrame | None = None, _meta: dict | None = None)[source][source]#

Bases: object

Generic radar-data archiving and analysis interface (see module docstring).

Examples

>>> db = RadDB(archive_dir="/data/raddb", crs=2056)
>>> db.archive(datatree_dir="/data/MCH_datatree")        # or datatree=dt
>>> rdf = db.open(time_period=("2024-08-26", "2024-08-27"))
>>> rdf.filter({"var": "DBZH", "logic": ">", "threshold": 20})    ...    .crop_by_bbox(extent=rdf.extent())    ...    .plot_ppi(variable="DBZH", save="ppi.png")

Create an archive-bound RadDB.

Parameters:
  • archive_dir (str, optional) – Base directory of the RadDB archive (LUT + POL parquet under {archive_dir}/{radar}/). Shared default for every task.

  • crs (int, str or CRS-coercible, optional) – Projection for the LUT (e.g. 2056 for CH1903+/LV95). Used both when generating LUTs during archive() and to resolve projected coordinates (extent(), to_geopandas()).

  • network (str, optional) – Network label stored in generated LUT metadata.

  • of (The private _data / _meta arguments carry the polars backend) –

  • use (a data-carrying instance and are not meant to be passed directly;) –

:param open().:

add_feature(name: str, compute_fn) RadDB[source][source]#

Add a computed column name and return a new RadDB.

compute_fn receives the polars DataFrame and returns a polars Expr or a column-length array/Series, e.g. rdf.add_feature("ZDR_lin", lambda df: 10 ** (df["ZDR"] / 10)).

add_lut_projection(radar: str, epsg: int | None = None, crs=None) DataFrame[source][source]#

Return the radar LUT enriched with projected x_{epsg} / y_{epsg} columns.

archive(datatree_dir: str | None = None, datatree=None, archive_dir: str | None = None, crs: int | str | None = None, radar: str | list[str] | None = None, filter: dict | None = None, time_period=None) dict[source][source]#

Archive DataTree volumes to the RadDB Parquet store.

Exactly one input source must be given:

  • datatree — an in-memory xr.DataTree, a list of them, a {label: DataTree} dict (one radar), or a {radar: [DataTree, ...]} dict (several radars).

  • datatree_dir — a directory of saved DataTree files (.zarr / .nc); volumes are grouped by radar from the filename.

Passing both raises ValueError.

Parameters:
  • datatree_dir (str, optional) – Directory of saved DataTree files to archive.

  • datatree (xr.DataTree, list, or dict, optional) – In-memory volume(s) to archive.

  • archive_dir (optional) – Override the instance defaults for this call.

  • crs (optional) – Override the instance defaults for this call.

  • radar (str or list of str, optional) – Restrict / assign the radar(s). For datatree_dir with radar=None (default) the radar is inferred per file from the filename and all radars are archived. For a single in-memory DataTree / list, radar is required.

  • filter (dict, optional) – Gate filter {"var", "logic", "threshold"} defining which gates to keep. Default: {"var": "DBZH", "logic": ">", "threshold": 0} (drops no-echo gates).

  • time_period (str, datetime or (start, end), optional) – For datatree_dir: keep only files whose filename timestamp falls in the period.

Returns:

{"n_archived": int, "n_failed": int, "radars": [...]}.

Return type:

dict

columns() list[str][source][source]#

Column names of the loaded data.

crop_around_point(point, distance: float, crs: int | str | None = None, quicklook: bool = False) RadDB[source][source]#

Crop to a circle of radius distance (metres) around point; returns a new RadDB. point is (x, y) or a shapely Point in crs.

crop_by_bbox(bounds=None, extent=None, crs: int | str = 2056, quicklook: bool = False) RadDB[source][source]#

Crop to a rectangle; returns a new RadDB.

Give exactly one of bounds=(xmin, ymin, xmax, ymax) or extent=[xmin, xmax, ymin, ymax] (the latter matches extent()), in crs (default EPSG:2056). A single horizontal footprint selects the whole vertical column above it.

crop_by_polygone(polygon, crs: int | str | None = None, quicklook: bool = False) RadDB[source][source]#

Crop to an arbitrary polygon (shapely, GeoDataFrame/GeoSeries, or a .shp/.geojson path); returns a new RadDB. crs=None auto-detects.

crs()[source][source]#

Projected CRS (x, y) as a pyproj.CRS.

property data: DataFrame[source]#

The loaded data as a polars DataFrame.

end_time() datetime[source][source]#

Latest volume/ray time in the loaded data.

extent() list[float][source][source]#

Projected bounding box [xmin, xmax, ymin, ymax] (in crs).

extract_cross_section(p1, p2, crs: int | str | None = None, beamwidth_deg: float = 1.0, quicklook: bool = False) RadDB[source][source]#

Extract a vertical cross-section along the line p1 -> p2; returns a new RadDB.

The line need not pass through a radar. Each selected gate gets a polygon in the (distance-along-line, altitude) plane (cs_polygon) plus d_near/d_far/z_near/z_far and d_center/z_center; visualize with plot_cross_section(). p1/p2 are (x, y) or shapely Points in crs; distance is measured from p1.

filter(filters) RadDB[source][source]#

Keep only gates satisfying filters (row removal); returns a new RadDB.

filters is a dict {"var", "logic", "threshold"} or a list of such dicts (combined with AND). Vertical subsetting works the same way on an altitude column, e.g. rdf.filter([{"var": "altitude", "logic": ">", "threshold": 2000}, {"var": "altitude", "logic": "<", "threshold": 4000}]).

Filtering on a static column (altitude, sweep, latitude …) joins it from the LUT just long enough to evaluate the predicate and drops it again, so the returned frame still carries dynamic values only.

geographic_crs()[source][source]#

Geographic CRS (lat, lon) as a pyproj.CRS (EPSG:4326).

geographic_extent() list[float][source][source]#

Geographic bounding box [lon_min, lon_max, lat_min, lat_max].

get_lut(radar: str) DataFrame[source][source]#

Load the LUT (static gate geometry) for a radar, as polars.

get_radar_info(radar: str) dict[source][source]#

Load radar metadata (location, sweep geometry).

head(n: int = 5) DataFrame[source][source]#

First n rows (polars).

interactive_crop(**kwargs)[source][source]#

Draw an AOI on an interactive ipyleaflet map and crop (Jupyter only).

Returns an AOISelector; after drawing a shape and clicking Apply crop, the cropped result (a new RadDB) is on selector.result.

inventory(datatree_dir: str | None = None, detailed: bool = False, archive_dir: str | None = None) None[source][source]#

Print what data is available on disk — which radars, which time periods.

Answers “what can I analyse?” before open() (archive side) or archive() (input side). Prints only; nothing is loaded into memory.

Parameters:
  • datatree_dir (str, optional) – Scan a directory of saved DataTree files (.zarr / .nc) — i.e. data not archived yet. Radar and time come from the filename (<RADAR>_<YYYYMMDD>_<HHMMSS>). When omitted, the RadDB archive is scanned instead.

  • detailed (bool) – Add a per-radar, per-day breakdown; for the archive also the LUT size, the sweep count and the moment columns stored per volume.

  • archive_dir (str, optional) – Override the archive location for this call.

Examples

>>> db.inventory()                                  # what is archived
>>> db.inventory(detailed=True)                     # ... day by day
>>> db.inventory(datatree_dir="/data/MCH_datatree") # what could be archived
list_radars() list[str][source][source]#

List radar identifiers that have data in the archive.

open(time_period=None, radars: str | list[str] | None = None, columns: list[str] | None = None, filters=None, archive_dir: str | None = None) RadDB[source][source]#

Load archived data into a data-carrying RadDB.

Parameters:
  • time_period (str, datetime or (start, end), optional) – Time range to load. None loads everything available.

  • radars (str or list of str, optional) – Radar(s) to load. None (default) loads all radars in the archive.

  • columns (list of str, optional) – Subset of moment columns to load (gate_id is always included).

  • filters (dict or list of dict, optional) – Gate filter(s) {"var", "logic", "threshold"} applied after load (a list is combined with AND).

  • archive_dir (str, optional) – Override the archive location for this call.

Returns:

A data-carrying instance (rdf).

Return type:

RadDB

plot_cross_section(variable: str = 'DBZH', radar: str | None = None, timestep=None, save: str | None = None, **kwargs)[source][source]#

Plot a vertical cross-section from an extract_cross_section() result.

plot_ppi(sweep: int | str = 1, variable: str = 'DBZH', radar: str | None = None, timestep=None, save: str | None = None, **kwargs)[source][source]#

Plot a PPI of one sweep from the loaded data. Returns the matplotlib artist.

plot_rhi(azimuth: float, variable: str = 'DBZH', radar: str | None = None, timestep=None, save: str | None = None, **kwargs)[source][source]#

Plot a pseudo-RHI through a radar site at azimuth. Returns the matplotlib artist.

radars() list[str][source][source]#

Radar identifiers present in the loaded data.

start_time() datetime[source][source]#

Earliest volume/ray time in the loaded data.

tail(n: int = 5) DataFrame[source][source]#

Last n rows (polars).

to_datatree(radar: str | None = None, timestep=None, label_column: str = 'DBZH') DataTree[source][source]#

Reconstruct a single-volume xr.DataTree from the loaded data.

Recovers geometry from the LUT and NaN-fills gates absent from the data. Selects one radar and one volume:

  • radar — inferred when the data covers exactly one radar, else required.

  • timestep — nearest volume_time; required only when several volumes.

to_geoarrow(geometry: str = 'point', columns: list[str] | None = None, max_rows: int | None = 5000000)[source][source]#

Return the data as a GeoArrow-tagged pyarrow.Table.

This is the export path for GPU map renderers (deck.gl / lonboard) and for GeoParquet. The dynamic columns come straight from the polars frame with no copy; the LUT geometry is joined here and only here.

Parameters:
  • geometry ({"point", "polygon"}) – "point" gives one gate centroid per row (~24 B/gate). "polygon" gives the exact wedge — the gate’s four corners taken from the LUT’s per-sweep edge arrays (~100 B/gate).

  • columns (list of str, optional) – Dynamic columns to export. None (default) exports all of them. gate_id is always included.

  • max_rows (int, optional) – Guardrail: raise if the frame is larger, since renderers top out around a few million features. Pass None to disable.

Returns:

The dynamic columns plus a geometry column carrying the GeoArrow extension metadata.

Return type:

pyarrow.Table

Examples

>>> import lonboard
>>> lonboard.viz(rdf.crop_by_bbox(bounds).to_geoarrow(geometry="polygon"))
to_geopandas()[source][source]#

Return a GeoDataFrame with a per-gate point geometry and CRS.

to_pandas(with_geometry: bool = False) DataFrame[source][source]#

Return the data as a pandas DataFrame.

With with_geometry=True the per-gate LUT geometry (latitude/longitude/altitude and, if crs is set, x_{epsg}/y_{epsg}) is merged in on gate_id.

Module contents#

RadDB Package — Generic radar data archiving and reconstruction.

RadDB archives xarray DataTree volumes as Parquet files with an efficient LUT-based layout. It is network-agnostic: any DataTree with the standard xradar coordinate layout can be archived and reconstructed.

MCH/METRANET-specific ingestion code lives in the private raddb.mch subpackage (gitignored in the public repository; never imported here).

class raddb.RadDB(archive_dir: str | None = None, crs: int | str | None = None, network: str = '', *, _data: DataFrame | None = None, _meta: dict | None = None)[source][source]#

Bases: object

Generic radar-data archiving and analysis interface (see module docstring).

Examples

>>> db = RadDB(archive_dir="/data/raddb", crs=2056)
>>> db.archive(datatree_dir="/data/MCH_datatree")        # or datatree=dt
>>> rdf = db.open(time_period=("2024-08-26", "2024-08-27"))
>>> rdf.filter({"var": "DBZH", "logic": ">", "threshold": 20})    ...    .crop_by_bbox(extent=rdf.extent())    ...    .plot_ppi(variable="DBZH", save="ppi.png")

Create an archive-bound RadDB.

Parameters:
  • archive_dir (str, optional) – Base directory of the RadDB archive (LUT + POL parquet under {archive_dir}/{radar}/). Shared default for every task.

  • crs (int, str or CRS-coercible, optional) – Projection for the LUT (e.g. 2056 for CH1903+/LV95). Used both when generating LUTs during archive() and to resolve projected coordinates (extent(), to_geopandas()).

  • network (str, optional) – Network label stored in generated LUT metadata.

  • of (The private _data / _meta arguments carry the polars backend) –

  • use (a data-carrying instance and are not meant to be passed directly;) –

:param open().:

add_feature(name: str, compute_fn) RadDB[source][source]#

Add a computed column name and return a new RadDB.

compute_fn receives the polars DataFrame and returns a polars Expr or a column-length array/Series, e.g. rdf.add_feature("ZDR_lin", lambda df: 10 ** (df["ZDR"] / 10)).

add_lut_projection(radar: str, epsg: int | None = None, crs=None) DataFrame[source][source]#

Return the radar LUT enriched with projected x_{epsg} / y_{epsg} columns.

archive(datatree_dir: str | None = None, datatree=None, archive_dir: str | None = None, crs: int | str | None = None, radar: str | list[str] | None = None, filter: dict | None = None, time_period=None) dict[source][source]#

Archive DataTree volumes to the RadDB Parquet store.

Exactly one input source must be given:

  • datatree — an in-memory xr.DataTree, a list of them, a {label: DataTree} dict (one radar), or a {radar: [DataTree, ...]} dict (several radars).

  • datatree_dir — a directory of saved DataTree files (.zarr / .nc); volumes are grouped by radar from the filename.

Passing both raises ValueError.

Parameters:
  • datatree_dir (str, optional) – Directory of saved DataTree files to archive.

  • datatree (xr.DataTree, list, or dict, optional) – In-memory volume(s) to archive.

  • archive_dir (optional) – Override the instance defaults for this call.

  • crs (optional) – Override the instance defaults for this call.

  • radar (str or list of str, optional) – Restrict / assign the radar(s). For datatree_dir with radar=None (default) the radar is inferred per file from the filename and all radars are archived. For a single in-memory DataTree / list, radar is required.

  • filter (dict, optional) – Gate filter {"var", "logic", "threshold"} defining which gates to keep. Default: {"var": "DBZH", "logic": ">", "threshold": 0} (drops no-echo gates).

  • time_period (str, datetime or (start, end), optional) – For datatree_dir: keep only files whose filename timestamp falls in the period.

Returns:

{"n_archived": int, "n_failed": int, "radars": [...]}.

Return type:

dict

columns() list[str][source][source]#

Column names of the loaded data.

crop_around_point(point, distance: float, crs: int | str | None = None, quicklook: bool = False) RadDB[source][source]#

Crop to a circle of radius distance (metres) around point; returns a new RadDB. point is (x, y) or a shapely Point in crs.

crop_by_bbox(bounds=None, extent=None, crs: int | str = 2056, quicklook: bool = False) RadDB[source][source]#

Crop to a rectangle; returns a new RadDB.

Give exactly one of bounds=(xmin, ymin, xmax, ymax) or extent=[xmin, xmax, ymin, ymax] (the latter matches extent()), in crs (default EPSG:2056). A single horizontal footprint selects the whole vertical column above it.

crop_by_polygone(polygon, crs: int | str | None = None, quicklook: bool = False) RadDB[source][source]#

Crop to an arbitrary polygon (shapely, GeoDataFrame/GeoSeries, or a .shp/.geojson path); returns a new RadDB. crs=None auto-detects.

crs()[source][source]#

Projected CRS (x, y) as a pyproj.CRS.

property data: DataFrame[source]#

The loaded data as a polars DataFrame.

end_time() datetime[source][source]#

Latest volume/ray time in the loaded data.

extent() list[float][source][source]#

Projected bounding box [xmin, xmax, ymin, ymax] (in crs).

extract_cross_section(p1, p2, crs: int | str | None = None, beamwidth_deg: float = 1.0, quicklook: bool = False) RadDB[source][source]#

Extract a vertical cross-section along the line p1 -> p2; returns a new RadDB.

The line need not pass through a radar. Each selected gate gets a polygon in the (distance-along-line, altitude) plane (cs_polygon) plus d_near/d_far/z_near/z_far and d_center/z_center; visualize with plot_cross_section(). p1/p2 are (x, y) or shapely Points in crs; distance is measured from p1.

filter(filters) RadDB[source][source]#

Keep only gates satisfying filters (row removal); returns a new RadDB.

filters is a dict {"var", "logic", "threshold"} or a list of such dicts (combined with AND). Vertical subsetting works the same way on an altitude column, e.g. rdf.filter([{"var": "altitude", "logic": ">", "threshold": 2000}, {"var": "altitude", "logic": "<", "threshold": 4000}]).

Filtering on a static column (altitude, sweep, latitude …) joins it from the LUT just long enough to evaluate the predicate and drops it again, so the returned frame still carries dynamic values only.

geographic_crs()[source][source]#

Geographic CRS (lat, lon) as a pyproj.CRS (EPSG:4326).

geographic_extent() list[float][source][source]#

Geographic bounding box [lon_min, lon_max, lat_min, lat_max].

get_lut(radar: str) DataFrame[source][source]#

Load the LUT (static gate geometry) for a radar, as polars.

get_radar_info(radar: str) dict[source][source]#

Load radar metadata (location, sweep geometry).

head(n: int = 5) DataFrame[source][source]#

First n rows (polars).

interactive_crop(**kwargs)[source][source]#

Draw an AOI on an interactive ipyleaflet map and crop (Jupyter only).

Returns an AOISelector; after drawing a shape and clicking Apply crop, the cropped result (a new RadDB) is on selector.result.

inventory(datatree_dir: str | None = None, detailed: bool = False, archive_dir: str | None = None) None[source][source]#

Print what data is available on disk — which radars, which time periods.

Answers “what can I analyse?” before open() (archive side) or archive() (input side). Prints only; nothing is loaded into memory.

Parameters:
  • datatree_dir (str, optional) – Scan a directory of saved DataTree files (.zarr / .nc) — i.e. data not archived yet. Radar and time come from the filename (<RADAR>_<YYYYMMDD>_<HHMMSS>). When omitted, the RadDB archive is scanned instead.

  • detailed (bool) – Add a per-radar, per-day breakdown; for the archive also the LUT size, the sweep count and the moment columns stored per volume.

  • archive_dir (str, optional) – Override the archive location for this call.

Examples

>>> db.inventory()                                  # what is archived
>>> db.inventory(detailed=True)                     # ... day by day
>>> db.inventory(datatree_dir="/data/MCH_datatree") # what could be archived
list_radars() list[str][source][source]#

List radar identifiers that have data in the archive.

open(time_period=None, radars: str | list[str] | None = None, columns: list[str] | None = None, filters=None, archive_dir: str | None = None) RadDB[source][source]#

Load archived data into a data-carrying RadDB.

Parameters:
  • time_period (str, datetime or (start, end), optional) – Time range to load. None loads everything available.

  • radars (str or list of str, optional) – Radar(s) to load. None (default) loads all radars in the archive.

  • columns (list of str, optional) – Subset of moment columns to load (gate_id is always included).

  • filters (dict or list of dict, optional) – Gate filter(s) {"var", "logic", "threshold"} applied after load (a list is combined with AND).

  • archive_dir (str, optional) – Override the archive location for this call.

Returns:

A data-carrying instance (rdf).

Return type:

RadDB

plot_cross_section(variable: str = 'DBZH', radar: str | None = None, timestep=None, save: str | None = None, **kwargs)[source][source]#

Plot a vertical cross-section from an extract_cross_section() result.

plot_ppi(sweep: int | str = 1, variable: str = 'DBZH', radar: str | None = None, timestep=None, save: str | None = None, **kwargs)[source][source]#

Plot a PPI of one sweep from the loaded data. Returns the matplotlib artist.

plot_rhi(azimuth: float, variable: str = 'DBZH', radar: str | None = None, timestep=None, save: str | None = None, **kwargs)[source][source]#

Plot a pseudo-RHI through a radar site at azimuth. Returns the matplotlib artist.

radars() list[str][source][source]#

Radar identifiers present in the loaded data.

start_time() datetime[source][source]#

Earliest volume/ray time in the loaded data.

tail(n: int = 5) DataFrame[source][source]#

Last n rows (polars).

to_datatree(radar: str | None = None, timestep=None, label_column: str = 'DBZH') DataTree[source][source]#

Reconstruct a single-volume xr.DataTree from the loaded data.

Recovers geometry from the LUT and NaN-fills gates absent from the data. Selects one radar and one volume:

  • radar — inferred when the data covers exactly one radar, else required.

  • timestep — nearest volume_time; required only when several volumes.

to_geoarrow(geometry: str = 'point', columns: list[str] | None = None, max_rows: int | None = 5000000)[source][source]#

Return the data as a GeoArrow-tagged pyarrow.Table.

This is the export path for GPU map renderers (deck.gl / lonboard) and for GeoParquet. The dynamic columns come straight from the polars frame with no copy; the LUT geometry is joined here and only here.

Parameters:
  • geometry ({"point", "polygon"}) – "point" gives one gate centroid per row (~24 B/gate). "polygon" gives the exact wedge — the gate’s four corners taken from the LUT’s per-sweep edge arrays (~100 B/gate).

  • columns (list of str, optional) – Dynamic columns to export. None (default) exports all of them. gate_id is always included.

  • max_rows (int, optional) – Guardrail: raise if the frame is larger, since renderers top out around a few million features. Pass None to disable.

Returns:

The dynamic columns plus a geometry column carrying the GeoArrow extension metadata.

Return type:

pyarrow.Table

Examples

>>> import lonboard
>>> lonboard.viz(rdf.crop_by_bbox(bounds).to_geoarrow(geometry="polygon"))
to_geopandas()[source][source]#

Return a GeoDataFrame with a per-gate point geometry and CRS.

to_pandas(with_geometry: bool = False) DataFrame[source][source]#

Return the data as a pandas DataFrame.

With with_geometry=True the per-gate LUT geometry (latitude/longitude/altitude and, if crs is set, x_{epsg}/y_{epsg}) is merged in on gate_id.

class raddb.StageTimer[source][source]#

Bases: object

Accumulates per-stage timing records for pipeline profiling.

Example

>>> timer = StageTimer()
>>> with timer.time_stage("my_stage", volume="vol_001", sweep=2):
...     do_work()
>>> timer.print_summary()
>>> plot_profiling_dashboard(timer)
print_summary()[source][source]#

Print a formatted profiling table to stdout.

record(stage: str, duration: float, volume: str | None = None, sweep: int | None = None, t_start: float | None = None)[source][source]#

Manually append a pre-measured timing entry.

summary() DataFrame[source][source]#

Aggregate by stage — returns (sum, mean, min, max, count) sorted by total time.

time_stage(stage: str, volume: str | None = None, sweep: int | None = None)[source][source]#

Context manager — records elapsed wall-clock time for stage.

to_dataframe() DataFrame[source][source]#

Return all records as a DataFrame with columns [volume, sweep, stage, duration].

raddb.add_feature_to_df(df: DataFrame, feature_name: str, compute_fn: callable) DataFrame[source][source]#

Add a new column to a DataFrame computed from existing columns.

Parameters:
  • df (pd.DataFrame) – Input DataFrame (e.g. from parquet_to_dataframe()).

  • feature_name (str) – Name of the new column to add.

  • compute_fn (callable) –

    Function that takes df and returns a Series or array of the same length. Example:

    def my_feature(df):
        return df["ZDR"] + df["DBZH"] * 0.1
    

Returns:

Copy of df with the new column appended.

Return type:

pd.DataFrame

raddb.add_feature_to_dt(dt: DataTree, feature_name: str, compute_fn: callable) DataTree[source][source]#

Add a new variable to every sweep in a DataTree.

Parameters:
  • dt (xr.DataTree) – Input DataTree with sweep_N groups.

  • feature_name (str) – Name of the new variable to add to each sweep Dataset.

  • compute_fn (callable) –

    Function that takes an xr.Dataset (one sweep) and returns an xr.DataArray with matching dimensions. Example:

    def kdp_proxy(ds):
        return (ds["PHIDP"].diff("range") / 0.250).clip(0)
    

Returns:

New DataTree with the computed variable added to every sweep.

Return type:

xr.DataTree

raddb.add_lut_projection(lut_df: DataFrame | DataFrame, epsg: int | None = None, crs=None) DataFrame | DataFrame[source][source]#

Add projected coordinates to a LUT DataFrame.

Converts the latitude / longitude columns to the target CRS and appends x_{suffix} / y_{suffix} columns, where suffix is the EPSG code (if available) or "custom".

Accepts either a polars or a pandas frame and returns the same kind — LUT loading is polars, but LUT generation still builds pandas frames.

Requires pyproj (pip install pyproj).

Parameters:
  • lut_df (pl.DataFrame or pd.DataFrame) – LUT DataFrame with latitude and longitude columns (degrees, WGS-84 / EPSG:4326).

  • epsg (int, optional) – EPSG code for the target CRS. Example: 2056 for CH1903+ / LV95 (Swiss national grid).

  • crs (pyproj.CRS or any CRS-coercible object, optional) – Alternative to epsg. Used when epsg is None. Can be a pyproj CRS object, a WKT string, or any value accepted by pyproj.CRS().

Returns:

Copy of lut_df (same kind) with two new columns: x_{suffix} (easting / metres) and y_{suffix} (northing / metres).

Return type:

pl.DataFrame or pd.DataFrame

Raises:

Examples

Add Swiss LV95 (CH1903+) coordinates to a LUT:

>>> lut_ch = add_lut_projection(lut_df, epsg=2056)
>>> lut_ch[["x_2056", "y_2056"]].head()

Use a custom pyproj CRS:

>>> import pyproj
>>> my_crs = pyproj.CRS.from_epsg(32632)   # UTM zone 32N
>>> lut_utm = add_lut_projection(lut_df, crs=my_crs)
raddb.antenna_vectors_to_cartesian(ranges: ndarray, azimuths: ndarray, elevations: ndarray, ke: float = 1.3333333333333333, edges: bool = False) tuple[ndarray, ndarray, ndarray][source][source]#

Convert radar antenna coordinates to Cartesian (x, y, z).

Replicates the standard radar beam propagation formula used by PyART (pyart.core.transforms.antenna_vectors_to_cartesian) using only numpy. The 4/3 effective-Earth-radius model is used by default.

Parameters:
  • ranges (1-D array, shape (n_gates,)) – Range to each gate in meters.

  • azimuths (1-D array, shape (n_rays,)) – Azimuth angle of each ray in degrees (0 = North, clockwise).

  • elevations (1-D array, shape (n_rays,)) – Elevation angle of each ray in degrees.

  • ke (float) – Effective Earth radius scale factor (default 4/3).

  • edges (bool) – If True, interpolate ranges/azimuths/elevations to gate edges (size N+1) before computing Cartesian coords. Used for pcolormesh(shading="flat") rendering.

Returns:

x, y, z – Cartesian coordinates in meters relative to the radar.

Return type:

arrays, shape (n_rays, n_gates) — or (n_rays+1, n_gates+1) if edges.

raddb.archive_multiple_volumes(volumes: list[DataTree] | dict[str, DataTree], radar: str, base_output_path: str, filter_feature: str = 'DBZH', filter_threshold: float = 0.0, filter_logic: str = '>', verbose: bool = True, timer: StageTimer | None = None) list[dict][source][source]#

Archive multiple DataTree volumes sequentially.

Parameters:
  • volumes (list or dict) – If a list, each element is a DataTree. If a dict, keys are volume labels and values are DataTrees.

  • radar (str) – Radar identifier.

  • base_output_path (str) – Base output directory.

  • filter_feature (str) – Column to use for gate filtering (default "DBZH").

  • filter_threshold (float) – Threshold value (default 0.0).

  • filter_logic (str) – Comparison operator (default ">").

  • verbose (bool) – Print progress.

  • timer (StageTimer, optional) – Profiling timer.

Returns:

Results with keys: label, success, error, polar_path, n_gates.

Return type:

list of dict

raddb.archive_volume(dt: DataTree, radar: str, base_output_path: str, filter_feature: str = 'DBZH', filter_threshold: float = 0.0, filter_logic: str = '>', timer=None, volume: str | None = None) str[source][source]#

Archive a single DataTree volume to Parquet format.

Converts the DataTree to a DataFrame, generates a gate_id for each gate (linking back to the LUT), drops gates that do not satisfy filter_feature [filter_logic] filter_threshold, and saves the result as a POL parquet file.

Non-matching gates are dropped (row removal), so zero values in surviving gates are never converted to NaN. NaN values in the reconstruction come only from gates absent in the parquet (i.e. gates that were filtered out or had no data in the original DataTree).

HZT availability check: if "HZT" is not present in the DataTree, "HC_PYART" is also skipped because it requires HZT to be meaningful.

Parameters:
  • dt (xr.DataTree) – Processed volume (any radar network).

  • radar (str) – Radar identifier (single letter, e.g. "A").

  • base_output_path (str) – Base output directory for parquet files.

  • filter_feature (str) – Column to use for gate filtering (default "DBZH").

  • filter_threshold (float) – Threshold value for the filter (default 0.0).

  • filter_logic (str) – Comparison operator (default ">").

  • timer (StageTimer, optional) – Profiling timer.

  • volume (str, optional) – Volume label for timer records.

Returns:

Path to the saved POL parquet file.

Return type:

str

raddb.archive_volumes_multi_radar(volumes_by_radar: dict[str, list[DataTree] | dict[str, DataTree]], base_output_path: str, filter_feature: str = 'DBZH', filter_threshold: float = 0.0, filter_logic: str = '>', verbose: bool = True, timer: StageTimer | None = None) dict[str, list[dict]][source][source]#

Archive volumes for multiple radars sequentially.

Parameters:
  • volumes_by_radar (dict) – Keys are radar names, values are lists or dicts of DataTrees. Example: {"A": [dt1, dt2], "D": [dt3, dt4]}

  • base_output_path (str) – Base output directory.

  • filter_feature (str) – Column to use for gate filtering (default "DBZH").

  • filter_threshold (float) – Threshold value (default 0.0).

  • filter_logic (str) – Comparison operator (default ">").

  • verbose (bool) – Print progress.

  • timer (StageTimer, optional) – Profiling timer.

Returns:

Keys are radar names, values are lists of result dicts.

Return type:

dict

raddb.cartesian_to_geographic(x: ndarray, y: ndarray, z: ndarray, radar_lat: float, radar_lon: float, radar_alt: float) tuple[ndarray, ndarray, ndarray][source][source]#

Convert gate Cartesian offsets to geographic coordinates.

Uses an equirectangular approximation which is accurate for typical radar ranges (< 250 km).

Parameters:
  • x (arrays) – Cartesian offsets from the radar in meters (x = east, y = north, z = height above radar).

  • y (arrays) – Cartesian offsets from the radar in meters (x = east, y = north, z = height above radar).

  • z (arrays) – Cartesian offsets from the radar in meters (x = east, y = north, z = height above radar).

  • radar_lat (float) – Radar site latitude and longitude in degrees.

  • radar_lon (float) – Radar site latitude and longitude in degrees.

  • radar_alt (float) – Radar site altitude in meters above sea level.

Returns:

lat, lon, alt – Geographic coordinates of each gate.

Return type:

arrays (same shape as input)

raddb.check_dataframe(df: DataFrame) None[source][source]#
raddb.compute_gate_xyz(ranges: ndarray, azimuths: ndarray, elevations: ndarray, ke: float = 1.25) tuple[ndarray, ndarray, ndarray][source][source]#

Compute gate Cartesian coordinates from polar coordinates.

Thin wrapper around antenna_vectors_to_cartesian() for backwards compatibility.

raddb.dataframe_to_datatree(df: DataFrame, radar: str, base_path: str | Path, label_column: str = 'DBZH', max_workers: int = 1) DataTree[source][source]#

Reconstruct a DataTree from an in-memory per-gate DataFrame.

The df→DataTree core shared by parquet_to_datatree() and by DataFrame plotting: it joins df with the radar LUT on gate_id to recover geometry (sweep/azimuth/range + lat/lon/alt/x/y/z and any projection cols), fills the full (azimuth × range) grid, and NaN-fills gates absent from df — so a cropped/filtered DataFrame reconstructs to a DataTree that carries the correct geometry but only the rows present in df, with the DataFrame’s own values (honouring crops or added feature columns).

df must be a single radar and a single volume already (see raddb.RadDB.datatree_from_df() for the radar/volume selection helper). Any of the LUT’s geometry columns already on df are dropped and taken from the LUT instead, so a crop_bbox frame (which carries sweep/x_2056/y_2056/z/altitude) reconstructs cleanly.

Parameters:
  • df (pd.DataFrame) – Per-gate rows with a gate_id column (+ measurement columns).

  • radar (str) – Radar identifier whose LUT to join against.

  • base_path (str or Path) – RadDB archive base directory.

  • label_column (str) – Feature used for reconstruction (default "DBZH").

  • max_workers (int) – Parallel workers for sweep reconstruction.

Return type:

xr.DataTree

Raises:
raddb.datatree_to_dataframe(dt: DataTree, max_workers: int = 1) DataFrame[source][source]#

Flatten a DataTree into a single pandas DataFrame.

Each sweep is converted independently and concatenated, with a sweep column indicating the source sweep number.

raddb.datatree_to_dataset(dt: DataTree, sweep: str | int) Dataset[source][source]#

Extract a single sweep Dataset from a DataTree.

raddb.datatree_to_parquet(dt: DataTree, radar: str, base_output_path: str, filter_feature: str = 'DBZH', filter_threshold: float = 0.0, filter_logic: str = '>', max_workers: int = 1) str[source][source]#

Convert a DataTree to a filtered POL parquet file.

Gates that do not satisfy filter_feature [filter_logic] filter_threshold are dropped (row removal) before saving. Zero values in surviving gates are preserved as-is.

If "HZT" is absent from the DataTree, "HC_PYART" is also skipped because it requires HZT to be meaningful.

raddb.filter_df(df: DataFrame, feature: str = 'DBZH', threshold: float = 0.0, logic: str = '>') DataFrame[source][source]#

Filter a DataFrame, keeping rows where feature [logic] threshold.

Parameters:
  • df (pd.DataFrame) – Input DataFrame.

  • feature (str) – Column name to filter on (default "DBZH").

  • threshold (float) – Comparison value.

  • logic (str) – Comparison operator: '>', '>=', '<', '<=', '==', '!='. Default '>'.

Returns:

Filtered DataFrame with non-matching rows dropped (index reset).

Return type:

pd.DataFrame

Raises:
  • KeyError – If feature is not a column of df.

  • ValueError – If logic is not one of the supported operators.

raddb.filter_dt(dt: DataTree, feature: str = 'DBZH', threshold: float = 0.0, logic: str = '>') DataTree[source][source]#

Filter a DataTree, masking gates where feature [logic] threshold is False.

Gates that do not satisfy the condition are set to NaN across all data variables in each sweep. Gates that do satisfy the condition keep their original values unchanged — including legitimate zero values.

Note

This operates on the multidimensional DataTree structure via xr.Dataset.where(). For tabular (row-level) filtering use filter_df() instead, which drops non-matching rows entirely.

Parameters:
  • dt (xr.DataTree) – Input DataTree with sweep_N groups.

  • feature (str) – Variable name to use as the filter criterion (default "DBZH").

  • threshold (float) – Comparison value.

  • logic (str) – Comparison operator: '>', '>=', '<', '<=', '==', '!='. Default '>'.

Returns:

New DataTree where non-matching gates have NaN for all variables. Matching gates are left entirely unchanged (zeros remain zeros).

Return type:

xr.DataTree

Raises:

ValueError – If logic is not a supported operator.

raddb.find_datatree_files(directory: str | Path, recursive: bool = True, extensions: tuple[str, ...] = ('.nc', '.nc4', '.cdf', '.zarr'), start_time: str | Timestamp | None = None, end_time: str | Timestamp | None = None, strict_time: bool = False) list[Path][source][source]#

Find DataTree files (NetCDF files / Zarr stores) under directory.

Zarr stores are directories — they are matched as leaves and never descended into. Results are sorted by (filename timestamp, path); files whose stem has no parseable timestamp sort last.

Parameters:
  • directory (str or Path) – Directory to scan.

  • recursive (bool) – Recurse into subdirectories (default True).

  • extensions (tuple of str) – File/store suffixes to match (case-insensitive).

  • start_time (str or Timestamp, optional) – Keep only files whose filename timestamp (see _parse_datatree_file_time()) falls in this range.

  • end_time (str or Timestamp, optional) – Keep only files whose filename timestamp (see _parse_datatree_file_time()) falls in this range.

  • strict_time (bool) – When a time range is given, drop files whose timestamp cannot be parsed from the filename (default False — they are kept).

Returns:

Matching files/stores, time-sorted.

Return type:

list of Path

raddb.generate_gate_id(radar: str, sweep: int, azimuth: float, range_m: float) int[source][source]#

Create a unique gate identifier as a 64-bit integer.

Scalar convenience wrapper around encode_gate_ids() — see there for the encoding definition.

raddb.generate_lut_from_datatree(dt: DataTree, radar: str, output_base_path: str, ke: float = 1.25, network: str = '', projection_epsg: int | None = None, projection_crs=None) str[source][source]#

Generate a LUT from an xarray DataTree.

This is the generic LUT generator — it works with any DataTree that has the standard xradar coordinate layout (azimuth, range, elevation per sweep).

For MCH-specific LUT generation from raw METRANET files, use raddb.mch.generate_mch_lut() instead.

Parameters:
  • dt (xr.DataTree) – DataTree with sweep_N groups, each containing azimuth, range, and elevation coordinates.

  • radar (str) – Radar identifier (single letter, e.g. "A").

  • output_base_path (str) – Base directory for LUT storage.

  • ke (float) – Effective Earth radius scale factor (default 1.25 for Switzerland).

  • network (str, optional) – Network identifier stored in radar info YAML.

  • projection_epsg (int, optional) – EPSG code for an additional projected coordinate system to include in the LUT (e.g. 2056 for CH1903+ / LV95). Adds x_{epsg} / y_{epsg} columns via add_lut_projection().

  • projection_crs (pyproj.CRS or CRS-coercible, optional) – Alternative to projection_epsg.

Returns:

Path to the saved LUT parquet file.

Return type:

str

raddb.get_full_sweep_index(lut_df: DataFrame, sweep: int) MultiIndex[source][source]#

Get the full (azimuth, range) MultiIndex for a sweep from the LUT.

raddb.join_labels_with_lut(df_labels: DataFrame, lut_path: str | Path) DataFrame[source][source]#

Join label data with the LUT to recover spatial coordinates.

raddb.labels_to_dataframe(labels: ndarray, gate_ids, extra_columns: dict | None = None) DataFrame[source][source]#

Create a DataFrame from prediction labels and gate IDs.

raddb.list_sweep_names(dt: DataTree) list[str][source][source]#

Return sorted sweep group names from a DataTree.

raddb.load_radar_info(radar: str, lut_base_path: str | Path) dict[source][source]#

Load the radar info YAML for a radar.

raddb.load_radar_lut(radar: str, lut_base_path: str | Path) DataFrame[source][source]#

Load the LUT parquet for a radar as a polars DataFrame.

Call polars.DataFrame.to_pandas() on the result if you need pandas.

raddb.normalize_radar_name(radar: str) str[source][source]#

Normalize radar name to use only the single letter identifier.

Handles both formats: - “MLA” -> “A” - “A” -> “A” - “MLW” -> “W”

Parameters:

radar (str) – Radar name (can be “ML*” format or just the letter)

Returns:

Normalized single-letter radar name (uppercase)

Return type:

str

Examples

>>> normalize_radar_name("MLA")
'A'
>>> normalize_radar_name("A")
'A'
>>> normalize_radar_name("MLW")
'W'
raddb.open_any_datatree(path: str | Path, engine: str | None = None, **open_kwargs) DataTree[source][source]#

Open a DataTree from disk — NetCDF file or Zarr store.

Engine resolution: an explicit engine wins; a *.zarr suffix or a directory containing .zgroup / zarr.json selects "zarr"; otherwise xarray auto-detects the NetCDF backend (netCDF4 / h5netcdf).

Parameters:
  • path (str or Path) – Path to a NetCDF file or Zarr store.

  • engine (str, optional) – xarray backend override (e.g. "h5netcdf", "zarr").

  • **open_kwargs – Forwarded to xarray.open_datatree().

Return type:

xr.DataTree

raddb.parquet_to_dataframe(radar: str, base_path: str | Path, start_time: str | Timestamp | None = None, end_time: str | Timestamp | None = None, columns: list[str] | None = None, merge_lut: bool = False) DataFrame[source][source]#

Load archived POLAR parquet files as a single DataFrame.

Parameters:
  • radar (str) – Single-letter radar identifier.

  • base_path (str or Path) – RadDB base directory.

  • start_time (optional) – Filter by volume timestamp.

  • end_time (optional) – Filter by volume timestamp.

  • columns (list of str, optional) – Columns to load from parquet files.

  • merge_lut (bool) – If True, merge with the LUT to add spatial coordinates (azimuth, range, latitude, longitude, altitude, x, y, z).

Returns:

Includes a volume_time column (the volume timestamp parsed from each source filename) so a multi-volume frame can be split back into single volumes — used by dataframe_to_datatree() / PPI plotting.

Return type:

pd.DataFrame

raddb.parquet_to_datatree(radar: str, base_path: str | Path, start_time: str | Timestamp | None = None, end_time: str | Timestamp | None = None, label_column: str = 'DBZH', max_workers: int = 1) DataTree[source][source]#

Load archived POLAR parquet files and reconstruct a DataTree.

Loads all volumes in the given time range, joins with the LUT to recover azimuth/range coordinates, and reconstructs an xarray DataTree.

Parameters:
  • radar (str) – Single-letter radar identifier.

  • base_path (str or Path) – RadDB base directory.

  • start_time (optional) – Filter by volume timestamp.

  • end_time (optional) – Filter by volume timestamp.

  • label_column (str) – Column to use for reconstruction (default "DBZH").

  • max_workers (int) – Parallel workers for sweep reconstruction.

Return type:

xr.DataTree

Raises:
raddb.plot_latent_scatter(df: DataFrame, config: list[dict], figsize: tuple[float, float] | None = None, fig_height: float = 4.6, **scatter_kwargs)[source][source]#

Publication-ready 2×3 AMT latent-space scatter figure.

Creates a 2-row × 3-column figure with width=6.9 inches (AMT full-column width). Each subplot shows a scatter of df["L1"] vs df["L2"] coloured by one radar variable. A compact inset colorbar with a white semi-transparent background is placed inside each subplot.

x-tick labels and x-axis labels are shown only on the bottom row. y-tick labels and y-axis labels are shown only on the first column.

Parameters:
  • df (pd.DataFrame) – Must contain columns "L1", "L2", and the variable column named in each panel’s "var" key.

  • config (list of dict) –

    Exactly 6 panel descriptors, one per subplot (row-major: panels 0-2 fill row 0, panels 3-5 fill row 1). Each dict must have:

    • "var" : str — column in df to use for colouring.

    • "label" : str — colorbar label text.

    • "cmap" : str or Colormap — colormap passed to scatter.

    • "norm" : matplotlib Normalize, optional — normalisation.

    • "cbar_kwargs" : dict — extra kwargs forwarded to fig.colorbar() (e.g. {"ticks": [...]}, {"extend": "both"}).

    • "scatter_kwargs" : dict, optional — extra kwargs for ax.scatter() (e.g. {"s": 1, "alpha": 0.5}).

  • figsize (tuple, optional) – Override figure size. Default (6.9, fig_height).

  • fig_height (float) – Figure height in inches. Default 4.6.

  • **scatter_kwargs – Global fallback kwargs for ax.scatter() (overridden per-panel by config[i]["scatter_kwargs"]).

Returns:

  • fig (matplotlib.figure.Figure)

  • axes (ndarray of shape (2, 3))

  • Example config

  • ————–

  • config = [

    {“var”: “DBZH”, “label”: “DBZH [dBZ]”, “cmap”: “turbo”,

    ”norm”: Normalize(-10, 60), “cbar_kwargs”: {}, “scatter_kwargs”: {“s”: 0.5}},

    {“var”: “ZDR”, “label”: “ZDR [dB]”, “cmap”: “RdBu_r”,

    ”norm”: Normalize(-2, 5), “cbar_kwargs”: {}, “scatter_kwargs”: {“s”: 0.5}},

  • ]

raddb.plot_ppi(dt, sweep, variable: str, ax=None, use_cartopy: bool | None = None, coords: str = 'geo', add_range_rings: bool = True, add_colorbar: bool = True, title: str | None = None, figsize: tuple[float, float] = (6, 6), xlim: tuple[float, float] | None = None, ylim: tuple[float, float] | None = None, **plot_kwargs)[source][source]#

Plan Position Indicator from a single sweep of a reconstructed DataTree.

Parameters:
  • dt (xr.DataTree or xr.Dataset) – Reconstructed DataTree (from parquet_to_datatree) or single sweep Dataset.

  • sweep (int or str) – Sweep index (3) or group name ("sweep_3"). Ignored if dt is already a Dataset.

  • variable (str) – Variable to plot: "DBZH", "ZDR", "RHOHV", "PHIDP", "HZT", "TEMP", "HC_MCH", "HC_PYART".

  • ax (matplotlib.axes.Axes, optional) –

  • use_cartopy (bool, optional) – If None (default), auto-detect: use cartopy when installed. Pass False to force plain matplotlib. Pass True to require cartopy (raises ImportError if missing).

  • coords ({"geo", "cartesian", "swiss"}) –

    Coordinate frame for the axes:

    • "geo"(longitude, latitude) degrees.

    • "cartesian"(x, y) km relative to the radar (origin at the site). With cartopy this is an AEQD map centred on the radar.

    • "swiss" (aka "lv95" / "2056") — absolute Swiss LV95 (x_2056, y_2056), axis ticks in km (E from the 2 000 km false-easting, N from 1 000 km). This is the same frame as the AOI quicklook, so a cropped-df PPI lines up with crop_* overlays. With cartopy (use_cartopy) it overlays country borders reprojected to LV95 as a basemap; otherwise the same plot without borders.

  • add_range_rings (bool) – For cartesian / swiss modes, draw 50/100/150 km range rings.

  • add_colorbar (bool) –

  • title (str, optional) –

  • figsize (tuple) – Figure size in inches. Default (6, 6).

  • xlim (tuple, optional) – x-axis limits in the natural units of the chosen coords (degrees for "geo", km for "cartesian").

  • ylim (tuple, optional) – y-axis limits (same units as xlim).

  • **plot_kwargs – Forwarded to pcolormesh (overrides defaults for cmap, vmin, vmax, norm, etc.).

Return type:

matplotlib.collections.QuadMesh

raddb.plot_profiling_dashboard(timer: StageTimer, title_prefix: str = '', save_path: str | None = None)[source][source]#

3x2 profiling dashboard: stage totals, distribution pie, per-volume bars, stage breakdown per volume, and mean time per sweep line chart.

raddb.plot_rhi(dt, azimuth: float, variable: str, radar: str = '', az_tol: float = 1.0, max_range_km: float | None = None, max_height_km: float | None = 20.0, ke: float = 1.3333333333333333, ax=None, add_colorbar: bool = True, title: str | None = None, figsize: tuple[float, float] = (10, 4), **plot_kwargs)[source][source]#

Pseudo-RHI from a volume PPI scan — PyART’s cross_section_ppi.

For every sweep in the DataTree, selects the ray whose azimuth is closest to azimuth (within az_tol, wrap-around safe), regrids all rays to a common range axis, and renders a 2-D (ground_range, height_ASL) pcolormesh using gate edges computed with the 4/3 Earth-radius model — so curved-trapezoid gates and Earth curvature are both physically correct.

Parameters:
  • dt (xr.DataTree) – Reconstructed DataTree (from parquet_to_datatree).

  • azimuth (float) – Target azimuth in degrees (0..360; 0 = North, clockwise).

  • variable (str) –

  • radar (str, optional) – Radar identifier shown in the plot title (e.g. "A").

  • az_tol (float) – Maximum allowed angular distance between the requested azimuth and the nearest available ray.

  • max_range_km (float, optional) – Clip the ground-range axis.

  • max_height_km (float, optional) – Clip the height axis.

  • ke (float) – Effective Earth radius factor (default 4/3, pyart standard).

  • ax (see plot_ppi.) –

  • add_colorbar (see plot_ppi.) –

  • title (see plot_ppi.) –

  • figsize (see plot_ppi.) –

  • **plot_kwargs (see plot_ppi.) –

Return type:

matplotlib.collections.QuadMesh

raddb.plot_stage_totals(timer: StageTimer, title: str = 'Pipeline Total Time per Stage', save_path: str | None = None)[source][source]#

Horizontal bar chart: total wall-clock time per pipeline stage.

raddb.plot_sweep_timing(timer: StageTimer, title: str = 'Processing Time per Sweep', save_path: str | None = None)[source][source]#

Box-plot: distribution of total sweep processing time across volumes.

raddb.plot_volume_timing(timer: StageTimer, title: str = 'Processing Time per Volume', save_path: str | None = None)[source][source]#

Stacked bar chart: per-volume time breakdown by pipeline stage.

raddb.read_parquet_files(base_path: str | Path, pattern: str = '**/*POL.parquet', columns: list[str] | None = None, verbose: bool = True) DataFrame[source][source]#
raddb.reconstruct_datatree(df_joined: DataFrame, lut_path: str | Path, radar_info_path: str | Path, label_column: str = 'hydrometeor_class', max_workers: int = 1) DataTree[source][source]#

Reconstruct a full DataTree from joined data + LUT + radar info.

raddb.reconstruct_sweep_dataset(df_joined: DataFrame, sweep: int, lut_df: DataFrame, radar_info: dict, label_column: str = 'hydrometeor_class', sweep_corners: dict | None = None) Dataset[source][source]#

Reconstruct a single sweep Dataset from joined data.

Data variables come from the filtered POL parquet (NaN where gates were dropped). Per-gate spatial coords (lat/lon/alt/x/y/z and any x_<epsg>/ y_<epsg>) come from the LUT directly so every gate has a valid geometry regardless of filtering.

If sweep_corners is provided, per-gate edge arrays (x_edges, y_edges, z_edges, lon_edges, lat_edges) of shape (n_az+1, n_range+1) are attached as data_vars for pcolormesh rendering with shading="flat".

raddb.scan_polar_parquet(radar: str, base_path: str | Path, start_time: str | pd.Timestamp | None = None, end_time: str | pd.Timestamp | None = None, columns: list[str] | None = None) pl.LazyFrame | None[source][source]#

Scan archived POLAR parquet files as a single polars LazyFrame.

The polars counterpart of parquet_to_dataframe(), used by raddb.RadDB.open(). Scanning (rather than reading) lets polars push the column projection into the parquet reader, and avoids the pandas intermediate frames and the pd.concat copy of the full result.

Unlike parquet_to_dataframe() this never merges the LUT: the static geometry stays in its own table and is joined only by the to_* converters.

Parameters:
  • radar (str) – Single-letter radar identifier.

  • base_path (str or Path) – RadDB base directory.

  • start_time (optional) – Filter by volume timestamp.

  • end_time (optional) – Filter by volume timestamp.

  • columns (list of str, optional) – Columns to project. volume_time / radar are added here rather than read, so they are dropped from the parquet projection.

Returns:

None when no volume matches — callers decide what an empty result means. The frame carries a volume_time and a radar column.

Return type:

pl.LazyFrame or None