Skip to content

Commit 4c71b4b

Browse files
committed
Combine radar sweeps into a logical radar volume.
1 parent 6496432 commit 4c71b4b

File tree

2 files changed

+136
-0
lines changed

2 files changed

+136
-0
lines changed

tests/test_util.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
"""Tests for `xradar` util package."""
66

7+
import datetime
8+
import fnmatch
9+
import random
10+
711
import numpy as np
812
import pytest
913
import xarray as xr
@@ -536,3 +540,32 @@ def test_select_sweep_dataset_vars():
536540

537541
metadata = required_metadata + ["polarization_mode", "nyquist_velocity"]
538542
assert sorted(sweep2.data_vars) == sorted(select + ["quality1"] + metadata)
543+
544+
545+
def test_create_volume():
546+
pattern = "T_PAZ?63_C_LFPW_20230420*"
547+
filenames = list(DATASETS.registry.keys())
548+
files = fnmatch.filter(filenames, pattern)
549+
random.shuffle(files)
550+
sweeps = [io.open_odim_datatree(DATASETS.fetch(fn)) for fn in files]
551+
552+
time_coverage_start = "2023-04-20T06:50:00"
553+
time_coverage_start = datetime.datetime.fromisoformat(time_coverage_start)
554+
time_coverage_end = "2023-04-20T06:55:00"
555+
time_coverage_end = datetime.datetime.fromisoformat(time_coverage_end)
556+
print(sweeps[0].ds.sweep_group_name)
557+
volume = util.create_volume(
558+
sweeps=sweeps,
559+
time_coverage_start=time_coverage_start,
560+
time_coverage_end=time_coverage_end,
561+
min_angle=0.5,
562+
max_angle=5.0,
563+
)
564+
np.testing.assert_array_equal(volume.ds.sweep_group_name, [0, 1, 2])
565+
angles = [3.6, 1.6, 1.0]
566+
np.testing.assert_array_equal(volume.ds.sweep_fixed_angle, angles)
567+
sweep_fixed_angle = [
568+
volume[sweep].ds.sweep_fixed_angle.values.item()
569+
for sweep in util.get_sweep_keys(volume)
570+
]
571+
np.testing.assert_array_equal(sweep_fixed_angle, angles)

xradar/util.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@
2929
"get_sweep_dataset_vars",
3030
"get_sweep_metadata_vars",
3131
"select_sweep_dataset_vars",
32+
"create_volume",
3233
]
3334

3435
__doc__ = __doc__.format("\n ".join(__all__))
3536

3637
import contextlib
38+
import datetime
3739
import functools
3840
import gzip
3941
import importlib.util
@@ -513,6 +515,7 @@ def get_sweep_keys(dtree):
513515
keys : list
514516
List of associated keys with sweep_n
515517
"""
518+
516519
sweep_group_keys = []
517520
for key in list(dtree.children):
518521
parts = key.split("_")
@@ -740,3 +743,103 @@ def select_sweep_dataset_vars(sweep, select, ancillary=False, optional_metadata=
740743
sweep_out = sweep[select]
741744

742745
return sweep_out
746+
747+
748+
def create_volume(
749+
sweeps: list[xr.DataTree],
750+
time_coverage_start: datetime.datetime = None,
751+
time_coverage_end: datetime.datetime = None,
752+
min_angle: float = None,
753+
max_angle: float = None,
754+
volume_number: int = 0,
755+
) -> xr.DataTree:
756+
"""
757+
Combine sweeps into a single stacked radar volume with optional time and angle filtering.
758+
759+
Parameters
760+
----------
761+
sweeps : list of xr.DataTree
762+
Each DataTree represents one or more radar sweeps.
763+
time_coverage_start : datetime, optional
764+
Minimum start time for sweeps to include.
765+
time_coverage_end : datetime, optional
766+
Maximum start time for sweeps to include.
767+
min_angle : float, optional
768+
Minimum sweep_fixed_angle to include (inclusive).
769+
max_angle : float, optional
770+
Maximum sweep_fixed_angle to include (inclusive).
771+
volume_number : int, default 0
772+
Identifier for the volume, stored in the root node's attributes.
773+
774+
Returns
775+
-------
776+
xr.DataTree
777+
A volume tree with root metadata and child nodes named 'sweep_0', 'sweep_1', etc.
778+
"""
779+
780+
# Step 1: Extract (ds, time, angle) tuples from all sweeps
781+
sweep_entries = []
782+
for dt in sweeps:
783+
for key in get_sweep_keys(dt):
784+
ds = xr.decode_cf(dt[key].ds)
785+
time = ds["time"].values.astype("datetime64[ns]").astype("int64")
786+
mask = time != np.iinfo("int64").min
787+
time = time[mask]
788+
t0 = np.min(time)
789+
t0 = datetime.datetime.fromtimestamp(t0 / 1e9)
790+
angle = float(ds["sweep_fixed_angle"].item())
791+
sweep_entries.append((ds, t0, angle))
792+
793+
# Step 2: Filter by time bounds
794+
if time_coverage_start is not None:
795+
sweep_entries = [
796+
entry for entry in sweep_entries if entry[1] >= time_coverage_start
797+
]
798+
if time_coverage_end is not None:
799+
sweep_entries = [
800+
entry for entry in sweep_entries if entry[1] <= time_coverage_end
801+
]
802+
803+
# Step 3: Filter by angle bounds
804+
if min_angle is not None:
805+
sweep_entries = [entry for entry in sweep_entries if entry[2] >= min_angle]
806+
if max_angle is not None:
807+
sweep_entries = [entry for entry in sweep_entries if entry[2] <= max_angle]
808+
809+
# Step 4: Sort by time
810+
sweep_entries.sort(key=lambda x: x[1])
811+
812+
if not sweep_entries:
813+
raise ValueError("No sweeps remain after filtering.")
814+
815+
# Step 5: Prepare root metadata
816+
root_ds = sweeps[0].ds.copy(deep=True)
817+
root_ds = root_ds.drop_vars(
818+
["sweep_group_name", "sweep_fixed_angle"], errors="ignore"
819+
)
820+
821+
root_ds["sweep_group_name"] = xr.DataArray(
822+
np.array([f"sweep_{i}" for i in range(len(sweep_entries))], dtype="S"),
823+
dims="sweep",
824+
)
825+
root_ds["sweep_fixed_angle"] = xr.DataArray(
826+
[angle for _, _, angle in sweep_entries], dims="sweep"
827+
)
828+
829+
# Step 6: Assign time coverage
830+
def format_zulu(dt: datetime.datetime) -> str:
831+
return dt.strftime("%Y-%m-%dT%H:%M:%SZ")
832+
833+
time_coverage_start = time_coverage_start or sweep_entries[0][1]
834+
time_coverage_end = time_coverage_end or sweep_entries[-1][1]
835+
836+
root_ds["time_coverage_start"] = format_zulu(time_coverage_start)
837+
root_ds["time_coverage_end"] = format_zulu(time_coverage_end)
838+
root_ds.attrs["volume_number"] = volume_number
839+
840+
# Step 7: Assemble final tree
841+
volume = xr.DataTree(root_ds, name="root")
842+
for i, (ds, _, _) in enumerate(sweep_entries):
843+
volume[f"sweep_{i}"] = xr.DataTree(ds.copy(deep=True))
844+
845+
return volume

0 commit comments

Comments
 (0)