Skip to content

Commit 17f3e12

Browse files
committed
expose metadata to interface, fix component selection for multiple masks
1 parent 329c74d commit 17f3e12

File tree

1 file changed

+49
-18
lines changed

1 file changed

+49
-18
lines changed

nipype/algorithms/confounds.py

Lines changed: 49 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,16 @@ class CompCorInputSpec(BaseInterfaceInputSpec):
390390
'components_file.txt',
391391
usedefault=True,
392392
desc='Filename to store physiological components')
393-
num_components = traits.Int(6, usedefault=True) # 6 for BOLD, 4 for ASL
393+
num_components = traits.Float(6, usedefault=True,
394+
desc='Number of components to return from the decomposition.'
395+
'If `num_components` is a positive integer, then '
396+
'`num_components` components will be retained. If '
397+
'`num_components` is a fractional value between 0 and 1, then '
398+
'the number of components retained will be equal to the minimum '
399+
'number of components necessary to explain the provided '
400+
'fraction of variance in the masked time series. If '
401+
'`num_components` is -1, then all components will be retained.')
402+
# 6 for BOLD, 4 for ASL
394403
pre_filter = traits.Enum(
395404
'polynomial',
396405
'cosine',
@@ -418,6 +427,8 @@ class CompCorInputSpec(BaseInterfaceInputSpec):
418427
'unspecified')
419428
save_pre_filter = traits.Either(
420429
traits.Bool, File, desc='Save pre-filter basis as text file')
430+
save_metadata = traits.Either(
431+
traits.Bool, File, desc='Save component metadata as text file')
421432
ignore_initial_volumes = traits.Range(
422433
low=0,
423434
usedefault=True,
@@ -433,6 +444,7 @@ class CompCorOutputSpec(TraitedSpec):
433444
components_file = File(
434445
exists=True, desc='text file containing the noise components')
435446
pre_filter_file = File(desc='text file containing high-pass filter basis')
447+
metadata_file = File(desc='text file containing component metadata')
436448

437449

438450
class CompCor(BaseInterface):
@@ -548,7 +560,7 @@ def _run_interface(self, runtime):
548560
'{} cannot detect repetition time from image - '
549561
'Set the repetition_time input'.format(self._header))
550562

551-
components, filter_basis = compute_noise_components(
563+
components, filter_basis, metadata = compute_noise_components(
552564
imgseries.get_data(), mask_images, self.inputs.num_components,
553565
self.inputs.pre_filter, degree, self.inputs.high_pass_cutoff, TR)
554566

@@ -597,6 +609,16 @@ def _run_interface(self, runtime):
597609
header='\t'.join(header),
598610
comments='')
599611

612+
if self.inputs.save_metadata:
613+
metadata_file = self._list_outputs()['metadata_file']
614+
np.savetxt(
615+
metadata_file,
616+
np.vstack(metadata.values()).T,
617+
fmt=['%s', b'%.10f', b'%.10f', b'%.10f'],
618+
delimiter='\t',
619+
header='\t'.join(list(metadata.keys())),
620+
comments='')
621+
600622
return runtime
601623

602624
def _process_masks(self, mask_images, timeseries=None):
@@ -613,6 +635,12 @@ def _list_outputs(self):
613635
save_pre_filter = os.path.abspath('pre_filter.tsv')
614636
outputs['pre_filter_file'] = save_pre_filter
615637

638+
save_metadata = self.inputs.save_metadata
639+
if save_metadata:
640+
if isinstance(save_metadata, bool):
641+
save_metadata = os.path.abspath('component_metadata.tsv')
642+
outputs['metadata_file'] = save_metadata
643+
616644
return outputs
617645

618646
def _make_headers(self, num_col):
@@ -1139,7 +1167,7 @@ def combine_mask_files(mask_files, mask_method=None, mask_index=None):
11391167
return [img]
11401168

11411169

1142-
def compute_noise_components(imgseries, mask_images, num_components=0.5,
1170+
def compute_noise_components(imgseries, mask_images, components_criterion=0.5,
11431171
filter_type=False, degree=0, period_cut=128,
11441172
repetition_time=None, failure_mode='error'):
11451173
"""Compute the noise components from the imgseries for each mask
@@ -1153,11 +1181,11 @@ def compute_noise_components(imgseries, mask_images, num_components=0.5,
11531181
according to the spatial extent of each mask, and the subset data is
11541182
then decomposed using principal component analysis. Masks should be
11551183
coextensive with either anatomical or spatial noise ROIs.
1156-
num_components: float
1184+
components_criterion: float
11571185
Number of noise components to return. If this is a decimal value
11581186
between 0 and 1, then `create_noise_components` will instead return
11591187
the smallest number of components necessary to explain the indicated
1160-
fraction of variance. If `num_components` is -1, then all
1188+
fraction of variance. If `components_criterion` is -1, then all
11611189
components will be returned.
11621190
filter_type: str
11631191
Type of filter to apply to time series before computing
@@ -1204,38 +1232,40 @@ def compute_noise_components(imgseries, mask_images, num_components=0.5,
12041232
voxel_timecourses, repetition_time, period_cut)
12051233
elif filter_type in ('polynomial', False):
12061234
# from paper:
1207-
# "The constant and linear trends of the columns in the matrix M were
1208-
# removed [prior to ...]"
1235+
# "The constant and linear trends of the columns in the matrix M
1236+
# were removed [prior to ...]"
12091237
voxel_timecourses, basis = regress_poly(degree, voxel_timecourses)
12101238

1211-
# "Voxel time series from the noise ROI (either anatomical or tSTD) were
1212-
# placed in a matrix M of size Nxm, with time along the row dimension
1213-
# and voxels along the column dimension."
1239+
# "Voxel time series from the noise ROI (either anatomical or tSTD)
1240+
# were placed in a matrix M of size Nxm, with time along the row
1241+
# dimension and voxels along the column dimension."
12141242
M = voxel_timecourses.T
12151243

12161244
# "[... were removed] prior to column-wise variance normalization."
12171245
M = M / _compute_tSTD(M, 1.)
12181246

1219-
# "The covariance matrix C = MMT was constructed and decomposed into its
1220-
# principal components using a singular value decomposition."
1247+
# "The covariance matrix C = MMT was constructed and decomposed into
1248+
# its principal components using a singular value decomposition."
12211249
try:
12221250
u, s, _ = fallback_svd(M, full_matrices=False)
12231251
except np.linalg.LinAlgError:
12241252
if self.inputs.failure_mode == 'error':
12251253
raise
1226-
if num_components >= 1:
1227-
u = np.empty((M.shape[0], num_components),
1254+
if components_criterion >= 1:
1255+
u = np.empty((M.shape[0], components_criterion),
12281256
dtype=np.float32) * np.nan
12291257
else:
12301258
continue
12311259

12321260
variance_explained = np.array([value**2/np.sum(s**2) for value in s])
12331261
cumulative_variance_explained = np.cumsum(variance_explained)
1234-
if 0 < num_components < 1:
1262+
if 0 < components_criterion < 1:
12351263
num_components = np.searchsorted(cumulative_variance_explained,
1236-
num_components) + 1
1237-
elif num_components == -1:
1264+
components_criterion) + 1
1265+
elif components_criterion == -1:
12381266
num_components = len(s)
1267+
else:
1268+
num_components = components_criterion
12391269
if components is None:
12401270
components = u[:, :num_components]
12411271
metadata = {
@@ -1258,7 +1288,8 @@ def compute_noise_components(imgseries, mask_images, num_components=0.5,
12581288
if components is None and num_components != 0:
12591289
if self.inputs.failure_mode == 'error':
12601290
raise ValueError('No components found')
1261-
components = np.ones((M.shape[0], num_components), dtype=np.float32) * np.nan
1291+
components = np.ones((M.shape[0], num_components),
1292+
dtype=np.float32) * np.nan
12621293
return components, basis, metadata
12631294

12641295

0 commit comments

Comments
 (0)