Skip to content

Commit

Permalink
Merge pull request #37 from nguyen-td/feature
Browse files Browse the repository at this point in the history
Add PAC
  • Loading branch information
nguyen-td authored May 11, 2023
2 parents dbf9d10 + db79cdf commit 057785c
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 54 deletions.
30 changes: 18 additions & 12 deletions libs/nolte/data2bs_threenorm.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,13 @@
% nave: number of averages

[ndat,nchan]=size(data);
maxfreqbin=sum(freqpairs)-1;
f1=freqpairs(1);f2=freqpairs(2);
[nf, ndum] = size(freqpairs);

if nf == 1
maxfreqbin=sum(freqpairs)-1;
else
maxfreqbin = sum(max(freqpairs(:, 1)) + max(freqpairs(:, 2) - 1));
end
mywindow=repmat(hanning(segleng),1,nchan);
kontrand=0;
nrun=1;
Expand All @@ -43,16 +48,14 @@

end

norms=zeros(nchan,nchan,nchan);

nep=floor(ndat/epleng);

nseg=floor((epleng-segleng)/segshift)+1; %total number of segments
datafft=zeros(maxfreqbin,nchan,nseg,nep);
norm1=zeros(nchan,1);
norm2=zeros(nchan,1);
norm3=zeros(nchan,1);
norms=zeros(nchan,nchan,nchan);
norm1 = zeros(nchan, nf);
norm2 = zeros(nchan, nf);
norm3 = zeros(nchan, nf);
norms = zeros(nchan, nchan, nchan, nf);

for j=1:nep
dataep=data((j-1)*epleng+1:j*epleng,:);
Expand All @@ -68,9 +71,12 @@
for j=1:nep
for i=1:nseg
for k=1:nchan
norm1(k)=norm1(k)+abs(datafft(f1,k,i,j)).^3;
norm2(k)=norm2(k)+abs(datafft(f2,k,i,j)).^3;
norm3(k)=norm3(k)+abs(datafft(f1+f2-1,k,i,j)).^3;
for f = 1:nf
f1 = freqpairs(f, 1); f2 = freqpairs(f, 2);
norm1(k, f) = norm1(k, f) + abs(datafft(f1, k, i,j )).^3;
norm2(k, f) = norm2(k, f) + abs(datafft(f2, k, i, j)).^3;
norm3(k, f) = norm3(k, f) + abs(datafft(f1+f2-1, k, i, j)).^3;
end
end
nave=nave+1;
end
Expand All @@ -82,7 +88,7 @@
for i1=1:nchan
for i2=1:nchan
for i3=1:nchan
norms(i1,i2,i3)=norm1(i1)*norm2(i2)*norm3(i3);
norms(i1, i2, i3, :) = norm1(i1, :) .* norm2(i2, :) .* norm3(i3, :);
end
end
end
64 changes: 43 additions & 21 deletions pac_bispec.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
% Outputs:
% b_orig - ROI x ROI bispectrum
% b_anti - ROI x ROI antisymmetrized bispectrum
% b_orig_norm - ROI x ROI normalized bispectrum
% b_anti_norm - ROI x ROI normalized antisymmetrized bispectrum
% b_orig_norm - ROI x ROI bicoherence (normalized by threenorm)
% b_anti_norm - ROI x ROI antisymmetrized bicoherence (normalized by threenorm)
%
% Copyright (C) Franziska Pellegrini, [email protected]

Expand All @@ -20,45 +20,67 @@
segleng = params.segleng;
segshift = params.segshift;
epleng = params.epleng;
filt = params.filt;
fcomb = params.fcomb;
fs = params.fs;

fres = fs;
frqs = sfreqs(fres, fs);
freqinds_low = [find(frqs==filt.low) find(frqs==filt.high-filt.low)];
freqinds_up = [find(frqs==filt.low) find(frqs==filt.high)];

for proi = 1:nroi
% extract all frequencies in the selected bands
size_low = size(fcomb.low, 2);
size_high = size(fcomb.high, 2);
inds_low = frqs >= fcomb.low(1) & frqs <= fcomb.low(size_low);
inds_high = frqs >= fcomb.high(1) & frqs <= fcomb.high(size_high);
frqs_low = frqs(inds_low);
frqs_high = frqs(inds_high);

% determine all frequency combinations
[m, n] = ndgrid(frqs_low, frqs_high);
frqs_combs = [m(:),n(:)];
n_combs = size(frqs_combs, 1);
warning('PAC is going to be estimated on %d frequency pair(s).', n_combs);

freqinds_low = zeros(n_combs, 2);
freqinds_up = zeros(n_combs, 2);
for i = 1:n_combs
low = frqs_combs(i,1);
high = frqs_combs(i,2);
freqinds_low(i,:) = [find(frqs == low) find(frqs == high - low)];
freqinds_up(i,:) = [find(frqs == low) find(frqs == high)];
end

for proi = 1:nroi
for aroi = proi:nroi

% upper freqs
X = data([proi aroi],:,:);
[bs_up,~] = data2bs_event(X(:,:)', segleng, segshift, epleng, freqinds_up);
biv_orig_up = ([abs(bs_up(1, 2, 2)) abs(bs_up(2, 1, 1))]);
xx = bs_up - permute(bs_up, [2 1 3]); %Bkmm - Bmkm
biv_anti_up = ([abs(xx(1, 2, 2)) abs(xx(2, 1, 1))]);
[bs_up,~] = data2bs_event(X(:,:)', segleng, segshift, epleng, freqinds_up);
biv_orig_up = ([abs(bs_up(1, 2, 2, :)) abs(bs_up(2, 1, 1, :))]);
xx = bs_up - permute(bs_up, [2 1 3 4]); %Bkmm - Bmkm
biv_anti_up = ([abs(xx(1, 2, 2, :)) abs(xx(2, 1, 1, :))]);

% normalized by threenorm
[RTP_up,~]=data2bs_threenorm(X(:,:)',segleng,segshift,epleng,freqinds_up);
bicoh_up=bs_up./sqrt(RTP_up);
[RTP_up,~] = data2bs_threenorm(X(:,:)', segleng, segshift, epleng, freqinds_up);
bicoh_up = bs_up ./ RTP_up;
bicoh_up = mean(bicoh_up, 4);
biv_orig_up_norm = ([abs(bicoh_up(1, 2, 2)) abs(bicoh_up(2, 1, 1))]);
xx=bicoh_up-permute(bicoh_up, [2 1 3]);
xx = bicoh_up-permute(bicoh_up, [2 1 3]);
biv_anti_up_norm = ([abs(xx(1, 2, 2)) abs(xx(2, 1, 1))]);

% lower freqs
[bs_low,~] = data2bs_event(X(:,:)', segleng, segshift, epleng, freqinds_low);
biv_orig_low = ([abs(bs_low(1, 2, 2)) abs(bs_low(2, 1, 1))]);
xx = bs_low - permute(bs_low, [2 1 3]);
biv_anti_low =([abs(xx(1, 2, 2)) abs(xx(2, 1, 1))]);
biv_orig_low = ([abs(bs_low(1, 2, 2, :)) abs(bs_low(2, 1, 1, :))]);
xx = bs_low - permute(bs_low, [2 1 3, 4]);
biv_anti_low =([abs(xx(1, 2, 2, :)) abs(xx(2, 1, 1, :))]);

% normalized by threenorm
[RTP_low,~]=data2bs_threenorm(X(:,:)',segleng,segshift,epleng,freqinds_low);
bicoh_low=bs_low./sqrt(RTP_low);
[RTP_low,~] = data2bs_threenorm(X(:,:)', segleng, segshift, epleng, freqinds_low);
bicoh_low = bs_low ./ RTP_low;
bicoh_low = mean(bicoh_low, 4);
biv_orig_low_norm = ([abs(bicoh_low(1, 2, 2)) abs(bicoh_low(2, 1, 1))]);
xx=bicoh_low-permute(bicoh_low, [2 1 3]);
xx = bicoh_low-permute(bicoh_low, [2 1 3]);
biv_anti_low_norm = ([abs(xx(1, 2, 2)) abs(xx(2, 1, 1))]);

b_orig(aroi,proi) = mean([biv_orig_up(1) biv_orig_low(1)]); % mean across the two possible bispec combinations
b_orig(aroi,proi) = mean([biv_orig_up(1) biv_orig_low(1)]);
b_orig(proi,aroi) = mean([biv_orig_up(2) biv_orig_low(2)]);
b_anti(aroi,proi) = mean([biv_anti_up(1) biv_anti_low(1)]);
b_anti(proi,aroi) = mean([biv_anti_up(2) biv_anti_low(2)]);
Expand Down
11 changes: 6 additions & 5 deletions pop_roi_connect.m
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@
% (shape: 101,68,68) or all snippets (shape: n_snips,101,68,68). Default is 'mean_snips.'
% 'freqresolution' - [integer] Desired frequency resolution (in number of frequencies).
% If specified, the signal is zero padded accordingly. Default is 0 (means no padding).
% 'filt' - [struct] Frequency combination for which PAC is computed. Must have fields 'low' and
% 'high' with filt.low < filt.high. For example, filt.low = 10 (Hz), filt.high = 50 (Hz).
% Default is {} (this will cause an error).
% 'fcomb' - [struct] Frequency combination for which PAC is computed (in Hz). Must have fields 'low' and
% 'high' with fcomb.low < fcomb.high. For example, fcomb.low = 10 and fcomb.high = 50 if single
% frequencies are used. fcomb.low = [4 8] and fcomb.high = [48 50] if frequency bands are used
% (might take a long time to compute, so use with caution). Default is {} (this will cause an error).
% 'bs_outopts' - [integer] Option which bispectral tensors should be stored in EEG.roi.PAC. Default is 1.
% 1 - store all tensors: b_orig, b_anti, b_orig_norm, b_anti_norm
% 2 - only store: b_orig, b_anti
Expand Down Expand Up @@ -160,7 +161,7 @@
'snip_length' 'integer' { } 60;
'fcsave_format' 'string' { 'mean_snips', 'all_snips'} 'mean_snips';
'freqresolution' 'integer' { } 0;
'filt' 'struct' { } struct; ...
'fcomb' 'struct' { } struct; ...
'bs_outopts' 'integer' { } 1}, 'pop_roi_connect');
if ischar(g), error(g); end

Expand Down Expand Up @@ -259,7 +260,7 @@
end

if ~isempty(intersect(g.methods, {'PAC'}))
EEG = roi_pac(EEG, g.filt, g.bs_outopts);
EEG = roi_pac(EEG, g.fcomb, g.bs_outopts);
end

if nargout > 1
Expand Down
48 changes: 46 additions & 2 deletions pop_roi_connectplot.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
% 'crossspecpow' : Average cross-spectrum power for each ROI
% 'mic' : Maximized Imaginary Coherency for each ROI
% 'mim' : Multivariate Interaction Measure for each ROI
% 'pac' : Phase-amplitude coupling for a certain frequency (band) combination based on bicoherence
% 'pac_anti': Phase-amplitude coupling for a certain frequency (band) combination based on the antisymmetrized bicoherence
% 'freqrange' - [min max] frequency range in Hz. Default is to plot broadband power.
% 'smooth' - [float] smoothing factor for cortex surface plotting
% 'plotcortex' - ['on'|'off'] plot results on smooth cortex. Default is 'on'
Expand Down Expand Up @@ -197,8 +199,29 @@
splot(end ).psd = 0;
splot(end ).plot3d = plot3dFlag;
end


if isfield(EEG.roi, 'PAC')
splot(end+1).label = 'ROI to ROI Phase-amplitude coupling';
splot(end ).labelshort = 'Phase-amplitude coupling';
splot(end ).acronym = 'PAC'; % PAC based on bicoherence
splot(end ).unit = 'PAC'; % not used yet
splot(end ).cortex = cortexFlag;
splot(end ).matrix = 1;
splot(end ).psd = 0;
splot(end ).plot3d = plot3dFlag;
end

if isfield(EEG.roi, 'PAC')
splot(end+1).label = 'ROI to ROI Phase-amplitude coupling';
splot(end ).labelshort = 'Phase-amplitude coupling';
splot(end ).acronym = 'PAC_anti'; % PAC based on antisymmetrized bicoherence
splot(end ).unit = 'PAC'; % not used yet
splot(end ).cortex = cortexFlag;
splot(end ).matrix = 1;
splot(end ).psd = 0;
splot(end ).plot3d = plot3dFlag;
end

if nargin < 2

cb_select = [ 'usrdat = get(gcf, ''userdata'');' ...
Expand Down Expand Up @@ -395,6 +418,27 @@
plotPSDFreq = S.freqs(frq_inds);
plotPSD = PS(frq_inds, :);
matrix = PSarea2area;

case {'pac'}
if isfield(S.PAC, 'b_orig_norm')
matrix = S.PAC.b_orig_norm;
elseif isfield(S.PAC, 'b_orig')
matrix = S.PAC.b_orig;
else
error('PAC (original bicoherence) cannot be plotted, field is missing.')
end
cortexPlot = mean(matrix, 2);

case {'pac_anti'}
if isfield(S.PAC, 'b_anti_norm')
matrix = S.PAC.b_anti_norm;
elseif isfield(S.PAC, 'b_anti')
matrix = S.PAC.b_anti;
else
error('PAC (antisymmetrized bicoherence) cannot be plotted, field is missing.')
end
cortexPlot = mean(matrix, 2);

end

% get seed
Expand Down Expand Up @@ -721,7 +765,7 @@ function roi_plotcoloredlobes( EEG, matrix, titleStr, measure, hemisphere, group
else
set(gca,'ytick',1:n_roi_labels,'yticklabel',labels(hem_idx{1}:hem_idx{2}:n_roi_labels), 'fontsize', 7, 'TickLength',[0.015, 0.02], 'LineWidth',0.75);
end
h = title([ 'ROI to ROI ' upper(measure) ' (' titleStr ')' ]);
h = title([ 'ROI to ROI ' upper(replace_underscores(measure)) ' (' titleStr ')' ]);
set(h, 'fontsize', 16);
xtickangle(90)
end
Expand Down
27 changes: 13 additions & 14 deletions roi_pac.m
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
% roi_pac() - Compute phase-amplitude coupling (PAC) between ROIs
% (cf. Zandvoort and Nolte, 2021). Wrapper function for
% pac_bispec().
% roi_pac() - Compute phase-amplitude coupling (PAC) between ROIs (cf. Zandvoort and Nolte, 2021).
% Wrapper function for pac_bispec().
%
% Usage:
% EEG = roi_pac(EEG, filt, bs_outopts);
% EEG = roi_pac(EEG, fcomb, bs_outopts);
%
% Inputs:
% EEG - EEGLAB dataset with ROI activity computed.
% filt - [struct] Frequency combination for which PAC is computed. Must have fields
% 'low' and 'high' with filt.low < filt.high. For example, filt.low =
% 10 (Hz), filt.high = 50 (Hz).
% fcomb - [struct] Frequency combination for which PAC is computed. Must have fields
% 'low' and 'high' with fcomb.low < fcomb.high. For example, fcomb.low =
% 10 (Hz), fcomb.high = 50 (Hz).
% bs_outopts - [integer] Option which bispectral tensors should be stored in EEG.roi.PAC. Default is 1.
% 1 - store all tensors: b_orig, b_anti, b_orig_norm, b_anti_norm
% 2 - only store: b_orig, b_anti
Expand All @@ -21,7 +20,7 @@
% EEG - EEG structure with EEG.roi field updated and now containing
% connectivity information.

function EEG = roi_pac(EEG, filt, bs_outopts)
function EEG = roi_pac(EEG, fcomb, bs_outopts)

if nargin < 2
help roi_pac;
Expand All @@ -34,29 +33,29 @@
data = EEG.roi.source_roi_data;
end

if ~isfield(filt, 'low') || ~isfield(filt, 'high')
if ~isfield(fcomb, 'low') || ~isfield(fcomb, 'high')
help roi_pac;
error('Frequency pair cannot be found - check the documentation for the filt input parameter')
error('Frequency pair cannot be found - check the documentation for the fcomb input parameter')
end

if filt.high < filt.low
if fcomb.high < fcomb.low
help roi_pac;
error('filt.high must be smaller than filt.low - check the documentation for the filt input parameter')
error('fcomb.high must be smaller than fcomb.low - check the documentation for the fcomb input parameter')
end

[~, ndat, ~] = size(data);
nROI = EEG.roi.nROI;
segleng = ndat;
segshift = floor(ndat/2);
epleng = ndat;
fs = ndat/2;
fs = EEG.srate;

params = [];
params.nROI = nROI;
params.segleng = segleng;
params.segshift = segshift;
params.epleng = epleng;
params.filt = filt;
params.fcomb = fcomb;
params.fs = fs;

[b_orig, b_anti, b_orig_norm,b_anti_norm] = pac_bispec(data, params);
Expand Down
61 changes: 61 additions & 0 deletions test_pipes/test_pac.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
% Test PAC implementation for different inputs: single frequencies and frequency bands.
%% Run pipeline
clear
eeglab

eeglabp = fileparts(which('eeglab.m'));
EEG = pop_loadset('filename','eeglab_data_epochs_ica.set','filepath',fullfile(eeglabp, 'sample_data/'));
EEG = pop_resample( EEG, 100);
EEG = pop_epoch( EEG, { }, [-0.5 1.5], 'newname', 'EEG Data epochs epochs', 'epochinfo', 'yes');
EEG = pop_select( EEG, 'trial',1:30);
[ALLEEG, EEG, CURRENTSET] = eeg_store(ALLEEG, EEG);
eeglab redraw;

EEG = pop_dipfit_settings( EEG, 'hdmfile',fullfile(eeglabp, 'plugins','dipfit','standard_BEM','standard_vol.mat'), ...
'coordformat','MNI','mrifile',fullfile(eeglabp, 'plugins','dipfit','standard_BEM','standard_mri.mat'),...
'chanfile',fullfile(eeglabp, 'plugins','dipfit','standard_BEM','elec', 'standard_1005.elc'),...
'coord_transform',[0.83215 -15.6287 2.4114 0.081214 0.00093739 -1.5732 1.1742 1.0601 1.1485] ,'chansel',[1:32] );

EEG = pop_leadfield(EEG, 'sourcemodel',fullfile(eeglabp,'functions','supportfiles','head_modelColin27_5003_Standard-10-5-Cap339.mat'), ...
'sourcemodel2mni',[0 -24 -45 0 0 -1.5708 1000 1000 1000] ,'downsample',1);
EEG = pop_roi_activity(EEG, 'leadfield',EEG.dipfit.sourcemodel,'model','LCMV','modelparams',{0.05},'atlas','LORETA-Talairach-BAs','nPCA',3);

%% Test bispectrum for single frequency inputs
low = 10;
high = 50;

fcomb.low = low;
fcomb.high = high;

EEG1 = pop_roi_connect(EEG, 'methods', {'PAC', 'MIM', 'COH'}, 'fcomb', fcomb); % test all 3 connectivity functions (data2spwctrgc, data2strgcmim, roi_pac)
EEG2 = pop_roi_connect(EEG, 'methods', {'PAC'}, 'fcomb', fcomb, 'bs_outopts', 4); % compute only b_orig, b_orig_norm
EEG3 = pop_roi_connect(EEG, 'methods', {'PAC'}, 'fcomb', fcomb, 'bs_outopts', 5); % compute only b_anti, b_anti_norm

%% Test bispectrum for frequency band inputs
low = [4 8];
high = [48 50];

fcomb.low = low;
fcomb.high = high;

tic
EEG4 = pop_roi_connect(EEG, 'methods', {'PAC', 'MIM', 'COH'}, 'fcomb', fcomb); % test all 3 connectivity functions (data2spwctrgc, data2strgcmim, roi_pac)toc
toc

%% Test PAC plotting
% Test for single frequency inputs
pop_roi_connectplot(EEG1, 'measure', 'pac', 'plotmatrix', 'on');
pop_roi_connectplot(EEG1, 'measure', 'pac_anti', 'plotmatrix', 'on');

% Provoke errors by plotting bispectral tensors that do not exist
pop_roi_connectplot(EEG2, 'measure', 'pac_anti', 'plotmatrix', 'on'); % bs_outopts 4 means only original bispectra are computed, so cannot plot anti
pop_roi_connectplot(EEG3, 'measure', 'pac', 'plotmatrix', 'on'); % bs_outopts 5 means only antisymm. bispectra are computed, so cannot plot normal bispectrum

% Test for frequency bands
pop_roi_connectplot(EEG4, 'measure', 'pac', 'plotmatrix', 'on');
pop_roi_connectplot(EEG4, 'measure', 'pac_anti', 'plotmatrix', 'on');

% plot MIM and COH as a sanity check
pop_roi_connectplot(EEG1, 'measure', 'mim', 'plotmatrix', 'on');
pop_roi_connectplot(EEG1, 'measure', 'coh', 'plotmatrix', 'on');

0 comments on commit 057785c

Please sign in to comment.