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

Support MCRIBS derivatives #1029

Merged
merged 17 commits into from
Jan 29, 2024
1 change: 1 addition & 0 deletions xcp_d/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""A method for calling the command-line interface."""

from xcp_d.cli.run import main

if __name__ == "__main__":
Expand Down
1 change: 1 addition & 0 deletions xcp_d/cli/parser_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Utility functions for xcp_d command-line interfaces."""

import argparse
import json
import logging
Expand Down
1 change: 1 addition & 0 deletions xcp_d/ingression/__init__.py
Original file line number Diff line number Diff line change
@@ -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__ = [
Expand Down
1 change: 1 addition & 0 deletions xcp_d/ingression/ukbiobank.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Functions to convert preprocessed UK Biobank BOLD data to BIDS derivatives format."""

import glob
import json
import os
Expand Down
1 change: 1 addition & 0 deletions xcp_d/interfaces/ants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""ANTS interfaces."""

import logging
import os

Expand Down
157 changes: 157 additions & 0 deletions xcp_d/interfaces/bids.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -33,3 +42,151 @@
_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(

Check warning on line 159 in xcp_d/interfaces/bids.py

View check run for this annotation

Codecov / codecov/patch

xcp_d/interfaces/bids.py#L159

Added line #L159 was not covered by tests
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(

Check warning on line 170 in xcp_d/interfaces/bids.py

View check run for this annotation

Codecov / codecov/patch

xcp_d/interfaces/bids.py#L170

Added line #L170 was not covered by tests
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(

Check warning on line 178 in xcp_d/interfaces/bids.py

View check run for this annotation

Codecov / codecov/patch

xcp_d/interfaces/bids.py#L178

Added line #L178 was not covered by tests
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(

Check warning on line 186 in xcp_d/interfaces/bids.py

View check run for this annotation

Codecov / codecov/patch

xcp_d/interfaces/bids.py#L186

Added line #L186 was not covered by tests
self.inputs.segmentation_dir,
"templates_fsLR",
f"tpl-dHCP_space-fsLR_hemi-{hemisphere}_den-32k_desc-week42_sphere.surf.gii",
)

return runtime
7 changes: 4 additions & 3 deletions xcp_d/interfaces/censoring.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Interfaces for the post-processing workflows."""

import os

import nibabel as nb
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions xcp_d/interfaces/concatenation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Interfaces for the concatenation workflow."""

import itertools
import os
import re
Expand Down
1 change: 1 addition & 0 deletions xcp_d/interfaces/nilearn.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Interfaces for Nilearn code."""

import os

from nilearn import maskers
Expand Down
1 change: 1 addition & 0 deletions xcp_d/interfaces/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Miscellaneous utility interfaces."""

from nipype import logging
from nipype.interfaces.base import (
BaseInterfaceInputSpec,
Expand Down
1 change: 1 addition & 0 deletions xcp_d/interfaces/workbench.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Custom wb_command interfaces."""

import os

import nibabel as nb
Expand Down
1 change: 1 addition & 0 deletions xcp_d/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Fixtures for the CircleCI tests."""

import base64
import os

Expand Down
1 change: 1 addition & 0 deletions xcp_d/tests/test_TR.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions xcp_d/tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Command-line interface tests."""

import os
import shutil

Expand Down
1 change: 1 addition & 0 deletions xcp_d/tests/test_cli_run.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for functions in the cli.run module."""

import logging
import os
from copy import deepcopy
Expand Down
1 change: 1 addition & 0 deletions xcp_d/tests/test_cli_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for the xcp_d.cli.parser_utils module."""

from argparse import ArgumentTypeError

import pytest
Expand Down
1 change: 1 addition & 0 deletions xcp_d/tests/test_confounds.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Test confounds handling."""

import os

import numpy as np
Expand Down
1 change: 1 addition & 0 deletions xcp_d/tests/test_despike.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions xcp_d/tests/test_filtering.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for filtering methods."""

import re

import numpy as np
Expand Down
1 change: 1 addition & 0 deletions xcp_d/tests/test_interfaces_censoring.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for framewise displacement calculation."""

import json
import os

Expand Down
1 change: 1 addition & 0 deletions xcp_d/tests/test_interfaces_concatenation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for the xcp_d.interfaces.concatenation module."""

import os

from nipype.interfaces.base import Undefined, isdefined
Expand Down
1 change: 1 addition & 0 deletions xcp_d/tests/test_interfaces_nilearn.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for the xcp_d.interfaces.nilearn module."""

import os

import nibabel as nb
Expand Down
1 change: 1 addition & 0 deletions xcp_d/tests/test_interfaces_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for xcp_d.interfaces.utils module."""

import os

import nibabel as nb
Expand Down
1 change: 1 addition & 0 deletions xcp_d/tests/test_smoothing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for smoothing methods."""

import os
import re
import tempfile
Expand Down
1 change: 1 addition & 0 deletions xcp_d/tests/test_utils_atlas.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for the xcp_d.utils.atlas module."""

import os

import pytest
Expand Down
31 changes: 13 additions & 18 deletions xcp_d/tests/test_utils_bids.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for the xcp_d.utils.bids module."""

import json
import os

Expand Down Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions xcp_d/tests/test_utils_concatenation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for the xcp_d.utils.concatenation module."""

import os

import nibabel as nb
Expand Down
1 change: 1 addition & 0 deletions xcp_d/tests/test_utils_doc.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for the xcp_d.utils.doc module."""

import os

from xcp_d.utils import doc
Expand Down
1 change: 1 addition & 0 deletions xcp_d/tests/test_utils_execsummary.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Test functions in xcp_d.utils.execsummary."""

import os

import matplotlib.pyplot as plt
Expand Down
Loading