Skip to content

Commit 2aed15d

Browse files
authored
Merge pull request #1595 from FCP-INDI/feature/correlation-matrix
✨ FEAT: Correlation matrices
2 parents f401439 + 7a952bb commit 2aed15d

40 files changed

+589
-119
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- Added changelog
1313
- Added CHD8 mouse template (`/cpac_templates/chd8_functional_template_noise_mask_ag.nii.gz`)
1414
- Added commandline flags `--T1w_label` and `--bold_label`
15+
- Added AFNI and Nilearn implementations of Pearson and partial correlation matrices
1516

1617
### Changed
1718

CPAC/aroma/aroma_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
file, an output_dir which can be either be mentioned or if it is set to
88
None will write it in the current working directory.
99
10-
The argument run can either be tset to true(default) or to false. If
10+
The argument run can either be set to true(default) or to false. If
1111
set to False, it should connect to the nipype workflow and return the
1212
workflow object instead.
1313
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
"""Functions for creating connectome connectivity matrices."""
4+
from warnings import warn
5+
import numpy as np
6+
from nilearn.connectome import ConnectivityMeasure
7+
from nilearn.input_data import NiftiLabelsMasker
8+
from nipype import logging
9+
from nipype.interfaces import utility as util
10+
from CPAC.pipeline import nipype_pipeline_engine as pe
11+
from CPAC.utils.interfaces.function import Function
12+
from CPAC.utils.interfaces.netcorr import NetCorr, strip_afni_output_header
13+
14+
logger = logging.getLogger('nipype.workflow')
15+
connectome_methods = {
16+
'afni': {'Pearson': '',
17+
'Partial': '-part_corr'},
18+
'nilearn': {'Pearson': 'correlation',
19+
'Partial': 'partial correlation'}
20+
}
21+
22+
23+
def connectome_name(timeseries, atlas_name, tool, method):
24+
"""Helper function to create connectome file filename
25+
26+
Parameters
27+
----------
28+
timeseries : str
29+
path to input timeseries
30+
31+
atlas_name : str
32+
atlas name
33+
34+
tool : str
35+
connectome tool
36+
37+
method : str
38+
BIDS entity value for `desc-` key
39+
40+
Returns
41+
-------
42+
str
43+
"""
44+
method = ''.join(word.capitalize() for word in [tool, method])
45+
new_filename_parts = [part for part in timeseries.split('_')[:-1][::-1] if
46+
not part.startswith('space-')]
47+
atlas_index = len(new_filename_parts) - 1
48+
if any(filename_part.startswith('desc-') for filename_part in
49+
new_filename_parts):
50+
for i, filename_part in enumerate(new_filename_parts):
51+
if filename_part.startswith('desc-'):
52+
new_filename_parts[-i] = f'desc-{method}'
53+
atlas_index = -(i - 1)
54+
break
55+
new_filename_parts.insert(atlas_index, f'atlas-{atlas_name}')
56+
return '_'.join([*new_filename_parts[::-1], 'connectome.tsv'])
57+
58+
59+
def get_connectome_method(method, tool):
60+
"""Helper function to get tool's method string
61+
62+
Parameters
63+
----------
64+
method : str
65+
66+
tool : str
67+
68+
Returns
69+
-------
70+
str or NotImplemented
71+
72+
Examples
73+
--------
74+
>>> get_connectome_method('Pearson', 'AFNI')
75+
''
76+
>>> get_connectome_method('Pearson', 'Nilearn')
77+
'correlation'
78+
>>> get_connectome_method('Spearman', 'AFNI')
79+
NotImplemented
80+
"""
81+
cm_method = connectome_methods[tool.lower()].get(method, NotImplemented)
82+
if cm_method is NotImplemented:
83+
warning_message = (
84+
f'{method} has not yet been implemented for {tool} in C-PAC.')
85+
if logger:
86+
logger.warning(NotImplementedError(warning_message))
87+
else:
88+
warn(warning_message, category=Warning)
89+
return cm_method
90+
91+
92+
def compute_connectome_nilearn(in_rois, in_file, method, atlas_name):
93+
"""Function to compute a connectome matrix using Nilearn
94+
95+
Parameters
96+
----------
97+
in_rois : Niimg-like object
98+
http://nilearn.github.io/manipulating_images/input_output.html#niimg-like-objects
99+
Region definitions, as one image of labels.
100+
101+
in_file : str
102+
path to timeseries image
103+
104+
method: str
105+
'Pearson' or 'Partial'
106+
107+
atlas_name: str
108+
109+
Returns
110+
-------
111+
numpy.ndarray or NotImplemented
112+
"""
113+
tool = 'Nilearn'
114+
output = connectome_name(in_file, atlas_name, tool, method)
115+
method = get_connectome_method(method, tool)
116+
if method is NotImplemented:
117+
return NotImplemented
118+
masker = NiftiLabelsMasker(labels_img=in_rois,
119+
standardize=True,
120+
verbose=True)
121+
timeser = masker.fit_transform(in_file)
122+
correlation_measure = ConnectivityMeasure(kind=method)
123+
corr_matrix = correlation_measure.fit_transform([timeser])[0]
124+
np.fill_diagonal(corr_matrix, 0)
125+
np.savetxt(output, corr_matrix, delimiter='\t')
126+
return output
127+
128+
129+
def create_connectome_afni(name, method, pipe_num):
130+
wf = pe.Workflow(name=name)
131+
inputspec = pe.Node(
132+
util.IdentityInterface(fields=[
133+
'in_rois', # parcellation
134+
'in_file', # timeseries,
135+
'mask',
136+
'method',
137+
'atlas_name'
138+
]),
139+
name='inputspec'
140+
)
141+
outputspec = pe.Node(
142+
util.IdentityInterface(fields=[
143+
'out_file',
144+
]),
145+
name='outputspec'
146+
)
147+
148+
timeseries_correlation = pe.Node(NetCorr(), name=name)
149+
if method:
150+
timeseries_correlation.inputs.part_corr = (method == 'Partial')
151+
152+
strip_header_node = pe.Node(Function(
153+
input_names=['in_file', 'out_file'], output_names=['out_file'],
154+
imports=['import subprocess'],
155+
function=strip_afni_output_header),
156+
name='netcorrStripHeader'
157+
f'{method}_{pipe_num}')
158+
159+
name_output_node = pe.Node(Function(input_names=['timeseries',
160+
'atlas_name',
161+
'tool',
162+
'method'],
163+
output_names=['filename'],
164+
function=connectome_name),
165+
name=f'connectomeName{method}_{pipe_num}')
166+
name_output_node.inputs.tool = 'Afni'
167+
168+
wf.connect([
169+
(inputspec, timeseries_correlation, [('in_rois', 'in_rois'),
170+
('in_file', 'in_file'),
171+
('mask', 'mask')]),
172+
(inputspec, name_output_node, [('in_file', 'timeseries'),
173+
('atlas_name', 'atlas_name'),
174+
('method', 'method')]),
175+
(timeseries_correlation, strip_header_node, [
176+
('out_corr_matrix', 'in_file')]),
177+
(name_output_node, strip_header_node, [('filename', 'out_file')]),
178+
(strip_header_node, outputspec, [('out_file', 'out_file')])])
179+
return wf
180+
181+
182+
def create_connectome_nilearn(name='connectomeNilearn'):
183+
wf = pe.Workflow(name=name)
184+
inputspec = pe.Node(
185+
util.IdentityInterface(fields=[
186+
'in_rois', # parcellation
187+
'in_file', # timeseries
188+
'method',
189+
'atlas_name'
190+
]),
191+
name='inputspec'
192+
)
193+
outputspec = pe.Node(
194+
util.IdentityInterface(fields=[
195+
'out_file',
196+
]),
197+
name='outputspec'
198+
)
199+
node = pe.Node(Function(input_names=['in_rois', 'in_file', 'method',
200+
'atlas_name'],
201+
output_names=['out_file'],
202+
function=compute_connectome_nilearn,
203+
as_module=True),
204+
name='connectome')
205+
wf.connect([
206+
(inputspec, node, [('in_rois', 'in_rois'),
207+
('in_file', 'in_file'),
208+
('method', 'method'),
209+
('atlas_name', 'atlas_name')]),
210+
(node, outputspec, [('out_file', 'out_file')]),
211+
])
212+
return wf

CPAC/connectome/pipeline.py

Lines changed: 0 additions & 65 deletions
This file was deleted.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""Tests for CPAC.connectome"""
2+
import os
3+
import random
4+
import re
5+
import numpy as np
6+
import pytest
7+
from CPAC.pipeline.schema import valid_options
8+

CPAC/distortion_correction/tests/test_distortion_correction.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def run_warp_nipype(inputs, output_dir=None, run=True):
2525
# warp_nipe file, an output_dir which can be either be mentioned
2626
# or if it is set to none will write it in the current working
2727
# directory.
28-
# The argument run can either be tset to true(default) or to
28+
# The argument run can either be set to true(default) or to
2929
# false. If set to false, it should connect to the nipype workflow
3030
# and return the workflow object instead
3131
# What all should it return?: if the run had been set to true, it

CPAC/pipeline/cpac_pipeline.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,7 +984,36 @@ def build_segmentation_stack(rpool, cfg, pipeline_blocks=None):
984984
return pipeline_blocks
985985

986986

987+
def list_blocks(pipeline_blocks, indent=None):
988+
"""Function to list node blocks line by line
989+
990+
Parameters
991+
----------
992+
pipeline_blocks : list or tuple
993+
994+
indent : int or None
995+
number of spaces after a tab indent
996+
997+
Returns
998+
-------
999+
str
1000+
"""
1001+
blockstring = yaml.dump([
1002+
getattr(block, '__name__', getattr(block, 'name', yaml.safe_load(
1003+
list_blocks(list(block))) if
1004+
isinstance(block, (tuple, list, set)) else str(block))
1005+
) for block in pipeline_blocks
1006+
])
1007+
if isinstance(indent, int):
1008+
blockstring = '\n'.join([
1009+
'\t' + ' ' * indent + line for line in blockstring.split('\n')])
1010+
return blockstring
1011+
1012+
9871013
def connect_pipeline(wf, cfg, rpool, pipeline_blocks):
1014+
logger.info('\n'.join([
1015+
'Connecting pipeline blocks:',
1016+
list_blocks(pipeline_blocks, indent=1)]))
9881017

9891018
for block in pipeline_blocks:
9901019
try:

CPAC/pipeline/schema.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,13 @@
4242
'template': ['EPI_Template', 'T1_Template'],
4343
},
4444
'timeseries': {
45-
'roi_paths': {'Avg', 'Voxel', 'SpatialReg', 'PearsonCorr',
46-
'PartialCorr'},
45+
'roi_paths': {'Avg', 'Voxel', 'SpatialReg'},
46+
},
47+
'connectivity_matrix': {
48+
'using': {'AFNI', 'Nilearn'},
49+
'measure': {'Pearson', 'Partial', 'Spearman', 'MGC',
50+
# 'TangentEmbed' # "Skip tangent embedding for now"
51+
},
4752
},
4853
'Regressors': {
4954
'CompCor': {
@@ -965,8 +970,11 @@ def _changes_1_8_0_to_1_8_1(config_dict):
965970
'tse_roi_paths', valid_options['timeseries']['roi_paths'])
966971
),
967972
'realignment': In({'ROI_to_func', 'func_to_ROI'}),
973+
'connectivity_matrix': {
974+
option: Maybe([In(valid_options['connectivity_matrix'][option])])
975+
for option in ['using', 'measure']
976+
},
968977
},
969-
970978
'seed_based_correlation_analysis': {
971979
'run': bool,
972980
Optional('roi_paths_fully_specified'): bool,

CPAC/resources/configs/pipeline_config_benchmark-ANTS.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ timeseries_extraction:
206206
# Enter paths to region-of-interest (ROI) NIFTI files (.nii or .nii.gz) to be used for time-series extraction, and then select which types of analyses to run.
207207
# Denote which analyses to run for each ROI path by listing the names below. For example, if you wish to run Avg and SpatialReg, you would enter: '/path/to/ROI.nii.gz': Avg, SpatialReg
208208
# available analyses:
209-
# /path/to/atlas.nii.gz: Avg, Voxel, SpatialReg, PearsonCorr, PartialCorr
209+
# /path/to/atlas.nii.gz: Avg, Voxel, SpatialReg
210210
tse_roi_paths:
211211
s3://fcp-indi/resources/cpac/resources/rois_2mm.nii.gz: Avg, Voxel, SpatialReg
212212

CPAC/resources/configs/pipeline_config_benchmark-FNIRT.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ timeseries_extraction:
225225
# Enter paths to region-of-interest (ROI) NIFTI files (.nii or .nii.gz) to be used for time-series extraction, and then select which types of analyses to run.
226226
# Denote which analyses to run for each ROI path by listing the names below. For example, if you wish to run Avg and SpatialReg, you would enter: '/path/to/ROI.nii.gz': Avg, SpatialReg
227227
# available analyses:
228-
# /path/to/atlas.nii.gz: Avg, Voxel, SpatialReg, PearsonCorr, PartialCorr
228+
# /path/to/atlas.nii.gz: Avg, Voxel, SpatialReg
229229
tse_roi_paths:
230230
s3://fcp-indi/resources/cpac/resources/rois_2mm.nii.gz: Avg, Voxel, SpatialReg
231231

0 commit comments

Comments
 (0)