├── README.md ├── data └── ts.mat ├── fcn ├── community_louvain.m └── fcn_boxpts.m ├── hcp200.mat └── main.m /README.md: -------------------------------------------------------------------------------- 1 | # edge-ts 2 | 3 | Example code for transforming parcellated fMRI BOLD activity into co-fluctuation time series ("edge time series") and obtaining the RSS time series. 4 | 5 | The RSS time series is thresholded to retain high- and low-amplitude frames, which are used to obtain corresponding estimates of functional connectivity (FC). 6 | 7 | We then cluster these FC patterns using modularity maximization. 8 | -------------------------------------------------------------------------------- /data/ts.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brain-networks/edge-ts/90687626b4f5129725cc919a4f2df65a95b6ccd9/data/ts.mat -------------------------------------------------------------------------------- /fcn/community_louvain.m: -------------------------------------------------------------------------------- 1 | function [M,Q]=community_louvain(W,gamma,M0,B) 2 | %COMMUNITY_LOUVAIN Optimal community structure 3 | % 4 | % M = community_louvain(W); 5 | % [M,Q] = community_louvain(W,gamma); 6 | % [M,Q] = community_louvain(W,gamma,M0); 7 | % [M,Q] = community_louvain(W,gamma,M0,'potts'); 8 | % [M,Q] = community_louvain(W,gamma,M0,'negative_asym'); 9 | % [M,Q] = community_louvain(W,[],[],B); 10 | % 11 | % The optimal community structure is a subdivision of the network into 12 | % nonoverlapping groups of nodes which maximizes the number of within- 13 | % group edges, and minimizes the number of between-group edges. 14 | % 15 | % This function is a fast and accurate multi-iterative generalization of 16 | % the Louvain community detection algorithm. This function subsumes and 17 | % improves upon, 18 | % modularity_louvain_und.m, modularity_finetune_und.m, 19 | % modularity_louvain_dir.m, modularity_finetune_dir.m, 20 | % modularity_louvain_und_sign.m 21 | % and additionally allows to optimize other objective functions (includes 22 | % built-in Potts-model Hamiltonian, allows for custom objective-function 23 | % matrices). 24 | % 25 | % Inputs: 26 | % W, 27 | % directed/undirected weighted/binary connection matrix with 28 | % positive and possibly negative weights. 29 | % gamma, 30 | % resolution parameter (optional) 31 | % gamma>1, detects smaller modules 32 | % 0<=gamma<1, detects larger modules 33 | % gamma=1, classic modularity (default) 34 | % M0, 35 | % initial community affiliation vector (optional) 36 | % B, 37 | % objective-function type or custom objective matrix (optional) 38 | % 'modularity', modularity (default) 39 | % 'potts', Potts-model Hamiltonian (for binary networks) 40 | % 'negative_sym', symmetric treatment of negative weights 41 | % 'negative_asym', asymmetric treatment of negative weights 42 | % B, custom objective-function matrix 43 | % 44 | % Note: see Rubinov and Sporns (2011) for a discussion of 45 | % symmetric vs. asymmetric treatment of negative weights. 46 | % 47 | % Outputs: 48 | % M, 49 | % community affiliation vector 50 | % Q, 51 | % optimized community-structure statistic (modularity by default) 52 | % 53 | % Example: 54 | % % Iterative community finetuning. 55 | % % W is the input connection matrix. 56 | % n = size(W,1); % number of nodes 57 | % M = 1:n; % initial community affiliations 58 | % Q0 = -1; Q1 = 0; % initialize modularity values 59 | % while Q1-Q0>1e-5; % while modularity increases 60 | % Q0 = Q1; % perform community detection 61 | % [M, Q1] = community_louvain(W, [], M); 62 | % end 63 | % 64 | % References: 65 | % Blondel et al. (2008) J. Stat. Mech. P10008. 66 | % Reichardt and Bornholdt (2006) Phys. Rev. E 74, 016110. 67 | % Ronhovde and Nussinov (2008) Phys. Rev. E 80, 016109 68 | % Sun et al. (2008) Europhysics Lett 86, 28004. 69 | % Rubinov and Sporns (2011) Neuroimage 56:2068-79. 70 | % 71 | % Mika Rubinov, U Cambridge 2015-2016 72 | 73 | % Modification history 74 | % 2015: Original 75 | % 2016: Included generalization for negative weights. 76 | % Enforced binary network input for Potts-model Hamiltonian. 77 | % Streamlined code and expanded documentation. 78 | 79 | W=double(W); % convert to double format 80 | n=length(W); % get number of nodes 81 | s=sum(sum(W)); % get sum of edges 82 | 83 | if ~exist('B','var') || isempty(B) 84 | type_B = 'modularity'; 85 | elseif ischar(B) 86 | type_B = B; 87 | else 88 | type_B = 0; 89 | if exist('gamma','var') && ~isempty(gamma) 90 | warning('Value of gamma is ignored in generalized mode.') 91 | end 92 | end 93 | if ~exist('gamma','var') || isempty(gamma) 94 | gamma = 1; 95 | end 96 | 97 | if strcmp(type_B,'negative_sym') || strcmp(type_B,'negative_asym') 98 | W0 = W.*(W>0); %positive weights matrix 99 | s0 = sum(sum(W0)); %weight of positive links 100 | B0 = W0-gamma*(sum(W0,2)*sum(W0,1))/s0; %positive modularity 101 | 102 | W1 =-W.*(W<0); %negative weights matrix 103 | s1 = sum(sum(W1)); %weight of negative links 104 | if s1 %negative modularity 105 | B1 = W1-gamma*(sum(W1,2)*sum(W1,1))/s1; 106 | else 107 | B1 = 0; 108 | end 109 | elseif min(min(W))<-1e-10 110 | err_string = [ 111 | 'The input connection matrix contains negative weights.\nSpecify ' ... 112 | '''negative_sym'' or ''negative_asym'' objective-function types.']; 113 | error(sprintf(err_string)) %#ok 114 | end 115 | if strcmp(type_B,'potts') && any(any(W ~= logical(W))) 116 | error('Potts-model Hamiltonian requires a binary W.') 117 | end 118 | 119 | if type_B 120 | switch type_B 121 | case 'modularity'; B = (W-gamma*(sum(W,2)*sum(W,1))/s)/s; 122 | case 'potts'; B = W-gamma*(~W); 123 | case 'negative_sym'; B = B0/(s0+s1) - B1/(s0+s1); 124 | case 'negative_asym'; B = B0/s0 - B1/(s0+s1); 125 | otherwise; error('Unknown objective function.'); 126 | end 127 | else % custom objective function matrix as input 128 | B = double(B); 129 | if ~isequal(size(W),size(B)) 130 | error('W and B must have the same size.') 131 | end 132 | end 133 | if ~exist('M0','var') || isempty(M0) 134 | M0=1:n; 135 | elseif numel(M0)~=n 136 | error('M0 must contain n elements.') 137 | end 138 | 139 | [~,~,Mb] = unique(M0); 140 | M = Mb; 141 | 142 | B = (B+B.')/2; % symmetrize modularity matrix 143 | Hnm=zeros(n,n); % node-to-module degree 144 | for m=1:max(Mb) % loop over modules 145 | Hnm(:,m)=sum(B(:,Mb==m),2); 146 | end 147 | 148 | Q0 = -inf; 149 | Q = sum(B(bsxfun(@eq,M0,M0.'))); % compute modularity 150 | first_iteration = true; 151 | while Q-Q0>1e-10 152 | flag = true; % flag for within-hierarchy search 153 | while flag 154 | flag = false; 155 | for u=randperm(n) % loop over all nodes in random order 156 | ma = Mb(u); % current module of u 157 | dQ = Hnm(u,:) - Hnm(u,ma) + B(u,u); 158 | dQ(ma) = 0; % (line above) algorithm condition 159 | 160 | [max_dQ,mb] = max(dQ); % maximal increase in modularity and corresponding module 161 | if max_dQ>1e-10 % if maximal increase is positive 162 | flag = true; 163 | Mb(u) = mb; % reassign module 164 | 165 | Hnm(:,mb) = Hnm(:,mb)+B(:,u); % change node-to-module strengths 166 | Hnm(:,ma) = Hnm(:,ma)-B(:,u); 167 | end 168 | end 169 | end 170 | [~,~,Mb] = unique(Mb); % new module assignments 171 | 172 | M0 = M; 173 | if first_iteration 174 | M=Mb; 175 | first_iteration=false; 176 | else 177 | for u=1:n % loop through initial module assignments 178 | M(M0==u)=Mb(u); % assign new modules 179 | end 180 | end 181 | 182 | n=max(Mb); % new number of modules 183 | B1=zeros(n); % new weighted matrix 184 | for u=1:n 185 | for v=u:n 186 | bm=sum(sum(B(Mb==u,Mb==v))); % pool weights of nodes in same module 187 | B1(u,v)=bm; 188 | B1(v,u)=bm; 189 | end 190 | end 191 | B=B1; 192 | 193 | Mb=1:n; % initial module assignments 194 | Hnm=B; % node-to-module strength 195 | 196 | Q0=Q; 197 | Q=trace(B); % compute modularity 198 | end 199 | -------------------------------------------------------------------------------- /fcn/fcn_boxpts.m: -------------------------------------------------------------------------------- 1 | function [f,hdl,pts] = fcn_boxpts(x,lab,cmap,samenum,w) 2 | % clear all 3 | % close all 4 | % clc 5 | 6 | % load boxdata.mat 7 | % x = mu; 8 | 9 | if ~exist('samenum','var') | isempty(samenum) 10 | samenum = 0; 11 | end 12 | h = hist(lab,1:max(lab)); 13 | 14 | if ~exist('w','var') | isempty(samenum) 15 | w = 1.5; 16 | end 17 | 18 | sz = size(x); 19 | if sz(2) > 1 20 | lab = repmat(1:sz(2),sz(1),1); 21 | x = x(:); 22 | lab = lab(:); 23 | end 24 | 25 | if isempty(cmap) 26 | cmap = jet(max(lab)); 27 | end 28 | 29 | iqr = [25,50,75]; 30 | wid = 0.3; 31 | cmapdark = cmap - 0.25; 32 | cmapdark(cmapdark < 0) = 0; 33 | 34 | u = unique(lab); 35 | 36 | f = figure(gcf); 37 | ax = axes; 38 | hold(ax,'on'); 39 | 40 | for j = 1:length(u) 41 | idx = lab == u(j); 42 | vals = x(idx); 43 | prct = prctile(vals,iqr); 44 | 45 | dr = prct(3) - prct(1); 46 | upper = prct(3) + w*dr; 47 | lower = prct(1) - w*dr; 48 | 49 | whisk1 = min(vals(vals >= lower)); 50 | whisk2 = max(vals(vals <= upper)); 51 | 52 | if samenum ~= 0 53 | r = randperm(length(vals),samenum); 54 | vals = vals(r); 55 | end 56 | 57 | xvals = j + linspace(-wid*0.5,wid*0.5,length(vals)); 58 | s = randperm(length(xvals)); 59 | xvals = xvals(s); 60 | 61 | hdl.pointshandle{j} = scatter(xvals,vals,5,'filled'); 62 | set(hdl.pointshandle{j},'markerfacecolor',cmap(j,:),'markerfacealpha',0.5); 63 | 64 | pts{j}=[xvals(:),vals(:)]; 65 | 66 | xvals = j + [-wid,+wid,+wid,-wid,-wid]; 67 | yvals = [prct(1),prct(1),prct(3),prct(3),prct(1)]; 68 | hdl.boxhandle{j} = plot(xvals,yvals,'color','k','linewidth',0.75); 69 | hdl.medianhandle{j} = plot(j + [-wid,wid],prct(2)*ones(1,2),'color','k','linewidth',0.75); 70 | 71 | 72 | xvals = [j*ones(1,2), nan, j*ones(1,2)]; 73 | yvals = [prct(1),whisk1,nan,prct(3),whisk2]; 74 | hdl.whiskerhandle{j} = plot(xvals,yvals,'color','k','linewidth',0.75); 75 | 76 | end 77 | set(gca,... 78 | 'xlim',[-2*wid + 1,length(u) + 2*wid],... 79 | 'ylim',[min(x) - 0.05*range(x),max(x) + 0.05*range(x)]) 80 | 81 | -------------------------------------------------------------------------------- /hcp200.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brain-networks/edge-ts/90687626b4f5129725cc919a4f2df65a95b6ccd9/hcp200.mat -------------------------------------------------------------------------------- /main.m: -------------------------------------------------------------------------------- 1 | clear all 2 | close all 3 | clc 4 | 5 | % add helper functions to path 6 | addpath(genpath('fcn')); 7 | 8 | % load regional time series 9 | load data/ts.mat 10 | 11 | % z-score time series 12 | ts = zscore(ts); 13 | 14 | % get dimensions 15 | [ntime,nnodes] = size(ts); 16 | 17 | % calculate number of edges 18 | nedges = nnodes*(nnodes - 1)/2; 19 | 20 | % indices of unique edges (upper triangle) 21 | [u,v] = find(triu(ones(nnodes),1)); 22 | idx = (v - 1)*nnodes + u; 23 | 24 | %% calculate static fc 25 | fc = corr(ts); 26 | 27 | %% generate edge time series 28 | ets = ts(:,u).*ts(:,v); 29 | 30 | %% plot average time averaged edge time series against upper triangle fc 31 | fc_upper_triangle = fc(idx); 32 | figure,plot(fc_upper_triangle,mean(ets),'ko') 33 | xlabel('upper triangle fc matrix'); 34 | ylabel('time average of edge time series'); 35 | xlim([-1,1]); ylim([-1,1]); 36 | text(0.8,-0.8,sprintf('r = %.2f',corr(fc_upper_triangle,mean(ets)'))); 37 | 38 | %% calculate event amplitude 39 | 40 | % calculate co-fluctuation amplitude at each frame 41 | rms = sum(ets.^2,2).^0.5; 42 | 43 | % fraction of high- and low-amplitude frames to retain 44 | frackeep = 0.1; 45 | nkeep = round(ntime*frackeep); 46 | 47 | % sort co-fluctuation amplitude 48 | [~,idxsort] = sort(rms,'descend'); 49 | 50 | % estimate fc using just high-amplitude frames 51 | fctop = corr(ts(idxsort(1:nkeep),:)); 52 | 53 | % do the same using just low-amplitude 54 | fcbot = corr(ts(idxsort(end - nkeep + 1:end),:)); 55 | 56 | 57 | figure('position',[560,528,700,420]) 58 | subplot(3,2,2); plot(rms); 59 | xlabel('frames'); ylabel('rss'); 60 | 61 | subplot(3,2,1); imagesc(fc,[-1,1]); axis square; 62 | title('fc all time points'); 63 | 64 | subplot(3,2,3); imagesc(fctop,[-1,1]); axis square; 65 | title('fc high-amplitude time points'); 66 | 67 | subplot(3,2,4); scatter(fctop(idx),fc(idx),'k.'); xlim([-1,1]); ylim([-1,1]); 68 | text(-1,1,sprintf('r = %.2f',corr(fctop(idx),fc(idx)))); 69 | 70 | subplot(3,2,5); imagesc(fcbot,[-1,1]); axis square; 71 | title('fc low-amplitude time points'); 72 | 73 | subplot(3,2,6); scatter(fcbot(idx),fc(idx),'k.'); xlim([-1,1]); ylim([-1,1]); 74 | text(-1,1,sprintf('r = %.2f',corr(fcbot(idx),fc(idx)))); 75 | 76 | %% calculate similarity of time-averaged fc with high-/low-amplitude frames 77 | rho(1) = corr(fctop(idx),fc(idx)); 78 | rho(2) = corr(fcbot(idx),fc(idx)); 79 | 80 | %% calculate modularity of high-/low-amplitude fc 81 | numiter = 100; 82 | qtop = zeros(numiter,1); qbot = qtop; 83 | for iter = 1:numiter 84 | [~,qtop(iter)] = community_louvain(fctop,[],[],'negative_asym'); 85 | [~,qbot(iter)] = community_louvain(fcbot,[],[],'negative_asym'); 86 | end 87 | figure; 88 | f = fcn_boxpts([qtop,qbot],[],jet(2)); 89 | set(gca,'xtick',1:2,'xticklabel',{'high-amp','low-amp'}); 90 | ylabel('modularity, q'); --------------------------------------------------------------------------------