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