OSCR

A learning-evoked slow-oscillatory architecture paces population activity for offline reactivation across the human medial temporal lobe.

Code ↔ Paper

1 match 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 1 match
  1. [1] § Star★Methods › Method Details › Decomposition of LFPs into oscillatory components ↔ tmEMD.py, lines 233–280 · score 0.79 · Empirical Mode Decomposition, intrinsic mode functions, mask signal, optimal, mode mixing, phase

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

Python · 1,604 lines · 61 KB · CC-BY-4.0 · 1 match

  1. import numpy as np
  2. from scipy import stats
  3. import emd
  4. import multiprocessing
  5. import multiprocessing.pool
  6. import time
  7. import itertools
  8. from it_emd import it_emd
  9. ### --- Multiprocessing --- ###
  10. class NoDaemonProcess(multiprocessing.Process):
  11. # make 'daemon' attribute always return False
  12. def _get_daemon(self):
  13. return False
  14. def _set_daemon(self, value):
  15. pass
  16. daemon = property(_get_daemon, _set_daemon)
  17. class _pool(multiprocessing.pool.Pool):
  18. Process = NoDaemonProcess
  19. ### --- Power Spectral Density (PSD) --- ###
  20. def get_psd(X, sample_rate):
  21. """
  22. !! Warning: This function may need to be modified so that the returned variable freqAx_psd covers a suitable frequency range
  23. Computes and return the Power Spectral Density (PSD) estimate of a signal.
  24. Parameters
  25. ----------
  26. X : ndarray
  27. 1D array signal
  28. sample_rate : float
  29. The sampling rate of X
  30. Returns
  31. ----------
  32. freqAx_psd, psd
  33. freqAx_psd : ndarray
  34. 1D array frequency axis
  35. psd : ndarray
  36. 1D array PSD estimate
  37. """
  38. window = 'hann' # use a Hanning window for Fourier transform
  39. def get_psd_(X, sample_rate, maxFreq, pointsPerHz):
  40. from scipy.signal import welch
  41. psdIndMax = int(maxFreq*pointsPerHz)
  42. psd = welch(X, fs=sample_rate, window=window, nperseg=int(sample_rate)*pointsPerHz)
  43. freqAx_psd, psd = psd[0][0:psdIndMax], psd[1][0:psdIndMax]
  44. return freqAx_psd, psd
  45. freqAx_psd, psd = [], []
  46. maxFreqs = [1, 10, 100, 200, 500]
  47. pointsPerHzs = [20, 4, 1, 0.04, 0.02]
  48. for maxFreq, pointsPerHz in zip(maxFreqs, pointsPerHzs):
  49. freqAx_psd_, psd_ = get_psd_(X, sample_rate, maxFreq, pointsPerHz)
  50. if not len(freqAx_psd):
  51. i = 0
  52. else:
  53. i = np.flatnonzero(freqAx_psd_ > freqAx_psd[-1][-1])[0]
  54. freqAx_psd.append(freqAx_psd_[i:])
  55. psd.append(psd_[i:])
  56. freqAx_psd = np.concatenate(freqAx_psd)
  57. psd = np.concatenate(psd)
  58. return freqAx_psd, psd
  59. '''
  60. def get_psd2(X, sample_rate, window='hann'):
  61. def get_psd_(X, sample_rate, maxFreq, pointsPerHz):
  62. from scipy.signal import welch
  63. psdIndMax = int(maxFreq*pointsPerHz)
  64. psd = welch(X, fs=sample_rate, window=window, nperseg=int(sample_rate)*pointsPerHz)
  65. freqAx_psd, psd = psd[0][0:psdIndMax], psd[1][0:psdIndMax]
  66. return freqAx_psd, psd
  67. freqAx_psd, psd = [], []
  68. maxFreqs = [301]
  69. pointsPerHzs = [1]
  70. for maxFreq, pointsPerHz in zip(maxFreqs, pointsPerHzs):
  71. freqAx_psd_, psd_ = get_psd_(X, sample_rate, maxFreq, pointsPerHz)
  72. if not len(freqAx_psd):
  73. i = 0
  74. else:
  75. i = np.flatnonzero(freqAx_psd_ > freqAx_psd[-1][-1])[0]
  76. freqAx_psd.append(freqAx_psd_[i:])
  77. psd.append(psd_[i:])
  78. freqAx_psd = np.concatenate(freqAx_psd)
  79. psd = np.concatenate(psd)
  80. return freqAx_psd, psd
  81. '''
  82. ### --- Mode mixing --- ###
  83. def get_modeMixScore_corr(imfs, imfis_4_scoring, sample_rate=None, compute=True,
  84. return_label=False, label='Mode mixing score (r)'):
  85. """
  86. Computes the mode mixing score for a set of IMFs by measuring their pairwise Pearson correlations.
  87. Parameters
  88. ----------
  89. imfs : ndarray
  90. 2D [time x N_imfs] array. (Note: this is called 'imf' in the emd package.)
  91. imfis_4_scoring : ndarray | None
  92. 1D array containing the indices of the IMFs to be used to compute mix scoring. If None, all indices will be used.
  93. sample_rate : float | None
  94. The sampling rate for all the data provided in Xs. This is not needed for this function, so default = None.
  95. compute : bool
  96. Should the score be computed. Set to False if only the label is needed.
  97. return_label : bool
  98. This will be set to True when this function is used for plotting functions [figplot_tmEMD], so that
  99. the axis is labelled appropriately.
  100. label : string
  101. A label for an axis for mode mixing plots (see above).
  102. Returns
  103. -------
  104. mixScore, label
  105. mixScore : float
  106. The mode mixing score for the set of IMFs.
  107. label : string
  108. Only if return_label=True
  109. """
  110. if compute:
  111. corMat = np.abs(np.corrcoef(imfs[:, imfis_4_scoring].T))
  112. corMat[np.tril_indices(corMat.shape[0], k=0)] = np.nan
  113. mixScore = np.nanmean(corMat)
  114. else:
  115. mixScore = None
  116. if return_label:
  117. return mixScore, label
  118. return mixScore
  119. def get_modeMixScore_imfPSDs(imfs, imfis_4_scoring, sample_rate, psd_func=get_psd, compute=True,
  120. return_label=False, label='Mode mixing score (IMF PSD corr.)'):
  121. """
  122. Computes the mode mixing score for a set of IMFs by measuring the pairwise Pearson correlations between IMF power spectra (PSDs).
  123. Parameters
  124. ----------
  125. imfs : ndarray
  126. 2D [time x N_imfs] array. (Note: this is called 'imf' in the emd package.)
  127. imfis_4_scoring : ndarray | None
  128. 1D array containing the indices of the IMFs to be used to compute mix scoring. If None, all indices will be used.
  129. sample_rate : float | None
  130. The sampling rate for all the data provided in Xs
  131. psd_func : function
  132. Function to compute the Power Spectral Density (PSD) of IMFs.
  133. The function should take two arguments: (X, sample_rate) and return the frequency axis and PSD.
  134. The frequency axis returned should cover an appropriate range for frequencies of interest.
  135. compute : bool
  136. Should the score be computed. Set to False if only the label is needed.
  137. return_label : bool
  138. This will be set to True when this function is used for plotting functions [figplot_tmEMD], so that
  139. the axis is labelled appropriately.
  140. label : string
  141. A label for an axis for mode mixing plots (see above).
  142. Returns
  143. -------
  144. mixScore, label
  145. mixScore : float
  146. The mode mixing score for the set of IMFs.
  147. label : string
  148. Only if return_label=True
  149. """
  150. if compute:
  151. freqAx_psd, imfPSDs = get_imfPSDs(imfs, sample_rate=sample_rate, psd_func=psd_func)
  152. corMat = np.abs(np.corrcoef(imfPSDs[imfis_4_scoring, :]))
  153. corMat[np.tril_indices(corMat.shape[0], k=0)] = np.nan
  154. mixScore = np.nanmean(corMat)
  155. else:
  156. mixScore = None
  157. if return_label:
  158. return mixScore, label
  159. return mixScore
  160. def get_modeMixScore_4_imfPSDs(imfPSDs, imfis_4_scoring, sample_rate=None, compute=True,
  161. return_label=False, label='Mode mixing score (IMF PSD corr.)'):
  162. """
  163. Computes the mode mixing score for a set of IMF Power Spectra (PSDs) by measuring their pairwise Pearson correlations.
  164. Parameters
  165. ----------
  166. imfPSDs : ndarray
  167. 2D [N_imfs x frequency] array; returned by get_imfPSDs().
  168. imfis_4_scoring : ndarray | None
  169. 1D array containing the indices of the IMFs to be used to compute mix scoring. If None, all indices will be used.
  170. sample_rate : float | None
  171. The sampling rate for all the data provided in Xs. This is not needed for this function, so default = None.
  172. compute : bool
  173. Should the score be computed. Set to False if only the label is needed.
  174. return_label : bool
  175. This will be set to True when this function is used for plotting functions [figplot_tmEMD], so that
  176. the axis is labelled appropriately.
  177. label : string
  178. A label for an axis for mode mixing plots (see above).
  179. Returns
  180. -------
  181. mixScore, label
  182. mixScore : float
  183. The mode mixing score for the set of IMFs.
  184. label : string
  185. Only if return_label=True
  186. """
  187. if compute:
  188. corMat = np.abs(np.corrcoef(imfPSDs[imfis_4_scoring, :]))
  189. corMat[np.tril_indices(corMat.shape[0], k=0)] = np.nan
  190. mixScore = np.nanmean(corMat)
  191. else:
  192. mixScore = None
  193. if return_label:
  194. return mixScore, label
  195. return mixScore
  196. def PMSI(imfs, imfi, method='both'):
  197. """
  198. #########################
  199. Code author: Marco Fabus
  200. https://gitlab.com/marcoFabus/fabus2021_itemd/-/blob/main/Tools/analysis.py
  201. Method reference:
  202. Wang Y-H,Hu K,Lo M-T. (2018)
  203. Uniform phase empirical mode decomposition: an optimal hybridization of masking signal and ensemble approaches.
  204. ###########################
  205. Computes pseudo-mode mixing index of an intrinsic mode function.
  206. Parameters
  207. ----------
  208. imf : 2D array
  209. Set of IMFs.
  210. m : int
  211. Mode to calculate the PMSI of.
  212. method : string, optional
  213. Calculate PMSI as sum of PMSI between mode m and both above / below
  214. modes, or only above / below mode. The default is 'both'.
  215. Returns
  216. -------
  217. pmsi
  218. pmsi : float
  219. PMSI calculated.
  220. """
  221. if method == 'both':
  222. abs1 = (imfs[:, imfi].dot(imfs[:, imfi]) + imfs[:, imfi-1].dot(imfs[:, imfi-1]))
  223. pmsi1 = np.max([np.dot(imfs[:, imfi], imfs[:,imfi-1]) / abs1, 0])
  224. abs2 = (imfs[:, imfi].dot(imfs[:, imfi]) + imfs[:, imfi+1].dot(imfs[:, imfi+1]))
  225. pmsi2 = np.max([np.dot(imfs[:, imfi], imfs[:,imfi+1]) / abs2, 0])
  226. return pmsi1 + pmsi2
  227. if method == 'above':
  228. abs1 = (imfs[:, imfi].dot(imfs[:, imfi]) + imfs[:, imfi-1].dot(imfs[:, imfi-1]))
  229. pmsi1 = np.max([np.dot(imfs[:, imfi], imfs[:,imfi-1]) / abs1, 0])
  230. return pmsi1
  231. if method == 'below':
  232. abs2 = (imfs[:, imfi].dot(imfs[:, imfi]) + imfs[:, imfi+1].dot(imfs[:, imfi+1]))
  233. pmsi2 = np.max([np.dot(imfs[:, imfi], imfs[:,imfi+1]) / abs2, 0])
  234. return pmsi2
  235. def get_modeMixScore_pmsi(imfs, imfis_4_scoring, sample_rate=None, compute=True,
  236. return_label=False, label='Mode mixing score (PMSI)'):
  237. """
  238. Computes the mode mixing score for a set of IMFs by measuring the pseudo mode-splitting index (PMSI) for each IMF of interest.
  239. Parameters
  240. ----------
  241. imfs : ndarray
  242. 2D [time x N_imfs] array. (Note: this is called 'imf' in the emd package.)
  243. imfis_4_scoring : ndarray | None
  244. 1D array containing the indices of the IMFs to be used to compute mix scoring. If None, all indices will be used.
  245. sample_rate : float | None
  246. The sampling rate for all the data provided in Xs. This is not needed for this function, so default = None.
  247. compute : bool
  248. Should the score be computed. Set to False if only the label is needed.
  249. return_label : bool
  250. This will be set to True when this function is used for plotting functions [figplot_tmEMD], so that
  251. the axis is labelled appropriately.
  252. label : string
  253. A label for an axis for mode mixing plots (see above).
  254. Returns
  255. -------
  256. mixScore, label
  257. mixScore : float
  258. The mode mixing score for the set of IMFs.
  259. label : string
  260. Only if return_label=True
  261. """
  262. if compute:
  263. mixScore = np.array([PMSI(imfs, imfi) for imfi in imfis_4_scoring if imfi+1 < imfs.shape[1]]).mean()
  264. else:
  265. mixScore = None
  266. if return_label:
  267. return mixScore, label
  268. return mixScore
  269. ### --- Consistency --- ###
  270. def get_consistencyScores(freqAx_psd, X_imfPSDs, imfis_4_scoring, f_ranges0=None, use_f_ranges0=False,
  271. compute=True, return_label=False, label='Consistency (IMF PSD corr.)'):
  272. """
  273. Computes the consistency scores for the IMF PSDs for each input signal X.
  274. Parameters
  275. ----------
  276. freqAx_psd : ndarray
  277. 1D frequency axis array, corresponding to the last dimension of X_imfPSDs.
  278. X_imfPSDs : ndarray
  279. 3D [N_X x N_IMFs x frequency] array, containing the IMF PSDs obtained from the IMFs of each X.
  280. imfis_4_scoring : ndarray | None
  281. 1D array containing the indices of the IMFs to be used to compute mix scoring. If None, all indices will be used.
  282. f_ranges0 : ndarray | None
  283. if not None and use_f_ranges0 is True, the PSDs of each IMF will be trimmed according to the frequency ranges
  284. specified by f_ranges0. If None (default), the entire PSD will be used.
  285. use_f_ranges0 : bool
  286. If True, the PSDs will be trimmed according to the frequency limits specified in f_ranges0 before measuring
  287. consistency.
  288. compute : bool
  289. Should the score be computed. Set to False if only the label is needed.
  290. return_label : bool
  291. This will be set to True when this function is used for plotting functions [figplot_tmEMD], so that
  292. the axis is labelled appropriately
  293. label : string
  294. A label for an axis for consistency plots (see above).
  295. Returns
  296. -------
  297. consistencyScores, label
  298. consistencyScores : ndarray
  299. 1D ndarray of length X_imfPSDs.shape[0]; each element being the mean consistency score for the IMF PSDs of that
  300. X to all other Xs.
  301. label : string
  302. Only if return_label=True
  303. """
  304. try:
  305. n_X, n_imfs, _ = X_imfPSDs.shape
  306. except AttributeError:
  307. n_X, n_imfs = None, None
  308. if compute and n_X > 1:
  309. if imfis_4_scoring is None:
  310. imfis_4_scoring = np.arange(n_imfs)
  311. if f_ranges0 is None or not use_f_ranges0:
  312. f_sts = [0]*n_imfs
  313. f_ens = [len(freqAx_psd)]*n_imfs
  314. else:
  315. f_sts = [np.abs(freqAx_psd-f).argmin() for f in f_ranges0[:, 0]]
  316. f_ens = [np.abs(freqAx_psd-f).argmin() for f in f_ranges0[:, 1]]
  317. obs = np.row_stack([np.concatenate([imfPSDs[imfi, st:en] for imfi, st, en in \
  318. zip(imfis_4_scoring, f_sts, f_ens)]) for imfPSDs in X_imfPSDs])
  319. corMat = np.corrcoef(obs)
  320. consistencyScores = np.array([corMat[np.setdiff1d(np.arange(n_X), [xi]), xi].mean() for xi in range(n_X)])
  321. elif compute:
  322. consistencyScores = np.array([np.nan])
  323. else:
  324. consistencyScores = None
  325. if return_label:
  326. return consistencyScores, label
  327. return consistencyScores
  328. ### --- Utilities --- ###
  329. def get_inds4propPSD(psd, prop_psd):
  330. """
  331. Computes the consistency scores across IMF PSDs for each input signal X.
  332. Parameters
  333. ----------
  334. psd : ndarray
  335. 1D Power Spectral Density vector
  336. prop_psd : float (0 < prop_psd < 1)
  337. The proportion of the PSD to cover. Larger values will give a wider range
  338. Returns
  339. -------
  340. st, en
  341. st, en : int
  342. The indices corresponding to the start and end of the psd that cover prop_psd
  343. """
  344. a0 = psd.sum()
  345. maxi = psd.argmax()
  346. if psd[maxi] / a0 > prop_psd:
  347. if maxi == 0:
  348. st, en = [maxi, maxi+2]
  349. elif maxi == len(psd)-1:
  350. st, en = [maxi-2, maxi]
  351. else:
  352. st, en = [maxi-1, maxi+1]
  353. return st, en
  354. if maxi in [0, len(psd)-1]:
  355. if maxi == 0:
  356. st, en = 0, 1
  357. finished_start, finished_end = True, False
  358. else:
  359. st, en = maxi-1, maxi
  360. finished_start, finished_end = False, True
  361. else:
  362. st, en = maxi-1, maxi+1
  363. finished_start, finished_end = False, False
  364. for _ in psd:
  365. if st == 0:
  366. finished_start = True
  367. elif en == len(psd)-1:
  368. finished_end = True
  369. if finished_start:
  370. en += 1
  371. elif finished_end:
  372. st -= 1
  373. elif psd[st-1] > psd[en+1]:
  374. st -= 1
  375. else:
  376. en += 1
  377. if psd[st:en].sum()/a0 > prop_psd:
  378. break
  379. return st, en
  380. def it_X_2array(it_mask_freqs, it_mix_scores, it_adj_mix_scores, it_consistency_scores):
  381. """
  382. Converts arguments (which exist in list format between iterations) into numpy arrays.
  383. Returns
  384. ----------
  385. it_mask_freqs, it_mix_scores, it_adj_mix_scores, it_consistency_scores
  386. """
  387. if isinstance(it_mask_freqs, list):
  388. it_mask_freqs = np.row_stack(it_mask_freqs)
  389. if isinstance(it_mix_scores, list):
  390. it_mix_scores = np.row_stack(it_mix_scores)
  391. if isinstance(it_adj_mix_scores, list):
  392. it_adj_mix_scores = np.concatenate(it_adj_mix_scores)
  393. if isinstance(it_consistency_scores, list):
  394. it_consistency_scores = np.row_stack(it_consistency_scores)
  395. return it_mask_freqs, it_mix_scores, it_adj_mix_scores, it_consistency_scores
  396. def get_imfPSDs(imfs, sample_rate, psd_func=get_psd):
  397. """
  398. Get the power spectral density (PSD) estimates for each IMF.
  399. Parameters
  400. ----------
  401. X : ndarray
  402. 1D array signal
  403. sample_rate : float
  404. The sampling rate of X
  405. psd_func : function
  406. Function to compute the Power Spectral Density (PSD) of IMFs.
  407. The function should take two arguments: (X, sample_rate) and return the frequency axis and PSD.
  408. The frequency axis returned should cover an appropriate range for frequencies of interest.
  409. Returns
  410. ----------
  411. freqAx_psd, imfPSDs
  412. freqAx_psd : ndarray
  413. 1D array frequency axis
  414. imfPSDs : ndarray
  415. 2D [nImfs x frequencies] array, containing the PSD estimates of each IMF
  416. """
  417. imfPSDs = []
  418. for imf in imfs.T:
  419. freqAx_psd, psd = psd_func(imf, sample_rate)
  420. imfPSDs.append(psd)
  421. imfPSDs = np.row_stack(imfPSDs)
  422. return freqAx_psd, imfPSDs
  423. def get_f_ranges_from_imfPSDs(freqAx_psd, imfPSDs, prop_psd=0.8, f_set=None):
  424. """
  425. Defines frequency ranges for each IMF according to its PSD.
  426. Parameters
  427. ----------
  428. freqAx_psd : ndarray
  429. 1D frequency axis array, corresponding to the last dimension of imfPSDs.
  430. imfPSDs : ndarray
  431. 2D [n_imfs X frequency] array, containing the PSDs of each IMF
  432. prop_psd : float (0 < prop_psd < 1)
  433. The proportion of the PSD used to define the frequency range. Larger values will yield a wider range
  434. f_set : None | ndarray
  435. 1D array specifying whether a mask frequency is to be fixed or variable for the algorithm. If fixed, the entry
  436. should be the desired frequency (in Hz). If variable, the entry should be None.
  437. Returns
  438. -------
  439. f_ranges
  440. f_ranges : ndarray
  441. 2D [N_imfs X 2] array containing the minimum and maximum frequency values for that IMF.
  442. """
  443. f_ranges = []
  444. for imfi, psd in enumerate(imfPSDs):
  445. st, en = get_inds4propPSD(psd, prop_psd)
  446. f_ranges.append([freqAx_psd[st], freqAx_psd[en]])
  447. f_ranges = np.row_stack(f_ranges)
  448. if f_set is not None:
  449. for i, f in enumerate(f_set):
  450. if f is not None:
  451. f_ranges[i] = [f, f]
  452. return f_ranges
  453. def get_adj_fis(nImfs):
  454. adj_fis = np.column_stack([np.arange(nImfs)[:-1], np.arange(nImfs)[1:]])
  455. return adj_fis
  456. def get_adj_prop(it_mix_scores, it_adj_mix_scores, top_n):
  457. it_mix_scores_M = it_mix_scores.mean(axis=1)
  458. inds = np.flatnonzero(np.argsort(np.argsort(it_mix_scores_M)) < top_n)
  459. adj_prop = it_adj_mix_scores[inds].mean(axis=-1).mean(axis=0)
  460. for i in np.flatnonzero(adj_prop < 0):
  461. adj_prop[i] = 0
  462. adj_prop /= adj_prop.sum()
  463. return adj_prop
  464. def get_f_adjis(nImfs):
  465. f_adjis = []
  466. for fi in range(nImfs):
  467. if fi == 0:
  468. f_adjis.append([fi])
  469. elif fi < nImfs-1:
  470. f_adjis.append([fi-1, fi])
  471. else:
  472. f_adjis.append([fi-1])
  473. return f_adjis
  474. def get_f_freqs(freqs0, f_ranges):
  475. """
  476. Get the frequency values to be used to generate mask frequency combinations.
  477. """
  478. f_freqs = [freqs0[np.flatnonzero(np.logical_and(freqs0 >= fMin, freqs0 <= fMax))] for fMin, fMax in f_ranges]
  479. return f_freqs
  480. def get_f_ranges(it_mask_freqs, it_mix_scores, it_adj_mix_scores, it_consistency_scores, top_n, f_set=None):
  481. """
  482. Defines frequency ranges for each IMF to select mask frequency values for the next iteration.
  483. Parameters
  484. ----------
  485. it_mask_freqs : list | ndarray
  486. 2D array [N_sub-iterations x N_maskFreqs] containing the mask frequencies used for each mEMD sub-iteration
  487. it_mix_scores : list | ndarray
  488. 2D array [N_sub-iterations x N_X] containing the mode mixing scores yeilded for each sub-iteration for each X
  489. it_adj_mix_scores : list | ndarray
  490. 3D array [N_sub-iterations x N_maskFreqs-1 x N_X] containing the mode mixing scores for adjacent IMFs
  491. yeilded for each sub-iteration for each X. The index of the second dimension (N_adj) corresponds to the mixing
  492. between that IMF index and the successive one.
  493. it_consistency_scores : list | ndarray
  494. 2D array [N_sub-iterations x N_X] containing the consistency scores yeilded for each mEMD sub-iteration
  495. top_n : int
  496. Frequency ranges will be narrowed according those appearing in top N least mixed sub-iterations.
  497. f_set : None | ndarray
  498. 1D array specifying whether a mask frequency is to be fixed or variable for the algorithm. If fixed, the entry
  499. should be the desired frequency (in Hz). If variable, the entry should be None.
  500. Returns
  501. -------
  502. f_ranges
  503. f_ranges : ndarray
  504. 2D [N_imfs X 2] array containing the minimum and maximum frequency values for that IMF.
  505. """
  506. it_mask_freqs, it_mix_scores, it_adj_mix_scores, it_consistency_scores = \
  507. it_X_2array(it_mask_freqs, it_mix_scores, it_adj_mix_scores, it_consistency_scores)
  508. it_mix_scores_M = it_mix_scores.mean(axis=1)
  509. it_adj_mix_scores_M = it_adj_mix_scores.mean(axis=-1)
  510. it_sortInds = np.argsort(np.argsort(it_mix_scores_M))
  511. adj_fis = get_adj_fis(it_mask_freqs.shape[-1])
  512. topi = np.flatnonzero(it_sortInds == 0)[0]
  513. inds = np.flatnonzero(it_sortInds < top_n)
  514. f_ranges = np.column_stack([it_mask_freqs[topi]]*2)
  515. adj_mix_scores_top = it_adj_mix_scores_M[topi]
  516. for adji, mix_score_top in enumerate(adj_mix_scores_top):
  517. betterInds = inds[np.flatnonzero(mix_score_top > it_adj_mix_scores_M[inds][:, adji])]
  518. if not len(betterInds):
  519. continue
  520. for fi in adj_fis[adji]:
  521. fmin, fmax = [f(it_mask_freqs[betterInds][:, fi]) for f in [np.min, np.max]]
  522. if f_ranges[fi, 0] > fmin:
  523. f_ranges[fi, 0] = fmin
  524. if f_ranges[fi, 1] < fmax:
  525. f_ranges[fi, 1] = fmax
  526. if f_set is not None:
  527. for i, f in enumerate(f_set):
  528. if f is not None:
  529. f_ranges[i] = [f, f]
  530. return f_ranges
  531. def reduce_f_freqs(f_freqs, it_mix_scores, it_mask_freqs, it_adj_mix_scores, top_n, dim_red_prop):
  532. """
  533. This function will only be implemented in the algorithm if max_iterations > max_iterations_b4_dim_red in run_tmEMD (not recommended).
  534. """
  535. it_mask_freqs, it_mix_scores, it_adj_mix_scores = it_X_2array(it_mask_freqs, it_mix_scores, it_adj_mix_scores)
  536. nImfs = len(f_freqs)
  537. adj_fis = get_adj_fis(nImfs)
  538. adj_prop = get_adj_prop(it_mix_scores, it_adj_mix_scores, top_n)
  539. f_adjis = get_f_adjis(nImfs)
  540. f_prop = np.array([np.mean([adj_prop[adji] for adji in adjis]) for adjis in f_adjis])
  541. f_nFreqs = np.array([len(fs) for fs in f_freqs])
  542. n_freqDim0 = f_nFreqs.sum()
  543. n2lose = n_freqDim0 - int(n_freqDim0*dim_red_prop)
  544. f_weights = np.array([((ni-1)/n_freqDim0) * (1-propi) for ni, propi in zip(f_nFreqs, f_prop)])
  545. f_weights /= f_weights.sum()
  546. f_n2lose = np.array(n2lose*f_weights, dtype=int)
  547. f_freqs_ = []
  548. for fi, n2lose, nFreqs, freqs in zip(range(nImfs), f_n2lose, f_nFreqs, f_freqs):
  549. if n2lose:
  550. n_left = nFreqs - n2lose
  551. if n_left >= 2:
  552. if freqs[0]:
  553. # consider sampling from freqs0 rather than introduce novel freqs?
  554. freqs_ = np.geomspace(freqs[0], freqs[-1], n_left)
  555. else:
  556. freqs_ = np.concatenate([[0.], np.geomspace(freqs[1], freqs[-1], n_left-1)])
  557. else:
  558. # pick the mask freq from the current space which yielded the lowest mixing scores for adjacent IMFs
  559. adjis = np.flatnonzero([fi in inds for inds in adj_fis])
  560. i = np.argmin([it_adj_mix_scores[it_mask_freqs[:, fi] == f][:, adjis].mean() for f in freqs])
  561. freqs_ = np.array([freqs[i]])
  562. else:
  563. freqs_ = freqs
  564. f_freqs_.append(freqs_)
  565. return f_freqs_
  566. ### --- tmEMD algorithm --- ###
  567. def run_subIteration(args):
  568. """
  569. Randomly generates mask frequencies to apply mask-EMD to Xs and returns the
  570. mask freqs and the mixing scores
  571. """
  572. Xs, f_freqs, imfis_4_scoring, sample_rate, mask_args, mixScore_func, consistency_func, compute_consistency, f_ranges0, nprocesses = args
  573. nImfs = len(f_freqs)
  574. # use fRange to randomly generate mask freq within each range
  575. np.random.seed()
  576. nTries = 50
  577. invalid = False
  578. perms = itertools.combinations(np.arange((len(f_freqs))), 2)
  579. if all([np.array_equal(f_freqs[i], f_freqs[j]) for i, j in list(perms)]):
  580. mask_freqs = np.array(sorted([np.random.choice(freqs) for freqs in f_freqs]))[::-1]
  581. else:
  582. for tryi in range(nTries):
  583. mask_freqs = np.array([np.random.choice(freqs) for freqs in f_freqs])
  584. if all(np.diff(mask_freqs) <= 0): # if randomly selected freqs are in descending order
  585. break
  586. else:
  587. if tryi == nTries-1:
  588. mask_freqs = np.repeat(np.nan, nImfs)
  589. invalid = True
  590. if invalid:
  591. mix_scores_ = np.repeat(np.nan, len(Xs))
  592. adjMixScores_ = np.full([nImfs-1, len(Xs)], np.nan)
  593. consistencyScores_ = np.repeat(np.nan, len(Xs))
  594. return mask_freqs, mix_scores_, adjMixScores_, consistencyScores_
  595. # get sift args
  596. sift_config = emd.sift.get_config('mask_sift')
  597. sift_config['mask_freqs'] = mask_freqs/sample_rate
  598. sift_config['max_imfs'] = len(mask_freqs)
  599. for k in mask_args:
  600. if mask_args[k] is not None:
  601. sift_config[k] = mask_args[k]
  602. mix_scores_ = []
  603. adjMixScores_ = []
  604. if compute_consistency:
  605. X_imfPSDs = []
  606. for X in Xs:
  607. imfs = emd.sift.mask_sift(X, **sift_config)
  608. if imfs.shape[1] != nImfs:
  609. mix_scores_.append(np.nan)
  610. adjMixScores_.append(np.repeat(np.nan, nImfs-1))
  611. continue
  612. mix_scores_.append(mixScore_func(imfs, imfis_4_scoring, sample_rate))
  613. adj_fis = get_adj_fis(nImfs)
  614. corMat = np.corrcoef(imfs.T)
  615. adjMixScores_.append(np.array([corMat[x, y] for x, y in adj_fis]))
  616. if compute_consistency:
  617. freqAx_psd, imfPSDs = get_imfPSDs(imfs, sample_rate)
  618. X_imfPSDs.append(imfPSDs)
  619. if compute_consistency:
  620. X_imfPSDs = np.array(X_imfPSDs)
  621. consistencyScores_ = consistency_func(freqAx_psd, X_imfPSDs, imfis_4_scoring, f_ranges0)
  622. else:
  623. consistencyScores_ = None
  624. mix_scores_ = np.array(mix_scores_)
  625. adjMixScores_ = np.column_stack(adjMixScores_)
  626. return mask_freqs, mix_scores_, adjMixScores_, consistencyScores_
  627. def run_iteration(Xs, f_freqs, imfis_4_scoring, sample_rate, mask_args, mixScore_func, consistency_func, compute_consistency,
  628. f_ranges0, nprocesses, n_per_it):
  629. pool = _pool(nprocesses)
  630. args = (Xs, f_freqs, imfis_4_scoring, sample_rate, mask_args, mixScore_func, consistency_func, compute_consistency,
  631. f_ranges0, nprocesses)
  632. it_outputs = pool.map(run_subIteration, [args for i in range(n_per_it)])
  633. pool.close()
  634. pool.join()
  635. it_mask_freqs = np.row_stack([it_outputsi[0] for it_outputsi in it_outputs])
  636. it_mix_scores = np.row_stack([it_outputsi[1] for it_outputsi in it_outputs])
  637. it_adj_mix_scores = np.array([it_outputsi[2] for it_outputsi in it_outputs])
  638. if compute_consistency:
  639. it_consistency_scores = np.row_stack([it_outputsi[3] for it_outputsi in it_outputs])
  640. else:
  641. it_consistency_scores = None
  642. return it_mask_freqs, it_mix_scores, it_adj_mix_scores, it_consistency_scores
  643. def run_tmEMD(Xs, sample_rate, psd_func=get_psd, freqs0=None, pre_emd_mode='eEMD', ignore_1st_pre_emd_imf=False, prop_psd=0.8,
  644. nensembles=4, X_paths2imfs=None, max_imfs=10, f_set=None, imfis_4_scoring=None, mixScore_func=get_modeMixScore_imfPSDs,
  645. compute_consistency=False, consistency_func=get_consistencyScores, n_per_it=200, top_n=10, max_iterations=12,
  646. max_iterations_b4_dim_red=20, dim_red_prop=0.5, nprocesses=1, mask_amp=1, mask_amp_mode='ratio_imf',
  647. sift_thresh=1e-08, nphases=4, imf_opts={}, envelope_opts={}, extrema_opts={}):
  648. """
  649. Find the set of mask frequencies which yeild IMFs with the lowest loss function output.
  650. Parameters
  651. ----------
  652. Xs : list
  653. Each element is a 1D array containing a sample time-series used to tune the optimisation.
  654. sample_rate : float
  655. The sampling rate for all the data provided in Xs.
  656. psd_func : function
  657. Function to compute the Power Spectral Density (PSD) of IMFs.
  658. The function should take two arguments: (X, sample_rate) and return the frequency axis and PSD.
  659. The frequency axis returned should cover an appropriate range for frequencies of interest.
  660. freqs0 : ndarray | None
  661. 1D array containing all the frequency values (in Hz) for the algorithm to select mask frequencies from. If None,
  662. frequency values from the freqAx_psd returned by psd_func will be used.
  663. pre_emd_mode : str | None
  664. The EMD variant to be used to first estimate mask frequency ramges from IMF PSDs. Default is ensemble EMD.
  665. If None, mask frequency ranges will not be narrowed.
  666. ignore_1st_pre_emd_imf : bool
  667. If pre-EMD is used, should the first IMF be discarded. While the first IMF from eEMD is merely a product of noise,
  668. tmEMD tends to perform better when this extra IMF is kept, so it is advisable to keep this set to False.
  669. prop_psd : float
  670. If pre-EMD is used as per the above argument, this will specify the proportion of the PSD of each IMF from the
  671. pre-EMD to specify the frequency range.
  672. nensembles : int
  673. If eEMD is used, the number of ensembles to run
  674. X_paths2imfs : list | None
  675. If a list is given, it should the same length as Xs; each element being a path a .npy file which corresponds to
  676. the IMFs of that signal (X) which are used instead of running pre-EMD. If the path is None, pre-EMD will be run
  677. for that X as above.
  678. max_imfs : int
  679. The maximum number of IMFs. Used for pre-EMD and to specify the number of mask frequencies to be used for the
  680. mEMD sub-iterations.
  681. f_set : None | ndarray
  682. 1D array specifying whether a mask frequency is to be fixed or variable for the algorithm. If fixed, the entry
  683. should be the desired frequency (in Hz). If variable, the entry should be None.
  684. imfis_4_scoring : ndarray | None
  685. 1D array containing the indices of the IMFs to be used to compute mix scoring. If None, all indices will be used
  686. mixScore_func : function
  687. The function used to compute the mode mixing between IMFs. It should take three arguments:
  688. (imfs, imfis_4_scoring, sample_rate) and return a single number; lower meaning less mode mixing (desirable)
  689. compute_consistency : bool
  690. Measure the IMF consistency for each mEMD process
  691. consistency_func : function
  692. The function to be used to compute the IMF consistency. It should take (freqAx_psd, X_imfPSDs, imfis_4_scoring)
  693. as key arguments and return a 1D ndarray of length N_X; each element being the mean consistency score for that X to all other Xs
  694. n_per_it : int
  695. Number of mEMD sub-iterations to run within an iteration.
  696. top_n : int
  697. After each iteration, the frequency ranges for each mask frequency will be retricted by the ranges seen in the
  698. to the best (i.e. least-mixed) sub-iterations.
  699. max_iterations : int
  700. The maximum number of iterations to run
  701. max_iterations_b4_dim_red : int
  702. If lower than max_iterations, once max_iterations_b4_dim_red is reached, the algorithm will attempt to increase
  703. convergence speed by reducing the number of frequencies to choose from. This reduction is guided according to where
  704. mode mixing is stronger for adjacent IMFs, and how many frequencies currently can be selected for a given mask frequency.
  705. Note: this option is still work in progess!
  706. dim_red_prop : float
  707. Proportion of frequencies to loose as per above (a lower number will increase convergence rate).
  708. nprocesses : int
  709. Integer number of parallel processes to compute (Default value = 1)
  710. mask_amp : float or array_like
  711. Amplitude of mask signals as specified by mask_amp_mode. If float the same value is applied to all IMFs,
  712. if an array is passed each value is applied to each IMF in turn (Default value = 1)
  713. mask_amp_mode : {'abs', 'ratio_imf', 'ratio_sig'}:
  714. Method for computing mask amplitude. Either in absolute units ('abs'), or as a ratio of the standard deviation
  715. of the input signal ('ratio_sig') or previous imf ('ratio_imf') (Default value = 'ratio_imf')
  716. sift_thresh : float
  717. By default will be ignored inplace of max_imfs. The threshold at which the overall sifting process
  718. will stop. (Default value = 1e-8)
  719. nphases : int > 0
  720. The number of separate sinusoidal masks to apply for each IMF, the phase of masks are uniformly spread
  721. across a 0<=p<2pi range (Default = 4).
  722. imf_opts : dict
  723. Optional dictionary of keyword arguments to be passed to emd.get_next_imf
  724. envelope_opts : dict
  725. Optional dictionary of keyword options to be passed to emd.interp_envelope
  726. extrema_opts : dict
  727. Optional dictionary of keyword options to be passed to emd.get_padded_extrema
  728. Returns
  729. -------
  730. it_mask_freqs, it_mix_scores, it_adj_mix_scores, it_consistency_scores, it_is, optimised_mask_freqs, converged
  731. it_mask_freqs : ndarray
  732. 2D array [N_sub-iterations x N_maskFreqs] containing the mask frequencies used for each mEMD sub-iteration
  733. it_mix_scores : ndarray
  734. 2D array [N_sub-iterations x N_X] containing the mode mixing scores yeilded for each sub-iteration for each X
  735. it_adj_mix_scores : ndarray
  736. 3D array [N_sub-iterations x N_maskFreqs-1 x N_X] containing the mode mixing scores for adjacent IMFs
  737. yeilded for each sub-iteration for each X. The index of the second dimension (N_adj) corresponds to the mixing
  738. between that IMF index and the successive one.
  739. it_consistency_scores : ndarray
  740. 2D array [N_sub-iterations x N_X] containing the consistency scores yeilded for each mEMD sub-iteration
  741. it_is : ndarray
  742. 1D array [N_sub-iterations] containing the iteration index corresponding to each mEMD sub-iteration
  743. optimised_mask_freqs : ndarray
  744. 1D array containing the mask frequencies which yeilded the lowest average score from mixScore_func
  745. converged : bool
  746. True if the algorithm converged to an 'optimised' set of mask frequencies
  747. """
  748. if f_set is not None:
  749. if len(f_set) != max_imfs:
  750. print('Warning max_imfs different from f_set')
  751. return
  752. if len(Xs) == 1 and compute_consistency:
  753. print('Warning: len(Xs) must be more than 1 in order to compute consistency')
  754. compute_consistency = False
  755. if pre_emd_mode is not None:
  756. X_f_ranges = []
  757. if X_paths2imfs is None:
  758. X_paths2imfs = [None]*len(Xs)
  759. for X, path2imfs in zip(Xs, X_paths2imfs):
  760. if path2imfs is None:
  761. if pre_emd_mode == 'eEMD':
  762. imfs = emd.sift.ensemble_sift(X, nensembles=nensembles, max_imfs=max_imfs, nprocesses=nprocesses)
  763. if ignore_1st_pre_emd_imf:
  764. imfs = imfs[:, 1:]
  765. elif pre_emd_mode == 'itEMD':
  766. imfs = it_emd(X, sample_rate, N_imf=max_imfs)[0]
  767. else:
  768. imfs = np.load(path2imfs)
  769. freqAx_psd, imfPSDs = get_imfPSDs(imfs, sample_rate, psd_func=psd_func)
  770. f_ranges = get_f_ranges_from_imfPSDs(freqAx_psd, imfPSDs, f_set=f_set)
  771. X_f_ranges.append(f_ranges)
  772. X_nImfs = [X_f_rangesi.shape[0] for X_f_rangesi in X_f_ranges]
  773. if len(np.unique(X_nImfs)) > 1:
  774. print('Warning: Inconsistent number of IMFs detected between samples - using median number of IMFs')
  775. X_f_ranges_ = np.array([X_f_ranges[i] for i in np.flatnonzero(X_nImfs == np.median(X_nImfs))])
  776. else:
  777. X_f_ranges_ = np.array(X_f_ranges)
  778. f_ranges = np.column_stack([X_f_ranges_[:, :, 0].min(axis=0), X_f_ranges_[:, :, 1].max(axis=0)])
  779. else:
  780. freqAx_psd, _ = psd_func(Xs[0], sample_rate)
  781. f_ranges = np.row_stack([[0, freqAx_psd[-1]] for _ in range(max_imfs)])
  782. if f_set is not None:
  783. for i, f in enumerate(f_set):
  784. if f is not None:
  785. f_ranges[i] = [f, f]
  786. f_ranges0 = f_ranges.copy() # can be used for consistency scores
  787. if imfis_4_scoring is None:
  788. imfis_4_scoring = np.arange(f_ranges.shape[0])
  789. if freqs0 is None:
  790. freqs0 = freqAx_psd
  791. if f_set is not None:
  792. fs = np.array([f for f in f_set if f is not None])
  793. freqs0 = np.append(freqs0, np.setdiff1d(fs, freqs0))
  794. # mask_freq optimisation
  795. mask_args = {'mask_amp' : mask_amp,
  796. 'mask_amp_mode' : mask_amp_mode,
  797. 'sift_thresh' : sift_thresh,
  798. 'nphases' : nphases,
  799. 'imf_opts' : imf_opts,
  800. 'envelope_opts' : envelope_opts,
  801. 'extrema_opts' : extrema_opts
  802. }
  803. it_mix_scores = []
  804. it_mask_freqs = []
  805. it_adj_mix_scores = []
  806. it_consistency_scores = [None, []][compute_consistency]
  807. it_is = []
  808. for iti in range(max_iterations):
  809. if iti:
  810. f_ranges = get_f_ranges(it_mask_freqs, it_mix_scores, it_adj_mix_scores, it_consistency_scores, top_n, f_set=f_set)
  811. f_freqs = get_f_freqs(freqs0, f_ranges)
  812. if iti >= max_iterations_b4_dim_red:
  813. print('reducing for iti=', iti)
  814. #return f_freqs, it_mix_scores, it_mask_freqs, it_adj_mix_scores, top_n, dim_red_prop
  815. f_freqs = reduce_f_freqs(f_freqs, it_mix_scores, it_mask_freqs, it_adj_mix_scores, top_n, dim_red_prop)
  816. it_mask_freqs_, it_mix_scores_, it_adj_mix_scores_, it_consistency_scores_ = run_iteration(Xs, f_freqs, imfis_4_scoring, sample_rate, mask_args, mixScore_func,
  817. consistency_func, compute_consistency, f_ranges0, nprocesses, n_per_it)
  818. it_mix_scores.append(it_mix_scores_)
  819. it_mask_freqs.append(it_mask_freqs_)
  820. it_adj_mix_scores.append(it_adj_mix_scores_)
  821. if compute_consistency:
  822. it_consistency_scores.append(it_consistency_scores_)
  823. it_is.append(np.repeat(iti, it_mix_scores_.shape[0]))
  824. if iti and np.sum(np.subtract(f_ranges[:,1], f_ranges[:,0])) == 0 : #(n_main_freqs*freq_int): # if all freqs optimised
  825. converged = True
  826. break
  827. elif iti == (max_iterations-1):
  828. print('Warning: Did not converge')
  829. print('First : plt.plot(it_mix_scores.mean(axis=1)) - if the scores look plateaued, there is likely no need to run further')
  830. print('Otherwise, consider:')
  831. print(' - increasing max_iterations, n_per_it')
  832. print(' - making max_iterations_b4_dim_red lower than max_iterations')
  833. converged = False
  834. it_mask_freqs = np.row_stack(it_mask_freqs)
  835. it_mix_scores = np.row_stack(it_mix_scores)
  836. it_adj_mix_scores = np.concatenate(it_adj_mix_scores)
  837. if compute_consistency:
  838. it_consistency_scores = np.concatenate(it_consistency_scores)
  839. it_is = np.concatenate(it_is)
  840. optimised_mask_freqs = it_mask_freqs[np.nan_to_num(it_mix_scores.mean(axis=1), nan=1).argmin()]
  841. return it_mask_freqs, it_mix_scores, it_adj_mix_scores, it_consistency_scores, it_is, optimised_mask_freqs, converged
  842. ### --- Extra utilities --- ###
  843. def merge_low_imfs(imfs, n_merge):
  844. nImfs = imfs.shape[1]
  845. imfis_merge = np.arange(nImfs)[-n_merge:]
  846. imfis_preserve = np.setdiff1d(np.arange(nImfs), imfis_merge)
  847. imfs_ = []
  848. for imfi in imfis_preserve:
  849. imfs_.append(imfs[:, imfi])
  850. imfs_.append(imfs[:, imfis_merge].sum(axis=1))
  851. imfs_ = np.column_stack(imfs_)
  852. return imfs_
  853. ### --- Figures --- ###
  854. def get_figure_1():
  855. sample_rate = 1250.
  856. seconds = 5
  857. timeAx0 = np.linspace(0, seconds, int(seconds*sample_rate))
  858. # Create an amplitude modulation
  859. am = np.sin(2*np.pi*timeAx0)
  860. am[am < 0] = 0
  861. # Create a 25Hz signal and introduce the amplitude modulation
  862. xx = am*np.sin(2*np.pi*25*timeAx0)
  863. # Create a non-modulated 6Hz signal
  864. yy = .5*np.sin(2*np.pi*6*timeAx0)
  865. # Sum the 25Hz and 6Hz components together
  866. xy = xx+yy
  867. signals = np.column_stack([xx, yy])
  868. X = signals.sum(axis=1)
  869. signal_colors = sns.color_palette('Set2', signals.shape[1])
  870. return X, signals, signal_colors, sample_rate
  871. emd_variants = ['EMD', 'eEMD', 'ceEMD', 'itEMD', 'mEMD_zc', 'mEMD']
  872. def run_emd(X, sample_rate, variant, max_imfs=9, args=None):
  873. """
  874. Get the IMFs for a signal.
  875. Parameters
  876. ----------
  877. X : ndarray
  878. 1D array signal
  879. sample_rate : float
  880. The sampling rate of X
  881. variant : string
  882. The EMD variant to run. See the variable: variants
  883. max_imfs : int
  884. The maximum number of IMFs.
  885. args : list | None
  886. Additional arguments to be parsed to the function: run_emd:
  887. for mEMD, args[0] should be the mask_freqs (in Hz) to be used
  888. Returns
  889. ----------
  890. imfs : ndarray
  891. 2D [time x N_imfs] array. (Note: this is called 'imf' in the emd package.)
  892. """
  893. import emd
  894. if X is None:
  895. print(emd_variants)
  896. return
  897. if variant == 'EMD':
  898. imfs = emd.sift.sift(X, max_imfs=max_imfs)
  899. elif variant == 'eEMD':
  900. imfs = emd.sift.ensemble_sift(X, max_imfs=max_imfs)
  901. elif variant == 'ceEMD':
  902. imfs, noise = emd.sift.complete_ensemble_sift(X, max_imfs=max_imfs)
  903. elif variant == 'itEMD':
  904. imfs = it_emd(X, sample_rate, N_imf=max_imfs)[0]
  905. elif variant == 'mEMD_zc':
  906. imfs = emd.sift.mask_sift(X, max_imfs=max_imfs)
  907. elif variant == 'mEMD':
  908. mask_freqs = args[0]
  909. imfs = emd.sift.mask_sift(X, mask_freqs=mask_freqs/sample_rate, max_imfs=max_imfs)
  910. else:
  911. print('method not recognised')
  912. imfs = None
  913. return imfs
  914. def get_modeMixScores_4_emd(Xs, sample_rate, variant, psd_func, max_imfs, args=None,
  915. mixScore_funcs=[get_modeMixScore_corr, get_modeMixScore_imfPSDs],
  916. consistency_func=get_consistencyScores):
  917. """
  918. Find the mode mixing scores for a given EMD variant.
  919. Parameters
  920. ----------
  921. Xs : list
  922. Each element is a 1D array containing a sample time-series used to tune the optimisation.
  923. sample_rate : float
  924. The sampling rate for all the data provided in Xs.
  925. variant : string
  926. The EMD variant to run. See the variable: variants
  927. psd_func : function
  928. Function to compute the Power Spectral Density (PSD) of IMFs.
  929. The function should take two arguments: (X, sample_rate) and return the frequency axis and PSD.
  930. The frequency axis returned should cover an appropriate range for frequencies of interest.
  931. max_imfs : int
  932. The maximum number of IMFs.
  933. args : list | None
  934. Additional arguments to be parsed to the function: run_emd. See the documentation of this function for details.
  935. mixScore_funcs : list
  936. Each entry is a function which takes (imfs, imfis_4_scoring, sample_rate) as required keyword arguments and has an
  937. additional argument, return_label. This last argument should return a string label denoting the type of mixing score.
  938. See functions that start with "get_modeMixScore_" for details.
  939. consistency_func : function
  940. The function to be used to compute the IMF consistency. It should take (freqAx_psd, X_imfPSDs, imfis_4_scoring)
  941. as key arguments and return a 1D ndarray of length N_X; each element being the mean consistency score for that X to all other Xs.
  942. Returns
  943. ----------
  944. labelScores : dict
  945. The key for each entry is given by the label returned by a mixScore_func or a consistency_func, and contains the
  946. mixing/consistency scores yeilded by that function.
  947. """
  948. labelScores = {}
  949. for mixScore_func in mixScore_funcs:
  950. _, label = mixScore_func(None, None, None, compute=False, return_label=True)
  951. labelScores[label] = []
  952. X_imfPSDs = []
  953. for X in Xs:
  954. if variant == 'eEMD':
  955. imfs = run_emd(X, sample_rate, variant, max_imfs+1, args)
  956. imfis_4_scoring = np.arange(1, imfs.shape[1])
  957. else:
  958. imfs = run_emd(X, sample_rate, variant, max_imfs, args)
  959. if imfs is not None:
  960. imfis_4_scoring = np.arange(imfs.shape[1])
  961. for mixScore_func in mixScore_funcs:
  962. mixScore, label = mixScore_func(imfs, imfis_4_scoring, sample_rate, return_label=True)
  963. labelScores[label].append(mixScore)
  964. freqAx_psd, imfPSDs = get_imfPSDs(imfs, sample_rate, psd_func)
  965. X_imfPSDs.append(imfPSDs)
  966. X_imfPSDs = np.array(X_imfPSDs)
  967. for label in labelScores:
  968. labelScores[label] = np.array(labelScores[label])
  969. consistencyScores, label = consistency_func(freqAx_psd, X_imfPSDs, imfis_4_scoring, return_label=True)
  970. labelScores[label] = consistencyScores
  971. return labelScores
  972. ### --- Plotting --- ###
  973. import seaborn as sns
  974. import matplotlib.pyplot as plt
  975. def set_plotStyle(i=0):
  976. s = ['Solarize_Light2', 'dark_background'][i]
  977. print(s)
  978. plt.style.use(s)
  979. def plot_mask_freq_scores(it_mask_freqs, it_mix_scores, xi=None, imfis=None, log_mixScore=False, ms=5, alpha=0.5, imf_cols=None,
  980. cmap='husl', inds=[], color_='k', ms_=8):
  981. """
  982. Plot the mode mixing scores yeilded from sets of mask frequencies.
  983. Parameters
  984. ----------
  985. it_mask_freqs : list | ndarray
  986. 2D array [N_sub-iterations x N_maskFreqs] containing the mask frequencies used for each mEMD sub-iteration
  987. it_mix_scores : list | ndarray
  988. 2D array [N_sub-iterations x N_X] containing the mode mixing scores yeilded for each sub-iteration for each X
  989. xi : int | None
  990. The X-index of the mixing scores to plot. If None, then the mean mixing score will be computed.
  991. imfis : ndarray | None
  992. the IMF indices to select and plot. If None, then all IMFs will be plotted.
  993. log_mixScore : bool
  994. Should the y-axis measuring mode mixing be logarithmic.
  995. ms : int
  996. Markersize of each mask frequency point.
  997. alpha : float {0-1}
  998. Color saturation level.
  999. imf_cols : ndarray | None
  1000. Colors for each IMF. If None, this will be defined by the argument 'cmap'
  1001. cmap : string
  1002. The colormap for the IMFs.
  1003. inds : list
  1004. Iteration indices of mask frequencies to highlight.
  1005. color_ : string | tuple
  1006. The color to highlight mask frequenies of interest.
  1007. ms_ : int
  1008. Markersize of each highlighted mask frequency point.
  1009. Returns
  1010. ----------
  1011. imf_cols : ndarray
  1012. Colors for each IMF plotted.
  1013. """
  1014. if xi is None:
  1015. it_mix_scores_M = it_mix_scores.mean(axis=1)
  1016. else:
  1017. it_mix_scores_M = it_mix_scores[:, xi]
  1018. if imfis is None:
  1019. imfis = np.arange(it_mask_freqs.shape[1])
  1020. if imf_cols is None:
  1021. imf_cols = sns.color_palette(cmap, len(imfis))
  1022. if cmap in ['husl', 'Spectral']:
  1023. imf_cols = imf_cols[::-1]
  1024. for fi, col in enumerate(imf_cols):
  1025. plt.plot(it_mask_freqs[:,imfis][:, fi], it_mix_scores_M, 's', color=col, ms=ms, alpha=alpha, lw=0)
  1026. for ind in inds:
  1027. plt.plot(it_mask_freqs[ind, imfis], np.repeat(it_mix_scores_M[ind], it_mask_freqs.shape[1]), 's', ms=ms_, color=color_)
  1028. if log_mixScore:
  1029. plt.yscale('log')
  1030. return imf_cols
  1031. def get_nearestInd(val, array):
  1032. """
  1033. Returns the index in an array which is closest to a given value.
  1034. """
  1035. array = np.array(array)
  1036. d = np.abs(array - val)
  1037. np.nan_to_num(d, False, np.nanmax(d))
  1038. ind = d.argmin()
  1039. return ind
  1040. def plot_emd(imfs, sample_rate, X=None, IA=None, window=None, timeAx=None, color_X='k', imf_cols=None, col_IA='k', cmap='gray',
  1041. alpha=1, ls='-', lw_X=1, lw_imfs=1, lw_IA=2, spaceFactor=0.2, X_shift=0., imfs_shift=0., flipCols=False,
  1042. focus_imfis=None, unfocus_col='gray', unfocus_alpha=0.3, unfocus_lw=1, zorder=2, alpha_se=0.5, return_imfYs=False):
  1043. """
  1044. Plot signal and its IMFs underneath.
  1045. Parameters
  1046. ----------
  1047. imfs : ndarray
  1048. 2D [time x N_imfs] array. (Note: this is called 'imf' in the emd package.)
  1049. sample_rate : float
  1050. The sampling rate of the IMFs
  1051. X : ndarray | None
  1052. The original signal. If None, the sum of the IMFs will be used.
  1053. IA : ndarray | None
  1054. 2D [time x N_imfs] array of imf amplitudes
  1055. window : tuple | None
  1056. Start and end indices to plot. If None, then the whole time window will be plotted.
  1057. timeAx : ndarray | None
  1058. A time axis for the IMFs (which should correspond to the window length if specified). If None, then a time axis will be
  1059. generated automatically.
  1060. color_X : string | tuple
  1061. The color of the original signal.
  1062. imf_cols : ndarray | None
  1063. Colors for each IMF. If None, the colors will be determined by the argument, cmap.
  1064. col_IA : string | tuple
  1065. The color of the instantaneous amplitude signal.
  1066. cmap : string
  1067. The colormap for the IMFs
  1068. alpha : float {0-1}
  1069. The color saturation of the IMF signals.
  1070. ls : string
  1071. Linestyle for the IMF signals.
  1072. lw_X, lw_imfs, lw_IA : int
  1073. The linewidth of the signal, IMFs and amplitudes, respectively.
  1074. spaceFactor : float
  1075. Increase this value to space out the IMF signals more from each other.
  1076. X_shift, imfs_shift : float
  1077. Move the original signal or IMFs (respectively) up or down
  1078. flipCols : bool
  1079. Should the IMF colors be reversed
  1080. focus_imfis : ndarray | None
  1081. the IMF indices to focus on
  1082. unfocus_col : string | tuple
  1083. The color of the IMFs which are not to be focussed on (if there focus_imfis is not None)
  1084. unfocus_alpha : float {0-1}
  1085. The color saturation of the IMFs which are not to be focussed on (if there focus_imfis is not None)
  1086. unfocus_lw : int
  1087. The linewidth of the IMFs which are not to be focussed on (if there focus_imfis is not None)
  1088. zorder : int
  1089. Higher numbers will be plotted on top.
  1090. return_imfYs : bool
  1091. Should the Y-values of the IMFs plotted be returned.
  1092. """
  1093. if window is not None:
  1094. st, en = window
  1095. else:
  1096. st, en = [0, imfs.shape[0]-1]
  1097. if imf_cols is None:
  1098. try:
  1099. imf_cols = sns.color_palette(cmap, imfs.shape[1])
  1100. except:
  1101. imf_cols = [cmap]*imfs.shape[1]
  1102. #
  1103. if flipCols:
  1104. imf_cols = imf_cols[::-1]
  1105. if X is None:
  1106. X = imfs[st:en, :].sum(axis=1)
  1107. else:
  1108. X = X[st:en]
  1109. if IA is not None:
  1110. IA4plot = IA[st:en, :].T
  1111. if timeAx is None:
  1112. timeAx = np.linspace(0, len(X)/sample_rate, len(X))
  1113. plt.plot(timeAx, X+X_shift, color=color_X, lw=lw_X, zorder=zorder)
  1114. lfpMin, lfpMax = [f(X) for f in [np.min, np.max]]
  1115. emdYSt = lfpMin - (lfpMax-lfpMin)*spaceFactor + imfs_shift
  1116. imfSpace = (lfpMax-lfpMin)*spaceFactor
  1117. imfs4plot = imfs[st:en, :].T
  1118. lfpMin, lfpMax = [f(X) for f in [np.min, np.max]]
  1119. emdYSt = lfpMin - (lfpMax-lfpMin)*spaceFactor + imfs_shift
  1120. imfSpace = (lfpMax-lfpMin)*spaceFactor
  1121. imfYs = []
  1122. for imfi, imfTrace in enumerate(imfs4plot):
  1123. if focus_imfis is None:
  1124. col = imf_cols[imfi]
  1125. alpha = 1
  1126. lw = lw_imfs
  1127. else:
  1128. if imfi in focus_imfis:
  1129. col = imf_cols[imfi]
  1130. alpha = alpha
  1131. lw = lw_imfs
  1132. zorder = 3
  1133. else:
  1134. col = unfocus_col
  1135. alpha = unfocus_alpha
  1136. lw = unfocus_lw
  1137. zorder = 2
  1138. plt.plot(timeAx, imfTrace+emdYSt-(imfSpace*imfi), color=col, alpha=alpha, ls=ls, lw=lw, zorder=zorder)
  1139. imfYs.append(emdYSt-(imfSpace*imfi))
  1140. if IA is not None:
  1141. plt.plot(timeAx, IA4plot[imfi]+emdYSt-(imfSpace*imfi), color=col_IA, alpha=alpha, ls=ls, lw=lw_IA, zorder=zorder)
  1142. plt.xlim(timeAx[0], timeAx[-1])
  1143. if return_imfYs:
  1144. return imfYs
  1145. def plot_imfPSDs(freqAx_psd, imfPSDs, normalise=True, fill=True, alpha=0.5, space=0.7, imf_cols=None, cmap='husl'):
  1146. """
  1147. Plot the Power Spectral Density (PSD) estimates of IMFs.
  1148. Parameters
  1149. ----------
  1150. freqAx_psd : ndarray
  1151. 1D frequency axis array, corresponding to the last dimension of imfPSDs.
  1152. imfPSDs : ndarray
  1153. 2D [N_imfs x frequency] array; returned by get_imfPSDs().
  1154. normalise : bool
  1155. Should the PSDs of each IMF be normalised by their maximum values.
  1156. fill : bool
  1157. Should the area under each PSD be filled.
  1158. alpha : float {0-1}
  1159. The saturation of the PSD fill color.
  1160. space : float
  1161. The Y-spacing between each PSD.
  1162. imf_cols : ndarray | None
  1163. Colors for each IMF. If None, the colors will be determined by the argument, cmap.
  1164. cmap : string
  1165. The colormap for the IMFs.
  1166. """
  1167. if imf_cols is None:
  1168. imf_cols = sns.color_palette(cmap, imfPSDs.shape[0])
  1169. if cmap in ['husl', 'Spectral']:
  1170. imf_cols = imf_cols[::-1]
  1171. for imfi, psd in enumerate(imfPSDs):
  1172. if normalise:
  1173. psd /= psd.max()
  1174. y = psd-imfi*space
  1175. plt.plot(freqAx_psd, y, color=imf_cols[imfi])
  1176. if fill:
  1177. plt.fill_between(freqAx_psd, np.zeros_like(y)-imfi*space, y, color=imf_cols[imfi], zorder=imfi, alpha=alpha)
  1178. plt.yticks([])
  1179. plt.xscale('log')
  1180. def figplot_tmEMD(Xs, xi, it_mask_freqs, it_X_scores, sample_rate, mixScore_func, log_mixScore=False, show_variants=True,
  1181. variants=['EMD', 'eEMD', 'itEMD'], psd_func=get_psd, lw_variant=2, show_egs=True, window=None,
  1182. eg_percs=[80, 30, 0], imf_cols=None, cmap='husl', fill=True, set_style=True, spaceFactor=0.2, fontsize=16,
  1183. ms=4, ms_=6, nSecs=30, title=None, opt2xi=False, large=True, pad_egs=False):
  1184. """
  1185. Plot a figure to visualise the tmEMD process.
  1186. Parameters
  1187. ----------
  1188. Xs : list
  1189. Each element is a 1D array containing a sample time-series used to tune the optimisation.
  1190. xi : int
  1191. The X-index for the example signal to plot.
  1192. it_mask_freqs : list | ndarray
  1193. 2D array [N_sub-iterations x N_maskFreqs] containing the mask frequencies used for each mEMD sub-iteration.
  1194. it_mix_scores : list | ndarray
  1195. 2D array [N_sub-iterations x N_X] containing the mode mixing scores yeilded for each sub-iteration for each X.
  1196. sample_rate : float
  1197. The sampling rate for all the data provided in Xs.
  1198. mixScore_func : function
  1199. The function used to compute the mode mixing between IMFs. It should take three arguments:
  1200. (imfs, imfis_4_scoring, sample_rate) and return a single number; lower meaning less mode mixing (desirable)
  1201. log_mixScore : bool
  1202. Should the y-axis measuring mode mixing be logarithmic.
  1203. show_variants : bool
  1204. Should the mode mixing scores of EMD variants be shown.
  1205. variants : list
  1206. Each entry is a string variant (see the variable, variants) whereby the mode mixing resulting from this EMD variant is
  1207. also measured (if show_variants=True).
  1208. psd_func : function
  1209. Function to compute the Power Spectral Density (PSD) of IMFs.
  1210. The function should take two arguments: (X, sample_rate) and return the frequency axis and PSD.
  1211. The frequency axis returned should cover an appropriate range for frequencies of interest.
  1212. lw_variant : int
  1213. The linewidth corresponding to the mode mixing score of an EMD variant.
  1214. show_egs : bool
  1215. Should example IMFs (of Xs[xi]) yeilded by mask frequencies be shown.
  1216. window : tuple | None
  1217. Start and end indices of example signal to plot. If None, then the whole time window of Xs[xi] will be plotted.
  1218. eg_percs : list
  1219. For each percentile in this list, the mask frequencies corresponding to the percentile-matched mode mixing score will be
  1220. used for example mEMD operations.
  1221. imf_cols : ndarray | None
  1222. Colors for each IMF. If None, the colors will be determined by the argument, cmap.
  1223. cmap : string
  1224. The colormap for the IMFs.
  1225. fill : bool
  1226. Should the area under each PSD be filled.
  1227. set_style : bool
  1228. Should the background of the figure be determined by the cmap.
  1229. spaceFactor : float
  1230. Increase this value to space out the IMF signals more from each other.
  1231. fontsize : int
  1232. The fontsize of the axes text.
  1233. ms, ms_ : int
  1234. The markersize of the mask frequency and example-plot mask frequency points, respectively.
  1235. nSecs : float
  1236. The number of seconds of the example signal to plot (unless the argument, window is specified).
  1237. title : string
  1238. The title for the figure
  1239. opt2xi : bool
  1240. Should the mode mixing scores to be plotted be those just for the example X-index, xi, or should the mean mode mixing
  1241. score across all Xs be taken
  1242. large : bool
  1243. Should the plot be large or smaller. Large is recommended for more complex data
  1244. pad_egs : bool
  1245. Should there be a padding between each example mEMD operation plot.
  1246. """
  1247. if cmap in ['Spectral']:
  1248. color_text, color_eg, color_X = ['w']*3
  1249. if set_style:
  1250. set_plotStyle(1)
  1251. else:
  1252. color_text, color_eg, color_X = ['k']*3
  1253. if set_style:
  1254. set_plotStyle(0)
  1255. facecolor=None
  1256. _, label = mixScore_func(None, None, None, compute=False, return_label=True)
  1257. if opt2xi:
  1258. it_X_scores_M = it_X_scores[:, xi]
  1259. else:
  1260. it_X_scores_M = it_X_scores.mean(axis=1)
  1261. X = Xs[xi]
  1262. if window is None:
  1263. if nSecs > len(X)/sample_rate:
  1264. window = [0, len(X)-1]
  1265. else:
  1266. nSamples = int(sample_rate*nSecs)
  1267. start = np.random.choice(np.arange(len(X)-nSamples))
  1268. end = start+nSamples
  1269. window = start, end
  1270. if show_egs:
  1271. eg_percs = sorted(eg_percs)[::-1]
  1272. eg_inds = np.array([get_nearestInd(np.nanpercentile(np.unique(it_X_scores_M), p), it_X_scores_M) for p in eg_percs])
  1273. if large:
  1274. w_freqs = [8, 6][show_egs]
  1275. w_imfs = 20
  1276. w_psd = 6
  1277. wTot = w_freqs + w_imfs + w_psd
  1278. h_eg = 7
  1279. hTot = h_eg*len(eg_percs)
  1280. else:
  1281. w_freqs = 6
  1282. w_imfs = 12
  1283. w_psd = 3
  1284. wTot = w_freqs + w_imfs + w_psd
  1285. h_eg = [4, 2][len(eg_percs) > 3]
  1286. hTot = h_eg*len(eg_percs)
  1287. else:
  1288. eg_inds = np.array([])
  1289. w_freqs = 8
  1290. wTot = w_freqs
  1291. hTot = 6
  1292. if large:
  1293. hspace, wspace = 3, 3
  1294. else:
  1295. hspace, wspace = 0.1, 0.2
  1296. if pad_egs:
  1297. h_eg -= 1
  1298. h_pad = 1
  1299. else:
  1300. h_pad = 0
  1301. plt.figure(figsize=(wTot, hTot))
  1302. grid = plt.GridSpec(hTot, wTot, hspace=hspace, wspace=wspace)
  1303. currW = 0
  1304. # plot maskFreq space
  1305. plt.subplot(grid[:, currW:(currW+w_freqs)], facecolor=facecolor)
  1306. plt.title(title, loc='left', fontweight='bold', color=color_text)
  1307. plt.xticks(fontsize=fontsize-2)
  1308. plt.yticks(fontsize=fontsize-2)
  1309. imf_cols = plot_mask_freq_scores(it_mask_freqs, it_X_scores, xi=xi, log_mixScore=log_mixScore, imf_cols=imf_cols,
  1310. cmap=cmap, inds=eg_inds, ms=ms, color_=color_eg, ms_=ms_)
  1311. if show_variants:
  1312. max_imfs = it_mask_freqs.shape[-1]
  1313. fmin, fmax = 0, np.round(np.nanmax(it_mask_freqs), -1)
  1314. variant_colors = sns.color_palette('Set1', len(variants))
  1315. for variant, color in zip(variants, variant_colors):
  1316. if opt2xi:
  1317. labelScores = get_modeMixScores_4_emd([Xs[xi]], sample_rate, variant, psd_func, max_imfs, mixScore_funcs=[mixScore_func])
  1318. else:
  1319. labelScores = get_modeMixScores_4_emd(Xs, sample_rate, variant, psd_func, max_imfs, mixScore_funcs=[mixScore_func])
  1320. score = labelScores[label].mean()
  1321. plt.hlines(score, fmin, fmax, color=color, linestyles='--', lw=lw_variant, label=variant)
  1322. l = plt.legend()
  1323. for text in l.get_texts():
  1324. text.set_color(color_text)
  1325. plt.xlabel('Mask freq. (Hz)', fontsize=fontsize)
  1326. plt.ylabel(label, fontsize=fontsize)
  1327. plt.xscale('log')
  1328. currW += w_freqs
  1329. # plot e.g. mEMDs
  1330. if show_egs:
  1331. currW0 = np.copy(currW)
  1332. currH = 0
  1333. for egi, ind in enumerate(eg_inds):
  1334. mask_freqs = it_mask_freqs[ind]
  1335. sift_config = emd.sift.get_config('mask_sift')
  1336. sift_config['mask_freqs'] = mask_freqs/sample_rate
  1337. sift_config['max_imfs'] = len(mask_freqs)
  1338. currW = np.copy(currW0)
  1339. imfs = emd.sift.mask_sift(X, **sift_config)
  1340. freqAx_psd, imfPSDs = get_imfPSDs(imfs, sample_rate)
  1341. plt.subplot(grid[currH:(currH+h_eg), currW:(currW+w_imfs)], facecolor=facecolor)
  1342. plt.xticks(fontsize=fontsize-2)
  1343. plt.yticks([])
  1344. plot_emd(imfs, sample_rate, window=window, imf_cols=imf_cols, color_X=color_X, lw_imfs=2, spaceFactor=spaceFactor)
  1345. if egi == len(eg_inds)-1:
  1346. plt.xlabel('Time (s)', fontsize=fontsize)
  1347. currW += w_imfs
  1348. plt.subplot(grid[currH:(currH+h_eg), currW:(currW+w_psd)], facecolor=facecolor)
  1349. plt.xticks(fontsize=fontsize-2)
  1350. plot_imfPSDs(freqAx_psd, imfPSDs, fill=fill, imf_cols=imf_cols)
  1351. if egi == len(eg_inds)-1:
  1352. plt.xlabel('Freq. (Hz)', fontsize=fontsize)
  1353. currW += w_psd
  1354. currH += h_eg + h_pad

tmEMD.py, under CC-BY-4.0 · at the source

Overview

Authors: Adrien A Causse1,2, Jonathan Curot3,4, Vítor Lopes-dos-Santos1,2, Raphaël Nunes-da-Silva1, Helen C Barron1,2,5, Vincent Dornier3, Marie Denuelle3,4, Amaury De Barros6,7, Jean-Christophe Sol6,7, Jean-Albert Lotterie6,7, Katia Lehongre8, Sara Fernandez-Vidal8, Valerio Frazzini9,10, Vincent Navarro9,10, Luc Valton3,4, Emmanuel J Barbeau3, Tim Denison1,2, Leila Reddy3, David Dupret1,2
  1. Brain Network Dynamics Unit, Nuffield Department of Clinical Neurosciences, University of Oxford, Oxford, UK
  2. Medical Research Council Centre of Research Excellence in Restorative Neural Dynamics, Oxford, UK
  3. CerCo, CNRS UMR5549, University of Toulouse, Toulouse, France
  4. Brain Electrophysiology, Epilepsy and Sleep Unit, Neurology Department, Toulouse University Hospital, Toulouse, France
  5. Oxford Centre for Integrative Neuroimaging, University of Oxford, FMRIB, John Radcliffe Hospital, Oxford, UK
  6. Department of Neurology and Neurosurgery, Toulouse University Hospital, Toulouse, France
  7. Toulouse Neuro Imaging Center, INSERM, U1214, Toulouse, France
  8. Centre de Neuro-Imagerie de Recherche, ICM Paris Brain Institute, Pitié-Salpêtrière Hospital, Paris, France
  9. Sorbonne Université, Paris Brain Institute, ICM, Inserm, CNRS, Pitié-Salpêtrière Hospital, Paris, France
  10. Assistance Publique-Hôpitaux de Paris, Epilepsy and EEG Units and Reference Center of Rare Epilepsies, ERN EpiCare, Pitié-Salpêtrière Hospital, Paris, France
Journal: Neuron, pages S0896-6273(26)00375-2
Dates: published online 1 June 2026; in print June 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1016/j.neuron.2026.05.004 · PMID 42225066 · PMCID PMC7619154 · OpenAlex W7163068909
Open access: hybrid, a free copy (OpenAlex)
Status: code verified
Categories: intracranial EEG (iEEG / ECoG / SEEG) (modality), human (organism), systems (subfield)
Methods: Spectral & time-frequency, Preprocessing, Connectivity, Statistics, Machine learning, Evoked potentials, fMRI & imaging, Single-unit activity, calcium imaging, Smoothing, state filtering, decompositions, Physiology & signal measures
Keywords: Memory, Hippocampus, human brain, Intracranial Eeg, Medial Temporal Lobe, Theta, Ripples, Single Neurons, Network Oscillations, Population Spiking
Topic: Neural dynamics and brain function (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: Medical Research Council (MC_UU_00003/4, MR/W004860/1); UK Research and Innovation; UK Research and Innovation Medical Research Council
Citations: not cited yet (Europe PMC); 94 references in the paper

Abstract

Memory processing requires coordinated engagement of neuronal populations across brain networks and over time. How such coordination is organized in the human medial temporal lobe (MTL) remains unclear. Here, we show that MTL population activity is dynamically structured by a transient slow-oscillatory architecture that emerges during learning to promote offline consolidation and later recall. Using intracranial recordings that combine single-neuron spiking activity and local field potentials in human participants, we find that mnemonic engagement elicits on-demand slow-oscillatory bursts in the hippocampus. These hippocampal bursts synchronize gamma-band patterns across MTL regions, defining discrete coordination events that pace cross-regional coactivity motifs during learning. These learning-evoked population motifs are selectively reactivated during hippocampal ripples in post-learning rest, and the strength of their reactivation predicts subsequent recall accuracy. Together, these findings identify a multi-scale coordination mechanism that links distributed population activity across learning, consolidation, and recall in humans.

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 1 match between paragraphs and lines of code.

Zenodo 10351412

License: CC-BY-4.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: the resources table
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Matplotlib (3 files), NumPy (3 files), SciPy (2 files), seaborn (2 files)
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
5 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;
  • 3 scripts, each with its path and the digest of its content;
  • 1 match 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 and code availability

The electrophysiology dataset reported in this study is being used in ongoing projects and can be accessed under a data transfer agreement due to data protection requirements. We welcome inquiries for sharing it—please contact the lead contact.

This paper does not report original code.

Any additional information required to reanalyze the data reported in this paper is available from the lead contact upon request.

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 2, 28 September 2026

  • Publisher: n/a → Cell Press
  • Authors: added David Dupret (0000-0002-0040-1766); removed David Dupret

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, pages, dates, 19 authors, 10 keywords, 3 funders, 87 references.

Cite

This paper

Causse, A. A., Curot, J., Lopes-dos-Santos, V., Nunes-da-Silva, R., Barron, H. C., Dornier, V., Denuelle, M., De Barros, A., Sol, J.-C., Lotterie, J.-A., Lehongre, K., Fernandez-Vidal, S., Frazzini, V., Navarro, V., Valton, L., Barbeau, E. J., Denison, T., Reddy, L., & Dupret, D. (2026). A learning-evoked slow-oscillatory architecture paces population activity for offline reactivation across the human medial temporal lobe. Neuron, S0896-6273(26)00375-2. https://doi.org/10.1016/j.neuron.2026.05.004

BibTeX

@article{causse2026learning,
author = {Causse, Adrien A and Curot, Jonathan and Lopes-dos-Santos, Vítor and Nunes-da-Silva, Raphaël and Barron, Helen C and Dornier, Vincent and Denuelle, Marie and De Barros, Amaury and Sol, Jean-Christophe and Lotterie, Jean-Albert and Lehongre, Katia and Fernandez-Vidal, Sara and Frazzini, Valerio and Navarro, Vincent and Valton, Luc and Barbeau, Emmanuel J and Denison, Tim and Reddy, Leila and Dupret, David},
title = {{A learning-evoked slow-oscillatory architecture paces population activity for offline reactivation across the human medial temporal lobe}},
journal = {Neuron},
year = {2026},
month = jun,
pages = {S0896--6273(26)00375--2},
publisher = {Cell Press},
issn = {0896-6273},
doi = {10.1016/j.neuron.2026.05.004},
url = {https://doi.org/10.1016/j.neuron.2026.05.004},
pmid = {42225066},
pmcid = {PMC7619154}
}

RIS

TY - JOUR
AU - Causse, Adrien A
AU - Curot, Jonathan
AU - Lopes-dos-Santos, Vítor
AU - Nunes-da-Silva, Raphaël
AU - Barron, Helen C
AU - Dornier, Vincent
AU - Denuelle, Marie
AU - De Barros, Amaury
AU - Sol, Jean-Christophe
AU - Lotterie, Jean-Albert
AU - Lehongre, Katia
AU - Fernandez-Vidal, Sara
AU - Frazzini, Valerio
AU - Navarro, Vincent
AU - Valton, Luc
AU - Barbeau, Emmanuel J
AU - Denison, Tim
AU - Reddy, Leila
AU - Dupret, David
TI - A learning-evoked slow-oscillatory architecture paces population activity for offline reactivation across the human medial temporal lobe
T2 - Neuron
J2 - Neuron
PY - 2026
DA - 2026/06/01
SP - S0896
EP - 6273(26)00375-2
SN - 0896-6273
PB - Cell Press
DO - 10.1016/j.neuron.2026.05.004
UR - https://doi.org/10.1016/j.neuron.2026.05.004
LA - en
ER -

CSL-JSON

{
"id": "10.1016/j.neuron.2026.05.004",
"type": "article-journal",
"title": "A learning-evoked slow-oscillatory architecture paces population activity for offline reactivation across the human medial temporal lobe",
"container-title": "Neuron",
"author": [
{
"family": "Causse",
"given": "Adrien A"
},
{
"family": "Curot",
"given": "Jonathan"
},
{
"family": "Lopes-dos-Santos",
"given": "Vítor"
},
{
"family": "Nunes-da-Silva",
"given": "Raphaël"
},
{
"family": "Barron",
"given": "Helen C"
},
{
"family": "Dornier",
"given": "Vincent"
},
{
"family": "Denuelle",
"given": "Marie"
},
{
"family": "De Barros",
"given": "Amaury"
},
{
"family": "Sol",
"given": "Jean-Christophe"
},
{
"family": "Lotterie",
"given": "Jean-Albert"
},
{
"family": "Lehongre",
"given": "Katia"
},
{
"family": "Fernandez-Vidal",
"given": "Sara"
},
{
"family": "Frazzini",
"given": "Valerio"
},
{
"family": "Navarro",
"given": "Vincent"
},
{
"family": "Valton",
"given": "Luc"
},
{
"family": "Barbeau",
"given": "Emmanuel J"
},
{
"family": "Denison",
"given": "Tim"
},
{
"family": "Reddy",
"given": "Leila"
},
{
"family": "Dupret",
"given": "David"
}
],
"container-title-short": "Neuron",
"page": "S0896-6273(26)00375-2",
"DOI": "10.1016/j.neuron.2026.05.004",
"PMID": "42225066",
"PMCID": "PMC7619154",
"ISSN": "0896-6273",
"publisher": "Cell Press",
"URL": "https://doi.org/10.1016/j.neuron.2026.05.004",
"language": "en",
"issued": {
"date-parts": [
[
2026,
6,
1
]
]
}
}

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.1038/s41593-026-02357-2 [code]
Experience reorganizes content-specific memory traces in macaques.
Journal: Nature neuroscience
In common: seaborn, SciPy, Matplotlib, 1 other tool, 9 references
[2] doi:10.1016/j.celrep.2026.117646 [code]
Medial entorhinal-hippocampal desynchronization parallels the emergence of memory impairment in a mouse model of Alzheimer's disease pathology.
Journal: Cell reports
In common: SciPy, Matplotlib, NumPy, systems, 8 references
[3] doi:10.7554/elife.110795 [code]
REM sleep prefrontal high-frequency oscillation chains mediate distinct cortical - hippocampal reactivation patterns compared to NREM sleep.
Journal: eLife
In common: 10 references
[4] doi:10.1093/sleep/zsag168 [code]
Deltas' and spindles' cross-area synchronization and ripple subtypes.
Journal: Sleep
In common: SciPy, Matplotlib, NumPy, 8 references
[5] doi:10.7554/elife.108023 [code]
Challenges in replay detection by TDLM in post-encoding resting state.
Journal: eLife
In common: seaborn, SciPy, Matplotlib, 1 other tool, 8 references
[6] doi:10.3389/fncom.2026.1786996 [code]
Schumann-anchored golden ratio organization of human neural oscillations.
Journal: Frontiers in computational neuroscience
In common: seaborn, SciPy, Matplotlib, 1 other tool, systems, 8 references
[7] doi:10.1038/s41467-026-75345-6 [code]
Hippocampal ripples initiate cortical dimensionality expansion for memory retrieval.
Journal: Nature communications
In common: 9 references
[8] doi:10.1002/hipo.70089 [code]
The Role of Plasticity in Replay: Stability Through Anti-Hebbian Rules.
Journal: Hippocampus
In common: seaborn, SciPy, Matplotlib, 1 other tool, 6 references
[9] doi:10.1038/s41467-026-77318-1 [code]
Offline generative network reconfiguration guides insight-like accelerated learning by assimilation into schema in rats.
Journal: Nature communications
In common: seaborn, SciPy, Matplotlib, systems, 6 references
[10] doi:10.1093/braincomms/fcag255 [code]
Impaired consolidation of spatial memory during sleep in patients with leucine-rich glioma-inactivated 1-associated limbic encephalitis.
Journal: Brain communications
In common: Matplotlib, NumPy, 7 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.