Skip to content

Commit 055285d

Browse files
committed
RF: Simplify high-pass filtering in algorithms.confounds
Legendre and cosine detrending are implemented almost identically, although with several minor variations. Here I separate regressor creation from detrending to unify the implementations. This now uses `np.linalg.pinv(X)` to estimate the betas in both cases, rather than using `np.linalg.lstsq` in the cosine filter. lstsq uses SVD and can thus fail to converge in rare cases. Under no circumstances should (X.T @ X) be singular, so the pseudoinverse is unique and precisely what we want.
1 parent 9f17357 commit 055285d

File tree

1 file changed

+46
-38
lines changed

1 file changed

+46
-38
lines changed

nipype/algorithms/confounds.py

+46-38
Original file line numberDiff line numberDiff line change
@@ -1185,31 +1185,61 @@ def is_outlier(points, thresh=3.5):
11851185
return timepoints_to_discard
11861186

11871187

1188-
def cosine_filter(
1189-
data, timestep, period_cut, remove_mean=True, axis=-1, failure_mode="error"
1188+
def _make_cosine_regressors(ntimepoints, timestep, period_cut):
1189+
return _full_rank(_cosine_drift(period_cut, timestep * np.arange(ntimepoints)))[0]
1190+
1191+
1192+
def _make_legendre_regressors(ntimepoints, degree):
1193+
X = np.ones((ntimepoints, 1)) # quick way to calc degree 0
1194+
for i in range(degree):
1195+
polynomial_func = Legendre.basis(i + 1)
1196+
value_array = np.linspace(-1, 1, ntimepoints)
1197+
X = np.hstack((X, polynomial_func(value_array)[:, np.newaxis]))
1198+
return X
1199+
1200+
1201+
def _high_pass_filter(
1202+
data, *args, filter_type, remove_mean=True, axis=-1, failure_mode="error"
11901203
):
11911204
datashape = data.shape
11921205
timepoints = datashape[axis]
11931206
if datashape[0] == 0 and failure_mode != "error":
11941207
return data, np.array([])
11951208

1196-
data = data.reshape((-1, timepoints))
1209+
filters = {
1210+
'cosine': _make_cosine_regressors,
1211+
'legendre': _make_legendre_regressors,
1212+
}
11971213

1198-
frametimes = timestep * np.arange(timepoints)
1199-
X = _full_rank(_cosine_drift(period_cut, frametimes))[0]
1214+
X = filters[filter_type](timepoints, *args)
12001215
non_constant_regressors = X[:, :-1] if X.shape[1] > 1 else np.array([])
12011216

1202-
betas = np.linalg.lstsq(X, data.T)[0]
1217+
data = data.reshape((-1, timepoints))
1218+
1219+
betas = np.linalg.pinv(X) @ data.T
12031220

12041221
if not remove_mean:
12051222
X = X[:, :-1]
12061223
betas = betas[:-1]
12071224

1208-
residuals = data - X.dot(betas).T
1209-
1225+
residuals = data - (X @ betas).T
12101226
return residuals.reshape(datashape), non_constant_regressors
12111227

12121228

1229+
def cosine_filter(
1230+
data, timestep, period_cut, remove_mean=True, axis=-1, failure_mode="error"
1231+
):
1232+
return _high_pass_filter(
1233+
data,
1234+
timestep,
1235+
period_cut,
1236+
filter_type='cosine',
1237+
remove_mean=remove_mean,
1238+
axis=axis,
1239+
failure_mode=failure_mode,
1240+
)
1241+
1242+
12131243
def regress_poly(degree, data, remove_mean=True, axis=-1, failure_mode="error"):
12141244
"""
12151245
Returns data with degree polynomial regressed out.
@@ -1221,36 +1251,14 @@ def regress_poly(degree, data, remove_mean=True, axis=-1, failure_mode="error"):
12211251
IFLOGGER.debug(
12221252
"Performing polynomial regression on data of shape %s", str(data.shape)
12231253
)
1224-
1225-
datashape = data.shape
1226-
timepoints = datashape[axis]
1227-
if datashape[0] == 0 and failure_mode != "error":
1228-
return data, np.array([])
1229-
1230-
# Rearrange all voxel-wise time-series in rows
1231-
data = data.reshape((-1, timepoints))
1232-
1233-
# Generate design matrix
1234-
X = np.ones((timepoints, 1)) # quick way to calc degree 0
1235-
for i in range(degree):
1236-
polynomial_func = Legendre.basis(i + 1)
1237-
value_array = np.linspace(-1, 1, timepoints)
1238-
X = np.hstack((X, polynomial_func(value_array)[:, np.newaxis]))
1239-
1240-
non_constant_regressors = X[:, :-1] if X.shape[1] > 1 else np.array([])
1241-
1242-
# Calculate coefficients
1243-
betas = np.linalg.pinv(X).dot(data.T)
1244-
1245-
# Estimation
1246-
if remove_mean:
1247-
datahat = X.dot(betas).T
1248-
else: # disregard the first layer of X, which is degree 0
1249-
datahat = X[:, 1:].dot(betas[1:, ...]).T
1250-
regressed_data = data - datahat
1251-
1252-
# Back to original shape
1253-
return regressed_data.reshape(datashape), non_constant_regressors
1254+
return _high_pass_filter(
1255+
data,
1256+
degree,
1257+
filter_type='legendre',
1258+
remove_mean=remove_mean,
1259+
axis=axis,
1260+
failure_mode=failure_mode,
1261+
)
12541262

12551263

12561264
def combine_mask_files(mask_files, mask_method=None, mask_index=None):

0 commit comments

Comments
 (0)