OSCR

A generative framework for estimating brain energy landscapes from continuous neuroimaging observations.

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 · 2 of them tie a paragraph to a whole file without given lines: weak matches, whose lines are not tinted
  1. [1] § Methods › EEG leadfield observation model ↔ pmem_leadfield.py, lines 1 to 36 · score 0.71 · sensor noise, sensor space, compound symmetry, forward model, maps, mixing · Report a mismatch
  2. [2] § Methods › EEG leadfield observation model ↔ pmem_leadfield.py, lines 116 to 171 · score 0.69 · signal subtracted residuals, source signal, compound symmetry, bulk, raw, fit · Report a mismatch
  3. [3] § Methods › Experimental datasets and target network ↔ __init__.py, the whole file · a weak match · score 0.66 · continuous neuroimaging, source space, forward model, pMEM, MEG, simulation · Report a mismatch
  4. [4] § Results › Parameter recovery under cross-channel residual correlation › Recovery evaluation at a representative correlation level ↔ pmem_obs.py, lines 133 to 158 · score 0.65 · cross channel noise, GMM thresholding, full model, correlation, demean, node · Report a mismatch
  5. [5] § Methods › The EM algorithm › Initialization and convergence ↔ pmem_leadfield.py, lines 116 to 171 · score 0.63 · subtracted residual, source signal, regressing, median, raw, leadfield · Report a mismatch
  6. [6] § Methods › EEG leadfield observation model ↔ pmem_leadfield.py, lines 1 to 36 · score 0.61 · cortical source, sensor space, binary states, leadfield, EEG, model · Report a mismatch
  7. [7] § Methods › EEG leadfield observation model ↔ __init__.py, the whole file · a weak match · score 0.53 · leadfield matrix, sensor space, observation model, MEG, EEG · Report a mismatch

Paper

Loaded from Europe PMC by your browser; OSCR does not store it: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 409 lines · 18 KB · no license · 4 matches

  1. """
  2. Hierarchical pMEM for EEG with a lead-field forward model.
  3. Model
  4. -----
  5. z(t) ~ pMEM(h, J) binary state at each of N cortical sources
  6. s(t) = beta0 + beta1 ⊙ z(t) source amplitude
  7. y(t) = L s(t) + eps(t), eps ~ N(0, Sigma_eps)
  8. Sigma_eps = sigma2 * (I + rho*(J_M - I)) compound-symmetry sensor noise (background EEG)
  9. What this file fixes relative to `pmem.pMEM_Leadfield`
  10. ------------------------------------------------------
  11. 1. Mean-field reference for the M-step uses EXACT enumeration of the pMEM
  12. when N <= MAX_EXACT_N, else block-Gibbs marginals. No more chasing a
  13. biased single-mode mean-field fixed point.
  14. 2. Closed-form WLS update for (beta0, beta1) jointly given m, accounting for
  15. the lead-field mixing (i.e. L^T P L is dense). Original code used
  16. element-wise heuristics that ignored cross-source coupling through L.
  17. 3. Gauge / sign-flip protection: enforces the canonical convention "active
  18. state corresponds to higher signal in the sensor-projected source space"
  19. so that latent identifiability is resolved consistently across runs.
  20. 4. Sensor-space noise sigma2 estimated from data.
  21. 5. ELBO tracking, MAP latent extraction, source-projected warm start using a
  22. Tikhonov-regularized minimum-norm inverse.
  23. 6. Ridge prior on beta0 to handle the null space of L (M < N).
  24. """
  25. from __future__ import annotations
  26. import itertools
  27. from dataclasses import dataclass, field
  28. import numpy as np
  29. from scipy.special import expit
  30. MAX_EXACT_N = 14 # 2^14 = 16384 states; beyond this fall back to Gibbs
  31. # ------------------------------------------------------------
  32. # pMEM prior helpers
  33. # ------------------------------------------------------------
  34. def _enumerate_states(N: int) -> np.ndarray:
  35. return np.array(list(itertools.product([0, 1], repeat=N)), dtype=float)
  36. def exact_prior_moments(h, J, Zs=None):
  37. N = len(h)
  38. if Zs is None:
  39. Zs = _enumerate_states(N)
  40. J_up = np.triu(J, 1)
  41. E = Zs @ h + np.einsum('ki,ij,kj->k', Zs, J_up, Zs)
  42. E -= E.max()
  43. w = np.exp(E)
  44. w /= w.sum()
  45. m0 = w @ Zs
  46. m0_outer = (Zs.T * w) @ Zs
  47. return m0, m0_outer
  48. def gibbs_prior_moments(h, J, n_burn=200, n_samples=2000, rng=None):
  49. """Fallback prior moment estimator for N > MAX_EXACT_N."""
  50. if rng is None:
  51. rng = np.random.default_rng()
  52. N = len(h)
  53. z = rng.integers(0, 2, size=N).astype(float)
  54. m1 = np.zeros(N)
  55. m2 = np.zeros((N, N))
  56. for k in range(n_burn + n_samples):
  57. for i in range(N):
  58. field = h[i] + np.dot(J[i], z) - J[i, i] * z[i]
  59. z[i] = float(rng.random() < expit(field))
  60. if k >= n_burn:
  61. m1 += z
  62. m2 += np.outer(z, z)
  63. m1 /= n_samples
  64. m2 /= n_samples
  65. return m1, m2
  66. def make_sym_upper(M):
  67. M = np.triu(M, 1)
  68. M = M + M.T
  69. np.fill_diagonal(M, 0.0)
  70. return M
  71. # ------------------------------------------------------------
  72. # Lead-field pMEM
  73. # ------------------------------------------------------------
  74. @dataclass
  75. class pMEM_Leadfield_v2:
  76. Y: np.ndarray # [T, M]
  77. L: np.ndarray # [M, N]
  78. n_iter: int = 80
  79. lr_h: float = 0.2
  80. lr_J: float = 0.2
  81. sigma2: float | None = None # sensor noise variance; estimated if None
  82. rho: float | None = None # compound-symmetry correlation; estimated if None
  83. use_exact_prior: bool = True
  84. use_exact_posterior: bool = True # exact enumeration for E-step (N<=MAX_EXACT_N)
  85. sign_flip_protection: bool = True
  86. estimate_noise: bool = True
  87. beta0_ridge: float = 1e-2 # Tikhonov on beta0 (handles M<N null space)
  88. seed: int = 0
  89. # populated in fit
  90. h: np.ndarray = field(init=False)
  91. J: np.ndarray = field(init=False)
  92. beta0: np.ndarray = field(init=False)
  93. beta1: np.ndarray = field(init=False)
  94. m: np.ndarray = field(init=False)
  95. elbo_history: list = field(default_factory=list)
  96. def __post_init__(self):
  97. self.T, self.M = self.Y.shape
  98. _, self.N = self.L.shape
  99. rng = np.random.default_rng(self.seed)
  100. # ---- sensor noise: initialize rho from signal-subtracted residuals ----
  101. # Raw Y includes source signal (L s) which is NOT compound-symmetric and
  102. # would bias rho upward. A quick Tikhonov inverse removes the bulk of
  103. # the signal before fitting the compound-symmetry noise model.
  104. if self.sigma2 is None:
  105. Yc = self.Y - self.Y.mean(axis=0, keepdims=True)
  106. LtL = self.L.T @ self.L
  107. lam_init = 0.1 * np.trace(LtL) / max(self.N, 1)
  108. W = np.linalg.solve(LtL + lam_init * np.eye(self.N), self.L.T)
  109. R0 = Yc - (Yc @ W.T) @ self.L.T # [T, M] approx noise residual
  110. S = (R0.T @ R0) / self.T
  111. self.sigma2 = float(np.diag(S).mean())
  112. off = (S.sum() - np.trace(S)) / (self.M * (self.M - 1))
  113. self.rho = float(np.clip(off / (self.sigma2 + 1e-9), -0.3, 0.95))
  114. if self.rho is None:
  115. self.rho = 0.0
  116. self._update_P()
  117. # ---- pMEM init ------------------------------------------------
  118. self.h = np.zeros(self.N)
  119. J0 = 0.01 * rng.standard_normal((self.N, self.N))
  120. self.J = make_sym_upper(J0)
  121. # ---- warm start: MNE-style Tikhonov inverse -------------------
  122. Yc = self.Y - self.Y.mean(axis=0, keepdims=True)
  123. # noise-normalized leadfield gain ≈ scale of L^T y_t
  124. LtL = self.L.T @ self.L
  125. lam_w = 1e-2 * np.trace(LtL) / max(self.N, 1)
  126. W = np.linalg.solve(LtL + lam_w * np.eye(self.N), self.L.T)
  127. S_hat = Yc @ W.T # [T, N] source estimates
  128. med = np.median(S_hat, axis=0, keepdims=True)
  129. z_warm = (S_hat > med).astype(float)
  130. self.m = 0.05 + 0.90 * z_warm
  131. # beta1 init by sign-aware regression of S_hat on z_warm
  132. b1 = np.zeros(self.N)
  133. for i in range(self.N):
  134. zi = z_warm[:, i] - z_warm[:, i].mean()
  135. num = ((S_hat[:, i] - S_hat[:, i].mean()) * zi).sum()
  136. den = (zi ** 2).sum() + 1e-9
  137. b1[i] = num / den
  138. self.beta1 = np.maximum(np.abs(b1), 0.1)
  139. # beta0 closed form below; placeholder
  140. self.beta0 = S_hat.mean(axis=0) - self.beta1 * z_warm.mean(axis=0)
  141. if self.N <= MAX_EXACT_N:
  142. self._Zs = _enumerate_states(self.N)
  143. else:
  144. self._Zs = None
  145. # --------------------------------------------------------
  146. # Precision and projected leadfield quantities
  147. # --------------------------------------------------------
  148. def _update_P(self):
  149. a = max(1.0 - self.rho, 1e-4)
  150. b = self.rho
  151. denom = a * (a + self.M * b)
  152. I = np.eye(self.M)
  153. O = np.ones((self.M, self.M))
  154. self.P = (1.0 / max(self.sigma2, 1e-6)) * ((1.0 / a) * I - (b / denom) * O)
  155. self.LtPL = self.L.T @ self.P @ self.L # [N, N]
  156. self.LtP = self.L.T @ self.P # [N, M]
  157. self.LtPL_diag = np.diag(self.LtPL)
  158. # --------------------------------------------------------
  159. # Posterior natural parameters at time t
  160. # --------------------------------------------------------
  161. def _posterior_nat_params(self, y_t):
  162. # residual to baseline drive Lβ0
  163. r = y_t - self.L @ self.beta0
  164. # ∂/∂z_i of -0.5 (y - Lβ0 - L diag(β1) z)^T P (...) yields:
  165. # β1_i * [L^T P r]_i for the linear term
  166. # -0.5 * β1_i^2 * (L^T P L)_ii for the diagonal quadratic
  167. # -β1_i β1_j (L^T P L)_ij for the cross term in tilde_J
  168. Lp_r = self.LtP @ r # [N]
  169. tilde_h = self.h + self.beta1 * Lp_r \
  170. - 0.5 * (self.beta1 ** 2) * self.LtPL_diag
  171. B1B1 = np.outer(self.beta1, self.beta1)
  172. cross = B1B1 * self.LtPL
  173. np.fill_diagonal(cross, 0.0)
  174. tilde_J = self.J - cross
  175. return tilde_h, tilde_J
  176. # --------------------------------------------------------
  177. # E-step (mean-field OR exact enumeration)
  178. #
  179. # When exact posterior is used, we also store the pairwise posterior
  180. # <z_i z_j> per time step so the M-step J update is not biased by the
  181. # mean-field independence assumption.
  182. # --------------------------------------------------------
  183. def E_step(self, sweeps: int = 20):
  184. if self.use_exact_posterior and self.N <= MAX_EXACT_N:
  185. self._E_step_exact()
  186. else:
  187. self._E_step_meanfield(sweeps=sweeps)
  188. def _E_step_meanfield(self, sweeps: int = 20):
  189. # Sweep i = N-1, N-2, ..., 0 (downward) so that j > i nodes (already
  190. # updated in this sweep) feed into node i's field — consistent with the
  191. # manuscript's sequential update direction.
  192. for t in range(self.T):
  193. tilde_h, tilde_J = self._posterior_nat_params(self.Y[t])
  194. m_t = self.m[t].copy()
  195. for _ in range(sweeps):
  196. m_old = m_t.copy()
  197. for i in range(self.N - 1, -1, -1):
  198. field = tilde_h[i] + np.dot(tilde_J[i], m_t) # tilde_J diagonal is 0
  199. m_t[i] = expit(field)
  200. if np.max(np.abs(m_t - m_old)) < 1e-6:
  201. break
  202. self.m[t] = np.clip(m_t, 1e-4, 1.0 - 1e-4)
  203. # mean-field has no exact pairwise moments stored
  204. self._m_outer_post = None
  205. def _E_step_exact(self):
  206. """
  207. Vectorised exact E-step: no Python loop over T.
  208. tilde_J is constant across time (depends only on fixed params).
  209. tilde_h(t) varies linearly with Y[t]. We batch all T at once:
  210. E_all[k,t] = z_k · tilde_h(t) + z_k · tJ_up · z_k (K×T)
  211. then softmax over k, and accumulate m_post and m_outer_post via
  212. matrix multiplies — loop-free and GPU-ready.
  213. """
  214. Zs = self._Zs # (K, N), K=2^N
  215. # ---- constant part: tilde_J and its quadratic contribution ----
  216. cross = np.outer(self.beta1, self.beta1) * self.LtPL
  217. np.fill_diagonal(cross, 0.0)
  218. tJ_up = np.triu(self.J - cross, 1) # (N, N) constant
  219. E_quad = np.einsum('ki,ij,kj->k', Zs, tJ_up, Zs) # (K,) constant
  220. # ---- batch tilde_h over all T ----
  221. # r(t) = Y[t] - L @ beta0 → R = Y - L@beta0 shape (T, M)
  222. # LtP @ r(t) = LtP @ R[t] → batch: R @ LtP.T shape (T, N)
  223. R = self.Y - (self.L @ self.beta0) # (T, M)
  224. LtP_R = R @ self.LtP.T # (T, N)
  225. h_const = self.h - 0.5 * (self.beta1 ** 2) * self.LtPL_diag # (N,)
  226. tilde_H = h_const + self.beta1 * LtP_R # (T, N)
  227. # ---- energy matrix ----
  228. E_lin = Zs @ tilde_H.T # (K, T)
  229. E_all = E_lin + E_quad[:, None] # (K, T)
  230. E_all -= E_all.max(axis=0) # numerical stability
  231. # ---- softmax weights ----
  232. w_all = np.exp(E_all) # (K, T)
  233. w_all /= w_all.sum(axis=0) # normalise over K
  234. # ---- posterior statistics ----
  235. m_post = w_all.T @ Zs # (T, N)
  236. w_sum = w_all.sum(axis=1) # (K,) Σ_t w[k,t]
  237. m_outer_post = (Zs.T * w_sum) @ Zs / self.T # (N, N)
  238. self.m = np.clip(m_post, 1e-4, 1.0 - 1e-4)
  239. self._m_outer_post = m_outer_post # for M-step
  240. # --------------------------------------------------------
  241. # Gauge protection: canonical "active ⇒ stronger projected source"
  242. # --------------------------------------------------------
  243. def _fix_sign_flips(self):
  244. # use Tikhonov-projected source estimate as the reference signal per node
  245. Yc = self.Y - self.Y.mean(axis=0, keepdims=True)
  246. LtL = self.L.T @ self.L
  247. lam_w = 1e-2 * np.trace(LtL) / max(self.N, 1)
  248. S_hat = Yc @ np.linalg.solve(LtL + lam_w * np.eye(self.N), self.L.T).T
  249. for i in range(self.N):
  250. if self.m[:, i].std() < 1e-6 or S_hat[:, i].std() < 1e-6:
  251. continue
  252. c = np.corrcoef(self.m[:, i], S_hat[:, i])[0, 1]
  253. if np.isnan(c) or c >= -0.05:
  254. continue
  255. self.m[:, i] = 1.0 - self.m[:, i]
  256. self.beta0[i] = self.beta0[i] + self.beta1[i]
  257. self.beta1[i] = -self.beta1[i]
  258. neg = self.beta1 < 0
  259. if np.any(neg):
  260. self.beta1[neg] = -self.beta1[neg]
  261. self.m[:, neg] = 1.0 - self.m[:, neg]
  262. self.beta0[neg] = self.beta0[neg] - self.beta1[neg]
  263. self.beta1 = np.maximum(self.beta1, 0.05)
  264. # --------------------------------------------------------
  265. # M-step
  266. # --------------------------------------------------------
  267. def M_step(self):
  268. # ---- (beta0, beta1): two alternating closed-form sweeps ----------
  269. # Both updates are optimal given the other fixed; two sweeps of the
  270. # outer EM loop are enough for practical convergence.
  271. for _ in range(2):
  272. # β0 — Tikhonov-regularized source-space WLS (δI handles null(L))
  273. r_bar = self.Y.mean(axis=0) - self.L @ (self.beta1 * self.m.mean(axis=0))
  274. A0 = self.LtPL + self.beta0_ridge * np.eye(self.N)
  275. self.beta0 = np.linalg.solve(A0, self.LtP @ r_bar)
  276. # β1 — N×N linear system A β1 = b, A_{kk'} = K_{kk'} * sum_t C_{kk'}(t)
  277. # K = L^T P L. Diagonal exact: C_{kk}(t) = m_k(t) (binary identity).
  278. # Off-diagonal: exact posterior E_q[z_k z_k'] when available (enumeration),
  279. # mean-field m_k m_k' + diagonal V(t) correction otherwise.
  280. R0 = self.Y - (self.L @ self.beta0)[None, :] # [T, M]
  281. G = R0 @ self.LtP.T # [T, N]
  282. numer = (self.m * G).sum(axis=0) # [N]
  283. if self._m_outer_post is not None:
  284. sum_C = self._m_outer_post * self.T # [N, N]: sum_t C(t)
  285. A1 = self.LtPL * sum_C # diagonal already exact
  286. else:
  287. mm = self.m.T @ self.m # [N, N]: sum_t m_i m_j
  288. sum_V = (self.m * (1.0 - self.m)).sum(axis=0) # [N]
  289. A1 = self.LtPL * mm + np.diag(self.LtPL_diag * sum_V)
  290. A1 += 1e-9 * np.eye(self.N)
  291. self.beta1 = np.maximum(np.linalg.solve(A1, numer), 0.05)
  292. # ---- h and J moment matching ---------------------------------
  293. m_bar = self.m.mean(axis=0)
  294. # Use exact posterior pairwise <z_i z_j> if available; otherwise fall
  295. # back to the (biased) mean-field outer product.
  296. if getattr(self, "_m_outer_post", None) is not None:
  297. m_outer = self._m_outer_post
  298. else:
  299. m_outer = (self.m.T @ self.m) / self.T
  300. if self.use_exact_prior and self.N <= MAX_EXACT_N:
  301. m0, m0_outer = exact_prior_moments(self.h, self.J, Zs=self._Zs)
  302. else:
  303. m0, m0_outer = gibbs_prior_moments(self.h, self.J)
  304. self.h += self.lr_h * (m_bar - m0)
  305. grad_J = np.triu(m_outer - m0_outer, 1)
  306. self.J += self.lr_J * grad_J
  307. self.J = make_sym_upper(self.J)
  308. # ---- sensor noise update (compound-symmetry) ------------------
  309. # E[r̄ r̄^T] = Σ_ε + L diag(β₁) Cov_q[z] diag(β₁) L^T
  310. # Subtract the sensor-space posterior-variance contamination to recover Σ_ε.
  311. if self.estimate_noise:
  312. S_drive = self.beta0[None, :] + self.beta1[None, :] * self.m
  313. mu = S_drive @ self.L.T # [T, M]
  314. R = self.Y - mu # [T, M]
  315. S = (R.T @ R) / self.T # [M, M]
  316. if self._m_outer_post is not None:
  317. avg_cov_q = self._m_outer_post - (self.m.T @ self.m) / self.T # [N, N]
  318. else:
  319. avg_cov_q = np.diag((self.m * (1.0 - self.m)).mean(axis=0)) # diagonal
  320. B = self.L * self.beta1[None, :] # [M, N]: L diag(β₁)
  321. S = S - B @ avg_cov_q @ B.T # [M, M]
  322. diag_var = max(float(np.diag(S).mean()), 1e-4)
  323. off_cov = float((S.sum() - np.trace(S)) / (self.M * (self.M - 1)))
  324. sigma2_new = diag_var
  325. rho_new = float(np.clip(off_cov / (sigma2_new + 1e-9), -0.3, 0.95))
  326. self.sigma2 = 0.5 * self.sigma2 + 0.5 * sigma2_new
  327. self.rho = 0.5 * self.rho + 0.5 * rho_new
  328. self._update_P()
  329. # --------------------------------------------------------
  330. # ELBO (exact prior log-Z when N small)
  331. # --------------------------------------------------------
  332. def _elbo(self):
  333. m_bar = self.m.mean(axis=0)
  334. m_outer = (self.m.T @ self.m) / self.T
  335. if self._Zs is not None:
  336. E = self._Zs @ self.h + np.einsum('ki,ij,kj->k', self._Zs, np.triu(self.J, 1), self._Zs)
  337. logZ = E.max() + np.log(np.exp(E - E.max()).sum())
  338. else:
  339. logZ = 0.0
  340. prior_term = self.T * (m_bar @ self.h
  341. + np.einsum('ij,ij->', np.triu(self.J, 1), m_outer)
  342. - logZ)
  343. mu = (self.beta0 + self.beta1 * self.m) @ self.L.T
  344. R = self.Y - mu
  345. obs = -0.5 * np.einsum('ti,ij,tj->', R, self.P, R)
  346. eps = 1e-12
  347. mc = np.clip(self.m, eps, 1 - eps)
  348. H = -(mc * np.log(mc) + (1 - mc) * np.log(1 - mc)).sum()
  349. return float(prior_term + obs + H)
  350. # --------------------------------------------------------
  351. def fit(self, verbose=False):
  352. for it in range(self.n_iter):
  353. self.E_step()
  354. if self.sign_flip_protection and (it < 5 or it % 20 == 0):
  355. self._fix_sign_flips()
  356. self.M_step()
  357. if verbose and ((it + 1) % 10 == 0 or it == self.n_iter - 1):
  358. elbo = self._elbo()
  359. self.elbo_history.append(elbo)
  360. print(f"[pMEM_Leadfield_v2] it {it+1}/{self.n_iter} ELBO={elbo:.2f} "
  361. f"σ²={self.sigma2:.3f} ρ={self.rho:.3f}")
  362. return self.h, self.J, self.beta0, self.beta1, self.m

pmem_leadfield.py at commit b1ca5dd, no license · at the source

Overview

Authors: Seok-Oh Jeong1, Euisun Kim2,3, Jiyoung Kang4, Jinseok Eo2,3, Dongmyeong Lee3, Hae-Jeong Park2,3,5,6
  1. Department of Statistics, Hankuk University of Foreign Studies, Yong-In, Republic of Korea
  2. Graduate School of Medical Science, Brain Korea 21 Project, Department of Nuclear Medicine, Department of Psychiatry, Yonsei University College of Medicine, Seoul, Republic of Korea
  3. Center for Systems and Translational Brain Science, Institute of Human Complexity and Systems Science, Yonsei University, Seoul, Republic of Korea
  4. Department of Scientific Computing, Pukyong National University, Busan, Republic of Korea
  5. Department of Cognitive Science, Yonsei University, Seoul, Republic of Korea
  6. Brain Research Institute, Epilepsy Research Institute, Yonsei University College of Medicine, Seoul, Republic of Korea
Institutions: Hankuk University of Foreign Studies (South Korea); Yonsei University (South Korea); Yonsei University Health System (South Korea); Pukyong National University (South Korea)
Journal: Imaging neuroscience (Cambridge, Mass.), volume 4, article IMAG.a.1415
Dates: received 13 June 2026; accepted 22 September 2026; published online 27 October 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1162/imag.a.1415 · PMID 42953883 · PMCID PMC13716286 · OpenAlex W7220429905
Open access: diamond, a free copy (OpenAlex)
Status: code verified (how the code is found and verified)
Categories: EEG (modality), MEG (modality), human (organism), methods / tools (subfield)
Methods: Spectral & time-frequency, Connectivity, fMRI & imaging, Smoothing, state filtering, decompositions
Keywords: pairwise maximum entropy model, nonlinear state dynamics, latent binary states, correlated noise, EEG/MEG source, energy landscape
MeSH: Brain*, Models, Neurological*, Neuroimaging*, Electroencephalography, Entropy, Humans, Magnetoencephalography (* major topic)
Topic: Neural dynamics and brain function (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Citations: not cited yet (Europe PMC); 27 references in the paper
Research resources: RRID:SCR_007345

Abstract

Pairwise maximum entropy models (pMEMs) are gaining increasing attention as a principled way to study nonlinear brain-state transitions. By representing each brain region as a binary active/inactive state and estimating the least-biased probability distribution over all such network configurations, pMEMs reconstruct an energy landscape in which local minima correspond to stable or metastable brain states and transitions reflect movement across attractor basins. However, neuroimaging recordings are continuous and spatially mixed, forcing conventional pMEM analyses to demean and hard-threshold each channel. This ad hoc binarization discards amplitude information and can conflate spatially correlated noise or source mixing with genuine neural coupling. We introduce a hierarchical generative pMEM that links latent binary network states to continuous observations. Node-specific baseline and gain parameters model amplitude modulation, compound-symmetry noise captures shared residual covariance, and, for electroencephalography/magnetoencephalography (EEG/MEG), the electromagnetic leadfield is included as a generative forward model rather than a preprocessing inverse. Model parameters and latent states are jointly estimated using exact posterior enumeration for the small networks analyzed here and a mean-field extension for larger networks. Controlled simulations demonstrated two complementary advantages of the proposed framework. Residual-covariance modeling improved recovery of pMEM fields, couplings, and latent states by accounting for shared fluctuations, with the clearest gains when residual spatial correlation was sufficiently strong. The generative leadfield model further improved sensor-space inference by avoiding inverse-first source reconstruction, including settings in which covariance modeling alone offered limited gains. Resting-state EEG validation showed higher residual correlation during eyes-closed than eyes-open rest, consistent with elevated and spatially coherent alpha activity. Applied further to propofol sedation EEG (alpha-band amplitude envelopes, 8–13 Hz), the fitted 6-node default mode network (DMN) model showed pipeline-dependent condition effects in residual spatial correlation and basin entropy, not apparent under the conventional demean-based pMEM.

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.

psalmhim/pMEM-EEGMEG

License: none: the authors keep all their rights
State: the link answers, verified on 30 October 2026
Evidence: files inventoried
Commit: b1ca5dd2adb28f102a69d9dd0b660b8c2ae2f795, 13 June 2026
Languages: Python (5)
Size: 8 files, 5 scripts
Software Heritage: not archived
Found in: “Data and Code Availability”
Holds: README, environment (requirements.txt)
Not found: license file, CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (3 files), SciPy (2 files), scikit-learn (1 file)
Availability: 1 check, the latest on 30 October 2026: the link answers
  • 30 October 2026: the link answers
6 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;
  • 5 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

Datasets cited

Data and Code Availability

The PhysioNet EEG Motor Movement/Imagery dataset is freely available at https://physionet.org/content/eegmmidb/1.0.0/ (Goldberger et al., 2000; Schalk, 2009). The MNE sample MEG dataset is distributed with MNE-Python and is available at https://mne.tools/stable/index.html (Gramfort et al., 2014). The propofol sedation EEG dataset (Chennu et al., 2016) is openly available from the University of Cambridge Data Repository at https://doi.org/10.17863/CAM.68959, under a Creative Commons Attribution 2.0 UK: England & Wales license.

The simulation and analysis code is available at https://github.com/psalmhim/pMEM-EEGMEG, implemented in Python (≥3.9) using NumPy (≥1.23), SciPy (≥1.9), scikit-learn (≥1.1), MNE-Python (≥1.3), Nilearn (≥0.10), and Matplotlib (≥3.6); exact package versions are pinned in requirements.txt in the repository.

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, 30 October 2026: the first record

Recorded: type, language, journal, volume, pages, dates, 6 authors, 6 keywords, 7 MeSH terms, 27 references, 1 RRID.

Cite

This paper

Jeong, S.-O., Kim, E., Kang, J., Eo, J., Lee, D., & Park, H.-J. (2026). A generative framework for estimating brain energy landscapes from continuous neuroimaging observations. Imaging neuroscience (Cambridge, Mass.), 4, IMAG.a.1415. https://doi.org/10.1162/imag.a.1415

BibTeX

@article{jeong2026generative,
author = {Jeong, Seok-Oh and Kim, Euisun and Kang, Jiyoung and Eo, Jinseok and Lee, Dongmyeong and Park, Hae-Jeong},
title = {{A generative framework for estimating brain energy landscapes from continuous neuroimaging observations}},
journal = {Imaging neuroscience (Cambridge, Mass.)},
year = {2026},
month = oct,
volume = {4},
pages = {IMAG.a.1415},
publisher = {MIT Press},
issn = {2837-6056},
doi = {10.1162/imag.a.1415},
url = {https://doi.org/10.1162/imag.a.1415},
pmid = {42953883},
pmcid = {PMC13716286}
}

RIS

TY - JOUR
AU - Jeong, Seok-Oh
AU - Kim, Euisun
AU - Kang, Jiyoung
AU - Eo, Jinseok
AU - Lee, Dongmyeong
AU - Park, Hae-Jeong
TI - A generative framework for estimating brain energy landscapes from continuous neuroimaging observations
T2 - Imaging neuroscience (Cambridge, Mass.)
J2 - Imaging Neurosci (Camb)
PY - 2026
DA - 2026/10/27
VL - 4
SP - IMAG.a.1415
SN - 2837-6056
PB - MIT Press
DO - 10.1162/imag.a.1415
UR - https://doi.org/10.1162/imag.a.1415
LA - en
ER -

CSL-JSON

{
"id": "10.1162/imag.a.1415",
"type": "article-journal",
"title": "A generative framework for estimating brain energy landscapes from continuous neuroimaging observations",
"container-title": "Imaging neuroscience (Cambridge, Mass.)",
"author": [
{
"family": "Jeong",
"given": "Seok-Oh"
},
{
"family": "Kim",
"given": "Euisun"
},
{
"family": "Kang",
"given": "Jiyoung"
},
{
"family": "Eo",
"given": "Jinseok"
},
{
"family": "Lee",
"given": "Dongmyeong"
},
{
"family": "Park",
"given": "Hae-Jeong"
}
],
"container-title-short": "Imaging Neurosci (Camb)",
"volume": "4",
"page": "IMAG.a.1415",
"DOI": "10.1162/imag.a.1415",
"PMID": "42953883",
"PMCID": "PMC13716286",
"ISSN": "2837-6056",
"publisher": "MIT Press",
"URL": "https://doi.org/10.1162/imag.a.1415",
"language": "en",
"issued": {
"date-parts": [
[
2026,
10,
27
]
]
}
}

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.1016/j.patter.2026.101560 [code]
Automating region selection with genetic algorithms for energy landscape analyses of brain dynamics.
Journal: Patterns (New York, N.Y.)
In common: SciPy, NumPy, methods / tools, 6 references
[2] doi:10.1002/advs.202414016 [code]
Comparison of Large Language Model with Aphasia.
Journal: Advanced science (Weinheim, Baden-Wurttemberg, Germany)
In common: NumPy, 5 references
[3] doi:10.1038/s41562-025-02121-9 [code]
General anaesthesia decreases the uniqueness of brain functional connectivity across individuals and species.
Journal: Nature human behaviour
In common: scikit-learn, SciPy, NumPy, 5 references
[4] doi:10.1038/s41598-025-32207-3 [code]
Multi-scale EEG feature decoding with Swin Transformers for subject independent motor imagery BCIs.
Journal: Scientific reports
In common: scikit-learn, SciPy, NumPy, physionet.org/content/eegmmidb, methods / tools, EEG, 1 reference
[5] doi:10.1002/hbm.70029 [code]
The alterations of repetitive transcranial magnetic stimulation on the energy landscape of resting-state networks differ across the human cortex.
Journal: Human brain mapping
In common: 5 references
[6] doi:10.1371/journal.pcbi.1010811 [code]
LSD-induced increase of Ising temperature and algorithmic complexity of brain dynamics.
Journal: PLoS computational biology
In common: SciPy, NumPy, 5 references
[7] doi:10.7554/elife.97107 [code]
A dynamic generative model can extract interpretable oscillatory components from multichannel neurophysiological recordings.
Journal: eLife
In common: scikit-learn, SciPy, NumPy, MEG, EEG, 3 references
[8] doi:10.1038/s41598-025-07703-1
Scalable geometric learning with correlation-based functional brain networks.
Journal: Scientific reports
In common: EEG, 2 references, author Hae-Jeong Park
[9] doi:10.3389/fnhum.2022.992649 [code]
Paradoxical markers of conscious levels: Effects of propofol on patients in disorders of consciousness.
Journal: Frontiers in human neuroscience
In common: SciPy, NumPy, EEG, 4 references
[10] doi:10.1186/s12868-024-00854-3 [code]
State-transition dynamics of resting-state functional magnetic resonance imaging data: model comparison and test-to-retest analysis.
Journal: BMC neuroscience
In common: scikit-learn, SciPy, NumPy, MEG, EEG, 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.

Add the badge to the code's README

OSCR: the badge of this paper

If you are an author or a maintainer of this paper's code, you can add this badge. It says what OSCR knows of the paper (its code, its validated tracing map) and links the code to this page. Paste one of these snippets under the README's title. Keep the two marks around it, so that the badge can be replaced in place and is never added twice. OSCR changes nothing in your repository.

Markdown
<!-- oscr-badge:start -->
[![OSCR](https://openscicode.org/badge/doi/10.1162/imag.a.1415.svg)](https://openscicode.org/badge/doi/10.1162/imag.a.1415)
<!-- oscr-badge:end -->
reStructuredText
.. oscr-badge:start

.. image:: https://openscicode.org/badge/doi/10.1162/imag.a.1415.svg
   :target: https://openscicode.org/badge/doi/10.1162/imag.a.1415
   :alt: OSCR

.. oscr-badge:end
HTML
<!-- oscr-badge:start -->
<a href="https://openscicode.org/badge/doi/10.1162/imag.a.1415"><img src="https://openscicode.org/badge/doi/10.1162/imag.a.1415.svg" alt="OSCR"></a>
<!-- oscr-badge:end -->

On GitHub, open the README, choose Edit (the pencil), paste the snippet under the title, then "Commit changes".

If you choose to, you can have the badge kept up to date for you, with oscr badge --write from the command line or with the GitHub Action in your repository's workflows. Every other form and option (a card, AsciiDoc, LaTeX, shields.io) is on the badge 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: code errors and mismatches between the code and the paper about this paper and its code, from readers and its authors, beside the paper and its tracing map.

Report a mismatch between the code and the paper · Report a code error (signed in)

Reproductions: reports from readers who ran the authors' code and did not get the paper's result, what they ran, with which environment, commit and data.

Report a reproduction failure (signed in)

Versions of its code: the releases of its code tied to a version of this paper (the preprint, the accepted manuscript, the version of record…), each with the tracing map versioned with it.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.