|
29 | 29 | "get_sweep_dataset_vars", |
30 | 30 | "get_sweep_metadata_vars", |
31 | 31 | "select_sweep_dataset_vars", |
| 32 | + "create_volume", |
32 | 33 | ] |
33 | 34 |
|
34 | 35 | __doc__ = __doc__.format("\n ".join(__all__)) |
35 | 36 |
|
36 | 37 | import contextlib |
| 38 | +import datetime |
37 | 39 | import functools |
38 | 40 | import gzip |
39 | 41 | import importlib.util |
@@ -513,6 +515,7 @@ def get_sweep_keys(dtree): |
513 | 515 | keys : list |
514 | 516 | List of associated keys with sweep_n |
515 | 517 | """ |
| 518 | + |
516 | 519 | sweep_group_keys = [] |
517 | 520 | for key in list(dtree.children): |
518 | 521 | parts = key.split("_") |
@@ -740,3 +743,103 @@ def select_sweep_dataset_vars(sweep, select, ancillary=False, optional_metadata= |
740 | 743 | sweep_out = sweep[select] |
741 | 744 |
|
742 | 745 | 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