Skip to main content

RadDB: Radar Database

Deployment PyPI Conda
Activity PyPI Downloads Conda Downloads
Python Versions Python Versions
Supported Systems Linux macOS Windows
Project Status Project Status
Build Status Tests Lint Docs
Linting Black Ruff Codespell
Code Coverage Coveralls Codecov
Code Quality Codefactor Codacy Codescene
License License

| Citation | DOI | Documentation

RadDB archives xarray DataTree radar volumes as compact Parquet files and gives you a small, fluent interface to load, filter, crop, extract cross-section and plot them. It is network-agnostic: any DataTree with the standard xradar coordinate layout (NEXRAD, ODIM, IRIS, …) can be archived and analysed.

Storage model

A radar is stored as a static LUT (per-gate geometry, generated once) plus one POL parquet per volume (the variables), linked by an integer gate_id:

{archive_dir}/{radar}/LUT/{radar}_LUT.parquet          # gate_id, lat/lon/alt, x_<epsg>/y_<epsg>, sweep, …
{archive_dir}/{radar}/{YYYY}/{MM}/{DD}/{radar}_{YYYYMMDD}_{HHMMSS}_POL.parquet

No-echo gates are dropped at archive time (default DBZH > 0), so the archive stays small: a 12-sweep WSR-88D volume of 8,791,200 polar gates becomes 8.2 MB.

Installation

pip install raddb
pip install "raddb[viz]"       # interactive Jupyter map and cartopy basemaps

Core runtime dependencies: numpy, pandas, polars, geopandas, shapely, pyproj, xarray, pyyaml, pyarrow, matplotlib, netcdf4, zarr.

Quick start

RadDB is a single, dual-role class:

  • archive-bound — db = RadDB(archive_dir, crs=…); use it to archive() and open().
  • data-carrying — the object returned by open() (call it rdf). It holds the data as a polars DataFrame (rdf.data) and exposes the query / convert / crop / cross-section / plot methods. Each of those returns a new RadDB, so calls chain fluently.
import raddb

db = raddb.RadDB(archive_dir="/data/raddb", crs=32614)

A projected CRS is mandatory to write an archive and never needed to read one. There is no default: the wrong projection is silently wrong. raddb.lut.suggest_crs(longitude, latitude) tells you which one to pass.

Archive

# From saved DataTree files on disk (.zarr / .nc); the LUT is auto-generated:
db.archive(datatree_dir="/data/NEXRAD_datatree")  # radar inferred per file

# ...or archive in-memory DataTrees directly:
db.archive(datatree=dt, radar="KTLX")
db.archive(datatree=[dt1, dt2], radar="KTLX")
db.archive(datatree={"KTLX": [dt1], "KMLB": [dt2]})  # multi-radar

Pass filter= to decide which gates ever reach the disk — the main control on archive size:

db.archive(
    datatree=dt, radar="KTLX", filter={"var": "DBZH", "logic": ">", "threshold": 20}
)

Open

rdf = db.open(time_period=("2024-06-12", "2024-06-13"), radars="KTLX")
print(rdf)  # summary: gates, radars, time range, columns

len(rdf), rdf.columns(), rdf.radars()
rdf.start_time(), rdf.end_time()
rdf.extent()  # [xmin, xmax, ymin, ymax] in `crs`
rdf.geographic_extent()  # [lon_min, lon_max, lat_min, lat_max]
rdf.crs(), rdf.geographic_crs()

columns= and filters= are pushed down into the scan, so only the rows you asked for are ever materialised.

Filter and convert

filtered_rdf = rdf.filter({"var": "DBZH", "logic": ">", "threshold": 20})
filtered_rdf = rdf.filter(
    [
        {"var": "DBZH", "logic": ">", "threshold": 20},
        {"var": "RHOHV", "logic": ">", "threshold": 0.9},
    ]
)

df = rdf.to_pandas(with_geometry=True)  # pandas + gate coordinates
gdf = rdf.to_geopandas()  # GeoDataFrame (with CRS)
dt = rdf.to_datatree()  # back to xarray

Filters are {"var", "logic", "threshold"} dicts, where logic is one of ==, !=, >, >=, <, <=. crs is an EPSG int (e.g. 32614), a CRS object, or None.

Crop to an area of interest

box = rdf.crop_by_bbox(extent=[636_504, 676_504, 3_891_333, 3_931_333])
poly = rdf.crop_by_polygone("catchment.geojson")
disc = rdf.crop_around_point((656_504, 3_911_333), distance=20_000)  # metres
# rdf.interactive_crop()   # draw an AOI on a Jupyter map

Cut a cross-section

cs = rdf.extract_cross_section(p1=(626_504, 3_911_333), p2=(686_504, 3_911_333))

Plot

rdf.plot_ppi(sweep=1, variable="DBZH", save="ppi.png")
rdf.plot_rhi(azimuth=270, variable="DBZH")
rdf.plot_cappi(altitude=3000, variable="DBZH")
cs.plot_cross_section(variable="DBZH", save="xsec.png")

Each plot draws into one Axes and returns the matplotlib artist, so you compose panels by passing ax=.

Chain them

rdf.filter({"var": "DBZH", "logic": ">", "threshold": 20}).crop_by_bbox(
    extent=rdf.extent()
).plot_ppi(variable="DBZH", save="ppi_plot_example.png")

What is on disk? (archive-bound)

db.inventory()  # radars, volume counts, time ranges, size
db.inventory(detailed=True)  # + LUT info, stored variables, day-by-day counts
db.inventory(datatree_dir="/data/NEXRAD_datatree")  # DataTree files not archived yet

LUT accessors (archive-bound)

db.list_radars()  # radars present in the archive
db.get_lut("KTLX")  # the static LUT (polars)
db.get_radar_info("KTLX")  # site location / sweep geometry
db.add_lut_projection("KTLX", epsg=32614)

Module structure

raddb/
├── __init__.py
├── main.py         # the RadDB class
├── io_core.py      # DataTree <-> DataFrame <-> Parquet + archive backends
├── lut.py          # LUT generation / geo projection
├── aoi.py          # AOI / crop / cross-section geometry
├── discovery.py    # find_datatree_files + filename-time parsing
├── helper.py       # filters, radar-name normalisation, timers
└── viz/            # plot.py (PPI/RHI/CAPPI/cross-section), interactive.py

Notes

  • Projected coordinates / crs. Generating a LUT with a projection (e.g. crs=32614) and the projected accessors (extent, to_geopandas) use pyproj, which needs the PROJ database. A PROJ_DATA / PROJ_LIB inherited from another environment (a conda base env, a system PROJ) points at a proj.db of the wrong PROJ version and makes every projection fail with "no database context specified" — import raddb detects that and repoints PROJ at the running interpreter's own share/proj (see raddb/_proj.py; raddb.PROJ_DATA reports what it changed, None when nothing had to be).
  • Zarr / NetCDF. Saved volumes are read back with raddb.open_any_datatree (engine auto-detected); either format works.

License

See LICENSE (MIT).

Metadata

Release files for raddb 0.0.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for raddb 0.0.3
File Size Uploaded Provenance Actions
raddb-0.0.3.tar.gz 139.5 kB Without provenance Details for raddb-0.0.3.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for raddb 0.0.3
File Interpreter ABI Platform Actions
raddb-0.0.3-py3-none-any.whl Python 3 none any Details for raddb-0.0.3-py3-none-any.whl

Total release size: 267.2 kB

Release files / raddb-0.0.3.tar.gz

Download URL raddb-0.0.3.tar.gz
Size 139.5 kB
Tags Source
SHA-256 checksum
How to use checksums
c67b2cf705fb34ca50e64d945113c9fdd7f2c30e6f77b11710c7ca782e4ac045
BLAKE2b-256 checksum
How to use checksums
f837a93fbd26b3a22fd09b0a1878ceec7e66ae77c8f992fac764668e25ea0de0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / raddb-0.0.3-py3-none-any.whl

Download URL raddb-0.0.3-py3-none-any.whl
Size 127.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1ec3de9096812c133785ffd8d83618eb9ff5ed03afc7bf0a3256d0f879effa3c
BLAKE2b-256 checksum
How to use checksums
b486dbce7e992812d43d4ff7b27b1ab610a55d1ec7e232df20498e5335157321
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Security for raddb 0.0.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page