Skip to content

Commit 4ebd2a6

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

File tree

2 files changed

+135
-0
lines changed

2 files changed

+135
-0
lines changed

tests/test_util.py

Lines changed: 46 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,45 @@ 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:00Z"
553+
time_coverage_start = datetime.datetime.fromisoformat(time_coverage_start)
554+
time_coverage_end = "2023-04-20T06:55:00Z"
555+
time_coverage_end = datetime.datetime.fromisoformat(time_coverage_end)
556+
volume = util.create_volume(
557+
sweeps=sweeps,
558+
time_coverage_start=time_coverage_start,
559+
time_coverage_end=time_coverage_end,
560+
min_angle=0.5,
561+
max_angle=5.0,
562+
)
563+
assert volume.ds.time_coverage_start == "2023-04-20T06:50:00Z"
564+
assert volume.ds.time_coverage_end == "2023-04-20T06:55:00Z"
565+
sweep_group_name = ["sweep_0", "sweep_1", "sweep_2"]
566+
np.testing.assert_array_equal(volume.ds.sweep_group_name, sweep_group_name)
567+
angles = [3.6, 1.6, 1.0]
568+
np.testing.assert_array_equal(volume.ds.sweep_fixed_angle, angles)
569+
sweep_fixed_angle = [
570+
volume[sweep].ds.sweep_fixed_angle.values.item()
571+
for sweep in util.get_sweep_keys(volume)
572+
]
573+
np.testing.assert_array_equal(sweep_fixed_angle, angles)
574+
575+
time_coverage_start = "2023-04-19T06:50:00Z"
576+
time_coverage_start = datetime.datetime.fromisoformat(time_coverage_start)
577+
time_coverage_end = "2023-04-19T06:55:00Z"
578+
time_coverage_end = datetime.datetime.fromisoformat(time_coverage_end)
579+
with pytest.raises(ValueError, match="No sweeps remain after filtering."):
580+
volume = util.create_volume(
581+
sweeps=sweeps,
582+
time_coverage_start=time_coverage_start,
583+
time_coverage_end=time_coverage_end,
584+
)

xradar/util.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
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__))
@@ -513,6 +514,7 @@ def get_sweep_keys(dtree):
513514
keys : list
514515
List of associated keys with sweep_n
515516
"""
517+
516518
sweep_group_keys = []
517519
for key in list(dtree.children):
518520
parts = key.split("_")
@@ -740,3 +742,90 @@ def select_sweep_dataset_vars(sweep, select, ancillary=False, optional_metadata=
740742
sweep_out = sweep[select]
741743

742744
return sweep_out
745+
746+
747+
def format_zulu(t: np.datetime64) -> str:
748+
return str(t).split(".")[0] + "Z"
749+
750+
751+
def create_volume(
752+
sweeps: list[xr.DataTree],
753+
time_coverage_start: np.datetime64 = None,
754+
time_coverage_end: np.datetime64 = None,
755+
min_angle: float = None,
756+
max_angle: float = None,
757+
volume_number: int = 0,
758+
) -> xr.DataTree:
759+
"""
760+
Combine sweeps into a single stacked radar volume with optional time and angle filtering.
761+
All timestamps are handled as np.datetime64[ns] internally.
762+
"""
763+
764+
# Normalize time bounds
765+
if time_coverage_start is not None:
766+
time_coverage_start = np.datetime64(time_coverage_start, "ns")
767+
if time_coverage_end is not None:
768+
time_coverage_end = np.datetime64(time_coverage_end, "ns")
769+
770+
# Step 1: Extract (ds, time, angle) tuples
771+
sweep_entries = []
772+
for dt in sweeps:
773+
for key in get_sweep_keys(dt):
774+
ds = xr.decode_cf(dt[key].ds)
775+
time = ds["time"].values.astype("datetime64[ns]")
776+
mask = time != np.datetime64("NaT", "ns")
777+
time = time[mask]
778+
t0 = time.min()
779+
angle = float(ds["sweep_fixed_angle"].item())
780+
sweep_entries.append((ds, t0, angle))
781+
782+
# Step 2: Filter by time
783+
if time_coverage_start is not None:
784+
sweep_entries = [
785+
entry for entry in sweep_entries if entry[1] >= time_coverage_start
786+
]
787+
if time_coverage_end is not None:
788+
sweep_entries = [
789+
entry for entry in sweep_entries if entry[1] <= time_coverage_end
790+
]
791+
792+
# Step 3: Filter by angle
793+
if min_angle is not None:
794+
sweep_entries = [entry for entry in sweep_entries if entry[2] >= min_angle]
795+
if max_angle is not None:
796+
sweep_entries = [entry for entry in sweep_entries if entry[2] <= max_angle]
797+
798+
# Step 4: Sort by time
799+
sweep_entries.sort(key=lambda x: x[1])
800+
801+
if not sweep_entries:
802+
raise ValueError("No sweeps remain after filtering.")
803+
804+
# Step 5: Prepare root metadata
805+
root_ds = sweeps[0].ds.copy(deep=True)
806+
root_ds = root_ds.drop_vars(
807+
["sweep_group_name", "sweep_fixed_angle"], errors="ignore"
808+
)
809+
810+
root_ds["sweep_group_name"] = xr.DataArray(
811+
np.array([f"sweep_{i}" for i in range(len(sweep_entries))]),
812+
dims="sweep",
813+
)
814+
root_ds["sweep_fixed_angle"] = xr.DataArray(
815+
[angle for _, _, angle in sweep_entries], dims="sweep"
816+
)
817+
818+
# Step 6: Assign time coverage
819+
time_coverage_start = time_coverage_start or sweep_entries[0][1]
820+
time_coverage_end = time_coverage_end or sweep_entries[-1][1]
821+
822+
root_ds["time_coverage_start"] = format_zulu(time_coverage_start)
823+
root_ds["time_coverage_end"] = format_zulu(time_coverage_end)
824+
root_ds.attrs["volume_number"] = volume_number
825+
826+
# Step 7: Assemble final tree
827+
volume = xr.DataTree(root_ds, name="root")
828+
for i, (ds, _, _) in enumerate(sweep_entries):
829+
volume[f"sweep_{i}"] = xr.DataTree(ds.copy(deep=True))
830+
831+
return volume

0 commit comments

Comments
 (0)