# The usual boilerplate
import numpy as np
np.set_printoptions(precision=4, suppress=True)
import matplotlib.pyplot as plt
%matplotlib inline
# - set gray colormap and nearest neighbor interpolation by default
plt.rcParams['image.cmap'] = 'gray'
plt.rcParams['image.interpolation'] = 'nearest'
# Imports from nipy
import nipy
from nipy.modalities.fmri.design import block_design, stack_designs
from nipy.modalities.fmri import hrf
from nipy.algorithms.statistics.api import (OLSModel, ARModel, make_recarray)
from nipy.modalities.fmri.design_matrix import _cosine_drift as dft_basis
First we get the raw functional data and drop the first four scans, as dummy scans.
n_dummies = 4
func_img = nipy.load_image('ds114_sub009_t2r1.nii')
func_img = func_img[..., n_dummies:]
func_img.shape
We get the times at which each remaining volume was acquired relative to the start of the first volume. The first volume is the first of the four dummy scans we dropped:
TR = 2.5
n_vols = func_img.shape[-1]
volume_times = np.arange(n_vols) * TR + n_dummies * TR
We make the volume times into a structured array with field name t, so that nipy knows this is an array of time values.
volume_times_rec = make_recarray(volume_times, 't')
volume_times_rec
Next we make the definition of a block design, in a format nipy understands.
Here are the onsets of the blocks and their durations, in the standard OpenFMRI format:
block_defs = np.loadtxt('ds114_sub009_t2r1_cond.txt')
block_defs
Make this into a structured array with fields start (onset time) and stop (offset time):
onsets, durations, amplitudes = block_defs.T
block_spec_arr = np.column_stack((onsets, onsets + durations))
block_spec = make_recarray(block_spec_arr, ('start', 'end'))
block_spec
Feed this block definition into the nipy utility for constructing the design matrix for a block design.
We'll use the SPM HRF definition, but you can explore the hrf model for other HRF definitions we could have used:
X_exper, cons_exper = block_design(block_spec,
volume_times_rec,
hrfs=(hrf.spm,))
Because this is a single condition, this design is a single regressor - the block on/off step function convolved with the HRF:
plt.plot(volume_times, X_exper)
Nipy made the contrast for this part of the design. In this case it's just a single value == 1 (more or less!) but if we had included other columns in the block definition, these would have been factor levels, and there could be more contrasts here, for main effects and interactions:
cons_exper
Model the low-frequency drift with the discrete cosine basis. This is the default model used by SPM. See https://en.wikipedia.org/wiki/Discrete_cosine_transform.
# 128 seconds wavelength cut-off high-pass filter.
drift = dft_basis(128, volume_times)
plt.imshow(drift, aspect=0.1)
# Stack the experimental and drift designs, keeping track of
# the columns in the contrast matrix.
X, cons = stack_designs((X_exper, cons_exper), (drift, {}))
plt.imshow(X, aspect=0.1)
First we do an OLS fit of this model to the data, rearranged to a time by voxel array:
model_0 = OLSModel(X)
data = func_img.get_data()
data_2d = data.reshape(-1, n_vols).T
result = model_0.fit(data_2d)
result
betas_2d = result.theta
betas_2d.shape
We rearrange the beta matrix from number of parameters (==P) by voxel, to (I, J, K, P).
vol_shape = data.shape[:3]
betas_4d = betas_2d.T.reshape(vol_shape + (-1,))
Here is the center slice from the first parameter volume (p=0). This is the parameter for the HRF regressor. It shows some activation in the left frontal lobe.
plt.imshow(betas_4d[:, :, 15, 0])
Next we make an estimate for the AR(1) parameter from the residuals of the OLS model.
result.resid.shape
# Estimate AR(1) coefficient
lag_1 = (result.resid[1:] * result.resid[:-1]).sum(axis=0)
resid_ss = (result.resid**2).sum(axis=0)
nz_ss = resid_ss != 0
ar1_estimate = lag_1
ar1_estimate[~nz_ss] = 0
ar1_estimate[nz_ss] /= resid_ss[nz_ss]
ar1_estimate.shape
# We round AR(1) to nearest one-hundredth and group voxels by their rounded
# AR(1) value, fitting an AR(1) model to each batch of voxels.
ar1_estimate = np.round(ar1_estimate * 100) / 100.
white_betas_2d = np.zeros_like(betas_2d)
# Loop across bins estimating model for each
for ar_val in np.unique(ar1_estimate):
ar_mask = ar1_estimate == ar_val
m = ARModel(X, ar_val)
d = data_2d[:,ar_mask]
results = m.fit(d)
white_betas_2d[:, ar_mask] = results.theta
Here are the estimates for the first parameter (HRF parameter) after AR(1) modeling:
white_betas_4d = white_betas_2d.T.reshape(vol_shape + (-1,))
plt.imshow(white_betas_4d[:, :, 15, 0])
plt.imshow(white_betas_4d[:, :, 15, 0] - betas_4d[:, :, 15, 0])