OSCR

The Virtual Brain links transcranial magnetic stimulation evoked potentials and inhibitory neurotransmitter changes in major depressive disorder.

Code ↔ Paper

6 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 6 matches
  1. [1] § Methods and Materials › Whole-brain simulations with the Virtual Brain › Neural mass model ↔ functions.py, lines 88–96 · score 0.84 · firing threshold, neuronal population, rate operator, firing rate, steepness, NMM
  2. [2] § Methods and Materials › Data ↔ functions.py, lines 155–256 · score 0.67 · Rit neural mass, SC matrices, structural connectivity, transformed, Jansen, excitatory
  3. [3] § Methods and Materials › Whole-brain simulations with the Virtual Brain › Neural mass model ↔ functions.py, lines 88–96 · score 0.64 · neuronal populations, sigmoidal function, operator, NMM, Jansen, Rit
  4. [4] § Methods and Materials › Whole-brain simulations with the Virtual Brain › Neural mass model ↔ functions.py, lines 155–256 · score 0.63 · coupled activity, inhibitory population, transformed, noise, sigmoid, excitatory
  5. [5] § Methods and Materials › Whole-brain simulations with the Virtual Brain › Neural mass model ↔ 4_bids_conversion.py, lines 212–283 · score 0.60 · neural populations, sigmoid function, external, coupled, excitatory, synapses
  6. [6] § Methods and Materials › Data ↔ 4_bids_conversion.py, lines 212–283 · score 0.57 · connection weights, Rit neural mass, ms, excitatory, global, synapses

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 · 600 lines · 30 KB · EUPL-1.2 · 4 matches

  1. # ------------------------------------------------------------------------------
  2. # functions.py
  3. # Author: Dr. Timo Hofsähs
  4. #
  5. # Description:
  6. # This Python script is part of the code accompanying the scientific publication:
  7. # The Virtual Brain links transcranial magnetic stimulation evoked potentials and
  8. # neurotransmitter changes in major depressive disorder
  9. # Dr. Timo Hofsähs, Marius Pille, Dr. Jil Meier, Prof. Petra Ritter
  10. # (in prep)
  11. #
  12. # This code provides all functions necessary to perform the fitting in 'fitting.py'.
  13. # The fitting method and all functions in this script except from 'gmfa' and
  14. # 'gmfa_timepoint' are based on the code provided with the following publication:
  15. # Momi D, Wang Z, Griffiths JD. 2023. TMS-EEG evoked responses are driven by recurrent
  16. # large-scale network dynamics. eLife2023;12:e83232 DOI: https://doi.org/10.7554/eLife.83232
  17. # Licensed under a Creative Commons Attribution license (CC-BY)
  18. # The original code can be found at:
  19. # https://github.com/GriffithsLab/PyTepFit/blob/main/tepfit/fit.py
  20. #
  21. # Copyright (c) 2024 Dr. Timo Hofsähs. All rights reserved.
  22. #
  23. # License: This code is licensed under the Creative Commons Attribution 4.0 International
  24. # License (CC-BY 4.0), which allows for redistribution, adaptation, and use in source
  25. # and binary forms, with or without modification, provided proper credit is given to
  26. # the original authors. You can view the full terms of this license at:
  27. # https://creativecommons.org/licenses/by/4.0/
  28. # ------------------------------------------------------------------------------
  29. import numpy as np
  30. import scipy.stats
  31. import torch
  32. import torch.optim as optim
  33. from torch.nn.parameter import Parameter
  34. from sklearn.metrics.pairwise import cosine_similarity
  35. import pickle
  36. class OutputNM():
  37. mode_all = ['train', 'test']
  38. stat_vars_all = ['m', 'v']
  39. def __init__(self, model_name, node_size, param, fit_weights=False):
  40. self.loss = np.array([])
  41. if model_name == "JR":
  42. state_names = ['E', 'Ev', 'I', 'Iv', 'P', 'Pv']
  43. self.output_name = "eeg"
  44. for name in state_names + [self.output_name]:
  45. for m in self.mode_all:
  46. setattr(self, name + '_' + m, [])
  47. vars = [a for a in dir(param) if not a.startswith('__') and not callable(getattr(param, a))]
  48. for var in vars:
  49. if np.any(getattr(param, var)[1] > 0):
  50. if var != 'std_in':
  51. setattr(self, var, np.array([]))
  52. for stat_var in self.stat_vars_all:
  53. setattr(self, var + '_' + stat_var, [])
  54. else:
  55. setattr(self, var, [])
  56. self.weights = []
  57. def save(self, filename):
  58. with open(filename, 'wb') as f:
  59. pickle.dump(self, f)
  60. class ParamsJR():
  61. '''Jansen & Rit neural mass model'''
  62. def __init__(self, model_name, **kwargs):
  63. if model_name == "JR":
  64. param = {"A ": [3.25, 0], "a": [100, 0.], "B": [22, 0], "b": [50, 0], "g": [100, 0],"c1": [135, 0.], "c2": [135 * 0.8, 0.],"c3": [135 * 0.25, 0.],"c4": [135 * 0.25, 0.],"std_in": [100, 0], "vmax": [5, 0], "v0": [6, 0], "r": [0.56, 0], "mu": [.5, 0],"speed": [2.5, 0],"k": [5, 0],"ki": [1, 0]}
  65. for var in param:
  66. setattr(self, var, param[var])
  67. for var in kwargs:
  68. setattr(self, var, kwargs[var])
  69. def sys2nd(A, a, u, x, v):
  70. '''
  71. Jansen & Rit neural mass model equation
  72. A = maximum amplitude of PSP (A=EPSP, B=IPSP)
  73. a = maximum firing rate of populations (a = excitatory = PC, EIN; b = inhibitory = IIN)
  74. u = input activity
  75. x,v = state variable activity
  76. '''
  77. return A*a*u -2*a*v-a**2*x
  78. def sigmoid(x, vmax, v0, r):
  79. '''
  80. Jansen & Rit neural mass model sigmoid function (potential to rate operator)
  81. x = input membrane potential
  82. vmax = maximum firing rate of all neuronal populations (default = 5 s^-1)
  83. v0 = PSP for which half of the maximum firing rate of the neuronal population is achieved; can be interpreted as excitability of all neuronal populations (default = 6mV)
  84. r = steepness at the firing threshold; represents the variance of firing thresholds within the NMM (default = 0.56 mV^-1)
  85. '''
  86. return vmax/(1+torch.exp(r*(v0-x)))
  87. class RNNJANSEN(torch.nn.Module):
  88. '''
  89. Embedding of whole-brain simulation parameters in fitting algorithm.
  90. Defines parameters of the whole-brain simulation and if and to what extent they are included in the fitting.
  91. '''
  92. state_names = ['E', 'Ev', 'I', 'Iv', 'P', 'Pv']
  93. model_name = "JR"
  94. def __init__(self,
  95. input_size: int,
  96. node_size: int,
  97. batch_size: int,
  98. step_size: float,
  99. output_size: int,
  100. tr: float,
  101. sc: float,
  102. lm: float,
  103. dist: float,
  104. param: ParamsJR,
  105. seed_int: int) -> None:
  106. super(RNNJANSEN, self).__init__()
  107. self.state_size = 6 # number of state variables
  108. self.input_size = input_size # 1 or 2 or 3
  109. self.tr = tr # tr ms (integration step 0.1 ms)
  110. self.step_size = torch.tensor(step_size, dtype=torch.float32) # integration step 0.1 ms
  111. self.hidden_size = int(tr / step_size)
  112. self.batch_size = batch_size # size of batch in ms applied at each fittin iteration
  113. self.node_size = node_size # number of regions of the whole-brain simulation
  114. self.output_size = output_size # number of EEG channels
  115. self.sc = sc # structural connectivity weights matrix (shaped node_size^2)
  116. self.dist = torch.tensor(dist, dtype=torch.float32) # structural connectivity tract length matrix (shaped node_size^2)
  117. self.param = param # parameters of the whole-brain simulation
  118. self.seed_int = seed_int # number of the seed
  119. np.random.seed(self.seed_int) # define seed
  120. self.lm = torch.tensor(lm, dtype=torch.float32) # leadfield matrix from sourced data to eeg
  121. # define if a parameter is fitted or not and add variance of default parameter if defined
  122. vars = [a for a in dir(param) if not a.startswith('__') and not callable(getattr(param, a))]
  123. for var in vars:
  124. if np.any(getattr(param, var)[1] > 0):
  125. setattr(self, var, Parameter(torch.tensor(getattr(param, var)[0] + getattr(param, var)[2] * np.random.randn(1,)[0], dtype=torch.float32)))
  126. dict_nv = {}
  127. dict_nv['m'] = getattr(param, var)[0]
  128. dict_nv['v'] = getattr(param, var)[1]
  129. dict_np = {}
  130. dict_np['m'] = var + '_m'
  131. dict_np['v'] = var + '_v'
  132. for key in dict_nv:
  133. setattr(self, dict_np[key], Parameter(torch.tensor(dict_nv[key], dtype=torch.float32)))
  134. else:
  135. setattr(self, var, torch.tensor(getattr(param, var)[0], dtype=torch.float32))
  136. def forward(self, input, noise_in, noise_out, hx, hE):
  137. '''
  138. Forward function of the concurrent simulation and fitting.
  139. Calculates the state variables of the neural mass model for every time step.
  140. '''
  141. bound_coupling = 500 # upper and lower limit of the coupling function
  142. dt = self.step_size
  143. self.delays = (self.dist / self.speed).type(torch.int64) # delays of the coupling per region
  144. w = torch.exp(self.w_bb) * torch.tensor(self.sc, dtype=torch.float32) # multiplication of SC weights matrix with fitting transformation factor w_bb
  145. w_n = torch.log1p(0.5 * (w + w.T)) / torch.linalg.norm(torch.log1p(0.5 * (w + w.T))) # normalization, logarithmization, symmetrication of altered SC weights matrix
  146. self.sc_m = w_n
  147. dg = -torch.diag(torch.sum(w_n, axis=1)) # negative of the sum of all SC weights per region
  148. M = hx[:, 0:1] # current of main population
  149. E = hx[:, 1:2] # current of excitory population
  150. I = hx[:, 2:3] # current of inhibitory population
  151. Mv = hx[:, 3:4] # voltage of main population
  152. Ev = hx[:, 4:5] # voltage of exictory population
  153. Iv = hx[:, 5:6] # voltage of inhibitory population
  154. current_state = torch.zeros_like(hx)
  155. next_state = {}
  156. eeg_batch = []
  157. E_batch = []
  158. I_batch = []
  159. M_batch = []
  160. Ev_batch = []
  161. Iv_batch = []
  162. Mv_batch = []
  163. for i_batch in range(self.batch_size):
  164. # noiseEEG = noise_out[:, i_batch:i_batch + 1]
  165. for i_hidden in range(self.hidden_size):
  166. # Define SC matrix & coupling
  167. hE_new = hE.clone() # hE = history object, history of excitatory state variable
  168. Ed = torch.tensor(np.zeros((self.node_size, self.node_size)), dtype=torch.float32) # delays of E
  169. Ed = hE_new.gather(1, self.delays) # delay of E as input through structural connectivity
  170. LEd = torch.reshape(torch.sum(w_n * torch.transpose(Ed, 0, 1), 1), (self.node_size, 1)) # weighted delayed input from E
  171. # Define noise
  172. noiseE = noise_in[:, i_hidden, i_batch, 0:1] * self.std_in
  173. # noiseI = noise_in[:, i_hidden, i_batch, 1:2] * self.std_in
  174. # noiseM = noise_in[:, i_hidden, i_batch, 2:3] * self.std_in
  175. # Define stimulus
  176. u = input[:, i_hidden:i_hidden + 1, i_batch]
  177. stimulus = self.k * self.ki * u
  178. # Define activity input for Jansen & Rit equations
  179. rM = sigmoid(E - I, self.vmax, self.v0, self.r)
  180. rE = self.c2 * sigmoid(self.c1 * M , self.vmax, self.v0, self.r)
  181. rI = self.c4 * sigmoid(self.c3 * M, self.vmax, self.v0, self.r)
  182. # Define coupling
  183. coupled_activity = self.g * (LEd + torch.matmul(dg, E-I))
  184. coupling = bound_coupling * torch.tanh(coupled_activity / bound_coupling)
  185. # Jansen & Rit neural mass model equations
  186. ddM = M + dt * Mv
  187. ddE = E + dt * Ev
  188. ddI = I + dt * Iv
  189. ddMv = Mv + dt * sys2nd(self.A, self.a, rM, M, Mv)
  190. ddEv = Ev + dt * sys2nd(self.A, self.a, (rE + coupling + stimulus + noiseE + self.mu), E, Ev)
  191. ddIv = Iv + dt * sys2nd(self.B, self.b, rI, I, Iv)
  192. E = ddE
  193. I = ddI
  194. M = ddM
  195. Ev = ddEv
  196. Iv = ddIv
  197. Mv = ddMv
  198. hE[:, 0] = E[:, 0] - I[:, 0] # create new time step for history of E object
  199. # Put M E I Mv Ev and Iv at every tr to the placeholders for checking them visually.
  200. M_batch.append(M)
  201. I_batch.append(I)
  202. E_batch.append(E)
  203. Mv_batch.append(Mv)
  204. Iv_batch.append(Iv)
  205. Ev_batch.append(Ev)
  206. hE = torch.cat([(E - I), hE[:, :-1]], axis=1) # update placeholders for E buffer
  207. # Put the EEG signal each tr to the placeholder being used in the cost calculation.
  208. eeg_currently = 0.0005 * torch.matmul(self.lm, E - I)
  209. eeg_batch.append(eeg_currently)
  210. # Update the current state.
  211. current_state = torch.cat([M, E, I, Mv, Ev, Iv], axis=1)
  212. next_state['current_state'] = current_state
  213. next_state['eeg_batch'] = torch.cat(eeg_batch, axis=1)
  214. next_state['E_batch'] = torch.cat(E_batch, axis=1)
  215. next_state['I_batch'] = torch.cat(I_batch, axis=1)
  216. next_state['P_batch'] = torch.cat(M_batch, axis=1)
  217. next_state['Ev_batch'] = torch.cat(Ev_batch, axis=1)
  218. next_state['Iv_batch'] = torch.cat(Iv_batch, axis=1)
  219. next_state['Pv_batch'] = torch.cat(Mv_batch, axis=1)
  220. return next_state, hE
  221. class Costs:
  222. '''
  223. Class that defines equations for the cost function of the fittin algorithm
  224. '''
  225. def __init__(self, method):
  226. self.method = method
  227. def cost_dist(self, sim, emp):
  228. losses = torch.sqrt(torch.mean((sim - emp) ** 2))
  229. return losses
  230. def cost_beamform(self, model, emp):
  231. corr = torch.matmul(emp, emp.T)
  232. corr_inv = torch.inverse(corr)
  233. corr_inv_s = torch.inverse(torch.matmul(model.lm.T, torch.matmul(corr_inv, model.lm)))
  234. W = torch.matmul(corr_inv_s, torch.matmul(model.lm.T, corr_inv))
  235. return torch.trace(torch.matmul(W, torch.matmul(corr, W.T)))
  236. def cost_r(self, logits_series_tf, labels_series_tf):
  237. node_size = logits_series_tf.shape[0]
  238. labels_series_tf_n = labels_series_tf - torch.reshape(torch.mean(labels_series_tf, 1), [node_size, 1]) # remove mean across time
  239. logits_series_tf_n = logits_series_tf - torch.reshape(torch.mean(logits_series_tf, 1), [node_size, 1])
  240. cov_sim = torch.matmul(logits_series_tf_n, torch.transpose(logits_series_tf_n, 0, 1)) # correlation
  241. cov_def = torch.matmul(labels_series_tf_n, torch.transpose(labels_series_tf_n, 0, 1))
  242. FC_sim_T = torch.matmul(torch.matmul(torch.diag(torch.reciprocal(torch.sqrt(torch.diag(cov_sim)))), cov_sim), torch.diag(torch.reciprocal(torch.sqrt(torch.diag(cov_sim))))) # fc for sim and empirical BOLDs
  243. FC_T = torch.matmul(torch.matmul(torch.diag(torch.reciprocal(torch.sqrt(torch.diag(cov_def)))), cov_def), torch.diag(torch.reciprocal(torch.sqrt(torch.diag(cov_def)))))
  244. ones_tri = torch.tril(torch.ones_like(FC_T), -1) # mask for lower triangle without diagonal
  245. zeros = torch.zeros_like(FC_T) # create a tensor all ones
  246. mask = torch.greater(ones_tri, zeros) # boolean tensor, mask[i] = True iff x[i] > 1
  247. FC_tri_v = torch.masked_select(FC_T, mask) # mask out fc to vector with elements of the lower triangle
  248. FC_sim_tri_v = torch.masked_select(FC_sim_T, mask)
  249. FC_v = FC_tri_v - torch.mean(FC_tri_v) # remove the mean across the elements
  250. FC_sim_v = FC_sim_tri_v - torch.mean(FC_sim_tri_v)
  251. corr_FC = torch.sum(torch.multiply(FC_v, FC_sim_v)) * torch.reciprocal(torch.sqrt(torch.sum(torch.multiply(FC_v, FC_v)))) * torch.reciprocal(torch.sqrt(torch.sum(torch.multiply(FC_sim_v, FC_sim_v)))) # corr_coef
  252. losses_corr = -torch.log(0.5000 + 0.5 * corr_FC) # use surprise: corr to calculate probability and -log
  253. return losses_corr
  254. def cost_eff(self, model, sim, emp):
  255. if self.method == 0: # in the current version this methd is applied
  256. return self.cost_dist(sim, emp)
  257. elif self.method == 1:
  258. return self.cost_beamform(model, emp) + self.cost_dist(sim, emp)
  259. else:
  260. return self.cost_r(sim, emp)
  261. class Model_fitting:
  262. '''Fitting algorithm equations'''
  263. def __init__(self, model, ts, num_epoches, cost):
  264. self.model = model
  265. self.num_epoches = num_epoches
  266. self.ts = ts
  267. self.cost = Costs(cost)
  268. def save(self, filename):
  269. with open(filename, 'wb') as f:
  270. pickle.dump(self, f)
  271. def train(self, u=0):
  272. '''Train function of the fitting algorithm'''
  273. # define some constants
  274. lb = 0.001
  275. delays_max = 500
  276. state_ub = 2 # lower bound for initial conditions
  277. state_lb = 0.5 # upper bound for initial conditions
  278. w_cost = 1.0 # factor to scale cost of similarity against cost of parameter change
  279. epoch_min = 200 # minimum amount of epochs to run, stop criterium
  280. r_lb = 0.85 # minimum pearson correlation value, stop criterium
  281. self.u = u # stimulus
  282. self.output_sim = OutputNM(self.model.model_name, # placeholder for output(EEG and histoty of model parameters and loss)
  283. self.model.node_size,
  284. self.model.param)
  285. # define an optimizor(ADAM)
  286. optimizer = optim.Adam(self.model.parameters(), lr=0.05, eps=1e-7)
  287. X = torch.tensor(np.random.uniform(state_lb, state_ub, (self.model.node_size, self.model.state_size)), dtype=torch.float32) # initial conditions
  288. hE = torch.tensor(np.random.uniform(state_lb, state_ub, (self.model.node_size, delays_max)), dtype=torch.float32) # history object
  289. # define masks for geting lower triangle matrix
  290. mask = np.tril_indices(self.model.node_size, -1)
  291. mask_e = np.tril_indices(self.model.output_size, -1)
  292. # placeholders for the history of model parameters
  293. fit_param = {}
  294. fit_sc = [self.model.sc[mask].copy()] # sc weights history
  295. for key, value in self.model.state_dict().items():
  296. fit_param[key] = [value.detach().numpy().ravel().copy()]
  297. loss_his = []
  298. num_batches = int(self.ts.shape[2] / self.model.batch_size) # define num_batches
  299. # empty dicts and arrays for course of parameter values, gradients, similarity metrics and loss
  300. parameters_fitted_initial = {}
  301. for key, value in self.model.state_dict().items():
  302. if '_' not in key:
  303. parameters_fitted_initial[key] = value.detach().numpy().astype(np.float32)
  304. parameters_fitted_export = {}
  305. parameters_fitted_export[-1] = parameters_fitted_initial
  306. # parameters_gradients_export = {}
  307. course_loss = np.zeros((3, self.num_epoches))
  308. course_cos_sim = np.zeros((self.num_epoches,))
  309. course_pcc = np.zeros((self.num_epoches,))
  310. course_sc_values = np.zeros((self.num_epoches+1, self.model.node_size, self.model.node_size))
  311. course_sc_values[0] = self.model.sc
  312. for i_epoch in range(self.num_epoches):
  313. eeg = self.ts[i_epoch % self.ts.shape[0]]
  314. # Create placeholders for the simulated EEG E I M Ev Iv and Mv of entire time series.
  315. for name in self.model.state_names + [self.output_sim.output_name]:
  316. setattr(self.output_sim, name + '_train', [])
  317. external = torch.tensor(np.zeros([self.model.node_size, self.model.hidden_size, self.model.batch_size]), dtype=torch.float32)
  318. # Perform the training in batches
  319. for i_batch in range(num_batches):
  320. optimizer.zero_grad() # Reset the gradient to zeros after update model parameters.
  321. # Get the input and output noises for the module.
  322. noise_in = torch.tensor(np.random.randn(self.model.node_size, self.model.hidden_size, self.model.batch_size, self.model.input_size), dtype=torch.float32)
  323. noise_out = torch.tensor(np.random.randn(self.model.node_size, self.model.batch_size), dtype=torch.float32)
  324. if not isinstance(self.u, int):
  325. external = torch.tensor((self.u[:, :, i_batch * self.model.batch_size:(i_batch + 1) * self.model.batch_size]), dtype=torch.float32)
  326. # Use the model.forward() function to update next state and get simulated EEG in this batch.
  327. next_batch, hE_new = self.model(external, noise_in, noise_out, X, hE)
  328. # Get the batch of emprical EEG signal.
  329. ts_batch = torch.tensor((eeg.T[i_batch * self.model.batch_size:(i_batch + 1) * self.model.batch_size, :]).T, dtype=torch.float32)
  330. loss_prior = []
  331. m = torch.nn.ReLU() # define the relu function
  332. variables_p = [a for a in dir(self.model.param) if
  333. not a.startswith('__') and not callable(getattr(self.model.param, a))]
  334. # get penalty on each fitted model parameter based on the derivation from default
  335. for var in variables_p:
  336. if np.any(getattr(self.model.param, var)[1] > 0):
  337. dict_np = {}
  338. dict_np['m'] = var + '_m'
  339. dict_np['v'] = var + '_v'
  340. loss_prior.append(torch.sum((lb + (1 / m(self.model.get_parameter(dict_np['v'])))) * (m(self.model.get_parameter(var)) - m(self.model.get_parameter(dict_np['m']))) ** 2))
  341. # calculate total loss
  342. loss_similarity = w_cost * self.cost.cost_eff(self.model, next_batch['eeg_batch'], ts_batch) # loss from difference between simulated and empirical timeseries
  343. loss_complexity = sum(loss_prior) # loss from derivation of fitted parameters from default
  344. loss = loss_similarity + loss_complexity
  345. # Put the batch of the simulated EEG, E I M Ev Iv Mv in to placeholders for entire time-series.
  346. for name in self.model.state_names + [self.output_sim.output_name]:
  347. name_next = name + '_batch'
  348. tmp_ls = getattr(self.output_sim, name + '_train')
  349. tmp_ls.append(next_batch[name_next].detach().numpy())
  350. setattr(self.output_sim, name + '_train', tmp_ls)
  351. loss_his.append(loss.detach().numpy())
  352. loss.backward(retain_graph=True) # Calculate gradient using backward (backpropagation) method of the loss function.
  353. optimizer.step() # Optimize the model based on the gradient method in updating the model parameters.
  354. for key, value in self.model.state_dict().items(): # Put the updated model parameters into the history placeholders.
  355. fit_param[key].append(value.detach().numpy().ravel().copy())
  356. fit_sc.append(self.model.sc_m.detach().numpy()[mask].copy()) # add newly fitted SC to list
  357. X = torch.tensor(next_batch['current_state'].detach().numpy(), dtype=torch.float32)
  358. hE = torch.tensor(hE_new.detach().numpy(), dtype=torch.float32) # update history object
  359. fc = np.corrcoef(self.ts.mean(0))
  360. tmp_ls = getattr(self.output_sim, self.output_sim.output_name + '_train')
  361. ts_sim = np.concatenate(tmp_ls, axis=1)
  362. fc_sim = np.corrcoef(ts_sim[:, 10:])
  363. for name in self.model.state_names + [self.output_sim.output_name]:
  364. tmp_ls = getattr(self.output_sim, name + '_train')
  365. setattr(self.output_sim, name + '_train', np.concatenate(tmp_ls, axis=1))
  366. self.output_sim.loss = np.array(loss_his)
  367. # store current SC and current SC gradients
  368. current_sc = np.zeros((200,200))
  369. current_sc_mask = np.tril_indices(200,-1)
  370. current_sc[current_sc_mask] = np.array(fit_sc)[-10:,:].mean(0)
  371. current_sc = current_sc+current_sc.T
  372. course_sc_values[i_epoch+1] = current_sc
  373. # course_sc_gradients[i_epoch] = self.model.w_bb.grad.detach().numpy()
  374. # store current parameter values and gradients for export
  375. parameters_fitted_epoch = {}
  376. # parameters_gradients_epoch = {}
  377. for key, value in self.model.state_dict().items():
  378. if '_' not in key:
  379. parameters_fitted_epoch[key] = value.detach().numpy().astype(np.float32)
  380. # parameters_gradients_epoch[key] = np.array(value.grad, dtype=np.float32)
  381. parameters_fitted_export[i_epoch] = parameters_fitted_epoch
  382. # parameters_gradients_export[i_epoch] = parameters_gradients_epoch
  383. # store current loss values
  384. course_loss[0] = loss_similarity.detach().numpy()
  385. course_loss[1] = loss_complexity.detach().numpy()
  386. course_loss[2] = loss.detach().numpy()
  387. # store and print current cosine similarity value
  388. course_cos_sim[i_epoch] = np.round(np.diag(cosine_similarity(ts_sim, self.ts.mean(0))).mean(), 4)
  389. print(f'epoch: {i_epoch} - cosine similarity: {np.round(course_cos_sim[i_epoch], 4)}')
  390. pcc_value = np.zeros((62,))
  391. for j in range(62):
  392. channel_emp = self.ts.mean(0).T[:,j]
  393. channel_sim = ts_sim.T[:,j]
  394. r, p = scipy.stats.pearsonr(channel_emp, channel_sim)
  395. pcc_value[j] = r
  396. course_pcc[i_epoch] = np.round(np.mean(pcc_value), 4)
  397. # print(f'epoch: {i_epoch} - pcc: {course_pcc[i_epoch]}')
  398. # FITTING CHECK
  399. # if i_epoch > 0 and i_epoch % 10 == 0:
  400. # # PLOT FITTED HETEROGENEOUS PARAMETERS
  401. # fig, axs = plt.subplots(len(parameters_fitted_epoch), 1, figsize=(5, 7), dpi=150)
  402. # for i, key in enumerate(parameters_fitted_epoch):
  403. # axs[i].plot(np.array(parameters_fitted_epoch[key]))
  404. # axs[i].set_ylabel(key)
  405. # yticks = axs[i].get_yticks()
  406. # axs[i].set_yticklabels(['{:,.2f}'.format(y) for y in yticks])
  407. # plt.tight_layout()
  408. # plt.show()
  409. # # PLOT FITTED SC
  410. # plt.figure(figsize=(5, 5), dpi=200)
  411. # plt.imshow(current_sc)
  412. # plt.colorbar()
  413. # plt.title('SC')
  414. # plt.show()
  415. # # PLOT TIMESERIES
  416. # fig, axs = plt.subplots(1, 2, figsize=(8, 2), dpi=70)
  417. # for j in range(200):
  418. # axs[0].plot(np.arange(-100, 300), (E_train[j] - I_train[j]).T, alpha=0.2, c='red' if self.model.ki[j] != 0 else 'black')
  419. # axs[0].set_title('fitted RAW')
  420. # axs[0].set_xlim(-100, 300)
  421. # axs[1].plot(np.arange(-100, 300), ts_sim.T)
  422. # axs[1].set_title('fitted EEG')
  423. # axs[1].set_xlim(-100, 300)
  424. # plt.tight_layout()
  425. # plt.show()
  426. if i_epoch > epoch_min and np.corrcoef(fc_sim[mask_e], fc[mask_e])[0, 1] > r_lb:
  427. break
  428. # store outputs
  429. self.output_sim.weights = np.array(fit_sc)
  430. self.output_sim.course_parameter_values = parameters_fitted_export
  431. # self.output_sim.course_parameter_gradients = parameters_gradients_export
  432. self.output_sim.course_sc_values = course_sc_values
  433. # self.output_sim.course_sc_gradients = course_sc_gradients
  434. self.output_sim.course_loss = course_loss
  435. self.output_sim.course_cos_sim = course_cos_sim
  436. self.output_sim.course_pcc = course_pcc
  437. for key, value in fit_param.items():
  438. setattr(self.output_sim, key, np.array(value))
  439. def test(self, x0, he0, base_batch_num, u=0):
  440. '''Test function of the fitting algorithm'''
  441. transient_num = 10
  442. self.u = u
  443. # initial state
  444. X = torch.tensor(x0, dtype=torch.float32)
  445. hE = torch.tensor(he0, dtype=torch.float32)
  446. num_batches = int(self.ts.shape[2] / self.model.batch_size) + base_batch_num
  447. # Create placeholders for the simulated BOLD E I x f and q of entire time series.
  448. for name in self.model.state_names + [self.output_sim.output_name]:
  449. setattr(self.output_sim, name + '_test', [])
  450. u_hat = np.zeros((self.model.node_size, self.model.hidden_size, base_batch_num * self.model.batch_size + self.ts.shape[2]))
  451. u_hat[:, :, base_batch_num * self.model.batch_size:] = self.u
  452. # Perform the training in batches.
  453. for i_batch in range(num_batches):
  454. # Get the input and output noises for the module.
  455. noise_in = torch.tensor(np.random.randn(self.model.node_size, self.model.hidden_size, self.model.batch_size, self.model.input_size), dtype=torch.float32)
  456. noise_out = torch.tensor(np.random.randn(self.model.node_size, self.model.batch_size), dtype=torch.float32)
  457. external = torch.tensor((u_hat[:, :, i_batch * self.model.batch_size:(i_batch + 1) * self.model.batch_size]), dtype=torch.float32)
  458. # Use the model.forward() function to update next state and get simulated EEG in this batch.
  459. next_batch, hE_new = self.model(external, noise_in, noise_out, X, hE)
  460. if i_batch > base_batch_num - 1:
  461. for name in self.model.state_names + [self.output_sim.output_name]:
  462. name_next = name + '_batch'
  463. tmp_ls = getattr(self.output_sim, name + '_test')
  464. tmp_ls.append(next_batch[name_next].detach().numpy())
  465. setattr(self.output_sim, name + '_test', tmp_ls)
  466. X = torch.tensor(next_batch['current_state'].detach().numpy(), dtype=torch.float32)
  467. hE = torch.tensor(hE_new.detach().numpy(), dtype=torch.float32)
  468. fc = np.corrcoef(self.ts.mean(0))
  469. tmp_ls = getattr(self.output_sim, self.output_sim.output_name + '_test')
  470. ts_sim = np.concatenate(tmp_ls, axis=1)
  471. fc_sim = np.corrcoef(ts_sim[:, transient_num:])
  472. for name in self.model.state_names + [self.output_sim.output_name]:
  473. tmp_ls = getattr(self.output_sim, name + '_test')
  474. setattr(self.output_sim, name + '_test', np.concatenate(tmp_ls, axis=1))
  475. # GMFA
  476. def gmfa(data, start_time, end_time):
  477. '''
  478. Calculates the Global Mean Field Amplitude of an EEG time series for a defined time interval
  479. data: EEG data (n_timepoints, n_channels)
  480. start_time: Start index in the time series
  481. end_time: End index in the time series
  482. all: array shaped (n_timepoints, ), contains the gmfp for every time point
  483. '''
  484. interval_data = data[start_time:end_time, :]
  485. mean_values = np.mean(interval_data, axis=1)
  486. deviations = interval_data - mean_values[:, np.newaxis]
  487. squared_deviations = deviations ** 2
  488. sum_squared_deviations = np.sum(squared_deviations, axis=1)
  489. all = np.sqrt(sum_squared_deviations / interval_data[0].shape[0])
  490. return all
  491. def gmfa_timepoint(data, timepoint):
  492. '''
  493. Calculates the Global Mean Field Amplitude of an EEG time series for a defined time interval
  494. data: EEG data (n_timepoints, n_channels)
  495. start_time: Start index in the time series
  496. end_time: End index in the time series
  497. Returns 2 arrays
  498. all: array shaped (n_timepoints, ), contains the gmfp for every time point
  499. avg: average value of all time points
  500. '''
  501. data_timepoint = data[timepoint, :]
  502. mean_value = np.mean(data_timepoint)
  503. deviations = data_timepoint - mean_value
  504. squared_deviations = deviations ** 2
  505. sum_squared_deviations = np.sum(squared_deviations)
  506. division = np.sqrt(sum_squared_deviations / data.shape[1])
  507. return division

functions.py at commit 78d5511, under EUPL-1.2 · at the source

Overview

Authors: Timo Hofsähs1,2, Marius Pille1,2, Lucas Kern1,2,3, Anuja Negi1,2,4,5, Jil Mona Meier1,2, Petra Ritter1,2,4,6,7
  1. Berlin Institute of Health at Charité - Universitätsmedizin Berlin, Berlin, Germany
  2. Charité - Universitätsmedizin Berlin, Corporate Member of Freie Universität Berlin and Humboldt-Universität zu Berlin, Department of Neurology with Experimental Neurology, Brain Simulation Section, Berlin, Germany
  3. FIL Methods Group - Department for Imaging Neuroscience (formerly The Wellcome Centre for Human Neuroimaging), UCL Queen Square Institute of Neurology, University College London, London, United Kingdom
  4. Bernstein Focus State Dependencies of Learning and Bernstein Center for Computational Neuroscience, Berlin, Germany
  5. Technical University Berlin, Berlin, Germany
  6. Einstein Center for Neuroscience Berlin, Berlin, Germany
  7. Einstein Center Digital Future, Berlin, Germany
Journal: Imaging neuroscience (Cambridge, Mass.), volume 4, article IMAG.a.1147
Dates: received 16 April 2025; accepted 29 January 2026; published online 3 March 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1162/imag.a.1147 · PMID 41799679 · PMCID PMC12961309 · OpenAlex W7128439752
Open access: diamond, a free copy (OpenAlex)
Status: code verified
Categories: other (modality), human (organism), depression (population), computational (subfield)
Methods: Single-unit activity, calcium imaging
Keywords: GABA, major depressive disorder, neural mass model, TEP, TMS, whole-brain simulations
Topic: Transcranial Magnetic Stimulation Studies (Neurology, Neuroscience), according to OpenAlex
Funding: Deutsche Forschungsgemeinschaft (SFB-1436, SFB 936, SFB 1315, RI 2073/6-1, 504745852, 785907, 327654276, TRR-295, RI 2073/10-2, 424778381, RI 2073/9-1, 178316478, 425899996); Berlin Institute of Health; European Regional Development Fund (101147319, #945539, EU‐H2020, 826421, 785907); HORIZON EUROPE Framework Programme (101057655, 945539, 785907, SGA3 945539, 101058516, 101147319, 101137289); Horizon 2020 (EU H2020 Virtual Brain Cloud 826421, AISN – 101057655, EBRAIN-Health 101058516, Digital Europe TEF-Health 101100700, Virtual Brain Twin (101137289), EBRAINS2.0 (101147319), EBRAINS-PREP 101079717)
Citations: cited by 1 paper (Europe PMC); 108 references in the paper

Abstract

Transcranial magnetic stimulation evoked potentials (TEPs) show promise as a biomarker in major depressive disorder (MDD), but the origin of the increased TEP amplitude in these patients remains unclear. Gamma aminobutyric acid (GABA) may be involved, as TEP peak amplitude is known to increase with GABAergic activity in healthy controls. We employed a computational modeling approach to investigate this phenomenon. Whole-brain simulations in ‘The Virtual Brain’ (thevirtualbrain.org), employing the Jansen and Rit neural mass model, were optimized to simulate TEPs of healthy individuals (Nsubs = 20, 14 females, 24.5 ± 4.9 years). To mimic MDD-like impaired inhibition, a GABAergic deficit was introduced to the simulations by altering one of two selected inhibitory parameters, the inhibitory synaptic decay rate b or the number of inhibitory synapses C4. The TEP amplitude was quantified and compared for all simulations. The inhibitory synaptic decay rate showed a quadratic correlation (r = 0.99, p < 0.001) and the number of inhibitory synapses a negative exponential correlation (r = 0.99, p < 0.001) with the TEP amplitude. Moreover, significant correlations between these simulation-derived values and all TEP peaks and troughs were detected (p < 0.001). Thus, under local parameter changes, we were able to alter the TEP amplitude toward pathological levels, that is, creating an MDD-like increase of the global mean field amplitude in line with empirical results. Our model suggests specific GABAergic deficits as the cause of increased TEP amplitude in MDD patients, which may serve as therapeutic targets. This work highlights the potential of whole-brain simulations in the investigation of neuropsychiatric diseases.

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 6 matches between paragraphs and lines of code.

virtual-twin/TMS_MDD

License: EUPL-1.2
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: 78d55110979aa6621c2e25ce96a029b26c76f3b3, 8 January 2026
Languages: Python (6)
Size: 14 files, 6 scripts
Software Heritage: not archived
Found in: “Data and Code Availability”
Holds: README, license file, environment (environment.yml, requirements.txt)
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (6 files), pandas (4 files), SciPy (4 files), Numba (2 files), The Virtual Brain (2 files), Matplotlib (1 file), MNE-Python (1 file), PyTorch (1 file), scikit-learn (1 file), seaborn (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
8 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;
  • 6 scripts, each with its path and the digest of its content;
  • 6 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data and Code Availability

The empirical TEP dataset (Biabani & Rogasch, 2019) is publicly available for download (https://bridges.monash.edu/articles/dataset/TEPs-SEPs/7440713?file=13772894). The entire code to generate the results presented in this study is openly available (https://github.com/virtual-twin/TMS_MDD).

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 September 2026: the first record

Recorded: type, language, journal, volume, pages, dates, 6 authors, 6 keywords, 5 funders, 106 references.

Cite

This paper

Hofsähs, T., Pille, M., Kern, L., Negi, A., Meier, J. M., & Ritter, P. (2026). The Virtual Brain links transcranial magnetic stimulation evoked potentials and inhibitory neurotransmitter changes in major depressive disorder. Imaging neuroscience (Cambridge, Mass.), 4, IMAG.a.1147. https://doi.org/10.1162/imag.a.1147

BibTeX

@article{hofsahs2026virtual,
author = {Hofsähs, Timo and Pille, Marius and Kern, Lucas and Negi, Anuja and Meier, Jil Mona and Ritter, Petra},
title = {{The Virtual Brain links transcranial magnetic stimulation evoked potentials and inhibitory neurotransmitter changes in major depressive disorder}},
journal = {Imaging neuroscience (Cambridge, Mass.)},
year = {2026},
month = mar,
volume = {4},
pages = {IMAG.a.1147},
publisher = {MIT Press},
issn = {2837-6056},
doi = {10.1162/imag.a.1147},
url = {https://doi.org/10.1162/imag.a.1147},
pmid = {41799679},
pmcid = {PMC12961309}
}

RIS

TY - JOUR
AU - Hofsähs, Timo
AU - Pille, Marius
AU - Kern, Lucas
AU - Negi, Anuja
AU - Meier, Jil Mona
AU - Ritter, Petra
TI - The Virtual Brain links transcranial magnetic stimulation evoked potentials and inhibitory neurotransmitter changes in major depressive disorder
T2 - Imaging neuroscience (Cambridge, Mass.)
J2 - Imaging Neurosci (Camb)
PY - 2026
DA - 2026/03/03
VL - 4
SP - IMAG.a.1147
SN - 2837-6056
PB - MIT Press
DO - 10.1162/imag.a.1147
UR - https://doi.org/10.1162/imag.a.1147
LA - en
ER -

CSL-JSON

{
"id": "10.1162/imag.a.1147",
"type": "article-journal",
"title": "The Virtual Brain links transcranial magnetic stimulation evoked potentials and inhibitory neurotransmitter changes in major depressive disorder",
"container-title": "Imaging neuroscience (Cambridge, Mass.)",
"author": [
{
"family": "Hofsähs",
"given": "Timo"
},
{
"family": "Pille",
"given": "Marius"
},
{
"family": "Kern",
"given": "Lucas"
},
{
"family": "Negi",
"given": "Anuja"
},
{
"family": "Meier",
"given": "Jil Mona"
},
{
"family": "Ritter",
"given": "Petra"
}
],
"container-title-short": "Imaging Neurosci (Camb)",
"volume": "4",
"page": "IMAG.a.1147",
"DOI": "10.1162/imag.a.1147",
"PMID": "41799679",
"PMCID": "PMC12961309",
"ISSN": "2837-6056",
"publisher": "MIT Press",
"URL": "https://doi.org/10.1162/imag.a.1147",
"language": "en",
"issued": {
"date-parts": [
[
2026,
3,
3
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1371/journal.pcbi.1013463 [code]
A multi-frequency whole-brain neural mass model with homeostatic feedback inhibition.
Journal: PLoS computational biology
In common: Numba, scikit-learn, SciPy, 2 other tools, computational, 9 references
[2] doi:10.1038/s41467-026-71918-7 [code]
Developmental disinhibition gates language lateralization in childhood.
Journal: Nature communications
In common: MNE-Python, PyTorch, seaborn, 5 other tools, 5 references
[3] doi:10.1073/pnas.2532072123 [code]
Spatially structured heterogeneity shapes large-scale cortical dynamics in a model of the human cortex.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: The Virtual Brain, SciPy, Matplotlib, 1 other tool, 5 references
[4] doi:10.1371/journal.pdig.0001445 [code]
A digital twin approach for simultaneous reconstruction of brain anatomy and dynamics from neural data.
Journal: PLOS digital health
In common: 8 references
[5] doi:10.1371/journal.pcbi.1014222 [code]
Neural population models for EEG: From Canonical models to alternative model structures.
Journal: PLoS computational biology
In common: computational, 7 references
[6] doi:10.1038/s41398-026-04107-1
Abnormal left prefrontal N100 and its relationship with fronto-limbic metabolism in major depressive disorder.
Journal: Translational psychiatry
In common: depression, other, 6 references
[7] doi:10.1162/netn.a.543 [code]
High-resolution Bayesian Virtual Epileptic Patient using neural field models.
Journal: Network neuroscience (Cambridge, Mass.)
In common: The Virtual Brain, MNE-Python, SciPy, 2 other tools, computational, 2 references
[8] doi:10.1038/s41593-026-02205-3 [code]
Competitive interactions shape mammalian brain network dynamics and computation.
Journal: Nature neuroscience
In common: PyTorch, seaborn, scikit-learn, 4 other tools, 3 references
[9] doi:10.1162/imag.a.1356
Sources of the N15 TMS-evoked potential following motor cortex stimulation localize rostrals to TMS-induced electric fields and depend on dose.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: other, 6 references
[10] doi:10.1162/imag.a.1222 [code]
Network-based near-scalp personalized brain stimulation targets.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: PyTorch, scikit-learn, SciPy, 2 other tools, other, 3 references

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

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