|
a |
|
b/Analysis/jPCA_ForDistribution/jPCA.m |
|
|
1 |
|
|
|
2 |
% [Projection, Summary] = jPCA(Data, analyzeTimes, params) |
|
|
3 |
% |
|
|
4 |
% OUTPUTS: |
|
|
5 |
% Projection is a struct with one element per condition. |
|
|
6 |
% It contains the following fields: |
|
|
7 |
% .proj The projection into the 6D jPCA space. |
|
|
8 |
% .times Those times that were used |
|
|
9 |
% .projAllTimes Pojection of all the data into the jPCs (which were derived from a subset of the data) |
|
|
10 |
% .allTimes All the times (exactly the same as Data(1).times) |
|
|
11 |
% .tradPCAproj The traditional PCA projection (same times as for 'proj') |
|
|
12 |
% .tradPCAprojAllTimes Above but for all times. |
|
|
13 |
% |
|
|
14 |
% Summary contains the following fields: |
|
|
15 |
% .jPCs The jPCs in terms of the PCs (not the full-D space) |
|
|
16 |
% .PCs The PCs (each column is a PC, each row a neuron) |
|
|
17 |
% .jPCs_highD The jPCs, but in the original high-D space. This is just PCs * jPCs, and is thus of size neurons x numPCs |
|
|
18 |
% .varCaptEachJPC The data variance captured by each jPC |
|
|
19 |
% .varCaptEachPC The data variance captured by each PC |
|
|
20 |
% .R2_Mskew_2D Fit quality (fitting dx with x) provided by Mskew, in the first 2 jPCs. |
|
|
21 |
% .R2_Mbest_2D Same for the best M |
|
|
22 |
% .R2_Mskew_kD Fit quality (fitting dx with x) provided by Mskew, in all the jPCs (the number of which is set by 'numPCs'. |
|
|
23 |
% .R2_Mbest_kD Same for the best M |
|
|
24 |
% |
|
|
25 |
% There are some other useful currently undocumented (but often self-explanatory) fields in |
|
|
26 |
% 'Summary' |
|
|
27 |
% |
|
|
28 |
% Summary also contains the following, useful for projecting new data into the jPCA space. Also |
|
|
29 |
% useful for getting from what is in 'Projection' back into the high-D land of PSTHs |
|
|
30 |
% Note that during preprocessing we FIRST normalize, and then (when doing PCA) subtract the |
|
|
31 |
% overall mean firing rate (no relationship to the cross-condition mean subtraction). For new |
|
|
32 |
% data you must do this in the same order (using the ORIGINAL normFactors and mean firing rats. |
|
|
33 |
% To get from low-D to high D you must do just the reverse: add back the mean FRs and the multiply |
|
|
34 |
% by the normFactors. |
|
|
35 |
% .preprocessing.normFactors = normFactors; |
|
|
36 |
% .preprocessing.meanFReachNeuron = meanFReachNeuron; |
|
|
37 |
% |
|
|
38 |
% INPUTS: |
|
|
39 |
% The input 'Data' needs to be a struct, with one entry per condition. |
|
|
40 |
% For a given condition, Data(c).A should hold the data (e.g. firing rates). |
|
|
41 |
% Each column of A corresponds to a neuron, and each row to a timepoint. |
|
|
42 |
% |
|
|
43 |
% Data(c).times is an optional field. If you provide it, only those entries that match |
|
|
44 |
% 'analyzeTimes' will be used for the analysis. If analyzeTimes == [], all times will be used. |
|
|
45 |
% |
|
|
46 |
% If you don't provide it, a '.times' field |
|
|
47 |
% is created that starts at 1. 'analyzeTimes' then refers to those times. |
|
|
48 |
% |
|
|
49 |
% 'params' is optional, and can contain the following fields: |
|
|
50 |
% .numPCs Default is 6. The number of traditional PCs to use (all jPCs live within this space) |
|
|
51 |
% .normalize Default is 'true'. Whether or not to normalize each neurons response by its FR range. |
|
|
52 |
% .softenNorm Default is 10. Determines how much we undernormalize for low FR neurons. 0 means |
|
|
53 |
% complete normalization. 10 means a neuron with FR range of 10 gets mapped to a range of 0.5. |
|
|
54 |
% .meanSubtract Default is true. Whether or not we remove the across-condition mean from each |
|
|
55 |
% neurons rate. |
|
|
56 |
% .suppressBWrosettes if present and true, the black & white rosettes are not plotted |
|
|
57 |
% .suppressHistograms if present and true, the blue histograms are not plotted |
|
|
58 |
% .suppressText if present and true, no text is output to the command window |
|
|
59 |
% |
|
|
60 |
% As a note on projecting more data into the same space. This can be done with the function |
|
|
61 |
% 'projectNewData'. However, if you are going to go this route then you should turn OFF |
|
|
62 |
% 'meanSubtract'. If you wish to still mean subtract the data you should do it by hand yourself. |
|
|
63 |
% That way you can make a principled decision regarding how to treat the original data versus the |
|
|
64 |
% new-to-be-projected data. For example, you may subtract off the mean manually across 108 |
|
|
65 |
% conditions. You might then leave out one condition and compute the jCPA plane (with meanSubtract set to false). |
|
|
66 |
% You could then project the remaining condition into that plane using 'projectNewData'. |
|
|
67 |
% |
|
|
68 |
% |
|
|
69 |
function [Projection, Summary] = jPCA(Data, analyzeTimes, params) |
|
|
70 |
|
|
|
71 |
|
|
|
72 |
%% quick bookkeeping |
|
|
73 |
|
|
|
74 |
numConds = length(Data); |
|
|
75 |
numTimes = size(Data(1).A,1); |
|
|
76 |
% there is also a 'numAnalyzedTimes' defined below. |
|
|
77 |
|
|
|
78 |
%% setting parameters that may or may not have been specified |
|
|
79 |
|
|
|
80 |
% 'times' field |
|
|
81 |
% if the user didn't specify a times field, create one that starts with '1' |
|
|
82 |
if ~isfield(Data(1),'times') |
|
|
83 |
for c = 1:length(Data) |
|
|
84 |
Data(c).times = 1:numTimes; |
|
|
85 |
end |
|
|
86 |
end |
|
|
87 |
|
|
|
88 |
if exist('analyzeTimes', 'var') && ~isempty(analyzeTimes) && max(diff(analyzeTimes)) > max(diff(Data(1).times)) |
|
|
89 |
disp('error, you can use a subset of times but you may not skip times within that subset'); |
|
|
90 |
Projection = []; Summary = []; return; |
|
|
91 |
end |
|
|
92 |
|
|
|
93 |
% the number of PCs to look within |
|
|
94 |
numPCs = 6; |
|
|
95 |
if exist('params', 'var') && isfield(params,'numPCs') |
|
|
96 |
numPCs = params.numPCs; |
|
|
97 |
end |
|
|
98 |
if rem(numPCs,2)>0 |
|
|
99 |
disp('you MUST ask for an even number of PCs.'); return; |
|
|
100 |
end |
|
|
101 |
|
|
|
102 |
% do we normalize |
|
|
103 |
normalize = true; |
|
|
104 |
if exist('params', 'var') && isfield(params,'normalize') |
|
|
105 |
normalize = params.normalize; |
|
|
106 |
end |
|
|
107 |
|
|
|
108 |
% do we soften the normalization (so weak signals stay smallish) |
|
|
109 |
% numbers larger than zero mean soften the norm. |
|
|
110 |
% The default (10) means that 10 spikes a second gets mapped to 0.5, infinity to 1, and zero to zero. |
|
|
111 |
% Beware if you are using data that isn't in terms of spikes/s, as 10 may be a terrible default |
|
|
112 |
softenNorm = 10; |
|
|
113 |
if exist('params', 'var') && isfield(params,'softenNorm') |
|
|
114 |
softenNorm = params.softenNorm; |
|
|
115 |
end |
|
|
116 |
|
|
|
117 |
% do we mean subtract |
|
|
118 |
meanSubtract = true; |
|
|
119 |
if exist('params', 'var') && isfield(params,'meanSubtract') |
|
|
120 |
meanSubtract = params.meanSubtract; |
|
|
121 |
end |
|
|
122 |
if length(Data)==1, meanSubtract = false; end % cant mean subtract if there is only one condition |
|
|
123 |
|
|
|
124 |
if ~exist('analyzeTimes', 'var') || isempty(analyzeTimes) |
|
|
125 |
disp('analyzing all times'); |
|
|
126 |
analyzeTimes = Data(1).times; |
|
|
127 |
end |
|
|
128 |
|
|
|
129 |
%% figure out which times to analyze and make masks |
|
|
130 |
% |
|
|
131 |
analyzeIndices = ismember(round(Data(1).times), analyzeTimes); |
|
|
132 |
if size(analyzeIndices,1) == 1 |
|
|
133 |
analyzeIndices = analyzeIndices'; % orientation matters for the repmat below |
|
|
134 |
end |
|
|
135 |
analyzeMask = repmat(analyzeIndices,numConds,1); % used to mask bigA |
|
|
136 |
if diff( Data(1).times(analyzeIndices) ) <= 5 |
|
|
137 |
disp('mild warning!!!!: you are using a short time base which might make the computation of the derivative a bit less reliable'); |
|
|
138 |
end |
|
|
139 |
|
|
|
140 |
% these are used to take the derivative |
|
|
141 |
bunchOtruth = true(sum(analyzeIndices)-1,1); |
|
|
142 |
maskT1 = repmat( [bunchOtruth;false],numConds,1); % skip the last time for each condition |
|
|
143 |
maskT2 = repmat( [false;bunchOtruth],numConds,1); % skip the first time for each condition |
|
|
144 |
|
|
|
145 |
if sum(analyzeIndices) < 5 |
|
|
146 |
disp('warning, analyzing few or no times'); |
|
|
147 |
disp('if this wasnt your intent, check to be sure that you are asking for times that really exist'); |
|
|
148 |
end |
|
|
149 |
|
|
|
150 |
%% make a version of A that has all the data from all the conditions. |
|
|
151 |
% in doing so, mean subtract and normalize |
|
|
152 |
|
|
|
153 |
bigA = vertcat(Data.A); % append conditions vertically |
|
|
154 |
|
|
|
155 |
% note that normalization is done based on ALL the supplied data, not just what will be analyzed |
|
|
156 |
if normalize % normalize (incompletely unless asked otherwise) |
|
|
157 |
ranges = range(bigA); % For each neuron, the firing rate range across all conditions and times. |
|
|
158 |
normFactors = (ranges+softenNorm); |
|
|
159 |
bigA = bsxfun(@times, bigA, 1./normFactors); % normalize |
|
|
160 |
else |
|
|
161 |
normFactors = ones(1,size(bigA,2)); |
|
|
162 |
end |
|
|
163 |
|
|
|
164 |
|
|
|
165 |
|
|
|
166 |
sumA = 0; |
|
|
167 |
for c = 1:numConds |
|
|
168 |
sumA = sumA + bsxfun(@times, Data(c).A, 1./normFactors); % using the same normalization as above |
|
|
169 |
end |
|
|
170 |
meanA = sumA/numConds; |
|
|
171 |
if meanSubtract % subtract off the across-condition mean from each neurons response |
|
|
172 |
bigA = bigA-repmat(meanA,numConds,1); |
|
|
173 |
end |
|
|
174 |
|
|
|
175 |
|
|
|
176 |
|
|
|
177 |
%% now do traditional PCA |
|
|
178 |
|
|
|
179 |
smallA = bigA(analyzeMask,:); |
|
|
180 |
[PCvectors,rawScores] = princomp(smallA,'econ'); % apply PCA to the analyzed times |
|
|
181 |
meanFReachNeuron = mean(smallA); % this will be kept for use by future attempts to project onto the PCs |
|
|
182 |
|
|
|
183 |
% these are the directions in the high-D space (the PCs themselves) |
|
|
184 |
if numPCs > size(PCvectors,2) |
|
|
185 |
disp('You asked for more PCs than there are dimensions of data'); |
|
|
186 |
disp('Giving up'); |
|
|
187 |
end |
|
|
188 |
PCvectors = PCvectors(:,1:numPCs); % cut down to the right number of PCs |
|
|
189 |
|
|
|
190 |
% CRITICAL STEP |
|
|
191 |
% This is what we are really after: the projection of the data onto the PCs |
|
|
192 |
Ared = rawScores(:,1:numPCs); % cut down to the right number of PCs |
|
|
193 |
|
|
|
194 |
% Some extra steps |
|
|
195 |
% |
|
|
196 |
% projection of all the data |
|
|
197 |
% princomp subtracts off means automatically so we need to do that too when projecting all the data |
|
|
198 |
bigAred = bsxfun(@minus, bigA, mean(smallA)) * PCvectors(:,1:numPCs); % projection of all the data (not just teh analyzed chunk) into the low-D space. |
|
|
199 |
% need to subtract off the mean for smallA, as this was done automatically when computing the PC |
|
|
200 |
% scores from smallA, and we want that projection to match this one. |
|
|
201 |
|
|
|
202 |
% projection of the mean |
|
|
203 |
|
|
|
204 |
meanAred = bsxfun(@minus, meanA, mean(smallA)) * PCvectors(:,1:numPCs); % projection of the across-cond mean (which we subtracted out) into the low-D space. |
|
|
205 |
|
|
|
206 |
% will need this later for some indexing |
|
|
207 |
numAnalyzedTimes = size(Ared,1)/numConds; |
|
|
208 |
|
|
|
209 |
%% GET M & Mskew |
|
|
210 |
% compute dState, and use that to find the best M and Mskew that predict dState from the state |
|
|
211 |
|
|
|
212 |
% we are interested in the eqn that explains the derivative as a function of the state: dState/dt = M*State |
|
|
213 |
dState = Ared(maskT2,:) - Ared(maskT1,:); % the masks just give us earlier and later times within each condition |
|
|
214 |
preState = Ared(maskT1,:); % just for convenience, keep the earlier time in its own variable |
|
|
215 |
|
|
|
216 |
% first compute the best M (of any type) |
|
|
217 |
% note, we have converted dState and Ared to have time running horizontally |
|
|
218 |
M = (dState'/preState'); % M takes the state and provides a fit to dState |
|
|
219 |
% Note on sizes of matrices: |
|
|
220 |
% dState' and preState' have time running horizontally and state dimension running vertically |
|
|
221 |
% We are thus solving for dx = Mx. |
|
|
222 |
% M is a matrix that takes a column state vector and gives the derivative |
|
|
223 |
|
|
|
224 |
% now compute Mskew using John's method |
|
|
225 |
% Mskew expects time to run vertically, transpose result so Mskew in the same format as M |
|
|
226 |
% (that is, Mskew will transform a column state vector into dx) |
|
|
227 |
Mskew = skewSymRegress(dState,preState)'; % this is the best Mskew for the same equation |
|
|
228 |
|
|
|
229 |
%% USE Mskew to get the jPCs |
|
|
230 |
|
|
|
231 |
% get the eigenvalues and eigenvectors |
|
|
232 |
[V,D] = eig(Mskew); % V are the eigenvectors, D contains the eigenvalues |
|
|
233 |
evals = diag(D); % eigenvalues |
|
|
234 |
|
|
|
235 |
% the eigenvalues are usually in order, but not always. We want the biggest |
|
|
236 |
[~,sortIndices] = sort(abs(evals),1,'descend'); |
|
|
237 |
evals = evals(sortIndices); % reorder the eigenvalues |
|
|
238 |
evals = imag(evals); % get rid of any tiny real part |
|
|
239 |
V = V(:,sortIndices); % reorder the eigenvectors (base on eigenvalue size) |
|
|
240 |
|
|
|
241 |
% Eigenvalues will be displayed to confirm that everything is working |
|
|
242 |
% unless we are asked not to output text |
|
|
243 |
if ~exist('params', 'var') || ~isfield(params,'suppressText') || ~params.suppressText |
|
|
244 |
disp('eigenvalues of Mskew: '); |
|
|
245 |
for i = 1:length(evals) |
|
|
246 |
if evals(i) > 0; |
|
|
247 |
fprintf(' %1.3fi', evals(i)); |
|
|
248 |
else |
|
|
249 |
fprintf(' %1.3fi \n', evals(i)); |
|
|
250 |
end |
|
|
251 |
end |
|
|
252 |
end |
|
|
253 |
|
|
|
254 |
jPCs = zeros(size(V)); |
|
|
255 |
for pair = 1:numPCs/2 |
|
|
256 |
vi1 = 1+2*(pair-1); |
|
|
257 |
vi2 = 2*pair; |
|
|
258 |
|
|
|
259 |
VconjPair = V(:,[vi1,vi2]); % a conjugate pair of eigenvectors |
|
|
260 |
evConjPair = evals([vi1,vi2]); % and their eigenvalues |
|
|
261 |
VconjPair = getRealVs(VconjPair,evConjPair); |
|
|
262 |
|
|
|
263 |
jPCs(:,[vi1,vi2]) = VconjPair; |
|
|
264 |
end |
|
|
265 |
|
|
|
266 |
%% Get the projections |
|
|
267 |
|
|
|
268 |
proj = Ared * jPCs; |
|
|
269 |
projAllTimes = bigAred * jPCs; |
|
|
270 |
tradPCA_AllTimes = bsxfun(@minus, bigA, mean(smallA)) * PCvectors; % mean center in exactly the same way as for the shorter time period. |
|
|
271 |
crossCondMeanAllTimes = meanAred * jPCs; |
|
|
272 |
|
|
|
273 |
% Do some annoying output formatting. |
|
|
274 |
% Put things back so we have one entry per condition |
|
|
275 |
index1 = 1; |
|
|
276 |
index2 = 1; |
|
|
277 |
for c = 1:numConds |
|
|
278 |
index1b = index1 + numAnalyzedTimes -1; % we will go from index1 to this point |
|
|
279 |
index2b = index2 + numTimes -1; % we will go from index2 to this point |
|
|
280 |
|
|
|
281 |
Projection(c).proj = proj(index1:index1b,:); |
|
|
282 |
Projection(c).times = Data(1).times(analyzeIndices); |
|
|
283 |
Projection(c).projAllTimes = projAllTimes(index2:index2b,:); |
|
|
284 |
Projection(c).allTimes = Data(1).times; |
|
|
285 |
Projection(c).tradPCAproj = Ared(index1:index1b,:); |
|
|
286 |
Projection(c).tradPCAprojAllTimes = tradPCA_AllTimes(index2:index2b,:); |
|
|
287 |
|
|
|
288 |
index1 = index1+numAnalyzedTimes; |
|
|
289 |
index2 = index2+numTimes; |
|
|
290 |
end |
|
|
291 |
|
|
|
292 |
%% Done computing the projections, plot the rosette |
|
|
293 |
|
|
|
294 |
% do this unless params contains a field 'suppressBWrosettes' that is true |
|
|
295 |
if ~exist('params', 'var') || ~isfield(params,'suppressBWrosettes') || ~params.suppressBWrosettes |
|
|
296 |
plotRosette(Projection, 1); % primary plane |
|
|
297 |
plotRosette(Projection, 2); % secondary plane |
|
|
298 |
end |
|
|
299 |
|
|
|
300 |
|
|
|
301 |
%% SUMMARY STATS |
|
|
302 |
%% compute R2 for the fit provided by M and Mskew |
|
|
303 |
|
|
|
304 |
% R2 Full-D |
|
|
305 |
fitErrorM = dState'- M*preState'; |
|
|
306 |
fitErrorMskew = dState'- Mskew*preState'; |
|
|
307 |
varDState = sum(dState(:).^2); % original data variance |
|
|
308 |
|
|
|
309 |
R2_Mbest_kD = (varDState - sum(fitErrorM(:).^2)) / varDState; % how much is explained by the overall fit via M |
|
|
310 |
R2_Mskew_kD = (varDState - sum(fitErrorMskew(:).^2)) / varDState; % how much by is explained via Mskew |
|
|
311 |
|
|
|
312 |
% unless asked to not output text |
|
|
313 |
if ~exist('params', 'var') || ~isfield(params,'suppressText') || ~params.suppressText |
|
|
314 |
fprintf('%% R^2 for Mbest (all %d dims): %1.2f\n', numPCs, R2_Mbest_kD); |
|
|
315 |
fprintf('%% R^2 for Mskew (all %d dims): %1.2f <<---------------\n', numPCs, R2_Mskew_kD); |
|
|
316 |
end |
|
|
317 |
|
|
|
318 |
|
|
|
319 |
% R2 2-D primary jPCA plane |
|
|
320 |
fitErrorM_2D = jPCs(:,1:2)' * fitErrorM; % error projected into the primary plane |
|
|
321 |
fitErrorMskew_2D = jPCs(:,1:2)' * fitErrorMskew; % error projected into the primary plane |
|
|
322 |
dState_2D = jPCs(:,1:2)' * dState'; % project dState into the primary plane |
|
|
323 |
varDState_2D = sum(dState_2D(:).^2); % and get its variance |
|
|
324 |
|
|
|
325 |
R2_Mbest_2D = (varDState_2D - sum(fitErrorM_2D(:).^2)) / varDState_2D; % how much is explained by the overall fit via M |
|
|
326 |
R2_Mskew_2D = (varDState_2D - sum(fitErrorMskew_2D(:).^2)) / varDState_2D; % how much by is explained via Mskew |
|
|
327 |
|
|
|
328 |
if ~exist('params', 'var') || ~isfield(params,'suppressText') || ~params.suppressText |
|
|
329 |
fprintf('%% R^2 for Mbest (primary 2D plane): %1.2f\n', R2_Mbest_2D); |
|
|
330 |
fprintf('%% R^2 for Mskew (primary 2D plane): %1.2f <<---------------\n', R2_Mskew_2D); |
|
|
331 |
end |
|
|
332 |
|
|
|
333 |
%% variance catpured by the jPCs |
|
|
334 |
origVar = sum(sum( bsxfun(@minus, smallA, mean(smallA)).^2)); |
|
|
335 |
varCaptEachPC = sum(Ared.^2) / origVar; % this equals latent(1:numPCs) / sum(latent) |
|
|
336 |
varCaptEachJPC = sum((Ared*jPCs).^2) / origVar; |
|
|
337 |
varCaptEachPlane = reshape(varCaptEachJPC, 2, numPCs/2); |
|
|
338 |
varCaptEachPlane = sum(varCaptEachPlane); |
|
|
339 |
|
|
|
340 |
|
|
|
341 |
%% Analysis of whether things really look like rotations (makes plots) |
|
|
342 |
|
|
|
343 |
for jPCplane = 1:2 |
|
|
344 |
phaseData = getPhase(Projection, jPCplane); % does the key analysis |
|
|
345 |
|
|
|
346 |
if exist('params', 'var') |
|
|
347 |
cstats = plotPhaseDiff(phaseData, params, jPCplane); % plots the histogram. 'params' is just what the user passed, so plots can be suppressed |
|
|
348 |
else |
|
|
349 |
cstats = plotPhaseDiff(phaseData, [], jPCplane); |
|
|
350 |
end |
|
|
351 |
|
|
|
352 |
if jPCplane == 1 |
|
|
353 |
circStats = cstats; % keep only for the primary plane |
|
|
354 |
end |
|
|
355 |
end |
|
|
356 |
|
|
|
357 |
%% Make the summary output structure |
|
|
358 |
|
|
|
359 |
Summary.jPCs = jPCs; |
|
|
360 |
Summary.PCs = PCvectors; |
|
|
361 |
Summary.jPCs_highD = PCvectors * jPCs; |
|
|
362 |
Summary.varCaptEachJPC = varCaptEachJPC; |
|
|
363 |
Summary.varCaptEachPC = varCaptEachPC; |
|
|
364 |
Summary.varCaptEachPlane = varCaptEachPlane; |
|
|
365 |
Summary.Mbest = M; |
|
|
366 |
Summary.Mskew = Mskew; |
|
|
367 |
Summary.fitErrorM = fitErrorM; |
|
|
368 |
Summary.fitErrorMskew = fitErrorMskew; |
|
|
369 |
Summary.R2_Mskew_2D = R2_Mskew_2D; |
|
|
370 |
Summary.R2_Mbest_2D = R2_Mbest_2D; |
|
|
371 |
Summary.R2_Mskew_kD = R2_Mskew_kD; |
|
|
372 |
Summary.R2_Mbest_kD = R2_Mbest_kD; |
|
|
373 |
Summary.circStats = circStats; |
|
|
374 |
Summary.acrossCondMeanRemoved = meanSubtract; |
|
|
375 |
Summary.crossCondMean = crossCondMeanAllTimes(analyzeIndices,:); |
|
|
376 |
Summary.crossCondMeanAllTimes = crossCondMeanAllTimes; |
|
|
377 |
Summary.preprocessing.normFactors = normFactors; % Used for projecting new data from the same neurons into the jPC space |
|
|
378 |
Summary.preprocessing.meanFReachNeuron = meanFReachNeuron; % You should first normalize and then mean subtract using this (the original) mean |
|
|
379 |
% conversely, to come back out, you must add the mean back on and then MULTIPLY by the normFactors |
|
|
380 |
% to undo the normalization. |
|
|
381 |
|
|
|
382 |
%% DONE (only functions below) |
|
|
383 |
|
|
|
384 |
|
|
|
385 |
%% Inline function that gets the real analogue of the eigenvectors |
|
|
386 |
function Vr = getRealVs(V,evals) |
|
|
387 |
|
|
|
388 |
% get real vectors made from the eigenvectors |
|
|
389 |
|
|
|
390 |
% by paying attention to this order, things will always rotate CCW |
|
|
391 |
if abs(evals(1))>0 % if the eigenvalue with negative imaginary component comes first |
|
|
392 |
Vr = [V(:,1) + V(:,2), (V(:,1) - V(:,2))*1i]; |
|
|
393 |
else |
|
|
394 |
Vr = [V(:,2) + V(:,1), (V(:,2) - V(:,1))*1i]; |
|
|
395 |
end |
|
|
396 |
Vr = Vr / sqrt(2); |
|
|
397 |
|
|
|
398 |
% now get axes aligned so that plan is spread mostly along the horizontal axis |
|
|
399 |
testProj = (Vr'*Ared(1:numAnalyzedTimes:end,:)')'; % just picks out the plan times |
|
|
400 |
rotV = princomp(testProj); |
|
|
401 |
crossProd = cross([rotV(:,1);0], [rotV(:,2);0]); |
|
|
402 |
if crossProd(3) < 0, rotV(:,2) = -rotV(:,2); end % make sure the second vector is 90 degrees clockwise from the first |
|
|
403 |
Vr = Vr*rotV; |
|
|
404 |
|
|
|
405 |
% flip both axes if necessary so that the maximum move excursion is in the positive direction |
|
|
406 |
testProj = (Vr'*Ared')'; % all the times |
|
|
407 |
if max(abs(testProj(:,2))) > max(testProj(:,2)) % 2nd column is the putative 'muscle potent' direction. |
|
|
408 |
Vr = -Vr; |
|
|
409 |
end |
|
|
410 |
end |
|
|
411 |
|
|
|
412 |
end |
|
|
413 |
%% end of main function |
|
|
414 |
|
|
|
415 |
|
|
|
416 |
%% Some inline functions are below |
|
|
417 |
|
|
|
418 |
%% Plot the rosette itself (may need to spruce this up and move to a subfunction) |
|
|
419 |
function plotRosette(Proj, whichPair) |
|
|
420 |
|
|
|
421 |
d1 = 1 + 2*(whichPair-1); |
|
|
422 |
d2 = d1+1; |
|
|
423 |
|
|
|
424 |
numConds = length(Proj); |
|
|
425 |
|
|
|
426 |
figure; |
|
|
427 |
|
|
|
428 |
% first deal with the ellipse for the plan variance (we want this under the rest of the data) |
|
|
429 |
planData = zeros(numConds,2); |
|
|
430 |
for c = 1:numConds |
|
|
431 |
planData(c,:) = Proj(c).proj(1,[d1,d2]); |
|
|
432 |
end |
|
|
433 |
planVars = var(planData); |
|
|
434 |
circle([0 0], 2*planVars.^0.5, 0.6*[1 1 1], 1); hold on; |
|
|
435 |
%fprintf('ratio of plan variances = %1.3f (hor var / vert var)\n', planVars(1)/planVars(2)); |
|
|
436 |
|
|
|
437 |
allD = vertcat(Proj(:).proj); % just for getting axes |
|
|
438 |
allD = allD(:,d1:d2); |
|
|
439 |
mxVal = max(abs(allD(:))); |
|
|
440 |
axLim = mxVal*1.05*[-1 1 -1 1]; |
|
|
441 |
arrowSize = 5; |
|
|
442 |
for c = 1:numConds |
|
|
443 |
plot(Proj(c).proj(:,d1), Proj(c).proj(:,d2), 'k'); |
|
|
444 |
plot(Proj(c).proj(1,d1), Proj(c).proj(1,d2), 'ko', 'markerFaceColor', [0.7 0.9 0.9]); |
|
|
445 |
|
|
|
446 |
penultimatePoint = [Proj(c).proj(end-1,d1), Proj(c).proj(end-1,d2)]; |
|
|
447 |
lastPoint = [Proj(c).proj(end,d1), Proj(c).proj(end,d2)]; |
|
|
448 |
arrowMMC(penultimatePoint, lastPoint, [], arrowSize, axLim); |
|
|
449 |
|
|
|
450 |
end |
|
|
451 |
|
|
|
452 |
axis(axLim); |
|
|
453 |
axis square; |
|
|
454 |
plot(0,0,'k+'); |
|
|
455 |
|
|
|
456 |
title(sprintf('jPCA plane %d', whichPair)); |
|
|
457 |
end |
|
|
458 |
|
|
|
459 |
|
|
|
460 |
%% Getting the phases |
|
|
461 |
function phaseData = getPhase(Proj, whichPair) |
|
|
462 |
numConds = length(Proj); |
|
|
463 |
d1 = 1 + 2*(whichPair-1); |
|
|
464 |
d2 = d1+1; |
|
|
465 |
|
|
|
466 |
for c=1:numConds |
|
|
467 |
data = Proj(c).proj(:,[d1,d2]); |
|
|
468 |
phase = atan2(data(:,2), data(:,1)); % Y comes first for atan2 |
|
|
469 |
|
|
|
470 |
deltaData = diff(data); |
|
|
471 |
phaseOfDelta = atan2(deltaData(:,2), deltaData(:,1)); % Y comes first for atan2 |
|
|
472 |
phaseOfDelta = [phaseOfDelta(1); phaseOfDelta]; %#ok<AGROW> % so same length as phase |
|
|
473 |
radius = sum(data.^2,2).^0.5; |
|
|
474 |
|
|
|
475 |
% collect and format |
|
|
476 |
% make things run horizontally so they can be easily concatenated. |
|
|
477 |
phaseData(c).phase = phase'; %#ok<AGROW> |
|
|
478 |
phaseData(c).phaseOfDelta = phaseOfDelta'; %#ok<AGROW> |
|
|
479 |
phaseData(c).radius = radius'; %#ok<AGROW> |
|
|
480 |
|
|
|
481 |
% angle between state vector and Dstate vector |
|
|
482 |
% between -pi and pi |
|
|
483 |
phaseData(c).phaseDiff = minusPi2Pi(phaseData(c).phaseOfDelta - phaseData(c).phase); %#ok<AGROW> |
|
|
484 |
end |
|
|
485 |
|
|
|
486 |
end |
|
|
487 |
|
|
|
488 |
|
|
|
489 |
|
|
|
490 |
%% plotting the phase difference between dx(t)/dt and x(t) where x is the 2D state |
|
|
491 |
function circStatsSummary = plotPhaseDiff(phaseData, params, jPCplane) |
|
|
492 |
% compute the circular mean of the data, weighted by the r's |
|
|
493 |
circMn = circ_mean([phaseData.phaseDiff]', [phaseData.radius]'); |
|
|
494 |
resultantVect = circ_r([phaseData.phaseDiff]', [phaseData.radius]'); |
|
|
495 |
|
|
|
496 |
|
|
|
497 |
bins = pi*(-1:0.1:1); |
|
|
498 |
cnts = histc([phaseData.phaseDiff], bins); % not for plotting, but for passing back out |
|
|
499 |
|
|
|
500 |
|
|
|
501 |
% do this unless params contains a field 'suppressHistograms' that is true |
|
|
502 |
if ~exist('params', 'var') || ~isfield(params,'suppressHistograms') || ~params.suppressHistograms |
|
|
503 |
figure; |
|
|
504 |
hist([phaseData.phaseDiff], bins); hold on; |
|
|
505 |
plot(circMn, 20, 'ro', 'markerFa', 'r', 'markerSiz', 8); |
|
|
506 |
plot(pi/2*[-1 1], [0 0], 'ko', 'markerFa', 'r', 'markerSiz', 8); |
|
|
507 |
set(gca,'XLim',pi*[-1 1]); |
|
|
508 |
title(sprintf('jPCs plane %d', jPCplane)); |
|
|
509 |
end |
|
|
510 |
|
|
|
511 |
%fprintf('(pi/2 is %1.2f) The circular mean (weighted) is %1.2f\n', pi/2, circMn); |
|
|
512 |
|
|
|
513 |
% compute the average dot product of each datum (the angle difference for one time and condition) |
|
|
514 |
% with pi/2. Will be one for perfect rotations, and zero for random data or expansions / |
|
|
515 |
% contractions. |
|
|
516 |
avgDP = averageDotProduct([phaseData.phaseDiff]', pi/2); |
|
|
517 |
%fprintf('the average dot product with pi/2 is %1.4f <<---------------\n', avgDP); |
|
|
518 |
|
|
|
519 |
circStatsSummary.circMn = circMn; |
|
|
520 |
circStatsSummary.resultantVect = resultantVect; |
|
|
521 |
circStatsSummary.avgDPwithPiOver2 = avgDP; % note this basically cant be <0 and definitely cant be >1 |
|
|
522 |
circStatsSummary.DISTRIBUTION.bins = bins; |
|
|
523 |
circStatsSummary.DISTRIBUTION.binCenters = (bins(1:end-1) + bins(2:end))/2; |
|
|
524 |
circStatsSummary.DISTRIBUTION.counts = cnts(1:end-1); |
|
|
525 |
circStatsSummary.RAW.rawData = [phaseData.phaseDiff]'; |
|
|
526 |
circStatsSummary.RAW.rawRadii = [phaseData.radius]'; |
|
|
527 |
|
|
|
528 |
end |
|
|
529 |
|
|
|
530 |
%% for computing the average dot product with a comparison angle |
|
|
531 |
function avgDP = averageDotProduct(angles, compAngle, varargin) |
|
|
532 |
|
|
|
533 |
x = cos(angles-compAngle); |
|
|
534 |
|
|
|
535 |
if ~isempty(varargin) |
|
|
536 |
avgDP = mean(x.*varargin{1}) / mean(varargin{1}); % weighted sum |
|
|
537 |
else |
|
|
538 |
avgDP = mean(x); |
|
|
539 |
end |
|
|
540 |
end |
|
|
541 |
|
|
|
542 |
|
|
|
543 |
|
|
|
544 |
|
|
|
545 |
|
|
|
546 |
|
|
|
547 |
|
|
|
548 |
|
|
|
549 |
|
|
|
550 |
|
|
|
551 |
|
|
|
552 |
|
|
|
553 |
|
|
|
554 |
|
|
|
555 |
|
|
|
556 |
|
|
|
557 |
|