diff --git a/.gitignore b/.gitignore index b862b72..101a4c6 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ logs # files *.nii *.h5 -*.html *desc-bidsmreye_eyetrack.tsv pybids_db tmp.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b690145..221cea6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -60,7 +60,7 @@ repos: rev: v1.11.1 hooks: - id: mypy - additional_dependencies: [pydantic, numpy] + additional_dependencies: [pydantic, numpy, types-Jinja2] files: bidsmreye args: [--config-file, pyproject.toml] diff --git a/bidsmreye/_cli.py b/bidsmreye/_cli.py index 919a739..fd893e5 100644 --- a/bidsmreye/_cli.py +++ b/bidsmreye/_cli.py @@ -11,7 +11,7 @@ from bidsmreye.bidsmreye import bidsmreye from bidsmreye.defaults import default_log_level, log_levels from bidsmreye.download import download -from bidsmreye.logging import bidsmreye_log +from bidsmreye.logger import bidsmreye_log log = bidsmreye_log(name="bidsmreye") diff --git a/bidsmreye/bids_utils.py b/bidsmreye/bids_utils.py index 434e011..cdb890a 100644 --- a/bidsmreye/bids_utils.py +++ b/bidsmreye/bids_utils.py @@ -15,7 +15,7 @@ get_bidsname_config, get_pybids_config, ) -from bidsmreye.logging import bidsmreye_log +from bidsmreye.logger import bidsmreye_log from bidsmreye.methods import methods from bidsmreye.utils import copy_license, create_dir_if_absent, return_regex diff --git a/bidsmreye/bidsmreye.py b/bidsmreye/bidsmreye.py index 76c0a9e..ca85362 100755 --- a/bidsmreye/bidsmreye.py +++ b/bidsmreye/bidsmreye.py @@ -9,7 +9,7 @@ from bidsmreye._version import __version__ from bidsmreye.configuration import Config from bidsmreye.defaults import default_log_level -from bidsmreye.logging import bidsmreye_log +from bidsmreye.logger import bidsmreye_log log = bidsmreye_log(name="bidsmreye") diff --git a/bidsmreye/config/config_bidsname.json b/bidsmreye/config/config_bidsname.json index 8eae1f9..4fb5fc9 100644 --- a/bidsmreye/config/config_bidsname.json +++ b/bidsmreye/config/config_bidsname.json @@ -1,6 +1,6 @@ { "mask": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}][_res-{res}][_den-{den}]_desc-eye_mask.p", - "report": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}][_res-{res}][_den-{den}]_desc-eye_report.html", + "report": "sub-{subject}/[ses-{session}]/figures/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}][_res-{res}][_den-{den}]_desc-eye_report.html", "no_label": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}][_res-{res}][_den-{den}]_desc-nolabel_bidsmreye.npz", "confounds_tsv": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}]_desc-bidsmreye_eyetrack.tsv", "confounds_json": "sub-{subject}/[ses-{session}]/func/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ce}][_rec-{rec}][_dir-{dir}][_run-{run}][_space-{space}]_desc-bidsmreye_eyetrack.json", diff --git a/bidsmreye/configuration.py b/bidsmreye/configuration.py index f856731..35e4b5e 100644 --- a/bidsmreye/configuration.py +++ b/bidsmreye/configuration.py @@ -9,7 +9,7 @@ from attrs import asdict, converters, define, field from bids import BIDSLayout # type: ignore -from bidsmreye.logging import bidsmreye_log +from bidsmreye.logger import bidsmreye_log log = bidsmreye_log(name="bidsmreye") diff --git a/bidsmreye/download.py b/bidsmreye/download.py index 9f9dd06..51e5a40 100644 --- a/bidsmreye/download.py +++ b/bidsmreye/download.py @@ -10,7 +10,7 @@ import bidsmreye from bidsmreye.defaults import available_models, default_model -from bidsmreye.logging import bidsmreye_log +from bidsmreye.logger import bidsmreye_log log = bidsmreye_log(name="bidsmreye") diff --git a/bidsmreye/generalize.py b/bidsmreye/generalize.py index 45f7b00..cb57fc5 100644 --- a/bidsmreye/generalize.py +++ b/bidsmreye/generalize.py @@ -22,7 +22,7 @@ list_subjects, ) from bidsmreye.configuration import Config -from bidsmreye.logging import bidsmreye_log +from bidsmreye.logger import bidsmreye_log from bidsmreye.quality_control import quality_control_output from bidsmreye.utils import ( add_sidecar_in_root, diff --git a/bidsmreye/logging.py b/bidsmreye/logger.py similarity index 100% rename from bidsmreye/logging.py rename to bidsmreye/logger.py diff --git a/bidsmreye/methods.py b/bidsmreye/methods.py index 49973b3..fe9c7cb 100644 --- a/bidsmreye/methods.py +++ b/bidsmreye/methods.py @@ -6,10 +6,9 @@ import warnings from pathlib import Path -import chevron - from bidsmreye._version import __version__ from bidsmreye.defaults import available_models, default_model +from bidsmreye.report import TEMPLATES_DIR, return_jinja_env from bidsmreye.utils import create_dir_for_file @@ -44,29 +43,26 @@ def methods( if not model: model = default_model() - is_known_models = False + is_known_model = False is_default_model = False if model in available_models(): - is_known_models = True + is_known_model = True if model == default_model(): is_default_model = True - if not is_known_models: + if not is_known_model: warnings.warn(f"{model} is not a known model name.", stacklevel=3) - template_file = str(Path(__file__).parent / "templates" / "CITATION.mustache") - with open(template_file) as template: - output = chevron.render( - template=template, - data={ - "version": __version__, - "model": model, - "is_default_model": is_default_model, - "is_known_models": is_known_models, - "qc_only": qc_only, - }, - warn=True, - ) + env = return_jinja_env(searchpath=TEMPLATES_DIR) + template = env.get_template("CITATION.jinja") + + output = template.render( + version=__version__, + model=model, + is_default_model=is_default_model, + is_known_model=is_known_model, + qc_only=qc_only, + ) output_file.write_text(output) diff --git a/bidsmreye/prepare_data.py b/bidsmreye/prepare_data.py index 3cba8ca..3568b31 100644 --- a/bidsmreye/prepare_data.py +++ b/bidsmreye/prepare_data.py @@ -20,7 +20,8 @@ save_sampling_frequency_to_json, ) from bidsmreye.configuration import Config -from bidsmreye.logging import bidsmreye_log +from bidsmreye.logger import bidsmreye_log +from bidsmreye.report import generate_report from bidsmreye.utils import ( check_if_file_found, get_deepmreye_filename, @@ -213,4 +214,7 @@ def prepare_data(cfg: Config) -> None: ) for subject_label in subjects: process_subject(cfg, layout_in, layout_out, subject_label) + generate_report( + output_dir=cfg.output_dir, subject_label=subject_label, action="prepare" + ) progress.update(subject_loop, advance=1) diff --git a/bidsmreye/quality_control.py b/bidsmreye/quality_control.py index e8df3ea..8c09a03 100644 --- a/bidsmreye/quality_control.py +++ b/bidsmreye/quality_control.py @@ -3,7 +3,6 @@ from __future__ import annotations import json -import logging import math from pathlib import Path @@ -20,7 +19,8 @@ list_subjects, ) from bidsmreye.configuration import Config -from bidsmreye.logging import bidsmreye_log +from bidsmreye.logger import bidsmreye_log +from bidsmreye.report import generate_report from bidsmreye.utils import ( check_if_file_found, create_dir_for_file, @@ -142,9 +142,7 @@ def perform_quality_control( add_qc_to_sidecar(confounds, sidecar_name) fig = visualize_eye_gaze_data(confounds) - fig.update_layout(title=Path(confounds_tsv).name) - if log.isEnabledFor(logging.DEBUG): - fig.show() + fig.update_layout(showlegend=False, height=800) create_dir_for_file(visualization_html_file) fig.write_html(visualization_html_file) @@ -184,6 +182,11 @@ def quality_control_output(cfg: Config) -> None: ) for subject_label in subjects: qc_subject(cfg, layout_out, subject_label) + generate_report( + output_dir=cfg.output_dir, + subject_label=subject_label, + action="generalize", + ) progress.update(subject_loop, advance=1) @@ -203,6 +206,11 @@ def quality_control_input(cfg: Config) -> None: ) for subject_label in subjects: qc_subject(cfg, layout_in, subject_label, layout_out) + generate_report( + output_dir=cfg.output_dir, + subject_label=subject_label, + action="generalize", + ) progress.update(subject_loop, advance=1) diff --git a/bidsmreye/report.py b/bidsmreye/report.py new file mode 100644 index 0000000..30576cf --- /dev/null +++ b/bidsmreye/report.py @@ -0,0 +1,80 @@ +"""Compile outputs from all tasks, spaces, runs into a single HTML.""" + +import datetime +from pathlib import Path + +from jinja2 import Environment, FileSystemLoader, select_autoescape + +from bidsmreye._version import __version__ +from bidsmreye.logger import bidsmreye_log + +log = bidsmreye_log(name="bidsmreye") + +TEMPLATES_DIR = Path(__file__).parent / "templates" + + +def return_jinja_env(searchpath=None) -> Environment: + if searchpath is None: + searchpath = TEMPLATES_DIR / "report" + return Environment( + loader=FileSystemLoader(searchpath), + autoescape=select_autoescape(), + lstrip_blocks=True, + trim_blocks=True, + ) + + +def generate_report(output_dir: Path, subject_label: str, action: str) -> None: + + env = return_jinja_env() + template = env.get_template("base.html") + + if action == "prepare": + input_files = sorted(output_dir.glob(f"sub-{subject_label}/**/*report.html")) + elif action == "generalize": + input_files = sorted(output_dir.glob(f"sub-{subject_label}/**/*eyetrack.html")) + + files = [] + for html_report in input_files: + with open(html_report) as f: + content = f.read() + + name: str = html_report.stem + if action == "prepare": + name = name.replace("_desc-eye_report", "_desc-preproc_bold") + + files.append({"name": name, "content": content, "path": html_report}) + + date = datetime.datetime.now().astimezone().replace(microsecond=0).isoformat() + + report = template.render( + action=action, + files=files, + subject_label=subject_label, + date=date, + version=__version__, + ) + + report_filename = ( + output_dir / f"sub-{subject_label}" / f"sub-{subject_label}_{action}.html" + ) + + with open(report_filename, "w") as f: + f.write(report) + + log.info(f"Report saved at: '{report_filename}'.") + + +if __name__ == "__main__": + + cwd = Path("/home/remi/github/cpp-lln-lab/bidsMReye") + + output_dir = cwd / "outputs" / "moae_fmriprep" / "derivatives" / "bidsmreye" + subject = "01" + + output_dir = Path("/home/remi/gin/CPP/yin/bidsmreye") + subject_label = "02" + + action = "prepare" + + generate_report(output_dir, subject_label, action) diff --git a/bidsmreye/templates/CITATION.mustache b/bidsmreye/templates/CITATION.jinja similarity index 92% rename from bidsmreye/templates/CITATION.mustache rename to bidsmreye/templates/CITATION.jinja index 78f46bd..c3f71d9 100644 --- a/bidsmreye/templates/CITATION.mustache +++ b/bidsmreye/templates/CITATION.jinja @@ -6,7 +6,7 @@ performed using [*bidsMReye*](https://github.com/cpp-lln-lab/bidsMReye) (version a BIDS app relying on [deepMReye](https://github.com/DeepMReye/DeepMReye) (@deepmreye) to decode eye motion from fMRI time series data. -{{^qc_only}} +{% if not qc_only %} ### data extraction The data of each BOLD runs underwent co-registration conducted @@ -28,19 +28,18 @@ across voxels (spatial normalization). Voxels time series were used as inputs for generalization decoding using a -{{#is_known_models}} + {% if is_known_model %} pre-trained model {{ model }} from deepMReye from [OSF](https://osf.io/23t5v). - {{#is_default_model}} + {% if is_default_model %} This model was trained on the following datasets: guided fixations (@alexander_open_2017), smooth pursuit (@nau_real-motion_2018, @polti_rapid_2022, @nau_hexadirectional_2018), free viewing (@julian_human_2018). - {{/is_default_model}} -{{/is_known_models}} -{{^is_known_models}} + {% endif %} + {% else %} model trained on calibration data from the current study. -{{/is_known_models}} -{{/qc_only}} + {% endif %} +{% endif %} ### quality control diff --git a/bidsmreye/templates/report/base.html b/bidsmreye/templates/report/base.html new file mode 100644 index 0000000..809e922 --- /dev/null +++ b/bidsmreye/templates/report/base.html @@ -0,0 +1,59 @@ + + + + + + + bidsmreye report - sub-{{ subject_label }} - {{ action }} + + + + + + + + + + +
{% include "header.html" %}
+ +
+ +

subject {{ subject_label }}: {{ action }}

+ + {% for file in files %} +
+
+

{{ file.name }}

+
+
{{ file.content|safe }}
+
+
+
+ {% endfor %} + +
+

About

+

+ Report generated +

+

+
+ +
+ + + + + diff --git a/bidsmreye/templates/report/footer.html b/bidsmreye/templates/report/footer.html new file mode 100644 index 0000000..7b420cb --- /dev/null +++ b/bidsmreye/templates/report/footer.html @@ -0,0 +1,27 @@ +
+
+
+

bidsmreye

+

+ documentation +

+

+ issue tracker +

+

+ github +

+

pypi

+

+ dockerhub +

+

+ FAQ +

+
+
+
diff --git a/bidsmreye/templates/report/header.html b/bidsmreye/templates/report/header.html new file mode 100644 index 0000000..b0c17c3 --- /dev/null +++ b/bidsmreye/templates/report/header.html @@ -0,0 +1,51 @@ + diff --git a/bidsmreye/utils.py b/bidsmreye/utils.py index 4f66250..e9f29f8 100644 --- a/bidsmreye/utils.py +++ b/bidsmreye/utils.py @@ -20,11 +20,14 @@ ) from bidsmreye.configuration import Config -from bidsmreye.logging import bidsmreye_log +from bidsmreye.logger import bidsmreye_log log = bidsmreye_log(name="bidsmreye") +TEMPLATES_DIR = Path(__file__).parent / "templates" + + def progress_bar(text: str, color: str = "green") -> Progress: return Progress( TextColumn(f"[{color}]{text}"), @@ -43,7 +46,7 @@ def copy_license(output_dir: Path) -> Path: :param output_dir: :type output_dir: Path """ - input_file = str(Path(__file__).parent / "templates" / "CCO") + input_file = str(TEMPLATES_DIR / "CCO") output_file = output_dir / "LICENSE" create_dir_if_absent(output_dir) if not (output_dir / "LICENSE").is_file(): diff --git a/bidsmreye/visualize.py b/bidsmreye/visualize.py index 580b7d1..fc1c565 100644 --- a/bidsmreye/visualize.py +++ b/bidsmreye/visualize.py @@ -13,7 +13,7 @@ from bidsmreye._version import __version__ from bidsmreye.bids_utils import get_dataset_layout, list_subjects from bidsmreye.configuration import Config -from bidsmreye.logging import bidsmreye_log +from bidsmreye.logger import bidsmreye_log from bidsmreye.utils import check_if_file_found, set_this_filter LINE_WIDTH = 3 diff --git a/docs/source/images/bidsMReye_logo_on_white.png b/docs/source/images/bidsMReye_logo_on_white.png new file mode 100644 index 0000000..4d99bd4 Binary files /dev/null and b/docs/source/images/bidsMReye_logo_on_white.png differ diff --git a/pyproject.toml b/pyproject.toml index 69de0c7..b12826a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,7 @@ build-backend = "hatchling.build" requires = ["hatchling", "hatch-vcs"] [project] -authors = [ - {name = "Remi Gau", email = "remi.gau@gmail.com"} -] +authors = [{name = "Remi Gau", email = "remi.gau@gmail.com"}] classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Science/Research", @@ -19,16 +17,16 @@ classifiers = [ "Topic :: Scientific/Engineering :: Image Processing" ] dependencies = [ + "antspyx<0.5", "attrs", - "chevron>=0.14.0", "deepmreye>=0.2.1", + "jinja2", "kaleido", + "keras<3.0.0", "pooch>=1.6.0", "pybids", - "antspyx<0.5", "tqdm", "tomli; python_version < '3.11'", - "keras<3.0.0", "rich_argparse" ] description = "bids app using deepMReye to decode eye motion for fMRI time series data" @@ -62,21 +60,8 @@ doc = [ "sphinxcontrib-bibtex" ] docs = ["bidsmreye[doc]"] -style = [ - "black", - "codespell", - "flake8", - "flake8-docstrings", - "mypy", - 'types-all', - 'pandas-stubs', - "pre-commit", - "sourcery" -] -test = [ - "pytest", - "pytest-cov" -] +style = ["pre-commit", "sourcery"] +test = ["pytest", "pytest-cov"] tests = ["bidsmreye[test]"] [project.scripts] @@ -105,9 +90,7 @@ packages = ["bidsmreye"] source = "vcs" [tool.importlinter] -ignore_imports = [ - "bidsmreye._version" -] +ignore_imports = ["bidsmreye._version"] root_package = "bidsmreye" [[tool.importlinter.contracts]] @@ -120,9 +103,10 @@ layers = [ "visualize", "bids_utils", "methods", + "report", "utils", "configuration", - "logging", + "logger", "defaults" ] name = "Layered architecture" @@ -137,24 +121,18 @@ skip_gitignore = true [tool.mypy] check_untyped_defs = true disallow_any_generics = true -disallow_incomplete_defs = true -disallow_untyped_defs = true +disallow_incomplete_defs = false +disallow_untyped_defs = false # enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] exclude = ['tests/'] no_implicit_optional = true -plugins = [ - "numpy.typing.mypy_plugin", - "pydantic.mypy" -] +plugins = ["numpy.typing.mypy_plugin", "pydantic.mypy"] warn_redundant_casts = true warn_unused_ignores = true [[tool.mypy.overrides]] ignore_errors = true -module = [ - 'bids.*', - "bidsmreye._version" -] +module = ['bids.*', "bidsmreye._version"] [[tool.mypy.overrides]] ignore_missing_imports = true diff --git a/requirements.txt b/requirements.txt index 68ae0d1..75b68d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -56,8 +56,6 @@ charset-normalizer==3.3.2 # via requests chart-studio==1.1.0 # via antspyx -chevron==0.14.0 - # via bidsmreye (pyproject.toml) click==8.1.7 # via pybids comm==0.2.2 @@ -145,6 +143,7 @@ jedi==0.19.1 # via ipython jinja2==3.1.4 # via + # bidsmreye (pyproject.toml) # jupyter-server # jupyterlab # jupyterlab-server diff --git a/tests/test_report.py b/tests/test_report.py new file mode 100644 index 0000000..59853c9 --- /dev/null +++ b/tests/test_report.py @@ -0,0 +1,16 @@ +import pathlib + +from bidsmreye.report import generate_report + + +def test_generate_report(tmp_path): + + output_dir = pathlib.Path() / "outputs" + output_dir = tmp_path + + (output_dir / "sub-01").mkdir(parents=True) + generate_report(output_dir=output_dir, subject_label="01", action="prepare") + generate_report(output_dir=output_dir, subject_label="01", action="generalize") + + assert (output_dir / "sub-01" / "sub-01_prepare.html").exists() + assert (output_dir / "sub-01" / "sub-01_generalize.html").exists()