OSCR

The dominance of large-scale phase dynamics in human cortex, from delta to gamma.

Code ↔ Paper

7 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 7 matches
  1. [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. [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. [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. [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. [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. [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. [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

  1. # %%
  2. import numpy as np
  3. #import cupy as cp #uncomment this if cuda gpu available and cupy installed
  4. from matplotlib.pyplot import *
  5. import matplotlib.ticker as ticker
  6. from scipy.optimize import curve_fit
  7. from pickle import dump,load,HIGHEST_PROTOCOL
  8. from os.path import exists
  9. import warnings
  10. from glob import glob
  11. from IPython.display import clear_output
  12. # %%
  13. ###select one###
  14. ###sEEG inter-contact distances have been computed using geodesic distances on surface models of cortex###
  15. measurement = 'sEEG' #example subjects
  16. ###MEG inter-contact distances have been computed from a spheroid approximation of the sensory array###
  17. #measurement = 'MEG' #example subjects
  18. # %%
  19. #the data-files used in these examples can be downloaded from https://osf.io/48ztm/files/osfstorage#
  20. if measurement=='sEEG':
  21. file_path = 'E:\\sensor_coordinates_distance_data_files\\' #path to where you put the input files listed below
  22. output_path = 'E:\\output\\' #path where you want the output files to go (keep short, output files have long names!)
  23. subjects = ['subject15','subject22'] #example subjects
  24. geodesic_distance_files = ['subject15_depth_geodesic_distances','subject22_depth_geodesic_distances'] #file-name without '.pkl' affix
  25. triangle_files = ['subject15_depth_equilateral_triplets','subject22_depth_equilateral_triplets'] #file-name without '.pkl' affix
  26. coordinate_files = ['subject15_depth_coordinatesAP-IS-LR','subject22_depth_coordinatesAP-IS-LR'] #file-name without '.csv' affix
  27. TS_files = ['subject15_depth_timeseries','subject22_depth_timeseries'] #file-name without '.npy' affix
  28. tdel_values = [1.0,1.0] # one over the sampling frequency, in ms.
  29. frequencies = [1.0, 1.148698354997035, 1.3195079107728942, 1.5157165665103982, 1.7411011265922482,
  30. 2.0, 2.29739670999407, 2.639015821545789, 3.0314331330207964, 3.4822022531844965,
  31. 4.0, 4.59479341998814, 5.278031643091579, 6.062866266041593, 6.964404506368995,
  32. 8.0, 9.18958683997628, 10.556063286183157, 12.125732532083186, 13.92880901273799,
  33. 16.0, 18.37917367995256, 21.112126572366314, 24.25146506416638, 27.857618025475986,
  34. 32.0, 36.75834735990512, 42.22425314473263, 48.50293012833276, 55.71523605095197,
  35. 64.0, 73.51669471981025, 84.44850628946526, 97.00586025666551] #temporal frequencies to analyze
  36. min_of_range = 1.0/50.0 #m, for plotting
  37. elif measurement=='MEG':
  38. file_path = 'E:\\sensor_coordinates_distance_data_files\\' #path to where you put the input files listed below
  39. output_path = 'E:\\output\\' #path where you want the output files to go (keep short, output files have long names!)
  40. subjects = ['subjectG0','subjectP9'] #example subjects
  41. geodesic_distance_files = ['subjectG0_MEG_spheroid_distances','subjectP9_MEG_spheroid_distances'] #file-name without '.pkl' affix
  42. triangle_files = ['subjectG0_MEG_equilateral_triplets','subjectP9_MEG_equilateral_triplets'] #file-name without '.pkl' affix
  43. coordinate_files = ['subjectG0_MEG_coordinatesAP-IS-LR','subjectP9_MEG_coordinatesAP-IS-LR'] #file-name without '.csv' affix
  44. TS_files = ['subjectG0_MEG_timeseries','subjectP9_MEG_timeseries'] #file-name without '.npy' affix
  45. tdel_values = [3.2,3.2] # one over the sampling frequency, in ms.
  46. frequencies = [1.0, 1.148698354997035, 1.3195079107728942, 1.5157165665103982, 1.7411011265922482,
  47. 2.0, 2.29739670999407, 2.639015821545789, 3.0314331330207964, 3.4822022531844965,
  48. 4.0, 4.59479341998814, 5.278031643091579, 6.062866266041593, 6.964404506368995,
  49. 8.0, 9.18958683997628, 10.556063286183157, 12.125732532083186, 13.92880901273799,
  50. 16.0, 18.37917367995256, 21.112126572366314, 24.25146506416638, 27.857618025475986,
  51. 32.0] #temporal frequencies to analyze
  52. min_of_range = 1.0/20.0 #m, for plotting
  53. print(subjects)
  54. # %%
  55. #more parameters
  56. aggregate_subjects = {} #for storing the spectra and other used parametres for later analysis
  57. verbose = True #used as a global variable, set to False once everything is working ok
  58. distance_units = 1000.0 #conversion of given sensor units of coordinates to metres; here mm->m
  59. minf = min(frequencies) #minimum frequency, used to window the Morlet wavelet inputs
  60. nFrequencies = len(frequencies) #number of frequencies
  61. include_normed_power = False #if False will compute the SF spectrum of the phase only
  62. forward_waves = True #separate standing wave components into pure traveling wave components
  63. nBases = 14 #number of bases to used in SVD
  64. N_cycles = 2 #number of cycles is the Morlet wavelet to estimate time-series of phase
  65. 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
  66. smallest_triangle_size = 0.01 #units of linear m; used for binning the triangles
  67. largest_triangle_size = 0.32 #units of linear m; used for binning the triangles
  68. N_histogram_bins = 32 #for plotting
  69. if verbose==False: warnings.filterwarnings("ignore")
  70. # %%
  71. #main functions
  72. 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
  73. print('Doing spatial SVD')
  74. phi_Cs = phi_cts.reshape(-1,phi_cts.shape[-1])
  75. if use_bases is None: #generate new bases from phi
  76. u_sB,s_B,vh_BC= np.linalg.svd(phi_Cs.T,full_matrices=False) #b<B
  77. s_b = s_B[:nBases] #diagonals of sigma
  78. print('Percentage root variance',100.0*s_b/s_B.sum())
  79. bases_sb = u_sB[:,:nBases] #left singular vectors
  80. print('Percentage variance',100.0*(s_b)**2/((s_B)**2).sum())
  81. bases_sb = u_sB[:,:nBases] #left singular vectors
  82. else: bases_sb = use_bases #otherwise, use_bases is a previously generated basis set
  83. if bases_only==False: #calculate fits, right singular vectors, and reduced rank model
  84. if use_bases is None: betas_bC = (np.diag(s_b)@vh_BC[:nBases,:]) #weighted right singular vectors
  85. else: betas_bC = (phi_Cs@bases_sb.conj()).T
  86. betas_ctb = betas_bC.T.reshape(phi_cts.shape[0],-1,bases_sb.shape[1])
  87. if unit_phase_out:
  88. model_Cs = np.exp(1j*np.angle(bases_sb@betas_bC).T) #reduced rank model with unit length complex phases
  89. model_cts = model_Cs.reshape(phi_cts.shape)
  90. fit_C = (phi_Cs/model_Cs).mean(-1).real #for good fits, phi/model will point along the real axis
  91. fit_ct = fit_C.reshape(phi_cts.shape[0],phi_cts.shape[1])
  92. if use_bases is None: return bases_sb,s_b,fit_ct,betas_ctb,model_cts #number of outputs change depending on input flags
  93. else: return bases_sb,fit_ct,betas_ctb,model_cts #number of outputs change depending on input flags
  94. else:
  95. model_Cs = (bases_sb@betas_bC).T #reduced rank model
  96. model_cts = model_Cs.reshape(phi_cts.shape)
  97. if use_bases is None: return bases_sb,s_b,betas_ctb,model_cts #number of outputs change depending on input flags
  98. else: return bases_sb,betas_ctb,model_cts #number of outputs change depending on input flags
  99. else: return bases_sb #number of outputs change depending on input flags
  100. def make_SF_power(dmetres_SS,binned_triplets_av,normalization_a,sigmas_b,Bases_sb):
  101. #estimate wavelength and power for empirical data
  102. bases_avb = Bases_sb[binned_triplets_av,:]
  103. if verbose: print('bases_avb shape',bases_avb.shape)
  104. r_avb = np.absolute(bases_avb) #magnitude of contribution by each vertex (over triangles, bases)
  105. if verbose: print('r_avb min',r_avb.min(),'r_avb max',r_avb.max())
  106. 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
  107. if verbose: print('estpower_ba shape',estpower_ba.shape)
  108. dphidx_ba,dphi_ba = dphi_dx_from_triangles(bases_avb.transpose(2,0,1),dmetres_SS,binned_triplets_av)
  109. if verbose: print('dphidx_ba shape',dphidx_ba.shape)
  110. distance_per_radian_ba = 1.0/dphidx_ba
  111. metres_per_cycle_ba = 2.0*np.pi*distance_per_radian_ba #dmetres_SS already in metres
  112. if verbose: print('metres_per_cycle_ba shape',metres_per_cycle_ba.shape)
  113. return estpower_ba,metres_per_cycle_ba,dphidx_ba,dphi_ba
  114. def dphi_dx_from_triangles(phi_cav,dmetres_SS,triplets_av): #compute the rate of change of phase across triangles
  115. a = dmetres_SS[triplets_av[:,0],triplets_av[:,1]]
  116. b = dmetres_SS[triplets_av[:,1],triplets_av[:,2]]
  117. c = dmetres_SS[triplets_av[:,2],triplets_av[:,0]]
  118. ABC_v_xyz = coordinates_of_triangle_given_lengths(a,b,c) #nvd
  119. ABC_vd = ABC_v_xyz[:,:,:2] #don't need zero Z coordinate
  120. _ABC_vd = ABC_vd / np.linalg.norm(ABC_vd,axis=-2)[:,np.newaxis,:] #normalized area to one
  121. relative_phase = np.angle(phi_cav[:,:,0:1]/phi_cav[:,:,0:3]) #relative to first index; units of radians
  122. dphi_dx = []
  123. dphi = []
  124. for c in range(phi_cav.shape[0]):
  125. dphi_dx_c = []
  126. dphi_c = []
  127. for n in range(phi_cav.shape[1]):
  128. y = relative_phase[c,n,:,np.newaxis] #vo
  129. X = ABC_vd[n,:,:] #vd
  130. X1 = np.concatenate((X,np.ones((X.shape[0],1),float)),axis=1)
  131. b = np.linalg.inv(X1.T@X1)@(X1.T@y) #dv,vd->dd; dv,vo->d0: dd,d0->d0
  132. dphi_dx_c += [np.linalg.norm(b[:,0],axis=0)]
  133. _X = _ABC_vd[n,:,:] #vd
  134. _X1 = np.concatenate((_X,np.ones((_X.shape[0],1),float)),axis=1)
  135. _b = np.linalg.inv(_X1.T@_X1)@(_X1.T@y) #dv,vd->dd; dv,vo->d0: dd,d0->d0
  136. dphi_c += [np.linalg.norm(_b[:,0],axis=0)]
  137. dphi_dx += [np.array(dphi_dx_c)]
  138. dphi += [np.array(dphi_c)]
  139. dphi_dx = np.array(dphi_dx)
  140. dphi = np.array(dphi)
  141. return dphi_dx,dphi
  142. def wavelet(data_cts,N_cycles,frequency,tdel,minf,use_cuda=False,PHname=None): #compute the phase and power using Morlet wavelets
  143. #read in the files if they arlready exist
  144. if PHname is not None:
  145. LPname = PHname.replace('PH_','LP_')
  146. print('Looking for',PHname)
  147. print('Looking for',LPname)
  148. if PHname is not None and exists(PHname+'.npy') and exists(LPname+'.npy'):
  149. print('Reading in phase file')
  150. phase_CTS = np.load(PHname+'.npy')
  151. if verbose: print('phase_CTS min max',phase_CTS.min(),phase_CTS.max(),'phase_CTS shape',phase_CTS.shape)
  152. print('Reading in power file')
  153. logpower_CTS = np.load(LPname+'.npy')
  154. if verbose: print('logpower_CTS min max',logpower_CTS.min(),logpower_CTS.max(),'logpower_CTS shape',logpower_CTS.shape)
  155. 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
  156. else:
  157. #data_cts is the time series data in shape cases (trials, epochs), times (samples), sensors (contacts, electrodes)
  158. #N_cycles is the number of cycles in the Morlet wavelet; typically 2 in Alexander et al.
  159. #frequency is the centre frequency, typically chosen from oversampled, logarithmically spaced frequncies
  160. #tdel is the time interval between samples e.g. 1ms
  161. #minf is the minimum frequency used in the analysis, need to ensure the final phase/power estimates all have the same number of samples
  162. print('Estimating short time-series Fourier components for temporal frequency %.2fHz'%frequency)
  163. nSamples = data_cts.shape[1]
  164. nSensors = data_cts.shape[2]
  165. N_cycles_of_samples = int(N_cycles*1000.0/(frequency*tdel))
  166. #the extra samples of data required at both start and beginning of estimated phase/power values
  167. PH_unpad_at_minf = int(N_cycles*500.0/(minf*tdel))
  168. #the number of samples of phase/power that will be generated
  169. PHSamples = nSamples - 2*PH_unpad_at_minf
  170. #half the wavelet cycles at lowest frequency minus half the wavelet cycles at this frequency
  171. PH_pad_at_f = PH_unpad_at_minf - int(N_cycles*500.0/(frequency*tdel))
  172. W = gausian_window(N_cycles_of_samples) #size of Morlet window
  173. f_indexes = []
  174. #how many sets of indexes to make? one for each sample in phase
  175. for t in range(PHSamples):
  176. #make a list of all data samples that are used in calculating each phase estimate
  177. padded_range = range(t+PH_pad_at_f,t+PH_pad_at_f+N_cycles_of_samples)
  178. f_indexes += padded_range
  179. if use_cuda: #works with cupy if available https://cupy.dev
  180. f_indexes = cp.asarray(f_indexes)
  181. power_cTs = cp.zeros((data_cts.shape[0],PHSamples,nSensors),float)
  182. phi_cTs = cp.zeros((data_cts.shape[0],PHSamples,nSensors),complex)
  183. W = cp.asarray(W)
  184. for c,data_ts in enumerate(data_cts):
  185. print('#',end='')
  186. data_ts = cp.asarray(data_ts)
  187. for s in range(nSensors):
  188. data_tw = data_ts[f_indexes,s].reshape(PHSamples,N_cycles_of_samples) #tw
  189. data_tw = data_tw - data_tw.mean(1)[:,cp.newaxis] #mean of time-series to zero
  190. t_f = 2.0*cp.pi*N_cycles*cp.arange(N_cycles_of_samples)/N_cycles_of_samples
  191. t_phi = cp.exp(1j*(t_f)) * W
  192. fourier_components = (data_tw * t_phi[cp.newaxis,:]).sum(1)
  193. phi_cTs[c,:,s] = cp.exp(1j*cp.angle(fourier_components)) #over w
  194. power_cTs[c,:,s] = cp.absolute(fourier_components)
  195. print()
  196. return cp.asnumpy(phi_cTs),cp.asnumpy(power_cTs)
  197. else:
  198. f_indexes = np.asarray(f_indexes)
  199. power_cTs = np.zeros((data_cts.shape[0],PHSamples,nSensors),float)
  200. phi_cTs = np.zeros((data_cts.shape[0],PHSamples,nSensors),complex)
  201. for c,data_ts in enumerate(data_cts):
  202. print('#',end='')
  203. for s in range(nSensors):
  204. data_tw = data_ts[f_indexes,s].reshape(PHSamples,N_cycles_of_samples) #tw
  205. data_tw = data_tw - data_tw.mean(1)[:,np.newaxis] #mean of time-series to zero
  206. t_f = 2.0*np.pi*N_cycles*np.arange(N_cycles_of_samples)/N_cycles_of_samples
  207. t_phi = np.exp(1j*(t_f)) * W
  208. fourier_components = (data_tw * t_phi[np.newaxis,:]).sum(1)
  209. phi_cTs[c,:,s] = np.exp(1j*np.angle(fourier_components)) #over w
  210. power_cTs[c,:,s] = np.absolute(fourier_components)
  211. print()
  212. if PHname is not None: #i.e. looked for save files but didn't find them
  213. print('Writing to',PHname+'.npy',end = ' ')
  214. np.save(PHname+'.npy',np.angle(phi_cTs)) #saved as the real angle
  215. print('Writing to',LPname+'.npy',end = ' ')
  216. np.save(LPname+'.npy',np.log10(power_cTs)) #save the log power
  217. return phi_cTs,power_cTs
  218. def gaus(x,a,x0,sigma): #define a Gaussian
  219. return a*np.exp(-(x-x0)**2/(2*sigma**2))
  220. def gausian_window(x): #make a Gaussian window
  221. if type(x) is int:
  222. n = x
  223. x = np.arange(n)
  224. else:
  225. n = len(x)
  226. cosine_window = 0.5*(1.0 - np.cos(2*np.pi*x/(n-1))) #approximate the window as a cosine first
  227. mean = sum(x*cosine_window)/n
  228. sigma = np.sqrt(sum(cosine_window*(x-mean)**2)/n)
  229. popt,pcov = curve_fit(gaus,x,cosine_window,p0=[1,mean,sigma]) #turn it into a Gaussian window
  230. G = gaus(x,*popt)
  231. return G
  232. # %%
  233. #helper functions
  234. def save_pkl(output_path,obj,name): #the format used for saving dictionaries, lists
  235. print('Saving',output_path+ name + '.pkl')
  236. with open(output_path+ name + '.pkl', 'wb') as f:
  237. dump(obj, f, HIGHEST_PROTOCOL)
  238. def load_pkl(output_path,name): #open previously saved dictionaries, lists
  239. print('Loading',output_path + name + '.pkl')
  240. with open(output_path + name + '.pkl', 'rb') as f:
  241. return load(f)
  242. def get_coordinates_and_distances(file_path,coordinate_file,distance_file,distance_units): # convenience function for grabbing sensor coordinates and cross-sensor distances
  243. print('Opening coordinate file',coordinate_file)
  244. 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
  245. coordinates_metres = coordinates_sd/distance_units
  246. dmetres_ss = load_pkl(file_path,distance_file)/distance_units #mm->m in this data
  247. print('dmetres_ss min',dmetres_ss[dmetres_ss>0.0].min(),'dmetres_ss max',dmetres_ss[dmetres_ss<0.5].max())
  248. if 'depth' in coordinate_file:
  249. #initialize distances between contacts
  250. dmetres_SS = np.linalg.norm(coordinates_metres[:,np.newaxis,:]-coordinates_metres[np.newaxis,:,:],axis=2)
  251. if verbose: print('dmetres_SS min',dmetres_SS.min(),'dmetres_SS max',dmetres_SS.max())
  252. 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())
  253. #grab geodesic distances
  254. for s in range(dmetres_SS.shape[0]):
  255. for ss in range(dmetres_SS.shape[1]):
  256. 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
  257. elif 'MEG' in coordinate_file:
  258. dmetres_SS = dmetres_ss
  259. return coordinates_metres,dmetres_SS
  260. def angles_of_triangle_from_lengths(dmetres_SS,triplets_av): #used to construct approximate equilateral triangles from inter-contact distances
  261. """a, b and c are lengths of the sides of a triangle"""
  262. a = dmetres_SS[triplets_av[:,0],triplets_av[:,1]]
  263. b = dmetres_SS[triplets_av[:,1],triplets_av[:,2]]
  264. c = dmetres_SS[triplets_av[:,2],triplets_av[:,0]]
  265. # Square of lengths a2, b2, c2
  266. a2 = a**2
  267. b2 = b**2
  268. c2 = c**2
  269. # From Cosine law
  270. alpha = np.arccos((b2 + c2 - a2) / (2 * b * c))
  271. beta = np.arccos((a2 + c2 - b2) / (2 * a * c))
  272. gamma = np.arccos((a2 + b2 - c2) / (2 * a * b))
  273. return alpha,beta,gamma
  274. def triangle_normal(triangles):
  275. # The cross product of two sides is a normal vector
  276. return np.cross(triangles[:,1] - triangles[:,0],
  277. triangles[:,2] - triangles[:,0], axis=1)
  278. def triangle_area(triangles):
  279. # The norm of the cross product of two sides is twice the area
  280. return np.linalg.norm(triangle_normal(triangles), axis=1) / 2
  281. 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
  282. nSensors = dmetres_SS.shape[0]
  283. if verbose: print('nSensors',nSensors)
  284. if verbose: print('dmetres_SS shape',dmetres_SS.shape)
  285. if verbose: print('dmetres_SS')
  286. if verbose: print(dmetres_SS)
  287. print('Making nonskinny triangles of contacts')
  288. unique_triplets = []
  289. for s in range(nSensors):
  290. print('.',end='')
  291. for ss in range(nSensors):
  292. for sss in range(nSensors):
  293. if s!=ss and ss!=sss and sss!=s and \
  294. dmetres_SS[s,ss]>0.0 and dmetres_SS[ss,sss]>0.0 and dmetres_SS[sss,s]>0.0 and \
  295. 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:
  296. u = set(np.array([s,ss,sss]))
  297. if u not in unique_triplets: unique_triplets += [u]
  298. print()
  299. for u,un in enumerate(unique_triplets): unique_triplets[u] = np.sort(np.array(list(un))) #set becomes array
  300. unique_triplets = np.array(unique_triplets)
  301. print(unique_triplets.shape)
  302. alpha_a,beta_a,gamma_a = angles_of_triangle_from_lengths(dmetres_SS,unique_triplets) #ss,av
  303. angles_va = np.array([alpha_a,beta_a,gamma_a])
  304. angles_min_a = angles_va.min(0)
  305. nonskinnyity_met = angles_min_a>nonskinny_threshold
  306. nonskinny_triplets = np.array(unique_triplets)[nonskinnyity_met]
  307. if verbose:
  308. print('unique_triplets shape',unique_triplets.shape)
  309. print('nonskinnyity_met shape',nonskinnyity_met.shape)
  310. print('nonskinny_triplets shape',nonskinny_triplets.shape)
  311. print('nonskinny_triplets')
  312. print(nonskinny_triplets)
  313. return nonskinny_triplets #the indexes of contacts in each triangle
  314. 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
  315. print('Binning triangles')
  316. try:
  317. #calculate areas of each triangle from geodesic distances
  318. nominal_min_SF = 500.0 # cycles per metre
  319. n_bins = int(largest_bin_size/smallest_bin_size) #this is silly way to get n=32
  320. #get the triangle edges from the geodesic distances and triplets
  321. ta = nonskinny_triplets
  322. dm = dmetres_SS
  323. dm_ta01 = dm[ta[:,0],ta[:,1]]
  324. dm_ta12 = dm[ta[:,1],ta[:,2]]
  325. dm_ta20 = dm[ta[:,2],ta[:,0]]
  326. nominal_triangle_coordinates_te = coordinates_of_triangle_given_lengths(dm_ta01,dm_ta12,dm_ta20) #te: triangle x edge
  327. triplet_areas = triangle_area(nominal_triangle_coordinates_te)
  328. root_areas = np.sqrt(triplet_areas) #linear size of each triangle
  329. order_root_areas = root_areas.argsort(0)
  330. ordered_root_areas = root_areas[order_root_areas]
  331. ordered_nonskinny_triplets = nonskinny_triplets[order_root_areas]
  332. #fill the bins
  333. nonskinny_triplets_linearSF_bins = [] #triangles assigned to each bin
  334. nonskinny_triplets_a = [] #flat list of triangles assigned, ordered by triangle linear area
  335. root_areas_linearSF_bins = [] #areas of triangles assigned to each bin
  336. root_areas_a = [] #flat list of areas of triangles assigned to each bin
  337. n_per_linearSF_bins = [] #number of triangles in each bin
  338. if ordered_root_areas.shape[0]==0: return None
  339. linearSF_bin_edges = [ordered_root_areas[0]] #first two edges, because n_bins+1 edges
  340. linearSF_ranges_bins = [] #min and max of each bin
  341. old_size_cutoff = 1.0/nominal_min_SF
  342. 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
  343. size_cutoff = 1.0/SF_cutoff
  344. size_cutoff_boolean = ((ordered_root_areas<size_cutoff) * (ordered_root_areas>=old_size_cutoff)).astype(bool)
  345. nThese = size_cutoff_boolean.sum(0)
  346. these_nonskinny_triplets = ordered_nonskinny_triplets[size_cutoff_boolean,:]
  347. these_root_areas = ordered_root_areas[size_cutoff_boolean]
  348. if verbose:
  349. print(old_size_cutoff,size_cutoff,nThese)
  350. if nThese>0: print(these_root_areas.min(0),these_root_areas.max(0))
  351. else: print('-','-')
  352. if nThese>0:
  353. nonskinny_triplets_linearSF_bins += [list(these_nonskinny_triplets)]
  354. nonskinny_triplets_a += list(these_nonskinny_triplets)
  355. root_areas_linearSF_bins += [list(these_root_areas)]
  356. root_areas_a += list(these_root_areas)
  357. linearSF_bin_edges += [size_cutoff]
  358. 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
  359. n_per_linearSF_bins += [these_nonskinny_triplets.shape[0]]
  360. else:
  361. nonskinny_triplets_linearSF_bins += [[np.array([-1,-1,-1])]]
  362. root_areas_linearSF_bins += [[-1]]
  363. linearSF_bin_edges += [-1]
  364. linearSF_ranges_bins += [[-1,-1]]
  365. n_per_linearSF_bins += [0]
  366. old_size_cutoff = 1*size_cutoff
  367. if len(root_areas_a)==0: return None
  368. root_areas_a = np.array(root_areas_a)
  369. print('root_areas shape',root_areas.shape,'ordered_root_areas shape',ordered_root_areas.shape,'root_areas_a shape',root_areas_a.shape)
  370. n_per_linearSF_bins = np.array(n_per_linearSF_bins)
  371. #some book-keeping
  372. nonzero_root_areas_linearSF_bins = []
  373. nonzero_n_per_linearSF_bins = []
  374. nonzero_nonskinny_triplets_linearSF_bins = []
  375. nonzero_linearSF_bin_edges = []
  376. nonzero_linearSF_ranges_bins = [linearSF_ranges_bins[0]]
  377. for B,n_bin in enumerate(n_per_linearSF_bins):
  378. if n_bin>0:
  379. nonzero_root_areas_linearSF_bins += [root_areas_linearSF_bins[B]]
  380. nonzero_n_per_linearSF_bins += [n_per_linearSF_bins[B]]
  381. nonzero_nonskinny_triplets_linearSF_bins += [nonskinny_triplets_linearSF_bins[B]]
  382. if len(nonzero_linearSF_bin_edges)==0: nonzero_linearSF_bin_edges += [linearSF_bin_edges[B]]
  383. nonzero_linearSF_bin_edges += [linearSF_bin_edges[B+1]] #n_bins + 1 values
  384. nonzero_linearSF_ranges_bins += [linearSF_ranges_bins[B]]
  385. nonzero_linearSF_bin_edges[-1] = ordered_root_areas[-1] #correct the last edges
  386. nonzero_linearSF_ranges_bins[-1][1] = ordered_root_areas[-1] #correct the second term of the last nonzero bin range
  387. nonzero_linearSF_bin_edges = np.array(nonzero_linearSF_bin_edges)
  388. nonzero_linearSF_ranges_bins = np.array(nonzero_linearSF_ranges_bins)
  389. nonzero_n_per_linearSF_bins = np.array(nonzero_n_per_linearSF_bins)
  390. range_a = [ordered_root_areas[0],ordered_root_areas[-1]]
  391. print('nonzero_linearSF_ranges_bins')
  392. print(nonzero_linearSF_ranges_bins)
  393. max_of_linearSF_bins = max(nonzero_n_per_linearSF_bins)
  394. normalization_linearSF_bins = -np.ones(n_per_linearSF_bins.shape,float)
  395. normalization_linearSF_a = [] #'linearSF' because its based on assumption of linearly increasing SF
  396. print('n_per_linearSF_bins sum',n_per_linearSF_bins.sum(),'nonzero_n_per_linearSF_bins sum',nonzero_n_per_linearSF_bins.sum())
  397. for B,bin_n in enumerate(n_per_linearSF_bins):
  398. if bin_n>0:
  399. normalization_linearSF_bins[B] = max_of_linearSF_bins/bin_n #will eventually multiply by normalization weight
  400. normalization_linearSF_a += bin_n*[normalization_linearSF_bins[B]] #grab copies equal to the number of triangles in bin
  401. normalization_linearSF_a = np.array(normalization_linearSF_a)
  402. print('normalization_linearSF_a shape',normalization_linearSF_a.shape)
  403. if verbose:
  404. print('linearSF_bin_edges')
  405. print(linearSF_bin_edges)
  406. print('linearSF_ranges_bins')
  407. print(linearSF_ranges_bins)
  408. print('n_per_linearSF_bins')
  409. print(n_per_linearSF_bins)
  410. print('normalization_linearSF_a')
  411. print(normalization_linearSF_a)
  412. print('nonzero_linearSF_bin_edges')
  413. print(nonzero_linearSF_bin_edges)
  414. print('nonzero_n_per_linearSF_bins')
  415. print(nonzero_n_per_linearSF_bins)
  416. 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)
  417. if return_binwise: return n_per_linearSF_bins,nonskinny_triplets_linearSF_bins,normalization_linearSF_bins,\
  418. linearSF_ranges_bins,root_areas_linearSF_bins,linearSF_bin_edges #first array is used to avoid empty bins
  419. else: return n_per_linearSF_bins,np.array(nonskinny_triplets_a),normalization_linearSF_a,range_a,root_areas_a,linearSF_bin_edges
  420. except:
  421. print('No binned triplets returned')
  422. return None
  423. def coordinates_of_triangle_given_lengths(a,b,c): #abstract triangle coordinates from geodesic distances
  424. """a, b and c are lengths of the sides of a triangle"""
  425. A = np.array([[0]*len(a),[0]*len(a),[0]*len(a)]) # coordinates of vertex A
  426. B = np.array([c,[0]*len(a),[0]*len(a)]) # coordinates of vertex B
  427. #if verbose: print(A.shape,B.shape)
  428. C_x = b * (b**2 + c**2 - a**2) / (2 * b * c)
  429. C_y = np.sqrt(b**2 - C_x**2) # square root
  430. C = np.array([C_x,C_y,[0]*len(a)]) # coordinates of vertex C
  431. return np.concatenate([[A],[B],[C]],axis=0).transpose(2,0,1) #tvd: triangle, vertex, cartesian dimension
  432. def decompose_rphi_into_forward_waves(rphi_cts,tdel,frequency): #removes the negative going component of the wave and estimates normalized velocity
  433. #tdel is 1/sampling_frequency in ms
  434. #frequency in Hertz
  435. #rphi_cts is complex valued phase, possibly with magnitude weighting
  436. print('Decompose spatial vectors of phase into pure traveling wave components')
  437. one_cycle_of_samples = int(1000.0/(tdel*frequency)) #only requires four samples over the cycle, so this int could be replace with 4
  438. rphi_Cs = rphi_cts.reshape(-1,rphi_cts.shape[2])
  439. vel_C = [] #normalized velocity, 0 is pure SW, 1 is pure TW
  440. TWf_C = [] #forward spatial component
  441. for C in range(rphi_Cs.shape[0]):
  442. rphi_Ts = np.exp(1j*np.linspace(-np.pi,np.pi,one_cycle_of_samples,endpoint=False))[:,np.newaxis] * rphi_Cs[C,:][np.newaxis,:]
  443. u_r,s_r,vt_r = np.linalg.svd(rphi_Ts.real)
  444. TWf_s = vt_r[0,:] + 1j*vt_r[1,:] #throw away the time dimension we added, only need the spatial dimension
  445. TWf_C += [TWf_s[np.newaxis]]
  446. vel_C += [np.array([s_r[1]/s_r[0]])[np.newaxis]]
  447. vel_ct = np.concatenate(vel_C,axis=0).reshape(rphi_cts.shape[0],rphi_cts.shape[1])
  448. TWf_cts = np.concatenate(TWf_C,axis=0).reshape(rphi_cts.shape[0],rphi_cts.shape[1],rphi_cts.shape[2])
  449. return vel_ct,TWf_cts
  450. # %%
  451. #plotting functions
  452. def sf_histogram(imaging_path,plot_name,output_file,list_of_estpower,list_of_wavelength,list_of_max_distances, #plot the histogram of SF
  453. max_of_range,min_of_range,N_histogram_bins,
  454. stacked=False,colourmap='RdYlBu_r',linewidth=1,ticklabelsize=10.0,axislabelsize=12.0,listwise_weights=None):
  455. print('Plotting',plot_name)
  456. if verbose: print('max_of_range',max_of_range,'min_of_range',min_of_range)
  457. if verbose: print('1.0/max_of_range',1.0/max_of_range,'1.0/min_of_range',1.0/min_of_range)
  458. cmap = cm.get_cmap(colourmap)
  459. colours = []
  460. for l in range(len(list_of_estpower)):
  461. colours += [cmap(l/len(list_of_estpower))] #avoid division by zero
  462. #colours = ['red','green','blue','magenta','cyan']
  463. fig1 = figure(figsize=(5,5),dpi=90)
  464. axs1 = []
  465. axs1 += [fig1.add_subplot(1,1,1)]
  466. axs1[-1].set_title(plot_name.replace('_',' '))
  467. axs1[-1].tick_params(axis='x',which = 'both',labelsize=ticklabelsize)
  468. axs1[-1].tick_params(axis='y',labelsize=ticklabelsize)
  469. axs1[-1].set_xscale('log')
  470. if 'sEEG' in plot_name:
  471. locs = np.append(np.arange(1.0,10.0,1.0),np.arange(10.0,100.0,10.0))
  472. locs = np.delete(locs,np.where(locs==9.0))
  473. locs = np.delete(locs,np.where(locs==40.0))
  474. locs = np.delete(locs,np.where(locs==60.0))
  475. locs = np.delete(locs,np.where(locs==70.0))
  476. locs = np.delete(locs,np.where(locs==80.0))
  477. locs = np.delete(locs,np.where(locs==90.0))
  478. elif 'MEG' in plot_name:
  479. locs = np.append(np.arange(1.0,10.0,1.0),np.arange(10.0,50.0,10.0))
  480. locs = np.delete(locs,np.where(locs==9.0))
  481. locs = np.delete(locs,np.where(locs==40.0))
  482. axs1[-1].xaxis.set_minor_locator(ticker.FixedLocator(locs))
  483. axs1[-1].xaxis.set_major_locator(ticker.NullLocator())
  484. axs1[-1].xaxis.set_minor_formatter(ticker.ScalarFormatter())
  485. axs1[-1].set_xlabel('Spatial frequency (cycles/metre)',fontsize=axislabelsize)
  486. axs1[-1].set_ylabel('Estimated power (A.U.)',fontsize=axislabelsize)
  487. if listwise_weights is None: listwise_weights = np.ones((len(list_of_estpower)),float)
  488. logscale_bins = np.power(10,np.linspace(np.log10(1.0/max_of_range),np.log10(1.0/min_of_range),N_histogram_bins))
  489. list_of_hist_power = []
  490. list_of_hist_sf = []
  491. if stacked:
  492. cumulative_hist = np.zeros((N_histogram_bins-1),float)
  493. for l,estpower,wavelength,case_weight in zip(range(len(list_of_estpower)),list_of_estpower,list_of_wavelength,listwise_weights):
  494. wavelength = np.array(wavelength)
  495. estpower = np.array(estpower)
  496. sf = 1.0/wavelength
  497. power_hist = np.histogram(sf,bins=logscale_bins,weights=estpower)
  498. 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
  499. cumulative_hist += w_power_hist
  500. axs1[-1].plot((power_hist[1][:-1]+power_hist[1][1:])/2.0,cumulative_hist,c=colours[l],linewidth=linewidth)
  501. for md in list_of_max_distances:
  502. axs1[-1].plot(1.0/md,0.0,'.',color='k')
  503. list_of_hist_power += [w_power_hist]
  504. list_of_hist_sf += [(power_hist[1][:-1]+power_hist[1][1:])/2.0]
  505. else:
  506. for l,estpower,wavelength,case_weight in zip(range(len(list_of_estpower)),list_of_estpower,list_of_wavelength,listwise_weights):
  507. wavelength = np.array(wavelength)
  508. estpower = np.array(estpower)
  509. sf = 1.0/wavelength
  510. power_hist = np.histogram(sf,bins=logscale_bins,weights=estpower)
  511. w_power_hist = case_weight * power_hist[0]
  512. axs1[-1].plot((power_hist[1][:-1]+power_hist[1][1:])/2.0,w_power_hist,c=colours[l],linewidth=linewidth)
  513. for md in list_of_max_distances:
  514. axs1[-1].plot(1.0/md,0.0,'.',color='k')
  515. list_of_hist_power += [w_power_hist]
  516. list_of_hist_sf += [(power_hist[1][:-1]+power_hist[1][1:])/2.0]
  517. display(fig1)
  518. fig1.savefig((imaging_path+plot_name+'_'+output_file).replace(' ','_').replace('c/m','cperm')+'.jpg',facecolor='white',transparent=False,dpi=600)
  519. fig1.savefig((imaging_path+plot_name+'_'+output_file).replace(' ','_').replace('c/m','cperm')+'.svg',facecolor='white',transparent=False,dpi=600)
  520. fig1.clf()
  521. close(fig1)
  522. del fig1
  523. return list_of_hist_power,list_of_hist_sf
  524. 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
  525. print('Plotting bin counts')
  526. fig = figure(figsize=(5,2.5))
  527. ax = fig.add_subplot(1,1,1)
  528. if SFscale:
  529. ax.plot(1.0/bin_edges[1:],bin_counts,'4',color='g')
  530. ax.plot(1.0/bin_edges[:-1],bin_counts,'3',color='r')
  531. ax.set_xlabel('Bin edges (1/m)')
  532. ax.set_ylabel('Triangle count')
  533. else:
  534. ax.plot(bin_edges[1:],bin_counts,'3',color='g')
  535. ax.plot(bin_edges[:-1],bin_counts,'4',color='r')
  536. ax.set_xlabel('Bin edges (m)')
  537. ax.set_ylabel('Triangle count')
  538. xlims = ax.get_xlim()
  539. ax.set_xlim(xlims[0],1.2/bin_edges[1])
  540. display(fig)
  541. fig.clf()
  542. close(fig)
  543. del fig
  544. print('Plotting binwise normalization')
  545. fig = figure(figsize=(5,2.5))
  546. ax = fig.add_subplot(1,1,1)
  547. if SFscale:
  548. ax.plot(1.0/used_root_areas,1.0/normalization_a,'.',color='k',markersize=0.2) #normalization_a is divisor in SF power calculation
  549. ax.set_xlabel('Triangle SF')
  550. ax.set_ylabel('1.0 / normalization factor')
  551. else:
  552. ax.plot(used_root_areas,1.0/normalization_a,'.',color='k',markersize=0.2) #normalization_a is divisor in SF power calculation
  553. ax.set_xlabel('Triangle size')
  554. ax.set_ylabel('1.0 / normalization factor')
  555. xlims = ax.get_xlim()
  556. ax.set_xlim(xlims[0],1.2/bin_edges[1])
  557. display(fig)
  558. fig.clf()
  559. close(fig)
  560. del fig
  561. def phase_plot(bases_sb,frequency,contact_xyz,vector_name='wave map',mag=False,figout=None): #assumes complex valued bases
  562. print('Plotting phases')
  563. nBases = bases_sb.shape[1]
  564. cols = 3
  565. rows = (nBases//3)+1
  566. fig = figure(figsize=(cols*8,rows*8),dpi=200)
  567. axs = []
  568. if mag: alpha = np.absolute(bases_sb)/np.absolute(bases_sb).max()
  569. else: alpha = np.ones(bases_sb.shape,float)
  570. for b in range(nBases):
  571. axs += [fig.add_subplot(rows,cols,b+1,projection='3d')]
  572. axs[b].view_init(elev=90,azim=-90)
  573. axs[b].set_title('Frequency %.1fHz %s %d'%(frequency,vector_name,b+1))
  574. for s,xyz in enumerate(contact_xyz):
  575. color = cm.hsv(np.angle(bases_sb[s,b]) / (2.0*np.pi) + 0.5) #put angles on range 0 to 1
  576. axs[b].plot(xyz[0],xyz[1],xyz[2],'.',markersize=20.0,markerfacecolor=color,markeredgecolor=None,alpha=alpha[s,b])
  577. axs[b].plot(xyz[0],xyz[1],xyz[2],'.',markersize=20.0,markeredgecolor=color,fillstyle='none',alpha=1.0)
  578. axs[b].set_aspect('equal')
  579. axs[b].set_xlabel('Anterior-Posterior')
  580. axs[b].set_ylabel('Inferior-Superior')
  581. fig.subplots_adjust(wspace=0.0, hspace=0.0)
  582. if figout is not None:
  583. fig.savefig(figout+'.png',facecolor='white',transparent=False)
  584. fig.savefig(figout+'.svg',facecolor='white',transparent=False)
  585. else:
  586. rect = fig.patch
  587. rect.set_facecolor('white')
  588. display(fig)
  589. fig.clf()
  590. close(fig)
  591. del fig
  592. 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
  593. print('Plotting triangles over size bins')
  594. nBinPlots = bin_counts.shape[0]
  595. cols = 3
  596. rows = (nBinPlots//3)+1
  597. fig = figure(figsize=(cols*10,rows*10),dpi=150)
  598. axs = []
  599. cumulative_bin_count = 0
  600. for br in range(nBinPlots):
  601. axs += [fig.add_subplot(rows,cols,br+1,projection='3d')]
  602. axs[br].view_init(elev=90,azim=-90)
  603. axs[br].plot(contact_xyz[:,0],contact_xyz[:,1],contact_xyz[:,2],'.',color='k')
  604. if bin_counts[br]>0:
  605. triplets = binned_triplets[cumulative_bin_count:cumulative_bin_count+bin_counts[br]]
  606. mean_root_area = root_areas[cumulative_bin_count:cumulative_bin_count+bin_counts[br]].mean(0)
  607. axs[br].set_title('Size bin %d, mean size %.3f'%(br+1,mean_root_area))
  608. for t,trip in enumerate(triplets):
  609. if triplets.shape[0]>1: colour = cm.cool(t/(triplets.shape[0])) #0 to 1
  610. else: colour = cm.cool(0.0)
  611. T = np.concatenate((trip,trip[0:1]),axis=0) #close the triangle
  612. 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
  613. 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)
  614. 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)
  615. axs[br].set_aspect('equal')
  616. xticks = axs[br].get_xticks()
  617. axs[br].set_xticks(xticks[::2])
  618. zticks = axs[br].get_zticks()
  619. axs[br].set_zticks(zticks[::4])
  620. cumulative_bin_count += bin_counts[br]
  621. set_axes_equal(axs[br]) # aspect ratio is 1:1:1 in data space
  622. tight_layout()
  623. fig.savefig((file_path+title).replace('c/m','cperm')+'.png')
  624. fig.savefig((file_path+title).replace('c/m','cperm')+'.svg')
  625. display(fig)
  626. fig.clf()
  627. close(fig)
  628. del fig
  629. def set_axes_equal(ax): #to draw veridical relative distances in plots
  630. """
  631. Make axes of 3D plot have equal scale so that spheres appear as spheres,
  632. cubes as cubes, etc.
  633. Input
  634. ax: a matplotlib axis, e.g., as output from plt.gca().
  635. """
  636. x_limits = ax.get_xlim3d()
  637. y_limits = ax.get_ylim3d()
  638. z_limits = ax.get_zlim3d()
  639. x_range = abs(x_limits[1] - x_limits[0])
  640. x_middle = np.mean(x_limits)
  641. y_range = abs(y_limits[1] - y_limits[0])
  642. y_middle = np.mean(y_limits)
  643. z_range = abs(z_limits[1] - z_limits[0])
  644. z_middle = np.mean(z_limits)
  645. # The plot bounding box is a sphere in the sense of the infinity
  646. # norm, hence I call half the max range the plot radius.
  647. plot_radius = 0.5*max([x_range, y_range, z_range])
  648. ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius])
  649. ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius])
  650. ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius])
  651. # %%
  652. # main script
  653. binning_success = 0 #keep track of whether triangles were successfuly constructed and binned
  654. for j,subject in enumerate(subjects): #loop through the participant's files
  655. tdel = tdel_values[j]
  656. print('Doing subject',subject)
  657. #MEG, sEEG files; made using other resources not included in this code base
  658. coordinate_file = coordinate_files[j]
  659. distance_file = geodesic_distance_files[j]
  660. timeseries_file = TS_files[j]
  661. triangle_file = triangle_files[j]
  662. print(triangle_file)
  663. metres_sd,dmetres_SS = get_coordinates_and_distances(file_path,coordinate_file,distance_file,distance_units)
  664. #save relevant paramenters with subject for later analysis
  665. aggregate_subjects[subject] = {}
  666. aggregate_subjects[subject]['tdel'] = tdel
  667. aggregate_subjects[subject]['metres_sd'] = metres_sd
  668. aggregate_subjects[subject]['dmetres_SS'] = dmetres_SS
  669. nSensors = metres_sd.shape[0]
  670. #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)
  671. 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)
  672. else:
  673. nonskinny_triplets = collate_nonskinny_triangles(dmetres_SS,nonskinny_threshold=nonskinny_threshold)
  674. save_pkl(file_path,nonskinny_triplets,triangle_file+'_nonskinny_threshold_%.4f'%nonskinny_threshold)
  675. if verbose: print('nonskinny_triplets shape',nonskinny_triplets.shape)
  676. #put the triangles into bins of the approximately the same linear sizes
  677. binning_out = bin_triplets_linearSF(dmetres_SS,nonskinny_triplets,return_binwise=False,smallest_bin_size=smallest_triangle_size,largest_bin_size=largest_triangle_size)
  678. 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
  679. #housekeeping from the binning
  680. binning_success += 1
  681. 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)
  682. print('analysis_info_string',analysis_info_string)
  683. n_per_linearSF_bins,binned_triplets_av,normalization_a,range_a,root_areas_a,linearSF_bin_edges = binning_out
  684. aggregate_subjects[subject]['binned_triplets_av'] = binned_triplets_av
  685. aggregate_subjects[subject]['normalization_linearSF_a'] = normalization_a #for naming consistency
  686. aggregate_subjects[subject]['range_a'] = range_a
  687. aggregate_subjects[subject]['root_areas_a'] = root_areas_a
  688. aggregate_subjects[subject]['linearSF_bin_edges'] = linearSF_bin_edges
  689. bin_counts = n_per_linearSF_bins[n_per_linearSF_bins>0]
  690. do_bins_string = ''
  691. min_triangle = range_a[0]
  692. max_triangle = range_a[-1]
  693. print('n_per_linearSF_bins',n_per_linearSF_bins)
  694. print(j,binning_success,subject)
  695. if verbose:
  696. 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)
  697. draw_triangles(metres_sd,binned_triplets_av,bin_counts,output_path,plot_title,root_areas_a)
  698. else: continue #try the next subject
  699. 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
  700. max_of_range = 2*max_triangle #used for plotting
  701. aggregate_subjects[subject]['max_distance'] = max_distance
  702. aggregate_subjects[subject]['max_of_range'] = max_of_range
  703. aggregate_subjects[subject]['min_of_range'] = min_of_range
  704. timeseries_cts = np.load(file_path+timeseries_file+'.npy')
  705. if verbose: print('timeseries_cts shape',timeseries_cts.shape)
  706. for frequency in frequencies:
  707. aggregate_subjects[subject][frequency] = {}
  708. list_of_estpower = []
  709. list_of_wavelength = []
  710. #estimate phase
  711. data_label = 'empirical'
  712. print('Empirical bases from SVD')
  713. PHname = "%sPH_%.4fHz_%s_%s_%.4fminf" %(file_path,frequency,subject,measurement,minf)
  714. phi_cts,pow_cts = wavelet(timeseries_cts,N_cycles,frequency,tdel,minf,PHname=PHname)
  715. if include_normed_power:
  716. data_label = 'npower'
  717. pow___s = pow_cts.reshape(-1,pow_cts.shape[2]).mean(0) #normalize power per each grey-matter contact
  718. pow_cts = pow_cts / pow___s[np.newaxis,np.newaxis,:]
  719. phi_cts = pow_cts * phi_cts #now weighted by normalized power
  720. else: data_label = 'phase'
  721. del pow_cts
  722. # and make left singular vectors
  723. 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)
  724. if verbose:
  725. print('bases_sb shape',bases_sb.shape)
  726. print('absolute(bases_sb) min',np.absolute(bases_sb).min(),'absolute(bases_sb) max',np.absolute(bases_sb).max())
  727. phase_plot(bases_sb,frequency,metres_sd,mag=True)
  728. # remove the negative wave components
  729. if forward_waves:
  730. vel_ct,TWf_cts = decompose_rphi_into_forward_waves(bases_sb.T[:,np.newaxis,:],tdel,frequency)
  731. if verbose: print('TWf_cts shape',TWf_cts.shape)
  732. print('Normalized velocity of spatial vector of phase (SW=0,TW=1)')
  733. print(vel_ct.T)
  734. Bases_sb = TWf_cts[:,0,:].T
  735. if verbose: print('absolute(Bases_sb) min',np.absolute(Bases_sb).min(),'absolute(Bases_sb) max',np.absolute(Bases_sb).max())
  736. print('Empirical traveling wave components')
  737. if verbose: phase_plot(Bases_sb,frequency,metres_sd,mag=True)
  738. else: Bases_sb = bases_sb
  739. aggregate_subjects[subject][frequency]['Bases_sb'] = Bases_sb
  740. #estimate the SF
  741. estpower_ba,metres_per_cycle_ba,dphidx_ba,dphi_ba = make_SF_power(dmetres_SS,binned_triplets_av,normalization_a,sigmas_b,Bases_sb)
  742. list_of_estpower += [estpower_ba.reshape(-1)]
  743. list_of_wavelength += [metres_per_cycle_ba.reshape(-1)]
  744. plot_name = '%s %s SF %s TF %.2fHz'%(subject,measurement,data_label,frequency)
  745. aggregate_subjects[subject][frequency]['list_of_estpower'] = list_of_estpower
  746. aggregate_subjects[subject][frequency]['list_of_wavelength'] = list_of_wavelength
  747. save_pkl(output_path,aggregate_subjects[subject],(plot_name+'_'+analysis_info_string).replace(' ','_'))
  748. sf_histogram(output_path,plot_name,analysis_info_string,list_of_estpower,list_of_wavelength,\
  749. [max_triangle],max_of_range,min_of_range,N_histogram_bins,colourmap='cool')
  750. del aggregate_subjects[subject][frequency]
  751. clear_output(wait=True) #.pynb file can get very large, especially with verbose==True
  752. print('binning success',binning_success)
  753. # %%

estimation_of_SF_of_phase_of_cortical_activity_on_irregular_measurement_arrays.ipynb at commit 42c7fe1, under GPL-3.0 · at the source

Overview

  1. Université Paris Cité, CNRS, Integrative Neuroscience and Cognition Center Paris France
  2. GHU-Paris Psychiatrie et Neurosciences, Hôpital Sainte-Anne Paris France
  3. Institut Universitaire de France (IUF) Paris France
Journal: eLife, volume 13, article RP100674
Dates: published online 9 April 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.7554/elife.100674 · PMID 41954599 · PMCID PMC13065328 · OpenAlex W4403758897
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: EEG (modality), intracranial EEG (iEEG / ECoG / SEEG) (modality), human (organism)
Methods: Preprocessing, Connectivity, Statistics, Spectral & time-frequency, Evoked potentials, Machine learning, fMRI & imaging
Keywords: cortex, traveling waves, sEEG, spatial frequency, temporal frequency, large-scale, Human
MeSH: Cerebral Cortex*, Delta Rhythm*, Gamma Rhythm*, Electroencephalography, Gray Matter, Humans (* major topic)
Journal subjects: Neuroscience
Topic: Neural dynamics and brain function (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: European Research Council (European Union's Horizon 2020 research and innovation programme (grant No 852139 - Laura Dugué))
Citations: cited by 2 papers (Europe PMC); 103 references in the paper

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

License: GPL-3.0
State: the link answers, verified on 29 September 2026
Evidence: files inventoried
Commit: 42c7fe112c99a06e05aaf31939a0eb3e218e0fcc, 6 January 2026
Languages: Python (5), Jupyter (5)
Size: 15 files, 10 scripts
Software Heritage: not archived
Found in: “Data availability”
Holds: README, license file, 5 notebooks
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (10 files), Matplotlib (5 files), SciPy (2 files)
Availability: 1 check, the latest on 29 September 2026: the link answers
  • 29 September 2026: the link answers
12 files

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://github.com/DugueLab/Traveling-wave-analysis/blob/main/estimation_of_SF_of_phase_of_cortical_activity_on_irregular_measurement_arrays.ipynb (copy archived at Alexander, 2026).

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://doi.org/10.7554/elife.100674

BibTeX

@article{alexander2026dominance,
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/elife.100674},
url = {https://doi.org/10.7554/elife.100674},
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/04/09
VL - 13
SP - RP100674
SN - 2050-084X
PB - eLife Sciences Publications, Ltd
DO - 10.7554/elife.100674
UR - https://doi.org/10.7554/elife.100674
LA - en
ER -

CSL-JSON

{
"id": "10.7554/elife.100674",
"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": "eLife",
"volume": "13",
"page": "RP100674",
"DOI": "10.7554/elife.100674",
"PMID": "41954599",
"PMCID": "PMC13065328",
"ISSN": "2050-084X",
"publisher": "eLife Sciences Publications, Ltd",
"URL": "https://doi.org/10.7554/elife.100674",
"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 approach
Journal: n/a
In 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/a
In 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 communications
In 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 America
In 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 neuroscience
In 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: eLife
In 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: iScience
In 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 communications
In 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 communications
In 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 biology
In 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.

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.