OSCR

LFP-LOC: an LFP power-based method for validating the anatomical placement of high-density neural probes in rodents.

Code ↔ Paper

13 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 13 matches
  1. [1] § Materials and methods › Step 1 - data processing and spectral power estimation ↔ src/lfploc/main.py, lines 256–279 · score 0.97 · 12–30 Hz, 30–100 Hz, 8–12 Hz, 100–250 Hz, ripple band, Power spectral density
  2. [2] § Materials and methods › Steps 2 and 3 - dimensionality reduction and clustering ↔ src/lfploc/main.py, lines 481–528 · score 0.75 · StandardScaler, explained variance, Dimensionality reduction, cumulative, module, PCA
  3. [3] § Materials and methods › Step 1 - data processing and spectral power estimation ↔ src/lfploc/main.py, lines 105–253 · score 0.72 · power spectral density, outlier removal, Hierarchical clustering, smoothing, PCA, PSD
  4. [4] § Materials and methods › Step 4 - brain atlas matching ↔ src/lfploc/main.py, lines 531–626 · score 0.69 · atlas voxel, brain atlas, stereotactic coordinates, slice, ML, DV
  5. [5] § Results › Spatial LFP band-power maps provide robust localization features ↔ src/lfploc/main.py, lines 256–279 · score 0.68 · 100–250 Hz, ripple band, beta, delta, gamma, alpha
  6. [6] § Materials and methods › Brain atlas selection and coordinates convention ↔ src/lfploc/main.py, lines 105–253 · score 0.67 · brainglobe atlasapi, ML coordinates, DV coordinates, locations, positions, probe
  7. [7] § Materials and methods › Steps 2 and 3 - dimensionality reduction and clustering ↔ src/lfploc/plotting.py, lines 208–246 · score 0.64 · cumulative explained variance, principal component, PCA, scored
  8. [8] § Materials and methods › Steps 2 and 3 - dimensionality reduction and clustering ↔ src/lfploc/main.py, lines 419–479 · score 0.63 · AgglomerativeClustering, silhouette_score, hierarchical, electrode
  9. [9] § Results › Atlas-based refinement improves electrode cluster–anatomy agreement ↔ src/lfploc/main.py, lines 282–299 · score 0.62 · brainglobe atlasapi, kim_mouse_isotropic_20um, probe geometry, clusters, electrode
  10. [10] § Materials and methods › Brain atlas selection and coordinates convention ↔ src/lfploc/utils.py, lines 137–156 · score 0.61 · relabelled atlas, rgb map, GitHub, repository
  11. [11] § Materials and methods › Brain atlas selection and coordinates convention ↔ src/lfploc/main.py, lines 282–299 · score 0.56 · brainglobe atlasapi, kim_mouse_isotropic_20um, clusters, electrode
  12. [12] § Materials and methods › Step 4 - brain atlas matching ↔ src/lfploc/main.py, lines 756–829 · score 0.51 · best match, coronal, shifted, slices, ML, DV
  13. [13] § Materials and methods › Brain atlas selection and coordinates convention ↔ src/lfploc/main.py, lines 531–626 · score 0.51 · cortical layers, native, axis, cortex, atlas, Brain

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 · 829 lines · 38 KB · MIT · 11 matches

  1. import numpy as np
  2. import os
  3. import pandas as pd
  4. import re
  5. import statistics
  6. import json
  7. from brainglobe_atlasapi import BrainGlobeAtlas
  8. from pathlib import Path
  9. from scipy.cluster.hierarchy import linkage
  10. from scipy.stats import zscore
  11. from sklearn.cluster import KMeans, DBSCAN, AgglomerativeClustering
  12. from sklearn.decomposition import PCA
  13. from sklearn.manifold import TSNE
  14. from sklearn.metrics import silhouette_score
  15. from sklearn.neighbors import NearestNeighbors
  16. from sklearn.preprocessing import StandardScaler
  17. from spikeinterface.core import BaseRecording
  18. from .plotting import plot_cluster_labels, plot_clusters_on_probe, plot_coordinates_on_atlas, plot_dbscan, plot_dendogram, plot_evaluation_matrix, plot_feature_grid_maps, plot_pca, plot_probe_on_atlas, plot_selected_features
  19. from .utils import bandpass_filter, build_border_image, cluster_color_map_for_labels, compute_psd, get_probe_properties, get_relabeled_atlas, remap_labels
  20. try:
  21. import umap
  22. HAS_UMAP = True
  23. except:
  24. HAS_UMAP = False
  25. try:
  26. import hdbscan
  27. HAS_HDBSCAN = True
  28. except:
  29. HAS_HDBSCAN = False
  30. class Lfploc:
  31. def __init__(self, rec : BaseRecording, order_traces_by_shank : bool = False, custom_n_cols : int = None):
  32. """
  33. Initialiaze an instance of the main Lfploc class.
  34. Parameters
  35. ----------
  36. rec : BaseRecording
  37. Recording initialized with SpikeInterface with probe pre-configured
  38. order_traces_by_shank : bool
  39. Orders channels in recording array by shank ID. Required by some Neuropixels datasets where traces are not ordered by shank, as required by LFP-LOC.
  40. custom_n_cols : int = None
  41. Specify number of columns per shank. Required by some Neuropixels datasets where pitch between shanks is not the same (like when selecting alternating columns).
  42. """
  43. self.rec = rec
  44. self.electrode_positions = ""
  45. self.df = None
  46. self.clustering_method = ""
  47. self.dimensionality_reduction_method = ""
  48. self.n_ch = 0
  49. self.new_order = None
  50. assert self.rec.has_probe(), "Recording does not have probe attached. Exiting."
  51. probe = self.rec.get_probe()
  52. self.electrode_positions = self.rec.get_channel_locations().T
  53. n_shanks = probe.get_shank_count()
  54. n_ch = probe.get_contact_count()
  55. n_cols, shank_spacing = get_probe_properties(self.electrode_positions, n_shanks)
  56. if custom_n_cols:
  57. n_cols = custom_n_cols
  58. self.probe_specs = {
  59. "x_left": 8,
  60. "x_right": 44,
  61. "y_top": 26,
  62. "y_bottom": 13,
  63. "shank_spacing": shank_spacing,
  64. "tip_angle": 26,
  65. "n_cols": n_cols,
  66. "n_shanks": n_shanks,
  67. "n_ch_per_shank": n_ch // n_shanks
  68. }
  69. if order_traces_by_shank and n_shanks > 1:
  70. self.rec.set_property("group", probe.shank_ids)
  71. split_recording = self.rec.split_by("group")
  72. temp_ep = np.concatenate(
  73. [split_recording[str(x)].get_channel_locations().T for x in range(n_shanks)],
  74. axis=1
  75. )
  76. self.new_order = np.lexsort((temp_ep[1, :], temp_ep[0, :]))
  77. self.electrode_positions = temp_ep.T[self.new_order].T
  78. else:
  79. order_traces_by_shank = False
  80. self.custom_device_indexing = order_traces_by_shank
  81. self.n_ch = n_ch
  82. self.cluster_labels = None
  83. def run(
  84. self,
  85. feature_extraction_method : str = "PCA",
  86. clustering_method : str = "hierarchical",
  87. start_time : float = 0.0,
  88. end_time : float = 30.0,
  89. save_report_dir : Path | str = None,
  90. do_car : bool = False,
  91. z_score_threshold : float = 2.5,
  92. window_size_outlier_removal : int = 20,
  93. window_size_smoothening : int = 8,
  94. save_report_format : str = "png",
  95. atlas_id : str = "kim_mouse_isotropic_20um",
  96. ap : float = None,
  97. ml : float = None,
  98. dv : float = None
  99. ) -> np.ndarray:
  100. """
  101. Run main localization algorithm. If ap, ml, and dv are not passed only the cluster labels
  102. for each electrode are returned. If a directory is passed through the 'save_report_dir' argument
  103. plots generated at each step are locally saved in the format defined in 'save_report_format'.
  104. Parameters
  105. ----------
  106. feature_extraction_method : str = 'PCA'
  107. Dimensionality reduction method to run on the power spectral features
  108. clustering_method : str = 'hierarchical'
  109. Clustering method to run after feature extraction
  110. start_time : float = 0.0
  111. Start time of recording segment to calculate power spectral features on
  112. end_time : float = 30.0
  113. End time of recording segment to calculate power spectral features on. If over length of recording
  114. automatically defaults to end of recording
  115. save_report_dir : Path | str = None
  116. Path to directory where to save plots generated during analysis
  117. do_car : bool = False
  118. Apply Common Reference Average
  119. z_score_threshold : float = 2.5
  120. Channels with normalized psd features above this z-score-threshold get labeled as noise and discarded.
  121. window_size_outlier_removal : int = 20
  122. Number of electrodes to consider in window when computing z-score for identifying channels to discard
  123. window_size_smoothening : int = 8
  124. Number of electrodes to consider in window when smoothening
  125. save_report_format : str = 'png'
  126. Format in which to save each plot generated. All methods supported by matplotlib accepted
  127. atlas_id : str = 'kim_mouse_isotropic_20um'
  128. ID of atlas to use, available through brainglobe-atlasapi
  129. ap : str = None
  130. AP coordinates. If not passed placement of probe on atlas is skipped.
  131. ml : str = None
  132. ML coordinates. If not passed placement of probe on atlas is skipped.
  133. dv : str = None
  134. DV coordinates. If not passed placement of probe on atlas is skipped.
  135. Returns
  136. -------
  137. full_labels : np.ndarray
  138. (n_chs, 1) numpy array with cluster labels matching the generalized location of each electrode.
  139. """
  140. if save_report_dir:
  141. os.makedirs(save_report_dir, exist_ok=True)
  142. fs = self.rec.sampling_frequency
  143. if end_time > self.rec.get_total_duration():
  144. end_time = self.rec.get_total_duration()
  145. if do_car:
  146. from spikeinterface.preprocessing import common_reference
  147. self.rec = common_reference(self.rec, operator="average")
  148. if self.custom_device_indexing:
  149. split_recording = self.rec.split_by("group")
  150. traces = np.concatenate(
  151. [split_recording[str(x)].get_traces(start_frame=start_time*fs, end_frame=end_time*fs).T for x in range(self.probe_specs["n_shanks"])],
  152. axis=0
  153. )
  154. traces = traces[self.new_order]
  155. else:
  156. traces = self.rec.get_traces(start_frame=start_time*fs, end_frame=end_time*fs, return_in_uV=True).T
  157. psd_features = self.get_psd_features(traces)
  158. n_ch_per_shank = self.probe_specs["n_ch_per_shank"]
  159. n_shanks = self.probe_specs["n_shanks"]
  160. n_cols = self.probe_specs["n_cols"]
  161. # remove outliers before smoothing
  162. for key in psd_features.keys():
  163. feature = psd_features[key]
  164. feature = feature.copy()
  165. window_size = window_size_outlier_removal
  166. for shank in range(n_shanks):
  167. shank_feature = feature[shank*n_ch_per_shank:(shank+1)*n_ch_per_shank]
  168. for start in range(0, len(shank_feature), window_size):
  169. end = min(start + window_size, len(feature))
  170. window = shank_feature[start:end]
  171. z_scores = zscore(window, nan_policy='omit')
  172. outlier_mask = np.abs(z_scores) > z_score_threshold
  173. window[outlier_mask] = np.nan
  174. shank_feature[start:end] = window
  175. feature[shank*n_ch_per_shank:(shank+1)*n_ch_per_shank] = shank_feature
  176. psd_features[key] = feature
  177. moving_avg_window = window_size_smoothening
  178. # split into shanks before smoothing and recombine after smoothing
  179. psd_features_smoothed = psd_features.copy()
  180. for key in psd_features.keys():
  181. feature = psd_features[key]
  182. smoothed_feature = np.copy(feature)
  183. for shank in range(n_shanks):
  184. shank_feature = feature[shank*n_ch_per_shank:(shank+1)*n_ch_per_shank]
  185. smoothed_shank_feature = pd.Series(shank_feature).interpolate(method='linear', limit_direction='both').rolling(window=moving_avg_window, min_periods=1, center=True).mean()
  186. smoothed_feature[shank*n_ch_per_shank:(shank+1)*n_ch_per_shank] = smoothed_shank_feature
  187. psd_features_smoothed[key] = smoothed_feature
  188. if save_report_dir:
  189. plot_selected_features(psd_features, n_ch_per_shank, n_cols, n_shanks, save_report_dir, title="Selected Features before Smoothing", custom_format=save_report_format)
  190. plot_selected_features(psd_features_smoothed, n_ch_per_shank, n_cols, n_shanks, save_report_dir, title="Selected Features after Smoothing", custom_format=save_report_format)
  191. reduced_features = self.dimensionality_reduction(psd_features_smoothed, feature_extraction_method, 0.95, save_report_dir=save_report_dir, save_report_format=save_report_format)
  192. if save_report_dir:
  193. reduced_features_dict = {f'Component {i+1}': reduced_features[:, i] for i in range(reduced_features.shape[1])}
  194. plot_selected_features(reduced_features_dict, n_ch_per_shank, n_cols, n_shanks, save_report_dir, title=f'Reduced Features ({feature_extraction_method})', custom_format=save_report_format)
  195. plot_feature_grid_maps(psd_features_smoothed, self.electrode_positions, save_report_dir, method='linear', title_prefix='Smoothed_Features', custom_format=save_report_format)
  196. full_labels, norm = self.clustering(reduced_features, clustering_method, save_report_dir=save_report_dir, save_report_format=save_report_format)
  197. psd_features["Cluster Labels"] = full_labels
  198. psd_features_smoothed["Cluster Labels"] = full_labels
  199. if save_report_dir:
  200. plot_selected_features(psd_features, n_ch_per_shank, n_cols, n_shanks, save_report_dir, title=f"Relative_Power_Bands_and_Clusters", custom_format=save_report_format)
  201. plot_selected_features(psd_features_smoothed, n_ch_per_shank, n_cols, n_shanks, save_report_dir, title="Relative_Power_Bands_and_Clusters_Smoothed", custom_format=save_report_format)
  202. self.df = pd.DataFrame({
  203. "Channel": np.arange(n_shanks * n_ch_per_shank),
  204. "Cluster Label": full_labels,
  205. "X (um)": self.electrode_positions[0],
  206. "Y (um)": self.electrode_positions[1],
  207. "probe_params": [self.probe_specs]*(n_shanks*n_ch_per_shank)
  208. })
  209. if ap and ml and dv:
  210. if save_report_dir:
  211. self.place_probe_on_atlas(ap, ml, dv, save_report_dir, atlas_id, save_report_format, norm)
  212. else:
  213. print("AP, ML, DV coordinates were provided but no directory to save report in (save_report_dir). Run place_probe_on_atlas and specify a path to save the report.")
  214. return full_labels
  215. def get_psd_features(self, traces):
  216. # add check if recording already filtered in LFP band
  217. data_lfp = bandpass_filter(traces, 1, 300, self.rec.sampling_frequency, order=2)
  218. frequencies, psd = compute_psd(data_lfp, self.rec.sampling_frequency)
  219. delta_band = (frequencies >= 1) & (frequencies < 4)
  220. theta_band = (frequencies >= 4) & (frequencies < 8)
  221. alpha_band = (frequencies >= 8) & (frequencies < 12)
  222. beta_band = (frequencies >= 12) & (frequencies < 30)
  223. gamma_band = (frequencies >= 30) & (frequencies < 100)
  224. ripple_band = (frequencies >= 100) & (frequencies < 250)
  225. total_band = (frequencies >= 1) & (frequencies < 300)
  226. selected_features = {
  227. "Delta (1-4 Hz)": np.sum(psd[:, delta_band], axis=1) / np.sum(psd[:, total_band], axis=1),
  228. "Theta (4-8 Hz)": np.sum(psd[:, theta_band], axis=1) / np.sum(psd[:, total_band], axis=1),
  229. "Alpha (8-12 Hz)": np.sum(psd[:, alpha_band], axis=1) / np.sum(psd[:, total_band], axis=1),
  230. "Beta (12-30 Hz)": np.sum(psd[:, beta_band], axis=1) / np.sum(psd[:, total_band], axis=1),
  231. "Gamma (30-100 Hz)": np.sum(psd[:, gamma_band], axis=1) / np.sum(psd[:, total_band], axis=1),
  232. "Ripple (100-250 Hz)": np.sum(psd[:, ripple_band], axis=1) / np.sum(psd[:, total_band], axis=1)
  233. }
  234. return selected_features
  235. def get_run_arguments_info(self):
  236. return """
  237. 'feature_extraction_method': Dimensionality reduction algorithm to run on power spectral features. Accepted: 'PCA', 'tSNE', 'UMAP'. Default 'PCA'.
  238. 'clustering_method': Clustering algorithm to run after dim. reduction. Accepted: 'k-means', 'hierarchical', 'DBSCAN', 'HDBSCAN'. Default 'hierarchical'.
  239. 'start_time': Start time of segment to calculate power spectral features on. Default 0.
  240. 'end_time': End time of segment to calculate power spectral features on. Default 30.
  241. 'save_report_dir': Directory to save a report containg every plots generated during each step. Default None (disabled).
  242. 'do_car': Apply Common Average Reference.
  243. 'z_score_threshold': Channels with normalized psd features above this z-score-threshold get labeled as noise and discarded.
  244. 'window_size_outlier_removal': Number of electrodes to consider in window when computing z-score for identifying channels to discard. Recommended to keep default, but can be useful to change if working with unique probe geometries.
  245. 'window_size_smoothening': Number of electrodes to consider in window when smoothening. Recommended to keep default, but can be useful to change if working with unique probe geometries.
  246. 'save_report_format': Image format to save all plots generated into. Default 'png'.
  247. 'atlas_id': ID of Atlas to use supported by brainglobe-atlasapi. Accepted: any atlas provided by the brainglobe-atlasapi library. Default 'kim_mouse_isotropic_20um'.
  248. 'ap': Insertion AP coordinates in millimeters. If not defined placement of probe on atlas is skipped.
  249. 'ml': Insertion ML coordinates in millimeters. If not defined placement of probe on atlas is skipped.
  250. 'dv': Insertion DV coordinates in millimeters. If not defined placement of probe on atlas is skipped.
  251. """
  252. def clustering(self, selected_features, method, save_report_dir : str | Path = None, save_report_format : str = "png"):
  253. if save_report_dir:
  254. os.makedirs(save_report_dir, exist_ok=True)
  255. self.clustering_method = method
  256. n_shanks = self.probe_specs["n_shanks"]
  257. if method == 'k-means':
  258. min_cluster_size = 10
  259. silhouette_scores = []
  260. if n_shanks == 1:
  261. cluster_range = range(3, 8)
  262. else:
  263. cluster_range = range(4, 12)
  264. for k in cluster_range:
  265. kmeans_tmp = KMeans(n_clusters=k, random_state=0).fit(selected_features)
  266. labels_tmp = kmeans_tmp.labels_.copy()
  267. # Mark clusters with <= min_cluster_size elements as outliers (-1)
  268. for cluster_id in range(k):
  269. if np.sum(labels_tmp == cluster_id) <= min_cluster_size:
  270. labels_tmp[labels_tmp == cluster_id] = -1
  271. # Only compute silhouette score for non-outlier points if there are enough valid clusters
  272. n_valid_clusters = len(set(labels_tmp) - {-1})
  273. if n_valid_clusters >= cluster_range[0]:
  274. score = silhouette_score(selected_features[labels_tmp != -1], labels_tmp[labels_tmp != -1])
  275. else:
  276. score = -1
  277. silhouette_scores.append(score)
  278. optimal_k = cluster_range[np.argmax(silhouette_scores)]
  279. print(f"Optimal number of clusters based on silhouette score: {optimal_k}")
  280. kmeans = KMeans(n_clusters=optimal_k, random_state=0).fit(selected_features)
  281. labels = kmeans.labels_.copy()
  282. for cluster_id in range(optimal_k):
  283. if np.sum(labels == cluster_id) <= min_cluster_size:
  284. labels[labels == cluster_id] = -1
  285. unique_labels = np.unique(labels)
  286. labels_new = labels.copy()
  287. new_id = 0
  288. for elements in unique_labels:
  289. if elements == -1:
  290. continue
  291. labels_new[labels == elements] = new_id
  292. new_id += 1
  293. kmeans.labels_ = labels_new
  294. full_labels = kmeans.labels_
  295. elif method == 'DBSCAN':
  296. # Use k-nearest neighbor distances to estimate a good eps value
  297. min_samples = max(5, selected_features.shape[1] * 2) # heuristic: twice the number of features, at least 5
  298. neigh = NearestNeighbors(n_neighbors=min_samples)
  299. nbrs = neigh.fit(selected_features)
  300. distances, indices = nbrs.kneighbors(selected_features)
  301. k_distances = np.sort(distances[:, -1])
  302. if save_report_dir:
  303. plot_dbscan(k_distances, min_samples, save_report_dir, custom_format=save_report_format)
  304. # Heuristic: set eps at the value where the "elbow" occurs, or use the 90th percentile as a starting point
  305. eps = np.percentile(k_distances, 90)
  306. # Try several eps values around the heuristic and pick the one with the best silhouette score (ignore all-noise cases)
  307. best_score = -1
  308. best_labels = None
  309. best_eps = eps
  310. for eps_test in np.linspace(eps * 0.5, eps * 1.5, 10):
  311. dbscan = DBSCAN(eps=eps_test, min_samples=min_samples).fit(selected_features)
  312. labels = dbscan.labels_
  313. n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
  314. if n_clusters < 2 or np.all(labels == -1):
  315. continue
  316. try:
  317. score = silhouette_score(selected_features[labels != -1], labels[labels != -1])
  318. except Exception:
  319. score = -1
  320. if score > best_score:
  321. best_score = score
  322. best_labels = labels.copy()
  323. best_eps = eps_test
  324. if best_labels is None:
  325. # fallback: run with default eps
  326. dbscan = DBSCAN(eps=eps, min_samples=min_samples).fit(selected_features)
  327. best_labels = dbscan.labels_
  328. best_eps = eps
  329. print(f"DBSCAN: selected eps={best_eps:.3f}, min_samples={min_samples}, clusters={len(set(best_labels)) - (1 if -1 in best_labels else 0)}, silhouette={best_score:.3f}")
  330. full_labels = best_labels
  331. elif method == 'HDBSCAN':
  332. if HAS_HDBSCAN:
  333. clusterer = hdbscan.HDBSCAN(min_cluster_size=10)
  334. labels = clusterer.fit_predict(selected_features)
  335. full_labels = labels
  336. else:
  337. print("HDBSCAN is not installed. Install with 'pip install hdbscan'")
  338. raise ModuleNotFoundError
  339. elif method == 'hierarchical':
  340. n_samples = selected_features.shape[0]
  341. min_cluster_size = max(10, n_samples // 50)
  342. max_k = min(12, max(2, n_samples))
  343. best_k = None
  344. best_score = -np.inf
  345. for k in range(4, max_k + 1):
  346. model = AgglomerativeClustering(n_clusters=k)
  347. labels = model.fit_predict(selected_features)
  348. sizes = np.bincount(labels)
  349. if np.sum(sizes < min_cluster_size) > 2:
  350. continue
  351. cv = sizes.std() / (sizes.mean() + 1e-8)
  352. try:
  353. sil = silhouette_score(selected_features, labels)
  354. except Exception:
  355. sil = -1
  356. score = sil * (1.0 / (1.0 + cv))
  357. if score > best_score:
  358. best_score = score
  359. best_k = k
  360. if best_k is None:
  361. best_cv = np.inf
  362. for k in range(2, max_k + 1):
  363. labels = AgglomerativeClustering(n_clusters=k).fit_predict(selected_features)
  364. sizes = np.bincount(labels)
  365. cv = sizes.std() / (sizes.mean() + 1e-8)
  366. if cv < best_cv:
  367. best_cv = cv
  368. best_k = k
  369. hierarchical = AgglomerativeClustering(n_clusters=best_k)
  370. labels = hierarchical.fit_predict(selected_features)
  371. linked = linkage(selected_features, method='ward')
  372. threshold = sorted(linked[:, 2], reverse=True)[best_k - 1]
  373. if save_report_dir:
  374. plot_dendogram(best_k, linked, threshold, save_report_dir, custom_format=save_report_format)
  375. full_labels = labels
  376. if save_report_dir:
  377. norm = plot_cluster_labels(
  378. selected_features,
  379. full_labels,
  380. full_labels,
  381. save_report_dir,
  382. self.probe_specs["n_ch_per_shank"],
  383. self.probe_specs["n_cols"],
  384. self.probe_specs["n_shanks"],
  385. self.electrode_positions,
  386. self.n_ch,
  387. tip_angle=self.probe_specs["tip_angle"],
  388. y_top=self.probe_specs["y_top"],
  389. y_bottom=self.probe_specs["y_bottom"],
  390. custom_format=save_report_format
  391. )
  392. else:
  393. norm = None
  394. return full_labels, norm
  395. def dimensionality_reduction(self, selected_features, method, n_components, save_report_dir : str | Path = None, save_report_format : str = "png"):
  396. self.dimensionality_reduction_method = method
  397. features = np.vstack([v for v in selected_features.values()]).T
  398. # Define outlier threshold (e.g., any value > 2.5 is an outlier)
  399. outlier_mask = (np.isnan(features)).any(axis=1)
  400. # Store outlier indices
  401. outlier_indices = np.where(outlier_mask)[0]
  402. valid_indices = np.where(~outlier_mask)[0]
  403. # Exclude outliers
  404. features_no_outliers = features[~outlier_mask]
  405. # Proceed with scaling and dimensionality reduction
  406. scaler = StandardScaler()
  407. features_scaled = scaler.fit_transform(features_no_outliers)
  408. if method != "PCA":
  409. n_components = 3
  410. if method == 'PCA':
  411. pca = PCA(n_components=None)
  412. features_reduced = pca.fit_transform(features_scaled)
  413. explained_variance = pca.explained_variance_ratio_
  414. cumulative_variance = np.cumsum(explained_variance)
  415. n_components_95 = np.where(cumulative_variance >= n_components)[0][0] + 1 if n_components <= 1.0 else n_components
  416. n_components_95 = min(n_components_95, 4)
  417. features_reduced = features_reduced[:, :n_components_95]
  418. loadings = pca.components_[:n_components_95]
  419. # Calculate feature relevance
  420. feature_importance = np.sum(np.abs(loadings.T) * explained_variance[:n_components_95], axis=1)
  421. if save_report_dir:
  422. plot_pca(explained_variance, cumulative_variance, n_components_95, save_report_dir, feature_importance, selected_features, loadings, custom_format=save_report_format)
  423. elif method == 'tSNE':
  424. tsne = TSNE(n_components=n_components, random_state=0, perplexity=30)
  425. features_reduced = tsne.fit_transform(features_scaled)
  426. elif method == 'UMAP':
  427. if HAS_UMAP:
  428. reducer = umap.UMAP(n_components=n_components, random_state=0)
  429. features_reduced = reducer.fit_transform(features_scaled)
  430. else:
  431. print("UMAP is not installed. Install with 'pip install umap-learn'")
  432. raise ModuleNotFoundError
  433. return features_reduced
  434. def place_coordinates_on_atlas(
  435. self,
  436. ap : float,
  437. ml : float,
  438. dv : float,
  439. save_report_dir : str | Path,
  440. atlas_id : str = "kim_mouse_isotropic_20um",
  441. save_report_format : str = "png",
  442. borders : bool = False
  443. ):
  444. os.makedirs(save_report_dir, exist_ok=True)
  445. # Bregma position in atlas (in micron)
  446. bregma_pos = [5400, 0, 0]
  447. # Load the Allen Mouse Brain Atlas
  448. atlas = BrainGlobeAtlas(atlas_id)
  449. # get resolution from atlas_id
  450. # Extract resolution from atlas_id
  451. match = re.search(r'(\d+)\s*um', atlas_id)
  452. if match:
  453. resolution_um = int(match.group(1))
  454. else:
  455. resolution_um = None
  456. print(f"Atlas resolution: {resolution_um} um")
  457. # load structure information using json file
  458. json_path = os.path.join(atlas.root_dir, "structures.json")
  459. with open(json_path, "r") as f:
  460. structures = json.load(f)
  461. # Build a lookup: region_id -> RGB color
  462. id_to_rgb = {}
  463. all_rgb = []
  464. for region in structures:
  465. region_id = region['id']
  466. rgb = region.get('rgb_triplet', [255, 255, 255]) # fallback to white
  467. id_to_rgb[region_id] = rgb
  468. all_rgb.append(rgb)
  469. # annotation image
  470. annotation_image = atlas.annotation
  471. annotation_shape = annotation_image.shape
  472. # position of text defining stereotactic coordinated
  473. text_pos = (int(0.12*annotation_shape[1]), int(0.72*annotation_shape[0]))
  474. # Convert AP, ML, DV (mm) to µm
  475. AP_um = ap * 1000
  476. ML_um = ml * 1000
  477. DV_um = dv * 1000
  478. # Convert AP, ML, DV (µm) to atlas voxel indices
  479. # AP: z axis, ML: x axis, DV: y axis
  480. slice_z = int((bregma_pos[0] - AP_um )/ resolution_um)
  481. slice_y = int((bregma_pos[1] + DV_um) / resolution_um)
  482. slice_x = int((bregma_pos[2] - ML_um) / resolution_um)
  483. selected_atlas = atlas.annotation
  484. selected_rgb_map = id_to_rgb
  485. # Prepare the annotation slice
  486. selected_annotated_image = selected_atlas[slice_z, :, :]
  487. # Map annotation IDs either to their native region colors or to border-only view.
  488. rgb_image = np.zeros((*selected_annotated_image.shape, 3), dtype=np.uint8)
  489. # get offsets based on atlas
  490. x_atlas_offset = annotation_shape[2]//2
  491. y_atlas_offset = rgb_image.shape[2]
  492. if borders:
  493. rgb_image = build_border_image(selected_annotated_image)
  494. else:
  495. for region_id, rgb in selected_rgb_map.items():
  496. mask = selected_annotated_image == region_id
  497. rgb_image[mask] = rgb
  498. # Add distance between border of image and first cortical layer to DV
  499. # get first channel of RGB to simplify method
  500. rgb_image_first = rgb_image[:, :, 0]
  501. # get only values on the y axis aligned to the first point on the x axis in the probe
  502. values_y = rgb_image_first[:, int(slice_x + x_atlas_offset)]
  503. # find first index where the image on the y-axis isn't equal to 0 (should lead to start of cortex)
  504. idx_cortex = np.argmax(values_y != values_y[0])
  505. # subtract this offset from DV and probe_y_px coordinates
  506. offset_DV_um = idx_cortex * resolution_um
  507. DV_um = DV_um - offset_DV_um
  508. dv = DV_um / 1000
  509. print(f"Calculated offset between border and cortex: {offset_DV_um}um")
  510. x_plot = slice_x + x_atlas_offset
  511. y_plot = y_atlas_offset - slice_y + idx_cortex
  512. plot_coordinates_on_atlas(ap, ml, dv, rgb_image, x_plot, y_plot, text_pos, resolution_um, borders, save_report_dir, save_report_format)
  513. def place_probe_on_atlas(
  514. self,
  515. ap : float,
  516. ml : float,
  517. dv : float,
  518. save_report_dir : str | Path,
  519. atlas_id : str = "kim_mouse_isotropic_20um",
  520. save_report_format : str = "png",
  521. use_relabeled_if_available : bool = True,
  522. norm = None
  523. ):
  524. if not isinstance(self.df, pd.DataFrame):
  525. print("Labelling algorithm not run. Find labels with run()")
  526. return
  527. # Bregma position in atlas (in micron)
  528. bregma_pos = [5400, 0, 0]
  529. # Load the Allen Mouse Brain Atlas
  530. atlas = BrainGlobeAtlas(atlas_id)
  531. # Load relabelled atlas where similar regions are plotted with the same color
  532. if use_relabeled_if_available:
  533. relabeled_atlas, rgb_map_relabelled = get_relabeled_atlas(atlas_id)
  534. else:
  535. relabeled_atlas = rgb_map_relabelled = None
  536. # get resolution from atlas_id
  537. match = re.search(r'(\d+)\s*um', atlas_id)
  538. if match:
  539. resolution_um = int(match.group(1))
  540. print(f"Atlas resolution: {resolution_um} um")
  541. else:
  542. resolution_um = None
  543. print("Resolution not found in atlas_id.")
  544. # load structure information using json file
  545. json_path = os.path.join(atlas.root_dir, "structures.json")
  546. with open(json_path, "r") as f:
  547. structures = json.load(f)
  548. # Build a lookup: region_id -> RGB color
  549. id_to_rgb = {}
  550. all_rgb = []
  551. for region in structures:
  552. region_id = region['id']
  553. rgb = region.get('rgb_triplet', [255, 255, 255]) # fallback to white
  554. id_to_rgb[region_id] = rgb
  555. all_rgb.append(rgb)
  556. ## add warning no labels found in the atlas
  557. # reference image
  558. reference_image = atlas.reference
  559. # annotation image
  560. annotation_image = atlas.annotation
  561. annotation_shape = annotation_image.shape
  562. # convert AP, ML, DV (mm) to µm
  563. AP_um = ap * 1000
  564. ML_um = ml * 1000
  565. DV_um = dv * 1000
  566. # Convert AP, ML, DV (µm) to atlas voxel indices
  567. # AP: z axis, ML: x axis, DV: y axis
  568. slice_z = int((bregma_pos[0] - AP_um )/ resolution_um)
  569. slice_y = int((bregma_pos[1] + DV_um) / resolution_um)
  570. slice_x = int((bregma_pos[2] - ML_um) / resolution_um)
  571. if relabeled_atlas and rgb_map_relabelled and self.probe_specs["n_shanks"] > 1:
  572. selected_atlas = relabeled_atlas
  573. selected_rgb_map = rgb_map_relabelled
  574. else:
  575. selected_atlas = atlas.annotation
  576. selected_rgb_map = id_to_rgb
  577. # Prepare the annotation slice
  578. selected_annotated_image = selected_atlas[slice_z, :, :]
  579. selected_reference_image = reference_image[slice_z, :,:]
  580. # Map annotation IDs to RGB colors
  581. rgb_image = np.zeros((*selected_annotated_image.shape, 3), dtype=np.uint8)
  582. for region_id, rgb in selected_rgb_map.items():
  583. mask = selected_annotated_image == region_id
  584. rgb_image[mask] = rgb
  585. # Add distance between border of image and first cortical layer to DV
  586. # get first channel of RGB to simplify method
  587. rgb_image_first = rgb_image[:, :, 0]
  588. # get only values on the y axis aligned to the first point on the x axis in the probe
  589. values_y = rgb_image_first[:, int(slice_x + annotation_shape[2]//2)]
  590. # find first index where the image on the y-axis isn't equal to 0 (should lead to start of cortex)
  591. idx_cortex = np.argmax(values_y != values_y[0])
  592. # subtract this offset from DV and probe_y_px coordinates
  593. offset_DV_um = idx_cortex * resolution_um
  594. DV_um = DV_um - offset_DV_um
  595. dv = DV_um / 1000
  596. print(f"Calculated offset between border and cortex: {offset_DV_um}um")
  597. save_path = save_report_dir + "/atlas"
  598. os.makedirs(save_path, exist_ok=True)
  599. probe_x_um = self.df['X (um)'].values
  600. probe_y_um = self.df['Y (um)'].values
  601. # calculate offsets in the x and y directions
  602. x_offset = annotation_shape[2]//2 # center at the midline of the brain
  603. y_offset = int(np.max(probe_y_um)/resolution_um)
  604. # flip y_values to match atlas direction
  605. probe_y_um = np.flip(probe_y_um)
  606. cluster_labels = self.df['Cluster Label'].values
  607. # Convert probe coordinates (um) to atlas pixel coordinates
  608. probe_x_px = x_offset + slice_x + (probe_x_um / resolution_um)
  609. probe_y_px = rgb_image.shape[2] - y_offset - slice_y + idx_cortex + (probe_y_um / resolution_um)
  610. # Create blue shades mapping going darker from top clusters to bottom clusters
  611. cluster_labels, sorted_labels = remap_labels(cluster_labels, self.probe_specs)
  612. unique_labels = np.unique(cluster_labels)
  613. cluster_to_color, colors = cluster_color_map_for_labels(cluster_labels, cmap_name='Blues', min_shade=0.3)
  614. plot_clusters_on_probe(cluster_labels, cluster_to_color, self.probe_specs, save_report_dir, save_report_format)
  615. # Plot anatomy and probe clusters
  616. text_pos = (int(0.12*annotation_shape[1]), int(0.72*annotation_shape[0]))
  617. plot_probe_on_atlas(rgb_image, colors, norm, probe_x_px, probe_y_px, self.probe_specs, np.round(ap,1), np.round(ml,1), np.round(dv,1), os.path.join(save_path, f'probe_labelled_atlas_original_pos.{save_report_format}'), resolution_um, text_pos, custom_format=save_report_format)
  618. plot_probe_on_atlas(selected_reference_image, colors, norm, probe_x_px, probe_y_px, self.probe_specs, np.round(ap), np.round(ml,1), np.round(dv,1), os.path.join(save_path, f'probe_reference_atlas_original_pos.{save_report_format}'), resolution_um, text_pos, custom_format=save_report_format)
  619. # REALIGNMENT
  620. # find the best match in the x, y and z position to minimize the variation within clusters
  621. n_pixel_region = np.zeros(len(unique_labels))
  622. label_regions = np.zeros(len(unique_labels))
  623. x_shift = np.arange(-30, 30) # x shift range in pixels
  624. y_shift = np.arange(-30, 30) # y shift range in pixels
  625. z_shift = np.arange(-8, 8) # z shift on the coronal plane
  626. shape = [len(x_shift), len(y_shift), len(z_shift)]
  627. evaluation_mat_label = np.zeros(shape) # evaluation matrix to check where the cluster match best the anatomy
  628. annotated_image = selected_atlas[slice_z , :, :]
  629. # Assign to each region the mode value of where it was originally placed
  630. for k, label in enumerate(unique_labels):
  631. label_x_px = probe_x_px[cluster_labels == label].astype(np.int16)
  632. label_y_px = probe_y_px[cluster_labels == label].astype(np.int16)
  633. values_label = annotated_image[label_y_px, label_x_px]
  634. label_regions[k] = statistics.mode(values_label)
  635. for m, z_s in enumerate(z_shift):
  636. selected_reference_image = reference_image[slice_z + z_s, :,:]
  637. annotated_image = selected_atlas[slice_z + z_s, :, :]
  638. for i, x_s in enumerate(x_shift):
  639. for j, y_s in enumerate(y_shift):
  640. for k, label in enumerate(unique_labels):
  641. # if there is only one shank, only consider shifts that keep the probe in the same hemisphere
  642. if self.probe_specs["n_shanks"] == 1:
  643. if (probe_x_px[cluster_labels == label].mean() < annotation_shape[2]//2 and probe_x_px[cluster_labels == label].mean() + x_s > annotation_shape[2]//2) or (probe_x_px[cluster_labels == label].mean() > annotation_shape[2]//2 and probe_x_px[cluster_labels == label].mean() + x_s < annotation_shape[2]//2):
  644. continue
  645. label_x_px = probe_x_px[cluster_labels == label].astype(np.int16) + x_s
  646. label_y_px = probe_y_px[cluster_labels == label].astype(np.int16) + y_s
  647. values_label = annotated_image[label_y_px, label_x_px]
  648. n_pixel_region[k] = np.sum(values_label==label_regions[k] )
  649. evaluation_mat_label[i, j, m] = np.sum(n_pixel_region)
  650. # Find the indices of the minimum value in the evaluation matrix
  651. max_ind = np.unravel_index(np.argmax(evaluation_mat_label), evaluation_mat_label.shape)
  652. best_x_shift = x_shift[max_ind[0]]
  653. best_y_shift = y_shift[max_ind[1]]
  654. best_z_shift = z_shift[max_ind[2]]
  655. print("Best shift:", best_x_shift * resolution_um, best_y_shift * resolution_um, best_z_shift * resolution_um)
  656. print("N clusters in appropriate region = ",evaluation_mat_label[max_ind[0], max_ind[1], max_ind[2]] )
  657. plot_extent = [x_shift[0]*resolution_um, x_shift[-1]*resolution_um, y_shift[0]*resolution_um, y_shift[-1]*resolution_um]
  658. plot_evaluation_matrix(evaluation_mat_label, max_ind, z_shift, best_x_shift, best_y_shift, resolution_um, plot_extent, save_report_dir, save_report_format)
  659. selected_annotated_image = selected_atlas[slice_z + best_z_shift, :, :]
  660. # Map annotation IDs to RGB colors
  661. rgb_image_sel = np.zeros((*selected_annotated_image.shape, 3), dtype=np.uint8)
  662. for region_id, rgb in selected_rgb_map.items():
  663. mask = selected_annotated_image == region_id
  664. rgb_image_sel[mask] = rgb
  665. selected_reference_image = reference_image[slice_z + best_z_shift,:,:]
  666. # Plot anatomy and probe clusters in adjusted position
  667. AP_new = np.round(ap - best_z_shift*resolution_um/1000,1)
  668. ML_new = np.round(ml - best_x_shift * resolution_um/1000, 1)
  669. DV_new = np.round(dv - best_y_shift*resolution_um/1000, 1)
  670. plot_probe_on_atlas(selected_reference_image, colors, norm, probe_x_px + best_x_shift, probe_y_px + best_y_shift, self.probe_specs, AP_new, ML_new, DV_new, os.path.join(save_path, f'probe_reference_atlas_adjusted.{save_report_format}'), resolution_um, text_pos, custom_format=save_report_format)
  671. plot_probe_on_atlas(rgb_image_sel, colors, norm, probe_x_px + best_x_shift, probe_y_px + best_y_shift, self.probe_specs, AP_new, ML_new, DV_new, os.path.join(save_path, f'probe_original_label_atlas_adjusted.{save_report_format}'), resolution_um, text_pos, custom_format=save_report_format)

main.py at commit bb553e9, under MIT · at the source

Overview

Authors: Alberto Perna1, Raffaele Adamo1,2, Matteo Vincenzi1, Gian Nicola Angotzi1, João Filipe Ribeiro1, Luca Berdondini1
  1. Microtechnology for Neuroelectronics Laboratory, Fondazione Istituto Italiano di Tecnologia, Genova, Italy
  2. The Open University Affiliated Research Centre at Istituto Italiano di Tecnologia, Genova, Italy
Institutions: Italian Institute of Technology (Italy); The Open University (United Kingdom)
Journal: Frontiers in neuroscience, volume 20, article 1816533
Dates: received 24 February 2026; accepted 23 April 2026; published online 11 May 2026
Type: Methods article · Language: English
License: CC BY
Identifiers: DOI 10.3389/fnins.2026.1816533 · PMID 42200042 · PMCID PMC13199280 · OpenAlex W7160847978
Open access: gold, a free copy (OpenAlex)
Status: code verified
Methods: Spectral & time-frequency, Connectivity, Smoothing, state filtering, decompositions, Machine learning, Preprocessing, fMRI & imaging
Keywords: anatomical localization, brain atlas, CMOS neural interfaces, local field potentials, online probe placement, power spectral density, SiNAPS neural probes, spatial distribution
Topic: Neuroscience and Neural Engineering (Cellular and Molecular Neuroscience, Neuroscience), according to OpenAlex
Citations: not cited yet (Europe PMC); 51 references in the paper

Abstract

High-density CMOS-based neural probes provide unprecedented spatiotemporal resolution for in-vivo electrophysiology, yet accurate validation of implant position remains challenging. Here we present LFP-LOC, a simple and interpretable method for intraoperative validation and refinement of probe anatomical location based on the spatial distribution of local field potential (LFP) power. Using spontaneous activity recordings performed in rodents, we compute power spectral densities in canonical LFP bands and apply dimensionality reduction and clustering to identify electrodes with shared spectral signatures. Across multiple implant sites, probe technologies, electrode layouts, and experimental conditions, the resulting clusters consistently align with anatomical boundaries. Applied to high-density probes with up to 1,024 electrodes/channels and sub-30 μm pitch, power features converge within approximately 20 s of recordings, allowing online intraoperative assessment. By leveraging the robust relationship between LFP power and brain structure, LFP-LOC enables rapid validation and adjustment of probe placement during surgery, complements histological validation, and may facilitate mesoscale electrophysiological studies.

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

raffaele222/LFP-LOC

License: MIT
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: bb553e9992483dea7d92c85c0d2f4bfb38c08542, 11 May 2026
Languages: Python (4), Jupyter (1)
Size: 13 files, 5 scripts
Software Heritage: not archived
Found in: the text, “Step 1 - data processing and spectral power esti”
Holds: README, license file, environment (pyproject.toml), 1 notebook
Not found: CITATION.cff, tests, continuous integration, documentation
Tools: NumPy (3 files), SciPy (3 files), Matplotlib (2 files), SpikeInterface (2 files), pandas (1 file), scikit-learn (1 file), UMAP (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
7 files

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;
  • 13 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

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

Data

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

Data availability statement

Requests to access these datasets should be directed to .

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, pages, dates, 6 authors, 8 keywords, 49 references.

Cite

This paper

Perna, A., Adamo, R., Vincenzi, M., Angotzi, G. N., Ribeiro, J. F., & Berdondini, L. (2026). LFP-LOC: an LFP power-based method for validating the anatomical placement of high-density neural probes in rodents. Frontiers in neuroscience, 20, 1816533. https://doi.org/10.3389/fnins.2026.1816533

BibTeX

@article{perna2026lfp,
author = {Perna, Alberto and Adamo, Raffaele and Vincenzi, Matteo and Angotzi, Gian Nicola and Ribeiro, João Filipe and Berdondini, Luca},
title = {{LFP-LOC: an LFP power-based method for validating the anatomical placement of high-density neural probes in rodents}},
journal = {Frontiers in neuroscience},
year = {2026},
month = may,
volume = {20},
pages = {1816533},
publisher = {Frontiers Media SA},
issn = {1662-4548},
doi = {10.3389/fnins.2026.1816533},
url = {https://doi.org/10.3389/fnins.2026.1816533},
pmid = {42200042},
pmcid = {PMC13199280}
}

RIS

TY - JOUR
AU - Perna, Alberto
AU - Adamo, Raffaele
AU - Vincenzi, Matteo
AU - Angotzi, Gian Nicola
AU - Ribeiro, João Filipe
AU - Berdondini, Luca
TI - LFP-LOC: an LFP power-based method for validating the anatomical placement of high-density neural probes in rodents
T2 - Frontiers in neuroscience
J2 - Front Neurosci
PY - 2026
DA - 2026/05/11
VL - 20
SP - 1816533
SN - 1662-4548
PB - Frontiers Media SA
DO - 10.3389/fnins.2026.1816533
UR - https://doi.org/10.3389/fnins.2026.1816533
LA - en
ER -

CSL-JSON

{
"id": "10.3389/fnins.2026.1816533",
"type": "article-journal",
"title": "LFP-LOC: an LFP power-based method for validating the anatomical placement of high-density neural probes in rodents",
"container-title": "Frontiers in neuroscience",
"author": [
{
"family": "Perna",
"given": "Alberto"
},
{
"family": "Adamo",
"given": "Raffaele"
},
{
"family": "Vincenzi",
"given": "Matteo"
},
{
"family": "Angotzi",
"given": "Gian Nicola"
},
{
"family": "Ribeiro",
"given": "João Filipe"
},
{
"family": "Berdondini",
"given": "Luca"
}
],
"container-title-short": "Front Neurosci",
"volume": "20",
"page": "1816533",
"DOI": "10.3389/fnins.2026.1816533",
"PMID": "42200042",
"PMCID": "PMC13199280",
"ISSN": "1662-4548",
"publisher": "Frontiers Media SA",
"URL": "https://doi.org/10.3389/fnins.2026.1816533",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
11
]
]
}
}

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.7554/elife.110588 [code]
Opening the black box toward a modular approach to spike sorting.
Journal: eLife
In common: SpikeInterface, UMAP, scikit-learn, 4 other tools, extracellular electrophysiology (units, LFP), methods / tools, 5 references
[2] doi:10.1038/s41467-026-76939-w [code]
HIPPIE: a generative model for electrophysiological analysis across species, technologies, and modalities.
Journal: Nature communications
In common: SpikeInterface, UMAP, scikit-learn, 4 other tools, methods / tools, 4 references
[3] doi:10.1016/j.patter.2026.101590 [code]
Density-based longitudinal neuron tracking in high-density electrophysiological recordings.
Journal: Patterns (New York, N.Y.)
In common: SpikeInterface, UMAP, scikit-learn, 4 other tools, extracellular electrophysiology (units, LFP), 3 references
[4] doi:10.1038/s41467-026-71331-0 [code]
A multimodal approach for visualizing and identifying electrophysiological cell types in vivo.
Journal: Nature communications
In common: UMAP, scikit-learn, pandas, 3 other tools, extracellular electrophysiology (units, LFP), 3 references
[5] doi:10.1038/s41592-026-03076-z [code]
Neuropixels Opto: combining high-resolution electrophysiology and optogenetics.
Journal: Nature methods
In common: SpikeInterface, pandas, SciPy, 2 other tools, extracellular electrophysiology (units, LFP), 3 references
[6] doi:10.7554/elife.110170 [code]
Efficient and reproducible pipelines for spike sorting large-scale electrophysiology data.
Journal: eLife
In common: SpikeInterface, pandas, NumPy, extracellular electrophysiology (units, LFP), methods / tools, 3 references
[7] doi:10.1038/s41593-026-02232-0 [code]
Entorhinal cortex represents task-relevant remote locations independently of CA1.
Journal: Nature neuroscience
In common: SpikeInterface, scikit-learn, pandas, 3 other tools, 2 references
[8] doi:10.1038/s41467-026-71887-x [code]
Magnetic resonance identification tags for ultra-flexible electrodes.
Journal: Nature communications
In common: scikit-learn, pandas, SciPy, 2 other tools, methods / tools, 3 references
[9] doi:10.1126/sciadv.aef0343 [code]
Learning induces activation-mechanism-dependent neural plasticity in an intracortical microstimulation task.
Journal: Science advances
In common: SpikeInterface, UMAP, scikit-learn, 4 other tools
[10] doi:10.1038/s41467-026-76581-6 [code]
Thalamocortical bursts encode reward contingencies and drive associative learning.
Journal: Nature communications
In common: scikit-learn, pandas, SciPy, 2 other tools, 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.