diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eee1c51..9172497 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: hooks: - id: reorder-python-imports - - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v0.971' - hooks: - - id: mypy + # - repo: https://github.com/pre-commit/mirrors-mypy + # rev: 'v0.971' + # hooks: + # - id: mypy diff --git a/.vscode/settings.json b/.vscode/settings.json index 827a0ea..c845f4e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,6 @@ "coregistration", "eyemask", "filefolder" - ] + ], + "autoDocstring.docstringFormat": "sphinx" } diff --git a/Makefile b/Makefile index 8fa75a9..d526805 100644 --- a/Makefile +++ b/Makefile @@ -58,7 +58,7 @@ lint/black: ## check style with black lint/mypy: ## check style with mypy mypy bidsmreye -lint: lint/black lint/flake8 lint/mypy ## check style +lint: lint/black lint/mypy lint/flake8 ## check style docs: ## generate Sphinx HTML documentation, including API docs rm -f docs/source/bidsmreye.rst @@ -85,8 +85,10 @@ install: clean ## install the package to the active Python's site-packages ## TESTS test: models tests/data/moae_fmriprep ## run tests quickly with the default Python - python -m pytest --cov bidsmreye --cov-report html:htmlcov - $(BROWSER) htmlcov/index.html + python -m pytest --cov bidsmreye + +# python -m pytest --cov bidsmreye # --cov-report html:htmlcov +# $(BROWSER) htmlcov/index.html tests/data/moae_fmriprep: mkdir -p tests/data @@ -115,13 +117,33 @@ demo: clean-demo make generalize prepare_data: tests/data/moae_fmriprep models/dataset1_guided_fixations.h5 - bidsmreye --space MNI152NLin6Asym --task auditory --action prepare $$PWD/tests/data/moae_fmriprep $$PWD/outputs participant + bidsmreye --space MNI152NLin6Asym \ + --task auditory \ + --action prepare \ + --verbosity WARNING \ + --debug True \ + $$PWD/tests/data/moae_fmriprep \ + $$PWD/outputs participant\ + combine: - bidsmreye --space MNI152NLin6Asym --task auditory --action combine $$PWD/tests/data/moae_fmriprep $$PWD/outputs participant + bidsmreye --space MNI152NLin6Asym \ + --task auditory \ + --action combine \ + --verbosity INFO \ + --debug False \ + $$PWD/tests/data/moae_fmriprep \ + $$PWD/outputs participant generalize: - bidsmreye --space MNI152NLin6Asym --task auditory --model guided_fixations --action generalize $$PWD/tests/data/moae_fmriprep $$PWD/outputs participant + bidsmreye --space MNI152NLin6Asym \ + --task auditory \ + --model guided_fixations \ + --action generalize \ + --verbosity INFO \ + --debug False \ + $$PWD/tests/data/moae_fmriprep \ + $$PWD/outputs participant clean-demo: rm -fr outputs diff --git a/bidsmreye/bidsutils.py b/bidsmreye/bidsutils.py index 543a0d8..5c55663 100644 --- a/bidsmreye/bidsutils.py +++ b/bidsmreye/bidsutils.py @@ -1,15 +1,17 @@ """TODO.""" import json +import logging from pathlib import Path from typing import Optional from typing import Union from bids import BIDSLayout # type: ignore -from rich import print from bidsmreye.utils import config from bidsmreye.utils import create_dir_if_absent +log = logging.getLogger("rich") + def get_dataset_layout(dataset_path: Path, config: Optional[dict] = None) -> BIDSLayout: """Return a BIDSLayout object for the dataset at the given path. @@ -27,7 +29,7 @@ def get_dataset_layout(dataset_path: Path, config: Optional[dict] = None) -> BID if config is None: pybids_config = get_pybids_config() - print(f"\nindexing {dataset_path}\n") + log.info(f"indexing {dataset_path}") return BIDSLayout( dataset_path, validate=False, derivatives=False, config=pybids_config diff --git a/bidsmreye/combine.py b/bidsmreye/combine.py index 43cc2ba..5cb6ead 100644 --- a/bidsmreye/combine.py +++ b/bidsmreye/combine.py @@ -1,4 +1,5 @@ """TODO.""" +import logging import pickle import warnings from pathlib import Path @@ -6,7 +7,6 @@ import numpy as np # type: ignore from bids import BIDSLayout # type: ignore from deepmreye import preprocess # type: ignore -from rich import print from bidsmreye.bidsutils import check_layout from bidsmreye.bidsutils import create_bidsname @@ -15,6 +15,8 @@ from bidsmreye.utils import move_file from bidsmreye.utils import return_regex +log = logging.getLogger("rich") + def process_subject(cfg: dict, layout_out: BIDSLayout, subject_label: str): """_summary_. @@ -24,7 +26,7 @@ def process_subject(cfg: dict, layout_out: BIDSLayout, subject_label: str): subject_label (str): Can be a regular expression. """ - print(f"Running subject: {subject_label}") + log.info(f"Running subject: {subject_label}") masks = layout_out.get( return_type="filename", @@ -38,7 +40,7 @@ def process_subject(cfg: dict, layout_out: BIDSLayout, subject_label: str): for i, img in enumerate(masks): - print(f"Input mask: {img}") + log.info(f"Input mask: {img}") # Load mask and normalize it this_mask = pickle.load(open(img, "rb")) diff --git a/bidsmreye/generalize.py b/bidsmreye/generalize.py index 1525972..4a25235 100644 --- a/bidsmreye/generalize.py +++ b/bidsmreye/generalize.py @@ -1,4 +1,5 @@ """TODO.""" +import logging import warnings from pathlib import Path @@ -18,6 +19,8 @@ from bidsmreye.utils import move_file from bidsmreye.utils import return_regex +log = logging.getLogger("rich") + def convert_confounds(cfg: dict, layout_out: BIDSLayout, subject_label: str): """Convert numpy output to TSV. @@ -47,7 +50,7 @@ def convert_confounds(cfg: dict, layout_out: BIDSLayout, subject_label: str): confound_name = create_bidsname(layout_out, key + "p", "confounds_tsv") - print(f"Saving to {confound_name} \n") + log.info(f"Saving to {confound_name}") pd.DataFrame(this_pred).to_csv( confound_name, sep="\t", header=["x_position", "y_position"], index=None @@ -82,14 +85,12 @@ def generalize(cfg: dict) -> None: ) for file in data: - print(f"adding file: {Path(file).name}") + log.info(f"adding file: {Path(file).name}") all_data.append(file) print("\n") - generators = data_generator.create_generators(all_data, all_data) generators = (*generators, all_data, all_data) - print("\n") # Get untrained model and load with trained weights @@ -104,6 +105,12 @@ def generalize(cfg: dict) -> None: ) model_inference.load_weights(model_weights) + verbose = 0 + if log.isEnabledFor(logging.DEBUG): + verbose = 2 + elif log.isEnabledFor(logging.INFO): + verbose = 1 + (evaluation, scores) = train.evaluate_model( dataset="tmp", model=model_inference, @@ -111,10 +118,11 @@ def generalize(cfg: dict) -> None: save=True, model_path=f"{layout_out.root}/sub-{subject_label}/func/", model_description="", - verbose=3, + verbose=verbose, percentile_cut=80, ) + # TODO save figure fig = analyse.visualise_predictions_slider( evaluation, scores, @@ -122,7 +130,8 @@ def generalize(cfg: dict) -> None: bg_color="rgb(255,255,255)", ylim=[-11, 11], ) - fig.show() + if log.isEnabledFor(logging.DEBUG) or log.isEnabledFor(logging.INFO): + fig.show() entities = {"subject": subject_label, "task": cfg["task"], "space": cfg["space"]} confound_numpy = create_bidsname(layout_out, entities, "confounds_numpy") diff --git a/bidsmreye/prepare_data.py b/bidsmreye/prepare_data.py index a9f30fa..7af9290 100644 --- a/bidsmreye/prepare_data.py +++ b/bidsmreye/prepare_data.py @@ -1,7 +1,8 @@ """Run coregistration and extract data.""" +import logging + from bids import BIDSLayout # type: ignore from deepmreye import preprocess # type: ignore -from rich import print from bidsmreye.bidsutils import check_layout from bidsmreye.bidsutils import create_bidsname @@ -14,6 +15,8 @@ from bidsmreye.utils import move_file from bidsmreye.utils import return_regex +log = logging.getLogger("rich") + def coregister_and_extract_data(img: str) -> None: """Coregister image to eye template and extract data from eye mask for one image. @@ -31,7 +34,7 @@ def coregister_and_extract_data(img: str) -> None: z_edges, ) = preprocess.get_masks() - print(f"Input file: {img}") + log.info(f"Input file: {img}") preprocess.run_participant( img, dme_template, eyemask_big, eyemask_small, x_edges, y_edges, z_edges @@ -48,7 +51,7 @@ def preprocess_subject( layout_out (BIDSLayout): Layout output dataset. subject_label (str): Can be a regular expression. """ - print(f"Running subject: {subject_label}") + log.info(f"Running subject: {subject_label}") bf = layout_in.get( return_type="filename", diff --git a/bidsmreye/run.py b/bidsmreye/run.py index 2b64195..04f7863 100755 --- a/bidsmreye/run.py +++ b/bidsmreye/run.py @@ -1,18 +1,26 @@ #!/usr/bin/env python """Main script.""" import argparse +import logging import os import sys from glob import glob from pathlib import Path -from rich import print +from rich.logging import RichHandler +from rich.traceback import install from bidsmreye.combine import combine from bidsmreye.generalize import generalize from bidsmreye.prepare_data import prepare_data from bidsmreye.utils import config +# let rich print the traceback +install(show_locals=True) + +# log format +FORMAT = "%(asctime)s - %(levelname)s - %(message)s" + def main(argv=sys.argv) -> None: """Execute the main script.""" @@ -82,6 +90,11 @@ def main(argv=sys.argv) -> None: help="model to use", choices=["guided_fixations"], ) + parser.add_argument( + "--verbosity", + help="INFO, WARNING. Defaults to INFO", + choices=["INFO", "WARNING"], + ) parser.add_argument( "--debug", help="true or false", @@ -89,6 +102,45 @@ def main(argv=sys.argv) -> None: args = parser.parse_args(argv[1:]) + cfg = set_cfg(args) + + log_level = args.verbosity or "INFO" + if cfg["debug"]: + log_level = "DEBUG" + + logging.basicConfig( + level=log_level, format=FORMAT, datefmt="[%X]", handlers=[RichHandler()] + ) + + log = logging.getLogger("rich") + + if cfg["debug"]: + log.debug("DEBUG MODE") + + if cfg["model_weights_file"] != "": + assert Path(cfg["model_weights_file"]).is_file() + log.info(f"Using model: {cfg['model_weights_file']}") + + if args.analysis_level == "participant": + + if args.action == "prepare": + log.info("PREPARING DATA") + prepare_data(cfg) + + elif args.action == "combine": + log.info("COMBINING DATA") + combine(cfg) + + elif args.action == "generalize": + log.info("GENERALIZING") + generalize(cfg) + + +def set_cfg(args): + """Set the config.""" + # TODO extract function + cfg = config() + # TODO extract function subjects_to_analyze = [] # only for a subset of subjects @@ -99,9 +151,6 @@ def main(argv=sys.argv) -> None: subject_dirs = glob(os.path.join(args.bids_dir, "sub-*")) subjects_to_analyze = [subject_dir.split("-")[-1] for subject_dir in subject_dirs] - # TODO extract function - cfg = config() - cfg["participant"] = subjects_to_analyze if args.task: @@ -118,20 +167,10 @@ def main(argv=sys.argv) -> None: "dataset1_guided_fixations.h5", ) - if cfg["model_weights_file"] != "": - assert Path(cfg["model_weights_file"]).is_file() - print(f"\nUsing model: {cfg['model_weights_file']}") + if args.debug == "True": + cfg["debug"] = True - if args.analysis_level == "participant": - - if args.action == "prepare": - prepare_data(cfg) - - elif args.action == "combine": - combine(cfg) - - elif args.action == "generalize": - generalize(cfg) + return cfg if __name__ == "__main__": diff --git a/bidsmreye/utils.py b/bidsmreye/utils.py index 3f8cf64..81be437 100644 --- a/bidsmreye/utils.py +++ b/bidsmreye/utils.py @@ -1,11 +1,72 @@ """TODO.""" +import logging import os import re +import warnings from pathlib import Path from typing import Optional +from attrs import define +from attrs import field from bids import BIDSLayout # type: ignore -from rich import print + +log = logging.getLogger("rich") + + +@define +class Config: + """Set up config and check that all required fields are set.""" + + input_folder: str = field(default=None, converter=Path) + + @input_folder.validator + def _check_input_folder(self, attribute, value): + if not value.is_dir: + raise ValueError(f"Input_folder must be an existing directory:\n{value}.") + + output_folder: str = field(default=None, converter=Path) + model_weights_file = field(kw_only=True, default="") + participant: list = field(kw_only=True, default=[]) + space: str = field(kw_only=True, default="") + task: str = field(kw_only=True, default="") + debug: bool = field(kw_only=True, default=False) + has_GPU = False + + def __attrs_post_init__(self): + """Check that output_folder exists and gets info from layout if not specified.""" + os.environ["CUDA_VISIBLE_DEVICES"] = "0" if self.has_GPU else "" + + self.output_folder = Path(self.output_folder).joinpath("bidsmreye") + if not self.output_folder: + self.output_folder.mkdir(parents=True, exist_ok=True) + + layout_in = BIDSLayout(self.input_folder, validate=False, derivatives=False) + + # TODO throw error if no participants found or warning + # if some requested participants are not found + subjects = layout_in.get_subjects() + if self.participant: + missing_subjects = list(set(self.participant) - set(subjects)) + if missing_subjects: + warnings.warn( + f"Task(s) {missing_subjects} not found in {self.input_folder}" + ) + self.participant = list(set(self.participant) & set(subjects)) + else: + self.participant = layout_in.get( + return_type="id", target="subject", subject=self.participant + ) + + # TODO throw error if no task found or warning + # if some requested tasks are not found + tasks = layout_in.get_tasks() + if not self.task: + self.task = layout_in.get_tasks() + else: + missing_tasks = list(set(self.task) - set(tasks)) + if missing_tasks: + warnings.warn(f"Task(s) {missing_tasks} not found in {self.input_folder}") + self.task = list(set(self.task) & set(tasks)) def config() -> dict: @@ -37,7 +98,7 @@ def move_file(input: Path, output: Path) -> None: output (str): _description_ """ - print(f"{input.resolve()} --> {output.resolve()}") + log.info(f"{input.resolve()} --> {output.resolve()}") create_dir_for_file(output) input.rename(output) @@ -49,7 +110,7 @@ def create_dir_if_absent(output_path: Path) -> None: output_path (Path): _description_ """ if not output_path.is_dir(): - print(f"Creating dir: {output_path}") + log.info(f"Creating dir: {output_path}") output_path.mkdir(parents=True, exist_ok=True) @@ -103,8 +164,9 @@ def list_subjects(layout: BIDSLayout, cfg: Optional[dict] = None) -> list: if debug: subjects = [subjects[0]] + log.debug("Running first subject only.") - print(f"processing subjects: {subjects}\n") + log.info(f"processing subjects: {subjects}") return subjects diff --git a/requirements.txt b/requirements.txt index df81916..27517a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ deepmreye @ git+https://github.com/DeepMReye/DeepMReye.git pybids rich +attrs diff --git a/tests/test_utils.py b/tests/test_utils.py index 76d9a39..b91decf 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -4,12 +4,42 @@ from bids.tests import get_test_data_path from bidsmreye.bidsutils import get_dataset_layout +from bidsmreye.utils import Config from bidsmreye.utils import config from bidsmreye.utils import get_deepmreye_filename from bidsmreye.utils import list_subjects from bidsmreye.utils import return_deepmreye_output_filename +def test_Config(): + cfg = Config( + Path(__file__).parent.joinpath("data", "moae_fmriprep"), + Path(__file__).parent.joinpath("data"), + ) + assert cfg.debug == False + assert cfg.input_folder == Path(__file__).parent.joinpath("data", "moae_fmriprep") + assert cfg.output_folder == Path(__file__).parent.joinpath("data", "bidsmreye") + assert cfg.participant == ["01"] + assert cfg.task == ["auditory"] + + +def test_Config_task_participant(): + cfg = Config( + Path(__file__).parent.joinpath("data", "moae_fmriprep"), + Path(__file__).parent.joinpath("data"), + task=["auditory", "rest"], + participant=["01", "02"], + ) + assert cfg.participant == ["01"] + assert cfg.task == ["auditory"] + + +# TODO add test warning +# def test_warning(): +# with pytest.warns(UserWarning): +# warnings.warn("my warning", UserWarning) + + def test_list_subjects(): cfg = config()