From d03d28262b0c62d1c7099da6682cbcbe690d6529 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 29 Jan 2024 09:59:28 -0500 Subject: [PATCH] Support MCRIBS derivatives (#1029) --- xcp_d/__main__.py | 1 + xcp_d/cli/parser_utils.py | 1 + xcp_d/ingression/__init__.py | 1 + xcp_d/ingression/ukbiobank.py | 1 + xcp_d/interfaces/ants.py | 1 + xcp_d/interfaces/bids.py | 157 +++++++++++++++++++ xcp_d/interfaces/censoring.py | 7 +- xcp_d/interfaces/concatenation.py | 1 + xcp_d/interfaces/nilearn.py | 1 + xcp_d/interfaces/utils.py | 1 + xcp_d/interfaces/workbench.py | 1 + xcp_d/tests/conftest.py | 1 + xcp_d/tests/test_TR.py | 1 + xcp_d/tests/test_cli.py | 1 + xcp_d/tests/test_cli_run.py | 1 + xcp_d/tests/test_cli_utils.py | 1 + xcp_d/tests/test_confounds.py | 1 + xcp_d/tests/test_despike.py | 1 + xcp_d/tests/test_filtering.py | 1 + xcp_d/tests/test_interfaces_censoring.py | 1 + xcp_d/tests/test_interfaces_concatenation.py | 1 + xcp_d/tests/test_interfaces_nilearn.py | 1 + xcp_d/tests/test_interfaces_utils.py | 1 + xcp_d/tests/test_smoothing.py | 1 + xcp_d/tests/test_utils_atlas.py | 1 + xcp_d/tests/test_utils_bids.py | 31 ++-- xcp_d/tests/test_utils_concatenation.py | 1 + xcp_d/tests/test_utils_doc.py | 1 + xcp_d/tests/test_utils_execsummary.py | 1 + xcp_d/tests/test_utils_plotting.py | 1 + xcp_d/tests/test_utils_qcmetrics.py | 1 + xcp_d/tests/test_utils_utils.py | 1 + xcp_d/tests/test_utils_write_save.py | 1 + xcp_d/tests/test_workflows_anatomical.py | 1 + xcp_d/tests/test_workflows_connectivity.py | 1 + xcp_d/tests/test_workflows_execsummary.py | 1 + xcp_d/tests/test_workflows_restingstate.py | 1 + xcp_d/tests/utils.py | 1 + xcp_d/utils/bids.py | 116 ++++++-------- xcp_d/utils/qcmetrics.py | 1 + xcp_d/utils/utils.py | 6 +- xcp_d/workflows/anatomical.py | 134 ++++++---------- xcp_d/workflows/concatenation.py | 1 + xcp_d/workflows/plotting.py | 1 + 44 files changed, 309 insertions(+), 180 deletions(-) diff --git a/xcp_d/__main__.py b/xcp_d/__main__.py index 383fe3109..396b7a298 100755 --- a/xcp_d/__main__.py +++ b/xcp_d/__main__.py @@ -1,4 +1,5 @@ """A method for calling the command-line interface.""" + from xcp_d.cli.run import main if __name__ == "__main__": diff --git a/xcp_d/cli/parser_utils.py b/xcp_d/cli/parser_utils.py index b7d0abe65..a30fdacfd 100644 --- a/xcp_d/cli/parser_utils.py +++ b/xcp_d/cli/parser_utils.py @@ -1,4 +1,5 @@ """Utility functions for xcp_d command-line interfaces.""" + import argparse import json import logging diff --git a/xcp_d/ingression/__init__.py b/xcp_d/ingression/__init__.py index 12c499679..5cd7f1b3e 100644 --- a/xcp_d/ingression/__init__.py +++ b/xcp_d/ingression/__init__.py @@ -1,4 +1,5 @@ """Tools for converting derivatives from various pipelines to an fMRIPrep-like format.""" + from xcp_d.ingression import abcdbids, hcpya, ukbiobank, utils __all__ = [ diff --git a/xcp_d/ingression/ukbiobank.py b/xcp_d/ingression/ukbiobank.py index 5f6890738..4f0d49130 100644 --- a/xcp_d/ingression/ukbiobank.py +++ b/xcp_d/ingression/ukbiobank.py @@ -1,4 +1,5 @@ """Functions to convert preprocessed UK Biobank BOLD data to BIDS derivatives format.""" + import glob import json import os diff --git a/xcp_d/interfaces/ants.py b/xcp_d/interfaces/ants.py index de00737c9..88f1c03b5 100644 --- a/xcp_d/interfaces/ants.py +++ b/xcp_d/interfaces/ants.py @@ -1,4 +1,5 @@ """ANTS interfaces.""" + import logging import os diff --git a/xcp_d/interfaces/bids.py b/xcp_d/interfaces/bids.py index 95b5ae7ef..06e1c18d9 100644 --- a/xcp_d/interfaces/bids.py +++ b/xcp_d/interfaces/bids.py @@ -1,9 +1,18 @@ """Adapted interfaces from Niworkflows.""" + from json import loads from pathlib import Path from bids.layout import Config from nipype import logging +from nipype.interfaces.base import ( + BaseInterfaceInputSpec, + Directory, + File, + SimpleInterface, + TraitedSpec, + traits, +) from niworkflows.interfaces.bids import DerivativesDataSink as BaseDerivativesDataSink from pkg_resources import resource_filename as pkgrf @@ -33,3 +42,151 @@ class DerivativesDataSink(BaseDerivativesDataSink): _config_entities = config_entities _config_entities_dict = merged_entities _file_patterns = xcp_d_spec["default_path_patterns"] + + +class _CollectRegistrationFilesInputSpec(BaseInterfaceInputSpec): + segmentation_dir = Directory( + exists=True, + required=True, + desc="Path to FreeSurfer or MCRIBS derivatives.", + ) + software = traits.Enum( + "FreeSurfer", + "MCRIBS", + required=True, + desc="The software used for segmentation.", + ) + hemisphere = traits.Enum( + "L", + "R", + required=True, + desc="The hemisphere being used.", + ) + participant_id = traits.Str( + required=True, + desc="Participant ID. Used to select the subdirectory of the FreeSurfer derivatives.", + ) + + +class _CollectRegistrationFilesOutputSpec(TraitedSpec): + subject_sphere = File( + exists=True, + desc="Subject-space sphere.", + ) + source_sphere = File( + exists=True, + desc="Source-space sphere (namely, fsaverage).", + ) + target_sphere = File( + exists=True, + desc="Target-space sphere (fsLR for FreeSurfer, dHCP-in-fsLR for MCRIBS).", + ) + sphere_to_sphere = File( + exists=True, + desc="Warp file going from source space to target space.", + ) + + +class CollectRegistrationFiles(SimpleInterface): + """Collect registration files for fsnative-to-fsLR transformation.""" + + input_spec = _CollectRegistrationFilesInputSpec + output_spec = _CollectRegistrationFilesOutputSpec + + def _run_interface(self, runtime): + import os + + from pkg_resources import resource_filename as pkgrf + from templateflow.api import get as get_template + + hemisphere = self.inputs.hemisphere + hstr = f"{hemisphere.lower()}h" + participant_id = self.inputs.participant_id + if not participant_id.startswith("sub-"): + participant_id = f"sub-{participant_id}" + + if self.inputs.software == "FreeSurfer": + # Find the subject's sphere in the FreeSurfer derivatives. + # TODO: Collect from the preprocessing derivatives if they're a compliant version. + # Namely, fMRIPrep >= 23.1.2, Nibabies >= 24.0.0a1. + self._results["subject_sphere"] = os.path.join( + self.inputs.segmentation_dir, + participant_id, + "surf", + f"{hstr}.sphere.reg", + ) + + # Load the fsaverage-164k sphere + # FreeSurfer: tpl-fsaverage_hemi-?_den-164k_sphere.surf.gii + self._results["source_sphere"] = str( + get_template( + template="fsaverage", + space=None, + hemi=hemisphere, + density="164k", + desc=None, + suffix="sphere", + ) + ) + + # TODO: Collect from templateflow once it's uploaded. + # FreeSurfer: fs_?/fs_?-to-fs_LR_fsaverage.?_LR.spherical_std.164k_fs_?.surf.gii + self._results["sphere_to_sphere"] = pkgrf( + "xcp_d", + ( + f"data/standard_mesh_atlases/fs_{hemisphere}/" + f"fs_{hemisphere}-to-fs_LR_fsaverage.{hemisphere}_LR.spherical_std." + f"164k_fs_{hemisphere}.surf.gii" + ), + ) + + # FreeSurfer: tpl-fsLR_hemi-?_den-32k_sphere.surf.gii + self._results["target_sphere"] = str( + get_template( + template="fsLR", + space=None, + hemi=hemisphere, + density="32k", + desc=None, + suffix="sphere", + ) + ) + + elif self.inputs.software == "MCRIBS": + # Find the subject's sphere in the MCRIBS derivatives. + # TODO: Collect from the preprocessing derivatives if they're a compliant version. + # Namely, fMRIPrep >= 23.1.2, Nibabies >= 24.0.0a1. + self._results["subject_sphere"] = os.path.join( + self.inputs.segmentation_dir, + participant_id, + "freesurfer", + participant_id, + "surf", + f"{hstr}.sphere.reg2", + ) + + # TODO: Collect from templateflow once it's uploaded. + # MCRIBS: tpl-fsaverage_hemi-?_den-41k_desc-reg_sphere.surf.gii + self._results["source_sphere"] = os.path.join( + self.inputs.segmentation_dir, + "templates_fsLR", + f"tpl-fsaverage_hemi-{hemisphere}_den-41k_desc-reg_sphere.surf.gii", + ) + + # TODO: Collect from templateflow once it's uploaded. + # MCRIBS: tpl-dHCP_space-fsaverage_hemi-?_den-41k_desc-reg_sphere.surf.gii + self._results["sphere_to_sphere"] = os.path.join( + self.inputs.segmentation_dir, + "templates_fsLR", + f"tpl-dHCP_space-fsaverage_hemi-{hemisphere}_den-41k_desc-reg_sphere.surf.gii", + ) + + # TODO: Collect from templateflow once it's uploaded. + # MCRIBS: tpl-dHCP_space-fsLR_hemi-?_den-32k_desc-week42_sphere.surf.gii + self._results["target_sphere"] = os.path.join( + self.inputs.segmentation_dir, + "templates_fsLR", + f"tpl-dHCP_space-fsLR_hemi-{hemisphere}_den-32k_desc-week42_sphere.surf.gii", + ) + + return runtime diff --git a/xcp_d/interfaces/censoring.py b/xcp_d/interfaces/censoring.py index 929b77a3b..a4ad519fa 100644 --- a/xcp_d/interfaces/censoring.py +++ b/xcp_d/interfaces/censoring.py @@ -1,4 +1,5 @@ """Interfaces for the post-processing workflows.""" + import os import nibabel as nb @@ -104,9 +105,9 @@ def _run_interface(self, runtime): if dummy_scans == 0: # write the output out self._results["bold_file_dropped_TR"] = self.inputs.bold_file - self._results[ - "fmriprep_confounds_file_dropped_TR" - ] = self.inputs.fmriprep_confounds_file + self._results["fmriprep_confounds_file_dropped_TR"] = ( + self.inputs.fmriprep_confounds_file + ) self._results["confounds_file_dropped_TR"] = self.inputs.confounds_file self._results["motion_file_dropped_TR"] = self.inputs.motion_file self._results["temporal_mask_dropped_TR"] = self.inputs.temporal_mask diff --git a/xcp_d/interfaces/concatenation.py b/xcp_d/interfaces/concatenation.py index 4e00a4ca8..41e204750 100644 --- a/xcp_d/interfaces/concatenation.py +++ b/xcp_d/interfaces/concatenation.py @@ -1,4 +1,5 @@ """Interfaces for the concatenation workflow.""" + import itertools import os import re diff --git a/xcp_d/interfaces/nilearn.py b/xcp_d/interfaces/nilearn.py index 81951551d..532a4ac48 100644 --- a/xcp_d/interfaces/nilearn.py +++ b/xcp_d/interfaces/nilearn.py @@ -1,4 +1,5 @@ """Interfaces for Nilearn code.""" + import os from nilearn import maskers diff --git a/xcp_d/interfaces/utils.py b/xcp_d/interfaces/utils.py index 56a5be733..86c7821f8 100644 --- a/xcp_d/interfaces/utils.py +++ b/xcp_d/interfaces/utils.py @@ -1,4 +1,5 @@ """Miscellaneous utility interfaces.""" + from nipype import logging from nipype.interfaces.base import ( BaseInterfaceInputSpec, diff --git a/xcp_d/interfaces/workbench.py b/xcp_d/interfaces/workbench.py index f809db9c9..b28116cf2 100644 --- a/xcp_d/interfaces/workbench.py +++ b/xcp_d/interfaces/workbench.py @@ -1,4 +1,5 @@ """Custom wb_command interfaces.""" + import os import nibabel as nb diff --git a/xcp_d/tests/conftest.py b/xcp_d/tests/conftest.py index 18786370f..20ea1ab11 100644 --- a/xcp_d/tests/conftest.py +++ b/xcp_d/tests/conftest.py @@ -1,4 +1,5 @@ """Fixtures for the CircleCI tests.""" + import base64 import os diff --git a/xcp_d/tests/test_TR.py b/xcp_d/tests/test_TR.py index c2a6c8317..858b9651b 100644 --- a/xcp_d/tests/test_TR.py +++ b/xcp_d/tests/test_TR.py @@ -5,6 +5,7 @@ Arguments have to be passed to these functions because the data may be mounted in a container somewhere unintuitively. """ + import os.path as op import nibabel as nb diff --git a/xcp_d/tests/test_cli.py b/xcp_d/tests/test_cli.py index 1e7569b26..4880657fd 100644 --- a/xcp_d/tests/test_cli.py +++ b/xcp_d/tests/test_cli.py @@ -1,4 +1,5 @@ """Command-line interface tests.""" + import os import shutil diff --git a/xcp_d/tests/test_cli_run.py b/xcp_d/tests/test_cli_run.py index a9a67dcfa..31ca80dde 100644 --- a/xcp_d/tests/test_cli_run.py +++ b/xcp_d/tests/test_cli_run.py @@ -1,4 +1,5 @@ """Tests for functions in the cli.run module.""" + import logging import os from copy import deepcopy diff --git a/xcp_d/tests/test_cli_utils.py b/xcp_d/tests/test_cli_utils.py index d8d584d33..3bc5982da 100644 --- a/xcp_d/tests/test_cli_utils.py +++ b/xcp_d/tests/test_cli_utils.py @@ -1,4 +1,5 @@ """Tests for the xcp_d.cli.parser_utils module.""" + from argparse import ArgumentTypeError import pytest diff --git a/xcp_d/tests/test_confounds.py b/xcp_d/tests/test_confounds.py index 22a71765f..bb8306b4c 100644 --- a/xcp_d/tests/test_confounds.py +++ b/xcp_d/tests/test_confounds.py @@ -1,4 +1,5 @@ """Test confounds handling.""" + import os import numpy as np diff --git a/xcp_d/tests/test_despike.py b/xcp_d/tests/test_despike.py index ee39e21c2..7dfe38e3b 100644 --- a/xcp_d/tests/test_despike.py +++ b/xcp_d/tests/test_despike.py @@ -6,6 +6,7 @@ Arguments have to be passed to these functions because the data may be mounted in a container somewhere unintuitively. """ + import os import nibabel as nb diff --git a/xcp_d/tests/test_filtering.py b/xcp_d/tests/test_filtering.py index 18766b2cb..ee42ab8c9 100644 --- a/xcp_d/tests/test_filtering.py +++ b/xcp_d/tests/test_filtering.py @@ -1,4 +1,5 @@ """Tests for filtering methods.""" + import re import numpy as np diff --git a/xcp_d/tests/test_interfaces_censoring.py b/xcp_d/tests/test_interfaces_censoring.py index 98cab51b7..f5fc3be1f 100644 --- a/xcp_d/tests/test_interfaces_censoring.py +++ b/xcp_d/tests/test_interfaces_censoring.py @@ -1,4 +1,5 @@ """Tests for framewise displacement calculation.""" + import json import os diff --git a/xcp_d/tests/test_interfaces_concatenation.py b/xcp_d/tests/test_interfaces_concatenation.py index 1ec4953ac..5c1402acb 100644 --- a/xcp_d/tests/test_interfaces_concatenation.py +++ b/xcp_d/tests/test_interfaces_concatenation.py @@ -1,4 +1,5 @@ """Tests for the xcp_d.interfaces.concatenation module.""" + import os from nipype.interfaces.base import Undefined, isdefined diff --git a/xcp_d/tests/test_interfaces_nilearn.py b/xcp_d/tests/test_interfaces_nilearn.py index 6e4e90f50..4283b7a07 100644 --- a/xcp_d/tests/test_interfaces_nilearn.py +++ b/xcp_d/tests/test_interfaces_nilearn.py @@ -1,4 +1,5 @@ """Tests for the xcp_d.interfaces.nilearn module.""" + import os import nibabel as nb diff --git a/xcp_d/tests/test_interfaces_utils.py b/xcp_d/tests/test_interfaces_utils.py index 62601c926..c2dba02bb 100644 --- a/xcp_d/tests/test_interfaces_utils.py +++ b/xcp_d/tests/test_interfaces_utils.py @@ -1,4 +1,5 @@ """Tests for xcp_d.interfaces.utils module.""" + import os import nibabel as nb diff --git a/xcp_d/tests/test_smoothing.py b/xcp_d/tests/test_smoothing.py index 8fe68e16d..b68efe7e9 100644 --- a/xcp_d/tests/test_smoothing.py +++ b/xcp_d/tests/test_smoothing.py @@ -1,4 +1,5 @@ """Tests for smoothing methods.""" + import os import re import tempfile diff --git a/xcp_d/tests/test_utils_atlas.py b/xcp_d/tests/test_utils_atlas.py index c6ffeab58..32a252711 100644 --- a/xcp_d/tests/test_utils_atlas.py +++ b/xcp_d/tests/test_utils_atlas.py @@ -1,4 +1,5 @@ """Tests for the xcp_d.utils.atlas module.""" + import os import pytest diff --git a/xcp_d/tests/test_utils_bids.py b/xcp_d/tests/test_utils_bids.py index d8ef80530..21089caf7 100644 --- a/xcp_d/tests/test_utils_bids.py +++ b/xcp_d/tests/test_utils_bids.py @@ -1,4 +1,5 @@ """Tests for the xcp_d.utils.bids module.""" + import json import os @@ -196,31 +197,25 @@ def test_get_tr(ds001419_data): def test_get_freesurfer_dir(datasets): - """Test get_freesurfer_dir and get_freesurfer_sphere.""" - with pytest.raises(NotADirectoryError, match="No FreeSurfer derivatives found."): + """Test get_freesurfer_dir.""" + with pytest.raises(NotADirectoryError, match="No FreeSurfer/MCRIBS derivatives found"): xbids.get_freesurfer_dir(".") - fs_dir = xbids.get_freesurfer_dir(datasets["nibabies"]) + fs_dir, software = xbids.get_freesurfer_dir(datasets["nibabies"]) assert os.path.isdir(fs_dir) + assert software == "FreeSurfer" - # Create fake FreeSurfer folder so there are two possible folders - tmp_fs_dir = os.path.join(os.path.dirname(fs_dir), "freesurfer-fail") - os.mkdir(tmp_fs_dir) - with pytest.raises(ValueError, match="More than one candidate"): - xbids.get_freesurfer_dir(datasets["nibabies"]) + # Create fake FreeSurfer folder so there are two possible folders and it grabs the closest + tmp_fs_dir = os.path.join(datasets["nibabies"], "sourcedata/mcribs") + os.makedirs(tmp_fs_dir, exist_ok=True) + fs_dir, software = xbids.get_freesurfer_dir(datasets["nibabies"]) + assert os.path.isdir(fs_dir) + assert software == "MCRIBS" os.rmdir(tmp_fs_dir) - fs_dir = xbids.get_freesurfer_dir(datasets["pnc"]) + fs_dir, software = xbids.get_freesurfer_dir(datasets["pnc"]) assert os.path.isdir(fs_dir) - - sphere_file = xbids.get_freesurfer_sphere(fs_dir, "1648798153", "L") - assert os.path.isfile(sphere_file) - - sphere_file = xbids.get_freesurfer_sphere(fs_dir, "sub-1648798153", "L") - assert os.path.isfile(sphere_file) - - with pytest.raises(FileNotFoundError, match="Sphere file not found at"): - sphere_file = xbids.get_freesurfer_sphere(fs_dir, "fail", "L") + assert software == "FreeSurfer" def test_get_entity(datasets): diff --git a/xcp_d/tests/test_utils_concatenation.py b/xcp_d/tests/test_utils_concatenation.py index 6d4ea94f5..bbe6baf90 100644 --- a/xcp_d/tests/test_utils_concatenation.py +++ b/xcp_d/tests/test_utils_concatenation.py @@ -1,4 +1,5 @@ """Tests for the xcp_d.utils.concatenation module.""" + import os import nibabel as nb diff --git a/xcp_d/tests/test_utils_doc.py b/xcp_d/tests/test_utils_doc.py index 5413194e4..fb75d5a43 100644 --- a/xcp_d/tests/test_utils_doc.py +++ b/xcp_d/tests/test_utils_doc.py @@ -1,4 +1,5 @@ """Tests for the xcp_d.utils.doc module.""" + import os from xcp_d.utils import doc diff --git a/xcp_d/tests/test_utils_execsummary.py b/xcp_d/tests/test_utils_execsummary.py index 7902b4772..0557bea40 100644 --- a/xcp_d/tests/test_utils_execsummary.py +++ b/xcp_d/tests/test_utils_execsummary.py @@ -1,4 +1,5 @@ """Test functions in xcp_d.utils.execsummary.""" + import os import matplotlib.pyplot as plt diff --git a/xcp_d/tests/test_utils_plotting.py b/xcp_d/tests/test_utils_plotting.py index 52aa82e09..9918d9a6f 100644 --- a/xcp_d/tests/test_utils_plotting.py +++ b/xcp_d/tests/test_utils_plotting.py @@ -1,4 +1,5 @@ """Tests for xcp_d.utils.plotting module.""" + import os from xcp_d.utils import plotting diff --git a/xcp_d/tests/test_utils_qcmetrics.py b/xcp_d/tests/test_utils_qcmetrics.py index 87ebb1053..4ab5df4e9 100644 --- a/xcp_d/tests/test_utils_qcmetrics.py +++ b/xcp_d/tests/test_utils_qcmetrics.py @@ -1,4 +1,5 @@ """Tests for the xcp_d.utils.qcmetrics module.""" + import numpy as np from xcp_d.utils import qcmetrics diff --git a/xcp_d/tests/test_utils_utils.py b/xcp_d/tests/test_utils_utils.py index a2e673837..a8acb846d 100644 --- a/xcp_d/tests/test_utils_utils.py +++ b/xcp_d/tests/test_utils_utils.py @@ -1,4 +1,5 @@ """Test functions in xcp_d.utils.utils.""" + import os import numpy as np diff --git a/xcp_d/tests/test_utils_write_save.py b/xcp_d/tests/test_utils_write_save.py index 4f6965aea..ef0bb0b0a 100644 --- a/xcp_d/tests/test_utils_write_save.py +++ b/xcp_d/tests/test_utils_write_save.py @@ -1,4 +1,5 @@ """Tests for the xcp_d.utils.write_save module.""" + import os import pytest diff --git a/xcp_d/tests/test_workflows_anatomical.py b/xcp_d/tests/test_workflows_anatomical.py index cb4e6e8d6..782939b5a 100644 --- a/xcp_d/tests/test_workflows_anatomical.py +++ b/xcp_d/tests/test_workflows_anatomical.py @@ -1,4 +1,5 @@ """Tests for the xcp_d.workflows.anatomical module.""" + import os import shutil diff --git a/xcp_d/tests/test_workflows_connectivity.py b/xcp_d/tests/test_workflows_connectivity.py index f38bff27e..d98ffdf5d 100644 --- a/xcp_d/tests/test_workflows_connectivity.py +++ b/xcp_d/tests/test_workflows_connectivity.py @@ -1,4 +1,5 @@ """Tests for connectivity matrix calculation.""" + import os import sys diff --git a/xcp_d/tests/test_workflows_execsummary.py b/xcp_d/tests/test_workflows_execsummary.py index 3128afc64..ba0a6321e 100644 --- a/xcp_d/tests/test_workflows_execsummary.py +++ b/xcp_d/tests/test_workflows_execsummary.py @@ -1,4 +1,5 @@ """Test xcp_d.workflows.execsummary.""" + import os from nilearn import image diff --git a/xcp_d/tests/test_workflows_restingstate.py b/xcp_d/tests/test_workflows_restingstate.py index 194985e5f..62ce8049b 100644 --- a/xcp_d/tests/test_workflows_restingstate.py +++ b/xcp_d/tests/test_workflows_restingstate.py @@ -1,4 +1,5 @@ """Test for alff.""" + import os import shutil diff --git a/xcp_d/tests/utils.py b/xcp_d/tests/utils.py index 2a88d035e..ba5740f07 100644 --- a/xcp_d/tests/utils.py +++ b/xcp_d/tests/utils.py @@ -1,4 +1,5 @@ """Utility functions for tests.""" + import os import subprocess import tarfile diff --git a/xcp_d/utils/bids.py b/xcp_d/utils/bids.py index 492ab9d3b..926a3f270 100644 --- a/xcp_d/utils/bids.py +++ b/xcp_d/utils/bids.py @@ -737,9 +737,11 @@ def get_preproc_pipeline_info(input_type, fmri_dir): info_dict = { "name": dataset_dict["GeneratedBy"][0]["Name"], - "version": dataset_dict["GeneratedBy"][0]["Version"] - if "Version" in dataset_dict["GeneratedBy"][0].keys() - else "unknown", + "version": ( + dataset_dict["GeneratedBy"][0]["Version"] + if "Version" in dataset_dict["GeneratedBy"][0].keys() + else "unknown" + ), } if input_type == "fmriprep": info_dict["references"] = "[@esteban2019fmriprep;@esteban2020analysis, RRID:SCR_016216]" @@ -779,7 +781,7 @@ def _get_tr(img): def get_freesurfer_dir(fmri_dir): - """Find FreeSurfer derivatives associated with preprocessing pipeline. + """Find FreeSurfer or MCRIBS derivatives associated with preprocessing pipeline. NOTE: This is a Node function. @@ -790,8 +792,9 @@ def get_freesurfer_dir(fmri_dir): Returns ------- - freesurfer_path : :obj:`str` - Path to FreeSurfer derivatives. + seg_path : :obj:`str` + Path to FreeSurfer or MCRIBS derivatives. + seg Raises ------ @@ -800,78 +803,53 @@ def get_freesurfer_dir(fmri_dir): NotADirectoryError If no FreeSurfer derivatives are found. """ - import glob import os - # for fMRIPrep/Nibabies versions >=20.2.1 - freesurfer_paths = sorted(glob.glob(os.path.join(fmri_dir, "sourcedata/*freesurfer*"))) - if len(freesurfer_paths) == 0: - # for fMRIPrep/Nibabies versions <20.2.1 - freesurfer_paths = sorted( - glob.glob(os.path.join(os.path.dirname(fmri_dir), "*freesurfer*")) - ) - - if len(freesurfer_paths) == 1: - freesurfer_path = freesurfer_paths[0] - - elif len(freesurfer_paths) > 1: - freesurfer_paths_str = "\n\t".join(freesurfer_paths) - raise ValueError( - "More than one candidate for FreeSurfer derivatives found. " - "We recommend mounting only one FreeSurfer directory in your Docker/Singularity " - "image. " - f"Detected candidates:\n\t{freesurfer_paths_str}" - ) - - else: - raise NotADirectoryError("No FreeSurfer derivatives found.") - - return freesurfer_path - - -def get_freesurfer_sphere(freesurfer_path, subject_id, hemisphere): - """Find FreeSurfer sphere file. - - NOTE: This is a Node function. - - Parameters - ---------- - freesurfer_path : :obj:`str` - Path to the FreeSurfer derivatives. - subject_id : :obj:`str` - Subject ID. This may or may not be prefixed with "sub-". - hemisphere : {"L", "R"} - The hemisphere to grab. + from nipype import logging - Returns - ------- - sphere_raw : :obj:`str` - Sphere file for the requested subject and hemisphere. + LOGGER = logging.getLogger("nipype.utils") - Raises - ------ - FileNotFoundError - If the sphere file cannot be found. - """ - import os + patterns = { + "Nibabies >= 24.0.0a1": ( + os.path.join(fmri_dir, "sourcedata/mcribs"), + "MCRIBS", + ), + "fMRIPrep >= 20.2.1": ( + os.path.join(fmri_dir, "sourcedata/freesurfer"), + "FreeSurfer", + ), + "Nibabies >= 21.0.0": ( + os.path.join(fmri_dir, "sourcedata/infant-freesurfer"), + "FreeSurfer", + ), + "fMRIPrep < 20.2.1": ( + os.path.join(os.path.dirname(fmri_dir), "freesurfer"), + "FreeSurfer", + ), + "Nibabies < 21.0.0": ( + os.path.join(os.path.dirname(fmri_dir), "infant-freesurfer"), + "FreeSurfer", + ), + } - assert hemisphere in ("L", "R"), hemisphere + for desc, key in patterns.items(): + pattern, software = key + if os.path.isdir(pattern): + LOGGER.info( + f"{software} derivatives associated with {desc} preprocessing derivatives found " + f"at {pattern}" + ) + return pattern, software - if not subject_id.startswith("sub-"): - subject_id = f"sub-{subject_id}" + # Otherwise, continue to the next pattern - sphere_raw = os.path.join( - freesurfer_path, - subject_id, - "surf", - f"{hemisphere.lower()}h.sphere.reg", + seg_patterns = [pattern[0] for pattern in patterns.values()] + patterns_str = "\n\t".join(seg_patterns) + raise NotADirectoryError( + "No FreeSurfer/MCRIBS derivatives found in any of the following locations:" + f"\n\t{patterns_str}" ) - if not os.path.isfile(sphere_raw): - raise FileNotFoundError(f"Sphere file not found at '{sphere_raw}'") - - return sphere_raw - def get_entity(filename, entity): """Extract a given entity from a BIDS filename via string manipulation. diff --git a/xcp_d/utils/qcmetrics.py b/xcp_d/utils/qcmetrics.py index 9619b8b1a..7fdc55e54 100644 --- a/xcp_d/utils/qcmetrics.py +++ b/xcp_d/utils/qcmetrics.py @@ -1,4 +1,5 @@ """Quality control metrics.""" + import h5py import nibabel as nb import numpy as np diff --git a/xcp_d/utils/utils.py b/xcp_d/utils/utils.py index 4017716d9..5b11bf059 100644 --- a/xcp_d/utils/utils.py +++ b/xcp_d/utils/utils.py @@ -446,9 +446,9 @@ def denoise_with_nilearn( f"Outlier volumes at beginning of run ({first_outliers[0]}-{first_outliers[1]}) " "will be replaced with first non-outlier volume's values." ) - interpolated_unfiltered_bold[ - : first_outliers[1] + 1, : - ] = interpolated_unfiltered_bold[first_outliers[1] + 1, :] + interpolated_unfiltered_bold[: first_outliers[1] + 1, :] = ( + interpolated_unfiltered_bold[first_outliers[1] + 1, :] + ) # Replace outliers at end of run if last_outliers[1] == n_volumes - 1: diff --git a/xcp_d/workflows/anatomical.py b/xcp_d/workflows/anatomical.py index 59187628f..a968deba3 100644 --- a/xcp_d/workflows/anatomical.py +++ b/xcp_d/workflows/anatomical.py @@ -7,7 +7,6 @@ from nipype.interfaces.freesurfer import MRIsConvert from nipype.pipeline import engine as pe from niworkflows.engine.workflows import LiterateWorkflow as Workflow -from pkg_resources import resource_filename as pkgrf from templateflow.api import get as get_template from xcp_d.interfaces.ants import ( @@ -15,7 +14,7 @@ CompositeInvTransformUtil, ConvertTransformFile, ) -from xcp_d.interfaces.bids import DerivativesDataSink +from xcp_d.interfaces.bids import CollectRegistrationFiles, DerivativesDataSink from xcp_d.interfaces.c3 import C3d # TM from xcp_d.interfaces.nilearn import BinaryMath, Merge from xcp_d.interfaces.workbench import ( # MB,TM @@ -28,7 +27,7 @@ SurfaceGenerateInflated, SurfaceSphereProjectUnproject, ) -from xcp_d.utils.bids import get_freesurfer_dir, get_freesurfer_sphere +from xcp_d.utils.bids import get_freesurfer_dir from xcp_d.utils.doc import fill_doc from xcp_d.utils.utils import list_to_str from xcp_d.workflows.execsummary import ( @@ -414,6 +413,7 @@ def init_postprocess_surfaces_wf( ), name="inputnode", ) + workflow.__desc__ = "" if dcan_qc and mesh_available: # Plot the white and pial surfaces on the brain in a brainsprite figure. @@ -461,6 +461,10 @@ def init_postprocess_surfaces_wf( ) if morphometry_files: + workflow.__desc__ += ( + " fsLR-space morphometry surfaces were copied from the preprocessing derivatives to " + "the XCP-D derivatives." + ) for morphometry_file in morphometry_files: # fmt:off workflow.connect([ @@ -471,6 +475,10 @@ def init_postprocess_surfaces_wf( # fmt:on if mesh_available: + workflow.__desc__ += ( + " HCP-style midthickness, inflated, and very-inflated surfaces were generated from " + "the white-matter and pial surface meshes." + ) # Generate and output HCP-style surface files. hcp_surface_wfs = { hemi: init_generate_hcp_surfaces_wf( @@ -489,6 +497,10 @@ def init_postprocess_surfaces_wf( # fmt:on if mesh_available and standard_space_mesh: + workflow.__desc__ += ( + " All surface files were already in fsLR space, and were copied to the output " + "directory." + ) # Mesh files are already in fsLR. # fmt:off workflow.connect([ @@ -510,6 +522,7 @@ def init_postprocess_surfaces_wf( # fmt:on elif mesh_available: + workflow.__desc__ += " fsnative-space surfaces were then warped to fsLR space." # Mesh files are in fsnative and must be warped to fsLR. warp_surfaces_to_template_wf = init_warp_surfaces_to_template_wf( fmri_dir=fmri_dir, @@ -658,7 +671,7 @@ def init_warp_surfaces_to_template_wf( Function( function=get_freesurfer_dir, input_names=["fmri_dir"], - output_names=["freesurfer_path"], + output_names=["freesurfer_path", "segmentation_software"], ), name="get_freesurfer_dir_node", ) @@ -712,6 +725,7 @@ def init_warp_surfaces_to_template_wf( workflow.connect([ (get_freesurfer_dir_node, apply_transforms_wf, [ ("freesurfer_path", "inputnode.freesurfer_path"), + ("segmentation_software", "inputnode.segmentation_software"), ]), (update_xfm_wf, apply_transforms_wf, [ ("outputnode.merged_warpfield", "inputnode.merged_warpfield"), @@ -1242,6 +1256,8 @@ def init_warp_one_hemisphere_wf( merged_inv_warpfield freesurfer_path Path to FreeSurfer derivatives. Used to load the subject's sphere file. + segmentation_software : {"FreeSurfer", "MCRIBS"} + The software used for the segmentation. participant_id Set from parameters. @@ -1259,6 +1275,7 @@ def init_warp_one_hemisphere_wf( "merged_warpfield", "merged_inv_warpfield", "freesurfer_path", + "segmentation_software", "participant_id", ], ), @@ -1266,46 +1283,19 @@ def init_warp_one_hemisphere_wf( ) inputnode.inputs.participant_id = participant_id - # Load the fsaverage-164k sphere - # NOTE: Why do we need the fsaverage mesh? - fsaverage_mesh = str( - get_template( - template="fsaverage", - space=None, - hemi=hemisphere, - density="164k", - desc=None, - suffix="sphere", - ) - ) - - # NOTE: Can we upload these to templateflow? - fs_hemisphere_to_fsLR = pkgrf( - "xcp_d", - ( - f"data/standard_mesh_atlases/fs_{hemisphere}/" - f"fs_{hemisphere}-to-fs_LR_fsaverage.{hemisphere}_LR.spherical_std." - f"164k_fs_{hemisphere}.surf.gii" - ), - ) - get_freesurfer_sphere_node = pe.Node( - Function( - function=get_freesurfer_sphere, - input_names=["freesurfer_path", "subject_id", "hemisphere"], - output_names=["sphere_raw"], - ), - name="get_freesurfer_sphere_node", + collect_registration_files = pe.Node( + CollectRegistrationFiles(hemisphere=hemisphere), + name="collect_registration_files", + mem_gb=0.1, + n_procs=1, ) - get_freesurfer_sphere_node.inputs.hemisphere = hemisphere - - # fmt:off workflow.connect([ - (inputnode, get_freesurfer_sphere_node, [ - ("freesurfer_path", "freesurfer_path"), - ("participant_id", "subject_id"), - ]) - ]) - # fmt:on + (inputnode, collect_registration_files, [ + ("freesurfer_path", "segmentation_dir"), + ("participant_id", "participant_id"), + ("segmentation_software", "software"), + ]), + ]) # fmt:skip # NOTE: What does this step do? sphere_to_surf_gii = pe.Node( @@ -1314,58 +1304,37 @@ def init_warp_one_hemisphere_wf( mem_gb=mem_gb, n_procs=omp_nthreads, ) - - # fmt:off workflow.connect([ - (get_freesurfer_sphere_node, sphere_to_surf_gii, [("sphere_raw", "in_file")]), - ]) - # fmt:on + (collect_registration_files, sphere_to_surf_gii, [("subject_sphere", "in_file")]), + ]) # fmt:skip # NOTE: What does this step do? surface_sphere_project_unproject = pe.Node( - SurfaceSphereProjectUnproject( - sphere_project_to=fsaverage_mesh, - sphere_unproject_from=fs_hemisphere_to_fsLR, - ), + SurfaceSphereProjectUnproject(), name="surface_sphere_project_unproject", ) - - # fmt:off workflow.connect([ + (collect_registration_files, surface_sphere_project_unproject, [ + ("source_sphere", "sphere_project_to"), + ("sphere_to_sphere", "sphere_unproject_from"), + ]), (sphere_to_surf_gii, surface_sphere_project_unproject, [("converted", "in_file")]), - ]) - # fmt:on - - fsLR_sphere = str( - get_template( - template="fsLR", - space=None, - hemi=hemisphere, - density="32k", - desc=None, - suffix="sphere", - ) - ) + ]) # fmt:skip # resample the surfaces to fsLR-32k # NOTE: Does that mean the data are in fsLR-164k before this? resample_to_fsLR32k = pe.MapNode( - CiftiSurfaceResample( - new_sphere=fsLR_sphere, - metric=" BARYCENTRIC ", - ), + CiftiSurfaceResample(metric="BARYCENTRIC"), name="resample_to_fsLR32k", mem_gb=mem_gb, n_procs=omp_nthreads, iterfield=["in_file"], ) - - # fmt:off workflow.connect([ (inputnode, resample_to_fsLR32k, [("hemi_files", "in_file")]), + (collect_registration_files, resample_to_fsLR32k, [("target_sphere", "new_sphere")]), (surface_sphere_project_unproject, resample_to_fsLR32k, [("out_file", "current_sphere")]), - ]) - # fmt:on + ]) # fmt:skip # apply affine to 32k surfs # NOTE: What does this step do? Aren't the data in fsLR-32k from resample_to_fsLR32k? @@ -1376,13 +1345,10 @@ def init_warp_one_hemisphere_wf( n_procs=omp_nthreads, iterfield=["in_file"], ) - - # fmt:off workflow.connect([ - (resample_to_fsLR32k, apply_affine_to_fsLR32k, [("out_file", "in_file")]), (inputnode, apply_affine_to_fsLR32k, [("world_xfm", "affine")]), - ]) - # fmt:on + (resample_to_fsLR32k, apply_affine_to_fsLR32k, [("out_file", "in_file")]), + ]) # fmt:skip # apply FNIRT-format warpfield # NOTE: What does this step do? @@ -1393,26 +1359,20 @@ def init_warp_one_hemisphere_wf( n_procs=omp_nthreads, iterfield=["in_file"], ) - - # fmt:off workflow.connect([ (inputnode, apply_warpfield_to_fsLR32k, [ ("merged_warpfield", "forward_warp"), ("merged_inv_warpfield", "warpfield"), ]), (apply_affine_to_fsLR32k, apply_warpfield_to_fsLR32k, [("out_file", "in_file")]), - ]) - # fmt:on + ]) # fmt:skip outputnode = pe.Node( niu.IdentityInterface(fields=["warped_hemi_files"]), name="outputnode", ) - - # fmt:off workflow.connect([ (apply_warpfield_to_fsLR32k, outputnode, [("out_file", "warped_hemi_files")]), - ]) - # fmt:on + ]) # fmt:skip return workflow diff --git a/xcp_d/workflows/concatenation.py b/xcp_d/workflows/concatenation.py index ad9395c4f..09b0e8862 100644 --- a/xcp_d/workflows/concatenation.py +++ b/xcp_d/workflows/concatenation.py @@ -1,4 +1,5 @@ """Workflows for concatenating postprocessed data.""" + from nipype.interfaces import utility as niu from nipype.pipeline import engine as pe from niworkflows.engine.workflows import LiterateWorkflow as Workflow diff --git a/xcp_d/workflows/plotting.py b/xcp_d/workflows/plotting.py index e19ebc503..26e719e94 100644 --- a/xcp_d/workflows/plotting.py +++ b/xcp_d/workflows/plotting.py @@ -1,4 +1,5 @@ """Plotting workflows.""" + from nipype import Function from nipype.interfaces import utility as niu from nipype.pipeline import engine as pe