diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 0000000..4761ddb --- /dev/null +++ b/.codespellrc @@ -0,0 +1,4 @@ +[codespell] +skip = .git,env,build,outputs,.mypy_cache,moae_fmriprep +ignore-words-list = fo +builtin = clear,rare diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dc5fca1..c70af37 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,7 +41,7 @@ repos: rev: 'v0.991' hooks: - id: mypy - additional_dependencies: [types-all] + additional_dependencies: [types-all, pydantic] files: bidsmreye args: ["--config-file", "setup.cfg"] diff --git a/Makefile b/Makefile index e232d90..e795165 100644 --- a/Makefile +++ b/Makefile @@ -135,28 +135,29 @@ clean-demo: rm -fr outputs/moae_fmriprep demo: clean-demo tests/data/moae_fmriprep ## demo: runs all demo steps on MOAE dataset - bidsmreye --action all \ - $$PWD/tests/data/moae_fmriprep \ + bidsmreye $$PWD/tests/data/moae_fmriprep \ $$PWD/outputs/moae_fmriprep/derivatives \ - participant + participant \ + --action all \ + -vv \ prepare: tests/data/moae_fmriprep ## demo: prepares the data of MOAE dataset - bidsmreye --action prepare \ - --verbosity INFO \ - --debug true \ - --reset_database true \ - $$PWD/tests/data/moae_fmriprep \ + bidsmreye $$PWD/tests/data/moae_fmriprep \ $$PWD/outputs/moae_fmriprep/derivatives \ - participant + participant \ + --action prepare \ + -vv \ + --debug \ + --reset_database generalize: ## demo: predicts labels of MOAE dataset - bidsmreye --action generalize \ - --verbosity WARNING \ - --debug true \ - --reset_database true \ - $$PWD/tests/data/moae_fmriprep \ + bidsmreye $$PWD/tests/data/moae_fmriprep \ $$PWD/outputs/moae_fmriprep/derivatives \ - participant + participant \ + --action generalize \ + -vv \ + --debug \ + --reset_database ## Openneuro data @@ -173,35 +174,37 @@ get_ds002799: tests/data/data_ds002799 datalad get sub-30[27]/ses-*/func/*run-*preproc*bold* ds002799_prepare: get_ds002799 - bidsmreye --action prepare \ - --debug true \ - $$PWD/tests/data/ds002799/derivatives/fmriprep \ + bidsmreye $$PWD/tests/data/ds002799/derivatives/fmriprep \ $$PWD/outputs/ds002799/derivatives \ participant \ + --action prepare \ + --debug \ --participant_label 302 307 \ --space MNI152NLin2009cAsym T1w \ --run 1 2 + ds002799_generalize: - bidsmreye --action generalize \ - --debug true \ - $$PWD/tests/data/ds002799/derivatives/fmriprep \ + bidsmreye $$PWD/tests/data/ds002799/derivatives/fmriprep \ $$PWD/outputs/ds002799/derivatives \ participant \ + --action generalize \ + --debug \ --participant_label 302 307 \ --space MNI152NLin2009cAsym T1w \ --run 1 2 + ds002799: clean-ds002799 get_ds002799 - bidsmreye --action all \ - --debug true \ - $$PWD/tests/data/ds002799/derivatives/fmriprep \ + bidsmreye $$PWD/tests/data/ds002799/derivatives/fmriprep \ $$PWD/outputs/ds002799/derivatives \ participant \ + --action all \ + --debug \ --participant_label 302 307 \ --space MNI152NLin2009cAsym T1w \ - --run 1 2 - + --run 1 2 \ + -vv ## DOCKER .PHONY: docker/Dockerfile docker/Dockerfile_dev @@ -276,8 +279,8 @@ docker_prepare_data: /home/neuro/outputs/ \ participant \ --action prepare \ - --debug true \ - --reset_database true + --debug \ + --reset_database docker_generalize: docker run --rm -it \ diff --git a/bidsmreye/run.py b/bidsmreye/bidsmreye.py similarity index 85% rename from bidsmreye/run.py rename to bidsmreye/bidsmreye.py index a2f4cca..6f36513 100755 --- a/bidsmreye/run.py +++ b/bidsmreye/bidsmreye.py @@ -15,13 +15,15 @@ from bidsmreye.prepare_data import prepare_data from bidsmreye.utils import bidsmreye_log from bidsmreye.utils import Config +from bidsmreye.utils import default_log_level +from bidsmreye.utils import log_levels __version__ = _version.get_versions()["version"] log = bidsmreye_log(name="bidsmreye") -def main(argv: Any = sys.argv) -> None: +def cli(argv: Any = sys.argv) -> None: """Run the bids app. :param argv: _description_, defaults to sys.argv @@ -46,9 +48,15 @@ def main(argv: Any = sys.argv) -> None: bids_filter=args.bids_filter_file, ) # type: ignore - log_level = "DEBUG" if cfg.debug else args.verbosity - - log.setLevel(log_level) + # TODO integrate as part of base config + # https://stackoverflow.com/a/53293042/14223310 + log_level = log_levels().index(default_log_level()) + # For each "-v" flag, adjust the logging verbosity accordingly + # making sure to clamp off the value from 0 to 4, inclusive of both + for adjustment in args.log_level or (): + log_level = min(len(log_levels()) - 1, max(log_level + adjustment, 0)) + log_level_name = log_levels()[log_level] + log.setLevel(log_level_name) log.info("Running bidsmreye version %s", __version__) @@ -91,7 +99,7 @@ def common_parser() -> MuhParser: description="BIDS app using deepMReye to decode eye motion for fMRI time series data.", epilog=""" For a more readable version of this help section, - see the online https://bidsmreye.readthedocs.io/". + see the online https://bidsmreye.readthedocs.io/. """, ) @@ -119,14 +127,7 @@ def common_parser() -> MuhParser: parser.add_argument( "--action", help=""" - What action to perform: - - - all: run all steps - - - prepare: prepare data for analysis coregister to template, - normalize and extract data - - - generalize: generalize from data to give predicted labels + What action to perform. """, choices=["all", "prepare", "generalize"], default="all", @@ -174,21 +175,19 @@ def common_parser() -> MuhParser: nargs="+", ) parser.add_argument( - "--verbosity", - help="ERROR, INFO, WARNING, DEBUG", - choices=["ERROR", "INFO", "WARNING", "DEBUG"], - default="INFO", - ) - parser.add_argument( - "--debug", help="true or false.", choices=["true", "false"], default="false" + "-v", + "--verbose", + dest="log_level", + action="append_const", + const=-1, ) + parser.add_argument("--debug", help="Switch to debug mode", action="store_true") parser.add_argument( "--reset_database", help=""" Resets the database of the input dataset. """, - choices=["true", "false"], - default="false", + action="store_true", ) parser.add_argument( "--bids_filter_file", @@ -222,7 +221,3 @@ def common_parser() -> MuhParser: ) return parser - - -if __name__ == "__main__": - main() diff --git a/bidsmreye/download.py b/bidsmreye/download.py index 23cd394..2269913 100644 --- a/bidsmreye/download.py +++ b/bidsmreye/download.py @@ -2,10 +2,14 @@ from __future__ import annotations import argparse +import sys from pathlib import Path +from typing import Any +from typing import IO import pkg_resources import pooch +import rich from bidsmreye.utils import bidsmreye_log from bidsmreye.utils import move_file @@ -13,13 +17,22 @@ log = bidsmreye_log(name="bidsmreye") -def main() -> None: - """Download the models from OSF. +class MuhParser(argparse.ArgumentParser): + """Parser for the main script.""" - :return: _description_ - :rtype: _type_ - """ - parser = argparse.ArgumentParser(description="bidsMReye model downloader.") + def _print_message(self, message: str, file: IO[str] | None = None) -> None: + rich.print(message, file=file) + + +def download_parser() -> MuhParser: + """Execute the main script.""" + parser = MuhParser( + description="Download deepmreye pretrained model from OSF.", + epilog=""" + For a more readable version of this help section, + see the online https://bidsmreye.readthedocs.io/. + """, + ) parser.add_argument( "--model_name", help=""" @@ -44,7 +57,17 @@ def main() -> None: default=Path.cwd().joinpath("models"), ) - args = parser.parse_args() + return parser + + +def cli(argv: Any = sys.argv) -> None: + """Download the models from OSF. + + :return: _description_ + :rtype: _type_ + """ + parser = download_parser() + args = parser.parse_args(argv[1:]) download(model_name=args.model_name, output_dir=args.output_dir) @@ -77,7 +100,7 @@ def download( "4_pursuit": "96nyp", "5_free_viewing": "89nky", "6_1-to-5": "datasets_1to5.h5", - "7_1-to-6": "datasets_1to5.h5", + "7_1-to-6": "datasets_1to6.h5", } if model_name == "all": diff --git a/bidsmreye/generalize.py b/bidsmreye/generalize.py index b3f6f0d..d07d238 100644 --- a/bidsmreye/generalize.py +++ b/bidsmreye/generalize.py @@ -147,6 +147,8 @@ def process_subject(cfg: Config, layout_out: BIDSLayout, subject_label: str) -> if cfg.run: this_filter["run"] = return_regex(cfg.run) + log.info(f"Running subject: {subject_label}") + log.debug(f"Looking for files with filter\n{this_filter}") data = layout_out.get( @@ -155,7 +157,8 @@ def process_subject(cfg: Config, layout_out: BIDSLayout, subject_label: str) -> **this_filter, ) - log.debug(f"Found files\n{data}") + to_print = [str(Path(x).relative_to(layout_out.root)) for x in data] + log.debug(f"Found files\n{to_print}") for file in data: diff --git a/bidsmreye/prepare_data.py b/bidsmreye/prepare_data.py index 7f0d999..7382ba2 100644 --- a/bidsmreye/prepare_data.py +++ b/bidsmreye/prepare_data.py @@ -145,11 +145,12 @@ def process_subject( **this_filter, ) - log.debug(f"Found files\n{bf}") + to_print = [str(Path(x).relative_to(layout_in.root)) for x in bf] + log.debug(f"Found files\n{to_print}") for img in bf: - log.info(f"Processing {img}") + log.info(f"Processing file: {Path(img).name}") coregister_and_extract_data(img) diff --git a/bidsmreye/templates/CITATION.mustache b/bidsmreye/templates/CITATION.mustache index 3bf7baf..ae308a7 100644 --- a/bidsmreye/templates/CITATION.mustache +++ b/bidsmreye/templates/CITATION.mustache @@ -6,4 +6,22 @@ to decode eye motion for fMRI time series data. ### data extraction +All imaging data underwent co-registration conducted +using Advanced Normalization Tools (ANTs, RRID:SCR_004757) within Python (ANTsPy). +First, each participant's mean EPI was non-linearly co-registered +to an average template. +Second, we co-registered all voxels within a bounding box that included the eyes +to a preselected bounding box in our group template to further improve the fit. + +Each voxel within those bounding box underwent two normalization steps. +First, the across-run median signal intensity was subtracted +from each voxel and sample +and was divided by the median absolute deviation over time (temporal normalization). +Second, for each sample, the mean across all voxels +within the eye masks was subtracted and divided by the standard deviation +across voxels (spatial normalization). + ### eyemotion decoding + +Voxels time series were used as inputs for generalization decoding +using using the the pre-trained model {{model}} from deepMReye ({{model_url}}). diff --git a/bidsmreye/utils.py b/bidsmreye/utils.py index 81d0381..cf596f2 100644 --- a/bidsmreye/utils.py +++ b/bidsmreye/utils.py @@ -24,6 +24,16 @@ __version__ = _version.get_versions()["version"] +def default_log_level() -> str: + """Return default log level.""" + return "WARNING" + + +def log_levels() -> list[str]: + """Return a list of log levels.""" + return ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + + def bidsmreye_log(name: str | None = None) -> logging.Logger: """Create log. @@ -38,7 +48,7 @@ def bidsmreye_log(name: str | None = None) -> logging.Logger: FORMAT = "bidsMReye - %(asctime)s - %(message)s" - log_level = "INFO" + log_level = default_log_level() if not name: name = "rich" @@ -187,8 +197,11 @@ def move_file(input: Path, output: Path) -> None: :param output: :type output: Path + + :param root: Optional. If specified, the printed path will be relative to this path. + :type root: Path """ - log.info(f"{input.resolve()} --> {output.resolve()}") + log.debug(f"{input.resolve()} --> {output.resolve()}") create_dir_for_file(output) shutil.copy(input, output) input.unlink() @@ -203,7 +216,7 @@ def create_dir_if_absent(output_path: str | Path) -> None: if isinstance(output_path, str): output_path = Path(output_path) if not output_path.is_dir(): - log.info(f"Creating dir: {output_path}") + log.debug(f"Creating dir: {output_path}") output_path.mkdir(parents=True, exist_ok=True) diff --git a/boutiques/bidsmreye.json b/boutiques/bidsmreye.json index f0b3c3a..eafb91d 100644 --- a/boutiques/bidsmreye.json +++ b/boutiques/bidsmreye.json @@ -61,7 +61,7 @@ "list": false, "value-choices": [ "prepare", - "genralize", + "generalize", "all" ], "optional": false, diff --git a/setup.cfg b/setup.cfg index aae88ed..cdcb657 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,7 +42,7 @@ zip_safe = False [options.entry_points] console_scripts = - bidsmreye = bidsmreye.run:main + bidsmreye = bidsmreye.bidsmreye:cli bidsmreye_model = bidsmreye.download:main [options.extras_require] @@ -61,6 +61,7 @@ docs = %(doc)s style = black + codespell flake8 flake8-docstrings mypy @@ -97,6 +98,8 @@ versionfile_source = bidsmreye/_version.py tag_prefix = '' [mypy] +exclude = ['tests/'] +plugins = pydantic.mypy check_untyped_defs = true disallow_any_generics = true disallow_incomplete_defs = true @@ -105,6 +108,15 @@ no_implicit_optional = true warn_redundant_casts = true warn_unused_ignores = true +[pydantic-mypy] +init_forbid_extra = True +init_typed = True +warn_required_dynamic_aliases = True +warn_untyped_fields = True + +[mypy-tests.*] +disallow_untyped_defs = false + [mypy-bidsmreye._version] ignore_errors = True diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e8562af..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Unit test package for bidsmreye.""" -from __future__ import annotations diff --git a/tests/test_bidsmreye.py b/tests/test_bidsmreye.py new file mode 100644 index 0000000..f7393f9 --- /dev/null +++ b/tests/test_bidsmreye.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from bidsmreye.bidsmreye import common_parser + + +def test_parser() -> None: + """Test parser.""" + parser = common_parser() + assert ( + parser.description + == "BIDS app using deepMReye to decode eye motion for fMRI time series data." + ) + + args, unknowns = parser.parse_known_args( + [ + "/path/to/bids", + "/path/to/output", + "participant", + "--action", + "prepare", + "--task", + "foo", + "bar", + ] + ) + + assert args.task == ["foo", "bar"] diff --git a/tests/test_download.py b/tests/test_download.py new file mode 100644 index 0000000..2ac2cde --- /dev/null +++ b/tests/test_download.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from bidsmreye.download import download_parser + + +def test_download_parser(): + """Test parser.""" + parser = download_parser() + assert parser.description == "Download deepmreye pretrained model from OSF." + + args, unknowns = parser.parse_known_args( + [ + "--model_name", + "1_guided_fixations", + "--output_dir", + "/home/bob/models", + ] + ) + + assert args.output_dir == "/home/bob/models" diff --git a/tests/test_generalize.py b/tests/test_generalize.py index 9538326..a54fb1f 100644 --- a/tests/test_generalize.py +++ b/tests/test_generalize.py @@ -2,8 +2,6 @@ from pathlib import Path -from bids.tests import get_test_data_path - from bidsmreye.generalize import convert_confounds from bidsmreye.utils import get_dataset_layout diff --git a/tests/test_methods.py b/tests/test_methods.py index 4ceec8b..b244611 100644 --- a/tests/test_methods.py +++ b/tests/test_methods.py @@ -8,9 +8,9 @@ def test_methods(): - ouptut_dir = Path.cwd().joinpath("temp") - output_file = methods(ouptut_dir) + output_dir = Path.cwd().joinpath("temp") + output_file = methods(output_dir) assert output_file.is_file() - shutil.rmtree(ouptut_dir) + shutil.rmtree(output_dir)