diff --git a/docs/source/howto/run_httomo.rst b/docs/source/howto/run_httomo.rst index cabac0a72..142e55626 100644 --- a/docs/source/howto/run_httomo.rst +++ b/docs/source/howto/run_httomo.rst @@ -183,6 +183,8 @@ The :code:`run` command Number of frames per-chunk in intermediate data (0 = write as contiguous, -1 = decide automatically) [x>=-1] + --recon-filename-stem TEXT Name of output recon file without file + extension (assumes `.h5`) --help Show this message and exit. Arguments @@ -224,7 +226,7 @@ directory created by HTTomo would be Options/flags ############# -The :code:`run` command has 13 options/flags: +The :code:`run` command has 14 options/flags: - :code:`--output-folder-name` - :code:`--save-all` @@ -239,6 +241,7 @@ The :code:`run` command has 13 options/flags: - :code:`--syslog-host` - :code:`--syslog-port` - :code:`--frames-per-chunk` +- :code:`--recon-filename-stem` :code:`--output-folder-name` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -448,3 +451,23 @@ This flag sets the number of frames in a chunk for the intermediate file. For most cases the default -1 should be sufficient as the actual number of frames in a chunk is optimised by considering the saturation bandwidth of the filesystem. + +:code:`--recon-filename-stem` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default, if the output of a method is saved to a file, the filename is of +the form :code:`task_{N}-{PACKAGE_NAME}-{METHOD_NAME}.h5`, where: + +- :code:`N` is the index of the method in the pipeline (zero-indexing) +- :code:`PACKAGE_NAME` is the name of the package that the method comes from +- :code:`METHOD_NAME` is the name of the method + +For the output of a reconstruction method specifically, if a filename different +to the above format is desired, this can be provided using the +:code:`--recon-filename-stem` flag. The files created will always be hdf5 +files, so the file extension should always be :code:`h5`. Therefore, only the +"stem" of the desired filename needs to be provided (the part of the filename +before the file extension). + +For example, if the desired reconstruction filename is :code:`my-recon.h5`, +then the flag should be used as :code:`--recon-filename-stem=my-recon`. diff --git a/httomo/cli.py b/httomo/cli.py index 21a3785a7..a66c808aa 100644 --- a/httomo/cli.py +++ b/httomo/cli.py @@ -144,6 +144,12 @@ def check(yaml_config: Path, in_data_file: Optional[Path] = None): "(0 = write as contiguous, -1 = decide automatically)" ), ) +@click.option( + "--recon-filename-stem", + type=click.STRING, + default=None, + help="Name of output recon file without file extension (assumes `.h5`)", +) def run( in_data_file: Path, yaml_config: Path, @@ -161,6 +167,7 @@ def run( syslog_host: str, syslog_port: int, frames_per_chunk: int, + recon_filename_stem: Optional[str], ): """Run a pipeline defined in YAML on input data.""" set_global_constants( @@ -172,6 +179,7 @@ def run( syslog_host, syslog_port, output_folder_name, + recon_filename_stem, ) does_contain_sweep = is_sweep_pipeline(yaml_config) @@ -250,6 +258,7 @@ def set_global_constants( syslog_host: str, syslog_port: int, output_folder_name: Optional[Path], + recon_filename_stem: Optional[str], ) -> None: if compress_intermediate and frames_per_chunk == 0: # 0 means write contiguously but compression must have chunk @@ -260,6 +269,7 @@ def set_global_constants( httomo.globals.FRAMES_PER_CHUNK = frames_per_chunk httomo.globals.SYSLOG_SERVER = syslog_host httomo.globals.SYSLOG_PORT = syslog_port + httomo.globals.RECON_FILENAME_STEM = recon_filename_stem if output_folder_name is None: httomo.globals.run_out_dir = out_dir.joinpath( diff --git a/httomo/globals.py b/httomo/globals.py index a6ce1cfd0..1110dc63f 100644 --- a/httomo/globals.py +++ b/httomo/globals.py @@ -1,5 +1,6 @@ import os from pathlib import Path +from typing import Optional run_out_dir: os.PathLike = Path(".") gpu_id: int = -1 @@ -14,3 +15,4 @@ COMPRESS_INTERMEDIATE: bool = False SYSLOG_SERVER = "localhost" SYSLOG_PORT = 514 +RECON_FILENAME_STEM: Optional[str] = None diff --git a/httomo/method_wrappers/save_intermediate.py b/httomo/method_wrappers/save_intermediate.py index 893a1d723..8c2538192 100644 --- a/httomo/method_wrappers/save_intermediate.py +++ b/httomo/method_wrappers/save_intermediate.py @@ -47,8 +47,11 @@ def __init__( assert prev_method is not None filename = f"{prev_method.task_id}-{prev_method.package_name}-{prev_method.method_name}" - if prev_method.recon_algorithm is not None: + is_saving_recon = prev_method.module_path.endswith(".algorithm") + if is_saving_recon and prev_method.recon_algorithm is not None: filename += f"-{prev_method.recon_algorithm}" + if is_saving_recon and httomo.globals.RECON_FILENAME_STEM is not None: + filename = httomo.globals.RECON_FILENAME_STEM if out_dir is None: out_dir = httomo.globals.run_out_dir diff --git a/tests/method_wrappers/test_save_intermediate.py b/tests/method_wrappers/test_save_intermediate.py index 9681fdc9c..697cf0391 100644 --- a/tests/method_wrappers/test_save_intermediate.py +++ b/tests/method_wrappers/test_save_intermediate.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import List, Tuple +from typing import Callable, List, Optional, Tuple from unittest import mock import pytest from pytest_mock import MockerFixture @@ -278,3 +278,137 @@ def save_intermediate_data( # Execute the padded block with the intermediate wrapper and let the assertions in the # dummy method function run the appropriate checks wrp.execute(block) + + +@pytest.mark.parametrize("recon_filename_stem_global_var", [None, "some-recon"]) +@pytest.mark.parametrize( + "recon_algorithm", + [None, "gridrec"], + ids=["specify-recon-algorithm", "dont-specify-recon-algorithm"], +) +def test_recon_method_output_filename( + get_files: Callable, + mocker: MockerFixture, + tmp_path: Path, + recon_filename_stem_global_var: Optional[str], + recon_algorithm: Optional[str], +): + httomo.globals.RECON_FILENAME_STEM = recon_filename_stem_global_var + loader: LoaderInterface = mocker.create_autospec( + LoaderInterface, instance=True, detector_x=10, detector_y=20 + ) + + class FakeModule: + def save_intermediate_data( + data, + global_shape: Tuple[int, int, int], + global_index: Tuple[int, int, int], + slicing_dim: int, + file: h5py.File, + frames_per_chunk: int, + path: str, + detector_x: int, + detector_y: int, + angles: np.ndarray, + ): + pass + + mocker.patch( + "httomo.method_wrappers.generic.import_module", return_value=FakeModule + ) + TASK_ID = "task1" + PACKAGE_NAME = "testpackage" + METHOD_NAME = "testreconmethod" + MODULE_PATH = f"{PACKAGE_NAME}.algorithm" + if recon_filename_stem_global_var is None and recon_algorithm is None: + expected_filename = f"{TASK_ID}-{PACKAGE_NAME}-{METHOD_NAME}" + if recon_filename_stem_global_var is None and recon_algorithm is not None: + expected_filename = f"{TASK_ID}-{PACKAGE_NAME}-{METHOD_NAME}-{recon_algorithm}" + if recon_filename_stem_global_var is not None: + expected_filename = recon_filename_stem_global_var + expected_filename += ".h5" + prev_method = mocker.create_autospec( + MethodWrapper, + instance=True, + task_id=TASK_ID, + package_name=PACKAGE_NAME, + method_name=METHOD_NAME, + module_path=MODULE_PATH, + recon_algorithm=recon_algorithm, + ) + mocker.patch.object(httomo.globals, "run_out_dir", tmp_path) + wrp = make_method_wrapper( + make_mock_repo(mocker, implementation="cpu"), + "httomo.methods", + "save_intermediate_data", + MPI.COMM_WORLD, + make_mock_preview_config(mocker), + loader=loader, + prev_method=prev_method, + ) + + assert isinstance(wrp, SaveIntermediateFilesWrapper) + files = get_files(tmp_path) + assert len(files) == 1 + assert Path(files[0]).name == expected_filename + + +@pytest.mark.parametrize("recon_filename_stem_global_var", [None, "some-recon"]) +def test_non_recon_method_output_filename( + get_files: Callable, + mocker: MockerFixture, + tmp_path: Path, + recon_filename_stem_global_var: Optional[str], +): + httomo.globals.RECON_FILENAME_STEM = recon_filename_stem_global_var + loader: LoaderInterface = mocker.create_autospec( + LoaderInterface, instance=True, detector_x=10, detector_y=20 + ) + + class FakeModule: + def save_intermediate_data( + data, + global_shape: Tuple[int, int, int], + global_index: Tuple[int, int, int], + slicing_dim: int, + file: h5py.File, + frames_per_chunk: int, + path: str, + detector_x: int, + detector_y: int, + angles: np.ndarray, + ): + pass + + mocker.patch( + "httomo.method_wrappers.generic.import_module", return_value=FakeModule + ) + TASK_ID = "task1" + PACKAGE_NAME = "testpackage" + METHOD_NAME = "testmethod" + MODULE_PATH = f"{PACKAGE_NAME}.notalgorithm" + EXPECTED_FILENAME = f"{TASK_ID}-{PACKAGE_NAME}-{METHOD_NAME}.h5" + prev_method = mocker.create_autospec( + MethodWrapper, + instance=True, + task_id=TASK_ID, + package_name=PACKAGE_NAME, + method_name=METHOD_NAME, + module_path=MODULE_PATH, + recon_algorithm=None, + ) + mocker.patch.object(httomo.globals, "run_out_dir", tmp_path) + wrp = make_method_wrapper( + make_mock_repo(mocker, implementation="cpu"), + "httomo.methods", + "save_intermediate_data", + MPI.COMM_WORLD, + make_mock_preview_config(mocker), + loader=loader, + prev_method=prev_method, + ) + + assert isinstance(wrp, SaveIntermediateFilesWrapper) + files = get_files(tmp_path) + assert len(files) == 1 + assert Path(files[0]).name == EXPECTED_FILENAME diff --git a/tests/test_cli.py b/tests/test_cli.py index ad47625c9..93adec00a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -84,5 +84,37 @@ def test_output_folder_name_correctly_sets_run_out_dir_global_constant(output_fo syslog_host="localhost", syslog_port=514, output_folder_name=Path(dir_name), + recon_filename_stem=None, ) assert httomo.globals.run_out_dir == custom_output_dir + + +@pytest.mark.parametrize( + "use_recon_filename_stem_flag", + [True, False], +) +def test_cli_recon_filename_stem_flag( + standard_data, standard_loader, output_folder, use_recon_filename_stem_flag: bool +): + runner = CliRunner() + if use_recon_filename_stem_flag: + filename_stem = "my-file" + runner.invoke( + main, + [ + "run", + standard_data, + standard_loader, + output_folder, + "--recon-filename-stem", + filename_stem, + ], + ) + assert httomo.globals.RECON_FILENAME_STEM is not None + assert httomo.globals.RECON_FILENAME_STEM == filename_stem + else: + runner.invoke( + main, + ["run", standard_data, standard_loader, output_folder], + ) + assert httomo.globals.RECON_FILENAME_STEM is None