diff --git a/docs/conf.py b/docs/conf.py index fb94ed84..d1cd8dba 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -39,6 +39,7 @@ "sphinx.ext.napoleon", "sphinx.ext.viewcode", "sphinxcontrib.apidoc", + "sphinxarg.ext", ] autodoc_mock_imports = [ diff --git a/docs/developers.rst b/docs/developers.rst index 8cd62d50..2bbaaa1f 100644 --- a/docs/developers.rst +++ b/docs/developers.rst @@ -28,6 +28,7 @@ Information on specific functions, classes, and methods. .. toctree:: :glob: + api/eddymotion.cli api/eddymotion.data api/eddymotion.data.dmri api/eddymotion.estimator diff --git a/docs/index.rst b/docs/index.rst index 848e3cc2..013b36bf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,5 +14,6 @@ Contents :maxdepth: 3 installation + running developers changes diff --git a/docs/running.rst b/docs/running.rst new file mode 100644 index 00000000..010250e1 --- /dev/null +++ b/docs/running.rst @@ -0,0 +1,11 @@ +.. _running_eddymotion: + +Running *Eddymotion* +******************** +Command line interface +---------------------- +.. argparse:: + :ref: eddymotion.cli.parser._build_parser + :prog: eddymotion + :nodefault: + :nodefaultconst: \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 12afe70e..c9de63d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,8 @@ doc = [ "sphinx_rtd_theme", "sphinxcontrib-apidoc ~= 0.3.0", "sphinxcontrib-napoleon", - "sphinxcontrib-versioning" + "sphinxcontrib-versioning", + "sphinx-argparse", ] dev = [ @@ -79,6 +80,9 @@ docs = ["eddymotion[doc]"] tests = ["eddymotion[test]"] all = ["eddymotion[doc,test,dev,plotting,resmon,popylar]"] +[project.scripts] +eddymotion = "eddymotion.cli.run:main" + # # Hatch configurations # diff --git a/src/eddymotion/__main__.py b/src/eddymotion/__main__.py new file mode 100644 index 00000000..6cc72833 --- /dev/null +++ b/src/eddymotion/__main__.py @@ -0,0 +1,33 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright 2024 The NiPreps Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +"""Entry point for Eddymotion.""" + +import sys + +from . import __name__ as module +from .cli.run import main + +if __name__ == "__main__": + if "__main__.py" in sys.argv[0]: + sys.argv[0] = f"{sys.executable} -m {module}" + main() diff --git a/src/eddymotion/cli/__init__.py b/src/eddymotion/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/eddymotion/cli/parser.py b/src/eddymotion/cli/parser.py new file mode 100644 index 00000000..c1a4b564 --- /dev/null +++ b/src/eddymotion/cli/parser.py @@ -0,0 +1,134 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright 2024 The NiPreps Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +"""Parser module.""" + +from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, Namespace +from pathlib import Path +from typing import Optional + +import yaml + + +def _parse_yaml_config(file_path: Path) -> dict: + """ + Parse YAML configuration file. + + Parameters + ---------- + file_path : Path + Path to the YAML configuration file. + + Returns + ------- + dict + A dictionary containing the parsed YAML configuration. + """ + with open(file_path, "r") as file: + config = yaml.safe_load(file) + return config + + +def _build_parser() -> ArgumentParser: + """ + Build parser object. + + Returns + ------- + :obj:`~argparse.ArgumentParser` + The parser object defining the interface for the command-line. + """ + parser = ArgumentParser( + description="A model-based algorithm for the realignment of 4D brain images.", + formatter_class=ArgumentDefaultsHelpFormatter, + ) + + parser.add_argument( + "input_file", + action="store", + type=Path, + help="Path to the HDF5 file containing the original DWI data.", + ) + parser.add_argument( + "--align_config", + action="store", + type=_parse_yaml_config, + default=None, + help="Path to the yaml file containing the parameters to configure the image registration process.", + ) + parser.add_argument( + "--models", + action="store", + nargs="+", + default=["b0"], + help="Select the diffusion model for registration targets.", + ) + parser.add_argument( + "--nthreads", + action="store", + type=int, + default=None, + help="Maximum number of threads an individual process may use.", + ) + parser.add_argument( + "--njobs", + action="store", + type=int, + default=None, + help="Number of parallel jobs.", + ) + parser.add_argument( + "--seed", + action="store", + type=int, + default=None, + help="Seed the random number generator for deterministic estimation.", + ) + parser.add_argument( + "--output_dir", + action="store", + type=Path, + default=Path.cwd(), + help="Path to the output directory. Defaults to the current directory. The output file will have the same name as the input file.", + ) + + return parser + + +def parse_args(args: Optional[list] = None, namespace: Optional[Namespace] = None) -> Namespace: + """ + Parse args and run further checks on the command line. + + Parameters + ---------- + args : list of str, optional + List of strings representing the command line arguments. Defaults to None. + namespace : :class:`~argparse.Namespace`, optional + An object to parse the arguments into. Defaults to None. + + Returns + ------- + :class:`~argparse.Namespace` + An object holding the parsed arguments. + """ + parser = _build_parser() + return parser.parse_args(args, namespace) diff --git a/src/eddymotion/cli/run.py b/src/eddymotion/cli/run.py new file mode 100644 index 00000000..b181e72b --- /dev/null +++ b/src/eddymotion/cli/run.py @@ -0,0 +1,65 @@ +# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +# +# Copyright 2024 The NiPreps Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# We support and encourage derived works from this project, please read +# about our expectations at +# +# https://www.nipreps.org/community/licensing/ +# +"""Eddymotion runner.""" + +from pathlib import Path + +from eddymotion.cli.parser import parse_args +from eddymotion.data.dmri import DWI +from eddymotion.estimator import EddyMotionEstimator + + +def main() -> None: + """ + Entry point. + + Returns + ------- + None + """ + args = parse_args() + + # Open the data with the given file path + dwi_dataset: DWI = DWI.from_filename(args.input_file) + + estimator: EddyMotionEstimator = EddyMotionEstimator() + + _ = estimator.estimate( + dwi_dataset, + align_kwargs=args.align_config, + models=args.models, + omp_nthreads=args.nthreads, + njobs=args.njobs, + seed=args.seed, + ) + + # Set the output filename to be the same as the input filename + output_filename: str = Path(args.input_file).name + output_path: Path = Path(args.output_dir) / output_filename + + # Save the DWI dataset to the output path + dwi_dataset.to_filename(output_path) + + +if __name__ == "__main__": + main()