Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add summary reportlets to HTML report #61

Merged
merged 21 commits into from
Aug 20, 2024
Merged
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ dependencies = [
"nibabel <= 5.2.0",
"nilearn == 0.10.1",
"nipype == 1.8.6",
"nireports",
"nireports ~= 23.2.2",
"niworkflows >=1.9,<= 1.10",
"numpy <= 1.26.3",
"pandas < 2.0.0",
Expand Down
10 changes: 1 addition & 9 deletions qsirecon/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,6 @@ def main():
finally:

from ..reports.core import generate_reports
from ..workflows.base import _load_recon_spec
from .workflow import copy_boilerplate

# Generate reports phase
Expand All @@ -184,15 +183,8 @@ 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)))
qsirecon_suffixes = config.workflow.qsirecon_suffixes
config.loggers.cli.info(f"QSIRecon pipeline suffixes: {qsirecon_suffixes}")
failed_reports = []
for qsirecon_suffix in qsirecon_suffixes:
Expand Down
14 changes: 14 additions & 0 deletions qsirecon/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,20 @@ class workflow(_Config):
"""Recon workflow specification."""
output_resolution = None
"""Isotropic voxel size for outputs."""
qsirecon_suffixes = []
mattcieslak marked this conversation as resolved.
Show resolved Hide resolved
"""List of reconstruction workflow names, derived from the recon_spec."""

@classmethod
def init(cls):
from .workflows.base import _load_recon_spec

workflow_spec = _load_recon_spec(cls.recon_spec)
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 []

cls.qsirecon_suffixes = sorted(list(set(qsirecon_suffixes)))


class loggers:
Expand Down
2 changes: 1 addition & 1 deletion qsirecon/data/io_spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"sub-{subject}[/ses-{session}]/{datatype<dwi>|dwi}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_atlas-{atlas}][_model-{model}][_bundles-{bundles}][_fit-{fit}][_mdp-{mdp}][_mfp-{mfp}][_bundle-{bundle}][_label-{label}][_desc-{desc}]_{suffix<mu>}.{extension<txt|json>|txt}",
"sub-{subject}[/ses-{session}]/{datatype<dwi>|dwi}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_atlas-{atlas}][_model-{model}][_bundles-{bundles}][_fit-{fit}][_mdp-{mdp}][_mfp-{mfp}][_bundle-{bundle}][_label-{label}][_desc-{desc}]_{suffix<exemplarbundles>}.{extension<zip|json>|zip}",
"sub-{subject}[/ses-{session}]/{datatype<dwi>|dwi}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_atlas-{atlas}][_model-{model}][_bundles-{bundles}][_fit-{fit}][_mdp-{mdp}][_mfp-{mfp}][_bundle-{bundle}][_label-{label}][_desc-{desc}]_{suffix<connectivity>}.{extension<mat|json>|mat}",
"sub-{subject}/{datatype<figures>}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_atlas-{atlas}][_model-{model}][_bundles-{bundles}][_fit-{fit}][_mdp-{mdp}][_mfp-{mfp}][_bundle-{bundle}][_label-{label}][_desc-{desc}]_{suffix<dwi>}.{extension<html|svg|png|json>}",
"sub-{subject}/{datatype<figures>}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_atlas-{atlas}][_model-{model}][_bundles-{bundles}][_fit-{fit}][_mdp-{mdp}][_mfp-{mfp}][_bundle-{bundle}][_label-{label}][_desc-{desc}]_{suffix<dwi|T1w|T2w>}.{extension<html|svg|png|json>}",
"sub-{subject}/{datatype<figures>}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_atlas-{atlas}][_model-{model}][_bundles-{bundles}][_fit-{fit}][_mdp-{mdp}][_mfp-{mfp}][_bundle-{bundle}][_label-{label}][_desc-{desc}]_{suffix<peaks>}.{extension<html|svg|png|json>}",
"sub-{subject}/{datatype<figures>}/sub-{subject}[_ses-{session}][_acq-{acquisition}][_ce-{ceagent}][_dir-{direction}][_rec-{reconstruction}][_run-{run}][_space-{space}][_cohort-{cohort}][_atlas-{atlas}][_model-{model}][_bundles-{bundles}][_fit-{fit}][_mdp-{mdp}][_mfp-{mfp}][_bundle-{bundle}][_label-{label}][_desc-{desc}]_{suffix<odfs|matrices>}.{extension<html|svg|png|json>}"
]
Expand Down
103 changes: 36 additions & 67 deletions qsirecon/interfaces/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
CommandLineInputSpec,
Directory,
File,
InputMultiObject,
InputMultiPath,
SimpleInterface,
Str,
Expand All @@ -35,45 +36,25 @@

from .qc import createB0_ColorFA_Mask_Sprites, createSprite4D

SUBJECT_TEMPLATE = """\t<ul class="elem-desc">
SUBJECT_TEMPLATE = """\
\t<ul class="elem-desc">
\t\t<li>Subject ID: {subject_id}</li>
\t\t<li>Structural images: {n_t1s:d} T1-weighted {t2w}</li>
\t\t<li>Diffusion-weighted series: inputs {n_dwis:d}, outputs {n_outputs:d}</li>
{groupings}
\t\t<li>Resampling targets: T1wACPC
\t\t<li>Structural images: {n_t1w:d} T1-weighted {t2w}</li>
\t\t<li>Diffusion-weighted series: {n_dwi:d}</li>
\t\t<li>Standard output spaces: {std_spaces}</li>
\t\t<li>Non-standard output spaces: {nstd_spaces}</li>
\t\t<li>FreeSurfer reconstruction: {freesurfer_status}</li>
\t</ul>
"""

DIFFUSION_TEMPLATE = """\t\t<h3 class="elem-title">Summary</h3>
\t\t<ul class="elem-desc">
\t\t\t<li>Phase-encoding (PE) direction: {pedir}</li>
\t\t\t<li>Susceptibility distortion correction: {sdc}</li>
\t\t\t<li>Coregistration Transform: {coregistration}</li>
\t\t\t<li>Denoising Method: {denoise_method}</li>
\t\t\t<li>Denoising Window: {denoise_window}</li>
\t\t\t<li>HMC Transform: {hmc_transform}</li>
\t\t\t<li>HMC Model: {hmc_model}</li>
\t\t\t<li>DWI series resampled to spaces: T1wACPC</li>
\t\t\t<li>Confounds collected: {confounds}</li>
\t\t\t<li>Impute slice threshold: {impute_slice_threshold}</li>
\t\t</ul>
{validation_reports}
"""

ABOUT_TEMPLATE = """\t<ul>
\t\t<li>QSIRecon version: {version}</li>
\t\t<li>QSIRecon command: <code>{command}</code></li>
\t\t<li>Date preprocessed: {date}</li>
\t\t<li>Date postprocessed: {date}</li>
\t</ul>
</div>
"""

GROUPING_TEMPLATE = """\t<ul>
\t\t<li>Output Name: {output_name}</li>
{input_files}
</ul>
"""

INTERACTIVE_TEMPLATE = """
<script src="https://unpkg.com/vue"></script>
<script src="https://nipreps.github.io/dmriprep-viewer/dmriprepReport.umd.min.js"></script>
Expand Down Expand Up @@ -128,11 +109,18 @@ def _run_interface(self, runtime):
class SubjectSummaryInputSpec(BaseInterfaceInputSpec):
t1w = InputMultiPath(File(exists=True), desc="T1w structural images")
t2w = InputMultiPath(File(exists=True), desc="T2w structural images")
subjects_dir = Directory(desc="FreeSurfer subjects directory")
subjects_dir = traits.Either(
Directory,
None,
desc="FreeSurfer subjects directory",
)
subject_id = Str(desc="Subject ID")
dwi_groupings = traits.Dict(desc="groupings of DWI files and their output names")
output_spaces = traits.List(desc="Target spaces")
template = traits.Enum("MNI152NLin2009cAsym", desc="Template space")
dwi = InputMultiObject(
traits.Either(File(exists=True), traits.List(File(exists=True))),
desc="Preprocessed DWI series",
)
std_spaces = traits.List(Str, desc="list of standard spaces")
nstd_spaces = traits.List(Str, desc="list of non-standard spaces")


class SubjectSummaryOutputSpec(SummaryOutputSpec):
Expand All @@ -148,49 +136,30 @@ class SubjectSummary(SummaryInterface):
def _run_interface(self, runtime):
if isdefined(self.inputs.subject_id):
self._results["subject_id"] = self.inputs.subject_id
return super(SubjectSummary, self)._run_interface(runtime)
return super()._run_interface(runtime)

def _generate_segment(self):
if not isdefined(self.inputs.subjects_dir):
freesurfer_status = "Not run"
else:
freesurfer_status = "Pre-existing directory"

t2w_seg = ""
if self.inputs.t2w:
t2w_seg = "(+ {:d} T2-weighted)".format(len(self.inputs.t2w))

# Add text for how the dwis are grouped
n_dwis = 0
n_outputs = 0
groupings = ""
if isdefined(self.inputs.dwi_groupings):
for output_fname, group_info in self.inputs.dwi_groupings.items():
n_outputs += 1
files_desc = []
files_desc.append(
"\t\t\t<li>Scan group: %s (PE Dir %s)</li><ul>"
% (output_fname, group_info["dwi_series_pedir"])
)
files_desc.append("\t\t\t\t<li>DWI Files: </li>")
for dwi_file in group_info["dwi_series"]:
files_desc.append("\t\t\t\t\t<li> %s </li>" % dwi_file)
n_dwis += 1
fieldmap_type = group_info["fieldmap_info"]["suffix"]
if fieldmap_type is not None:
files_desc.append("\t\t\t\t<li>Fieldmap type: %s </li>" % fieldmap_type)

for key, value in group_info["fieldmap_info"].items():
files_desc.append("\t\t\t\t\t<li> %s: %s </li>" % (key, str(value)))
n_dwis += 1
files_desc.append("</ul>")
groupings += GROUPING_TEMPLATE.format(
output_name=output_fname, input_files="\n".join(files_desc)
)
t2w_seg = f"(+ {len(self.inputs.t2w):d} T2-weighted)"

# Add list of tasks with number of runs
dwi_series = self.inputs.dwi if isdefined(self.inputs.dwi) else []
dwi_series = [s[0] if isinstance(s, list) else s for s in dwi_series]

return SUBJECT_TEMPLATE.format(
subject_id=self.inputs.subject_id,
n_t1s=len(self.inputs.t1w),
n_t1w=len(self.inputs.t1w),
t2w=t2w_seg,
n_dwis=n_dwis,
n_outputs=n_outputs,
groupings=groupings,
output_spaces="T1wACPC",
n_dwi=len(dwi_series),
std_spaces=", ".join(self.inputs.std_spaces),
nstd_spaces=", ".join(self.inputs.nstd_spaces),
freesurfer_status=freesurfer_status,
)


Expand Down
19 changes: 19 additions & 0 deletions qsirecon/interfaces/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,25 @@ def _run_interface(self, runtime):
return runtime


class _GetUniqueInputSpec(BaseInterfaceInputSpec):
inlist = traits.List(mandatory=True, desc="list of things")


class _GetUniqueOutputSpec(TraitedSpec):
outlist = traits.List()


class GetUnique(SimpleInterface):
input_spec = _GetUniqueInputSpec
output_spec = _GetUniqueOutputSpec

def _run_interface(self, runtime):
in_list = self.inputs.inlist
in_list = [x for x in in_list if isdefined(x)]
self._results["outlist"] = sorted(list(set(in_list)))
return runtime


def _resample_atlas(input_atlas, output_atlas, transform, ref_image):
xform = ants.ApplyTransforms(
transforms=[transform],
Expand Down
Loading