diff --git a/docs/api.rst b/docs/api.rst index 83c5c692..125a4ea5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -35,6 +35,5 @@ Other Utilities api/qsirecon.interfaces api/qsirecon.utils - api/qsirecon.report - api/qsirecon.viz + api/qsirecon.reports api/qsirecon.qc diff --git a/pyproject.toml b/pyproject.toml index d44d7a09..8682f17e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ "nibabel <= 5.2.0", "nilearn == 0.10.1", "nipype == 1.8.6", + "nireports", "niworkflows >=1.9,<= 1.10", "numpy <= 1.26.3", "pandas < 2.0.0", @@ -177,6 +178,7 @@ markers = [ "pyafq_recon_full: test 19", "mrtrix3_recon: test 20", "tortoise_recon: test 21", + "autotrack: test 22", ] env = [ "RUNNING_PYTEST = 1", diff --git a/qsirecon/cli/parser.py b/qsirecon/cli/parser.py index bfa0119f..9d7bf996 100644 --- a/qsirecon/cli/parser.py +++ b/qsirecon/cli/parser.py @@ -450,9 +450,6 @@ def parse_args(args=None, namespace=None): work_dir = config.execution.work_dir version = config.environment.version - if config.execution.reportlets_dir is None: - config.execution.reportlets_dir = work_dir / "reportlets" - # Update the config with an empty dict to trigger initialization of all config # sections (we used `init=False` above). # This must be done after cleaning the work directory, or we could delete an @@ -479,7 +476,6 @@ def parse_args(args=None, namespace=None): config.execution.log_dir = config.execution.output_dir / "logs" # Check and create output and working directories config.execution.log_dir.mkdir(exist_ok=True, parents=True) - config.execution.reportlets_dir.mkdir(exist_ok=True, parents=True) work_dir.mkdir(exist_ok=True, parents=True) # Force initialization of the BIDSLayout diff --git a/qsirecon/cli/run.py b/qsirecon/cli/run.py index 5e4b87ab..213b35b4 100644 --- a/qsirecon/cli/run.py +++ b/qsirecon/cli/run.py @@ -171,17 +171,13 @@ def main(): errno = 0 finally: - from ..viz.reports import generate_reports + from ..reports.core import generate_reports + from ..workflows.base import _load_recon_spec + from .workflow import copy_boilerplate # Generate reports phase - # session_list = ( - # config.execution.get().get('bids_filters', {}).get('dwi', {}).get('session') - # ) + session_list = config.execution.get().get("bids_filters", {}).get("dwi", {}).get("session") - failed_reports = generate_reports( - config.execution.participant_label, - # session_list=session_list, - ) write_derivative_description( config.execution.bids_dir, config.execution.output_dir, @@ -189,21 +185,39 @@ def main(): ) write_bidsignore(config.execution.output_dir) + workflow_spec = _load_recon_spec() + # Compile list of output folders + # TODO: Retain QSIRecon pipeline names in the config object + qsirecon_suffixes = [] + for node_spec in workflow_spec["nodes"]: + qsirecon_suffix = node_spec.get("qsirecon_suffix", None) + qsirecon_suffixes += [qsirecon_suffix] if qsirecon_suffix else [] + + qsirecon_suffixes = sorted(list(set(qsirecon_suffixes))) + config.loggers.cli.info(f"QSIRecon pipeline suffixes: {qsirecon_suffixes}") + failed_reports = [] + for qsirecon_suffix in qsirecon_suffixes: + suffix_dir = str(config.execution.output_dir / f"qsirecon-{qsirecon_suffix}") + + # Copy the boilerplate files + copy_boilerplate(config.execution.output_dir, suffix_dir) + + suffix_failed_reports = generate_reports( + config.execution.participant_label, + suffix_dir, + config.execution.run_uuid, + session_list=session_list, + qsirecon_suffix=qsirecon_suffix, + ) + failed_reports += suffix_failed_reports + write_derivative_description( + config.execution.bids_dir, + suffix_dir, + # dataset_links=config.execution.dataset_links, + ) + write_bidsignore(suffix_dir) + if failed_reports: print(failed_reports) - # msg = ( - # 'Report generation was not successful for the following participants ' - # f': {", ".join(failed_reports)}.' - # ) - # config.loggers.cli.error(msg) - # if sentry_sdk is not None: - # sentry_sdk.capture_message(msg, level='error') - - # If preprocessing and recon are requested in the same call, start the recon workflow now. - if errno > 0: - if config.nipype.stop_on_first_crash: - config.loggers.workflow.critical( - "Errors occurred during preprocessing - Recon will not run." - ) - sys.exit(int((errno + failed_reports) > 0)) + sys.exit(int(errno + len(failed_reports)) > 0) diff --git a/qsirecon/cli/workflow.py b/qsirecon/cli/workflow.py index b37e329f..5466a32e 100644 --- a/qsirecon/cli/workflow.py +++ b/qsirecon/cli/workflow.py @@ -45,8 +45,8 @@ def build_workflow(config_file, retval): # from niworkflows.utils.misc import check_valid_fs_license # from ..utils.bids import check_pipeline_version from .. import config + from ..reports.core import generate_reports from ..utils.misc import check_deps - from ..viz.reports import generate_reports from ..workflows.base import init_qsirecon_wf config.load(config_file) @@ -99,6 +99,7 @@ def build_workflow(config_file, retval): config.execution.output_dir, config.execution.run_uuid, session_list=session_list, + qsirecon_suffix="", ) if failed_reports: config.loggers.cli.error( @@ -224,3 +225,17 @@ def build_boilerplate(config_file, workflow): check_call(cmd, timeout=10) except (FileNotFoundError, CalledProcessError, TimeoutExpired): config.loggers.cli.warning("Could not generate CITATION.tex file:\n%s", " ".join(cmd)) + + +def copy_boilerplate(in_dir, out_dir): + import shutil + + in_logs_path = Path(in_dir) / "logs" + out_logs_path = Path(out_dir) / "logs" + out_logs_path.mkdir(exist_ok=True, parents=True) + citation_files = { + ext: in_logs_path / ("CITATION.%s" % ext) for ext in ("bib", "tex", "md", "html") + } + for ext, citation_file in citation_files.items(): + if citation_file.exists(): + shutil.copy(citation_file, out_logs_path / f"CITATION.{ext}") diff --git a/qsirecon/config.py b/qsirecon/config.py index c08cd119..86cba184 100644 --- a/qsirecon/config.py +++ b/qsirecon/config.py @@ -426,8 +426,6 @@ class execution(_Config): """List of participant identifiers that are to be preprocessed.""" freesurfer_input = None """Directory containing FreeSurfer directories to use for recon workflows.""" - reportlets_dir = None - """Path where reportlets are written.""" templateflow_home = _templateflow_home """The root folder of the TemplateFlow client.""" work_dir = Path("work").absolute() @@ -436,6 +434,8 @@ class execution(_Config): """Write out the computational graph corresponding to the planned postprocessing.""" dataset_links = {} """A dictionary of dataset links to be used to track Sources in sidecars.""" + aggr_ses_reports = 4 # TODO: Change to None when implemented on command line + """Maximum number of sessions aggregated in one subject's visual report.""" _layout = None @@ -450,7 +450,6 @@ class execution(_Config): "layout", "log_dir", "output_dir", - "reportlets_dir", "templateflow_home", "work_dir", ) diff --git a/qsirecon/data/__init__.py b/qsirecon/data/__init__.py index 2cc826d4..0b93b473 100644 --- a/qsirecon/data/__init__.py +++ b/qsirecon/data/__init__.py @@ -1,5 +1,182 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- -# vi: set ft=python sts=4 ts=4 sw=4 et: -""" Data fetchers module """ +"""QSIRecon data files + +.. autofunction:: load + +.. automethod:: load.readable + +.. automethod:: load.as_path + +.. automethod:: load.cached + +.. autoclass:: Loader +""" + +from __future__ import annotations + +import atexit +import os +from contextlib import AbstractContextManager, ExitStack +from functools import cached_property +from pathlib import Path +from types import ModuleType + +try: + from functools import cache +except ImportError: # PY38 + from functools import lru_cache as cache + +try: # Prefer backport to leave consistency to dependency spec + from importlib_resources import as_file, files +except ImportError: + from importlib.resources import as_file, files # type: ignore + +try: # Prefer stdlib so Sphinx can link to authoritative documentation + from importlib.resources.abc import Traversable +except ImportError: + from importlib_resources.abc import Traversable + +__all__ = ["load"] + + +class Loader: + """A loader for package files relative to a module + + This class wraps :mod:`importlib.resources` to provide a getter + function with an interpreter-lifetime scope. For typical packages + it simply passes through filesystem paths as :class:`~pathlib.Path` + objects. For zipped distributions, it will unpack the files into + a temporary directory that is cleaned up on interpreter exit. + + This loader accepts a fully-qualified module name or a module + object. + + Expected usage:: + + '''Data package + + .. autofunction:: load_data + + .. automethod:: load_data.readable + + .. automethod:: load_data.as_path + + .. automethod:: load_data.cached + ''' + + from qsirecon.data import Loader + + load_data = Loader(__package__) + + :class:`~Loader` objects implement the :func:`callable` interface + and generate a docstring, and are intended to be treated and documented + as functions. + + For greater flexibility and improved readability over the ``importlib.resources`` + interface, explicit methods are provided to access resources. + + +---------------+----------------+------------------+ + | On-filesystem | Lifetime | Method | + +---------------+----------------+------------------+ + | `True` | Interpreter | :meth:`cached` | + +---------------+----------------+------------------+ + | `True` | `with` context | :meth:`as_path` | + +---------------+----------------+------------------+ + | `False` | n/a | :meth:`readable` | + +---------------+----------------+------------------+ + + It is also possible to use ``Loader`` directly:: + + from qsirecon.data import Loader + + Loader(other_package).readable('data/resource.ext').read_text() + + with Loader(other_package).as_path('data') as pkgdata: + # Call function that requires full Path implementation + func(pkgdata) + + # contrast to + + from importlib_resources import files, as_file + + files(other_package).joinpath('data/resource.ext').read_text() + + with as_file(files(other_package) / 'data') as pkgdata: + func(pkgdata) + + .. automethod:: readable + + .. automethod:: as_path + + .. automethod:: cached + """ + + def __init__(self, anchor: str | ModuleType): + self._anchor = anchor + self.files = files(anchor) + self.exit_stack = ExitStack() + atexit.register(self.exit_stack.close) + # Allow class to have a different docstring from instances + self.__doc__ = self._doc + + @cached_property + def _doc(self): + """Construct docstring for instances + + Lists the public top-level paths inside the location, where + non-public means has a `.` or `_` prefix or is a 'tests' + directory. + """ + top_level = sorted( + os.path.relpath(p, self.files) + "/"[: p.is_dir()] + for p in self.files.iterdir() + if p.name[0] not in (".", "_") and p.name != "tests" + ) + doclines = [ + f"Load package files relative to ``{self._anchor}``.", + "", + "This package contains the following (top-level) files/directories:", + "", + *(f"* ``{path}``" for path in top_level), + ] + + return "\n".join(doclines) + + def readable(self, *segments) -> Traversable: + """Provide read access to a resource through a Path-like interface. + + This file may or may not exist on the filesystem, and may be + efficiently used for read operations, including directory traversal. + + This result is not cached or copied to the filesystem in cases where + that would be necessary. + """ + return self.files.joinpath(*segments) + + def as_path(self, *segments) -> AbstractContextManager[Path]: + """Ensure data is available as a :class:`~pathlib.Path`. + + This method generates a context manager that yields a Path when + entered. + + This result is not cached, and any temporary files that are created + are deleted when the context is exited. + """ + return as_file(self.files.joinpath(*segments)) + + @cache # noqa: B019 + def cached(self, *segments) -> Path: + """Ensure data is available as a :class:`~pathlib.Path`. + + Any temporary files that are created remain available throughout + the duration of the program, and are deleted when Python exits. + + Results are cached so that multiple calls do not unpack the same + data multiple times, but the cache is sensitive to the specific + argument(s) passed. + """ + return self.exit_stack.enter_context(as_file(self.files.joinpath(*segments))) + + __call__ = cached + + +load = Loader(__package__) diff --git a/qsirecon/data/reports-spec-anat.yml b/qsirecon/data/reports-spec-anat.yml new file mode 100644 index 00000000..1b3ba4cf --- /dev/null +++ b/qsirecon/data/reports-spec-anat.yml @@ -0,0 +1,40 @@ +package: qsirecon +title: Anatomical report for participant '{subject}' - QSIRecon-{qsirecon_suffix} +sections: +- name: Summary + reportlets: + - bids: {datatype: figures, desc: summary, suffix: T1w} + +- name: Anatomical + reportlets: + - bids: + datatype: figures + desc: conform + extension: [.html] + suffix: T1w + - bids: {datatype: figures, suffix: dseg} + caption: | + This panel shows the template T1-weighted image (if several T1w images were found), + with contours delineating the detected brain mask and brain tissue segmentations. + subtitle: Brain mask and brain tissue segmentation of the T1w + +- name: About + reportlets: + - bids: {datatype: figures, desc: about, suffix: T1w} + - custom: boilerplate + path: '{out_dir}/logs' + bibfile: ['qsirecon', 'data/boilerplate.bib'] + caption: | +

We kindly ask to report results preprocessed with this tool using the following boilerplate.

+ + title: Methods + - custom: errors + path: '{out_dir}/sub-{subject}/log/{run_uuid}' + captions: NiReports may have recorded failure conditions. + title: Errors diff --git a/qsirecon/data/reports-spec-dwi.yml b/qsirecon/data/reports-spec-dwi.yml new file mode 100644 index 00000000..13330904 --- /dev/null +++ b/qsirecon/data/reports-spec-dwi.yml @@ -0,0 +1,132 @@ +package: qsirecon +title: Diffusion report for participant '{subject}', session '{session}' - QSIRecon-{qsirecon_suffix} +sections: +- name: MRtrix3 + ordering: session,task,acquisition,ceagent,reconstruction,direction,run,echo + reportlets: + - bids: {datatype: figures, desc: wmFOD, suffix: peaks} + subtitle: Constrained Spherical Deconvolution + caption: Directionally color-coded FOD peaks overlaid on the b=0 reference image. + - bids: {datatype: figures, desc: wmFOD, suffix: odfs} + subtitle: Constrained Spherical Deconvolution + caption: Three views of ODFs in ROIs. + - bids: {datatype: figures, suffix: unringing} + subtitle: Gibbs Ringing Removal + caption: Effect of removing Gibbs ringing on a low and high-b image. + - bids: {datatype: figures, suffix: biascorr} + subtitle: DWI Bias Correction + caption: | + Effect of bias correction on a low and high-b image. Bias field contour lines are drawn as an overlay. + - bids: {datatype: figures, desc: MRtrix3Connectivity, suffix: matrices} + subtitle: MRtrix3 Connectivity + caption: Connectivity estimated by tck2connectome. + +- name: DSI Studio + ordering: session,task,acquisition,ceagent,reconstruction,direction,run,echo + reportlets: + - bids: {datatype: figures, desc: GQIODF, suffix: peaks} + subtitle: Generalized q-sampling Imaging (GQI) + caption: Directionally color-coded ODF peaks overlaid on the b=0 reference image. + - bids: {datatype: figures, desc: GQIODF, suffix: odfs} + subtitle: GQI + caption: Three views of ODFs in ROIs. + - bids: {datatype: figures, desc: DSIStudioConnectivity, suffix: matrices} + subtitle: Connectivity + caption: Connectivity estimated by DSI Studio. + +- name: Dipy + ordering: session,task,acquisition,ceagent,reconstruction,direction,run,echo + reportlets: + - bids: {datatype: figures, desc: 3dSHOREODF, suffix: peaks} + subtitle: 3dSHORE + caption: Directionally color-coded ODF peaks overlaid on the b=0 reference image. + - bids: {datatype: figures, desc: 3dSHOREODF, suffix: odfs} + subtitle: 3dSHORE + caption: Three views of ODFs in ROIs. + - bids: {datatype: figures, desc: MAPLMRIODF, suffix: peaks} + subtitle: MAP(L)MRI + caption: Directionally color-coded ODF peaks overlaid on the b=0 reference image. + - bids: {datatype: figures, desc: MAPLMRIODF, suffix: odfs} + subtitle: MAP(L)MRI + caption: Three views of ODFs in ROIs. + +- name: AMICO + ordering: session,task,acquisition,ceagent,reconstruction,direction,run,echo + reportlets: + - bids: {datatype: figures, desc: NODDI, suffix: peaks} + subtitle: NODDI + caption: Directionally color-coded ODF peaks overlaid on the b=0 reference image. + +- name: Connectome + ordering: session,task,acquisition,ceagent,reconstruction,direction,run,echo + reportlets: + - bids: {datatype: figures, suffix: summary} + - bids: {datatype: figures, suffix: validation} + - bids: {datatype: figures, suffix: b0ref} + subtitle: b=0 Reference Image + caption: | + b=0 template and final mask output. The t1 and signal intersection mask is blue, + their xor is red and the entire mask is plotted in cyan. + - bids: {datatype: figures, desc: sampling, suffix: scheme} + subtitle: DWI Sampling Scheme + caption: Animation of the DWI sampling scheme. Each separate scan is its own color. + - bids: {datatype: figures, desc: shoreline, suffix: iterdata} + subtitle: SHORELine Convergence + caption: | + Difference in motion estimates over SHORELine iterations. + Values close to zero indicate good convergence. + - bids: {datatype: figures, suffix: intramodal} + subtitle: Registration to Intramodal Template + caption: | + b0 reference image warped to the across-scan/session b0 template + - bids: {datatype: figures, desc: shoreline, suffix: animation} + subtitle: SHORELine Registration + caption: | + Maximum intensity projections of each DWI before and after SHORELine registration. + Orange lines are from the observed image and magenta lines are from the model-based registration target. + - bids: {datatype: figures, desc: fmap, suffix: reg} + subtitle: Fieldmap to EPI registration + caption: | + Results of affine coregistration between the magnitude image of the fieldmap and the reference EPI image. + - bids: {datatype: figures, desc: fmap, suffix: regvsm} + subtitle: Fieldmap + caption: | + Overlaid on the reference EPI image. + - bids: {datatype: figures, suffix: sdc} + subtitle: Susceptibility distortion correction + caption: | + Results of performing susceptibility distortion correction (SDC) on the EPI. + - bids: {datatype: figures, suffix: forcedsyn} + subtitle: Experimental fieldmap-less susceptibility distortion correction + caption: | + The dataset contained some fieldmap information, but the argument --force-syn was used. + The higher-priority SDC method was used. + Here, we show the results of performing SyN-based SDC on the EPI for comparison. + - bids: {datatype: figures, suffix: coreg} + subtitle: b0 to T1 registration + caption: | + antsRegistration was used to generate transformations from b0I-space to T1w-space. + - bids: {datatype: figures, suffix: intramodal} + subtitle: Registration to the Intramodal Template + caption: | + The B0 template registered to the modpoint B=0 image. + - bids: {datatype: figures, suffix: interactive} + subtitle: INTERACT + caption: | + Interactive QC Widgets. + - bids: {datatype: figures, suffix: carpetplot} + subtitle: DWI Summary + caption: | + Summary statistics are plotted, which may reveal trends or artifacts in the DWI data. + Global signals calculated within the whole-brain (GS), + within the white-matter (WM) and within cerebro-spinal fluid (CSF) show the mean DWI signal in their + corresponding masks. + DVARS and FD show the standardized DVARS and framewise-displacement measures for each time point. +
+ A carpet plot shows the time series for all voxels within the brain mask. + Voxels are grouped into cortical (blue), and subcortical (orange) gray matter, + cerebellum (green) and white matter and CSF (red), indicated by the color map on the left-hand side. + +- name: About + reportlets: + - bids: {datatype: figures, desc: about, suffix: T1w} diff --git a/qsirecon/data/reports-spec.yml b/qsirecon/data/reports-spec.yml new file mode 100644 index 00000000..2a05314c --- /dev/null +++ b/qsirecon/data/reports-spec.yml @@ -0,0 +1,167 @@ +package: qsirecon +title: Visual report for participant '{subject}' - QSIRecon-{qsirecon_suffix} +sections: +- name: Summary + reportlets: + - bids: {datatype: figures, desc: summary, suffix: [T1w, T2w]} + +- name: Anatomical + reportlets: + - bids: + datatype: figures + desc: conform + extension: [.html] + suffix: T1w + - bids: {datatype: figures, suffix: dseg} + caption: | + This panel shows the template T1-weighted image (if several T1w images were found), + with contours delineating the detected brain mask and brain tissue segmentations. + subtitle: Brain mask and brain tissue segmentation of the T1w + +- name: MRtrix3 + ordering: session,task,acquisition,ceagent,reconstruction,direction,run,echo + reportlets: + - bids: {datatype: figures, desc: wmFOD, suffix: peaks} + subtitle: Constrained Spherical Deconvolution + caption: Directionally color-coded FOD peaks overlaid on the b=0 reference image. + - bids: {datatype: figures, desc: wmFOD, suffix: odfs} + subtitle: Constrained Spherical Deconvolution + caption: Three views of ODFs in ROIs. + - bids: {datatype: figures, suffix: unringing} + subtitle: Gibbs Ringing Removal + caption: Effect of removing Gibbs ringing on a low and high-b image. + - bids: {datatype: figures, suffix: biascorr} + subtitle: DWI Bias Correction + caption: | + Effect of bias correction on a low and high-b image. Bias field contour lines are drawn as an overlay. + - bids: {datatype: figures, desc: MRtrix3Connectivity, suffix: matrices} + subtitle: MRtrix3 Connectivity + caption: Connectivity estimated by tck2connectome. + +- name: DSI Studio + ordering: session,task,acquisition,ceagent,reconstruction,direction,run,echo + reportlets: + - bids: {datatype: figures, desc: GQIODF, suffix: peaks} + subtitle: Generalized q-sampling Imaging (GQI) + caption: Directionally color-coded ODF peaks overlaid on the b=0 reference image. + - bids: {datatype: figures, desc: GQIODF, suffix: odfs} + subtitle: GQI + caption: Three views of ODFs in ROIs. + - bids: {datatype: figures, desc: DSIStudioConnectivity, suffix: matrices} + subtitle: Connectivity + caption: Connectivity estimated by DSI Studio. + +- name: Dipy + ordering: session,task,acquisition,ceagent,reconstruction,direction,run,echo + reportlets: + - bids: {datatype: figures, desc: 3dSHOREODF, suffix: peaks} + subtitle: 3dSHORE + caption: Directionally color-coded ODF peaks overlaid on the b=0 reference image. + - bids: {datatype: figures, desc: 3dSHOREODF, suffix: odfs} + subtitle: 3dSHORE + caption: Three views of ODFs in ROIs. + - bids: {datatype: figures, desc: MAPLMRIODF, suffix: peaks} + subtitle: MAP(L)MRI + caption: Directionally color-coded ODF peaks overlaid on the b=0 reference image. + - bids: {datatype: figures, desc: MAPLMRIODF, suffix: odfs} + subtitle: MAP(L)MRI + caption: Three views of ODFs in ROIs. + +- name: AMICO + ordering: session,task,acquisition,ceagent,reconstruction,direction,run,echo + reportlets: + - bids: {datatype: figures, desc: NODDI, suffix: peaks} + subtitle: NODDI + caption: Directionally color-coded ODF peaks overlaid on the b=0 reference image. + +- name: Connectome + ordering: session,task,acquisition,ceagent,reconstruction,direction,run,echo + reportlets: + - bids: {datatype: figures, suffix: summary} + - bids: {datatype: figures, suffix: validation} + - bids: {datatype: figures, suffix: b0ref} + subtitle: b=0 Reference Image + caption: | + b=0 template and final mask output. The t1 and signal intersection mask is blue, + their xor is red and the entire mask is plotted in cyan. + - bids: {datatype: figures, desc: sampling, suffix: scheme} + subtitle: DWI Sampling Scheme + caption: Animation of the DWI sampling scheme. Each separate scan is its own color. + - bids: {datatype: figures, desc: shoreline, suffix: iterdata} + subtitle: SHORELine Convergence + caption: | + Difference in motion estimates over SHORELine iterations. + Values close to zero indicate good convergence. + - bids: {datatype: figures, suffix: intramodal} + subtitle: Registration to Intramodal Template + caption: | + b0 reference image warped to the across-scan/session b0 template + - bids: {datatype: figures, desc: shoreline, suffix: animation} + subtitle: SHORELine Registration + caption: | + Maximum intensity projections of each DWI before and after SHORELine registration. + Orange lines are from the observed image and magenta lines are from the model-based registration target. + - bids: {datatype: figures, desc: fmap, suffix: reg} + subtitle: Fieldmap to EPI registration + caption: | + Results of affine coregistration between the magnitude image of the fieldmap and the reference EPI image. + - bids: {datatype: figures, desc: fmap, suffix: regvsm} + subtitle: Fieldmap + caption: | + Overlaid on the reference EPI image. + - bids: {datatype: figures, suffix: sdc} + subtitle: Susceptibility distortion correction + caption: | + Results of performing susceptibility distortion correction (SDC) on the EPI. + - bids: {datatype: figures, suffix: forcedsyn} + subtitle: Experimental fieldmap-less susceptibility distortion correction + caption: | + The dataset contained some fieldmap information, but the argument --force-syn was used. + The higher-priority SDC method was used. + Here, we show the results of performing SyN-based SDC on the EPI for comparison. + - bids: {datatype: figures, suffix: coreg} + subtitle: b0 to T1 registration + caption: | + antsRegistration was used to generate transformations from b0I-space to T1w-space. + - bids: {datatype: figures, suffix: intramodal} + subtitle: Registration to the Intramodal Template + caption: | + The B0 template registered to the modpoint B=0 image. + - bids: {datatype: figures, suffix: interactive} + subtitle: INTERACT + caption: | + Interactive QC Widgets. + - bids: {datatype: figures, suffix: carpetplot} + subtitle: DWI Summary + caption: | + Summary statistics are plotted, which may reveal trends or artifacts in the DWI data. + Global signals calculated within the whole-brain (GS), + within the white-matter (WM) and within cerebro-spinal fluid (CSF) show the mean DWI signal in their + corresponding masks. + DVARS and FD show the standardized DVARS and framewise-displacement measures for each time point. +
+ A carpet plot shows the time series for all voxels within the brain mask. + Voxels are grouped into cortical (blue), and subcortical (orange) gray matter, + cerebellum (green) and white matter and CSF (red), indicated by the color map on the left-hand side. + +- name: About + nested: true + reportlets: + - bids: {datatype: figures, desc: about, suffix: T1w} + - custom: boilerplate + path: '{out_dir}/logs' + bibfile: ['qsirecon', 'data/boilerplate.bib'] + caption: | +

We kindly ask to report results preprocessed with this tool using the following boilerplate.

+ + title: Methods + - custom: errors + path: '{out_dir}/sub-{subject}/log/{run_uuid}' + captions: NiReports may have recorded failure conditions. + title: Errors diff --git a/qsirecon/reports/core.py b/qsirecon/reports/core.py index a9924d4b..c7df2323 100644 --- a/qsirecon/reports/core.py +++ b/qsirecon/reports/core.py @@ -35,6 +35,7 @@ def run_reports( out_filename="report.html", reportlets_dir=None, errorname="report.err", + metadata=None, **entities, ): """ @@ -48,7 +49,7 @@ def run_reports( reportlets_dir=reportlets_dir, plugins=None, plugin_meta=None, - metadata=None, + metadata=metadata, **entities, ) @@ -67,7 +68,13 @@ def run_reports( def generate_reports( - subject_list, output_dir, run_uuid, session_list=None, bootstrap_file=None, work_dir=None + subject_list, + output_dir, + run_uuid, + session_list=None, + bootstrap_file=None, + work_dir=None, + qsirecon_suffix="", ): """Generate reports for a list of subjects.""" reportlets_dir = None @@ -105,6 +112,7 @@ def generate_reports( out_filename=html_report, reportlets_dir=reportlets_dir, errorname=f"report-{run_uuid}-{subject_label}.err", + metadata={"qsirecon_suffix": qsirecon_suffix}, subject=subject_label, ) # If the report generation failed, append the subject label for which it failed @@ -136,6 +144,7 @@ def generate_reports( out_filename=html_report, reportlets_dir=reportlets_dir, errorname=f"report-{run_uuid}-{subject_label}-func.err", + metadata={"qsirecon_suffix": qsirecon_suffix}, subject=subject_label, session=session_label, ) diff --git a/qsirecon/tests/data/amico_noddi_outputs.txt b/qsirecon/tests/data/amico_noddi_outputs.txt index 9032b10a..e6f6b1b8 100644 --- a/qsirecon/tests/data/amico_noddi_outputs.txt +++ b/qsirecon/tests/data/amico_noddi_outputs.txt @@ -4,10 +4,15 @@ logs/CITATION.bib logs/CITATION.html logs/CITATION.md logs/CITATION.tex -qsirecon -qsirecon qsirecon-NODDI +qsirecon-NODDI/dataset_description.json +qsirecon-NODDI/logs +qsirecon-NODDI/logs/CITATION.bib +qsirecon-NODDI/logs/CITATION.html +qsirecon-NODDI/logs/CITATION.md +qsirecon-NODDI/logs/CITATION.tex qsirecon-NODDI/sub-PNC +qsirecon-NODDI/sub-PNC.html qsirecon-NODDI/sub-PNC/dwi qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_desc-preproc_dwimap.fib.gz qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_desc-preproc_model-NODDI_mfp-AMICOconfig_dwimap.pickle.gz @@ -15,8 +20,4 @@ qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_desc-preproc_model-no qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_desc-preproc_model-noddi_mdp-isovf_dwimap.nii.gz qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_desc-preproc_model-noddi_mdp-od_dwimap.nii.gz qsirecon-NODDI/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_desc-preproc_model-noddi_mfp-direction_dwimap.nii.gz -qsirecon/dwiqc.json -qsirecon/sub-PNC -qsirecon/sub-PNC -qsirecon/sub-PNC.html sub-PNC diff --git a/qsirecon/tests/data/autotrack_outputs.txt b/qsirecon/tests/data/autotrack_outputs.txt index 27283456..295bca89 100644 --- a/qsirecon/tests/data/autotrack_outputs.txt +++ b/qsirecon/tests/data/autotrack_outputs.txt @@ -4,9 +4,14 @@ logs/CITATION.bib logs/CITATION.html logs/CITATION.md logs/CITATION.tex -qsirecon -qsirecon qsirecon-DSIStudio +qsirecon-DSIStudio/dataset_description.json +qsirecon-DSIStudio/logs +qsirecon-DSIStudio/logs/CITATION.bib +qsirecon-DSIStudio/logs/CITATION.html +qsirecon-DSIStudio/logs/CITATION.md +qsirecon-DSIStudio/logs/CITATION.tex +qsirecon-DSIStudio/sub-ABCD.html qsirecon-DSIStudio/sub-ABCD qsirecon-DSIStudio/sub-ABCD/dwi qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_bundle-Association_ArcuateFasciculusL_streamlines.tck.gz @@ -14,7 +19,4 @@ qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_bun qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_bundlestats.csv qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_dwimap.fib.gz qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_mapping.map.gz -qsirecon/dwiqc.json -qsirecon/sub-ABCD -qsirecon/sub-ABCD.html sub-ABCD diff --git a/qsirecon/tests/data/cuda_outputs.txt b/qsirecon/tests/data/cuda_outputs.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/qsirecon/tests/data/dipy_dki_outputs.txt b/qsirecon/tests/data/dipy_dki_outputs.txt index fd7a1eb4..b8cbed1a 100644 --- a/qsirecon/tests/data/dipy_dki_outputs.txt +++ b/qsirecon/tests/data/dipy_dki_outputs.txt @@ -4,10 +4,15 @@ logs/CITATION.bib logs/CITATION.html logs/CITATION.md logs/CITATION.tex -qsirecon -qsirecon qsirecon-DKI +qsirecon-DKI/dataset_description.json +qsirecon-DKI/logs +qsirecon-DKI/logs/CITATION.bib +qsirecon-DKI/logs/CITATION.html +qsirecon-DKI/logs/CITATION.md +qsirecon-DKI/logs/CITATION.tex qsirecon-DKI/sub-ABCD +qsirecon-DKI/sub-ABCD.html qsirecon-DKI/sub-ABCD/dwi qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-dki_mdp-AD_dwimap.nii.gz qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-dki_mdp-AK_dwimap.nii.gz @@ -18,7 +23,4 @@ qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-dki qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-dki_mdp-RD_dwimap.nii.gz qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-dki_mdp-RK_dwimap.nii.gz qsirecon-DKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-tensor_mdp-FA_dwimap.nii.gz -qsirecon/dwiqc.json -qsirecon/sub-ABCD -qsirecon/sub-ABCD.html sub-ABCD diff --git a/qsirecon/tests/data/dipy_mapmri_outputs.txt b/qsirecon/tests/data/dipy_mapmri_outputs.txt index 916b992a..6bbd454b 100644 --- a/qsirecon/tests/data/dipy_mapmri_outputs.txt +++ b/qsirecon/tests/data/dipy_mapmri_outputs.txt @@ -4,10 +4,15 @@ logs/CITATION.bib logs/CITATION.html logs/CITATION.md logs/CITATION.tex -qsirecon -qsirecon qsirecon-DIPYMAPMRI +qsirecon-DIPYMAPMRI/dataset_description.json +qsirecon-DIPYMAPMRI/logs +qsirecon-DIPYMAPMRI/logs/CITATION.bib +qsirecon-DIPYMAPMRI/logs/CITATION.html +qsirecon-DIPYMAPMRI/logs/CITATION.md +qsirecon-DIPYMAPMRI/logs/CITATION.tex qsirecon-DIPYMAPMRI/sub-ABCD +qsirecon-DIPYMAPMRI/sub-ABCD.html qsirecon-DIPYMAPMRI/sub-ABCD/dwi qsirecon-DIPYMAPMRI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_dwimap.fib.gz qsirecon-DIPYMAPMRI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_dwimap.mif @@ -20,7 +25,4 @@ qsirecon-mapmri_recon/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_ qsirecon-mapmri_recon/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-mapmri_mdp-RTOP_dwimap.nii.gz qsirecon-mapmri_recon/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-mapmri_mdp-RTPP_dwimap.nii.gz qsirecon-mapmri_recon/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-mapmri_mfp-lapnorm_dwimap.nii.gz -qsirecon/dwiqc.json -qsirecon/sub-ABCD -qsirecon/sub-ABCD.html sub-ABCD diff --git a/qsirecon/tests/data/mrtrix3_recon_outputs.txt b/qsirecon/tests/data/mrtrix3_recon_outputs.txt index ab52c710..18ca052d 100644 --- a/qsirecon/tests/data/mrtrix3_recon_outputs.txt +++ b/qsirecon/tests/data/mrtrix3_recon_outputs.txt @@ -4,10 +4,15 @@ logs/CITATION.bib logs/CITATION.html logs/CITATION.md logs/CITATION.tex -qsirecon -qsirecon qsirecon-MRtrix3_act-FAST +qsirecon-MRtrix3_act-FAST/dataset_description.json +qsirecon-MRtrix3_act-FAST/logs +qsirecon-MRtrix3_act-FAST/logs/CITATION.bib +qsirecon-MRtrix3_act-FAST/logs/CITATION.html +qsirecon-MRtrix3_act-FAST/logs/CITATION.md +qsirecon-MRtrix3_act-FAST/logs/CITATION.tex qsirecon-MRtrix3_act-FAST/sub-ABCD +qsirecon-MRtrix3_act-FAST/sub-ABCD.html qsirecon-MRtrix3_act-FAST/sub-ABCD/dwi qsirecon-MRtrix3_act-FAST/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_connectivity.mat qsirecon-MRtrix3_act-FAST/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_exemplarbundles.zip @@ -46,7 +51,4 @@ qsirecon-anat/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_atlas-sc qsirecon-anat/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_atlas-schaefer400_dseg.mif.gz qsirecon-anat/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_atlas-schaefer400_dseg.nii.gz qsirecon-anat/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_atlas-schaefer400_dseg.txt -qsirecon/dwiqc.json -qsirecon/sub-ABCD -qsirecon/sub-ABCD.html sub-ABCD diff --git a/qsirecon/tests/data/mrtrix_singleshell_ss3t_act_outputs.txt b/qsirecon/tests/data/mrtrix_singleshell_ss3t_act_outputs.txt index 1cf9586d..eb015d09 100644 --- a/qsirecon/tests/data/mrtrix_singleshell_ss3t_act_outputs.txt +++ b/qsirecon/tests/data/mrtrix_singleshell_ss3t_act_outputs.txt @@ -4,9 +4,14 @@ logs/CITATION.bib logs/CITATION.html logs/CITATION.md logs/CITATION.tex -qsirecon -qsirecon qsirecon-MRtrix3_fork-SS3T_act-FAST +qsirecon-MRtrix3_fork-SS3T_act-FAST/dataset_description.json +qsirecon-MRtrix3_fork-SS3T_act-FAST/logs +qsirecon-MRtrix3_fork-SS3T_act-FAST/logs/CITATION.bib +qsirecon-MRtrix3_fork-SS3T_act-FAST/logs/CITATION.html +qsirecon-MRtrix3_fork-SS3T_act-FAST/logs/CITATION.md +qsirecon-MRtrix3_fork-SS3T_act-FAST/logs/CITATION.tex +qsirecon-MRtrix3_fork-SS3T_act-FAST/sub-PNC.html qsirecon-MRtrix3_fork-SS3T_act-FAST/sub-PNC qsirecon-MRtrix3_fork-SS3T_act-FAST/sub-PNC/dwi qsirecon-MRtrix3_fork-SS3T_act-FAST/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_desc-preproc_connectivity.mat @@ -46,8 +51,4 @@ qsirecon-anat/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_desc-preproc_atlas-sch qsirecon-anat/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_desc-preproc_atlas-schaefer400_dseg.mif.gz qsirecon-anat/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_desc-preproc_atlas-schaefer400_dseg.nii.gz qsirecon-anat/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_desc-preproc_atlas-schaefer400_dseg.txt -qsirecon/dwiqc.json -qsirecon/sub-PNC -qsirecon/sub-PNC -qsirecon/sub-PNC.html sub-PNC diff --git a/qsirecon/tests/data/mrtrix_singleshell_ss3t_noact_outputs.txt b/qsirecon/tests/data/mrtrix_singleshell_ss3t_noact_outputs.txt index 878e39d2..0fa67003 100644 --- a/qsirecon/tests/data/mrtrix_singleshell_ss3t_noact_outputs.txt +++ b/qsirecon/tests/data/mrtrix_singleshell_ss3t_noact_outputs.txt @@ -4,10 +4,15 @@ logs/CITATION.bib logs/CITATION.html logs/CITATION.md logs/CITATION.tex -qsirecon -qsirecon qsirecon-MRtrix3_fork-SS3T_act-None +qsirecon-MRtrix3_fork-SS3T_act-None/dataset_description.json +qsirecon-MRtrix3_fork-SS3T_act-None/logs +qsirecon-MRtrix3_fork-SS3T_act-None/logs/CITATION.bib +qsirecon-MRtrix3_fork-SS3T_act-None/logs/CITATION.html +qsirecon-MRtrix3_fork-SS3T_act-None/logs/CITATION.md +qsirecon-MRtrix3_fork-SS3T_act-None/logs/CITATION.tex qsirecon-MRtrix3_fork-SS3T_act-None/sub-PNC +qsirecon-MRtrix3_fork-SS3T_act-None/sub-PNC.html qsirecon-MRtrix3_fork-SS3T_act-None/sub-PNC/dwi qsirecon-MRtrix3_fork-SS3T_act-None/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_desc-preproc_connectivity.mat qsirecon-MRtrix3_fork-SS3T_act-None/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_desc-preproc_exemplarbundles.zip @@ -46,8 +51,4 @@ qsirecon-anat/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_desc-preproc_atlas-sch qsirecon-anat/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_desc-preproc_atlas-schaefer400_dseg.mif.gz qsirecon-anat/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_desc-preproc_atlas-schaefer400_dseg.nii.gz qsirecon-anat/sub-PNC/dwi/sub-PNC_acq-realistic_space-T1w_desc-preproc_atlas-schaefer400_dseg.txt -qsirecon/dwiqc.json -qsirecon/sub-PNC -qsirecon/sub-PNC -qsirecon/sub-PNC.html sub-PNC diff --git a/qsirecon/tests/data/pyafq_recon_external_trk_outputs.txt b/qsirecon/tests/data/pyafq_recon_external_trk_outputs.txt index 74d57ded..fb16ee0f 100644 --- a/qsirecon/tests/data/pyafq_recon_external_trk_outputs.txt +++ b/qsirecon/tests/data/pyafq_recon_external_trk_outputs.txt @@ -1,16 +1,19 @@ -qsirecon dataset_description.json -qsirecon/dwiqc.json logs logs/CITATION.bib logs/CITATION.html logs/CITATION.md logs/CITATION.tex sub-ABCD -qsirecon/sub-ABCD.html -qsirecon qsirecon-MRtrix3 +qsirecon-MRtrix3/dataset_description.json +qsirecon-MRtrix3/logs +qsirecon-MRtrix3/logs/CITATION.bib +qsirecon-MRtrix3/logs/CITATION.html +qsirecon-MRtrix3/logs/CITATION.md +qsirecon-MRtrix3/logs/CITATION.tex qsirecon-MRtrix3/sub-ABCD +qsirecon-MRtrix3/sub-ABCD.html qsirecon-MRtrix3/sub-ABCD/dwi qsirecon-MRtrix3/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-SD_Stream_streamlines.tck qsirecon-MRtrix3/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-msmt_csd_mfp-FOD_label-CSF_dwimap.mif.gz @@ -24,7 +27,14 @@ qsirecon-MRtrix3/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model qsirecon-MRtrix3/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-sift2_mu.txt qsirecon-MRtrix3/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-sift2_streamlineweights.csv qsirecon-PYAFQ +qsirecon-PYAFQ/dataset_description.json +qsirecon-PYAFQ/logs +qsirecon-PYAFQ/logs/CITATION.bib +qsirecon-PYAFQ/logs/CITATION.html +qsirecon-PYAFQ/logs/CITATION.md +qsirecon-PYAFQ/logs/CITATION.tex qsirecon-PYAFQ/sub-ABCD +qsirecon-PYAFQ/sub-ABCD.html qsirecon-PYAFQ/sub-ABCD/dwi qsirecon-PYAFQ/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_dwimap qsirecon-PYAFQ/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_dwimap/ROIs @@ -120,4 +130,3 @@ qsirecon-PYAFQ/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_dwimap/ qsirecon-PYAFQ/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_dwimap/viz_core_bundles/sub-ABCD_acq-10per000_coordsys-RASMM_trkmethod-probCSD_recogmethod-AFQ_desc-LeftCorticospinalviz_dwi.html qsirecon-PYAFQ/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_dwimap/viz_core_bundles/sub-ABCD_acq-10per000_coordsys-RASMM_trkmethod-probCSD_recogmethod-AFQ_desc-LeftSuperiorLongitudinalviz_dwi.html qsirecon-PYAFQ/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_dwimap/viz_core_bundles/sub-ABCD_acq-10per000_coordsys-RASMM_trkmethod-probCSD_recogmethod-AFQ_desc-RightCorticospinalviz_dwi.html -qsirecon/sub-ABCD \ No newline at end of file diff --git a/qsirecon/tests/data/pyafq_recon_full_outputs.txt b/qsirecon/tests/data/pyafq_recon_full_outputs.txt index 54745c2b..cf9c709e 100644 --- a/qsirecon/tests/data/pyafq_recon_full_outputs.txt +++ b/qsirecon/tests/data/pyafq_recon_full_outputs.txt @@ -4,10 +4,15 @@ logs/CITATION.bib logs/CITATION.html logs/CITATION.md logs/CITATION.tex -qsirecon -qsirecon qsirecon-PYAFQ +qsirecon-PYAFQ/dataset_description.json +qsirecon-PYAFQ/logs +qsirecon-PYAFQ/logs/CITATION.bib +qsirecon-PYAFQ/logs/CITATION.html +qsirecon-PYAFQ/logs/CITATION.md +qsirecon-PYAFQ/logs/CITATION.tex qsirecon-PYAFQ/sub-ABCD +qsirecon-PYAFQ/sub-ABCD.html qsirecon-PYAFQ/sub-ABCD/dwi qsirecon-PYAFQ/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_dwimap qsirecon-PYAFQ/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_dwimap/ROIs @@ -108,7 +113,4 @@ qsirecon-PYAFQ/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_dwimap/ qsirecon-PYAFQ/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_dwimap/viz_core_bundles/sub-ABCD_acq-10per000_coordsys-RASMM_trkmethod-probCSD_recogmethod-AFQ_desc-LeftCorticospinalviz_dwi.html qsirecon-PYAFQ/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_dwimap/viz_core_bundles/sub-ABCD_acq-10per000_coordsys-RASMM_trkmethod-probCSD_recogmethod-AFQ_desc-LeftSuperiorLongitudinalviz_dwi.html qsirecon-PYAFQ/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_dwimap/viz_core_bundles/sub-ABCD_acq-10per000_coordsys-RASMM_trkmethod-probCSD_recogmethod-AFQ_desc-RightCorticospinalviz_dwi.html -qsirecon/dwiqc.json -qsirecon/sub-ABCD -qsirecon/sub-ABCD.html sub-ABCD diff --git a/qsirecon/tests/data/scalar_mapper_outputs.txt b/qsirecon/tests/data/scalar_mapper_outputs.txt index c1e51c72..fb7cf69c 100644 --- a/qsirecon/tests/data/scalar_mapper_outputs.txt +++ b/qsirecon/tests/data/scalar_mapper_outputs.txt @@ -4,10 +4,15 @@ logs/CITATION.bib logs/CITATION.html logs/CITATION.md logs/CITATION.tex -qsirecon -qsirecon qsirecon-DIPYDKI +qsirecon-DIPYDKI/dataset_description.json +qsirecon-DIPYDKI/logs +qsirecon-DIPYDKI/logs/CITATION.bib +qsirecon-DIPYDKI/logs/CITATION.html +qsirecon-DIPYDKI/logs/CITATION.md +qsirecon-DIPYDKI/logs/CITATION.tex qsirecon-DIPYDKI/sub-ABCD +qsirecon-DIPYDKI/sub-ABCD.html qsirecon-DIPYDKI/sub-ABCD/dwi qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_desc-preproc_model-dki_mdp-AD_dwimap.nii.gz qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_desc-preproc_model-dki_mdp-AK_dwimap.nii.gz @@ -29,7 +34,14 @@ qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-dki_mdp-RK_dwimap.nii.gz qsirecon-DIPYDKI/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-tensor_mdp-FA_dwimap.nii.gz qsirecon-DSIStudio +qsirecon-DSIStudio/dataset_description.json +qsirecon-DSIStudio/logs +qsirecon-DSIStudio/logs/CITATION.bib +qsirecon-DSIStudio/logs/CITATION.html +qsirecon-DSIStudio/logs/CITATION.md +qsirecon-DSIStudio/logs/CITATION.tex qsirecon-DSIStudio/sub-ABCD +qsirecon-DSIStudio/sub-ABCD.html qsirecon-DSIStudio/sub-ABCD/dwi qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_desc-preproc_fit-GQI_mdp-gfa_dwimap.nii.gz qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-MNI152NLin2009cAsym_desc-preproc_fit-GQI_mdp-iso_dwimap.nii.gz @@ -70,7 +82,4 @@ qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_mod qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-tensor_mfp-tyy_dwimap.nii.gz qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-tensor_mfp-tyz_dwimap.nii.gz qsirecon-DSIStudio/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-tensor_mfp-tzz_dwimap.nii.gz -qsirecon/dwiqc.json -qsirecon/sub-ABCD -qsirecon/sub-ABCD.html sub-ABCD diff --git a/qsirecon/tests/data/tortoise_recon_outputs.txt b/qsirecon/tests/data/tortoise_recon_outputs.txt index 6fe4c50f..d1bd8b82 100644 --- a/qsirecon/tests/data/tortoise_recon_outputs.txt +++ b/qsirecon/tests/data/tortoise_recon_outputs.txt @@ -4,10 +4,15 @@ logs/CITATION.bib logs/CITATION.html logs/CITATION.md logs/CITATION.tex -qsirecon -qsirecon qsirecon-TORTOISE +qsirecon-TORTOISE/dataset_description.json +qsirecon-TORTOISE/logs +qsirecon-TORTOISE/logs/CITATION.bib +qsirecon-TORTOISE/logs/CITATION.html +qsirecon-TORTOISE/logs/CITATION.md +qsirecon-TORTOISE/logs/CITATION.tex qsirecon-TORTOISE/sub-ABCD +qsirecon-TORTOISE/sub-ABCD.html qsirecon-TORTOISE/sub-ABCD/dwi qsirecon-TORTOISE/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-mapmri_mdp-NG_dwimap.nii.gz qsirecon-TORTOISE/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-mapmri_mdp-NGpar_dwimap.nii.gz @@ -22,7 +27,4 @@ qsirecon-TORTOISE/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_mode qsirecon-TORTOISE/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-tensor_mdp-li_dwimap.nii.gz qsirecon-TORTOISE/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-tensor_mdp-rd_dwimap.nii.gz qsirecon-TORTOISE/sub-ABCD/dwi/sub-ABCD_acq-10per000_space-T1w_desc-preproc_model-tensor_mfp-AM_dwimap.nii.gz -qsirecon/dwiqc.json -qsirecon/sub-ABCD -qsirecon/sub-ABCD.html sub-ABCD diff --git a/qsirecon/tests/test_cli.py b/qsirecon/tests/test_cli.py index 5bb5572b..116b50e9 100644 --- a/qsirecon/tests/test_cli.py +++ b/qsirecon/tests/test_cli.py @@ -10,13 +10,13 @@ from qsirecon.cli import run from qsirecon.cli.parser import parse_args from qsirecon.cli.workflow import build_boilerplate, build_workflow +from qsirecon.reports.core import generate_reports from qsirecon.tests.utils import ( check_generated_files, download_test_data, get_test_data_path, ) from qsirecon.utils.bids import write_derivative_description -from qsirecon.viz.reports import generate_reports nipype_config.enable_debug_mode() @@ -465,6 +465,7 @@ def _run_and_generate(test_name, parameters, test_main=True): output_dir=config.execution.output_dir, run_uuid=config.execution.run_uuid, session_list=session_list, + qsirecon_suffix="", ) output_list_file = os.path.join(get_test_data_path(), f"{test_name}_outputs.txt") diff --git a/qsirecon/viz/__init__.py b/qsirecon/viz/__init__.py deleted file mode 100644 index 45a15998..00000000 --- a/qsirecon/viz/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- -# vi: set ft=python sts=4 ts=4 sw=4 et: -""" -The qsirecon reporting engine for visual assessment -""" -from .reports import generate_reports, run_reports diff --git a/qsirecon/viz/report.tpl b/qsirecon/viz/report.tpl deleted file mode 100644 index 0f73ff67..00000000 --- a/qsirecon/viz/report.tpl +++ /dev/null @@ -1,186 +0,0 @@ - - - - - - - - - - - - - - - - - - -{% for sub_report in sections %} -
-

{{ sub_report.name }}

- {% if sub_report.isnested %} - {% for run_report in sub_report.reportlets %} -
-

Reports for {{ run_report.title }}

- {% for elem in run_report.reportlets %} - {% if elem.contents %} - {% if elem.title %}

{{ elem.title }}

{% endif %} - {% if elem.description %}

{{ elem.description }}

{% endif %} - {% for content in elem.contents %} - {% if elem.raw %}{{ content }}{% else %} -

- - Problem loading figure {{ content }}. If the link below works, please try reloading the report in your browser. -
-
- Get figure file: {{ content }} -
- {% endif %} - {% endfor %} - {% endif %} - {% endfor %} -
- {% endfor %} - {% else %} - {% for elem in sub_report.reportlets %} - {% if elem.contents %} - {% if elem.title %}

{{ elem.title }}

{% endif %} - {% if elem.description %}

{{ elem.description }}


{% endif %} - {% for content in elem.contents %} - {% if elem.raw %}{{ content }}{% else %} -

- filename:{{ content }} -
-
- Get figure file: {{ content }} -
- {% endif %} - {% endfor %} - {% endif %} - {% endfor %} - {% endif %} -
-{% endfor %} - -
-

Methods

- {% if boilerplate %} -

We kindly ask to report results preprocessed with qsirecon using the following - boilerplate

- -
- {% for b in boilerplate %} -
{{ b[2] }}
- {% endfor %} -
- {% else %} -

Failed to generate the boilerplate

- {% endif %} -

Alternatively, an interactive boilerplate generator is available in the documentation website.

-
- -
-

Errors

- -
- - - - - diff --git a/qsirecon/viz/reports.py b/qsirecon/viz/reports.py deleted file mode 100644 index 8bcfe747..00000000 --- a/qsirecon/viz/reports.py +++ /dev/null @@ -1,436 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- -# vi: set ft=python sts=4 ts=4 sw=4 et: -""" -qsirecon reports builder -^^^^^^^^^^^^^^^^^^^^^^^^ - - -""" -import html -import json -import re -from pathlib import Path - -import jinja2 -from nipype.utils.filemanip import copyfile, loadcrash -from pkg_resources import resource_filename as pkgrf - -from .. import config - - -class Element(object): - """ - Just a basic component of a report - """ - - def __init__(self, name, title=None): - self.name = name - self.title = title - - -class Reportlet(Element): - """ - A reportlet has title, description and a list of graphical components - """ - - def __init__( - self, name, imgtype=None, file_pattern=None, title=None, description=None, raw=False - ): - self.name = name - self.file_pattern = re.compile(file_pattern) - self.title = title - self.description = description - self.source_files = [] - self.contents = [] - self.raw = raw - self.imgtype = imgtype - - -class SubReport(Element): - """ - SubReports are sections within a Report - """ - - def __init__(self, name, reportlets=None, title=""): - self.name = name - self.title = title - self.reportlets = [] - if reportlets: - self.reportlets += reportlets - self.isnested = False - - -class Report(object): - """ - The full report object - """ - - def __init__( - self, - path, - viz_config, - out_dir, - run_uuid, - out_filename="report.html", - pipeline_type="qsirecon", - ): - self.root = path - self.sections = [] - self.errors = [] - self.out_dir = Path(out_dir) - self.out_filename = out_filename - self.run_uuid = run_uuid - self.pipeline_type = pipeline_type - - self._load_config(viz_config) - - def _load_config(self, viz_config): - with open(viz_config, "r") as configfh: - viz_config = json.load(configfh) - - self.index(viz_config["sections"]) - - def index(self, viz_config): - fig_dir = "figures" - subject_dir = self.root.split("/")[-1] - subject = re.search("^(?Psub-[a-zA-Z0-9]+)$", subject_dir).group() - svg_dir = self.out_dir / self.pipeline_type / subject / fig_dir - svg_dir.mkdir(parents=True, exist_ok=True) - reportlet_list = list(sorted([str(f) for f in Path(self.root).glob("**/*.*")])) - - for subrep_cfg in viz_config: - reportlets = [] - for reportlet_cfg in subrep_cfg["reportlets"]: - rlet = Reportlet(**reportlet_cfg) - for src in reportlet_list: - ext = src.split(".")[-1] - if rlet.file_pattern.search(src): - contents = None - if ext == "html": - with open(src) as fp: - contents = fp.read().strip() - elif ext in ("svg", "gif", "png"): - fbase = Path(src).name - copyfile(src, str(svg_dir / fbase), copy=True, use_hardlink=True) - contents = str(Path(subject) / fig_dir / fbase) - if contents: - rlet.source_files.append(src) - rlet.contents.append(contents) - - if rlet.source_files: - reportlets.append(rlet) - - if reportlets: - sub_report = SubReport( - subrep_cfg["name"], reportlets=reportlets, title=subrep_cfg.get("title") - ) - self.sections.append(order_by_run(sub_report)) - - error_dir = self.out_dir / self.pipeline_type / subject / "log" / self.run_uuid - if error_dir.is_dir(): - self.index_error_dir(error_dir) - - def index_error_dir(self, error_dir): - """ - Crawl subjects crash directory for the corresponding run and return text for - .pklz crash file found. - """ - for crashfile in error_dir.glob("crash*.*"): - if crashfile.suffix == ".pklz": - self.errors.append(self._read_pkl(crashfile)) - elif crashfile.suffix == ".txt": - self.errors.append(self._read_txt(crashfile)) - - @staticmethod - def _read_pkl(path): - fname = str(path) - crash_data = loadcrash(fname) - data = { - "file": fname, - "traceback": "".join(crash_data["traceback"]).replace("\\n", "
"), - } - if "node" in crash_data: - data["node"] = crash_data["node"] - if data["node"].base_dir: - data["node_dir"] = data["node"].output_dir() - else: - data["node_dir"] = "Node crashed before execution" - data["inputs"] = sorted(data["node"].inputs.trait_get().items()) - return data - - @staticmethod - def _read_txt(path): - lines = path.read_text(encoding="UTF-8").splitlines() - data = {"file": str(path)} - traceback_start = 0 - if lines[0].startswith("Node"): - data["node"] = lines[0].split(": ", 1)[1] - data["node_dir"] = lines[1].split(": ", 1)[1] - inputs = [] - for i, line in enumerate(lines[5:], 5): - if not line: - traceback_start = i + 1 - break - inputs.append(tuple(map(html.escape, line.split(" = ", 1)))) - data["inputs"] = sorted(inputs) - else: - data["node_dir"] = "Node crashed before execution" - data["traceback"] = "\n".join(lines[traceback_start:]) - return data - - def generate_report(self): - logs_path = self.out_dir / self.pipeline_type / "logs" - - boilerplate = [] - boiler_idx = 0 - - if (logs_path / "CITATION.html").exists(): - text = (logs_path / "CITATION.html").read_text(encoding="UTF-8") - text = ( - '
%s
' - % re.compile("(.*?)", re.DOTALL | re.IGNORECASE) - .findall(text)[0] - .strip() - ) - boilerplate.append((boiler_idx, "HTML", text)) - boiler_idx += 1 - - if (logs_path / "CITATION.md").exists(): - text = "
%s
\n" % (logs_path / "CITATION.md").read_text(encoding="UTF-8") - boilerplate.append((boiler_idx, "Markdown", text)) - boiler_idx += 1 - - if (logs_path / "CITATION.tex").exists(): - text = (logs_path / "CITATION.tex").read_text(encoding="UTF-8") - text = ( - re.compile(r"\\begin{document}(.*?)\\end{document}", re.DOTALL | re.IGNORECASE) - .findall(text)[0] - .strip() - ) - text = "
%s
\n" % text - text += "

Bibliography

\n" - text += "
%s
\n" % Path(pkgrf("qsirecon", "data/boilerplate.bib")).read_text( - encoding="UTF-8" - ) - boilerplate.append((boiler_idx, "LaTeX", text)) - boiler_idx += 1 - - searchpath = pkgrf("qsirecon", "/") - env = jinja2.Environment( - loader=jinja2.FileSystemLoader(searchpath=searchpath), - trim_blocks=True, - lstrip_blocks=True, - ) - report_tpl = env.get_template("viz/report.tpl") - report_render = report_tpl.render( - sections=self.sections, errors=self.errors, boilerplate=boilerplate - ) - - # Write out report - (self.out_dir / self.pipeline_type / self.out_filename).write_text( - report_render, encoding="UTF-8" - ) - return len(self.errors) - - -def order_by_run(subreport): - ordered = [] - run_reps = {} - for element in subreport.reportlets: - if len(element.source_files) == 1 and element.source_files[0]: - ordered.append(element) - continue - - for filename, file_contents in zip(element.source_files, element.contents): - name, title = generate_name_title(filename) - if not filename or not name: - continue - - new_element = Reportlet( - name=element.name, - title=element.title, - file_pattern=element.file_pattern, - description=element.description, - raw=element.raw, - imgtype=element.imgtype, - ) - new_element.contents.append(file_contents) - new_element.source_files.append(filename) - - if name not in run_reps: - run_reps[name] = SubReport(name, title=title) - - run_reps[name].reportlets.append(new_element) - - if run_reps: - keys = list(sorted(run_reps.keys())) - for key in keys: - ordered.append(run_reps[key]) - subreport.isnested = True - - subreport.reportlets = ordered - return subreport - - -def generate_name_title(filename): - fname = Path(filename).name - expr = re.compile( - "^sub-(?P[a-zA-Z0-9]+)(_ses-(?P[a-zA-Z0-9]+))?" - "(_task-(?P[a-zA-Z0-9]+))?(_acq-(?P[a-zA-Z0-9]+))?" - "(_rec-(?P[a-zA-Z0-9]+))?(_run-(?P[a-zA-Z0-9]+))?" - ) - outputs = expr.search(fname) - if outputs: - outputs = outputs.groupdict() - else: - return None, None - - name = "{session}{task}{acq}{rec}{run}".format( - session="_ses-" + outputs["session_id"] if outputs["session_id"] else "", - task="_task-" + outputs["task_id"] if outputs["task_id"] else "", - acq="_acq-" + outputs["acq_id"] if outputs["acq_id"] else "", - rec="_rec-" + outputs["rec_id"] if outputs["rec_id"] else "", - run="_run-" + outputs["run_id"] if outputs["run_id"] else "", - ) - title = "{session}{task}{acq}{rec}{run}".format( - session=" Session: " + outputs["session_id"] if outputs["session_id"] else "", - task=" Task: " + outputs["task_id"] if outputs["task_id"] else "", - acq=" Acquisition: " + outputs["acq_id"] if outputs["acq_id"] else "", - rec=" Reconstruction: " + outputs["rec_id"] if outputs["rec_id"] else "", - run=" Run: " + outputs["run_id"] if outputs["run_id"] else "", - ) - return name.strip("_"), title - - -def run_reports(reportlets_dir, out_dir, subject_label, run_uuid, report_type="qsirecon"): - """ - Runs the reports - - >>> import os - >>> from shutil import copytree - >>> from tempfile import TemporaryDirectory - >>> filepath = os.path.dirname(os.path.realpath(__file__)) - >>> test_data_path = os.path.realpath(os.path.join(filepath, - ... '../data/tests/work')) - >>> curdir = os.getcwd() - >>> tmpdir = TemporaryDirectory() - >>> os.chdir(tmpdir.name) - >>> data_dir = copytree(test_data_path, os.path.abspath('work')) - >>> os.makedirs('out/qsirecon', exist_ok=True) - >>> run_reports(os.path.abspath('work/reportlets'), - ... os.path.abspath('out'), - ... '01', 'madeoutuuid') - 0 - >>> os.chdir(curdir) - >>> tmpdir.cleanup() - - """ - reportlet_path = str(Path(reportlets_dir) / report_type / ("sub-%s" % subject_label)) - if report_type == "qsirecon": - viz_config = pkgrf("qsirecon", "viz/config.json") - else: - viz_config = pkgrf("qsirecon", "viz/recon_config.json") - - out_filename = "sub-{}.html".format(subject_label) - report = Report( - reportlet_path, viz_config, out_dir, run_uuid, out_filename, pipeline_type=report_type - ) - return report.generate_report() - - -def generate_reports(subject_list, pipeline_mode="qsirecon"): - """ - A wrapper to run_reports on a given ``subject_list`` - """ - reports_dir = str(config.execution.reportlets_dir) - run_uuid = config.execution.run_uuid - output_dir = str(config.execution.output_dir) - report_errors = [ - run_reports( - reports_dir, output_dir, subject_label, run_uuid=run_uuid, report_type=pipeline_mode - ) - for subject_label in subject_list - ] - - errno = sum(report_errors) - errno += generate_interactive_report_summary(Path(output_dir) / pipeline_mode) - if errno: - import logging - - logger = logging.getLogger("cli") - logger.warning( - "Errors occurred while generating reports for participants: %s.", - ", ".join( - ["%s (%d)" % (subid, err) for subid, err in zip(subject_list, report_errors)] - ), - ) - return errno - - -def generate_interactive_report_summary(output_dir): - """ - Gather the dwiqc values from the outputs in a - """ - report_errors = [] - qc_report = { - "report_type": "dwi_qc_report", - "pipeline": "qsirecon", - "pipeline_version": 0, - "boilerplate": "", - "metric_explanation": { - "raw_dimension_x": "Number of x voxels in raw images", - "raw_dimension_y": "Number of y voxels in raw images", - "raw_dimension_z": "Number of z voxels in raw images", - "raw_voxel_size_x": "Voxel size in x direction in raw images", - "raw_voxel_size_y": "Voxel size in y direction in raw images", - "raw_voxel_size_z": "Voxel size in z direction in raw images", - "raw_max_b": "Maximum b-value in s/mm^2 in raw images", - "raw_neighbor_corr": "Neighboring DWI Correlation (NDC) of raw images", - "raw_num_bad_slices": "Number of bad slices in raw images (from DSI Studio)", - "raw_num_directions": "Number of directions sampled in raw images", - "t1_dimension_x": "Number of x voxels in preprocessed images", - "t1_dimension_y": "Number of y voxels in preprocessed images", - "t1_dimension_z": "Number of z voxels in preprocessed images", - "t1_voxel_size_x": "Voxel size in x direction in preprocessed images", - "t1_voxel_size_y": "Voxel size in y direction in preprocessed images", - "t1_voxel_size_z": "Voxel size in z direction in preprocessed images", - "t1_max_b": "Maximum b-value s/mm^2 in preprocessed images", - "t1_neighbor_corr": "Neighboring DWI Correlation (NDC) of preprocessed images", - "t1_num_bad_slices": "Number of bad slices in preprocessed images (from DSI Studio)", - "t1_num_directions": "Number of directions sampled in preprocessed images", - "mean_fd": "Mean framewise displacement from head motion", - "max_fd": "Maximum framewise displacement from head motion", - "max_rotation": "Maximum rotation from head motion", - "max_translation": "Maximum translation from head motion", - "max_rel_rotation": "Maximum rotation relative to the previous head position", - "max_rel_translation": "Maximum translation relative to the previous head position", - "t1_dice_distance": "Dice score for the overlap of the T1w-based brain mask " - "and the b=0 ref mask", - }, - } - qc_values = [] - output_path = Path(output_dir) - dwiqc_jsons = output_path.rglob("**/sub-*dwiqc.json") - - for qc_file in dwiqc_jsons: - try: - with open(qc_file, "r") as qc_json: - dwi_qc = json.load(qc_json)["qc_scores"] - dwi_qc["participant_id"] = dwi_qc.get("subject_id", "subject") - qc_values.append(dwi_qc) - except Exception: - report_errors.append(1) - - errno = sum(report_errors) - if errno: - import logging - - logger = logging.getLogger("cli") - logger.warning("Errors occurred while generating interactive report summary.") - qc_report["subjects"] = qc_values - with open(output_path / "dwiqc.json", "w") as project_qc: - json.dump(qc_report, project_qc, indent=2) - - return errno diff --git a/qsirecon/workflows/base.py b/qsirecon/workflows/base.py index 8a4e0ce7..eeae7a1b 100644 --- a/qsirecon/workflows/base.py +++ b/qsirecon/workflows/base.py @@ -239,16 +239,10 @@ def init_single_subject_recon_wf(subject_id): for trait in anatomical_workflow_outputs]) ]) # fmt:skip - # Fill-in datasinks and reportlet datasinks for the anatomical workflow - for _node in workflow.list_node_names(): - node_suffix = _node.split(".")[-1] - if node_suffix.startswith("ds"): - base_dir = ( - config.execution.reportlets_dir - if "report" in node_suffix - else config.execution.output_dir - ) - workflow.get_node(_node).inputs.base_directory = base_dir + # Fill-in datasinks of reportlets seen so far + for node in workflow.list_node_names(): + if node.split(".")[-1].startswith("ds"): + workflow.get_node(node).inputs.base_directory = config.execution.output_dir return workflow diff --git a/qsirecon/workflows/recon/anatomical.py b/qsirecon/workflows/recon/anatomical.py index cd1ebde6..29e622f1 100644 --- a/qsirecon/workflows/recon/anatomical.py +++ b/qsirecon/workflows/recon/anatomical.py @@ -783,8 +783,9 @@ def _get_status(): for node in workflow.list_node_names(): node_suffix = node.split(".")[-1] if node_suffix.startswith("dsatlas_"): - workflow.connect( - inputnode, 'dwi_file', workflow.get_node(node), 'source_file') # fmt:skip + workflow.connect([ + (inputnode, workflow.get_node(node), [("dwi_file", "source_file")]), + ]) # fmt:skip if "mrtrix_5tt_hsv" in extras_to_make and not has_qsiprep_5tt_hsvs: raise Exception("Unable to create a 5tt HSV image given input data.") diff --git a/qsirecon/workflows/recon/build_workflow.py b/qsirecon/workflows/recon/build_workflow.py index 8d9dcdab..d3aa953e 100644 --- a/qsirecon/workflows/recon/build_workflow.py +++ b/qsirecon/workflows/recon/build_workflow.py @@ -151,20 +151,13 @@ def init_dwi_recon_workflow( node, "inputnode.mapping_metadata") # fmt:skip - # Fill-in datasinks and reportlet datasinks seen so far + # Set the source_file for any datasinks for node in workflow.list_node_names(): - node_suffix = node.split(".")[-1] - if node_suffix.startswith("ds_") or node_suffix.startswith("recon_scalars"): - base_dir = ( - config.execution.reportlets_dir - if "report" in node_suffix - else config.execution.output_dir - ) - workflow.connect(inputnode, 'dwi_file', - workflow.get_node(node), 'source_file') # fmt:skip - # config.loggers.workflow.info("setting %s base dir to %s", node_suffix, base_dir ) - if node_suffix.startswith("ds"): - workflow.get_node(node).inputs.base_directory = base_dir + node_name = node.split(".")[-1] + if node_name.startswith("ds_") or node_name.startswith("recon_scalars"): + workflow.connect([ + (inputnode, workflow.get_node(node), [("dwi_file", "source_file")]), + ]) # fmt:skip return workflow diff --git a/qsirecon/workflows/recon/mrtrix.py b/qsirecon/workflows/recon/mrtrix.py index 8b6497a1..0198d9f8 100644 --- a/qsirecon/workflows/recon/mrtrix.py +++ b/qsirecon/workflows/recon/mrtrix.py @@ -289,8 +289,7 @@ def init_mrtrix_csd_recon_wf( name="ds_wm_txt", run_without_submitting=True, ) - workflow.connect(outputnode, 'wm_txt', - ds_wm_txt, 'in_file') # fmt:skip + workflow.connect([(outputnode, ds_wm_txt, [("wm_txt", "in_file")])]) # If multitissue write out FODs for csf, gm if using_multitissue: diff --git a/qsirecon/workflows/reports.py b/qsirecon/workflows/reports.py index 2708a61c..078fe58a 100644 --- a/qsirecon/workflows/reports.py +++ b/qsirecon/workflows/reports.py @@ -85,8 +85,7 @@ def init_single_subject_json_report_wf(subject_id, name): wf = init_single_subject_json_report_wf( subject_id='test', name='single_subject_qsirecontest_wf', - reportlets_dir='.', - output_dir='.') + ) Parameters diff --git a/tests/test_lesion.py b/tests/test_lesion.py index 2c50990d..1f1041da 100644 --- a/tests/test_lesion.py +++ b/tests/test_lesion.py @@ -12,7 +12,6 @@ def test_roi_found(tmp_path): wf = base.init_single_subject_wf( subject_id="1", name="test_roi", - reportlets_dir=tmp_path, output_dir=output_dir, bids_dir=bids_dir, ignore=[],