Skip to content

Add CLI flag for choosing recon filename stem #562

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Mar 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion docs/source/howto/run_httomo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`
Expand 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`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -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`.
10 changes: 10 additions & 0 deletions httomo/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -172,6 +179,7 @@ def run(
syslog_host,
syslog_port,
output_folder_name,
recon_filename_stem,
)

does_contain_sweep = is_sweep_pipeline(yaml_config)
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions httomo/globals.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
from pathlib import Path
from typing import Optional

run_out_dir: os.PathLike = Path(".")
gpu_id: int = -1
Expand All @@ -14,3 +15,4 @@
COMPRESS_INTERMEDIATE: bool = False
SYSLOG_SERVER = "localhost"
SYSLOG_PORT = 514
RECON_FILENAME_STEM: Optional[str] = None
5 changes: 4 additions & 1 deletion httomo/method_wrappers/save_intermediate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
136 changes: 135 additions & 1 deletion tests/method_wrappers/test_save_intermediate.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
32 changes: 32 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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