Skip to content

[ENH] Issue 3345: Adding FreeSurfer longitudinal interfaces #3529

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

Merged
merged 18 commits into from
Jul 5, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .zenodo.json
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,11 @@
{
"name": "Schwartz, Yannick"
},
{
"affiliation": "Medical College of Wisconsin",
"name": "Espana, Lezlie",
"orcid": "0000-0002-6466-4653"
},
{
"affiliation": "The University of Iowa",
"name": "Ghayoor, Ali",
Expand Down
21 changes: 19 additions & 2 deletions nipype/interfaces/freesurfer/longitudinal.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,25 @@
import os

from ... import logging
from ..base import TraitedSpec, File, traits, InputMultiPath, OutputMultiPath, isdefined
from .base import FSCommand, FSTraitedSpec, FSCommandOpenMP, FSTraitedSpecOpenMP
from ..base import (
TraitedSpec,
File,
traits,
InputMultiPath,
OutputMultiPath,
isdefined,
InputMultiObject,
Directory,
)
from .base import (
FSCommand,
FSTraitedSpec,
FSCommandOpenMP,
FSTraitedSpecOpenMP,
CommandLine,
)
from .preprocess import ReconAllInputSpec
from ..io import FreeSurferSource

__docformat__ = "restructuredtext"
iflogger = logging.getLogger("nipype.interface")
Expand Down
107 changes: 96 additions & 11 deletions nipype/interfaces/freesurfer/preprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
CommandLine,
CommandLineInputSpec,
isdefined,
InputMultiObject,
)
from .base import FSCommand, FSTraitedSpec, FSTraitedSpecOpenMP, FSCommandOpenMP, Info
from .utils import copy2subjdir
Expand Down Expand Up @@ -816,7 +817,9 @@ def _gen_filename(self, name):

class ReconAllInputSpec(CommandLineInputSpec):
subject_id = traits.Str(
"recon_all", argstr="-subjid %s", desc="subject name", usedefault=True
"recon_all",
argstr="-subjid %s",
desc="subject name",
)
directive = traits.Enum(
"all",
Expand Down Expand Up @@ -927,6 +930,29 @@ class ReconAllInputSpec(CommandLineInputSpec):
)
flags = InputMultiPath(traits.Str, argstr="%s", desc="additional parameters")

# Longitudinal runs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need to introduce additional constraints to avoid using cross-sectional specific arguments with base and long modes.

I am thinking of the T1_files, T2_file and FLAIR_file parameters at least. These should probably get a requirement on subject_id, which would achieve what we want thanks to your xor constraints.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That didn't cross my mind but excellent thought. I've add some requires and will test some variations next week to make sure that works as expected.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me 👍

base_template_id = traits.Str(
argstr="-base %s",
desc="base template id",
xor=["subject_id", "longitudinal_timepoint_id"],
requires=["base_timepoint_ids"],
)
base_timepoint_ids = InputMultiObject(
traits.Str(),
argstr="-base-tp %s...",
desc="processed timepoint to use in template",
)
longitudinal_timepoint_id = traits.Str(
argstr="-long %s",
desc="longitudinal session/timepoint id",
xor=["subject_id", "base_template_id"],
requires=["longitudinal_template_id"],
position=1,
)
longitudinal_template_id = traits.Str(
argstr="%s", desc="longitudinal base tempalte id", position=2
)

# Expert options
talairach = traits.Str(desc="Flags to pass to talairach commands", xor=["expert"])
mri_normalize = traits.Str(
Expand Down Expand Up @@ -1019,7 +1045,7 @@ class ReconAll(CommandLine):
>>> reconall.inputs.subject_id = 'foo'
>>> reconall.inputs.directive = 'all'
>>> reconall.inputs.subjects_dir = '.'
>>> reconall.inputs.T1_files = 'structural.nii'
>>> reconall.inputs.T1_files = ['structural.nii']
>>> reconall.cmdline
'recon-all -all -i structural.nii -subjid foo -sd .'
>>> reconall.inputs.flags = "-qcache"
Expand Down Expand Up @@ -1049,7 +1075,7 @@ class ReconAll(CommandLine):
>>> reconall_subfields.inputs.subject_id = 'foo'
>>> reconall_subfields.inputs.directive = 'all'
>>> reconall_subfields.inputs.subjects_dir = '.'
>>> reconall_subfields.inputs.T1_files = 'structural.nii'
>>> reconall_subfields.inputs.T1_files = ['structural.nii']
>>> reconall_subfields.inputs.hippocampal_subfields_T1 = True
>>> reconall_subfields.cmdline
'recon-all -all -i structural.nii -hippocampal-subfields-T1 -subjid foo -sd .'
Expand All @@ -1060,6 +1086,24 @@ class ReconAll(CommandLine):
>>> reconall_subfields.inputs.hippocampal_subfields_T1 = False
>>> reconall_subfields.cmdline
'recon-all -all -i structural.nii -hippocampal-subfields-T2 structural.nii test -subjid foo -sd .'

Base template creation for longitudinal pipeline:
>>> baserecon = ReconAll()
>>> baserecon.inputs.base_template_id = 'sub-template'
>>> baserecon.inputs.base_timepoint_ids = ['ses-1','ses-2']
>>> baserecon.inputs.directive = 'all'
>>> baserecon.inputs.subjects_dir = '.'
>>> baserecon.cmdline
'recon-all -all -base sub-template -base-tp ses-1 -base-tp ses-2 -sd .'

Longitudinal timepoint run:
>>> longrecon = ReconAll()
>>> longrecon.inputs.longitudinal_timepoint_id = 'ses-1'
>>> longrecon.inputs.longitudinal_template_id = 'sub-template'
>>> longrecon.inputs.directive = 'all'
>>> longrecon.inputs.subjects_dir = '.'
>>> longrecon.cmdline
'recon-all -all -long ses-1 sub-template -sd .'
"""

_cmd = "recon-all"
Expand Down Expand Up @@ -1523,21 +1567,62 @@ def _list_outputs(self):

outputs = self._outputs().get()

outputs.update(
FreeSurferSource(
subject_id=self.inputs.subject_id, subjects_dir=subjects_dir, hemi=hemi
)._list_outputs()
)
outputs["subject_id"] = self.inputs.subject_id
# If using longitudinal pipeline, update subject id accordingly,
# otherwise use original/default subject_id
if isdefined(self.inputs.base_template_id):
outputs.update(
FreeSurferSource(
subject_id=self.inputs.base_template_id,
subjects_dir=subjects_dir,
hemi=hemi,
)._list_outputs()
)
outputs["subject_id"] = self.inputs.base_template_id
elif isdefined(self.inputs.longitudinal_timepoint_id):
subject_id = f"{self.inputs.longitudinal_timepoint_id}.long.{self.inputs.longitudinal_template_id}"
outputs.update(
FreeSurferSource(
subject_id=subject_id, subjects_dir=subjects_dir, hemi=hemi
)._list_outputs()
)
outputs["subject_id"] = subject_id
else:
outputs.update(
FreeSurferSource(
subject_id=self.inputs.subject_id,
subjects_dir=subjects_dir,
hemi=hemi,
)._list_outputs()
)
outputs["subject_id"] = self.inputs.subject_id

outputs["subjects_dir"] = subjects_dir
return outputs

def _is_resuming(self):
subjects_dir = self.inputs.subjects_dir
if not isdefined(subjects_dir):
subjects_dir = self._gen_subjects_dir()
if os.path.isdir(os.path.join(subjects_dir, self.inputs.subject_id, "mri")):
return True

# Check for longitudinal pipeline
if not isdefined(self.inputs.subject_id):
if isdefined(self.inputs.base_template_id):
if os.path.isdir(
os.path.join(subjects_dir, self.inputs.base_template_id, "mri")
):
return True
elif isdefined(self.inputs.longitudinal_template_id):
if os.path.isdir(
os.path.join(
subjects_dir,
f"{self.inputs.longitudinal_timepoint_id}.long.{self.inputs.longitudinal_template_id}",
"mri",
)
):
return True
else:
if os.path.isdir(os.path.join(subjects_dir, self.inputs.subject_id, "mri")):
return True
return False

def _format_arg(self, name, trait_spec, value):
Expand Down
19 changes: 18 additions & 1 deletion nipype/interfaces/freesurfer/tests/test_auto_ReconAll.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ def test_ReconAll_inputs():
args=dict(
argstr="%s",
),
base_template_id=dict(
argstr="-base %s",
requires=["base_timepoint_ids"],
xor=["subject_id", "longitudinal_timepoint_id"],
),
base_timepoint_ids=dict(
argstr="-base-tp %s...",
),
big_ventricles=dict(
argstr="-bigventricles",
),
Expand Down Expand Up @@ -57,6 +65,16 @@ def test_ReconAll_inputs():
argstr="-hires",
min_ver="6.0.0",
),
longitudinal_template_id=dict(
argstr="%s",
position=2,
),
longitudinal_timepoint_id=dict(
argstr="-long %s",
position=1,
requires=["longitudinal_template_id"],
xor=["subject_id", "base_template_id"],
),
mprage=dict(
argstr="-mprage",
),
Expand Down Expand Up @@ -143,7 +161,6 @@ def test_ReconAll_inputs():
),
subject_id=dict(
argstr="-subjid %s",
usedefault=True,
),
subjects_dir=dict(
argstr="-sd %s",
Expand Down
107 changes: 0 additions & 107 deletions nipype/interfaces/tests/test_auto_Dcm2nii.py

This file was deleted.