OSCR

Exploring neural manifolds across a wide range of intrinsic dimensions.

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] § Materials and methods › Recurrent neural networks performing cog-tasks ↔ Code/task_handler2.py, lines 150–165 · score 0.60 · connection matrices, 0–1, Win, Wout, Wrec

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 · 601 lines · 20 KB · MIT · 1 match

  1. import numpy as np
  2. import matplotlib.pyplot as plt
  3. import h5py
  4. from ID_estimator import *
  5. from network_structures import BPTT
  6. import tensorflow as tf
  7. import sys
  8. sys.path.append('../multitask/')
  9. from task import generate_trials, rule_name
  10. from network import Model
  11. import tools
  12. from pyFCI import pyFCI
  13. class Task:
  14. # Class to handle all operations each task needs
  15. def __init__(self, task_name, task_dir, basedir=".", batch_size=200, verbose=False, **kwargs):
  16. # initialize all the matrices
  17. initial_time = kwargs.get('initial_time', None)
  18. self.task_name = task_name
  19. self.basedir = basedir
  20. self.task_dir = task_dir
  21. self.q = batch_size
  22. with h5py.File(f"{basedir}/{task_dir}/RNN_params.h5", "r") as f:
  23. self.Win = f["w_in"][:]
  24. self.Wrec = f["w_rec"][:]
  25. self.Wout = f["w_out"][:]
  26. self.brec = f["brec"][:]
  27. self.bout = f["bout"][:]
  28. with h5py.File(f"{basedir}/{task_dir}/network_activity.h5", "r") as f:
  29. self.r = f["r"][:]
  30. self.z = f["z"][:]
  31. self.x_train = f["x_train"][:]
  32. self.y_train = f["y_train"][:]
  33. if verbose:
  34. print(f"Task {task_name} loaded.")
  35. print(f"Number of neurons: {np.size(self.Win.T[0])}")
  36. def network(self):
  37. # compute the back-propagation through time on the given task
  38. r, z = BPTT(self.Win.T, self.Wrec.T, self.Wout.T, self.brec, self.bout, self.x_train)
  39. self.r = r
  40. self.z = z
  41. return r, z # return recurrent and output part
  42. def network_yang(self, task_name=None, basedir=None,batch_size=200, **kwargs):
  43. basedir = self.basedir if basedir is None else basedir
  44. task_name = self.task_name if task_name is None else task_name
  45. model_dir = basedir+"/"+ task_name
  46. rule = task_name
  47. model = Model(model_dir)
  48. hp = model.hp
  49. initial_time = kwargs.get('initial_time', None)
  50. with tf.compat.v1.Session() as sess:
  51. model.restore()
  52. #trial = generate_trials(rule, hp, mode='test',batch_size = 1)
  53. if initial_time:
  54. trial = generate_trials(rule, hp, mode='random',batch_size = batch_size, initial_time = initial_time)
  55. else:
  56. trial = generate_trials(rule, hp, mode='random',batch_size = batch_size)
  57. feed_dict = tools.gen_feed_dict(model, trial, hp)
  58. h, y_hat = sess.run([model.h, model.y_hat], feed_dict=feed_dict)
  59. # All matrices have shape (n_time, n_condition, n_neuron)
  60. # print(np.shape(trial.x), np.shape(h), np.shape(y_hat))
  61. if initial_time:
  62. h = h[initial_time:,:,:]
  63. y_hat = y_hat[initial_time:,:,:]
  64. var_list = model.var_list
  65. # evaluate the parameters after training
  66. params = [sess.run(var) for var in var_list]
  67. # get name of each variable
  68. names = [var.name for var in var_list]
  69. y_trained = np.vstack(np.swapaxes(y_hat, 0, 1))
  70. h_trained = np.vstack(np.swapaxes(h, 0, 1))
  71. self.r = h_trained
  72. self.z = y_trained
  73. return h_trained, y_trained
  74. def find_relevant_points(self):
  75. # find relevant points for the task
  76. x_train = self.x_train
  77. y_train = self.y_train
  78. trials_len = x_train.shape[0]//self.q
  79. self.trials_len = trials_len
  80. stim = np.where(x_train[:trials_len,1:65].max(1)>0.3)[0]
  81. stim_start = stim[0] if len(stim)>0 else 0
  82. stim_end = stim[-1] if len(stim)>0 else 0
  83. go = np.where(y_train[:trials_len,0]<0.8)[0]
  84. go_start = go[0] if len(go)>0 else 0
  85. go_end = go[-1] if len(go)>0 else 0
  86. self.stim_start = stim_start
  87. self.stim_end = stim_end
  88. self.go_start = go_start
  89. self.go_end = go_end
  90. return stim_start, stim_end, go_start, go_end, trials_len
  91. def compute_angle(self):
  92. # decode target angle from output
  93. # z: output part
  94. # n: trial length
  95. angle = np.zeros(self.q)
  96. z = self.z
  97. n = self.trials_len
  98. q = self.q
  99. for i in range(q):
  100. f = z[n*(i+1)-10:n*(i+1),1:]
  101. fm = np.mean(f,axis=0)
  102. angle[i] = np.argmax(fm)
  103. self.angle = angle
  104. return angle
  105. def split_data(self):
  106. q = self.q
  107. n = self.trials_len
  108. n1 = self.stim_start
  109. n2 = self.go_start
  110. beginning = np.repeat(np.arange(0, n*q, n), n1) + np.tile(np.arange(0, n1), q)
  111. if n2>n1:
  112. middle = np.repeat(np.arange(0, n*q, n), n2-n1) + np.tile(np.arange(n1, n2), q)
  113. else:
  114. middle = np.array([])
  115. final = np.repeat(np.arange(0, n*q, n), n-n2) + np.tile(np.arange(n2, n), q)
  116. self.beginning = beginning
  117. self.middle = middle
  118. self.final = final
  119. def connectivity_matrix(self):
  120. # plot the connectivity matrix
  121. fig, ax = plt.subplots(2, 2, figsize=(4, 4),tight_layout=True)
  122. A = ax[0,0].imshow(self.Win.T)
  123. plt.colorbar(A)
  124. ax[0,0].set_title("$W_{in}$")
  125. B = ax[0,1].imshow(self.Wrec.T)
  126. plt.colorbar(B)
  127. ax[0,1].set_title("$W_{rec}$")
  128. C = ax[1,0].imshow(self.Wout.T)
  129. plt.colorbar(C)
  130. ax[1,0].set_title("$W_{out}$")
  131. plt.show()
  132. def network_plot(self, lim):
  133. # display the dynamics of the network
  134. fig, axs = plt.subplots(2, 2, figsize=(7, 4),tight_layout=True)
  135. fig.suptitle("Train")
  136. axs[0,0].plot(self.z[:,1:65])
  137. axs[0,0].set_xlim(0, lim)
  138. axs[0,0].set_xlabel("time step (ms)")
  139. axs[0,0].set_ylabel("output")
  140. axs[0,1].plot(self.y_train[:,1:65])
  141. axs[0,1].set_xlim(0, lim)
  142. axs[0,1].set_xlabel("time step (ms)")
  143. axs[0,1].set_ylabel("target")
  144. axs[1,0].plot((self.z - self.y_train)[:,1:65])
  145. axs[1,0].set_xlim(0, lim)
  146. axs[1,0].set_title("Training error")
  147. axs[1,0].set_xlabel("time step (ms)")
  148. axs[1,0].set_ylabel(r"$\Delta$")
  149. axs[1,1].set_axis_off()
  150. plt.show()
  151. def sample_plots(self, lim=200):
  152. """
  153. Plots the values of the different input an output channels
  154. task : task object with the task data and the trial information
  155. lim: time limit
  156. """
  157. relevant = self.find_relevant_points()
  158. x_train = self.x_train[:lim+1]
  159. z = self.z[:lim+1]
  160. r = self.r[:lim+1]
  161. len_trial = self.trials_len
  162. fig = plt.figure(figsize=(8,6))
  163. gs = fig.add_gridspec(4, height_ratios=[1.4,1,1.07,1.07], hspace=0, bottom=0.14,left=0.12)
  164. axs = gs.subplots(sharex=True)
  165. numcols = lim+1
  166. numrows = 32
  167. norm_r = (r - r.mean()) / r.std()
  168. #print(norm_r.min(), norm_r.max())
  169. #adj_r = r - r.min()
  170. #adj_r /= adj_r.max()
  171. #print(adj_r.min(), adj_r.max())
  172. axs[0].imshow(norm_r.T, vmin=-1,vmax=10, cmap="viridis",origin="lower", aspect="auto")
  173. axs[0].set_yticks([50,150,250],labels=(50,150,250), fontsize="x-large")
  174. igo = np.repeat(x_train[:,0].reshape(1,-1), 2, axis=0)
  175. i1go = np.concatenate((igo,x_train[:lim+1,1:33].T), axis=0)
  176. fix = np.repeat(z[:,0].reshape(1,-1), 2, axis=0)
  177. ofix = np.concatenate((fix,z[:,1:33].T), axis=0)
  178. axs[1].imshow(x_train[:,33:-1].T, vmin=0,vmax=1, origin="lower",extent=(-0.5,numcols-0.5,-0.5,numrows-0.5), aspect="auto")
  179. axs[2].imshow(i1go, vmin=0,vmax=1, origin="lower",extent=(-0.5,numcols-0.5,-2.5,numrows-0.5), aspect="auto")
  180. axs[3].imshow(ofix, vmin=0,vmax=1, origin="lower", extent=(-0.5,numcols-0.5,-2.5,numrows-0.5), aspect="auto")
  181. for i in range(lim//len_trial):
  182. axs[0].axvline((i+1)*len_trial-0.5, color="lime", linewidth=1.5)
  183. axs[1].axvline((i+1)*len_trial-0.5, color="lime", linewidth=1.5)
  184. axs[2].axvline((i+1)*len_trial-0.5, color="lime", linewidth=1.5)
  185. axs[3].axvline((i+1)*len_trial-0.5, color="lime", linewidth=1.5)
  186. axs[2].axhline(-0.5, color="darkslategray", linewidth=1.5)
  187. axs[3].axhline(-0.5, color="slategray", linewidth=1.5)
  188. axs[1].set_xlim(0, lim)
  189. axs[2].set_xlim(0, lim)
  190. axs[3].set_xlim(0, lim)
  191. axs[0].set_ylabel(r"$\mathbf{N}\ $",rotation=0, fontsize="x-large")
  192. axs[1].set_ylabel(r"$\mathbf{I_2}$",rotation=0, fontsize="x-large")
  193. axs[2].set_ylabel(r"$\mathbf{I_1}$",rotation=0, fontsize="x-large")
  194. axs[3].set_ylabel(r"$\mathbf{O}$", rotation=0, fontsize="x-large")
  195. axs[3].set_xlabel("t", fontweight="bold")
  196. axs[2].annotate(r"$\mathbf{I_{fix}}$", xy=(0, -1.5), xycoords="data", xytext=(-38, -0.7), textcoords="axes points",fontsize="large", fontweight="bold")
  197. axs[3].annotate(r"$\mathbf{O_{fix}}$", xy=(0, -1.5), xycoords="data", xytext=(-38, -0.7), textcoords="axes points",fontsize="large", fontweight="bold")
  198. axs[0].spines["bottom"].set_edgecolor("silver")
  199. axs[1].spines["top"].set_edgecolor("silver")
  200. axs[1].spines["bottom"].set_edgecolor("darkslategray")
  201. axs[2].spines["top"].set_edgecolor("darkslategray")
  202. axs[2].spines["bottom"].set_edgecolor("silver")
  203. axs[3].spines["top"].set_edgecolor("silver")
  204. axs[1].set_yticks([0,16,32],labels=(0,r"$\pi$",r"$2\pi$"), fontsize="x-large")
  205. axs[2].set_yticks([0,16],labels=(0,r"$\pi$"), fontsize="x-large")
  206. axs[3].set_yticks([0,16],labels=(0,r"$\pi$"), fontsize="x-large")
  207. fig.align_ylabels()
  208. def ID_FCI_estimator(self, Niter, full, method = "full", **kwargs):
  209. """
  210. compute the intrinsic dimension using the FCI method
  211. Parameters:
  212. -----------
  213. Niter : int
  214. the number of iterations if method is "mc"
  215. full : boolean
  216. if True returns also the fit parameters and the fci integral
  217. method : string
  218. the method used to compute the intrinsic dimension, default is "full",
  219. the other option is "mc".
  220. Returns:
  221. --------
  222. id_fci : float
  223. the intrinsic dimension using FCI
  224. """
  225. if method == "mc":
  226. # compute the FCI using montecarlo
  227. if full:
  228. id_fci, fit, fci = ID_FCI_MC(self.r, Niter, full, **kwargs)
  229. else:
  230. id_fci = ID_FCI_MC(self.r, Niter, **kwargs)
  231. elif method == "full":
  232. # compute the FCI
  233. if full:
  234. id_fci, fit, fci = ID_FCI(self.r, full, **kwargs)
  235. else:
  236. id_fci = ID_FCI(self.r, **kwargs)
  237. else:
  238. raise ValueError("method must be 'full' or 'mc'")
  239. self.id_fci = id_fci
  240. if full:
  241. return id_fci, fit, fci
  242. else:
  243. return id_fci
  244. def ID_pca(self,normalize, full):
  245. """
  246. Compute the intrinsic dimension using the PCA explained variance.
  247. Parameters
  248. ----------
  249. normalize : boolean
  250. if True, the data points are normalized
  251. full : boolean
  252. if True returns also the PCs and the explained variance
  253. Returns
  254. -------
  255. id_pca : float
  256. the intrinsic dimension using PCA
  257. y : array
  258. the projected data points
  259. v : array
  260. the explained variance ratio
  261. """
  262. if full:
  263. id_pca, y, v = ID_PCA(self.r, normalize, full)
  264. self.id_pca = id_pca
  265. return id_pca, y, v
  266. else:
  267. id_pca = ID_PCA(self.r, normalize)
  268. self.id_pca = id_pca
  269. return id_pca
  270. def ID_participation_ratio(self):
  271. """
  272. Compute the intrinsic dimension using the participation ratio.
  273. Parameters
  274. ----------
  275. None
  276. Returns
  277. -------
  278. id_pr : float
  279. The intrinsic dimension using the participation ratio
  280. """
  281. d_pr = participation_ratio(self.r)
  282. self.id_pr = d_pr
  283. return d_pr
  284. def ID_parallel_analysis(self,data_range = None,**kwargs):
  285. """
  286. Compute the intrinsic dimension using the parallel analysis.
  287. Parameters
  288. ----------
  289. data_range : array, optional, default None
  290. the range of the data for the parallel analysis
  291. kwargs : dict
  292. keyword arguments for the parallel analysis:
  293. - num_shuffles: default 250,
  294. - percentile: default 95
  295. Returns
  296. -------
  297. id_pa : float
  298. The intrinsic dimension using the parallel analysis
  299. """
  300. if data_range is None:
  301. id_pa = id_parallel_analysis(self.r, **kwargs)
  302. else:
  303. id_pa = id_parallel_analysis(self.r[data_range], **kwargs)
  304. self.id_pa = id_pa
  305. return id_pa
  306. def id_twoNN(self, twonn=True, scale_dependent=False,**kwargs):
  307. # compute the intrinsic dimension using two nearest neighbors
  308. drange = kwargs.get('drange', None)
  309. if twonn and scale_dependent:
  310. id_2NN, id_2NN_scale, fitting_params = ID_twoNN_dadapy(self.r[drange].squeeze(), twonn, scale_dependent, **kwargs)
  311. self.id_2NN = id_2NN
  312. self.id_2NN_scale = id_2NN_scale
  313. return id_2NN, id_2NN_scale, fitting_params
  314. elif twonn and not scale_dependent:
  315. id_2NN, fitting_params = ID_twoNN_dadapy(self.r[drange].squeeze(), twonn, scale_dependent, **kwargs)
  316. self.id_2NN = id_2NN
  317. return id_2NN, fitting_params
  318. elif not twonn and scale_dependent:
  319. id_2NN_scale, fitting_params = ID_twoNN_dadapy(self.r[drange].squeeze(), twonn, scale_dependent, **kwargs)
  320. self.id_2NN_scale = id_2NN_scale
  321. return id_2NN_scale, fitting_params
  322. def ID(self):
  323. # compute the intrinsic dimension
  324. d, x, y = ID_estimator(self.r)
  325. self.d = d
  326. self.x = x
  327. self.y = y
  328. def collect_ID_FCI(self, n_iter = 20, method = "mc", **kwargs):
  329. """
  330. compute the intrinsic dimension using FCI
  331. parameters:
  332. -----------
  333. n_iter : int
  334. the number of iterations if method is "mc"
  335. method : string
  336. the method used to compute the intrinsic dimension, default is "mc",
  337. the other option is "full".
  338. """
  339. q = self.q
  340. n = self.trials_len
  341. n1 = self.stim_start
  342. n2 = self.go_start
  343. r = self.r
  344. norm_r = pyFCI.center_and_normalize(r)
  345. beginning = self.beginning
  346. middle = self.middle
  347. final = self.final
  348. if method == "full":
  349. d_beg_fci = ID_FCI(norm_r[beginning,:], normalize=False, **kwargs)
  350. else:
  351. d_beg_fci = ID_FCI_MC(norm_r[beginning,:], n_iter, normalize=False, **kwargs)
  352. print(f"Beginning dimension: {d_beg_fci}")
  353. if n2-n1>0:
  354. if method == "full":
  355. d_mid_fci = ID_FCI(norm_r[middle,:], normalize=False, **kwargs)
  356. else:
  357. d_mid_fci = ID_FCI_MC(norm_r[middle,:], n_iter, normalize=False, **kwargs)
  358. print(f"Middle dimension: {d_mid_fci}")
  359. else:
  360. d_mid_fci = 0
  361. if method == "full":
  362. d_fin_fci = ID_FCI(norm_r[final,:], normalize=False, **kwargs)
  363. else:
  364. d_fin_fci = ID_FCI_MC(norm_r[final,:], n_iter, normalize=False, **kwargs)
  365. print(f"Final dimension: {d_fin_fci}")
  366. if method == "full":
  367. d_tot_fci = ID_FCI(norm_r, normalize=False, **kwargs)
  368. else:
  369. d_tot_fci = ID_FCI_MC(norm_r, n_iter, normalize=False, **kwargs)
  370. print(f"Total dimension: {d_tot_fci}")
  371. dim_vector_fci = np.array([d_beg_fci, d_mid_fci, d_fin_fci, d_tot_fci])
  372. self.dim_fci_regions = dim_vector_fci
  373. return dim_vector_fci
  374. def collect_ID_twoNN(self, display=False, **kwargs):
  375. beginning = self.beginning
  376. middle = self.middle
  377. final = self.final
  378. r = self.r
  379. # norm_r = (r - np.mean(r, axis=0)) / np.std(r, axis=0)
  380. if len(beginning)>10:
  381. d_beg, beg = ID_twoNN_dadapy(r[beginning, :])
  382. print(f"Beginning dimension: {d_beg}")
  383. else:
  384. d_beg = 0
  385. if len(middle)>10:
  386. d_mid, mid = ID_twoNN_dadapy(r[middle, :])
  387. print(f"Middle dimension: {d_mid}")
  388. else:
  389. d_mid = 0
  390. if len(final)>10:
  391. d_fin, fin = ID_twoNN_dadapy(r[final, :])
  392. print(f"Final dimension: {d_fin}")
  393. else:
  394. d_fin = 0
  395. d_tot, tot = ID_twoNN_dadapy(r)
  396. print(f"Total dimension: {d_tot}")
  397. dim_vector = np.array([d_beg, d_mid, d_fin, d_tot])
  398. self.dim_twonn_regions = dim_vector
  399. if display:
  400. self.display_ID(beg, mid, fin, tot)
  401. return dim_vector
  402. def display_ID(self, beg, mid, fin, tot):
  403. # plot the intrinsic dimension
  404. dims = self.dim_vector_twonn
  405. d_beg = dims[0]
  406. d_mid = dims[1]
  407. d_fin = dims[2]
  408. d_tot = dims[3]
  409. fig, axs = plt.subplots(2, 2, figsize=(10, 8))
  410. axs[0, 0].scatter(beg[1], beg[0], s=20, marker="x")
  411. axs[0, 0].plot(beg[1], beg[1]*d_beg, c="red")
  412. axs[0, 0].set_title(f"Beginning dimension: {np.round(d_beg, 2)}")
  413. axs[0, 1].scatter(mid[1], mid[0], s=20, marker="x")
  414. axs[0, 1].plot(mid[1], mid[1]*d_mid, c="red")
  415. axs[0, 1].set_title(f"Middle dimension: {np.round(d_mid, 2)}")
  416. axs[1, 0].scatter(fin[1], fin[0], s=20, marker="x")
  417. axs[1, 0].plot(fin[1], fin[1]*d_fin, c="red")
  418. axs[1, 0].set_title(f"Final dimension: {np.round(d_fin, 2)}")
  419. axs[1, 1].scatter(tot[1], tot[0], s=10, marker="x")
  420. axs[1, 1].plot(tot[1], tot[1]*d_tot, c="red")
  421. axs[1, 1].set_title(f"Total dimension: {np.round(d_tot, 2)}")
  422. plt.show()
  423. def scale_ID_twoNN(self, display = False, **kwargs):
  424. # compute scale dependent ID for every range defined before
  425. beginning = self.beginning
  426. middle = self.middle
  427. final = self.final
  428. r = self.r
  429. # compute the intrinsic dimension
  430. if len(beginning)>10:
  431. dim_beg, beg = ID_twoNN_dadapy(r[beginning, :],twonn=False, scale_dependent=True, **kwargs)
  432. else:
  433. dim_beg = 0
  434. if len(middle)>10:
  435. dim_mid, mid = ID_twoNN_dadapy(r[middle, :],twonn=False, scale_dependent=True, **kwargs)
  436. else:
  437. dim_mid = 0
  438. if len(final)>10:
  439. dim_fin, fin = ID_twoNN_dadapy(r[final, :],twonn=False, scale_dependent=True, **kwargs)
  440. else:
  441. dim_fin = 0
  442. dim_tot, tot = ID_twoNN_dadapy(r,twonn=False, scale_dependent=True, **kwargs)
  443. # compute and return the four dimensions
  444. res = np.array([dim_beg, dim_mid, dim_fin, dim_tot])
  445. self.dim_twonn_scale_regions = res
  446. if display:
  447. if "beg" in locals():
  448. fig, axs = plt.subplots(2, 2, figsize=(12, 7))
  449. axs[0, 0].errorbar(beg[1], beg[0], yerr=beg[2], marker="o", capsize=4)
  450. axs[0, 0].set_title(f"Beginning dimension")
  451. #axs[0, 0].set_xscale("log")
  452. axs[0, 0].grid(axis="y")
  453. if "mid" in locals():
  454. axs[0, 1].errorbar(mid[1], mid[0], yerr=mid[2], marker="o", capsize=4)
  455. axs[0, 1].set_title(f"Middle dimension")
  456. #axs[0, 1].set_xscale("log")
  457. axs[0, 1].grid(axis="y")
  458. if "fin" in locals():
  459. axs[1, 0].errorbar(fin[1], fin[0], yerr=fin[2], marker="o", capsize=4)
  460. axs[1, 0].set_title(f"Final dimension")
  461. #axs[1, 0].set_xscale("log")
  462. axs[1, 0].grid(axis="y")
  463. axs[1, 1].errorbar(tot[1], tot[0], yerr=tot[2], marker="o", capsize=4)
  464. axs[1, 1].set_title(f"Total dimension")
  465. #axs[1, 1].set_xscale("log")
  466. axs[1, 1].grid(axis="y")
  467. plt.show()
  468. return res, dim_beg, dim_mid, dim_fin, dim_tot
  469. def find_dim(self, dim):
  470. # find the dimension from the plateaux in the scale dependent ID
  471. plateaus, _, _ = find_plateaus(dims, min_length=2, tolerance = 0.3)
  472. if len(plateaus) > 0:
  473. means = []
  474. for p in plateaus:
  475. means.append(np.mean(dims[p[0]:p[1]]))
  476. p_dim = np.min(means)
  477. else:
  478. p_dim = ids_scaling[-1]
  479. return p_dim

task_handler2.py at commit 8fd202d, under MIT · at the source

Overview

Authors: Jacopo Fadanni1, Rosalba Pacelli2, Alberto Zucchetta2, Pietro Rotondo3, Michele Allegra1,4
  1. Physics and Astronomy Department, University of Padova, Padova, Italy
  2. Istituto Nazionale di Fisica Nucleare, Sezione di Padova, Padova, Italy
  3. Department of Mathematical, Physical and Computer Sciences, University of Parma, Parma, Italy
  4. Padova Neuroscience Center, University of Padova, Padova, Italy
Journal: PLoS computational biology, volume 22, issue 4, article e1014162
Dates: received 18 September 2025; accepted 23 March 2026; published online 3 April 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1371/journal.pcbi.1014162 · PMID 41931565 · PMCID PMC13068349 · OpenAlex W7148705637
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: human (organism), computational (subfield)
Methods: Connectivity, Smoothing, state filtering, decompositions, Statistics, Machine learning, Single-unit activity, calcium imaging
MeSH: Models, Neurological*, Neurons*, Algorithms, Animals, Computational Biology, Humans, Neural Networks, Computer, Recurrent Neural Networks (* major topic)
Journal subjects: Physical sciences, Mathematics, Geometry, Non-Euclidean geometry, Topology, Manifolds, Biology and Life Sciences, Cell Biology, Cellular Types, Animal Cells, Neurons, Neuroscience, Cellular Neuroscience, Research and Analysis Methods, Mathematical and Statistical Techniques, Statistical Methods, Multivariate Analysis, Principal Component Analysis, Statistics, Cognitive Science, Cognitive Psychology, Perception, Sensory Perception, Vision, Psychology, Social Sciences, Tangents, Curvature, Computer and Information Sciences, Neural Networks, Recurrent Neural Networks
Topic: Face Recognition and Perception (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: MUR (PRIN grant 2022HSKLK9)
Citations: not cited yet (Europe PMC); 82 references in the paper

Abstract

The rapid surge in the number of simultaneously recorded neurons demands reliable tools to explore the latent geometry of high-dimensional neural spaces. Within such spaces, neuronal activity typically lies on a subspace or manifold characterized by an intrinsic dimension (ID) that is much lower than the total number of recorded units. The ID can provide immediate information about the neural code, such as the minimum number of encoded variables and the relation between collective and individual neural activity. Existing studies rely on disparate and potentially unreliable ID estimators, which can contribute to conflicting reports of high-dimensional vs. low-dimensional manifolds. Here, we propose a robust and versatile pipeline for ID estimation, exploiting a local version of the full correlation integral estimator (lFCI). Being able to simultaneously cope with high dimensionality and non-linearity, lFCI overcomes some major limitations of common ID estimation methods. We prove the strength and accuracy of lFCI by applying it to synthetic benchmark data by Altan et al., 2019, where other methods typically underestimate the ID. We apply lFCI to study neural manifolds arising in recurrent neural networks trained on the 20 tasks of the well-known ‘cog-Task’ battery. Across tasks and training repetitions, lFCI uncovers a consistently low ID, which we show to be fundamentally related to the task structure. Finally, we apply lFCI to a reference experimental dataset by Stringer et al., 2019, comprising visual responses to a large set of natural images, strongly supporting previous reports that responses are organized in a high-dimensional manifold. lFCI has the potential to shed light on the current debate about the geometry of neural codes, and its dependence on structural constraints and computational goals in biological and artificial neural networks.

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.

JFadanni/ExploringNeuralManifolds

License: MIT
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 8fd202d5ef149c5b77e1cc25cd06863f849422eb, 21 September 2026
Languages: Python (10), Jupyter (5), Shell (1)
Size: 20 files, 16 scripts
Software Heritage: not archived
Found in: “Data Availability”
Holds: README, license file, 5 notebooks
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (14 files), Matplotlib (11 files), h5py (10 files), SciPy (8 files), scikit-learn (7 files), pandas (1 file), TensorFlow (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
18 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;
  • 16 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 Availability

The source code and data used to produce the results and analyses presented in this manuscript are available on GitHub: https://github.com/JFadanni/ExploringNeuralManifolds.git.

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

Recorded: type, language, journal, volume, issue, pages, dates, 5 authors, 8 MeSH terms, 1 funder, 62 references.

Cite

This paper

Fadanni, J., Pacelli, R., Zucchetta, A., Rotondo, P., & Allegra, M. (2026). Exploring neural manifolds across a wide range of intrinsic dimensions. PLoS computational biology, 22(4), e1014162. https://doi.org/10.1371/journal.pcbi.1014162

BibTeX

@article{fadanni2026exploring,
author = {Fadanni, Jacopo and Pacelli, Rosalba and Zucchetta, Alberto and Rotondo, Pietro and Allegra, Michele},
title = {{Exploring neural manifolds across a wide range of intrinsic dimensions}},
journal = {PLoS computational biology},
year = {2026},
month = apr,
volume = {22},
number = {4},
pages = {e1014162},
publisher = {PLOS},
issn = {1553-734X},
doi = {10.1371/journal.pcbi.1014162},
url = {https://doi.org/10.1371/journal.pcbi.1014162},
pmid = {41931565},
pmcid = {PMC13068349}
}

RIS

TY - JOUR
AU - Fadanni, Jacopo
AU - Pacelli, Rosalba
AU - Zucchetta, Alberto
AU - Rotondo, Pietro
AU - Allegra, Michele
TI - Exploring neural manifolds across a wide range of intrinsic dimensions
T2 - PLoS computational biology
J2 - PLoS Comput Biol
PY - 2026
DA - 2026/04/03
VL - 22
IS - 4
SP - e1014162
SN - 1553-734X
PB - PLOS
DO - 10.1371/journal.pcbi.1014162
UR - https://doi.org/10.1371/journal.pcbi.1014162
LA - en
ER -

CSL-JSON

{
"id": "10.1371/journal.pcbi.1014162",
"type": "article-journal",
"title": "Exploring neural manifolds across a wide range of intrinsic dimensions",
"container-title": "PLoS computational biology",
"author": [
{
"family": "Fadanni",
"given": "Jacopo"
},
{
"family": "Pacelli",
"given": "Rosalba"
},
{
"family": "Zucchetta",
"given": "Alberto"
},
{
"family": "Rotondo",
"given": "Pietro"
},
{
"family": "Allegra",
"given": "Michele"
}
],
"container-title-short": "PLoS Comput Biol",
"volume": "22",
"issue": "4",
"page": "e1014162",
"DOI": "10.1371/journal.pcbi.1014162",
"PMID": "41931565",
"PMCID": "PMC13068349",
"ISSN": "1553-734X",
"publisher": "PLOS",
"URL": "https://doi.org/10.1371/journal.pcbi.1014162",
"language": "en",
"issued": {
"date-parts": [
[
2026,
4,
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.1038/s41593-026-02333-w [code]
Learning shapes neural geometry in the primate prefrontal cortex.
Journal: Nature neuroscience
In common: scikit-learn, pandas, SciPy, 2 other tools, 8 references
[2] doi:10.1080/26941899.2026.2619222
Neurodatascience: Past, Present, and Future.
Journal: Data science in science
In common: computational, 10 references
[3] doi:10.3390/biomimetics11080569 [code]
Pretraining of Embodied Recurrent Networks Bridges the Gap Between Artificial and Cortical Neural Activities.
Journal: Biomimetics (Basel, Switzerland)
In common: scikit-learn, SciPy, Matplotlib, 1 other tool, 8 references
[4] doi:10.1371/journal.pbio.3003831 [code]
Disinhibitory signaling enables flexible coding of top-down information in cortical networks.
Journal: PLoS biology
In common: TensorFlow, h5py, scikit-learn, 4 other tools, 5 references
[5] doi:10.1038/s41598-026-55225-1 [code]
Benchmarking criteria to determine latent linear dimensionality in neural data.
Journal: Scientific reports
In common: pandas, 9 references
[6] doi:10.1177/26331055261460858 [code]
The Geometric Signatures of Brain State Transitions: Recursive Informational Curvature Reveals Hidden Dynamics in Primate Cortex.
Journal: Neuroscience insights
In common: scikit-learn, pandas, SciPy, 2 other tools, 7 references
[7] doi:10.1016/j.neuron.2026.07.016 [code]
Inferring brain-wide interactions using data-constrained recurrent neural network models.
Journal: Neuron
In common: Matplotlib, NumPy, 8 references
[8] doi:10.1038/s41467-026-75924-7 [code]
Data-driven reduced modeling of neural dynamics.
Journal: Nature communications
In common: h5py, scikit-learn, SciPy, 2 other tools, 6 references
[9] doi:10.1038/s41467-026-76104-3 [code]
Sensorimotor remapping drives task specialization in prefrontal cortex.
Journal: Nature communications
In common: scikit-learn, pandas, SciPy, 2 other tools, 5 references
[10] doi:10.1038/s41467-026-74347-8 [code]
Compositionality of social gaze in the prefrontal-amygdala circuits.
Journal: Nature communications
In common: scikit-learn, pandas, SciPy, 2 other tools, 5 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.