Skip to content

Commit d9b90cf

Browse files
committed
perform concatenation using DataTree
1 parent 8f0ab3f commit d9b90cf

2 files changed

Lines changed: 84 additions & 162 deletions

File tree

concatenator/run_stitchee.py

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,6 @@ def parse_args(args: list) -> argparse.Namespace:
4444
)
4545

4646
# Optional arguments
47-
parser.add_argument(
48-
"--copy_input_files",
49-
action="store_true",
50-
help="By default, input files are not copied. "
51-
"This option copies the input files into a temporary directory to avoid modification "
52-
"of input files. This is useful for testing, but uses more disk space. "
53-
"By specifying this argument, no copying is performed.",
54-
)
55-
parser.add_argument(
56-
"--keep_tmp_files",
57-
action="store_true",
58-
help="Prevents removal, after successful execution, of "
59-
"(1) the flattened concatenated file and "
60-
"(2) the input directory copy if created by '--make_dir_copy'.",
61-
)
6247
parser.add_argument(
6348
"--concat_method",
6449
choices=["xarray-concat", "xarray-combine"],
@@ -148,10 +133,8 @@ def validate_parsed_args(
148133
input_files,
149134
output_path,
150135
parsed.concat_dim,
151-
bool(parsed.keep_tmp_files),
152136
parsed.concat_method,
153137
concat_kwargs,
154-
parsed.copy_input_files,
155138
parsed.group_delim,
156139
)
157140

@@ -162,10 +145,8 @@ def run_stitchee(args: list) -> None:
162145
input_files,
163146
output_path,
164147
concat_dim,
165-
keep_tmp_files,
166148
concat_method,
167149
concat_kwargs,
168-
copy_input_files,
169150
group_delimiter,
170151
) = validate_parsed_args(parse_args(args))
171152
num_inputs = len(input_files)
@@ -183,13 +164,10 @@ def run_stitchee(args: list) -> None:
183164
stitchee(
184165
input_files,
185166
output_path,
186-
write_tmp_flat_concatenated=keep_tmp_files,
187-
keep_tmp_files=keep_tmp_files,
188167
concat_method=concat_method,
189168
concat_dim=concat_dim,
190169
concat_kwargs=concat_kwargs,
191170
history_to_append=new_history_json,
192-
copy_input_files=copy_input_files,
193171
group_delimiter=group_delimiter,
194172
)
195173
logging.info("STITCHEE complete. Result in %s", output_path)

concatenator/stitchee.py

Lines changed: 84 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,31 @@
33
from __future__ import annotations
44

55
import logging
6-
import os
76
import shutil
87
import time
9-
from contextlib import ExitStack
108
from logging import Logger
11-
from pathlib import Path
129
from warnings import warn
10+
from functools import reduce
1311

14-
import netCDF4 as nc
1512
import xarray as xr
1613

1714
import 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
2516
from 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

3221
default_logger = logging.getLogger(__name__)
3322

34-
3523
def 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

Comments
 (0)