@@ -390,7 +390,16 @@ class CompCorInputSpec(BaseInterfaceInputSpec):
390
390
'components_file.txt' ,
391
391
usedefault = True ,
392
392
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
394
403
pre_filter = traits .Enum (
395
404
'polynomial' ,
396
405
'cosine' ,
@@ -418,6 +427,8 @@ class CompCorInputSpec(BaseInterfaceInputSpec):
418
427
'unspecified' )
419
428
save_pre_filter = traits .Either (
420
429
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' )
421
432
ignore_initial_volumes = traits .Range (
422
433
low = 0 ,
423
434
usedefault = True ,
@@ -433,6 +444,7 @@ class CompCorOutputSpec(TraitedSpec):
433
444
components_file = File (
434
445
exists = True , desc = 'text file containing the noise components' )
435
446
pre_filter_file = File (desc = 'text file containing high-pass filter basis' )
447
+ metadata_file = File (desc = 'text file containing component metadata' )
436
448
437
449
438
450
class CompCor (BaseInterface ):
@@ -548,7 +560,7 @@ def _run_interface(self, runtime):
548
560
'{} cannot detect repetition time from image - '
549
561
'Set the repetition_time input' .format (self ._header ))
550
562
551
- components , filter_basis = compute_noise_components (
563
+ components , filter_basis , metadata = compute_noise_components (
552
564
imgseries .get_data (), mask_images , self .inputs .num_components ,
553
565
self .inputs .pre_filter , degree , self .inputs .high_pass_cutoff , TR )
554
566
@@ -597,6 +609,16 @@ def _run_interface(self, runtime):
597
609
header = '\t ' .join (header ),
598
610
comments = '' )
599
611
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
+
600
622
return runtime
601
623
602
624
def _process_masks (self , mask_images , timeseries = None ):
@@ -613,6 +635,12 @@ def _list_outputs(self):
613
635
save_pre_filter = os .path .abspath ('pre_filter.tsv' )
614
636
outputs ['pre_filter_file' ] = save_pre_filter
615
637
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
+
616
644
return outputs
617
645
618
646
def _make_headers (self , num_col ):
@@ -1139,7 +1167,7 @@ def combine_mask_files(mask_files, mask_method=None, mask_index=None):
1139
1167
return [img ]
1140
1168
1141
1169
1142
- def compute_noise_components (imgseries , mask_images , num_components = 0.5 ,
1170
+ def compute_noise_components (imgseries , mask_images , components_criterion = 0.5 ,
1143
1171
filter_type = False , degree = 0 , period_cut = 128 ,
1144
1172
repetition_time = None , failure_mode = 'error' ):
1145
1173
"""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,
1153
1181
according to the spatial extent of each mask, and the subset data is
1154
1182
then decomposed using principal component analysis. Masks should be
1155
1183
coextensive with either anatomical or spatial noise ROIs.
1156
- num_components : float
1184
+ components_criterion : float
1157
1185
Number of noise components to return. If this is a decimal value
1158
1186
between 0 and 1, then `create_noise_components` will instead return
1159
1187
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
1161
1189
components will be returned.
1162
1190
filter_type: str
1163
1191
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,
1204
1232
voxel_timecourses , repetition_time , period_cut )
1205
1233
elif filter_type in ('polynomial' , False ):
1206
1234
# 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 ...]"
1209
1237
voxel_timecourses , basis = regress_poly (degree , voxel_timecourses )
1210
1238
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."
1214
1242
M = voxel_timecourses .T
1215
1243
1216
1244
# "[... were removed] prior to column-wise variance normalization."
1217
1245
M = M / _compute_tSTD (M , 1. )
1218
1246
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."
1221
1249
try :
1222
1250
u , s , _ = fallback_svd (M , full_matrices = False )
1223
1251
except np .linalg .LinAlgError :
1224
1252
if self .inputs .failure_mode == 'error' :
1225
1253
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 ),
1228
1256
dtype = np .float32 ) * np .nan
1229
1257
else :
1230
1258
continue
1231
1259
1232
1260
variance_explained = np .array ([value ** 2 / np .sum (s ** 2 ) for value in s ])
1233
1261
cumulative_variance_explained = np .cumsum (variance_explained )
1234
- if 0 < num_components < 1 :
1262
+ if 0 < components_criterion < 1 :
1235
1263
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 :
1238
1266
num_components = len (s )
1267
+ else :
1268
+ num_components = components_criterion
1239
1269
if components is None :
1240
1270
components = u [:, :num_components ]
1241
1271
metadata = {
@@ -1258,7 +1288,8 @@ def compute_noise_components(imgseries, mask_images, num_components=0.5,
1258
1288
if components is None and num_components != 0 :
1259
1289
if self .inputs .failure_mode == 'error' :
1260
1290
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
1262
1293
return components , basis , metadata
1263
1294
1264
1295
0 commit comments