Switch to unified view

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