===== Introduction ===== This tutorial will take you through fitting a pRF encoding model (see [[http://www.sciencedirect.com/science/article/pii/S1053811907008269|Dumoulin and Wandell (2008)]] for details) to actual data. You will write all the code to do this - either in Matlab or Python (there are worked examples of code for both Matlab and Python). You can check [[:mrtools:tutorialsprf|here]] for some information on how pRF fitting works and introduction to a GUI implementation. Here there will be no GUI, you will code the analysis from first principles using nothing but simple matrix computations. The full tutorial takes you through fitting a pRF model to data using a grid search, plotting the data on a triangulated cortical surface, fitting with a non-linear search algorithm (nelder-mead) and then using your fit model to decode a stimulus that was never used in training. A few snapshots from the process to give you an idea of what you will get by going through the tutorial below:
git clone https://github.com/justingardner/pRFtutorial.git pRFtutorial
cd pRFtutorial
- **Start matlab**
- **load datafiles**
load pRF
To setup for Python:
- **Get datafiles** Get the git repo with the datafiles
git clone https://github.com/justingardner/pRFtutorial.git pRFtutorial
cd pRFtutorial
- **Start iPython**
- **Load data**
# import numpy for matrix operations
import numpy
# import scipy for optimization routines
import scipy
# import scipy for the matlab reading code
import scipy.io as sio
# setup matplotlib
import matplotlib.pyplot as plt
# Load the data
pRFLoad = sio.loadmat('pRF.mat')
# get the stored matrices
stimImage = pRFLoad['stimImage']
stimX = pRFLoad['stimX']
stimY = pRFLoad['stimY']
canonical = numpy.transpose(pRFLoad['canonical'])
canonical = canonical[:,0]
c = pRFLoad['c']
v = pRFLoad['v']
t = pRFLoad['t']
tSeries = pRFLoad['tSeries'];
tSeries1 = pRFLoad['tSeries1'];
tSeries2 = pRFLoad['tSeries2'];
tSeries1 = tSeries1[:,0];
tSeries2 = tSeries2[:,0];
unknownTSeries = pRFLoad['unknownTSeries']
possibleStimImages = pRFLoad['possibleStimImages'];
base2scan = pRFLoad['base2scan']
roiScanCoords = pRFLoad['roiScanCoords'];
===== Look at the stimulus =====
Let's start by just taking a look at the stimulus that was presented to the subject. Remember the subject was looking at the center of the screen where there was a fixation cross on which they had to perform a task (just to keep their attentional state and fixation under control) and bars moving in different directions were shown. Each frame of the image is stored as a 2D matrix that has the pixel values for each position on the screen. For this purpose, the matrix just contains 0's and 1's. Each of these 2D frames are stacked into a 3D matrix where the third dimension is time. So, good. Let's see if we can figure out what the stimulus looked like on some arbitrary frame of the stimulus, say frame 36. You should be able to do that just by looking at the values. The variable that holds the stimulus images is called: stimImage
++++Matlab coding hint|In matlab if you want to look at the values of a matrix just type its name. If you want to look at see all the values at one index value then you can specify that like this stimImage(:,:,36).++++
Probably there were too many values to look at all at once, so let's look at a subset of values between 70 and 85 and see if you can tell what the stimulus was.
++++Matlab coding hint|Ranges are specified as start:end, so to look at the range of values between 70 and 85, you would stimImage(70:85,70:85,36)++++
++++Python cheat sheet|stimImage[69:84,69:84,35]++++
Got the answer?
++++Answer|Horizontal bar++++
Ok, it might be easier to look at this as an image, so let's go ahead and display the whole thing and see what you get.
++++Matlab coding hint|Use the command imagesc++++
++++Matlab Cheat sheet|
% Display the 36th image
imagesc(stimImage(:,:,36));
% set the colormap to grayscale so that you get a black and white image
colormap(gray);
++++
++++Python cheat sheet|
# Display the 36th image, with the gray colormap
plt.imshow(stimImage[:,:,36],cmap="gray");
++++
++++Answer|You should get something like this
% loop over all frames of the image
for iFrame = 1:size(stimImage,3)
% draw it
imagesc(stimImage(:,:,iFrame));
% and flush graphics events
drawnow
end
++++
++++Python Cheat sheet|
# This for some reason is excruciatingly slow on my machine
# so only go 30 images instead of showing the whole thing...
# get figure
fig = plt.figure()
# loop over all frames of the image
for iFrame in range(0,30):
# draw it
plt.imshow(stimImage[:,:,iFrame],cmap="gray");
# flush
fig.canvas.draw()
++++
Good, you should have seen a bar move in various directions across the screen. That was what was shown to the subject. Run again, and count how many times a bar crossed the screen - we'll need that number as a consistency check in a following section!
++++Answer|8 times++++
===== Compute a gaussian receptive field =====
Now we need to make our encoding model. In this case it is a simple gaussian receptive field. Let's start by making an arbitrary one at the location (-12, -3) degrees (i.e. 12 degrees to the left and 3 degrees below center) We also need to set the size of the receptive field, let's do one that has a standard deviation of 3 degrees. So, first, your going need to know the equation of a gaussian, right?
++++Equation|
% set variables for the center and width
xCenter = -12;yCenter = -3;rfstd = 3;
% compute the receptive field
rf = exp(-((stimX-xCenter).^2 + (stimY-yCenter).^2)/(2*rfstd^2));
% and display
imagesc(rf);
% use the "hot" colormap to make it prettier
colormap hot
++++
++++Python cheat sheet|
# set variables for the center and width
xCenter = -12
yCenter = -3
rfstd = 3
# compute the receptive field
rf = numpy.exp(-(numpy.power(stimX-xCenter,2) + numpy.power(stimY-yCenter,2))/(2*rfstd**2));
# and display
plt.imshow(rf,cmap="hot");
++++
++++Answer|You should get something like this
% loop over frames
for iFrame = 1:size(stimImage,3)
% compute overlap
rfStimulusOverlap = stimImage(:,:,iFrame) .* rf;
% sum over all points
modelNeuralResponse(iFrame) = sum(sum(rfStimulusOverlap));
end
% plot what we got
plot(modelNeuralResponse);
++++
++++Python cheat sheet|
# init array
modelNeuralResponse = numpy.zeros(stimImage.shape[2])
# loop over frames
for iFrame in range(0,stimImage.shape[2]):
# compute overlap
rfStimulusOverlap = numpy.multiply(stimImage[:,:,iFrame],rf);
# sum over all points
modelNeuralResponse[iFrame] = numpy.sum(rfStimulusOverlap);
# plot what we got
plt.clf()
plt.plot(modelNeuralResponse);
++++
How many peaks in this simulated neural response do you see? Does it match the number of times there was a bar moving across the visual field that you counted above?
++++Answer|You should see something like this
% do the convolution
modelBoldResponse = conv(modelNeuralResponse,canonical);
% and display
plot(modelBoldResponse);
++++
++++Python Cheat sheet|
# do the convolution
modelBoldResponse = numpy.convolve(modelNeuralResponse,canonical);
# and display
plt.clf()
plt.plot(modelBoldResponse);
++++
++++Answer|Should look like the following
clf;
hold on;
plot(modelNeuralResponse,'k-');
plot(modelBoldResponse,'r-');
++++
++++Python Cheat sheet|
plt.clf();
plt.plot(modelNeuralResponse,'k-');
plt.plot(modelBoldResponse,'r-');
++++
++++Answer|Should look like the following
modelBoldResponse = modelBoldResponse(1:length(modelNeuralResponse))';
++++
++++Python cheat sheet|
modelBoldResponse = numpy.transpose(modelBoldResponse[0:modelNeuralResponse.shape[0]]);
++++
===== Compare the model to measured responses =====
Ok. So, we can compute the model BOLD response for a specific receptive field - now we want to know how to compare that to measured responses. We're going to use the time-point by time-point correlation between the model and the measured responses. Correlation is a nice measure to work with, since it isn't affected by the magnitude of responses. Remember that we haven't really done anything to make the units of the model match the units of response (look back at the units you have for the model BOLD response - typical BOLD responses are about 1 or 2% signal change - is that the size of the fluctuations you have in the model? (no. right!). We could fit a parameter there so that we get the model output to match in magnitude to the BOLD response (how would you do that?), but here we'll just ignore the whole issue by using correlation. Ok, we have two actual time series for you to work with //tSeries1// and //tSeries2// (they were measured from a subject viewing the stimuli). Go ahead and compute the correlation between these time series and the measured bold response from above. What do you get? For which tSeries is this model rf a good model for?
++++Matlab coding hint|Do a help on //corr//++++
++++Matlab cheat sheet|
corr(tSeries1,modelBoldResponse)
corr(tSeries2,modelBoldResponse)
++++
++++Pyton cheat sheet|
numpy.corrcoef(tSeries1,modelBoldResponse)[0,1]
numpy.corrcoef(tSeries2,modelBoldResponse)[0,1]
++++
++++Answer|
The correlation for tSeries1 is: 0.839672
The correlation for tSeries2 is: 0.1711145
So, the model RF is best for tSeries1.
++++
Ok. Let's confirm that visually, by plotting the model and the time series together and see that they match. Now, we have to worry a little bit about the magnitudes which are going to be different between the model and the actual bold responses - let's handle this by normalizing the model and the bold responses to have zero mean and vector length of 1 (i.e. subtract the mean and divide by the square root of the sum of squares). This normalization is useful since later it will allow us to compute the correlation efficiently. Probably worth noting here an important maxim. Anything you do to your data - you are going to need to do to the model as well. So if we divide by the mean of the bold responses, then divide by the mean in the model as well. If you high pass filter your data, then you need to high pass filter your data. Ok. Go ahead and do that and then plot the model and the time series.
++++Matlab coding hint|The square of every value in an array can be computed using .^2. You already know about //sum// and square root is //sqrt//++++
++++Matlab cheat sheet|
% normalize time series
tSeries1 = tSeries1-mean(tSeries1);
tSeries1 = tSeries1/sqrt(sum(tSeries1.^2));
tSeries2 = tSeries2-mean(tSeries2);
tSeries2 = tSeries2/sqrt(sum(tSeries2.^2));
% everything that is done to the data should also be done to the model
modelBoldResponse = modelBoldResponse-mean(modelBoldResponse);
modelBoldResponse = modelBoldResponse/sqrt(sum(modelBoldResponse.^2));
% now plot
clf;
hold on;
plot(tSeries1,'k-');
plot(tSeries2,'b-');
plot(modelBoldResponse,'r-');
% label stuff
xlabel('Volume number');
ylabel('Model and time series response');
legend('tSeries1','tSeries2','modelBoldResponse')
++++
++++Python cheat sheet|
# normalize time series
tSeries1 = tSeries1-numpy.mean(tSeries1);
tSeries1 = tSeries1/numpy.sqrt(numpy.sum(numpy.power(tSeries1,2)));
tSeries2 = tSeries2-numpy.mean(tSeries2);
tSeries2 = tSeries2/numpy.sqrt(numpy.sum(numpy.power(tSeries2,2)));
# everything that is done to the data should also be done to the model
modelBoldResponse = modelBoldResponse-numpy.mean(modelBoldResponse);
modelBoldResponse = modelBoldResponse/numpy.sqrt(numpy.sum(numpy.power(modelBoldResponse,2)));
# now plot
plt.clf()
plt.plot(tSeries1,'k-')
plt.plot(tSeries2,'b-')
plt.plot(modelBoldResponse,'r-')
# label stuff
plt.xlabel('Volume number')
plt.ylabel('Model and time series response')
plt.legend(['tSeries1','tSeries2','modelBoldResponse'])
++++
So, should be a great match for tSeries1 and a pretty bad match for tSeries2.
++++Answer|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Note that running this code may take a minute or two to complete...
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% init space for modelResponses and params
k = 4805;n = size(stimImage,3);
modelResponses = nan(k,n);
params = nan(k,3);
% index for where to put each model response
modelNum = 1;
% loop over x
for x = -15:15
% loop over y
for y = -15:15
% loop over s
for s = 1:5
% get the rf
rf = exp(-((stimX-x).^2 + (stimY-y).^2)/(2*s^2));
% instead of looping over frames, we use a slight trick
% to do this as a matrix operation - we replicate the rf for
% each time point and then compute the sum over all points in one pass
% matlab is very fast with matrix operations, so if you can rewrite any
% loop as a matrix operation it will generally speed things up significantly
modelNeuralResponse = squeeze(sum(sum(stimImage .* repmat(rf,1,1,n))));
% do the convolution
modelBoldResponse = conv(modelNeuralResponse,canonical);
% trim length
modelBoldResponse = modelBoldResponse(1:n)';
% normalize
modelBoldResponse = modelBoldResponse-mean(modelBoldResponse);
modelBoldResponse = modelBoldResponse/sqrt(sum(modelBoldResponse.^2));
% now place into giant matrix
modelResponses(modelNum,:) = modelBoldResponse;
% keep the params
params(modelNum,:) = [x y s];
% update modelNum
modelNum = modelNum + 1;
end
end
% display something so that we know that things are progressing
disp(sprintf('Computed receptive fields for x = %i',x));
end
++++
++++Python cheat sheet|
# to paste this into the buffer, copy the text below and then in the
# ipython window type %paste and hit enter
#################################
# Note that running this code may take a minute or two to complete...
#################################
# init space for modelResponses and params
k = 4805
n = stimImage.shape[2]
modelResponses = numpy.zeros([k,n])
params = numpy.zeros([k,3])
# index for where to put each model response
modelNum = 1
# loop over x
for x in range(-15,15):
# loop over y
for y in range(-15,15):
# loop over s
for s in range(1,5):
# compute the receptive field
rf = numpy.exp(-(numpy.power(stimX-x,2) + numpy.power(stimY-y,2))/(2*s**2))
# init array
modelNeuralResponse = numpy.zeros(stimImage.shape[2])
# loop over frames
for iFrame in range(0,stimImage.shape[2]):
# compute overlap
rfStimulusOverlap = numpy.multiply(stimImage[:,:,iFrame],rf)
# sum over all points
modelNeuralResponse[iFrame] = numpy.sum(rfStimulusOverlap)
# do the convolution
modelBoldResponse = numpy.convolve(modelNeuralResponse,canonical)
# trim length
modelBoldResponse = numpy.transpose(modelBoldResponse[0:modelNeuralResponse.shape[0]])
# normalize
modelBoldResponse = modelBoldResponse-numpy.mean(modelBoldResponse)
modelBoldResponse = modelBoldResponse/numpy.sqrt(numpy.sum(numpy.power(modelBoldResponse,2)))
# now place into giant matrix
modelResponses[modelNum,:] = modelBoldResponse
# keep the params
params[modelNum,:] = [x,y,s]
# update modelNum
modelNum = modelNum + 1
# display something so that we know that things are progressing
print('Computed receptive fields for x =',x)
++++
Ok, now that we have these matrix of modelResponses for a grid of different parameter settings, let's do some sanity checks to make sure we have what we think we have (As you see, I'm a big believe in sanity checks!). First, plot a few model responses. Do they have 8 peaks as you expect? Second, remember that tSeries1 was best correlated with the rf we calculated above for [-12, -3] and a standard deviation of 3? That should be true here to. First, go find which modelNum has those parameters.
++++hint|528 (411 for python)++++
Now compute the correlation between tSeries1 and tSeries2 for that modelRow number, you should be able to replicate the correlation numbers from above. Note that you will need to take the transpose of the modelResponses to make the dimensions work out correctly.
++++Matlab cheat sheet|
% check correlation with tSeries1
corr(modelResponses(528,:)',tSeries1)
% check correlation with tSeries2
corr(modelResponses(528,:)',tSeries2)
++++
++++Python cheat sheet|
# check correlation with tSeries1
numpy.corrcoef(modelResponses[411,:],tSeries1)[0,1]
# check correlation with tSeries2
numpy.corrcoef(modelResponses[411,:],tSeries2)[0,1]
++++
Everything check out? Good. Now let's find a better set of parameters for tSeries2. We can do this by computing the correlation of tSeries2 against every model in the matrix. We could build a for loop to do this, but it's actually much simper then that. Correlation is computed as the dot product of two unit length vectors (remember I told you that normalization was going to be useful?) So, all we have to do is compute the matrix product of the modelResponses matrix with the tSeries and we will get all the correlation values we need. So, let's try first with tSeries1 and confirm we get back the model that we already know is best. Find the correlation that is maximum - this should be the one with parameters [-12, -3] and standard deviation of 3.
++++Matlab cheat sheet|
% compute correlation with every modelResponses by taking the matrix product
r = modelResponses*tSeries1;
% now find the correlation that is highest
[maxR bestModel] = max(r);
% check what those parameters are
params(bestModel,:)
++++
++++Python cheat sheet|
# compute correlation with every modelResponses by taking the matrix product
r = numpy.matmul(modelResponses,tSeries1);
# now find the correlation that is highest
maxR = numpy.max(r)
bestModel = numpy.argmax(r)
# check what those parameters are
params[bestModel,:]
++++
If that worked, then try it with the other time series, tSeries2, and you should get a model with good fit. Go ahead and plot that best model against tSeries2 to confirm that is the case.
++++Matlab cheat sheet|
% compute correlation with every modelResponses by taking the matrix product
r = modelResponses*tSeries2;
% now find the correlation that is highest
[maxR bestModel] = max(r);
% check what those parameters are
params(bestModel,:)
% plot the model and the response
clf;hold on;
plot(modelResponses(bestModel,:),'r-');
plot(tSeries2,'k-');
% some labels
xlabel('Time in volumes');
ylabel('Response');
legend('Best model','tSeries2');
++++
++++Python cheat sheet|
# compute correlation with every modelResponses by taking the matrix product
r = numpy.matmul(modelResponses,tSeries2);
# now find the correlation that is highest
maxR = numpy.max(r)
bestModel = numpy.argmax(r)
# check what those parameters are
params[bestModel,:]
# plot the model and the response
plt.clf()
plt.plot(modelResponses[bestModel,:],'r-');
plt.plot(tSeries2,'k-');
# some labels
plt.xlabel('Time in volumes');
plt.ylabel('Response');
plt.legend(['Best model','tSeries2']);
++++
++++Answer|
The best fitting parameters are [-3, -9] with a standard deviation of 5
% do the grid search to find the model with the best
% correlation with each time series
[maxR bestModel] = max(modelResponses*tSeries');
++++
++++Python cheat sheet|
# do the grid search to find the model with the best
# correlation with each time series
r = numpy.matmul(modelResponses,numpy.transpose(tSeries))
maxR = numpy.max(r,0)
bestModel = numpy.argmax(r,0)
++++
Ok. You guessed it - now we need to do a sanity check to make sure that we got the right thing. We know that the best model for tSeries1 was 528, so let's make sure that's true for this computation to. In the tSeries matrix, the tSeries1 is the 134 response. So check maxR and bestModel and make sure that you get the expected values.
And another sanity check. Receptive fields in primary visual cortex should all be contralateral. The set of voxels you just fit were all in the right visual cortex - so let's make sure that the receptive fields found were all in the left visual field. To do that, let's figure out how to plot the contour of a receptive field. Remember the equation we used for the gaussian? Let's plot a set of x,y points for the one standard deviation contour. How do we do that? We can do that by finding all the y vales for a set of x values using the equation. So, take all x values from -std to +std and find the appropriate y-values. You will need to rearrange the gaussian equation to get y as a function of x.
++++hint|Find out what the equation equals when x is one std and y is 0. Set the gaussian equation equal to that and take the logarithm of both sides to get rid of the exponent++++
Got it? Then start by plotting a gaussian contour with center at, say, [-5, +3] and std = 3.
++++Matlab cheat sheet|
% set the center and std
centerX = -5;centerY = 3; rfstd = 3;
% set the x-values
x = -rfstd:0.1:rfstd;
% clear the graph
clf;hold on
% plot the positive y-values
plot(x+centerX,sqrt(rfstd^2 - x.^2)+centerY,'k-');
% plot the negative y-values
plot(x+centerX,-sqrt(rfstd^2 - x.^2)+centerY,'k-');
++++
++++Python cheat sheet|
# set the center and std
centerX = -5
centerY = 3
rfstd = 3
# set the x-values
x = numpy.linspace(-rfstd,rfstd,100)
# clear the graph
plt.clf()
# plot the positive y-values
plt.plot(x+centerX,numpy.sqrt(numpy.power(rfstd,2) - numpy.power(x,2))+centerY,'k-');
# plot the negative y-values
plt.plot(x+centerX,-numpy.sqrt(numpy.power(rfstd,2) - numpy.power(x,2))+centerY,'k-');
++++
You should see something that looks like a circle. If you got that, then now go ahead and use that code to plot all the contours for all the fit receptive fields.
++++Matlab cheat sheet|
% clear the figure
clf; hold on;
% cycle over voxels
for iVox = 1:length(bestModel)
% get the parameters
centerX = params(bestModel(iVox),1);
centerY = params(bestModel(iVox),2);
rfstd = params(bestModel(iVox),3);
% now plot the RF
% set the x-values
x = -rfstd:0.1:rfstd;
% plot the positive y-values
plot(x+centerX,sqrt(rfstd^2 - x.^2)+centerY,'k-');
% plot the negative y-values
plot(x+centerX,-sqrt(rfstd^2 - x.^2)+centerY,'k-');
end
% set axis and label
axis([-20 20 -20 20]);
xlabel('Horizontal position (deg)');
ylabel('vertical position (deg)');
++++
++++Python cheat sheet|
# You may want to use the %paste trick here...
# clear the figure
plt.clf()
# cycle over voxels
for iVox in range(0,bestModel.size):
# get the parameters
centerX = params[bestModel[iVox],0];
centerY = params[bestModel[iVox],1];
rfstd = params[bestModel[iVox],2];
# now plot the RF
# set the x-values
x = numpy.linspace(-rfstd,rfstd,100)
# plot the positive y-values
plt.plot(x+centerX,numpy.sqrt(numpy.power(rfstd,2) - numpy.power(x,2))+centerY,'k-');
# plot the negative y-values
plt.plot(x+centerX,-numpy.sqrt(numpy.power(rfstd,2) - numpy.power(x,2))+centerY,'k-');
# set axis and label
plt.xlim(-20,20)
plt.ylim(-20,20)
plt.xlabel('Horizontal position (deg)');
plt.ylabel('vertical position (deg)');
++++
Does it look like most of the receptive fields are in the left visual field, as expected?
++++answer|
% clear the figure
clf; hold on;
% cycle over voxels
for iVox = 1:length(bestModel)
if maxR(iVox) > 0.4
% get the parameters
centerX = params(bestModel(iVox),1);
centerY = params(bestModel(iVox),2);
rfstd = params(bestModel(iVox),3);
% now plot the RF
% set the x-values
x = -rfstd:0.1:rfstd;
% plot the positive y-values
plot(x+centerX,sqrt(rfstd^2 - x.^2)+centerY,'k-');
% plot the negative y-values
plot(x+centerX,-sqrt(rfstd^2 - x.^2)+centerY,'k-');
end
end
% set axis and label
axis([-20 20 -20 20]);
xlabel('Horizontal position (deg)');
ylabel('vertical position (deg)');
++++
++++Python cheat sheet|
# You may want to use the %paste trick here...
# clear the figure
plt.clf()
# cycle over voxels
for iVox in range(0,bestModel.size):
if maxR[iVox] > 0.4:
# get the parameters
centerX = params[bestModel[iVox],0];
centerY = params[bestModel[iVox],1];
rfstd = params[bestModel[iVox],2];
# now plot the RF
# set the x-values
x = numpy.linspace(-rfstd,rfstd,100)
# plot the positive y-values
plt.plot(x+centerX,numpy.sqrt(numpy.power(rfstd,2) - numpy.power(x,2))+centerY,'k-');
# plot the negative y-values
plt.plot(x+centerX,-numpy.sqrt(numpy.power(rfstd,2) - numpy.power(x,2))+centerY,'k-');
# set axis and label
plt.xlim(-20,20)
plt.ylim(-20,20)
plt.xlabel('Horizontal position (deg)');
plt.ylabel('vertical position (deg)');
++++
Cool. One last thing to check here, if it really is the case that those pRFs with low correlation are bad fits, then you should be able to go find one of those voxels with a low r and plot it against the model time course and see that it doesn't look good.
++++Matlab cheat sheet|
% find all the voxels with correlations greater than 0.4
find(maxR<0.4)
% let's just choose an arbitrary one to look at, say voxel 58
checkVoxel = 58;
% find out what it's parameters were
params(bestModel(checkVoxel),:)
% plot it's time series - make sure you have which dimension is which, correct!
clf;hold on
plot(tSeries(checkVoxel,:),'k-');
plot(modelResponses(bestModel(checkVoxel),:),'r-');
% label graph
xlabel('Time in volumes');
ylabel('Response');
++++
++++Python cheat sheet|
# find all the voxels with correlations greater than 0.4
numpy.where(maxR<0.4)
# let's just choose an arbitrary one to look at, say voxel 57
checkVoxel = 57;
# find out what it's parameters were
params[bestModel[checkVoxel],:]
# plot it's time series - make sure you have which dimension is which, correct!
plt.clf()
plt.plot(tSeries[checkVoxel,:],'k-');
plt.plot(modelResponses[bestModel[checkVoxel],:],'r-');
% label graph
plt.xlabel('Time in volumes');
plt.ylabel('Response');
++++
++++Answer|
clf
% draw the surface
patch('vertices',v,'faces',t,'FaceVertexCData',c,'facecolor','interp','edgecolor','none')
% set the axis to make it display nicely
axis equal
axis off
If you click the circular arrow tool in the figure, you should be able to use the mouse to turn the surface around to view it from any angle you like.
++++Answer|
% pick an arbitrary vertex
vertexIndex = 1640;
% and compute where it lives in the scan
vertexScanCoords = base2scan * [v(vertexIndex,:) 1]'
++++
++++Answer|
vertexScanCoords =
39.0905160547765
75.1531615844523
21.9470110421143
1
++++
Now that we know where it lives in the scan, we need to figure out if it is close to any of the voxels that we have computed the pRF for. To do that, compute the euclidean distance from each one of the roiScanCoords and see which one it is closest to. Note that roiScanCoords is a 3xr matrix where r is the number of voxels in the roi.
++++Cheat sheet|
% loop over voxels in roi
for iVox = 1:size(roiScanCoords,2)
% compute distance
d(iVox) = sqrt(sum((vertexScanCoords(1:3) - roiScanCoords(1:3,iVox)).^2));
end
% now see which is the closest.
[minD minIndex] = min(d)
++++
++++Answer|
minD =
0.185632585453267
minIndex =
50
++++
Ok. Good, now we know how to find which voxel is closest, we should be able to set the color of that vertex accordingly . Let's choose a value from the hot colormap according to the r value. To do this, we get the hot colormap for 256 colors - and then map the r value (0-1) into the range `1:256. Try that out.
++++Matlab coding hint|Do a help on hot. You may also need to use //ceil//.++++
++++Cheat sheet|
% get the hot colormap
nColors = 256;
hotmap = hot(nColors);
% get the correlation value of the closest voxel
r = maxR(minIndex);
% convert r to a number between 1 and 256
colormapIndex = ceil(r*nColors);
% display the color value
hotmap(colormapIndex,:)
++++
++++Answer|
ans =
1 0.104166666666667 0
++++
Got it? Then let's put this all together. Compute a new colormap for the surface in which you put the appropriate hotmap colors in according to the max correlation value for the corresponding voxel from the pRF. Also, if the distance between the vertex and the pRF scan coordinate is too far away, just use the original grayscale curvature value.
++++Cheat sheet|
% get the hot colormap
nColors = 256;
cmap = hot(nColors);
% figure out where each surface vertex lives in the functional scan.
vScan = (base2scan*[v ones(size(v,1),1)]')';
% cycle over vertices in the surface
for iVertex = 1:size(vScan,1)
% get the distance between the current vertex and all of the voxels that we have in the scan
% we're using the repmat trick again so that we don't need to do a for loop here
d = sqrt(sum((roiScanCoords - repmat(vScan(iVertex,1:3)',1,size(roiScanCoords,2))).^2));
% find the voxel that is the closest
[minD minIndex] = min(d);
% if it is closer than 2 voxels then use the correlation value (actually, this should
% probably be more like half a voxel - but we're making it a bit bigger here so that
% it fills in some holes that are missing since our roi has fewer voxels then it could have in it).
if (minD < 2)
% set the color according to the correlation value
vertexColor(iVertex,:) = cmap(ceil(maxR(minIndex)*256),:);
else
%otherwise set the color to the curvature
vertexColor(iVertex,:) = c(iVertex,:);
end
end
% display the surface
clf;
patch('vertices',v,'faces',t,'FaceVertexCData',vertexColor,'facecolor','interp','edgecolor','none');
% set the axis
axis off
axis equal
++++
++++Answer|
% cycle over voxels
for iVox = 1:length(bestModel)
% get x and y for this voxel
x = params(bestModel(iVox),1);
y = params(bestModel(iVox),2);
% if x is 0, we have a special case
if x == 0
if y >= 0
% positive y, then polarAngle is 90 degrees
polarAngle(iVox) = pi/2;
else
% negative y, then polarAngle is -90 degrees
polarAngle(iVox) = -pi/2;
end
elseif (x >= 0)
% use the arctan of opposite over adjacent to get polar angle
polarAngle(iVox) = atan(y/x);
else
% for fields in the left, need to flip the output of atan
if y >= 0
polarAngle(iVox) = pi-atan(y/abs(x));
else
polarAngle(iVox) = -pi-atan(y/abs(x));
end
end
end
++++
Now that we have the polar angle for each voxel, put them on the surface just like you do for the r values - this time scaling the range -pi to pi between 1 and 256. You may want to use a different colormap - like hsv so that the polar angles are easy to distinguish
++++Cheat sheet|
% get the hot colormap
nColors = 256;
cmap = hsv(nColors);
% figure out where each surface vertex lives in the functional scan.
vScan = (base2scan*[v ones(size(v,1),1)]')';
% cycle over vertices in the surface
for iVertex = 1:size(vScan,1)
% get the distance between the current vertex and all of the voxels that we have in the scan
% we're using the repmat trick again so that we don't need to do a for loop here
d = sqrt(sum((roiScanCoords - repmat(vScan(iVertex,1:3)',1,size(roiScanCoords,2))).^2));
% find the voxel that is the closest
[minD minIndex] = min(d);
% if it is closer than 2 voxels then use the correlation value (actually, this should
% probably be more like half a voxel - but we're making it a bit bigger here so that
% it fills in some holes that are missing since our roi has fewer voxels then it could have in it).
if (minD < 2)
% set the color according to the correlation value
vertexColor(iVertex,:) = cmap(ceil((polarAngle(minIndex)+pi)/(2*pi)*256),:);
else
%otherwise set the color to the curvature
vertexColor(iVertex,:) = c(iVertex,:);
end
end
% display the surface
clf;
patch('vertices',v,'faces',t,'FaceVertexCData',vertexColor,'facecolor','interp','edgecolor','none');
% set the axis
axis off
axis equal
% draw a colorbar for reference
colormap(cmap);
h = colorbar;
% and label it with the polar angles
set(h,'YTick',[0 0.25 0.5 0.75 1]);
set(h,'YTickLabel',{-180 -90 0 90 180})
++++
Sanity check! Does it look like the upper visual field is on the lower bank of the Calcarine and the lower visual field is on the upper bank?
++++Answer|
% function declarations
function valueToBeMinimized = pRFCorrelation(params,stimImage,stimX,stimY,canonical,tSeries)
% do your stuff
...
% set the return value
valueToBeMinimized = 1-r;
++++
++++Matlab cheat sheet|
% function that will be called by Nelder-Mead algorithm to compute
% the corelation of the model with the params and the tSeries.
% It needs to return 1-correlation because Nelder-Mead minimizes
function valueToBeMinimized = pRFCorr(params,stimImage,stimX,stimY,canonical,tSeries)
% number of volumes
n = length(tSeries);
% unpack parameters
x = params(1);
y = params(2);
s = params(3);
% compute rf
rf = exp(-((stimX-x).^2 + (stimY-y).^2)/(2*s^2));
% compute model neural response
modelNeuralResponse = squeeze(sum(sum(stimImage .* repmat(rf,1,1,n))));
% compute the model bold response from the above
modelBoldResponse = conv(modelNeuralResponse,canonical);
% trim length
modelBoldResponse = modelBoldResponse(1:n)';
% normalize
modelBoldResponse = modelBoldResponse-mean(modelBoldResponse);
modelBoldResponse = modelBoldResponse/sqrt(sum(modelBoldResponse.^2));
% compute correlation (note the (:) which is just to make sure
% that both arrays are column vectors
r = corr(modelBoldResponse(:),tSeries(:));
% and return 1-r
valueToBeMinimized = 1-r;
++++
++++Python cheat sheet|
# function that will be called by Nelder-Mead algorithm to compute
# the corelation of the model with the params and the tSeries.
# It needs to return 1-correlation because Nelder-Mead minimizes
def pRFCorr(params,stimImage,stimX,stimY,canonical,tSeries):
# number of volumes
n = tSeries.size
# unpack parameters
x = params[0]
y = params[1]
s = params[2]
# compute rf
rf = numpy.exp(-(numpy.power(stimX-x,2) + numpy.power(stimY-y,2))/(2*s**2))
# init array
modelNeuralResponse = numpy.zeros(stimImage.shape[2])
# loop over frames
for iFrame in range(0,stimImage.shape[2]):
# compute overlap
rfStimulusOverlap = numpy.multiply(stimImage[:,:,iFrame],rf)
# sum over all points
modelNeuralResponse[iFrame] = numpy.sum(rfStimulusOverlap)
# compute the model bold response from the above
modelBoldResponse = numpy.convolve(modelNeuralResponse,canonical)
# trim length
modelBoldResponse = numpy.transpose(modelBoldResponse[0:modelNeuralResponse.shape[0]])
# normalize
modelBoldResponse = modelBoldResponse-numpy.mean(modelBoldResponse)
modelBoldResponse = modelBoldResponse/numpy.sqrt(numpy.sum(numpy.power(modelBoldResponse,2)))
# compute correlation (note the (:) which is just to make sure
# that both arrays are column vectors
r = numpy.corrcoef(modelBoldResponse,tSeries)[0,1]
# and return 1-r
return 1-r
++++
Ok, what next? A sanity check, of course. Use the parameter values from the beginning of this tutorial and see whether you get back the correct correlation values for tSeries1 and tSeries2.
++++Cheat sheet|
>> 1-pRFCorrelation([-12 -3 3],stimImage,stimX,stimY,canonical,tSeries1)
ans =
0.8396721
>> 1-pRFCorrelation([-12 -3 3],stimImage,stimX,stimY,canonical,tSeries2)
ans =
0.1711145
++++
++++Python cheat sheet|
In [27]: 1-pRFCorr([-12,-3,3],stimImage,stimX,stimY,canonical,tSeries1)
Out[27]: 0.83967201549958426
In [28]: 1-pRFCorr([-12,-3,3],stimImage,stimX,stimY,canonical,tSeries2)
Out[28]: 0.17111449220774433
++++
Now, we need to figure out how to call the non-linear fitting function which in matlab is called fminsearch. It takes a **function pointer** (that's just the function name with an @ sign before it: @pRFCorr) and the starting parameters. What starting parameters should we use? Well, we already have a good starting place from the grid search! So use those as the starting parameters. It then takes some options, we can be set by optionset, and then the parameters for the function pRFCorr. So, it looks like the following:
++++Matlab cheat sheet|
% set default options
options = optimset;
% run fminsearch
fminsearch(@pRFCorr,[-12 -3 3],options,stimImage,stimX,stimY,canonical,tSeries1)
++++
++++Python cheat sheet|
scipy.optimize.minimize(pRFCorr,[-12,-3,2],(stimImage,stimX,stimY,canonical,tSeries1),method='Nelder-Mead')
++++
++++Answer|
You should have gotten back parameters like: -11.7454432888196 -3.44219061246559 3.31014763992902
++++
But, are these really right? When debugging a non-linear fit, it is a really, really good idea to check out what it is doing by looking at how the fit changes as it searches the parameter space - at least for a voxels - once you are sure that it is doing what you hope to do (sometimes non-linear fits can find really weird parts of the parameter space that can get a fit by optimizing for a noisy feature of the data - for example a big spike in the data can be produced by a model that gets a really tiny receptive field that only gets excited at that one time point. So, let's put some drawing code into the pRFCorr function so that we can see what is happening as we fit.
So, add the following code to the end of the pRFCorr routine:
++++Matlab drawing code|
% display the fit - this is worthwhile to do, particularly while testing
% to see what the non-linear fit is doing (which can sometimes do some
% really weird things)
clf;
subplot(1,3,1:2);
hold on
tSeries = tSeries-mean(tSeries);
tSeries = tSeries/sqrt(sum(tSeries.^2));
plot(tSeries,'k-');
plot(modelBoldResponse,'r-');
xlabel('Time (volumes)');
ylabel('Response');
title(sprintf('[%0.3f %0.3f] std: %0.3f corr: %0.4f',x,y,s,r));
subplot(1,3,3);
imagesc(rf);
colormap(hot);
axis square
axis off
drawnow
++++
++++Python drawing code|
# function that will be called by Nelder-Mead algorithm to compute
# the corelation of the model with the params and the tSeries.
# It needs to return 1-correlation because Nelder-Mead minimizes
def pRFCorr(params,stimImage,stimX,stimY,canonical,tSeries):
# number of volumes
n = tSeries.size
# unpack parameters
x = params[0]
y = params[1]
s = params[2]
# compute rf
rf = numpy.exp(-(numpy.power(stimX-x,2) + numpy.power(stimY-y,2))/(2*s**2))
# init array
modelNeuralResponse = numpy.zeros(stimImage.shape[2])
# loop over frames
for iFrame in range(0,stimImage.shape[2]):
# compute overlap
rfStimulusOverlap = numpy.multiply(stimImage[:,:,iFrame],rf)
# sum over all points
modelNeuralResponse[iFrame] = numpy.sum(rfStimulusOverlap)
# compute the model bold response from the above
modelBoldResponse = numpy.convolve(modelNeuralResponse,canonical)
# trim length
modelBoldResponse = numpy.transpose(modelBoldResponse[0:modelNeuralResponse.shape[0]])
# normalize
modelBoldResponse = modelBoldResponse-numpy.mean(modelBoldResponse)
modelBoldResponse = modelBoldResponse/numpy.sqrt(numpy.sum(numpy.power(modelBoldResponse,2)))
# compute correlation (note the (:) which is just to make sure
# that both arrays are column vectors
r = numpy.corrcoef(modelBoldResponse,tSeries)[0,1]
# plto
plt.clf()
plt.subplot(1,3,(1,2))
tSeries = tSeries-numpy.mean(tSeries)
tSeries = tSeries/numpy.sqrt(numpy.sum(numpy.power(tSeries,2)))
plt.plot(tSeries,'k-')
plt.plot(modelBoldResponse,'r-')
plt.title('x: {:.2f} y: {:.2f} s: {:.2f} r: {:.2f}'.format(x,y,s,r))
plt.subplot(1,3,3)
plt.imshow(rf,cmap='hot')
plt.draw()
# and return 1-r
return 1-r
++++
And run fminsearch again. See it fitting?
Try it with some initial parameters that are not perfect.
++++Matlab cheat sheet|
fminsearch(@pRFCorr,[-10 2 4],options,stimImage,stimX,stimY,canonical,tSeries1)
++++
++++Python cheat sheet|
scipy.optimize.minimize(pRFCorr,[-10,-2, 4],(stimImage,stimX,stimY,canonical,tSeries1),method='Nelder-Mead')
++++
Did it find the same set of parameters?
++++Answer|
fminsearch(@pRFCorr,[8 4 5],options,stimImage,stimX,stimY,canonical,tSeries1)
++++
++++Python cheat sheet|
scipy.optimize.minimize(pRFCorr,[8,4, 5],(stimImage,stimX,stimY,canonical,tSeries1),method='Nelder-Mead')
++++
What happened?
++++Answer|
% get the rf
rf = exp(-((stimX-x).^2 + (stimY-y).^2)/(2*s^2));
% compute the modelNeuralResponse for the stimulus
modelNeuralResponse = squeeze(sum(sum(thisStimImage .* repmat(rf,1,1,size(thisStimImage,3)))));
% do the convolution
modelBoldResponse = conv(modelNeuralResponse,canonical);
% trim length
modelBoldResponse = modelBoldResponse(1:n)';
% normalize
modelBoldResponse = modelBoldResponse-mean(modelBoldResponse);
modelBoldResponse = modelBoldResponse/sqrt(sum(modelBoldResponse.^2));
++++
++++Python code|
# get the rf
rf = numpy.exp(-(numpy.power(stimX-x,2) + numpy.power(stimY-y,2))/(2*s**2))
# compute the modelNeuralResponse for the stimulus
# init array
modelNeuralResponse = numpy.zeros(stimImage.shape[2])
# loop over frames
for iFrame in range(0,stimImage.shape[2]):
# compute overlap
rfStimulusOverlap = numpy.multiply(stimImage[:,:,iFrame],rf)
# sum over all points
modelNeuralResponse[iFrame] = numpy.sum(rfStimulusOverlap)
# do the convolution
modelBoldResponse = numpy.convolve(modelNeuralResponse,canonical)
# trim length
modelBoldResponse = numpy.transpose(modelBoldResponse[0:modelNeuralResponse.shape[0]])
# normalize
modelBoldResponse = modelBoldResponse-numpy.mean(modelBoldResponse)
modelBoldResponse = modelBoldResponse/numpy.sqrt(numpy.sum(numpy.power(modelBoldResponse,2)))
++++
So, three places we need to fix. One, get rid of normalization, of course (we'll assume the measured responses have had nothing done to them - not even removing the mean). Two, the rf area should be normalized so that if the stimulus completely covers it, it will generate the maximum response - in this case we want that to be 3% signal change. Third, we may want to normalize the canonical response function. There are various ways to do this depending on the need (e.g. you could make the maximum of the canonical to be 1) - we will make the area under the canonical to be 1. This is considered the right thing to do with convolution since it will give interpretable units in terms of percent signal change per unit time. So, go ahead and try to make those changes.
++++Matlab cheat sheet|
% use one of the stimuli
thisStimImage = squeeze(possibleStimImages(1,:,:,:));
n = size(thisStimImage,3);
% set maximum bold (for real data this may vary from voxel-to-voxel and would need to be estimaged)
maxBold = 3;
% normalize the canonical response
canonical = canonical/sum(canonical(:));
% get the rf
rf = exp(-((stimX-x).^2 + (stimY-y).^2)/(2*s^2));
% normalize rf so that a stimulus that completely covers the RF will generate the max possible bold response
rf = maxBold * rf/sum(rf(:));
% compute the modelNeuralResponse for the stimulus
modelNeuralResponse = squeeze(sum(sum(thisStimImage .* repmat(rf,1,1,size(thisStimImage,3)))));
% do the convolution
modelBoldResponse = conv(modelNeuralResponse,canonical);
% trim length
modelBoldResponse = modelBoldResponse(1:n)';
++++
++++Python cheat sheet|
# use one of the stimuli
thisStimImage = possibleStimImages[1,:,:,:].squeeze()
n = thisStimImage.shape[2]
# set maximum bold (for real data this may vary from voxel-to-voxel and would need to be estimaged)
maxBold = 3;
# normalize the canonical response
canonical = canonical/numpy.sum(canonical)
# get the rf
rf = numpy.exp(-(numpy.power(stimX-x,2) + numpy.power(stimY-y,2))/(2*s**2))
# compute the modelNeuralResponse for the stimulus
# init array
modelNeuralResponse = numpy.zeros(thisStimImage.shape[2])
# loop over frames
for iFrame in range(0,thisStimImage.shape[2]):
# compute overlap
rfStimulusOverlap = numpy.multiply(thisStimImage[:,:,iFrame],rf)
# sum over all points
modelNeuralResponse[iFrame] = numpy.sum(rfStimulusOverlap)
# do the convolution
modelBoldResponse = numpy.convolve(modelNeuralResponse,canonical)
# trim length
modelBoldResponse = numpy.transpose(modelBoldResponse[0:modelNeuralResponse.shape[0]])
++++
Ok. Now that we have that taken care, let's do some decoding! In the array unknownTSeries we have responses of our voxels to a, well, unknown stimulus. In this case, one of the letters of the alphabet. Can you guess which letter by looking at the responses?
To figure this out, we can try to predict what the response should be to any letter of the alphabet and then find the response that best correlates with the unkownTSeries. The letters of the alphabet are in a variable called possibleStimImages which is indexed by letter of the alphabet, x, y and t [26 x 108 x 192 x 60] (i.e the first dimension is for each of the letters of the alphabet and the last 3 are like the stimImage we have been working for - except a bit shorter - only 60 volumes). You should be able to cycle through all letters and then compute the response of all voxels. Then correlate that predicted response with the measured response and then choose which image was presented.
++++Matlab cheat sheet|
% set maximum bold (for real data this may vary from voxel-to-voxel and would need to be estimated)
maxBold = 3;
% normalize the canonical response
canonical = canonical/sum(canonical(:));
% cycle over the letter stimuli
for iLetter = 1:26
% get the stimImage for this letter
thisStimImage = squeeze(possibleStimImages(iLetter,:,:,:));
n = size(thisStimImage,3);
% now cycle over all voxels to generate each predicted response
for iVox = 1:length(bestModel)
% get parameters for that voxel
bestParams = params(bestModel(iVox),:);
x = bestParams(1);
y = bestParams(2);
s = bestParams(3);
% get the rf
rf = exp(-((stimX-x).^2 + (stimY-y).^2)/(2*s^2));
% normalize rf so that a stimulus that completely covers the RF will generate the max possible bold response
rf = maxBold * rf/sum(rf(:));
% compute the modelNeuralResponse for the stimulus
modelNeuralResponse = squeeze(sum(sum(thisStimImage .* repmat(rf,1,1,size(thisStimImage,3)))));
% do the convolution
modelBoldResponse = conv(modelNeuralResponse,canonical);
% trim length
modelBoldResponse = modelBoldResponse(1:n)';
% and stick into a matrix of all model responses
thisModelResponses(iVox,:) = modelBoldResponse;
end
% compute the correlation of the modelResponses to the "measured" unknownTSeries
rWithLetter(iLetter) = corr(unknownTSeries(:),thisModelResponses(:));
end
% find which letter we have the best correlation with
[~,whichLetter] = max(rWithLetter)
% to be more dramatic - draw the stimulus
clf;colormap(gray)
imagesc(squeeze(possibleStimImages(whichLetter,:,:,1)));
++++
++++Python cheat sheet|
nVox = bestModel.size
# set maximum bold (for real data this may vary from voxel-to-voxel and would need to be estimaged)
maxBold = 3;
# normalize the canonical response
canonical = canonical/numpy.sum(canonical)
# initialize correlations
rWithLetter = numpy.zeros(26)
# get the stimImage for this letter
for iLetter in range(0,26):
# get stim image
thisStimImage = possibleStimImages[iLetter,:,:,:].squeeze()
n = thisStimImage.shape[2]
# init thisModelResponses
thisModelResponses = numpy.zeros([nVox,n])
# cycle over voxels
for iVox in range(0,nVox):
# get parameters for voxel
x = params[bestModel[iVox],0]
y = params[bestModel[iVox],1]
s = params[bestModel[iVox],2]
# get the rf
rf = numpy.exp(-(numpy.power(stimX-x,2) + numpy.power(stimY-y,2))/(2*s**2))
# compute the modelNeuralResponse for the stimulus
# init array
modelNeuralResponse = numpy.zeros(thisStimImage.shape[2])
# loop over frames
for iFrame in range(0,thisStimImage.shape[2]):
# compute overlap
rfStimulusOverlap = numpy.multiply(thisStimImage[:,:,iFrame],rf)
# sum over all points
modelNeuralResponse[iFrame] = numpy.sum(rfStimulusOverlap)
# do the convolution
modelBoldResponse = numpy.convolve(modelNeuralResponse,canonical)
# trim length
modelBoldResponse = numpy.transpose(modelBoldResponse[0:modelNeuralResponse.shape[0]])
# and stick into a matrix of all model responses
thisModelResponses[iVox,:] = modelBoldResponse;
# compute the correlation of the modelResponses to the "measured" unknownTSeries
rWithLetter[iLetter] = numpy.corrcoef(unknownTSeries.reshape(1,nVox*n),thisModelResponses.reshape(1,nVox*n))[0,1]
print(rWithLetter[iLetter])
# find which letter we have the best correlation with
whichLetter = numpy.argmax(rWithLetter)
# to be more dramatic - draw the stimulus
plt.clf()
plt.imshow(possibleStimImages[whichLetter,:,:,1].squeeze(),cmap="gray");
++++
++++Answer|X++++