From 15e4d545f592dd6dc94d406c1ee6a51cee8d4073 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 09:35:43 -0400 Subject: [PATCH 01/19] Add "About" reportlet. --- qsirecon/interfaces/reports.py | 13 +----- qsirecon/workflows/base.py | 74 +++++++++++++++++++++++++--------- 2 files changed, 57 insertions(+), 30 deletions(-) diff --git a/qsirecon/interfaces/reports.py b/qsirecon/interfaces/reports.py index 5b97842b..12c36b6f 100644 --- a/qsirecon/interfaces/reports.py +++ b/qsirecon/interfaces/reports.py @@ -39,23 +39,14 @@ \t\t
  • Subject ID: {subject_id}
  • \t\t
  • Structural images: {n_t1s:d} T1-weighted {t2w}
  • \t\t
  • Diffusion-weighted series: inputs {n_dwis:d}, outputs {n_outputs:d}
  • -{groupings} -\t\t
  • Resampling targets: T1wACPC +\t\t
  • FreeSurfer input: {freesurfer}
  • +\t\t
  • Resampling targets: {output_spaces}
  • \t """ DIFFUSION_TEMPLATE = """\t\t

    Summary

    \t\t {validation_reports} """ diff --git a/qsirecon/workflows/base.py b/qsirecon/workflows/base.py index 52728da0..41c5f928 100644 --- a/qsirecon/workflows/base.py +++ b/qsirecon/workflows/base.py @@ -6,6 +6,7 @@ import json import os.path as op +import sys from copy import deepcopy from glob import glob @@ -16,6 +17,7 @@ from nipype import __version__ as nipype_ver from nipype.utils.filemanip import split_filename from niworkflows.engine.workflows import LiterateWorkflow as Workflow +from niworkflows.utils.misc import fix_multi_T1w_source_name from packaging.version import Version from pkg_resources import resource_filename as pkgrf @@ -76,6 +78,7 @@ def init_single_subject_recon_wf(subject_id): subject_id : str Single subject label """ + from ..interfaces.bids import DerivativesDataSink from ..interfaces.ingress import QsiReconDWIIngress, UKBioBankDWIIngress from ..interfaces.interchange import ( ReconWorkflowInputs, @@ -84,6 +87,7 @@ def init_single_subject_recon_wf(subject_id): recon_workflow_anatomical_input_fields, recon_workflow_input_fields, ) + from ..interfaces.reports import AboutSummary from .recon.anatomical import ( init_dwi_recon_anatomical_workflow, init_highres_recon_anatomical_wf, @@ -152,13 +156,14 @@ def init_single_subject_recon_wf(subject_id): # Get the preprocessed DWI and all the related preprocessed images if config.workflow.recon_input_pipeline == "qsiprep": dwi_ingress_nodes[dwi_file] = pe.Node( - QsiReconDWIIngress(dwi_file=dwi_file), name=wf_name + "_ingressed_dwi_data" + QsiReconDWIIngress(dwi_file=dwi_file), + name=f"{wf_name}_ingressed_dwi_data", ) elif config.workflow.recon_input_pipeline == "ukb": dwi_ingress_nodes[dwi_file] = pe.Node( UKBioBankDWIIngress(dwi_file=dwi_file, data_dir=str(dwi_input["path"].absolute())), - name=wf_name + "_ingressed_ukb_dwi_data", + name=f"{wf_name}_ingressed_ukb_dwi_data", ) anat_ingress_nodes[dwi_file], available_anatomical_data = ( init_highres_recon_anatomical_wf( @@ -167,7 +172,7 @@ def init_single_subject_recon_wf(subject_id): extras_to_make=spec.get("anatomical", []), pipeline_source="ukb", needs_t1w_transform=needs_t1w_transform, - name=wf_name + "_ingressed_ukb_anat_data", + name=f"{wf_name}_ingressed_ukb_anat_data", ) ) @@ -179,7 +184,7 @@ def init_single_subject_recon_wf(subject_id): prefer_dwi_mask=False, needs_t1w_transform=needs_t1w_transform, extras_to_make=spec.get("anatomical", []), - name=wf_name + "_dwi_specific_anat_wf", + name=f"{wf_name}_dwi_specific_anat_wf", **available_anatomical_data, ) ) @@ -187,42 +192,73 @@ def init_single_subject_recon_wf(subject_id): # This node holds all the inputs that will go to the recon workflow. # It is the definitive place to check what the input files are recon_full_inputs[dwi_file] = pe.Node( - ReconWorkflowInputs(), name=wf_name + "_recon_inputs" + ReconWorkflowInputs(), + name=f"{wf_name}_recon_inputs", ) # This is the actual recon workflow for this dwi file dwi_recon_wfs[dwi_file] = init_dwi_recon_workflow( available_anatomical_data=dwi_available_anatomical_data, workflow_spec=spec, - name=wf_name + "_recon_wf", + name=f"{wf_name}_recon_wf", ) # Connect the collected diffusion data (gradients, etc) to the inputnode workflow.connect([ # The dwi data (dwi_ingress_nodes[dwi_file], recon_full_inputs[dwi_file], [ - (trait, trait) for trait in qsiprep_output_names]), + (trait, trait) for trait in qsiprep_output_names + ]), # Session-specific anatomical data - (dwi_ingress_nodes[dwi_file], dwi_individual_anatomical_wfs[dwi_file], - [(trait, "inputnode." + trait) for trait in qsiprep_output_names]), + (dwi_ingress_nodes[dwi_file], dwi_individual_anatomical_wfs[dwi_file], [ + (trait, f"inputnode.{trait}") for trait in qsiprep_output_names + ]), # subject dwi-specific anatomical to a special node in recon_full_inputs so # we have a record of what went in. Otherwise it would be lost in an IdentityInterface - (dwi_individual_anatomical_wfs[dwi_file], recon_full_inputs[dwi_file], - [("outputnode." + trait, trait) for trait in recon_workflow_anatomical_input_fields]), + (dwi_individual_anatomical_wfs[dwi_file], recon_full_inputs[dwi_file], [ + (f"outputnode.{trait}", trait) for trait in recon_workflow_anatomical_input_fields + ]), # send the recon_full_inputs to the dwi recon workflow - (recon_full_inputs[dwi_file], dwi_recon_wfs[dwi_file], - [(trait, "inputnode." + trait) for trait in recon_workflow_input_fields]), - - (anat_ingress_node if config.workflow.recon_input_pipeline == "qsiprep" - else anat_ingress_nodes[dwi_file], - dwi_individual_anatomical_wfs[dwi_file], - [(f"outputnode.{trait}", f"inputnode.{trait}") - for trait in anatomical_workflow_outputs]) + (recon_full_inputs[dwi_file], dwi_recon_wfs[dwi_file], [ + (trait, f"inputnode.{trait}") for trait in recon_workflow_input_fields + ]), + + ( + anat_ingress_node if config.workflow.recon_input_pipeline == "qsiprep" + else anat_ingress_nodes[dwi_file], + dwi_individual_anatomical_wfs[dwi_file], + [ + (f"outputnode.{trait}", f"inputnode.{trait}") + for trait in anatomical_workflow_outputs + ] + ), ]) # fmt:skip + # Preprocessing of anatomical data (includes possible registration template) + about = pe.Node( + AboutSummary(version=config.environment.version, command=" ".join(sys.argv)), + name="about", + run_without_submitting=True, + ) + ds_report_about = pe.Node( + DerivativesDataSink( + base_directory=config.execution.output_dir, + datatype="figures", + suffix="about", + ), + name="ds_report_about", + run_without_submitting=True, + ) + workflow.connect([ + (anat_ingress_nodes[dwi_file], ds_report_about, [ + (('t1w_preproc', fix_multi_T1w_source_name), 'source_file'), + ]), + (about, ds_report_about, [('out_report', 'in_file')]), + ]) # fmt:skip + # Fill-in datasinks of reportlets seen so far for node in workflow.list_node_names(): if node.split(".")[-1].startswith("ds_report"): From 724d98f34b5e76ecb38fc321349245fbbff593d2 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 09:51:12 -0400 Subject: [PATCH 02/19] Update base.py --- qsirecon/workflows/base.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/qsirecon/workflows/base.py b/qsirecon/workflows/base.py index 41c5f928..c71fdfdb 100644 --- a/qsirecon/workflows/base.py +++ b/qsirecon/workflows/base.py @@ -203,6 +203,12 @@ def init_single_subject_recon_wf(subject_id): name=f"{wf_name}_recon_wf", ) + file_anat_ingress_node = ( + anat_ingress_node + if config.workflow.recon_input_pipeline == "qsiprep" + else anat_ingress_nodes[dwi_file] + ) + # Connect the collected diffusion data (gradients, etc) to the inputnode workflow.connect([ # The dwi data @@ -226,15 +232,10 @@ def init_single_subject_recon_wf(subject_id): (trait, f"inputnode.{trait}") for trait in recon_workflow_input_fields ]), - ( - anat_ingress_node if config.workflow.recon_input_pipeline == "qsiprep" - else anat_ingress_nodes[dwi_file], - dwi_individual_anatomical_wfs[dwi_file], - [ - (f"outputnode.{trait}", f"inputnode.{trait}") - for trait in anatomical_workflow_outputs - ] - ), + (file_anat_ingress_node, dwi_individual_anatomical_wfs[dwi_file], [ + (f"outputnode.{trait}", f"inputnode.{trait}") + for trait in anatomical_workflow_outputs + ]), ]) # fmt:skip # Preprocessing of anatomical data (includes possible registration template) @@ -253,7 +254,7 @@ def init_single_subject_recon_wf(subject_id): run_without_submitting=True, ) workflow.connect([ - (anat_ingress_nodes[dwi_file], ds_report_about, [ + (file_anat_ingress_node, ds_report_about, [ (('t1w_preproc', fix_multi_T1w_source_name), 'source_file'), ]), (about, ds_report_about, [('out_report', 'in_file')]), From 92e7df4372244fcd12e6ffdc704f60df831ae4fe Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 10:10:19 -0400 Subject: [PATCH 03/19] Update base.py --- qsirecon/workflows/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qsirecon/workflows/base.py b/qsirecon/workflows/base.py index c71fdfdb..b3c2b4af 100644 --- a/qsirecon/workflows/base.py +++ b/qsirecon/workflows/base.py @@ -255,7 +255,7 @@ def init_single_subject_recon_wf(subject_id): ) workflow.connect([ (file_anat_ingress_node, ds_report_about, [ - (('t1w_preproc', fix_multi_T1w_source_name), 'source_file'), + (('outputnode.t1w_preproc', fix_multi_T1w_source_name), 'source_file'), ]), (about, ds_report_about, [('out_report', 'in_file')]), ]) # fmt:skip From ee3ec0b011804ba7e8878da2442c782411aecced Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 10:26:59 -0400 Subject: [PATCH 04/19] Try fixing again. --- qsirecon/interfaces/reports.py | 73 ++++++++++++++-------------------- qsirecon/workflows/base.py | 10 +++-- 2 files changed, 36 insertions(+), 47 deletions(-) diff --git a/qsirecon/interfaces/reports.py b/qsirecon/interfaces/reports.py index 12c36b6f..dfbc88c3 100644 --- a/qsirecon/interfaces/reports.py +++ b/qsirecon/interfaces/reports.py @@ -23,6 +23,7 @@ CommandLineInputSpec, Directory, File, + InputMultiObject, InputMultiPath, SimpleInterface, Str, @@ -35,12 +36,14 @@ from .qc import createB0_ColorFA_Mask_Sprites, createSprite4D -SUBJECT_TEMPLATE = """\t
      +SUBJECT_TEMPLATE = """\ +\t
        \t\t
      • Subject ID: {subject_id}
      • -\t\t
      • Structural images: {n_t1s:d} T1-weighted {t2w}
      • -\t\t
      • Diffusion-weighted series: inputs {n_dwis:d}, outputs {n_outputs:d}
      • -\t\t
      • FreeSurfer input: {freesurfer}
      • -\t\t
      • Resampling targets: {output_spaces}
      • +\t\t
      • Structural images: {n_t1w:d} T1-weighted {t2w}
      • +\t\t
      • Diffusion-weighted series: {n_dwi:d}
      • +\t\t
      • Standard output spaces: {std_spaces}
      • +\t\t
      • Non-standard output spaces: {nstd_spaces}
      • +\t\t
      • FreeSurfer reconstruction: {freesurfer_status}
      • \t
      """ @@ -121,9 +124,12 @@ class SubjectSummaryInputSpec(BaseInterfaceInputSpec): t2w = InputMultiPath(File(exists=True), desc="T2w structural images") subjects_dir = Directory(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): @@ -139,49 +145,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
    • Scan group: %s (PE Dir %s)
      • " - % (output_fname, group_info["dwi_series_pedir"]) - ) - files_desc.append("\t\t\t\t
      • DWI Files:
      • ") - for dwi_file in group_info["dwi_series"]: - files_desc.append("\t\t\t\t\t
      • %s
      • " % 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
      • Fieldmap type: %s
      • " % fieldmap_type) - - for key, value in group_info["fieldmap_info"].items(): - files_desc.append("\t\t\t\t\t
      • %s: %s
      • " % (key, str(value))) - n_dwis += 1 - files_desc.append("
      ") - 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, ) diff --git a/qsirecon/workflows/base.py b/qsirecon/workflows/base.py index b3c2b4af..56402605 100644 --- a/qsirecon/workflows/base.py +++ b/qsirecon/workflows/base.py @@ -239,13 +239,18 @@ def init_single_subject_recon_wf(subject_id): ]) # fmt:skip # Preprocessing of anatomical data (includes possible registration template) + dwi_basename = fix_multi_T1w_source_name(dwi_recon_inputs) about = pe.Node( - AboutSummary(version=config.environment.version, command=" ".join(sys.argv)), + AboutSummary( + version=config.environment.version, + command=" ".join(sys.argv), + ), name="about", run_without_submitting=True, ) ds_report_about = pe.Node( DerivativesDataSink( + source_file=dwi_basename, base_directory=config.execution.output_dir, datatype="figures", suffix="about", @@ -254,9 +259,6 @@ def init_single_subject_recon_wf(subject_id): run_without_submitting=True, ) workflow.connect([ - (file_anat_ingress_node, ds_report_about, [ - (('outputnode.t1w_preproc', fix_multi_T1w_source_name), 'source_file'), - ]), (about, ds_report_about, [('out_report', 'in_file')]), ]) # fmt:skip From 5dba5e665e43e93c7962fa810797b57e249fb418 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 11:18:57 -0400 Subject: [PATCH 05/19] Add SubjectSummary. --- qsirecon/interfaces/utils.py | 19 ++++++++++++ qsirecon/workflows/base.py | 60 ++++++++++++++++++++++++++++-------- 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/qsirecon/interfaces/utils.py b/qsirecon/interfaces/utils.py index 308dd4a1..2fe37867 100644 --- a/qsirecon/interfaces/utils.py +++ b/qsirecon/interfaces/utils.py @@ -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], diff --git a/qsirecon/workflows/base.py b/qsirecon/workflows/base.py index 56402605..485c7e93 100644 --- a/qsirecon/workflows/base.py +++ b/qsirecon/workflows/base.py @@ -15,6 +15,7 @@ from dipy import __version__ as dipy_ver from nilearn import __version__ as nilearn_ver from nipype import __version__ as nipype_ver +from nipype.interfaces import utility as niu from nipype.utils.filemanip import split_filename from niworkflows.engine.workflows import LiterateWorkflow as Workflow from niworkflows.utils.misc import fix_multi_T1w_source_name @@ -87,7 +88,8 @@ def init_single_subject_recon_wf(subject_id): recon_workflow_anatomical_input_fields, recon_workflow_input_fields, ) - from ..interfaces.reports import AboutSummary + from ..interfaces.reports import AboutSummary, SubjectSummary + from ..interfaces.utils import GetUnique from .recon.anatomical import ( init_dwi_recon_anatomical_workflow, init_highres_recon_anatomical_wf, @@ -142,6 +144,11 @@ def init_single_subject_recon_wf(subject_id): "Anatomical (T1w) available for recon: %s", available_anatomical_data ) + aggregate_anatomical_nodes = pe.Node( + niu.Merge(len(dwi_recon_inputs)), + name="aggregate_anatomical_nodes", + ) + # create a processing pipeline for the dwis in each session dwi_recon_wfs = {} dwi_individual_anatomical_wfs = {} @@ -149,7 +156,7 @@ def init_single_subject_recon_wf(subject_id): dwi_ingress_nodes = {} anat_ingress_nodes = {} print(dwi_recon_inputs) - for dwi_input in dwi_recon_inputs: + for i_run, dwi_input in enumerate(dwi_recon_inputs): dwi_file = dwi_input["bids_dwi_file"] wf_name = _get_wf_name(dwi_file) @@ -159,6 +166,7 @@ def init_single_subject_recon_wf(subject_id): QsiReconDWIIngress(dwi_file=dwi_file), name=f"{wf_name}_ingressed_dwi_data", ) + anat_ingress_nodes[dwi_file] = anat_ingress_node elif config.workflow.recon_input_pipeline == "ukb": dwi_ingress_nodes[dwi_file] = pe.Node( @@ -176,6 +184,13 @@ def init_single_subject_recon_wf(subject_id): ) ) + # Aggregate the anatomical data from all the dwi files + workflow.connect([ + (anat_ingress_nodes[dwi_file], aggregate_anatomical_nodes, [ + ("outputnode.t1_preproc", f"in{i_run + 1}") + ]), + ]) # fmt:skip + # Create scan-specific anatomical data (mask, atlas configs, odf ROIs for reports) print(available_anatomical_data) dwi_individual_anatomical_wfs[dwi_file], dwi_available_anatomical_data = ( @@ -203,12 +218,6 @@ def init_single_subject_recon_wf(subject_id): name=f"{wf_name}_recon_wf", ) - file_anat_ingress_node = ( - anat_ingress_node - if config.workflow.recon_input_pipeline == "qsiprep" - else anat_ingress_nodes[dwi_file] - ) - # Connect the collected diffusion data (gradients, etc) to the inputnode workflow.connect([ # The dwi data @@ -232,7 +241,7 @@ def init_single_subject_recon_wf(subject_id): (trait, f"inputnode.{trait}") for trait in recon_workflow_input_fields ]), - (file_anat_ingress_node, dwi_individual_anatomical_wfs[dwi_file], [ + (anat_ingress_nodes[dwi_file], dwi_individual_anatomical_wfs[dwi_file], [ (f"outputnode.{trait}", f"inputnode.{trait}") for trait in anatomical_workflow_outputs ]), @@ -258,9 +267,36 @@ def init_single_subject_recon_wf(subject_id): name="ds_report_about", run_without_submitting=True, ) - workflow.connect([ - (about, ds_report_about, [('out_report', 'in_file')]), - ]) # fmt:skip + workflow.connect([(about, ds_report_about, [("out_report", "in_file")])]) + + reduce_t1_preproc = pe.Node( + GetUnique(), + name="reduce_t1_preproc", + ) + workflow.connect([(aggregate_anatomical_nodes, reduce_t1_preproc, [("out", "inlist")])]) + summary = pe.Node( + SubjectSummary( + subject_id=subject_id, + subjects_dir=config.execution.fs_subjects_dir, + std_spaces=["MNIInfant" if config.workflow.infant else "MNI152NLin2009cAsym"], + nstd_spaces=[], + dwi=dwi_recon_inputs, + ), + name="summary", + run_without_submitting=True, + ) + workflow.connect([(reduce_t1_preproc, summary, [("out", "t1w")])]) + ds_report_summary = pe.Node( + DerivativesDataSink( + source_file=dwi_basename, + base_directory=config.execution.output_dir, + datatype="figures", + suffix="summary", + ), + name="ds_report_summary", + run_without_submitting=True, + ) + workflow.connect([(summary, ds_report_summary, [("out_report", "in_file")])]) # Fill-in datasinks of reportlets seen so far for node in workflow.list_node_names(): From ee6ede03f99cfa3c39083fe6c00e3e5b9a0154e8 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 11:32:12 -0400 Subject: [PATCH 06/19] Update base.py --- qsirecon/workflows/base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qsirecon/workflows/base.py b/qsirecon/workflows/base.py index 485c7e93..7c800442 100644 --- a/qsirecon/workflows/base.py +++ b/qsirecon/workflows/base.py @@ -156,6 +156,7 @@ def init_single_subject_recon_wf(subject_id): dwi_ingress_nodes = {} anat_ingress_nodes = {} print(dwi_recon_inputs) + dwi_files = [dwi_input["bids_dwi_file"] for dwi_input in dwi_recon_inputs] for i_run, dwi_input in enumerate(dwi_recon_inputs): dwi_file = dwi_input["bids_dwi_file"] wf_name = _get_wf_name(dwi_file) @@ -248,7 +249,7 @@ def init_single_subject_recon_wf(subject_id): ]) # fmt:skip # Preprocessing of anatomical data (includes possible registration template) - dwi_basename = fix_multi_T1w_source_name(dwi_recon_inputs) + dwi_basename = fix_multi_T1w_source_name(dwi_files) about = pe.Node( AboutSummary( version=config.environment.version, @@ -280,7 +281,7 @@ def init_single_subject_recon_wf(subject_id): subjects_dir=config.execution.fs_subjects_dir, std_spaces=["MNIInfant" if config.workflow.infant else "MNI152NLin2009cAsym"], nstd_spaces=[], - dwi=dwi_recon_inputs, + dwi=dwi_files, ), name="summary", run_without_submitting=True, From 3e3f35e8694da59aa68c2ca9be0a97c7439950b5 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 11:42:54 -0400 Subject: [PATCH 07/19] Update reports.py --- qsirecon/interfaces/reports.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qsirecon/interfaces/reports.py b/qsirecon/interfaces/reports.py index dfbc88c3..a8efe4d4 100644 --- a/qsirecon/interfaces/reports.py +++ b/qsirecon/interfaces/reports.py @@ -122,7 +122,11 @@ 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 = InputMultiObject( traits.Either(File(exists=True), traits.List(File(exists=True))), From b58f18056925f09feafa13dd3429e0e89eff24a9 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 11:47:01 -0400 Subject: [PATCH 08/19] Set nireports version. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8682f17e..d9557018 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", From 33d7a1886757653f3566110f2bdbaf0882a0a66b Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 12:02:01 -0400 Subject: [PATCH 09/19] Update base.py --- qsirecon/workflows/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qsirecon/workflows/base.py b/qsirecon/workflows/base.py index 7c800442..fcdd1369 100644 --- a/qsirecon/workflows/base.py +++ b/qsirecon/workflows/base.py @@ -286,7 +286,7 @@ def init_single_subject_recon_wf(subject_id): name="summary", run_without_submitting=True, ) - workflow.connect([(reduce_t1_preproc, summary, [("out", "t1w")])]) + workflow.connect([(reduce_t1_preproc, summary, [("outlist", "t1w")])]) ds_report_summary = pe.Node( DerivativesDataSink( source_file=dwi_basename, From d9e8b9af006d2873cb026d39622d6b8aa69c6920 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 12:20:42 -0400 Subject: [PATCH 10/19] Update io_spec.json --- qsirecon/data/io_spec.json | 1 + 1 file changed, 1 insertion(+) diff --git a/qsirecon/data/io_spec.json b/qsirecon/data/io_spec.json index b4063143..8328183b 100644 --- a/qsirecon/data/io_spec.json +++ b/qsirecon/data/io_spec.json @@ -52,6 +52,7 @@ "sub-{subject}[/ses-{session}]/{datatype|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}.{extension|txt}", "sub-{subject}[/ses-{session}]/{datatype|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}.{extension|zip}", "sub-{subject}[/ses-{session}]/{datatype|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}.{extension|mat}", + "sub-{subject}/{datatype}/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}.{extension}", "sub-{subject}/{datatype}/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}.{extension}", "sub-{subject}/{datatype}/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}.{extension}", "sub-{subject}/{datatype}/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}.{extension}" From 9095ff2cdbbc87220c9f3a7a05475d47db0e33db Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 12:37:29 -0400 Subject: [PATCH 11/19] Whoops! --- qsirecon/data/io_spec.json | 1 - qsirecon/workflows/base.py | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/qsirecon/data/io_spec.json b/qsirecon/data/io_spec.json index 8328183b..b4063143 100644 --- a/qsirecon/data/io_spec.json +++ b/qsirecon/data/io_spec.json @@ -52,7 +52,6 @@ "sub-{subject}[/ses-{session}]/{datatype|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}.{extension|txt}", "sub-{subject}[/ses-{session}]/{datatype|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}.{extension|zip}", "sub-{subject}[/ses-{session}]/{datatype|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}.{extension|mat}", - "sub-{subject}/{datatype}/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}.{extension}", "sub-{subject}/{datatype}/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}.{extension}", "sub-{subject}/{datatype}/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}.{extension}", "sub-{subject}/{datatype}/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}.{extension}" diff --git a/qsirecon/workflows/base.py b/qsirecon/workflows/base.py index fcdd1369..a664f190 100644 --- a/qsirecon/workflows/base.py +++ b/qsirecon/workflows/base.py @@ -263,7 +263,8 @@ def init_single_subject_recon_wf(subject_id): source_file=dwi_basename, base_directory=config.execution.output_dir, datatype="figures", - suffix="about", + desc="about", + suffix="T1w", ), name="ds_report_about", run_without_submitting=True, @@ -292,7 +293,8 @@ def init_single_subject_recon_wf(subject_id): source_file=dwi_basename, base_directory=config.execution.output_dir, datatype="figures", - suffix="summary", + desc="summary", + suffix="T1w", ), name="ds_report_summary", run_without_submitting=True, From 775442cd1051a833648e96b8efca01d44b73a6fc Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 12:49:44 -0400 Subject: [PATCH 12/19] Update io_spec.json --- qsirecon/data/io_spec.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qsirecon/data/io_spec.json b/qsirecon/data/io_spec.json index b4063143..789ac39f 100644 --- a/qsirecon/data/io_spec.json +++ b/qsirecon/data/io_spec.json @@ -52,7 +52,7 @@ "sub-{subject}[/ses-{session}]/{datatype|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}.{extension|txt}", "sub-{subject}[/ses-{session}]/{datatype|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}.{extension|zip}", "sub-{subject}[/ses-{session}]/{datatype|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}.{extension|mat}", - "sub-{subject}/{datatype}/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}.{extension}", + "sub-{subject}/{datatype}/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}.{extension}", "sub-{subject}/{datatype}/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}.{extension}", "sub-{subject}/{datatype}/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}.{extension}" ] From 77d956497ce139d5472c9b7e30ab7692e749830c Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 14:26:06 -0400 Subject: [PATCH 13/19] Copy HTML figures. --- qsirecon/cli/run.py | 9 ++++++++- qsirecon/cli/workflow.py | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/qsirecon/cli/run.py b/qsirecon/cli/run.py index c192f234..7ede341a 100644 --- a/qsirecon/cli/run.py +++ b/qsirecon/cli/run.py @@ -172,7 +172,7 @@ def main(): from ..reports.core import generate_reports from ..workflows.base import _load_recon_spec - from .workflow import copy_boilerplate + from .workflow import copy_boilerplate, copy_reportlets # Generate reports phase session_list = config.execution.get().get("bids_filters", {}).get("dwi", {}).get("session") @@ -201,6 +201,13 @@ def main(): # Copy the boilerplate files copy_boilerplate(config.execution.output_dir, suffix_dir) + # Copy general reportlets + copy_reportlets( + config.execution.output_dir, + suffix_dir, + config.execution.participant_label, + ) + suffix_failed_reports = generate_reports( config.execution.participant_label, suffix_dir, diff --git a/qsirecon/cli/workflow.py b/qsirecon/cli/workflow.py index 5466a32e..457eeabe 100644 --- a/qsirecon/cli/workflow.py +++ b/qsirecon/cli/workflow.py @@ -239,3 +239,23 @@ def copy_boilerplate(in_dir, out_dir): for ext, citation_file in citation_files.items(): if citation_file.exists(): shutil.copy(citation_file, out_logs_path / f"CITATION.{ext}") + + +def copy_reportlets(in_dir, out_dir, participant_label): + import shutil + + in_path = Path(in_dir) + out_path = Path(out_dir) + + subject_htmls = (in_path / participant_label).glob("figures/*.html") + for subject_html in subject_htmls: + out_html = out_path / subject_html.relative_to(in_path) + out_html.parent.mkdir(exist_ok=True, parents=True) + shutil.copy(subject_html, out_html) + + # Session-wise reportlets + session_htmls = (in_path / participant_label).glob("ses-*/figures/*.html") + for session_html in session_htmls: + out_html = out_path / session_html.relative_to(in_path) + out_html.parent.mkdir(exist_ok=True, parents=True) + shutil.copy(session_html, out_html) From d087179c3d57eeca29cd8afff85853662647f11f Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 14:57:03 -0400 Subject: [PATCH 14/19] Fix! --- qsirecon/cli/workflow.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/qsirecon/cli/workflow.py b/qsirecon/cli/workflow.py index 457eeabe..60b9246c 100644 --- a/qsirecon/cli/workflow.py +++ b/qsirecon/cli/workflow.py @@ -241,21 +241,22 @@ def copy_boilerplate(in_dir, out_dir): shutil.copy(citation_file, out_logs_path / f"CITATION.{ext}") -def copy_reportlets(in_dir, out_dir, participant_label): +def copy_reportlets(in_dir, out_dir, participant_labels): import shutil in_path = Path(in_dir) out_path = Path(out_dir) - subject_htmls = (in_path / participant_label).glob("figures/*.html") - for subject_html in subject_htmls: - out_html = out_path / subject_html.relative_to(in_path) - out_html.parent.mkdir(exist_ok=True, parents=True) - shutil.copy(subject_html, out_html) - - # Session-wise reportlets - session_htmls = (in_path / participant_label).glob("ses-*/figures/*.html") - for session_html in session_htmls: - out_html = out_path / session_html.relative_to(in_path) - out_html.parent.mkdir(exist_ok=True, parents=True) - shutil.copy(session_html, out_html) + for subject_id in participant_labels: + subject_htmls = (in_path / subject_id).glob("figures/*.html") + for subject_html in subject_htmls: + out_html = out_path / subject_html.relative_to(in_path) + out_html.parent.mkdir(exist_ok=True, parents=True) + shutil.copy(subject_html, out_html) + + # Session-wise reportlets + session_htmls = (in_path / subject_id).glob("ses-*/figures/*.html") + for session_html in session_htmls: + out_html = out_path / session_html.relative_to(in_path) + out_html.parent.mkdir(exist_ok=True, parents=True) + shutil.copy(session_html, out_html) From ae671070b837c325b30bc2119923716c44f45dbd Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 15:18:49 -0400 Subject: [PATCH 15/19] FIX!!! --- qsirecon/cli/workflow.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/qsirecon/cli/workflow.py b/qsirecon/cli/workflow.py index 60b9246c..937ac562 100644 --- a/qsirecon/cli/workflow.py +++ b/qsirecon/cli/workflow.py @@ -247,7 +247,16 @@ def copy_reportlets(in_dir, out_dir, participant_labels): in_path = Path(in_dir) out_path = Path(out_dir) + if not participant_labels: + from bids.layout import BIDSLayout + + layout = BIDSLayout(out_dir, config="figures", validate=False) + participant_labels = layout.get_subjects() + for subject_id in participant_labels: + if not subject_id.startswith("sub-"): + subject_id = f"sub-{subject_id}" + subject_htmls = (in_path / subject_id).glob("figures/*.html") for subject_html in subject_htmls: out_html = out_path / subject_html.relative_to(in_path) From a42ef41b8bb4133cad1c05b23513be6626d0a960 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 15:37:11 -0400 Subject: [PATCH 16/19] Remove unused elements. --- qsirecon/interfaces/reports.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/qsirecon/interfaces/reports.py b/qsirecon/interfaces/reports.py index a8efe4d4..ee498e8c 100644 --- a/qsirecon/interfaces/reports.py +++ b/qsirecon/interfaces/reports.py @@ -47,27 +47,14 @@ \t
    """ -DIFFUSION_TEMPLATE = """\t\t

    Summary

    -\t\t
      -\t\t\t
    • Phase-encoding (PE) direction: {pedir}
    • -\t\t
    -{validation_reports} -""" - ABOUT_TEMPLATE = """\t
      \t\t
    • QSIRecon version: {version}
    • \t\t
    • QSIRecon command: {command}
    • -\t\t
    • Date preprocessed: {date}
    • +\t\t
    • Date postprocessed: {date}
    • \t
    """ -GROUPING_TEMPLATE = """\t
      -\t\t
    • Output Name: {output_name}
    • -{input_files} -
    -""" - INTERACTIVE_TEMPLATE = """ From 14849641f9100feeaebd8f21d4f793b6aafa5fef Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 16:07:00 -0400 Subject: [PATCH 17/19] Try using MapNodes. --- qsirecon/cli/run.py | 19 ++----------- qsirecon/cli/workflow.py | 30 --------------------- qsirecon/config.py | 13 +++++++++ qsirecon/workflows/base.py | 55 ++++++++++++++++++++++---------------- 4 files changed, 47 insertions(+), 70 deletions(-) diff --git a/qsirecon/cli/run.py b/qsirecon/cli/run.py index 7ede341a..892a747f 100644 --- a/qsirecon/cli/run.py +++ b/qsirecon/cli/run.py @@ -171,8 +171,7 @@ def main(): finally: from ..reports.core import generate_reports - from ..workflows.base import _load_recon_spec - from .workflow import copy_boilerplate, copy_reportlets + from .workflow import copy_boilerplate # Generate reports phase session_list = config.execution.get().get("bids_filters", {}).get("dwi", {}).get("session") @@ -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: @@ -201,13 +193,6 @@ def main(): # Copy the boilerplate files copy_boilerplate(config.execution.output_dir, suffix_dir) - # Copy general reportlets - copy_reportlets( - config.execution.output_dir, - suffix_dir, - config.execution.participant_label, - ) - suffix_failed_reports = generate_reports( config.execution.participant_label, suffix_dir, diff --git a/qsirecon/cli/workflow.py b/qsirecon/cli/workflow.py index 937ac562..5466a32e 100644 --- a/qsirecon/cli/workflow.py +++ b/qsirecon/cli/workflow.py @@ -239,33 +239,3 @@ def copy_boilerplate(in_dir, out_dir): for ext, citation_file in citation_files.items(): if citation_file.exists(): shutil.copy(citation_file, out_logs_path / f"CITATION.{ext}") - - -def copy_reportlets(in_dir, out_dir, participant_labels): - import shutil - - in_path = Path(in_dir) - out_path = Path(out_dir) - - if not participant_labels: - from bids.layout import BIDSLayout - - layout = BIDSLayout(out_dir, config="figures", validate=False) - participant_labels = layout.get_subjects() - - for subject_id in participant_labels: - if not subject_id.startswith("sub-"): - subject_id = f"sub-{subject_id}" - - subject_htmls = (in_path / subject_id).glob("figures/*.html") - for subject_html in subject_htmls: - out_html = out_path / subject_html.relative_to(in_path) - out_html.parent.mkdir(exist_ok=True, parents=True) - shutil.copy(subject_html, out_html) - - # Session-wise reportlets - session_htmls = (in_path / subject_id).glob("ses-*/figures/*.html") - for session_html in session_htmls: - out_html = out_path / session_html.relative_to(in_path) - out_html.parent.mkdir(exist_ok=True, parents=True) - shutil.copy(session_html, out_html) diff --git a/qsirecon/config.py b/qsirecon/config.py index ca575c43..5b62471f 100644 --- a/qsirecon/config.py +++ b/qsirecon/config.py @@ -553,6 +553,19 @@ class workflow(_Config): """Recon workflow specification.""" output_resolution = None """Isotropic voxel size for outputs.""" + qsirecon_suffixes = [] + """List of reconstruction workflow names, derived from the recon_spec.""" + + 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: diff --git a/qsirecon/workflows/base.py b/qsirecon/workflows/base.py index 592c6d22..5faf34bf 100644 --- a/qsirecon/workflows/base.py +++ b/qsirecon/workflows/base.py @@ -96,7 +96,7 @@ def init_single_subject_recon_wf(subject_id): ) from .recon.build_workflow import init_dwi_recon_workflow - spec = _load_recon_spec() + spec = _load_recon_spec(config.workflow.recon_spec) dwi_recon_inputs = _get_iterable_dwi_inputs(subject_id) workflow = Workflow(name=f"sub-{subject_id}_{spec['name']}") @@ -250,6 +250,13 @@ def init_single_subject_recon_wf(subject_id): # Preprocessing of anatomical data (includes possible registration template) dwi_basename = fix_multi_T1w_source_name(dwi_files) + + reduce_t1_preproc = pe.Node( + GetUnique(), + name="reduce_t1_preproc", + ) + workflow.connect([(aggregate_anatomical_nodes, reduce_t1_preproc, [("out", "inlist")])]) + about = pe.Node( AboutSummary( version=config.environment.version, @@ -258,24 +265,6 @@ def init_single_subject_recon_wf(subject_id): name="about", run_without_submitting=True, ) - ds_report_about = pe.Node( - DerivativesDataSink( - source_file=dwi_basename, - base_directory=config.execution.output_dir, - datatype="figures", - desc="about", - suffix="T1w", - ), - name="ds_report_about", - run_without_submitting=True, - ) - workflow.connect([(about, ds_report_about, [("out_report", "in_file")])]) - - reduce_t1_preproc = pe.Node( - GetUnique(), - name="reduce_t1_preproc", - ) - workflow.connect([(aggregate_anatomical_nodes, reduce_t1_preproc, [("out", "inlist")])]) summary = pe.Node( SubjectSummary( subject_id=subject_id, @@ -288,17 +277,38 @@ def init_single_subject_recon_wf(subject_id): run_without_submitting=True, ) workflow.connect([(reduce_t1_preproc, summary, [("outlist", "t1w")])]) - ds_report_summary = pe.Node( + + suffix_dirs = [] + for qsirecon_suffix in config.workflow.qsirecon_suffixes: + suffix_dir = str(config.execution.output_dir / f"qsirecon-{qsirecon_suffix}") + suffix_dirs.append(suffix_dir) + + ds_report_about = pe.MapNode( + DerivativesDataSink( + source_file=dwi_basename, + datatype="figures", + desc="about", + suffix="T1w", + ), + name="ds_report_about", + run_without_submitting=True, + iterfield=["base_directory"], + ) + ds_report_about.inputs.base_directory = suffix_dirs + workflow.connect([(about, ds_report_about, [("out_report", "in_file")])]) + + ds_report_summary = pe.MapNode( DerivativesDataSink( source_file=dwi_basename, - base_directory=config.execution.output_dir, datatype="figures", desc="summary", suffix="T1w", ), name="ds_report_summary", run_without_submitting=True, + iterfield=["base_directory"], ) + ds_report_summary.inputs.base_directory = suffix_dirs workflow.connect([(summary, ds_report_summary, [("out_report", "in_file")])]) # Fill-in datasinks of reportlets seen so far @@ -321,10 +331,9 @@ def _get_wf_name(dwi_file): return "_".join(tokens[:-1]).replace("-", "_") -def _load_recon_spec(): +def _load_recon_spec(spec_name): from ..utils.sloppy_recon import make_sloppy - spec_name = config.workflow.recon_spec prepackaged_dir = pkgrf("qsirecon", "data/pipelines") prepackaged = [op.split(fname)[1][:-5] for fname in glob(prepackaged_dir + "/*.json")] if op.exists(spec_name): From 2c340492cc8cb58d12d0cfd49b476040ea20edee Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 16:17:24 -0400 Subject: [PATCH 18/19] Update config.py --- qsirecon/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qsirecon/config.py b/qsirecon/config.py index 5b62471f..1b491a18 100644 --- a/qsirecon/config.py +++ b/qsirecon/config.py @@ -556,6 +556,7 @@ class workflow(_Config): qsirecon_suffixes = [] """List of reconstruction workflow names, derived from the recon_spec.""" + @classmethod def init(cls): from .workflows.base import _load_recon_spec From bcd8bcd2d7d576d7bd0c15e90c5f21c96eed090f Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 17:07:26 -0400 Subject: [PATCH 19/19] Update base.py --- qsirecon/workflows/base.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/qsirecon/workflows/base.py b/qsirecon/workflows/base.py index 5faf34bf..ab73cf48 100644 --- a/qsirecon/workflows/base.py +++ b/qsirecon/workflows/base.py @@ -66,6 +66,11 @@ def init_qsirecon_wf(): log_dir.mkdir(exist_ok=True, parents=True) config.to_filename(log_dir / "qsirecon.toml") + # Dump a copy of the recon spec into the log directory as well + recon_spec = _load_recon_spec(config.workflow.recon_spec) + with open(log_dir / "recon_spec.json", "w") as f: + json.dump(recon_spec, f, indent=2, sort_keys=True) + return qsirecon_wf @@ -342,6 +347,7 @@ def _load_recon_spec(spec_name): recon_spec = op.join(prepackaged_dir + "/{}.json".format(spec_name)) else: raise Exception("{} is not a file that exists or in {}".format(spec_name, prepackaged)) + with open(recon_spec, "r") as f: try: spec = json.load(f) @@ -350,6 +356,7 @@ def _load_recon_spec(spec_name): if config.execution.sloppy: config.loggers.workflow.warning("Forcing reconstruction to use unrealistic parameters") spec = make_sloppy(spec) + return spec