diff --git a/.circleci/config.yml b/.circleci/config.yml index d9c92b3..862e1f7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -100,9 +100,41 @@ jobs: --space MNI152NLin2009cAsym \ --reset_database \ -vv - no_output_timeout: 6h + - run: + name: rerun prepare - fast as output already exists + command: | + user_name=cpplab + repo_name=$(echo "${CIRCLE_PROJECT_REPONAME}" | tr '[:upper:]' '[:lower:]') + docker run -ti --rm \ + -v /tmp/workspace/data/ds002799/derivatives/fmriprep:/bids_dataset \ + -v ${HOME}/outputs:/outputs \ + ${user_name}/${repo_name} \ + /bids_dataset \ + /outputs \ + participant \ + prepare \ + --participant_label 302 307 \ + --space MNI152NLin2009cAsym \ + --reset_database \ + -vv + - run: + name: run group level QC + command: | + user_name=cpplab + repo_name=$(echo "${CIRCLE_PROJECT_REPONAME}" | tr '[:upper:]' '[:lower:]') + docker run -ti --rm \ + -v ${HOME}/outputs:/outputs \ + ${user_name}/${repo_name} \ + /outputs/bidsmreye \ + /outputs \ + group \ + qc \ + --participant_label 302 307 \ + --space MNI152NLin2009cAsym \ + --reset_database \ + -vv - store_artifacts: - path: ~/output + path: /home/circleci/outputs/bidsmreye deploy: machine: diff --git a/.github/workflows/system_tests.yml b/.github/workflows/system_tests.yml index 4e2997e..04b543a 100644 --- a/.github/workflows/system_tests.yml +++ b/.github/workflows/system_tests.yml @@ -23,7 +23,6 @@ jobs: strategy: matrix: python-version: ['3.11'] - dataset: [demo] steps: @@ -50,5 +49,10 @@ jobs: run: | datalad wtf - - name: Run ${{ matrix.dataset }} - run: make ${{ matrix.dataset }} + - name: Run demo + run: make demo + + - name: Re run demo (should be faster) + run: | + make prepare + make generalize diff --git a/Makefile b/Makefile index 83002d0..aaf4387 100644 --- a/Makefile +++ b/Makefile @@ -185,11 +185,46 @@ ds002799: clean-ds002799 get_ds002799 --reset_database \ -vv -## ds002799 +## ds000114 get_ds000114: datalad install -s ///openneuro-derivatives/ds000114-fmriprep tests/data/ds000114-fmriprep cd tests/data/ds000114-fmriprep && datalad get sub-0[1-2]/ses-*/func/*MNI*desc-preproc*bold.nii.gz -J 12 +ds000114_all: get_ds000114 + bidsmreye $$PWD/tests/data/ds000114-fmriprep \ + $$PWD/outputs/ds000114/derivatives \ + participant \ + all \ + --participant_label 01 02 \ + --space MNI152NLin2009cAsym \ + --task linebisection overtverbgeneration -vv --force + +ds000114_prepare: get_ds000114 + bidsmreye $$PWD/tests/data/ds000114-fmriprep \ + $$PWD/outputs/ds000114/derivatives \ + participant \ + prepare \ + --participant_label 01 02 \ + --space MNI152NLin2009cAsym \ + --task linebisection -vv + +ds000114_generalize: + bidsmreye $$PWD/tests/data/ds000114-fmriprep \ + $$PWD/outputs/ds000114/derivatives \ + participant \ + generalize \ + --participant_label 01 02 \ + --space MNI152NLin2009cAsym + +ds000114_qc: + bidsmreye $$PWD/outputs/ds000114/derivatives/bidsmreye \ + $$PWD/outputs/ds000114/derivatives \ + group \ + qc \ + --participant_label 01 02 \ + --space MNI152NLin2009cAsym \ + -vvv + ## DOCKER .PHONY: diff --git a/bidsmreye/_cli.py b/bidsmreye/_cli.py index 1af88b2..54e605f 100644 --- a/bidsmreye/_cli.py +++ b/bidsmreye/_cli.py @@ -39,7 +39,7 @@ def cli(argv: Any = sys.argv) -> None: model_weights_file = None if getattr(args, "model", None) is not None: - model_weights_file = str(getattr(args, "model")) + model_weights_file = str(getattr(args, "model")) # noqa: B009 bidsmreye( bids_dir=args.bids_dir[0], @@ -56,6 +56,7 @@ def cli(argv: Any = sys.argv) -> None: bids_filter_file=args.bids_filter_file, non_linear_coreg=bool(getattr(args, "non_linear_coreg", False)), log_level_name=log_level_name, + force=bool(getattr(args, "force", False)), ) diff --git a/bidsmreye/_parsers.py b/bidsmreye/_parsers.py index 531c0ef..05b1e61 100644 --- a/bidsmreye/_parsers.py +++ b/bidsmreye/_parsers.py @@ -127,6 +127,13 @@ def _add_common_arguments(parser: ArgumentParser) -> ArgumentParser: For further details, please check out TBD. """, ) + parser.add_argument( + "--force", + help=""" +Overwrite previous output. + """, + action="store_true", + ) return parser @@ -233,7 +240,7 @@ def download_parser( help=""" The directory where the model files will be stored. """, - default=Path.cwd().joinpath("models"), + default=Path.cwd() / "models", ) return parser diff --git a/bidsmreye/bids_utils.py b/bidsmreye/bids_utils.py index 0671516..2a97344 100644 --- a/bidsmreye/bids_utils.py +++ b/bidsmreye/bids_utils.py @@ -119,9 +119,8 @@ def create_bidsname( output_file = layout.build_path(entities, bids_name_config[filetype], validate=False) - output_file = Path(layout.root).joinpath(output_file) - - return output_file.resolve() + output_file = Path(layout.root) / output_file + return output_file.absolute() def create_sidecar( @@ -180,7 +179,7 @@ def get_dataset_layout( dataset_path = Path(dataset_path) create_dir_if_absent(dataset_path) - dataset_path = dataset_path.resolve() + dataset_path = dataset_path.absolute() pybids_config = config if config is None: @@ -193,7 +192,7 @@ def get_dataset_layout( dataset_path, validate=False, derivatives=False, config=pybids_config ) - database_path = dataset_path.joinpath("pybids_db") + database_path = dataset_path / "pybids_db" return BIDSLayout( dataset_path, validate=False, @@ -321,7 +320,7 @@ def write_dataset_description(layout: BIDSLayout) -> None: :param layout: BIDSLayout of the dataset to update. :type layout: BIDSLayout """ - output_file = Path(layout.root).joinpath("dataset_description.json") + output_file = Path(layout.root) / "dataset_description.json" with open(output_file, "w") as ff: json.dump(layout.dataset_description, ff, indent=4) diff --git a/bidsmreye/bidsmreye.py b/bidsmreye/bidsmreye.py index 6080b11..c82fbc6 100755 --- a/bidsmreye/bidsmreye.py +++ b/bidsmreye/bidsmreye.py @@ -29,6 +29,7 @@ def bidsmreye( bids_filter_file: str | None = None, non_linear_coreg: bool = False, log_level_name: str | None = None, + force: bool = False, ) -> None: bids_filter = None if bids_filter_file is not None and Path(bids_filter_file).is_file(): @@ -50,6 +51,7 @@ def bidsmreye( reset_database=reset_database, bids_filter=bids_filter, non_linear_coreg=non_linear_coreg, + force=force, ) # type: ignore if log_level_name is None: diff --git a/bidsmreye/configuration.py b/bidsmreye/configuration.py index 61c1def..213025b 100644 --- a/bidsmreye/configuration.py +++ b/bidsmreye/configuration.py @@ -31,14 +31,14 @@ class Config: def _check_input_dir(self, attribute: str, value: Path) -> None: if not value.is_dir: # type: ignore raise ValueError( - f"input_dir must be an existing directory:\n{value.resolve()}." + f"input_dir must be an existing directory:\n{value.absolute()}." ) - if not value.joinpath("dataset_description.json").is_file(): + if not (value / "dataset_description.json").is_file(): raise ValueError( f"""input_dir does not seem to be a valid BIDS dataset. No dataset_description.json found: -\t{value.resolve()}.""" +\t{value.absolute()}.""" ) output_dir: Path = field(default=None, converter=Path) @@ -55,6 +55,7 @@ def _check_input_dir(self, attribute: str, value: Path) -> None: debug: str | bool | None = field(kw_only=True, default=None) reset_database: bool = field(kw_only=True, default=False) non_linear_coreg: bool = field(kw_only=True, default=False) + force: bool = field(kw_only=True, default=False) has_GPU: bool = False @@ -74,11 +75,11 @@ def __attrs_post_init__(self) -> None: if not self.bids_filter: self.bids_filter = get_bids_filter_config() - self.output_dir = self.output_dir.joinpath("bidsmreye") + self.output_dir = self.output_dir / "bidsmreye" if not self.output_dir: self.output_dir.mkdir(parents=True, exist_ok=True) - database_path = self.input_dir.joinpath("pybids_db") + database_path = self.input_dir / "pybids_db" layout_in = BIDSLayout( self.input_dir, @@ -225,8 +226,8 @@ def get_config(config_file: Path | None = None, default: str = "") -> dict[str, :rtype: dict """ if config_file is None or not Path(config_file).exists(): - my_path = Path(__file__).resolve().parent.joinpath("config") - config_file = my_path.joinpath(default) + my_path = Path(__file__).absolute().parent / "config" + config_file = my_path / default if config_file is None or not Path(config_file).exists(): raise FileNotFoundError(f"Config file {config_file} not found") diff --git a/bidsmreye/download.py b/bidsmreye/download.py index 1476b9b..1bf56b6 100644 --- a/bidsmreye/download.py +++ b/bidsmreye/download.py @@ -33,13 +33,13 @@ def download( model_name = default_model() if isinstance(model_name, Path): assert model_name.is_file() - return model_name.resolve() + return model_name.absolute() if model_name not in available_models(): warnings.warn(f"{model_name} is not a valid model name.", stacklevel=3) return None if output_dir is None: - output_dir = Path.cwd().joinpath("models") + output_dir = Path.cwd() / "models" if isinstance(output_dir, str): output_dir = Path(output_dir) @@ -48,11 +48,11 @@ def download( base_url="https://osf.io/download/", registry=None, ) - source = resources.files(bidsmreye).joinpath("models/registry.txt") + source = resources.files(bidsmreye) / "models" / "registry.txt" with resources.as_file(source) as registry_file: POOCH.load_registry(registry_file) - output_file = output_dir.joinpath(f"dataset_{model_name}") + output_file = output_dir / f"dataset_{model_name}" if not output_file.is_file(): file_idx = available_models().index(model_name) diff --git a/bidsmreye/generalize.py b/bidsmreye/generalize.py index b2c70a5..41aefea 100644 --- a/bidsmreye/generalize.py +++ b/bidsmreye/generalize.py @@ -1,4 +1,4 @@ -"""TODO.""" +"""Compute eyetracking movement from preprocessed extracted data.""" from __future__ import annotations @@ -29,6 +29,7 @@ check_if_file_found, create_dir_for_file, move_file, + progress_bar, set_this_filter, ) @@ -130,9 +131,7 @@ def create_confounds_tsv(layout_out: BIDSLayout, file: str, subject_label: str) """ confound_numpy = create_bidsname(layout_out, file, "confounds_numpy") - source_file = Path(layout_out.root).joinpath( - f"sub-{subject_label}", "results_tmp.npy" - ) + source_file = Path(layout_out.root) / f"sub-{subject_label}" / "results_tmp.npy" move_file( source_file, @@ -210,9 +209,6 @@ def generalize(cfg: Config) -> None: :param cfg: Configuration object :type cfg: Config """ - log.info("GENERALIZING") - log.info(f"Using model: {cfg.model_weights_file}") - layout_out = get_dataset_layout(cfg.output_dir) check_layout(cfg, layout_out) @@ -220,7 +216,14 @@ def generalize(cfg: Config) -> None: subjects = list_subjects(cfg, layout_out) - for subject_label in subjects: - process_subject(cfg, layout_out, subject_label) + text = "GENERALIZING" + with progress_bar(text=text) as progress: + subject_loop = progress.add_task( + description="processing subject", total=len(subjects) + ) + log.info(f"Using model: {cfg.model_weights_file}") + for subject_label in subjects: + process_subject(cfg, layout_out, subject_label) + progress.update(subject_loop, advance=1) quality_control_output(cfg) diff --git a/bidsmreye/methods.py b/bidsmreye/methods.py index 8c95a79..ba6b47b 100644 --- a/bidsmreye/methods.py +++ b/bidsmreye/methods.py @@ -33,12 +33,12 @@ def methods( output_dir = Path(".") if isinstance(output_dir, str): output_dir = Path(output_dir) - output_dir = output_dir.joinpath("logs") + output_dir = output_dir / "logs" - output_file = output_dir.joinpath("CITATION.md") + output_file = output_dir / "CITATION.md" create_dir_for_file(output_file) - bib_file = str(Path(__file__).parent.joinpath("templates", "CITATION.bib")) + bib_file = str(Path(__file__).parent / "templates" / "CITATION.bib") shutil.copy(bib_file, output_dir) if not model_name: @@ -54,7 +54,7 @@ def methods( if not is_known_models: warnings.warn(f"{model_name} is not a known model name.", stacklevel=3) - template_file = str(Path(__file__).parent.joinpath("templates", "CITATION.mustache")) + template_file = str(Path(__file__).parent / "templates" / "CITATION.mustache") with open(template_file) as template: output = chevron.render( template=template, diff --git a/bidsmreye/prepare_data.py b/bidsmreye/prepare_data.py index 452c54d..bc983d9 100644 --- a/bidsmreye/prepare_data.py +++ b/bidsmreye/prepare_data.py @@ -1,4 +1,4 @@ -"""Run coregistration and extract data.""" +"""Run coregistration and extract data from eye masks in MNI space.""" from __future__ import annotations @@ -24,6 +24,7 @@ check_if_file_found, get_deepmreye_filename, move_file, + progress_bar, set_this_filter, ) @@ -40,7 +41,7 @@ def coregister_and_extract_data(img: str, non_linear_coreg: bool = False) -> Non eyemask_small, eyemask_big, dme_template, - mask, + _, x_edges, y_edges, z_edges, @@ -94,6 +95,7 @@ def combine_data_with_empty_labels(layout_out: BIDSLayout, img: Path, i: int = 1 subj["ids"].append(([entities["subject"]] * labels.shape[0], [i] * labels.shape[0])) output_file = create_bidsname(layout_out, Path(img), "no_label") + file_to_move = Path(layout_out.root) / ".." / "bidsmreye" / output_file.name preprocess.save_data( output_file.name, @@ -104,11 +106,7 @@ def combine_data_with_empty_labels(layout_out: BIDSLayout, img: Path, i: int = 1 center_labels=False, ) - file_to_move = Path(layout_out.root).joinpath("..", "bidsmreye", output_file.name) - - move_file(file_to_move, output_file) - - return output_file + return file_to_move def process_subject( @@ -141,24 +139,46 @@ def process_subject( check_if_file_found(bf, this_filter, layout_in) for img in bf: - log.info(f"Processing file: {Path(img).name}") + prepapre_image(cfg, layout_in, layout_out, img) - coregister_and_extract_data(img, non_linear_coreg=cfg.non_linear_coreg) - report_name = create_bidsname(layout_out, filename=img, filetype="report") - deepmreye_mask_report = get_deepmreye_filename( - layout_in, img=img, filetype="report" +def prepapre_image( + cfg: Config, layout_in: BIDSLayout, layout_out: BIDSLayout, img: str +) -> None: + """Preprocess a single functional image.""" + report_name = create_bidsname(layout_out, filename=img, filetype="report") + mask_name = create_bidsname(layout_out, filename=img, filetype="mask") + output_file = create_bidsname(layout_out, Path(img), "no_label") + + if ( + not cfg.force + and report_name.exists() + and mask_name.exists() + and output_file.exists() + ): + log.debug( + "Output for the following file already exists. " + "Use the '--force' option to overwrite. " + f"\n '{Path(img).name}'" ) - move_file(deepmreye_mask_report, report_name) + return - mask_name = create_bidsname(layout_out, filename=img, filetype="mask") - deepmreye_mask_name = get_deepmreye_filename(layout_in, img=img, filetype="mask") - move_file(deepmreye_mask_name, mask_name) + log.info(f"Processing file: {Path(img).name}") - source = str(Path(img).relative_to(layout_in.root)) - save_sampling_frequency_to_json(layout_out, img=img, source=source) + coregister_and_extract_data(img, non_linear_coreg=cfg.non_linear_coreg) - combine_data_with_empty_labels(layout_out, mask_name) + deepmreye_mask_report = get_deepmreye_filename(layout_in, img=img, filetype="report") + move_file(deepmreye_mask_report, report_name) + + deepmreye_mask_name = get_deepmreye_filename(layout_in, img=img, filetype="mask") + move_file(deepmreye_mask_name, mask_name) + + source = str(Path(img).relative_to(layout_in.root)) + save_sampling_frequency_to_json(layout_out, img=img, source=source) + + combine_data_with_empty_labels(layout_out, mask_name) + file_to_move = Path(layout_out.root) / ".." / "bidsmreye" / output_file.name + move_file(file_to_move, output_file) def prepare_data(cfg: Config) -> None: @@ -167,8 +187,6 @@ def prepare_data(cfg: Config) -> None: :param cfg: Configuration object :type cfg: Config """ - log.info("PREPARING DATA") - layout_in = get_dataset_layout( cfg.input_dir, use_database=True, @@ -181,8 +199,14 @@ def prepare_data(cfg: Config) -> None: subjects = list_subjects(cfg, layout_in) + text = "PREPARING DATA" if cfg.non_linear_coreg: - log.debug("Using non-linear coregistration") + log.info("Using non-linear coregistration") - for subject_label in subjects: - process_subject(cfg, layout_in, layout_out, subject_label) + with progress_bar(text=text) as progress: + subject_loop = progress.add_task( + description="processing subject", total=len(subjects) + ) + for subject_label in subjects: + process_subject(cfg, layout_in, layout_out, subject_label) + progress.update(subject_loop, advance=1) diff --git a/bidsmreye/quality_control.py b/bidsmreye/quality_control.py index de0d7af..5b22719 100644 --- a/bidsmreye/quality_control.py +++ b/bidsmreye/quality_control.py @@ -1,4 +1,4 @@ -"""TODO.""" +"""Tools to compute and plot quality controls at the file or group level.""" from __future__ import annotations @@ -21,7 +21,12 @@ ) from bidsmreye.configuration import Config from bidsmreye.logging import bidsmreye_log -from bidsmreye.utils import check_if_file_found, create_dir_for_file, set_this_filter +from bidsmreye.utils import ( + check_if_file_found, + create_dir_for_file, + progress_bar, + set_this_filter, +) from bidsmreye.visualize import visualize_eye_gaze_data log = bidsmreye_log("bidsmreye") @@ -86,7 +91,10 @@ def compute_displacement_and_outliers(confounds: pd.DataFrame) -> pd.DataFrame: def perform_quality_control( - layout_in: BIDSLayout, confounds_tsv: str | Path, layout_out: BIDSLayout | None = None + cfg: Config, + layout_in: BIDSLayout, + confounds_tsv: str | Path, + layout_out: BIDSLayout | None = None, ) -> None: """Perform quality control on the confounds. @@ -103,6 +111,15 @@ def perform_quality_control( layout_out = layout_in confounds_tsv = Path(confounds_tsv) + visualization_html_file = create_bidsname(layout_out, confounds_tsv, "confounds_html") + if not cfg.force and visualization_html_file.exists(): + log.debug( + "Output for the following file already exists. " + "Use the '--force' option to overwrite. " + f"\n '{confounds_tsv.name}'" + ) + return + confounds = pd.read_csv(confounds_tsv, sep="\t") if "eye_timestamp" not in confounds.columns: @@ -128,7 +145,7 @@ def perform_quality_control( fig.update_layout(title=Path(confounds_tsv).name) if log.isEnabledFor(logging.DEBUG): fig.show() - visualization_html_file = create_bidsname(layout_out, confounds_tsv, "confounds_html") + create_dir_for_file(visualization_html_file) fig.write_html(visualization_html_file) @@ -155,21 +172,23 @@ def get_sampling_frequency(layout: BIDSLayout, file: str | Path) -> float | None def quality_control_output(cfg: Config) -> None: """Run quality control on the output dataset.""" - log.info("QUALITY CONTROL") - layout_out = get_dataset_layout(cfg.output_dir) check_layout(cfg, layout_out) subjects = list_subjects(cfg, layout_out) - for subject_label in subjects: - qc_subject(cfg, layout_out, subject_label) + text = "QUALITY CONTROL" + with progress_bar(text=text) as progress: + subject_loop = progress.add_task( + description="processing subject", total=len(subjects) + ) + for subject_label in subjects: + qc_subject(cfg, layout_out, subject_label) + progress.update(subject_loop, advance=1) def quality_control_input(cfg: Config) -> None: """Run quality control on the input dataset.""" - log.info("QUALITY CONTROL") - layout_in = get_dataset_layout(cfg.input_dir) check_layout(cfg, layout_in, "eyetrack") @@ -177,8 +196,14 @@ def quality_control_input(cfg: Config) -> None: subjects = list_subjects(cfg, layout_in) - for subject_label in subjects: - qc_subject(cfg, layout_in, subject_label, layout_out) + text = "QUALITY CONTROL" + with progress_bar(text=text) as progress: + subject_loop = progress.add_task( + description="processing subject", total=len(subjects) + ) + for subject_label in subjects: + qc_subject(cfg, layout_in, subject_label, layout_out) + progress.update(subject_loop, advance=1) def qc_subject( @@ -201,7 +226,7 @@ def qc_subject( check_if_file_found(bf, this_filter, layout_in) for file in bf: - perform_quality_control(layout_in, file, layout_out) + perform_quality_control(cfg, layout_in, file, layout_out) def compute_robust_outliers( diff --git a/bidsmreye/utils.py b/bidsmreye/utils.py index f8dc4ed..be77500 100644 --- a/bidsmreye/utils.py +++ b/bidsmreye/utils.py @@ -7,6 +7,16 @@ from typing import Any from bids import BIDSLayout # type: ignore +from rich.progress import ( + BarColumn, + MofNCompleteColumn, + Progress, + SpinnerColumn, + TaskProgressColumn, + TextColumn, + TimeElapsedColumn, + TimeRemainingColumn, +) from bidsmreye.configuration import Config from bidsmreye.logging import bidsmreye_log @@ -14,18 +24,30 @@ log = bidsmreye_log(name="bidsmreye") +def progress_bar(text: str, color: str = "green") -> Progress: + return Progress( + TextColumn(f"[{color}]{text}"), + SpinnerColumn("dots"), + TimeElapsedColumn(), + BarColumn(), + MofNCompleteColumn(), + TaskProgressColumn(), + TimeRemainingColumn(), + ) + + def copy_license(output_dir: Path) -> Path: """Copy CCO license to output directory. :param output_dir: :type output_dir: Path """ - input_file = str(Path(__file__).parent.joinpath("templates", "CCO")) - output_file = output_dir.joinpath("LICENSE") + input_file = str(Path(__file__).parent / "templates" / "CCO") + output_file = output_dir / "LICENSE" create_dir_if_absent(output_dir) - if not output_dir.joinpath("LICENSE").is_file(): + if not (output_dir / "LICENSE").is_file(): shutil.copy(input_file, output_dir) - move_file(output_dir.joinpath("CCO"), output_file) + move_file(output_dir / "CCO", output_file) return output_file @@ -37,7 +59,7 @@ def add_sidecar_in_root(layout_out: BIDSLayout) -> None: "SampleCoordinateSystem": "gaze-on-screen", "RecordedEye": "both", } - sidecar_name = Path(layout_out.root).joinpath("desc-bidsmreye_eyetrack.json") + sidecar_name = Path(layout_out.root) / "desc-bidsmreye_eyetrack.json" json.dump(content, open(sidecar_name, "w"), indent=4) @@ -80,7 +102,7 @@ def move_file(input: Path, output: Path) -> None: :param root: Optional. If specified, the printed path will be relative to this path. :type root: Path """ - log.debug(f"{input.resolve()} --> {output.resolve()}") + log.debug(f"{input.absolute()} --> {output.absolute()}") create_dir_for_file(output) shutil.copy(input, output) input.unlink() @@ -105,7 +127,7 @@ def create_dir_for_file(file: Path) -> None: :param file: :type file: Path """ - output_path = file.resolve().parent + output_path = file.absolute().parent create_dir_if_absent(output_path) # TODO refactor with create_dir_if_absent @@ -165,10 +187,7 @@ def get_deepmreye_filename( filename = return_deepmreye_output_filename(filename, filetype) - filefolder = Path(img).parent - filefolder = filefolder.joinpath(filename) - - return Path(filefolder).resolve() + return Path(img).parent.absolute() / filename def return_deepmreye_output_filename(filename: str, filetype: str | None = None) -> str: diff --git a/bidsmreye/visualize.py b/bidsmreye/visualize.py index cd61000..c84a6eb 100644 --- a/bidsmreye/visualize.py +++ b/bidsmreye/visualize.py @@ -39,7 +39,7 @@ log = bidsmreye_log(name="bidsmreye") -def collect_group_qc_data(cfg: Config) -> pd.DataFrame: +def collect_group_qc_data(cfg: Config) -> pd.DataFrame | None: """Collect QC metrics data from all subjects json in a BIDS dataset. :param input_dir: @@ -75,6 +75,9 @@ def collect_group_qc_data(cfg: Config) -> pd.DataFrame: df["subject"] = entities["subject"] qc_data = df if i == 0 else pd.concat([qc_data, df], sort=False) + if qc_data is None: + return None + cols = [ "subject", "filename", @@ -84,7 +87,7 @@ def collect_group_qc_data(cfg: Config) -> pd.DataFrame: "eye1XVar", "eye1YVar", ] - qc_data = qc_data[cols] # type: ignore + qc_data = qc_data[cols] return qc_data @@ -133,6 +136,10 @@ def group_report(cfg: Config) -> None: """ qc_data = collect_group_qc_data(cfg) + if qc_data is None: + log.warning("No data found.") + return + fig = go.FigureWidget( make_subplots( rows=2, @@ -229,10 +236,10 @@ def group_report(cfg: Config) -> None: ) fig.show() - group_report_file = Path(cfg.output_dir).joinpath("group_eyetrack.html") + group_report_file = cfg.output_dir / "group_eyetrack.html" fig.write_html(group_report_file) - qc_data_file = Path(cfg.output_dir).joinpath("group_eyetrack.tsv") + qc_data_file = cfg.output_dir / "group_eyetrack.tsv" qc_data.to_csv(qc_data_file, sep="\t", index=False) diff --git a/pyproject.toml b/pyproject.toml index 7ce7375..899b4ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,6 @@ dependencies = [ "kaleido", "pooch>=1.6.0", "pybids", - "rich", "tqdm", "tomli; python_version < '3.11'", "keras<3.0.0", diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..98d5ec3 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import json +import shutil +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest +from bids.tests import get_test_data_path + +from bidsmreye.quality_control import compute_displacement, compute_robust_outliers + + +@pytest.fixture +def pybids_test_dataset() -> Path: + return Path(get_test_data_path()) / "synthetic" / "derivatives" / "fmriprep" + + +@pytest.fixture +def data_dir(): + return Path(__file__).parent / "data" + + +@pytest.fixture +def output_dir(tmp_path, data_dir): + src_dir = data_dir / "bidsmreye" + target_dir = tmp_path / "bidsmreye" + target_dir.mkdir() + shutil.copytree(src_dir, target_dir, dirs_exist_ok=True) + return target_dir + + +@pytest.fixture +def bidsmreye_eyetrack_tsv(output_dir): + return ( + output_dir + / "sub-01" + / "func" + / "sub-01_task-nback_space-MNI152NLin2009cAsym_desc-bidsmreye_eyetrack.tsv" + ) + + +@pytest.fixture +def create_basic_data(): + return { + "eye1_x_coordinate": np.random.randn(400), + "eye1_y_coordinate": np.random.randn(400), + } + + +@pytest.fixture +def create_basic_json(output_dir): + sidecar_name = ( + output_dir + / "sub-01" + / "func" + / "sub-01_task-nback_space-MNI152NLin2009cAsym_desc-bidsmreye_eyetrack.json" + ) + + content = {"SamplingFrequency": 0.14285714285714285} + + json.dump(content, open(sidecar_name, "w"), indent=4) + + +@pytest.fixture +def create_confounds_tsv(bidsmreye_eyetrack_tsv, generate_confounds_tsv): + generate_confounds_tsv(bidsmreye_eyetrack_tsv) + + +@pytest.fixture +def generate_confounds_tsv(create_data_with_outliers): + + def _generate_confounds_tsv(filename): + df = pd.DataFrame(create_data_with_outliers) + + df["displacement"] = compute_displacement( + df["eye1_x_coordinate"], + df["eye1_y_coordinate"], + ) + df["eye1_x_outliers"] = compute_robust_outliers( + df["eye1_x_coordinate"], outlier_type="Carling" + ) + df["eye1_y_outliers"] = compute_robust_outliers( + df["eye1_y_coordinate"], outlier_type="Carling" + ) + df["displacement_outliers"] = compute_robust_outliers( + df["displacement"], outlier_type="Carling" + ) + + cols = df.columns.tolist() + cols.insert(0, cols.pop(cols.index("eye_timestamp"))) + df = df[cols] + + df.to_csv(filename, sep="\t", index=False) + + return _generate_confounds_tsv + + +@pytest.fixture +def create_data_with_outliers(create_basic_data): + data = create_basic_data + + data["eye_timestamp"] = np.arange(400) + + eye1_x_coordinate = data["eye1_x_coordinate"] + eye1_y_coordinate = data["eye1_y_coordinate"] + + data["eye1_x_coordinate"][200] = ( + eye1_x_coordinate.mean() + eye1_x_coordinate.std() * 4 + ) + data["eye1_y_coordinate"][200] = ( + eye1_y_coordinate.mean() - eye1_y_coordinate.std() * 5 + ) + data["eye1_x_coordinate"][50] = eye1_x_coordinate.mean() - eye1_x_coordinate.std() * 5 + data["eye1_y_coordinate"][50] = eye1_y_coordinate.mean() + eye1_y_coordinate.std() * 4 + + return data + + +def rm_dir(some_dir): + if Path(some_dir).is_dir(): + shutil.rmtree(some_dir, ignore_errors=False) diff --git a/tests/data/bidsmreye/sub-01/func/sub-01_task-nback_space-MNI152NLin2009cAsym_desc-bidsmreye_eyetrack.json b/tests/data/bidsmreye/sub-01/func/sub-01_task-nback_space-MNI152NLin2009cAsym_desc-bidsmreye_eyetrack.json index bcb4986..0e12a8f 100644 --- a/tests/data/bidsmreye/sub-01/func/sub-01_task-nback_space-MNI152NLin2009cAsym_desc-bidsmreye_eyetrack.json +++ b/tests/data/bidsmreye/sub-01/func/sub-01_task-nback_space-MNI152NLin2009cAsym_desc-bidsmreye_eyetrack.json @@ -1,8 +1,3 @@ { - "SamplingFrequency": 0.14285714285714285, - "NbDisplacementOutliers": 5.0, - "NbXOutliers": 2.0, - "NbYOutliers": 2.0, - "eye1XVar": 0.9617292851848753, - "eye1YVar": 1.0699594318785 + "SamplingFrequency": 0.14285714285714285 } diff --git a/tests/test_bids_utils.py b/tests/test_bids_utils.py index 61c2ae7..c0eb310 100644 --- a/tests/test_bids_utils.py +++ b/tests/test_bids_utils.py @@ -17,27 +17,30 @@ from bidsmreye.prepare_data import save_sampling_frequency_to_json from bidsmreye.utils import set_this_filter -from .utils import pybids_test_dataset - def test_create_bidsname(tmp_path): output_dir = tmp_path / "derivatives" layout = get_dataset_layout(output_dir) - filename = Path("inputs").joinpath( - "raw", - "sub-01", - "ses-01", - "func", - "sub-01_ses-01_task-motion_run-01_bold.nii", + filename = ( + Path("inputs") + / "raw" + / "sub-01" + / "ses-01" + / "func" + / "sub-01_ses-01_task-motion_run-01_bold.nii" ) output_file = create_bidsname(layout, filename=filename, filetype="mask") rel_path = output_file.relative_to(layout.root) - assert rel_path == Path("sub-01").joinpath( - "ses-01", "func", "sub-01_ses-01_task-motion_run-01_desc-eye_mask.p" + assert ( + rel_path + == Path("sub-01") + / "ses-01" + / "func" + / "sub-01_ses-01_task-motion_run-01_desc-eye_mask.p" ) @@ -45,35 +48,35 @@ def test_get_dataset_layout_smoke_test(tmp_path): get_dataset_layout(tmp_path / "data") -def test_init_dataset(tmp_path): +def test_init_dataset(tmp_path, pybids_test_dataset): output_dir = tmp_path / "derivatives" cfg = Config( - pybids_test_dataset(), + pybids_test_dataset, output_dir, ) init_dataset(cfg) -def test_list_subjects(): +def test_list_subjects(data_dir, pybids_test_dataset): cfg = Config( - pybids_test_dataset(), - Path(__file__).parent.joinpath("data"), + pybids_test_dataset, + data_dir, ) - layout = get_dataset_layout(pybids_test_dataset()) + layout = get_dataset_layout(pybids_test_dataset) subjects = list_subjects(cfg, layout) assert len(subjects) == 5 -def test_save_sampling_frequency_to_json(): - layout_in = get_dataset_layout(pybids_test_dataset()) +def test_save_sampling_frequency_to_json(data_dir, pybids_test_dataset): + layout_in = get_dataset_layout(pybids_test_dataset) cfg = Config( - pybids_test_dataset(), - Path(__file__).parent.joinpath("data"), + pybids_test_dataset, + data_dir, ) this_filter = set_this_filter(cfg, "01", "bold") @@ -91,10 +94,10 @@ def test_save_sampling_frequency_to_json(): assert content["Sources"][0] == "foo" -def test_check_layout_prepare_data(): +def test_check_layout_prepare_data(data_dir, pybids_test_dataset): cfg = Config( - pybids_test_dataset(), - Path(__file__).parent.joinpath("data"), + pybids_test_dataset, + data_dir, ) layout_in = get_dataset_layout( @@ -106,8 +109,8 @@ def test_check_layout_prepare_data(): check_layout(cfg, layout_in) -def test_check_layout_error_no_space_entity(tmp_path): - shutil.copytree(pybids_test_dataset(), tmp_path, dirs_exist_ok=True) +def test_check_layout_error_no_space_entity(tmp_path, pybids_test_dataset): + shutil.copytree(pybids_test_dataset, tmp_path, dirs_exist_ok=True) for file in tmp_path.rglob("*_space-*"): file.unlink() diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 72b09f7..eac2598 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1,7 +1,5 @@ from __future__ import annotations -from pathlib import Path - import pytest from bidsmreye.configuration import ( @@ -12,27 +10,25 @@ get_pybids_config, ) -from .utils import pybids_test_dataset - -def test_Config(): +def test_Config(data_dir, pybids_test_dataset): cfg = Config( - pybids_test_dataset(), - Path(__file__).parent.joinpath("data"), + pybids_test_dataset, + data_dir, ) assert not cfg.debug assert not cfg.non_linear_coreg - assert cfg.input_dir == pybids_test_dataset() - assert cfg.output_dir == Path(__file__).parent.joinpath("data", "bidsmreye") + assert cfg.input_dir == pybids_test_dataset + assert cfg.output_dir == data_dir / "bidsmreye" assert sorted(cfg.subjects) == ["01", "02", "03", "04", "05"] assert sorted(cfg.task) == ["nback", "rest"] assert sorted(cfg.space) == ["MNI152NLin2009cAsym", "T1w"] -def test_config_to_dict_smoke(): +def test_config_to_dict_smoke(data_dir, pybids_test_dataset): cfg = Config( - pybids_test_dataset(), - Path(__file__).parent.joinpath("data"), + pybids_test_dataset, + data_dir, ) config_to_dict(cfg) @@ -52,55 +48,55 @@ def test_get_pybids_config_smoke(): assert cfg is not None -def test_missing_subject(): +def test_missing_subject(data_dir, pybids_test_dataset): with pytest.warns(UserWarning): Config( - pybids_test_dataset(), - Path(__file__).parent.joinpath("data"), + pybids_test_dataset, + data_dir, subjects=["01", "07"], ) -def test_missing_task(): +def test_missing_task(data_dir, pybids_test_dataset): with pytest.warns(UserWarning): Config( - pybids_test_dataset(), - Path(__file__).parent.joinpath("data"), + pybids_test_dataset, + data_dir, task=["auditory", "rest"], ) -def test_no_subject(): +def test_no_subject(data_dir, pybids_test_dataset): with pytest.raises(RuntimeError): Config( - pybids_test_dataset(), - Path(__file__).parent.joinpath("data"), + pybids_test_dataset, + data_dir, subjects=["99"], ) -def test_no_task(): +def test_no_task(data_dir, pybids_test_dataset): with pytest.raises(RuntimeError): Config( - pybids_test_dataset(), - Path(__file__).parent.joinpath("data"), + pybids_test_dataset, + data_dir, task=["foo"], ) -def test_missing_space(): +def test_missing_space(data_dir, pybids_test_dataset): with pytest.warns(UserWarning): Config( - pybids_test_dataset(), - Path(__file__).parent.joinpath("data"), + pybids_test_dataset, + data_dir, space=["T1w", "T2w"], ) -def test_task_omit_missing_values(): +def test_task_omit_missing_values(data_dir, pybids_test_dataset): cfg = Config( - pybids_test_dataset(), - Path(__file__).parent.joinpath("data"), + pybids_test_dataset, + data_dir, task=["auditory", "rest"], subjects=["01", "07"], space=["T1w", "T2w"], diff --git a/tests/test_download.py b/tests/test_download.py index 22d25f6..aba4076 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -19,10 +19,10 @@ def test_download_basic(): download() output_file = download() - output_dir = Path.cwd().joinpath("models") + output_dir = Path.cwd() / "models" print(output_file) assert output_dir.is_dir() - assert output_dir.joinpath("dataset_1to6.h5").is_file() + assert (output_dir / "dataset_1to6.h5").is_file() shutil.rmtree(output_dir) diff --git a/tests/test_generalize.py b/tests/test_generalize.py index a2113fe..21e621c 100644 --- a/tests/test_generalize.py +++ b/tests/test_generalize.py @@ -1,22 +1,19 @@ from __future__ import annotations import shutil -from pathlib import Path from bidsmreye.bids_utils import get_dataset_layout from bidsmreye.generalize import convert_confounds -def test_convert_confounds(): - output_dir = Path().resolve() - output_dir = output_dir.joinpath("tests", "data", "bidsmreye") - +def test_convert_confounds(output_dir): layout_out = get_dataset_layout(output_dir) - file = output_dir.joinpath( - "sub-01", - "func", - "sub-01_task-nback_space-MNI152NLin2009cAsym_desc-bidsmreye_confounds.npy", + file = ( + output_dir + / "sub-01" + / "func" + / "sub-01_task-nback_space-MNI152NLin2009cAsym_desc-bidsmreye_confounds.npy" ) shutil.copy(file, file.with_suffix(".bak")) diff --git a/tests/test_prepare.py b/tests/test_prepare.py index 0bff4ac..cbb5637 100644 --- a/tests/test_prepare.py +++ b/tests/test_prepare.py @@ -2,32 +2,26 @@ from pathlib import Path -from bidsmreye.bids_utils import get_dataset_layout +from bidsmreye.bids_utils import create_bidsname, get_dataset_layout from bidsmreye.prepare_data import combine_data_with_empty_labels -def test_combine_data_with_empty_labels(): - output_dir = Path().resolve() - output_dir = output_dir.joinpath("tests", "data", "bidsmreye") - +def test_combine_data_with_empty_labels(output_dir): layout_out = get_dataset_layout(output_dir) - file = output_dir.joinpath( - "sub-01", - "func", - "sub-01_task-nback_run-01_space-MNI152NLin2009cAsym_desc-eye_mask.p", + file = ( + output_dir + / "sub-01" + / "func" + / "sub-01_task-nback_run-01_space-MNI152NLin2009cAsym_desc-eye_mask.p" ) no_label_file = combine_data_with_empty_labels(layout_out, file) - assert no_label_file.is_file() - - expected_file = output_dir.joinpath( - "sub-01", - "func", - "sub-01_task-nback_run-01_space-MNI152NLin2009cAsym_desc-nolabel_bidsmreye.npz", - ) + assert no_label_file.exists() - assert no_label_file == expected_file + output_file = create_bidsname(layout_out, file, "no_label") + file_to_move = Path(layout_out.root) / ".." / "bidsmreye" / output_file.name + assert no_label_file == file_to_move no_label_file.unlink() diff --git a/tests/test_quality_control.py b/tests/test_quality_control.py index 54f2703..6f500c0 100644 --- a/tests/test_quality_control.py +++ b/tests/test_quality_control.py @@ -18,13 +18,7 @@ quality_control_output, ) -from .utils import ( - create_basic_data, - create_basic_json, - create_confounds_tsv, - return_bidsmreye_eyetrack_tsv, - rm_dir, -) +from .conftest import rm_dir def time_series(): @@ -52,9 +46,8 @@ def time_series(): ] -def test_get_sampling_frequency(): - ds_location = Path().resolve().joinpath("tests", "data", "bidsmreye") - layout = get_dataset_layout(ds_location) +def test_get_sampling_frequency(output_dir): + layout = get_dataset_layout(output_dir) file = layout.get(return_type="filename", suffix="eyetrack")[0] @@ -64,8 +57,8 @@ def test_get_sampling_frequency(): @pytest.mark.xfail(reason="not implemented yet") -def test_get_sampling_frequency_in_root(): - ds_location = Path().resolve().joinpath("tests", "data", "ds000201-der") +def test_get_sampling_frequency_in_root(data_dir): + ds_location = data_dir / "ds000201-der" layout = get_dataset_layout(ds_location) file = layout.get(return_type="filename", subject="9001", suffix="eyetrack")[0] @@ -116,27 +109,24 @@ def test_compute_robust_with_nan(): assert outliers == expected_outliers -def test_quality_control_output(): - create_basic_json() +def test_quality_control_output( + create_basic_data, create_basic_json, bidsmreye_eyetrack_tsv, data_dir +): + output_dir = data_dir - output_dir = Path().resolve() - output_dir = output_dir.joinpath("tests", "data") - - confounds_tsv = return_bidsmreye_eyetrack_tsv() - - df = pd.DataFrame(create_basic_data()) - df.to_csv(confounds_tsv, sep="\t", index=False) + df = pd.DataFrame(create_basic_data) + df.to_csv(bidsmreye_eyetrack_tsv, sep="\t", index=False) cfg = Config( - output_dir.joinpath("bidsmreye"), + output_dir / "bidsmreye", output_dir, ) quality_control_output(cfg) -def test_quality_control_input(tmp_path): - input_dir = Path("tests") / "data" / "ds000201-der" +def test_quality_control_input(tmp_path, data_dir): + input_dir = data_dir / "ds000201-der" output_dir = tmp_path / "derivatives" cfg = Config( @@ -147,57 +137,63 @@ def test_quality_control_input(tmp_path): quality_control_input(cfg) -def test_perform_quality_control(): - create_basic_json() - - output_dir = Path().resolve() - output_dir = output_dir.joinpath("tests", "data", "bidsmreye") +def test_perform_quality_control( + output_dir, + pybids_test_dataset, + create_basic_data, + create_basic_json, + bidsmreye_eyetrack_tsv, +): layout = get_dataset_layout(output_dir) - confounds_tsv = return_bidsmreye_eyetrack_tsv() - df = pd.DataFrame(create_basic_data()) - df.to_csv(confounds_tsv, sep="\t", index=False) + df = pd.DataFrame(create_basic_data) + df.to_csv(bidsmreye_eyetrack_tsv, sep="\t", index=False) - perform_quality_control(layout, confounds_tsv) + cfg = Config( + pybids_test_dataset, + Path(__file__).parent / "data", + ) + perform_quality_control(cfg, layout, bidsmreye_eyetrack_tsv) -def test_perform_quality_control_with_different_output(): - input_dir = Path().resolve().joinpath("tests", "data", "ds000201-der") + +def test_perform_quality_control_with_different_output(data_dir, pybids_test_dataset): + input_dir = data_dir / "ds000201-der" layout_in = get_dataset_layout(input_dir) - output_dir = input_dir.joinpath("derivatives", "bidsmreye") + output_dir = input_dir / "derivatives" / "bidsmreye" layout_out = get_dataset_layout(output_dir) confounds_tsv = layout_in.get( return_type="filename", subject="9001", suffix="eyetrack", extension=".tsv" )[0] + cfg = Config( + pybids_test_dataset, + Path(__file__).parent / "data", + ) + perform_quality_control( - layout_in=layout_in, confounds_tsv=confounds_tsv, layout_out=layout_out + cfg=cfg, layout_in=layout_in, confounds_tsv=confounds_tsv, layout_out=layout_out ) rm_dir(output_dir) -def test_add_qc_to_sidecar(): - create_basic_json() - - create_confounds_tsv() - - output_dir = Path().resolve() - output_dir = output_dir.joinpath("tests", "data", "bidsmreye") +def test_add_qc_to_sidecar( + output_dir, create_confounds_tsv, create_basic_json, bidsmreye_eyetrack_tsv +): layout_out = get_dataset_layout(output_dir) - confounds_tsv = return_bidsmreye_eyetrack_tsv() - confounds = pd.read_csv(confounds_tsv, sep="\t") + confounds = pd.read_csv(bidsmreye_eyetrack_tsv, sep="\t") - sidecar_name = create_bidsname(layout_out, confounds_tsv, "confounds_json") + sidecar_name = create_bidsname(layout_out, bidsmreye_eyetrack_tsv, "confounds_json") add_qc_to_sidecar(confounds, sidecar_name) -def test_add_qc_to_sidecar_if_missing(): - ds_location = Path().resolve().joinpath("tests", "data", "ds000201-der") +def test_add_qc_to_sidecar_if_missing(data_dir): + ds_location = data_dir / "ds000201-der" layout = get_dataset_layout(ds_location) file = layout.get( diff --git a/tests/test_utils.py b/tests/test_utils.py index 985dc2d..67c50d5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -12,8 +12,6 @@ set_this_filter, ) -from .utils import pybids_test_dataset - def test_copy_license(tmp_path): output_dir = tmp_path / "derivatives" @@ -21,22 +19,23 @@ def test_copy_license(tmp_path): license_file = copy_license(output_dir) assert license_file.is_file() - assert str(license_file) == str(output_dir.joinpath("LICENSE")) + assert str(license_file) == str(output_dir / "LICENSE") copy_license(output_dir) -def test_get_deepmreye_filename(): - layout = get_dataset_layout(pybids_test_dataset()) +def test_get_deepmreye_filename(pybids_test_dataset): + layout = get_dataset_layout(pybids_test_dataset) - output_file = Path(pybids_test_dataset()).joinpath( - "sub-01", - "ses-01", - "func", - ( + output_file = ( + Path(pybids_test_dataset) + / "sub-01" + / "ses-01" + / "func" + / ( "mask_sub-01_ses-01_task-nback_run-01_" "space-MNI152NLin2009cAsym_desc-preproc_bold.p" - ), + ) ) img = layout.get( @@ -72,12 +71,9 @@ def test_return_regex(): assert return_regex(["foo", "bar"]) == "^foo$|^bar$" -def test_set_this_filter_bold(): - output_dir = Path().resolve() - output_dir = Path.joinpath(output_dir, "derivatives") - +def test_set_this_filter_bold(pybids_test_dataset, output_dir): cfg = Config( - pybids_test_dataset(), + pybids_test_dataset, output_dir, ) @@ -97,11 +93,8 @@ def test_set_this_filter_bold(): } -def test_set_this_filter_bidsmreye(): - output_dir = Path().resolve() - output_dir = Path.joinpath(output_dir, "data", "bidsmreye") - - cfg = Config(pybids_test_dataset(), output_dir, run="1") +def test_set_this_filter_bidsmreye(output_dir, pybids_test_dataset): + cfg = Config(pybids_test_dataset, output_dir, run="1") this_filter = set_this_filter(cfg, subject_label="001", filter_type="eyetrack") @@ -116,7 +109,7 @@ def test_set_this_filter_bidsmreye(): } -def test_set_this_filter_with_bids_filter_file(): +def test_set_this_filter_with_bids_filter_file(output_dir, pybids_test_dataset): bids_filter = { "eyetrack": { "suffix": "^eyetrack$$", @@ -125,10 +118,7 @@ def test_set_this_filter_with_bids_filter_file(): } } - output_dir = Path().resolve() - output_dir = Path.joinpath(output_dir, "data", "bidsmreye") - - cfg = Config(pybids_test_dataset(), output_dir, run="1", bids_filter=bids_filter) + cfg = Config(pybids_test_dataset, output_dir, run="1", bids_filter=bids_filter) this_filter = set_this_filter(cfg, subject_label="001", filter_type="eyetrack") diff --git a/tests/test_visualize.py b/tests/test_visualize.py index c6d873d..acec136 100644 --- a/tests/test_visualize.py +++ b/tests/test_visualize.py @@ -1,6 +1,6 @@ from __future__ import annotations -from pathlib import Path +import shutil import pandas as pd @@ -8,39 +8,44 @@ from bidsmreye.configuration import Config from bidsmreye.visualize import group_report, visualize_eye_gaze_data -from .utils import create_confounds_tsv, return_bidsmreye_eyetrack_tsv - - -def test_visualize_eye_gaze_data(): - confounds_tsv = return_bidsmreye_eyetrack_tsv() - - create_confounds_tsv() - - eye_gaze_data = pd.read_csv(confounds_tsv, sep="\t") +def test_visualize_eye_gaze_data(create_confounds_tsv, bidsmreye_eyetrack_tsv): + eye_gaze_data = pd.read_csv(bidsmreye_eyetrack_tsv, sep="\t") fig = visualize_eye_gaze_data(eye_gaze_data) - fig.show() -def test_group_report(): - input_dir = Path().resolve().joinpath("tests", "data", "derivatives", "bidsmreye") +def test_group_report(tmp_path, data_dir): + src_dir = data_dir / "derivatives" / "bidsmreye" + target_dir = tmp_path / "bidsmreye" + target_dir.mkdir() + shutil.copytree(src_dir, target_dir, dirs_exist_ok=True) cfg = Config( - input_dir=input_dir, - output_dir=input_dir.parent, + input_dir=target_dir, + output_dir=target_dir.parent, ) group_report(cfg) + assert (target_dir / "group_eyetrack.html").exists() + assert (target_dir / "group_eyetrack.tsv").exists() + -def test_group_report_cli(): - bids_dir = Path().resolve().joinpath("tests", "data", "derivatives", "bidsmreye") +def test_group_report_cli(tmp_path, data_dir): + + src_dir = data_dir / "derivatives" / "bidsmreye" + target_dir = tmp_path / "bidsmreye" + target_dir.mkdir() + shutil.copytree(src_dir, target_dir, dirs_exist_ok=True) bidsmreye( - bids_dir=bids_dir, - output_dir=bids_dir.parent, + bids_dir=target_dir, + output_dir=target_dir.parent, analysis_level="group", action="qc", participant_label=["9001", "9008"], ) + + assert (target_dir / "group_eyetrack.html").exists() + assert (target_dir / "group_eyetrack.tsv").exists() diff --git a/tests/utils.py b/tests/utils.py deleted file mode 100644 index 6cf5db1..0000000 --- a/tests/utils.py +++ /dev/null @@ -1,99 +0,0 @@ -from __future__ import annotations - -import json -import shutil -from pathlib import Path - -import numpy as np -import pandas as pd -from bids.tests import get_test_data_path - -from bidsmreye.quality_control import compute_displacement, compute_robust_outliers - - -def create_basic_data(): - return { - "eye1_x_coordinate": np.random.randn(400), - "eye1_y_coordinate": np.random.randn(400), - } - - -def create_basic_json(): - output_dir = Path().resolve() - output_dir = output_dir.joinpath("tests", "data", "bidsmreye") - - sidecar_name = output_dir.joinpath( - "sub-01", - "func", - "sub-01_task-nback_space-MNI152NLin2009cAsym_desc-bidsmreye_eyetrack.json", - ) - - content = {"SamplingFrequency": 0.14285714285714285} - - json.dump(content, open(sidecar_name, "w"), indent=4) - - -def create_confounds_tsv(): - confounds_tsv = return_bidsmreye_eyetrack_tsv() - - df = pd.DataFrame(create_data_with_outliers()) - - df["displacement"] = compute_displacement( - df["eye1_x_coordinate"], - df["eye1_y_coordinate"], - ) - df["eye1_x_outliers"] = compute_robust_outliers( - df["eye1_x_coordinate"], outlier_type="Carling" - ) - df["eye1_y_outliers"] = compute_robust_outliers( - df["eye1_y_coordinate"], outlier_type="Carling" - ) - df["displacement_outliers"] = compute_robust_outliers( - df["displacement"], outlier_type="Carling" - ) - - cols = df.columns.tolist() - cols.insert(0, cols.pop(cols.index("eye_timestamp"))) - df = df[cols] - - df.to_csv(confounds_tsv, sep="\t", index=False) - - -def create_data_with_outliers(): - data = create_basic_data() - - data["eye_timestamp"] = np.arange(400) - - eye1_x_coordinate = data["eye1_x_coordinate"] - eye1_y_coordinate = data["eye1_y_coordinate"] - - data["eye1_x_coordinate"][200] = ( - eye1_x_coordinate.mean() + eye1_x_coordinate.std() * 4 - ) - data["eye1_y_coordinate"][200] = ( - eye1_y_coordinate.mean() - eye1_y_coordinate.std() * 5 - ) - data["eye1_x_coordinate"][50] = eye1_x_coordinate.mean() - eye1_x_coordinate.std() * 5 - data["eye1_y_coordinate"][50] = eye1_y_coordinate.mean() + eye1_y_coordinate.std() * 4 - - return data - - -def pybids_test_dataset(): - return Path(get_test_data_path()).joinpath("synthetic", "derivatives", "fmriprep") - - -def return_bidsmreye_eyetrack_tsv(): - output_dir = Path().resolve() - output_dir = output_dir.joinpath("tests", "data", "bidsmreye") - - return output_dir.joinpath( - "sub-01", - "func", - "sub-01_task-nback_space-MNI152NLin2009cAsym_desc-bidsmreye_eyetrack.tsv", - ) - - -def rm_dir(some_dir): - if Path(some_dir).is_dir(): - shutil.rmtree(some_dir, ignore_errors=False)