Skip to content

Commit 329c74d

Browse files
committed
add variance-driven component selection, return component metadata
1 parent ebbd062 commit 329c74d

File tree

1 file changed

+63
-18
lines changed

1 file changed

+63
-18
lines changed

nipype/algorithms/confounds.py

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,15 +1139,29 @@ def combine_mask_files(mask_files, mask_method=None, mask_index=None):
11391139
return [img]
11401140

11411141

1142-
def compute_noise_components(imgseries, mask_images, num_components,
1143-
filter_type, degree, period_cut, repetition_time):
1142+
def compute_noise_components(imgseries, mask_images, num_components=0.5,
1143+
filter_type=False, degree=0, period_cut=128,
1144+
repetition_time=None, failure_mode='error'):
11441145
"""Compute the noise components from the imgseries for each mask
11451146
1146-
imgseries: a nibabel img
1147-
mask_images: a list of nibabel images
1148-
num_components: number of noise components to return
1149-
filter_type: type off filter to apply to time series before computing
1150-
noise components.
1147+
Parameters
1148+
----------
1149+
imgseries: nibabel NIfTI object
1150+
Time series data to be decomposed.
1151+
mask_images: list
1152+
List of nibabel images. Time series data from `img_series` is subset
1153+
according to the spatial extent of each mask, and the subset data is
1154+
then decomposed using principal component analysis. Masks should be
1155+
coextensive with either anatomical or spatial noise ROIs.
1156+
num_components: float
1157+
Number of noise components to return. If this is a decimal value
1158+
between 0 and 1, then `create_noise_components` will instead return
1159+
the smallest number of components necessary to explain the indicated
1160+
fraction of variance. If `num_components` is -1, then all
1161+
components will be returned.
1162+
filter_type: str
1163+
Type of filter to apply to time series before computing
1164+
noise components.
11511165
'polynomial' - Legendre polynomial basis
11521166
'cosine' - Discrete cosine (DCT) basis
11531167
False - None (mean-removal only)
@@ -1158,16 +1172,20 @@ def compute_noise_components(imgseries, mask_images, num_components,
11581172
period_cut: minimum period (in sec) for DCT high-pass filter
11591173
repetition_time: time (in sec) between volume acquisitions
11601174
1161-
returns:
1162-
1163-
components: a numpy array
1164-
basis: a numpy array containing the (non-constant) filter regressors
1165-
1175+
Outputs
1176+
-------
1177+
components: numpy array
1178+
Numpy array containing the requested set of noise components
1179+
basis: numpy array
1180+
Numpy array containing the (non-constant) filter regressors
1181+
metadata: dict(numpy array)
1182+
Dictionary of eigenvalues, fractional explained variances, and
1183+
cumulative explained variances.
11661184
"""
11671185
components = None
11681186
basis = np.array([])
1169-
for img in mask_images:
1170-
mask = img.get_data().astype(np.bool)
1187+
for i, img in enumerate(mask_images):
1188+
mask = img.get_data().astype(np.bool).squeeze()
11711189
if imgseries.shape[:3] != mask.shape:
11721190
raise ValueError(
11731191
'Inputs for CompCor, timeseries and mask, do not have '
@@ -1201,20 +1219,47 @@ def compute_noise_components(imgseries, mask_images, num_components,
12011219
# "The covariance matrix C = MMT was constructed and decomposed into its
12021220
# principal components using a singular value decomposition."
12031221
try:
1204-
u, _, _ = fallback_svd(M, full_matrices=False)
1222+
u, s, _ = fallback_svd(M, full_matrices=False)
12051223
except np.linalg.LinAlgError:
12061224
if self.inputs.failure_mode == 'error':
12071225
raise
1208-
u = np.ones((M.shape[0], num_components), dtype=np.float32) * np.nan
1226+
if num_components >= 1:
1227+
u = np.empty((M.shape[0], num_components),
1228+
dtype=np.float32) * np.nan
1229+
else:
1230+
continue
1231+
1232+
variance_explained = np.array([value**2/np.sum(s**2) for value in s])
1233+
cumulative_variance_explained = np.cumsum(variance_explained)
1234+
if 0 < num_components < 1:
1235+
num_components = np.searchsorted(cumulative_variance_explained,
1236+
num_components) + 1
1237+
elif num_components == -1:
1238+
num_components = len(s)
12091239
if components is None:
12101240
components = u[:, :num_components]
1241+
metadata = {
1242+
'mask': np.array([i] * len(s)),
1243+
'singular_values': s,
1244+
'variance_explained': variance_explained,
1245+
'cumulative_variance_explained': cumulative_variance_explained
1246+
}
12111247
else:
12121248
components = np.hstack((components, u[:, :num_components]))
1213-
if components is None and num_components > 0:
1249+
metadata['mask'] = np.hstack((metadata['mask'], [i] * len(s)))
1250+
metadata['singular_values'] = (
1251+
np.hstack((metadata['singular_values'], s)))
1252+
metadata['variance_explained'] = (
1253+
np.hstack((metadata['variance_explained'],
1254+
variance_explained)))
1255+
metadata['cumulative_variance_explained'] = (
1256+
np.hstack((metadata['cumulative_variance_explained'],
1257+
cumulative_variance_explained)))
1258+
if components is None and num_components != 0:
12141259
if self.inputs.failure_mode == 'error':
12151260
raise ValueError('No components found')
12161261
components = np.ones((M.shape[0], num_components), dtype=np.float32) * np.nan
1217-
return components, basis
1262+
return components, basis, metadata
12181263

12191264

12201265
def _compute_tSTD(M, x, axis=0):

0 commit comments

Comments
 (0)