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

Collect and apply field maps #79

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
7 changes: 4 additions & 3 deletions src/fmripost_aroma/data/io_spec.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"name": "aroma",
"queries": {
"raw": {
"bold_raw": {
Expand Down Expand Up @@ -151,8 +152,8 @@
},
"entities": [
{
"name": "datatype",
"pattern": "[/\\\\]+(anat|func)[/\\\\]+"
"name": "fmapid",
"pattern": "fmapid-([a-zA-Z0-9]+)"
},
{
"name": "cohort",
Expand All @@ -172,7 +173,7 @@
"pattern": "(?:^|_)thresh-([a-zA-Z0-9]+)"
}
],
"patterns": [
"default_path_patterns": [
"sub-{subject}[/ses-{session}]/{datatype<func>|func}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_echo-{echo}][_part-{part}][_space-{space}][_res-{res}][_desc-{desc}]_{suffix<bold|boldref|dseg|mask>}.{extension<nii|nii.gz|json>|nii.gz}",
"sub-{subject}[/ses-{session}]/{datatype<func>|func}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_echo-{echo}][_part-{part}][_space-{space}][_res-{res}][_stat-{statistic}][_desc-{desc}]_{suffix<components|mixing>}.{extension<nii|nii.gz|json>|nii.gz}",
"sub-{subject}[/ses-{session}]/{datatype<func>|func}/sub-{subject}[_ses-{session}]_task-{task}[_acq-{acquisition}][_ce-{ceagent}][_rec-{reconstruction}][_run-{run}][_echo-{echo}][_part-{part}][_space-{space}][_res-{res}][_stat-{statistic}][_desc-{desc}]_{suffix<components|mixing>}.{extension<tsv|json>|tsv}",
Expand Down
2 changes: 1 addition & 1 deletion src/fmripost_aroma/interfaces/bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ class DerivativesDataSink(BaseDerivativesDataSink):
_allowed_entities = set(config_entities)
_config_entities = config_entities
_config_entities_dict = merged_entities
_file_patterns = fmripost_aroma_spec['patterns']
_file_patterns = fmripost_aroma_spec['default_path_patterns']
11 changes: 9 additions & 2 deletions src/fmripost_aroma/utils/bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,21 @@ def collect_derivatives(
if not entities:
entities = {}

_spec = None
if spec is None or patterns is None:
_spec = json.loads(load_data.readable('io_spec.json').read_text())

if spec is None:
spec = _spec['queries']

if patterns is None:
patterns = _spec['patterns']
patterns = _spec['default_path_patterns']

_spec.pop('queries')

config = ['bids', 'derivatives']
if _spec:
config = ['bids', 'derivatives', _spec]

# Search for derivatives data
derivs_cache = defaultdict(list, {})
Expand All @@ -114,7 +121,7 @@ def collect_derivatives(
if isinstance(layout, Path):
layout = BIDSLayout(
layout,
config=['bids', 'derivatives'],
config=config,
validate=False,
)

Expand Down
111 changes: 84 additions & 27 deletions src/fmripost_aroma/workflows/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
if config.execution.derivatives:
# Raw dataset + derivatives dataset
config.loggers.workflow.info('Raw+derivatives workflow mode enabled')
# Just build a list of BOLD files right now
subject_data = collect_derivatives(
raw_dataset=config.execution.layout,
derivatives_dataset=None,
Expand All @@ -191,6 +192,7 @@
else:
# Derivatives dataset only
config.loggers.workflow.info('Derivatives-only workflow mode enabled')
# Just build a list of BOLD files right now
subject_data = collect_derivatives(
raw_dataset=None,
derivatives_dataset=config.execution.layout,
Expand Down Expand Up @@ -304,14 +306,32 @@

entities = extract_entities(bold_file)

# Attempt to extract the associated fmap ID
fmapid = None
all_fmapids = config.execution.layout.get_fmapids(

Check warning on line 311 in src/fmripost_aroma/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

src/fmripost_aroma/workflows/base.py#L310-L311

Added lines #L310 - L311 were not covered by tests
subject=entities['subject'],
session=entities.get('session', None),
)
if all_fmapids:
fmap_file = config.execution.layout.get_nearest(

Check warning on line 316 in src/fmripost_aroma/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

src/fmripost_aroma/workflows/base.py#L316

Added line #L316 was not covered by tests
bold_file,
to=all_fmapids,
suffix='xfm',
extension='.txt',
strict=False,
**{'from': 'boldref'},
)
if fmap_file:
fmapid = config.execution.layout.get_file(fmap_file).entities['to']

Check warning on line 325 in src/fmripost_aroma/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

src/fmripost_aroma/workflows/base.py#L325

Added line #L325 was not covered by tests

functional_cache = defaultdict(list, {})
if config.execution.derivatives:
# Collect native-space derivatives and transforms
functional_cache = collect_derivatives(
raw_dataset=config.execution.layout,
derivatives_dataset=None,
entities=entities,
fieldmap_id=None,
fieldmap_id=fmapid,
allow_multiple=False,
spaces=None,
)
Expand All @@ -322,7 +342,7 @@
raw_dataset=None,
derivatives_dataset=deriv_dir,
entities=entities,
fieldmap_id=None,
fieldmap_id=fmapid,
allow_multiple=False,
spaces=spaces,
),
Expand All @@ -347,7 +367,7 @@
raw_dataset=None,
derivatives_dataset=config.execution.layout,
entities=entities,
fieldmap_id=None,
fieldmap_id=fmapid,
allow_multiple=False,
spaces=spaces,
),
Expand All @@ -371,30 +391,60 @@
)
skip_vols = get_nss(functional_cache['bold_confounds'])

inputnode = pe.Node(

Check warning on line 394 in src/fmripost_aroma/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

src/fmripost_aroma/workflows/base.py#L394

Added line #L394 was not covered by tests
niu.IdentityInterface(
fields=[
'bold_raw',
'bold_confounds',
'bold_mni152nlin6asym',
'motion_xfm',
'boldref2fmap_xfm',
'boldref2anat_xfm',
'anat2std_xfm',
'bold_ref_file',
'fmap',
'bold_mask_native',
],
),
name='inputnode',
)
inputnode.inputs.bold_raw = functional_cache['bold_raw']
inputnode.inputs.bold_confounds = functional_cache['bold_confounds']
inputnode.inputs.bold_mni152nlin6asym = functional_cache['bold_mni152nlin6asym']
inputnode.inputs.bold_mask_native = functional_cache['bold_mask_native']

Check warning on line 414 in src/fmripost_aroma/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

src/fmripost_aroma/workflows/base.py#L411-L414

Added lines #L411 - L414 were not covered by tests
# Transforms
inputnode.inputs.bold_hmc = functional_cache['bold_hmc']
inputnode.inputs.boldref2fmap = functional_cache['boldref2fmap']
inputnode.inputs.boldref2anat = functional_cache['boldref2anat']
inputnode.inputs.anat2mni152nlin6asym = functional_cache['anat2mni152nlin6asym']

Check warning on line 419 in src/fmripost_aroma/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

src/fmripost_aroma/workflows/base.py#L416-L419

Added lines #L416 - L419 were not covered by tests
# Field maps
inputnode.inputs.fmap = functional_cache['fmap']

Check warning on line 421 in src/fmripost_aroma/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

src/fmripost_aroma/workflows/base.py#L421

Added line #L421 was not covered by tests

# Run ICA-AROMA
ica_aroma_wf = init_ica_aroma_wf(bold_file=bold_file, metadata=bold_metadata, mem_gb=mem_gb)
ica_aroma_wf.inputs.inputnode.confounds = functional_cache['bold_confounds']
ica_aroma_wf.inputs.inputnode.skip_vols = skip_vols
workflow.connect([(inputnode, ica_aroma_wf, [('bold_confounds', 'inputnode.confounds')])])

Check warning on line 426 in src/fmripost_aroma/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

src/fmripost_aroma/workflows/base.py#L426

Added line #L426 was not covered by tests

mni6_buffer = pe.Node(niu.IdentityInterface(fields=['bold', 'bold_mask']), name='mni6_buffer')

if ('bold_mni152nlin6asym' not in functional_cache) and ('bold_raw' in functional_cache):
# Resample to MNI152NLin6Asym:res-2, for ICA-AROMA classification
from fmriprep.workflows.bold.apply import init_bold_volumetric_resample_wf
from fmriprep.workflows.bold.stc import init_bold_stc_wf
from niworkflows.interfaces.header import ValidateImage
from templateflow.api import get as get_template

from fmripost_aroma.interfaces.misc import ApplyTransforms
from fmripost_aroma.workflows.resample import init_bold_volumetric_resample_wf

Check warning on line 437 in src/fmripost_aroma/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

src/fmripost_aroma/workflows/base.py#L437

Added line #L437 was not covered by tests

workflow.__desc__ += """\
Raw BOLD series were resampled to MNI152NLin6Asym:res-2, for ICA-AROMA classification.
"""

validate_bold = pe.Node(
ValidateImage(in_file=functional_cache['bold_raw']),
ValidateImage(),
name='validate_bold',
)
workflow.connect([(inputnode, validate_bold, [('bold_raw', 'in_file')])])

Check warning on line 447 in src/fmripost_aroma/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

src/fmripost_aroma/workflows/base.py#L447

Added line #L447 was not covered by tests

stc_buffer = pe.Node(
niu.IdentityInterface(fields=['bold_file']),
Expand Down Expand Up @@ -427,30 +477,27 @@

bold_MNI6_wf = init_bold_volumetric_resample_wf(
metadata=bold_metadata,
fieldmap_id=None, # XXX: Ignoring the field map for now
fieldmap_id=fmapid,
omp_nthreads=omp_nthreads,
mem_gb=mem_gb,
jacobian='fmap-jacobian' not in config.workflow.ignore,
name='bold_MNI6_wf',
)
bold_MNI6_wf.inputs.inputnode.motion_xfm = functional_cache['bold_hmc']
bold_MNI6_wf.inputs.inputnode.boldref2fmap_xfm = functional_cache['boldref2fmap']
bold_MNI6_wf.inputs.inputnode.boldref2anat_xfm = functional_cache['boldref2anat']
bold_MNI6_wf.inputs.inputnode.anat2std_xfm = functional_cache['anat2mni152nlin6asym']
bold_MNI6_wf.inputs.inputnode.resolution = '02'
# use mask as boldref?
bold_MNI6_wf.inputs.inputnode.bold_ref_file = functional_cache['bold_mask_native']
bold_MNI6_wf.inputs.inputnode.target_mask = mni6_mask
bold_MNI6_wf.inputs.inputnode.target_ref_file = mni6_mask

# Pass transforms and warped fieldmap to bold_MNI6_wf
workflow.connect([
(inputnode, bold_MNI6_wf, [
('bold_hmc', 'inputnode.motion_xfm'),
('boldref2fmap', 'inputnode.boldref2fmap_xfm'),
('boldref2anat', 'inputnode.boldref2anat_xfm'),
('anat2mni152nlin6asym', 'inputnode.anat2std_xfm'),
# use mask as boldref?
('bold_mask_native', 'inputnode.bold_ref_file'),
]),
# Resample BOLD to MNI152NLin6Asym, may duplicate bold_std_wf above
# XXX: Ignoring the field map for now
# (inputnode, bold_MNI6_wf, [
# ('fmap_ref', 'inputnode.fmap_ref'),
# ('fmap_coeff', 'inputnode.fmap_coeff'),
# ('fmap_id', 'inputnode.fmap_id'),
# ]),
(stc_buffer, bold_MNI6_wf, [('bold_file', 'inputnode.bold_file')]),
(bold_MNI6_wf, mni6_buffer, [('outputnode.bold_file', 'bold')]),
]) # fmt:skip
Expand All @@ -459,7 +506,6 @@
mask_to_mni6 = pe.Node(
ApplyTransforms(
interpolation='GenericLabel',
input_image=functional_cache['bold_mask_native'],
reference_image=mni6_mask,
transforms=[
functional_cache['anat2mni152nlin6asym'],
Expand All @@ -468,15 +514,22 @@
),
name='mask_to_mni6',
)
workflow.connect([(mask_to_mni6, mni6_buffer, [('output_image', 'bold_mask')])])
workflow.connect([

Check warning on line 517 in src/fmripost_aroma/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

src/fmripost_aroma/workflows/base.py#L517

Added line #L517 was not covered by tests
(inputnode, mask_to_mni6, [('bold_mask_native', 'input_image')]),
(mask_to_mni6, mni6_buffer, [('output_image', 'bold_mask')]),
]) # fmt:skip

elif 'bold_mni152nlin6asym' in functional_cache:
workflow.__desc__ += """\
Preprocessed BOLD series in MNI152NLin6Asym:res-2 space were collected for ICA-AROMA
classification.
"""
mni6_buffer.inputs.bold = functional_cache['bold_mni152nlin6asym']
mni6_buffer.inputs.bold_mask = functional_cache['bold_mask_mni152nlin6asym']
workflow.connect([

Check warning on line 527 in src/fmripost_aroma/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

src/fmripost_aroma/workflows/base.py#L527

Added line #L527 was not covered by tests
(inputnode, mni6_buffer, [
('bold_mni152nlin6asym', 'bold'),
('bold_mask_native', 'bold_mask'),
]),
]) # fmt:skip

else:
raise ValueError('No valid BOLD series found for ICA-AROMA classification.')
Expand All @@ -491,19 +544,23 @@
# Generate reportlets
func_fit_reports_wf = init_func_fit_reports_wf(output_dir=config.execution.output_dir)
func_fit_reports_wf.inputs.inputnode.source_file = bold_file
func_fit_reports_wf.inputs.inputnode.anat2std_xfm = functional_cache['anat2mni152nlin6asym']
func_fit_reports_wf.inputs.inputnode.anat_dseg = functional_cache['anat_dseg']
workflow.connect([(mni6_buffer, func_fit_reports_wf, [('bold', 'inputnode.bold_mni6')])])
workflow.connect([

Check warning on line 547 in src/fmripost_aroma/workflows/base.py

View check run for this annotation

Codecov / codecov/patch

src/fmripost_aroma/workflows/base.py#L547

Added line #L547 was not covered by tests
(inputnode, func_fit_reports_wf, [
('anat2mni152nlin6asym', 'inputnode.anat2std_xfm'),
('anat_dseg', 'inputnode.anat_dseg'),
]),
(mni6_buffer, func_fit_reports_wf, [('bold', 'inputnode.bold_mni6')]),
]) # fmt:skip

if config.workflow.denoise_method:
# Now denoise the output-space BOLD data using ICA-AROMA
denoise_wf = init_denoise_wf(bold_file=bold_file, metadata=bold_metadata)
denoise_wf.inputs.inputnode.skip_vols = skip_vols
denoise_wf.inputs.inputnode.space = 'MNI152NLin6Asym'
denoise_wf.inputs.inputnode.res = '2'
denoise_wf.inputs.inputnode.confounds_file = functional_cache['bold_confounds']

workflow.connect([
(inputnode, denoise_wf, [('bold_confounds', 'inputnode.confounds_file')]),
(mni6_buffer, denoise_wf, [
('bold', 'inputnode.bold_file'),
('bold_mask', 'inputnode.bold_mask'),
Expand Down
Loading
Loading