The dominance of large-scale phase dynamics in human cortex, from delta to gamma.
The 7 matches
- [1] § Methods › SVD for empirical fourier decomposition ↔ estimation_of_SF_of_phase_of_cortical_activity_on_irregular_measurement_arrays.ipynb, lines 264–281 · score 0.61 · inferior superior, anterior posterior, sensor, MEG, SF, phase
- [2] § Methods › Multi-scale differencing of phase ↔ estimation_of_SF_of_phase_of_cortical_activity_on_irregular_measurement_arrays.ipynb, lines 689–790 · score 0.60 · smallest triangle, geodesic distances, edge, bin, vectors, weight
- [3] § Results › Overview ↔ estimation_of_SF_of_phase_of_cortical_activity_on_irregular_measurement_arrays.ipynb, lines 594–624 · score 0.60 · inferior superior, anterior posterior, Wave maps, angle, vector, phase
- [4] § Methods › Multi-scale differencing of phase ↔ estimation_of_SF_of_phase_of_cortical_activity_on_irregular_measurement_arrays.ipynb, lines 308–340 · score 0.59 · approximately equilateral, sEEG, collated, hemisphere, triplet, angle
- [5] § Methods › Numerical methods › Phase estimation ↔ estimation_of_SF_of_phase_of_cortical_activity_on_irregular_measurement_arrays.ipynb, lines 159–234 · score 0.58 · logarithmically spaced, Morlet wavelets, oversampling, cycle, phase
- [6] § Methods › Surrogate testing ↔ estimation_of_SF_of_phase_of_cortical_activity_on_irregular_measurement_arrays.ipynb, lines 689–790 · score 0.54 · singular vector, sEEG, wavelength, empirical, weightings, cortical
- [7] § Methods › Multi-scale differencing of phase ↔ estimation_of_SF_of_phase_of_cortical_activity_on_irregular_measurement_arrays.ipynb, lines 13–20 · score 0.50 · inter contact distances, sEEG, irregularity, MEG, phase, SF
Paper
Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC
The paper is loaded when this pane is shown.
The authors' code
Jupyter notebook · 793 lines · 46 KB · GPL-3.0 · 7 matches
- # %%
- import numpy as np
- #import cupy as cp #uncomment this if cuda gpu available and cupy installed
- from matplotlib.pyplot import *
- import matplotlib.ticker as ticker
- from scipy.optimize import curve_fit
- from pickle import dump,load,HIGHEST_PROTOCOL
- from os.path import exists
- import warnings
- from glob import glob
- from IPython.display import clear_output
- # %%
- ###select one###
- ###sEEG inter-contact distances have been computed using geodesic distances on surface models of cortex###
- measurement = 'sEEG' #example subjects
- ###MEG inter-contact distances have been computed from a spheroid approximation of the sensory array###
- #measurement = 'MEG' #example subjects
- # %%
- #the data-files used in these examples can be downloaded from https://osf.io/48ztm/files/osfstorage#
- if measurement=='sEEG':
- file_path = 'E:\\sensor_coordinates_distance_data_files\\' #path to where you put the input files listed below
- output_path = 'E:\\output\\' #path where you want the output files to go (keep short, output files have long names!)
- subjects = ['subject15','subject22'] #example subjects
- geodesic_distance_files = ['subject15_depth_geodesic_distances','subject22_depth_geodesic_distances'] #file-name without '.pkl' affix
- triangle_files = ['subject15_depth_equilateral_triplets','subject22_depth_equilateral_triplets'] #file-name without '.pkl' affix
- coordinate_files = ['subject15_depth_coordinatesAP-IS-LR','subject22_depth_coordinatesAP-IS-LR'] #file-name without '.csv' affix
- TS_files = ['subject15_depth_timeseries','subject22_depth_timeseries'] #file-name without '.npy' affix
- tdel_values = [1.0,1.0] # one over the sampling frequency, in ms.
- frequencies = [1.0, 1.148698354997035, 1.3195079107728942, 1.5157165665103982, 1.7411011265922482,
- 2.0, 2.29739670999407, 2.639015821545789, 3.0314331330207964, 3.4822022531844965,
- 4.0, 4.59479341998814, 5.278031643091579, 6.062866266041593, 6.964404506368995,
- 8.0, 9.18958683997628, 10.556063286183157, 12.125732532083186, 13.92880901273799,
- 16.0, 18.37917367995256, 21.112126572366314, 24.25146506416638, 27.857618025475986,
- 32.0, 36.75834735990512, 42.22425314473263, 48.50293012833276, 55.71523605095197,
- 64.0, 73.51669471981025, 84.44850628946526, 97.00586025666551] #temporal frequencies to analyze
- min_of_range = 1.0/50.0 #m, for plotting
- elif measurement=='MEG':
- file_path = 'E:\\sensor_coordinates_distance_data_files\\' #path to where you put the input files listed below
- output_path = 'E:\\output\\' #path where you want the output files to go (keep short, output files have long names!)
- subjects = ['subjectG0','subjectP9'] #example subjects
- geodesic_distance_files = ['subjectG0_MEG_spheroid_distances','subjectP9_MEG_spheroid_distances'] #file-name without '.pkl' affix
- triangle_files = ['subjectG0_MEG_equilateral_triplets','subjectP9_MEG_equilateral_triplets'] #file-name without '.pkl' affix
- coordinate_files = ['subjectG0_MEG_coordinatesAP-IS-LR','subjectP9_MEG_coordinatesAP-IS-LR'] #file-name without '.csv' affix
- TS_files = ['subjectG0_MEG_timeseries','subjectP9_MEG_timeseries'] #file-name without '.npy' affix
- tdel_values = [3.2,3.2] # one over the sampling frequency, in ms.
- frequencies = [1.0, 1.148698354997035, 1.3195079107728942, 1.5157165665103982, 1.7411011265922482,
- 2.0, 2.29739670999407, 2.639015821545789, 3.0314331330207964, 3.4822022531844965,
- 4.0, 4.59479341998814, 5.278031643091579, 6.062866266041593, 6.964404506368995,
- 8.0, 9.18958683997628, 10.556063286183157, 12.125732532083186, 13.92880901273799,
- 16.0, 18.37917367995256, 21.112126572366314, 24.25146506416638, 27.857618025475986,
- 32.0] #temporal frequencies to analyze
- min_of_range = 1.0/20.0 #m, for plotting
- print(subjects)
- # %%
- #more parameters
- aggregate_subjects = {} #for storing the spectra and other used parametres for later analysis
- verbose = True #used as a global variable, set to False once everything is working ok
- distance_units = 1000.0 #conversion of given sensor units of coordinates to metres; here mm->m
- minf = min(frequencies) #minimum frequency, used to window the Morlet wavelet inputs
- nFrequencies = len(frequencies) #number of frequencies
- include_normed_power = False #if False will compute the SF spectrum of the phase only
- forward_waves = True #separate standing wave components into pure traveling wave components
- nBases = 14 #number of bases to used in SVD
- N_cycles = 2 #number of cycles is the Morlet wavelet to estimate time-series of phase
- nonskinny_threshold = np.pi/4.0 #45 degrees; used for making triangular regions between contacts; should be as close as 60 degrees as possible without reducing the number of triangles too severely
- smallest_triangle_size = 0.01 #units of linear m; used for binning the triangles
- largest_triangle_size = 0.32 #units of linear m; used for binning the triangles
- N_histogram_bins = 32 #for plotting
- if verbose==False: warnings.filterwarnings("ignore")
- # %%
- #main functions
- def complex_spatial_SVD(phi_cts,nBases=3,bases_only=False,use_bases=None,unit_phase_out=True): #singular value decomposition of the complex-valued phase data
- print('Doing spatial SVD')
- phi_Cs = phi_cts.reshape(-1,phi_cts.shape[-1])
- if use_bases is None: #generate new bases from phi
- u_sB,s_B,vh_BC= np.linalg.svd(phi_Cs.T,full_matrices=False) #b<B
- s_b = s_B[:nBases] #diagonals of sigma
- print('Percentage root variance',100.0*s_b/s_B.sum())
- bases_sb = u_sB[:,:nBases] #left singular vectors
- print('Percentage variance',100.0*(s_b)**2/((s_B)**2).sum())
- bases_sb = u_sB[:,:nBases] #left singular vectors
- else: bases_sb = use_bases #otherwise, use_bases is a previously generated basis set
- if bases_only==False: #calculate fits, right singular vectors, and reduced rank model
- if use_bases is None: betas_bC = (np.diag(s_b)@vh_BC[:nBases,:]) #weighted right singular vectors
- else: betas_bC = (phi_Cs@bases_sb.conj()).T
- betas_ctb = betas_bC.T.reshape(phi_cts.shape[0],-1,bases_sb.shape[1])
- if unit_phase_out:
- model_Cs = np.exp(1j*np.angle(bases_sb@betas_bC).T) #reduced rank model with unit length complex phases
- model_cts = model_Cs.reshape(phi_cts.shape)
- fit_C = (phi_Cs/model_Cs).mean(-1).real #for good fits, phi/model will point along the real axis
- fit_ct = fit_C.reshape(phi_cts.shape[0],phi_cts.shape[1])
- if use_bases is None: return bases_sb,s_b,fit_ct,betas_ctb,model_cts #number of outputs change depending on input flags
- else: return bases_sb,fit_ct,betas_ctb,model_cts #number of outputs change depending on input flags
- else:
- model_Cs = (bases_sb@betas_bC).T #reduced rank model
- model_cts = model_Cs.reshape(phi_cts.shape)
- if use_bases is None: return bases_sb,s_b,betas_ctb,model_cts #number of outputs change depending on input flags
- else: return bases_sb,betas_ctb,model_cts #number of outputs change depending on input flags
- else: return bases_sb #number of outputs change depending on input flags
- def make_SF_power(dmetres_SS,binned_triplets_av,normalization_a,sigmas_b,Bases_sb):
- #estimate wavelength and power for empirical data
- bases_avb = Bases_sb[binned_triplets_av,:]
- if verbose: print('bases_avb shape',bases_avb.shape)
- r_avb = np.absolute(bases_avb) #magnitude of contribution by each vertex (over triangles, bases)
- if verbose: print('r_avb min',r_avb.min(),'r_avb max',r_avb.max())
- estpower_ba = r_avb.mean(1).T * normalization_a[np.newaxis,:] * sigmas_b[:,np.newaxis] #weight variance explained by average vertex contribution for each triangle and normalize for number of triangles at this size
- if verbose: print('estpower_ba shape',estpower_ba.shape)
- dphidx_ba,dphi_ba = dphi_dx_from_triangles(bases_avb.transpose(2,0,1),dmetres_SS,binned_triplets_av)
- if verbose: print('dphidx_ba shape',dphidx_ba.shape)
- distance_per_radian_ba = 1.0/dphidx_ba
- metres_per_cycle_ba = 2.0*np.pi*distance_per_radian_ba #dmetres_SS already in metres
- if verbose: print('metres_per_cycle_ba shape',metres_per_cycle_ba.shape)
- return estpower_ba,metres_per_cycle_ba,dphidx_ba,dphi_ba
- def dphi_dx_from_triangles(phi_cav,dmetres_SS,triplets_av): #compute the rate of change of phase across triangles
- a = dmetres_SS[triplets_av[:,0],triplets_av[:,1]]
- b = dmetres_SS[triplets_av[:,1],triplets_av[:,2]]
- c = dmetres_SS[triplets_av[:,2],triplets_av[:,0]]
- ABC_v_xyz = coordinates_of_triangle_given_lengths(a,b,c) #nvd
- ABC_vd = ABC_v_xyz[:,:,:2] #don't need zero Z coordinate
- _ABC_vd = ABC_vd / np.linalg.norm(ABC_vd,axis=-2)[:,np.newaxis,:] #normalized area to one
- relative_phase = np.angle(phi_cav[:,:,0:1]/phi_cav[:,:,0:3]) #relative to first index; units of radians
- dphi_dx = []
- dphi = []
- for c in range(phi_cav.shape[0]):
- dphi_dx_c = []
- dphi_c = []
- for n in range(phi_cav.shape[1]):
- y = relative_phase[c,n,:,np.newaxis] #vo
- X = ABC_vd[n,:,:] #vd
- X1 = np.concatenate((X,np.ones((X.shape[0],1),float)),axis=1)
- b = np.linalg.inv(X1.T@X1)@(X1.T@y) #dv,vd->dd; dv,vo->d0: dd,d0->d0
- dphi_dx_c += [np.linalg.norm(b[:,0],axis=0)]
- _X = _ABC_vd[n,:,:] #vd
- _X1 = np.concatenate((_X,np.ones((_X.shape[0],1),float)),axis=1)
- _b = np.linalg.inv(_X1.T@_X1)@(_X1.T@y) #dv,vd->dd; dv,vo->d0: dd,d0->d0
- dphi_c += [np.linalg.norm(_b[:,0],axis=0)]
- dphi_dx += [np.array(dphi_dx_c)]
- dphi += [np.array(dphi_c)]
- dphi_dx = np.array(dphi_dx)
- dphi = np.array(dphi)
- return dphi_dx,dphi
- def wavelet(data_cts,N_cycles,frequency,tdel,minf,use_cuda=False,PHname=None): #compute the phase and power using Morlet wavelets
- #read in the files if they arlready exist
- if PHname is not None:
- LPname = PHname.replace('PH_','LP_')
- print('Looking for',PHname)
- print('Looking for',LPname)
- if PHname is not None and exists(PHname+'.npy') and exists(LPname+'.npy'):
- print('Reading in phase file')
- phase_CTS = np.load(PHname+'.npy')
- if verbose: print('phase_CTS min max',phase_CTS.min(),phase_CTS.max(),'phase_CTS shape',phase_CTS.shape)
- print('Reading in power file')
- logpower_CTS = np.load(LPname+'.npy')
- if verbose: print('logpower_CTS min max',logpower_CTS.min(),logpower_CTS.max(),'logpower_CTS shape',logpower_CTS.shape)
- return np.exp(1j*phase_CTS),np.power(10,logpower_CTS) #convert the phase to complex-valued unit vectors (phi), convert the logpower to power
- else:
- #data_cts is the time series data in shape cases (trials, epochs), times (samples), sensors (contacts, electrodes)
- #N_cycles is the number of cycles in the Morlet wavelet; typically 2 in Alexander et al.
- #frequency is the centre frequency, typically chosen from oversampled, logarithmically spaced frequncies
- #tdel is the time interval between samples e.g. 1ms
- #minf is the minimum frequency used in the analysis, need to ensure the final phase/power estimates all have the same number of samples
- print('Estimating short time-series Fourier components for temporal frequency %.2fHz'%frequency)
- nSamples = data_cts.shape[1]
- nSensors = data_cts.shape[2]
- N_cycles_of_samples = int(N_cycles*1000.0/(frequency*tdel))
- #the extra samples of data required at both start and beginning of estimated phase/power values
- PH_unpad_at_minf = int(N_cycles*500.0/(minf*tdel))
- #the number of samples of phase/power that will be generated
- PHSamples = nSamples - 2*PH_unpad_at_minf
- #half the wavelet cycles at lowest frequency minus half the wavelet cycles at this frequency
- PH_pad_at_f = PH_unpad_at_minf - int(N_cycles*500.0/(frequency*tdel))
- W = gausian_window(N_cycles_of_samples) #size of Morlet window
- f_indexes = []
- #how many sets of indexes to make? one for each sample in phase
- for t in range(PHSamples):
- #make a list of all data samples that are used in calculating each phase estimate
- padded_range = range(t+PH_pad_at_f,t+PH_pad_at_f+N_cycles_of_samples)
- f_indexes += padded_range
- if use_cuda: #works with cupy if available https://cupy.dev
- f_indexes = cp.asarray(f_indexes)
- power_cTs = cp.zeros((data_cts.shape[0],PHSamples,nSensors),float)
- phi_cTs = cp.zeros((data_cts.shape[0],PHSamples,nSensors),complex)
- W = cp.asarray(W)
- for c,data_ts in enumerate(data_cts):
- print('#',end='')
- data_ts = cp.asarray(data_ts)
- for s in range(nSensors):
- data_tw = data_ts[f_indexes,s].reshape(PHSamples,N_cycles_of_samples) #tw
- data_tw = data_tw - data_tw.mean(1)[:,cp.newaxis] #mean of time-series to zero
- t_f = 2.0*cp.pi*N_cycles*cp.arange(N_cycles_of_samples)/N_cycles_of_samples
- t_phi = cp.exp(1j*(t_f)) * W
- fourier_components = (data_tw * t_phi[cp.newaxis,:]).sum(1)
- phi_cTs[c,:,s] = cp.exp(1j*cp.angle(fourier_components)) #over w
- power_cTs[c,:,s] = cp.absolute(fourier_components)
- print()
- return cp.asnumpy(phi_cTs),cp.asnumpy(power_cTs)
- else:
- f_indexes = np.asarray(f_indexes)
- power_cTs = np.zeros((data_cts.shape[0],PHSamples,nSensors),float)
- phi_cTs = np.zeros((data_cts.shape[0],PHSamples,nSensors),complex)
- for c,data_ts in enumerate(data_cts):
- print('#',end='')
- for s in range(nSensors):
- data_tw = data_ts[f_indexes,s].reshape(PHSamples,N_cycles_of_samples) #tw
- data_tw = data_tw - data_tw.mean(1)[:,np.newaxis] #mean of time-series to zero
- t_f = 2.0*np.pi*N_cycles*np.arange(N_cycles_of_samples)/N_cycles_of_samples
- t_phi = np.exp(1j*(t_f)) * W
- fourier_components = (data_tw * t_phi[np.newaxis,:]).sum(1)
- phi_cTs[c,:,s] = np.exp(1j*np.angle(fourier_components)) #over w
- power_cTs[c,:,s] = np.absolute(fourier_components)
- print()
- if PHname is not None: #i.e. looked for save files but didn't find them
- print('Writing to',PHname+'.npy',end = ' ')
- np.save(PHname+'.npy',np.angle(phi_cTs)) #saved as the real angle
- print('Writing to',LPname+'.npy',end = ' ')
- np.save(LPname+'.npy',np.log10(power_cTs)) #save the log power
- return phi_cTs,power_cTs
- def gaus(x,a,x0,sigma): #define a Gaussian
- return a*np.exp(-(x-x0)**2/(2*sigma**2))
- def gausian_window(x): #make a Gaussian window
- if type(x) is int:
- n = x
- x = np.arange(n)
- else:
- n = len(x)
- cosine_window = 0.5*(1.0 - np.cos(2*np.pi*x/(n-1))) #approximate the window as a cosine first
- mean = sum(x*cosine_window)/n
- sigma = np.sqrt(sum(cosine_window*(x-mean)**2)/n)
- popt,pcov = curve_fit(gaus,x,cosine_window,p0=[1,mean,sigma]) #turn it into a Gaussian window
- G = gaus(x,*popt)
- return G
- # %%
- #helper functions
- def save_pkl(output_path,obj,name): #the format used for saving dictionaries, lists
- print('Saving',output_path+ name + '.pkl')
- with open(output_path+ name + '.pkl', 'wb') as f:
- dump(obj, f, HIGHEST_PROTOCOL)
- def load_pkl(output_path,name): #open previously saved dictionaries, lists
- print('Loading',output_path + name + '.pkl')
- with open(output_path + name + '.pkl', 'rb') as f:
- return load(f)
- def get_coordinates_and_distances(file_path,coordinate_file,distance_file,distance_units): # convenience function for grabbing sensor coordinates and cross-sensor distances
- print('Opening coordinate file',coordinate_file)
- coordinates_sd = np.loadtxt(file_path+coordinate_file+'.csv',skiprows=1,delimiter=',').T #native coordinate system units; A-P,I-S,L-R: anterior-posterior, inferior-superior, left-right
- coordinates_metres = coordinates_sd/distance_units
- dmetres_ss = load_pkl(file_path,distance_file)/distance_units #mm->m in this data
- print('dmetres_ss min',dmetres_ss[dmetres_ss>0.0].min(),'dmetres_ss max',dmetres_ss[dmetres_ss<0.5].max())
- if 'depth' in coordinate_file:
- #initialize distances between contacts
- dmetres_SS = np.linalg.norm(coordinates_metres[:,np.newaxis,:]-coordinates_metres[np.newaxis,:,:],axis=2)
- if verbose: print('dmetres_SS min',dmetres_SS.min(),'dmetres_SS max',dmetres_SS.max())
- print('dmetres_SS[dmetres_SS>0] min',dmetres_SS[dmetres_SS>0].min(),'dmetres_SS[dmetres_SS<0.5] max',dmetres_SS[dmetres_SS<0.5].max())
- #grab geodesic distances
- for s in range(dmetres_SS.shape[0]):
- for ss in range(dmetres_SS.shape[1]):
- dmetres_SS[s,ss] = max(dmetres_SS[s,ss],dmetres_ss[s,ss]) #take the largest of the two because geodesic can give zero for neighbouring contacts
- elif 'MEG' in coordinate_file:
- dmetres_SS = dmetres_ss
- return coordinates_metres,dmetres_SS
- def angles_of_triangle_from_lengths(dmetres_SS,triplets_av): #used to construct approximate equilateral triangles from inter-contact distances
- """a, b and c are lengths of the sides of a triangle"""
- a = dmetres_SS[triplets_av[:,0],triplets_av[:,1]]
- b = dmetres_SS[triplets_av[:,1],triplets_av[:,2]]
- c = dmetres_SS[triplets_av[:,2],triplets_av[:,0]]
- # Square of lengths a2, b2, c2
- a2 = a**2
- b2 = b**2
- c2 = c**2
- # From Cosine law
- alpha = np.arccos((b2 + c2 - a2) / (2 * b * c))
- beta = np.arccos((a2 + c2 - b2) / (2 * a * c))
- gamma = np.arccos((a2 + b2 - c2) / (2 * a * b))
- return alpha,beta,gamma
- def triangle_normal(triangles):
- # The cross product of two sides is a normal vector
- return np.cross(triangles[:,1] - triangles[:,0],
- triangles[:,2] - triangles[:,0], axis=1)
- def triangle_area(triangles):
- # The norm of the cross product of two sides is twice the area
- return np.linalg.norm(triangle_normal(triangles), axis=1) / 2
- def collate_nonskinny_triangles(dmetres_SS,nonskinny_threshold=np.pi/4.0): #make a list of triplets, with each triplet specifying an approximately equilateral triangle, where nonskinny_threshold is the smallest allowed angle
- nSensors = dmetres_SS.shape[0]
- if verbose: print('nSensors',nSensors)
- if verbose: print('dmetres_SS shape',dmetres_SS.shape)
- if verbose: print('dmetres_SS')
- if verbose: print(dmetres_SS)
- print('Making nonskinny triangles of contacts')
- unique_triplets = []
- for s in range(nSensors):
- print('.',end='')
- for ss in range(nSensors):
- for sss in range(nSensors):
- if s!=ss and ss!=sss and sss!=s and \
- dmetres_SS[s,ss]>0.0 and dmetres_SS[ss,sss]>0.0 and dmetres_SS[sss,s]>0.0 and \
- dmetres_SS[s,ss]<0.5 and dmetres_SS[ss,sss]<0.5 and dmetres_SS[sss,s]<0.5: #0.5m is the flag for cross-hemisphere distances in sEEG distance calculation:
- u = set(np.array([s,ss,sss]))
- if u not in unique_triplets: unique_triplets += [u]
- print()
- for u,un in enumerate(unique_triplets): unique_triplets[u] = np.sort(np.array(list(un))) #set becomes array
- unique_triplets = np.array(unique_triplets)
- print(unique_triplets.shape)
- alpha_a,beta_a,gamma_a = angles_of_triangle_from_lengths(dmetres_SS,unique_triplets) #ss,av
- angles_va = np.array([alpha_a,beta_a,gamma_a])
- angles_min_a = angles_va.min(0)
- nonskinnyity_met = angles_min_a>nonskinny_threshold
- nonskinny_triplets = np.array(unique_triplets)[nonskinnyity_met]
- if verbose:
- print('unique_triplets shape',unique_triplets.shape)
- print('nonskinnyity_met shape',nonskinnyity_met.shape)
- print('nonskinny_triplets shape',nonskinny_triplets.shape)
- print('nonskinny_triplets')
- print(nonskinny_triplets)
- return nonskinny_triplets #the indexes of contacts in each triangle
- def bin_triplets_linearSF(dmetres_SS,nonskinny_triplets,return_binwise=False,smallest_bin_size=0.01,largest_bin_size=0.32): #can return triangles in bins, or as a flat list ordered by triangle size
- print('Binning triangles')
- try:
- #calculate areas of each triangle from geodesic distances
- nominal_min_SF = 500.0 # cycles per metre
- n_bins = int(largest_bin_size/smallest_bin_size) #this is silly way to get n=32
- #get the triangle edges from the geodesic distances and triplets
- ta = nonskinny_triplets
- dm = dmetres_SS
- dm_ta01 = dm[ta[:,0],ta[:,1]]
- dm_ta12 = dm[ta[:,1],ta[:,2]]
- dm_ta20 = dm[ta[:,2],ta[:,0]]
- nominal_triangle_coordinates_te = coordinates_of_triangle_given_lengths(dm_ta01,dm_ta12,dm_ta20) #te: triangle x edge
- triplet_areas = triangle_area(nominal_triangle_coordinates_te)
- root_areas = np.sqrt(triplet_areas) #linear size of each triangle
- order_root_areas = root_areas.argsort(0)
- ordered_root_areas = root_areas[order_root_areas]
- ordered_nonskinny_triplets = nonskinny_triplets[order_root_areas]
- #fill the bins
- nonskinny_triplets_linearSF_bins = [] #triangles assigned to each bin
- nonskinny_triplets_a = [] #flat list of triangles assigned, ordered by triangle linear area
- root_areas_linearSF_bins = [] #areas of triangles assigned to each bin
- root_areas_a = [] #flat list of areas of triangles assigned to each bin
- n_per_linearSF_bins = [] #number of triangles in each bin
- if ordered_root_areas.shape[0]==0: return None
- linearSF_bin_edges = [ordered_root_areas[0]] #first two edges, because n_bins+1 edges
- linearSF_ranges_bins = [] #min and max of each bin
- old_size_cutoff = 1.0/nominal_min_SF
- for SF_cutoff in np.append(np.linspace(1.0/largest_bin_size,1.0/smallest_bin_size,n_bins),nominal_min_SF)[:-1][::-1]: #last value is taken first, but is already used in initialization
- size_cutoff = 1.0/SF_cutoff
- size_cutoff_boolean = ((ordered_root_areas<size_cutoff) * (ordered_root_areas>=old_size_cutoff)).astype(bool)
- nThese = size_cutoff_boolean.sum(0)
- these_nonskinny_triplets = ordered_nonskinny_triplets[size_cutoff_boolean,:]
- these_root_areas = ordered_root_areas[size_cutoff_boolean]
- if verbose:
- print(old_size_cutoff,size_cutoff,nThese)
- if nThese>0: print(these_root_areas.min(0),these_root_areas.max(0))
- else: print('-','-')
- if nThese>0:
- nonskinny_triplets_linearSF_bins += [list(these_nonskinny_triplets)]
- nonskinny_triplets_a += list(these_nonskinny_triplets)
- root_areas_linearSF_bins += [list(these_root_areas)]
- root_areas_a += list(these_root_areas)
- linearSF_bin_edges += [size_cutoff]
- linearSF_ranges_bins += [np.array([these_root_areas.min(0),these_root_areas.max(0)])] #zeroth is zero, 1st is smallest_bin_size, nth is largest_bin_size
- n_per_linearSF_bins += [these_nonskinny_triplets.shape[0]]
- else:
- nonskinny_triplets_linearSF_bins += [[np.array([-1,-1,-1])]]
- root_areas_linearSF_bins += [[-1]]
- linearSF_bin_edges += [-1]
- linearSF_ranges_bins += [[-1,-1]]
- n_per_linearSF_bins += [0]
- old_size_cutoff = 1*size_cutoff
- if len(root_areas_a)==0: return None
- root_areas_a = np.array(root_areas_a)
- print('root_areas shape',root_areas.shape,'ordered_root_areas shape',ordered_root_areas.shape,'root_areas_a shape',root_areas_a.shape)
- n_per_linearSF_bins = np.array(n_per_linearSF_bins)
- #some book-keeping
- nonzero_root_areas_linearSF_bins = []
- nonzero_n_per_linearSF_bins = []
- nonzero_nonskinny_triplets_linearSF_bins = []
- nonzero_linearSF_bin_edges = []
- nonzero_linearSF_ranges_bins = [linearSF_ranges_bins[0]]
- for B,n_bin in enumerate(n_per_linearSF_bins):
- if n_bin>0:
- nonzero_root_areas_linearSF_bins += [root_areas_linearSF_bins[B]]
- nonzero_n_per_linearSF_bins += [n_per_linearSF_bins[B]]
- nonzero_nonskinny_triplets_linearSF_bins += [nonskinny_triplets_linearSF_bins[B]]
- if len(nonzero_linearSF_bin_edges)==0: nonzero_linearSF_bin_edges += [linearSF_bin_edges[B]]
- nonzero_linearSF_bin_edges += [linearSF_bin_edges[B+1]] #n_bins + 1 values
- nonzero_linearSF_ranges_bins += [linearSF_ranges_bins[B]]
- nonzero_linearSF_bin_edges[-1] = ordered_root_areas[-1] #correct the last edges
- nonzero_linearSF_ranges_bins[-1][1] = ordered_root_areas[-1] #correct the second term of the last nonzero bin range
- nonzero_linearSF_bin_edges = np.array(nonzero_linearSF_bin_edges)
- nonzero_linearSF_ranges_bins = np.array(nonzero_linearSF_ranges_bins)
- nonzero_n_per_linearSF_bins = np.array(nonzero_n_per_linearSF_bins)
- range_a = [ordered_root_areas[0],ordered_root_areas[-1]]
- print('nonzero_linearSF_ranges_bins')
- print(nonzero_linearSF_ranges_bins)
- max_of_linearSF_bins = max(nonzero_n_per_linearSF_bins)
- normalization_linearSF_bins = -np.ones(n_per_linearSF_bins.shape,float)
- normalization_linearSF_a = [] #'linearSF' because its based on assumption of linearly increasing SF
- print('n_per_linearSF_bins sum',n_per_linearSF_bins.sum(),'nonzero_n_per_linearSF_bins sum',nonzero_n_per_linearSF_bins.sum())
- for B,bin_n in enumerate(n_per_linearSF_bins):
- if bin_n>0:
- normalization_linearSF_bins[B] = max_of_linearSF_bins/bin_n #will eventually multiply by normalization weight
- normalization_linearSF_a += bin_n*[normalization_linearSF_bins[B]] #grab copies equal to the number of triangles in bin
- normalization_linearSF_a = np.array(normalization_linearSF_a)
- print('normalization_linearSF_a shape',normalization_linearSF_a.shape)
- if verbose:
- print('linearSF_bin_edges')
- print(linearSF_bin_edges)
- print('linearSF_ranges_bins')
- print(linearSF_ranges_bins)
- print('n_per_linearSF_bins')
- print(n_per_linearSF_bins)
- print('normalization_linearSF_a')
- print(normalization_linearSF_a)
- print('nonzero_linearSF_bin_edges')
- print(nonzero_linearSF_bin_edges)
- print('nonzero_n_per_linearSF_bins')
- print(nonzero_n_per_linearSF_bins)
- plot_bin_counts_and_normalization(nonzero_linearSF_bin_edges,nonzero_n_per_linearSF_bins,root_areas_a,normalization_linearSF_a,SFscale=True,cut_final_bin=True)
- if return_binwise: return n_per_linearSF_bins,nonskinny_triplets_linearSF_bins,normalization_linearSF_bins,\
- linearSF_ranges_bins,root_areas_linearSF_bins,linearSF_bin_edges #first array is used to avoid empty bins
- else: return n_per_linearSF_bins,np.array(nonskinny_triplets_a),normalization_linearSF_a,range_a,root_areas_a,linearSF_bin_edges
- except:
- print('No binned triplets returned')
- return None
- def coordinates_of_triangle_given_lengths(a,b,c): #abstract triangle coordinates from geodesic distances
- """a, b and c are lengths of the sides of a triangle"""
- A = np.array([[0]*len(a),[0]*len(a),[0]*len(a)]) # coordinates of vertex A
- B = np.array([c,[0]*len(a),[0]*len(a)]) # coordinates of vertex B
- #if verbose: print(A.shape,B.shape)
- C_x = b * (b**2 + c**2 - a**2) / (2 * b * c)
- C_y = np.sqrt(b**2 - C_x**2) # square root
- C = np.array([C_x,C_y,[0]*len(a)]) # coordinates of vertex C
- return np.concatenate([[A],[B],[C]],axis=0).transpose(2,0,1) #tvd: triangle, vertex, cartesian dimension
- def decompose_rphi_into_forward_waves(rphi_cts,tdel,frequency): #removes the negative going component of the wave and estimates normalized velocity
- #tdel is 1/sampling_frequency in ms
- #frequency in Hertz
- #rphi_cts is complex valued phase, possibly with magnitude weighting
- print('Decompose spatial vectors of phase into pure traveling wave components')
- one_cycle_of_samples = int(1000.0/(tdel*frequency)) #only requires four samples over the cycle, so this int could be replace with 4
- rphi_Cs = rphi_cts.reshape(-1,rphi_cts.shape[2])
- vel_C = [] #normalized velocity, 0 is pure SW, 1 is pure TW
- TWf_C = [] #forward spatial component
- for C in range(rphi_Cs.shape[0]):
- rphi_Ts = np.exp(1j*np.linspace(-np.pi,np.pi,one_cycle_of_samples,endpoint=False))[:,np.newaxis] * rphi_Cs[C,:][np.newaxis,:]
- u_r,s_r,vt_r = np.linalg.svd(rphi_Ts.real)
- TWf_s = vt_r[0,:] + 1j*vt_r[1,:] #throw away the time dimension we added, only need the spatial dimension
- TWf_C += [TWf_s[np.newaxis]]
- vel_C += [np.array([s_r[1]/s_r[0]])[np.newaxis]]
- vel_ct = np.concatenate(vel_C,axis=0).reshape(rphi_cts.shape[0],rphi_cts.shape[1])
- TWf_cts = np.concatenate(TWf_C,axis=0).reshape(rphi_cts.shape[0],rphi_cts.shape[1],rphi_cts.shape[2])
- return vel_ct,TWf_cts
- # %%
- #plotting functions
- def sf_histogram(imaging_path,plot_name,output_file,list_of_estpower,list_of_wavelength,list_of_max_distances, #plot the histogram of SF
- max_of_range,min_of_range,N_histogram_bins,
- stacked=False,colourmap='RdYlBu_r',linewidth=1,ticklabelsize=10.0,axislabelsize=12.0,listwise_weights=None):
- print('Plotting',plot_name)
- if verbose: print('max_of_range',max_of_range,'min_of_range',min_of_range)
- if verbose: print('1.0/max_of_range',1.0/max_of_range,'1.0/min_of_range',1.0/min_of_range)
- cmap = cm.get_cmap(colourmap)
- colours = []
- for l in range(len(list_of_estpower)):
- colours += [cmap(l/len(list_of_estpower))] #avoid division by zero
- #colours = ['red','green','blue','magenta','cyan']
- fig1 = figure(figsize=(5,5),dpi=90)
- axs1 = []
- axs1 += [fig1.add_subplot(1,1,1)]
- axs1[-1].set_title(plot_name.replace('_',' '))
- axs1[-1].tick_params(axis='x',which = 'both',labelsize=ticklabelsize)
- axs1[-1].tick_params(axis='y',labelsize=ticklabelsize)
- axs1[-1].set_xscale('log')
- if 'sEEG' in plot_name:
- locs = np.append(np.arange(1.0,10.0,1.0),np.arange(10.0,100.0,10.0))
- locs = np.delete(locs,np.where(locs==9.0))
- locs = np.delete(locs,np.where(locs==40.0))
- locs = np.delete(locs,np.where(locs==60.0))
- locs = np.delete(locs,np.where(locs==70.0))
- locs = np.delete(locs,np.where(locs==80.0))
- locs = np.delete(locs,np.where(locs==90.0))
- elif 'MEG' in plot_name:
- locs = np.append(np.arange(1.0,10.0,1.0),np.arange(10.0,50.0,10.0))
- locs = np.delete(locs,np.where(locs==9.0))
- locs = np.delete(locs,np.where(locs==40.0))
- axs1[-1].xaxis.set_minor_locator(ticker.FixedLocator(locs))
- axs1[-1].xaxis.set_major_locator(ticker.NullLocator())
- axs1[-1].xaxis.set_minor_formatter(ticker.ScalarFormatter())
- axs1[-1].set_xlabel('Spatial frequency (cycles/metre)',fontsize=axislabelsize)
- axs1[-1].set_ylabel('Estimated power (A.U.)',fontsize=axislabelsize)
- if listwise_weights is None: listwise_weights = np.ones((len(list_of_estpower)),float)
- logscale_bins = np.power(10,np.linspace(np.log10(1.0/max_of_range),np.log10(1.0/min_of_range),N_histogram_bins))
- list_of_hist_power = []
- list_of_hist_sf = []
- if stacked:
- cumulative_hist = np.zeros((N_histogram_bins-1),float)
- for l,estpower,wavelength,case_weight in zip(range(len(list_of_estpower)),list_of_estpower,list_of_wavelength,listwise_weights):
- wavelength = np.array(wavelength)
- estpower = np.array(estpower)
- sf = 1.0/wavelength
- power_hist = np.histogram(sf,bins=logscale_bins,weights=estpower)
- w_power_hist = case_weight * power_hist[0] #case_weight is a post-hoc way to add weighting to each curve e.g. to mimic sigma
- cumulative_hist += w_power_hist
- axs1[-1].plot((power_hist[1][:-1]+power_hist[1][1:])/2.0,cumulative_hist,c=colours[l],linewidth=linewidth)
- for md in list_of_max_distances:
- axs1[-1].plot(1.0/md,0.0,'.',color='k')
- list_of_hist_power += [w_power_hist]
- list_of_hist_sf += [(power_hist[1][:-1]+power_hist[1][1:])/2.0]
- else:
- for l,estpower,wavelength,case_weight in zip(range(len(list_of_estpower)),list_of_estpower,list_of_wavelength,listwise_weights):
- wavelength = np.array(wavelength)
- estpower = np.array(estpower)
- sf = 1.0/wavelength
- power_hist = np.histogram(sf,bins=logscale_bins,weights=estpower)
- w_power_hist = case_weight * power_hist[0]
- axs1[-1].plot((power_hist[1][:-1]+power_hist[1][1:])/2.0,w_power_hist,c=colours[l],linewidth=linewidth)
- for md in list_of_max_distances:
- axs1[-1].plot(1.0/md,0.0,'.',color='k')
- list_of_hist_power += [w_power_hist]
- list_of_hist_sf += [(power_hist[1][:-1]+power_hist[1][1:])/2.0]
- display(fig1)
- fig1.savefig((imaging_path+plot_name+'_'+output_file).replace(' ','_').replace('c/m','cperm')+'.jpg',facecolor='white',transparent=False,dpi=600)
- fig1.savefig((imaging_path+plot_name+'_'+output_file).replace(' ','_').replace('c/m','cperm')+'.svg',facecolor='white',transparent=False,dpi=600)
- fig1.clf()
- close(fig1)
- del fig1
- return list_of_hist_power,list_of_hist_sf
- def plot_bin_counts_and_normalization(bin_edges,bin_counts,used_root_areas,normalization_a,SFscale=True,cut_final_bin=False): #for checking the triangle construction, binning, normalization
- print('Plotting bin counts')
- fig = figure(figsize=(5,2.5))
- ax = fig.add_subplot(1,1,1)
- if SFscale:
- ax.plot(1.0/bin_edges[1:],bin_counts,'4',color='g')
- ax.plot(1.0/bin_edges[:-1],bin_counts,'3',color='r')
- ax.set_xlabel('Bin edges (1/m)')
- ax.set_ylabel('Triangle count')
- else:
- ax.plot(bin_edges[1:],bin_counts,'3',color='g')
- ax.plot(bin_edges[:-1],bin_counts,'4',color='r')
- ax.set_xlabel('Bin edges (m)')
- ax.set_ylabel('Triangle count')
- xlims = ax.get_xlim()
- ax.set_xlim(xlims[0],1.2/bin_edges[1])
- display(fig)
- fig.clf()
- close(fig)
- del fig
- print('Plotting binwise normalization')
- fig = figure(figsize=(5,2.5))
- ax = fig.add_subplot(1,1,1)
- if SFscale:
- ax.plot(1.0/used_root_areas,1.0/normalization_a,'.',color='k',markersize=0.2) #normalization_a is divisor in SF power calculation
- ax.set_xlabel('Triangle SF')
- ax.set_ylabel('1.0 / normalization factor')
- else:
- ax.plot(used_root_areas,1.0/normalization_a,'.',color='k',markersize=0.2) #normalization_a is divisor in SF power calculation
- ax.set_xlabel('Triangle size')
- ax.set_ylabel('1.0 / normalization factor')
- xlims = ax.get_xlim()
- ax.set_xlim(xlims[0],1.2/bin_edges[1])
- display(fig)
- fig.clf()
- close(fig)
- del fig
- def phase_plot(bases_sb,frequency,contact_xyz,vector_name='wave map',mag=False,figout=None): #assumes complex valued bases
- print('Plotting phases')
- nBases = bases_sb.shape[1]
- cols = 3
- rows = (nBases//3)+1
- fig = figure(figsize=(cols*8,rows*8),dpi=200)
- axs = []
- if mag: alpha = np.absolute(bases_sb)/np.absolute(bases_sb).max()
- else: alpha = np.ones(bases_sb.shape,float)
- for b in range(nBases):
- axs += [fig.add_subplot(rows,cols,b+1,projection='3d')]
- axs[b].view_init(elev=90,azim=-90)
- axs[b].set_title('Frequency %.1fHz %s %d'%(frequency,vector_name,b+1))
- for s,xyz in enumerate(contact_xyz):
- color = cm.hsv(np.angle(bases_sb[s,b]) / (2.0*np.pi) + 0.5) #put angles on range 0 to 1
- axs[b].plot(xyz[0],xyz[1],xyz[2],'.',markersize=20.0,markerfacecolor=color,markeredgecolor=None,alpha=alpha[s,b])
- axs[b].plot(xyz[0],xyz[1],xyz[2],'.',markersize=20.0,markeredgecolor=color,fillstyle='none',alpha=1.0)
- axs[b].set_aspect('equal')
- axs[b].set_xlabel('Anterior-Posterior')
- axs[b].set_ylabel('Inferior-Superior')
- fig.subplots_adjust(wspace=0.0, hspace=0.0)
- if figout is not None:
- fig.savefig(figout+'.png',facecolor='white',transparent=False)
- fig.savefig(figout+'.svg',facecolor='white',transparent=False)
- else:
- rect = fig.patch
- rect.set_facecolor('white')
- display(fig)
- fig.clf()
- close(fig)
- del fig
- def draw_triangles(contact_xyz,binned_triplets,bin_counts,file_path,title,root_areas): #for visualization of the equilateral triangles, draws in simple cartesian coordinates
- print('Plotting triangles over size bins')
- nBinPlots = bin_counts.shape[0]
- cols = 3
- rows = (nBinPlots//3)+1
- fig = figure(figsize=(cols*10,rows*10),dpi=150)
- axs = []
- cumulative_bin_count = 0
- for br in range(nBinPlots):
- axs += [fig.add_subplot(rows,cols,br+1,projection='3d')]
- axs[br].view_init(elev=90,azim=-90)
- axs[br].plot(contact_xyz[:,0],contact_xyz[:,1],contact_xyz[:,2],'.',color='k')
- if bin_counts[br]>0:
- triplets = binned_triplets[cumulative_bin_count:cumulative_bin_count+bin_counts[br]]
- mean_root_area = root_areas[cumulative_bin_count:cumulative_bin_count+bin_counts[br]].mean(0)
- axs[br].set_title('Size bin %d, mean size %.3f'%(br+1,mean_root_area))
- for t,trip in enumerate(triplets):
- if triplets.shape[0]>1: colour = cm.cool(t/(triplets.shape[0])) #0 to 1
- else: colour = cm.cool(0.0)
- T = np.concatenate((trip,trip[0:1]),axis=0) #close the triangle
- axs[br].plot(contact_xyz[T,0],contact_xyz[T,1],contact_xyz[T,2],color=colour,alpha=1.0/np.sqrt(bin_counts[br])) #density of colour on plot increases with line length
- if t==0: axs[br].plot(contact_xyz[T,0],contact_xyz[T,1],contact_xyz[T,2],color=cm.cool(0.0),alpha=1.0)
- elif t==triplets.shape[0]-1: axs[br].plot(contact_xyz[T,0],contact_xyz[T,1],contact_xyz[T,2],color=cm.cool(1.0),alpha=1.0)
- axs[br].set_aspect('equal')
- xticks = axs[br].get_xticks()
- axs[br].set_xticks(xticks[::2])
- zticks = axs[br].get_zticks()
- axs[br].set_zticks(zticks[::4])
- cumulative_bin_count += bin_counts[br]
- set_axes_equal(axs[br]) # aspect ratio is 1:1:1 in data space
- tight_layout()
- fig.savefig((file_path+title).replace('c/m','cperm')+'.png')
- fig.savefig((file_path+title).replace('c/m','cperm')+'.svg')
- display(fig)
- fig.clf()
- close(fig)
- del fig
- def set_axes_equal(ax): #to draw veridical relative distances in plots
- """
- Make axes of 3D plot have equal scale so that spheres appear as spheres,
- cubes as cubes, etc.
- Input
- ax: a matplotlib axis, e.g., as output from plt.gca().
- """
- x_limits = ax.get_xlim3d()
- y_limits = ax.get_ylim3d()
- z_limits = ax.get_zlim3d()
- x_range = abs(x_limits[1] - x_limits[0])
- x_middle = np.mean(x_limits)
- y_range = abs(y_limits[1] - y_limits[0])
- y_middle = np.mean(y_limits)
- z_range = abs(z_limits[1] - z_limits[0])
- z_middle = np.mean(z_limits)
- # The plot bounding box is a sphere in the sense of the infinity
- # norm, hence I call half the max range the plot radius.
- plot_radius = 0.5*max([x_range, y_range, z_range])
- ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius])
- ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius])
- ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius])
- # %%
- # main script
- binning_success = 0 #keep track of whether triangles were successfuly constructed and binned
- for j,subject in enumerate(subjects): #loop through the participant's files
- tdel = tdel_values[j]
- print('Doing subject',subject)
- #MEG, sEEG files; made using other resources not included in this code base
- coordinate_file = coordinate_files[j]
- distance_file = geodesic_distance_files[j]
- timeseries_file = TS_files[j]
- triangle_file = triangle_files[j]
- print(triangle_file)
- metres_sd,dmetres_SS = get_coordinates_and_distances(file_path,coordinate_file,distance_file,distance_units)
- #save relevant paramenters with subject for later analysis
- aggregate_subjects[subject] = {}
- aggregate_subjects[subject]['tdel'] = tdel
- aggregate_subjects[subject]['metres_sd'] = metres_sd
- aggregate_subjects[subject]['dmetres_SS'] = dmetres_SS
- nSensors = metres_sd.shape[0]
- #look for triplet files, make them if they don't exist (slow, taking many hours for large number of contacts, so we save them after making)
- if exists(file_path+triangle_file+'_nonskinny_threshold_%.4f.pkl'%nonskinny_threshold): nonskinny_triplets = load_pkl(file_path,triangle_file+'_nonskinny_threshold_%.4f'%nonskinny_threshold)
- else:
- nonskinny_triplets = collate_nonskinny_triangles(dmetres_SS,nonskinny_threshold=nonskinny_threshold)
- save_pkl(file_path,nonskinny_triplets,triangle_file+'_nonskinny_threshold_%.4f'%nonskinny_threshold)
- if verbose: print('nonskinny_triplets shape',nonskinny_triplets.shape)
- #put the triangles into bins of the approximately the same linear sizes
- binning_out = bin_triplets_linearSF(dmetres_SS,nonskinny_triplets,return_binwise=False,smallest_bin_size=smallest_triangle_size,largest_bin_size=largest_triangle_size)
- if binning_out is not None: #if None, failed to bin the triplets with current values for nonskinny_threshold, smallest_bin_proportion, or because small number of distances
- #housekeeping from the binning
- binning_success += 1
- analysis_info_string = 'nB_%d_nC_%d_nonskinny_th_%.4f_t_range_%.3f_to_%.3f'%(nBases,N_cycles,nonskinny_threshold,smallest_triangle_size,largest_triangle_size)
- print('analysis_info_string',analysis_info_string)
- n_per_linearSF_bins,binned_triplets_av,normalization_a,range_a,root_areas_a,linearSF_bin_edges = binning_out
- aggregate_subjects[subject]['binned_triplets_av'] = binned_triplets_av
- aggregate_subjects[subject]['normalization_linearSF_a'] = normalization_a #for naming consistency
- aggregate_subjects[subject]['range_a'] = range_a
- aggregate_subjects[subject]['root_areas_a'] = root_areas_a
- aggregate_subjects[subject]['linearSF_bin_edges'] = linearSF_bin_edges
- bin_counts = n_per_linearSF_bins[n_per_linearSF_bins>0]
- do_bins_string = ''
- min_triangle = range_a[0]
- max_triangle = range_a[-1]
- print('n_per_linearSF_bins',n_per_linearSF_bins)
- print(j,binning_success,subject)
- if verbose:
- plot_title = triangle_file+'_nonskinny_threshold_%.4f_smallest_triangle_size_%.3f_largest_triangle_size_%.3f%s'%(nonskinny_threshold,smallest_triangle_size,largest_triangle_size,do_bins_string)
- draw_triangles(metres_sd,binned_triplets_av,bin_counts,output_path,plot_title,root_areas_a)
- else: continue #try the next subject
- max_distance = dmetres_SS[dmetres_SS<0.5].max() #0.5 (metres) is used as the mask term since it is larger than the largest human head
- max_of_range = 2*max_triangle #used for plotting
- aggregate_subjects[subject]['max_distance'] = max_distance
- aggregate_subjects[subject]['max_of_range'] = max_of_range
- aggregate_subjects[subject]['min_of_range'] = min_of_range
- timeseries_cts = np.load(file_path+timeseries_file+'.npy')
- if verbose: print('timeseries_cts shape',timeseries_cts.shape)
- for frequency in frequencies:
- aggregate_subjects[subject][frequency] = {}
- list_of_estpower = []
- list_of_wavelength = []
- #estimate phase
- data_label = 'empirical'
- print('Empirical bases from SVD')
- PHname = "%sPH_%.4fHz_%s_%s_%.4fminf" %(file_path,frequency,subject,measurement,minf)
- phi_cts,pow_cts = wavelet(timeseries_cts,N_cycles,frequency,tdel,minf,PHname=PHname)
- if include_normed_power:
- data_label = 'npower'
- pow___s = pow_cts.reshape(-1,pow_cts.shape[2]).mean(0) #normalize power per each grey-matter contact
- pow_cts = pow_cts / pow___s[np.newaxis,np.newaxis,:]
- phi_cts = pow_cts * phi_cts #now weighted by normalized power
- else: data_label = 'phase'
- del pow_cts
- # and make left singular vectors
- bases_sb,sigmas_b,betas_ctb,reduced_cts = complex_spatial_SVD(phi_cts,nBases=nBases,bases_only=False,use_bases=None,unit_phase_out=False)
- if verbose:
- print('bases_sb shape',bases_sb.shape)
- print('absolute(bases_sb) min',np.absolute(bases_sb).min(),'absolute(bases_sb) max',np.absolute(bases_sb).max())
- phase_plot(bases_sb,frequency,metres_sd,mag=True)
- # remove the negative wave components
- if forward_waves:
- vel_ct,TWf_cts = decompose_rphi_into_forward_waves(bases_sb.T[:,np.newaxis,:],tdel,frequency)
- if verbose: print('TWf_cts shape',TWf_cts.shape)
- print('Normalized velocity of spatial vector of phase (SW=0,TW=1)')
- print(vel_ct.T)
- Bases_sb = TWf_cts[:,0,:].T
- if verbose: print('absolute(Bases_sb) min',np.absolute(Bases_sb).min(),'absolute(Bases_sb) max',np.absolute(Bases_sb).max())
- print('Empirical traveling wave components')
- if verbose: phase_plot(Bases_sb,frequency,metres_sd,mag=True)
- else: Bases_sb = bases_sb
- aggregate_subjects[subject][frequency]['Bases_sb'] = Bases_sb
- #estimate the SF
- estpower_ba,metres_per_cycle_ba,dphidx_ba,dphi_ba = make_SF_power(dmetres_SS,binned_triplets_av,normalization_a,sigmas_b,Bases_sb)
- list_of_estpower += [estpower_ba.reshape(-1)]
- list_of_wavelength += [metres_per_cycle_ba.reshape(-1)]
- plot_name = '%s %s SF %s TF %.2fHz'%(subject,measurement,data_label,frequency)
- aggregate_subjects[subject][frequency]['list_of_estpower'] = list_of_estpower
- aggregate_subjects[subject][frequency]['list_of_wavelength'] = list_of_wavelength
- save_pkl(output_path,aggregate_subjects[subject],(plot_name+'_'+analysis_info_string).replace(' ','_'))
- sf_histogram(output_path,plot_name,analysis_info_string,list_of_estpower,list_of_wavelength,\
- [max_triangle],max_of_range,min_of_range,N_histogram_bins,colourmap='cool')
- del aggregate_subjects[subject][frequency]
- clear_output(wait=True) #.pynb file can get very large, especially with verbose==True
- print('binning success',binning_success)
- # %%
estimation_of_SF_of_phase_of_cortical_activity_on_irregular_measurement_arrays.ipynb at commit 42c7fe1, under GPL-3.0 · at the source
Overview
- Université Paris Cité, CNRS, Integrative Neuroscience and Cognition Center Paris France
- GHU-Paris Psychiatrie et Neurosciences, Hôpital Sainte-Anne Paris France
- Institut Universitaire de France (IUF) Paris France
Abstract
The organization of the phase of electrical activity in the cortex is critical to inter-site communication, but the balance of this communication across large-scale (>8 cm), macroscopic (>1 cm), and mesoscopic (1 cm to 1 mm) ranges is an open question. The spatial frequencies (i.e. the spatial scales) of cortical waves have been characterized in the gray matter for micro- and mesoscopic scales of cortex and show decreasing spatial power with increasing spatial frequency. This research, however, has been limited by the size of the measurement array, thus excluding large-scale traveling waves. Obversely, poor spatial resolution of extracranial measurements prevents incontrovertible large-scale estimates of spatial power. We estimate the spatial frequency spectrum of phase dynamics in order to quantify the uncertain large-scale range, utilizing stereotactic electroencephalogram to measure local-field potentials within the gray matter. We take advantage of the large extent of spatial coverage of the cortical sheet, and irregular sampling is offset by use of linear algebra techniques. We find the spatial power of the phase is highest at the lowest spatial frequencies (longest wavelengths), consistent with the power spectra ranges for micro- and meso-scale dynamics, but here shown up to the size of the measurement array (up to 8–16 cm). This result arises across a wide range of temporal frequencies, from the delta band (1–3 Hz) through to the high gamma range (60–100 Hz).
Reproduced under the paper's license (CC BY), from the paper cited above.
Repository
Its files are read in the Code ↔ Paper reader above, with 7 matches between paragraphs and lines of code.
duguelab/traveling-wave-analysis
42c7fe112c99a06e05aaf31939a0eb3e218e0fcc, 6 January 2026Availability: 1 check, the latest on 29 September 2026: the link answers
- 29 September 2026: the link answers
12 files
- TW_analysis_complex_valu
ed_unit_length_phase.py , Python, 27 lines - decompose_into_pure_trav
eling_wave.py , Python, 20 lines - estimation_of_SF_of_phas
e_of_cortical_activity_o , Jupyter, 793 lines, 7 matchesn_irregular_measurement_ arrays.ipynb - example_morlet_wavemap.i
pynb , Jupyter, 53 lines - example_oneway_permutati
on_testing.ipynb , Jupyter, 89 lines - example_permutation_test
ing.ipynb , Jupyter, 79 lines - example_velocity_decompo
sition.ipynb , Jupyter, 96 lines - make_wavelet.py, Python, 55 lines
- oneway_permutation_testi
ng_multiple_dimensions.p , Python, 98 linesy - pairwise_permutation_tes
ting_multiple_dimensions , Python, 94 lines.py - LICENSE, License, 674 lines
- README.md, Text, 9 lines
The paper's code and data availability statement is in the Data section.
Tracing map
Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.
What the map holds:
- 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
- 10 scripts, each with its path and the digest of its content;
- 7 matches between paragraphs of the paper and lines of the code (method lexical-v1);
- neither the text of the paper nor the code itself.
Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.
Data
No dataset and no data link were found in the paper.
Data availability
The key Python routines used in this research can be found here: https://
Reproduced under the paper's license (CC BY), from the paper cited above.
Versions
The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.
Version 1, 29 September 2026: the first record
Recorded: type, language, journal, volume, pages, dates, 2 authors, 7 keywords, 6 MeSH terms, 1 funder, 95 references.
Cite
This paper
Alexander, D. M., & Dugué, L. (2026). The dominance of large-scale phase dynamics in human cortex, from delta to gamma. eLife, 13, RP100674. https://
BibTeX
@article{alexander2026do
author = {Alexander, David M and Dugué, Laura},
title = {{The dominance of large-scale phase dynamics in human cortex, from delta to gamma}},
journal = {eLife},
year = {2026},
month = apr,
volume = {13},
pages = {RP100674},
publisher = {eLife Sciences Publications, Ltd},
issn = {2050-084X},
doi = {10.7554/
url = {https://
pmid = {41954599},
pmcid = {PMC13065328}
}
RIS
TY - JOUR
AU - Alexander, David M
AU - Dugué, Laura
TI - The dominance of large-scale phase dynamics in human cortex, from delta to gamma
T2 - eLife
J2 - eLife
PY - 2026
DA - 2026/
VL - 13
SP - RP100674
SN - 2050-084X
PB - eLife Sciences Publications, Ltd
DO - 10.7554/
UR - https://
LA - en
ER -
CSL-JSON
{
"id": "10.7554/
"type": "article-journal",
"title": "The dominance of large-scale phase dynamics in human cortex, from delta to gamma",
"container-title": "eLife",
"author": [
{
"family": "Alexander",
"given": "David M"
},
{
"family": "Dugué",
"given": "Laura"
}
],
"container-title-short":
"volume": "13",
"page": "RP100674",
"DOI": "10.7554/
"PMID": "41954599",
"PMCID": "PMC13065328",
"ISSN": "2050-084X",
"publisher": "eLife Sciences Publications, Ltd",
"URL": "https://
"language": "en",
"issued": {
"date-parts": [
[
2026,
4,
9
]
]
}
}
The tracing map gets a citation of its own once an author has validated it and it has a DOI.
Similar papers
The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.
- [1] doi:10.1371/journal.pcbi.1013007 [code]
- Traveling waves in the human visual cortex: An MEG-EEG model-based approachJournal: n/aIn common: SciPy, Matplotlib, NumPy, EEG, 15 references, author Laura Dugué
- [2] doi:10.7554/elife.106753 [code]
- Traveling waves across scales: Different mechanisms but same canonical computation?Journal: n/aIn common: 14 references, author Laura Dugué
- [3] doi:10.1038/s41467-026-71386-z [code]
- Planar, spiral, and concentric traveling waves distinguish behavioral states in human memory.Journal: Nature communicationsIn common: SciPy, Matplotlib, NumPy, 10 references
- [4] doi:10.1073/pnas.2527296123
- Traveling-wave transcranial alternating current stimulation (twtACS) causally links neural timing to cognitive function.Journal: Proceedings of the National Academy of Sciences of the United States of AmericaIn common: intracranial EEG (iEEG / ECoG / SEEG), 8 references
- [5] doi:10.3389/fncom.2026.1844662
- Quantifying cortex-wide traveling brain waves of complex patterns with a graph-based algorithm.Journal: Frontiers in computational neuroscienceIn common: intracranial EEG (iEEG / ECoG / SEEG), 8 references
- [6] doi:10.7554/elife.108208 [code]
- Realistic coupling enables flexible macroscopic traveling waves in the mouse cortex.Journal: eLifeIn common: NumPy, 7 references
- [7] doi:10.1016/j.isci.2026.116728 [code]
- Awake cortex stabilizes traveling waves for global and reliable information routing.Journal: iScienceIn common: SciPy, NumPy, 4 references
- [8] doi:10.1038/s41467-026-72931-6 [code]
- Three parsimonious spatiotemporal patterns in cerebellum reveal individual traits in function and behavior.Journal: Nature communicationsIn common: SciPy, Matplotlib, NumPy, 3 references
- [9] doi:10.1038/s41467-026-76011-7 [code]
- Human cortex organizes dynamic co-fluctuations along the sensorimotor-association
axis. Journal: Nature communicationsIn common: SciPy, Matplotlib, NumPy, 3 references - [10] doi:10.1038/s42003-025-09444-3 [code]
- Decoupling of neurophysiological activity from structure mirrors global microarchitectural and neuromodulatory trends.Journal: Communications biologyIn common: SciPy, Matplotlib, NumPy, 3 references
Contribute
The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.
Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.
Claim this paper
Correct its record
Say what each link of this record is, remove the ones that are not the paper's, add the ones that are missing. The correction becomes a new version of the record, in its Versions section.
Validate its tracing map
You validate the map as this page shows it: 1 repository of the authors' code, each at its verified commit and with its license, 10 scripts, and 7 matches between paragraphs and code (see the Code and Map sections). It then receives a DOI on Zenodo, with you (your ORCID iD) and OSCR as its creators; the code itself is not deposited.
The map's fingerprint: sha256:197cd6a5ca5ec698…
Add the badge to its README
The badge links the code to this page. Copy one of these into the README of the paper's code: only you decide where it goes, and nothing is changed for you.
Markdown
[, paste the snippet at the top, then “Commit changes…” and, to review it first, “Create a new branch and start a pull request”. You open the pull request; OSCR asks for no permission.
Request its removal
To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).
Discussion, reproductions, activity
Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.
Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.
Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.
