33from __future__ import annotations
44
55import logging
6- import os
76import shutil
87import time
9- from contextlib import ExitStack
108from logging import Logger
11- from pathlib import Path
129from warnings import warn
10+ from functools import reduce
1311
14- import netCDF4 as nc
1512import xarray as xr
1613
1714import concatenator
18- from concatenator .attribute_handling import flatten_string_with_groups
19- from concatenator .dataset_and_group_handling import (
20- flatten_grouped_dataset ,
21- regroup_flattened_dataset ,
22- validate_workable_files ,
23- )
24- from concatenator .dimension_cleanup import remove_duplicate_dims
15+ from concatenator .dataset_and_group_handling import validate_workable_files
2516from concatenator .file_ops import (
26- add_label_to_path ,
27- make_temp_dir_with_input_file_copies ,
2817 validate_input_path ,
2918 validate_output_path ,
3019)
3120
3221default_logger = logging .getLogger (__name__ )
3322
34-
3523def stitchee (
3624 files_to_concat : list [str ],
3725 output_file : str ,
38- write_tmp_flat_concatenated : bool = False ,
39- keep_tmp_files : bool = True ,
4026 concat_method : str | None = "xarray-concat" ,
4127 concat_dim : str = "" ,
4228 concat_kwargs : dict | None = None ,
4329 sorting_variable : str | None = None ,
4430 history_to_append : str | None = None ,
45- copy_input_files : bool = False ,
4631 overwrite_output_file : bool = False ,
4732 group_delimiter : str = "__" ,
4833 logger : Logger = default_logger ,
@@ -55,10 +40,6 @@ def stitchee(
5540 netCDF files to concatenate
5641 output_file
5742 file path for output file
58- write_tmp_flat_concatenated
59- whether to save intermediate flattened files or not (default: False).
60- keep_tmp_files
61- whether to keep all temporary files created (default: True).
6243 concat_method
6344 either "xarray-concat" (default) or "xarray-combine".
6445 concat_dim
@@ -71,8 +52,6 @@ def stitchee(
7152 E.g., `time`.
7253 history_to_append
7354 JSON string to append to the history attribute of the concatenated file (default: None).
74- copy_input_files
75- whether to copy input files or not (default: False).
7655 overwrite_output_file
7756 whether to overwrite output file (default: False).
7857 group_delimiter
@@ -87,11 +66,8 @@ def stitchee(
8766 validate_input_path (files_to_concat )
8867 concatenator .group_delim = group_delimiter
8968
90- intermediate_flat_filepaths : list [str ] = []
9169 benchmark_log = {
92- "flattening" : 0.0 ,
9370 "concatenating" : 0.0 ,
94- "reconstructing_groups" : 0.0 ,
9571 }
9672
9773 # Proceed to concatenate only files that are workable (can be opened and are not empty).
@@ -116,134 +92,102 @@ def stitchee(
11692 "selected."
11793 )
11894
119- # If requested, make a temporary directory with new copies of the original input files
120- temporary_dir_to_remove = None
121- if copy_input_files :
122- input_files , temporary_dir_to_remove = make_temp_dir_with_input_file_copies (
123- input_files , Path (output_file )
124- )
125-
12695 try :
127- # Instead of "with nc.Dataset() as" inside the loop, we use a context manager stack.
128- # This way all files are cleanly closed outside the loop.
129- with ExitStack () as context_stack :
130- logger .info ("Flattening all input files..." )
131- xrdataset_list = []
132- concat_dim_order = []
133- for i , filepath in enumerate (input_files ):
134- # The group structure is flattened.
135- start_time = time .time ()
136- logger .info (" ..file %03d/%03d <%s>.." , i + 1 , num_input_files , filepath )
137-
138- ncfile = context_stack .enter_context (nc .Dataset (filepath , "r+" ))
139-
140- flat_dataset , coord_vars , _ = flatten_grouped_dataset (
141- ncfile , ensure_all_dims_are_coords = True
142- )
143-
144- logger .info ("Removing duplicate dimensions" )
145- flat_dataset = remove_duplicate_dims (flat_dataset )
146-
147- logger .info ("Opening flattened file with xarray." )
148- xrds = xr .open_dataset (
149- xr .backends .NetCDF4DataStore (flat_dataset ),
150- decode_times = False ,
151- decode_coords = False ,
152- drop_variables = coord_vars ,
153- mask_and_scale = False ,
154- )
155-
156- # Determine value for later dataset sorting.
157- if sorting_variable :
158- first_value = xrds [
159- flatten_string_with_groups (sorting_variable )
160- ].values .flatten ()[0 ]
161- else :
162- first_value = i
163- # first_value = xrds[concatenator.group_delim + concat_dim].values.flatten()[0]
164- concat_dim_order .append (first_value )
165-
166- benchmark_log ["flattening" ] = time .time () - start_time
167-
168- # The flattened file is written to disk.
169- # flat_file_path = add_label_to_path(filepath, label="_flat_intermediate")
170- # xrds.to_netcdf(flat_file_path, encoding={v_name: {'dtype': 'str'} for v_name in string_vars})
171- # intermediate_flat_filepaths.append(flat_file_path)
172- # xrdataset_list.append(xr.open_dataset(flat_file_path))
173- xrdataset_list .append (xrds )
174-
175- # Reorder the xarray datasets according to the concat dim values.
176- xrdataset_list = [
177- dataset
178- for _ , dataset in sorted (zip (concat_dim_order , xrdataset_list ), key = lambda x : x [0 ])
179- ]
180-
181- # Flattened files are concatenated together (Using XARRAY).
96+ logger .info ("Flattening all input files..." )
97+ xrdatatree_list = []
98+ concat_dim_order = []
99+ for i , filepath in enumerate (input_files ):
100+ # The group structure is flattened.
182101 start_time = time .time ()
183- logger .info ("Concatenating flattened files..." )
184- # combined_ds = xr.open_mfdataset(intermediate_flat_filepaths,
185- # decode_times=False,
186- # decode_coords=False,
187- # data_vars='minimal',
188- # coords='minimal',
189- # compat='override')
190-
191- if concat_kwargs is None :
192- concat_kwargs = {}
193-
194- if concat_method == "xarray-concat" :
195- combined_ds = xr .concat (
196- xrdataset_list ,
197- dim = concatenator .group_delim + concat_dim ,
102+ logger .info (" ..file %03d/%03d <%s>.." , i + 1 , num_input_files , filepath )
103+
104+ logger .info ("Opening flattened file with xarray." )
105+ datatree = xr .open_datatree (
106+ filepath ,
107+ decode_times = False ,
108+ decode_coords = False ,
109+ mask_and_scale = False ,
110+ )
111+
112+ # Determine value for later dataset sorting.
113+ if sorting_variable :
114+ first_value = datatree [sorting_variable ].values .flatten ()[0 ]
115+ else :
116+ first_value = i
117+ # first_value = xrds[concatenator.group_delim + concat_dim].values.flatten()[0]
118+ concat_dim_order .append (first_value )
119+
120+ xrdatatree_list .append (datatree )
121+
122+ # Reorder the xarray datasets according to the concat dim values.
123+ xrdatatree_list = [
124+ datatree
125+ for _ , datatree in sorted (zip (concat_dim_order , xrdatatree_list ), key = lambda x : x [0 ])
126+ ]
127+
128+ tree_dicts = [tree .to_dict () for tree in xrdatatree_list ]
129+ keys_list = [set (t .keys ()) for t in tree_dicts ]
130+ symmetric_diff = reduce (lambda x , y : x ^ y , keys_list )
131+
132+ if symmetric_diff :
133+ raise KeyError (f"Datatrees do not have matching Dataset nodes. Nodes that do not match: { symmetric_diff } " )
134+
135+ # Files are concatenated together (Using XARRAY).
136+ start_time = time .time ()
137+ logger .info ("Concatenating files..." )
138+ # combined_ds = xr.open_mfdataset(intermediate_flat_filepaths,
139+ # decode_times=False,
140+ # decode_coords=False,
141+ # data_vars='minimal',
142+ # coords='minimal',
143+ # compat='override')
144+
145+ if concat_kwargs is None :
146+ concat_kwargs = {}
147+
148+ tree_keys = keys_list [0 ]
149+
150+ if concat_method == "xarray-concat" :
151+ combined_dict = {
152+ kk : xr .concat (
153+ [tree_dict [kk ] for tree_dict in tree_dicts ],
198154 data_vars = "minimal" ,
199155 coords = "minimal" ,
200156 ** concat_kwargs ,
157+ dim = concat_dim ,
201158 )
202- elif concat_method == "xarray-combine" :
203- combined_ds = xr .combine_by_coords (
204- xrdataset_list ,
159+ for kk in tree_keys
160+ }
161+
162+ elif concat_method == "xarray-combine" :
163+ combined_dict = {
164+ kk : xr .combine_by_coords (
165+ [tree_dict [kk ] for tree_dict in tree_dicts ],
205166 data_vars = "minimal" ,
206167 coords = "minimal" ,
207168 ** concat_kwargs ,
169+ dim = concat_dim ,
208170 )
209- else :
210- raise ValueError (f"Unexpected concatenation method, <{ concat_method } >." )
171+ for kk in tree_keys
172+ }
173+ else :
174+ raise ValueError (f"Unexpected concatenation method, <{ concat_method } >." )
211175
212- benchmark_log [ "concatenating" ] = time . time () - start_time
176+ xr . DataTree . from_dict ( combined_dict ). to_netcdf ( output_file )
213177
214- if write_tmp_flat_concatenated :
215- logger .info ("Writing concatenated flattened temporary file to disk..." )
216- # The concatenated, yet still flat, file is written to disk for debugging.
217- tmp_flat_concatenated_path = add_label_to_path (
218- output_file , label = "_flat_intermediate"
219- )
220- combined_ds .to_netcdf (tmp_flat_concatenated_path , format = "NETCDF4" )
221- else :
222- tmp_flat_concatenated_path = None
178+ benchmark_log ["concatenating" ] = time .time () - start_time
223179
224- # new_global_attributes = create_new_attributes(combined_ds, request_parameters=dict())
180+ # new_global_attributes = create_new_attributes(combined_ds, request_parameters=dict())
225181
226- # The group hierarchy of the concatenated file is reconstructed (using XARRAY).
227- start_time = time .time ()
228- logger .info ("Reconstructing groups within concatenated file..." )
229- regroup_flattened_dataset (combined_ds , output_file , history_to_append )
230- benchmark_log ["reconstructing_groups" ] = time .time () - start_time
231-
232- logger .info ("--- Benchmark results ---" )
233- total_time = 0.0
234- for k , v in benchmark_log .items ():
235- logger .info ("%s: %f" , k , v )
236- total_time += v
237- logger .info ("-- total processing time: %f" , total_time )
238-
239- # If requested, remove temporary intermediate files.
240- if not keep_tmp_files :
241- for file in intermediate_flat_filepaths :
242- os .remove (file )
243- if tmp_flat_concatenated_path :
244- os .remove (tmp_flat_concatenated_path )
245- if not keep_tmp_files and temporary_dir_to_remove :
246- shutil .rmtree (temporary_dir_to_remove )
182+ # The group hierarchy of the concatenated file is reconstructed (using XARRAY).
183+ start_time = time .time ()
184+
185+ logger .info ("--- Benchmark results ---" )
186+ total_time = 0.0
187+ for k , v in benchmark_log .items ():
188+ logger .info ("%s: %f" , k , v )
189+ total_time += v
190+ logger .info ("-- total processing time: %f" , total_time )
247191
248192 except Exception as err :
249193 logger .info ("Stitchee encountered an error!" )
0 commit comments