├── emptyEEG.mat ├── GED_tutorial.pdf ├── __pycache__ ├── pytopo.cpython-37.pyc └── filterFGxfun.cpython-37.pyc ├── README.md ├── pytopo.py ├── filterFGx.m ├── filterFGxfun.py ├── topoplotIndie.m ├── Cohen_GEDsimEEG.m └── Cohen_GEDsimEEG.ipynb /emptyEEG.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikexcohen/GED_tutorial/HEAD/emptyEEG.mat -------------------------------------------------------------------------------- /GED_tutorial.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikexcohen/GED_tutorial/HEAD/GED_tutorial.pdf -------------------------------------------------------------------------------- /__pycache__/pytopo.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikexcohen/GED_tutorial/HEAD/__pycache__/pytopo.cpython-37.pyc -------------------------------------------------------------------------------- /__pycache__/filterFGxfun.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikexcohen/GED_tutorial/HEAD/__pycache__/filterFGxfun.cpython-37.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GED_tutorial 2 | Code accompanying publication (currently preprint submitted to NeuroImage) on GED tutorial. 3 | 4 | See the pdf manuscript, which is also available via https://arxiv.org/abs/2104.12356 5 | 6 | -------------------------------------------------------------------------------- /pytopo.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from matplotlib import patches 4 | # import scipy.io as sio 5 | # from scipy import interpolate 6 | from scipy.interpolate import griddata 7 | 8 | def topoplotIndie(Values,chanlocs,title='',ax=0): 9 | 10 | ## import and convert channel locations from EEG structure 11 | labels = [] 12 | Th = [] 13 | Rd = [] 14 | x = [] 15 | y = [] 16 | 17 | # 18 | for ci in range(len(chanlocs[0])): 19 | labels.append(chanlocs[0]['labels'][ci][0]) 20 | Th.append(np.pi/180*chanlocs[0]['theta'][ci][0][0]) 21 | Rd.append(chanlocs[0]['radius'][ci][0][0]) 22 | x.append( Rd[ci]*np.cos(Th[ci]) ) 23 | y.append( Rd[ci]*np.sin(Th[ci]) ) 24 | 25 | 26 | 27 | ## remove infinite and NaN values 28 | # ... 29 | 30 | # plotting factors 31 | headrad = .5 32 | plotrad = .6 33 | 34 | # squeeze coords into head 35 | squeezefac = headrad/plotrad 36 | # to plot all inside the head cartoon 37 | x = np.array(x)*squeezefac 38 | y = np.array(y)*squeezefac 39 | 40 | 41 | ## create grid 42 | xmin = np.min( [-headrad,np.min(x)] ) 43 | xmax = np.max( [ headrad,np.max(x)] ) 44 | ymin = np.min( [-headrad,np.min(y)] ) 45 | ymax = np.max( [ headrad,np.max(y)] ) 46 | xi = np.linspace(xmin,xmax,67) 47 | yi = np.linspace(ymin,ymax,67) 48 | 49 | # spatially interpolated data 50 | Xi, Yi = np.mgrid[xmin:xmax:67j,ymin:ymax:67j] 51 | Zi = griddata(np.array([y,x]).T,Values,(Yi,Xi)) 52 | # f = interpolate.interp2d(y,x,Values) 53 | # Zi = f(yi,xi) 54 | 55 | ## Mask out data outside the head 56 | mask = np.sqrt(Xi**2 + Yi**2) <= headrad 57 | Zi[mask == 0] = np.nan 58 | 59 | 60 | ## create topography 61 | # make figure 62 | if ax==0: 63 | fig = plt.figure() 64 | ax = fig.add_subplot(111, aspect = 1) 65 | clim = np.max(np.abs(Zi[np.isfinite(Zi)]))*.8 66 | ax.contourf(yi,xi,Zi,60,cmap=plt.cm.jet,zorder=1, vmin=-clim,vmax=clim) 67 | 68 | # head ring 69 | circle = patches.Circle(xy=[0,0],radius=headrad,edgecolor='k',facecolor='w',zorder=0) 70 | ax.add_patch(circle) 71 | 72 | # ears 73 | circle = patches.Ellipse(xy=[np.min(xi),0],width=.05,height=.2,angle=0,edgecolor='k',facecolor='w',zorder=-1) 74 | ax.add_patch(circle) 75 | circle = patches.Ellipse(xy=[np.max(xi),0],width=.05,height=.2,angle=0,edgecolor='k',facecolor='w',zorder=-1) 76 | ax.add_patch(circle) 77 | 78 | # nose (top, left, right) 79 | xy = [[0,np.max(yi)+.06], [-.2,.2],[.2,.2]] 80 | polygon = patches.Polygon(xy=xy,facecolor='w',edgecolor='k',zorder=-1) 81 | ax.add_patch(polygon) 82 | 83 | 84 | # add the electrode markers 85 | ax.scatter(y,x,marker='o', c='k', s=15, zorder = 3) 86 | 87 | ax.set_xlim([-.6,.6]) 88 | ax.set_ylim([-.6,.6]) 89 | ax.axis('off') 90 | ax.set_title(title) 91 | ax.set_aspect('equal') 92 | # plt.show() 93 | -------------------------------------------------------------------------------- /filterFGx.m: -------------------------------------------------------------------------------- 1 | function [filtdat,empVals,fx] = filterFGx(data,srate,f,fwhm,showplot) 2 | % filterFGx Narrow-band filter via frequency-domain Gaussian 3 | % [filtdat,empVals] = filterFGx(data,srate,f,fwhm,showplot) 4 | % 5 | % 6 | % INPUTS 7 | % data : 1 X time or chans X time 8 | % srate : sampling rate in Hz 9 | % f : peak frequency of filter 10 | % fhwm : standard deviation of filter, 11 | % defined as full-width at half-maximum in Hz 12 | % showplot : set to true to show the frequency-domain filter shape 13 | % 14 | % OUTPUTS 15 | % filtdat : filtered data 16 | % empVals : the empirical frequency and FWHM (in Hz and in ms) 17 | % 18 | % Empirical frequency and FWHM depend on the sampling rate and the 19 | % number of time points, and may thus be slightly different from 20 | % the requested values. 21 | % 22 | % mikexcohen@gmail.com 23 | 24 | %% input check 25 | 26 | if size(data,1)>size(data,2) 27 | % help filterFGx 28 | % error('Check data size') 29 | end 30 | 31 | if (f-fwhm)<0 32 | % help filterFGx 33 | % error('increase frequency or decrease FWHM') 34 | end 35 | 36 | if nargin<4 37 | help filterFGx 38 | error('Not enough inputs') 39 | end 40 | 41 | if fwhm<=0 42 | error('FWHM must be greater than 0') 43 | end 44 | 45 | if nargin<5 46 | showplot=false; 47 | end 48 | 49 | %% compute and apply filter 50 | 51 | % frequencies 52 | hz = linspace(0,srate,size(data,2)); 53 | 54 | % create Gaussian 55 | s = fwhm*(2*pi-1)/(4*pi); % normalized width 56 | x = hz-f; % shifted frequencies 57 | fx = exp(-.5*(x/s).^2); % gaussian 58 | fx = fx./abs(max(fx)); % gain-normalized 59 | 60 | %% filter 61 | 62 | % filtdat = 2*real( ifft( bsxfun(@times,fft(data,[],2),fx) ,[],2) ); 63 | filtdat = 2*real(ifft( fft(data,[],2).*fx ,[],2)); 64 | 65 | %% compute empirical frequency and standard deviation 66 | 67 | idx = dsearchn(hz',f); 68 | empVals(1) = hz(idx); 69 | 70 | % find values closest to .5 after MINUS before the peak 71 | empVals(2) = hz(idx-1+dsearchn(fx(idx:end)',.5)) - hz(dsearchn(fx(1:idx)',.5)); 72 | 73 | % also temporal FWHM 74 | tmp = abs(hilbert(real(fftshift(ifft(fx))))); 75 | tmp = tmp./max(tmp); 76 | tx = (0:length(data)-1)/srate; 77 | [~,idxt] = max(tmp); 78 | empVals(3) = (tx(idxt-1+dsearchn(tmp(idxt:end)',.5)) - tx(dsearchn(tmp(1:idxt)',.5)))*1000; 79 | 80 | %% inspect the Gaussian (turned off by default) 81 | 82 | if showplot 83 | figure(10001+showplot),clf 84 | subplot(211) 85 | plot(hz,fx,'o-') 86 | hold on 87 | plot([hz(dsearchn(fx(1:idx)',.5)) hz(idx-1+dsearchn(fx(idx:end)',.5))],[fx(dsearchn(fx(1:idx)',.5)) fx(idx-1+dsearchn(fx(idx:end)',.5))],'k--') 88 | set(gca,'xlim',[max(f-10,0) f+10]); 89 | 90 | title([ 'Requested: ' num2str(f) ', ' num2str(fwhm) ' Hz; Empirical: ' num2str(empVals(1)) ', ' num2str(empVals(2)) ' Hz' ]) 91 | xlabel('Frequency (Hz)'), ylabel('Amplitude gain') 92 | 93 | subplot(212) 94 | tmp1 = real(fftshift(ifft(fx))); tmp1 = tmp1./max(tmp1); 95 | tmp2 = abs(hilbert(tmp1)); 96 | plot(tx,tmp1, tx,tmp2), zoom on 97 | xlabel('Time (s)'), ylabel('Amplitude gain') 98 | end 99 | 100 | %% done. 101 | -------------------------------------------------------------------------------- /filterFGxfun.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import scipy 4 | import scipy.signal 5 | 6 | 7 | def filterFGx(data,srate,f,fwhm,showplot=False): 8 | ''' 9 | :: filterFGx Narrow-band filter via frequency-domain Gaussian 10 | filtdat,empVals]= filterFGx(data,srate,f,fwhm,showplot=0) 11 | 12 | 13 | INPUTS 14 | data : 1 X time or chans X time 15 | srate : sampling rate in Hz 16 | f : peak frequency of filter 17 | fhwm : standard deviation of filter, 18 | defined as full-width at half-maximum in Hz 19 | showplot : set to true to show the frequency-domain filter shape (default=false) 20 | 21 | OUTPUTS 22 | filtdat : filtered data 23 | empVals : the empirical frequency and FWHM (in Hz and in ms) 24 | 25 | Empirical frequency and FWHM depend on the sampling rate and the 26 | number of time points, and may thus be slightly different from 27 | the requested values. 28 | 29 | mikexcohen@gmail.com 30 | ''' 31 | 32 | ## compute filter 33 | 34 | # frequencies 35 | hz = np.linspace(0,srate,data.shape[1]) 36 | 37 | # create Gaussian 38 | s = fwhm*(2*np.pi-1)/(4*np.pi) # normalized width 39 | x = hz-f # shifted frequencies 40 | fx = np.exp(-.5*(x/s)**2) # gaussian 41 | fx = fx/np.max(fx) # gain-normalized 42 | 43 | # apply the filter 44 | filtdat = np.zeros( np.shape(data) ) 45 | for ci in range(filtdat.shape[0]): 46 | filtdat[ci,:] = 2*np.real( np.fft.ifft( np.fft.fft(data[ci,:])*fx ) ) 47 | 48 | 49 | 50 | ## compute empirical frequency and standard deviation 51 | 52 | empVals = [0,0,0] 53 | 54 | idx = np.argmin(np.abs(hz-f)) 55 | empVals[0] = hz[idx] 56 | 57 | # find values closest to .5 after MINUS before the peak 58 | empVals[1] = hz[idx-1+np.argmin(np.abs(fx[idx:]-.5))] - hz[np.argmin(np.abs(fx[:idx]-.5))] 59 | 60 | # also temporal FWHM 61 | tmp = np.abs(scipy.signal.hilbert(np.real(np.fft.fftshift(np.fft.ifft(fx))))) 62 | tmp = tmp / np.max(tmp) 63 | tx = np.arange(0,data.shape[1])/srate 64 | idxt = np.argmax(tmp) 65 | 66 | empVals[2] = (tx[idxt-1+np.argmin(np.abs(tmp[idxt:]-.5))] - tx[np.argmin(np.abs(tmp[0:idxt]-.5))])*1000 67 | 68 | 69 | 70 | ## inspect the Gaussian (turned off by default) 71 | 72 | # showplot=True 73 | 74 | if showplot: 75 | plt.subplot(211) 76 | plt.plot(hz,fx,'o-') 77 | xx = [ hz[np.argmin(np.abs(fx[:idx]-.5))], hz[idx-1+np.argmin(np.abs(fx[idx:]-.5))] ] 78 | yy = [ fx[np.argmin(np.abs(fx[:idx]-.5))], fx[idx-1+np.argmin(np.abs(fx[idx:]-.5))] ] 79 | plt.plot(xx,yy,'k--') 80 | plt.xlim([np.max(f-10,0),f+10]) 81 | 82 | plt.title('Requested: %g, %g Hz; Empirical: %.2f, %.2f Hz' %(f,fwhm,empVals[0],empVals[1]) ) 83 | plt.xlabel('Frequency (Hz)') 84 | plt.ylabel('Amplitude gain') 85 | 86 | plt.subplot(212) 87 | tmp1 = np.real(np.fft.fftshift(np.fft.ifft(fx))) 88 | tmp1 = tmp1 / np.max(tmp1) 89 | tmp2 = np.abs(scipy.signal.hilbert(tmp1)) 90 | plt.plot(tx-np.mean(tx),tmp1, tx-np.mean(tx),tmp2) 91 | plt.xlim([-empVals[2]*2/1000,empVals[2]*2/1000]) 92 | plt.xlabel('Time (s)') 93 | plt.ylabel('Amplitude gain') 94 | plt.show() 95 | # 96 | 97 | ## outputs 98 | return filtdat,empVals -------------------------------------------------------------------------------- /topoplotIndie.m: -------------------------------------------------------------------------------- 1 | function [handle,pltchans,epos] = topoplotIndie(Values,chanlocs,varargin) 2 | 3 | %% Set defaults 4 | 5 | headrad = 0.5; % actual head radius - Don't change this! 6 | GRID_SCALE = 67; % plot map on a 67X67 grid 7 | CIRCGRID = 201; % number of angles to use in drawing circles 8 | HEADCOLOR = [0 0 0]; % default head color (black) 9 | HLINEWIDTH = 1.7; % default linewidth for head, nose, ears 10 | BLANKINGRINGWIDTH = .035;% width of the blanking ring 11 | HEADRINGWIDTH = .007;% width of the cartoon head ring 12 | plotrad = .6; 13 | Values = double(Values); 14 | SHADING = 'interp'; 15 | CONTOURNUM = 6; 16 | ELECTRODES = 'on'; 17 | 18 | nargs = nargin; 19 | if nargs > 2 20 | for i = 1:2:length(varargin) 21 | Param = lower(varargin{i}); 22 | Value = varargin{i+1}; 23 | switch Param 24 | case 'numcontour' 25 | CONTOURNUM = Value; 26 | case 'electrodes' 27 | ELECTRODES = lower(Value); 28 | case 'plotrad' 29 | plotrad = Value; 30 | case 'shading' 31 | SHADING = lower(Value); 32 | if ~any(strcmp(SHADING,{'flat','interp'})) 33 | error('Invalid shading parameter') 34 | end 35 | end 36 | end 37 | end 38 | 39 | Values = Values(:); % make Values a column vector 40 | 41 | %% Read channel location 42 | labels={chanlocs.labels}; 43 | Th=[chanlocs.theta]; 44 | Rd=[chanlocs.radius]; 45 | 46 | Th = pi/180*Th; % convert degrees to radians 47 | allchansind = 1:length(Th); 48 | plotchans = 1:length(chanlocs); 49 | 50 | %% remove infinite and NaN values 51 | 52 | inds = union(find(isnan(Values)), find(isinf(Values))); % NaN and Inf values 53 | for chani=1:length(chanlocs) 54 | if isempty(chanlocs(chani).X); inds = [inds chani]; end 55 | end 56 | 57 | plotchans = setdiff(plotchans,inds); 58 | 59 | [x,y] = pol2cart(Th,Rd); % transform electrode locations from polar to cartesian coordinates 60 | plotchans = abs(plotchans); % reverse indicated channel polarities 61 | allchansind = allchansind(plotchans); 62 | Th = Th(plotchans); 63 | Rd = Rd(plotchans); 64 | x = x(plotchans); 65 | y = y(plotchans); 66 | labels = char(labels(plotchans)); % remove labels for electrodes without locations 67 | Values = Values(plotchans); 68 | intrad = min(1.0,max(Rd)*1.02); % default: just outside the outermost electrode location 69 | 70 | %% Find plotting channels 71 | pltchans = find(Rd <= plotrad); % plot channels inside plotting circle 72 | intchans = find(x <= intrad & y <= intrad); % interpolate and plot channels inside interpolation square 73 | 74 | %% Eliminate channels not plotted 75 | 76 | allx = x; 77 | ally = y; 78 | allchansind = allchansind(pltchans); 79 | intTh = Th(intchans); % eliminate channels outside the interpolation area 80 | intRd = Rd(intchans); 81 | intx = x(intchans); 82 | inty = y(intchans); 83 | Th = Th(pltchans); % eliminate channels outside the plotting area 84 | Rd = Rd(pltchans); 85 | x = x(pltchans); 86 | y = y(pltchans); 87 | 88 | intValues = Values(intchans); 89 | Values = Values(pltchans); 90 | 91 | labels= labels(pltchans,:); 92 | 93 | %% Squeeze channel locations to <= headrad 94 | squeezefac = headrad/plotrad; 95 | intRd = intRd*squeezefac; % squeeze electrode arc_lengths towards the vertex 96 | Rd = Rd*squeezefac; % squeeze electrode arc_lengths towards the vertex 97 | % to plot all inside the head cartoon 98 | intx = intx*squeezefac; 99 | inty = inty*squeezefac; 100 | x = x*squeezefac; 101 | y = y*squeezefac; 102 | allx = allx*squeezefac; 103 | ally = ally*squeezefac; 104 | 105 | %% create grid 106 | xmin = min(-headrad,min(intx)); xmax = max(headrad,max(intx)); 107 | ymin = min(-headrad,min(inty)); ymax = max(headrad,max(inty)); 108 | xi = linspace(xmin,xmax,GRID_SCALE); % x-axis description (row vector) 109 | yi = linspace(ymin,ymax,GRID_SCALE); % y-axis description (row vector) 110 | 111 | [Xi,Yi,Zi] = griddata(inty,intx,intValues,yi',xi,'v4'); % interpolate data 112 | 113 | %% Mask out data outside the head 114 | mask = (sqrt(Xi.^2 + Yi.^2) <= headrad); % mask outside the plotting circle 115 | Zi(mask == 0) = NaN; % mask non-plotting voxels with NaNs 116 | grid = plotrad; % unless 'noplot', then 3rd output arg is plotrad 117 | delta = xi(2)-xi(1); % length of grid entry 118 | 119 | %% Scale the axes and make the plot 120 | cla % clear current axis 121 | hold on 122 | h = gca; % uses current axes 123 | AXHEADFAC = 1.05; % do not leave room for external ears if head cartoon 124 | set(gca,'Xlim',[-headrad headrad]*AXHEADFAC,'Ylim',[-headrad headrad]*AXHEADFAC); 125 | unsh = (GRID_SCALE+1)/GRID_SCALE; % un-shrink the effects of 'interp' SHADING 126 | 127 | if strcmp(SHADING,'interp') 128 | handle = surface(Xi*unsh,Yi*unsh,zeros(size(Zi)),Zi,'EdgeColor','none','FaceColor',SHADING); 129 | else 130 | handle = surface(Xi-delta/2,Yi-delta/2,zeros(size(Zi)),Zi,'EdgeColor','none','FaceColor',SHADING); 131 | end 132 | contour(Xi,Yi,Zi,CONTOURNUM,'k','hittest','off'); 133 | 134 | %% Plot filled ring to mask jagged grid boundary 135 | hwidth = HEADRINGWIDTH; % width of head ring 136 | hin = squeezefac*headrad*(1- hwidth/2); % inner head ring radius 137 | 138 | if strcmp(SHADING,'interp') 139 | rwidth = BLANKINGRINGWIDTH*1.3; % width of blanking outer ring 140 | else 141 | rwidth = BLANKINGRINGWIDTH; % width of blanking outer ring 142 | end 143 | rin = headrad*(1-rwidth/2); % inner ring radius 144 | if hin>rin 145 | rin = hin; % dont blank inside the head ring 146 | end 147 | 148 | circ = linspace(0,2*pi,CIRCGRID); 149 | rx = sin(circ); 150 | ry = cos(circ); 151 | ringx = [[rx(:)' rx(1) ]*(rin+rwidth) [rx(:)' rx(1)]*rin]; 152 | ringy = [[ry(:)' ry(1) ]*(rin+rwidth) [ry(:)' ry(1)]*rin]; 153 | ringh = patch(ringx,ringy,0.01*ones(size(ringx)),get(gcf,'color'),'edgecolor','none','hittest','off'); hold on 154 | 155 | %% Plot cartoon head, ears, nose 156 | 157 | headx = [[rx(:)' rx(1) ]*(hin+hwidth) [rx(:)' rx(1)]*hin]; 158 | heady = [[ry(:)' ry(1) ]*(hin+hwidth) [ry(:)' ry(1)]*hin]; 159 | ringh = patch(headx,heady,ones(size(headx)),HEADCOLOR,'edgecolor',HEADCOLOR,'hittest','off'); hold on 160 | 161 | % Plot ears and nose 162 | base = headrad-.0046; 163 | basex = 0.18*headrad; % nose width 164 | tip = 1.15*headrad; 165 | tiphw = .04*headrad; % nose tip half width 166 | tipr = .01*headrad; % nose tip rounding 167 | q = .04; % ear lengthening 168 | EarX = [.497-.005 .510 .518 .5299 .5419 .54 .547 .532 .510 .489-.005]; % headrad = 0.5 169 | EarY = [q+.0555 q+.0775 q+.0783 q+.0746 q+.0555 -.0055 -.0932 -.1313 -.1384 -.1199]; 170 | sf = headrad/plotrad; % squeeze the model ears and nose 171 | % by this factor 172 | plot3([basex;tiphw;0;-tiphw;-basex]*sf,[base;tip-tipr;tip;tip-tipr;base]*sf,2*ones(size([basex;tiphw;0;-tiphw;-basex])),'Color',HEADCOLOR,'LineWidth',HLINEWIDTH,'hittest','off'); % plot nose 173 | plot3(EarX*sf,EarY*sf,2*ones(size(EarX)),'color',HEADCOLOR,'LineWidth',HLINEWIDTH,'hittest','off') % plot left ear 174 | plot3(-EarX*sf,EarY*sf,2*ones(size(EarY)),'color',HEADCOLOR,'LineWidth',HLINEWIDTH,'hittest','off') % plot right ear 175 | 176 | %% Mark electrode locations 177 | 178 | if strcmp(ELECTRODES,'on') % plot electrodes as spots 179 | hp2 = plot3(y,x,ones(size(x)),'.','Color',[0 0 0],'markersize',5,'linewidth',.5,'hittest','off'); 180 | elseif strcmp(ELECTRODES,'labels') % print electrode names (labels) 181 | for i = 1:size(labels,1) 182 | text(double(y(i)),double(x(i)),1,labels(i,:),'HorizontalAlignment','center','VerticalAlignment','middle','Color',[0 0 0],'hittest','off') 183 | end 184 | elseif strcmp(ELECTRODES,'numbers') 185 | for i = 1:size(labels,1) 186 | text(double(y(i)),double(x(i)),1,int2str(allchansind(i)),'HorizontalAlignment','center','VerticalAlignment','middle','Color',[0 0 0],'hittest','off') 187 | end 188 | end 189 | 190 | epos=[x; y]; 191 | axis off 192 | axis equal 193 | 194 | -------------------------------------------------------------------------------- /Cohen_GEDsimEEG.m: -------------------------------------------------------------------------------- 1 | %% 2 | % 3 | % MATLAB code accompanying the paper: 4 | % A tutorial on generalized eigendecomposition for denoising, 5 | % contrast enhancement, and dimension reduction in multichannel electrophysiology 6 | % 7 | % Mike X Cohen (mikexcohen@gmail.com) 8 | % 9 | % 10 | % No MATLAB or 3rd party toolboxes are necessary. 11 | % The files emptyEEG.mat, filterFGx.m, and topoplotindie.m need to be in 12 | % the MATLAB path or current directory. 13 | % 14 | %% 15 | 16 | % a clear MATLAB workspace is a clear mental workspace 17 | close all; clear, clc 18 | 19 | %% 20 | 21 | % load mat file containing EEG, leadfield and channel locations 22 | load emptyEEG 23 | 24 | % pick a dipole location in the brain 25 | % It's fairly arbitrary; you can try different dipoles, although not 26 | % all dipoles have strong projections to the scalp electrodes. 27 | diploc = 109; 28 | 29 | 30 | % normalize dipoles (not necessary but simplifies the code) 31 | lf.GainN = bsxfun(@times,squeeze(lf.Gain(:,1,:)),lf.GridOrient(:,1)') + bsxfun(@times,squeeze(lf.Gain(:,2,:)),lf.GridOrient(:,2)') + bsxfun(@times,squeeze(lf.Gain(:,3,:)),lf.GridOrient(:,3)'); 32 | 33 | 34 | % plot brain dipoles 35 | figure(1), clf, subplot(221) 36 | plot3(lf.GridLoc(:,1), lf.GridLoc(:,2), lf.GridLoc(:,3), 'o') 37 | hold on 38 | plot3(lf.GridLoc(diploc,1), lf.GridLoc(diploc,2), lf.GridLoc(diploc,3), 's','markerfacecolor','w','markersize',10) 39 | rotate3d on, axis square, axis off 40 | title('Brain dipole locations') 41 | 42 | 43 | % Each dipole can be projected onto the scalp using the forward model. 44 | % The code below shows this projection from one dipole. 45 | subplot(222) 46 | topoplotIndie(lf.GainN(:,diploc), EEG.chanlocs,'numcontour',0,'electrodes','numbers','shading','interp'); 47 | title('Signal dipole projection') 48 | 49 | 50 | % Now we generate random data in brain dipoles. 51 | % create 1000 time points of random data in brain dipoles 52 | % (note: the '1' before randn controls the amount of noise) 53 | dipole_data = 1*randn(length(lf.Gain),1000); 54 | 55 | % add signal to second half of dataset 56 | dipole_data(diploc,501:end) = 15*sin(2*pi*10*(0:499)/EEG.srate); 57 | 58 | % project dipole data to scalp electrodes 59 | EEG.data = lf.GainN*dipole_data; 60 | 61 | % meaningless time series 62 | EEG.times = (0:size(EEG.data,2)-1)/EEG.srate; 63 | 64 | % plot the data from one channel 65 | subplot(212), hold on 66 | plot(EEG.times,dipole_data(diploc,:)/norm(dipole_data(diploc,:)),'linew',4) 67 | plot(EEG.times,EEG.data(31,:)/norm(EEG.data(31,:)),'linew',2) 68 | plot([.5 .5],get(gca,'ylim'),'k--','HandleVisibility','off'); 69 | xlabel('Time (s)'), ylabel('Amplitude (norm.)') 70 | legend({'Dipole';'Electrode'}) 71 | 72 | %% Create covariance matrices 73 | 74 | % compute covariance matrix R is first half of data 75 | tmpd = EEG.data(:,1:500); 76 | tmpd = bsxfun(@minus,tmpd,mean(tmpd,2)); 77 | covR = tmpd*tmpd'/500; 78 | 79 | % compute covariance matrix S is second half of data 80 | tmpd = EEG.data(:,501:end); 81 | tmpd = bsxfun(@minus,tmpd,mean(tmpd,2)); 82 | covS = tmpd*tmpd'/500; 83 | 84 | 85 | %%% plot the two covariance matrices 86 | figure(2), clf 87 | 88 | % S matrix 89 | subplot(131) 90 | imagesc(covS) 91 | title('S matrix') 92 | axis square, set(gca,'clim',[-1 1]*1e6) 93 | 94 | % R matrix 95 | subplot(132) 96 | imagesc(covR) 97 | title('R matrix') 98 | axis square, set(gca,'clim',[-1 1]*1e6) 99 | 100 | % R^{-1}S 101 | % Note: GED doesn't require the explicit inverse of R; 102 | % it's here only for visualization 103 | subplot(133) 104 | imagesc(inv(covR)*covS) 105 | title('R^-^1S matrix') 106 | axis square, set(gca,'clim',[-10 10]) 107 | 108 | 109 | %% ----------------------------- %% 110 | % % 111 | % Dimension compression via PCA % 112 | % % 113 | %%% --------------------------- %%% 114 | 115 | % This code cell demonstrates that PCA is unable 116 | % recover the simulated dipole signal. 117 | 118 | % PCA 119 | [evecs,evals] = eig( (covS+covR)/2 ); 120 | 121 | % sort eigenvalues/vectors 122 | [evals,sidx] = sort(diag(evals),'descend'); 123 | evecs = evecs(:,sidx); 124 | 125 | 126 | 127 | % plot the eigenspectrum 128 | figure(3), clf 129 | subplot(231) 130 | plot(evals,'ks-','markersize',10,'markerfacecolor','r') 131 | axis square 132 | set(gca,'xlim',[0 20.5]) 133 | title('PCA eigenvalues') 134 | xlabel('Component number'), ylabel('Power ratio (\lambda)') 135 | 136 | 137 | % component time series is eigenvector as spatial filter for data 138 | comp_ts = evecs(:,1)'*EEG.data; 139 | 140 | 141 | % normalize time series (for visualization) 142 | dipl_ts = dipole_data(diploc,:) / norm(dipole_data(diploc,:)); 143 | comp_ts = comp_ts / norm(comp_ts); 144 | chan_ts = EEG.data(31,:) / norm(EEG.data(31,:)); 145 | 146 | 147 | % plot the time series 148 | subplot(212), hold on 149 | plot(EEG.times,.3+dipl_ts,'linew',2) 150 | plot(EEG.times,.15+chan_ts) 151 | plot(EEG.times,comp_ts) 152 | legend({'Truth';'EEG channel';'PCA time series'}) 153 | set(gca,'ytick',[]) 154 | xlabel('Time (a.u.)') 155 | 156 | 157 | %% spatial filter forward model 158 | 159 | % The filter forward model is what the source "sees" when it looks through the 160 | % electrodes. It is obtained by passing the covariance matrix through the filter. 161 | filt_topo = evecs(:,1); 162 | 163 | % Eigenvector sign uncertainty can cause a sign-flip, which is corrected for by 164 | % forcing the largest-magnitude projection electrode to be positive. 165 | [~,se] = max(abs( filt_topo )); 166 | filt_topo = filt_topo * sign(filt_topo(se)); 167 | 168 | 169 | % plot the maps 170 | subplot(232) 171 | topoplotIndie(lf.GainN(:,diploc), EEG.chanlocs,'numcontour',0,'electrodes','off','shading','interp'); 172 | title('Truth topomap') 173 | 174 | subplot(233) 175 | topoplotIndie(filt_topo,EEG.chanlocs,'electrodes','off','numcontour',0); 176 | title('PCA forward model') 177 | 178 | %% ----------------------------- %% 179 | % % 180 | % Source separation via GED % 181 | % % 182 | %%% --------------------------- %%% 183 | 184 | 185 | % Generalized eigendecomposition (GED) 186 | [evecs,evals] = eig(covS,covR); 187 | 188 | % sort eigenvalues/vectors 189 | [evals,sidx] = sort(diag(evals),'descend'); 190 | evecs = evecs(:,sidx); 191 | 192 | 193 | 194 | % plot the eigenspectrum 195 | figure(4), clf 196 | subplot(231) 197 | plot(evals,'ks-','markersize',10,'markerfacecolor','m') 198 | axis square 199 | set(gca,'xlim',[0 20.5]) 200 | title('GED eigenvalues') 201 | xlabel('Component number'), ylabel('Power ratio (\lambda)') 202 | 203 | % component time series is eigenvector as spatial filter for data 204 | comp_ts = evecs(:,1)'*EEG.data; 205 | 206 | %% plot for comparison 207 | 208 | % normalize time series (for visualization) 209 | dipl_ts = dipole_data(diploc,:) / norm(dipole_data(diploc,:)); 210 | comp_ts = comp_ts / norm(comp_ts); 211 | chan_ts = EEG.data(31,:) / norm(EEG.data(31,:)); 212 | 213 | 214 | % plot the time series 215 | subplot(212), hold on 216 | plot(EEG.times,.3+dipl_ts,'linew',2) 217 | plot(EEG.times,.15+chan_ts) 218 | plot(EEG.times,comp_ts) 219 | legend({'Truth';'EEG channel';'GED time series'}) 220 | set(gca,'ytick',[]) 221 | xlabel('Time (a.u.)') 222 | 223 | 224 | %% spatial filter forward model 225 | 226 | % The filter forward model is what the source "sees" when it looks through the 227 | % electrodes. It is obtained by passing the covariance matrix through the filter. 228 | filt_topo = covS*evecs(:,1); 229 | 230 | 231 | % Eigenvector sign uncertainty can cause a sign-flip, which is corrected for by 232 | % forcing the largest-magnitude projection electrode to be positive. 233 | [~,se] = max(abs( filt_topo )); 234 | filt_topo = filt_topo * sign(filt_topo(se)); 235 | 236 | 237 | % plot the maps 238 | subplot(232) 239 | topoplotIndie(lf.GainN(:,diploc), EEG.chanlocs,'numcontour',0,'electrodes','off','shading','interp'); 240 | title('Truth topomap') 241 | 242 | subplot(233) 243 | topoplotIndie(filt_topo,EEG.chanlocs,'electrodes','off','numcontour',0); 244 | title('GED forward model') 245 | 246 | 247 | %% ICA 248 | 249 | % NOTE: This cell computes ICA based on the jade algorithm. It's not 250 | % discussed or shown in the paper, but you can uncomment this section if 251 | % you are curious. Make sure the jader() function is in the MATLAB path 252 | % (you can download it from the web if you don't have it). 253 | 254 | % ivecs = jader(EEG.data,40); 255 | % ic_scores = ivecs*EEG.data; 256 | % icmaps = pinv(ivecs'); 257 | % evals = diag(icmaps*icmaps'); 258 | % 259 | % 260 | % % plot the IC energy 261 | % figure(5), clf 262 | % subplot(231) 263 | % plot(evals,'ks-','markersize',10,'markerfacecolor','m') 264 | % axis square 265 | % set(gca,'xlim',[0 20.5]) 266 | % title('ICA RMS') 267 | % xlabel('Component number'), ylabel('IC energy') 268 | % 269 | % % component time series is eigenvector as spatial filter for data 270 | % comp_ts = ic_scores(1,:);%evecs(:,1)'*EEG.data; 271 | % 272 | % % plot for comparison 273 | % 274 | % % normalize time series (for visualization) 275 | % dipl_ts = dipole_data(diploc,:) / norm(dipole_data(diploc,:)); 276 | % comp_ts = comp_ts / norm(comp_ts); 277 | % chan_ts = EEG.data(31,:) / norm(EEG.data(31,:)); 278 | % 279 | % 280 | % % plot the time series 281 | % subplot(212), hold on 282 | % plot(EEG.times,.3+dipl_ts,'linew',2) 283 | % plot(EEG.times,.15+chan_ts) 284 | % plot(EEG.times,comp_ts) 285 | % legend({'Truth';'EEG channel';'ICA time series'}) 286 | % set(gca,'ytick',[]) 287 | % xlabel('Time (a.u.)') 288 | % 289 | % 290 | % % plot the maps 291 | % subplot(232) 292 | % topoplotIndie(lf.GainN(:,diploc), EEG.chanlocs,'numcontour',0,'electrodes','off','shading','interp'); 293 | % title('Truth topomap') 294 | % 295 | % subplot(233) 296 | % topoplotIndie(icmaps(1,:),EEG.chanlocs,'electrodes','off','numcontour',0); 297 | % title('ICA forward model') 298 | 299 | 300 | %% ----------------------------- %% 301 | % % 302 | % Example GED in richer data % 303 | % % 304 | %%% --------------------------- %%% 305 | 306 | % The above simulation is overly simplistic. The goal of 307 | % this section is to simulate data that shares more 308 | % characteristics to real EEG data, including non-sinusoidal 309 | % rhythms, background noise, and multiple trials. 310 | 311 | % This code will simulate resting-state that has been segmented 312 | % into 2-second non-overlapping epochs. 313 | 314 | 315 | % signal parameters in Hz 316 | peakfreq = 10; % "alpha" 317 | fwhm = 5; % full-width at half-maximum around the alpha peak 318 | 319 | 320 | % EEG parameters for the simulation 321 | EEG.srate = 500; % sampling rate in Hz 322 | EEG.pnts = 2*EEG.srate; % each data segment is 2 seconds 323 | EEG.trials = 50; 324 | 325 | 326 | %%% create frequency-domain Gaussian 327 | hz = linspace(0,EEG.srate,EEG.pnts); 328 | s = fwhm*(2*pi-1)/(4*pi); % normalized width 329 | x = hz-peakfreq; % shifted frequencies 330 | fg = exp(-.5*(x/s).^2); % gaussian 331 | 332 | 333 | 334 | % loop over trials and generate data 335 | for triali=1:EEG.trials 336 | 337 | % random Fourier coefficients 338 | fc = rand(1,EEG.pnts) .* exp(1i*2*pi*rand(1,EEG.pnts)); 339 | 340 | % taper with the Gaussian 341 | fc = fc .* fg; 342 | 343 | % back to time domain to get the source activity 344 | source_ts = 2*real( ifft(fc) )*EEG.pnts; 345 | dipole_ts(:,triali) = source_ts; 346 | 347 | % simulate dipole data: all noise and replace target dipole with source_ts 348 | dipole_data = randn(length(lf.GainN),EEG.pnts); 349 | dipole_data(diploc,:) = .5*source_ts; 350 | % Note: the source time series has low amplitude to highlight the 351 | % sensitivity of GED. Increasing this gain to, e.g., 1 will show 352 | % accurate though noiser reconstruction in the channel data. 353 | 354 | % now project the dipole data through the forward model to the electrodes 355 | EEG.data(:,:,triali) = lf.GainN*dipole_data; 356 | end 357 | 358 | 359 | % power spectrum of the ground-truth source activity 360 | sourcepowerAve = mean(abs(fft(dipole_ts,[],1)).^2,2); 361 | 362 | %% topoplot of alpha power 363 | 364 | channelpower = abs(fft(EEG.data,[],2)).^2; 365 | channelpowerAve = squeeze(mean(channelpower,3)); 366 | 367 | % vector of frequencies 368 | hz = linspace(0,EEG.srate/2,floor(EEG.pnts/2)+1); 369 | 370 | %% Create a covariance tensor (one covmat per trial) 371 | 372 | % filter the data around 10 Hz 373 | alphafilt = filterFGx(EEG.data,EEG.srate,10,4); 374 | 375 | % initialize covariance matrices (one for each trial) 376 | [allCovS,allCovR] = deal( zeros(EEG.trials,EEG.nbchan,EEG.nbchan) ); 377 | 378 | % loop over trials (data segments) and compute each covariance matrix 379 | for triali=1:EEG.trials 380 | 381 | % cut out a segment 382 | tmpdat = alphafilt(:,:,triali); 383 | 384 | % mean-center 385 | tmpdat = tmpdat-mean(tmpdat,2); 386 | 387 | % add to S tensor 388 | allCovS(triali,:,:) = tmpdat*tmpdat' / EEG.pnts; 389 | 390 | % repeat for broadband data 391 | tmpdat = EEG.data(:,:,triali); 392 | tmpdat = tmpdat-mean(tmpdat,2); 393 | allCovR(triali,:,:) = tmpdat*tmpdat' / EEG.pnts; 394 | end 395 | 396 | %%% illustration of cleaning covariance matrices 397 | 398 | % clean R 399 | meanR = squeeze(mean(allCovR)); % average covariance 400 | dists = zeros(EEG.trials,1); % vector of distances to mean 401 | for segi=1:size(allCovR,1) 402 | r = allCovR(segi,:,:); 403 | % Euclidean distance 404 | dists(segi) = sqrt( sum((r(:)-meanR(:)).^2) ); 405 | end 406 | 407 | % finally, average trial-covariances together, excluding outliers 408 | covR = squeeze(mean( allCovR(zscore(dists)<3,:,:) ,1)); 409 | 410 | 411 | %%%%% Normally you'd repeat the above for S; ommitted here for simplicity 412 | covS = squeeze(mean( allCovS ,1)); 413 | 414 | %% now for the GED 415 | 416 | %%% NOTE: You can test PCA on these data by using only covS, or only covR, 417 | % in the eig() function. 418 | 419 | % eig and sort 420 | [evecs,evals] = eig(covS,covR); 421 | [evals,sidx] = sort(diag(evals),'descend'); 422 | evecs = evecs(:,sidx); 423 | 424 | %%% compute the component time series 425 | % for the multiplication, the data need to be reshaped into 2D 426 | data2D = reshape(EEG.data,EEG.nbchan,[]); 427 | compts = evecs(:,1)' * data2D; 428 | % and then reshaped back into trials 429 | compts = reshape(compts,EEG.pnts,EEG.trials); 430 | 431 | %%% power spectrum 432 | comppower = abs(fft(compts,[],1)).^2; 433 | comppowerAve = squeeze(mean(comppower,2)); 434 | 435 | %%% component map 436 | compmap = evecs(:,1)' * covS; 437 | % flip map sign 438 | [~,se] = max(abs( compmap )); 439 | compmap = compmap * sign(compmap(se)); 440 | 441 | %% visualization 442 | 443 | 444 | figure(5), clf 445 | 446 | subplot(241) 447 | topoplotIndie(lf.GainN(:,diploc), EEG.chanlocs,'numcontour',0,'electrodes','off','shading','interp'); 448 | title('Truth topomap') 449 | set(gca,'clim',[-1 1]*30) 450 | 451 | subplot(242) 452 | plot(evals,'ks-','markersize',10,'markerfacecolor','r') 453 | axis square, box off 454 | set(gca,'xlim',[0 20.5]) 455 | title('GED scree plot') 456 | xlabel('Component number'), ylabel('Power ratio (\lambda)') 457 | % Note that the max eigenvalue is <1, 458 | % because R has more overall energy than S. 459 | 460 | subplot(243) 461 | topoplotIndie(compmap,EEG.chanlocs,'numcontour',0); 462 | title('Alpha component') 463 | set(gca,'clim',[-1 1]*10) 464 | 465 | subplot(244) 466 | topoplotIndie(channelpowerAve(:,dsearchn(hz',10)),EEG.chanlocs,'numcontour',0);%,'electrodes','numbers'); 467 | title('Elecr. power (10 Hz)') 468 | set(gca,'clim',[1e8 1.5e9]) 469 | 470 | 471 | 472 | 473 | subplot(212), cla, hold on 474 | plot(hz,sourcepowerAve(1:length(hz))/max(sourcepowerAve(1:length(hz))),'m','linew',3) 475 | plot(hz,comppowerAve(1:length(hz))/max(comppowerAve(1:length(hz))),'r','linew',3) 476 | plot(hz,channelpowerAve(31,1:length(hz))/max(channelpowerAve(31,1:length(hz))),'k','linew',3) 477 | legend({'Source','Component','Electrode 31'},'box','off') 478 | xlabel('Frequency (Hz)') 479 | ylabel('Power (norm to max power)') 480 | set(gca,'xlim',[0 80]) 481 | 482 | % font size for all axes 483 | set(get(gcf,'children'),'fontsize',13) 484 | 485 | %% done. 486 | -------------------------------------------------------------------------------- /Cohen_GEDsimEEG.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## PYTHON code accompanying the paper:\n", 8 | "### A tutorial on generalized eigendecomposition for denoising, contrast enhancement, and dimension reduction in multichannel electrophysiology\n", 9 | "\n", 10 | "Mike X Cohen (mikexcohen@gmail.com)\n", 11 | " \n", 12 | "The files emptyEEG.mat, filterFGx.m, and topoplotindie.m need to be in \n", 13 | "the current directory. " 14 | ] 15 | }, 16 | { 17 | "cell_type": "code", 18 | "execution_count": null, 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "import numpy as np\n", 23 | "import matplotlib.pyplot as plt\n", 24 | "from mpl_toolkits.mplot3d import Axes3D\n", 25 | "import copy\n", 26 | "import scipy\n", 27 | "import scipy.io as sio\n", 28 | "from pytopo import topoplotIndie\n", 29 | "from filterFGxfun import filterFGx\n", 30 | "# import jade" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": null, 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "## preliminary \n", 40 | "\n", 41 | "# mat file containing EEG, leadfield and channel locations\n", 42 | "matfile = sio.loadmat('emptyEEG')\n", 43 | "lf = matfile['lf'][0,0]\n", 44 | "EEG = matfile['EEG'][0,0]\n", 45 | "\n", 46 | "diploc = 108\n", 47 | "\n", 48 | "# normal dipoles (normal to the surface of the cortex)\n", 49 | "lf_GainN = np.zeros((64,2004))\n", 50 | "for i in range(3):\n", 51 | " lf_GainN += lf['Gain'][:,i,:]*lf['GridOrient'][:,i]\n", 52 | "\n", 53 | "\n", 54 | "### simulate the data\n", 55 | "dipole_data = 1*np.random.randn(lf['Gain'].shape[2],1000)\n", 56 | "# add signal to second half of dataset\n", 57 | "dipole_data[diploc,500:] = 15*np.sin(2*np.pi*10*np.arange(500)/EEG['srate'])\n", 58 | "# project dipole data to scalp electrodes\n", 59 | "EEG['data'] = lf_GainN@dipole_data\n", 60 | "# meaningless time series\n", 61 | "EEG['times'] = np.squeeze(np.arange(EEG['data'].shape[1])/EEG['srate'])\n", 62 | "\n", 63 | "\n", 64 | "\n", 65 | "\n", 66 | "\n", 67 | "\n", 68 | "\n", 69 | "# plot brain dipoles\n", 70 | "ax = Axes3D(plt.figure())\n", 71 | "ax.scatter(lf['GridLoc'][:,0], lf['GridLoc'][:,1], lf['GridLoc'][:,2], 'bo')\n", 72 | "ax.scatter(lf['GridLoc'][diploc,0], lf['GridLoc'][diploc,1], lf['GridLoc'][diploc,2], marker='o',s=100)\n", 73 | "plt.title('Brain dipole locations')\n", 74 | "plt.show()\n", 75 | "\n", 76 | "_,axs = plt.subplots(2,1)\n", 77 | "topoplotIndie(lf_GainN[:,diploc],EEG['chanlocs'],'Signal dipole proj.',axs[0])\n", 78 | "\n", 79 | "axs[1].plot(EEG['times'],dipole_data[diploc,:]/np.linalg.norm(dipole_data[diploc,:]),linewidth=4,label='Dipole')\n", 80 | "axs[1].plot(EEG['times'],EEG['data'][30,:]/np.linalg.norm(EEG['data'][30,:]),linewidth=2,label='Electrode')\n", 81 | "axs[1].legend()\n", 82 | "plt.show()" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": null, 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "## Create covariance matrices\n", 92 | "\n", 93 | "# compute covariance matrix R is first half of data\n", 94 | "tmpd = EEG['data'][:,:500]\n", 95 | "covR = np.cov(tmpd)\n", 96 | "\n", 97 | "# compute covariance matrix S is second half of data\n", 98 | "tmpd = EEG['data'][:,500:]\n", 99 | "covS = np.cov(tmpd)\n", 100 | "\n", 101 | "\n", 102 | "### plot the two covariance matrices\n", 103 | "_,axs = plt.subplots(1,3,figsize=(8,4))\n", 104 | "\n", 105 | "# S matrix\n", 106 | "axs[0].imshow(covS,vmin=-1e6,vmax=1e6,cmap='jet')\n", 107 | "axs[0].set_title('S matrix')\n", 108 | "\n", 109 | "# R matrix\n", 110 | "axs[1].imshow(covR,vmin=-1e6,vmax=1e6,cmap='jet')\n", 111 | "axs[1].set_title('R matrix')\n", 112 | "\n", 113 | "# R^{-1}S\n", 114 | "axs[2].imshow(np.linalg.inv(covR)@covS,vmin=-10,vmax=10,cmap='jet')\n", 115 | "axs[2].set_title('$R^{-1}S$ matrix')\n", 116 | "\n", 117 | "plt.tight_layout()\n", 118 | "plt.show()" 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": null, 124 | "metadata": {}, 125 | "outputs": [], 126 | "source": [ 127 | "## Dimension compression via PCA\n", 128 | "\n", 129 | "# PCA\n", 130 | "evals,evecs = scipy.linalg.eigh(covS+covR)\n", 131 | "\n", 132 | "# sort eigenvalues/vectors\n", 133 | "sidx = np.argsort(evals)[::-1]\n", 134 | "evals = evals[sidx]\n", 135 | "evecs = evecs[:,sidx]\n", 136 | "\n", 137 | "\n", 138 | "\n", 139 | "# plot the eigenspectrum\n", 140 | "_,axs = plt.subplots(1,3,figsize=(8,3))\n", 141 | "axs[0].plot(evals/np.max(evals),'s-',markersize=15,markerfacecolor='k')\n", 142 | "axs[0].set_xlim([-.5,20.5])\n", 143 | "axs[0].set_title('PCA eigenvalues')\n", 144 | "axs[0].set_xlabel('Component number')\n", 145 | "axs[0].set_ylabel('Power ratio (norm-$\\lambda$)')\n", 146 | "\n", 147 | "# filter forward model\n", 148 | "filt_topo = evecs[:,0]\n", 149 | "\n", 150 | "# Eigenvector sign\n", 151 | "se = np.argmax(np.abs( filt_topo ))\n", 152 | "filt_topo = filt_topo * np.sign(filt_topo[se])\n", 153 | "\n", 154 | "# plot the maps\n", 155 | "topoplotIndie(lf_GainN[:,diploc],EEG['chanlocs'],'Truth topomap',axs[1])\n", 156 | "topoplotIndie(filt_topo,EEG['chanlocs'],'PCA forward model',axs[2])\n", 157 | "\n", 158 | "plt.show()\n", 159 | "\n", 160 | "\n", 161 | "\n", 162 | "# component time series is eigenvector as spatial filter for data\n", 163 | "comp_ts = evecs[:,0].T@EEG['data']\n", 164 | "\n", 165 | "\n", 166 | "# normalize time series (for visualization)\n", 167 | "dipl_ts = dipole_data[diploc,:] / np.linalg.norm(dipole_data[diploc,:])\n", 168 | "comp_ts = comp_ts / np.linalg.norm(comp_ts)\n", 169 | "chan_ts = EEG['data'][30,:] / np.linalg.norm(EEG['data'][30,:])\n", 170 | "\n", 171 | "\n", 172 | "# plot the time series\n", 173 | "plt.figure(figsize=(9,4))\n", 174 | "plt.plot(EEG['times'],.3+dipl_ts,linewidth=2)\n", 175 | "plt.plot(EEG['times'],.15+chan_ts)\n", 176 | "plt.plot(EEG['times'],comp_ts)\n", 177 | "plt.legend(['Truth','EEG channel','PCA time series'])\n", 178 | "plt.yticks([])\n", 179 | "plt.xlabel('Time (a.u.)')\n", 180 | "plt.show()" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": null, 186 | "metadata": {}, 187 | "outputs": [], 188 | "source": [ 189 | "## Source separation via GED\n", 190 | "\n", 191 | "# GED\n", 192 | "evals,evecs = scipy.linalg.eigh(covS,covR)\n", 193 | "\n", 194 | "# sort eigenvalues/vectors\n", 195 | "sidx = np.argsort(evals)[::-1]\n", 196 | "evals = evals[sidx]\n", 197 | "evecs = evecs[:,sidx]\n", 198 | "\n", 199 | "\n", 200 | "\n", 201 | "# plot the eigenspectrum\n", 202 | "_,axs = plt.subplots(1,3,figsize=(8,3))\n", 203 | "axs[0].plot(evals/np.max(evals),'s-',markersize=15,markerfacecolor='k')\n", 204 | "axs[0].set_xlim([-.5,20.5])\n", 205 | "axs[0].set_title('GED eigenvalues')\n", 206 | "axs[0].set_xlabel('Component number')\n", 207 | "axs[0].set_ylabel('Power ratio (norm-$\\lambda$)')\n", 208 | "\n", 209 | "# filter forward model\n", 210 | "filt_topo = evecs[:,0].T@covS\n", 211 | "\n", 212 | "# Eigenvector sign\n", 213 | "se = np.argmax(np.abs( filt_topo ))\n", 214 | "filt_topo = filt_topo * np.sign(filt_topo[se])\n", 215 | "\n", 216 | "# plot the maps\n", 217 | "topoplotIndie(lf_GainN[:,diploc],EEG['chanlocs'],'Truth topomap',axs[1])\n", 218 | "topoplotIndie(filt_topo,EEG['chanlocs'],'GED forward model',axs[2])\n", 219 | "\n", 220 | "plt.show()\n", 221 | "\n", 222 | "\n", 223 | "\n", 224 | "# component time series is eigenvector as spatial filter for data\n", 225 | "comp_ts = evecs[:,0].T@EEG['data']\n", 226 | "\n", 227 | "\n", 228 | "# normalize time series (for visualization)\n", 229 | "comp_ts = comp_ts / np.linalg.norm(comp_ts)\n", 230 | "\n", 231 | "\n", 232 | "# plot the time series\n", 233 | "plt.figure(figsize=(9,4))\n", 234 | "plt.plot(EEG['times'],.3+dipl_ts,linewidth=2)\n", 235 | "plt.plot(EEG['times'],.15+chan_ts)\n", 236 | "plt.plot(EEG['times'],comp_ts)\n", 237 | "plt.legend(['Truth','EEG channel','GED time series'])\n", 238 | "plt.yticks([])\n", 239 | "plt.xlabel('Time (a.u.)')\n", 240 | "plt.show()" 241 | ] 242 | }, 243 | { 244 | "cell_type": "code", 245 | "execution_count": null, 246 | "metadata": {}, 247 | "outputs": [], 248 | "source": [] 249 | }, 250 | { 251 | "cell_type": "markdown", 252 | "metadata": {}, 253 | "source": [ 254 | "# Example GED in richer data" 255 | ] 256 | }, 257 | { 258 | "cell_type": "code", 259 | "execution_count": null, 260 | "metadata": {}, 261 | "outputs": [], 262 | "source": [ 263 | "# The above simulation is overly simplistic. The goal of\n", 264 | "# this section is to simulate data that shares more \n", 265 | "# characteristics to real EEG data, including non-sinusoidal\n", 266 | "# rhythms, background noise, and multiple trials.\n", 267 | "\n", 268 | "# This code will simulate resting-state that has been segmented\n", 269 | "# into 2-second non-overlapping epochs." 270 | ] 271 | }, 272 | { 273 | "cell_type": "code", 274 | "execution_count": null, 275 | "metadata": {}, 276 | "outputs": [], 277 | "source": [ 278 | "### simulate the data\n", 279 | "\n", 280 | "# signal parameters in Hz\n", 281 | "peakfreq = 10 # \"alpha\"\n", 282 | "fwhm = 5 # full-width at half-maximum around the alpha peak\n", 283 | "\n", 284 | "\n", 285 | "# EEG parameters for the simulation\n", 286 | "EEG['srate'] = 500 # sampling rate in Hz\n", 287 | "EEG['pnts'] = 2*EEG['srate'] # each data segment is 2 seconds\n", 288 | "EEG['trials'] = 50\n", 289 | "EEG['data'] = np.zeros((EEG['nbchan'][0][0],EEG['pnts'],EEG['trials']))\n", 290 | "\n", 291 | "\n", 292 | "### create frequency-domain Gaussian\n", 293 | "hz = np.linspace(0,EEG['srate'],EEG['pnts'])\n", 294 | "s = fwhm*(2*np.pi-1)/(4*np.pi) # normalized width\n", 295 | "x = hz-peakfreq # shifted frequencies\n", 296 | "fg = np.exp(-.5*(x/s)**2) # gaussian\n", 297 | "\n", 298 | "\n", 299 | "\n", 300 | "# loop over trials and generate data\n", 301 | "for triali in range(EEG['trials']):\n", 302 | " \n", 303 | " # random Fourier coefficients\n", 304 | " fc = np.random.rand(EEG['pnts']) * np.exp(1j*2*np.pi*np.random.rand(1,EEG['pnts']))\n", 305 | " \n", 306 | " # taper with the Gaussian\n", 307 | " fc = fc * fg\n", 308 | " \n", 309 | " # back to time domain to get the source activity\n", 310 | " source_ts = 2*np.real( np.fft.ifft(fc) )*EEG['pnts']\n", 311 | " \n", 312 | " # simulate dipole data: all noise and replace target dipole with source_ts\n", 313 | " dipole_data = np.random.randn(np.shape(lf_GainN)[1],EEG['pnts'])\n", 314 | " dipole_data[diploc,:] = .5*source_ts\n", 315 | " # Note: the source time series has low amplitude to highlight the\n", 316 | " # sensitivity of GED. Increasing this gain to, e.g., 1 will show\n", 317 | " # accurate though noiser reconstruction in the channel data.\n", 318 | " \n", 319 | " # now project the dipole data through the forward model to the electrodes\n", 320 | " EEG['data'][:,:,triali] = lf_GainN@dipole_data\n", 321 | " \n" 322 | ] 323 | }, 324 | { 325 | "cell_type": "code", 326 | "execution_count": null, 327 | "metadata": {}, 328 | "outputs": [], 329 | "source": [ 330 | "## topoplot of alpha power\n", 331 | "\n", 332 | "channelpower = np.abs(np.fft.fft(EEG['data'],axis=1))**2\n", 333 | "channelpowerAve = np.mean(channelpower,axis=2)\n", 334 | "\n", 335 | "# vector of frequencies\n", 336 | "hz = np.linspace(0,EEG['srate']/2,EEG['pnts']//2+1)\n" 337 | ] 338 | }, 339 | { 340 | "cell_type": "code", 341 | "execution_count": null, 342 | "metadata": {}, 343 | "outputs": [], 344 | "source": [ 345 | "## Create a covariance tensor (one covmat per trial)\n", 346 | "\n", 347 | "# filter the data around 10 Hz\n", 348 | "alphafilt = copy.deepcopy(EEG['data'])\n", 349 | "for ti in range(int(EEG['trials'])):\n", 350 | " tmdat = EEG['data'][:,:,ti]\n", 351 | " alphafilt[:,:,ti] = filterFGx(tmdat,EEG['srate'],10,4)[0]\n", 352 | "\n", 353 | "# initialize covariance matrices (one for each trial)\n", 354 | "allCovS = np.zeros((EEG['trials'],EEG['nbchan'][0][0],EEG['nbchan'][0][0]))\n", 355 | "allCovR = np.zeros((EEG['trials'],EEG['nbchan'][0][0],EEG['nbchan'][0][0]))\n", 356 | "\n", 357 | "\n", 358 | "# loop over trials (data segments) and compute each covariance matrix\n", 359 | "for triali in range(EEG['trials']):\n", 360 | " \n", 361 | " # cut out a segment\n", 362 | " tmpdat = alphafilt[:,:,triali]\n", 363 | " \n", 364 | " # mean-center\n", 365 | " tmpdat = tmpdat-np.mean(tmpdat,axis=1,keepdims=True)\n", 366 | " \n", 367 | " # add to S tensor\n", 368 | " allCovS[triali,:,:] = tmpdat@tmpdat.T / EEG['pnts']\n", 369 | " \n", 370 | " # repeat for broadband data\n", 371 | " tmpdat = EEG['data'][:,:,triali]\n", 372 | " tmpdat = tmpdat-np.mean(tmpdat,axis=1,keepdims=True)\n", 373 | " allCovR[triali,:,:] = tmpdat@tmpdat.T / EEG['pnts']" 374 | ] 375 | }, 376 | { 377 | "cell_type": "code", 378 | "execution_count": null, 379 | "metadata": {}, 380 | "outputs": [], 381 | "source": [ 382 | "## illustration of cleaning covariance matrices\n", 383 | "\n", 384 | "# clean R\n", 385 | "meanR = np.mean(allCovR,axis=0) # average covariance\n", 386 | "dists = np.zeros(EEG['trials']) # vector of distances to mean\n", 387 | "for segi in range(EEG['trials']):\n", 388 | " r = allCovR[segi,:,:]\n", 389 | " # Euclidean distance\n", 390 | " dists[segi] = np.sqrt( np.sum((r.reshape(1,-1)-meanR.reshape(1,-1))**2) )\n", 391 | "\n", 392 | "# compute zscored distances\n", 393 | "distsZ = (dists-np.mean(dists)) / np.std(dists)\n", 394 | "\n", 395 | "# finally, average trial-covariances together, excluding outliers\n", 396 | "covR = np.mean( allCovR[distsZ<3,:,:] ,axis=0)\n", 397 | "\n", 398 | "\n", 399 | "## Normally you'd repeat the above for S; ommitted here for simplicity\n", 400 | "covS = np.mean( allCovS ,axis=0)\n" 401 | ] 402 | }, 403 | { 404 | "cell_type": "code", 405 | "execution_count": null, 406 | "metadata": {}, 407 | "outputs": [], 408 | "source": [ 409 | "## now for the GED\n", 410 | "\n", 411 | "### NOTE: You can test PCA on these data by using only covS, or only covR,\n", 412 | "# in the eig() function.\n", 413 | "\n", 414 | "# eig and sort\n", 415 | "evals,evecs = scipy.linalg.eigh(covS,covR)\n", 416 | "sidx = np.argsort(evals)[::-1]\n", 417 | "evals = evals[sidx]\n", 418 | "evecs = evecs[:,sidx]\n", 419 | "\n", 420 | "\n", 421 | "### compute the component time series\n", 422 | "# for the multiplication, the data need to be reshaped into 2D\n", 423 | "data2D = np.reshape(EEG['data'],(EEG['nbchan'][0][0],-1),order='F')\n", 424 | "compts = evecs[:,0].T @ data2D\n", 425 | "# and then reshaped back into trials\n", 426 | "compts = np.reshape(compts,(EEG['pnts'],EEG['trials']),order='F')\n", 427 | "\n", 428 | "### power spectrum\n", 429 | "comppower = np.abs(np.fft.fft(compts,axis=0))**2\n", 430 | "comppowerAve = np.mean(comppower,axis=1)\n", 431 | "\n", 432 | "### component map\n", 433 | "compmap = evecs[:,0].T @ covS\n", 434 | "# flip map sign\n", 435 | "se = np.argmax(np.abs( compmap ))\n", 436 | "compmap = compmap * np.sign(compmap[se])\n" 437 | ] 438 | }, 439 | { 440 | "cell_type": "code", 441 | "execution_count": null, 442 | "metadata": {}, 443 | "outputs": [], 444 | "source": [ 445 | "## visualization\n", 446 | "\n", 447 | "_,axs = plt.subplots(1,4,figsize=(10,3))\n", 448 | "\n", 449 | "topoplotIndie(lf_GainN[:,diploc], EEG['chanlocs'],'Truth topomap',axs[0])\n", 450 | "\n", 451 | "axs[1].plot(evals,'ks-',markersize=10,markerfacecolor='r')\n", 452 | "axs[1].set_xlim([0,20.5])\n", 453 | "axs[1].set_title('GED scree plot')\n", 454 | "axs[1].set_xlabel('Component number')\n", 455 | "axs[1].set_ylabel('Power ratio ($\\lambda$)')\n", 456 | "# Note that the max eigenvalue is <1, \n", 457 | "# because R has more overall energy than S.\n", 458 | "\n", 459 | "# GED component\n", 460 | "topoplotIndie(compmap, EEG['chanlocs'],'Alpha component',axs[2])\n", 461 | "\n", 462 | "# channel 10 Hz power\n", 463 | "where10 = np.argmin(np.abs(hz-10))\n", 464 | "topoplotIndie(channelpowerAve[:,where10], EEG['chanlocs'],'Elecr. power (10 Hz)',axs[3])\n", 465 | "plt.tight_layout()\n", 466 | "plt.show()\n", 467 | "\n", 468 | "\n", 469 | "# spectra\n", 470 | "plt.figure(figsize=(10,4))\n", 471 | "plt.plot(hz,comppowerAve[:len(hz)]/np.max(comppowerAve[:len(hz)]),'r',linewidth=3)\n", 472 | "plt.plot(hz,channelpowerAve[30,:len(hz)]/np.max(channelpowerAve[30,:len(hz)]),'k',linewidth=3)\n", 473 | "plt.legend(['Component','Electrode 31'])\n", 474 | "plt.xlabel('Frequency (Hz)')\n", 475 | "plt.ylabel('Power (norm to max power)')\n", 476 | "plt.xlim([0,80])\n", 477 | "plt.show()" 478 | ] 479 | }, 480 | { 481 | "cell_type": "code", 482 | "execution_count": null, 483 | "metadata": {}, 484 | "outputs": [], 485 | "source": [] 486 | } 487 | ], 488 | "metadata": { 489 | "kernelspec": { 490 | "display_name": "Python 3", 491 | "language": "python", 492 | "name": "python3" 493 | }, 494 | "language_info": { 495 | "codemirror_mode": { 496 | "name": "ipython", 497 | "version": 3 498 | }, 499 | "file_extension": ".py", 500 | "mimetype": "text/x-python", 501 | "name": "python", 502 | "nbconvert_exporter": "python", 503 | "pygments_lexer": "ipython3", 504 | "version": "3.7.4" 505 | } 506 | }, 507 | "nbformat": 4, 508 | "nbformat_minor": 2 509 | } 510 | --------------------------------------------------------------------------------